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

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

View file

@ -60,7 +60,7 @@ jobs:
path: ./FreeDATA-Installer.exe
- 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

View file

@ -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

View file

@ -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

View file

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

Before

Width:  |  Height:  |  Size: 225 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

View file

@ -105,6 +105,8 @@ Spectrum.prototype.drawSpectrum = function () {
var linePositionHigh = 178.4; //150 + bandwidth/20
var 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 =

View file

@ -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>

View file

@ -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">

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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,

View file

@ -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)">

View file

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

View file

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

View file

@ -9,7 +9,6 @@ import settings_view from "./settings.vue";
import main_footer_navbar from "./main_footer_navbar.vue";
import 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"

View file

@ -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>

View file

@ -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">

View file

@ -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() {

View file

@ -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":

View file

@ -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",

View file

@ -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
View file

@ -129,4 +129,7 @@ dmypy.json
.pyre/
# FreeDATA config
config.ini
config.ini
#FreeData DB
freedata-messages.db

View file

@ -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()

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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}")

View file

@ -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
}

View file

@ -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()

View file

@ -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):

View file

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

View file

@ -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,

View file

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

View file

@ -15,7 +15,6 @@ input_device = 5a1c
output_device = bd6c
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

View file

@ -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()

View file

@ -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)

View file

@ -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

View file

@ -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):

View file

@ -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:

View file

@ -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

View file

@ -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):

View file

@ -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 [

View file

@ -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)

View file

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

View file

@ -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'],

View file

@ -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:

94
modem/maidenhead.py Normal file
View file

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

View file

@ -9,10 +9,7 @@ Created on Wed Dec 23 07:04:24 2020
# pylint: disable=invalid-name, line-too-long, c-extension-no-member
# pylint: disable=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)

View file

@ -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
View file

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

315
modem/p2p_connection.py Normal file
View file

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

View file

@ -299,13 +299,13 @@ class radio:
# PTT Port and Type
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

View file

@ -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)

View file

@ -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
View file

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

View file

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

View file

@ -23,6 +23,7 @@ class StateManager:
self.setARQ(False)
self.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]

View file

@ -35,6 +35,10 @@ class TestModem:
samples += codec2.api.freedv_get_n_tx_modem_samples(c2instance)
samples += codec2.api.freedv_get_n_tx_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]

View file

@ -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)

View file

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

View file

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