From 2b6aeb26d362b8bbc105152f92e94776dd5d3eba Mon Sep 17 00:00:00 2001 From: DJ2LS <75909252+DJ2LS@users.noreply.github.com> Date: Mon, 2 Jun 2025 12:16:46 +0200 Subject: [PATCH 001/141] attempt of streaming audio to gui --- .../src/components/dynamic_components.vue | 14 ++++- .../src/components/grid/grid_audio.vue | 61 +++++++++++++++++++ freedata_gui/src/js/audioStreamHandler.js | 12 ++++ freedata_gui/src/js/event_sock.js | 6 ++ freedata_gui/src/js/waterfallHandler.js | 1 + freedata_gui/src/store/audioStore.js | 2 + freedata_server/api/websocket.py | 17 ++++++ freedata_server/context.py | 2 + freedata_server/modem.py | 6 ++ freedata_server/websocket_manager.py | 42 ++++++++++++- 10 files changed, 159 insertions(+), 4 deletions(-) create mode 100644 freedata_gui/src/components/grid/grid_audio.vue create mode 100644 freedata_gui/src/js/audioStreamHandler.js diff --git a/freedata_gui/src/components/dynamic_components.vue b/freedata_gui/src/components/dynamic_components.vue index 092d058de..cb5743ffe 100644 --- a/freedata_gui/src/components/dynamic_components.vue +++ b/freedata_gui/src/components/dynamic_components.vue @@ -28,6 +28,7 @@ import grid_tune from "./grid/grid_tune.vue"; import grid_CQ_btn from "./grid/grid_CQ.vue"; import grid_ping from "./grid/grid_ping.vue"; import grid_freq from "./grid/grid_frequency.vue"; +import grid_audio from "./grid/grid_audio.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"; @@ -325,8 +326,19 @@ const gridWidgets = [ 18, false, { x: 16, y: 8, w: 2, h: 8 } + ), + new gridWidget( + grid_audio, + { x: 16, y: 8, w: 2, h: 8 }, + "Audio widget", + false, + true, + "Audio", + 24, + false, + { x: 16, y: 8, w: 2, h: 8 } ) - //Next new widget ID should be 23 + //Next new widget ID should be 24 ]; function updateFrequencyAndApply(frequency) { diff --git a/freedata_gui/src/components/grid/grid_audio.vue b/freedata_gui/src/components/grid/grid_audio.vue new file mode 100644 index 000000000..ddd395334 --- /dev/null +++ b/freedata_gui/src/components/grid/grid_audio.vue @@ -0,0 +1,61 @@ + + + + Start Audio + Stop Audio + diff --git a/freedata_gui/src/js/audioStreamHandler.js b/freedata_gui/src/js/audioStreamHandler.js new file mode 100644 index 000000000..a27684f15 --- /dev/null +++ b/freedata_gui/src/js/audioStreamHandler.js @@ -0,0 +1,12 @@ +import { setActivePinia } from "pinia"; +import pinia from "../store/index"; +setActivePinia(pinia); + +import { useAudioStore } from "../store/audioStore.js"; +const audio = useAudioStore(pinia); + +export function addDataToAudio(data) { + const int16 = new Int16Array(data); // ArrayBuffer → Int16 PCM + const copied = new Int16Array(int16); + audio.rxStream.push(copied); +} diff --git a/freedata_gui/src/js/event_sock.js b/freedata_gui/src/js/event_sock.js index ef03ec746..87d800408 100644 --- a/freedata_gui/src/js/event_sock.js +++ b/freedata_gui/src/js/event_sock.js @@ -5,6 +5,7 @@ import { loadAllData, } from "../js/eventHandler.js"; import { addDataToWaterfall } from "../js/waterfallHandler.js"; +import { addDataToAudio } from "../js/audioStreamHandler.js"; // ----------------- init pinia stores ------------- import { setActivePinia } from "pinia"; @@ -22,6 +23,10 @@ function connect(endpoint, dispatcher) { `${wsProtocol}//${hostname}:${adjustedPort}/${endpoint}`, ); + if (endpoint.includes("audio")){ + socket.binaryType = "arraybuffer"; + } + // handle opening socket.addEventListener("open", function () { console.log(`Connected to the WebSocket server: ${endpoint}`); @@ -56,4 +61,5 @@ export function initConnections() { connect("states", stateDispatcher); connect("events", eventDispatcher); connect("fft", addDataToWaterfall); + connect("audio_rx", addDataToAudio); } diff --git a/freedata_gui/src/js/waterfallHandler.js b/freedata_gui/src/js/waterfallHandler.js index 6ea51afaf..8636b7f40 100644 --- a/freedata_gui/src/js/waterfallHandler.js +++ b/freedata_gui/src/js/waterfallHandler.js @@ -28,6 +28,7 @@ export function addDataToWaterfall(data) { }); //window.dispatchEvent(new CustomEvent("wf-data-avail", {bubbles:true, detail: data })); } + /** * Setwaterfall colormap array by index * @param {number} index colormap index to use diff --git a/freedata_gui/src/store/audioStore.js b/freedata_gui/src/store/audioStore.js index cc62888bc..38118f072 100644 --- a/freedata_gui/src/store/audioStore.js +++ b/freedata_gui/src/store/audioStore.js @@ -15,6 +15,7 @@ const skel = [ export const useAudioStore = defineStore("audioStore", () => { const audioInputs = ref([]); const audioOutputs = ref([]); + const rxStream = ref([]); const loadAudioDevices = async () => { try { @@ -35,5 +36,6 @@ export const useAudioStore = defineStore("audioStore", () => { audioInputs, audioOutputs, loadAudioDevices, + rxStream, }; }); diff --git a/freedata_server/api/websocket.py b/freedata_server/api/websocket.py index b586f800b..cab682569 100644 --- a/freedata_server/api/websocket.py +++ b/freedata_server/api/websocket.py @@ -47,3 +47,20 @@ async def websocket_states( ctx.websocket_manager.states_client_list, ctx.state_queue ) + +@router.websocket("/audio_rx") +async def websocket_audio_rx( + websocket: WebSocket, + ctx: AppContext = Depends(get_ctx) +): + """ + WebSocket endpoint for state updates. + """ + await websocket.accept() + await ctx.websocket_manager.handle_connection( + websocket, + ctx.websocket_manager.audio_rx_client_list, + ctx.state_queue + ) + #while True: + # await websocket.send_bytes(b"\x00" * 1024) diff --git a/freedata_server/context.py b/freedata_server/context.py index d0ccf6b25..a81686e36 100644 --- a/freedata_server/context.py +++ b/freedata_server/context.py @@ -19,6 +19,8 @@ def __init__(self, config_file: str): self.modem_events = Queue() self.modem_fft = Queue() self.modem_service = Queue() + self.audio_rx_queue = Queue(maxsize=1) + self.event_manager = EventManager(self, [self.modem_events]) self.state_manager = StateManager(self.state_queue) self.schedule_manager = ScheduleManager(self) diff --git a/freedata_server/modem.py b/freedata_server/modem.py index d2a213fe6..bcbad2cf2 100644 --- a/freedata_server/modem.py +++ b/freedata_server/modem.py @@ -374,6 +374,8 @@ def sd_output_audio_callback(self, outdata: np.ndarray, frames: int, time, statu audio.calculate_fft(audio_8k, self.ctx.modem_fft, self.ctx.state_manager) outdata[:] = chunk.reshape(outdata.shape) + + else: # reset transmitting state only, if we are not actively processing audio # for avoiding a ptt toggle state bug @@ -409,6 +411,10 @@ def sd_input_audio_callback(self, indata: np.ndarray, frames: int, time, status) try: audio_48k = np.frombuffer(indata, dtype=np.int16) audio_8k = self.resampler.resample48_to_8(audio_48k) + + #self.ctx.audio_rx_queue.put({"audio": audio_8k}) + self.ctx.audio_rx_queue.put(audio_8k) + if self.ctx.config_manager.config['AUDIO'].get('rx_auto_audio_level'): audio_8k = audio.normalize_audio(audio_8k) diff --git a/freedata_server/websocket_manager.py b/freedata_server/websocket_manager.py index f31b9249e..a49e65cd0 100644 --- a/freedata_server/websocket_manager.py +++ b/freedata_server/websocket_manager.py @@ -1,6 +1,9 @@ import threading import json import asyncio +from asyncio import run_coroutine_threadsafe + +import numpy as np import structlog @@ -27,10 +30,12 @@ def __init__(self, ctx): self.events_client_list = set() self.fft_client_list = set() self.states_client_list = set() + self.audio_rx_client_list = set() self.events_thread = None self.states_thread = None self.fft_thread = None + self.audio_rx_thread = None async def handle_connection(self, websocket, client_list, event_queue): """Handles a WebSocket connection. @@ -45,6 +50,7 @@ async def handle_connection(self, websocket, client_list, event_queue): event_queue (queue.Queue): The event queue. Currently unused. """ client_list.add(websocket) + self.log.info(f"Client websocket connection established", ws=websocket) while not self.shutdown_flag.is_set(): try: await websocket.receive_text() @@ -70,7 +76,6 @@ def transmit_sock_data_worker(self, client_list, event_queue): while not self.shutdown_flag.is_set(): try: event = event_queue.get(timeout=1) - if event: json_event = json.dumps(event) clients = client_list.copy() @@ -82,6 +87,32 @@ def transmit_sock_data_worker(self, client_list, event_queue): except Exception: continue + def transmit_sock_audio_worker(self, client_list, audio_queue): + """Worker thread function for transmitting data to WebSocket clients. + + This method continuously retrieves events from the provided queue and + sends them as JSON strings to all connected clients in the specified + list. It handles client disconnections gracefully. + + Args: + client_list (set): The set of connected WebSocket clients. + event_queue (queue.Queue): The queue containing events to be transmitted. + """ + while not self.shutdown_flag.is_set(): + #loop = asyncio.get_event_loop() + try: + audio = audio_queue.get(timeout=1) + if isinstance(audio, np.ndarray): + audio = audio.tobytes() + clients = client_list.copy() + for client in clients: + try: + asyncio.run(client.send_bytes(audio)) + + except Exception: + client_list.remove(client) + except Exception: + continue def startWorkerThreads(self, app): @@ -103,7 +134,10 @@ def startWorkerThreads(self, app): self.fft_thread = threading.Thread(target=self.transmit_sock_data_worker, daemon=True, args=(self.fft_client_list, self.ctx.modem_fft)) self.fft_thread.start() - + + self.audio_rx_thread = threading.Thread(target=self.transmit_sock_audio_worker, daemon=True, args=(self.audio_rx_client_list, self.ctx.audio_rx_queue)) + self.audio_rx_thread.start() + def shutdown(self): """Shuts down the WebSocket manager. @@ -117,6 +151,8 @@ def shutdown(self): self.events_thread.join(0.5) if self.states_thread: self.states_thread.join(0.5) - if self.states_thread: + if self.fft_thread: self.fft_thread.join(0.5) + if self.audio_rx_thread: + self.audio_rx_thread.join(0.5) self.log.warning("[SHUTDOWN] websockets closed") \ No newline at end of file From c32e409d527042f0312e104531092e3d1c1ee47a Mon Sep 17 00:00:00 2001 From: DJ2LS <75909252+DJ2LS@users.noreply.github.com> Date: Mon, 2 Jun 2025 12:33:56 +0200 Subject: [PATCH 002/141] attempt of streaming audio to gui --- freedata_gui/src/components/grid/grid_audio.vue | 2 +- freedata_gui/src/js/audioStreamHandler.js | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/freedata_gui/src/components/grid/grid_audio.vue b/freedata_gui/src/components/grid/grid_audio.vue index ddd395334..12e0c6510 100644 --- a/freedata_gui/src/components/grid/grid_audio.vue +++ b/freedata_gui/src/components/grid/grid_audio.vue @@ -12,7 +12,7 @@ let isPlaying = false; function playRxStream() { if (isPlaying) return; - console.log("Starte PLayback"); + console.log("Start Playback"); audioCtx = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 8000 }); isPlaying = true; diff --git a/freedata_gui/src/js/audioStreamHandler.js b/freedata_gui/src/js/audioStreamHandler.js index a27684f15..70427a6f5 100644 --- a/freedata_gui/src/js/audioStreamHandler.js +++ b/freedata_gui/src/js/audioStreamHandler.js @@ -5,8 +5,17 @@ setActivePinia(pinia); import { useAudioStore } from "../store/audioStore.js"; const audio = useAudioStore(pinia); +const MAX_BLOCKS = 10; + export function addDataToAudio(data) { - const int16 = new Int16Array(data); // ArrayBuffer → Int16 PCM - const copied = new Int16Array(int16); - audio.rxStream.push(copied); + const int16 = new Int16Array(data); + const copy = new Int16Array(int16); // Kopie für Sicherheit + + const stream = audio.rxStream; + + if (stream.length >= MAX_BLOCKS) { + stream.shift(); + } + + stream.push(copy); } From 15603046015bf9b163a3dd517307d0cd85c705ed Mon Sep 17 00:00:00 2001 From: DJ2LS <75909252+DJ2LS@users.noreply.github.com> Date: Mon, 2 Jun 2025 15:36:19 +0200 Subject: [PATCH 003/141] adjusted streaming --- freedata_gui/src/components/grid/grid_audio.vue | 6 +++++- freedata_server/modem.py | 14 ++++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/freedata_gui/src/components/grid/grid_audio.vue b/freedata_gui/src/components/grid/grid_audio.vue index 12e0c6510..ba30407f3 100644 --- a/freedata_gui/src/components/grid/grid_audio.vue +++ b/freedata_gui/src/components/grid/grid_audio.vue @@ -24,9 +24,13 @@ function playRxStream() { //const BLOCK_DURATION_MS = 1024 / 8000 * 1000; const BLOCK_DURATION_MS = 10 + const MIN_BLOCKS_TO_START = 5 function loop() { if (!isPlaying) return; - + if (audio.rxStream.length < MIN_BLOCKS_TO_START){ + setTimeout(loop, 5); + return; + } if (audio.rxStream.length > 0) { const block = audio.rxStream.shift(); // Nächstes Audioblock holen const float32 = Float32Array.from(block, s => s / 32768); diff --git a/freedata_server/modem.py b/freedata_server/modem.py index bcbad2cf2..e26d3b858 100644 --- a/freedata_server/modem.py +++ b/freedata_server/modem.py @@ -75,6 +75,7 @@ def __init__(self, ctx) -> None: self.MODE = 0 self.rms_counter = 0 + self.AUDIO_STREAMING_CHUNK_SIZE = 600 self.audio_out_queue = queue.Queue() # Make sure our resampler will work @@ -351,6 +352,16 @@ def enqueue_audio_out(self, audio_48k) -> None: return + CHUNK_SIZE = 600 # z. B. 600 Samples = 75ms @ 8kHz + + def enqueue_streaming_audio_chunks(self, audio_block, queue): + total_samples = len(audio_block) + for start in range(0, total_samples, self.AUDIO_STREAMING_CHUNK_SIZE): + end = start + self.AUDIO_STREAMING_CHUNK_SIZE + chunk = audio_block[start:end] + queue.put(chunk.tobytes()) + + def sd_output_audio_callback(self, outdata: np.ndarray, frames: int, time, status) -> None: """Callback function for the audio output stream. @@ -412,8 +423,7 @@ def sd_input_audio_callback(self, indata: np.ndarray, frames: int, time, status) audio_48k = np.frombuffer(indata, dtype=np.int16) audio_8k = self.resampler.resample48_to_8(audio_48k) - #self.ctx.audio_rx_queue.put({"audio": audio_8k}) - self.ctx.audio_rx_queue.put(audio_8k) + self.enqueue_streaming_audio_chunks(audio_8k, self.ctx.audio_rx_queue) if self.ctx.config_manager.config['AUDIO'].get('rx_auto_audio_level'): audio_8k = audio.normalize_audio(audio_8k) From afafd71fb5dbef97a295c0391020bcd9d47abed1 Mon Sep 17 00:00:00 2001 From: DJ2LS <75909252+DJ2LS@users.noreply.github.com> Date: Mon, 2 Jun 2025 15:58:37 +0200 Subject: [PATCH 004/141] adjusted streaming --- .../src/components/grid/grid_audio.vue | 6 +++-- freedata_gui/src/js/audioStreamHandler.js | 1 - freedata_server/modem.py | 22 +++++++++++++------ 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/freedata_gui/src/components/grid/grid_audio.vue b/freedata_gui/src/components/grid/grid_audio.vue index ba30407f3..27d6f2886 100644 --- a/freedata_gui/src/components/grid/grid_audio.vue +++ b/freedata_gui/src/components/grid/grid_audio.vue @@ -22,13 +22,15 @@ function playRxStream() { }); } - //const BLOCK_DURATION_MS = 1024 / 8000 * 1000; - const BLOCK_DURATION_MS = 10 + const BLOCK_DURATION_MS = 600 / 8000 * 1000; + //const BLOCK_DURATION_MS = 10 const MIN_BLOCKS_TO_START = 5 function loop() { if (!isPlaying) return; + console.log(audio.rxStream.length) if (audio.rxStream.length < MIN_BLOCKS_TO_START){ setTimeout(loop, 5); + console.log("timeout....") return; } if (audio.rxStream.length > 0) { diff --git a/freedata_gui/src/js/audioStreamHandler.js b/freedata_gui/src/js/audioStreamHandler.js index 70427a6f5..3ac288fbd 100644 --- a/freedata_gui/src/js/audioStreamHandler.js +++ b/freedata_gui/src/js/audioStreamHandler.js @@ -10,7 +10,6 @@ const MAX_BLOCKS = 10; export function addDataToAudio(data) { const int16 = new Int16Array(data); const copy = new Int16Array(int16); // Kopie für Sicherheit - const stream = audio.rxStream; if (stream.length >= MAX_BLOCKS) { diff --git a/freedata_server/modem.py b/freedata_server/modem.py index e26d3b858..5f85378de 100644 --- a/freedata_server/modem.py +++ b/freedata_server/modem.py @@ -352,14 +352,22 @@ def enqueue_audio_out(self, audio_48k) -> None: return - CHUNK_SIZE = 600 # z. B. 600 Samples = 75ms @ 8kHz - def enqueue_streaming_audio_chunks(self, audio_block, queue): - total_samples = len(audio_block) - for start in range(0, total_samples, self.AUDIO_STREAMING_CHUNK_SIZE): - end = start + self.AUDIO_STREAMING_CHUNK_SIZE - chunk = audio_block[start:end] - queue.put(chunk.tobytes()) + #total_samples = len(audio_block) + #for start in range(0, total_samples, self.AUDIO_STREAMING_CHUNK_SIZE): + # end = start + self.AUDIO_STREAMING_CHUNK_SIZE + # chunk = audio_block[start:end] + # queue.put(chunk.tobytes()) + + block_size = self.AUDIO_STREAMING_CHUNK_SIZE + + pad_length = -len(audio_block) % block_size + padded_data = np.pad(audio_block, (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: + queue.put(block) + def sd_output_audio_callback(self, outdata: np.ndarray, frames: int, time, status) -> None: From 2834089bd42af319cdaa46f1a9964e22e9993cad Mon Sep 17 00:00:00 2001 From: DJ2LS <75909252+DJ2LS@users.noreply.github.com> Date: Tue, 3 Jun 2025 13:09:50 +0200 Subject: [PATCH 005/141] buffer adjustments --- freedata_gui/src/components/grid/grid_audio.vue | 4 ++-- freedata_server/context.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freedata_gui/src/components/grid/grid_audio.vue b/freedata_gui/src/components/grid/grid_audio.vue index 27d6f2886..b16c42304 100644 --- a/freedata_gui/src/components/grid/grid_audio.vue +++ b/freedata_gui/src/components/grid/grid_audio.vue @@ -22,8 +22,8 @@ function playRxStream() { }); } - const BLOCK_DURATION_MS = 600 / 8000 * 1000; - //const BLOCK_DURATION_MS = 10 + //const BLOCK_DURATION_MS = 600 / 8000 * 1000; + const BLOCK_DURATION_MS = 10 const MIN_BLOCKS_TO_START = 5 function loop() { if (!isPlaying) return; diff --git a/freedata_server/context.py b/freedata_server/context.py index a81686e36..1bac7aa03 100644 --- a/freedata_server/context.py +++ b/freedata_server/context.py @@ -19,7 +19,7 @@ def __init__(self, config_file: str): self.modem_events = Queue() self.modem_fft = Queue() self.modem_service = Queue() - self.audio_rx_queue = Queue(maxsize=1) + self.audio_rx_queue = Queue(maxsize=10) self.event_manager = EventManager(self, [self.modem_events]) self.state_manager = StateManager(self.state_queue) From 0b74baf4f6fa62051951ead55d27090d4645787b Mon Sep 17 00:00:00 2001 From: DJ2LS <75909252+DJ2LS@users.noreply.github.com> Date: Tue, 3 Jun 2025 13:29:18 +0200 Subject: [PATCH 006/141] buffer adjustments --- .../src/components/grid/grid_audio.vue | 50 ++++++++++--------- freedata_gui/src/js/audioStreamHandler.js | 5 ++ freedata_gui/src/store/audioStore.js | 49 ++++++++++++++++++ 3 files changed, 81 insertions(+), 23 deletions(-) diff --git a/freedata_gui/src/components/grid/grid_audio.vue b/freedata_gui/src/components/grid/grid_audio.vue index b16c42304..4360b2850 100644 --- a/freedata_gui/src/components/grid/grid_audio.vue +++ b/freedata_gui/src/components/grid/grid_audio.vue @@ -12,44 +12,48 @@ let isPlaying = false; function playRxStream() { if (isPlaying) return; - console.log("Start Playback"); - audioCtx = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 8000 }); + const SAMPLE_RATE = 8000; + const BLOCK_SIZE = 600; + const BLOCK_DURATION_MS = (BLOCK_SIZE / SAMPLE_RATE) * 1000; // z. B. 75ms + + audioCtx = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: SAMPLE_RATE }); isPlaying = true; + scheduledTime = audioCtx.currentTime; + - if (audioCtx.state === 'suspended') { - audioCtx.resume().then(() => { - console.log("AudioContext resumed"); - }); - } - //const BLOCK_DURATION_MS = 600 / 8000 * 1000; - const BLOCK_DURATION_MS = 10 - const MIN_BLOCKS_TO_START = 5 function loop() { if (!isPlaying) return; - console.log(audio.rxStream.length) - if (audio.rxStream.length < MIN_BLOCKS_TO_START){ - setTimeout(loop, 5); - console.log("timeout....") - return; - } - if (audio.rxStream.length > 0) { - const block = audio.rxStream.shift(); // Nächstes Audioblock holen + + const block = audio.getNextBlock(); + + if (block) { const float32 = Float32Array.from(block, s => s / 32768); - const buffer = audioCtx.createBuffer(1, float32.length, 8000); + const buffer = audioCtx.createBuffer(1, float32.length, SAMPLE_RATE); buffer.copyToChannel(float32, 0); const source = audioCtx.createBufferSource(); source.buffer = buffer; source.connect(audioCtx.destination); - source.start(); + + const now = audioCtx.currentTime; + const playAt = Math.max(now, scheduledTime); + source.start(playAt); + scheduledTime = playAt + buffer.duration; + + } else { - console.log("Buffer empty, waiting..."); + console.warn("Buffer underrun"); } - setTimeout(loop, BLOCK_DURATION_MS); + + setTimeout(loop, 4); } - loop(); + if (audioCtx.state === 'suspended') { + audioCtx.resume().then(loop); + } else { + loop(); + } } function stopRxStream() { diff --git a/freedata_gui/src/js/audioStreamHandler.js b/freedata_gui/src/js/audioStreamHandler.js index 3ac288fbd..dea67dcad 100644 --- a/freedata_gui/src/js/audioStreamHandler.js +++ b/freedata_gui/src/js/audioStreamHandler.js @@ -10,6 +10,7 @@ const MAX_BLOCKS = 10; export function addDataToAudio(data) { const int16 = new Int16Array(data); const copy = new Int16Array(int16); // Kopie für Sicherheit +/* const stream = audio.rxStream; if (stream.length >= MAX_BLOCKS) { @@ -17,4 +18,8 @@ export function addDataToAudio(data) { } stream.push(copy); + */ + audio.addBlock(copy); + + } diff --git a/freedata_gui/src/store/audioStore.js b/freedata_gui/src/store/audioStore.js index 38118f072..c90f7a29b 100644 --- a/freedata_gui/src/store/audioStore.js +++ b/freedata_gui/src/store/audioStore.js @@ -17,6 +17,47 @@ export const useAudioStore = defineStore("audioStore", () => { const audioOutputs = ref([]); const rxStream = ref([]); + const BUFFER_SIZE = 64; + const rxStreamBuffer = new Array(BUFFER_SIZE).fill(null); + + let writePtr = 0; + let readPtr = 0; + let readyBlocks = 0; + + function addBlock(block) { + buffer[writePtr] = block; + writePtr = (writePtr + 1) % BUFFER_SIZE; + + if (readyBlocks < BUFFER_SIZE) { + readyBlocks++; + } else { + readPtr = (readPtr + 1) % BUFFER_SIZE; + } + } + + function getNextBlock() { + if (readyBlocks === 0) return null; + + const block = buffer[readPtr]; + readPtr = (readPtr + 1) % BUFFER_SIZE; + readyBlocks--; + return block; + } + + function resetBuffer() { + writePtr = 0; + readPtr = 0; + readyBlocks = 0; + for (let i = 0; i < BUFFER_SIZE; i++) { + buffer[i] = null; + } + } + + + + + + const loadAudioDevices = async () => { try { const devices = await getAudioDevices(); @@ -37,5 +78,13 @@ export const useAudioStore = defineStore("audioStore", () => { audioOutputs, loadAudioDevices, rxStream, + + addBlock, + getNextBlock, + resetBuffer, + get bufferedBlockCount() { + return readyBlocks; + }, + }; }); From c283197bd6e1ce6f1e70eb1aa280edbba6ba6826 Mon Sep 17 00:00:00 2001 From: DJ2LS <75909252+DJ2LS@users.noreply.github.com> Date: Wed, 4 Jun 2025 11:18:30 +0200 Subject: [PATCH 007/141] adjusted streaming --- freedata_gui/src/store/audioStore.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/freedata_gui/src/store/audioStore.js b/freedata_gui/src/store/audioStore.js index c90f7a29b..6f7945b22 100644 --- a/freedata_gui/src/store/audioStore.js +++ b/freedata_gui/src/store/audioStore.js @@ -25,7 +25,7 @@ export const useAudioStore = defineStore("audioStore", () => { let readyBlocks = 0; function addBlock(block) { - buffer[writePtr] = block; + rxStreamBuffer[writePtr] = block; writePtr = (writePtr + 1) % BUFFER_SIZE; if (readyBlocks < BUFFER_SIZE) { @@ -38,7 +38,7 @@ export const useAudioStore = defineStore("audioStore", () => { function getNextBlock() { if (readyBlocks === 0) return null; - const block = buffer[readPtr]; + const block = rxStreamBuffer[readPtr]; readPtr = (readPtr + 1) % BUFFER_SIZE; readyBlocks--; return block; @@ -49,7 +49,7 @@ export const useAudioStore = defineStore("audioStore", () => { readPtr = 0; readyBlocks = 0; for (let i = 0; i < BUFFER_SIZE; i++) { - buffer[i] = null; + rxStreamBuffer[i] = null; } } @@ -78,7 +78,6 @@ export const useAudioStore = defineStore("audioStore", () => { audioOutputs, loadAudioDevices, rxStream, - addBlock, getNextBlock, resetBuffer, From 9f17b2bfc3d11de84ed7741b15a677d30a2b9546 Mon Sep 17 00:00:00 2001 From: DJ2LS <75909252+DJ2LS@users.noreply.github.com> Date: Wed, 4 Jun 2025 11:39:21 +0200 Subject: [PATCH 008/141] adjusted streaming --- .../src/components/grid/grid_audio.vue | 22 +++++++------------ 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/freedata_gui/src/components/grid/grid_audio.vue b/freedata_gui/src/components/grid/grid_audio.vue index 4360b2850..079674de6 100644 --- a/freedata_gui/src/components/grid/grid_audio.vue +++ b/freedata_gui/src/components/grid/grid_audio.vue @@ -6,27 +6,26 @@ setActivePinia(pinia); import { useAudioStore } from '@/store/audioStore'; const audio = useAudioStore(pinia); -let audioCtx = null; -let isPlaying = false; +var audioCtx = null; +var isPlaying = false; function playRxStream() { if (isPlaying) return; const SAMPLE_RATE = 8000; const BLOCK_SIZE = 600; - const BLOCK_DURATION_MS = (BLOCK_SIZE / SAMPLE_RATE) * 1000; // z. B. 75ms + const BLOCK_DURATION_MS = (BLOCK_SIZE / SAMPLE_RATE) * 1000; // ≈75ms audioCtx = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: SAMPLE_RATE }); + let scheduledTime = audioCtx.currentTime; isPlaying = true; - scheduledTime = audioCtx.currentTime; - + console.log("▶️ Echtzeit-Wiedergabe gestartet"); function loop() { if (!isPlaying) return; const block = audio.getNextBlock(); - if (block) { const float32 = Float32Array.from(block, s => s / 32768); const buffer = audioCtx.createBuffer(1, float32.length, SAMPLE_RATE); @@ -35,18 +34,13 @@ function playRxStream() { const source = audioCtx.createBufferSource(); source.buffer = buffer; source.connect(audioCtx.destination); - - const now = audioCtx.currentTime; - const playAt = Math.max(now, scheduledTime); - source.start(playAt); - scheduledTime = playAt + buffer.duration; - + source.start(0); } else { - console.warn("Buffer underrun"); + console.warn("⛔ Audio buffer underrun"); } - setTimeout(loop, 4); + setTimeout(loop, 4); } if (audioCtx.state === 'suspended') { From 5de53bfadf00dd6050bbb9bd116a75bbfa8f326e Mon Sep 17 00:00:00 2001 From: DJ2LS <75909252+DJ2LS@users.noreply.github.com> Date: Wed, 4 Jun 2025 14:06:26 +0200 Subject: [PATCH 009/141] adjusted streaming --- freedata_gui/src/components/grid/grid_audio.vue | 4 ++-- freedata_gui/src/store/audioStore.js | 2 +- freedata_server/modem.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/freedata_gui/src/components/grid/grid_audio.vue b/freedata_gui/src/components/grid/grid_audio.vue index 079674de6..51ba491d2 100644 --- a/freedata_gui/src/components/grid/grid_audio.vue +++ b/freedata_gui/src/components/grid/grid_audio.vue @@ -13,7 +13,7 @@ function playRxStream() { if (isPlaying) return; const SAMPLE_RATE = 8000; - const BLOCK_SIZE = 600; + const BLOCK_SIZE = 300; const BLOCK_DURATION_MS = (BLOCK_SIZE / SAMPLE_RATE) * 1000; // ≈75ms audioCtx = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: SAMPLE_RATE }); @@ -37,7 +37,7 @@ function playRxStream() { source.start(0); } else { - console.warn("⛔ Audio buffer underrun"); + //console.warn("⛔ Audio buffer underrun"); } setTimeout(loop, 4); diff --git a/freedata_gui/src/store/audioStore.js b/freedata_gui/src/store/audioStore.js index 6f7945b22..6a94f80e8 100644 --- a/freedata_gui/src/store/audioStore.js +++ b/freedata_gui/src/store/audioStore.js @@ -17,7 +17,7 @@ export const useAudioStore = defineStore("audioStore", () => { const audioOutputs = ref([]); const rxStream = ref([]); - const BUFFER_SIZE = 64; + const BUFFER_SIZE = 1024; const rxStreamBuffer = new Array(BUFFER_SIZE).fill(null); let writePtr = 0; diff --git a/freedata_server/modem.py b/freedata_server/modem.py index 5f85378de..86ca708ed 100644 --- a/freedata_server/modem.py +++ b/freedata_server/modem.py @@ -75,7 +75,7 @@ def __init__(self, ctx) -> None: self.MODE = 0 self.rms_counter = 0 - self.AUDIO_STREAMING_CHUNK_SIZE = 600 + self.AUDIO_STREAMING_CHUNK_SIZE = 2400 self.audio_out_queue = queue.Queue() # Make sure our resampler will work From e73a8ca6f6d568d836ddc23ddd43daa392a2f842 Mon Sep 17 00:00:00 2001 From: DJ2LS <75909252+DJ2LS@users.noreply.github.com> Date: Wed, 4 Jun 2025 20:27:39 +0200 Subject: [PATCH 010/141] adjusted streaming --- .../src/components/grid/grid_audio.vue | 51 ++++++++++++++----- freedata_gui/src/locales/de_Deutsch.json | 1 + freedata_gui/src/locales/en_English.json | 1 + 3 files changed, 41 insertions(+), 12 deletions(-) diff --git a/freedata_gui/src/components/grid/grid_audio.vue b/freedata_gui/src/components/grid/grid_audio.vue index 51ba491d2..0ec7599b7 100644 --- a/freedata_gui/src/components/grid/grid_audio.vue +++ b/freedata_gui/src/components/grid/grid_audio.vue @@ -1,29 +1,33 @@ - Start Audio - Stop Audio + + + + + {{ $t('grid.components.audiostream') }} + + + + + {{isPlaying ? 'Stop' : 'Start'}} + + + + + + diff --git a/freedata_gui/src/locales/de_Deutsch.json b/freedata_gui/src/locales/de_Deutsch.json index 2976ea333..ddee74bf4 100644 --- a/freedata_gui/src/locales/de_Deutsch.json +++ b/freedata_gui/src/locales/de_Deutsch.json @@ -85,6 +85,7 @@ "downloadpreset": "Einstell. Herunterladen", "downloadpreset_help": "Lade die GUI-Einstellungen herunter um sie zu speichern und zu teilen", "components": { + "audiostream": "Audio Stream", "tune": "Tune", "stop_help": "Sitzung abbrechen und Aussendung beenden", "transmissioncharts": "Übertragungs-Diagramme", diff --git a/freedata_gui/src/locales/en_English.json b/freedata_gui/src/locales/en_English.json index 5a34e612c..e05d3361e 100644 --- a/freedata_gui/src/locales/en_English.json +++ b/freedata_gui/src/locales/en_English.json @@ -85,6 +85,7 @@ "downloadpreset": "Download Preset", "downloadpreset_help": "Download preset file for sharing or saving", "components": { + "audiostream": "Audio Stream", "tune": "Tune", "stop_help": "Abort session and stop transmissions", "transmissioncharts": "Transmission charts", From 20ed1ad501a18e23ee69c9171295f2f0e36c358f Mon Sep 17 00:00:00 2001 From: DJ2LS <75909252+DJ2LS@users.noreply.github.com> Date: Wed, 4 Jun 2025 20:34:07 +0200 Subject: [PATCH 011/141] adjusted streaming --- freedata_gui/src/components/dynamic_components.vue | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freedata_gui/src/components/dynamic_components.vue b/freedata_gui/src/components/dynamic_components.vue index cb5743ffe..c966023e7 100644 --- a/freedata_gui/src/components/dynamic_components.vue +++ b/freedata_gui/src/components/dynamic_components.vue @@ -329,14 +329,14 @@ const gridWidgets = [ ), new gridWidget( grid_audio, - { x: 16, y: 8, w: 2, h: 8 }, - "Audio widget", + { x: 16, y: 8, w: 4, h: 24 }, + "Audio Stream", false, true, "Audio", 24, false, - { x: 16, y: 8, w: 2, h: 8 } + { x: 16, y: 8, w: 4, h: 24 } ) //Next new widget ID should be 24 ]; From 5433a001b47fed2bc3d7c5450b9f9146c7a65126 Mon Sep 17 00:00:00 2001 From: DJ2LS <75909252+DJ2LS@users.noreply.github.com> Date: Sun, 22 Jun 2025 21:49:36 +0200 Subject: [PATCH 012/141] first basic work on NORM implementation --- freedata_server/command_norm.py | 47 +++++ freedata_server/data_frame_factory.py | 98 +++++++++- freedata_server/frame_dispatcher.py | 4 + freedata_server/frame_handler.py | 20 +- freedata_server/frame_handler_norm.py | 22 +++ freedata_server/modem_frametypes.py | 4 + freedata_server/norm/__init__.py | 0 freedata_server/norm/norm_transmission.py | 123 +++++++++++++ freedata_server/norm/norm_transmission_irs.py | 42 +++++ freedata_server/norm/norm_transmission_iss.py | 83 +++++++++ .../norm/norm_transmission_resend.py | 1 + tests/test_norm_protocol.py | 174 ++++++++++++++++++ 12 files changed, 606 insertions(+), 12 deletions(-) create mode 100644 freedata_server/command_norm.py create mode 100644 freedata_server/frame_handler_norm.py create mode 100644 freedata_server/norm/__init__.py create mode 100644 freedata_server/norm/norm_transmission.py create mode 100644 freedata_server/norm/norm_transmission_irs.py create mode 100644 freedata_server/norm/norm_transmission_iss.py create mode 100644 freedata_server/norm/norm_transmission_resend.py create mode 100644 tests/test_norm_protocol.py diff --git a/freedata_server/command_norm.py b/freedata_server/command_norm.py new file mode 100644 index 000000000..4947f4e0c --- /dev/null +++ b/freedata_server/command_norm.py @@ -0,0 +1,47 @@ +import queue +from command import TxCommand +import api_validations +import base64 +from queue import Queue +import numpy as np +import threading +from norm.norm_transmission_iss import NormTransmissionISS + +class Norm(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.domain = apiParams['domain'] + if not api_validations.validate_freedata_callsign(self.domain): + self.domain = f"{self.domain}-0" + + self.data = base64.b64decode(apiParams['data']) + + if 'priority' not in apiParams: + self.priority = 1 + else: + self.priority = apiParams['priority'] + + self.msgtype = apiParams['type'] + self.gridsquare = apiParams['gridsquare'] + + + def run(self): + try: + self.emit_event() + self.logger.info(self.log_message()) + + # wait some random time and wait if we have an ongoing codec2 transmission + # on our channel. This should prevent some packet collision + random_delay = np.random.randint(0, 6) + threading.Event().wait(random_delay) + self.ctx.state_manager.channel_busy_condition_codec2.wait(0.5) + + NormTransmissionISS(self.ctx, self.origin, self.domain, self.gridsquare, self.data, self.priority, self.msgtype).prepare_and_transmit() + + except Exception as e: + self.log(f"Error starting NORM transmission: {e}", isWarning=True) + + return False \ No newline at end of file diff --git a/freedata_server/data_frame_factory.py b/freedata_server/data_frame_factory.py index 1c2d4435c..344a4eaf3 100644 --- a/freedata_server/data_frame_factory.py +++ b/freedata_server/data_frame_factory.py @@ -8,6 +8,7 @@ class DataFrameFactory: LENGTH_SIG0_FRAME = 14 LENGTH_SIG1_FRAME = 14 LENGTH_ACK_FRAME = 3 + LENGTH_NORM_FRAME = 126 """ helpers.set_flag(byte, 'DATA-ACK-NACK', True, FLAG_POSITIONS) @@ -28,6 +29,10 @@ class DataFrameFactory: 'ANNOUNCE_ARQ': 1, # Bit-position for announcing an ARQ session } + NORM_FLAGS = { + 'LAST_DATA': 0, # Bit-position for indicating the LAST DATA state + } + def __init__(self, ctx): self.ctx = ctx @@ -41,6 +46,7 @@ def __init__(self, ctx): self._load_ping_templates() self._load_arq_templates() self._load_p2p_connection_templates() + self._load_norm_templates() def _load_broadcast_templates(self): # cq frame @@ -224,7 +230,50 @@ def _load_p2p_connection_templates(self): "session_id": 1, } + def _load_norm_templates(self): + # data frame + self.template_list[FR_TYPE.NORM_DATA.value] = { + "frame_length": self.LENGTH_NORM_FRAME, + "origin": 6, + "domain": 6, + "gridsquare": 4, + "flag": 1, + "timestamp": 4, + "burst_info": 1, + "payload_size": 1, + "payload_data": 30 + } + # repair frame + # FIXME + self.template_list[FR_TYPE.NORM_REPAIR.value] = { + "frame_length": self.LENGTH_NORM_FRAME, + "origin": 6, + "domain": 6, + "flag": 1, + "timestamp": 4, + "burst_info": 1, + "payload_size": 1, + "payload_data": 34 + } + + # nack frame + # FIXME + self.template_list[FR_TYPE.NORM_NACK.value] = { + "frame_length": self.LENGTH_NORM_FRAME, + "origin": 6, + "domain": 4, + "flag": 2 + } + + # cmd frame + # FIXME + self.template_list[FR_TYPE.NORM_CMD.value] = { + "frame_length": self.LENGTH_NORM_FRAME, + "origin": 6, + "domain": 4, + "flag": 1 + } def construct(self, frametype, content, frame_length = LENGTH_SIG1_FRAME): frame_template = self.template_list[frametype.value] @@ -250,14 +299,13 @@ def construct(self, frametype, content, frame_length = LENGTH_SIG1_FRAME): #print(item_length) #print(content) if buffer_position + item_length > frame_length: - raise OverflowError("Frame data overflow!") + raise OverflowError(f"Frame data overflow! {buffer_position + item_length} of max {frame_length}") frame[buffer_position: buffer_position + item_length] = content[key] buffer_position += item_length return frame def deconstruct(self, frame, mode_name=None): - buffer_position = 1 # Handle the case where the frame type is not recognized #raise ValueError(f"Unknown frame type: {frametype}") @@ -266,6 +314,9 @@ def deconstruct(self, frame, mode_name=None): frame_template = self.template_list.get(frametype) frame = bytes([frametype]) + frame else: + print("------------------------") + print(frame) + print(type(frame)) # Extract frametype and get the corresponding template frametype = int.from_bytes(frame[:1], "big") frame_template = self.template_list.get(frametype) @@ -284,9 +335,12 @@ def deconstruct(self, frame, mode_name=None): data = frame[buffer_position: buffer_position + item_length] # Process the data based on the key - if key in ["origin", "destination"]: + if key in ["origin", "destination", "domain"]: extracted_data[key] = helpers.bytes_to_callsign(data).decode() + elif key in ["payload_data"]: + extracted_data[key] = data + elif key in ["origin_crc", "destination_crc", "total_crc"]: extracted_data[key] = data.hex() @@ -295,9 +349,9 @@ def deconstruct(self, frame, mode_name=None): elif key in ["session_id", "speed_level", "frames_per_burst", "version", - "offset", "total_length", "state", "type", "maximum_bandwidth", "protocol_version"]: + "offset", "total_length", "state", "type", "maximum_bandwidth", "protocol_version", "burst_info", "timestamp", "payload_size"]: extracted_data[key] = int.from_bytes(data, 'big') - + print(key, data) elif key in ["snr"]: extracted_data[key] = helpers.snr_from_bytes(data) @@ -327,6 +381,14 @@ def deconstruct(self, frame, mode_name=None): # 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.NORM_DATA.value, FR_TYPE.NORM_NACK.value, FR_TYPE.NORM_REPAIR.value, FR_TYPE.NORM_CMD.value]: + extracted_data[key] = data + # flag_dict = self.NORM_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 @@ -604,3 +666,29 @@ def build_p2p_connection_disconnect_ack(self, session_id): "session_id": session_id.to_bytes(1, 'big'), } return self.construct(FR_TYPE.P2P_CONNECTION_DISCONNECT_ACK, payload) + + def build_norm_data(self, origin, domain, gridsquare, timestamp, burst_info, payload_size, payload_data, flag): + + payload = { + "origin": helpers.callsign_to_bytes(origin), + "domain": helpers.callsign_to_bytes(domain), + "gridsquare": helpers.encode_grid(gridsquare), + "flag": flag.to_bytes(1, 'big'), + "timestamp": timestamp.to_bytes(4, 'big'), + "burst_info": burst_info.to_bytes(1, 'big'), + "payload_size": payload_size.to_bytes(1, 'big'), + "payload_data": payload_data, + } + return self.construct(FR_TYPE.NORM_DATA, payload) + + + + + def build_norm_nack(self): + pass + + def build_norm_repair(self, origin, domain, timestamp, burst_info, payload_size, payload, flag=None): + pass + + def build_norm_cmd(self): + pass \ No newline at end of file diff --git a/freedata_server/frame_dispatcher.py b/freedata_server/frame_dispatcher.py index 37439ca91..0a9251f19 100644 --- a/freedata_server/frame_dispatcher.py +++ b/freedata_server/frame_dispatcher.py @@ -15,6 +15,7 @@ from frame_handler_arq_session import ARQFrameHandler from frame_handler_p2p_connection import P2PConnectionFrameHandler from frame_handler_beacon import BeaconFrameHandler +from frame_handler_norm import NORMFrameHandler @@ -54,6 +55,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.NORM_DATA.value: {"class": NORMFrameHandler, "name": "NORM DATA"}, + + #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"}, diff --git a/freedata_server/frame_handler.py b/freedata_server/frame_handler.py index 70ff03e86..3a5b87e9a 100644 --- a/freedata_server/frame_handler.py +++ b/freedata_server/frame_handler.py @@ -7,7 +7,7 @@ from message_system_db_manager import DatabaseManager from message_system_db_station import DatabaseManagerStations from message_system_db_messages import DatabaseManagerMessages - +from collections.abc import Iterable import maidenhead TESTMODE = False @@ -81,6 +81,13 @@ def is_frame_for_me(self): if session_id in self.ctx.state_manager.arq_iss_sessions: valid = True + # check for NORM data + elif ft in ['NORM_DATA']: + # TODO + # maybe we can add a list of domains, we are listening to in state manager? + valid = True + + # check for p2p connection elif ft in ['P2P_CONNECTION_CONNECT']: #Need to make sure this does not affect any other features in FreeDATA. @@ -184,7 +191,7 @@ def add_to_activity_list(self): if "session_id" in frame: activity["session_id"] = frame["session_id"] - if "flag" in frame: + if "flag" in frame and isinstance(frame["flag"], (list, dict, Iterable)): if "AWAY_FROM_KEY" in frame["flag"]: activity["away_from_key"] = frame["flag"]["AWAY_FROM_KEY"] @@ -216,7 +223,7 @@ def add_to_heard_stations(self): distance_miles = distance_dict['miles'] away_from_key = False - if "flag" in self.details['frame']: + if "flag" in self.details['frame'] and isinstance(frame["flag"], (list, dict, Iterable)): if "AWAY_FROM_KEY" in self.details['frame']["flag"]: away_from_key = self.details['frame']["flag"]["AWAY_FROM_KEY"] @@ -265,7 +272,9 @@ def make_event(self): event['distance_kilometers'] = 0 event['distance_miles'] = 0 - if "flag" in self.details['frame'] and "AWAY_FROM_KEY" in self.details['frame']["flag"]: + + + if "flag" in self.details and isinstance(self.details["flag"], (list, dict, Iterable)) and "AWAY_FROM_KEY" in self.details['frame']["flag"]: event['away_from_key'] = self.details['frame']["flag"]["AWAY_FROM_KEY"] return event @@ -279,7 +288,6 @@ def emit_event(self): broadcasts this event through the event manager. """ event_data = self.make_event() - print(event_data) self.ctx.event_manager.broadcast(event_data) def get_tx_mode(self): @@ -347,8 +355,6 @@ def handle(self, frame, snr, frequency_offset, freedv_inst, bytes_per_frame): self.details['freedv_inst'] = freedv_inst self.details['bytes_per_frame'] = bytes_per_frame - print(self.details) - if 'origin' not in self.details['frame'] and 'session_id' in self.details['frame']: dxcall = self.ctx.state_manager.get_dxcall_by_session_id(self.details['frame']['session_id']) if dxcall: diff --git a/freedata_server/frame_handler_norm.py b/freedata_server/frame_handler_norm.py new file mode 100644 index 000000000..d058eb103 --- /dev/null +++ b/freedata_server/frame_handler_norm.py @@ -0,0 +1,22 @@ +import threading + +import frame_handler_ping +import helpers +import data_frame_factory +import frame_handler +from message_system_db_messages import DatabaseManagerMessages +import numpy as np + +from norm.norm_transmission_irs import NormTransmissionIRS + + +class NORMFrameHandler(frame_handler.FrameHandler): + + def follow_protocol(self): + #self.logger.debug(f"[NORM] handling burst:{self.details}") + + #origin = self.details["frame"]["origin"] + #print(origin) + + + NormTransmissionIRS(self.details["frame"]) \ No newline at end of file diff --git a/freedata_server/modem_frametypes.py b/freedata_server/modem_frametypes.py index 5b524dbfa..6f802b43f 100644 --- a/freedata_server/modem_frametypes.py +++ b/freedata_server/modem_frametypes.py @@ -25,6 +25,10 @@ class FRAME_TYPE(Enum): #MESH_BROADCAST = 100 #MESH_SIGNALLING_PING = 101 #MESH_SIGNALLING_PING_ACK = 102 + NORM_DATA = 100 + NORM_NACK = 101 + NORM_REPAIR = 102 + NORM_CMD = 103 CQ = 200 QRV = 201 PING = 210 diff --git a/freedata_server/norm/__init__.py b/freedata_server/norm/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/freedata_server/norm/norm_transmission.py b/freedata_server/norm/norm_transmission.py new file mode 100644 index 000000000..c7cef287b --- /dev/null +++ b/freedata_server/norm/norm_transmission.py @@ -0,0 +1,123 @@ +# base class for norm transmission + + +import structlog +import time +import data_frame_factory +from enum import IntEnum + +class NORMMsgType(IntEnum): + UNDEFINED = 0 + MESSAGE = 1 # Generic text/data message + POSITION = 2 # GPS or grid locator info + SITREP = 3 # Situation report + PING = 4 # Ping or keepalive + ACK = 5 # Acknowledgement + COMMAND = 6 # Control or remote command + STATUS = 7 # System or device status + ALERT = 8 # High-priority broadcast + + +class NORMMsgPriority(IntEnum): + LOW = 0 + NORMAL = 1 + HIGH = 2 + CRITICAL = 3 + ALERT = 4 + EMERGENCY = 5 + +class NormTransmission: + def __init__(self, ctx, origin, domain): + self.logger = structlog.get_logger(type(self).__name__) + self.ctx = ctx + self.origin = origin + self.domain = domain + + self.frame_factory = data_frame_factory.DataFrameFactory(self.ctx) + + def log(self, message, isWarning=False): + """Logs a message with session context. + + Logs a message, including the class name, session ID, and current state, + using the appropriate log level (warning or info). + + Args: + message: The message to be logged. + isWarning: A boolean indicating whether the message should be logged as a warning. + """ + msg = f"[{type(self).__name__}][origin={self.origin},domain={self.domain}][state={self.state.name}]: {message}" + logger = self.logger.warn if isWarning else self.logger.info + logger(msg) + + def set_state(self, state): + + self.last_state_change_timestamp = time.time() + 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} at {self.last_state_change_timestamp}") + self.state = state + + def on_frame_received(self, frame): + """Handles received frames based on the current session state. + + This method processes incoming frames, triggering state transitions and + data handling based on the frame type and current session state. + It logs received frame types and ignores unknown state transitions. + + Args: + frame: The received frame. + """ + 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 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 encode_flags(self, msg_type, priority, is_last): + """ + Encodes message type, priority and 'last burst' flag into a single byte. + + Bit layout: + Bit 7 → is_last (1 = letzter Burst) + Bits 6–3 → msg_type (0–15) + Bits 2–0 → priority (0–7) + """ + if isinstance(msg_type, IntEnum): # e.g., MsgType + msg_type = int(msg_type) + + assert 0 <= msg_type <= 15, "msg_type must be 0–15" + assert 0 <= priority <= 7, "priority must be 0–7" + + return ((1 if is_last else 0) << 7) | ((msg_type & 0x0F) << 3) | (priority & 0x07) + + + def decode_flags(self, flags): + """ + Decodes a flags byte into (is_last, msg_type, priority). + + Bit layout: + Bit 7 → is_last + Bits 6–3 → msg_type (0–15) + Bits 2–0 → priority (0–7) + """ + is_last = bool((flags >> 7) & 0x01) + msg_type = (flags >> 3) & 0x0F + priority = flags & 0x07 + return is_last, msg_type, priority + + def encode_burst_info(self, burst_number, total_bursts): + return ((burst_number & 0x0F) << 4) | (total_bursts & 0x0F) + + def decode_burst_info(self, burst_info): + burst_number = (burst_info >> 4) & 0x0F + burst_total = burst_info & 0x0F + return burst_number, burst_total \ No newline at end of file diff --git a/freedata_server/norm/norm_transmission_irs.py b/freedata_server/norm/norm_transmission_irs.py new file mode 100644 index 000000000..c2aa938f5 --- /dev/null +++ b/freedata_server/norm/norm_transmission_irs.py @@ -0,0 +1,42 @@ +# file for handling received data +from norm.norm_transmission import NormTransmission + +class NormTransmissionIRS(NormTransmission): + MAX_PAYLOAD_SIZE = 96 + + def __init__(self, frame): + print("burst:", frame) + + is_last, msg_type, priority = self.decode_flags(frame["flag"]) + burst_number, total_bursts = self.decode_burst_info(frame["burst_info"]) + payload_size = frame["payload_size"] + payload_data = frame["payload_data"] + self.origin = frame["origin"] + self.domain = frame["domain"] + self.gridsquare = frame["gridsquare"] + + # FIXME + #if payload_size > len(frame["payload_data"]) and total_bursts > 1: + # payload_data = frame["payload_data"] + #else: + # payload_data = frame["payload_data"][:self.MAX_PAYLOAD_SIZE * burst_number] + + + + + print("####################################") + + print("payload_size:", payload_size) + print("payload_data:", payload_data) + + print("origin", self.origin) + print("domain", self.domain) + print("gridsquare", self.gridsquare) + print("is_last", is_last) + print("msg_type", msg_type) + print("priority", priority) + print("burst_number", burst_number) + print("total_bursts", total_bursts) + + # TODO: + # add data to database or update it \ No newline at end of file diff --git a/freedata_server/norm/norm_transmission_iss.py b/freedata_server/norm/norm_transmission_iss.py new file mode 100644 index 000000000..564e14533 --- /dev/null +++ b/freedata_server/norm/norm_transmission_iss.py @@ -0,0 +1,83 @@ +# file for handling transmitting data +from norm.norm_transmission import NormTransmission +from norm.norm_transmission import NORMMsgType, NORMMsgPriority +from enum import Enum +import time +from codec2 import FREEDV_MODE + +class NORM_ISS_State(Enum): + NEW = 0 + TRANSMITTING = 1 + ENDED = 2 + FAILED = 3 + ABORTING = 4 + ABORTED = 5 + +class NormTransmissionISS(NormTransmission): + MAX_PAYLOAD_SIZE = 96 + + def __init__(self, ctx, origin, domain, gridsquare, data, priority=NORMMsgPriority.NORMAL, message_type=NORMMsgType.UNDEFINED): + + super().__init__(ctx, origin, domain) + self.ctx = ctx + self.origin = origin + self.domain = domain + self.gridsquare = gridsquare + self.data = data + self.priority = priority + self.message_type = message_type + self.payload_size = len(data) + + self.timestamp = int(time.time()) + + self.state = NORM_ISS_State.NEW + + self.log("Initialized") + + def prepare_and_transmit(self): + bursts = self.create_bursts() + self.transmit_bursts(bursts) + + def create_bursts(self): + self.message_type = NORMMsgType.MESSAGE + self.message_priority = NORMMsgPriority.NORMAL + + + full_data = self.data + + total_bursts = (len(full_data) + self.MAX_PAYLOAD_SIZE - 1) // self.MAX_PAYLOAD_SIZE + bursts = [] + + for burst_number in range(1, total_bursts + 1): + offset = (burst_number-1) * self.MAX_PAYLOAD_SIZE + payload = full_data[offset: offset + self.MAX_PAYLOAD_SIZE] + + burst_info = self.encode_burst_info(burst_number, total_bursts) + + # set flag for last burst + is_last = (burst_number == total_bursts - 1) + flags = self.encode_flags( + msg_type=self.message_type, + priority=self.message_priority, + is_last=is_last + ) + + burst_frame = self.frame_factory.build_norm_data( + origin=self.origin, + domain=self.domain, + gridsquare=self.gridsquare, + timestamp=self.timestamp, + burst_info=burst_info, + payload_size=len(payload), + payload_data=payload, + flag=flags + ) + print(burst_frame) + bursts.append(burst_frame) + + return bursts + + def transmit_bursts(self, bursts): + + for burst in bursts: + self.ctx.rf_modem.transmit(FREEDV_MODE.datac4, 1, 100, burst) \ No newline at end of file diff --git a/freedata_server/norm/norm_transmission_resend.py b/freedata_server/norm/norm_transmission_resend.py new file mode 100644 index 000000000..cba14075d --- /dev/null +++ b/freedata_server/norm/norm_transmission_resend.py @@ -0,0 +1 @@ +# file for "healing requested transmissions... \ No newline at end of file diff --git a/tests/test_norm_protocol.py b/tests/test_norm_protocol.py new file mode 100644 index 000000000..1654bcb03 --- /dev/null +++ b/tests/test_norm_protocol.py @@ -0,0 +1,174 @@ +import sys +import time +import unittest +import unittest.mock +import queue +import threading +import random +import structlog +import base64 +import numpy as np + +sys.path.append('freedata_server') + +from config import CONFIG +from context import AppContext +from event_manager import EventManager +from state_manager import StateManager +from data_frame_factory import DataFrameFactory +from frame_dispatcher import DISPATCHER +import codec2 +import command_norm + + +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: + tx_time = self.getFrameTransmissionTime(mode) + 0.1 + 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 TestMessageProtocol(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.logger = structlog.get_logger("TESTS") + + # ISS + + cls.ctx_ISS = AppContext('freedata_server/config.ini.example') + cls.ctx_ISS.TESTMODE = True + cls.ctx_ISS.startup() + + + # IRS + cls.ctx_IRS = AppContext('freedata_server/config.ini.example') + cls.ctx_IRS.TESTMODE = True + cls.ctx_IRS.startup() + + # simulate a busy condition + cls.ctx_IRS.state_manager.channel_busy_slot = [True, False, False, False, False] + # Frame loss probability in % + cls.loss_probability = 0 + + + cls.channels_running = False + + @classmethod + def tearDownClass(cls): + cls.ctx_IRS.shutdown() + cls.ctx_ISS.shutdown() + + + def channelWorker(self, ctx_a, ctx_b): + while self.channels_running: + try: + # Station A gets the data from its transmit queue + transmission = ctx_a.TESTMODE_TRANSMIT_QUEUE.get(timeout=1) + print(f"Station A sending: {transmission[1]}", len(transmission[1]), transmission[0]) + + transmission[1] += bytes(2) # 2bytes crc simulation + + if random.randint(0, 100) < self.loss_probability: + self.logger.info(f"[{threading.current_thread().name}] Frame lost...") + continue + + # Forward data from Station A to Station B's receive queue + if ctx_b: + for burst in transmission: + ctx_b.TESTMODE_RECEIVE_QUEUE.put(burst) + self.logger.info(f"Data forwarded to Station B") + + frame_bytes = transmission[1] + if len(frame_bytes) == 5: + mode_name = "SIGNALLING_ACK" + else: + mode_name = transmission[0] + + snr = 15 + ctx_b.service_manager.frame_dispatcher.process_data( + frame_bytes, None, len(frame_bytes), snr, 0, mode_name=mode_name + ) + + except queue.Empty: + continue + + self.logger.info(f"[{threading.current_thread().name}] Channel closed.") + + def waitForSession(self, event_queue, outbound=False): + key = 'arq-transfer-outbound' if outbound else 'arq-transfer-inbound' + while self.channels_running: + try: + ev = event_queue.get(timeout=2) + if key in ev and ('success' in ev[key] or 'ABORTED' in ev[key]): + self.logger.info(f"[{threading.current_thread().name}] {key} session ended.") + break + except queue.Empty: + continue + + def establishChannels(self): + self.channels_running = True + self.channelA = threading.Thread(target=self.channelWorker,args=[self.ctx_ISS, self.ctx_IRS],name = "channelA") + self.channelA.start() + + self.channelB = threading.Thread(target=self.channelWorker,args=[self.ctx_IRS, self.ctx_ISS],name = "channelB") + self.channelB.start() + + def waitAndCloseChannels(self): + self.waitForSession(self.ctx_ISS.modem_events, True) + self.channels_running = False + self.waitForSession(self.ctx_IRS.modem_events, False) + self.channels_running = False + + def testNormBroadcast(self): + self.loss_probability = 0 # no loss + self.establishChannels() + + params = { + 'origin': "AA1AAA-1", + 'domain': "BB1BBB-1", + 'gridsquare': "JN48ea", + 'type': 'MESSAGE', + 'priority': '1', + 'data': str(base64.b64encode(b"hello world!"), 'utf-8') + } + try: + command = command_norm.Norm(self.ctx_ISS, params) + command.run() + except Exception as e: + print(e) + + + #del cmd + #print(self.ctx_ISS.TESTMODE_EVENTS.empty()) + + while not self.ctx_ISS.TESTMODE_EVENTS.empty(): + event = self.ctx_ISS.TESTMODE_EVENTS.get() + success = event.get('arq-transfer-outbound', {}).get('success', None) + if success is not None: + self.assertTrue(success, f"Test failed because of wrong success: {success}") + +if __name__ == '__main__': + unittest.main() From 870e6e000168a8ecad0eb68802616a057a9d37fc Mon Sep 17 00:00:00 2001 From: DJ2LS <75909252+DJ2LS@users.noreply.github.com> Date: Sun, 22 Jun 2025 23:20:57 +0200 Subject: [PATCH 013/141] save data to database --- freedata_server/constants.py | 2 +- freedata_server/data_frame_factory.py | 6 +- freedata_server/frame_handler_norm.py | 2 +- .../message_system_db_broadcasts.py | 117 ++++++++++++++++++ freedata_server/message_system_db_model.py | 47 +++++++ freedata_server/norm/norm_transmission.py | 10 +- freedata_server/norm/norm_transmission_irs.py | 57 +++++++-- freedata_server/norm/norm_transmission_iss.py | 7 +- 8 files changed, 227 insertions(+), 21 deletions(-) create mode 100644 freedata_server/message_system_db_broadcasts.py diff --git a/freedata_server/constants.py b/freedata_server/constants.py index b40a436c9..e73bf3dd3 100644 --- a/freedata_server/constants.py +++ b/freedata_server/constants.py @@ -8,4 +8,4 @@ DOCUMENTATION_URL = 'https://wiki.freedata.app' STATS_API_URL = 'https://api.freedata.app/stats.php' EXPLORER_API_URL = 'https://api.freedata.app/explorer.php' -MESSAGE_SYSTEM_DATABASE_VERSION = 0 \ No newline at end of file +MESSAGE_SYSTEM_DATABASE_VERSION = 1 \ No newline at end of file diff --git a/freedata_server/data_frame_factory.py b/freedata_server/data_frame_factory.py index 344a4eaf3..25ab93343 100644 --- a/freedata_server/data_frame_factory.py +++ b/freedata_server/data_frame_factory.py @@ -240,6 +240,7 @@ def _load_norm_templates(self): "flag": 1, "timestamp": 4, "burst_info": 1, + "checksum": 3, "payload_size": 1, "payload_data": 30 } @@ -341,7 +342,7 @@ def deconstruct(self, frame, mode_name=None): elif key in ["payload_data"]: extracted_data[key] = data - elif key in ["origin_crc", "destination_crc", "total_crc"]: + elif key in ["origin_crc", "destination_crc", "total_crc", "checksum"]: extracted_data[key] = data.hex() elif key == "gridsquare": @@ -667,7 +668,7 @@ def build_p2p_connection_disconnect_ack(self, session_id): } return self.construct(FR_TYPE.P2P_CONNECTION_DISCONNECT_ACK, payload) - def build_norm_data(self, origin, domain, gridsquare, timestamp, burst_info, payload_size, payload_data, flag): + def build_norm_data(self, origin, domain, gridsquare, timestamp, burst_info, payload_size, payload_data, flag, checksum): payload = { "origin": helpers.callsign_to_bytes(origin), @@ -678,6 +679,7 @@ def build_norm_data(self, origin, domain, gridsquare, timestamp, burst_info, pay "burst_info": burst_info.to_bytes(1, 'big'), "payload_size": payload_size.to_bytes(1, 'big'), "payload_data": payload_data, + "checksum": checksum } return self.construct(FR_TYPE.NORM_DATA, payload) diff --git a/freedata_server/frame_handler_norm.py b/freedata_server/frame_handler_norm.py index d058eb103..eb293cf29 100644 --- a/freedata_server/frame_handler_norm.py +++ b/freedata_server/frame_handler_norm.py @@ -19,4 +19,4 @@ def follow_protocol(self): #print(origin) - NormTransmissionIRS(self.details["frame"]) \ No newline at end of file + NormTransmissionIRS(self.ctx, self.details["frame"]) \ No newline at end of file diff --git a/freedata_server/message_system_db_broadcasts.py b/freedata_server/message_system_db_broadcasts.py new file mode 100644 index 000000000..6b6bc8138 --- /dev/null +++ b/freedata_server/message_system_db_broadcasts.py @@ -0,0 +1,117 @@ +from message_system_db_manager import DatabaseManager +from message_system_db_attachments import DatabaseManagerAttachments +from message_system_db_model import Status, BroadcastMessage +from message_system_db_station import DatabaseManagerStations +from sqlalchemy.exc import IntegrityError +from datetime import datetime, timedelta +import json +import os +from exceptions import MessageStatusError +import helpers +class DatabaseManagerBroadcasts(DatabaseManager): + + def __init__(self, ctx): + super().__init__(ctx) + + self.stations_manager = DatabaseManagerStations(self.ctx) + + def process_broadcast_message( + self, + id: str, + origin: str, + burst_index: int, + burst_data: str, + total_bursts: int, + checksum: str, + repairing_callsigns: dict = None, + domain: str = None, + gridsquare: str = None, + msg_type: str = None, + received_at: datetime = None, + expires_at: datetime = None, + priority: int = 1, + is_read: bool = True, + status: str = "queued", + error_reason: str = None + ) -> bool: + """ + Handles both creation of a new broadcast message and addition of bursts. + + If the message does not exist, it will be created. + If it exists, the burst will be added. + When all bursts are present, the final payload will be assembled and CRC checked. + """ + session = self.get_thread_scoped_session() + try: + # Try to find existing message + msg = session.query(BroadcastMessage).filter_by(id=id).first() + + if not msg: + # Create station and status + origin_station = self.stations_manager.get_or_create_station(origin, session) + status_obj = self.get_or_create_status(session, status) if status else None + received_at = received_at or datetime.utcnow() + + # New message + msg = BroadcastMessage( + id=id, + origin=origin_station.callsign, + repairing_callsigns=repairing_callsigns, + domain=domain, + gridsquare=gridsquare, + priority=priority, + is_read=is_read, + payload_size=0, + payload_data={"bursts": {str(burst_index): burst_data}}, + msg_type=msg_type, + total_bursts=total_bursts, + checksum=checksum, + received_at=received_at, + expires_at=expires_at, + status_id=status_obj.id if status_obj else None, + error_reason=error_reason + ) + session.add(msg) + self.log(f"Created new broadcast message {id}") + + else: + # Add burst to existing message + if not msg.payload_data: + msg.payload_data = {} + + if "bursts" not in msg.payload_data: + msg.payload_data["bursts"] = {} + + msg.payload_data["bursts"][str(burst_index)] = burst_data + self.log(f"Added burst {burst_index} to message {id}") + + # Check for final assembly + received = msg.payload_data["bursts"] + total = msg.total_bursts or 0 + + if total > 0 and len(received) == total and all(str(i) in received for i in range(total)): + ordered = [received[str(i)] for i in range(total)] + final = "".join(ordered) + + # CRC check + crc = helpers.get_crc_24(final) + if msg.checksum is None: + self.log(f"Missing checksum for {id}", isWarning=True) + elif crc != msg.checksum: + self.log(f"Checksum mismatch for {id}: expected {msg.checksum}, got {crc}", isWarning=True) + else: + msg.payload_data["final"] = final + msg.payload_size = len(final.encode("utf-8")) + self.log(f"Final payload assembled and verified for {id}") + + session.commit() + self.ctx.event_manager.freedata_message_db_change(message_id=msg.id) + return True + + except Exception as e: + session.rollback() + self.log(f"Error processing broadcast message {id}: {e}", isWarning=True) + return False + + finally: + session.remove() diff --git a/freedata_server/message_system_db_model.py b/freedata_server/message_system_db_model.py index 8c962f205..41066502c 100644 --- a/freedata_server/message_system_db_model.py +++ b/freedata_server/message_system_db_model.py @@ -2,6 +2,7 @@ from sqlalchemy import Index, Boolean, Column, String, Integer, JSON, ForeignKey, DateTime from sqlalchemy.orm import declarative_base, relationship +from datetime import datetime Base = declarative_base() @@ -195,3 +196,49 @@ def to_dict(self): 'checksum_crc32': self.checksum_crc32, 'hash_sha512' : self.hash_sha512 } + + +class BroadcastMessage(Base): + __tablename__ = 'broadcast_messages' + + id = Column(String, primary_key=True) + origin = Column(String, ForeignKey('station.callsign')) + repairing_callsigns = Column(JSON, nullable=True) + domain = Column(String) + gridsquare = Column(String) + frequency = Column(Integer, default=0) + priority = Column(Integer, default=1) + is_read = Column(Boolean, default=True) + payload_size = Column(Integer, default=0) + payload_data = Column(JSON, nullable=True) + msg_type = Column(String) + total_bursts = Column(Integer, default=0) + checksum = Column(String) + received_at = Column(DateTime, default=datetime.utcnow) + expires_at = Column(DateTime, nullable=True) + status_id = Column(Integer, ForeignKey('status.id'), nullable=True) + status = relationship('Status', backref='broadcast_messages') + error_reason = Column(String, nullable=True) + + Index('idx_broadcast_domain_received', 'domain', 'received_at') + + def to_dict(self): + return { + 'id': self.id, + 'origin': self.origin, + 'repairing_callsigns': self.repairing_callsigns, + 'domain': self.domain, + 'gridsquare': self.gridsquare, + 'frequency': self.frequency, + 'priority': self.priority, + 'is_read': self.is_read, + 'payload_size': self.payload_size, + 'payload_data': self.payload_data, + 'msg_type': self.msg_type, + 'total_bursts': self.total_bursts, + 'checksum': self.checksum, + 'received_at': self.received_at.isoformat() if self.received_at else None, + 'expires_at': self.expires_at.isoformat() if self.expires_at else None, + 'status': self.status.name if self.status else None, + 'error_reason': self.error_reason + } diff --git a/freedata_server/norm/norm_transmission.py b/freedata_server/norm/norm_transmission.py index c7cef287b..04ad95a0b 100644 --- a/freedata_server/norm/norm_transmission.py +++ b/freedata_server/norm/norm_transmission.py @@ -5,6 +5,8 @@ import time import data_frame_factory from enum import IntEnum +import hashlib + class NORMMsgType(IntEnum): UNDEFINED = 0 @@ -120,4 +122,10 @@ def encode_burst_info(self, burst_number, total_bursts): def decode_burst_info(self, burst_info): burst_number = (burst_info >> 4) & 0x0F burst_total = burst_info & 0x0F - return burst_number, burst_total \ No newline at end of file + return burst_number, burst_total + + + def create_broadcast_id(self, timestamp: int, domain: str, checksum: str, length: int = 10) -> str: + raw = f"{self}|{timestamp}|{domain}|{checksum}" + digest = hashlib.blake2s(raw.encode(), digest_size=6).hexdigest() + return f"bc_{digest[:max(8, min(length, 12))]}" diff --git a/freedata_server/norm/norm_transmission_irs.py b/freedata_server/norm/norm_transmission_irs.py index c2aa938f5..3d5d8b4f9 100644 --- a/freedata_server/norm/norm_transmission_irs.py +++ b/freedata_server/norm/norm_transmission_irs.py @@ -1,34 +1,40 @@ # file for handling received data from norm.norm_transmission import NormTransmission +from message_system_db_broadcasts import DatabaseManagerBroadcasts +import base64 + class NormTransmissionIRS(NormTransmission): MAX_PAYLOAD_SIZE = 96 - def __init__(self, frame): + def __init__(self, ctx, frame): + self.ctx = ctx print("burst:", frame) is_last, msg_type, priority = self.decode_flags(frame["flag"]) burst_number, total_bursts = self.decode_burst_info(frame["burst_info"]) payload_size = frame["payload_size"] payload_data = frame["payload_data"] - self.origin = frame["origin"] - self.domain = frame["domain"] - self.gridsquare = frame["gridsquare"] - # FIXME - #if payload_size > len(frame["payload_data"]) and total_bursts > 1: - # payload_data = frame["payload_data"] - #else: - # payload_data = frame["payload_data"][:self.MAX_PAYLOAD_SIZE * burst_number] + if total_bursts == 1: + payload_data = payload_data[:payload_size] + else: + start = (burst_number -1) * self.MAX_PAYLOAD_SIZE + end = min(start + self.MAX_PAYLOAD_SIZE, payload_size) + payload_data = payload_data[start:end] + self.origin = frame["origin"] + self.domain = frame["domain"] + self.gridsquare = frame["gridsquare"] + self.checksum = frame["checksum"] + self.timestamp = frame["timestamp"] + print("####################################") - print("payload_size:", payload_size) print("payload_data:", payload_data) - print("origin", self.origin) print("domain", self.domain) print("gridsquare", self.gridsquare) @@ -37,6 +43,31 @@ def __init__(self, frame): print("priority", priority) print("burst_number", burst_number) print("total_bursts", total_bursts) + print("checksum", self.checksum) + + + payload_b64 = base64.b64encode(payload_data).decode("ascii") + print("payload_b64", payload_b64) + + self.id = self.create_broadcast_id(self.timestamp, self.domain, self.checksum) + + db = DatabaseManagerBroadcasts(self.ctx) + success = db.process_broadcast_message( + id=self.id, + origin=self.origin, + burst_index=burst_number, + burst_data=payload_b64, + total_bursts=total_bursts, + checksum=self.checksum, + repairing_callsigns=frame.get("repairing_callsigns"), + domain=self.domain, + gridsquare=self.gridsquare, + msg_type=msg_type, + priority=priority, + received_at=self.timestamp, + is_read=True, + status="received" + ) - # TODO: - # add data to database or update it \ No newline at end of file + if not success: + print("Failed to process burst in database.") diff --git a/freedata_server/norm/norm_transmission_iss.py b/freedata_server/norm/norm_transmission_iss.py index 564e14533..4838b853c 100644 --- a/freedata_server/norm/norm_transmission_iss.py +++ b/freedata_server/norm/norm_transmission_iss.py @@ -4,7 +4,7 @@ from enum import Enum import time from codec2 import FREEDV_MODE - +import helpers class NORM_ISS_State(Enum): NEW = 0 TRANSMITTING = 1 @@ -53,7 +53,7 @@ def create_bursts(self): payload = full_data[offset: offset + self.MAX_PAYLOAD_SIZE] burst_info = self.encode_burst_info(burst_number, total_bursts) - + checksum = helpers.get_crc_24(full_data) # set flag for last burst is_last = (burst_number == total_bursts - 1) flags = self.encode_flags( @@ -70,7 +70,8 @@ def create_bursts(self): burst_info=burst_info, payload_size=len(payload), payload_data=payload, - flag=flags + flag=flags, + checksum=checksum ) print(burst_frame) bursts.append(burst_frame) From 76518ca9c48a49e2cf09f122f07569dd310fceb0 Mon Sep 17 00:00:00 2001 From: DJ2LS <75909252+DJ2LS@users.noreply.github.com> Date: Mon, 23 Jun 2025 10:04:42 +0200 Subject: [PATCH 014/141] save data to database --- freedata_server/api/freedata.py | 17 +++++ freedata_server/data_frame_factory.py | 2 +- .../message_system_db_broadcasts.py | 65 +++++++++++++++++-- freedata_server/message_system_db_model.py | 6 +- freedata_server/norm/norm_transmission.py | 18 ++++- freedata_server/norm/norm_transmission_irs.py | 13 ++-- freedata_server/norm/norm_transmission_iss.py | 2 +- tests/test_norm_protocol.py | 3 +- 8 files changed, 107 insertions(+), 19 deletions(-) diff --git a/freedata_server/api/freedata.py b/freedata_server/api/freedata.py index fe816e005..374f00889 100644 --- a/freedata_server/api/freedata.py +++ b/freedata_server/api/freedata.py @@ -5,6 +5,8 @@ from message_system_db_attachments import DatabaseManagerAttachments from message_system_db_beacon import DatabaseManagerBeacon from message_system_db_station import DatabaseManagerStations +from message_system_db_broadcasts import DatabaseManagerBroadcasts + import command_message_send import adif_udp_logger import wavelog_api_logger @@ -25,6 +27,9 @@ def _mgr_beacon(ctx: AppContext): def _mgr_stations(ctx: AppContext): return DatabaseManagerStations(ctx) +def _mgr_broadcasts(ctx: AppContext): + return DatabaseManagerBroadcasts(ctx) + @router.get("/messages/{message_id}", summary="Get Message by ID", tags=["FreeDATA"], responses={ @@ -699,3 +704,15 @@ async def set_station_info( if result is None: api_abort("Station not found", 404) return api_response(result) + +@router.get("/broadcast", summary="Get All Broadcast Messages", tags=["FreeDATA"], responses={}) +async def get_freedata_broadcasts( + ctx: AppContext = Depends(get_ctx) +): + #filters = {k: v for k, v in ctx.config_manager.read().get('FILTERS', {}).items()} + # use query params if needed + # filters = dict(ctx.request.query_params) + result = _mgr_broadcasts(ctx).get_all_broadcasts_json() + return api_response(result) + + diff --git a/freedata_server/data_frame_factory.py b/freedata_server/data_frame_factory.py index 25ab93343..51378abd4 100644 --- a/freedata_server/data_frame_factory.py +++ b/freedata_server/data_frame_factory.py @@ -242,7 +242,7 @@ def _load_norm_templates(self): "burst_info": 1, "checksum": 3, "payload_size": 1, - "payload_data": 30 + "payload_data": 99 } # repair frame diff --git a/freedata_server/message_system_db_broadcasts.py b/freedata_server/message_system_db_broadcasts.py index 6b6bc8138..7304f8e1f 100644 --- a/freedata_server/message_system_db_broadcasts.py +++ b/freedata_server/message_system_db_broadcasts.py @@ -2,12 +2,16 @@ from message_system_db_attachments import DatabaseManagerAttachments from message_system_db_model import Status, BroadcastMessage from message_system_db_station import DatabaseManagerStations -from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm.attributes import flag_modified from datetime import datetime, timedelta import json import os from exceptions import MessageStatusError import helpers +import base64 + + + class DatabaseManagerBroadcasts(DatabaseManager): def __init__(self, ctx): @@ -19,6 +23,7 @@ def process_broadcast_message( self, id: str, origin: str, + timestamp: datetime, burst_index: int, burst_data: str, total_bursts: int, @@ -46,16 +51,18 @@ def process_broadcast_message( # Try to find existing message msg = session.query(BroadcastMessage).filter_by(id=id).first() + self.log(f"Broadcast ID: {id}, Burst: {burst_index}, Exists: {'yes' if msg else 'no'}") + if not msg: # Create station and status origin_station = self.stations_manager.get_or_create_station(origin, session) status_obj = self.get_or_create_status(session, status) if status else None - received_at = received_at or datetime.utcnow() # New message msg = BroadcastMessage( id=id, origin=origin_station.callsign, + timestamp=timestamp, repairing_callsigns=repairing_callsigns, domain=domain, gridsquare=gridsquare, @@ -83,23 +90,29 @@ def process_broadcast_message( msg.payload_data["bursts"] = {} msg.payload_data["bursts"][str(burst_index)] = burst_data + flag_modified(msg, "payload_data") + self.log(f"Added burst {burst_index} to message {id}") # Check for final assembly received = msg.payload_data["bursts"] - total = msg.total_bursts or 0 + total = msg.total_bursts - if total > 0 and len(received) == total and all(str(i) in received for i in range(total)): - ordered = [received[str(i)] for i in range(total)] + if total > 0 and len(received) == total and all(str(i) in received for i in range(1, total + 1)): + ordered = [received[str(i)] for i in range(1, total + 1)] final = "".join(ordered) # CRC check - crc = helpers.get_crc_24(final) + + final_bytes = base64.b64decode(final) + crc = helpers.get_crc_24(final_bytes).hex() + if msg.checksum is None: self.log(f"Missing checksum for {id}", isWarning=True) elif crc != msg.checksum: self.log(f"Checksum mismatch for {id}: expected {msg.checksum}, got {crc}", isWarning=True) else: + print("ja?=!") msg.payload_data["final"] = final msg.payload_size = len(final.encode("utf-8")) self.log(f"Final payload assembled and verified for {id}") @@ -115,3 +128,43 @@ def process_broadcast_message( finally: session.remove() + + def get_all_broadcasts_json(self) -> list: + """ + Returns all broadcast messages in JSON-serializable dict format. + """ + session = self.get_thread_scoped_session() + try: + messages = session.query(BroadcastMessage).all() + result = [] + + for msg in messages: + result.append({ + "id": msg.id, + "origin": msg.origin, + "timestamp": msg.timestamp.isoformat() if msg.timestamp else None, + "repairing_callsigns": msg.repairing_callsigns, + "domain": msg.domain, + "gridsquare": msg.gridsquare, + "frequency": msg.frequency, + "priority": msg.priority, + "is_read": msg.is_read, + "payload_size": msg.payload_size, + "payload_data": msg.payload_data, + "msg_type": msg.msg_type, + "total_bursts": msg.total_bursts, + "checksum": msg.checksum, + "received_at": msg.received_at.isoformat() if msg.received_at else None, + "expires_at": msg.expires_at.isoformat() if msg.expires_at else None, + "status": msg.status.name if msg.status else None, + "error_reason": msg.error_reason + }) + + return result + + except Exception as e: + self.log(f"Error fetching broadcasts: {e}", isWarning=True) + return [] + + finally: + session.remove() diff --git a/freedata_server/message_system_db_model.py b/freedata_server/message_system_db_model.py index 41066502c..8d153491a 100644 --- a/freedata_server/message_system_db_model.py +++ b/freedata_server/message_system_db_model.py @@ -203,6 +203,7 @@ class BroadcastMessage(Base): id = Column(String, primary_key=True) origin = Column(String, ForeignKey('station.callsign')) + timestamp = Column(DateTime) repairing_callsigns = Column(JSON, nullable=True) domain = Column(String) gridsquare = Column(String) @@ -214,8 +215,8 @@ class BroadcastMessage(Base): msg_type = Column(String) total_bursts = Column(Integer, default=0) checksum = Column(String) - received_at = Column(DateTime, default=datetime.utcnow) - expires_at = Column(DateTime, nullable=True) + received_at = Column(DateTime, default=0) + expires_at = Column(DateTime, default=0) status_id = Column(Integer, ForeignKey('status.id'), nullable=True) status = relationship('Status', backref='broadcast_messages') error_reason = Column(String, nullable=True) @@ -226,6 +227,7 @@ def to_dict(self): return { 'id': self.id, 'origin': self.origin, + 'timestamp': self.timestamp, 'repairing_callsigns': self.repairing_callsigns, 'domain': self.domain, 'gridsquare': self.gridsquare, diff --git a/freedata_server/norm/norm_transmission.py b/freedata_server/norm/norm_transmission.py index 04ad95a0b..b024ce81e 100644 --- a/freedata_server/norm/norm_transmission.py +++ b/freedata_server/norm/norm_transmission.py @@ -126,6 +126,18 @@ def decode_burst_info(self, burst_info): def create_broadcast_id(self, timestamp: int, domain: str, checksum: str, length: int = 10) -> str: - raw = f"{self}|{timestamp}|{domain}|{checksum}" - digest = hashlib.blake2s(raw.encode(), digest_size=6).hexdigest() - return f"bc_{digest[:max(8, min(length, 12))]}" + """ + Creates a deterministic broadcast ID using BLAKE2b. + + Args: + timestamp (int): UNIX timestamp (int). + domain (str): Domain/context string. + checksum (str): Checksum string. + length (int): Number of hex characters in result (max 128). + + Returns: + str: Broadcast ID, e.g., 'bc_8d12fa3b4e' + """ + base_str = f"{timestamp}:{domain}:{checksum}".encode("utf-8") + digest = hashlib.blake2b(base_str, digest_size=length // 2).hexdigest() # 1 hex char = 4 bits + return f"bc_{digest}" diff --git a/freedata_server/norm/norm_transmission_irs.py b/freedata_server/norm/norm_transmission_irs.py index 3d5d8b4f9..f856ed537 100644 --- a/freedata_server/norm/norm_transmission_irs.py +++ b/freedata_server/norm/norm_transmission_irs.py @@ -2,10 +2,10 @@ from norm.norm_transmission import NormTransmission from message_system_db_broadcasts import DatabaseManagerBroadcasts import base64 - +from datetime import datetime, timezone class NormTransmissionIRS(NormTransmission): - MAX_PAYLOAD_SIZE = 96 + MAX_PAYLOAD_SIZE = 99 def __init__(self, ctx, frame): self.ctx = ctx @@ -29,8 +29,7 @@ def __init__(self, ctx, frame): self.domain = frame["domain"] self.gridsquare = frame["gridsquare"] self.checksum = frame["checksum"] - self.timestamp = frame["timestamp"] - + self.timestamp = datetime.fromtimestamp(frame["timestamp"], tz=timezone.utc) print("####################################") print("payload_size:", payload_size) @@ -44,17 +43,20 @@ def __init__(self, ctx, frame): print("burst_number", burst_number) print("total_bursts", total_bursts) print("checksum", self.checksum) + print("timestamp", self.timestamp) payload_b64 = base64.b64encode(payload_data).decode("ascii") print("payload_b64", payload_b64) self.id = self.create_broadcast_id(self.timestamp, self.domain, self.checksum) + print("id", self.id) db = DatabaseManagerBroadcasts(self.ctx) success = db.process_broadcast_message( id=self.id, origin=self.origin, + timestamp=self.timestamp, burst_index=burst_number, burst_data=payload_b64, total_bursts=total_bursts, @@ -64,7 +66,8 @@ def __init__(self, ctx, frame): gridsquare=self.gridsquare, msg_type=msg_type, priority=priority, - received_at=self.timestamp, + received_at=datetime.now(timezone.utc), + expires_at=datetime.now(timezone.utc), is_read=True, status="received" ) diff --git a/freedata_server/norm/norm_transmission_iss.py b/freedata_server/norm/norm_transmission_iss.py index 4838b853c..140b02c92 100644 --- a/freedata_server/norm/norm_transmission_iss.py +++ b/freedata_server/norm/norm_transmission_iss.py @@ -14,7 +14,7 @@ class NORM_ISS_State(Enum): ABORTED = 5 class NormTransmissionISS(NormTransmission): - MAX_PAYLOAD_SIZE = 96 + MAX_PAYLOAD_SIZE = 99 def __init__(self, ctx, origin, domain, gridsquare, data, priority=NORMMsgPriority.NORMAL, message_type=NORMMsgType.UNDEFINED): diff --git a/tests/test_norm_protocol.py b/tests/test_norm_protocol.py index 1654bcb03..f57aca785 100644 --- a/tests/test_norm_protocol.py +++ b/tests/test_norm_protocol.py @@ -152,7 +152,8 @@ def testNormBroadcast(self): 'gridsquare': "JN48ea", 'type': 'MESSAGE', 'priority': '1', - 'data': str(base64.b64encode(b"hello world!"), 'utf-8') + #'data': str(base64.b64encode(b"hello world!"), 'utf-8') + 'data': str(base64.b64encode(b"hello world!hello world!hello world!hello world!hello world!hello world!hello world!hello world!hello world!hello world!hello world!hello world!hello world!hello world!hello world!hello world!hello world!hello world!hello world!hello world!"), 'utf-8') } try: command = command_norm.Norm(self.ctx_ISS, params) From 04efe8707560a9abc6f6ae4a6b459fcc20e03d7a Mon Sep 17 00:00:00 2001 From: DJ2LS <75909252+DJ2LS@users.noreply.github.com> Date: Mon, 23 Jun 2025 10:29:57 +0200 Subject: [PATCH 015/141] work on multi burst --- freedata_server/data_frame_factory.py | 6 +++--- freedata_server/norm/norm_transmission_irs.py | 13 +++++++------ freedata_server/norm/norm_transmission_iss.py | 13 +++++++++---- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/freedata_server/data_frame_factory.py b/freedata_server/data_frame_factory.py index 51378abd4..c196ee971 100644 --- a/freedata_server/data_frame_factory.py +++ b/freedata_server/data_frame_factory.py @@ -241,8 +241,8 @@ def _load_norm_templates(self): "timestamp": 4, "burst_info": 1, "checksum": 3, - "payload_size": 1, - "payload_data": 99 + "payload_size": 2, + "payload_data": 98 } # repair frame @@ -677,7 +677,7 @@ def build_norm_data(self, origin, domain, gridsquare, timestamp, burst_info, pay "flag": flag.to_bytes(1, 'big'), "timestamp": timestamp.to_bytes(4, 'big'), "burst_info": burst_info.to_bytes(1, 'big'), - "payload_size": payload_size.to_bytes(1, 'big'), + "payload_size": payload_size.to_bytes(2, 'big'), "payload_data": payload_data, "checksum": checksum } diff --git a/freedata_server/norm/norm_transmission_irs.py b/freedata_server/norm/norm_transmission_irs.py index f856ed537..1b3b843a4 100644 --- a/freedata_server/norm/norm_transmission_irs.py +++ b/freedata_server/norm/norm_transmission_irs.py @@ -5,7 +5,7 @@ from datetime import datetime, timezone class NormTransmissionIRS(NormTransmission): - MAX_PAYLOAD_SIZE = 99 + MAX_PAYLOAD_SIZE = 98 def __init__(self, ctx, frame): self.ctx = ctx @@ -18,12 +18,13 @@ def __init__(self, ctx, frame): if total_bursts == 1: - payload_data = payload_data[:payload_size] - else: - start = (burst_number -1) * self.MAX_PAYLOAD_SIZE - end = min(start + self.MAX_PAYLOAD_SIZE, payload_size) - payload_data = payload_data[start:end] + payload_data = payload_data[:self.MAX_PAYLOAD_SIZE] + if is_last:# + + payload_data = payload_data.strip(b'\x00') + + payload_data = payload_data[:payload_size] self.origin = frame["origin"] self.domain = frame["domain"] diff --git a/freedata_server/norm/norm_transmission_iss.py b/freedata_server/norm/norm_transmission_iss.py index 140b02c92..2c4f93e59 100644 --- a/freedata_server/norm/norm_transmission_iss.py +++ b/freedata_server/norm/norm_transmission_iss.py @@ -14,7 +14,7 @@ class NORM_ISS_State(Enum): ABORTED = 5 class NormTransmissionISS(NormTransmission): - MAX_PAYLOAD_SIZE = 99 + MAX_PAYLOAD_SIZE = 98 def __init__(self, ctx, origin, domain, gridsquare, data, priority=NORMMsgPriority.NORMAL, message_type=NORMMsgType.UNDEFINED): @@ -44,8 +44,13 @@ def create_bursts(self): full_data = self.data - + print(self.data) total_bursts = (len(full_data) + self.MAX_PAYLOAD_SIZE - 1) // self.MAX_PAYLOAD_SIZE + print("total_bursts: ", total_bursts) + print("MAX_PAYLOAD_SIZE: ", self.MAX_PAYLOAD_SIZE) + print("len full data: ", len(full_data)) + + bursts = [] for burst_number in range(1, total_bursts + 1): @@ -55,7 +60,7 @@ def create_bursts(self): burst_info = self.encode_burst_info(burst_number, total_bursts) checksum = helpers.get_crc_24(full_data) # set flag for last burst - is_last = (burst_number == total_bursts - 1) + is_last = (burst_number == total_bursts) flags = self.encode_flags( msg_type=self.message_type, priority=self.message_priority, @@ -68,7 +73,7 @@ def create_bursts(self): gridsquare=self.gridsquare, timestamp=self.timestamp, burst_info=burst_info, - payload_size=len(payload), + payload_size=len(full_data), payload_data=payload, flag=flags, checksum=checksum From ecc4ea462d6415f8509306375f66cdcac53770ad Mon Sep 17 00:00:00 2001 From: DJ2LS <75909252+DJ2LS@users.noreply.github.com> Date: Mon, 23 Jun 2025 11:03:41 +0200 Subject: [PATCH 016/141] fixing crc failure --- freedata_server/message_system_db_broadcasts.py | 10 ++++------ freedata_server/norm/norm_transmission_irs.py | 7 ++++--- freedata_server/norm/norm_transmission_iss.py | 1 + 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/freedata_server/message_system_db_broadcasts.py b/freedata_server/message_system_db_broadcasts.py index 7304f8e1f..8c4f8fcc6 100644 --- a/freedata_server/message_system_db_broadcasts.py +++ b/freedata_server/message_system_db_broadcasts.py @@ -99,12 +99,11 @@ def process_broadcast_message( total = msg.total_bursts if total > 0 and len(received) == total and all(str(i) in received for i in range(1, total + 1)): + ordered = [received[str(i)] for i in range(1, total + 1)] - final = "".join(ordered) + final_bytes = b"".join(base64.b64decode(b64part) for b64part in ordered) # CRC check - - final_bytes = base64.b64decode(final) crc = helpers.get_crc_24(final_bytes).hex() if msg.checksum is None: @@ -112,9 +111,8 @@ def process_broadcast_message( elif crc != msg.checksum: self.log(f"Checksum mismatch for {id}: expected {msg.checksum}, got {crc}", isWarning=True) else: - print("ja?=!") - msg.payload_data["final"] = final - msg.payload_size = len(final.encode("utf-8")) + msg.payload_data["final"] = base64.b64encode(final_bytes).decode() + msg.payload_size = len(final_bytes) self.log(f"Final payload assembled and verified for {id}") session.commit() diff --git a/freedata_server/norm/norm_transmission_irs.py b/freedata_server/norm/norm_transmission_irs.py index 1b3b843a4..90078fa83 100644 --- a/freedata_server/norm/norm_transmission_irs.py +++ b/freedata_server/norm/norm_transmission_irs.py @@ -21,10 +21,11 @@ def __init__(self, ctx, frame): payload_data = payload_data[:self.MAX_PAYLOAD_SIZE] if is_last:# + end = self.MAX_PAYLOAD_SIZE - ((total_bursts * self.MAX_PAYLOAD_SIZE) - payload_size) + print(end) + payload_data = payload_data[:end] - payload_data = payload_data.strip(b'\x00') - - payload_data = payload_data[:payload_size] + #payload_data = payload_data[:payload_size] self.origin = frame["origin"] self.domain = frame["domain"] diff --git a/freedata_server/norm/norm_transmission_iss.py b/freedata_server/norm/norm_transmission_iss.py index 2c4f93e59..cdccd5d7f 100644 --- a/freedata_server/norm/norm_transmission_iss.py +++ b/freedata_server/norm/norm_transmission_iss.py @@ -56,6 +56,7 @@ def create_bursts(self): for burst_number in range(1, total_bursts + 1): offset = (burst_number-1) * self.MAX_PAYLOAD_SIZE payload = full_data[offset: offset + self.MAX_PAYLOAD_SIZE] + print("payload: ", len(payload)) burst_info = self.encode_burst_info(burst_number, total_bursts) checksum = helpers.get_crc_24(full_data) From 6b42e3d382de85d80f98d1a4b199f0b93f4ec322 Mon Sep 17 00:00:00 2001 From: DJ2LS <75909252+DJ2LS@users.noreply.github.com> Date: Mon, 23 Jun 2025 11:10:39 +0200 Subject: [PATCH 017/141] update status if crc failure or received --- freedata_server/message_system_db_broadcasts.py | 8 ++++++++ freedata_server/message_system_db_manager.py | 5 +++-- freedata_server/norm/norm_transmission_irs.py | 2 +- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/freedata_server/message_system_db_broadcasts.py b/freedata_server/message_system_db_broadcasts.py index 8c4f8fcc6..071c4d44f 100644 --- a/freedata_server/message_system_db_broadcasts.py +++ b/freedata_server/message_system_db_broadcasts.py @@ -110,9 +110,17 @@ def process_broadcast_message( self.log(f"Missing checksum for {id}", isWarning=True) elif crc != msg.checksum: self.log(f"Checksum mismatch for {id}: expected {msg.checksum}, got {crc}", isWarning=True) + status_obj = self.get_or_create_status(session, "failed_checksum") + msg.status_id = status_obj.id + else: msg.payload_data["final"] = base64.b64encode(final_bytes).decode() msg.payload_size = len(final_bytes) + + status_obj = self.get_or_create_status(session, "received") + msg.status_id = status_obj.id + + self.log(f"Final payload assembled and verified for {id}") session.commit() diff --git a/freedata_server/message_system_db_manager.py b/freedata_server/message_system_db_manager.py index ddd3f6046..0fc827fc1 100644 --- a/freedata_server/message_system_db_manager.py +++ b/freedata_server/message_system_db_manager.py @@ -76,8 +76,9 @@ def initialize_default_values(self): "failed", "failed_checksum", "aborted", - "queued" - ] + "queued", + "receiving", + "assembling" ] # Add default statuses if they don't exist for status_name in statuses: diff --git a/freedata_server/norm/norm_transmission_irs.py b/freedata_server/norm/norm_transmission_irs.py index 90078fa83..828a99ff9 100644 --- a/freedata_server/norm/norm_transmission_irs.py +++ b/freedata_server/norm/norm_transmission_irs.py @@ -71,7 +71,7 @@ def __init__(self, ctx, frame): received_at=datetime.now(timezone.utc), expires_at=datetime.now(timezone.utc), is_read=True, - status="received" + status="assembling" ) if not success: From 1ff0da0fbbb3d49102837b38a009363f8d391d2f Mon Sep 17 00:00:00 2001 From: DJ2LS <75909252+DJ2LS@users.noreply.github.com> Date: Mon, 23 Jun 2025 12:40:16 +0200 Subject: [PATCH 018/141] first parts of implementing gui stuff --- .../src/components/broadcast_domains.vue | 84 +++++++++++ .../src/components/broadcasts_screen.vue | 134 ++++++++++++++++++ .../src/components/main_left_navbar.vue | 19 ++- freedata_gui/src/components/main_modals.vue | 56 ++++++++ freedata_gui/src/components/main_screen.vue | 13 ++ freedata_gui/src/js/api.js | 19 +++ freedata_gui/src/js/broadcastsHandler.js | 3 + freedata_gui/src/store/broadcastStore.js | 24 ++++ freedata_server/b64.py | 6 + 9 files changed, 356 insertions(+), 2 deletions(-) create mode 100644 freedata_gui/src/components/broadcast_domains.vue create mode 100644 freedata_gui/src/components/broadcasts_screen.vue create mode 100644 freedata_gui/src/js/broadcastsHandler.js create mode 100644 freedata_gui/src/store/broadcastStore.js create mode 100644 freedata_server/b64.py diff --git a/freedata_gui/src/components/broadcast_domains.vue b/freedata_gui/src/components/broadcast_domains.vue new file mode 100644 index 000000000..e7afd4456 --- /dev/null +++ b/freedata_gui/src/components/broadcast_domains.vue @@ -0,0 +1,84 @@ + + + + + + {{ $t('chat.startnewchat') }} + + + + + + + + + + {{ $t('chat.loadingMessages') }} + + + + + + {{ $t('chat.noConversations') }} + + + + + + + + {{ domain }} + Typ, Global, EU, US, Asia, ... EMCOM + + {{ sanitizeBody(details.body.substring(0, 35) + '...') || "\u003Cfile\u003E" }} + + + + {{ getDateTime(details.timestamp) }} + + + + + + + + + diff --git a/freedata_gui/src/components/broadcasts_screen.vue b/freedata_gui/src/components/broadcasts_screen.vue new file mode 100644 index 000000000..95c0be218 --- /dev/null +++ b/freedata_gui/src/components/broadcasts_screen.vue @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ chat.selectedCallsign }} + + + + + + + + + + + + + + + + + + + + + + + + {{ $t('chat.selectChat') }} + + + + + + + + + + + + + + + diff --git a/freedata_gui/src/components/main_left_navbar.vue b/freedata_gui/src/components/main_left_navbar.vue index 71ef57ee9..61fb602f9 100644 --- a/freedata_gui/src/components/main_left_navbar.vue +++ b/freedata_gui/src/components/main_left_navbar.vue @@ -2,7 +2,7 @@ import { computed } from 'vue'; import { getOverallHealth } from '../js/eventHandler.js'; -import { getFreedataMessages } from '../js/api'; +import { getFreedataMessages, getFreedataBroadcasts } from '../js/api'; import { loadAllData } from '../js/eventHandler'; import { setActivePinia } from 'pinia'; @@ -89,7 +89,7 @@ const isNetworkTraffic = computed(() => state.is_network_traffic); aria-controls="list-chat" :title="$t('navbar.chat_help')" :class="{ disabled: isNetworkDisconnected }" - @click="isNetworkDisconnected ? null : getFreedataMessages" + @click="isNetworkDisconnected ? null : getFreedataMessages()" > state.is_network_traffic); + + + + + ({ + + + + + + + + {{ $t('modals.startnewbroadcast') }} + + + + + + Select broadcadst domain + + + Select type + + .... + + + + + + + + + + + + + { + + // Indicator if we are loading data + var loading = ref(false); + + /* ------------------------------------------------ */ + // Scroll to bottom functions + const scrollTrigger = ref(0); + + function triggerScrollToBottom() { + scrollTrigger.value++; + } + + + return { + scrollTrigger, + triggerScrollToBottom, + loading, + }; +}); diff --git a/freedata_server/b64.py b/freedata_server/b64.py new file mode 100644 index 000000000..54e9deacc --- /dev/null +++ b/freedata_server/b64.py @@ -0,0 +1,6 @@ +import base64 + + +string = b'aGVsbG8gd29ybGQhaGVsbG8gd29ybGQhaGVsbG8gd29ybGQhaGVsbG8gd29ybGQhaGVsbG8gd29ybGQhaGVsbG8gd29ybGQhaGVsbG8gd29ybGQhaGVsbG8gd29ybGQhaGVsbG8gd29ybGQhaGVsbG8gd29ybGQhaGVsbG8gd29ybGQhaGVsbG8gd29ybGQhaGVsbG8gd29ybGQhaGVsbG8gd29ybGQhaGVsbG8gd29ybGQhaGVsbG8gd29ybGQhaGVsbG8gd29ybGQhaGVsbG8gd29ybGQhaGVsbG8gd29ybGQhaGVsbG8gd29ybGQh' + +print(base64.b64decode(string)) \ No newline at end of file From 5631f1ffb05b6c93a131d60a40629852f5b3eec8 Mon Sep 17 00:00:00 2001 From: DJ2LS <75909252+DJ2LS@users.noreply.github.com> Date: Tue, 24 Jun 2025 10:24:35 +0200 Subject: [PATCH 019/141] adjust database --- freedata_server/api/freedata.py | 12 +++++- .../message_system_db_broadcasts.py | 39 +++++++++++++++++++ freedata_server/message_system_db_model.py | 2 + freedata_server/norm/norm_transmission_irs.py | 1 + 4 files changed, 53 insertions(+), 1 deletion(-) diff --git a/freedata_server/api/freedata.py b/freedata_server/api/freedata.py index 374f00889..05fb1a4a2 100644 --- a/freedata_server/api/freedata.py +++ b/freedata_server/api/freedata.py @@ -705,7 +705,7 @@ async def set_station_info( api_abort("Station not found", 404) return api_response(result) -@router.get("/broadcast", summary="Get All Broadcast Messages", tags=["FreeDATA"], responses={}) +@router.get("/broadcasts", summary="Get All Broadcast Messages", tags=["FreeDATA"], responses={}) async def get_freedata_broadcasts( ctx: AppContext = Depends(get_ctx) ): @@ -715,4 +715,14 @@ async def get_freedata_broadcasts( result = _mgr_broadcasts(ctx).get_all_broadcasts_json() return api_response(result) +@router.get("/broadcasts/domains", summary="Get All Broadcast Messages", tags=["FreeDATA"], responses={}) +async def get_freedata_broadcasts( + ctx: AppContext = Depends(get_ctx) +): + #filters = {k: v for k, v in ctx.config_manager.read().get('FILTERS', {}).items()} + # use query params if needed + # filters = dict(ctx.request.query_params) + result = _mgr_broadcasts(ctx).get_broadcast_domains_json() + return api_response(result) + diff --git a/freedata_server/message_system_db_broadcasts.py b/freedata_server/message_system_db_broadcasts.py index 071c4d44f..491441663 100644 --- a/freedata_server/message_system_db_broadcasts.py +++ b/freedata_server/message_system_db_broadcasts.py @@ -36,6 +36,7 @@ def process_broadcast_message( expires_at: datetime = None, priority: int = 1, is_read: bool = True, + direction: str = None, status: str = "queued", error_reason: str = None ) -> bool: @@ -68,6 +69,7 @@ def process_broadcast_message( gridsquare=gridsquare, priority=priority, is_read=is_read, + direction=direction, payload_size=0, payload_data={"bursts": {str(burst_index): burst_data}}, msg_type=msg_type, @@ -155,6 +157,7 @@ def get_all_broadcasts_json(self) -> list: "frequency": msg.frequency, "priority": msg.priority, "is_read": msg.is_read, + "direction": msg.direction, "payload_size": msg.payload_size, "payload_data": msg.payload_data, "msg_type": msg.msg_type, @@ -174,3 +177,39 @@ def get_all_broadcasts_json(self) -> list: finally: session.remove() + + def get_broadcast_domains_json(self) -> dict: + """ + Returns a JSON-compatible dictionary where each key is a domain (e.g. 'BB1AA-2'), + and each value is a dict containing message stats for that domain. + """ + session = self.get_thread_scoped_session() + try: + messages = ( + session.query(BroadcastMessage) + .filter(BroadcastMessage.domain.isnot(None)) + .order_by(BroadcastMessage.timestamp.desc()) + .all() + ) + + result = {} + for msg in messages: + domain = msg.domain + if domain not in result: + result[domain] = { + "message_count": 1, + "last_message_id": msg.id, + "last_message_timestamp": msg.timestamp.isoformat() if msg.timestamp else None, + "last_origin": msg.origin + } + else: + result[domain]["message_count"] += 1 + + return result + + except Exception as e: + self.log(f"Error fetching domain summary: {e}", isWarning=True) + return {} + + finally: + session.remove() diff --git a/freedata_server/message_system_db_model.py b/freedata_server/message_system_db_model.py index 8d153491a..1cf473a3b 100644 --- a/freedata_server/message_system_db_model.py +++ b/freedata_server/message_system_db_model.py @@ -210,6 +210,7 @@ class BroadcastMessage(Base): frequency = Column(Integer, default=0) priority = Column(Integer, default=1) is_read = Column(Boolean, default=True) + direction = Column(String, nullable=True) payload_size = Column(Integer, default=0) payload_data = Column(JSON, nullable=True) msg_type = Column(String) @@ -234,6 +235,7 @@ def to_dict(self): 'frequency': self.frequency, 'priority': self.priority, 'is_read': self.is_read, + 'direction': self.direction, 'payload_size': self.payload_size, 'payload_data': self.payload_data, 'msg_type': self.msg_type, diff --git a/freedata_server/norm/norm_transmission_irs.py b/freedata_server/norm/norm_transmission_irs.py index 828a99ff9..0288c5279 100644 --- a/freedata_server/norm/norm_transmission_irs.py +++ b/freedata_server/norm/norm_transmission_irs.py @@ -71,6 +71,7 @@ def __init__(self, ctx, frame): received_at=datetime.now(timezone.utc), expires_at=datetime.now(timezone.utc), is_read=True, + direction="receive", status="assembling" ) From 93190180664d6e0d5582f0774e86f53ec346663d Mon Sep 17 00:00:00 2001 From: DJ2LS <75909252+DJ2LS@users.noreply.github.com> Date: Tue, 24 Jun 2025 11:03:23 +0200 Subject: [PATCH 020/141] get broadcasts per domain --- freedata_server/api/freedata.py | 9 ++++ .../message_system_db_broadcasts.py | 44 +++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/freedata_server/api/freedata.py b/freedata_server/api/freedata.py index 05fb1a4a2..1aec291a8 100644 --- a/freedata_server/api/freedata.py +++ b/freedata_server/api/freedata.py @@ -715,6 +715,15 @@ async def get_freedata_broadcasts( result = _mgr_broadcasts(ctx).get_all_broadcasts_json() return api_response(result) +@router.get("/broadcasts/{domain}/", summary="Get Broadcats per Domain", tags=["FreeDATA"], responses={}) +async def get_freedata_broadcasts_per_domain( + domain: str, + ctx: AppContext = Depends(get_ctx) +): + result = _mgr_broadcasts(ctx).get_broadcasts_per_domain_json(domain) + return api_response(result) + + @router.get("/broadcasts/domains", summary="Get All Broadcast Messages", tags=["FreeDATA"], responses={}) async def get_freedata_broadcasts( ctx: AppContext = Depends(get_ctx) diff --git a/freedata_server/message_system_db_broadcasts.py b/freedata_server/message_system_db_broadcasts.py index 491441663..9e7a0ee68 100644 --- a/freedata_server/message_system_db_broadcasts.py +++ b/freedata_server/message_system_db_broadcasts.py @@ -213,3 +213,47 @@ def get_broadcast_domains_json(self) -> dict: finally: session.remove() + + def get_broadcasts_per_domain_json(self, domain: str = None) -> dict: + + session = self.get_thread_scoped_session() + try: + query = session.query(BroadcastMessage).order_by(BroadcastMessage.timestamp.desc()) + + if domain: + query = query.filter(BroadcastMessage.domain == domain) + + messages = query.all() + result = {} + + for msg in messages: + d = msg.domain or "unknown" + + if domain and d != domain: + continue # just in case + + if d not in result: + result[d] = [] + + result[d].append({ + "id": msg.id, + "timestamp": msg.timestamp.isoformat() if msg.timestamp else None, + "origin": msg.origin, + "gridsquare": msg.gridsquare, + "msg_type": msg.msg_type, + "payload_size": msg.payload_size, + "direction": msg.direction, + "status": msg.status.name if msg.status else None, + "error_reason": msg.error_reason, + "received_at": msg.received_at.isoformat() if msg.received_at else None, + "expires_at": msg.expires_at.isoformat() if msg.expires_at else None + }) + + return result + + except Exception as e: + self.log(f"Error collecting broadcasts for domain '{domain}': {e}", isWarning=True) + return {} + + finally: + session.remove() From 6f57ade23c598da45112026fa128256d55a65c17 Mon Sep 17 00:00:00 2001 From: DJ2LS <75909252+DJ2LS@users.noreply.github.com> Date: Tue, 24 Jun 2025 11:59:46 +0200 Subject: [PATCH 021/141] adjusted gui --- .../src/components/broadcast_domains.vue | 102 ++++++++---- .../components/broadcast_message_received.vue | 109 +++++++++++++ .../src/components/broadcast_message_sent.vue | 127 +++++++++++++++ .../src/components/broadcast_messages.vue | 88 +++++++++++ .../src/components/broadcast_new_message.vue | 148 ++++++++++++++++++ .../src/components/broadcasts_screen.vue | 54 ++++--- .../src/components/main_left_navbar.vue | 4 +- freedata_gui/src/components/main_modals.vue | 57 +++++++ freedata_gui/src/js/api.js | 66 +++++++- freedata_gui/src/js/broadcastsHandler.js | 52 +++++- freedata_gui/src/store/broadcastStore.js | 25 ++- 11 files changed, 771 insertions(+), 61 deletions(-) create mode 100644 freedata_gui/src/components/broadcast_message_received.vue create mode 100644 freedata_gui/src/components/broadcast_message_sent.vue create mode 100644 freedata_gui/src/components/broadcast_messages.vue create mode 100644 freedata_gui/src/components/broadcast_new_message.vue diff --git a/freedata_gui/src/components/broadcast_domains.vue b/freedata_gui/src/components/broadcast_domains.vue index e7afd4456..46fbe8fb2 100644 --- a/freedata_gui/src/components/broadcast_domains.vue +++ b/freedata_gui/src/components/broadcast_domains.vue @@ -34,46 +34,52 @@ - {{ $t('chat.noConversations') }} + {{ $t('broadcast.noDomains') }} + + + + + {{ domain }} + + + {{ + details.body + ? sanitizeBody(details.body.substring(0, 35) + '...') + : "" + }} + + + + + {{ getDateTime(details.last_message_timestamp) }} + + + + + - - - - - {{ domain }} - Typ, Global, EU, US, Asia, ... EMCOM - - {{ sanitizeBody(details.body.substring(0, 35) + '...') || "\u003Cfile\u003E" }} - - - - {{ getDateTime(details.timestamp) }} - - - - - diff --git a/freedata_gui/src/components/broadcast_message_received.vue b/freedata_gui/src/components/broadcast_message_received.vue new file mode 100644 index 000000000..e4d8dfb69 --- /dev/null +++ b/freedata_gui/src/components/broadcast_message_received.vue @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + {{ $t('chat.adif') }} + + + + + + + + + + + + diff --git a/freedata_gui/src/components/broadcast_message_sent.vue b/freedata_gui/src/components/broadcast_message_sent.vue new file mode 100644 index 000000000..4b362b9b0 --- /dev/null +++ b/freedata_gui/src/components/broadcast_message_sent.vue @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + ADIF + + + + + + + + + + + + + + + + + diff --git a/freedata_gui/src/components/broadcast_messages.vue b/freedata_gui/src/components/broadcast_messages.vue new file mode 100644 index 000000000..ba7561984 --- /dev/null +++ b/freedata_gui/src/components/broadcast_messages.vue @@ -0,0 +1,88 @@ + + + + + + + + + + {{ getDate(item.timestamp) }} + + + + + + + + + + + + + + + + + + + + + + + diff --git a/freedata_gui/src/components/broadcast_new_message.vue b/freedata_gui/src/components/broadcast_new_message.vue new file mode 100644 index 000000000..85f2342cc --- /dev/null +++ b/freedata_gui/src/components/broadcast_new_message.vue @@ -0,0 +1,148 @@ + + + + + + + + + + + + + + + + + B + + + I + + + U + + + + + + + + + + + + + + + + + + + {{ $t('chat.insertemoji') }} + + + + + + + + + diff --git a/freedata_gui/src/components/broadcasts_screen.vue b/freedata_gui/src/components/broadcasts_screen.vue index 95c0be218..54f2f6261 100644 --- a/freedata_gui/src/components/broadcasts_screen.vue +++ b/freedata_gui/src/components/broadcasts_screen.vue @@ -3,23 +3,33 @@ // disable typescript check because of error with beacon histogram options import broadcast_domains from "./broadcast_domains.vue"; -import chat_messages from "./chat_messages.vue"; -import chat_new_message from "./chat_new_message.vue"; +import broadcast_messages from "./broadcast_messages.vue"; +import broadcast_new_message from "./broadcast_new_message.vue"; -import { getStationInfoByCallsign } from "./../js/stationHandler"; import { setActivePinia } from 'pinia'; import pinia from '../store/index'; setActivePinia(pinia); -import { useChatStore } from '../store/chatStore.js'; -const chat = useChatStore(pinia); +import { useBroadcastStore } from '../store/broadcastStore.js'; +const broadcast = useBroadcastStore(pinia); import { useIsMobile } from '../js/mobile_devices.js'; +import {getFreedataBroadcastsPerDomain} from "@/js/api"; const { isMobile } = useIsMobile(992); +function domainSelected(domain) { + broadcast.selectedDomain = domain.toUpperCase(); + broadcast.triggerScrollToBottom(); + //setMessagesAsRead(domain); + getFreedataBroadcastsPerDomain(domain); +} +function resetDomain() { + broadcast.selectedDomain = null; +} + @@ -31,7 +41,7 @@ const { isMobile } = useIsMobile(992); @@ -45,13 +55,13 @@ const { isMobile } = useIsMobile(992); @@ -61,27 +71,25 @@ const { isMobile } = useIsMobile(992); - - - - {{ chat.selectedCallsign }} + {{ broadcast.selectedDomain }} + + + @@ -90,7 +98,7 @@ const { isMobile } = useIsMobile(992); class="btn btn-outline-secondary ms-2" data-bs-target="#deleteChatModal" data-bs-toggle="modal" - @click="chatSelected(callsign)" + @click="domainSelected(domain)" > @@ -99,21 +107,21 @@ const { isMobile } = useIsMobile(992); - + - - + + - {{ $t('chat.selectChat') }} + {{ $t('broadcast.selectDomain') }} @@ -121,10 +129,10 @@ const { isMobile } = useIsMobile(992); - + diff --git a/freedata_gui/src/components/main_left_navbar.vue b/freedata_gui/src/components/main_left_navbar.vue index 61fb602f9..46b4810c6 100644 --- a/freedata_gui/src/components/main_left_navbar.vue +++ b/freedata_gui/src/components/main_left_navbar.vue @@ -2,7 +2,7 @@ import { computed } from 'vue'; import { getOverallHealth } from '../js/eventHandler.js'; -import { getFreedataMessages, getFreedataBroadcasts } from '../js/api'; +import { getFreedataMessages, getFreedataDomains } from '../js/api'; import { loadAllData } from '../js/eventHandler'; import { setActivePinia } from 'pinia'; @@ -109,7 +109,7 @@ const isNetworkTraffic = computed(() => state.is_network_traffic); aria-controls="list-broadcast" :title="$t('navbar.broadcast_help')" :class="{ disabled: isNetworkDisconnected }" - @click="isNetworkDisconnected ? null : getFreedataBroadcasts()" + @click="isNetworkDisconnected ? null : getFreedataDomains()" > diff --git a/freedata_gui/src/components/main_modals.vue b/freedata_gui/src/components/main_modals.vue index 88dade6eb..dc9cfd89b 100644 --- a/freedata_gui/src/components/main_modals.vue +++ b/freedata_gui/src/components/main_modals.vue @@ -483,6 +483,63 @@ const beaconHistogramData = computed(() => ({ + + + + + + + ... + + + + + + + {{ $t('general.statistics') }} + + + + + + ... + + + + + + + + + + + + + + + { // Scroll to bottom functions const scrollTrigger = ref(0); + // domains + const domains = ref({}); + const selectedDomain = ref({}); + + // broadcasts per domain + const domainBroadcasts = ref({}); + + // input text + const inputText = ref(""); + function triggerScrollToBottom() { scrollTrigger.value++; } + function setDomains(data){ + domains.value = data; + } + + function setBroadcastsForDomain(data){ + domainBroadcasts.value = data; + } return { scrollTrigger, triggerScrollToBottom, loading, + domains, + setDomains, + selectedDomain, + domainBroadcasts, + setBroadcastsForDomain, + inputText, }; -}); +}) From 5d67e19ef67ca3ac49349a7f99ff6a9d4cc4bd1e Mon Sep 17 00:00:00 2001 From: DJ2LS <75909252+DJ2LS@users.noreply.github.com> Date: Tue, 24 Jun 2025 11:59:54 +0200 Subject: [PATCH 022/141] adjusted database --- freedata_server/message_system_db_broadcasts.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freedata_server/message_system_db_broadcasts.py b/freedata_server/message_system_db_broadcasts.py index 9e7a0ee68..36a322adf 100644 --- a/freedata_server/message_system_db_broadcasts.py +++ b/freedata_server/message_system_db_broadcasts.py @@ -242,6 +242,7 @@ def get_broadcasts_per_domain_json(self, domain: str = None) -> dict: "gridsquare": msg.gridsquare, "msg_type": msg.msg_type, "payload_size": msg.payload_size, + "payload_data": msg.payload_data, "direction": msg.direction, "status": msg.status.name if msg.status else None, "error_reason": msg.error_reason, From 148def4570bf7342bd7b2bf8a7a028f8a88dfd4a Mon Sep 17 00:00:00 2001 From: DJ2LS <75909252+DJ2LS@users.noreply.github.com> Date: Tue, 24 Jun 2025 12:56:35 +0200 Subject: [PATCH 023/141] first working transmission via gui --- .../src/components/broadcast_new_message.vue | 19 +++++--- freedata_gui/src/js/api.js | 7 +-- freedata_gui/src/js/broadcastsHandler.js | 6 +-- freedata_server/api/freedata.py | 22 ++++++++- freedata_server/command_norm.py | 4 ++ .../message_system_db_broadcasts.py | 46 +++++++++++++++++++ freedata_server/norm/norm_transmission_iss.py | 43 ++++++++++++++++- 7 files changed, 131 insertions(+), 16 deletions(-) diff --git a/freedata_gui/src/components/broadcast_new_message.vue b/freedata_gui/src/components/broadcast_new_message.vue index 85f2342cc..9921e3807 100644 --- a/freedata_gui/src/components/broadcast_new_message.vue +++ b/freedata_gui/src/components/broadcast_new_message.vue @@ -4,13 +4,16 @@ import pinia from '../store/index'; setActivePinia(pinia); import { useBroadcastStore } from '../store/broadcastStore.js'; +import {settingsStore as settings} from '../store/settingsStore.js'; const broadcast = useBroadcastStore(pinia); + import { ref } from 'vue'; import { VuemojiPicker } from 'vuemoji-picker'; import { marked } from 'marked'; import DOMPurify from 'dompurify'; import { useIsMobile } from '../js/mobile_devices.js'; +import {newBroadcastMessage} from "@/js/broadcastsHandler"; const { isMobile } = useIsMobile(992); @@ -33,13 +36,17 @@ function transmitNewBroadcast() { const sanitizedInput = DOMPurify.sanitize(marked.parse(broadcast.inputText)); - // Broadcast sending function (replace with actual logic) - console.log("Send broadcast to domain:", broadcast.selectedDomain); - console.log("Payload:", sanitizedInput); + const base64data = btoa(sanitizedInput); + const params = { + origin: settings.remote.STATION.mycall + '-' + settings.remote.STATION.myssid, + domain: broadcast.selectedDomain, + gridsquare: settings.remote.STATION.mygrid, + type: "MESSAGE", + priority: "1", + data: base64data + } - // Reset input - broadcast.inputText = ''; - inputField.value = ''; + newBroadcastMessage(params) } // Markdown helper diff --git a/freedata_gui/src/js/api.js b/freedata_gui/src/js/api.js index e3348ae7b..c5ef458ab 100644 --- a/freedata_gui/src/js/api.js +++ b/freedata_gui/src/js/api.js @@ -371,11 +371,8 @@ export async function getFreedataDomains() { } } -export async function sendFreedataBroadcastMessage(domain, body) { - return await apiPost("/freedata/broadcasts", { - domain, - body, - }); +export async function sendFreedataBroadcastMessage(params) { + return await apiPost("/freedata/broadcasts", params); } export async function retransmitFreedataBroadcast(id) { diff --git a/freedata_gui/src/js/broadcastsHandler.js b/freedata_gui/src/js/broadcastsHandler.js index 209d7e4c2..622996ad8 100644 --- a/freedata_gui/src/js/broadcastsHandler.js +++ b/freedata_gui/src/js/broadcastsHandler.js @@ -23,9 +23,9 @@ export async function processFreedataDomains(data) { } -export function newBroadcastMessage(domain, body) { - sendFreedataBroadcastMessage(domain, body); - broadcastStore.triggerScrollToBottom(); +export function newBroadcastMessage(params) { + sendFreedataBroadcastMessage(params); + broadcast.triggerScrollToBottom(); } export function repeatBroadcastTransmission(id) { diff --git a/freedata_server/api/freedata.py b/freedata_server/api/freedata.py index 1aec291a8..27c860cbb 100644 --- a/freedata_server/api/freedata.py +++ b/freedata_server/api/freedata.py @@ -6,7 +6,7 @@ from message_system_db_beacon import DatabaseManagerBeacon from message_system_db_station import DatabaseManagerStations from message_system_db_broadcasts import DatabaseManagerBroadcasts - +import command_norm import command_message_send import adif_udp_logger import wavelog_api_logger @@ -705,6 +705,7 @@ async def set_station_info( api_abort("Station not found", 404) return api_response(result) + @router.get("/broadcasts", summary="Get All Broadcast Messages", tags=["FreeDATA"], responses={}) async def get_freedata_broadcasts( ctx: AppContext = Depends(get_ctx) @@ -735,3 +736,22 @@ async def get_freedata_broadcasts( return api_response(result) +@router.delete("/broadcasts/{id}", summary="Delete Message or Broadcast by ID", tags=["FreeDATA"], responses={}) +async def delete_freedata_broadcast_domain( + id: str, + ctx: AppContext = Depends(get_ctx) +): + ok = _mgr_msgs(ctx).delete_broadcast_message_or_domain(id) + if not ok: + api_abort("Message not found", 404) + return api_response({"message": f"{id} deleted", "status": "success"}) + +@router.post("/broadcasts", summary="Transmit Broadcast", tags=["FreeDATA"], responses={}) +async def post_freedata_broadcast( + payload: dict, + ctx: AppContext = Depends(get_ctx) +): + print(payload) + # Transmit FreeDATA message + await enqueue_tx_command(ctx, command_norm.Norm, payload) + return api_response(payload) diff --git a/freedata_server/command_norm.py b/freedata_server/command_norm.py index 4947f4e0c..515076f47 100644 --- a/freedata_server/command_norm.py +++ b/freedata_server/command_norm.py @@ -41,6 +41,10 @@ def run(self): NormTransmissionISS(self.ctx, self.origin, self.domain, self.gridsquare, self.data, self.priority, self.msgtype).prepare_and_transmit() + + + + except Exception as e: self.log(f"Error starting NORM transmission: {e}", isWarning=True) diff --git a/freedata_server/message_system_db_broadcasts.py b/freedata_server/message_system_db_broadcasts.py index 36a322adf..658ab0219 100644 --- a/freedata_server/message_system_db_broadcasts.py +++ b/freedata_server/message_system_db_broadcasts.py @@ -258,3 +258,49 @@ def get_broadcasts_per_domain_json(self, domain: str = None) -> dict: finally: session.remove() + + def delete_broadcast_message_or_domain(self, id) -> dict: + + session = self.get_thread_scoped_session() + try: + msg = session.query(BroadcastMessage).filter_by(id=id).first() + if msg: + session.delete(msg) + session.commit() + self.log(f"Deleted broadcast message {id}") + return { + "status": "success", + "deleted": 1, + "type": "message", + "id": id + } + + messages = session.query(BroadcastMessage).filter_by(domain=id).all() + if messages: + count = len(messages) + for m in messages: + session.delete(m) + session.commit() + self.log(f"Deleted {count} messages from domain '{id}'") + return { + "status": "success", + "deleted": count, + "type": "domain", + "domain": id + } + + return { + "status": "error", + "message": f"Neither broadcast message ID '{id}' nor domain found." + } + + except Exception as e: + session.rollback() + self.log(f"Error deleting broadcast message or domain '{id}': {e}", isWarning=True) + return { + "status": "error", + "message": str(e) + } + + finally: + session.remove() diff --git a/freedata_server/norm/norm_transmission_iss.py b/freedata_server/norm/norm_transmission_iss.py index cdccd5d7f..1a84f3daf 100644 --- a/freedata_server/norm/norm_transmission_iss.py +++ b/freedata_server/norm/norm_transmission_iss.py @@ -5,6 +5,13 @@ import time from codec2 import FREEDV_MODE import helpers +from datetime import datetime, timezone +import base64 +from message_system_db_broadcasts import DatabaseManagerBroadcasts + + + + class NORM_ISS_State(Enum): NEW = 0 TRANSMITTING = 1 @@ -36,6 +43,7 @@ def __init__(self, ctx, origin, domain, gridsquare, data, priority=NORMMsgPriori def prepare_and_transmit(self): bursts = self.create_bursts() + self.add_to_database() self.transmit_bursts(bursts) def create_bursts(self): @@ -87,4 +95,37 @@ def create_bursts(self): def transmit_bursts(self, bursts): for burst in bursts: - self.ctx.rf_modem.transmit(FREEDV_MODE.datac4, 1, 100, burst) \ No newline at end of file + self.ctx.rf_modem.transmit(FREEDV_MODE.datac4, 1, 200, burst) + + def add_to_database(self): + db = DatabaseManagerBroadcasts(self.ctx) + self.timestamp_dt = datetime.fromtimestamp(self.timestamp, tz=timezone.utc) + self.checksum = helpers.get_crc_24(self.data).hex() + self.id = self.create_broadcast_id(self.timestamp, self.domain, self.checksum) + + total_bursts = (len(self.data) + self.MAX_PAYLOAD_SIZE - 1) // self.MAX_PAYLOAD_SIZE + + for burst_index in range(1, total_bursts + 1): + offset = (burst_index - 1) * self.MAX_PAYLOAD_SIZE + payload_data = self.data[offset: offset + self.MAX_PAYLOAD_SIZE] + payload_b64 = base64.b64encode(payload_data).decode("ascii") + + db.process_broadcast_message( + id=self.id, + origin=self.origin, + timestamp=self.timestamp_dt, + burst_index=burst_index, + burst_data=payload_b64, + total_bursts=total_bursts, + checksum=self.checksum, + repairing_callsigns=None, + domain=self.domain, + gridsquare=self.gridsquare, + msg_type=self.message_type.name if hasattr(self.message_type, 'name') else str(self.message_type), + priority=self.priority.value if hasattr(self.priority, 'value') else int(self.priority), + received_at=datetime.now(timezone.utc), + expires_at=datetime.now(timezone.utc), + is_read=True, + direction="transmit", + status="assembling" + ) From 64b19c2030fbbc36bd9e9e10fe6e62dda4bb87a1 Mon Sep 17 00:00:00 2001 From: DJ2LS <75909252+DJ2LS@users.noreply.github.com> Date: Tue, 24 Jun 2025 13:04:00 +0200 Subject: [PATCH 024/141] fixing smaller stuff --- freedata_gui/src/components/broadcast_message_sent.vue | 4 ++-- freedata_server/api/freedata.py | 3 +-- freedata_server/message_system_db_broadcasts.py | 6 ++++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/freedata_gui/src/components/broadcast_message_sent.vue b/freedata_gui/src/components/broadcast_message_sent.vue index 4b362b9b0..ed7f8474f 100644 --- a/freedata_gui/src/components/broadcast_message_sent.vue +++ b/freedata_gui/src/components/broadcast_message_sent.vue @@ -62,7 +62,7 @@ import { marked } from "marked"; import DOMPurify from "dompurify"; -import {retransmitFreedataBroadcast, deleteFreedataBroadcast, sendBroadcastADIFviaUDP} from "@/js/broadcastsHandler"; +import {retransmitFreedataBroadcast, deleteBroadcastMessageFromDB, sendBroadcastADIFviaUDP} from "@/js/broadcastsHandler"; export default { props: { @@ -114,7 +114,7 @@ export default { async deleteBroadcast() { try { - await deleteFreedataBroadcast(this.message.id); + await deleteBroadcastMessageFromDB(this.message.id); } catch (e) { console.error("Delete failed:", e); } diff --git a/freedata_server/api/freedata.py b/freedata_server/api/freedata.py index 27c860cbb..9e2097e90 100644 --- a/freedata_server/api/freedata.py +++ b/freedata_server/api/freedata.py @@ -741,7 +741,7 @@ async def delete_freedata_broadcast_domain( id: str, ctx: AppContext = Depends(get_ctx) ): - ok = _mgr_msgs(ctx).delete_broadcast_message_or_domain(id) + ok = _mgr_broadcasts(ctx).delete_broadcast_message_or_domain(id) if not ok: api_abort("Message not found", 404) return api_response({"message": f"{id} deleted", "status": "success"}) @@ -751,7 +751,6 @@ async def post_freedata_broadcast( payload: dict, ctx: AppContext = Depends(get_ctx) ): - print(payload) # Transmit FreeDATA message await enqueue_tx_command(ctx, command_norm.Norm, payload) return api_response(payload) diff --git a/freedata_server/message_system_db_broadcasts.py b/freedata_server/message_system_db_broadcasts.py index 658ab0219..1165ad55b 100644 --- a/freedata_server/message_system_db_broadcasts.py +++ b/freedata_server/message_system_db_broadcasts.py @@ -82,6 +82,7 @@ def process_broadcast_message( ) session.add(msg) self.log(f"Created new broadcast message {id}") + self.ctx.event_manager.freedata_message_db_change(message_id=id) else: # Add burst to existing message @@ -95,7 +96,7 @@ def process_broadcast_message( flag_modified(msg, "payload_data") self.log(f"Added burst {burst_index} to message {id}") - + self.ctx.event_manager.freedata_message_db_change(message_id=id) # Check for final assembly received = msg.payload_data["bursts"] total = msg.total_bursts @@ -218,7 +219,7 @@ def get_broadcasts_per_domain_json(self, domain: str = None) -> dict: session = self.get_thread_scoped_session() try: - query = session.query(BroadcastMessage).order_by(BroadcastMessage.timestamp.desc()) + query = session.query(BroadcastMessage).order_by(BroadcastMessage.timestamp.asc()) if domain: query = query.filter(BroadcastMessage.domain == domain) @@ -282,6 +283,7 @@ def delete_broadcast_message_or_domain(self, id) -> dict: session.delete(m) session.commit() self.log(f"Deleted {count} messages from domain '{id}'") + self.ctx.event_manager.freedata_message_db_change(message_id=id) return { "status": "success", "deleted": count, From 35f187df7c084372923548126d67a643754ff37e Mon Sep 17 00:00:00 2001 From: DJ2LS <75909252+DJ2LS@users.noreply.github.com> Date: Tue, 24 Jun 2025 13:11:37 +0200 Subject: [PATCH 025/141] fixing smaller stuff --- .../src/components/broadcast_message_received.vue | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/freedata_gui/src/components/broadcast_message_received.vue b/freedata_gui/src/components/broadcast_message_received.vue index e4d8dfb69..38fb057b2 100644 --- a/freedata_gui/src/components/broadcast_message_received.vue +++ b/freedata_gui/src/components/broadcast_message_received.vue @@ -10,9 +10,17 @@ From a44aa6b15f27f9767aac6245a3be3b9c21319d60 Mon Sep 17 00:00:00 2001 From: DJ2LS <75909252+DJ2LS@users.noreply.github.com> Date: Tue, 24 Jun 2025 20:16:55 +0200 Subject: [PATCH 026/141] updated database --- freedata_server/message_system_db_broadcasts.py | 4 ++++ freedata_server/message_system_db_model.py | 2 ++ freedata_server/norm/norm_transmission_iss.py | 3 ++- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/freedata_server/message_system_db_broadcasts.py b/freedata_server/message_system_db_broadcasts.py index 1165ad55b..01c1648f6 100644 --- a/freedata_server/message_system_db_broadcasts.py +++ b/freedata_server/message_system_db_broadcasts.py @@ -34,6 +34,7 @@ def process_broadcast_message( msg_type: str = None, received_at: datetime = None, expires_at: datetime = None, + nexttransmission_at: datetime = None, priority: int = 1, is_read: bool = True, direction: str = None, @@ -76,6 +77,7 @@ def process_broadcast_message( total_bursts=total_bursts, checksum=checksum, received_at=received_at, + nexttransmission_at=nexttransmission_at, expires_at=expires_at, status_id=status_obj.id if status_obj else None, error_reason=error_reason @@ -166,6 +168,7 @@ def get_all_broadcasts_json(self) -> list: "checksum": msg.checksum, "received_at": msg.received_at.isoformat() if msg.received_at else None, "expires_at": msg.expires_at.isoformat() if msg.expires_at else None, + "nexttransmission_at": msg.nexttransmission_at.isoformat() if msg.nexttransmission_at else None, "status": msg.status.name if msg.status else None, "error_reason": msg.error_reason }) @@ -248,6 +251,7 @@ def get_broadcasts_per_domain_json(self, domain: str = None) -> dict: "status": msg.status.name if msg.status else None, "error_reason": msg.error_reason, "received_at": msg.received_at.isoformat() if msg.received_at else None, + "nexttransmission_at": msg.nexttransmission_at.isoformat() if msg.nexttransmission_at else None, "expires_at": msg.expires_at.isoformat() if msg.expires_at else None }) diff --git a/freedata_server/message_system_db_model.py b/freedata_server/message_system_db_model.py index 1cf473a3b..02dc7d4d2 100644 --- a/freedata_server/message_system_db_model.py +++ b/freedata_server/message_system_db_model.py @@ -217,6 +217,7 @@ class BroadcastMessage(Base): total_bursts = Column(Integer, default=0) checksum = Column(String) received_at = Column(DateTime, default=0) + nexttransmission_at = Column(DateTime, default=0) expires_at = Column(DateTime, default=0) status_id = Column(Integer, ForeignKey('status.id'), nullable=True) status = relationship('Status', backref='broadcast_messages') @@ -243,6 +244,7 @@ def to_dict(self): 'checksum': self.checksum, 'received_at': self.received_at.isoformat() if self.received_at else None, 'expires_at': self.expires_at.isoformat() if self.expires_at else None, + 'nexttransmission_at': self.nexttransmission_at.isoformat() if self.nexttransmission_at else None, 'status': self.status.name if self.status else None, 'error_reason': self.error_reason } diff --git a/freedata_server/norm/norm_transmission_iss.py b/freedata_server/norm/norm_transmission_iss.py index 1a84f3daf..7465fa509 100644 --- a/freedata_server/norm/norm_transmission_iss.py +++ b/freedata_server/norm/norm_transmission_iss.py @@ -5,7 +5,7 @@ import time from codec2 import FREEDV_MODE import helpers -from datetime import datetime, timezone +from datetime import datetime, timezone, timedelta import base64 from message_system_db_broadcasts import DatabaseManagerBroadcasts @@ -124,6 +124,7 @@ def add_to_database(self): msg_type=self.message_type.name if hasattr(self.message_type, 'name') else str(self.message_type), priority=self.priority.value if hasattr(self.priority, 'value') else int(self.priority), received_at=datetime.now(timezone.utc), + nexttransmission_at = datetime.now(timezone.utc) + timedelta(hours=1), expires_at=datetime.now(timezone.utc), is_read=True, direction="transmit", From e4beea71e990a148bc27c4ec827a4f45b3d5da12 Mon Sep 17 00:00:00 2001 From: DJ2LS <75909252+DJ2LS@users.noreply.github.com> Date: Tue, 24 Jun 2025 21:25:01 +0200 Subject: [PATCH 027/141] adjusted new broadcast --- .../src/components/broadcast_new_message.vue | 1 + .../src/components/broadcasts_screen.vue | 2 +- freedata_gui/src/components/main_modals.vue | 118 +++++++++++++++--- freedata_gui/src/js/eventHandler.js | 11 ++ freedata_gui/src/store/broadcastStore.js | 13 +- freedata_server/command_norm.py | 6 - freedata_server/norm/norm_transmission_iss.py | 9 +- 7 files changed, 132 insertions(+), 28 deletions(-) diff --git a/freedata_gui/src/components/broadcast_new_message.vue b/freedata_gui/src/components/broadcast_new_message.vue index 9921e3807..8f1fcf231 100644 --- a/freedata_gui/src/components/broadcast_new_message.vue +++ b/freedata_gui/src/components/broadcast_new_message.vue @@ -47,6 +47,7 @@ function transmitNewBroadcast() { } newBroadcastMessage(params) + broadcast.inputText = '' } // Markdown helper diff --git a/freedata_gui/src/components/broadcasts_screen.vue b/freedata_gui/src/components/broadcasts_screen.vue index 54f2f6261..433dd1e5a 100644 --- a/freedata_gui/src/components/broadcasts_screen.vue +++ b/freedata_gui/src/components/broadcasts_screen.vue @@ -127,7 +127,7 @@ function resetDomain() { - + { + getFreedataDomains() + getFreedataBroadcastsPerDomain(broadcast.newDomain) + + broadcast.selectedDomain = broadcast.newDomain; + broadcast.newDomain = ""; + }, 1000); + } // Function to delete selected chat @@ -639,10 +669,7 @@ const beaconHistogramData = computed(() => ({ - + {{ $t('modals.startnewbroadcast') }} ({ aria-label="Close" /> + - - Select broadcadst domain + + + Domain + + + EUROPE-1 + ASIA-1 + NA-1 + SA-1 + AFRICA-1 + - - Select type + + + + Type + + MESSAGE + + + + + + Priority + + Normal (1) + Low (0) + High (2) + - .... + + + Message + + + + { // domains const domains = ref({}); - const selectedDomain = ref({}); + const selectedDomain = ref(); // broadcasts per domain const domainBroadcasts = ref({}); @@ -21,6 +21,14 @@ export const useBroadcastStore = defineStore("broadcastStore", () => { // input text const inputText = ref(""); + // new message type + const newMessageType = ref(""); + // new domain + const newDomain = ref(""); + // new priority + const newPriority = ref(""); + + function triggerScrollToBottom() { scrollTrigger.value++; } @@ -43,5 +51,8 @@ export const useBroadcastStore = defineStore("broadcastStore", () => { domainBroadcasts, setBroadcastsForDomain, inputText, + newDomain, + newPriority, + newMessageType, }; }) diff --git a/freedata_server/command_norm.py b/freedata_server/command_norm.py index 515076f47..cc3c6c7c9 100644 --- a/freedata_server/command_norm.py +++ b/freedata_server/command_norm.py @@ -33,12 +33,6 @@ def run(self): self.emit_event() self.logger.info(self.log_message()) - # wait some random time and wait if we have an ongoing codec2 transmission - # on our channel. This should prevent some packet collision - random_delay = np.random.randint(0, 6) - threading.Event().wait(random_delay) - self.ctx.state_manager.channel_busy_condition_codec2.wait(0.5) - NormTransmissionISS(self.ctx, self.origin, self.domain, self.gridsquare, self.data, self.priority, self.msgtype).prepare_and_transmit() diff --git a/freedata_server/norm/norm_transmission_iss.py b/freedata_server/norm/norm_transmission_iss.py index 7465fa509..b645dd935 100644 --- a/freedata_server/norm/norm_transmission_iss.py +++ b/freedata_server/norm/norm_transmission_iss.py @@ -8,7 +8,8 @@ from datetime import datetime, timezone, timedelta import base64 from message_system_db_broadcasts import DatabaseManagerBroadcasts - +import threading +import numpy as np @@ -94,6 +95,12 @@ def create_bursts(self): def transmit_bursts(self, bursts): + # wait some random time and wait if we have an ongoing codec2 transmission + # on our channel. This should prevent some packet collision + random_delay = np.random.randint(0, 6) + threading.Event().wait(random_delay) + self.ctx.state_manager.channel_busy_condition_codec2.wait(0.5) + for burst in bursts: self.ctx.rf_modem.transmit(FREEDV_MODE.datac4, 1, 200, burst) From 00c4173a5d3b96dd1d3de664a1724636a2201b55 Mon Sep 17 00:00:00 2001 From: DJ2LS <75909252+DJ2LS@users.noreply.github.com> Date: Tue, 24 Jun 2025 22:13:04 +0200 Subject: [PATCH 028/141] adjusted new broadcast --- .../src/components/broadcast_domains.vue | 2 +- .../src/components/broadcasts_screen.vue | 2 +- freedata_gui/src/components/main_modals.vue | 67 ++++++++++++++++++- freedata_gui/src/js/broadcastsHandler.js | 6 +- .../message_system_db_broadcasts.py | 1 + 5 files changed, 70 insertions(+), 8 deletions(-) diff --git a/freedata_gui/src/components/broadcast_domains.vue b/freedata_gui/src/components/broadcast_domains.vue index 46fbe8fb2..55501fba7 100644 --- a/freedata_gui/src/components/broadcast_domains.vue +++ b/freedata_gui/src/components/broadcast_domains.vue @@ -8,7 +8,7 @@ data-bs-toggle="modal" @click="startNewBroadcast" > - {{ $t('chat.startnewchat') }} + {{ $t('broadcast.startnewbroadcast') }} diff --git a/freedata_gui/src/components/broadcasts_screen.vue b/freedata_gui/src/components/broadcasts_screen.vue index 433dd1e5a..6765c6e17 100644 --- a/freedata_gui/src/components/broadcasts_screen.vue +++ b/freedata_gui/src/components/broadcasts_screen.vue @@ -96,7 +96,7 @@ function resetDomain() { diff --git a/freedata_gui/src/components/main_modals.vue b/freedata_gui/src/components/main_modals.vue index 31ce08a2d..0ab83f293 100644 --- a/freedata_gui/src/components/main_modals.vue +++ b/freedata_gui/src/components/main_modals.vue @@ -11,7 +11,7 @@ import {getFreedataBroadcastsPerDomain, getFreedataDomains, sendModemTestFrame, sendSineTone} from "../js/api"; import { newMessage, deleteCallsignFromDB } from "../js/messagesHandler.js"; import main_startup_check from "./main_startup_check.vue"; - import {newBroadcastMessage} from "../js/broadcastsHandler"; + import {deleteBroadcastDomainFromDB, newBroadcastMessage} from "../js/broadcastsHandler"; // Chart.js imports import { @@ -88,7 +88,13 @@ function deleteChat() { deleteCallsignFromDB(chat.selectedCallsign); } - + + function deleteDomain() { + deleteBroadcastDomainFromDB(broadcast.selectedDomain); + } + + + // Chart options and data const skipped = (speedCtx, value) => speedCtx.p0.skip || speedCtx.p1.skip ? value : undefined; @@ -427,6 +433,63 @@ const beaconHistogramData = computed(() => ({ + + + + + + + {{ chat.selectedCallsign }} {{ $t('modals.options') }} + + + + + + + + + {{ $t('modals.furtheroptions') }} + + + + {{ $t('modals.deletebroadcastdomain') }} + + + + + + + + + dict: session.delete(msg) session.commit() self.log(f"Deleted broadcast message {id}") + self.ctx.event_manager.freedata_message_db_change(message_id=id) return { "status": "success", "deleted": 1, From e5a7bb03bfaca0df0c7f620269c077ec19d1f8b6 Mon Sep 17 00:00:00 2001 From: DJ2LS <75909252+DJ2LS@users.noreply.github.com> Date: Thu, 26 Jun 2025 10:32:20 +0200 Subject: [PATCH 029/141] enable datac4 listening and fix database inserting on IRS --- freedata_server/demodulator.py | 1 + freedata_server/norm/norm_transmission_irs.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/freedata_server/demodulator.py b/freedata_server/demodulator.py index 0aad30b22..5e5b3787d 100644 --- a/freedata_server/demodulator.py +++ b/freedata_server/demodulator.py @@ -57,6 +57,7 @@ def __init__(self, ctx): else: 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.datac4.value]["decode"] = True diff --git a/freedata_server/norm/norm_transmission_irs.py b/freedata_server/norm/norm_transmission_irs.py index 0288c5279..7dae092c2 100644 --- a/freedata_server/norm/norm_transmission_irs.py +++ b/freedata_server/norm/norm_transmission_irs.py @@ -2,7 +2,7 @@ from norm.norm_transmission import NormTransmission from message_system_db_broadcasts import DatabaseManagerBroadcasts import base64 -from datetime import datetime, timezone +from datetime import datetime, timezone, timedelta class NormTransmissionIRS(NormTransmission): MAX_PAYLOAD_SIZE = 98 @@ -32,7 +32,6 @@ def __init__(self, ctx, frame): self.gridsquare = frame["gridsquare"] self.checksum = frame["checksum"] self.timestamp = datetime.fromtimestamp(frame["timestamp"], tz=timezone.utc) - print("####################################") print("payload_size:", payload_size) print("payload_data:", payload_data) @@ -70,6 +69,7 @@ def __init__(self, ctx, frame): priority=priority, received_at=datetime.now(timezone.utc), expires_at=datetime.now(timezone.utc), + nexttransmission_at=datetime.now(timezone.utc), is_read=True, direction="receive", status="assembling" From 935e131e47c3dec84a251d234ae16889ce7fa9dc Mon Sep 17 00:00:00 2001 From: DJ2LS <75909252+DJ2LS@users.noreply.github.com> Date: Thu, 26 Jun 2025 10:51:39 +0200 Subject: [PATCH 030/141] adjusted payloads --- freedata_server/data_frame_factory.py | 4 ++-- freedata_server/modem.py | 1 - freedata_server/norm/norm_transmission_irs.py | 2 +- freedata_server/norm/norm_transmission_iss.py | 7 ++++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/freedata_server/data_frame_factory.py b/freedata_server/data_frame_factory.py index c196ee971..4cdd47c66 100644 --- a/freedata_server/data_frame_factory.py +++ b/freedata_server/data_frame_factory.py @@ -8,7 +8,7 @@ class DataFrameFactory: LENGTH_SIG0_FRAME = 14 LENGTH_SIG1_FRAME = 14 LENGTH_ACK_FRAME = 3 - LENGTH_NORM_FRAME = 126 + LENGTH_NORM_FRAME = 54 """ helpers.set_flag(byte, 'DATA-ACK-NACK', True, FLAG_POSITIONS) @@ -242,7 +242,7 @@ def _load_norm_templates(self): "burst_info": 1, "checksum": 3, "payload_size": 2, - "payload_data": 98 + "payload_data": 26 } # repair frame diff --git a/freedata_server/modem.py b/freedata_server/modem.py index d2a213fe6..0a183f977 100644 --- a/freedata_server/modem.py +++ b/freedata_server/modem.py @@ -292,7 +292,6 @@ def transmit( 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) diff --git a/freedata_server/norm/norm_transmission_irs.py b/freedata_server/norm/norm_transmission_irs.py index 7dae092c2..902be45d9 100644 --- a/freedata_server/norm/norm_transmission_irs.py +++ b/freedata_server/norm/norm_transmission_irs.py @@ -5,7 +5,7 @@ from datetime import datetime, timezone, timedelta class NormTransmissionIRS(NormTransmission): - MAX_PAYLOAD_SIZE = 98 + MAX_PAYLOAD_SIZE = 26 def __init__(self, ctx, frame): self.ctx = ctx diff --git a/freedata_server/norm/norm_transmission_iss.py b/freedata_server/norm/norm_transmission_iss.py index b645dd935..977226af5 100644 --- a/freedata_server/norm/norm_transmission_iss.py +++ b/freedata_server/norm/norm_transmission_iss.py @@ -22,7 +22,7 @@ class NORM_ISS_State(Enum): ABORTED = 5 class NormTransmissionISS(NormTransmission): - MAX_PAYLOAD_SIZE = 98 + MAX_PAYLOAD_SIZE = 26 def __init__(self, ctx, origin, domain, gridsquare, data, priority=NORMMsgPriority.NORMAL, message_type=NORMMsgType.UNDEFINED): @@ -100,10 +100,11 @@ def transmit_bursts(self, bursts): random_delay = np.random.randint(0, 6) threading.Event().wait(random_delay) self.ctx.state_manager.channel_busy_condition_codec2.wait(0.5) - + print("bursts: ", bursts) for burst in bursts: + print("transmitting burst: ", burst) self.ctx.rf_modem.transmit(FREEDV_MODE.datac4, 1, 200, burst) - + #self.ctx.rf_modem.transmit(FREEDV_MODE.datac4, 1, 200, bursts) def add_to_database(self): db = DatabaseManagerBroadcasts(self.ctx) self.timestamp_dt = datetime.fromtimestamp(self.timestamp, tz=timezone.utc) From e8becdb5f78242e259ad0452e20cb1067a260bc9 Mon Sep 17 00:00:00 2001 From: DJ2LS <75909252+DJ2LS@users.noreply.github.com> Date: Thu, 26 Jun 2025 12:06:29 +0200 Subject: [PATCH 031/141] first work on NACK --- freedata_server/data_frame_factory.py | 43 ++++++--- freedata_server/frame_dispatcher.py | 3 +- freedata_server/frame_handler_norm.py | 24 +++-- .../message_system_db_broadcasts.py | 90 +++++++++++++++++++ freedata_server/norm/norm_transmission.py | 8 +- freedata_server/norm/norm_transmission_irs.py | 2 + freedata_server/schedule_manager.py | 12 ++- 7 files changed, 164 insertions(+), 18 deletions(-) diff --git a/freedata_server/data_frame_factory.py b/freedata_server/data_frame_factory.py index 4cdd47c66..b72ef1e09 100644 --- a/freedata_server/data_frame_factory.py +++ b/freedata_server/data_frame_factory.py @@ -9,6 +9,7 @@ class DataFrameFactory: LENGTH_SIG1_FRAME = 14 LENGTH_ACK_FRAME = 3 LENGTH_NORM_FRAME = 54 + LENGTH_NORM_NACK_FRAME = 54 """ helpers.set_flag(byte, 'DATA-ACK-NACK', True, FLAG_POSITIONS) @@ -246,25 +247,25 @@ def _load_norm_templates(self): } # repair frame - # FIXME self.template_list[FR_TYPE.NORM_REPAIR.value] = { "frame_length": self.LENGTH_NORM_FRAME, "origin": 6, "domain": 6, + "gridsquare": 4, "flag": 1, "timestamp": 4, "burst_info": 1, - "payload_size": 1, - "payload_data": 34 + "checksum": 3, + "payload_size": 2, + "payload_data": 26 } # nack frame - # FIXME self.template_list[FR_TYPE.NORM_NACK.value] = { "frame_length": self.LENGTH_NORM_FRAME, "origin": 6, - "domain": 4, - "flag": 2 + "id": 10, + "burst_numbers":8 } # cmd frame @@ -348,6 +349,9 @@ def deconstruct(self, frame, mode_name=None): elif key == "gridsquare": extracted_data[key] = helpers.decode_grid(data) + elif key == "burst_numbers": + extracted_data[key] = list(data) + elif key in ["session_id", "speed_level", "frames_per_burst", "version", "offset", "total_length", "state", "type", "maximum_bandwidth", "protocol_version", "burst_info", "timestamp", "payload_size"]: @@ -686,11 +690,30 @@ def build_norm_data(self, origin, domain, gridsquare, timestamp, burst_info, pay - def build_norm_nack(self): - pass + def build_norm_nack(self, origin, id, burst_numbers:list[int]): + max_burst_numbers = self.template_list[FR_TYPE.NORM_NACK.value]["burst_numbers"] + padded_numbers = burst_numbers + [0] * (max_burst_numbers-len(burst_numbers)) + + payload = { + "origin": helpers.callsign_to_bytes(origin), + "id": bytes(id, 'utf-8'), + "burst_numbers": bytes(padded_numbers), + } + return self.construct(FR_TYPE.NORM_NACK, payload) - def build_norm_repair(self, origin, domain, timestamp, burst_info, payload_size, payload, flag=None): - pass + def build_norm_repair(self, origin, domain, gridsquare, timestamp, burst_info, payload_size, payload_data, flag, checksum): + payload = { + "origin": helpers.callsign_to_bytes(origin), + "domain": helpers.callsign_to_bytes(domain), + "gridsquare": helpers.encode_grid(gridsquare), + "flag": flag.to_bytes(1, 'big'), + "timestamp": timestamp.to_bytes(4, 'big'), + "burst_info": burst_info.to_bytes(1, 'big'), + "payload_size": payload_size.to_bytes(2, 'big'), + "payload_data": payload_data, + "checksum": checksum + } + return self.construct(FR_TYPE.NORM_REPAIR, payload) def build_norm_cmd(self): pass \ No newline at end of file diff --git a/freedata_server/frame_dispatcher.py b/freedata_server/frame_dispatcher.py index 0a9251f19..a319cb0db 100644 --- a/freedata_server/frame_dispatcher.py +++ b/freedata_server/frame_dispatcher.py @@ -56,7 +56,8 @@ class DISPATCHER: FR_TYPE.PING.value: {"class": PingFrameHandler, "name": "PING"}, FR_TYPE.QRV.value: {"class": FrameHandler, "name": "QRV"}, FR_TYPE.NORM_DATA.value: {"class": NORMFrameHandler, "name": "NORM DATA"}, - + FR_TYPE.NORM_NACK.value: {"class": NORMFrameHandler, "name": "NORM NACK"}, + FR_TYPE.NORM_REPAIR.value: {"class": NORMFrameHandler, "name": "NORM REPAIR"}, #FR_TYPE.IS_WRITING.value: {"class": FrameHandler, "name": "IS_WRITING"}, #FR_TYPE.FEC.value: {"class": FrameHandler, "name": "FEC"}, diff --git a/freedata_server/frame_handler_norm.py b/freedata_server/frame_handler_norm.py index eb293cf29..90dcf4ec0 100644 --- a/freedata_server/frame_handler_norm.py +++ b/freedata_server/frame_handler_norm.py @@ -1,14 +1,15 @@ import threading +from modem_frametypes import FRAME_TYPE as FR import frame_handler_ping import helpers import data_frame_factory import frame_handler -from message_system_db_messages import DatabaseManagerMessages +from message_system_db_broadcasts import DatabaseManagerBroadcasts import numpy as np from norm.norm_transmission_irs import NormTransmissionIRS - +from norm.norm_transmission_iss import NormTransmissionISS class NORMFrameHandler(frame_handler.FrameHandler): @@ -17,6 +18,19 @@ def follow_protocol(self): #origin = self.details["frame"]["origin"] #print(origin) - - - NormTransmissionIRS(self.ctx, self.details["frame"]) \ No newline at end of file + frame = self.details['frame'] + + if frame['frame_type_int'] == FR.NORM_DATA.value: + NormTransmissionIRS(self.ctx, frame) + + elif frame['frame_type_int'] == FR.NORM_REPAIR.value: + NormTransmissionIRS(self.ctx, frame) + + elif frame['frame_type_int'] == FR.NORM_NACK.value: + broadcast = DatabaseManagerBroadcasts(self.ctx).get_broadcast_per_id(frame["id"]) + if broadcast is not None: + print(broadcast) + NormTransmissionISS(self.ctx, broadcast.origin, broadcast.domain, broadcast.gridsquare, broadcast.data, priority=broadcast.priority, message_type=broadcast.message_type) + else: + self.logger.warning("DISCARDING FRAME", frame=frame) + return diff --git a/freedata_server/message_system_db_broadcasts.py b/freedata_server/message_system_db_broadcasts.py index ed3523293..589c8f632 100644 --- a/freedata_server/message_system_db_broadcasts.py +++ b/freedata_server/message_system_db_broadcasts.py @@ -6,6 +6,7 @@ from datetime import datetime, timedelta import json import os +from datetime import timezone from exceptions import MessageStatusError import helpers import base64 @@ -311,3 +312,92 @@ def delete_broadcast_message_or_domain(self, id) -> dict: finally: session.remove() + + def check_missing_bursts(self): + session = self.get_thread_scoped_session() + try: + one_minute_ago = datetime.now(timezone.utc) - timedelta(minutes=1) + + messages = ( + session.query(BroadcastMessage) + .filter( + BroadcastMessage.direction == "receive", + BroadcastMessage.received_at < one_minute_ago, + BroadcastMessage.total_bursts > 0 + ) + .order_by(BroadcastMessage.received_at.asc()) + .all() + ) + + for msg in messages: + if not msg.payload_data or "bursts" not in msg.payload_data: + continue + + bursts = msg.payload_data["bursts"] + total = msg.total_bursts + + if "final" in msg.payload_data: + continue # bereits komplett + + missing = [ + i for i in range(1, total + 1) + if str(i) not in bursts + ] + + if missing: + return { + "id": msg.id, + "origin": msg.origin, + "domain": msg.domain, + "missing_bursts": missing, + "total_bursts": total, + "received_bursts": list(bursts.keys()), + "received_at": msg.received_at.isoformat() if msg.received_at else None + } + + return None + + except Exception as e: + self.log(f"Fehler bei check_missing_bursts: {e}", isWarning=True) + return None + + finally: + session.remove() + + + def get_broadcast_per_id(self, id: str) -> dict | None: + session = self.get_thread_scoped_session() + try: + msg = session.query(BroadcastMessage).filter_by(id=id).first() + if not msg: + return None + + return { + "id": msg.id, + "origin": msg.origin, + "timestamp": msg.timestamp.isoformat() if msg.timestamp else None, + "repairing_callsigns": msg.repairing_callsigns, + "domain": msg.domain, + "gridsquare": msg.gridsquare, + "frequency": msg.frequency, + "priority": msg.priority, + "is_read": msg.is_read, + "direction": msg.direction, + "payload_size": msg.payload_size, + "payload_data": msg.payload_data, + "msg_type": msg.msg_type, + "total_bursts": msg.total_bursts, + "checksum": msg.checksum, + "received_at": msg.received_at.isoformat() if msg.received_at else None, + "expires_at": msg.expires_at.isoformat() if msg.expires_at else None, + "nexttransmission_at": msg.nexttransmission_at.isoformat() if msg.nexttransmission_at else None, + "status": msg.status.name if msg.status else None, + "error_reason": msg.error_reason + } + + except Exception as e: + self.log(f"Error fetching broadcast by id '{id}': {e}", isWarning=True) + return None + + finally: + session.remove() diff --git a/freedata_server/norm/norm_transmission.py b/freedata_server/norm/norm_transmission.py index b024ce81e..dcfdb25c8 100644 --- a/freedata_server/norm/norm_transmission.py +++ b/freedata_server/norm/norm_transmission.py @@ -6,6 +6,8 @@ import data_frame_factory from enum import IntEnum import hashlib +from codec2 import FREEDV_MODE + class NORMMsgType(IntEnum): @@ -140,4 +142,8 @@ def create_broadcast_id(self, timestamp: int, domain: str, checksum: str, length """ base_str = f"{timestamp}:{domain}:{checksum}".encode("utf-8") digest = hashlib.blake2b(base_str, digest_size=length // 2).hexdigest() # 1 hex char = 4 bits - return f"bc_{digest}" + return f"{digest}" + + def create_and_transmit_nack_burst(self, origin, id, burst_numbers): + burst = self.frame_factory.build_norm_nack(origin, id, burst_numbers) + self.ctx.rf_modem.transmit(FREEDV_MODE.datac4, 1, 200, burst) \ No newline at end of file diff --git a/freedata_server/norm/norm_transmission_irs.py b/freedata_server/norm/norm_transmission_irs.py index 902be45d9..daf296368 100644 --- a/freedata_server/norm/norm_transmission_irs.py +++ b/freedata_server/norm/norm_transmission_irs.py @@ -52,6 +52,7 @@ def __init__(self, ctx, frame): self.id = self.create_broadcast_id(self.timestamp, self.domain, self.checksum) print("id", self.id) + print("len-id", len(self.id)) db = DatabaseManagerBroadcasts(self.ctx) success = db.process_broadcast_message( @@ -77,3 +78,4 @@ def __init__(self, ctx, frame): if not success: print("Failed to process burst in database.") + diff --git a/freedata_server/schedule_manager.py b/freedata_server/schedule_manager.py index ad6b89c5e..968e050a6 100644 --- a/freedata_server/schedule_manager.py +++ b/freedata_server/schedule_manager.py @@ -7,6 +7,8 @@ #from message_system_db_manager import DatabaseManager from message_system_db_messages import DatabaseManagerMessages from message_system_db_beacon import DatabaseManagerBeacon +from message_system_db_broadcasts import DatabaseManagerBroadcasts +from norm.norm_transmission import NormTransmission import explorer import command_beacon import structlog @@ -44,6 +46,7 @@ def __init__(self, ctx): 'transmitting_beacon': {'function': self.transmit_beacon, 'interval': 600}, 'beacon_cleanup': {'function': self.delete_beacons, 'interval': 600}, 'update_transmission_state': {'function': self.update_transmission_state, 'interval': 10}, + 'check_missing_broadcast_bursts': {'function': self.check_missing_broadcast_bursts, 'interval': 10}, } self.running = False # Flag to control the running state self.scheduler_thread = None # Reference to the scheduler thread @@ -212,4 +215,11 @@ def update_transmission_state(self): self.ctx.state_manager.remove_arq_iss_session(session.id) except Exception as e: - self.log.warning("[SCHEDULE] error deleting ARQ session", error=e) \ No newline at end of file + self.log.warning("[SCHEDULE] error deleting ARQ session", error=e) + + def check_missing_broadcast_bursts(self): + missing_bursts = DatabaseManagerBroadcasts(self.ctx).check_missing_bursts() + print("missing_bursts", missing_bursts) + if missing_bursts: + myfullcall = self.ctx.config_manager.config['STATION']['mycall'] + '-' + str(self.ctx.config_manager.config['STATION']['myssid']) + NormTransmission(self.ctx, missing_bursts["origin"] , missing_bursts["domain"]).create_and_transmit_nack_burst(myfullcall, missing_bursts["id"], missing_bursts["missing_bursts"]) From 2eebd5b2dcf3647c6b64f1324630da7e9af58c73 Mon Sep 17 00:00:00 2001 From: DJ2LS <75909252+DJ2LS@users.noreply.github.com> Date: Thu, 26 Jun 2025 12:15:01 +0200 Subject: [PATCH 032/141] first work on NACK --- freedata_server/frame_handler_norm.py | 1 + freedata_server/message_system_db_broadcasts.py | 2 +- freedata_server/schedule_manager.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/freedata_server/frame_handler_norm.py b/freedata_server/frame_handler_norm.py index 90dcf4ec0..9931e7fc8 100644 --- a/freedata_server/frame_handler_norm.py +++ b/freedata_server/frame_handler_norm.py @@ -27,6 +27,7 @@ def follow_protocol(self): NormTransmissionIRS(self.ctx, frame) elif frame['frame_type_int'] == FR.NORM_NACK.value: + print(frame["id"]) broadcast = DatabaseManagerBroadcasts(self.ctx).get_broadcast_per_id(frame["id"]) if broadcast is not None: print(broadcast) diff --git a/freedata_server/message_system_db_broadcasts.py b/freedata_server/message_system_db_broadcasts.py index 589c8f632..fec4bcae6 100644 --- a/freedata_server/message_system_db_broadcasts.py +++ b/freedata_server/message_system_db_broadcasts.py @@ -365,7 +365,7 @@ def check_missing_bursts(self): session.remove() - def get_broadcast_per_id(self, id: str) -> dict | None: + def get_broadcast_per_id(self, id): session = self.get_thread_scoped_session() try: msg = session.query(BroadcastMessage).filter_by(id=id).first() diff --git a/freedata_server/schedule_manager.py b/freedata_server/schedule_manager.py index 968e050a6..b46de272d 100644 --- a/freedata_server/schedule_manager.py +++ b/freedata_server/schedule_manager.py @@ -46,7 +46,7 @@ def __init__(self, ctx): 'transmitting_beacon': {'function': self.transmit_beacon, 'interval': 600}, 'beacon_cleanup': {'function': self.delete_beacons, 'interval': 600}, 'update_transmission_state': {'function': self.update_transmission_state, 'interval': 10}, - 'check_missing_broadcast_bursts': {'function': self.check_missing_broadcast_bursts, 'interval': 10}, + 'check_missing_broadcast_bursts': {'function': self.check_missing_broadcast_bursts, 'interval': 60}, } self.running = False # Flag to control the running state self.scheduler_thread = None # Reference to the scheduler thread From dc6bb8ed01b4d64f215eaae9648de0f2c18c267b Mon Sep 17 00:00:00 2001 From: DJ2LS Date: Thu, 26 Jun 2025 12:47:07 +0200 Subject: [PATCH 033/141] work on nack --- freedata_server/frame_handler_norm.py | 20 +++- freedata_server/norm/norm_transmission_iss.py | 97 +++++++++++++------ 2 files changed, 81 insertions(+), 36 deletions(-) diff --git a/freedata_server/frame_handler_norm.py b/freedata_server/frame_handler_norm.py index 9931e7fc8..cf72f4176 100644 --- a/freedata_server/frame_handler_norm.py +++ b/freedata_server/frame_handler_norm.py @@ -27,11 +27,21 @@ def follow_protocol(self): NormTransmissionIRS(self.ctx, frame) elif frame['frame_type_int'] == FR.NORM_NACK.value: - print(frame["id"]) - broadcast = DatabaseManagerBroadcasts(self.ctx).get_broadcast_per_id(frame["id"]) - if broadcast is not None: - print(broadcast) - NormTransmissionISS(self.ctx, broadcast.origin, broadcast.domain, broadcast.gridsquare, broadcast.data, priority=broadcast.priority, message_type=broadcast.message_type) + try: + print(str(frame["id"])) + print(frame["id"].decode("utf-8")) + broadcast = DatabaseManagerBroadcasts(self.ctx).get_broadcast_per_id(frame["id"].decode("utf-8")) + if broadcast is not None: + print(broadcast) + print("oring", broadcast["origin"]) + print("domain", broadcast["domain"]) + print("gridsquare", broadcast["gridsquare"]) + print("payload_data", broadcast["payload_data"]["final"]) + print("priority", broadcast["priority"]) + print("message_type", broadcast["msg_type"]) + NormTransmissionISS(self.ctx, broadcast["origin"], broadcast["domain"], broadcast["gridsquare"], broadcast["payload_data"]["final"], priority=broadcast["priority"], message_type=broadcast["msg_type"], send_only_bursts=frame["missing_bursts"]).prepare_and_transmit() + except Exception as e: + print(e) else: self.logger.warning("DISCARDING FRAME", frame=frame) return diff --git a/freedata_server/norm/norm_transmission_iss.py b/freedata_server/norm/norm_transmission_iss.py index 977226af5..2f34f6b84 100644 --- a/freedata_server/norm/norm_transmission_iss.py +++ b/freedata_server/norm/norm_transmission_iss.py @@ -24,7 +24,7 @@ class NORM_ISS_State(Enum): class NormTransmissionISS(NormTransmission): MAX_PAYLOAD_SIZE = 26 - def __init__(self, ctx, origin, domain, gridsquare, data, priority=NORMMsgPriority.NORMAL, message_type=NORMMsgType.UNDEFINED): + def __init__(self, ctx, origin, domain, gridsquare, data, priority=NORMMsgPriority.NORMAL, message_type=NORMMsgType.UNDEFINED, send_only_bursts=None): super().__init__(ctx, origin, domain) self.ctx = ctx @@ -36,6 +36,8 @@ def __init__(self, ctx, origin, domain, gridsquare, data, priority=NORMMsgPriori self.message_type = message_type self.payload_size = len(data) + self.send_only_bursts = send_only_bursts + self.timestamp = int(time.time()) self.state = NORM_ISS_State.NEW @@ -61,35 +63,66 @@ def create_bursts(self): bursts = [] - - for burst_number in range(1, total_bursts + 1): - offset = (burst_number-1) * self.MAX_PAYLOAD_SIZE - payload = full_data[offset: offset + self.MAX_PAYLOAD_SIZE] - print("payload: ", len(payload)) - - burst_info = self.encode_burst_info(burst_number, total_bursts) - checksum = helpers.get_crc_24(full_data) - # set flag for last burst - is_last = (burst_number == total_bursts) - flags = self.encode_flags( - msg_type=self.message_type, - priority=self.message_priority, - is_last=is_last - ) - - burst_frame = self.frame_factory.build_norm_data( - origin=self.origin, - domain=self.domain, - gridsquare=self.gridsquare, - timestamp=self.timestamp, - burst_info=burst_info, - payload_size=len(full_data), - payload_data=payload, - flag=flags, - checksum=checksum - ) - print(burst_frame) - bursts.append(burst_frame) + if not self.send_only_bursts: + for burst_number in range(1, total_bursts + 1): + offset = (burst_number-1) * self.MAX_PAYLOAD_SIZE + payload = full_data[offset: offset + self.MAX_PAYLOAD_SIZE] + print("payload: ", len(payload)) + + burst_info = self.encode_burst_info(burst_number, total_bursts) + checksum = helpers.get_crc_24(full_data) + # set flag for last burst + is_last = (burst_number == total_bursts) + flags = self.encode_flags( + msg_type=self.message_type, + priority=self.message_priority, + is_last=is_last + ) + + burst_frame = self.frame_factory.build_norm_data( + origin=self.origin, + domain=self.domain, + gridsquare=self.gridsquare, + timestamp=self.timestamp, + burst_info=burst_info, + payload_size=len(full_data), + payload_data=payload, + flag=flags, + checksum=checksum + ) + print(burst_frame) + bursts.append(burst_frame) + + else: + + for burst_number in self.send_only_bursts: + offset = (burst_number - 1) * self.MAX_PAYLOAD_SIZE + payload = full_data[offset: offset + self.MAX_PAYLOAD_SIZE] + print("payload: ", len(payload)) + + burst_info = self.encode_burst_info(burst_number, total_bursts) + checksum = helpers.get_crc_24(full_data) + # set flag for last burst + is_last = (burst_number == total_bursts) + flags = self.encode_flags( + msg_type=self.message_type, + priority=self.message_priority, + is_last=is_last + ) + + burst_frame = self.frame_factory.build_norm_data( + origin=self.origin, + domain=self.domain, + gridsquare=self.gridsquare, + timestamp=self.timestamp, + burst_info=burst_info, + payload_size=len(full_data), + payload_data=payload, + flag=flags, + checksum=checksum + ) + print(burst_frame) + bursts.append(burst_frame) return bursts @@ -109,7 +142,9 @@ def add_to_database(self): db = DatabaseManagerBroadcasts(self.ctx) self.timestamp_dt = datetime.fromtimestamp(self.timestamp, tz=timezone.utc) self.checksum = helpers.get_crc_24(self.data).hex() - self.id = self.create_broadcast_id(self.timestamp, self.domain, self.checksum) + self.id = self.create_broadcast_id(self.timestamp_dt, self.domain, self.checksum) + print(self.create_broadcast_id(self.timestamp_dt, self.domain, self.checksum), self.create_broadcast_id(self.timestamp, self.domain, self.checksum)) + print(self.timestamp, self.timestamp_dt) total_bursts = (len(self.data) + self.MAX_PAYLOAD_SIZE - 1) // self.MAX_PAYLOAD_SIZE From 845bba340e32a811c7ea75662656a31f47f2584b Mon Sep 17 00:00:00 2001 From: DJ2LS <75909252+DJ2LS@users.noreply.github.com> Date: Wed, 2 Jul 2025 10:03:50 +0200 Subject: [PATCH 034/141] fix iss transmission --- freedata_server/norm/norm_transmission_iss.py | 63 ++++++++++--------- 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/freedata_server/norm/norm_transmission_iss.py b/freedata_server/norm/norm_transmission_iss.py index 2f34f6b84..a82c7ecdf 100644 --- a/freedata_server/norm/norm_transmission_iss.py +++ b/freedata_server/norm/norm_transmission_iss.py @@ -46,9 +46,11 @@ def __init__(self, ctx, origin, domain, gridsquare, data, priority=NORMMsgPriori def prepare_and_transmit(self): bursts = self.create_bursts() + print("add to database...") self.add_to_database() + print("transmit bursts...") self.transmit_bursts(bursts) - + print("done...") def create_bursts(self): self.message_type = NORMMsgType.MESSAGE self.message_priority = NORMMsgPriority.NORMAL @@ -64,6 +66,7 @@ def create_bursts(self): bursts = [] if not self.send_only_bursts: + for burst_number in range(1, total_bursts + 1): offset = (burst_number-1) * self.MAX_PAYLOAD_SIZE payload = full_data[offset: offset + self.MAX_PAYLOAD_SIZE] @@ -93,37 +96,35 @@ def create_bursts(self): print(burst_frame) bursts.append(burst_frame) - else: - - for burst_number in self.send_only_bursts: - offset = (burst_number - 1) * self.MAX_PAYLOAD_SIZE - payload = full_data[offset: offset + self.MAX_PAYLOAD_SIZE] - print("payload: ", len(payload)) - - burst_info = self.encode_burst_info(burst_number, total_bursts) - checksum = helpers.get_crc_24(full_data) - # set flag for last burst - is_last = (burst_number == total_bursts) - flags = self.encode_flags( - msg_type=self.message_type, - priority=self.message_priority, - is_last=is_last - ) - - burst_frame = self.frame_factory.build_norm_data( - origin=self.origin, - domain=self.domain, - gridsquare=self.gridsquare, - timestamp=self.timestamp, - burst_info=burst_info, - payload_size=len(full_data), - payload_data=payload, - flag=flags, - checksum=checksum - ) - print(burst_frame) - bursts.append(burst_frame) + else: + for burst_number in self.send_only_bursts: + offset = (burst_number - 1) * self.MAX_PAYLOAD_SIZE + payload = full_data[offset: offset + self.MAX_PAYLOAD_SIZE] + print("payload: ", len(payload)) + burst_info = self.encode_burst_info(burst_number, total_bursts) + checksum = helpers.get_crc_24(full_data) + # set flag for last burst + is_last = (burst_number == total_bursts) + flags = self.encode_flags( + msg_type=self.message_type, + priority=self.message_priority, + is_last=is_last + ) + burst_frame = self.frame_factory.build_norm_data( + origin=self.origin, + domain=self.domain, + gridsquare=self.gridsquare, + timestamp=self.timestamp, + burst_info=burst_info, + payload_size=len(full_data), + payload_data=payload, + flag=flags, + checksum=checksum + ) + print(burst_frame) + + bursts.append(burst_frame) return bursts def transmit_bursts(self, bursts): From b738feae7814cca58d2000ff46afeecdc9bb4d79 Mon Sep 17 00:00:00 2001 From: DJ2LS Date: Wed, 2 Jul 2025 10:39:47 +0200 Subject: [PATCH 035/141] work on nack --- freedata_server/data_frame_factory.py | 2 +- freedata_server/frame_handler_norm.py | 7 ++++++- freedata_server/norm/norm_transmission_iss.py | 18 ++++++++++++++++-- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/freedata_server/data_frame_factory.py b/freedata_server/data_frame_factory.py index b72ef1e09..3ea507a92 100644 --- a/freedata_server/data_frame_factory.py +++ b/freedata_server/data_frame_factory.py @@ -350,7 +350,7 @@ def deconstruct(self, frame, mode_name=None): extracted_data[key] = helpers.decode_grid(data) elif key == "burst_numbers": - extracted_data[key] = list(data) + extracted_data[key] = [x for x in data if x !=0] elif key in ["session_id", "speed_level", "frames_per_burst", "version", diff --git a/freedata_server/frame_handler_norm.py b/freedata_server/frame_handler_norm.py index cf72f4176..356943b7e 100644 --- a/freedata_server/frame_handler_norm.py +++ b/freedata_server/frame_handler_norm.py @@ -1,3 +1,4 @@ +import base64 import threading from modem_frametypes import FRAME_TYPE as FR @@ -32,6 +33,8 @@ def follow_protocol(self): print(frame["id"].decode("utf-8")) broadcast = DatabaseManagerBroadcasts(self.ctx).get_broadcast_per_id(frame["id"].decode("utf-8")) if broadcast is not None: + + data = base64.b64decode(broadcast["payload_data"]["final"]) print(broadcast) print("oring", broadcast["origin"]) print("domain", broadcast["domain"]) @@ -39,7 +42,9 @@ def follow_protocol(self): print("payload_data", broadcast["payload_data"]["final"]) print("priority", broadcast["priority"]) print("message_type", broadcast["msg_type"]) - NormTransmissionISS(self.ctx, broadcast["origin"], broadcast["domain"], broadcast["gridsquare"], broadcast["payload_data"]["final"], priority=broadcast["priority"], message_type=broadcast["msg_type"], send_only_bursts=frame["missing_bursts"]).prepare_and_transmit() + print("frame:", frame) + print("missing bursts:", frame["burst_numbers"]) + NormTransmissionISS(self.ctx, broadcast["origin"], broadcast["domain"], broadcast["gridsquare"], data, priority=broadcast["priority"], message_type=broadcast["msg_type"], send_only_bursts=frame["burst_numbers"]).prepare_and_transmit() except Exception as e: print(e) else: diff --git a/freedata_server/norm/norm_transmission_iss.py b/freedata_server/norm/norm_transmission_iss.py index a82c7ecdf..df8e90cb5 100644 --- a/freedata_server/norm/norm_transmission_iss.py +++ b/freedata_server/norm/norm_transmission_iss.py @@ -97,13 +97,20 @@ def create_bursts(self): bursts.append(burst_frame) else: + + print(self.send_only_bursts) for burst_number in self.send_only_bursts: offset = (burst_number - 1) * self.MAX_PAYLOAD_SIZE payload = full_data[offset: offset + self.MAX_PAYLOAD_SIZE] print("payload: ", len(payload)) - + print(type(burst_number)) + print(burst_number) + print(total_bursts) burst_info = self.encode_burst_info(burst_number, total_bursts) + print("burst_info", burst_info) + checksum = helpers.get_crc_24(full_data) + print("checksum", checksum) # set flag for last burst is_last = (burst_number == total_bursts) flags = self.encode_flags( @@ -111,7 +118,14 @@ def create_bursts(self): priority=self.message_priority, is_last=is_last ) - burst_frame = self.frame_factory.build_norm_data( + print("flags: ", flags) + print(self.timestamp) + print(self.origin) + print(self.domain) + print(self.gridsquare) + print(len(full_data)) + print(payload) + burst_frame = self.frame_factory.build_norm_repair( origin=self.origin, domain=self.domain, gridsquare=self.gridsquare, From 87ac1ac0c4202c1191c1870d4d79dc5f84c53b1e Mon Sep 17 00:00:00 2001 From: DJ2LS <75909252+DJ2LS@users.noreply.github.com> Date: Wed, 2 Jul 2025 10:57:59 +0200 Subject: [PATCH 036/141] fix iss transmission --- freedata_server/command_norm.py | 2 +- freedata_server/frame_handler_norm.py | 15 +- freedata_server/norm/norm_transmission.py | 6 +- freedata_server/norm/norm_transmission_iss.py | 182 +++++++----------- 4 files changed, 79 insertions(+), 126 deletions(-) diff --git a/freedata_server/command_norm.py b/freedata_server/command_norm.py index cc3c6c7c9..571571457 100644 --- a/freedata_server/command_norm.py +++ b/freedata_server/command_norm.py @@ -33,7 +33,7 @@ def run(self): self.emit_event() self.logger.info(self.log_message()) - NormTransmissionISS(self.ctx, self.origin, self.domain, self.gridsquare, self.data, self.priority, self.msgtype).prepare_and_transmit() + NormTransmissionISS(self.ctx).prepare_and_transmit_data(self.origin, self.domain, self.gridsquare, self.data, self.priority, self.msgtype) diff --git a/freedata_server/frame_handler_norm.py b/freedata_server/frame_handler_norm.py index 356943b7e..bb9afd99c 100644 --- a/freedata_server/frame_handler_norm.py +++ b/freedata_server/frame_handler_norm.py @@ -38,13 +38,14 @@ def follow_protocol(self): print(broadcast) print("oring", broadcast["origin"]) print("domain", broadcast["domain"]) - print("gridsquare", broadcast["gridsquare"]) - print("payload_data", broadcast["payload_data"]["final"]) - print("priority", broadcast["priority"]) - print("message_type", broadcast["msg_type"]) - print("frame:", frame) - print("missing bursts:", frame["burst_numbers"]) - NormTransmissionISS(self.ctx, broadcast["origin"], broadcast["domain"], broadcast["gridsquare"], data, priority=broadcast["priority"], message_type=broadcast["msg_type"], send_only_bursts=frame["burst_numbers"]).prepare_and_transmit() + #print("gridsquare", broadcast["gridsquare"]) + #print("payload_data", broadcast["payload_data"]["final"]) + #print("priority", broadcast["priority"]) + #print("message_type", broadcast["msg_type"]) + #print("frame:", frame) + #print("missing bursts:", frame["burst_numbers"]) + #NormTransmissionISS(self.ctx, broadcast["origin"], broadcast["domain"], broadcast["gridsquare"], data, priority=broadcast["priority"], message_type=broadcast["msg_type"], send_only_bursts=frame["burst_numbers"]).prepare_and_transmit() + NormTransmissionISS(self.ctx).create_repair(broadcast, frame["burst_numbers"]) except Exception as e: print(e) else: diff --git a/freedata_server/norm/norm_transmission.py b/freedata_server/norm/norm_transmission.py index dcfdb25c8..c0399a285 100644 --- a/freedata_server/norm/norm_transmission.py +++ b/freedata_server/norm/norm_transmission.py @@ -31,11 +31,11 @@ class NORMMsgPriority(IntEnum): EMERGENCY = 5 class NormTransmission: - def __init__(self, ctx, origin, domain): + def __init__(self, ctx): self.logger = structlog.get_logger(type(self).__name__) self.ctx = ctx - self.origin = origin - self.domain = domain + self.origin = None + self.domain = None self.frame_factory = data_frame_factory.DataFrameFactory(self.ctx) diff --git a/freedata_server/norm/norm_transmission_iss.py b/freedata_server/norm/norm_transmission_iss.py index df8e90cb5..ee39ac37e 100644 --- a/freedata_server/norm/norm_transmission_iss.py +++ b/freedata_server/norm/norm_transmission_iss.py @@ -11,8 +11,6 @@ import threading import numpy as np - - class NORM_ISS_State(Enum): NEW = 0 TRANSMITTING = 1 @@ -24,10 +22,13 @@ class NORM_ISS_State(Enum): class NormTransmissionISS(NormTransmission): MAX_PAYLOAD_SIZE = 26 - def __init__(self, ctx, origin, domain, gridsquare, data, priority=NORMMsgPriority.NORMAL, message_type=NORMMsgType.UNDEFINED, send_only_bursts=None): - - super().__init__(ctx, origin, domain) + def __init__(self, ctx): + super().__init__(ctx) self.ctx = ctx + self.state = NORM_ISS_State.NEW + self.log("Initialized") + + def prepare_and_transmit_data(self, origin, domain, gridsquare, data, priority=NORMMsgPriority.NORMAL, message_type=NORMMsgType.UNDEFINED): self.origin = origin self.domain = domain self.gridsquare = gridsquare @@ -35,131 +36,82 @@ def __init__(self, ctx, origin, domain, gridsquare, data, priority=NORMMsgPriori self.priority = priority self.message_type = message_type self.payload_size = len(data) - - self.send_only_bursts = send_only_bursts - self.timestamp = int(time.time()) - self.state = NORM_ISS_State.NEW - - self.log("Initialized") - - def prepare_and_transmit(self): - bursts = self.create_bursts() - print("add to database...") + bursts = self.create_data() self.add_to_database() - print("transmit bursts...") self.transmit_bursts(bursts) - print("done...") - def create_bursts(self): - self.message_type = NORMMsgType.MESSAGE - self.message_priority = NORMMsgPriority.NORMAL - + def create_data(self): full_data = self.data - print(self.data) total_bursts = (len(full_data) + self.MAX_PAYLOAD_SIZE - 1) // self.MAX_PAYLOAD_SIZE - print("total_bursts: ", total_bursts) - print("MAX_PAYLOAD_SIZE: ", self.MAX_PAYLOAD_SIZE) - print("len full data: ", len(full_data)) - bursts = [] - if not self.send_only_bursts: - - for burst_number in range(1, total_bursts + 1): - offset = (burst_number-1) * self.MAX_PAYLOAD_SIZE - payload = full_data[offset: offset + self.MAX_PAYLOAD_SIZE] - print("payload: ", len(payload)) - - burst_info = self.encode_burst_info(burst_number, total_bursts) - checksum = helpers.get_crc_24(full_data) - # set flag for last burst - is_last = (burst_number == total_bursts) - flags = self.encode_flags( - msg_type=self.message_type, - priority=self.message_priority, - is_last=is_last - ) - - burst_frame = self.frame_factory.build_norm_data( - origin=self.origin, - domain=self.domain, - gridsquare=self.gridsquare, - timestamp=self.timestamp, - burst_info=burst_info, - payload_size=len(full_data), - payload_data=payload, - flag=flags, - checksum=checksum - ) - print(burst_frame) - bursts.append(burst_frame) - - else: - - print(self.send_only_bursts) - for burst_number in self.send_only_bursts: - offset = (burst_number - 1) * self.MAX_PAYLOAD_SIZE - payload = full_data[offset: offset + self.MAX_PAYLOAD_SIZE] - print("payload: ", len(payload)) - print(type(burst_number)) - print(burst_number) - print(total_bursts) - burst_info = self.encode_burst_info(burst_number, total_bursts) - print("burst_info", burst_info) - - checksum = helpers.get_crc_24(full_data) - print("checksum", checksum) - # set flag for last burst - is_last = (burst_number == total_bursts) - flags = self.encode_flags( - msg_type=self.message_type, - priority=self.message_priority, - is_last=is_last - ) - print("flags: ", flags) - print(self.timestamp) - print(self.origin) - print(self.domain) - print(self.gridsquare) - print(len(full_data)) - print(payload) - burst_frame = self.frame_factory.build_norm_repair( - origin=self.origin, - domain=self.domain, - gridsquare=self.gridsquare, - timestamp=self.timestamp, - burst_info=burst_info, - payload_size=len(full_data), - payload_data=payload, - flag=flags, - checksum=checksum - ) - print(burst_frame) - - bursts.append(burst_frame) + for burst_number in range(1, total_bursts + 1): + offset = (burst_number - 1) * self.MAX_PAYLOAD_SIZE + payload = full_data[offset: offset + self.MAX_PAYLOAD_SIZE] + burst_info = self.encode_burst_info(burst_number, total_bursts) + checksum = helpers.get_crc_24(full_data) + is_last = (burst_number == total_bursts) + flags = self.encode_flags(self.message_type, self.priority, is_last) + + burst_frame = self.frame_factory.build_norm_data( + origin=self.origin, + domain=self.domain, + gridsquare=self.gridsquare, + timestamp=self.timestamp, + burst_info=burst_info, + payload_size=len(full_data), + payload_data=payload, + flag=flags, + checksum=checksum + ) + bursts.append(burst_frame) return bursts - def transmit_bursts(self, bursts): + def create_repair(self, db_msg_obj: dict, burst_numbers: list[int]): + repair_bursts = [] + data = base64.b64decode(db_msg_obj["payload_data"]["final"]) + total_bursts = db_msg_obj["total_bursts"] + priority = db_msg_obj["priority"] + message_type = NORMMsgType[db_msg_obj["msg_type"]] if isinstance(db_msg_obj["msg_type"], str) else db_msg_obj["msg_type"] + checksum = bytes.fromhex(db_msg_obj["checksum"]) + + for burst_number in burst_numbers: + offset = (burst_number - 1) * self.MAX_PAYLOAD_SIZE + payload = data[offset: offset + self.MAX_PAYLOAD_SIZE] + burst_info = self.encode_burst_info(burst_number, total_bursts) + is_last = (burst_number == total_bursts) + flags = self.encode_flags(message_type, priority, is_last) + + burst_frame = self.frame_factory.build_norm_repair( + origin=db_msg_obj["origin"], + domain=db_msg_obj["domain"], + gridsquare=db_msg_obj["gridsquare"], + timestamp=int(datetime.fromisoformat(db_msg_obj["timestamp"]).timestamp()), + burst_info=burst_info, + payload_size=len(data), + payload_data=payload, + flag=flags, + checksum=checksum + ) + repair_bursts.append(burst_frame) + + return repair_bursts - # wait some random time and wait if we have an ongoing codec2 transmission - # on our channel. This should prevent some packet collision + def transmit_bursts(self, bursts): random_delay = np.random.randint(0, 6) threading.Event().wait(random_delay) self.ctx.state_manager.channel_busy_condition_codec2.wait(0.5) - print("bursts: ", bursts) + for burst in bursts: - print("transmitting burst: ", burst) self.ctx.rf_modem.transmit(FREEDV_MODE.datac4, 1, 200, burst) - #self.ctx.rf_modem.transmit(FREEDV_MODE.datac4, 1, 200, bursts) + def add_to_database(self): db = DatabaseManagerBroadcasts(self.ctx) - self.timestamp_dt = datetime.fromtimestamp(self.timestamp, tz=timezone.utc) - self.checksum = helpers.get_crc_24(self.data).hex() - self.id = self.create_broadcast_id(self.timestamp_dt, self.domain, self.checksum) - print(self.create_broadcast_id(self.timestamp_dt, self.domain, self.checksum), self.create_broadcast_id(self.timestamp, self.domain, self.checksum)) - print(self.timestamp, self.timestamp_dt) + timestamp_dt = datetime.fromtimestamp(self.timestamp, tz=timezone.utc) + checksum = helpers.get_crc_24(self.data).hex() + broadcast_id = self.create_broadcast_id(timestamp_dt, self.domain, checksum) total_bursts = (len(self.data) + self.MAX_PAYLOAD_SIZE - 1) // self.MAX_PAYLOAD_SIZE @@ -169,20 +121,20 @@ def add_to_database(self): payload_b64 = base64.b64encode(payload_data).decode("ascii") db.process_broadcast_message( - id=self.id, + id=broadcast_id, origin=self.origin, - timestamp=self.timestamp_dt, + timestamp=timestamp_dt, burst_index=burst_index, burst_data=payload_b64, total_bursts=total_bursts, - checksum=self.checksum, + checksum=checksum, repairing_callsigns=None, domain=self.domain, gridsquare=self.gridsquare, msg_type=self.message_type.name if hasattr(self.message_type, 'name') else str(self.message_type), priority=self.priority.value if hasattr(self.priority, 'value') else int(self.priority), received_at=datetime.now(timezone.utc), - nexttransmission_at = datetime.now(timezone.utc) + timedelta(hours=1), + nexttransmission_at=datetime.now(timezone.utc) + timedelta(hours=1), expires_at=datetime.now(timezone.utc), is_read=True, direction="transmit", From 6de4e2f436cf97425f2fde79b8abe2a81bde113b Mon Sep 17 00:00:00 2001 From: DJ2LS <75909252+DJ2LS@users.noreply.github.com> Date: Mon, 7 Jul 2025 21:28:39 +0200 Subject: [PATCH 037/141] adjust database - added attempts --- freedata_server/message_system_db_broadcasts.py | 3 ++- freedata_server/message_system_db_model.py | 4 +++- freedata_server/schedule_manager.py | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/freedata_server/message_system_db_broadcasts.py b/freedata_server/message_system_db_broadcasts.py index fec4bcae6..4e824d221 100644 --- a/freedata_server/message_system_db_broadcasts.py +++ b/freedata_server/message_system_db_broadcasts.py @@ -392,7 +392,8 @@ def get_broadcast_per_id(self, id): "expires_at": msg.expires_at.isoformat() if msg.expires_at else None, "nexttransmission_at": msg.nexttransmission_at.isoformat() if msg.nexttransmission_at else None, "status": msg.status.name if msg.status else None, - "error_reason": msg.error_reason + "error_reason": msg.error_reason, + "attempts": msg.attempts } except Exception as e: diff --git a/freedata_server/message_system_db_model.py b/freedata_server/message_system_db_model.py index 02dc7d4d2..a135ca0e8 100644 --- a/freedata_server/message_system_db_model.py +++ b/freedata_server/message_system_db_model.py @@ -222,6 +222,7 @@ class BroadcastMessage(Base): status_id = Column(Integer, ForeignKey('status.id'), nullable=True) status = relationship('Status', backref='broadcast_messages') error_reason = Column(String, nullable=True) + attempts = Column(Integer, default=0) Index('idx_broadcast_domain_received', 'domain', 'received_at') @@ -246,5 +247,6 @@ def to_dict(self): 'expires_at': self.expires_at.isoformat() if self.expires_at else None, 'nexttransmission_at': self.nexttransmission_at.isoformat() if self.nexttransmission_at else None, 'status': self.status.name if self.status else None, - 'error_reason': self.error_reason + 'error_reason': self.error_reason, + 'attempts':self.attempts } diff --git a/freedata_server/schedule_manager.py b/freedata_server/schedule_manager.py index b46de272d..e57616b44 100644 --- a/freedata_server/schedule_manager.py +++ b/freedata_server/schedule_manager.py @@ -222,4 +222,4 @@ def check_missing_broadcast_bursts(self): print("missing_bursts", missing_bursts) if missing_bursts: myfullcall = self.ctx.config_manager.config['STATION']['mycall'] + '-' + str(self.ctx.config_manager.config['STATION']['myssid']) - NormTransmission(self.ctx, missing_bursts["origin"] , missing_bursts["domain"]).create_and_transmit_nack_burst(myfullcall, missing_bursts["id"], missing_bursts["missing_bursts"]) + NormTransmission(self.ctx).create_and_transmit_nack_burst(myfullcall, missing_bursts["id"], missing_bursts["missing_bursts"]) From 529556253b6534c7512993af451c3bd625a35ac3 Mon Sep 17 00:00:00 2001 From: DJ2LS <75909252+DJ2LS@users.noreply.github.com> Date: Mon, 7 Jul 2025 21:46:09 +0200 Subject: [PATCH 038/141] check for max attempts --- freedata_server/frame_handler_norm.py | 5 ++++- .../message_system_db_broadcasts.py | 20 +++++++++++++++++++ freedata_server/schedule_manager.py | 2 ++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/freedata_server/frame_handler_norm.py b/freedata_server/frame_handler_norm.py index bb9afd99c..8f10f1fec 100644 --- a/freedata_server/frame_handler_norm.py +++ b/freedata_server/frame_handler_norm.py @@ -33,7 +33,10 @@ def follow_protocol(self): print(frame["id"].decode("utf-8")) broadcast = DatabaseManagerBroadcasts(self.ctx).get_broadcast_per_id(frame["id"].decode("utf-8")) if broadcast is not None: - + if broadcast["attempts"] >= 30: + print("maximum attempts reached...") + return + DatabaseManagerBroadcasts(self.ctx).increment_attempts(broadcast["id"]) data = base64.b64decode(broadcast["payload_data"]["final"]) print(broadcast) print("oring", broadcast["origin"]) diff --git a/freedata_server/message_system_db_broadcasts.py b/freedata_server/message_system_db_broadcasts.py index 4e824d221..ba73eaf14 100644 --- a/freedata_server/message_system_db_broadcasts.py +++ b/freedata_server/message_system_db_broadcasts.py @@ -333,6 +333,10 @@ def check_missing_bursts(self): if not msg.payload_data or "bursts" not in msg.payload_data: continue + # break if we reached maximum attempts + if msg.attempts >= 30: + return None + bursts = msg.payload_data["bursts"] total = msg.total_bursts @@ -402,3 +406,19 @@ def get_broadcast_per_id(self, id): finally: session.remove() + + def increment_attempts(self, message_id: str): + session = self.get_thread_scoped_session() + try: + msg = session.query(BroadcastMessage).filter_by(id=message_id).first() + if msg: + msg.attempts = (msg.attempts or 0) + 1 + session.commit() + self.log(f"Increased attempts for message {message_id} to {msg.attempts}") + else: + self.log(f"Message {message_id} not found", isWarning=True) + except Exception as e: + session.rollback() + self.log(f"Error incrementing attempts for {message_id}: {e}", isWarning=True) + finally: + session.remove() diff --git a/freedata_server/schedule_manager.py b/freedata_server/schedule_manager.py index e57616b44..5dd5572f5 100644 --- a/freedata_server/schedule_manager.py +++ b/freedata_server/schedule_manager.py @@ -221,5 +221,7 @@ def check_missing_broadcast_bursts(self): missing_bursts = DatabaseManagerBroadcasts(self.ctx).check_missing_bursts() print("missing_bursts", missing_bursts) if missing_bursts: + # Increment attmepts + DatabaseManagerBroadcasts.increment_attempts(missing_bursts["id"]) myfullcall = self.ctx.config_manager.config['STATION']['mycall'] + '-' + str(self.ctx.config_manager.config['STATION']['myssid']) NormTransmission(self.ctx).create_and_transmit_nack_burst(myfullcall, missing_bursts["id"], missing_bursts["missing_bursts"]) From cebb09c17e8f380c1a2217dccbed7db6a05fc7a7 Mon Sep 17 00:00:00 2001 From: DJ2LS <75909252+DJ2LS@users.noreply.github.com> Date: Mon, 7 Jul 2025 21:57:41 +0200 Subject: [PATCH 039/141] run nack only if next transmission is reached --- .../message_system_db_broadcasts.py | 71 +++++++++++++++---- freedata_server/schedule_manager.py | 2 +- 2 files changed, 59 insertions(+), 14 deletions(-) diff --git a/freedata_server/message_system_db_broadcasts.py b/freedata_server/message_system_db_broadcasts.py index ba73eaf14..45d3f4e5c 100644 --- a/freedata_server/message_system_db_broadcasts.py +++ b/freedata_server/message_system_db_broadcasts.py @@ -1,13 +1,8 @@ from message_system_db_manager import DatabaseManager -from message_system_db_attachments import DatabaseManagerAttachments from message_system_db_model import Status, BroadcastMessage from message_system_db_station import DatabaseManagerStations from sqlalchemy.orm.attributes import flag_modified -from datetime import datetime, timedelta -import json -import os -from datetime import timezone -from exceptions import MessageStatusError +from datetime import datetime, timedelta, timezone import helpers import base64 @@ -18,6 +13,8 @@ class DatabaseManagerBroadcasts(DatabaseManager): def __init__(self, ctx): super().__init__(ctx) + self.MAX_ATTEMPTS = 20 + self.stations_manager = DatabaseManagerStations(self.ctx) def process_broadcast_message( @@ -317,6 +314,7 @@ def check_missing_bursts(self): session = self.get_thread_scoped_session() try: one_minute_ago = datetime.now(timezone.utc) - timedelta(minutes=1) + now = datetime.now(timezone.utc) messages = ( session.query(BroadcastMessage) @@ -333,16 +331,23 @@ def check_missing_bursts(self): if not msg.payload_data or "bursts" not in msg.payload_data: continue - # break if we reached maximum attempts - if msg.attempts >= 30: - return None + # Already complete + if "final" in msg.payload_data: + continue + + # Check next transmission time + if msg.nexttransmission_at and now < msg.nexttransmission_at: + self.log(f"Skip {msg.id}: wait until {msg.nexttransmission_at.isoformat()}") + continue + + # Max attempts + if msg.attempts >= self.MAX_ATTEMPTS: + self.log(f"Skip {msg.id}: max attempts reached") + continue bursts = msg.payload_data["bursts"] total = msg.total_bursts - if "final" in msg.payload_data: - continue # bereits komplett - missing = [ i for i in range(1, total + 1) if str(i) not in bursts @@ -368,7 +373,6 @@ def check_missing_bursts(self): finally: session.remove() - def get_broadcast_per_id(self, id): session = self.get_thread_scoped_session() try: @@ -422,3 +426,44 @@ def increment_attempts(self, message_id: str): self.log(f"Error incrementing attempts for {message_id}: {e}", isWarning=True) finally: session.remove() + + + + + def increment_attempts_and_update_next_transmission(self, message_id: str): + session = self.get_thread_scoped_session() + try: + msg = session.query(BroadcastMessage).filter_by(id=message_id).first() + if not msg: + self.log(f"Message {message_id} not found", isWarning=True) + return + + # Increment attempts + msg.attempts = (msg.attempts or 0) + 1 + + # Define backoff intervals (minutes) + backoff_minutes = [5, 15, 30, 60, 120, 240, 360, 720, 1440, 2880] + if msg.attempts <= len(backoff_minutes): + next_delay = backoff_minutes[msg.attempts - 1] + else: + next_delay = 2880 # after max backoff minutes reached in table above + + # Update next transmission + msg.nexttransmission_at = datetime.now(timezone.utc) + timedelta(minutes=next_delay) + + # Check max attempts + if msg.attempts >= self.MAX_ATTEMPTS: + # Mark as failed or set specific status + status_obj = self.get_or_create_status(session, "max_attempts_reached") + msg.status_id = status_obj.id + self.log(f"Max attempts reached for {message_id}, marking as failed.") + + session.commit() + self.log( + f"Incremented attempts for {message_id} to {msg.attempts}, next transmission at {msg.nexttransmission_at}") + + except Exception as e: + session.rollback() + self.log(f"Error incrementing attempts for {message_id}: {e}", isWarning=True) + finally: + session.remove() diff --git a/freedata_server/schedule_manager.py b/freedata_server/schedule_manager.py index 5dd5572f5..5f455a75f 100644 --- a/freedata_server/schedule_manager.py +++ b/freedata_server/schedule_manager.py @@ -222,6 +222,6 @@ def check_missing_broadcast_bursts(self): print("missing_bursts", missing_bursts) if missing_bursts: # Increment attmepts - DatabaseManagerBroadcasts.increment_attempts(missing_bursts["id"]) + DatabaseManagerBroadcasts(self.ctx).increment_attempts_and_update_next_transmission(missing_bursts["id"]) myfullcall = self.ctx.config_manager.config['STATION']['mycall'] + '-' + str(self.ctx.config_manager.config['STATION']['myssid']) NormTransmission(self.ctx).create_and_transmit_nack_burst(myfullcall, missing_bursts["id"], missing_bursts["missing_bursts"]) From dcf7884855f4da37906ae5a67cdf7d37c5fd8372 Mon Sep 17 00:00:00 2001 From: DJ2LS <75909252+DJ2LS@users.noreply.github.com> Date: Mon, 7 Jul 2025 22:18:52 +0200 Subject: [PATCH 040/141] work on timezone stuff... :-/ --- freedata_server/message_system_db_broadcasts.py | 8 +++++--- freedata_server/message_system_db_model.py | 12 +++++++----- freedata_server/schedule_manager.py | 2 +- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/freedata_server/message_system_db_broadcasts.py b/freedata_server/message_system_db_broadcasts.py index 45d3f4e5c..2364133ff 100644 --- a/freedata_server/message_system_db_broadcasts.py +++ b/freedata_server/message_system_db_broadcasts.py @@ -313,9 +313,8 @@ def delete_broadcast_message_or_domain(self, id) -> dict: def check_missing_bursts(self): session = self.get_thread_scoped_session() try: - one_minute_ago = datetime.now(timezone.utc) - timedelta(minutes=1) now = datetime.now(timezone.utc) - + one_minute_ago = now - timedelta(minutes=1) messages = ( session.query(BroadcastMessage) .filter( @@ -336,6 +335,9 @@ def check_missing_bursts(self): continue # Check next transmission time + print("ok....") + print("nexttransmissionat", msg.nexttransmission_at) + print("now", now) if msg.nexttransmission_at and now < msg.nexttransmission_at: self.log(f"Skip {msg.id}: wait until {msg.nexttransmission_at.isoformat()}") continue @@ -367,7 +369,7 @@ def check_missing_bursts(self): return None except Exception as e: - self.log(f"Fehler bei check_missing_bursts: {e}", isWarning=True) + self.log(f"Fehler at check_missing_bursts: {e}", isWarning=True) return None finally: diff --git a/freedata_server/message_system_db_model.py b/freedata_server/message_system_db_model.py index a135ca0e8..f1bb210ac 100644 --- a/freedata_server/message_system_db_model.py +++ b/freedata_server/message_system_db_model.py @@ -2,7 +2,7 @@ from sqlalchemy import Index, Boolean, Column, String, Integer, JSON, ForeignKey, DateTime from sqlalchemy.orm import declarative_base, relationship -from datetime import datetime +from datetime import datetime, timezone Base = declarative_base() @@ -203,7 +203,7 @@ class BroadcastMessage(Base): id = Column(String, primary_key=True) origin = Column(String, ForeignKey('station.callsign')) - timestamp = Column(DateTime) + timestamp = Column(DateTime, default=lambda: datetime.now(timezone.utc)) repairing_callsigns = Column(JSON, nullable=True) domain = Column(String) gridsquare = Column(String) @@ -216,9 +216,11 @@ class BroadcastMessage(Base): msg_type = Column(String) total_bursts = Column(Integer, default=0) checksum = Column(String) - received_at = Column(DateTime, default=0) - nexttransmission_at = Column(DateTime, default=0) - expires_at = Column(DateTime, default=0) + received_at = Column(DateTime, default=lambda: datetime.now(timezone.utc)) + nexttransmission_at = Column(DateTime, default=lambda: datetime.now(timezone.utc)) + expires_at = Column(DateTime, default=lambda: datetime.now(timezone.utc)) + + status_id = Column(Integer, ForeignKey('status.id'), nullable=True) status = relationship('Status', backref='broadcast_messages') error_reason = Column(String, nullable=True) diff --git a/freedata_server/schedule_manager.py b/freedata_server/schedule_manager.py index 5f455a75f..ab75bfeac 100644 --- a/freedata_server/schedule_manager.py +++ b/freedata_server/schedule_manager.py @@ -46,7 +46,7 @@ def __init__(self, ctx): 'transmitting_beacon': {'function': self.transmit_beacon, 'interval': 600}, 'beacon_cleanup': {'function': self.delete_beacons, 'interval': 600}, 'update_transmission_state': {'function': self.update_transmission_state, 'interval': 10}, - 'check_missing_broadcast_bursts': {'function': self.check_missing_broadcast_bursts, 'interval': 60}, + 'check_missing_broadcast_bursts': {'function': self.check_missing_broadcast_bursts, 'interval': 20}, } self.running = False # Flag to control the running state self.scheduler_thread = None # Reference to the scheduler thread From c8be34f98706d76a0ab9f3780e0b20548163916c Mon Sep 17 00:00:00 2001 From: DJ2LS <75909252+DJ2LS@users.noreply.github.com> Date: Mon, 7 Jul 2025 22:29:25 +0200 Subject: [PATCH 041/141] work on timezone stuff... :-/ --- freedata_server/message_system_db_broadcasts.py | 2 +- freedata_server/message_system_db_model.py | 17 +++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/freedata_server/message_system_db_broadcasts.py b/freedata_server/message_system_db_broadcasts.py index 2364133ff..2ae09acfa 100644 --- a/freedata_server/message_system_db_broadcasts.py +++ b/freedata_server/message_system_db_broadcasts.py @@ -452,7 +452,7 @@ def increment_attempts_and_update_next_transmission(self, message_id: str): # Update next transmission msg.nexttransmission_at = datetime.now(timezone.utc) + timedelta(minutes=next_delay) - + print("---------------", msg.nexttransmission_at) # Check max attempts if msg.attempts >= self.MAX_ATTEMPTS: # Mark as failed or set specific status diff --git a/freedata_server/message_system_db_model.py b/freedata_server/message_system_db_model.py index f1bb210ac..8a367c2a4 100644 --- a/freedata_server/message_system_db_model.py +++ b/freedata_server/message_system_db_model.py @@ -198,12 +198,16 @@ def to_dict(self): } +from sqlalchemy import Column, DateTime, String, Integer, Boolean, JSON, ForeignKey, Index +from sqlalchemy.orm import relationship +from datetime import datetime, timezone + class BroadcastMessage(Base): __tablename__ = 'broadcast_messages' id = Column(String, primary_key=True) origin = Column(String, ForeignKey('station.callsign')) - timestamp = Column(DateTime, default=lambda: datetime.now(timezone.utc)) + timestamp = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)) repairing_callsigns = Column(JSON, nullable=True) domain = Column(String) gridsquare = Column(String) @@ -216,17 +220,18 @@ class BroadcastMessage(Base): msg_type = Column(String) total_bursts = Column(Integer, default=0) checksum = Column(String) - received_at = Column(DateTime, default=lambda: datetime.now(timezone.utc)) - nexttransmission_at = Column(DateTime, default=lambda: datetime.now(timezone.utc)) - expires_at = Column(DateTime, default=lambda: datetime.now(timezone.utc)) - + received_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)) + nexttransmission_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)) + expires_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)) status_id = Column(Integer, ForeignKey('status.id'), nullable=True) status = relationship('Status', backref='broadcast_messages') error_reason = Column(String, nullable=True) attempts = Column(Integer, default=0) - Index('idx_broadcast_domain_received', 'domain', 'received_at') + __table_args__ = ( + Index('idx_broadcast_domain_received', 'domain', 'received_at'), + ) def to_dict(self): return { From d0594527cbcb2220efe5787e9276422dee9932d6 Mon Sep 17 00:00:00 2001 From: DJ2LS <75909252+DJ2LS@users.noreply.github.com> Date: Tue, 8 Jul 2025 08:59:28 +0200 Subject: [PATCH 042/141] fix transmission stuff --- freedata_server/norm/norm_transmission.py | 43 ++- freedata_server/norm/norm_transmission_iss.py | 271 ++++++++++++------ 2 files changed, 214 insertions(+), 100 deletions(-) diff --git a/freedata_server/norm/norm_transmission.py b/freedata_server/norm/norm_transmission.py index c0399a285..5e8372f57 100644 --- a/freedata_server/norm/norm_transmission.py +++ b/freedata_server/norm/norm_transmission.py @@ -90,19 +90,31 @@ def encode_flags(self, msg_type, priority, is_last): """ Encodes message type, priority and 'last burst' flag into a single byte. - Bit layout: - Bit 7 → is_last (1 = letzter Burst) - Bits 6–3 → msg_type (0–15) - Bits 2–0 → priority (0–7) + Supports msg_type as Enum or string (e.g., "MESSAGE"). """ - if isinstance(msg_type, IntEnum): # e.g., MsgType - msg_type = int(msg_type) + # Convert msg_type + if isinstance(msg_type, str): + try: + msg_type_enum = NORMMsgType[msg_type.upper()] + except KeyError: + raise ValueError(f"Invalid msg_type string: '{msg_type}'") + elif isinstance(msg_type, NORMMsgType): + msg_type_enum = msg_type + else: + raise TypeError("msg_type must be NORMMsgType or str") + + msg_type_int = int(msg_type_enum) - assert 0 <= msg_type <= 15, "msg_type must be 0–15" - assert 0 <= priority <= 7, "priority must be 0–7" + # Convert priority + if isinstance(priority, IntEnum): + priority_int = int(priority) + else: + priority_int = int(priority) - return ((1 if is_last else 0) << 7) | ((msg_type & 0x0F) << 3) | (priority & 0x07) + assert 0 <= msg_type_int <= 15, "msg_type must be 0–15" + assert 0 <= priority_int <= 7, "priority must be 0–7" + return ((1 if is_last else 0) << 7) | ((msg_type_int & 0x0F) << 3) | (priority_int & 0x07) def decode_flags(self, flags): """ @@ -112,10 +124,21 @@ def decode_flags(self, flags): Bit 7 → is_last Bits 6–3 → msg_type (0–15) Bits 2–0 → priority (0–7) + + Returns: + is_last (bool), + msg_type (NORMMsgType), + priority (int) """ is_last = bool((flags >> 7) & 0x01) - msg_type = (flags >> 3) & 0x0F + msg_type_int = (flags >> 3) & 0x0F priority = flags & 0x07 + + try: + msg_type = NORMMsgType(msg_type_int) + except ValueError: + msg_type = NORMMsgType.UNDEFINED # Fallback bei unbekanntem Typ + return is_last, msg_type, priority def encode_burst_info(self, burst_number, total_bursts): diff --git a/freedata_server/norm/norm_transmission_iss.py b/freedata_server/norm/norm_transmission_iss.py index ee39ac37e..4b4201172 100644 --- a/freedata_server/norm/norm_transmission_iss.py +++ b/freedata_server/norm/norm_transmission_iss.py @@ -43,100 +43,191 @@ def prepare_and_transmit_data(self, origin, domain, gridsquare, data, priority=N self.transmit_bursts(bursts) def create_data(self): - full_data = self.data - total_bursts = (len(full_data) + self.MAX_PAYLOAD_SIZE - 1) // self.MAX_PAYLOAD_SIZE - - bursts = [] - for burst_number in range(1, total_bursts + 1): - offset = (burst_number - 1) * self.MAX_PAYLOAD_SIZE - payload = full_data[offset: offset + self.MAX_PAYLOAD_SIZE] - burst_info = self.encode_burst_info(burst_number, total_bursts) - checksum = helpers.get_crc_24(full_data) - is_last = (burst_number == total_bursts) - flags = self.encode_flags(self.message_type, self.priority, is_last) - - burst_frame = self.frame_factory.build_norm_data( - origin=self.origin, - domain=self.domain, - gridsquare=self.gridsquare, - timestamp=self.timestamp, - burst_info=burst_info, - payload_size=len(full_data), - payload_data=payload, - flag=flags, - checksum=checksum - ) - bursts.append(burst_frame) - return bursts + try: + if not self.data: + self.log("Data is empty", isWarning=True) + raise ValueError("Data is empty") + + full_data = self.data + total_bursts = (len(full_data) + self.MAX_PAYLOAD_SIZE - 1) // self.MAX_PAYLOAD_SIZE + + if total_bursts <= 0: + self.log( + f"Invalid burst calculation (data length: {len(full_data)}, max payload: {self.MAX_PAYLOAD_SIZE})", + isWarning=True) + raise ValueError("Invalid burst calculation") + + bursts = [] + for burst_number in range(1, total_bursts + 1): + offset = (burst_number - 1) * self.MAX_PAYLOAD_SIZE + payload = full_data[offset: offset + self.MAX_PAYLOAD_SIZE] + + if not payload: + self.log(f"Empty payload at burst {burst_number}", isWarning=True) + raise ValueError(f"Empty payload at burst {burst_number}") + + burst_info = self.encode_burst_info(burst_number, total_bursts) + checksum = helpers.get_crc_24(full_data) + is_last = (burst_number == total_bursts) + print("so...") + print("message type", self.message_type) + print("priority", self.priority) + print("is_last", is_last) + flags = self.encode_flags(self.message_type, self.priority, is_last) + print("?=") + burst_frame = self.frame_factory.build_norm_data( + origin=self.origin, + domain=self.domain, + gridsquare=self.gridsquare, + timestamp=self.timestamp, + burst_info=burst_info, + payload_size=len(full_data), + payload_data=payload, + flag=flags, + checksum=checksum + ) + + self.log( + f"Burst {burst_number}/{total_bursts} created (offset={offset}, payload_size={len(payload)}, is_last={is_last})") + + bursts.append(burst_frame) + + self.log(f"All bursts created successfully (total={total_bursts})") + return bursts + + except Exception as e: + self.log(f"Error in create_data: {e}", isWarning=True) + raise def create_repair(self, db_msg_obj: dict, burst_numbers: list[int]): - repair_bursts = [] - data = base64.b64decode(db_msg_obj["payload_data"]["final"]) - total_bursts = db_msg_obj["total_bursts"] - priority = db_msg_obj["priority"] - message_type = NORMMsgType[db_msg_obj["msg_type"]] if isinstance(db_msg_obj["msg_type"], str) else db_msg_obj["msg_type"] - checksum = bytes.fromhex(db_msg_obj["checksum"]) - - for burst_number in burst_numbers: - offset = (burst_number - 1) * self.MAX_PAYLOAD_SIZE - payload = data[offset: offset + self.MAX_PAYLOAD_SIZE] - burst_info = self.encode_burst_info(burst_number, total_bursts) - is_last = (burst_number == total_bursts) - flags = self.encode_flags(message_type, priority, is_last) - - burst_frame = self.frame_factory.build_norm_repair( - origin=db_msg_obj["origin"], - domain=db_msg_obj["domain"], - gridsquare=db_msg_obj["gridsquare"], - timestamp=int(datetime.fromisoformat(db_msg_obj["timestamp"]).timestamp()), - burst_info=burst_info, - payload_size=len(data), - payload_data=payload, - flag=flags, - checksum=checksum - ) - repair_bursts.append(burst_frame) - - return repair_bursts + try: + if "payload_data" not in db_msg_obj or "final" not in db_msg_obj["payload_data"]: + self.log("Missing payload data in db_msg_obj", isWarning=True) + raise ValueError("Missing payload data") + + data = base64.b64decode(db_msg_obj["payload_data"]["final"]) + total_bursts = db_msg_obj.get("total_bursts") + priority = db_msg_obj.get("priority") + msg_type_val = db_msg_obj.get("msg_type") + message_type = NORMMsgType[msg_type_val] if isinstance(msg_type_val, str) else msg_type_val + checksum_hex = db_msg_obj.get("checksum") + + if not all([total_bursts, priority, message_type, checksum_hex]): + self.log("Missing required fields in db_msg_obj", isWarning=True) + raise ValueError("Missing required fields") + + checksum = bytes.fromhex(checksum_hex) + repair_bursts = [] + + for burst_number in burst_numbers: + if burst_number < 1 or burst_number > total_bursts: + self.log(f"Invalid burst number {burst_number}", isWarning=True) + raise ValueError(f"Invalid burst number {burst_number}") + + offset = (burst_number - 1) * self.MAX_PAYLOAD_SIZE + payload = data[offset: offset + self.MAX_PAYLOAD_SIZE] + + if not payload: + self.log(f"Empty payload for burst {burst_number}", isWarning=True) + raise ValueError(f"Empty payload for burst {burst_number}") + + burst_info = self.encode_burst_info(burst_number, total_bursts) + is_last = (burst_number == total_bursts) + flags = self.encode_flags(message_type, priority, is_last) + + burst_frame = self.frame_factory.build_norm_repair( + origin=db_msg_obj["origin"], + domain=db_msg_obj["domain"], + gridsquare=db_msg_obj["gridsquare"], + timestamp=int(datetime.fromisoformat(db_msg_obj["timestamp"]).timestamp()), + burst_info=burst_info, + payload_size=len(data), + payload_data=payload, + flag=flags, + checksum=checksum + ) + + self.log( + f"Repair burst {burst_number}/{total_bursts} created (offset={offset}, payload_size={len(payload)}, is_last={is_last})") + repair_bursts.append(burst_frame) + + self.log(f"All repair bursts created successfully (count={len(repair_bursts)})") + return repair_bursts + + except Exception as e: + self.log(f"Error in create_repair: {e}", isWarning=True) + raise def transmit_bursts(self, bursts): - random_delay = np.random.randint(0, 6) - threading.Event().wait(random_delay) - self.ctx.state_manager.channel_busy_condition_codec2.wait(0.5) + try: + if not bursts: + self.log("No bursts to transmit", isWarning=True) + return - for burst in bursts: - self.ctx.rf_modem.transmit(FREEDV_MODE.datac4, 1, 200, burst) + random_delay = np.random.randint(0, 6) + self.log(f"Random delay before transmit: {random_delay}s") + threading.Event().wait(random_delay) + + self.log("Waiting for channel busy condition") + self.ctx.state_manager.channel_busy_condition_codec2.wait(0.5) + + for i, burst in enumerate(bursts, 1): + self.ctx.rf_modem.transmit(FREEDV_MODE.datac4, 1, 200, burst) + self.log(f"Transmitted burst {i}/{len(bursts)} (size={len(burst)})") + + self.log("All bursts transmitted successfully") + + except Exception as e: + self.log(f"Error in transmit_bursts: {e}", isWarning=True) + raise def add_to_database(self): - db = DatabaseManagerBroadcasts(self.ctx) - timestamp_dt = datetime.fromtimestamp(self.timestamp, tz=timezone.utc) - checksum = helpers.get_crc_24(self.data).hex() - broadcast_id = self.create_broadcast_id(timestamp_dt, self.domain, checksum) - - total_bursts = (len(self.data) + self.MAX_PAYLOAD_SIZE - 1) // self.MAX_PAYLOAD_SIZE - - for burst_index in range(1, total_bursts + 1): - offset = (burst_index - 1) * self.MAX_PAYLOAD_SIZE - payload_data = self.data[offset: offset + self.MAX_PAYLOAD_SIZE] - payload_b64 = base64.b64encode(payload_data).decode("ascii") - - db.process_broadcast_message( - id=broadcast_id, - origin=self.origin, - timestamp=timestamp_dt, - burst_index=burst_index, - burst_data=payload_b64, - total_bursts=total_bursts, - checksum=checksum, - repairing_callsigns=None, - domain=self.domain, - gridsquare=self.gridsquare, - msg_type=self.message_type.name if hasattr(self.message_type, 'name') else str(self.message_type), - priority=self.priority.value if hasattr(self.priority, 'value') else int(self.priority), - received_at=datetime.now(timezone.utc), - nexttransmission_at=datetime.now(timezone.utc) + timedelta(hours=1), - expires_at=datetime.now(timezone.utc), - is_read=True, - direction="transmit", - status="assembling" - ) + try: + db = DatabaseManagerBroadcasts(self.ctx) + timestamp_dt = datetime.fromtimestamp(self.timestamp, tz=timezone.utc) + checksum = helpers.get_crc_24(self.data).hex() + broadcast_id = self.create_broadcast_id(timestamp_dt, self.domain, checksum) + + total_bursts = (len(self.data) + self.MAX_PAYLOAD_SIZE - 1) // self.MAX_PAYLOAD_SIZE + + if total_bursts < 1: + self.log("No bursts to store in database", isWarning=True) + return + + for burst_index in range(1, total_bursts + 1): + offset = (burst_index - 1) * self.MAX_PAYLOAD_SIZE + payload_data = self.data[offset: offset + self.MAX_PAYLOAD_SIZE] + if not payload_data: + self.log(f"Empty payload at burst index {burst_index}", isWarning=True) + continue + + payload_b64 = base64.b64encode(payload_data).decode("ascii") + + db.process_broadcast_message( + id=broadcast_id, + origin=self.origin, + timestamp=timestamp_dt, + burst_index=burst_index, + burst_data=payload_b64, + total_bursts=total_bursts, + checksum=checksum, + repairing_callsigns=None, + domain=self.domain, + gridsquare=self.gridsquare, + msg_type=self.message_type.name if hasattr(self.message_type, "name") else str(self.message_type), + priority=self.priority.value if hasattr(self.priority, "value") else int(self.priority), + received_at=datetime.now(timezone.utc), + nexttransmission_at=datetime.now(timezone.utc) + timedelta(hours=1), + expires_at=datetime.now(timezone.utc), + is_read=True, + direction="transmit", + status="assembling" + ) + + self.log(f"Stored burst {burst_index}/{total_bursts} in database (size={len(payload_data)})") + + self.log(f"All {total_bursts} bursts added to database successfully") + + except Exception as e: + self.log(f"Error in add_to_database: {e}", isWarning=True) + raise From 3b94eefae5c2b944bae12d8fc2c2d04fd6b0bab0 Mon Sep 17 00:00:00 2001 From: DJ2LS <75909252+DJ2LS@users.noreply.github.com> Date: Tue, 8 Jul 2025 09:39:35 +0200 Subject: [PATCH 043/141] change timestamps to unix timestamps --- .../message_system_db_broadcasts.py | 46 +++++++++++-------- freedata_server/message_system_db_model.py | 16 +++---- freedata_server/norm/norm_transmission_irs.py | 8 ++-- freedata_server/norm/norm_transmission_iss.py | 14 +++--- 4 files changed, 46 insertions(+), 38 deletions(-) diff --git a/freedata_server/message_system_db_broadcasts.py b/freedata_server/message_system_db_broadcasts.py index 2ae09acfa..f33d9e47c 100644 --- a/freedata_server/message_system_db_broadcasts.py +++ b/freedata_server/message_system_db_broadcasts.py @@ -21,7 +21,7 @@ def process_broadcast_message( self, id: str, origin: str, - timestamp: datetime, + timestamp: float, burst_index: int, burst_data: str, total_bursts: int, @@ -30,9 +30,9 @@ def process_broadcast_message( domain: str = None, gridsquare: str = None, msg_type: str = None, - received_at: datetime = None, - expires_at: datetime = None, - nexttransmission_at: datetime = None, + received_at: float = None, + expires_at: float = None, + nexttransmission_at: float = None, priority: int = 1, is_read: bool = True, direction: str = None, @@ -58,6 +58,11 @@ def process_broadcast_message( origin_station = self.stations_manager.get_or_create_station(origin, session) status_obj = self.get_or_create_status(session, status) if status else None + print("nexttransmission_at", nexttransmission_at) + print("received_at", received_at) + print("timestamp", timestamp) + print("exires_at", expires_at) + # New message msg = BroadcastMessage( id=id, @@ -151,7 +156,7 @@ def get_all_broadcasts_json(self) -> list: result.append({ "id": msg.id, "origin": msg.origin, - "timestamp": msg.timestamp.isoformat() if msg.timestamp else None, + "timestamp": msg.timestamp if msg.timestamp else None, "repairing_callsigns": msg.repairing_callsigns, "domain": msg.domain, "gridsquare": msg.gridsquare, @@ -164,9 +169,9 @@ def get_all_broadcasts_json(self) -> list: "msg_type": msg.msg_type, "total_bursts": msg.total_bursts, "checksum": msg.checksum, - "received_at": msg.received_at.isoformat() if msg.received_at else None, - "expires_at": msg.expires_at.isoformat() if msg.expires_at else None, - "nexttransmission_at": msg.nexttransmission_at.isoformat() if msg.nexttransmission_at else None, + "received_at": msg.received_at if msg.received_at else None, + "expires_at": msg.expires_at if msg.expires_at else None, + "nexttransmission_at": msg.nexttransmission_at if msg.nexttransmission_at else None, "status": msg.status.name if msg.status else None, "error_reason": msg.error_reason }) @@ -201,7 +206,7 @@ def get_broadcast_domains_json(self) -> dict: result[domain] = { "message_count": 1, "last_message_id": msg.id, - "last_message_timestamp": msg.timestamp.isoformat() if msg.timestamp else None, + "last_message_timestamp": msg.timestamp if msg.timestamp else None, "last_origin": msg.origin } else: @@ -239,7 +244,7 @@ def get_broadcasts_per_domain_json(self, domain: str = None) -> dict: result[d].append({ "id": msg.id, - "timestamp": msg.timestamp.isoformat() if msg.timestamp else None, + "timestamp": msg.timestamp if msg.timestamp else None, "origin": msg.origin, "gridsquare": msg.gridsquare, "msg_type": msg.msg_type, @@ -248,9 +253,9 @@ def get_broadcasts_per_domain_json(self, domain: str = None) -> dict: "direction": msg.direction, "status": msg.status.name if msg.status else None, "error_reason": msg.error_reason, - "received_at": msg.received_at.isoformat() if msg.received_at else None, - "nexttransmission_at": msg.nexttransmission_at.isoformat() if msg.nexttransmission_at else None, - "expires_at": msg.expires_at.isoformat() if msg.expires_at else None + "received_at": msg.received_at if msg.received_at else None, + "nexttransmission_at": msg.nexttransmission_at if msg.nexttransmission_at else None, + "expires_at": msg.expires_at if msg.expires_at else None }) return result @@ -338,8 +343,11 @@ def check_missing_bursts(self): print("ok....") print("nexttransmissionat", msg.nexttransmission_at) print("now", now) + print("expires_at", msg.expires_at) + print("timestamp", msg.timestamp) + print("received_at", msg.received_at) if msg.nexttransmission_at and now < msg.nexttransmission_at: - self.log(f"Skip {msg.id}: wait until {msg.nexttransmission_at.isoformat()}") + self.log(f"Skip {msg.id}: wait until {msg.nexttransmission_at}") continue # Max attempts @@ -363,7 +371,7 @@ def check_missing_bursts(self): "missing_bursts": missing, "total_bursts": total, "received_bursts": list(bursts.keys()), - "received_at": msg.received_at.isoformat() if msg.received_at else None + "received_at": msg.received_at if msg.received_at else None } return None @@ -385,7 +393,7 @@ def get_broadcast_per_id(self, id): return { "id": msg.id, "origin": msg.origin, - "timestamp": msg.timestamp.isoformat() if msg.timestamp else None, + "timestamp": msg.timestamp if msg.timestamp else None, "repairing_callsigns": msg.repairing_callsigns, "domain": msg.domain, "gridsquare": msg.gridsquare, @@ -398,9 +406,9 @@ def get_broadcast_per_id(self, id): "msg_type": msg.msg_type, "total_bursts": msg.total_bursts, "checksum": msg.checksum, - "received_at": msg.received_at.isoformat() if msg.received_at else None, - "expires_at": msg.expires_at.isoformat() if msg.expires_at else None, - "nexttransmission_at": msg.nexttransmission_at.isoformat() if msg.nexttransmission_at else None, + "received_at": msg.received_at if msg.received_at else None, + "expires_at": msg.expires_at if msg.expires_at else None, + "nexttransmission_at": msg.nexttransmission_at if msg.nexttransmission_at else None, "status": msg.status.name if msg.status else None, "error_reason": msg.error_reason, "attempts": msg.attempts diff --git a/freedata_server/message_system_db_model.py b/freedata_server/message_system_db_model.py index 8a367c2a4..3ffd7cae4 100644 --- a/freedata_server/message_system_db_model.py +++ b/freedata_server/message_system_db_model.py @@ -1,6 +1,6 @@ # models.py -from sqlalchemy import Index, Boolean, Column, String, Integer, JSON, ForeignKey, DateTime +from sqlalchemy import Index, Boolean, Column, String, Integer, JSON, ForeignKey, DateTime, Float from sqlalchemy.orm import declarative_base, relationship from datetime import datetime, timezone @@ -207,7 +207,7 @@ class BroadcastMessage(Base): id = Column(String, primary_key=True) origin = Column(String, ForeignKey('station.callsign')) - timestamp = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)) + timestamp = Column(Float, default=lambda: datetime.now(timezone.utc)) repairing_callsigns = Column(JSON, nullable=True) domain = Column(String) gridsquare = Column(String) @@ -220,9 +220,9 @@ class BroadcastMessage(Base): msg_type = Column(String) total_bursts = Column(Integer, default=0) checksum = Column(String) - received_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)) - nexttransmission_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)) - expires_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)) + received_at = Column(Float, default=lambda: datetime.now(timezone.utc)) + nexttransmission_at = Column(Float, default=lambda: datetime.now(timezone.utc)) + expires_at = Column(Float, default=lambda: datetime.now(timezone.utc)) status_id = Column(Integer, ForeignKey('status.id'), nullable=True) status = relationship('Status', backref='broadcast_messages') @@ -250,9 +250,9 @@ def to_dict(self): 'msg_type': self.msg_type, 'total_bursts': self.total_bursts, 'checksum': self.checksum, - 'received_at': self.received_at.isoformat() if self.received_at else None, - 'expires_at': self.expires_at.isoformat() if self.expires_at else None, - 'nexttransmission_at': self.nexttransmission_at.isoformat() if self.nexttransmission_at else None, + 'received_at': self.received_at if self.received_at else None, + 'expires_at': self.expires_at if self.expires_at else None, + 'nexttransmission_at': self.nexttransmission_at if self.nexttransmission_at else None, 'status': self.status.name if self.status else None, 'error_reason': self.error_reason, 'attempts':self.attempts diff --git a/freedata_server/norm/norm_transmission_irs.py b/freedata_server/norm/norm_transmission_irs.py index daf296368..5e6b9ccd8 100644 --- a/freedata_server/norm/norm_transmission_irs.py +++ b/freedata_server/norm/norm_transmission_irs.py @@ -31,7 +31,7 @@ def __init__(self, ctx, frame): self.domain = frame["domain"] self.gridsquare = frame["gridsquare"] self.checksum = frame["checksum"] - self.timestamp = datetime.fromtimestamp(frame["timestamp"], tz=timezone.utc) + self.timestamp = frame["timestamp"] print("####################################") print("payload_size:", payload_size) print("payload_data:", payload_data) @@ -68,9 +68,9 @@ def __init__(self, ctx, frame): gridsquare=self.gridsquare, msg_type=msg_type, priority=priority, - received_at=datetime.now(timezone.utc), - expires_at=datetime.now(timezone.utc), - nexttransmission_at=datetime.now(timezone.utc), + received_at=datetime.now(timezone.utc).timestamp(), + expires_at=datetime.now(timezone.utc).timestamp(), + nexttransmission_at=datetime.now(timezone.utc).timestamp(), is_read=True, direction="receive", status="assembling" diff --git a/freedata_server/norm/norm_transmission_iss.py b/freedata_server/norm/norm_transmission_iss.py index 4b4201172..3f2f2258e 100644 --- a/freedata_server/norm/norm_transmission_iss.py +++ b/freedata_server/norm/norm_transmission_iss.py @@ -184,9 +184,8 @@ def transmit_bursts(self, bursts): def add_to_database(self): try: db = DatabaseManagerBroadcasts(self.ctx) - timestamp_dt = datetime.fromtimestamp(self.timestamp, tz=timezone.utc) checksum = helpers.get_crc_24(self.data).hex() - broadcast_id = self.create_broadcast_id(timestamp_dt, self.domain, checksum) + broadcast_id = self.create_broadcast_id(int(self.timestamp), self.domain, checksum) total_bursts = (len(self.data) + self.MAX_PAYLOAD_SIZE - 1) // self.MAX_PAYLOAD_SIZE @@ -202,11 +201,12 @@ def add_to_database(self): continue payload_b64 = base64.b64encode(payload_data).decode("ascii") - + nexttransmission_at = datetime.now(timezone.utc) + timedelta(hours=1) + nexttransmission_at = nexttransmission_at.timestamp() db.process_broadcast_message( id=broadcast_id, origin=self.origin, - timestamp=timestamp_dt, + timestamp=self.timestamp, burst_index=burst_index, burst_data=payload_b64, total_bursts=total_bursts, @@ -216,9 +216,9 @@ def add_to_database(self): gridsquare=self.gridsquare, msg_type=self.message_type.name if hasattr(self.message_type, "name") else str(self.message_type), priority=self.priority.value if hasattr(self.priority, "value") else int(self.priority), - received_at=datetime.now(timezone.utc), - nexttransmission_at=datetime.now(timezone.utc) + timedelta(hours=1), - expires_at=datetime.now(timezone.utc), + received_at=datetime.now(timezone.utc).timestamp(), + nexttransmission_at=nexttransmission_at, + expires_at=datetime.now(timezone.utc).timestamp(), is_read=True, direction="transmit", status="assembling" From b61c068726095d8d412577602df1fd1a456bf2f4 Mon Sep 17 00:00:00 2001 From: DJ2LS <75909252+DJ2LS@users.noreply.github.com> Date: Tue, 8 Jul 2025 09:46:02 +0200 Subject: [PATCH 044/141] change timestamps to unix timestamps --- freedata_server/message_system_db_broadcasts.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/freedata_server/message_system_db_broadcasts.py b/freedata_server/message_system_db_broadcasts.py index f33d9e47c..424e42584 100644 --- a/freedata_server/message_system_db_broadcasts.py +++ b/freedata_server/message_system_db_broadcasts.py @@ -319,12 +319,15 @@ def check_missing_bursts(self): session = self.get_thread_scoped_session() try: now = datetime.now(timezone.utc) - one_minute_ago = now - timedelta(minutes=1) + one_minute_ago_ts = (now - timedelta(minutes=1)).timestamp() + now = now.timestamp() + print(now) + print(one_minute_ago_ts) messages = ( session.query(BroadcastMessage) .filter( BroadcastMessage.direction == "receive", - BroadcastMessage.received_at < one_minute_ago, + BroadcastMessage.received_at < one_minute_ago_ts, BroadcastMessage.total_bursts > 0 ) .order_by(BroadcastMessage.received_at.asc()) @@ -340,7 +343,6 @@ def check_missing_bursts(self): continue # Check next transmission time - print("ok....") print("nexttransmissionat", msg.nexttransmission_at) print("now", now) print("expires_at", msg.expires_at) @@ -459,7 +461,7 @@ def increment_attempts_and_update_next_transmission(self, message_id: str): next_delay = 2880 # after max backoff minutes reached in table above # Update next transmission - msg.nexttransmission_at = datetime.now(timezone.utc) + timedelta(minutes=next_delay) + msg.nexttransmission_at = (datetime.now(timezone.utc) + timedelta(minutes=next_delay)).timestamp() print("---------------", msg.nexttransmission_at) # Check max attempts if msg.attempts >= self.MAX_ATTEMPTS: From 0294570f29142beb5f28a9a0644a7ad4a5c49a7f Mon Sep 17 00:00:00 2001 From: DJ2LS <75909252+DJ2LS@users.noreply.github.com> Date: Tue, 8 Jul 2025 10:01:52 +0200 Subject: [PATCH 045/141] fixing message type --- freedata_server/norm/norm_transmission_irs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freedata_server/norm/norm_transmission_irs.py b/freedata_server/norm/norm_transmission_irs.py index 5e6b9ccd8..3222107f5 100644 --- a/freedata_server/norm/norm_transmission_irs.py +++ b/freedata_server/norm/norm_transmission_irs.py @@ -66,7 +66,7 @@ def __init__(self, ctx, frame): repairing_callsigns=frame.get("repairing_callsigns"), domain=self.domain, gridsquare=self.gridsquare, - msg_type=msg_type, + msg_type=msg_type.name, priority=priority, received_at=datetime.now(timezone.utc).timestamp(), expires_at=datetime.now(timezone.utc).timestamp(), From 34dd0fa7e3c92b247822374c3fc767141de6ab1c Mon Sep 17 00:00:00 2001 From: DJ2LS <75909252+DJ2LS@users.noreply.github.com> Date: Tue, 8 Jul 2025 10:29:25 +0200 Subject: [PATCH 046/141] fixing info modal --- .../components/broadcast_message_received.vue | 13 ++- .../src/components/broadcast_message_sent.vue | 6 +- freedata_gui/src/components/main_modals.vue | 81 +++++++++++++++++-- freedata_gui/src/store/broadcastStore.js | 3 + 4 files changed, 90 insertions(+), 13 deletions(-) diff --git a/freedata_gui/src/components/broadcast_message_received.vue b/freedata_gui/src/components/broadcast_message_received.vue index 38fb057b2..65c123fe8 100644 --- a/freedata_gui/src/components/broadcast_message_received.vue +++ b/freedata_gui/src/components/broadcast_message_received.vue @@ -31,7 +31,7 @@ class="btn btn-outline-secondary border-0 me-1" data-bs-target="#broadcastMessageInfoModal" data-bs-toggle="modal" - @click="showMessageInfo" + @click="showBroadcastMessageInfo" > @@ -60,6 +60,12 @@ import { marked } from 'marked'; import DOMPurify from 'dompurify'; import {deleteBroadcastMessageFromDB, sendBroadcastADIFviaUDP} from "@/js/broadcastsHandler"; +import { setActivePinia } from 'pinia'; +import pinia from '../store/index'; +import { useBroadcastStore } from '../store/broadcastStore.js'; +setActivePinia(pinia); +const broadcast = useBroadcastStore(pinia); + export default { props: { message: Object, @@ -109,8 +115,9 @@ export default { }, methods: { - showMessageInfo() { - this.$emit("show-info", this.message); + showBroadcastMessageInfo() { + console.log(this.message); + broadcast.selectedMessage = this.message; }, }, }; diff --git a/freedata_gui/src/components/broadcast_message_sent.vue b/freedata_gui/src/components/broadcast_message_sent.vue index ed7f8474f..4e954d637 100644 --- a/freedata_gui/src/components/broadcast_message_sent.vue +++ b/freedata_gui/src/components/broadcast_message_sent.vue @@ -7,7 +7,7 @@ class="btn btn-outline-secondary border-0 me-1" data-bs-target="#broadcastMessageInfoModal" data-bs-toggle="modal" - @click="showMessageInfo" + @click="showBroadcastMessageInfo" > @@ -100,8 +100,8 @@ export default { }, methods: { - showMessageInfo() { - this.$emit("show-info", this.message); + showBroadcastMessageInfo() { + broadcast.selectedMessage = this.message; }, async retransmitBroadcast() { diff --git a/freedata_gui/src/components/main_modals.vue b/freedata_gui/src/components/main_modals.vue index 0ab83f293..a39f4619c 100644 --- a/freedata_gui/src/components/main_modals.vue +++ b/freedata_gui/src/components/main_modals.vue @@ -436,7 +436,7 @@ const beaconHistogramData = computed(() => ({ ({ ({ - ... + {{ broadcast.selectedMessage?.origin?? 'NaN' }} - {{ broadcast.selectedMessage?.timestamp ?? 'NaN' }} ({ /> + - {{ $t('general.statistics') }} + {{ $t('broadcast.general') }} + + {{ $t('broadcast.id') }} + {{ broadcast.selectedMessage?.id ?? 'NaN' }} + + + + {{ $t('broadcast.msg_type') }} + {{ broadcast.selectedMessage?.msg_type ?? 'NaN' }} + + + + {{ $t('broadcast.timestamp') }} + {{ broadcast.selectedMessage?.timestamp ?? 'NaN' }} + + + + {{ $t('broadcast.nexttransmission_at') }} + {{ broadcast.selectedMessage?.nexttransmission_at ?? 'NaN' }} + - ... + + {{ $t('broadcast.expires_at') }} + {{ broadcast.selectedMessage?.expires_at ?? 'NaN' }} + + + + + + + {{ $t('broadcast.sender') }} + + + + + + {{ $t('broadcast.origin') }} + {{ broadcast.selectedMessage?.origin ?? 'NaN' }} + + + + {{ $t('broadcast.gridsquare') }} + {{ broadcast.selectedMessage?.gridsquare ?? 'NaN' }} + + + + + + + {{ $t('broadcast.data') }} + + + + + + + {{ $t('broadcast.payload_size') }} + {{ broadcast.selectedMessage?.payload_size ?? 'NaN' }} + + + + {{ $t('broadcast.bursts') }} + {{ broadcast.selectedMessage?.bursts ?? 'NaN' }} + + + + + +
+ {{ $t('chat.selectChat') }} +
- {{ $t('chat.selectChat') }} + {{ $t('broadcast.selectDomain') }}