mirror of
https://github.com/DJ2LS/FreeDATA
synced 2024-05-14 08:04:33 +00:00
Merge branch 'refs/heads/dev-datac14' into dev-appimage-installer
This commit is contained in:
commit
848aae3b4a
74 changed files with 2622 additions and 974 deletions
2
.github/workflows/build_nsis_bundle.yml
vendored
2
.github/workflows/build_nsis_bundle.yml
vendored
|
@ -60,7 +60,7 @@ jobs:
|
|||
path: ./FreeDATA-Installer.exe
|
||||
|
||||
- name: Upload Installer to Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@v2
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
with:
|
||||
draft: true
|
||||
|
|
3
.github/workflows/build_server.yml
vendored
3
.github/workflows/build_server.yml
vendored
|
@ -48,7 +48,6 @@ jobs:
|
|||
brew install portaudio
|
||||
python -m pip install --upgrade pip
|
||||
pip3 install pyaudio
|
||||
export PYTHONPATH=/Library/Frameworks/Python.framework/Versions/3.11/lib/:$PYTHONPATH
|
||||
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
|
@ -114,7 +113,7 @@ jobs:
|
|||
path: ./modem/server.dist/${{ matrix.zip_name }}.zip
|
||||
|
||||
- name: Release Modem
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@v2
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
with:
|
||||
draft: true
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"name": "FreeDATA",
|
||||
"description": "FreeDATA Client application for connecting to FreeDATA server",
|
||||
"private": true,
|
||||
"version": "0.14.2-alpha",
|
||||
"version": "0.15.2-alpha",
|
||||
"main": "dist-electron/main/index.js",
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
|
@ -40,12 +40,10 @@
|
|||
"blob-util": "2.0.2",
|
||||
"bootstrap": "5.3.2",
|
||||
"bootstrap-icons": "1.11.3",
|
||||
"bootswatch": "5.3.2",
|
||||
"browser-image-compression": "2.0.2",
|
||||
"chart.js": "4.4.1",
|
||||
"chart.js": "4.4.2",
|
||||
"chartjs-plugin-annotation": "3.0.1",
|
||||
"electron-log": "5.1.1",
|
||||
"electron-updater": "6.1.7",
|
||||
"electron-log": "5.1.2",
|
||||
"emoji-picker-element": "1.21.0",
|
||||
"emoji-picker-element-data": "1.6.0",
|
||||
"file-saver": "2.0.5",
|
||||
|
@ -57,16 +55,16 @@
|
|||
"qth-locator": "2.1.0",
|
||||
"socket.io": "4.7.4",
|
||||
"uuid": "^9.0.1",
|
||||
"vue": "3.4.15",
|
||||
"vue": "3.4.21",
|
||||
"vue-chartjs": "5.3.0",
|
||||
"vuemoji-picker": "0.2.0"
|
||||
"vuemoji-picker": "0.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/nconf": "^0.10.6",
|
||||
"@typescript-eslint/eslint-plugin": "6.21.0",
|
||||
"@vitejs/plugin-vue": "5.0.4",
|
||||
"electron": "28.2.2",
|
||||
"electron-builder": "24.9.1",
|
||||
"electron": "28.2.6",
|
||||
"electron-builder": "24.13.3",
|
||||
"eslint": "8.56.0",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
"eslint-config-standard-with-typescript": "43.0.1",
|
||||
|
@ -74,13 +72,13 @@
|
|||
"eslint-plugin-n": "16.6.2",
|
||||
"eslint-plugin-prettier": "5.1.3",
|
||||
"eslint-plugin-promise": "6.1.1",
|
||||
"eslint-plugin-vue": "9.20.1",
|
||||
"eslint-plugin-vue": "9.22.0",
|
||||
"typescript": "5.3.3",
|
||||
"vite": "5.1.3",
|
||||
"vite-plugin-electron": "0.28.2",
|
||||
"vite-plugin-electron-renderer": "0.14.5",
|
||||
"vitest": "1.2.2",
|
||||
"vue": "3.4.15",
|
||||
"vitest": "1.4.0",
|
||||
"vue": "3.4.21",
|
||||
"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 |
|
@ -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 |
|
@ -105,6 +105,8 @@ Spectrum.prototype.drawSpectrum = function () {
|
|||
var linePositionHigh = 178.4; //150 + bandwidth/20
|
||||
var linePositionLow2 = 65; //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.moveTo(linePositionLow, 0);
|
||||
this.ctx_wf.lineTo(linePositionLow, height);
|
||||
|
@ -114,6 +116,10 @@ Spectrum.prototype.drawSpectrum = function () {
|
|||
this.ctx_wf.lineTo(linePositionLow2, height);
|
||||
this.ctx_wf.moveTo(linePositionHigh2, 0);
|
||||
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.strokeStyle = "#C3C3C3";
|
||||
this.ctx_wf.stroke();
|
||||
|
@ -454,7 +460,7 @@ export function Spectrum(id, options) {
|
|||
this.centerHz = options && options.centerHz ? options.centerHz : 1500;
|
||||
this.spanHz = options && options.spanHz ? options.spanHz : 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 =
|
||||
options && options.spectrumPercent ? options.spectrumPercent : 0;
|
||||
this.spectrumPercentStep =
|
||||
|
|
|
@ -32,8 +32,7 @@ import grid_freq from "./grid/grid_frequency.vue";
|
|||
import grid_beacon from "./grid/grid_beacon.vue";
|
||||
import grid_mycall_small from "./grid/grid_mycall small.vue";
|
||||
import grid_scatter from "./grid/grid_scatter.vue";
|
||||
import { stateDispatcher } from "../js/eventHandler";
|
||||
import { Scatter } from "vue-chartjs";
|
||||
import grid_stats_chart from "./grid/grid_stats_chart.vue";
|
||||
|
||||
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
|
||||
|
@ -63,7 +62,8 @@ class gridWidget {
|
|||
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 = [
|
||||
new gridWidget(
|
||||
grid_activities,
|
||||
|
@ -247,8 +247,16 @@ new gridWidget(
|
|||
"Stats",
|
||||
19,
|
||||
),
|
||||
|
||||
//New new widget ID should be 20
|
||||
new gridWidget(
|
||||
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>
|
||||
</div>
|
||||
</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">
|
||||
<h5 class="mb-1">18.106 MHz</h5>
|
||||
<small>EU / US</small>
|
||||
|
|
|
@ -21,6 +21,14 @@ function startStopBeacon() {
|
|||
}
|
||||
}
|
||||
var dxcallPing = ref("");
|
||||
window.addEventListener(
|
||||
"stationSelected",
|
||||
function (eventdata) {
|
||||
let evt = <CustomEvent>eventdata;
|
||||
dxcallPing.value = evt.detail;
|
||||
},
|
||||
false,
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<div class="card h-100">
|
||||
|
|
|
@ -15,12 +15,31 @@ function transmitPing() {
|
|||
|
||||
function startStopBeacon() {
|
||||
if (state.beacon_state === true) {
|
||||
setModemBeacon(false);
|
||||
setModemBeacon(false, state.away_from_key);
|
||||
} 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("");
|
||||
window.addEventListener(
|
||||
"stationSelected",
|
||||
function (eventdata) {
|
||||
let evt = <CustomEvent>eventdata;
|
||||
dxcallPing.value = evt.detail;
|
||||
},
|
||||
false,
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<div class="card h-100">
|
||||
|
@ -31,7 +50,7 @@ var dxcallPing = ref("");
|
|||
<div class="card-body overflow-auto p-0">
|
||||
<div class="container text-center">
|
||||
<div class="row mb-2 mt-2">
|
||||
<div class="col-sm-8">
|
||||
<div class="col">
|
||||
<div class="input-group w-100">
|
||||
<div class="form-floating">
|
||||
<input
|
||||
|
@ -57,25 +76,10 @@ var dxcallPing = ref("");
|
|||
title="Send a ping request to a remote station"
|
||||
@click="transmitPing()"
|
||||
>
|
||||
<strong>Ping</strong>
|
||||
<strong>PING Station</strong>
|
||||
</button>
|
||||
</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 class="row">
|
||||
|
@ -91,6 +95,41 @@ var dxcallPing = ref("");
|
|||
</button>
|
||||
</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>
|
||||
|
|
|
@ -34,6 +34,11 @@ function getMaidenheadDistance(dxGrid) {
|
|||
//
|
||||
}
|
||||
}
|
||||
function pushToPing(origin) {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("stationSelected", { bubbles: true, detail: origin }),
|
||||
);
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="card h-100">
|
||||
|
@ -57,11 +62,16 @@ function getMaidenheadDistance(dxGrid) {
|
|||
<th scope="col" id="thType">Type</th>
|
||||
<th scope="col" id="thSnr">SNR</th>
|
||||
<!--<th scope="col">Off</th>-->
|
||||
<th scope="col" id="thSnr">AFK?</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="gridHeardStations">
|
||||
<!--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>
|
||||
{{ getDateTime(item.timestamp) }}
|
||||
</td>
|
||||
|
@ -79,6 +89,9 @@ function getMaidenheadDistance(dxGrid) {
|
|||
<td>
|
||||
{{ item.snr }}
|
||||
</td>
|
||||
<td>
|
||||
{{ item.away_from_key }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
@ -34,6 +34,10 @@ function getMaidenheadDistance(dxGrid) {
|
|||
//
|
||||
}
|
||||
}
|
||||
function pushToPing(origin)
|
||||
{
|
||||
window.dispatchEvent(new CustomEvent("stationSelected", {bubbles:true, detail: origin }));
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="card h-100">
|
||||
|
@ -54,7 +58,7 @@ function getMaidenheadDistance(dxGrid) {
|
|||
</thead>
|
||||
<tbody id="miniHeardStations">
|
||||
<!--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>
|
||||
<span class="fs-6">{{ getDateTime(item.timestamp) }}</span>
|
||||
</td>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<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 } from "vue";
|
||||
import { ref, computed, onMounted, nextTick, toRaw } from "vue";
|
||||
import { initWaterfall, setColormap } from "../../js/waterfallHandler.js";
|
||||
import { setActivePinia } from "pinia";
|
||||
import pinia from "../../store/index";
|
||||
|
@ -89,7 +89,7 @@ const transmissionSpeedChartData = computed(() => ({
|
|||
{
|
||||
type: "line",
|
||||
label: "SNR[dB]",
|
||||
data: state.arq_speed_list_snr,
|
||||
data: state.arq_speed_list_snr.value,
|
||||
borderColor: "rgb(75, 192, 192, 1.0)",
|
||||
pointRadius: 1,
|
||||
segment: {
|
||||
|
@ -106,7 +106,7 @@ const transmissionSpeedChartData = computed(() => ({
|
|||
{
|
||||
type: "bar",
|
||||
label: "Speed[bpm]",
|
||||
data: state.arq_speed_list_bpm,
|
||||
data: state.arq_speed_list_bpm.value,
|
||||
borderColor: "rgb(120, 100, 120, 1.0)",
|
||||
backgroundColor: "rgba(120, 100, 120, 0.2)",
|
||||
order: 0,
|
||||
|
|
|
@ -12,6 +12,15 @@ function transmitPing() {
|
|||
sendModemPing(dxcallPing.value.toUpperCase());
|
||||
}
|
||||
var dxcallPing = ref("");
|
||||
|
||||
window.addEventListener(
|
||||
"stationSelected",
|
||||
function (eventdata) {
|
||||
let evt = <CustomEvent>eventdata;
|
||||
dxcallPing.value = evt.detail;
|
||||
},
|
||||
false,
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<div class="input-group" style="width: calc(100% - 24px)">
|
||||
|
|
105
gui/src/components/grid/grid_stats_chart.vue
Normal file
105
gui/src/components/grid/grid_stats_chart.vue
Normal 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>
|
|
@ -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>
|
|
@ -9,7 +9,6 @@ import settings_view from "./settings.vue";
|
|||
import main_footer_navbar from "./main_footer_navbar.vue";
|
||||
|
||||
import chat from "./chat.vue";
|
||||
import infoScreen from "./infoScreen.vue";
|
||||
import main_modem_healthcheck from "./main_modem_healthcheck.vue";
|
||||
import Dynamic_components from "./dynamic_components.vue";
|
||||
|
||||
|
@ -75,17 +74,6 @@ import { loadAllData } from "../js/eventHandler";
|
|||
><i class="bi bi-rocket h3"></i
|
||||
></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
|
||||
class="list-group-item list-group-item-dark list-group-item-action d-none border-0 rounded-3 mb-2"
|
||||
id="list-logger-list"
|
||||
|
@ -136,27 +124,6 @@ import { loadAllData } from "../js/eventHandler";
|
|||
<!-------------------------------- 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>
|
||||
|
||||
|
@ -295,14 +262,7 @@ import { loadAllData } from "../js/eventHandler";
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="tab-pane fade"
|
||||
id="list-info"
|
||||
role="tabpanel"
|
||||
aria-labelledby="list-info-list"
|
||||
>
|
||||
<infoScreen />
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="tab-pane fade show active"
|
||||
id="list-grid"
|
||||
|
|
|
@ -45,7 +45,7 @@ const serialStore = useSerialStore();
|
|||
@change="onChange"
|
||||
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="2">Hamlib NET rigctl</option>
|
||||
<option value="4">FLRig FLRig</option>
|
||||
|
|
|
@ -165,20 +165,20 @@ const audioStore = useAudioStore();
|
|||
</div>
|
||||
|
||||
<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">
|
||||
<div class="form-check form-switch form-check-inline">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
id="250HzModeSwitch"
|
||||
v-model="settings.remote.MODEM.enable_low_bandwidth_mode"
|
||||
@change="onChange"
|
||||
/>
|
||||
<label class="form-check-label" for="250HzModeSwitch">250Hz</label>
|
||||
</div>
|
||||
</label>
|
||||
<label class="input-group-text w-50">Maximum used bandwidth</label>
|
||||
<select
|
||||
class="form-select form-select-sm"
|
||||
id="maximum_bandwidth"
|
||||
@change="onChange"
|
||||
v-model.number="settings.remote.MODEM.maximum_bandwidth"
|
||||
>
|
||||
<option value="250">250 Hz</option>
|
||||
<option value="563">563 Hz</option>
|
||||
<option value="1700">1700 Hz</option>
|
||||
<option value="2438">2438 Hz</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
|
|
|
@ -92,8 +92,11 @@ export async function getSerialDevices() {
|
|||
return await apiGet("/devices/serial");
|
||||
}
|
||||
|
||||
export async function setModemBeacon(enabled = false) {
|
||||
return await apiPost("/modem/beacon", { enabled: enabled });
|
||||
export async function setModemBeacon(enabled = false, away_from_key = false) {
|
||||
return await apiPost("/modem/beacon", {
|
||||
enabled: enabled,
|
||||
away_from_key: away_from_key,
|
||||
});
|
||||
}
|
||||
|
||||
export async function sendModemCQ() {
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
setStateFailed,
|
||||
} from "./chatHandler";
|
||||
*/
|
||||
import { toRaw } from "vue";
|
||||
import { displayToast } from "./popupHandler";
|
||||
import { getFreedataMessages, getModemState, getAudioDevices } from "./api";
|
||||
import { processFreedataMessages } from "./messagesHandler.ts";
|
||||
|
@ -29,8 +30,11 @@ import {
|
|||
getRemote,
|
||||
} from "../store/settingsStore.js";
|
||||
|
||||
export function loadAllData() {
|
||||
getModemState();
|
||||
export async function loadAllData() {
|
||||
// TODO: Make this working
|
||||
let stateData = await getModemState();
|
||||
console.log(stateData);
|
||||
|
||||
getRemote();
|
||||
getOverallHealth();
|
||||
audioStore.loadAudioDevices();
|
||||
|
@ -66,7 +70,10 @@ export function stateDispatcher(data) {
|
|||
);
|
||||
|
||||
stateStore.channel_busy_slot = data["channel_busy_slot"];
|
||||
|
||||
stateStore.beacon_state = data["is_beacon_running"];
|
||||
stateStore.is_away_from_key = data["is_away_from_key"];
|
||||
|
||||
stateStore.radio_status = data["radio_status"];
|
||||
stateStore.frequency = data["radio_frequency"];
|
||||
stateStore.mode = data["radio_mode"];
|
||||
|
@ -178,12 +185,16 @@ export function eventDispatcher(data) {
|
|||
100;
|
||||
stateStore.arq_total_bytes =
|
||||
data["arq-transfer-outbound"].received_bytes;
|
||||
stateStore.arq_speed_list_timestamp =
|
||||
data["arq-transfer-outbound"].statistics.time_histogram;
|
||||
stateStore.arq_speed_list_bpm =
|
||||
data["arq-transfer-outbound"].statistics.bpm_histogram;
|
||||
stateStore.arq_speed_list_snr =
|
||||
data["arq-transfer-outbound"].statistics.snr_histogram;
|
||||
stateStore.arq_speed_list_timestamp.value = toRaw(
|
||||
data["arq-transfer-outbound"].statistics.time_histogram,
|
||||
);
|
||||
stateStore.arq_speed_list_bpm.value = toRaw(
|
||||
data["arq-transfer-outbound"].statistics.bpm_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;
|
||||
|
||||
case "ABORTING":
|
||||
|
@ -226,13 +237,12 @@ export function eventDispatcher(data) {
|
|||
stateStore.dxcallsign = data["arq-transfer-inbound"].dxcall;
|
||||
stateStore.arq_transmission_percent = 0;
|
||||
stateStore.arq_total_bytes = 0;
|
||||
stateStore.arq_speed_list_timestamp =
|
||||
data["arq-transfer-inbound"].statistics.time_histogram;
|
||||
stateStore.arq_speed_list_bpm =
|
||||
data["arq-transfer-inbound"].statistics.bpm_histogram;
|
||||
stateStore.arq_speed_list_snr =
|
||||
data["arq-transfer-inbound"].statistics.snr_histogram;
|
||||
|
||||
//stateStore.arq_speed_list_timestamp =
|
||||
// [];
|
||||
//stateStore.arq_speed_list_bpm =
|
||||
// [];
|
||||
//stateStore.arq_speed_list_snr =
|
||||
// [];
|
||||
return;
|
||||
|
||||
case "OPEN_ACK_SENT":
|
||||
|
@ -266,6 +276,19 @@ export function eventDispatcher(data) {
|
|||
100;
|
||||
stateStore.arq_total_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;
|
||||
|
||||
case "ENDED":
|
||||
|
|
|
@ -54,11 +54,11 @@ const defaultConfig = {
|
|||
enable_protocol: false,
|
||||
},
|
||||
MODEM: {
|
||||
enable_low_bandwidth_mode: false,
|
||||
respond_to_cq: false,
|
||||
tx_delay: 0,
|
||||
enable_hamc: false,
|
||||
enable_morse_identifier: false,
|
||||
maximum_bandwidth: 3000,
|
||||
},
|
||||
RADIO: {
|
||||
control: "disabled",
|
||||
|
|
|
@ -38,6 +38,7 @@ export const useStateStore = defineStore("stateStore", () => {
|
|||
var arq_session_state = ref("");
|
||||
var arq_state = ref("");
|
||||
var beacon_state = ref(false);
|
||||
var away_from_key = ref(false);
|
||||
|
||||
var audio_recording = ref(false);
|
||||
|
||||
|
@ -115,6 +116,7 @@ export const useStateStore = defineStore("stateStore", () => {
|
|||
activities,
|
||||
heard_stations,
|
||||
beacon_state,
|
||||
away_from_key,
|
||||
rigctld_started,
|
||||
rigctld_process,
|
||||
python_version,
|
||||
|
|
5
modem/.gitignore
vendored
5
modem/.gitignore
vendored
|
@ -129,4 +129,7 @@ dmypy.json
|
|||
.pyre/
|
||||
|
||||
# FreeDATA config
|
||||
config.ini
|
||||
config.ini
|
||||
|
||||
#FreeData DB
|
||||
freedata-messages.db
|
|
@ -3,6 +3,7 @@
|
|||
import structlog
|
||||
import lzma
|
||||
import gzip
|
||||
import zlib
|
||||
from message_p2p import message_received, message_failed, message_transmitted
|
||||
from enum import Enum
|
||||
|
||||
|
@ -10,7 +11,8 @@ class ARQ_SESSION_TYPES(Enum):
|
|||
raw = 0
|
||||
raw_lzma = 10
|
||||
raw_gzip = 11
|
||||
p2pmsg_lzma = 20
|
||||
p2pmsg_zlib = 20
|
||||
p2p_connection = 30
|
||||
|
||||
class ARQDataTypeHandler:
|
||||
def __init__(self, event_manager, state_manager):
|
||||
|
@ -37,11 +39,17 @@ class ARQDataTypeHandler:
|
|||
'failed': self.failed_raw_gzip,
|
||||
'transmitted': self.transmitted_raw_gzip,
|
||||
},
|
||||
ARQ_SESSION_TYPES.p2pmsg_lzma: {
|
||||
'prepare': self.prepare_p2pmsg_lzma,
|
||||
'handle': self.handle_p2pmsg_lzma,
|
||||
'failed' : self.failed_p2pmsg_lzma,
|
||||
'transmitted': self.transmitted_p2pmsg_lzma,
|
||||
ARQ_SESSION_TYPES.p2pmsg_zlib: {
|
||||
'prepare': self.prepare_p2pmsg_zlib,
|
||||
'handle': self.handle_p2pmsg_zlib,
|
||||
'failed' : self.failed_p2pmsg_zlib,
|
||||
'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)
|
||||
return decompressed_data
|
||||
|
||||
def prepare_p2pmsg_lzma(self, data):
|
||||
def prepare_p2pmsg_zlib(self, 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
|
||||
|
||||
def handle_p2pmsg_lzma(self, data, statistics):
|
||||
decompressed_data = lzma.decompress(data)
|
||||
self.log(f"Handling LZMA compressed P2PMSG data: {len(decompressed_data)} Bytes from {len(data)} Bytes")
|
||||
def handle_p2pmsg_zlib(self, data, statistics):
|
||||
decompressor = zlib.decompressobj(wbits=-zlib.MAX_WBITS)
|
||||
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)
|
||||
return decompressed_data
|
||||
|
||||
def failed_p2pmsg_lzma(self, data, statistics):
|
||||
decompressed_data = lzma.decompress(data)
|
||||
self.log(f"Handling failed LZMA compressed P2PMSG data: {len(decompressed_data)} Bytes from {len(data)} Bytes", isWarning=True)
|
||||
def failed_p2pmsg_zlib(self, data, statistics):
|
||||
decompressor = zlib.decompressobj(wbits=-zlib.MAX_WBITS)
|
||||
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)
|
||||
return decompressed_data
|
||||
|
||||
def transmitted_p2pmsg_lzma(self, data, statistics):
|
||||
decompressed_data = lzma.decompress(data)
|
||||
def transmitted_p2pmsg_zlib(self, data, statistics):
|
||||
# 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)
|
||||
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()
|
|
@ -1,5 +1,5 @@
|
|||
import datetime
|
||||
import queue, threading
|
||||
import threading
|
||||
import codec2
|
||||
import data_frame_factory
|
||||
import structlog
|
||||
|
@ -9,35 +9,52 @@ import time
|
|||
from arq_data_type_handler import ARQDataTypeHandler
|
||||
|
||||
|
||||
class ARQSession():
|
||||
class ARQSession:
|
||||
|
||||
SPEED_LEVEL_DICT = {
|
||||
0: {
|
||||
'mode': codec2.FREEDV_MODE.datac4,
|
||||
'min_snr': -10,
|
||||
'duration_per_frame': 5.17,
|
||||
'bandwidth': 250,
|
||||
},
|
||||
1: {
|
||||
'mode': codec2.FREEDV_MODE.datac3,
|
||||
'min_snr': 0,
|
||||
'duration_per_frame': 3.19,
|
||||
'bandwidth': 563,
|
||||
},
|
||||
2: {
|
||||
'mode': codec2.FREEDV_MODE.datac1,
|
||||
'min_snr': 3,
|
||||
'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.config = config
|
||||
|
||||
self.event_manager: EventManager = modem.event_manager
|
||||
self.states = modem.states
|
||||
|
||||
#self.states = modem.states
|
||||
self.states = state_manager
|
||||
self.states.setARQ(True)
|
||||
|
||||
self.protocol_version = 1
|
||||
|
||||
self.snr = []
|
||||
|
||||
self.dxcall = dxcall
|
||||
|
@ -63,7 +80,7 @@ class ARQSession():
|
|||
self.bpm_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}"
|
||||
logger = self.logger.warn if isWarning else self.logger.info
|
||||
logger(msg)
|
||||
|
@ -99,21 +116,22 @@ class ARQSession():
|
|||
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]
|
||||
received_data, type_byte = getattr(self, action_name)(frame)
|
||||
if isinstance(received_data, bytearray) and isinstance(type_byte, int):
|
||||
self.arq_data_type_handler.dispatch(type_byte, received_data, self.update_histograms(len(received_data), len(received_data)))
|
||||
return
|
||||
if self.state in self.STATE_TRANSITION and frame_type in self.STATE_TRANSITION[self.state]:
|
||||
action_name = self.STATE_TRANSITION[self.state][frame_type]
|
||||
received_data, type_byte = getattr(self, action_name)(frame)
|
||||
if isinstance(received_data, bytearray) and isinstance(type_byte, int):
|
||||
self.arq_data_type_handler.dispatch(type_byte, received_data, self.update_histograms(len(received_data), len(received_data)))
|
||||
return
|
||||
|
||||
self.log(f"Ignoring unknown transition from state {self.state.name} with frame {frame['frame_type']}")
|
||||
|
||||
def is_session_outdated(self):
|
||||
session_alivetime = time.time() - self.session_max_age
|
||||
if self.session_ended < session_alivetime and self.state.name in ['FAILED', 'ENDED', 'ABORTED']:
|
||||
return True
|
||||
return False
|
||||
return self.session_ended < session_alivetime and self.state.name in [
|
||||
'FAILED',
|
||||
'ENDED',
|
||||
'ABORTED',
|
||||
]
|
||||
|
||||
def calculate_session_duration(self):
|
||||
if self.session_ended == 0:
|
||||
|
@ -123,7 +141,7 @@ class ARQSession():
|
|||
|
||||
def calculate_session_statistics(self, confirmed_bytes, total_bytes):
|
||||
duration = self.calculate_session_duration()
|
||||
#total_bytes = self.total_length
|
||||
# total_bytes = self.total_length
|
||||
# self.total_length
|
||||
duration_in_minutes = duration / 60 # Convert duration from seconds to minutes
|
||||
|
||||
|
@ -134,9 +152,9 @@ class ARQSession():
|
|||
bytes_per_minute = 0
|
||||
|
||||
# Convert histograms lists to dictionaries
|
||||
time_histogram_dict = {i: timestamp for i, timestamp in enumerate(self.time_histogram)}
|
||||
snr_histogram_dict = {i: snr for i, snr in enumerate(self.snr_histogram)}
|
||||
bpm_histogram_dict = {i: bpm for i, bpm in enumerate(self.bpm_histogram)}
|
||||
time_histogram_dict = dict(enumerate(self.time_histogram))
|
||||
snr_histogram_dict = dict(enumerate(self.snr_histogram))
|
||||
bpm_histogram_dict = dict(enumerate(self.bpm_histogram))
|
||||
|
||||
return {
|
||||
'total_bytes': total_bytes,
|
||||
|
@ -148,17 +166,33 @@ class ARQSession():
|
|||
}
|
||||
|
||||
def update_histograms(self, confirmed_bytes, total_bytes):
|
||||
|
||||
stats = self.calculate_session_statistics(confirmed_bytes, total_bytes)
|
||||
self.snr_histogram.append(self.snr)
|
||||
self.bpm_histogram.append(stats['bytes_per_minute'])
|
||||
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
|
||||
|
||||
def get_appropriate_speed_level(self, snr):
|
||||
# Start with the lowest speed level as default
|
||||
# In case of a not fitting SNR, we return the lowest speed level
|
||||
def get_appropriate_speed_level(self, snr, maximum_bandwidth=None):
|
||||
if maximum_bandwidth is None:
|
||||
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())
|
||||
|
||||
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
|
||||
return appropriate_speed_level
|
||||
|
||||
return appropriate_speed_level
|
||||
|
|
|
@ -18,7 +18,7 @@ class IRS_State(Enum):
|
|||
class ARQSessionIRS(arq_session.ARQSession):
|
||||
|
||||
TIMEOUT_CONNECT = 55 #14.2
|
||||
TIMEOUT_DATA = 60
|
||||
TIMEOUT_DATA = 120
|
||||
|
||||
STATE_TRANSITION = {
|
||||
IRS_State.NEW: {
|
||||
|
@ -59,8 +59,8 @@ class ARQSessionIRS(arq_session.ARQSession):
|
|||
},
|
||||
}
|
||||
|
||||
def __init__(self, config: dict, modem, dxcall: str, session_id: int):
|
||||
super().__init__(config, modem, dxcall)
|
||||
def __init__(self, config: dict, modem, dxcall: str, session_id: int, state_manager):
|
||||
super().__init__(config, modem, dxcall, state_manager)
|
||||
|
||||
self.id = session_id
|
||||
self.dxcall = dxcall
|
||||
|
@ -76,14 +76,16 @@ class ARQSessionIRS(arq_session.ARQSession):
|
|||
self.received_bytes = 0
|
||||
self.received_crc = None
|
||||
|
||||
self.maximum_bandwidth = 0
|
||||
|
||||
self.abort = False
|
||||
|
||||
def all_data_received(self):
|
||||
print(f"{self.total_length} vs {self.received_bytes}")
|
||||
return self.total_length == self.received_bytes
|
||||
|
||||
def final_crc_matches(self) -> bool:
|
||||
match = self.total_crc == helpers.get_crc_32(bytes(self.received_data)).hex()
|
||||
return match
|
||||
return self.total_crc == helpers.get_crc_32(bytes(self.received_data)).hex()
|
||||
|
||||
def transmit_and_wait(self, frame, timeout, mode):
|
||||
self.event_frame_received.clear()
|
||||
|
@ -99,13 +101,26 @@ class ARQSessionIRS(arq_session.ARQSession):
|
|||
thread_wait.start()
|
||||
|
||||
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(
|
||||
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(
|
||||
self.id,
|
||||
self.dxcall,
|
||||
self.version,
|
||||
self.snr, flag_abort=self.abort)
|
||||
|
||||
self.launch_transmit_and_wait(ack_frame, self.TIMEOUT_CONNECT, mode=FREEDV_MODE.signalling)
|
||||
if not self.abort:
|
||||
self.set_state(IRS_State.OPEN_ACK_SENT)
|
||||
|
@ -133,12 +148,14 @@ class ARQSessionIRS(arq_session.ARQSession):
|
|||
return None, None
|
||||
|
||||
def process_incoming_data(self, frame):
|
||||
print(frame)
|
||||
if frame['offset'] != self.received_bytes:
|
||||
self.log(f"Discarding data offset {frame['offset']}")
|
||||
return False
|
||||
# TODO: IF WE HAVE AN OFFSET BECAUSE OF A SPEED LEVEL CHANGE FOR EXAMPLE,
|
||||
# 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
|
||||
|
||||
# Is this the last data part?
|
||||
if remaining_data_length <= len(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']
|
||||
|
||||
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.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))
|
||||
|
@ -164,41 +182,35 @@ class ARQSessionIRS(arq_session.ARQSession):
|
|||
self.calibrate_speed_settings(burst_frame=burst_frame)
|
||||
ack = self.frame_factory.build_arq_burst_ack(
|
||||
self.id,
|
||||
self.received_bytes,
|
||||
self.speed_level,
|
||||
self.frames_per_burst,
|
||||
self.snr,
|
||||
flag_abort=self.abort
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
if self.final_crc_matches():
|
||||
self.log("All data received successfully!")
|
||||
ack = self.frame_factory.build_arq_burst_ack(self.id,
|
||||
self.received_bytes,
|
||||
self.speed_level,
|
||||
self.frames_per_burst,
|
||||
self.snr,
|
||||
flag_final=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.session_ended = time.time()
|
||||
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
|
||||
else:
|
||||
|
||||
ack = self.frame_factory.build_arq_burst_ack(self.id,
|
||||
self.received_bytes,
|
||||
self.speed_level,
|
||||
self.frames_per_burst,
|
||||
self.snr,
|
||||
flag_final=True,
|
||||
flag_checksum=False)
|
||||
self.transmit_frame(ack, mode=FREEDV_MODE.signalling)
|
||||
|
@ -212,7 +224,7 @@ class ARQSessionIRS(arq_session.ARQSession):
|
|||
received_speed_level = 0
|
||||
|
||||
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 = {}
|
||||
|
||||
# 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
|
||||
|
||||
def abort_transmission(self):
|
||||
self.log(f"Aborting transmission... setting abort flag")
|
||||
self.log("Aborting transmission... setting abort flag")
|
||||
self.abort = True
|
||||
|
||||
def send_stop_ack(self, stop_frame):
|
||||
|
@ -263,7 +275,7 @@ class ARQSessionIRS(arq_session.ARQSession):
|
|||
# final function for failed transmissions
|
||||
self.session_ended = time.time()
|
||||
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.states.setARQ(False)
|
||||
return None, None
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import threading
|
||||
import data_frame_factory
|
||||
import queue
|
||||
import random
|
||||
from codec2 import FREEDV_MODE
|
||||
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.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_TRANSFER = 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):
|
||||
super().__init__(config, modem, dxcall)
|
||||
super().__init__(config, modem, dxcall, state_manager)
|
||||
self.state_manager = state_manager
|
||||
self.data = data
|
||||
self.total_length = len(data)
|
||||
self.data_crc = ''
|
||||
self.type_byte = type_byte
|
||||
self.confirmed_bytes = 0
|
||||
self.expected_byte_offset = 0
|
||||
|
||||
self.state = ISS_State.NEW
|
||||
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:
|
||||
self.log("SENDING IN FALLBACK SPEED LEVEL", isWarning=True)
|
||||
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
|
||||
|
||||
self.set_state(ISS_State.FAILED)
|
||||
|
@ -105,9 +107,11 @@ class ARQSessionISS(arq_session.ARQSession):
|
|||
twr.start()
|
||||
|
||||
def start(self):
|
||||
maximum_bandwidth = self.config['MODEM']['maximum_bandwidth']
|
||||
print(maximum_bandwidth)
|
||||
self.event_manager.send_arq_session_new(
|
||||
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.set_state(ISS_State.OPEN_SENT)
|
||||
|
||||
|
@ -136,8 +140,7 @@ class ARQSessionISS(arq_session.ARQSession):
|
|||
def send_info(self, irs_frame):
|
||||
# check if we received an abort flag
|
||||
if irs_frame["flag"]["ABORT"]:
|
||||
self.transmission_aborted(irs_frame)
|
||||
return
|
||||
return self.transmission_aborted(irs_frame)
|
||||
|
||||
info_frame = self.frame_factory.build_arq_session_info(self.id, self.total_length,
|
||||
helpers.get_crc_32(self.data),
|
||||
|
@ -148,16 +151,26 @@ class ARQSessionISS(arq_session.ARQSession):
|
|||
|
||||
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
|
||||
self.update_histograms(self.confirmed_bytes, self.total_length)
|
||||
|
||||
self.update_speed_level(irs_frame)
|
||||
if 'offset' in irs_frame:
|
||||
self.confirmed_bytes = irs_frame['offset']
|
||||
self.log(f"IRS confirmed {self.confirmed_bytes}/{self.total_length} bytes")
|
||||
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))
|
||||
|
||||
|
||||
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.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))
|
||||
|
||||
# check if we received an abort flag
|
||||
if irs_frame["flag"]["ABORT"]:
|
||||
|
@ -174,12 +187,16 @@ class ARQSessionISS(arq_session.ARQSession):
|
|||
|
||||
payload_size = self.get_data_payload_size()
|
||||
burst = []
|
||||
for f in range(0, self.frames_per_burst):
|
||||
for _ in range(0, self.frames_per_burst):
|
||||
offset = self.confirmed_bytes
|
||||
#self.expected_byte_offset = offset
|
||||
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(
|
||||
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)
|
||||
self.launch_twr(burst, self.TIMEOUT_TRANSFER, self.RETRIES_CONNECT, mode='auto', isARQBurst=True)
|
||||
self.set_state(ISS_State.BURST_SENT)
|
||||
|
@ -191,6 +208,10 @@ class ARQSessionISS(arq_session.ARQSession):
|
|||
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.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.state_manager.remove_arq_iss_session(self.id)
|
||||
self.states.setARQ(False)
|
||||
|
@ -200,7 +221,7 @@ class ARQSessionISS(arq_session.ARQSession):
|
|||
# final function for failed transmissions
|
||||
self.session_ended = time.time()
|
||||
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.states.setARQ(False)
|
||||
|
||||
|
@ -209,7 +230,7 @@ class ARQSessionISS(arq_session.ARQSession):
|
|||
|
||||
def abort_transmission(self, irs_frame=None):
|
||||
# function for starting the abort sequence
|
||||
self.log(f"aborting transmission...")
|
||||
self.log("aborting transmission...")
|
||||
self.set_state(ISS_State.ABORTING)
|
||||
|
||||
self.event_manager.send_arq_session_finished(
|
||||
|
@ -218,9 +239,6 @@ class ARQSessionISS(arq_session.ARQSession):
|
|||
# break actual retries
|
||||
self.event_frame_received.set()
|
||||
|
||||
# start with abort sequence
|
||||
self.send_stop()
|
||||
|
||||
def send_stop(self):
|
||||
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)
|
||||
|
|
|
@ -1,16 +1,12 @@
|
|||
"""
|
||||
Gather information about audio devices.
|
||||
"""
|
||||
import atexit
|
||||
import multiprocessing
|
||||
import crcengine
|
||||
import sounddevice as sd
|
||||
import structlog
|
||||
import numpy as np
|
||||
import queue
|
||||
import threading
|
||||
|
||||
atexit.register(sd._terminate)
|
||||
|
||||
log = structlog.get_logger("audio")
|
||||
|
||||
|
@ -214,6 +210,36 @@ def set_audio_volume(datalist: np.ndarray, dB: float) -> np.ndarray:
|
|||
RMS_COUNTER = 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:
|
||||
"""
|
||||
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
|
||||
|
||||
try:
|
||||
data = prepare_data_for_fft(data, target_length_samples=800)
|
||||
fftarray = np.fft.rfft(data)
|
||||
|
||||
# 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
|
||||
if fft_queue.qsize() >= 1:
|
||||
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:
|
||||
print(f"[MDM] calculate_fft: Exception: {err}")
|
||||
|
|
366
modem/codec2.py
366
modem/codec2.py
|
@ -7,6 +7,8 @@ Python interface to the C-language codec2 library.
|
|||
# pylint: disable=import-outside-toplevel, attribute-defined-outside-init
|
||||
|
||||
import ctypes
|
||||
from ctypes import *
|
||||
import hashlib
|
||||
import glob
|
||||
import os
|
||||
import sys
|
||||
|
@ -25,12 +27,17 @@ class FREEDV_MODE(Enum):
|
|||
Enumeration for codec2 modes and names
|
||||
"""
|
||||
signalling = 19
|
||||
signalling_ack = 20
|
||||
datac0 = 14
|
||||
datac1 = 10
|
||||
datac3 = 12
|
||||
datac4 = 18
|
||||
datac13 = 19
|
||||
|
||||
datac14 = 20
|
||||
data_ofdm_500 = 21500
|
||||
data_ofdm_2438 = 2124381
|
||||
#data_qam_2438 = 2124382
|
||||
qam16c2 = 22
|
||||
|
||||
class FREEDV_MODE_USED_SLOTS(Enum):
|
||||
"""
|
||||
|
@ -43,9 +50,11 @@ class FREEDV_MODE_USED_SLOTS(Enum):
|
|||
datac3 = [False, False, True, False, False]
|
||||
datac4 = [False, False, True, False, False]
|
||||
datac13 = [False, False, True, False, False]
|
||||
fsk_ldpc = [False, False, True, False, False]
|
||||
fsk_ldpc_0 = [False, False, True, False, False]
|
||||
fsk_ldpc_1 = [False, False, True, False, False]
|
||||
datac14 = [False, False, True, False, False]
|
||||
data_ofdm_500 = [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
|
||||
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)
|
||||
else:
|
||||
files = []
|
||||
|
||||
api = None
|
||||
for file in files:
|
||||
try:
|
||||
|
@ -105,7 +113,7 @@ for file in files:
|
|||
if api is None or "api" not in locals():
|
||||
log.critical("[C2 ] Error: Libcodec2 not loaded - Exiting")
|
||||
sys.exit(1)
|
||||
log.info("[C2 ] Libcodec2 loaded...")
|
||||
#log.info("[C2 ] Libcodec2 loaded...", path=file)
|
||||
# ctypes function init
|
||||
|
||||
# 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
|
||||
|
||||
# -------------------------------- 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_NC_MAX = 50 + 1 * 2
|
||||
|
@ -236,6 +186,8 @@ MODEM_STATS_MAX_F_HZ = 4000
|
|||
MODEM_STATS_MAX_F_EST = 4
|
||||
|
||||
|
||||
|
||||
|
||||
class MODEMSTATS(ctypes.Structure):
|
||||
"""Modem statistics structure"""
|
||||
|
||||
|
@ -421,33 +373,21 @@ class resampler:
|
|||
return out48
|
||||
|
||||
def open_instance(mode: int) -> ctypes.c_void_p:
|
||||
"""
|
||||
Return a codec2 instance of the type `mode`
|
||||
data_custom = 21
|
||||
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]:
|
||||
custom_params = ofdm_configurations[mode]
|
||||
return ctypes.cast(
|
||||
api.freedv_open_advanced(
|
||||
data_custom,
|
||||
ctypes.byref(custom_params),
|
||||
),
|
||||
ctypes.c_void_p,
|
||||
)
|
||||
else:
|
||||
if mode not in [data_custom]:
|
||||
return ctypes.cast(api.freedv_open(mode), ctypes.c_void_p)
|
||||
|
||||
:param mode: Type of codec2 instance to return
|
||||
:type mode: Union[int, str]
|
||||
:return: C-function of the requested codec2 instance
|
||||
:rtype: ctypes.c_void_p
|
||||
"""
|
||||
# if mode in [FREEDV_MODE.fsk_ldpc_0.value]:
|
||||
# return ctypes.cast(
|
||||
# api.freedv_open_advanced(
|
||||
# FREEDV_MODE.fsk_ldpc.value,
|
||||
# ctypes.byref(api.FREEDV_MODE_FSK_LDPC_0_ADV),
|
||||
# ),
|
||||
# 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)
|
||||
|
||||
def get_bytes_per_frame(mode: int) -> int:
|
||||
"""
|
||||
|
@ -462,3 +402,249 @@ def get_bytes_per_frame(mode: int) -> int:
|
|||
# TODO add close session
|
||||
# get number of bytes per frame for mode
|
||||
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
|
||||
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ from arq_data_type_handler import ARQDataTypeHandler
|
|||
|
||||
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.logger = structlog.get_logger(type(self).__name__)
|
||||
self.state_manager = state_manager
|
||||
|
@ -16,6 +16,7 @@ class TxCommand():
|
|||
self.set_params_from_api(apiParams)
|
||||
self.frame_factory = DataFrameFactory(config)
|
||||
self.arq_data_type_handler = ARQDataTypeHandler(event_manager, state_manager)
|
||||
self.socket_command_handler = socket_command_handler
|
||||
|
||||
def log(self, message, isWarning = False):
|
||||
msg = f"[{type(self).__name__}]: {message}"
|
||||
|
@ -60,5 +61,4 @@ class TxCommand():
|
|||
def test(self, event_queue: queue.Queue):
|
||||
self.emit_event(event_queue)
|
||||
self.logger.info(self.log_message())
|
||||
frame = self.build_frame()
|
||||
return frame
|
||||
return self.build_frame()
|
||||
|
|
|
@ -3,7 +3,8 @@ from command import TxCommand
|
|||
class BeaconCommand(TxCommand):
|
||||
|
||||
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):
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from command import TxCommand
|
||||
|
||||
from codec2 import FREEDV_MODE
|
||||
class CQCommand(TxCommand):
|
||||
|
||||
def build_frame(self):
|
||||
|
|
|
@ -41,7 +41,7 @@ class SendMessageCommand(TxCommand):
|
|||
# Convert JSON string to bytes (using UTF-8 encoding)
|
||||
payload = message.to_payload().encode('utf-8')
|
||||
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,
|
||||
modem,
|
||||
|
|
37
modem/command_p2p_connection.py
Normal file
37
modem/command_p2p_connection.py
Normal 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
|
|
@ -15,7 +15,6 @@ input_device = 5a1c
|
|||
output_device = bd6c
|
||||
rx_audio_level = 0
|
||||
tx_audio_level = 0
|
||||
enable_auto_tune = False
|
||||
|
||||
[RIGCTLD]
|
||||
ip = 127.0.0.1
|
||||
|
@ -46,10 +45,17 @@ enable_protocol = False
|
|||
|
||||
[MODEM]
|
||||
enable_hmac = False
|
||||
enable_low_bandwidth_mode = False
|
||||
enable_morse_identifier = False
|
||||
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]
|
||||
enable_auto_repeat = False
|
||||
|
|
|
@ -26,7 +26,6 @@ class CONFIG:
|
|||
'output_device': str,
|
||||
'rx_audio_level': int,
|
||||
'tx_audio_level': int,
|
||||
'enable_auto_tune': bool,
|
||||
},
|
||||
'RADIO': {
|
||||
'control': str,
|
||||
|
@ -58,9 +57,17 @@ class CONFIG:
|
|||
'MODEM': {
|
||||
'enable_hmac': bool,
|
||||
'enable_morse_identifier': bool,
|
||||
'enable_low_bandwidth_mode': bool,
|
||||
'maximum_bandwidth': int,
|
||||
'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': {
|
||||
'enable_auto_repeat': bool,
|
||||
|
@ -87,7 +94,7 @@ class CONFIG:
|
|||
except Exception:
|
||||
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
|
||||
self.config_exists()
|
||||
|
|
|
@ -6,6 +6,7 @@ class DataFrameFactory:
|
|||
|
||||
LENGTH_SIG0_FRAME = 14
|
||||
LENGTH_SIG1_FRAME = 14
|
||||
LENGTH_ACK_FRAME = 3
|
||||
|
||||
"""
|
||||
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
|
||||
}
|
||||
|
||||
BEACON_FLAGS = {
|
||||
'AWAY_FROM_KEY': 0, # Bit-position for indicating the AWAY FROM KEY state
|
||||
}
|
||||
|
||||
def __init__(self, config):
|
||||
|
||||
self.myfullcall = f"{config['STATION']['mycall']}-{config['STATION']['myssid']}"
|
||||
self.mygrid = config['STATION']['mygrid']
|
||||
|
||||
|
@ -26,8 +32,8 @@ class DataFrameFactory:
|
|||
|
||||
self._load_broadcast_templates()
|
||||
self._load_ping_templates()
|
||||
self._load_fec_templates()
|
||||
self._load_arq_templates()
|
||||
self._load_p2p_connection_templates()
|
||||
|
||||
def _load_broadcast_templates(self):
|
||||
# cq frame
|
||||
|
@ -49,7 +55,8 @@ class DataFrameFactory:
|
|||
self.template_list[FR_TYPE.BEACON.value] = {
|
||||
"frame_length": self.LENGTH_SIG0_FRAME,
|
||||
"origin": 6,
|
||||
"gridsquare": 4
|
||||
"gridsquare": 4,
|
||||
"flag": 1
|
||||
}
|
||||
|
||||
def _load_ping_templates(self):
|
||||
|
@ -70,26 +77,6 @@ class DataFrameFactory:
|
|||
"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):
|
||||
|
||||
|
@ -98,6 +85,8 @@ class DataFrameFactory:
|
|||
"destination_crc": 3,
|
||||
"origin": 6,
|
||||
"session_id": 1,
|
||||
"maximum_bandwidth": 2,
|
||||
"protocol_version" : 1
|
||||
}
|
||||
|
||||
self.template_list[FR_TYPE.ARQ_SESSION_OPEN_ACK.value] = {
|
||||
|
@ -151,14 +140,71 @@ class DataFrameFactory:
|
|||
|
||||
# arq burst ack
|
||||
self.template_list[FR_TYPE.ARQ_BURST_ACK.value] = {
|
||||
"frame_length": self.LENGTH_SIG1_FRAME,
|
||||
"frame_length": self.LENGTH_ACK_FRAME,
|
||||
"session_id": 1,
|
||||
"offset":4,
|
||||
#"offset":4,
|
||||
"speed_level": 1,
|
||||
"frames_per_burst": 1,
|
||||
"snr": 1,
|
||||
#"frames_per_burst": 1,
|
||||
#"snr": 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):
|
||||
|
@ -168,10 +214,13 @@ class DataFrameFactory:
|
|||
frame_length = frame_template["frame_length"]
|
||||
else:
|
||||
frame_length -= 2
|
||||
frame = bytearray(frame_length)
|
||||
frame[:1] = bytes([frametype.value])
|
||||
|
||||
buffer_position = 1
|
||||
frame = bytearray(frame_length)
|
||||
if frametype in [FR_TYPE.ARQ_BURST_ACK]:
|
||||
buffer_position = 0
|
||||
else:
|
||||
frame[:1] = bytes([frametype.value])
|
||||
buffer_position = 1
|
||||
for key, item_length in frame_template.items():
|
||||
if key == "frame_length":
|
||||
continue
|
||||
|
@ -182,17 +231,22 @@ class DataFrameFactory:
|
|||
raise OverflowError("Frame data overflow!")
|
||||
frame[buffer_position: buffer_position + item_length] = content[key]
|
||||
buffer_position += item_length
|
||||
|
||||
return frame
|
||||
|
||||
def deconstruct(self, frame):
|
||||
buffer_position = 1
|
||||
# Extract frametype and get the corresponding template
|
||||
frametype = int.from_bytes(frame[:1], "big")
|
||||
frame_template = self.template_list.get(frametype)
|
||||
def deconstruct(self, frame, mode_name=None):
|
||||
|
||||
if not frame_template:
|
||||
# Handle the case where the frame type is not recognized
|
||||
raise ValueError(f"Unknown frame type: {frametype}")
|
||||
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
|
||||
frametype = int.from_bytes(frame[:1], "big")
|
||||
frame_template = self.template_list.get(frametype)
|
||||
|
||||
extracted_data = {"frame_type": FR_TYPE(frametype).name, "frame_type_int": frametype}
|
||||
|
||||
|
@ -202,6 +256,7 @@ class DataFrameFactory:
|
|||
|
||||
# data is always on the last payload slots
|
||||
if item_length in ["dynamic"] and key in["data"]:
|
||||
print(len(frame))
|
||||
data = frame[buffer_position:-2]
|
||||
item_length = len(data)
|
||||
else:
|
||||
|
@ -219,7 +274,7 @@ class DataFrameFactory:
|
|||
|
||||
elif key in ["session_id", "speed_level",
|
||||
"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')
|
||||
|
||||
elif key in ["snr"]:
|
||||
|
@ -229,7 +284,6 @@ class DataFrameFactory:
|
|||
|
||||
data = int.from_bytes(data, "big")
|
||||
extracted_data[key] = {}
|
||||
|
||||
# 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]:
|
||||
flag_dict = self.ARQ_FLAGS
|
||||
|
@ -237,6 +291,15 @@ class DataFrameFactory:
|
|||
# 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)
|
||||
|
||||
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:
|
||||
extracted_data[key] = data
|
||||
|
||||
|
@ -290,10 +353,16 @@ class DataFrameFactory:
|
|||
}
|
||||
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 = {
|
||||
"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)
|
||||
|
||||
|
@ -328,11 +397,13 @@ class DataFrameFactory:
|
|||
test_frame[:1] = bytes([FR_TYPE.TEST_FRAME.value])
|
||||
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 = {
|
||||
"destination_crc": helpers.get_crc_24(destination),
|
||||
"origin": helpers.callsign_to_bytes(self.myfullcall),
|
||||
"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)
|
||||
|
||||
|
@ -402,11 +473,11 @@ class DataFrameFactory:
|
|||
"offset": offset.to_bytes(4, 'big'),
|
||||
"data": data,
|
||||
}
|
||||
frame = self.construct(FR_TYPE.ARQ_BURST_FRAME, payload, self.get_bytes_per_frame(freedv_mode))
|
||||
return frame
|
||||
return self.construct(
|
||||
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,
|
||||
frames_per_burst: int, snr: int, flag_final=False, flag_checksum=False, flag_abort=False):
|
||||
def build_arq_burst_ack(self, session_id: bytes, speed_level: int, flag_final=False, flag_checksum=False, flag_abort=False):
|
||||
flag = 0b00000000
|
||||
if flag_final:
|
||||
flag = helpers.set_flag(flag, 'FINAL', True, self.ARQ_FLAGS)
|
||||
|
@ -419,10 +490,66 @@ class DataFrameFactory:
|
|||
|
||||
payload = {
|
||||
"session_id": session_id.to_bytes(1, 'big'),
|
||||
"offset": offset.to_bytes(4, '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'),
|
||||
}
|
||||
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)
|
||||
|
|
|
@ -4,10 +4,7 @@ import ctypes
|
|||
import structlog
|
||||
import threading
|
||||
import audio
|
||||
import os
|
||||
from modem_frametypes import FRAME_TYPE
|
||||
import itertools
|
||||
from time import sleep
|
||||
|
||||
TESTMODE = False
|
||||
|
||||
|
@ -16,23 +13,22 @@ class Demodulator():
|
|||
MODE_DICT = {}
|
||||
# Iterate over the FREEDV_MODE enum members
|
||||
for mode in codec2.FREEDV_MODE:
|
||||
MODE_DICT[mode.value] = {
|
||||
'decode': False,
|
||||
'bytes_per_frame': None,
|
||||
'bytes_out': None,
|
||||
'audio_buffer': None,
|
||||
'nin': None,
|
||||
'instance': None,
|
||||
'state_buffer': [],
|
||||
'name': mode.name.upper(),
|
||||
'decoding_thread': None
|
||||
}
|
||||
MODE_DICT[mode.value] = {
|
||||
'decode': False,
|
||||
'bytes_per_frame': None,
|
||||
'bytes_out': None,
|
||||
'audio_buffer': None,
|
||||
'nin': None,
|
||||
'instance': None,
|
||||
'state_buffer': [],
|
||||
'name': mode.name.upper(),
|
||||
'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.rx_audio_level = config['AUDIO']['rx_audio_level']
|
||||
|
||||
self.service_queue = service_queue
|
||||
self.AUDIO_FRAMES_PER_BUFFER_RX = 4800
|
||||
self.buffer_overflow_counter = [0, 0, 0, 0, 0, 0, 0, 0]
|
||||
self.is_codec2_traffic_counter = 0
|
||||
|
@ -53,6 +49,10 @@ class Demodulator():
|
|||
|
||||
# enable decoding of signalling modes
|
||||
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(
|
||||
target=self.tci_rx_callback,
|
||||
|
@ -73,15 +73,13 @@ class Demodulator():
|
|||
"""
|
||||
|
||||
# create codec2 instance
|
||||
c2instance = ctypes.cast(
|
||||
codec2.api.freedv_open(mode), ctypes.c_void_p
|
||||
)
|
||||
#c2instance = ctypes.cast(
|
||||
c2instance = codec2.open_instance(mode)
|
||||
|
||||
# get bytes per frame
|
||||
bytes_per_frame = int(
|
||||
codec2.api.freedv_get_bits_per_modem_frame(c2instance) / 8
|
||||
)
|
||||
|
||||
# create byte out buffer
|
||||
bytes_out = ctypes.create_string_buffer(bytes_per_frame)
|
||||
|
||||
|
@ -126,35 +124,6 @@ class Demodulator():
|
|||
)
|
||||
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:
|
||||
"""
|
||||
|
@ -219,7 +188,7 @@ class Demodulator():
|
|||
nin = codec2.api.freedv_nin(freedv)
|
||||
if nbytes == bytes_per_frame:
|
||||
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)
|
||||
self.get_scatter(freedv)
|
||||
|
@ -230,7 +199,9 @@ class Demodulator():
|
|||
'bytes_per_frame': bytes_per_frame,
|
||||
'snr': snr,
|
||||
'frequency_offset': self.get_frequency_offset(freedv),
|
||||
'mode_name': mode_name
|
||||
}
|
||||
|
||||
self.data_queue_received.put(item)
|
||||
|
||||
|
||||
|
@ -370,18 +341,20 @@ class Demodulator():
|
|||
for mode in self.MODE_DICT:
|
||||
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
|
||||
for m in self.MODE_DICT:
|
||||
self.MODE_DICT[m]["decode"] = False
|
||||
|
||||
# signalling is always 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
|
||||
self.MODE_DICT[codec2.FREEDV_MODE.datac4.value]["decode"] = True
|
||||
|
||||
# Enable specified modes
|
||||
for mode, decode in modes_to_decode.items():
|
||||
if mode in self.MODE_DICT:
|
||||
self.MODE_DICT[mode]["decode"] = decode
|
||||
if modes_to_decode:
|
||||
for mode, decode in modes_to_decode.items():
|
||||
if mode in self.MODE_DICT:
|
||||
self.MODE_DICT[mode]["decode"] = decode
|
||||
|
|
|
@ -12,6 +12,8 @@ class EventManager:
|
|||
def broadcast(self, data):
|
||||
for q in self.queues:
|
||||
self.logger.debug(f"Event: ", ev=data)
|
||||
if q.qsize() > 10:
|
||||
q.queue.clear()
|
||||
q.put(data)
|
||||
|
||||
def send_ptt_change(self, on:bool = False):
|
||||
|
|
|
@ -33,9 +33,10 @@ class explorer():
|
|||
callsign = str(self.config['STATION']['mycall']) + "-" + str(self.config["STATION"]['myssid'])
|
||||
gridsquare = str(self.config['STATION']['mygrid'])
|
||||
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)
|
||||
strength = str(self.states.s_meter_strength)
|
||||
away_from_key = str(self.states.is_away_from_key)
|
||||
|
||||
# stop pushing if default callsign
|
||||
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)
|
||||
|
||||
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:
|
||||
try:
|
||||
|
|
|
@ -13,8 +13,11 @@ from frame_handler import FrameHandler
|
|||
from frame_handler_ping import PingFrameHandler
|
||||
from frame_handler_cq import CQFrameHandler
|
||||
from frame_handler_arq_session import ARQFrameHandler
|
||||
from frame_handler_p2p_connection import P2PConnectionFrameHandler
|
||||
from frame_handler_beacon import BeaconFrameHandler
|
||||
|
||||
|
||||
|
||||
class DISPATCHER():
|
||||
|
||||
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_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_CONNECTION_CLOSE.value: {"class": ARQFrameHandler, "name": "ARQ CLOSE SESSION"},
|
||||
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.P2P_CONNECTION_CONNECT.value: {"class": P2PConnectionFrameHandler, "name": "P2P Connection CONNECT"},
|
||||
FR_TYPE.P2P_CONNECTION_CONNECT_ACK.value: {"class": P2PConnectionFrameHandler, "name": "P2P Connection CONNECT ACK"},
|
||||
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_ACK.value: {"class": ARQFrameHandler, "name": "ARQ STOP ACK"},
|
||||
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.value: {"class": PingFrameHandler, "name": "PING"},
|
||||
FR_TYPE.QRV.value: {"class": FrameHandler, "name": "QRV"},
|
||||
FR_TYPE.IS_WRITING.value: {"class": FrameHandler, "name": "IS_WRITING"},
|
||||
FR_TYPE.FEC.value: {"class": FrameHandler, "name": "FEC"},
|
||||
FR_TYPE.FEC_WAKEUP.value: {"class": FrameHandler, "name": "FEC WAKEUP"},
|
||||
#FR_TYPE.IS_WRITING.value: {"class": FrameHandler, "name": "IS_WRITING"},
|
||||
#FR_TYPE.FEC.value: {"class": FrameHandler, "name": "FEC"},
|
||||
#FR_TYPE.FEC_WAKEUP.value: {"class": FrameHandler, "name": "FEC WAKEUP"},
|
||||
}
|
||||
|
||||
def __init__(self, config, event_manager, states, modem):
|
||||
|
@ -67,22 +79,23 @@ class DISPATCHER():
|
|||
"""Queue received data for processing"""
|
||||
while True:
|
||||
data = self.data_queue_received.get()
|
||||
self.new_process_data(
|
||||
self.process_data(
|
||||
data['payload'],
|
||||
data['freedv'],
|
||||
data['bytes_per_frame'],
|
||||
data['snr'],
|
||||
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
|
||||
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"]
|
||||
|
||||
if frametype not in self.FRAME_HANDLER:
|
||||
self.log.warning(
|
||||
"[Modem] ARQ - other frame type", frametype=FR_TYPE(frametype).name)
|
||||
"[DISPATCHER] ARQ - other frame type", frametype=FR_TYPE(frametype).name)
|
||||
return
|
||||
|
||||
# instantiate handler
|
||||
|
|
|
@ -6,9 +6,11 @@ import structlog
|
|||
import time, uuid
|
||||
from codec2 import FREEDV_MODE
|
||||
from message_system_db_manager import DatabaseManager
|
||||
import maidenhead
|
||||
|
||||
TESTMODE = False
|
||||
|
||||
|
||||
class FrameHandler():
|
||||
|
||||
def __init__(self, name: str, config, states: StateManager, event_manager: EventManager,
|
||||
|
@ -34,7 +36,7 @@ class FrameHandler():
|
|||
ft = self.details['frame']['frame_type']
|
||||
valid = False
|
||||
# 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(
|
||||
call_with_ssid,
|
||||
self.details["frame"]["destination_crc"],
|
||||
|
@ -51,6 +53,20 @@ class FrameHandler():
|
|||
session_id = self.details['frame']['session_id']
|
||||
if session_id in self.states.arq_iss_sessions:
|
||||
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:
|
||||
valid = False
|
||||
|
||||
|
@ -85,8 +101,10 @@ class FrameHandler():
|
|||
if "session_id" in frame:
|
||||
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):
|
||||
frame = self.details['frame']
|
||||
|
@ -94,7 +112,15 @@ class FrameHandler():
|
|||
if 'origin' not in frame:
|
||||
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(
|
||||
frame['origin'],
|
||||
dxgrid,
|
||||
|
@ -103,8 +129,10 @@ class FrameHandler():
|
|||
self.details['frequency_offset'],
|
||||
self.states.radio_frequency,
|
||||
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):
|
||||
|
||||
event = {
|
||||
|
@ -118,6 +146,13 @@ class FrameHandler():
|
|||
if 'origin' in self.details['frame']:
|
||||
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
|
||||
|
||||
def emit_event(self):
|
||||
|
|
|
@ -32,7 +32,8 @@ class ARQFrameHandler(frame_handler.FrameHandler):
|
|||
session = ARQSessionIRS(self.config,
|
||||
self.modem,
|
||||
frame['origin'],
|
||||
session_id)
|
||||
session_id,
|
||||
self.states)
|
||||
self.states.register_arq_irs_session(session)
|
||||
|
||||
elif frame['frame_type_int'] in [
|
||||
|
|
|
@ -5,11 +5,23 @@ import frame_handler
|
|||
from message_system_db_messages import DatabaseManagerMessages
|
||||
|
||||
|
||||
class CQFrameHandler(frame_handler_ping.PingFrameHandler):
|
||||
class CQFrameHandler(frame_handler.FrameHandler):
|
||||
|
||||
def should_respond(self):
|
||||
self.logger.debug(f"Respond to CQ: {self.config['MODEM']['respond_to_cq']}")
|
||||
return self.config['MODEM']['respond_to_cq']
|
||||
#def should_respond(self):
|
||||
# self.logger.debug(f"Respond to CQ: {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):
|
||||
factory = data_frame_factory.DataFrameFactory(self.config)
|
||||
|
|
54
modem/frame_handler_p2p_connection.py
Normal file
54
modem/frame_handler_p2p_connection.py
Normal 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)
|
|
@ -15,15 +15,11 @@ class PingFrameHandler(frame_handler.FrameHandler):
|
|||
# ft = self.details['frame']['frame_type']
|
||||
# self.logger.info(f"[Modem] {ft} received but not for us.")
|
||||
# return valid
|
||||
|
||||
#def should_respond(self):
|
||||
# return self.is_frame_for_me()
|
||||
|
||||
def follow_protocol(self):
|
||||
|
||||
if not self.should_respond():
|
||||
if not bool(self.is_frame_for_me() and not self.states.getARQ()):
|
||||
return
|
||||
|
||||
|
||||
self.logger.debug(
|
||||
f"[Modem] Responding to request from [{self.details['frame']['origin']}]",
|
||||
snr=self.details['snr'],
|
||||
|
|
|
@ -121,60 +121,45 @@ def get_crc_32(data: str) -> bytes:
|
|||
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:
|
||||
dxcallsign:
|
||||
dxgrid:
|
||||
datatype:
|
||||
snr:
|
||||
offset:
|
||||
frequency:
|
||||
dxcallsign (str): The callsign of the DX station.
|
||||
dxgrid (str): The Maidenhead grid square of the DX station.
|
||||
datatype (str): The type of data received (e.g., FT8, CW).
|
||||
snr (int): Signal-to-noise ratio of the received signal.
|
||||
offset (float): Frequency offset.
|
||||
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:
|
||||
Nothing
|
||||
Nothing. The function updates the heard_stations_list in-place.
|
||||
"""
|
||||
# check if buffer empty
|
||||
if len(heard_stations_list) == 0:
|
||||
heard_stations_list.append(
|
||||
[dxcallsign, dxgrid, int(datetime.now(timezone.utc).timestamp()), datatype, snr, offset, frequency]
|
||||
)
|
||||
# if not, we search and update
|
||||
# Convert current timestamp to an integer
|
||||
current_timestamp = int(datetime.now(timezone.utc).timestamp())
|
||||
|
||||
# Initialize the new entry
|
||||
new_entry = [
|
||||
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:
|
||||
for i in range(len(heard_stations_list)):
|
||||
# Update callsign with new timestamp
|
||||
if heard_stations_list[i].count(dxcallsign) > 0:
|
||||
heard_stations_list[i] = [
|
||||
dxcallsign,
|
||||
dxgrid,
|
||||
int(time.time()),
|
||||
datatype,
|
||||
snr,
|
||||
offset,
|
||||
frequency,
|
||||
]
|
||||
# Search for the existing entry and update
|
||||
for i, entry in enumerate(heard_stations_list):
|
||||
if entry[0] == dxcallsign:
|
||||
heard_stations_list[i] = new_entry
|
||||
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:
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
94
modem/maidenhead.py
Normal file
94
modem/maidenhead.py
Normal 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}
|
362
modem/modem.py
362
modem/modem.py
|
@ -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=import-outside-toplevel
|
||||
|
||||
import atexit
|
||||
import ctypes
|
||||
import queue
|
||||
import threading
|
||||
import time
|
||||
import codec2
|
||||
import numpy as np
|
||||
|
@ -20,9 +17,9 @@ import sounddevice as sd
|
|||
import structlog
|
||||
import tci
|
||||
import cw
|
||||
from queues import RIGCTLD_COMMAND_QUEUE
|
||||
import audio
|
||||
import demodulator
|
||||
import modulator
|
||||
|
||||
TESTMODE = False
|
||||
|
||||
|
@ -44,35 +41,36 @@ class RF:
|
|||
self.audio_input_device = config['AUDIO']['input_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.rigctld_ip = config['RIGCTLD']['ip']
|
||||
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_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.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
|
||||
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
|
||||
#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
|
||||
self.AUDIO_CHANNELS = 1
|
||||
self.MODE = 0
|
||||
self.rms_counter = 0
|
||||
|
||||
self.audio_out_queue = queue.Queue()
|
||||
|
||||
# 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.data_queue_received = queue.Queue()
|
||||
|
@ -83,9 +81,12 @@ class RF:
|
|||
self.data_queue_received,
|
||||
self.states,
|
||||
self.event_manager,
|
||||
self.service_queue,
|
||||
self.fft_queue
|
||||
)
|
||||
|
||||
self.modulator = modulator.Modulator(self.config)
|
||||
|
||||
|
||||
|
||||
def tci_tx_callback(self, audio_48k) -> None:
|
||||
|
@ -103,10 +104,6 @@ class RF:
|
|||
if not self.init_audio():
|
||||
raise RuntimeError("Unable to init audio devices")
|
||||
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
|
||||
|
||||
|
@ -152,17 +149,25 @@ class RF:
|
|||
self.sd_input_stream = sd.InputStream(
|
||||
channels=1,
|
||||
dtype="int16",
|
||||
callback=self.demodulator.sd_input_audio_callback,
|
||||
callback=self.sd_input_audio_callback,
|
||||
device=in_dev_index,
|
||||
samplerate=self.AUDIO_SAMPLE_RATE,
|
||||
blocksize=4800,
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
|
||||
except Exception as audioerr:
|
||||
self.log.error("[MDM] init: starting pyaudio callback failed", e=audioerr)
|
||||
self.stop_modem()
|
||||
|
@ -185,191 +190,7 @@ class RF:
|
|||
|
||||
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):
|
||||
self.states.waitForTransmission()
|
||||
|
@ -380,32 +201,56 @@ class RF:
|
|||
)
|
||||
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)
|
||||
self.radio.set_ptt(False)
|
||||
self.event_manager.send_ptt_change(False)
|
||||
|
||||
self.states.setTransmitting(False)
|
||||
# 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 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)
|
||||
def transmit(
|
||||
self, mode, repeats: int, repeat_delay: int, frames: bytearray
|
||||
) -> bool:
|
||||
|
||||
self.demodulator.reset_data_sync()
|
||||
# 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.event_manager.send_ptt_change(True)
|
||||
|
||||
if self.radiocontrol in ["tci"]:
|
||||
|
@ -413,5 +258,74 @@ class RF:
|
|||
# we need to wait manually for tci processing
|
||||
self.tci_module.wait_until_transmitted(audio_48k)
|
||||
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
|
||||
|
||||
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)
|
||||
|
|
|
@ -6,9 +6,6 @@ from enum import Enum
|
|||
|
||||
class FRAME_TYPE(Enum):
|
||||
"""Lookup for frame types"""
|
||||
ARQ_CONNECTION_OPEN = 1
|
||||
ARQ_CONNECTION_HB = 2
|
||||
ARQ_CONNECTION_CLOSE = 3
|
||||
ARQ_STOP = 10
|
||||
ARQ_STOP_ACK = 11
|
||||
ARQ_SESSION_OPEN = 12
|
||||
|
@ -17,16 +14,24 @@ class FRAME_TYPE(Enum):
|
|||
ARQ_SESSION_INFO_ACK = 15
|
||||
ARQ_BURST_FRAME = 20
|
||||
ARQ_BURST_ACK = 21
|
||||
MESH_BROADCAST = 100
|
||||
MESH_SIGNALLING_PING = 101
|
||||
MESH_SIGNALLING_PING_ACK = 102
|
||||
P2P_CONNECTION_CONNECT = 30
|
||||
P2P_CONNECTION_CONNECT_ACK = 31
|
||||
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
|
||||
QRV = 201
|
||||
PING = 210
|
||||
PING_ACK = 211
|
||||
IS_WRITING = 215
|
||||
#IS_WRITING = 215
|
||||
BEACON = 250
|
||||
FEC = 251
|
||||
FEC_WAKEUP = 252
|
||||
#FEC = 251
|
||||
#FEC_WAKEUP = 252
|
||||
IDENT = 254
|
||||
TEST_FRAME = 255
|
||||
|
|
160
modem/modulator.py
Normal file
160
modem/modulator.py
Normal 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
315
modem/p2p_connection.py
Normal 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)
|
||||
|
|
@ -299,13 +299,13 @@ class radio:
|
|||
|
||||
# PTT Port and Type
|
||||
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')):
|
||||
args += ['--ptt-type', config['ptt_type']]
|
||||
args += ['-P', config['ptt_type']]
|
||||
|
||||
# Serial DCD and DTR
|
||||
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')):
|
||||
args += ['--set-conf', f'dtr_state={config["serial_dtr"]}']
|
||||
|
@ -323,7 +323,7 @@ class radio:
|
|||
# Handle custom arguments for rigctld
|
||||
# Custom args are split via ' ' so python doesn't add extranaeous quotes on windows
|
||||
args += config_rigctld["arguments"].split(" ")
|
||||
#print("Hamlib args ==>" + str(args))
|
||||
print("Hamlib args ==>" + str(args))
|
||||
|
||||
return args
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import time
|
||||
|
||||
from flask import Flask, request, jsonify, make_response, abort, Response
|
||||
from flask_sock import Sock
|
||||
from flask_cors import CORS
|
||||
|
@ -20,6 +22,8 @@ import command_test
|
|||
import command_arq_raw
|
||||
import command_message_send
|
||||
import event_manager
|
||||
import atexit
|
||||
|
||||
from message_system_db_manager import DatabaseManager
|
||||
from message_system_db_messages import DatabaseManagerMessages
|
||||
from message_system_db_attachments import DatabaseManagerAttachments
|
||||
|
@ -29,7 +33,7 @@ from schedule_manager import ScheduleManager
|
|||
app = Flask(__name__)
|
||||
CORS(app, resources={r"/*": {"origins": "*"}})
|
||||
sock = Sock(app)
|
||||
MODEM_VERSION = "0.14.2-alpha"
|
||||
MODEM_VERSION = "0.15.2-alpha"
|
||||
|
||||
# set config file to use
|
||||
def set_config():
|
||||
|
@ -142,14 +146,15 @@ def post_cqcqcq():
|
|||
def post_beacon():
|
||||
if request.method not in ['POST']:
|
||||
return api_response({"info": "endpoint for controlling BEACON STATE via POST"})
|
||||
|
||||
if not isinstance(request.json['enabled'], bool):
|
||||
if not isinstance(request.json['enabled'], bool) or not isinstance(request.json['away_from_key'], bool):
|
||||
api_abort(f"Incorrect value for 'enabled'. Shoud be bool.")
|
||||
if not app.state_manager.is_modem_running:
|
||||
api_abort('Modem not running', 503)
|
||||
|
||||
if not app.state_manager.is_beacon_running:
|
||||
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():
|
||||
enqueue_tx_command(command_beacon.BeaconCommand, request.json)
|
||||
else:
|
||||
|
@ -320,7 +325,23 @@ def sock_fft(sock):
|
|||
def sock_states(sock):
|
||||
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__":
|
||||
app.config['SOCK_SERVER_OPTIONS'] = {'ping_interval': 10}
|
||||
|
@ -331,6 +352,7 @@ if __name__ == "__main__":
|
|||
app.config_manager = CONFIG(config_file)
|
||||
|
||||
# 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.modem_events = queue.Queue() # queue which holds latest events
|
||||
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)
|
||||
# start service manager
|
||||
app.service_manager = service_manager.SM(app)
|
||||
|
||||
# start modem service
|
||||
app.modem_service.put("start")
|
||||
# initialize database default values
|
||||
|
@ -353,7 +376,9 @@ if __name__ == "__main__":
|
|||
modemport = conf['NETWORK']['modemport']
|
||||
|
||||
if not modemaddress:
|
||||
modemaddress = '0.0.0.0'
|
||||
modemaddress = '127.0.0.1'
|
||||
if not modemport:
|
||||
modemport = 5000
|
||||
|
||||
app.run(modemaddress, modemport)
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import structlog
|
|||
import audio
|
||||
|
||||
import radio_manager
|
||||
|
||||
from socket_interface import SocketInterfaceHandler
|
||||
|
||||
class SM:
|
||||
def __init__(self, app):
|
||||
|
@ -19,7 +19,7 @@ class SM:
|
|||
self.state_manager = app.state_manager
|
||||
self.event_manager = app.event_manager
|
||||
self.schedule_manager = app.schedule_manager
|
||||
|
||||
self.socket_interface_manager = None
|
||||
|
||||
runner_thread = threading.Thread(
|
||||
target=self.runner, name="runner thread", daemon=True
|
||||
|
@ -34,15 +34,24 @@ class SM:
|
|||
self.start_radio_manager()
|
||||
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:
|
||||
self.stop_modem()
|
||||
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
|
||||
threading.Event().wait(0.5)
|
||||
|
||||
elif cmd in ['restart']:
|
||||
self.stop_modem()
|
||||
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
|
||||
threading.Event().wait(0.5)
|
||||
|
||||
|
|
193
modem/socket_interface.py
Normal file
193
modem/socket_interface.py
Normal 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()
|
76
modem/socket_interface_commands.py
Normal file
76
modem/socket_interface_commands.py
Normal 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)
|
|
@ -23,6 +23,7 @@ class StateManager:
|
|||
self.setARQ(False)
|
||||
|
||||
self.is_beacon_running = False
|
||||
self.is_away_from_key = False
|
||||
|
||||
# If true, any wait() call is blocking
|
||||
self.transmitting_event = threading.Event()
|
||||
|
@ -38,6 +39,8 @@ class StateManager:
|
|||
self.arq_iss_sessions = {}
|
||||
self.arq_irs_sessions = {}
|
||||
|
||||
self.p2p_connection_sessions = {}
|
||||
|
||||
#self.mesh_routing_table = []
|
||||
|
||||
self.radio_frequency = 0
|
||||
|
@ -82,6 +85,7 @@ class StateManager:
|
|||
"type": msgtype,
|
||||
"is_modem_running": self.is_modem_running,
|
||||
"is_beacon_running": self.is_beacon_running,
|
||||
"is_away_from_key": self.is_away_from_key,
|
||||
"radio_status": self.radio_status,
|
||||
"radio_frequency": self.radio_frequency,
|
||||
"radio_mode": self.radio_mode,
|
||||
|
@ -182,7 +186,6 @@ class StateManager:
|
|||
# if frequency not provided, add it here
|
||||
if 'frequency' not in activity_data:
|
||||
activity_data['frequency'] = self.radio_frequency
|
||||
|
||||
self.activities_list[activity_id] = activity_data
|
||||
self.sendStateUpdate()
|
||||
|
||||
|
@ -214,3 +217,15 @@ class StateManager:
|
|||
"radio_rf_level": self.radio_rf_level,
|
||||
"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]
|
|
@ -35,6 +35,10 @@ class TestModem:
|
|||
samples += codec2.api.freedv_get_n_tx_modem_samples(c2instance)
|
||||
samples += codec2.api.freedv_get_n_tx_postamble_modem_samples(c2instance)
|
||||
time = samples / 8000
|
||||
#print(mode)
|
||||
#if mode == codec2.FREEDV_MODE.signalling:
|
||||
# time = 0.69
|
||||
#print(time)
|
||||
return time
|
||||
|
||||
def transmit(self, mode, repeats: int, repeat_delay: int, frames: bytearray) -> bool:
|
||||
|
@ -82,7 +86,7 @@ class TestARQSession(unittest.TestCase):
|
|||
cls.irs_modem)
|
||||
|
||||
# Frame loss probability in %
|
||||
cls.loss_probability = 30
|
||||
cls.loss_probability = 0
|
||||
|
||||
cls.channels_running = True
|
||||
|
||||
|
@ -91,12 +95,18 @@ class TestARQSession(unittest.TestCase):
|
|||
# Transfer data between both parties
|
||||
try:
|
||||
transmission = modem_transmit_queue.get(timeout=1)
|
||||
transmission["bytes"] += bytes(2) # simulate 2 bytes crc checksum
|
||||
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)
|
||||
|
||||
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:
|
||||
continue
|
||||
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.channels_running = False
|
||||
|
||||
def testARQSessionSmallPayload(self):
|
||||
def DisabledtestARQSessionSmallPayload(self):
|
||||
# set Packet Error Rate (PER) / frame loss probability
|
||||
self.loss_probability = 30
|
||||
|
||||
|
@ -160,7 +170,7 @@ class TestARQSession(unittest.TestCase):
|
|||
self.waitAndCloseChannels()
|
||||
del cmd
|
||||
|
||||
def testARQSessionAbortTransmissionISS(self):
|
||||
def DisabledtestARQSessionAbortTransmissionISS(self):
|
||||
# set Packet Error Rate (PER) / frame loss probability
|
||||
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.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:
|
||||
self.iss_state_manager.arq_iss_sessions[id].abort_transmission()
|
||||
|
||||
self.waitAndCloseChannels()
|
||||
del cmd
|
||||
|
||||
def testARQSessionAbortTransmissionIRS(self):
|
||||
def DisabledtestARQSessionAbortTransmissionIRS(self):
|
||||
# set Packet Error Rate (PER) / frame loss probability
|
||||
self.loss_probability = 0
|
||||
|
||||
|
@ -198,7 +208,7 @@ class TestARQSession(unittest.TestCase):
|
|||
self.waitAndCloseChannels()
|
||||
del cmd
|
||||
|
||||
def testSessionCleanupISS(self):
|
||||
def DisabledtestSessionCleanupISS(self):
|
||||
|
||||
params = {
|
||||
'dxcall': "AA1AAA-1",
|
||||
|
@ -217,11 +227,13 @@ class TestARQSession(unittest.TestCase):
|
|||
break
|
||||
del cmd
|
||||
|
||||
def testSessionCleanupIRS(self):
|
||||
def DisabledtestSessionCleanupIRS(self):
|
||||
session = arq_session_irs.ARQSessionIRS(self.config,
|
||||
self.irs_modem,
|
||||
'AA1AAA-1',
|
||||
random.randint(0, 255))
|
||||
random.randint(0, 255),
|
||||
self.irs_state_manager
|
||||
)
|
||||
self.irs_state_manager.register_arq_irs_session(session)
|
||||
for session_id in self.irs_state_manager.arq_irs_sessions:
|
||||
session = self.irs_state_manager.arq_irs_sessions[session_id]
|
||||
|
|
|
@ -32,7 +32,7 @@ class TestDataFrameFactory(unittest.TestCase):
|
|||
def testARQConnect(self):
|
||||
dxcall = "DJ2LS-4"
|
||||
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)
|
||||
|
||||
self.assertEqual(frame_data['origin'], self.factory.myfullcall)
|
||||
|
|
197
tests/test_p2p_connection.py
Normal file
197
tests/test_p2p_connection.py
Normal 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()
|
96
tools/custom_mode_tests/create_custom_ofdm_mod.py
Normal file
96
tools/custom_mode_tests/create_custom_ofdm_mod.py
Normal 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)
|
||||
|
Loading…
Reference in a new issue