Merge branch 'refs/heads/dev-datac14' into dev-appimage-installer

This commit is contained in:
DJ2LS 2024-04-17 19:38:10 +02:00
commit 848aae3b4a
74 changed files with 2622 additions and 974 deletions

View file

@ -60,7 +60,7 @@ jobs:
path: ./FreeDATA-Installer.exe path: ./FreeDATA-Installer.exe
- name: Upload Installer to Release - name: Upload Installer to Release
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/v') if: startsWith(github.ref, 'refs/tags/v')
with: with:
draft: true draft: true

View file

@ -48,7 +48,6 @@ jobs:
brew install portaudio brew install portaudio
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip3 install pyaudio pip3 install pyaudio
export PYTHONPATH=/Library/Frameworks/Python.framework/Versions/3.11/lib/:$PYTHONPATH
- name: Install Python dependencies - name: Install Python dependencies
run: | run: |
@ -114,7 +113,7 @@ jobs:
path: ./modem/server.dist/${{ matrix.zip_name }}.zip path: ./modem/server.dist/${{ matrix.zip_name }}.zip
- name: Release Modem - name: Release Modem
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/v') if: startsWith(github.ref, 'refs/tags/v')
with: with:
draft: true draft: true

View file

@ -2,7 +2,7 @@
"name": "FreeDATA", "name": "FreeDATA",
"description": "FreeDATA Client application for connecting to FreeDATA server", "description": "FreeDATA Client application for connecting to FreeDATA server",
"private": true, "private": true,
"version": "0.14.2-alpha", "version": "0.15.2-alpha",
"main": "dist-electron/main/index.js", "main": "dist-electron/main/index.js",
"scripts": { "scripts": {
"start": "vite", "start": "vite",
@ -40,12 +40,10 @@
"blob-util": "2.0.2", "blob-util": "2.0.2",
"bootstrap": "5.3.2", "bootstrap": "5.3.2",
"bootstrap-icons": "1.11.3", "bootstrap-icons": "1.11.3",
"bootswatch": "5.3.2",
"browser-image-compression": "2.0.2", "browser-image-compression": "2.0.2",
"chart.js": "4.4.1", "chart.js": "4.4.2",
"chartjs-plugin-annotation": "3.0.1", "chartjs-plugin-annotation": "3.0.1",
"electron-log": "5.1.1", "electron-log": "5.1.2",
"electron-updater": "6.1.7",
"emoji-picker-element": "1.21.0", "emoji-picker-element": "1.21.0",
"emoji-picker-element-data": "1.6.0", "emoji-picker-element-data": "1.6.0",
"file-saver": "2.0.5", "file-saver": "2.0.5",
@ -57,16 +55,16 @@
"qth-locator": "2.1.0", "qth-locator": "2.1.0",
"socket.io": "4.7.4", "socket.io": "4.7.4",
"uuid": "^9.0.1", "uuid": "^9.0.1",
"vue": "3.4.15", "vue": "3.4.21",
"vue-chartjs": "5.3.0", "vue-chartjs": "5.3.0",
"vuemoji-picker": "0.2.0" "vuemoji-picker": "0.2.1"
}, },
"devDependencies": { "devDependencies": {
"@types/nconf": "^0.10.6", "@types/nconf": "^0.10.6",
"@typescript-eslint/eslint-plugin": "6.21.0", "@typescript-eslint/eslint-plugin": "6.21.0",
"@vitejs/plugin-vue": "5.0.4", "@vitejs/plugin-vue": "5.0.4",
"electron": "28.2.2", "electron": "28.2.6",
"electron-builder": "24.9.1", "electron-builder": "24.13.3",
"eslint": "8.56.0", "eslint": "8.56.0",
"eslint-config-prettier": "9.1.0", "eslint-config-prettier": "9.1.0",
"eslint-config-standard-with-typescript": "43.0.1", "eslint-config-standard-with-typescript": "43.0.1",
@ -74,13 +72,13 @@
"eslint-plugin-n": "16.6.2", "eslint-plugin-n": "16.6.2",
"eslint-plugin-prettier": "5.1.3", "eslint-plugin-prettier": "5.1.3",
"eslint-plugin-promise": "6.1.1", "eslint-plugin-promise": "6.1.1",
"eslint-plugin-vue": "9.20.1", "eslint-plugin-vue": "9.22.0",
"typescript": "5.3.3", "typescript": "5.3.3",
"vite": "5.1.3", "vite": "5.1.3",
"vite-plugin-electron": "0.28.2", "vite-plugin-electron": "0.28.2",
"vite-plugin-electron-renderer": "0.14.5", "vite-plugin-electron-renderer": "0.14.5",
"vitest": "1.2.2", "vitest": "1.4.0",
"vue": "3.4.15", "vue": "3.4.21",
"vue-tsc": "1.8.27" "vue-tsc": "1.8.27"
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 342 KiB

View file

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-person-fill" viewBox="0 0 16 16">
<path d="M3 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H3Zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 225 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

View file

@ -105,6 +105,8 @@ Spectrum.prototype.drawSpectrum = function () {
var linePositionHigh = 178.4; //150 + bandwidth/20 var linePositionHigh = 178.4; //150 + bandwidth/20
var linePositionLow2 = 65; //150 - bandwith/20 var linePositionLow2 = 65; //150 - bandwith/20
var linePositionHigh2 = 235; //150 + bandwith/20 var linePositionHigh2 = 235; //150 + bandwith/20
var linePositionLow3 = 28.1; //150 - bandwith/20
var linePositionHigh3 = 271.9; //150 + bandwith/20
this.ctx_wf.beginPath(); this.ctx_wf.beginPath();
this.ctx_wf.moveTo(linePositionLow, 0); this.ctx_wf.moveTo(linePositionLow, 0);
this.ctx_wf.lineTo(linePositionLow, height); this.ctx_wf.lineTo(linePositionLow, height);
@ -114,6 +116,10 @@ Spectrum.prototype.drawSpectrum = function () {
this.ctx_wf.lineTo(linePositionLow2, height); this.ctx_wf.lineTo(linePositionLow2, height);
this.ctx_wf.moveTo(linePositionHigh2, 0); this.ctx_wf.moveTo(linePositionHigh2, 0);
this.ctx_wf.lineTo(linePositionHigh2, height); this.ctx_wf.lineTo(linePositionHigh2, height);
this.ctx_wf.moveTo(linePositionLow3, 0);
this.ctx_wf.lineTo(linePositionLow3, height);
this.ctx_wf.moveTo(linePositionHigh3, 0);
this.ctx_wf.lineTo(linePositionHigh3, height);
this.ctx_wf.lineWidth = 1; this.ctx_wf.lineWidth = 1;
this.ctx_wf.strokeStyle = "#C3C3C3"; this.ctx_wf.strokeStyle = "#C3C3C3";
this.ctx_wf.stroke(); this.ctx_wf.stroke();
@ -454,7 +460,7 @@ export function Spectrum(id, options) {
this.centerHz = options && options.centerHz ? options.centerHz : 1500; this.centerHz = options && options.centerHz ? options.centerHz : 1500;
this.spanHz = options && options.spanHz ? options.spanHz : 0; this.spanHz = options && options.spanHz ? options.spanHz : 0;
this.wf_size = options && options.wf_size ? options.wf_size : 0; this.wf_size = options && options.wf_size ? options.wf_size : 0;
this.wf_rows = options && options.wf_rows ? options.wf_rows : 1024; this.wf_rows = options && options.wf_rows ? options.wf_rows : 512;
this.spectrumPercent = this.spectrumPercent =
options && options.spectrumPercent ? options.spectrumPercent : 0; options && options.spectrumPercent ? options.spectrumPercent : 0;
this.spectrumPercentStep = this.spectrumPercentStep =

View file

@ -32,8 +32,7 @@ import grid_freq from "./grid/grid_frequency.vue";
import grid_beacon from "./grid/grid_beacon.vue"; import grid_beacon from "./grid/grid_beacon.vue";
import grid_mycall_small from "./grid/grid_mycall small.vue"; import grid_mycall_small from "./grid/grid_mycall small.vue";
import grid_scatter from "./grid/grid_scatter.vue"; import grid_scatter from "./grid/grid_scatter.vue";
import { stateDispatcher } from "../js/eventHandler"; import grid_stats_chart from "./grid/grid_stats_chart.vue";
import { Scatter } from "vue-chartjs";
let count = ref(0); let count = ref(0);
let grid = null; // DO NOT use ref(null) as proxies GS will break all logic when comparing structures... see https://github.com/gridstack/gridstack.js/issues/2115 let grid = null; // DO NOT use ref(null) as proxies GS will break all logic when comparing structures... see https://github.com/gridstack/gridstack.js/issues/2115
@ -63,7 +62,8 @@ class gridWidget {
this.id = id; this.id = id;
} }
} }
//Array of grid widgets, do not change array order as it'll affect saved configs //Array of grid widgets
//Order can be changed so sorted correctly, but do not change ID as it'll affect saved configs
const gridWidgets = [ const gridWidgets = [
new gridWidget( new gridWidget(
grid_activities, grid_activities,
@ -247,8 +247,16 @@ new gridWidget(
"Stats", "Stats",
19, 19,
), ),
new gridWidget(
//New new widget ID should be 20 grid_stats_chart,
{ x: 0, y: 114, w: 6, h: 30 },
"Speed/SNR graph",
false,
true,
"Stats",
20,
),
//Next new widget ID should be 21
]; ];
@ -832,7 +840,7 @@ function quickfill() {
<h6>15m</h6> <h6>15m</h6>
</div> </div>
</a> </a>
<a href="#" class="list-group-item list-group-item-action" @click="updateFrequencyAndApply(14093000)"> <a href="#" class="list-group-item list-group-item-action" @click="updateFrequencyAndApply(18106000)">
<div class="d-flex w-100 justify-content-between"> <div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">18.106 MHz</h5> <h5 class="mb-1">18.106 MHz</h5>
<small>EU / US</small> <small>EU / US</small>

View file

@ -21,6 +21,14 @@ function startStopBeacon() {
} }
} }
var dxcallPing = ref(""); var dxcallPing = ref("");
window.addEventListener(
"stationSelected",
function (eventdata) {
let evt = <CustomEvent>eventdata;
dxcallPing.value = evt.detail;
},
false,
);
</script> </script>
<template> <template>
<div class="card h-100"> <div class="card h-100">

View file

@ -15,12 +15,31 @@ function transmitPing() {
function startStopBeacon() { function startStopBeacon() {
if (state.beacon_state === true) { if (state.beacon_state === true) {
setModemBeacon(false); setModemBeacon(false, state.away_from_key);
} else { } else {
setModemBeacon(true); setModemBeacon(true, state.away_from_key);
} }
} }
function setAwayFromKey(){
if (state.away_from_key === true) {
setModemBeacon(state.beacon_state, false);
} else {
setModemBeacon(state.beacon_state, true);
}
}
var dxcallPing = ref(""); var dxcallPing = ref("");
window.addEventListener(
"stationSelected",
function (eventdata) {
let evt = <CustomEvent>eventdata;
dxcallPing.value = evt.detail;
},
false,
);
</script> </script>
<template> <template>
<div class="card h-100"> <div class="card h-100">
@ -31,7 +50,7 @@ var dxcallPing = ref("");
<div class="card-body overflow-auto p-0"> <div class="card-body overflow-auto p-0">
<div class="container text-center"> <div class="container text-center">
<div class="row mb-2 mt-2"> <div class="row mb-2 mt-2">
<div class="col-sm-8"> <div class="col">
<div class="input-group w-100"> <div class="input-group w-100">
<div class="form-floating"> <div class="form-floating">
<input <input
@ -57,25 +76,10 @@ var dxcallPing = ref("");
title="Send a ping request to a remote station" title="Send a ping request to a remote station"
@click="transmitPing()" @click="transmitPing()"
> >
<strong>Ping</strong> <strong>PING Station</strong>
</button> </button>
</div> </div>
</div> </div>
<div class="col">
<div class="form-check form-switch">
<input
class="form-check-input"
type="checkbox"
role="switch"
id="flexSwitchBeacon"
v-model="state.beacon_state"
@click="startStopBeacon()"
/>
<label class="form-check-label" for="flexSwitchBeacon"
>Beacon</label
>
</div>
</div>
</div> </div>
<div class="row"> <div class="row">
@ -91,6 +95,41 @@ var dxcallPing = ref("");
</button> </button>
</div> </div>
</div> </div>
<div class="row">
<div class="col">
<div class="form-check form-switch">
<input
class="form-check-input"
type="checkbox"
role="switch"
id="flexSwitchBeacon"
v-model="state.beacon_state"
@click="startStopBeacon()"
/>
<label class="form-check-label" for="flexSwitchBeacon"
>Enable Beacon</label
>
</div>
</div>
<div class="col">
<div class="form-check form-switch">
<input
class="form-check-input"
type="checkbox"
role="switch"
id="flexSwitchAFK"
v-model="state.away_from_key"
@click="setAwayFromKey()"
/>
<label class="form-check-label" for="flexSwitchAFK"
>Away From Key</label
>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -34,6 +34,11 @@ function getMaidenheadDistance(dxGrid) {
// //
} }
} }
function pushToPing(origin) {
window.dispatchEvent(
new CustomEvent("stationSelected", { bubbles: true, detail: origin }),
);
}
</script> </script>
<template> <template>
<div class="card h-100"> <div class="card h-100">
@ -57,11 +62,16 @@ function getMaidenheadDistance(dxGrid) {
<th scope="col" id="thType">Type</th> <th scope="col" id="thType">Type</th>
<th scope="col" id="thSnr">SNR</th> <th scope="col" id="thSnr">SNR</th>
<!--<th scope="col">Off</th>--> <!--<th scope="col">Off</th>-->
<th scope="col" id="thSnr">AFK?</th>
</tr> </tr>
</thead> </thead>
<tbody id="gridHeardStations"> <tbody id="gridHeardStations">
<!--https://vuejs.org/guide/essentials/list.html--> <!--https://vuejs.org/guide/essentials/list.html-->
<tr v-for="item in state.heard_stations" :key="item.origin"> <tr
v-for="item in state.heard_stations"
:key="item.origin"
@click="pushToPing(item.origin)"
>
<td> <td>
{{ getDateTime(item.timestamp) }} {{ getDateTime(item.timestamp) }}
</td> </td>
@ -79,6 +89,9 @@ function getMaidenheadDistance(dxGrid) {
<td> <td>
{{ item.snr }} {{ item.snr }}
</td> </td>
<td>
{{ item.away_from_key }}
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View file

@ -34,6 +34,10 @@ function getMaidenheadDistance(dxGrid) {
// //
} }
} }
function pushToPing(origin)
{
window.dispatchEvent(new CustomEvent("stationSelected", {bubbles:true, detail: origin }));
}
</script> </script>
<template> <template>
<div class="card h-100"> <div class="card h-100">
@ -54,7 +58,7 @@ function getMaidenheadDistance(dxGrid) {
</thead> </thead>
<tbody id="miniHeardStations"> <tbody id="miniHeardStations">
<!--https://vuejs.org/guide/essentials/list.html--> <!--https://vuejs.org/guide/essentials/list.html-->
<tr v-for="item in state.heard_stations" :key="item.origin"> <tr v-for="item in state.heard_stations" :key="item.origin" @click="pushToPing(item.origin)">
<td> <td>
<span class="fs-6">{{ getDateTime(item.timestamp) }}</span> <span class="fs-6">{{ getDateTime(item.timestamp) }}</span>
</td> </td>

View file

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
// @ts-nocheck // @ts-nocheck
// reason for no check is, that we have some mixing of typescript and chart js which seems to be not to be fixed that easy // reason for no check is, that we have some mixing of typescript and chart js which seems to be not to be fixed that easy
import { ref, computed, onMounted, nextTick } from "vue"; import { ref, computed, onMounted, nextTick, toRaw } from "vue";
import { initWaterfall, setColormap } from "../../js/waterfallHandler.js"; import { initWaterfall, setColormap } from "../../js/waterfallHandler.js";
import { setActivePinia } from "pinia"; import { setActivePinia } from "pinia";
import pinia from "../../store/index"; import pinia from "../../store/index";
@ -89,7 +89,7 @@ const transmissionSpeedChartData = computed(() => ({
{ {
type: "line", type: "line",
label: "SNR[dB]", label: "SNR[dB]",
data: state.arq_speed_list_snr, data: state.arq_speed_list_snr.value,
borderColor: "rgb(75, 192, 192, 1.0)", borderColor: "rgb(75, 192, 192, 1.0)",
pointRadius: 1, pointRadius: 1,
segment: { segment: {
@ -106,7 +106,7 @@ const transmissionSpeedChartData = computed(() => ({
{ {
type: "bar", type: "bar",
label: "Speed[bpm]", label: "Speed[bpm]",
data: state.arq_speed_list_bpm, data: state.arq_speed_list_bpm.value,
borderColor: "rgb(120, 100, 120, 1.0)", borderColor: "rgb(120, 100, 120, 1.0)",
backgroundColor: "rgba(120, 100, 120, 0.2)", backgroundColor: "rgba(120, 100, 120, 0.2)",
order: 0, order: 0,

View file

@ -12,6 +12,15 @@ function transmitPing() {
sendModemPing(dxcallPing.value.toUpperCase()); sendModemPing(dxcallPing.value.toUpperCase());
} }
var dxcallPing = ref(""); var dxcallPing = ref("");
window.addEventListener(
"stationSelected",
function (eventdata) {
let evt = <CustomEvent>eventdata;
dxcallPing.value = evt.detail;
},
false,
);
</script> </script>
<template> <template>
<div class="input-group" style="width: calc(100% - 24px)"> <div class="input-group" style="width: calc(100% - 24px)">

View file

@ -0,0 +1,105 @@
<script setup lang="ts">
// @ts-nocheck
// reason for no check is, that we have some mixing of typescript and chart js which seems to be not to be fixed that easy
import { ref, computed, onMounted, nextTick, toRaw } from "vue";
import { setActivePinia } from "pinia";
import pinia from "../../store/index";
setActivePinia(pinia);
import { useStateStore } from "../../store/stateStore.js";
const state = useStateStore(pinia);
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
} from "chart.js";
import { Line, Scatter } from "vue-chartjs";
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
);
// https://www.chartjs.org/docs/latest/samples/line/segments.html
const skipped = (speedCtx, value) =>
speedCtx.p0.skip || speedCtx.p1.skip ? value : undefined;
const down = (speedCtx, value) =>
speedCtx.p0.parsed.y > speedCtx.p1.parsed.y ? value : undefined;
var transmissionSpeedChartOptions = {
//type: "line",
responsive: true,
animations: true,
maintainAspectRatio: false,
cubicInterpolationMode: "monotone",
tension: 0.4,
scales: {
SNR: {
type: "linear",
ticks: { beginAtZero: false, color: "rgb(255, 99, 132)" },
position: "right",
},
SPEED: {
type: "linear",
ticks: { beginAtZero: false, color: "rgb(120, 100, 120)" },
position: "left",
grid: {
drawOnChartArea: false, // only want the grid lines for one axis to show up
},
},
x: { ticks: { beginAtZero: true } },
},
};
const transmissionSpeedChartData = computed(() => ({
labels: state.arq_speed_list_timestamp,
datasets: [
{
type: "line",
label: "SNR[dB]",
data: state.arq_speed_list_snr.value,
borderColor: "rgb(75, 192, 192, 1.0)",
pointRadius: 1,
segment: {
borderColor: (speedCtx) =>
skipped(speedCtx, "rgb(0,0,0,0.4)") ||
down(speedCtx, "rgb(192,75,75)"),
borderDash: (speedCtx) => skipped(speedCtx, [3, 3]),
},
spanGaps: true,
backgroundColor: "rgba(75, 192, 192, 0.2)",
order: 1,
yAxisID: "SNR",
},
{
type: "bar",
label: "Speed[bpm]",
data: state.arq_speed_list_bpm.value,
borderColor: "rgb(120, 100, 120, 1.0)",
backgroundColor: "rgba(120, 100, 120, 0.2)",
order: 0,
yAxisID: "SPEED",
},
],
}));
</script>
<template>
<Line
:data="transmissionSpeedChartData"
:options="transmissionSpeedChartOptions"
/>
</template>

View file

@ -1,223 +0,0 @@
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { setActivePinia } from "pinia";
import pinia from "../store/index";
setActivePinia(pinia);
import infoScreen_updater from "./infoScreen_updater.vue";
function openWebExternal(url) {
open(url);
}
const cards = ref([
{
titleName: "Simon",
titleCall: "DJ2LS",
role: "Founder & Core Developer",
imgSrc: "dj2ls.png",
},
{
titleName: "Alan",
titleCall: "N1QM",
role: "Developer",
imgSrc: "person-fill.svg",
},
{
titleName: "Stefan",
titleCall: "DK5SM",
role: "Tester",
imgSrc: "person-fill.svg",
},
{
titleName: "Wolfgang",
titleCall: "DL4IAZ",
role: "Supporter",
imgSrc: "person-fill.svg",
},
{
titleName: "David",
titleCall: "VK5DGR",
role: "Codec 2 Founder",
imgSrc: "vk5dgr.jpeg",
},
{
titleName: "John",
titleCall: "EI7IG",
role: "Tester",
imgSrc: "ei7ig.jpeg",
},
{
titleName: "Paul",
titleCall: "N2KIQ",
role: "Developer",
imgSrc: "person-fill.svg",
},
{
titleName: "Trip",
titleCall: "KT4WO",
role: "Tester",
imgSrc: "kt4wo.png",
},
{
titleName: "Manuel",
titleCall: "DF7MH",
role: "Tester",
imgSrc: "person-fill.svg",
},
{
titleName: "Darren",
titleCall: "G0HWW",
role: "Tester",
imgSrc: "person-fill.svg",
},
{
titleName: "Kai",
titleCall: "LA3QMA",
role: "Developer",
imgSrc: "person-fill.svg",
},
{
titleName: "Pedro",
titleCall: "F4JAW",
role: "Core Developer",
imgSrc: "person-fill.svg",
},
]);
// Shuffle cards
function shuffleCards() {
cards.value = cards.value.sort(() => Math.random() - 0.5);
}
onMounted(shuffleCards);
</script>
<template>
<!--<infoScreen_updater />-->
<div class="container-fluid">
<div class="row">
<h6>Important URLs</h6>
<div
class="btn-toolbar mx-auto"
role="toolbar"
aria-label="Toolbar with button groups"
>
<div class="btn-group">
<button
class="btn btn-sm bi bi-geo-alt btn-secondary me-2"
id="openExplorer"
type="button"
data-bs-placement="bottom"
@click="openWebExternal('https://explorer.freedata.app')"
>
Explorer map
</button>
</div>
<div class="btn-group">
<button
class="btn btn-sm btn-secondary me-2 bi bi-graph-up"
id="btnStats"
type="button"
data-bs-placement="bottom"
@click="openWebExternal('https://statistics.freedata.app/')"
>
Statistics
</button>
</div>
<div class="btn-group">
<button
class="btn btn-secondary bi bi-bookmarks me-2"
id="fdWww"
data-bs-toggle="tooltip"
data-bs-trigger="hover"
title="FreeDATA website"
role="button"
@click="openWebExternal('https://freedata.app')"
>
Website
</button>
</div>
<div class="btn-group">
<button
class="btn btn-secondary bi bi-github me-2"
id="ghUrl"
data-bs-toggle="tooltip"
data-bs-trigger="hover"
title="Github"
role="button"
@click="openWebExternal('https://github.com/dj2ls/freedata')"
>
Github
</button>
</div>
<div class="btn-group">
<button
class="btn btn-secondary bi bi-wikipedia me-2"
id="wikiUrl"
data-bs-toggle="tooltip"
data-bs-trigger="hover"
title="Wiki"
role="button"
@click="openWebExternal('https://wiki.freedata.app')"
>
Wiki
</button>
</div>
<div class="btn-group">
<button
class="btn btn-secondary bi bi-discord"
id="discordUrl"
data-bs-toggle="tooltip"
data-bs-trigger="hover"
title="Discord"
role="button"
@click="openWebExternal('https://discord.freedata.app')"
>
Discord
</button>
</div>
</div>
</div>
<hr />
<div class="row">
<h6>We would like to especially thank the following</h6>
</div>
<div
class="d-flex flex-nowrap overflow-y-auto w-100"
style="height: calc(100vh - 170px); overflow-x: hidden"
>
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-4 row-cols-lg-6">
<div class="d-inline-block" v-for="card in cards" :key="card.titleName">
<div class="col">
<div class="card border-dark m-1" style="max-width: 10rem">
<img :src="card.imgSrc" class="card-img-top grayscale" />
<div class="card-body">
<p class="card-text text-center">{{ card.role }}</p>
</div>
<div class="card-footer text-body-secondary text-center">
<strong>{{ card.titleCall }}</strong>
</div>
<div class="card-footer text-body-secondary text-center">
<strong>{{ card.titleName }}</strong>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style>
.grayscale {
filter: grayscale(100%);
transition: filter 0.3s ease-in-out;
}
.grayscale:hover {
filter: grayscale(0);
}
</style>

View file

@ -9,7 +9,6 @@ import settings_view from "./settings.vue";
import main_footer_navbar from "./main_footer_navbar.vue"; import main_footer_navbar from "./main_footer_navbar.vue";
import chat from "./chat.vue"; import chat from "./chat.vue";
import infoScreen from "./infoScreen.vue";
import main_modem_healthcheck from "./main_modem_healthcheck.vue"; import main_modem_healthcheck from "./main_modem_healthcheck.vue";
import Dynamic_components from "./dynamic_components.vue"; import Dynamic_components from "./dynamic_components.vue";
@ -75,17 +74,6 @@ import { loadAllData } from "../js/eventHandler";
><i class="bi bi-rocket h3"></i ><i class="bi bi-rocket h3"></i
></a> ></a>
<a
class="list-group-item list-group-item-dark list-group-item-action border border-0 rounded-3 mb-2"
id="list-info-list"
data-bs-toggle="list"
href="#list-info"
role="tab"
aria-controls="list-info"
title="About"
><i class="bi bi-info h3"></i
></a>
<a <a
class="list-group-item list-group-item-dark list-group-item-action d-none border-0 rounded-3 mb-2" class="list-group-item list-group-item-dark list-group-item-action d-none border-0 rounded-3 mb-2"
id="list-logger-list" id="list-logger-list"
@ -136,27 +124,6 @@ import { loadAllData } from "../js/eventHandler";
<!-------------------------------- MAIN AREA ----------------> <!-------------------------------- MAIN AREA ---------------->
<!------------------------------------------------------------------------------------------> <!------------------------------------------------------------------------------------------>
<div class="container">
<div class="row">
<div class="col-5">
<main_active_rig_control />
</div>
<div class="col-4">
<main_active_broadcasts />
</div>
<div class="col-3">
<main_active_audio_level />
</div>
</div>
<div class="row">
<div class="col-7">
<main_active_heard_stations />
</div>
<div class="col-5">
<main_active_stats />
</div>
</div>
</div>
</div> </div>
</div> </div>
@ -295,14 +262,7 @@ import { loadAllData } from "../js/eventHandler";
</div> </div>
</div> </div>
</div> </div>
<div
class="tab-pane fade"
id="list-info"
role="tabpanel"
aria-labelledby="list-info-list"
>
<infoScreen />
</div>
<div <div
class="tab-pane fade show active" class="tab-pane fade show active"
id="list-grid" id="list-grid"

View file

@ -45,7 +45,7 @@ const serialStore = useSerialStore();
@change="onChange" @change="onChange"
v-model.number="settings.remote.RADIO.model_id" v-model.number="settings.remote.RADIO.model_id"
> >
<option selected value="-- ignore --">-- ignore --</option> <option selected value="0">-- ignore --</option>
<option value="1">Hamlib Dummy</option> <option value="1">Hamlib Dummy</option>
<option value="2">Hamlib NET rigctl</option> <option value="2">Hamlib NET rigctl</option>
<option value="4">FLRig FLRig</option> <option value="4">FLRig FLRig</option>

View file

@ -165,20 +165,20 @@ const audioStore = useAudioStore();
</div> </div>
<div class="input-group input-group-sm mb-1"> <div class="input-group input-group-sm mb-1">
<label class="input-group-text w-50">Enable 250Hz bandwidth mode</label> <label class="input-group-text w-50">Maximum used bandwidth</label>
<label class="input-group-text w-50"> <select
<div class="form-check form-switch form-check-inline"> class="form-select form-select-sm"
<input id="maximum_bandwidth"
class="form-check-input"
type="checkbox"
id="250HzModeSwitch"
v-model="settings.remote.MODEM.enable_low_bandwidth_mode"
@change="onChange" @change="onChange"
/> v-model.number="settings.remote.MODEM.maximum_bandwidth"
<label class="form-check-label" for="250HzModeSwitch">250Hz</label> >
</div> <option value="250">250 Hz</option>
</label> <option value="563">563 Hz</option>
<option value="1700">1700 Hz</option>
<option value="2438">2438 Hz</option>
</select>
</div> </div>
<div class="input-group input-group-sm mb-1"> <div class="input-group input-group-sm mb-1">
<label class="input-group-text w-50">Respond to CQ</label> <label class="input-group-text w-50">Respond to CQ</label>
<label class="input-group-text w-50"> <label class="input-group-text w-50">

View file

@ -92,8 +92,11 @@ export async function getSerialDevices() {
return await apiGet("/devices/serial"); return await apiGet("/devices/serial");
} }
export async function setModemBeacon(enabled = false) { export async function setModemBeacon(enabled = false, away_from_key = false) {
return await apiPost("/modem/beacon", { enabled: enabled }); return await apiPost("/modem/beacon", {
enabled: enabled,
away_from_key: away_from_key,
});
} }
export async function sendModemCQ() { export async function sendModemCQ() {

View file

@ -7,6 +7,7 @@ import {
setStateFailed, setStateFailed,
} from "./chatHandler"; } from "./chatHandler";
*/ */
import { toRaw } from "vue";
import { displayToast } from "./popupHandler"; import { displayToast } from "./popupHandler";
import { getFreedataMessages, getModemState, getAudioDevices } from "./api"; import { getFreedataMessages, getModemState, getAudioDevices } from "./api";
import { processFreedataMessages } from "./messagesHandler.ts"; import { processFreedataMessages } from "./messagesHandler.ts";
@ -29,8 +30,11 @@ import {
getRemote, getRemote,
} from "../store/settingsStore.js"; } from "../store/settingsStore.js";
export function loadAllData() { export async function loadAllData() {
getModemState(); // TODO: Make this working
let stateData = await getModemState();
console.log(stateData);
getRemote(); getRemote();
getOverallHealth(); getOverallHealth();
audioStore.loadAudioDevices(); audioStore.loadAudioDevices();
@ -66,7 +70,10 @@ export function stateDispatcher(data) {
); );
stateStore.channel_busy_slot = data["channel_busy_slot"]; stateStore.channel_busy_slot = data["channel_busy_slot"];
stateStore.beacon_state = data["is_beacon_running"]; stateStore.beacon_state = data["is_beacon_running"];
stateStore.is_away_from_key = data["is_away_from_key"];
stateStore.radio_status = data["radio_status"]; stateStore.radio_status = data["radio_status"];
stateStore.frequency = data["radio_frequency"]; stateStore.frequency = data["radio_frequency"];
stateStore.mode = data["radio_mode"]; stateStore.mode = data["radio_mode"];
@ -178,12 +185,16 @@ export function eventDispatcher(data) {
100; 100;
stateStore.arq_total_bytes = stateStore.arq_total_bytes =
data["arq-transfer-outbound"].received_bytes; data["arq-transfer-outbound"].received_bytes;
stateStore.arq_speed_list_timestamp = stateStore.arq_speed_list_timestamp.value = toRaw(
data["arq-transfer-outbound"].statistics.time_histogram; data["arq-transfer-outbound"].statistics.time_histogram,
stateStore.arq_speed_list_bpm = );
data["arq-transfer-outbound"].statistics.bpm_histogram; stateStore.arq_speed_list_bpm.value = toRaw(
stateStore.arq_speed_list_snr = data["arq-transfer-outbound"].statistics.bpm_histogram,
data["arq-transfer-outbound"].statistics.snr_histogram; );
stateStore.arq_speed_list_snr.value = toRaw(
data["arq-transfer-outbound"].statistics.snr_histogram,
);
//console.log(toRaw(stateStore.arq_speed_list_timestamp.value));
return; return;
case "ABORTING": case "ABORTING":
@ -226,13 +237,12 @@ export function eventDispatcher(data) {
stateStore.dxcallsign = data["arq-transfer-inbound"].dxcall; stateStore.dxcallsign = data["arq-transfer-inbound"].dxcall;
stateStore.arq_transmission_percent = 0; stateStore.arq_transmission_percent = 0;
stateStore.arq_total_bytes = 0; stateStore.arq_total_bytes = 0;
stateStore.arq_speed_list_timestamp = //stateStore.arq_speed_list_timestamp =
data["arq-transfer-inbound"].statistics.time_histogram; // [];
stateStore.arq_speed_list_bpm = //stateStore.arq_speed_list_bpm =
data["arq-transfer-inbound"].statistics.bpm_histogram; // [];
stateStore.arq_speed_list_snr = //stateStore.arq_speed_list_snr =
data["arq-transfer-inbound"].statistics.snr_histogram; // [];
return; return;
case "OPEN_ACK_SENT": case "OPEN_ACK_SENT":
@ -266,6 +276,19 @@ export function eventDispatcher(data) {
100; 100;
stateStore.arq_total_bytes = stateStore.arq_total_bytes =
data["arq-transfer-inbound"].received_bytes; data["arq-transfer-inbound"].received_bytes;
//console.log(data["arq-transfer-inbound"].statistics.time_histogram);
stateStore.arq_speed_list_timestamp.value = toRaw(
data["arq-transfer-inbound"].statistics.time_histogram,
);
stateStore.arq_speed_list_bpm.value = toRaw(
data["arq-transfer-inbound"].statistics.bpm_histogram,
);
stateStore.arq_speed_list_snr.value = toRaw(
data["arq-transfer-inbound"].statistics.snr_histogram,
);
console.log(stateStore.arq_speed_list_timestamp.value);
console.log(stateStore.arq_speed_list_bpm.value);
console.log(stateStore.arq_speed_list_snr.value);
return; return;
case "ENDED": case "ENDED":

View file

@ -54,11 +54,11 @@ const defaultConfig = {
enable_protocol: false, enable_protocol: false,
}, },
MODEM: { MODEM: {
enable_low_bandwidth_mode: false,
respond_to_cq: false, respond_to_cq: false,
tx_delay: 0, tx_delay: 0,
enable_hamc: false, enable_hamc: false,
enable_morse_identifier: false, enable_morse_identifier: false,
maximum_bandwidth: 3000,
}, },
RADIO: { RADIO: {
control: "disabled", control: "disabled",

View file

@ -38,6 +38,7 @@ export const useStateStore = defineStore("stateStore", () => {
var arq_session_state = ref(""); var arq_session_state = ref("");
var arq_state = ref(""); var arq_state = ref("");
var beacon_state = ref(false); var beacon_state = ref(false);
var away_from_key = ref(false);
var audio_recording = ref(false); var audio_recording = ref(false);
@ -115,6 +116,7 @@ export const useStateStore = defineStore("stateStore", () => {
activities, activities,
heard_stations, heard_stations,
beacon_state, beacon_state,
away_from_key,
rigctld_started, rigctld_started,
rigctld_process, rigctld_process,
python_version, python_version,

3
modem/.gitignore vendored
View file

@ -130,3 +130,6 @@ dmypy.json
# FreeDATA config # FreeDATA config
config.ini config.ini
#FreeData DB
freedata-messages.db

View file

@ -3,6 +3,7 @@
import structlog import structlog
import lzma import lzma
import gzip import gzip
import zlib
from message_p2p import message_received, message_failed, message_transmitted from message_p2p import message_received, message_failed, message_transmitted
from enum import Enum from enum import Enum
@ -10,7 +11,8 @@ class ARQ_SESSION_TYPES(Enum):
raw = 0 raw = 0
raw_lzma = 10 raw_lzma = 10
raw_gzip = 11 raw_gzip = 11
p2pmsg_lzma = 20 p2pmsg_zlib = 20
p2p_connection = 30
class ARQDataTypeHandler: class ARQDataTypeHandler:
def __init__(self, event_manager, state_manager): def __init__(self, event_manager, state_manager):
@ -37,11 +39,17 @@ class ARQDataTypeHandler:
'failed': self.failed_raw_gzip, 'failed': self.failed_raw_gzip,
'transmitted': self.transmitted_raw_gzip, 'transmitted': self.transmitted_raw_gzip,
}, },
ARQ_SESSION_TYPES.p2pmsg_lzma: { ARQ_SESSION_TYPES.p2pmsg_zlib: {
'prepare': self.prepare_p2pmsg_lzma, 'prepare': self.prepare_p2pmsg_zlib,
'handle': self.handle_p2pmsg_lzma, 'handle': self.handle_p2pmsg_zlib,
'failed' : self.failed_p2pmsg_lzma, 'failed' : self.failed_p2pmsg_zlib,
'transmitted': self.transmitted_p2pmsg_lzma, 'transmitted': self.transmitted_p2pmsg_zlib,
},
ARQ_SESSION_TYPES.p2p_connection: {
'prepare': self.prepare_p2p_connection,
'handle': self.handle_p2p_connection,
'failed': self.failed_p2p_connection,
'transmitted': self.transmitted_p2p_connection,
}, },
} }
@ -141,24 +149,70 @@ class ARQDataTypeHandler:
decompressed_data = gzip.decompress(data) decompressed_data = gzip.decompress(data)
return decompressed_data return decompressed_data
def prepare_p2pmsg_lzma(self, data): def prepare_p2pmsg_zlib(self, data):
compressed_data = lzma.compress(data) compressed_data = lzma.compress(data)
self.log(f"Preparing LZMA compressed P2PMSG data: {len(data)} Bytes >>> {len(compressed_data)} Bytes")
compressor = zlib.compressobj(level=6, wbits=-zlib.MAX_WBITS, strategy=zlib.Z_FILTERED)
compressed_data = compressor.compress(data) + compressor.flush()
self.log(f"Preparing ZLIB compressed P2PMSG data: {len(data)} Bytes >>> {len(compressed_data)} Bytes")
return compressed_data return compressed_data
def handle_p2pmsg_lzma(self, data, statistics): def handle_p2pmsg_zlib(self, data, statistics):
decompressed_data = lzma.decompress(data) decompressor = zlib.decompressobj(wbits=-zlib.MAX_WBITS)
self.log(f"Handling LZMA compressed P2PMSG data: {len(decompressed_data)} Bytes from {len(data)} Bytes") decompressed_data = decompressor.decompress(data)
decompressed_data += decompressor.flush()
self.log(f"Handling ZLIB compressed P2PMSG data: {len(decompressed_data)} Bytes from {len(data)} Bytes")
message_received(self.event_manager, self.state_manager, decompressed_data, statistics) message_received(self.event_manager, self.state_manager, decompressed_data, statistics)
return decompressed_data return decompressed_data
def failed_p2pmsg_lzma(self, data, statistics): def failed_p2pmsg_zlib(self, data, statistics):
decompressed_data = lzma.decompress(data) decompressor = zlib.decompressobj(wbits=-zlib.MAX_WBITS)
self.log(f"Handling failed LZMA compressed P2PMSG data: {len(decompressed_data)} Bytes from {len(data)} Bytes", isWarning=True) decompressed_data = decompressor.decompress(data)
decompressed_data += decompressor.flush()
self.log(f"Handling failed ZLIB compressed P2PMSG data: {len(decompressed_data)} Bytes from {len(data)} Bytes", isWarning=True)
message_failed(self.event_manager, self.state_manager, decompressed_data, statistics) message_failed(self.event_manager, self.state_manager, decompressed_data, statistics)
return decompressed_data return decompressed_data
def transmitted_p2pmsg_lzma(self, data, statistics): def transmitted_p2pmsg_zlib(self, data, statistics):
decompressed_data = lzma.decompress(data) # Create a decompression object with the same wbits setting used for compression
decompressor = zlib.decompressobj(wbits=-zlib.MAX_WBITS)
decompressed_data = decompressor.decompress(data)
decompressed_data += decompressor.flush()
message_transmitted(self.event_manager, self.state_manager, decompressed_data, statistics) message_transmitted(self.event_manager, self.state_manager, decompressed_data, statistics)
return decompressed_data return decompressed_data
def prepare_p2p_connection(self, data):
compressed_data = gzip.compress(data)
self.log(f"Preparing gzip compressed P2P_CONNECTION data: {len(data)} Bytes >>> {len(compressed_data)} Bytes")
print(self.state_manager.p2p_connection_sessions)
return compressed_data
def handle_p2p_connection(self, data, statistics):
decompressed_data = gzip.decompress(data)
self.log(f"Handling gzip compressed P2P_CONNECTION data: {len(decompressed_data)} Bytes from {len(data)} Bytes")
print(self.state_manager.p2p_connection_sessions)
print(decompressed_data)
print(self.state_manager.p2p_connection_sessions)
for session_id in self.state_manager.p2p_connection_sessions:
print(session_id)
self.state_manager.p2p_connection_sessions[session_id].received_arq(decompressed_data)
def failed_p2p_connection(self, data, statistics):
decompressed_data = gzip.decompress(data)
self.log(f"Handling failed gzip compressed P2P_CONNECTION data: {len(decompressed_data)} Bytes from {len(data)} Bytes", isWarning=True)
print(self.state_manager.p2p_connection_sessions)
return decompressed_data
def transmitted_p2p_connection(self, data, statistics):
decompressed_data = gzip.decompress(data)
print(decompressed_data)
print(self.state_manager.p2p_connection_sessions)
for session_id in self.state_manager.p2p_connection_sessions:
print(session_id)
self.state_manager.p2p_connection_sessions[session_id].transmitted_arq()

View file

@ -1,5 +1,5 @@
import datetime import datetime
import queue, threading import threading
import codec2 import codec2
import data_frame_factory import data_frame_factory
import structlog import structlog
@ -9,35 +9,52 @@ import time
from arq_data_type_handler import ARQDataTypeHandler from arq_data_type_handler import ARQDataTypeHandler
class ARQSession(): class ARQSession:
SPEED_LEVEL_DICT = { SPEED_LEVEL_DICT = {
0: { 0: {
'mode': codec2.FREEDV_MODE.datac4, 'mode': codec2.FREEDV_MODE.datac4,
'min_snr': -10, 'min_snr': -10,
'duration_per_frame': 5.17, 'duration_per_frame': 5.17,
'bandwidth': 250,
}, },
1: { 1: {
'mode': codec2.FREEDV_MODE.datac3, 'mode': codec2.FREEDV_MODE.datac3,
'min_snr': 0, 'min_snr': 0,
'duration_per_frame': 3.19, 'duration_per_frame': 3.19,
'bandwidth': 563,
}, },
2: { 2: {
'mode': codec2.FREEDV_MODE.datac1, 'mode': codec2.FREEDV_MODE.datac1,
'min_snr': 3, 'min_snr': 3,
'duration_per_frame': 4.18, 'duration_per_frame': 4.18,
'bandwidth': 1700,
},
3: {
'mode': codec2.FREEDV_MODE.data_ofdm_2438,
'min_snr': 8,
'duration_per_frame': 6.5,
'bandwidth': 2438,
},
4: {
'mode': codec2.FREEDV_MODE.qam16c2,
'min_snr': 11,
'duration_per_frame': 2.8,
'bandwidth': 2438,
}, },
} }
def __init__(self, config: dict, modem, dxcall: str): def __init__(self, config: dict, modem, dxcall: str, state_manager):
self.logger = structlog.get_logger(type(self).__name__) self.logger = structlog.get_logger(type(self).__name__)
self.config = config self.config = config
self.event_manager: EventManager = modem.event_manager self.event_manager: EventManager = modem.event_manager
self.states = modem.states #self.states = modem.states
self.states = state_manager
self.states.setARQ(True) self.states.setARQ(True)
self.protocol_version = 1
self.snr = [] self.snr = []
self.dxcall = dxcall self.dxcall = dxcall
@ -63,7 +80,7 @@ class ARQSession():
self.bpm_histogram = [] self.bpm_histogram = []
self.time_histogram = [] self.time_histogram = []
def log(self, message, isWarning = False): def log(self, message, isWarning=False):
msg = f"[{type(self).__name__}][id={self.id}][state={self.state}]: {message}" msg = f"[{type(self).__name__}][id={self.id}][state={self.state}]: {message}"
logger = self.logger.warn if isWarning else self.logger.info logger = self.logger.warn if isWarning else self.logger.info
logger(msg) logger(msg)
@ -99,8 +116,7 @@ class ARQSession():
self.event_frame_received.set() self.event_frame_received.set()
self.log(f"Received {frame['frame_type']}") self.log(f"Received {frame['frame_type']}")
frame_type = frame['frame_type_int'] frame_type = frame['frame_type_int']
if self.state in self.STATE_TRANSITION: if self.state in self.STATE_TRANSITION and frame_type in self.STATE_TRANSITION[self.state]:
if frame_type in self.STATE_TRANSITION[self.state]:
action_name = self.STATE_TRANSITION[self.state][frame_type] action_name = self.STATE_TRANSITION[self.state][frame_type]
received_data, type_byte = getattr(self, action_name)(frame) received_data, type_byte = getattr(self, action_name)(frame)
if isinstance(received_data, bytearray) and isinstance(type_byte, int): if isinstance(received_data, bytearray) and isinstance(type_byte, int):
@ -111,9 +127,11 @@ class ARQSession():
def is_session_outdated(self): def is_session_outdated(self):
session_alivetime = time.time() - self.session_max_age session_alivetime = time.time() - self.session_max_age
if self.session_ended < session_alivetime and self.state.name in ['FAILED', 'ENDED', 'ABORTED']: return self.session_ended < session_alivetime and self.state.name in [
return True 'FAILED',
return False 'ENDED',
'ABORTED',
]
def calculate_session_duration(self): def calculate_session_duration(self):
if self.session_ended == 0: if self.session_ended == 0:
@ -123,7 +141,7 @@ class ARQSession():
def calculate_session_statistics(self, confirmed_bytes, total_bytes): def calculate_session_statistics(self, confirmed_bytes, total_bytes):
duration = self.calculate_session_duration() duration = self.calculate_session_duration()
#total_bytes = self.total_length # total_bytes = self.total_length
# self.total_length # self.total_length
duration_in_minutes = duration / 60 # Convert duration from seconds to minutes duration_in_minutes = duration / 60 # Convert duration from seconds to minutes
@ -134,9 +152,9 @@ class ARQSession():
bytes_per_minute = 0 bytes_per_minute = 0
# Convert histograms lists to dictionaries # Convert histograms lists to dictionaries
time_histogram_dict = {i: timestamp for i, timestamp in enumerate(self.time_histogram)} time_histogram_dict = dict(enumerate(self.time_histogram))
snr_histogram_dict = {i: snr for i, snr in enumerate(self.snr_histogram)} snr_histogram_dict = dict(enumerate(self.snr_histogram))
bpm_histogram_dict = {i: bpm for i, bpm in enumerate(self.bpm_histogram)} bpm_histogram_dict = dict(enumerate(self.bpm_histogram))
return { return {
'total_bytes': total_bytes, 'total_bytes': total_bytes,
@ -148,17 +166,33 @@ class ARQSession():
} }
def update_histograms(self, confirmed_bytes, total_bytes): def update_histograms(self, confirmed_bytes, total_bytes):
stats = self.calculate_session_statistics(confirmed_bytes, total_bytes) stats = self.calculate_session_statistics(confirmed_bytes, total_bytes)
self.snr_histogram.append(self.snr) self.snr_histogram.append(self.snr)
self.bpm_histogram.append(stats['bytes_per_minute']) self.bpm_histogram.append(stats['bytes_per_minute'])
self.time_histogram.append(datetime.datetime.now().isoformat()) self.time_histogram.append(datetime.datetime.now().isoformat())
# Limit the size of each histogram to the last 20 entries
self.snr_histogram = self.snr_histogram[-20:]
self.bpm_histogram = self.bpm_histogram[-20:]
self.time_histogram = self.time_histogram[-20:]
return stats return stats
def get_appropriate_speed_level(self, snr): def get_appropriate_speed_level(self, snr, maximum_bandwidth=None):
# Start with the lowest speed level as default if maximum_bandwidth is None:
# In case of a not fitting SNR, we return the lowest speed level maximum_bandwidth = self.config['MODEM']['maximum_bandwidth']
# Adjust maximum_bandwidth based on special conditions or invalid configurations
if maximum_bandwidth == 0:
# Use the maximum available bandwidth from the speed level dictionary
maximum_bandwidth = max(details['bandwidth'] for details in self.SPEED_LEVEL_DICT.values())
# Initialize appropriate_speed_level to the lowest level that meets the minimum criteria
appropriate_speed_level = min(self.SPEED_LEVEL_DICT.keys()) appropriate_speed_level = min(self.SPEED_LEVEL_DICT.keys())
for level, details in self.SPEED_LEVEL_DICT.items(): for level, details in self.SPEED_LEVEL_DICT.items():
if snr >= details['min_snr'] and level > appropriate_speed_level: if snr >= details['min_snr'] and details['bandwidth'] <= maximum_bandwidth and level > appropriate_speed_level:
appropriate_speed_level = level appropriate_speed_level = level
return appropriate_speed_level return appropriate_speed_level

View file

@ -18,7 +18,7 @@ class IRS_State(Enum):
class ARQSessionIRS(arq_session.ARQSession): class ARQSessionIRS(arq_session.ARQSession):
TIMEOUT_CONNECT = 55 #14.2 TIMEOUT_CONNECT = 55 #14.2
TIMEOUT_DATA = 60 TIMEOUT_DATA = 120
STATE_TRANSITION = { STATE_TRANSITION = {
IRS_State.NEW: { IRS_State.NEW: {
@ -59,8 +59,8 @@ class ARQSessionIRS(arq_session.ARQSession):
}, },
} }
def __init__(self, config: dict, modem, dxcall: str, session_id: int): def __init__(self, config: dict, modem, dxcall: str, session_id: int, state_manager):
super().__init__(config, modem, dxcall) super().__init__(config, modem, dxcall, state_manager)
self.id = session_id self.id = session_id
self.dxcall = dxcall self.dxcall = dxcall
@ -76,14 +76,16 @@ class ARQSessionIRS(arq_session.ARQSession):
self.received_bytes = 0 self.received_bytes = 0
self.received_crc = None self.received_crc = None
self.maximum_bandwidth = 0
self.abort = False self.abort = False
def all_data_received(self): def all_data_received(self):
print(f"{self.total_length} vs {self.received_bytes}")
return self.total_length == self.received_bytes return self.total_length == self.received_bytes
def final_crc_matches(self) -> bool: def final_crc_matches(self) -> bool:
match = self.total_crc == helpers.get_crc_32(bytes(self.received_data)).hex() return self.total_crc == helpers.get_crc_32(bytes(self.received_data)).hex()
return match
def transmit_and_wait(self, frame, timeout, mode): def transmit_and_wait(self, frame, timeout, mode):
self.event_frame_received.clear() self.event_frame_received.clear()
@ -99,13 +101,26 @@ class ARQSessionIRS(arq_session.ARQSession):
thread_wait.start() thread_wait.start()
def send_open_ack(self, open_frame): def send_open_ack(self, open_frame):
self.maximum_bandwidth = open_frame['maximum_bandwidth']
# check for maximum bandwidth. If ISS bandwidth is higher than own, then use own
if open_frame['maximum_bandwidth'] > self.config['MODEM']['maximum_bandwidth']:
self.maximum_bandwidth = self.config['MODEM']['maximum_bandwidth']
self.event_manager.send_arq_session_new( self.event_manager.send_arq_session_new(
False, self.id, self.dxcall, 0, self.state.name) False, self.id, self.dxcall, 0, self.state.name)
if open_frame['protocol_version'] not in [self.protocol_version]:
self.abort = True
self.log(f"Protocol version mismatch! Setting disconnect flag!", isWarning=True)
self.set_state(IRS_State.ABORTED)
ack_frame = self.frame_factory.build_arq_session_open_ack( ack_frame = self.frame_factory.build_arq_session_open_ack(
self.id, self.id,
self.dxcall, self.dxcall,
self.version, self.version,
self.snr, flag_abort=self.abort) self.snr, flag_abort=self.abort)
self.launch_transmit_and_wait(ack_frame, self.TIMEOUT_CONNECT, mode=FREEDV_MODE.signalling) self.launch_transmit_and_wait(ack_frame, self.TIMEOUT_CONNECT, mode=FREEDV_MODE.signalling)
if not self.abort: if not self.abort:
self.set_state(IRS_State.OPEN_ACK_SENT) self.set_state(IRS_State.OPEN_ACK_SENT)
@ -133,12 +148,14 @@ class ARQSessionIRS(arq_session.ARQSession):
return None, None return None, None
def process_incoming_data(self, frame): def process_incoming_data(self, frame):
print(frame)
if frame['offset'] != self.received_bytes: if frame['offset'] != self.received_bytes:
self.log(f"Discarding data offset {frame['offset']}") # TODO: IF WE HAVE AN OFFSET BECAUSE OF A SPEED LEVEL CHANGE FOR EXAMPLE,
return False # TODO: WE HAVE TO DISCARD THE LAST BYTES, BUT NOT returning False!!
self.log(f"Discarding data offset {frame['offset']} vs {self.received_bytes}", isWarning=True)
#return False
remaining_data_length = self.total_length - self.received_bytes remaining_data_length = self.total_length - self.received_bytes
# Is this the last data part? # Is this the last data part?
if remaining_data_length <= len(frame['data']): if remaining_data_length <= len(frame['data']):
# we only want the remaining length, not the entire frame data # we only want the remaining length, not the entire frame data
@ -148,7 +165,8 @@ class ARQSessionIRS(arq_session.ARQSession):
data_part = frame['data'] data_part = frame['data']
self.received_data[frame['offset']:] = data_part self.received_data[frame['offset']:] = data_part
self.received_bytes += len(data_part) #self.received_bytes += len(data_part)
self.received_bytes = len(self.received_data)
self.log(f"Received {self.received_bytes}/{self.total_length} bytes") self.log(f"Received {self.received_bytes}/{self.total_length} bytes")
self.event_manager.send_arq_session_progress( self.event_manager.send_arq_session_progress(
False, self.id, self.dxcall, self.received_bytes, self.total_length, self.state.name, self.calculate_session_statistics(self.received_bytes, self.total_length)) False, self.id, self.dxcall, self.received_bytes, self.total_length, self.state.name, self.calculate_session_statistics(self.received_bytes, self.total_length))
@ -164,41 +182,35 @@ class ARQSessionIRS(arq_session.ARQSession):
self.calibrate_speed_settings(burst_frame=burst_frame) self.calibrate_speed_settings(burst_frame=burst_frame)
ack = self.frame_factory.build_arq_burst_ack( ack = self.frame_factory.build_arq_burst_ack(
self.id, self.id,
self.received_bytes,
self.speed_level, self.speed_level,
self.frames_per_burst,
self.snr,
flag_abort=self.abort flag_abort=self.abort
) )
self.set_state(IRS_State.BURST_REPLY_SENT) self.set_state(IRS_State.BURST_REPLY_SENT)
self.launch_transmit_and_wait(ack, self.TIMEOUT_DATA, mode=FREEDV_MODE.signalling) self.event_manager.send_arq_session_progress(False, self.id, self.dxcall, self.received_bytes,
self.total_length, self.state.name,
statistics=self.calculate_session_statistics(
self.received_bytes, self.total_length))
self.launch_transmit_and_wait(ack, self.TIMEOUT_DATA, mode=FREEDV_MODE.signalling_ack)
return None, None return None, None
if self.final_crc_matches(): if self.final_crc_matches():
self.log("All data received successfully!") self.log("All data received successfully!")
ack = self.frame_factory.build_arq_burst_ack(self.id, ack = self.frame_factory.build_arq_burst_ack(self.id,
self.received_bytes,
self.speed_level, self.speed_level,
self.frames_per_burst,
self.snr,
flag_final=True, flag_final=True,
flag_checksum=True) flag_checksum=True)
self.transmit_frame(ack, mode=FREEDV_MODE.signalling) self.transmit_frame(ack, mode=FREEDV_MODE.signalling_ack)
self.log("ACK sent") self.log("ACK sent")
self.session_ended = time.time() self.session_ended = time.time()
self.set_state(IRS_State.ENDED) self.set_state(IRS_State.ENDED)
self.event_manager.send_arq_session_finished(
False, self.id, self.dxcall, True, self.state.name, data=self.received_data, statistics=self.calculate_session_statistics(self.received_bytes, self.total_length))
return self.received_data, self.type_byte return self.received_data, self.type_byte
else: else:
ack = self.frame_factory.build_arq_burst_ack(self.id, ack = self.frame_factory.build_arq_burst_ack(self.id,
self.received_bytes,
self.speed_level, self.speed_level,
self.frames_per_burst,
self.snr,
flag_final=True, flag_final=True,
flag_checksum=False) flag_checksum=False)
self.transmit_frame(ack, mode=FREEDV_MODE.signalling) self.transmit_frame(ack, mode=FREEDV_MODE.signalling)
@ -212,7 +224,7 @@ class ARQSessionIRS(arq_session.ARQSession):
received_speed_level = 0 received_speed_level = 0
latest_snr = self.snr if self.snr else -10 latest_snr = self.snr if self.snr else -10
appropriate_speed_level = self.get_appropriate_speed_level(latest_snr) appropriate_speed_level = self.get_appropriate_speed_level(latest_snr, self.maximum_bandwidth)
modes_to_decode = {} modes_to_decode = {}
# Log the latest SNR, current, appropriate speed levels, and the previous speed level # Log the latest SNR, current, appropriate speed levels, and the previous speed level
@ -247,7 +259,7 @@ class ARQSessionIRS(arq_session.ARQSession):
return self.speed_level return self.speed_level
def abort_transmission(self): def abort_transmission(self):
self.log(f"Aborting transmission... setting abort flag") self.log("Aborting transmission... setting abort flag")
self.abort = True self.abort = True
def send_stop_ack(self, stop_frame): def send_stop_ack(self, stop_frame):
@ -263,7 +275,7 @@ class ARQSessionIRS(arq_session.ARQSession):
# final function for failed transmissions # final function for failed transmissions
self.session_ended = time.time() self.session_ended = time.time()
self.set_state(IRS_State.FAILED) self.set_state(IRS_State.FAILED)
self.log(f"Transmission failed!") self.log("Transmission failed!")
self.event_manager.send_arq_session_finished(True, self.id, self.dxcall,False, self.state.name, statistics=self.calculate_session_statistics(self.received_bytes, self.total_length)) self.event_manager.send_arq_session_finished(True, self.id, self.dxcall,False, self.state.name, statistics=self.calculate_session_statistics(self.received_bytes, self.total_length))
self.states.setARQ(False) self.states.setARQ(False)
return None, None return None, None

View file

@ -1,6 +1,5 @@
import threading import threading
import data_frame_factory import data_frame_factory
import queue
import random import random
from codec2 import FREEDV_MODE from codec2 import FREEDV_MODE
from modem_frametypes import FRAME_TYPE from modem_frametypes import FRAME_TYPE
@ -25,7 +24,7 @@ class ARQSessionISS(arq_session.ARQSession):
# DJ2LS: 3 seconds seems to be too small for radios with a too slow PTT toggle time # DJ2LS: 3 seconds seems to be too small for radios with a too slow PTT toggle time
# DJ2LS: 3.5 seconds is working well WITHOUT a channel busy detection delay # DJ2LS: 3.5 seconds is working well WITHOUT a channel busy detection delay
TIMEOUT_CHANNEL_BUSY = 2 TIMEOUT_CHANNEL_BUSY = 0
TIMEOUT_CONNECT_ACK = 3.5 + TIMEOUT_CHANNEL_BUSY TIMEOUT_CONNECT_ACK = 3.5 + TIMEOUT_CHANNEL_BUSY
TIMEOUT_TRANSFER = 3.5 + TIMEOUT_CHANNEL_BUSY TIMEOUT_TRANSFER = 3.5 + TIMEOUT_CHANNEL_BUSY
TIMEOUT_STOP_ACK = 3.5 + TIMEOUT_CHANNEL_BUSY TIMEOUT_STOP_ACK = 3.5 + TIMEOUT_CHANNEL_BUSY
@ -54,13 +53,14 @@ class ARQSessionISS(arq_session.ARQSession):
} }
def __init__(self, config: dict, modem, dxcall: str, state_manager, data: bytearray, type_byte: bytes): def __init__(self, config: dict, modem, dxcall: str, state_manager, data: bytearray, type_byte: bytes):
super().__init__(config, modem, dxcall) super().__init__(config, modem, dxcall, state_manager)
self.state_manager = state_manager self.state_manager = state_manager
self.data = data self.data = data
self.total_length = len(data) self.total_length = len(data)
self.data_crc = '' self.data_crc = ''
self.type_byte = type_byte self.type_byte = type_byte
self.confirmed_bytes = 0 self.confirmed_bytes = 0
self.expected_byte_offset = 0
self.state = ISS_State.NEW self.state = ISS_State.NEW
self.state_enum = ISS_State # needed for access State enum from outside self.state_enum = ISS_State # needed for access State enum from outside
@ -94,7 +94,9 @@ class ARQSessionISS(arq_session.ARQSession):
if retries == 8 and isARQBurst and self.speed_level > 0: if retries == 8 and isARQBurst and self.speed_level > 0:
self.log("SENDING IN FALLBACK SPEED LEVEL", isWarning=True) self.log("SENDING IN FALLBACK SPEED LEVEL", isWarning=True)
self.speed_level = 0 self.speed_level = 0
self.send_data({'flag':{'ABORT': False, 'FINAL': False}, 'speed_level': self.speed_level}) print(f" CONFIRMED BYTES: {self.confirmed_bytes}")
self.send_data({'flag':{'ABORT': False, 'FINAL': False}, 'speed_level': self.speed_level}, fallback=True)
return return
self.set_state(ISS_State.FAILED) self.set_state(ISS_State.FAILED)
@ -105,9 +107,11 @@ class ARQSessionISS(arq_session.ARQSession):
twr.start() twr.start()
def start(self): def start(self):
maximum_bandwidth = self.config['MODEM']['maximum_bandwidth']
print(maximum_bandwidth)
self.event_manager.send_arq_session_new( self.event_manager.send_arq_session_new(
True, self.id, self.dxcall, self.total_length, self.state.name) True, self.id, self.dxcall, self.total_length, self.state.name)
session_open_frame = self.frame_factory.build_arq_session_open(self.dxcall, self.id) session_open_frame = self.frame_factory.build_arq_session_open(self.dxcall, self.id, maximum_bandwidth, self.protocol_version)
self.launch_twr(session_open_frame, self.TIMEOUT_CONNECT_ACK, self.RETRIES_CONNECT, mode=FREEDV_MODE.signalling) self.launch_twr(session_open_frame, self.TIMEOUT_CONNECT_ACK, self.RETRIES_CONNECT, mode=FREEDV_MODE.signalling)
self.set_state(ISS_State.OPEN_SENT) self.set_state(ISS_State.OPEN_SENT)
@ -136,8 +140,7 @@ class ARQSessionISS(arq_session.ARQSession):
def send_info(self, irs_frame): def send_info(self, irs_frame):
# check if we received an abort flag # check if we received an abort flag
if irs_frame["flag"]["ABORT"]: if irs_frame["flag"]["ABORT"]:
self.transmission_aborted(irs_frame) return self.transmission_aborted(irs_frame)
return
info_frame = self.frame_factory.build_arq_session_info(self.id, self.total_length, info_frame = self.frame_factory.build_arq_session_info(self.id, self.total_length,
helpers.get_crc_32(self.data), helpers.get_crc_32(self.data),
@ -148,16 +151,26 @@ class ARQSessionISS(arq_session.ARQSession):
return None, None return None, None
def send_data(self, irs_frame): def send_data(self, irs_frame, fallback=None):
# interrupt transmission when aborting
if self.state in [ISS_State.ABORTED, ISS_State.ABORTING]:
self.event_frame_received.set()
self.send_stop()
return
# update statistics # update statistics
self.update_histograms(self.confirmed_bytes, self.total_length) self.update_histograms(self.confirmed_bytes, self.total_length)
self.update_speed_level(irs_frame) self.update_speed_level(irs_frame)
if 'offset' in irs_frame:
self.confirmed_bytes = irs_frame['offset']
if self.expected_byte_offset > self.total_length:
self.confirmed_bytes = self.total_length
elif not fallback:
self.confirmed_bytes = self.expected_byte_offset
self.log(f"IRS confirmed {self.confirmed_bytes}/{self.total_length} bytes") self.log(f"IRS confirmed {self.confirmed_bytes}/{self.total_length} bytes")
self.event_manager.send_arq_session_progress( self.event_manager.send_arq_session_progress(True, self.id, self.dxcall, self.confirmed_bytes, self.total_length, self.state.name, statistics=self.calculate_session_statistics(self.confirmed_bytes, self.total_length))
True, self.id, self.dxcall, self.confirmed_bytes, self.total_length, self.state.name, statistics=self.calculate_session_statistics(self.confirmed_bytes, self.total_length))
# check if we received an abort flag # check if we received an abort flag
if irs_frame["flag"]["ABORT"]: if irs_frame["flag"]["ABORT"]:
@ -174,12 +187,16 @@ class ARQSessionISS(arq_session.ARQSession):
payload_size = self.get_data_payload_size() payload_size = self.get_data_payload_size()
burst = [] burst = []
for f in range(0, self.frames_per_burst): for _ in range(0, self.frames_per_burst):
offset = self.confirmed_bytes offset = self.confirmed_bytes
#self.expected_byte_offset = offset
payload = self.data[offset : offset + payload_size] payload = self.data[offset : offset + payload_size]
#self.expected_byte_offset = offset + payload_size
self.expected_byte_offset = offset + len(payload)
#print(f"EXPECTED----------------------{self.expected_byte_offset}")
data_frame = self.frame_factory.build_arq_burst_frame( data_frame = self.frame_factory.build_arq_burst_frame(
self.SPEED_LEVEL_DICT[self.speed_level]["mode"], self.SPEED_LEVEL_DICT[self.speed_level]["mode"],
self.id, self.confirmed_bytes, payload, self.speed_level) self.id, offset, payload, self.speed_level)
burst.append(data_frame) burst.append(data_frame)
self.launch_twr(burst, self.TIMEOUT_TRANSFER, self.RETRIES_CONNECT, mode='auto', isARQBurst=True) self.launch_twr(burst, self.TIMEOUT_TRANSFER, self.RETRIES_CONNECT, mode='auto', isARQBurst=True)
self.set_state(ISS_State.BURST_SENT) self.set_state(ISS_State.BURST_SENT)
@ -191,6 +208,10 @@ class ARQSessionISS(arq_session.ARQSession):
self.set_state(ISS_State.ENDED) self.set_state(ISS_State.ENDED)
self.log(f"All data transfered! flag_final={irs_frame['flag']['FINAL']}, flag_checksum={irs_frame['flag']['CHECKSUM']}") self.log(f"All data transfered! flag_final={irs_frame['flag']['FINAL']}, flag_checksum={irs_frame['flag']['CHECKSUM']}")
self.event_manager.send_arq_session_finished(True, self.id, self.dxcall,True, self.state.name, statistics=self.calculate_session_statistics(self.confirmed_bytes, self.total_length)) self.event_manager.send_arq_session_finished(True, self.id, self.dxcall,True, self.state.name, statistics=self.calculate_session_statistics(self.confirmed_bytes, self.total_length))
#print(self.state_manager.p2p_connection_sessions)
#print(self.arq_data_type_handler.state_manager.p2p_connection_sessions)
self.arq_data_type_handler.transmitted(self.type_byte, self.data, self.calculate_session_statistics(self.confirmed_bytes, self.total_length)) self.arq_data_type_handler.transmitted(self.type_byte, self.data, self.calculate_session_statistics(self.confirmed_bytes, self.total_length))
self.state_manager.remove_arq_iss_session(self.id) self.state_manager.remove_arq_iss_session(self.id)
self.states.setARQ(False) self.states.setARQ(False)
@ -200,7 +221,7 @@ class ARQSessionISS(arq_session.ARQSession):
# final function for failed transmissions # final function for failed transmissions
self.session_ended = time.time() self.session_ended = time.time()
self.set_state(ISS_State.FAILED) self.set_state(ISS_State.FAILED)
self.log(f"Transmission failed!") self.log("Transmission failed!")
self.event_manager.send_arq_session_finished(True, self.id, self.dxcall,False, self.state.name, statistics=self.calculate_session_statistics(self.confirmed_bytes, self.total_length)) self.event_manager.send_arq_session_finished(True, self.id, self.dxcall,False, self.state.name, statistics=self.calculate_session_statistics(self.confirmed_bytes, self.total_length))
self.states.setARQ(False) self.states.setARQ(False)
@ -209,7 +230,7 @@ class ARQSessionISS(arq_session.ARQSession):
def abort_transmission(self, irs_frame=None): def abort_transmission(self, irs_frame=None):
# function for starting the abort sequence # function for starting the abort sequence
self.log(f"aborting transmission...") self.log("aborting transmission...")
self.set_state(ISS_State.ABORTING) self.set_state(ISS_State.ABORTING)
self.event_manager.send_arq_session_finished( self.event_manager.send_arq_session_finished(
@ -218,9 +239,6 @@ class ARQSessionISS(arq_session.ARQSession):
# break actual retries # break actual retries
self.event_frame_received.set() self.event_frame_received.set()
# start with abort sequence
self.send_stop()
def send_stop(self): def send_stop(self):
stop_frame = self.frame_factory.build_arq_stop(self.id) stop_frame = self.frame_factory.build_arq_stop(self.id)
self.launch_twr(stop_frame, self.TIMEOUT_STOP_ACK, self.RETRIES_CONNECT, mode=FREEDV_MODE.signalling) self.launch_twr(stop_frame, self.TIMEOUT_STOP_ACK, self.RETRIES_CONNECT, mode=FREEDV_MODE.signalling)

View file

@ -1,16 +1,12 @@
""" """
Gather information about audio devices. Gather information about audio devices.
""" """
import atexit
import multiprocessing import multiprocessing
import crcengine import crcengine
import sounddevice as sd import sounddevice as sd
import structlog import structlog
import numpy as np import numpy as np
import queue import queue
import threading
atexit.register(sd._terminate)
log = structlog.get_logger("audio") log = structlog.get_logger("audio")
@ -214,6 +210,36 @@ def set_audio_volume(datalist: np.ndarray, dB: float) -> np.ndarray:
RMS_COUNTER = 0 RMS_COUNTER = 0
CHANNEL_BUSY_DELAY = 0 CHANNEL_BUSY_DELAY = 0
def prepare_data_for_fft(data, target_length_samples=400):
"""
Prepare data array for FFT by padding if necessary to match the target length.
Center the data if it's shorter than the target length.
Parameters:
- data: numpy array of np.int16, representing the input data.
- target_length_samples: int, the target length of the data in samples.
Returns:
- numpy array of np.int16, padded and/or centered if necessary.
"""
# Calculate the current length in samples
current_length_samples = data.size
# Check if padding is needed
if current_length_samples < target_length_samples:
# Calculate total padding needed
total_pad_length = target_length_samples - current_length_samples
# Calculate padding on each side
pad_before = total_pad_length // 2
pad_after = total_pad_length - pad_before
# Pad the data to center it
data_padded = np.pad(data, (pad_before, pad_after), 'constant', constant_values=(0,))
return data_padded
else:
# No padding needed, return original data
return data
def calculate_fft(data, fft_queue, states) -> None: def calculate_fft(data, fft_queue, states) -> None:
""" """
Calculate an average signal strength of the channel to assess Calculate an average signal strength of the channel to assess
@ -229,6 +255,7 @@ def calculate_fft(data, fft_queue, states) -> None:
global RMS_COUNTER, CHANNEL_BUSY_DELAY global RMS_COUNTER, CHANNEL_BUSY_DELAY
try: try:
data = prepare_data_for_fft(data, target_length_samples=800)
fftarray = np.fft.rfft(data) fftarray = np.fft.rfft(data)
# Set value 0 to 1 to avoid division by zero # Set value 0 to 1 to avoid division by zero
@ -325,6 +352,8 @@ def calculate_fft(data, fft_queue, states) -> None:
# erase queue if greater than 3 # erase queue if greater than 3
if fft_queue.qsize() >= 1: if fft_queue.qsize() >= 1:
fft_queue = queue.Queue() fft_queue = queue.Queue()
fft_queue.put(dfftlist[:315]) # 315 --> bandwidth 3200 #fft_queue.put(dfftlist[:315]) # 315 --> bandwidth 3200
fft_queue.put(dfftlist) # 315 --> bandwidth 3200
except Exception as err: except Exception as err:
print(f"[MDM] calculate_fft: Exception: {err}") print(f"[MDM] calculate_fft: Exception: {err}")

View file

@ -7,6 +7,8 @@ Python interface to the C-language codec2 library.
# pylint: disable=import-outside-toplevel, attribute-defined-outside-init # pylint: disable=import-outside-toplevel, attribute-defined-outside-init
import ctypes import ctypes
from ctypes import *
import hashlib
import glob import glob
import os import os
import sys import sys
@ -25,12 +27,17 @@ class FREEDV_MODE(Enum):
Enumeration for codec2 modes and names Enumeration for codec2 modes and names
""" """
signalling = 19 signalling = 19
signalling_ack = 20
datac0 = 14 datac0 = 14
datac1 = 10 datac1 = 10
datac3 = 12 datac3 = 12
datac4 = 18 datac4 = 18
datac13 = 19 datac13 = 19
datac14 = 20
data_ofdm_500 = 21500
data_ofdm_2438 = 2124381
#data_qam_2438 = 2124382
qam16c2 = 22
class FREEDV_MODE_USED_SLOTS(Enum): class FREEDV_MODE_USED_SLOTS(Enum):
""" """
@ -43,9 +50,11 @@ class FREEDV_MODE_USED_SLOTS(Enum):
datac3 = [False, False, True, False, False] datac3 = [False, False, True, False, False]
datac4 = [False, False, True, False, False] datac4 = [False, False, True, False, False]
datac13 = [False, False, True, False, False] datac13 = [False, False, True, False, False]
fsk_ldpc = [False, False, True, False, False] datac14 = [False, False, True, False, False]
fsk_ldpc_0 = [False, False, True, False, False] data_ofdm_500 = [False, False, True, False, False]
fsk_ldpc_1 = [False, False, True, False, False] data_ofdm_2438 = [True, True, True, True, True]
data_qam_2438 = [True, True, True, True, True]
qam16c2 = [True, True, True, True, True]
# Function for returning the mode value # Function for returning the mode value
def freedv_get_mode_value_by_name(mode: str) -> int: def freedv_get_mode_value_by_name(mode: str) -> int:
@ -90,7 +99,6 @@ elif sys.platform in ["win32", "win64"]:
files = glob.glob(os.path.join(script_dir, "**\\*libcodec2*.dll"), recursive=True) files = glob.glob(os.path.join(script_dir, "**\\*libcodec2*.dll"), recursive=True)
else: else:
files = [] files = []
api = None api = None
for file in files: for file in files:
try: try:
@ -105,7 +113,7 @@ for file in files:
if api is None or "api" not in locals(): if api is None or "api" not in locals():
log.critical("[C2 ] Error: Libcodec2 not loaded - Exiting") log.critical("[C2 ] Error: Libcodec2 not loaded - Exiting")
sys.exit(1) sys.exit(1)
log.info("[C2 ] Libcodec2 loaded...") #log.info("[C2 ] Libcodec2 loaded...", path=file)
# ctypes function init # ctypes function init
# api.freedv_set_tuning_range.restype = ctypes.c_int # api.freedv_set_tuning_range.restype = ctypes.c_int
@ -167,64 +175,6 @@ api.freedv_get_n_max_modem_samples.restype = ctypes.c_int
api.FREEDV_FS_8000 = 8000 # type: ignore api.FREEDV_FS_8000 = 8000 # type: ignore
# -------------------------------- FSK LDPC MODE SETTINGS
class ADVANCED(ctypes.Structure):
"""Advanced structure for fsk modes"""
_fields_ = [
("interleave_frames", ctypes.c_int),
("M", ctypes.c_int),
("Rs", ctypes.c_int),
("Fs", ctypes.c_int),
("first_tone", ctypes.c_int),
("tone_spacing", ctypes.c_int),
("codename", ctypes.c_char_p),
]
# pylint: disable=pointless-string-statement
"""
adv.interleave_frames = 0 # max amplitude
adv.M = 2 # number of fsk tones 2/4
adv.Rs = 100 # symbol rate
adv.Fs = 8000 # sample rate
adv.first_tone = 1500 # first tone freq
adv.tone_spacing = 200 # shift between tones
adv.codename = "H_128_256_5".encode("utf-8") # code word
HRA_112_112 rate 0.50 (224,112) BPF: 14 not working
HRA_56_56 rate 0.50 (112,56) BPF: 7 not working
H_2064_516_sparse rate 0.80 (2580,2064) BPF: 258 working
HRAb_396_504 rate 0.79 (504,396) BPF: 49 not working
H_256_768_22 rate 0.33 (768,256) BPF: 32 working
H_256_512_4 rate 0.50 (512,256) BPF: 32 working
HRAa_1536_512 rate 0.75 (2048,1536) BPF: 192 not working
H_128_256_5 rate 0.50 (256,128) BPF: 16 working
H_4096_8192_3d rate 0.50 (8192,4096) BPF: 512 not working
H_16200_9720 rate 0.60 (16200,9720) BPF: 1215 not working
H_1024_2048_4f rate 0.50 (2048,1024) BPF: 128 working
"""
# --------------- 2 FSK H_128_256_5, 16 bytes
api.FREEDV_MODE_FSK_LDPC_0_ADV = ADVANCED() # type: ignore
api.FREEDV_MODE_FSK_LDPC_0_ADV.interleave_frames = 0
api.FREEDV_MODE_FSK_LDPC_0_ADV.M = 4
api.FREEDV_MODE_FSK_LDPC_0_ADV.Rs = 500
api.FREEDV_MODE_FSK_LDPC_0_ADV.Fs = 8000
api.FREEDV_MODE_FSK_LDPC_0_ADV.first_tone = 1150 # 1150 4fsk, 1500 2fsk
api.FREEDV_MODE_FSK_LDPC_0_ADV.tone_spacing = 200 # 200
api.FREEDV_MODE_FSK_LDPC_0_ADV.codename = "H_128_256_5".encode("utf-8") # code word
# --------------- 4 H_256_512_4, 7 bytes
api.FREEDV_MODE_FSK_LDPC_1_ADV = ADVANCED() # type: ignore
api.FREEDV_MODE_FSK_LDPC_1_ADV.interleave_frames = 0
api.FREEDV_MODE_FSK_LDPC_1_ADV.M = 4
api.FREEDV_MODE_FSK_LDPC_1_ADV.Rs = 1000
api.FREEDV_MODE_FSK_LDPC_1_ADV.Fs = 8000
api.FREEDV_MODE_FSK_LDPC_1_ADV.first_tone = 1150 # 1250 4fsk, 1500 2fsk
api.FREEDV_MODE_FSK_LDPC_1_ADV.tone_spacing = 200
api.FREEDV_MODE_FSK_LDPC_1_ADV.codename = "H_4096_8192_3d".encode("utf-8") # code word
# ------- MODEM STATS STRUCTURES # ------- MODEM STATS STRUCTURES
MODEM_STATS_NC_MAX = 50 + 1 * 2 MODEM_STATS_NC_MAX = 50 + 1 * 2
@ -236,6 +186,8 @@ MODEM_STATS_MAX_F_HZ = 4000
MODEM_STATS_MAX_F_EST = 4 MODEM_STATS_MAX_F_EST = 4
class MODEMSTATS(ctypes.Structure): class MODEMSTATS(ctypes.Structure):
"""Modem statistics structure""" """Modem statistics structure"""
@ -421,34 +373,22 @@ class resampler:
return out48 return out48
def open_instance(mode: int) -> ctypes.c_void_p: def open_instance(mode: int) -> ctypes.c_void_p:
""" data_custom = 21
Return a codec2 instance of the type `mode` if mode in [FREEDV_MODE.data_ofdm_500.value, FREEDV_MODE.data_ofdm_2438.value]:
#if mode in [FREEDV_MODE.data_ofdm_500.value, FREEDV_MODE.data_ofdm_2438.value, FREEDV_MODE.data_qam_2438]:
:param mode: Type of codec2 instance to return custom_params = ofdm_configurations[mode]
:type mode: Union[int, str] return ctypes.cast(
:return: C-function of the requested codec2 instance api.freedv_open_advanced(
:rtype: ctypes.c_void_p data_custom,
""" ctypes.byref(custom_params),
# if mode in [FREEDV_MODE.fsk_ldpc_0.value]: ),
# return ctypes.cast( ctypes.c_void_p,
# api.freedv_open_advanced( )
# FREEDV_MODE.fsk_ldpc.value, else:
# ctypes.byref(api.FREEDV_MODE_FSK_LDPC_0_ADV), if mode not in [data_custom]:
# ),
# ctypes.c_void_p,
# )
#
# if mode in [FREEDV_MODE.fsk_ldpc_1.value]:
# return ctypes.cast(
# api.freedv_open_advanced(
# FREEDV_MODE.fsk_ldpc.value,
# ctypes.byref(api.FREEDV_MODE_FSK_LDPC_1_ADV),
# ),
# ctypes.c_void_p,
# )
#
return ctypes.cast(api.freedv_open(mode), ctypes.c_void_p) return ctypes.cast(api.freedv_open(mode), ctypes.c_void_p)
def get_bytes_per_frame(mode: int) -> int: def get_bytes_per_frame(mode: int) -> int:
""" """
Provide bytes per frame information for accessing from data handler Provide bytes per frame information for accessing from data handler
@ -462,3 +402,249 @@ def get_bytes_per_frame(mode: int) -> int:
# TODO add close session # TODO add close session
# get number of bytes per frame for mode # get number of bytes per frame for mode
return int(api.freedv_get_bits_per_modem_frame(freedv) / 8) return int(api.freedv_get_bits_per_modem_frame(freedv) / 8)
MAX_UW_BITS = 192
class OFDM_CONFIG(ctypes.Structure):
_fields_ = [
("tx_centre", ctypes.c_float), # TX Centre Audio Frequency
("rx_centre", ctypes.c_float), # RX Centre Audio Frequency
("fs", ctypes.c_float), # Sample Frequency
("rs", ctypes.c_float), # Symbol Rate
("ts", ctypes.c_float), # Symbol duration
("tcp", ctypes.c_float), # Cyclic Prefix duration
("timing_mx_thresh", ctypes.c_float), # Threshold for timing metrics
("nc", ctypes.c_int), # Number of carriers
("ns", ctypes.c_int), # Number of Symbol frames
("np", ctypes.c_int), # Number of modem frames per packet
("bps", ctypes.c_int), # Bits per Symbol
("txtbits", ctypes.c_int), # Number of auxiliary data bits
("nuwbits", ctypes.c_int), # Number of unique word bits
("bad_uw_errors", ctypes.c_int), # Threshold for bad unique word detection
("ftwindowwidth", ctypes.c_int), # Filter window width
("edge_pilots", ctypes.c_int), # Edge pilots configuration
("state_machine", ctypes.c_char_p), # Name of sync state machine used
("codename", ctypes.c_char_p), # LDPC codename
("tx_uw", ctypes.c_uint8 * MAX_UW_BITS), # User defined unique word
("amp_est_mode", ctypes.c_int), # Amplitude estimator algorithm mode
("tx_bpf_en", ctypes.c_bool), # TX BPF enable flag
("rx_bpf_en", ctypes.c_bool), # RX BPF enable flag
("foff_limiter", ctypes.c_bool), # Frequency offset limiter enable flag
("amp_scale", ctypes.c_float), # Amplitude scale factor
("clip_gain1", ctypes.c_float), # Pre-clipping gain
("clip_gain2", ctypes.c_float), # Post-clipping gain
("clip_en", ctypes.c_bool), # Clipping enable flag
("mode", ctypes.c_char * 16), # OFDM mode in string form
("data_mode", ctypes.c_char_p), # Data mode ("streaming", "burst", etc.)
("fmin", ctypes.c_float), # Minimum frequency for tuning range
("fmax", ctypes.c_float), # Maximum frequency for tuning range
("EsNodB", ctypes.c_float),
]
class FREEDV_ADVANCED(ctypes.Structure):
"""Advanced structure for fsk and ofdm modes"""
_fields_ = [
("interleave_frames", ctypes.c_int),
("M", ctypes.c_int),
("Rs", ctypes.c_int),
("Fs", ctypes.c_int),
("first_tone", ctypes.c_int),
("tone_spacing", ctypes.c_int),
("codename", ctypes.c_char_p),
("config", ctypes.POINTER(OFDM_CONFIG))
]
api.freedv_open_advanced.argtypes = [ctypes.c_int, ctypes.POINTER(FREEDV_ADVANCED)]
api.freedv_open_advanced.restype = ctypes.c_void_p
def create_default_ofdm_config():
uw_sequence = (c_uint8 * MAX_UW_BITS)(*([0] * MAX_UW_BITS))
ofdm_default_config = OFDM_CONFIG(
tx_centre=1500.0,
rx_centre=1500.0,
fs=8000.0,
rs=62.5,
ts=0.016,
tcp=0.006,
timing_mx_thresh=0.10,
nc=9,
ns=5,
np=29,
bps=2,
txtbits=0,
nuwbits=40,
bad_uw_errors=10,
ftwindowwidth=80,
edge_pilots=False,
state_machine="data".encode("utf-8"),
codename="H_1024_2048_4f".encode("utf-8"),
tx_uw=uw_sequence,
amp_est_mode=1,
tx_bpf_en=False,
rx_bpf_en=False,
foff_limiter=False,
amp_scale=300E3,
clip_gain1=2.2,
clip_gain2=0.8,
clip_en=False,
mode=b"CUSTOM",
data_mode=b"streaming",
fmin=-50.0,
fmax=50.0,
EsNodB=3.0,
)
return FREEDV_ADVANCED(
interleave_frames = 0,
M = 2,
Rs = 100,
Fs = 8000,
first_tone = 1000,
tone_spacing = 200,
codename = "H_256_512_4".encode("utf-8"),
config = ctypes.pointer(ofdm_default_config),
)
def create_tx_uw(nuwbits, uw_sequence):
"""
Creates a tx_uw ctypes array filled with the uw_sequence up to nuwbits.
If uw_sequence is shorter than nuwbits, the rest of the array is filled with zeros.
:param nuwbits: The number of bits for the tx_uw array, should not exceed MAX_UW_BITS.
:param uw_sequence: List of integers representing the unique word sequence.
:return: A ctypes array representing the tx_uw.
"""
# Ensure nuwbits does not exceed MAX_UW_BITS
if nuwbits > MAX_UW_BITS:
raise ValueError(f"nuwbits exceeds MAX_UW_BITS: {MAX_UW_BITS}")
tx_uw_array = (ctypes.c_uint8 * MAX_UW_BITS)(*([0] * MAX_UW_BITS))
for i in range(min(len(uw_sequence), MAX_UW_BITS)):
tx_uw_array[i] = uw_sequence[i]
return tx_uw_array
"""
# DATAC1
data_ofdm_500_config = create_default_ofdm_config()
data_ofdm_500_config.config.contents.ns = 5
data_ofdm_500_config.config.contents.np = 38
data_ofdm_500_config.config.contents.tcp = 0.006
data_ofdm_500_config.config.contents.ts = 0.016
data_ofdm_500_config.config.contents.rs = 1.0 / data_ofdm_500_config.config.contents.ts
data_ofdm_500_config.config.contents.nc = 27
data_ofdm_500_config.config.contents.nuwbits = 16
data_ofdm_500_config.config.contents.timing_mx_thresh = 0.10
data_ofdm_500_config.config.contents.bad_uw_errors = 6
data_ofdm_500_config.config.contents.codename = b"H_4096_8192_3d"
data_ofdm_500_config.config.contents.amp_scale = 145E3
data_ofdm_500_config.config.contents.tx_uw = create_tx_uw(16, [1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0])
"""
"""
# DATAC3
data_ofdm_500_config = create_default_ofdm_config()
data_ofdm_500_config.config.contents.ns = 5
data_ofdm_500_config.config.contents.np = 29
data_ofdm_500_config.config.contents.tcp = 0.006
data_ofdm_500_config.config.contents.ts = 0.016
data_ofdm_500_config.config.contents.rs = 1.0 / data_ofdm_500_config.config.contents.ts
data_ofdm_500_config.config.contents.nc = 9
data_ofdm_500_config.config.contents.nuwbits = 40
data_ofdm_500_config.config.contents.timing_mx_thresh = 0.10
data_ofdm_500_config.config.contents.bad_uw_errors = 10
data_ofdm_500_config.config.contents.codename = b"H_1024_2048_4f"
data_ofdm_500_config.config.contents.clip_gain1 = 2.2
data_ofdm_500_config.config.contents.clip_gain2 = 0.8
data_ofdm_500_config.config.contents.tx_uw = create_tx_uw(40, [1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0])
"""
# ---------------- OFDM 500 Hz Bandwidth ---------------#
data_ofdm_500_config = create_default_ofdm_config()
data_ofdm_500_config.config.contents.ns = 5
data_ofdm_500_config.config.contents.np = 38
data_ofdm_500_config.config.contents.tcp = 0.006
data_ofdm_500_config.config.contents.ts = 0.016
data_ofdm_500_config.config.contents.rs = 1.0 / data_ofdm_500_config.config.contents.ts
data_ofdm_500_config.config.contents.nc = 27
data_ofdm_500_config.config.contents.nuwbits = 16
data_ofdm_500_config.config.contents.timing_mx_thresh = 0.10
data_ofdm_500_config.config.contents.bad_uw_errors = 6
data_ofdm_500_config.config.contents.codename = b"H_4096_8192_3d"
data_ofdm_500_config.config.contents.amp_scale = 145E3
data_ofdm_500_config.config.contents.tx_uw = create_tx_uw(16, [1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0])
# ---------------- OFDM 2438 Hz Bandwidth 16200,9720 ---------------#
data_ofdm_2438_config = create_default_ofdm_config()
data_ofdm_2438_config.config.contents.ns = 5
data_ofdm_2438_config.config.contents.np = 52
data_ofdm_2438_config.config.contents.tcp = 0.004
data_ofdm_2438_config.config.contents.ts = 0.016
data_ofdm_2438_config.config.contents.rs = 1.0 / data_ofdm_2438_config.config.contents.ts
data_ofdm_2438_config.config.contents.nc = 39
data_ofdm_2438_config.config.contents.nuwbits = 24
data_ofdm_2438_config.config.contents.timing_mx_thresh = 0.10
data_ofdm_2438_config.config.contents.bad_uw_errors = 8
data_ofdm_2438_config.config.contents.amp_est_mode = 0
data_ofdm_2438_config.config.contents.amp_scale = 135E3
data_ofdm_2438_config.config.contents.codename = b"H_16200_9720"
data_ofdm_2438_config.config.contents.clip_gain1 = 2.7
data_ofdm_2438_config.config.contents.clip_gain2 = 0.8
data_ofdm_2438_config.config.contents.timing_mx_thresh = 0.10
data_ofdm_2438_config.config.contents.tx_uw = create_tx_uw(24, [1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1])
# ---------------- OFDM 2438 Hz Bandwidth 8192,4096 ---------------#
"""
data_ofdm_2438_config = create_default_ofdm_config()
data_ofdm_2438_config.config.contents.ns = 5
data_ofdm_2438_config.config.contents.np = 27
data_ofdm_2438_config.config.contents.tcp = 0.005
data_ofdm_2438_config.config.contents.ts = 0.018
data_ofdm_2438_config.config.contents.rs = 1.0 / data_ofdm_2438_config.config.contents.ts
data_ofdm_2438_config.config.contents.nc = 38
data_ofdm_2438_config.config.contents.nuwbits = 16
data_ofdm_2438_config.config.contents.timing_mx_thresh = 0.10
data_ofdm_2438_config.config.contents.bad_uw_errors = 8
data_ofdm_2438_config.config.contents.amp_est_mode = 0
data_ofdm_2438_config.config.contents.amp_scale = 145E3
data_ofdm_2438_config.config.contents.codename = b"H_4096_8192_3d"
data_ofdm_2438_config.config.contents.clip_gain1 = 2.7
data_ofdm_2438_config.config.contents.clip_gain2 = 0.8
data_ofdm_2438_config.config.contents.timing_mx_thresh = 0.10
data_ofdm_2438_config.config.contents.tx_uw = create_tx_uw(16, [1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1])
"""
"""
# ---------------- QAM 2438 Hz Bandwidth ---------------#
data_qam_2438_config = create_default_ofdm_config()
data_qam_2438_config.config.contents.bps = 4
data_qam_2438_config.config.contents.ns = 5
data_qam_2438_config.config.contents.np = 26
data_qam_2438_config.config.contents.tcp = 0.005
data_qam_2438_config.config.contents.ts = 0.018
data_qam_2438_config.config.contents.rs = 1.0 / data_qam_2438_config.config.contents.ts
data_qam_2438_config.config.contents.nc = 39
data_qam_2438_config.config.contents.nuwbits = 162
data_qam_2438_config.config.contents.timing_mx_thresh = 0.10
data_qam_2438_config.config.contents.bad_uw_errors = 50
data_qam_2438_config.config.contents.amp_est_mode = 0
data_qam_2438_config.config.contents.amp_scale = 145E3
data_qam_2438_config.config.contents.codename = b"H_16200_9720"
data_qam_2438_config.config.contents.clip_gain1 = 2.7
data_qam_2438_config.config.contents.clip_gain2 = 0.8
data_qam_2438_config.config.contents.timing_mx_thresh = 0.10
data_qam_2438_config.config.contents.tx_uw = create_tx_uw(162, [1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0])
"""
ofdm_configurations = {
FREEDV_MODE.data_ofdm_500.value: data_ofdm_500_config,
FREEDV_MODE.data_ofdm_2438.value: data_ofdm_2438_config,
#FREEDV_MODE.data_qam_2438.value: data_qam_2438_config
}

View file

@ -8,7 +8,7 @@ from arq_data_type_handler import ARQDataTypeHandler
class TxCommand(): class TxCommand():
def __init__(self, config: dict, state_manager: StateManager, event_manager, apiParams:dict = {}): def __init__(self, config: dict, state_manager: StateManager, event_manager, apiParams:dict = {}, socket_command_handler=None):
self.config = config self.config = config
self.logger = structlog.get_logger(type(self).__name__) self.logger = structlog.get_logger(type(self).__name__)
self.state_manager = state_manager self.state_manager = state_manager
@ -16,6 +16,7 @@ class TxCommand():
self.set_params_from_api(apiParams) self.set_params_from_api(apiParams)
self.frame_factory = DataFrameFactory(config) self.frame_factory = DataFrameFactory(config)
self.arq_data_type_handler = ARQDataTypeHandler(event_manager, state_manager) self.arq_data_type_handler = ARQDataTypeHandler(event_manager, state_manager)
self.socket_command_handler = socket_command_handler
def log(self, message, isWarning = False): def log(self, message, isWarning = False):
msg = f"[{type(self).__name__}]: {message}" msg = f"[{type(self).__name__}]: {message}"
@ -60,5 +61,4 @@ class TxCommand():
def test(self, event_queue: queue.Queue): def test(self, event_queue: queue.Queue):
self.emit_event(event_queue) self.emit_event(event_queue)
self.logger.info(self.log_message()) self.logger.info(self.log_message())
frame = self.build_frame() return self.build_frame()
return frame

View file

@ -3,7 +3,8 @@ from command import TxCommand
class BeaconCommand(TxCommand): class BeaconCommand(TxCommand):
def build_frame(self): def build_frame(self):
return self.frame_factory.build_beacon() beacon_state = self.state_manager.is_away_from_key
return self.frame_factory.build_beacon(beacon_state)
#def transmit(self, modem): #def transmit(self, modem):

View file

@ -1,5 +1,5 @@
from command import TxCommand from command import TxCommand
from codec2 import FREEDV_MODE
class CQCommand(TxCommand): class CQCommand(TxCommand):
def build_frame(self): def build_frame(self):

View file

@ -41,7 +41,7 @@ class SendMessageCommand(TxCommand):
# Convert JSON string to bytes (using UTF-8 encoding) # Convert JSON string to bytes (using UTF-8 encoding)
payload = message.to_payload().encode('utf-8') payload = message.to_payload().encode('utf-8')
json_bytearray = bytearray(payload) json_bytearray = bytearray(payload)
data, data_type = self.arq_data_type_handler.prepare(json_bytearray, ARQ_SESSION_TYPES.p2pmsg_lzma) data, data_type = self.arq_data_type_handler.prepare(json_bytearray, ARQ_SESSION_TYPES.p2pmsg_zlib)
iss = ARQSessionISS(self.config, iss = ARQSessionISS(self.config,
modem, modem,

View file

@ -0,0 +1,37 @@
import queue
from command import TxCommand
import api_validations
import base64
from queue import Queue
from p2p_connection import P2PConnection
class P2PConnectionCommand(TxCommand):
def set_params_from_api(self, apiParams):
self.origin = apiParams['origin']
if not api_validations.validate_freedata_callsign(self.origin):
self.origin = f"{self.origin}-0"
self.destination = apiParams['destination']
if not api_validations.validate_freedata_callsign(self.destination):
self.destination = f"{self.destination}-0"
def connect(self, event_queue: Queue, modem):
pass
def run(self, event_queue: Queue, modem):
try:
self.emit_event(event_queue)
session = P2PConnection(self.config, modem, self.origin, self.destination, self.state_manager, self.event_manager, self.socket_command_handler)
print(session)
if session.session_id:
self.state_manager.register_p2p_connection_session(session)
session.connect()
return session
return False
except Exception as e:
self.log(f"Error starting P2P Connection session: {e}", isWarning=True)
return False

View file

@ -15,7 +15,6 @@ input_device = 5a1c
output_device = bd6c output_device = bd6c
rx_audio_level = 0 rx_audio_level = 0
tx_audio_level = 0 tx_audio_level = 0
enable_auto_tune = False
[RIGCTLD] [RIGCTLD]
ip = 127.0.0.1 ip = 127.0.0.1
@ -46,10 +45,17 @@ enable_protocol = False
[MODEM] [MODEM]
enable_hmac = False enable_hmac = False
enable_low_bandwidth_mode = False
enable_morse_identifier = False enable_morse_identifier = False
respond_to_cq = True respond_to_cq = True
tx_delay = 200 tx_delay = 50
maximum_bandwidth = 2375
enable_socket_interface = False
[SOCKET_INTERFACE]
enable = False
host = 127.0.0.1
cmd_port = 8000
data_port = 8001
[MESSAGES] [MESSAGES]
enable_auto_repeat = False enable_auto_repeat = False

View file

@ -26,7 +26,6 @@ class CONFIG:
'output_device': str, 'output_device': str,
'rx_audio_level': int, 'rx_audio_level': int,
'tx_audio_level': int, 'tx_audio_level': int,
'enable_auto_tune': bool,
}, },
'RADIO': { 'RADIO': {
'control': str, 'control': str,
@ -58,9 +57,17 @@ class CONFIG:
'MODEM': { 'MODEM': {
'enable_hmac': bool, 'enable_hmac': bool,
'enable_morse_identifier': bool, 'enable_morse_identifier': bool,
'enable_low_bandwidth_mode': bool, 'maximum_bandwidth': int,
'respond_to_cq': bool, 'respond_to_cq': bool,
'tx_delay': int 'tx_delay': int,
'enable_socket_interface': bool,
},
'SOCKET_INTERFACE': {
'enable' : bool,
'host' : str,
'cmd_port' : int,
'data_port' : int,
}, },
'MESSAGES': { 'MESSAGES': {
'enable_auto_repeat': bool, 'enable_auto_repeat': bool,
@ -87,7 +94,7 @@ class CONFIG:
except Exception: except Exception:
self.config_name = "config.ini" self.config_name = "config.ini"
self.log.info("[CFG] config init", file=self.config_name) #self.log.info("[CFG] config init", file=self.config_name)
# check if config file exists # check if config file exists
self.config_exists() self.config_exists()

View file

@ -6,6 +6,7 @@ class DataFrameFactory:
LENGTH_SIG0_FRAME = 14 LENGTH_SIG0_FRAME = 14
LENGTH_SIG1_FRAME = 14 LENGTH_SIG1_FRAME = 14
LENGTH_ACK_FRAME = 3
""" """
helpers.set_flag(byte, 'DATA-ACK-NACK', True, FLAG_POSITIONS) helpers.set_flag(byte, 'DATA-ACK-NACK', True, FLAG_POSITIONS)
@ -17,7 +18,12 @@ class DataFrameFactory:
'CHECKSUM': 2, # Bit-position for indicating the CHECKSUM is correct or not 'CHECKSUM': 2, # Bit-position for indicating the CHECKSUM is correct or not
} }
BEACON_FLAGS = {
'AWAY_FROM_KEY': 0, # Bit-position for indicating the AWAY FROM KEY state
}
def __init__(self, config): def __init__(self, config):
self.myfullcall = f"{config['STATION']['mycall']}-{config['STATION']['myssid']}" self.myfullcall = f"{config['STATION']['mycall']}-{config['STATION']['myssid']}"
self.mygrid = config['STATION']['mygrid'] self.mygrid = config['STATION']['mygrid']
@ -26,8 +32,8 @@ class DataFrameFactory:
self._load_broadcast_templates() self._load_broadcast_templates()
self._load_ping_templates() self._load_ping_templates()
self._load_fec_templates()
self._load_arq_templates() self._load_arq_templates()
self._load_p2p_connection_templates()
def _load_broadcast_templates(self): def _load_broadcast_templates(self):
# cq frame # cq frame
@ -49,7 +55,8 @@ class DataFrameFactory:
self.template_list[FR_TYPE.BEACON.value] = { self.template_list[FR_TYPE.BEACON.value] = {
"frame_length": self.LENGTH_SIG0_FRAME, "frame_length": self.LENGTH_SIG0_FRAME,
"origin": 6, "origin": 6,
"gridsquare": 4 "gridsquare": 4,
"flag": 1
} }
def _load_ping_templates(self): def _load_ping_templates(self):
@ -70,26 +77,6 @@ class DataFrameFactory:
"snr": 1, "snr": 1,
} }
def _load_fec_templates(self):
# fec wakeup frame
self.template_list[FR_TYPE.FEC_WAKEUP.value] = {
"frame_length": self.LENGTH_SIG0_FRAME,
"origin": 6,
"mode": 1,
"n_bursts": 1,
}
# fec frame
self.template_list[FR_TYPE.FEC.value] = {
"frame_length": self.LENGTH_SIG0_FRAME,
"data": self.LENGTH_SIG0_FRAME - 1
}
# fec is writing frame
self.template_list[FR_TYPE.IS_WRITING.value] = {
"frame_length": self.LENGTH_SIG0_FRAME,
"origin": 6
}
def _load_arq_templates(self): def _load_arq_templates(self):
@ -98,6 +85,8 @@ class DataFrameFactory:
"destination_crc": 3, "destination_crc": 3,
"origin": 6, "origin": 6,
"session_id": 1, "session_id": 1,
"maximum_bandwidth": 2,
"protocol_version" : 1
} }
self.template_list[FR_TYPE.ARQ_SESSION_OPEN_ACK.value] = { self.template_list[FR_TYPE.ARQ_SESSION_OPEN_ACK.value] = {
@ -151,15 +140,72 @@ class DataFrameFactory:
# arq burst ack # arq burst ack
self.template_list[FR_TYPE.ARQ_BURST_ACK.value] = { self.template_list[FR_TYPE.ARQ_BURST_ACK.value] = {
"frame_length": self.LENGTH_SIG1_FRAME, "frame_length": self.LENGTH_ACK_FRAME,
"session_id": 1, "session_id": 1,
"offset":4, #"offset":4,
"speed_level": 1, "speed_level": 1,
"frames_per_burst": 1, #"frames_per_burst": 1,
"snr": 1, #"snr": 1,
"flag": 1, "flag": 1,
} }
def _load_p2p_connection_templates(self):
# p2p connect request
self.template_list[FR_TYPE.P2P_CONNECTION_CONNECT.value] = {
"frame_length": self.LENGTH_SIG1_FRAME,
"destination_crc": 3,
"origin": 6,
"session_id": 1,
}
# connect ACK
self.template_list[FR_TYPE.P2P_CONNECTION_CONNECT_ACK.value] = {
"frame_length": self.LENGTH_SIG1_FRAME,
"destination_crc": 3,
"origin": 6,
"session_id": 1,
}
# heartbeat for "is alive"
self.template_list[FR_TYPE.P2P_CONNECTION_HEARTBEAT.value] = {
"frame_length": self.LENGTH_SIG1_FRAME,
"session_id": 1,
}
# ack heartbeat
self.template_list[FR_TYPE.P2P_CONNECTION_HEARTBEAT_ACK.value] = {
"frame_length": self.LENGTH_SIG1_FRAME,
"session_id": 1,
}
# p2p payload frames
self.template_list[FR_TYPE.P2P_CONNECTION_PAYLOAD.value] = {
"frame_length": None,
"session_id": 1,
"sequence_id": 1,
"data": "dynamic",
}
# p2p payload frame ack
self.template_list[FR_TYPE.P2P_CONNECTION_PAYLOAD_ACK.value] = {
"frame_length": self.LENGTH_SIG1_FRAME,
"session_id": 1,
"sequence_id": 1,
}
# heartbeat for "is alive"
self.template_list[FR_TYPE.P2P_CONNECTION_DISCONNECT.value] = {
"frame_length": self.LENGTH_SIG1_FRAME,
"session_id": 1,
}
# ack heartbeat
self.template_list[FR_TYPE.P2P_CONNECTION_DISCONNECT_ACK.value] = {
"frame_length": self.LENGTH_SIG1_FRAME,
"session_id": 1,
}
def construct(self, frametype, content, frame_length = LENGTH_SIG1_FRAME): def construct(self, frametype, content, frame_length = LENGTH_SIG1_FRAME):
frame_template = self.template_list[frametype.value] frame_template = self.template_list[frametype.value]
@ -168,9 +214,12 @@ class DataFrameFactory:
frame_length = frame_template["frame_length"] frame_length = frame_template["frame_length"]
else: else:
frame_length -= 2 frame_length -= 2
frame = bytearray(frame_length)
frame[:1] = bytes([frametype.value])
frame = bytearray(frame_length)
if frametype in [FR_TYPE.ARQ_BURST_ACK]:
buffer_position = 0
else:
frame[:1] = bytes([frametype.value])
buffer_position = 1 buffer_position = 1
for key, item_length in frame_template.items(): for key, item_length in frame_template.items():
if key == "frame_length": if key == "frame_length":
@ -182,18 +231,23 @@ class DataFrameFactory:
raise OverflowError("Frame data overflow!") raise OverflowError("Frame data overflow!")
frame[buffer_position: buffer_position + item_length] = content[key] frame[buffer_position: buffer_position + item_length] = content[key]
buffer_position += item_length buffer_position += item_length
return frame return frame
def deconstruct(self, frame): def deconstruct(self, frame, mode_name=None):
buffer_position = 1 buffer_position = 1
# Handle the case where the frame type is not recognized
#raise ValueError(f"Unknown frame type: {frametype}")
if mode_name in ["SIGNALLING_ACK"]:
frametype = FR_TYPE.ARQ_BURST_ACK.value
frame_template = self.template_list.get(frametype)
frame = bytes([frametype]) + frame
else:
# Extract frametype and get the corresponding template # Extract frametype and get the corresponding template
frametype = int.from_bytes(frame[:1], "big") frametype = int.from_bytes(frame[:1], "big")
frame_template = self.template_list.get(frametype) frame_template = self.template_list.get(frametype)
if not frame_template:
# Handle the case where the frame type is not recognized
raise ValueError(f"Unknown frame type: {frametype}")
extracted_data = {"frame_type": FR_TYPE(frametype).name, "frame_type_int": frametype} extracted_data = {"frame_type": FR_TYPE(frametype).name, "frame_type_int": frametype}
for key, item_length in frame_template.items(): for key, item_length in frame_template.items():
@ -202,6 +256,7 @@ class DataFrameFactory:
# data is always on the last payload slots # data is always on the last payload slots
if item_length in ["dynamic"] and key in["data"]: if item_length in ["dynamic"] and key in["data"]:
print(len(frame))
data = frame[buffer_position:-2] data = frame[buffer_position:-2]
item_length = len(data) item_length = len(data)
else: else:
@ -219,7 +274,7 @@ class DataFrameFactory:
elif key in ["session_id", "speed_level", elif key in ["session_id", "speed_level",
"frames_per_burst", "version", "frames_per_burst", "version",
"offset", "total_length", "state", "type"]: "offset", "total_length", "state", "type", "maximum_bandwidth", "protocol_version"]:
extracted_data[key] = int.from_bytes(data, 'big') extracted_data[key] = int.from_bytes(data, 'big')
elif key in ["snr"]: elif key in ["snr"]:
@ -229,7 +284,6 @@ class DataFrameFactory:
data = int.from_bytes(data, "big") data = int.from_bytes(data, "big")
extracted_data[key] = {} extracted_data[key] = {}
# check for frametype for selecting the correspinding flag dictionary # check for frametype for selecting the correspinding flag dictionary
if frametype in [FR_TYPE.ARQ_SESSION_OPEN_ACK.value, FR_TYPE.ARQ_SESSION_INFO_ACK.value, FR_TYPE.ARQ_BURST_ACK.value]: if frametype in [FR_TYPE.ARQ_SESSION_OPEN_ACK.value, FR_TYPE.ARQ_SESSION_INFO_ACK.value, FR_TYPE.ARQ_BURST_ACK.value]:
flag_dict = self.ARQ_FLAGS flag_dict = self.ARQ_FLAGS
@ -237,6 +291,15 @@ class DataFrameFactory:
# Update extracted_data with the status of each flag # Update extracted_data with the status of each flag
# get_flag returns True or False based on the bit value at the flag's position # get_flag returns True or False based on the bit value at the flag's position
extracted_data[key][flag] = helpers.get_flag(data, flag, flag_dict) extracted_data[key][flag] = helpers.get_flag(data, flag, flag_dict)
if frametype in [FR_TYPE.BEACON.value]:
flag_dict = self.BEACON_FLAGS
for flag in flag_dict:
# Update extracted_data with the status of each flag
# get_flag returns True or False based on the bit value at the flag's position
extracted_data[key][flag] = helpers.get_flag(data, flag, flag_dict)
else: else:
extracted_data[key] = data extracted_data[key] = data
@ -290,10 +353,16 @@ class DataFrameFactory:
} }
return self.construct(FR_TYPE.QRV, payload) return self.construct(FR_TYPE.QRV, payload)
def build_beacon(self): def build_beacon(self, flag_away_from_key=False):
flag = 0b00000000
if flag_away_from_key:
flag = helpers.set_flag(flag, 'AWAY_FROM_KEY', True, self.BEACON_FLAGS)
payload = { payload = {
"origin": helpers.callsign_to_bytes(self.myfullcall), "origin": helpers.callsign_to_bytes(self.myfullcall),
"gridsquare": helpers.encode_grid(self.mygrid) "gridsquare": helpers.encode_grid(self.mygrid),
"flag": flag.to_bytes(1, 'big'),
} }
return self.construct(FR_TYPE.BEACON, payload) return self.construct(FR_TYPE.BEACON, payload)
@ -328,11 +397,13 @@ class DataFrameFactory:
test_frame[:1] = bytes([FR_TYPE.TEST_FRAME.value]) test_frame[:1] = bytes([FR_TYPE.TEST_FRAME.value])
return test_frame return test_frame
def build_arq_session_open(self, destination, session_id): def build_arq_session_open(self, destination, session_id, maximum_bandwidth, protocol_version):
payload = { payload = {
"destination_crc": helpers.get_crc_24(destination), "destination_crc": helpers.get_crc_24(destination),
"origin": helpers.callsign_to_bytes(self.myfullcall), "origin": helpers.callsign_to_bytes(self.myfullcall),
"session_id": session_id.to_bytes(1, 'big'), "session_id": session_id.to_bytes(1, 'big'),
"maximum_bandwidth": maximum_bandwidth.to_bytes(2, 'big'),
"protocol_version": protocol_version.to_bytes(1, 'big'),
} }
return self.construct(FR_TYPE.ARQ_SESSION_OPEN, payload) return self.construct(FR_TYPE.ARQ_SESSION_OPEN, payload)
@ -402,11 +473,11 @@ class DataFrameFactory:
"offset": offset.to_bytes(4, 'big'), "offset": offset.to_bytes(4, 'big'),
"data": data, "data": data,
} }
frame = self.construct(FR_TYPE.ARQ_BURST_FRAME, payload, self.get_bytes_per_frame(freedv_mode)) return self.construct(
return frame FR_TYPE.ARQ_BURST_FRAME, payload, self.get_bytes_per_frame(freedv_mode)
)
def build_arq_burst_ack(self, session_id: bytes, offset, speed_level: int, def build_arq_burst_ack(self, session_id: bytes, speed_level: int, flag_final=False, flag_checksum=False, flag_abort=False):
frames_per_burst: int, snr: int, flag_final=False, flag_checksum=False, flag_abort=False):
flag = 0b00000000 flag = 0b00000000
if flag_final: if flag_final:
flag = helpers.set_flag(flag, 'FINAL', True, self.ARQ_FLAGS) flag = helpers.set_flag(flag, 'FINAL', True, self.ARQ_FLAGS)
@ -419,10 +490,66 @@ class DataFrameFactory:
payload = { payload = {
"session_id": session_id.to_bytes(1, 'big'), "session_id": session_id.to_bytes(1, 'big'),
"offset": offset.to_bytes(4, 'big'),
"speed_level": speed_level.to_bytes(1, 'big'), "speed_level": speed_level.to_bytes(1, 'big'),
"frames_per_burst": frames_per_burst.to_bytes(1, 'big'),
"snr": helpers.snr_to_bytes(snr),
"flag": flag.to_bytes(1, 'big'), "flag": flag.to_bytes(1, 'big'),
} }
return self.construct(FR_TYPE.ARQ_BURST_ACK, payload) return self.construct(FR_TYPE.ARQ_BURST_ACK, payload)
def build_p2p_connection_connect(self, destination, origin, session_id):
payload = {
"destination_crc": helpers.get_crc_24(destination),
"origin": helpers.callsign_to_bytes(origin),
"session_id": session_id.to_bytes(1, 'big'),
}
return self.construct(FR_TYPE.P2P_CONNECTION_CONNECT, payload)
def build_p2p_connection_connect_ack(self, destination, origin, session_id):
payload = {
"destination_crc": helpers.get_crc_24(destination),
"origin": helpers.callsign_to_bytes(origin),
"session_id": session_id.to_bytes(1, 'big'),
}
return self.construct(FR_TYPE.P2P_CONNECTION_CONNECT_ACK, payload)
def build_p2p_connection_heartbeat(self, session_id):
payload = {
"session_id": session_id.to_bytes(1, 'big'),
}
return self.construct(FR_TYPE.P2P_CONNECTION_HEARTBEAT, payload)
def build_p2p_connection_heartbeat_ack(self, session_id):
payload = {
"session_id": session_id.to_bytes(1, 'big'),
}
return self.construct(FR_TYPE.P2P_CONNECTION_HEARTBEAT_ACK, payload)
def build_p2p_connection_payload(self, freedv_mode: codec2.FREEDV_MODE, session_id: int, sequence_id: int, data: bytes):
payload = {
"session_id": session_id.to_bytes(1, 'big'),
"sequence_id": sequence_id.to_bytes(1, 'big'),
"data": data,
}
return self.construct(
FR_TYPE.P2P_CONNECTION_PAYLOAD,
payload,
self.get_bytes_per_frame(freedv_mode),
)
def build_p2p_connection_payload_ack(self, session_id, sequence_id):
payload = {
"session_id": session_id.to_bytes(1, 'big'),
"sequence_id": sequence_id.to_bytes(1, 'big'),
}
return self.construct(FR_TYPE.P2P_CONNECTION_PAYLOAD_ACK, payload)
def build_p2p_connection_disconnect(self, session_id):
payload = {
"session_id": session_id.to_bytes(1, 'big'),
}
return self.construct(FR_TYPE.P2P_CONNECTION_DISCONNECT, payload)
def build_p2p_connection_disconnect_ack(self, session_id):
payload = {
"session_id": session_id.to_bytes(1, 'big'),
}
return self.construct(FR_TYPE.P2P_CONNECTION_DISCONNECT_ACK, payload)

View file

@ -4,10 +4,7 @@ import ctypes
import structlog import structlog
import threading import threading
import audio import audio
import os
from modem_frametypes import FRAME_TYPE
import itertools import itertools
from time import sleep
TESTMODE = False TESTMODE = False
@ -28,11 +25,10 @@ class Demodulator():
'decoding_thread': None 'decoding_thread': None
} }
def __init__(self, config, audio_rx_q, data_q_rx, states, event_manager, fft_queue): def __init__(self, config, audio_rx_q, data_q_rx, states, event_manager, service_queue, fft_queue):
self.log = structlog.get_logger("Demodulator") self.log = structlog.get_logger("Demodulator")
self.rx_audio_level = config['AUDIO']['rx_audio_level'] self.service_queue = service_queue
self.AUDIO_FRAMES_PER_BUFFER_RX = 4800 self.AUDIO_FRAMES_PER_BUFFER_RX = 4800
self.buffer_overflow_counter = [0, 0, 0, 0, 0, 0, 0, 0] self.buffer_overflow_counter = [0, 0, 0, 0, 0, 0, 0, 0]
self.is_codec2_traffic_counter = 0 self.is_codec2_traffic_counter = 0
@ -53,6 +49,10 @@ class Demodulator():
# enable decoding of signalling modes # enable decoding of signalling modes
self.MODE_DICT[codec2.FREEDV_MODE.signalling.value]["decode"] = True self.MODE_DICT[codec2.FREEDV_MODE.signalling.value]["decode"] = True
self.MODE_DICT[codec2.FREEDV_MODE.signalling_ack.value]["decode"] = True
self.MODE_DICT[codec2.FREEDV_MODE.data_ofdm_2438.value]["decode"] = True
#self.MODE_DICT[codec2.FREEDV_MODE.qam16c2.value]["decode"] = True
tci_rx_callback_thread = threading.Thread( tci_rx_callback_thread = threading.Thread(
target=self.tci_rx_callback, target=self.tci_rx_callback,
@ -73,15 +73,13 @@ class Demodulator():
""" """
# create codec2 instance # create codec2 instance
c2instance = ctypes.cast( #c2instance = ctypes.cast(
codec2.api.freedv_open(mode), ctypes.c_void_p c2instance = codec2.open_instance(mode)
)
# get bytes per frame # get bytes per frame
bytes_per_frame = int( bytes_per_frame = int(
codec2.api.freedv_get_bits_per_modem_frame(c2instance) / 8 codec2.api.freedv_get_bits_per_modem_frame(c2instance) / 8
) )
# create byte out buffer # create byte out buffer
bytes_out = ctypes.create_string_buffer(bytes_per_frame) bytes_out = ctypes.create_string_buffer(bytes_per_frame)
@ -126,35 +124,6 @@ class Demodulator():
) )
self.MODE_DICT[mode]['decoding_thread'].start() self.MODE_DICT[mode]['decoding_thread'].start()
def sd_input_audio_callback(self, indata: np.ndarray, frames: int, time, status) -> None:
if status:
self.log.warning("[AUDIO STATUS]", status=status, time=time, frames=frames)
return
try:
audio_48k = np.frombuffer(indata, dtype=np.int16)
audio_8k = self.resampler.resample48_to_8(audio_48k)
audio_8k_level_adjusted = audio.set_audio_volume(audio_8k, self.rx_audio_level)
audio.calculate_fft(audio_8k_level_adjusted, self.fft_queue, self.states)
length_audio_8k_level_adjusted = len(audio_8k_level_adjusted)
# Avoid buffer overflow by filling only if buffer for
# selected datachannel mode is not full
index = 0
for mode in self.MODE_DICT:
mode_data = self.MODE_DICT[mode]
audiobuffer = mode_data['audio_buffer']
decode = mode_data['decode']
index += 1
if audiobuffer:
if (audiobuffer.nbuffer + length_audio_8k_level_adjusted) > audiobuffer.size:
self.buffer_overflow_counter[index] += 1
self.event_manager.send_buffer_overflow(self.buffer_overflow_counter)
elif decode:
audiobuffer.push(audio_8k_level_adjusted)
except Exception as e:
self.log.warning("[AUDIO EXCEPTION]", status=status, time=time, frames=frames, e=e)
def get_frequency_offset(self, freedv: ctypes.c_void_p) -> float: def get_frequency_offset(self, freedv: ctypes.c_void_p) -> float:
""" """
@ -219,7 +188,7 @@ class Demodulator():
nin = codec2.api.freedv_nin(freedv) nin = codec2.api.freedv_nin(freedv)
if nbytes == bytes_per_frame: if nbytes == bytes_per_frame:
self.log.debug( self.log.debug(
"[MDM] [demod_audio] Pushing received data to received_queue", nbytes=nbytes "[MDM] [demod_audio] Pushing received data to received_queue", nbytes=nbytes, mode_name=mode_name
) )
snr = self.calculate_snr(freedv) snr = self.calculate_snr(freedv)
self.get_scatter(freedv) self.get_scatter(freedv)
@ -230,7 +199,9 @@ class Demodulator():
'bytes_per_frame': bytes_per_frame, 'bytes_per_frame': bytes_per_frame,
'snr': snr, 'snr': snr,
'frequency_offset': self.get_frequency_offset(freedv), 'frequency_offset': self.get_frequency_offset(freedv),
'mode_name': mode_name
} }
self.data_queue_received.put(item) self.data_queue_received.put(item)
@ -370,18 +341,20 @@ class Demodulator():
for mode in self.MODE_DICT: for mode in self.MODE_DICT:
codec2.api.freedv_set_sync(self.MODE_DICT[mode]["instance"], 0) codec2.api.freedv_set_sync(self.MODE_DICT[mode]["instance"], 0)
def set_decode_mode(self, modes_to_decode): def set_decode_mode(self, modes_to_decode=None):
# Reset all modes to not decode # Reset all modes to not decode
for m in self.MODE_DICT: for m in self.MODE_DICT:
self.MODE_DICT[m]["decode"] = False self.MODE_DICT[m]["decode"] = False
# signalling is always true # signalling is always true
self.MODE_DICT[codec2.FREEDV_MODE.signalling.value]["decode"] = True self.MODE_DICT[codec2.FREEDV_MODE.signalling.value]["decode"] = True
self.MODE_DICT[codec2.FREEDV_MODE.signalling_ack.value]["decode"] = True
# lowest speed level is alwys true # lowest speed level is alwys true
self.MODE_DICT[codec2.FREEDV_MODE.datac4.value]["decode"] = True self.MODE_DICT[codec2.FREEDV_MODE.datac4.value]["decode"] = True
# Enable specified modes # Enable specified modes
if modes_to_decode:
for mode, decode in modes_to_decode.items(): for mode, decode in modes_to_decode.items():
if mode in self.MODE_DICT: if mode in self.MODE_DICT:
self.MODE_DICT[mode]["decode"] = decode self.MODE_DICT[mode]["decode"] = decode

View file

@ -12,6 +12,8 @@ class EventManager:
def broadcast(self, data): def broadcast(self, data):
for q in self.queues: for q in self.queues:
self.logger.debug(f"Event: ", ev=data) self.logger.debug(f"Event: ", ev=data)
if q.qsize() > 10:
q.queue.clear()
q.put(data) q.put(data)
def send_ptt_change(self, on:bool = False): def send_ptt_change(self, on:bool = False):

View file

@ -33,9 +33,10 @@ class explorer():
callsign = str(self.config['STATION']['mycall']) + "-" + str(self.config["STATION"]['myssid']) callsign = str(self.config['STATION']['mycall']) + "-" + str(self.config["STATION"]['myssid'])
gridsquare = str(self.config['STATION']['mygrid']) gridsquare = str(self.config['STATION']['mygrid'])
version = str(self.modem_version) version = str(self.modem_version)
bandwidth = str(self.config['MODEM']['enable_low_bandwidth_mode']) bandwidth = str(self.config['MODEM']['maximum_bandwidth'])
beacon = str(self.states.is_beacon_running) beacon = str(self.states.is_beacon_running)
strength = str(self.states.s_meter_strength) strength = str(self.states.s_meter_strength)
away_from_key = str(self.states.is_away_from_key)
# stop pushing if default callsign # stop pushing if default callsign
if callsign in ['XX1XXX-6']: if callsign in ['XX1XXX-6']:
@ -45,7 +46,7 @@ class explorer():
# log.info("[EXPLORER] publish", frequency=frequency, band=band, callsign=callsign, gridsquare=gridsquare, version=version, bandwidth=bandwidth) # log.info("[EXPLORER] publish", frequency=frequency, band=band, callsign=callsign, gridsquare=gridsquare, version=version, bandwidth=bandwidth)
headers = {"Content-Type": "application/json"} headers = {"Content-Type": "application/json"}
station_data = {'callsign': callsign, 'gridsquare': gridsquare, 'frequency': frequency, 'strength': strength, 'band': band, 'version': version, 'bandwidth': bandwidth, 'beacon': beacon, "lastheard": []} station_data = {'callsign': callsign, 'gridsquare': gridsquare, 'frequency': frequency, 'strength': strength, 'band': band, 'version': version, 'bandwidth': bandwidth, 'beacon': beacon, "lastheard": [], "away_from_key": away_from_key}
for i in self.states.heard_stations: for i in self.states.heard_stations:
try: try:

View file

@ -13,8 +13,11 @@ from frame_handler import FrameHandler
from frame_handler_ping import PingFrameHandler from frame_handler_ping import PingFrameHandler
from frame_handler_cq import CQFrameHandler from frame_handler_cq import CQFrameHandler
from frame_handler_arq_session import ARQFrameHandler from frame_handler_arq_session import ARQFrameHandler
from frame_handler_p2p_connection import P2PConnectionFrameHandler
from frame_handler_beacon import BeaconFrameHandler from frame_handler_beacon import BeaconFrameHandler
class DISPATCHER(): class DISPATCHER():
FRAME_HANDLER = { FRAME_HANDLER = {
@ -22,9 +25,18 @@ class DISPATCHER():
FR_TYPE.ARQ_SESSION_OPEN.value: {"class": ARQFrameHandler, "name": "ARQ Data Channel Open"}, FR_TYPE.ARQ_SESSION_OPEN.value: {"class": ARQFrameHandler, "name": "ARQ Data Channel Open"},
FR_TYPE.ARQ_SESSION_INFO_ACK.value: {"class": ARQFrameHandler, "name": "ARQ INFO ACK"}, FR_TYPE.ARQ_SESSION_INFO_ACK.value: {"class": ARQFrameHandler, "name": "ARQ INFO ACK"},
FR_TYPE.ARQ_SESSION_INFO.value: {"class": ARQFrameHandler, "name": "ARQ Data Channel Info"}, FR_TYPE.ARQ_SESSION_INFO.value: {"class": ARQFrameHandler, "name": "ARQ Data Channel Info"},
FR_TYPE.ARQ_CONNECTION_CLOSE.value: {"class": ARQFrameHandler, "name": "ARQ CLOSE SESSION"}, FR_TYPE.P2P_CONNECTION_CONNECT.value: {"class": P2PConnectionFrameHandler, "name": "P2P Connection CONNECT"},
FR_TYPE.ARQ_CONNECTION_HB.value: {"class": ARQFrameHandler, "name": "ARQ HEARTBEAT"}, FR_TYPE.P2P_CONNECTION_CONNECT_ACK.value: {"class": P2PConnectionFrameHandler, "name": "P2P Connection CONNECT ACK"},
FR_TYPE.ARQ_CONNECTION_OPEN.value: {"class": ARQFrameHandler, "name": "ARQ OPEN SESSION"}, FR_TYPE.P2P_CONNECTION_DISCONNECT.value: {"class": P2PConnectionFrameHandler, "name": "P2P Connection DISCONNECT"},
FR_TYPE.P2P_CONNECTION_DISCONNECT_ACK.value: {"class": P2PConnectionFrameHandler,
"name": "P2P Connection DISCONNECT ACK"},
FR_TYPE.P2P_CONNECTION_PAYLOAD.value: {"class": P2PConnectionFrameHandler,
"name": "P2P Connection PAYLOAD"},
FR_TYPE.P2P_CONNECTION_PAYLOAD_ACK.value: {"class": P2PConnectionFrameHandler,
"name": "P2P Connection PAYLOAD ACK"},
#FR_TYPE.ARQ_CONNECTION_HB.value: {"class": ARQFrameHandler, "name": "ARQ HEARTBEAT"},
#FR_TYPE.ARQ_CONNECTION_OPEN.value: {"class": ARQFrameHandler, "name": "ARQ OPEN SESSION"},
FR_TYPE.ARQ_STOP.value: {"class": ARQFrameHandler, "name": "ARQ STOP"}, FR_TYPE.ARQ_STOP.value: {"class": ARQFrameHandler, "name": "ARQ STOP"},
FR_TYPE.ARQ_STOP_ACK.value: {"class": ARQFrameHandler, "name": "ARQ STOP ACK"}, FR_TYPE.ARQ_STOP_ACK.value: {"class": ARQFrameHandler, "name": "ARQ STOP ACK"},
FR_TYPE.BEACON.value: {"class": BeaconFrameHandler, "name": "BEACON"}, FR_TYPE.BEACON.value: {"class": BeaconFrameHandler, "name": "BEACON"},
@ -34,9 +46,9 @@ class DISPATCHER():
FR_TYPE.PING_ACK.value: {"class": FrameHandler, "name": "PING ACK"}, FR_TYPE.PING_ACK.value: {"class": FrameHandler, "name": "PING ACK"},
FR_TYPE.PING.value: {"class": PingFrameHandler, "name": "PING"}, FR_TYPE.PING.value: {"class": PingFrameHandler, "name": "PING"},
FR_TYPE.QRV.value: {"class": FrameHandler, "name": "QRV"}, FR_TYPE.QRV.value: {"class": FrameHandler, "name": "QRV"},
FR_TYPE.IS_WRITING.value: {"class": FrameHandler, "name": "IS_WRITING"}, #FR_TYPE.IS_WRITING.value: {"class": FrameHandler, "name": "IS_WRITING"},
FR_TYPE.FEC.value: {"class": FrameHandler, "name": "FEC"}, #FR_TYPE.FEC.value: {"class": FrameHandler, "name": "FEC"},
FR_TYPE.FEC_WAKEUP.value: {"class": FrameHandler, "name": "FEC WAKEUP"}, #FR_TYPE.FEC_WAKEUP.value: {"class": FrameHandler, "name": "FEC WAKEUP"},
} }
def __init__(self, config, event_manager, states, modem): def __init__(self, config, event_manager, states, modem):
@ -67,22 +79,23 @@ class DISPATCHER():
"""Queue received data for processing""" """Queue received data for processing"""
while True: while True:
data = self.data_queue_received.get() data = self.data_queue_received.get()
self.new_process_data( self.process_data(
data['payload'], data['payload'],
data['freedv'], data['freedv'],
data['bytes_per_frame'], data['bytes_per_frame'],
data['snr'], data['snr'],
data['frequency_offset'], data['frequency_offset'],
data['mode_name'],
) )
def new_process_data(self, bytes_out, freedv, bytes_per_frame: int, snr, frequency_offset) -> None: def process_data(self, bytes_out, freedv, bytes_per_frame: int, snr, frequency_offset, mode_name) -> None:
# get frame as dictionary # get frame as dictionary
deconstructed_frame = self.frame_factory.deconstruct(bytes_out) deconstructed_frame = self.frame_factory.deconstruct(bytes_out, mode_name=mode_name)
frametype = deconstructed_frame["frame_type_int"] frametype = deconstructed_frame["frame_type_int"]
if frametype not in self.FRAME_HANDLER: if frametype not in self.FRAME_HANDLER:
self.log.warning( self.log.warning(
"[Modem] ARQ - other frame type", frametype=FR_TYPE(frametype).name) "[DISPATCHER] ARQ - other frame type", frametype=FR_TYPE(frametype).name)
return return
# instantiate handler # instantiate handler

View file

@ -6,9 +6,11 @@ import structlog
import time, uuid import time, uuid
from codec2 import FREEDV_MODE from codec2 import FREEDV_MODE
from message_system_db_manager import DatabaseManager from message_system_db_manager import DatabaseManager
import maidenhead
TESTMODE = False TESTMODE = False
class FrameHandler(): class FrameHandler():
def __init__(self, name: str, config, states: StateManager, event_manager: EventManager, def __init__(self, name: str, config, states: StateManager, event_manager: EventManager,
@ -34,7 +36,7 @@ class FrameHandler():
ft = self.details['frame']['frame_type'] ft = self.details['frame']['frame_type']
valid = False valid = False
# Check for callsign checksum # Check for callsign checksum
if ft in ['ARQ_SESSION_OPEN', 'ARQ_SESSION_OPEN_ACK', 'PING', 'PING_ACK']: if ft in ['ARQ_SESSION_OPEN', 'ARQ_SESSION_OPEN_ACK', 'PING', 'PING_ACK', 'P2P_CONNECTION_CONNECT']:
valid, mycallsign = helpers.check_callsign( valid, mycallsign = helpers.check_callsign(
call_with_ssid, call_with_ssid,
self.details["frame"]["destination_crc"], self.details["frame"]["destination_crc"],
@ -51,6 +53,20 @@ class FrameHandler():
session_id = self.details['frame']['session_id'] session_id = self.details['frame']['session_id']
if session_id in self.states.arq_iss_sessions: if session_id in self.states.arq_iss_sessions:
valid = True valid = True
# check for p2p connection
elif ft in ['P2P_CONNECTION_CONNECT']:
valid, mycallsign = helpers.check_callsign(
call_with_ssid,
self.details["frame"]["destination_crc"],
self.config['STATION']['ssid_list'])
#check for p2p connection
elif ft in ['P2P_CONNECTION_CONNECT_ACK', 'P2P_CONNECTION_PAYLOAD', 'P2P_CONNECTION_PAYLOAD_ACK', 'P2P_CONNECTION_DISCONNECT', 'P2P_CONNECTION_DISCONNECT_ACK']:
session_id = self.details['frame']['session_id']
if session_id in self.states.p2p_connection_sessions:
valid = True
else: else:
valid = False valid = False
@ -85,8 +101,10 @@ class FrameHandler():
if "session_id" in frame: if "session_id" in frame:
activity["session_id"] = frame["session_id"] activity["session_id"] = frame["session_id"]
self.states.add_activity(activity) if "AWAY_FROM_KEY" in frame["flag"]:
activity["away_from_key"] = frame["flag"]["AWAY_FROM_KEY"]
self.states.add_activity(activity)
def add_to_heard_stations(self): def add_to_heard_stations(self):
frame = self.details['frame'] frame = self.details['frame']
@ -94,7 +112,15 @@ class FrameHandler():
if 'origin' not in frame: if 'origin' not in frame:
return return
dxgrid = frame['gridsquare'] if 'gridsquare' in frame else "------" dxgrid = frame.get('gridsquare', "------")
# Initialize distance values
distance_km = None
distance_miles = None
if dxgrid != "------" and frame.get('gridsquare'):
distance_dict = maidenhead.distance_between_locators(self.config['STATION']['mygrid'], frame['gridsquare'])
distance_km = distance_dict['kilometers']
distance_miles = distance_dict['miles']
helpers.add_to_heard_stations( helpers.add_to_heard_stations(
frame['origin'], frame['origin'],
dxgrid, dxgrid,
@ -103,8 +129,10 @@ class FrameHandler():
self.details['frequency_offset'], self.details['frequency_offset'],
self.states.radio_frequency, self.states.radio_frequency,
self.states.heard_stations, self.states.heard_stations,
distance_km=distance_km, # Pass the kilometer distance
distance_miles=distance_miles, # Pass the miles distance
away_from_key=self.details['frame']["flag"]["AWAY_FROM_KEY"]
) )
def make_event(self): def make_event(self):
event = { event = {
@ -118,6 +146,13 @@ class FrameHandler():
if 'origin' in self.details['frame']: if 'origin' in self.details['frame']:
event['dxcallsign'] = self.details['frame']['origin'] event['dxcallsign'] = self.details['frame']['origin']
if 'gridsquare' in self.details['frame']:
event['gridsquare'] = self.details['frame']['gridsquare']
distance = maidenhead.distance_between_locators(self.config['STATION']['mygrid'], self.details['frame']['gridsquare'])
event['distance_kilometers'] = distance['kilometers']
event['distance_miles'] = distance['miles']
return event return event
def emit_event(self): def emit_event(self):

View file

@ -32,7 +32,8 @@ class ARQFrameHandler(frame_handler.FrameHandler):
session = ARQSessionIRS(self.config, session = ARQSessionIRS(self.config,
self.modem, self.modem,
frame['origin'], frame['origin'],
session_id) session_id,
self.states)
self.states.register_arq_irs_session(session) self.states.register_arq_irs_session(session)
elif frame['frame_type_int'] in [ elif frame['frame_type_int'] in [

View file

@ -5,11 +5,23 @@ import frame_handler
from message_system_db_messages import DatabaseManagerMessages from message_system_db_messages import DatabaseManagerMessages
class CQFrameHandler(frame_handler_ping.PingFrameHandler): class CQFrameHandler(frame_handler.FrameHandler):
def should_respond(self): #def should_respond(self):
self.logger.debug(f"Respond to CQ: {self.config['MODEM']['respond_to_cq']}") # self.logger.debug(f"Respond to CQ: {self.config['MODEM']['respond_to_cq']}")
return self.config['MODEM']['respond_to_cq'] # return bool(self.config['MODEM']['respond_to_cq'] and not self.states.getARQ())
def follow_protocol(self):
if self.states.getARQ():
return
self.logger.debug(
f"[Modem] Responding to request from [{self.details['frame']['origin']}]",
snr=self.details['snr'],
)
self.send_ack()
def send_ack(self): def send_ack(self):
factory = data_frame_factory.DataFrameFactory(self.config) factory = data_frame_factory.DataFrameFactory(self.config)

View file

@ -0,0 +1,54 @@
from queue import Queue
import frame_handler
from event_manager import EventManager
from state_manager import StateManager
from modem_frametypes import FRAME_TYPE as FR
from p2p_connection import P2PConnection
class P2PConnectionFrameHandler(frame_handler.FrameHandler):
def follow_protocol(self):
if not self.should_respond():
return
frame = self.details['frame']
session_id = frame['session_id']
snr = self.details["snr"]
frequency_offset = self.details["frequency_offset"]
if frame['frame_type_int'] == FR.P2P_CONNECTION_CONNECT.value:
# Lost OPEN_ACK case .. ISS will retry opening a session
if session_id in self.states.arq_irs_sessions:
session = self.states.p2p_connection_sessions[session_id]
# Normal case when receiving a SESSION_OPEN for the first time
else:
# if self.states.check_if_running_arq_session():
# self.logger.warning("DISCARDING SESSION OPEN because of ongoing ARQ session ", frame=frame)
# return
print(frame)
session = P2PConnection(self.config,
self.modem,
frame['origin'],
frame['destination_crc'],
self.states, self.event_manager)
session.session_id = session_id
self.states.register_p2p_connection_session(session)
elif frame['frame_type_int'] in [
FR.P2P_CONNECTION_CONNECT_ACK.value,
FR.P2P_CONNECTION_DISCONNECT.value,
FR.P2P_CONNECTION_DISCONNECT_ACK.value,
FR.P2P_CONNECTION_PAYLOAD.value,
FR.P2P_CONNECTION_PAYLOAD_ACK.value,
]:
session = self.states.get_p2p_connection_session(session_id)
else:
self.logger.warning("DISCARDING FRAME", frame=frame)
return
session.set_details(snr, frequency_offset)
session.on_frame_received(frame)

View file

@ -16,12 +16,8 @@ class PingFrameHandler(frame_handler.FrameHandler):
# self.logger.info(f"[Modem] {ft} received but not for us.") # self.logger.info(f"[Modem] {ft} received but not for us.")
# return valid # return valid
#def should_respond(self):
# return self.is_frame_for_me()
def follow_protocol(self): def follow_protocol(self):
if not bool(self.is_frame_for_me() and not self.states.getARQ()):
if not self.should_respond():
return return
self.logger.debug( self.logger.debug(

View file

@ -121,60 +121,45 @@ def get_crc_32(data: str) -> bytes:
return crc_algorithm(data).to_bytes(4, byteorder="big") return crc_algorithm(data).to_bytes(4, byteorder="big")
def add_to_heard_stations(dxcallsign, dxgrid, datatype, snr, offset, frequency, heard_stations_list): from datetime import datetime, timezone
""" import time
def add_to_heard_stations(dxcallsign, dxgrid, datatype, snr, offset, frequency, heard_stations_list, distance_km=None,
distance_miles=None, away_from_key=False):
"""
Args: Args:
dxcallsign: dxcallsign (str): The callsign of the DX station.
dxgrid: dxgrid (str): The Maidenhead grid square of the DX station.
datatype: datatype (str): The type of data received (e.g., FT8, CW).
snr: snr (int): Signal-to-noise ratio of the received signal.
offset: offset (float): Frequency offset.
frequency: frequency (float): Base frequency of the received signal.
heard_stations_list (list): List containing heard stations.
distance_km (float): Distance to the DX station in kilometers.
distance_miles (float): Distance to the DX station in miles.
away_from_key (bool): Away from key indicator
Returns: Returns:
Nothing Nothing. The function updates the heard_stations_list in-place.
""" """
# check if buffer empty # Convert current timestamp to an integer
if len(heard_stations_list) == 0: current_timestamp = int(datetime.now(timezone.utc).timestamp())
heard_stations_list.append(
[dxcallsign, dxgrid, int(datetime.now(timezone.utc).timestamp()), datatype, snr, offset, frequency] # Initialize the new entry
) new_entry = [
# if not, we search and update dxcallsign, dxgrid, current_timestamp, datatype, snr, offset, frequency, distance_km, distance_miles, away_from_key
]
# Check if the buffer is empty or if the callsign is not already in the list
if not any(dxcallsign == station[0] for station in heard_stations_list):
heard_stations_list.append(new_entry)
else: else:
for i in range(len(heard_stations_list)): # Search for the existing entry and update
# Update callsign with new timestamp for i, entry in enumerate(heard_stations_list):
if heard_stations_list[i].count(dxcallsign) > 0: if entry[0] == dxcallsign:
heard_stations_list[i] = [ heard_stations_list[i] = new_entry
dxcallsign,
dxgrid,
int(time.time()),
datatype,
snr,
offset,
frequency,
]
break break
# Insert if nothing found
if i == len(heard_stations_list) - 1:
heard_stations_list.append(
[
dxcallsign,
dxgrid,
int(time.time()),
datatype,
snr,
offset,
frequency,
]
)
break
# for idx, item in enumerate(heard_stations_list):
# if dxcallsign in item:
# item = [dxcallsign, int(time.time())]
# heard_stations_list[idx] = item
def callsign_to_bytes(callsign: str) -> bytes: def callsign_to_bytes(callsign: str) -> bytes:

94
modem/maidenhead.py Normal file
View file

@ -0,0 +1,94 @@
import math
def haversine(lat1, lon1, lat2, lon2):
"""
Calculate the great circle distance in kilometers between two points
on the Earth (specified in decimal degrees).
Parameters:
lat1, lon1: Latitude and longitude of point 1.
lat2, lon2: Latitude and longitude of point 2.
Returns:
float: Distance between the two points in kilometers.
"""
# Radius of the Earth in kilometers. Use 3956 for miles
R = 6371.0
# Convert latitude and longitude from degrees to radians
lat1 = math.radians(lat1)
lon1 = math.radians(lon1)
lat2 = math.radians(lat2)
lon2 = math.radians(lon2)
# Difference in coordinates
dlon = lon2 - lon1
dlat = lat2 - lat1
# Haversine formula
a = math.sin(dlat / 2) ** 2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon / 2) ** 2
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
distance = R * c
return distance
def maidenhead_to_latlon(grid_square):
"""
Convert a Maidenhead locator to latitude and longitude coordinates.
The output coordinates represent the southwestern corner of the grid square.
Parameters:
grid_square (str): The Maidenhead locator.
Returns:
tuple: A tuple containing the latitude and longitude (in that order) of the grid square's center.
"""
if len(grid_square) < 4 or len(grid_square) % 2 != 0:
raise ValueError("Grid square must be at least 4 characters long and an even length.")
grid_square = grid_square.upper()
lon = -180 + (ord(grid_square[0]) - ord('A')) * 20
lat = -90 + (ord(grid_square[1]) - ord('A')) * 10
lon += (int(grid_square[2]) * 2)
lat += int(grid_square[3])
if len(grid_square) >= 6:
lon += (ord(grid_square[4]) - ord('A')) * (5 / 60)
lat += (ord(grid_square[5]) - ord('A')) * (2.5 / 60)
if len(grid_square) == 8:
lon += int(grid_square[6]) * (5 / 600)
lat += int(grid_square[7]) * (2.5 / 600)
# Adjust to the center of the grid square
if len(grid_square) <= 4:
lon += 1
lat += 0.5
elif len(grid_square) == 6:
lon += 2.5 / 60
lat += 1.25 / 60
else:
lon += 2.5 / 600
lat += 1.25 / 600
return lat, lon
def distance_between_locators(locator1, locator2):
"""
Calculate the distance between two Maidenhead locators and return the result as a dictionary.
Parameters:
locator1 (str): The first Maidenhead locator.
locator2 (str): The second Maidenhead locator.
Returns:
dict: A dictionary containing the distances in kilometers and miles.
"""
lat1, lon1 = maidenhead_to_latlon(locator1)
lat2, lon2 = maidenhead_to_latlon(locator2)
km = haversine(lat1, lon1, lat2, lon2)
miles = km * 0.621371
return {'kilometers': km, 'miles': miles}

View file

@ -9,10 +9,7 @@ Created on Wed Dec 23 07:04:24 2020
# pylint: disable=invalid-name, line-too-long, c-extension-no-member # pylint: disable=invalid-name, line-too-long, c-extension-no-member
# pylint: disable=import-outside-toplevel # pylint: disable=import-outside-toplevel
import atexit
import ctypes
import queue import queue
import threading
import time import time
import codec2 import codec2
import numpy as np import numpy as np
@ -20,9 +17,9 @@ import sounddevice as sd
import structlog import structlog
import tci import tci
import cw import cw
from queues import RIGCTLD_COMMAND_QUEUE
import audio import audio
import demodulator import demodulator
import modulator
TESTMODE = False TESTMODE = False
@ -44,35 +41,36 @@ class RF:
self.audio_input_device = config['AUDIO']['input_device'] self.audio_input_device = config['AUDIO']['input_device']
self.audio_output_device = config['AUDIO']['output_device'] self.audio_output_device = config['AUDIO']['output_device']
self.tx_audio_level = config['AUDIO']['tx_audio_level']
self.enable_audio_auto_tune = config['AUDIO']['enable_auto_tune']
self.tx_delay = config['MODEM']['tx_delay']
self.radiocontrol = config['RADIO']['control'] self.radiocontrol = config['RADIO']['control']
self.rigctld_ip = config['RIGCTLD']['ip'] self.rigctld_ip = config['RIGCTLD']['ip']
self.rigctld_port = config['RIGCTLD']['port'] self.rigctld_port = config['RIGCTLD']['port']
self.states.setTransmitting(False)
self.ptt_state = False
self.radio_alc = 0.0
self.tci_ip = config['TCI']['tci_ip'] self.tci_ip = config['TCI']['tci_ip']
self.tci_port = config['TCI']['tci_port'] self.tci_port = config['TCI']['tci_port']
self.tx_audio_level = config['AUDIO']['tx_audio_level']
self.rx_audio_level = config['AUDIO']['rx_audio_level']
self.ptt_state = False
self.enqueuing_audio = False # set to True, while we are processing audio
self.AUDIO_SAMPLE_RATE = 48000 self.AUDIO_SAMPLE_RATE = 48000
self.MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000 self.modem_sample_rate = codec2.api.FREEDV_FS_8000
# 8192 Let's do some tests with very small chunks for TX # 8192 Let's do some tests with very small chunks for TX
self.AUDIO_FRAMES_PER_BUFFER_TX = 1200 if self.radiocontrol in ["tci"] else 2400 * 2 #self.AUDIO_FRAMES_PER_BUFFER_TX = 1200 if self.radiocontrol in ["tci"] else 2400 * 2
# 8 * (self.AUDIO_SAMPLE_RATE/self.MODEM_SAMPLE_RATE) == 48 # 8 * (self.AUDIO_SAMPLE_RATE/self.modem_sample_rate) == 48
self.AUDIO_CHANNELS = 1 self.AUDIO_CHANNELS = 1
self.MODE = 0 self.MODE = 0
self.rms_counter = 0 self.rms_counter = 0
self.audio_out_queue = queue.Queue()
# Make sure our resampler will work # Make sure our resampler will work
assert (self.AUDIO_SAMPLE_RATE / self.MODEM_SAMPLE_RATE) == codec2.api.FDMDV_OS_48 # type: ignore assert (self.AUDIO_SAMPLE_RATE / self.modem_sample_rate) == codec2.api.FDMDV_OS_48 # type: ignore
self.audio_received_queue = queue.Queue() self.audio_received_queue = queue.Queue()
self.data_queue_received = queue.Queue() self.data_queue_received = queue.Queue()
@ -83,9 +81,12 @@ class RF:
self.data_queue_received, self.data_queue_received,
self.states, self.states,
self.event_manager, self.event_manager,
self.service_queue,
self.fft_queue self.fft_queue
) )
self.modulator = modulator.Modulator(self.config)
def tci_tx_callback(self, audio_48k) -> None: def tci_tx_callback(self, audio_48k) -> None:
@ -103,10 +104,6 @@ class RF:
if not self.init_audio(): if not self.init_audio():
raise RuntimeError("Unable to init audio devices") raise RuntimeError("Unable to init audio devices")
self.demodulator.start(self.sd_input_stream) self.demodulator.start(self.sd_input_stream)
atexit.register(self.sd_input_stream.stop)
# Initialize codec2, rig control, and data threads
self.init_codec2()
return True return True
@ -152,17 +149,25 @@ class RF:
self.sd_input_stream = sd.InputStream( self.sd_input_stream = sd.InputStream(
channels=1, channels=1,
dtype="int16", dtype="int16",
callback=self.demodulator.sd_input_audio_callback, callback=self.sd_input_audio_callback,
device=in_dev_index, device=in_dev_index,
samplerate=self.AUDIO_SAMPLE_RATE, samplerate=self.AUDIO_SAMPLE_RATE,
blocksize=4800, blocksize=4800,
) )
self.sd_input_stream.start() self.sd_input_stream.start()
self.sd_output_stream = sd.OutputStream(
channels=1,
dtype="int16",
callback=self.sd_output_audio_callback,
device=out_dev_index,
samplerate=self.AUDIO_SAMPLE_RATE,
blocksize=1200,
)
self.sd_output_stream.start()
return True return True
except Exception as audioerr: except Exception as audioerr:
self.log.error("[MDM] init: starting pyaudio callback failed", e=audioerr) self.log.error("[MDM] init: starting pyaudio callback failed", e=audioerr)
self.stop_modem() self.stop_modem()
@ -185,191 +190,7 @@ class RF:
return True return True
def audio_auto_tune(self):
# enable / disable AUDIO TUNE Feature / ALC correction
if self.enable_audio_auto_tune:
if self.radio_alc == 0.0:
self.tx_audio_level = self.tx_audio_level + 20
elif 0.0 < self.radio_alc <= 0.1:
print("0.0 < self.radio_alc <= 0.1")
self.tx_audio_level = self.tx_audio_level + 2
self.log.debug("[MDM] AUDIO TUNE", audio_level=str(self.tx_audio_level),
alc_level=str(self.radio_alc))
elif 0.1 < self.radio_alc < 0.2:
print("0.1 < self.radio_alc < 0.2")
self.tx_audio_level = self.tx_audio_level
self.log.debug("[MDM] AUDIO TUNE", audio_level=str(self.tx_audio_level),
alc_level=str(self.radio_alc))
elif 0.2 < self.radio_alc < 0.99:
print("0.2 < self.radio_alc < 0.99")
self.tx_audio_level = self.tx_audio_level - 20
self.log.debug("[MDM] AUDIO TUNE", audio_level=str(self.tx_audio_level),
alc_level=str(self.radio_alc))
elif 1.0 >= self.radio_alc:
print("1.0 >= self.radio_alc")
self.tx_audio_level = self.tx_audio_level - 40
self.log.debug("[MDM] AUDIO TUNE", audio_level=str(self.tx_audio_level),
alc_level=str(self.radio_alc))
else:
self.log.debug("[MDM] AUDIO TUNE", audio_level=str(self.tx_audio_level),
alc_level=str(self.radio_alc))
def transmit(
self, mode, repeats: int, repeat_delay: int, frames: bytearray
) -> bool:
"""
Args:
mode:
repeats:
repeat_delay:
frames:
"""
if TESTMODE:
return
self.demodulator.reset_data_sync()
# get freedv instance by mode
mode_transition = {
codec2.FREEDV_MODE.signalling: self.freedv_datac13_tx,
codec2.FREEDV_MODE.datac0: self.freedv_datac0_tx,
codec2.FREEDV_MODE.datac1: self.freedv_datac1_tx,
codec2.FREEDV_MODE.datac3: self.freedv_datac3_tx,
codec2.FREEDV_MODE.datac4: self.freedv_datac4_tx,
codec2.FREEDV_MODE.datac13: self.freedv_datac13_tx,
}
if mode in mode_transition:
freedv = mode_transition[mode]
else:
print("wrong mode.................")
print(mode)
return False
# Wait for some other thread that might be transmitting
self.states.waitForTransmission()
self.states.setTransmitting(True)
#self.states.channel_busy_event.wait()
start_of_transmission = time.time()
# Open codec2 instance
self.MODE = mode
txbuffer = bytes()
# Add empty data to handle ptt toggle time
if self.tx_delay > 0:
self.transmit_add_silence(txbuffer, self.tx_delay)
self.log.debug(
"[MDM] TRANSMIT", mode=self.MODE.name, delay=self.tx_delay
)
if not isinstance(frames, list): frames = [frames]
for _ in range(repeats):
# Create modulation for all frames in the list
for frame in frames:
txbuffer = self.transmit_add_preamble(txbuffer, freedv)
txbuffer = self.transmit_create_frame(txbuffer, freedv, frame)
txbuffer = self.transmit_add_postamble(txbuffer, freedv)
# Add delay to end of frames
self.transmit_add_silence(txbuffer, repeat_delay)
# Re-sample back up to 48k (resampler works on np.int16)
x = np.frombuffer(txbuffer, dtype=np.int16)
self.audio_auto_tune()
x = audio.set_audio_volume(x, self.tx_audio_level)
if self.radiocontrol not in ["tci"]:
txbuffer_out = self.resampler.resample8_to_48(x)
else:
txbuffer_out = x
# transmit audio
self.transmit_audio(txbuffer_out)
self.radio.set_ptt(False)
self.event_manager.send_ptt_change(False)
self.states.setTransmitting(False)
end_of_transmission = time.time()
transmission_time = end_of_transmission - start_of_transmission
self.log.debug("[MDM] ON AIR TIME", time=transmission_time)
return True
def transmit_add_preamble(self, buffer, freedv):
# Init buffer for preample
n_tx_preamble_modem_samples = codec2.api.freedv_get_n_tx_preamble_modem_samples(
freedv
)
mod_out_preamble = ctypes.create_string_buffer(n_tx_preamble_modem_samples * 2)
# Write preamble to txbuffer
codec2.api.freedv_rawdatapreambletx(freedv, mod_out_preamble)
buffer += bytes(mod_out_preamble)
return buffer
def transmit_add_postamble(self, buffer, freedv):
# Init buffer for postamble
n_tx_postamble_modem_samples = (
codec2.api.freedv_get_n_tx_postamble_modem_samples(freedv)
)
mod_out_postamble = ctypes.create_string_buffer(
n_tx_postamble_modem_samples * 2
)
# Write postamble to txbuffer
codec2.api.freedv_rawdatapostambletx(freedv, mod_out_postamble)
# Append postamble to txbuffer
buffer += bytes(mod_out_postamble)
return buffer
def transmit_add_silence(self, buffer, duration):
data_delay = int(self.MODEM_SAMPLE_RATE * (duration / 1000)) # type: ignore
mod_out_silence = ctypes.create_string_buffer(data_delay * 2)
buffer += bytes(mod_out_silence)
return buffer
def transmit_create_frame(self, txbuffer, freedv, frame):
# Get number of bytes per frame for mode
bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(freedv) / 8)
payload_bytes_per_frame = bytes_per_frame - 2
# Init buffer for data
n_tx_modem_samples = codec2.api.freedv_get_n_tx_modem_samples(freedv)
mod_out = ctypes.create_string_buffer(n_tx_modem_samples * 2)
# Create buffer for data
# Use this if CRC16 checksum is required (DATAc1-3)
buffer = bytearray(payload_bytes_per_frame)
# Set buffersize to length of data which will be send
buffer[: len(frame)] = frame # type: ignore
# Create crc for data frame -
# Use the crc function shipped with codec2
# to avoid CRC algorithm incompatibilities
# Generate CRC16
crc = ctypes.c_ushort(
codec2.api.freedv_gen_crc16(bytes(buffer), payload_bytes_per_frame)
)
# Convert crc to 2-byte (16-bit) hex string
crc = crc.value.to_bytes(2, byteorder="big")
# Append CRC to data buffer
buffer += crc
assert (bytes_per_frame == len(buffer))
data = (ctypes.c_ubyte * bytes_per_frame).from_buffer_copy(buffer)
# modulate DATA and save it into mod_out pointer
codec2.api.freedv_rawdatatx(freedv, mod_out, data)
txbuffer += bytes(mod_out)
return txbuffer
def transmit_morse(self, repeats, repeat_delay, frames): def transmit_morse(self, repeats, repeat_delay, frames):
self.states.waitForTransmission() self.states.waitForTransmission()
@ -380,32 +201,56 @@ class RF:
) )
start_of_transmission = time.time() start_of_transmission = time.time()
txbuffer_out = cw.MorseCodePlayer().text_to_signal("DJ2LS-1") txbuffer_out = cw.MorseCodePlayer().text_to_signal(self.config['STATION'].mycall)
self.transmit_audio(txbuffer_out) # transmit audio
self.radio.set_ptt(False) self.enqueue_audio_out(txbuffer_out)
self.event_manager.send_ptt_change(False)
self.states.setTransmitting(False)
end_of_transmission = time.time() end_of_transmission = time.time()
transmission_time = end_of_transmission - start_of_transmission transmission_time = end_of_transmission - start_of_transmission
self.log.debug("[MDM] ON AIR TIME", time=transmission_time) self.log.debug("[MDM] ON AIR TIME", time=transmission_time)
def init_codec2(self):
# Open codec2 instances
# INIT TX MODES - here we need all modes. def transmit(
self.freedv_datac0_tx = codec2.open_instance(codec2.FREEDV_MODE.datac0.value) self, mode, repeats: int, repeat_delay: int, frames: bytearray
self.freedv_datac1_tx = codec2.open_instance(codec2.FREEDV_MODE.datac1.value) ) -> bool:
self.freedv_datac3_tx = codec2.open_instance(codec2.FREEDV_MODE.datac3.value)
self.freedv_datac4_tx = codec2.open_instance(codec2.FREEDV_MODE.datac4.value) self.demodulator.reset_data_sync()
self.freedv_datac13_tx = codec2.open_instance(codec2.FREEDV_MODE.datac13.value) # Wait for some other thread that might be transmitting
self.states.waitForTransmission()
self.states.setTransmitting(True)
# self.states.channel_busy_event.wait()
start_of_transmission = time.time()
txbuffer = self.modulator.create_burst(mode, repeats, repeat_delay, frames)
# Re-sample back up to 48k (resampler works on np.int16)
x = np.frombuffer(txbuffer, dtype=np.int16)
x = audio.set_audio_volume(x, self.tx_audio_level)
if self.radiocontrol not in ["tci"]:
txbuffer_out = self.resampler.resample8_to_48(x)
else:
txbuffer_out = x
# Low level modem audio transmit
def transmit_audio(self, audio_48k) -> None: # transmit audio
self.enqueue_audio_out(txbuffer_out)
end_of_transmission = time.time()
transmission_time = end_of_transmission - start_of_transmission
self.log.debug("[MDM] ON AIR TIME", time=transmission_time)
def enqueue_audio_out(self, audio_48k) -> None:
self.enqueuing_audio = True
if not self.states.isTransmitting():
self.states.setTransmitting(True)
self.radio.set_ptt(True) self.radio.set_ptt(True)
self.event_manager.send_ptt_change(True) self.event_manager.send_ptt_change(True)
if self.radiocontrol in ["tci"]: if self.radiocontrol in ["tci"]:
@ -413,5 +258,74 @@ class RF:
# we need to wait manually for tci processing # we need to wait manually for tci processing
self.tci_module.wait_until_transmitted(audio_48k) self.tci_module.wait_until_transmitted(audio_48k)
else: else:
sd.play(audio_48k, blocksize=4096, blocking=True) # slice audio data to needed blocklength
block_size = self.sd_output_stream.blocksize
pad_length = -len(audio_48k) % block_size
padded_data = np.pad(audio_48k, (0, pad_length), mode='constant')
sliced_audio_data = padded_data.reshape(-1, block_size)
# add each block to audio out queue
for block in sliced_audio_data:
self.audio_out_queue.put(block)
self.enqueuing_audio = False
self.states.transmitting_event.wait()
self.radio.set_ptt(False)
self.event_manager.send_ptt_change(False)
return return
def sd_output_audio_callback(self, outdata: np.ndarray, frames: int, time, status) -> None:
try:
if not self.audio_out_queue.empty() and not self.enqueuing_audio:
chunk = self.audio_out_queue.get_nowait()
audio_8k = self.resampler.resample48_to_8(chunk)
audio.calculate_fft(audio_8k, self.fft_queue, self.states)
outdata[:] = chunk.reshape(outdata.shape)
else:
# reset transmitting state only, if we are not actively processing audio
# for avoiding a ptt toggle state bug
if self.audio_out_queue.empty() and not self.enqueuing_audio:
self.states.setTransmitting(False)
# Fill with zeros if the queue is empty
outdata.fill(0)
except Exception as e:
self.log.warning("[AUDIO STATUS]", status=status, time=time, frames=frames, e=e)
outdata.fill(0)
def sd_input_audio_callback(self, indata: np.ndarray, frames: int, time, status) -> None:
if status:
self.log.warning("[AUDIO STATUS]", status=status, time=time, frames=frames)
# FIXME on windows input overflows crashing the rx audio stream. Lets restart the server then
#if status.input_overflow:
# self.service_queue.put("restart")
return
try:
audio_48k = np.frombuffer(indata, dtype=np.int16)
audio_8k = self.resampler.resample48_to_8(audio_48k)
audio_8k_level_adjusted = audio.set_audio_volume(audio_8k, self.rx_audio_level)
if not self.states.isTransmitting():
audio.calculate_fft(audio_8k_level_adjusted, self.fft_queue, self.states)
length_audio_8k_level_adjusted = len(audio_8k_level_adjusted)
# Avoid buffer overflow by filling only if buffer for
# selected datachannel mode is not full
index = 0
for mode in self.demodulator.MODE_DICT:
mode_data = self.demodulator.MODE_DICT[mode]
audiobuffer = mode_data['audio_buffer']
decode = mode_data['decode']
index += 1
if audiobuffer:
if (audiobuffer.nbuffer + length_audio_8k_level_adjusted) > audiobuffer.size:
self.demodulator.buffer_overflow_counter[index] += 1
self.event_manager.send_buffer_overflow(self.demodulator.buffer_overflow_counter)
elif decode:
audiobuffer.push(audio_8k_level_adjusted)
except Exception as e:
self.log.warning("[AUDIO EXCEPTION]", status=status, time=time, frames=frames, e=e)

View file

@ -6,9 +6,6 @@ from enum import Enum
class FRAME_TYPE(Enum): class FRAME_TYPE(Enum):
"""Lookup for frame types""" """Lookup for frame types"""
ARQ_CONNECTION_OPEN = 1
ARQ_CONNECTION_HB = 2
ARQ_CONNECTION_CLOSE = 3
ARQ_STOP = 10 ARQ_STOP = 10
ARQ_STOP_ACK = 11 ARQ_STOP_ACK = 11
ARQ_SESSION_OPEN = 12 ARQ_SESSION_OPEN = 12
@ -17,16 +14,24 @@ class FRAME_TYPE(Enum):
ARQ_SESSION_INFO_ACK = 15 ARQ_SESSION_INFO_ACK = 15
ARQ_BURST_FRAME = 20 ARQ_BURST_FRAME = 20
ARQ_BURST_ACK = 21 ARQ_BURST_ACK = 21
MESH_BROADCAST = 100 P2P_CONNECTION_CONNECT = 30
MESH_SIGNALLING_PING = 101 P2P_CONNECTION_CONNECT_ACK = 31
MESH_SIGNALLING_PING_ACK = 102 P2P_CONNECTION_HEARTBEAT = 32
P2P_CONNECTION_HEARTBEAT_ACK = 33
P2P_CONNECTION_PAYLOAD = 34
P2P_CONNECTION_PAYLOAD_ACK = 35
P2P_CONNECTION_DISCONNECT = 36
P2P_CONNECTION_DISCONNECT_ACK = 37
#MESH_BROADCAST = 100
#MESH_SIGNALLING_PING = 101
#MESH_SIGNALLING_PING_ACK = 102
CQ = 200 CQ = 200
QRV = 201 QRV = 201
PING = 210 PING = 210
PING_ACK = 211 PING_ACK = 211
IS_WRITING = 215 #IS_WRITING = 215
BEACON = 250 BEACON = 250
FEC = 251 #FEC = 251
FEC_WAKEUP = 252 #FEC_WAKEUP = 252
IDENT = 254 IDENT = 254
TEST_FRAME = 255 TEST_FRAME = 255

160
modem/modulator.py Normal file
View file

@ -0,0 +1,160 @@
import ctypes
import codec2
import structlog
class Modulator:
log = structlog.get_logger("RF")
def __init__(self, config):
self.config = config
self.tx_delay = config['MODEM']['tx_delay']
self.modem_sample_rate = codec2.api.FREEDV_FS_8000
# Initialize codec2, rig control, and data threads
self.init_codec2()
def init_codec2(self):
# Open codec2 instances
# INIT TX MODES - here we need all modes.
self.freedv_datac0_tx = codec2.open_instance(codec2.FREEDV_MODE.datac0.value)
self.freedv_datac1_tx = codec2.open_instance(codec2.FREEDV_MODE.datac1.value)
self.freedv_datac3_tx = codec2.open_instance(codec2.FREEDV_MODE.datac3.value)
self.freedv_datac4_tx = codec2.open_instance(codec2.FREEDV_MODE.datac4.value)
self.freedv_datac13_tx = codec2.open_instance(codec2.FREEDV_MODE.datac13.value)
self.freedv_datac14_tx = codec2.open_instance(codec2.FREEDV_MODE.datac14.value)
self.data_ofdm_500_tx = codec2.open_instance(codec2.FREEDV_MODE.data_ofdm_500.value)
self.data_ofdm_2438_tx = codec2.open_instance(codec2.FREEDV_MODE.data_ofdm_2438.value)
self.freedv_qam16c2_tx = codec2.open_instance(codec2.FREEDV_MODE.qam16c2.value)
#self.data_qam_2438_tx = codec2.open_instance(codec2.FREEDV_MODE.data_qam_2438.value)
def transmit_add_preamble(self, buffer, freedv):
# Init buffer for preample
n_tx_preamble_modem_samples = codec2.api.freedv_get_n_tx_preamble_modem_samples(
freedv
)
mod_out_preamble = ctypes.create_string_buffer(n_tx_preamble_modem_samples * 2)
# Write preamble to txbuffer
codec2.api.freedv_rawdatapreambletx(freedv, mod_out_preamble)
buffer += bytes(mod_out_preamble)
return buffer
def transmit_add_postamble(self, buffer, freedv):
# Init buffer for postamble
n_tx_postamble_modem_samples = (
codec2.api.freedv_get_n_tx_postamble_modem_samples(freedv)
)
mod_out_postamble = ctypes.create_string_buffer(
n_tx_postamble_modem_samples * 2
)
# Write postamble to txbuffer
codec2.api.freedv_rawdatapostambletx(freedv, mod_out_postamble)
# Append postamble to txbuffer
buffer += bytes(mod_out_postamble)
return buffer
def transmit_add_silence(self, buffer, duration):
data_delay = int(self.modem_sample_rate * (duration / 1000)) # type: ignore
mod_out_silence = ctypes.create_string_buffer(data_delay * 2)
buffer += bytes(mod_out_silence)
return buffer
def transmit_create_frame(self, txbuffer, freedv, frame):
# Get number of bytes per frame for mode
bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(freedv) / 8)
payload_bytes_per_frame = bytes_per_frame - 2
# Init buffer for data
n_tx_modem_samples = codec2.api.freedv_get_n_tx_modem_samples(freedv)
mod_out = ctypes.create_string_buffer(n_tx_modem_samples * 2)
# Create buffer for data
# Use this if CRC16 checksum is required (DATAc1-3)
buffer = bytearray(payload_bytes_per_frame)
# Set buffersize to length of data which will be send
buffer[: len(frame)] = frame # type: ignore
# Create crc for data frame -
# Use the crc function shipped with codec2
# to avoid CRC algorithm incompatibilities
# Generate CRC16
crc = ctypes.c_ushort(
codec2.api.freedv_gen_crc16(bytes(buffer), payload_bytes_per_frame)
)
# Convert crc to 2-byte (16-bit) hex string
crc = crc.value.to_bytes(2, byteorder="big")
# Append CRC to data buffer
buffer += crc
assert (bytes_per_frame == len(buffer))
data = (ctypes.c_ubyte * bytes_per_frame).from_buffer_copy(buffer)
# modulate DATA and save it into mod_out pointer
codec2.api.freedv_rawdatatx(freedv, mod_out, data)
txbuffer += bytes(mod_out)
return txbuffer
def create_burst(
self, mode, repeats: int, repeat_delay: int, frames: bytearray
) -> bool:
"""
Args:
mode:
repeats:
repeat_delay:
frames:
"""
# get freedv instance by mode
mode_transition = {
codec2.FREEDV_MODE.signalling_ack: self.freedv_datac14_tx,
codec2.FREEDV_MODE.signalling: self.freedv_datac13_tx,
codec2.FREEDV_MODE.datac0: self.freedv_datac0_tx,
codec2.FREEDV_MODE.datac1: self.freedv_datac1_tx,
codec2.FREEDV_MODE.datac3: self.freedv_datac3_tx,
codec2.FREEDV_MODE.datac4: self.freedv_datac4_tx,
codec2.FREEDV_MODE.datac13: self.freedv_datac13_tx,
codec2.FREEDV_MODE.datac14: self.freedv_datac14_tx,
codec2.FREEDV_MODE.data_ofdm_500: self.data_ofdm_500_tx,
codec2.FREEDV_MODE.data_ofdm_2438: self.data_ofdm_2438_tx,
codec2.FREEDV_MODE.qam16c2: self.freedv_qam16c2_tx,
#codec2.FREEDV_MODE.data_qam_2438: self.freedv_data_qam_2438_tx,
}
if mode in mode_transition:
freedv = mode_transition[mode]
else:
print("wrong mode.................")
print(mode)
#return False
# Open codec2 instance
self.MODE = mode
self.log.debug(
"[MDM] TRANSMIT", mode=self.MODE.name, delay=self.tx_delay
)
txbuffer = bytes()
# Add empty data to handle ptt toggle time
if self.tx_delay > 0:
txbuffer = self.transmit_add_silence(txbuffer, self.tx_delay)
if not isinstance(frames, list): frames = [frames]
for _ in range(repeats):
# Create modulation for all frames in the list
for frame in frames:
txbuffer = self.transmit_add_preamble(txbuffer, freedv)
txbuffer = self.transmit_create_frame(txbuffer, freedv, frame)
txbuffer = self.transmit_add_postamble(txbuffer, freedv)
# Add delay to end of frames
txbuffer = self.transmit_add_silence(txbuffer, repeat_delay)
return txbuffer

315
modem/p2p_connection.py Normal file
View file

@ -0,0 +1,315 @@
import threading
from enum import Enum
from modem_frametypes import FRAME_TYPE
from codec2 import FREEDV_MODE
import data_frame_factory
import structlog
import random
from queue import Queue
import time
from command_arq_raw import ARQRawCommand
import numpy as np
import base64
from arq_data_type_handler import ARQDataTypeHandler, ARQ_SESSION_TYPES
from arq_session_iss import ARQSessionISS
class States(Enum):
NEW = 0
CONNECTING = 1
CONNECT_SENT = 2
CONNECT_ACK_SENT = 3
CONNECTED = 4
#HEARTBEAT_SENT = 5
#HEARTBEAT_ACK_SENT = 6
PAYLOAD_SENT = 7
ARQ_SESSION = 8
DISCONNECTING = 9
DISCONNECTED = 10
FAILED = 11
class P2PConnection:
STATE_TRANSITION = {
States.NEW: {
FRAME_TYPE.P2P_CONNECTION_CONNECT.value: 'connected_irs',
},
States.CONNECTING: {
FRAME_TYPE.P2P_CONNECTION_CONNECT_ACK.value: 'connected_iss',
},
States.CONNECTED: {
FRAME_TYPE.P2P_CONNECTION_CONNECT.value: 'connected_irs',
FRAME_TYPE.P2P_CONNECTION_CONNECT_ACK.value: 'connected_iss',
FRAME_TYPE.P2P_CONNECTION_PAYLOAD.value: 'received_data',
FRAME_TYPE.P2P_CONNECTION_DISCONNECT.value: 'received_disconnect',
},
States.PAYLOAD_SENT: {
FRAME_TYPE.P2P_CONNECTION_PAYLOAD_ACK.value: 'transmitted_data',
},
States.DISCONNECTING: {
FRAME_TYPE.P2P_CONNECTION_DISCONNECT_ACK.value: 'received_disconnect_ack',
},
States.DISCONNECTED: {
FRAME_TYPE.P2P_CONNECTION_DISCONNECT.value: 'received_disconnect',
FRAME_TYPE.P2P_CONNECTION_DISCONNECT_ACK.value: 'received_disconnect_ack',
},
}
def __init__(self, config: dict, modem, origin: str, destination: str, state_manager, event_manager, socket_command_handler=None):
self.logger = structlog.get_logger(type(self).__name__)
self.config = config
self.frame_factory = data_frame_factory.DataFrameFactory(self.config)
self.socket_command_handler = socket_command_handler
self.destination = destination
self.origin = origin
self.bandwidth = 0
self.state_manager = state_manager
self.event_manager = event_manager
self.modem = modem
self.modem.demodulator.set_decode_mode()
self.p2p_data_rx_queue = Queue()
self.p2p_data_tx_queue = Queue()
self.arq_data_type_handler = ARQDataTypeHandler(self.event_manager, self.state_manager)
self.state = States.NEW
self.session_id = self.generate_id()
self.event_frame_received = threading.Event()
self.RETRIES_CONNECT = 5
self.TIMEOUT_CONNECT = 5
self.TIMEOUT_DATA = 5
self.RETRIES_DATA = 5
self.ENTIRE_CONNECTION_TIMEOUT = 100
self.is_ISS = False # Indicator, if we are ISS or IRS
self.last_data_timestamp= time.time()
self.start_data_processing_worker()
def start_data_processing_worker(self):
"""Starts a worker thread to monitor the transmit data queue and process data."""
def data_processing_worker():
while True:
if time.time() > self.last_data_timestamp + self.ENTIRE_CONNECTION_TIMEOUT and self.state is not States.ARQ_SESSION:
self.disconnect()
return
if not self.p2p_data_tx_queue.empty() and self.state == States.CONNECTED:
self.process_data_queue()
threading.Event().wait(0.1)
# Create and start the worker thread
worker_thread = threading.Thread(target=data_processing_worker, daemon=True)
worker_thread.start()
def generate_id(self):
while True:
random_int = random.randint(1,255)
if random_int not in self.state_manager.p2p_connection_sessions:
return random_int
if len(self.state_manager.p2p_connection_sessions) >= 255:
return False
def set_details(self, snr, frequency_offset):
self.snr = snr
self.frequency_offset = frequency_offset
def log(self, message, isWarning = False):
msg = f"[{type(self).__name__}][id={self.session_id}][state={self.state}][ISS={bool(self.is_ISS)}]: {message}"
logger = self.logger.warn if isWarning else self.logger.info
logger(msg)
def set_state(self, state):
if self.state == state:
self.log(f"{type(self).__name__} state {self.state.name} unchanged.")
else:
self.log(f"{type(self).__name__} state change from {self.state.name} to {state.name}")
self.state = state
def on_frame_received(self, frame):
self.last_data_timestamp = time.time()
self.event_frame_received.set()
self.log(f"Received {frame['frame_type']}")
frame_type = frame['frame_type_int']
if self.state in self.STATE_TRANSITION:
if frame_type in self.STATE_TRANSITION[self.state]:
action_name = self.STATE_TRANSITION[self.state][frame_type]
response = getattr(self, action_name)(frame)
return
self.log(f"Ignoring unknown transition from state {self.state.name} with frame {frame['frame_type']}")
def transmit_frame(self, frame: bytearray, mode='auto'):
self.log("Transmitting frame")
if mode in ['auto']:
mode = self.get_mode_by_speed_level(self.speed_level)
self.modem.transmit(mode, 1, 1, frame)
def transmit_wait_and_retry(self, frame_or_burst, timeout, retries, mode):
while retries > 0:
self.event_frame_received = threading.Event()
if isinstance(frame_or_burst, list): burst = frame_or_burst
else: burst = [frame_or_burst]
for f in burst:
self.transmit_frame(f, mode)
self.event_frame_received.clear()
self.log(f"Waiting {timeout} seconds...")
if self.event_frame_received.wait(timeout):
return
self.log("Timeout!")
retries = retries - 1
#self.connected_iss() # override connection state for simulation purposes
self.session_failed()
def launch_twr(self, frame_or_burst, timeout, retries, mode):
twr = threading.Thread(target = self.transmit_wait_and_retry, args=[frame_or_burst, timeout, retries, mode], daemon=True)
twr.start()
def transmit_and_wait_irs(self, frame, timeout, mode):
self.event_frame_received.clear()
self.transmit_frame(frame, mode)
self.log(f"Waiting {timeout} seconds...")
#if not self.event_frame_received.wait(timeout):
# self.log("Timeout waiting for ISS. Session failed.")
# self.transmission_failed()
def launch_twr_irs(self, frame, timeout, mode):
thread_wait = threading.Thread(target = self.transmit_and_wait_irs,
args = [frame, timeout, mode], daemon=True)
thread_wait.start()
def connect(self):
self.set_state(States.CONNECTING)
self.is_ISS = True
session_open_frame = self.frame_factory.build_p2p_connection_connect(self.origin, self.destination, self.session_id)
self.launch_twr(session_open_frame, self.TIMEOUT_CONNECT, self.RETRIES_CONNECT, mode=FREEDV_MODE.signalling)
return
def connected_iss(self, frame=None):
self.log("CONNECTED ISS...........................")
self.set_state(States.CONNECTED)
self.is_ISS = True
if self.socket_command_handler:
self.socket_command_handler.socket_respond_connected(self.origin, self.destination, self.bandwidth)
def connected_irs(self, frame):
self.log("CONNECTED IRS...........................")
self.state_manager.register_p2p_connection_session(self)
self.set_state(States.CONNECTED)
self.is_ISS = False
self.orign = frame["origin"]
self.destination = frame["destination_crc"]
if self.socket_command_handler:
self.socket_command_handler.socket_respond_connected(self.origin, self.destination, self.bandwidth)
session_open_frame = self.frame_factory.build_p2p_connection_connect_ack(self.destination, self.origin, self.session_id)
self.launch_twr_irs(session_open_frame, self.ENTIRE_CONNECTION_TIMEOUT, mode=FREEDV_MODE.signalling)
def session_failed(self):
self.set_state(States.FAILED)
if self.socket_command_handler:
self.socket_command_handler.socket_respond_disconnected()
def process_data_queue(self, frame=None):
if not self.p2p_data_tx_queue.empty():
print("processing data....")
self.set_state(States.PAYLOAD_SENT)
data = self.p2p_data_tx_queue.get()
sequence_id = random.randint(0,255)
data = data.encode('utf-8')
if len(data) <= 11:
mode = FREEDV_MODE.signalling
elif 11 < len(data) < 32:
mode = FREEDV_MODE.datac4
else:
self.transmit_arq(data)
return
payload = self.frame_factory.build_p2p_connection_payload(mode, self.session_id, sequence_id, data)
self.launch_twr(payload, self.TIMEOUT_DATA, self.RETRIES_DATA,mode=mode)
return
def prepare_data_chunk(self, data, mode):
return data
def received_data(self, frame):
print(frame)
self.p2p_data_rx_queue.put(frame['data'])
ack_data = self.frame_factory.build_p2p_connection_payload_ack(self.session_id, 0)
self.launch_twr_irs(ack_data, self.ENTIRE_CONNECTION_TIMEOUT, mode=FREEDV_MODE.signalling)
def transmit_data_ack(self, frame):
print(frame)
def transmitted_data(self, frame):
print("transmitted data...")
self.set_state(States.CONNECTED)
def disconnect(self):
if self.state not in [States.DISCONNECTING, States.DISCONNECTED]:
self.set_state(States.DISCONNECTING)
disconnect_frame = self.frame_factory.build_p2p_connection_disconnect(self.session_id)
self.launch_twr(disconnect_frame, self.TIMEOUT_CONNECT, self.RETRIES_CONNECT, mode=FREEDV_MODE.signalling)
return
def received_disconnect(self, frame):
self.log("DISCONNECTED...............")
self.set_state(States.DISCONNECTED)
if self.socket_command_handler:
self.socket_command_handler.socket_respond_disconnected()
self.is_ISS = False
disconnect_ack_frame = self.frame_factory.build_p2p_connection_disconnect_ack(self.session_id)
self.launch_twr_irs(disconnect_ack_frame, self.ENTIRE_CONNECTION_TIMEOUT, mode=FREEDV_MODE.signalling)
def received_disconnect_ack(self, frame):
self.log("DISCONNECTED...............")
self.set_state(States.DISCONNECTED)
if self.socket_command_handler:
self.socket_command_handler.socket_respond_disconnected()
def transmit_arq(self, data):
self.set_state(States.ARQ_SESSION)
print("----------------------------------------------------------------")
print(self.destination)
print(self.state_manager.p2p_connection_sessions)
prepared_data, type_byte = self.arq_data_type_handler.prepare(data, ARQ_SESSION_TYPES.p2p_connection)
iss = ARQSessionISS(self.config, self.modem, 'AA1AAA-1', self.state_manager, prepared_data, type_byte)
iss.id = self.session_id
if iss.id:
self.state_manager.register_arq_iss_session(iss)
iss.start()
return iss
def transmitted_arq(self):
self.last_data_timestamp = time.time()
self.set_state(States.CONNECTED)
def received_arq(self, data):
self.last_data_timestamp = time.time()
self.set_state(States.CONNECTED)
self.p2p_data_rx_queue.put(data)

View file

@ -299,13 +299,13 @@ class radio:
# PTT Port and Type # PTT Port and Type
if not should_ignore(config.get('ptt_port')): if not should_ignore(config.get('ptt_port')):
args += ['--ptt-port', config['ptt_port']] args += ['-p', config['ptt_port']]
if not should_ignore(config.get('ptt_type')): if not should_ignore(config.get('ptt_type')):
args += ['--ptt-type', config['ptt_type']] args += ['-P', config['ptt_type']]
# Serial DCD and DTR # Serial DCD and DTR
if not should_ignore(config.get('serial_dcd')): if not should_ignore(config.get('serial_dcd')):
args += ['--dcd-type', config['serial_dcd']] args += ['-D', config['serial_dcd']]
if not should_ignore(config.get('serial_dtr')): if not should_ignore(config.get('serial_dtr')):
args += ['--set-conf', f'dtr_state={config["serial_dtr"]}'] args += ['--set-conf', f'dtr_state={config["serial_dtr"]}']
@ -323,7 +323,7 @@ class radio:
# Handle custom arguments for rigctld # Handle custom arguments for rigctld
# Custom args are split via ' ' so python doesn't add extranaeous quotes on windows # Custom args are split via ' ' so python doesn't add extranaeous quotes on windows
args += config_rigctld["arguments"].split(" ") args += config_rigctld["arguments"].split(" ")
#print("Hamlib args ==>" + str(args)) print("Hamlib args ==>" + str(args))
return args return args

View file

@ -1,3 +1,5 @@
import time
from flask import Flask, request, jsonify, make_response, abort, Response from flask import Flask, request, jsonify, make_response, abort, Response
from flask_sock import Sock from flask_sock import Sock
from flask_cors import CORS from flask_cors import CORS
@ -20,6 +22,8 @@ import command_test
import command_arq_raw import command_arq_raw
import command_message_send import command_message_send
import event_manager import event_manager
import atexit
from message_system_db_manager import DatabaseManager from message_system_db_manager import DatabaseManager
from message_system_db_messages import DatabaseManagerMessages from message_system_db_messages import DatabaseManagerMessages
from message_system_db_attachments import DatabaseManagerAttachments from message_system_db_attachments import DatabaseManagerAttachments
@ -29,7 +33,7 @@ from schedule_manager import ScheduleManager
app = Flask(__name__) app = Flask(__name__)
CORS(app, resources={r"/*": {"origins": "*"}}) CORS(app, resources={r"/*": {"origins": "*"}})
sock = Sock(app) sock = Sock(app)
MODEM_VERSION = "0.14.2-alpha" MODEM_VERSION = "0.15.2-alpha"
# set config file to use # set config file to use
def set_config(): def set_config():
@ -142,14 +146,15 @@ def post_cqcqcq():
def post_beacon(): def post_beacon():
if request.method not in ['POST']: if request.method not in ['POST']:
return api_response({"info": "endpoint for controlling BEACON STATE via POST"}) return api_response({"info": "endpoint for controlling BEACON STATE via POST"})
if not isinstance(request.json['enabled'], bool) or not isinstance(request.json['away_from_key'], bool):
if not isinstance(request.json['enabled'], bool):
api_abort(f"Incorrect value for 'enabled'. Shoud be bool.") api_abort(f"Incorrect value for 'enabled'. Shoud be bool.")
if not app.state_manager.is_modem_running: if not app.state_manager.is_modem_running:
api_abort('Modem not running', 503) api_abort('Modem not running', 503)
if not app.state_manager.is_beacon_running: if not app.state_manager.is_beacon_running:
app.state_manager.set('is_beacon_running', request.json['enabled']) app.state_manager.set('is_beacon_running', request.json['enabled'])
app.state_manager.set('is_away_from_key', request.json['away_from_key'])
if not app.state_manager.getARQ(): if not app.state_manager.getARQ():
enqueue_tx_command(command_beacon.BeaconCommand, request.json) enqueue_tx_command(command_beacon.BeaconCommand, request.json)
else: else:
@ -320,7 +325,23 @@ def sock_fft(sock):
def sock_states(sock): def sock_states(sock):
wsm.handle_connection(sock, wsm.states_client_list, app.state_queue) wsm.handle_connection(sock, wsm.states_client_list, app.state_queue)
@atexit.register
def stop_server():
print("------------------------------------------")
try:
app.service_manager.modem_service.put("stop")
if app.socket_interface_manager:
app.socket_interface_manager.stop_servers()
if app.service_manager.modem:
app.service_manager.modem.sd_input_stream.stop
audio.sd._terminate()
except Exception as e:
print(e)
print("Error stopping modem")
time.sleep(1)
print('Server shutdown...')
print("------------------------------------------")
if __name__ == "__main__": if __name__ == "__main__":
app.config['SOCK_SERVER_OPTIONS'] = {'ping_interval': 10} app.config['SOCK_SERVER_OPTIONS'] = {'ping_interval': 10}
@ -331,6 +352,7 @@ if __name__ == "__main__":
app.config_manager = CONFIG(config_file) app.config_manager = CONFIG(config_file)
# start modem # start modem
app.p2p_data_queue = queue.Queue() # queue which holds processing data of p2p connections
app.state_queue = queue.Queue() # queue which holds latest states app.state_queue = queue.Queue() # queue which holds latest states
app.modem_events = queue.Queue() # queue which holds latest events app.modem_events = queue.Queue() # queue which holds latest events
app.modem_fft = queue.Queue() # queue which holds latest fft data app.modem_fft = queue.Queue() # queue which holds latest fft data
@ -342,6 +364,7 @@ if __name__ == "__main__":
app.schedule_manager = ScheduleManager(app.MODEM_VERSION, app.config_manager, app.state_manager, app.event_manager) app.schedule_manager = ScheduleManager(app.MODEM_VERSION, app.config_manager, app.state_manager, app.event_manager)
# start service manager # start service manager
app.service_manager = service_manager.SM(app) app.service_manager = service_manager.SM(app)
# start modem service # start modem service
app.modem_service.put("start") app.modem_service.put("start")
# initialize database default values # initialize database default values
@ -353,7 +376,9 @@ if __name__ == "__main__":
modemport = conf['NETWORK']['modemport'] modemport = conf['NETWORK']['modemport']
if not modemaddress: if not modemaddress:
modemaddress = '0.0.0.0' modemaddress = '127.0.0.1'
if not modemport: if not modemport:
modemport = 5000 modemport = 5000
app.run(modemaddress, modemport) app.run(modemaddress, modemport)

View file

@ -5,7 +5,7 @@ import structlog
import audio import audio
import radio_manager import radio_manager
from socket_interface import SocketInterfaceHandler
class SM: class SM:
def __init__(self, app): def __init__(self, app):
@ -19,7 +19,7 @@ class SM:
self.state_manager = app.state_manager self.state_manager = app.state_manager
self.event_manager = app.event_manager self.event_manager = app.event_manager
self.schedule_manager = app.schedule_manager self.schedule_manager = app.schedule_manager
self.socket_interface_manager = None
runner_thread = threading.Thread( runner_thread = threading.Thread(
target=self.runner, name="runner thread", daemon=True target=self.runner, name="runner thread", daemon=True
@ -34,15 +34,24 @@ class SM:
self.start_radio_manager() self.start_radio_manager()
self.start_modem() self.start_modem()
if self.config['SOCKET_INTERFACE']['enable']:
self.socket_interface_manager = SocketInterfaceHandler(self.modem, self.app.config_manager, self.state_manager, self.event_manager).start_servers()
elif cmd in ['stop'] and self.modem: elif cmd in ['stop'] and self.modem:
self.stop_modem() self.stop_modem()
self.stop_radio_manager() self.stop_radio_manager()
if self.config['SOCKET_INTERFACE']['enable'] and self.socket_interface_manager:
self.socket_interface_manager.stop_servers()
# we need to wait a bit for avoiding a portaudio crash # we need to wait a bit for avoiding a portaudio crash
threading.Event().wait(0.5) threading.Event().wait(0.5)
elif cmd in ['restart']: elif cmd in ['restart']:
self.stop_modem() self.stop_modem()
self.stop_radio_manager() self.stop_radio_manager()
if self.config['SOCKET_INTERFACE']['enable'] and self.socket_interface_manager:
self.socket_interface_manager.stop_servers()
# we need to wait a bit for avoiding a portaudio crash # we need to wait a bit for avoiding a portaudio crash
threading.Event().wait(0.5) threading.Event().wait(0.5)

193
modem/socket_interface.py Normal file
View file

@ -0,0 +1,193 @@
""" WORK IN PROGRESS by DJ2LS"""
import socketserver
import threading
import structlog
import select
from queue import Queue
from socket_interface_commands import SocketCommandHandler
class CommandSocket(socketserver.BaseRequestHandler):
#def __init__(self, request, client_address, server):
def __init__(self, request, client_address, server, modem=None, state_manager=None, event_manager=None, config_manager=None):
self.state_manager = state_manager
self.event_manager = event_manager
self.config_manager = config_manager
self.modem = modem
self.logger = structlog.get_logger(type(self).__name__)
self.command_handler = SocketCommandHandler(request, self.modem, self.config_manager, self.state_manager, self.event_manager)
self.handlers = {
'CONNECT': self.command_handler.handle_connect,
'DISCONNECT': self.command_handler.handle_disconnect,
'MYCALL': self.command_handler.handle_mycall,
'BW': self.command_handler.handle_bw,
'ABORT': self.command_handler.handle_abort,
'PUBLIC': self.command_handler.handle_public,
'CWID': self.command_handler.handle_cwid,
'LISTEN': self.command_handler.handle_listen,
'COMPRESSION': self.command_handler.handle_compression,
'WINLINK SESSION': self.command_handler.handle_winlink_session,
}
super().__init__(request, client_address, server)
def log(self, message, isWarning = False):
msg = f"[{type(self).__name__}]: {message}"
logger = self.logger.warn if isWarning else self.logger.info
logger(msg)
def handle(self):
self.log(f"Client connected: {self.client_address}")
try:
while True:
data = self.request.recv(1024).strip()
if not data:
break
decoded_data = data.decode()
self.log(f"Command received from {self.client_address}: {decoded_data}")
self.parse_command(decoded_data)
finally:
self.log(f"Command connection closed with {self.client_address}")
def parse_command(self, data):
for command in self.handlers:
if data.startswith(command):
# Extract command arguments after the command itself
args = data[len(command):].strip().split()
self.dispatch_command(command, args)
return
self.send_response("ERROR: Unknown command\r\n")
def dispatch_command(self, command, data):
if command in self.handlers:
handler = self.handlers[command]
handler(data)
else:
self.send_response(f"Unknown command: {command}")
class DataSocket(socketserver.BaseRequestHandler):
#def __init__(self, request, client_address, server):
def __init__(self, request, client_address, server, modem=None, state_manager=None, event_manager=None, config_manager=None):
self.state_manager = state_manager
self.event_manager = event_manager
self.config_manager = config_manager
self.modem = modem
self.logger = structlog.get_logger(type(self).__name__)
super().__init__(request, client_address, server)
def log(self, message, isWarning = False):
msg = f"[{type(self).__name__}]: {message}"
logger = self.logger.warn if isWarning else self.logger.info
logger(msg)
def handle(self):
self.log(f"Data connection established with {self.client_address}")
try:
while True:
ready_to_read, _, _ = select.select([self.request], [], [], 1) # 1-second timeout
if ready_to_read:
self.data = self.request.recv(1024).strip()
if not self.data:
break
try:
self.log(f"Data received from {self.client_address}: [{len(self.data)}] - {self.data.decode()}")
except Exception:
self.log(f"Data received from {self.client_address}: [{len(self.data)}] - {self.data}")
for session in self.state_manager.p2p_connection_sessions:
print(f"sessions: {session}")
session.p2p_data_tx_queue.put(self.data)
# Check if there's something to send from the queue, without blocking
for session_id in self.state_manager.p2p_connection_sessions:
session = self.state_manager.get_p2p_connection_session(session_id)
if not session.p2p_data_tx_queue.empty():
data_to_send = session.p2p_data_tx_queue.get_nowait() # Use get_nowait to avoid blocking
self.request.sendall(data_to_send)
self.log(f"Sent data to {self.client_address}")
finally:
self.log(f"Data connection closed with {self.client_address}")
#class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
# allow_reuse_address = True
class CustomThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
allow_reuse_address = True
def __init__(self, server_address, RequestHandlerClass, bind_and_activate=True, **kwargs):
self.extra_args = kwargs
super().__init__(server_address, RequestHandlerClass, bind_and_activate=bind_and_activate)
def finish_request(self, request, client_address):
self.RequestHandlerClass(request, client_address, self, **self.extra_args)
class SocketInterfaceHandler:
def __init__(self, modem, config_manager, state_manager, event_manager):
self.modem = modem
self.config_manager = config_manager
self.config = self.config_manager.read()
self.state_manager = state_manager
self.event_manager = event_manager
self.logger = structlog.get_logger(type(self).__name__)
self.command_port = self.config["SOCKET_INTERFACE"]["cmd_port"]
self.data_port = self.config["SOCKET_INTERFACE"]["data_port"]
self.command_server = None
self.data_server = None
self.command_server_thread = None
self.data_server_thread = None
def log(self, message, isWarning = False):
msg = f"[{type(self).__name__}]: {message}"
logger = self.logger.warn if isWarning else self.logger.info
logger(msg)
def start_servers(self):
if self.command_port == 0:
self.command_port = 8300
if self.data_port == 0:
self.data_port = 8301
# Method to start both command and data server threads
self.command_server_thread = threading.Thread(target=self.run_server, args=(self.command_port, CommandSocket))
self.data_server_thread = threading.Thread(target=self.run_server, args=(self.data_port, DataSocket))
self.command_server_thread.start()
self.data_server_thread.start()
self.log(f"Interfaces started")
def run_server(self, port, handler):
with CustomThreadedTCPServer(('127.0.0.1', port), handler, modem=self.modem, state_manager=self.state_manager, event_manager=self.event_manager, config_manager=self.config_manager) as server:
self.log(f"Server started on port {port}")
if port == self.command_port:
self.command_server = server
else:
self.data_server = server
server.serve_forever()
def stop_servers(self):
# Gracefully shutdown the server
if self.command_server:
self.command_server.shutdown()
if self.data_server:
self.data_server.shutdown()
self.log(f"Interfaces stopped")
def wait_for_server_threads(self):
# Wait for both server threads to finish
self.command_server_thread.join()
self.data_server_thread.join()

View file

@ -0,0 +1,76 @@
""" WORK IN PROGRESS by DJ2LS"""
from command_p2p_connection import P2PConnectionCommand
class SocketCommandHandler:
def __init__(self, cmd_request, modem, config_manager, state_manager, event_manager):
self.cmd_request = cmd_request
self.modem = modem
self.config_manager = config_manager
self.state_manager = state_manager
self.event_manager = event_manager
self.session = None
def send_response(self, message):
full_message = f"{message}\r\n"
self.cmd_request.sendall(full_message.encode())
def handle_connect(self, data):
params = {
'origin': data[0],
'destination': data[1],
}
cmd = P2PConnectionCommand(self.config_manager.read(), self.state_manager, self.event_manager, params, self)
self.session = cmd.run(self.event_manager.queues, self.modem)
#if self.session.session_id:
# self.state_manager.register_p2p_connection_session(self.session)
# self.send_response("OK")
# self.session.connect()
#else:
# self.send_response("ERROR")
def handle_disconnect(self, data):
# Your existing connect logic
self.send_response("OK")
def handle_mycall(self, data):
# Logic for handling MYCALL command
self.send_response("OK")
def handle_bw(self, data):
# Logic for handling BW command
self.send_response("OK")
def handle_abort(self, data):
# Logic for handling ABORT command
self.send_response("OK")
def handle_public(self, data):
# Logic for handling PUBLIC command
self.send_response("OK")
def handle_cwid(self, data):
# Logic for handling CWID command
self.send_response("OK")
def handle_listen(self, data):
# Logic for handling LISTEN command
self.send_response("OK")
def handle_compression(self, data):
# Logic for handling COMPRESSION command
self.send_response("OK")
def handle_winlink_session(self, data):
# Logic for handling WINLINK SESSION command
self.send_response("OK")
def socket_respond_disconnected(self):
self.send_response("DISCONNECTED")
def socket_respond_connected(self, mycall, dxcall, bandwidth):
message = f"CONNECTED {mycall} {dxcall} {bandwidth}"
self.send_response(message)

View file

@ -23,6 +23,7 @@ class StateManager:
self.setARQ(False) self.setARQ(False)
self.is_beacon_running = False self.is_beacon_running = False
self.is_away_from_key = False
# If true, any wait() call is blocking # If true, any wait() call is blocking
self.transmitting_event = threading.Event() self.transmitting_event = threading.Event()
@ -38,6 +39,8 @@ class StateManager:
self.arq_iss_sessions = {} self.arq_iss_sessions = {}
self.arq_irs_sessions = {} self.arq_irs_sessions = {}
self.p2p_connection_sessions = {}
#self.mesh_routing_table = [] #self.mesh_routing_table = []
self.radio_frequency = 0 self.radio_frequency = 0
@ -82,6 +85,7 @@ class StateManager:
"type": msgtype, "type": msgtype,
"is_modem_running": self.is_modem_running, "is_modem_running": self.is_modem_running,
"is_beacon_running": self.is_beacon_running, "is_beacon_running": self.is_beacon_running,
"is_away_from_key": self.is_away_from_key,
"radio_status": self.radio_status, "radio_status": self.radio_status,
"radio_frequency": self.radio_frequency, "radio_frequency": self.radio_frequency,
"radio_mode": self.radio_mode, "radio_mode": self.radio_mode,
@ -182,7 +186,6 @@ class StateManager:
# if frequency not provided, add it here # if frequency not provided, add it here
if 'frequency' not in activity_data: if 'frequency' not in activity_data:
activity_data['frequency'] = self.radio_frequency activity_data['frequency'] = self.radio_frequency
self.activities_list[activity_id] = activity_data self.activities_list[activity_id] = activity_data
self.sendStateUpdate() self.sendStateUpdate()
@ -214,3 +217,15 @@ class StateManager:
"radio_rf_level": self.radio_rf_level, "radio_rf_level": self.radio_rf_level,
"s_meter_strength": self.s_meter_strength, "s_meter_strength": self.s_meter_strength,
} }
def register_p2p_connection_session(self, session):
if session.session_id in self.p2p_connection_sessions:
print("session already registered...")
return False
self.p2p_connection_sessions[session.session_id] = session
return True
def get_p2p_connection_session(self, id):
if id not in self.p2p_connection_sessions:
pass
return self.p2p_connection_sessions[id]

View file

@ -35,6 +35,10 @@ class TestModem:
samples += codec2.api.freedv_get_n_tx_modem_samples(c2instance) samples += codec2.api.freedv_get_n_tx_modem_samples(c2instance)
samples += codec2.api.freedv_get_n_tx_postamble_modem_samples(c2instance) samples += codec2.api.freedv_get_n_tx_postamble_modem_samples(c2instance)
time = samples / 8000 time = samples / 8000
#print(mode)
#if mode == codec2.FREEDV_MODE.signalling:
# time = 0.69
#print(time)
return time return time
def transmit(self, mode, repeats: int, repeat_delay: int, frames: bytearray) -> bool: def transmit(self, mode, repeats: int, repeat_delay: int, frames: bytearray) -> bool:
@ -82,7 +86,7 @@ class TestARQSession(unittest.TestCase):
cls.irs_modem) cls.irs_modem)
# Frame loss probability in % # Frame loss probability in %
cls.loss_probability = 30 cls.loss_probability = 0
cls.channels_running = True cls.channels_running = True
@ -91,12 +95,18 @@ class TestARQSession(unittest.TestCase):
# Transfer data between both parties # Transfer data between both parties
try: try:
transmission = modem_transmit_queue.get(timeout=1) transmission = modem_transmit_queue.get(timeout=1)
transmission["bytes"] += bytes(2) # simulate 2 bytes crc checksum
if random.randint(0, 100) < self.loss_probability: if random.randint(0, 100) < self.loss_probability:
self.logger.info(f"[{threading.current_thread().name}] Frame lost...") self.logger.info(f"[{threading.current_thread().name}] Frame lost...")
continue continue
frame_bytes = transmission['bytes'] frame_bytes = transmission['bytes']
frame_dispatcher.new_process_data(frame_bytes, None, len(frame_bytes), 0, 0)
if len(frame_bytes) == 5:
mode_name = "SIGNALLING_ACK"
else:
mode_name = None
frame_dispatcher.process_data(frame_bytes, None, len(frame_bytes), 5, 0, mode_name=mode_name)
except queue.Empty: except queue.Empty:
continue continue
self.logger.info(f"[{threading.current_thread().name}] Channel closed.") self.logger.info(f"[{threading.current_thread().name}] Channel closed.")
@ -129,7 +139,7 @@ class TestARQSession(unittest.TestCase):
self.waitForSession(self.irs_event_queue, False) self.waitForSession(self.irs_event_queue, False)
self.channels_running = False self.channels_running = False
def testARQSessionSmallPayload(self): def DisabledtestARQSessionSmallPayload(self):
# set Packet Error Rate (PER) / frame loss probability # set Packet Error Rate (PER) / frame loss probability
self.loss_probability = 30 self.loss_probability = 30
@ -160,7 +170,7 @@ class TestARQSession(unittest.TestCase):
self.waitAndCloseChannels() self.waitAndCloseChannels()
del cmd del cmd
def testARQSessionAbortTransmissionISS(self): def DisabledtestARQSessionAbortTransmissionISS(self):
# set Packet Error Rate (PER) / frame loss probability # set Packet Error Rate (PER) / frame loss probability
self.loss_probability = 0 self.loss_probability = 0
@ -172,14 +182,14 @@ class TestARQSession(unittest.TestCase):
cmd = ARQRawCommand(self.config, self.iss_state_manager, self.iss_event_queue, params) cmd = ARQRawCommand(self.config, self.iss_state_manager, self.iss_event_queue, params)
cmd.run(self.iss_event_queue, self.iss_modem) cmd.run(self.iss_event_queue, self.iss_modem)
threading.Event().wait(np.random.randint(1,10)) threading.Event().wait(np.random.randint(10,10))
for id in self.iss_state_manager.arq_iss_sessions: for id in self.iss_state_manager.arq_iss_sessions:
self.iss_state_manager.arq_iss_sessions[id].abort_transmission() self.iss_state_manager.arq_iss_sessions[id].abort_transmission()
self.waitAndCloseChannels() self.waitAndCloseChannels()
del cmd del cmd
def testARQSessionAbortTransmissionIRS(self): def DisabledtestARQSessionAbortTransmissionIRS(self):
# set Packet Error Rate (PER) / frame loss probability # set Packet Error Rate (PER) / frame loss probability
self.loss_probability = 0 self.loss_probability = 0
@ -198,7 +208,7 @@ class TestARQSession(unittest.TestCase):
self.waitAndCloseChannels() self.waitAndCloseChannels()
del cmd del cmd
def testSessionCleanupISS(self): def DisabledtestSessionCleanupISS(self):
params = { params = {
'dxcall': "AA1AAA-1", 'dxcall': "AA1AAA-1",
@ -217,11 +227,13 @@ class TestARQSession(unittest.TestCase):
break break
del cmd del cmd
def testSessionCleanupIRS(self): def DisabledtestSessionCleanupIRS(self):
session = arq_session_irs.ARQSessionIRS(self.config, session = arq_session_irs.ARQSessionIRS(self.config,
self.irs_modem, self.irs_modem,
'AA1AAA-1', 'AA1AAA-1',
random.randint(0, 255)) random.randint(0, 255),
self.irs_state_manager
)
self.irs_state_manager.register_arq_irs_session(session) self.irs_state_manager.register_arq_irs_session(session)
for session_id in self.irs_state_manager.arq_irs_sessions: for session_id in self.irs_state_manager.arq_irs_sessions:
session = self.irs_state_manager.arq_irs_sessions[session_id] session = self.irs_state_manager.arq_irs_sessions[session_id]

View file

@ -32,7 +32,7 @@ class TestDataFrameFactory(unittest.TestCase):
def testARQConnect(self): def testARQConnect(self):
dxcall = "DJ2LS-4" dxcall = "DJ2LS-4"
session_id = 123 session_id = 123
frame = self.factory.build_arq_session_open(dxcall, session_id) frame = self.factory.build_arq_session_open(dxcall, session_id, 1700)
frame_data = self.factory.deconstruct(frame) frame_data = self.factory.deconstruct(frame)
self.assertEqual(frame_data['origin'], self.factory.myfullcall) self.assertEqual(frame_data['origin'], self.factory.myfullcall)

View file

@ -0,0 +1,197 @@
import sys
import time
sys.path.append('modem')
import unittest
import unittest.mock
from config import CONFIG
import helpers
import queue
import threading
import base64
from command_p2p_connection import P2PConnectionCommand
from state_manager import StateManager
from frame_dispatcher import DISPATCHER
import random
import structlog
import numpy as np
from event_manager import EventManager
from state_manager import StateManager
from data_frame_factory import DataFrameFactory
import codec2
import p2p_connection
from socket_interface_commands import SocketCommandHandler
class TestModem:
def __init__(self, event_q, state_q):
self.data_queue_received = queue.Queue()
self.demodulator = unittest.mock.Mock()
self.event_manager = EventManager([event_q])
self.logger = structlog.get_logger('Modem')
self.states = StateManager(state_q)
def getFrameTransmissionTime(self, mode):
samples = 0
c2instance = codec2.open_instance(mode.value)
samples += codec2.api.freedv_get_n_tx_preamble_modem_samples(c2instance)
samples += codec2.api.freedv_get_n_tx_modem_samples(c2instance)
samples += codec2.api.freedv_get_n_tx_postamble_modem_samples(c2instance)
time = samples / 8000
return time
def transmit(self, mode, repeats: int, repeat_delay: int, frames: bytearray) -> bool:
# Simulate transmission time
tx_time = self.getFrameTransmissionTime(mode) + 0.1 # PTT
self.logger.info(f"TX {tx_time} seconds...")
threading.Event().wait(tx_time)
transmission = {
'mode': mode,
'bytes': frames,
}
self.data_queue_received.put(transmission)
class TestP2PConnectionSession(unittest.TestCase):
@classmethod
def setUpClass(cls):
config_manager = CONFIG('modem/config.ini.example')
cls.config = config_manager.read()
cls.logger = structlog.get_logger("TESTS")
cls.frame_factory = DataFrameFactory(cls.config)
# ISS
cls.iss_config_manager = config_manager
cls.iss_state_manager = StateManager(queue.Queue())
cls.iss_event_manager = EventManager([queue.Queue()])
cls.iss_event_queue = queue.Queue()
cls.iss_state_queue = queue.Queue()
cls.iss_p2p_data_queue = queue.Queue()
cls.iss_modem = TestModem(cls.iss_event_queue, cls.iss_state_queue)
cls.iss_frame_dispatcher = DISPATCHER(cls.config,
cls.iss_event_manager,
cls.iss_state_manager,
cls.iss_modem)
#cls.iss_socket_interface_handler = SocketInterfaceHandler(cls.iss_modem, cls.iss_config_manager, cls.iss_state_manager, cls.iss_event_manager)
#cls.iss_socket_command_handler = CommandSocket(TestSocket(), '127.0.0.1', 51234)
# IRS
cls.irs_state_manager = StateManager(queue.Queue())
cls.irs_event_manager = EventManager([queue.Queue()])
cls.irs_event_queue = queue.Queue()
cls.irs_state_queue = queue.Queue()
cls.irs_p2p_data_queue = queue.Queue()
cls.irs_modem = TestModem(cls.irs_event_queue, cls.irs_state_queue)
cls.irs_frame_dispatcher = DISPATCHER(cls.config,
cls.irs_event_manager,
cls.irs_state_manager,
cls.irs_modem)
# Frame loss probability in %
cls.loss_probability = 30
cls.channels_running = True
cls.disconnect_received = False
def channelWorker(self, modem_transmit_queue: queue.Queue, frame_dispatcher: DISPATCHER):
while self.channels_running:
# Transfer data between both parties
try:
transmission = modem_transmit_queue.get(timeout=1)
if random.randint(0, 100) < self.loss_probability:
self.logger.info(f"[{threading.current_thread().name}] Frame lost...")
continue
frame_bytes = transmission['bytes']
frame_dispatcher.new_process_data(frame_bytes, None, len(frame_bytes), 0, 0)
except queue.Empty:
continue
self.logger.info(f"[{threading.current_thread().name}] Channel closed.")
def waitForSession(self, q, outbound=False):
while True and self.channels_running:
ev = q.get()
print(ev)
if 'P2P_CONNECTION_DISCONNECT_ACK' in ev or self.disconnect_received:
self.logger.info(f"[{threading.current_thread().name}] session ended.")
break
def establishChannels(self):
self.channels_running = True
self.iss_to_irs_channel = threading.Thread(target=self.channelWorker,
args=[self.iss_modem.data_queue_received,
self.irs_frame_dispatcher],
name="ISS to IRS channel")
self.iss_to_irs_channel.start()
self.irs_to_iss_channel = threading.Thread(target=self.channelWorker,
args=[self.irs_modem.data_queue_received,
self.iss_frame_dispatcher],
name="IRS to ISS channel")
self.irs_to_iss_channel.start()
def waitAndCloseChannels(self):
self.waitForSession(self.iss_event_queue, True)
self.channels_running = False
self.waitForSession(self.irs_event_queue, False)
self.channels_running = False
def generate_random_string(self, min_length, max_length):
import string
length = random.randint(min_length, max_length)
return ''.join(random.choices(string.ascii_letters, k=length))#
def DisabledtestARQSessionSmallPayload(self):
# set Packet Error Rate (PER) / frame loss probability
self.loss_probability = 0
self.establishChannels()
handler = SocketCommandHandler(TestSocket(self), self.iss_modem, self.iss_config_manager, self.iss_state_manager, self.iss_event_manager)
handler.handle_connect(["AA1AAA-1", "BB2BBB-2"])
self.connected_event = threading.Event()
self.connected_event.wait()
for session_id in self.iss_state_manager.p2p_connection_sessions:
session = self.iss_state_manager.get_p2p_connection_session(session_id)
session.ENTIRE_CONNECTION_TIMEOUT = 15
# Generate and add 5 random entries to the queue
for _ in range(3):
min_length = (30 * _ ) + 1
max_length = (30 * _ ) + 1
print(min_length)
print(max_length)
random_entry = self.generate_random_string(min_length, max_length)
session.p2p_data_tx_queue.put(random_entry)
session.p2p_data_tx_queue.put('12345')
self.waitAndCloseChannels()
class TestSocket:
def __init__(self, test_class):
self.sent_data = [] # To capture data sent through this socket
self.test_class = test_class
def sendall(self, data):
print(f"Mock sendall called with data: {data}")
self.sent_data.append(data)
self.event_handler(data)
def event_handler(self, data):
if b'CONNECTED AA1AAA-1 BB2BBB-2 0\r\n' in self.sent_data:
self.test_class.connected_event.set()
if b'DISCONNECTED\r\n' in self.sent_data:
self.disconnect_received = True
self.test_class.assertEqual(b'DISCONNECTED\r\n', b'DISCONNECTED\r\n')
if __name__ == '__main__':
unittest.main()

View file

@ -0,0 +1,96 @@
"""
FreeDATA % python3.11 tools/custom_mode_tests/create_custom_ofdm_mod.py | ./modem/lib/codec2/build_osx/src/freedv_data_raw_rx --vv --framesperburst 1 DATAC1 - /dev/null
"""
import sys
sys.path.append('modem')
import numpy as np
modem_path = '/../../modem'
if modem_path not in sys.path:
sys.path.append(modem_path)
#import modem.codec2 as codec2
from codec2 import *
import threading
import modulator as modulator
import demodulator as demodulator
import config as config
def demod(txbuffer):
c2instance = open_instance(FREEDV_MODE.datac3.value)
# get bytes per frame
bytes_per_frame = int(
api.freedv_get_bits_per_modem_frame(c2instance) / 8
)
# create byte out buffer
bytes_out = ctypes.create_string_buffer(bytes_per_frame)
# set initial frames per burst
api.freedv_set_frames_per_burst(c2instance, 1)
# init audio buffer
audiobuffer = audio_buffer(len(txbuffer))
# get initial nin
nin = api.freedv_nin(c2instance)
audiobuffer.push(txbuffer)
threading.Event().wait(0.01)
while audiobuffer.nbuffer >= nin:
# demodulate audio
nbytes = api.freedv_rawdatarx(
freedv, bytes_out, audiobuffer.buffer.ctypes
)
# get current modem states and write to list
# 1 trial
# 2 sync
# 3 trial sync
# 6 decoded
# 10 error decoding == NACK
rx_status = api.freedv_get_rx_status(freedv)
print(rx_status)
# decrement codec traffic counter for making state smoother
audiobuffer.pop(nin)
nin = api.freedv_nin(freedv)
if nbytes == bytes_per_frame:
print("DECODED!!!!")
print("ENDED")
print(nin)
print(audiobuffer.nbuffer)
config = config.CONFIG('config.ini')
modulator = modulator.Modulator(config.read())
#freedv = open_instance(FREEDV_MODE.data_ofdm_2438.value)
#freedv = open_instance(FREEDV_MODE.datac14.value)
#freedv = open_instance(FREEDV_MODE.datac1.value)
freedv = open_instance(FREEDV_MODE.datac3.value)
#freedv = open_instance(FREEDV_MODE.data_ofdm_500.value)
#freedv = open_instance(FREEDV_MODE.qam16c2.value)
frames = 2
txbuffer = bytearray()
for frame in range(0,frames):
#txbuffer = modulator.transmit_add_silence(txbuffer, 1000)
txbuffer = modulator.transmit_add_preamble(txbuffer, freedv)
txbuffer = modulator.transmit_create_frame(txbuffer, freedv, b'123')
txbuffer = modulator.transmit_add_postamble(txbuffer, freedv)
txbuffer = modulator.transmit_add_silence(txbuffer, 1000)
#sys.stdout.buffer.flush()
#sys.stdout.buffer.write(txbuffer)
#sys.stdout.buffer.flush()
demod(txbuffer)