From e4744a113f326a18abdadd8955bc4737fb7fe77b Mon Sep 17 00:00:00 2001 From: DJ2LS Date: Mon, 11 Mar 2024 19:27:09 +0100 Subject: [PATCH 01/13] build fix --- gui/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/package.json b/gui/package.json index 53f4146f..7e0b1cd5 100644 --- a/gui/package.json +++ b/gui/package.json @@ -63,7 +63,7 @@ "@types/nconf": "^0.10.6", "@typescript-eslint/eslint-plugin": "6.21.0", "@vitejs/plugin-vue": "5.0.4", - "electron": "29.1.1", + "electron": "28.2.6", "electron-builder": "24.9.1", "eslint": "8.56.0", "eslint-config-prettier": "9.1.0", From 2b32fb740c37cdf4f7f2ff2b73afbe7898ba05d3 Mon Sep 17 00:00:00 2001 From: DJ2LS Date: Mon, 11 Mar 2024 19:52:03 +0100 Subject: [PATCH 02/13] restart server on input overflow --- modem/demodulator.py | 10 +++++----- modem/modem.py | 3 ++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/modem/demodulator.py b/modem/demodulator.py index ebf1139c..8549271f 100644 --- a/modem/demodulator.py +++ b/modem/demodulator.py @@ -4,10 +4,7 @@ import ctypes import structlog import threading import audio -import os -from modem_frametypes import FRAME_TYPE import itertools -from time import sleep TESTMODE = False @@ -28,11 +25,11 @@ class Demodulator(): 'decoding_thread': None } - def __init__(self, config, audio_rx_q, data_q_rx, states, event_manager, fft_queue): + def __init__(self, config, audio_rx_q, data_q_rx, states, event_manager, service_queue, fft_queue): self.log = structlog.get_logger("Demodulator") self.rx_audio_level = config['AUDIO']['rx_audio_level'] - + self.service_queue = service_queue self.AUDIO_FRAMES_PER_BUFFER_RX = 4800 self.buffer_overflow_counter = [0, 0, 0, 0, 0, 0, 0, 0] self.is_codec2_traffic_counter = 0 @@ -129,6 +126,9 @@ class Demodulator(): def sd_input_audio_callback(self, indata: np.ndarray, frames: int, time, status) -> None: if status: self.log.warning("[AUDIO STATUS]", status=status, time=time, frames=frames) + # FIXME on windows input overflows crashing the rx audio stream. Lets restart the server then + if status.input_overflow: + self.service_queue.put("restart") return try: audio_48k = np.frombuffer(indata, dtype=np.int16) diff --git a/modem/modem.py b/modem/modem.py index 63e04496..d86049c3 100644 --- a/modem/modem.py +++ b/modem/modem.py @@ -65,7 +65,7 @@ class RF: self.MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000 # 8192 Let's do some tests with very small chunks for TX - self.AUDIO_FRAMES_PER_BUFFER_TX = 1200 if self.radiocontrol in ["tci"] else 2400 * 2 + #self.AUDIO_FRAMES_PER_BUFFER_TX = 1200 if self.radiocontrol in ["tci"] else 2400 * 2 # 8 * (self.AUDIO_SAMPLE_RATE/self.MODEM_SAMPLE_RATE) == 48 self.AUDIO_CHANNELS = 1 self.MODE = 0 @@ -83,6 +83,7 @@ class RF: self.data_queue_received, self.states, self.event_manager, + self.service_queue, self.fft_queue ) From 6b7146e02c802da5b308895c929efbe586d22aa6 Mon Sep 17 00:00:00 2001 From: DJ2LS Date: Mon, 11 Mar 2024 20:07:15 +0100 Subject: [PATCH 03/13] more clean server shutdown --- modem/server.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/modem/server.py b/modem/server.py index ca5e8e96..0df93651 100644 --- a/modem/server.py +++ b/modem/server.py @@ -1,3 +1,5 @@ +import time + from flask import Flask, request, jsonify, make_response, abort, Response from flask_sock import Sock from flask_cors import CORS @@ -20,6 +22,8 @@ import command_test import command_arq_raw import command_message_send import event_manager +import atexit + from message_system_db_manager import DatabaseManager from message_system_db_messages import DatabaseManagerMessages from message_system_db_attachments import DatabaseManagerAttachments @@ -320,6 +324,11 @@ def sock_fft(sock): def sock_states(sock): wsm.handle_connection(sock, wsm.states_client_list, app.state_queue) +@atexit.register +def stop_server(): + app.service_manager.stop_modem() + time.sleep(1) + print('Server shutdown...') if __name__ == "__main__": @@ -353,7 +362,7 @@ if __name__ == "__main__": modemport = conf['NETWORK']['modemport'] if not modemaddress: - modemaddress = '0.0.0.0' + modemaddress = '127.0.0.1' if not modemport: modemport = 5000 app.run(modemaddress, modemport) From dd703b4bbd94ed1ce1700fa38fc1f558cc5060a7 Mon Sep 17 00:00:00 2001 From: DJ2LS Date: Tue, 12 Mar 2024 16:33:40 +0100 Subject: [PATCH 04/13] using audio transmit callback --- modem/modem.py | 63 +++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 50 insertions(+), 13 deletions(-) diff --git a/modem/modem.py b/modem/modem.py index d86049c3..d3e031a8 100644 --- a/modem/modem.py +++ b/modem/modem.py @@ -52,7 +52,6 @@ class RF: self.rigctld_ip = config['RIGCTLD']['ip'] self.rigctld_port = config['RIGCTLD']['port'] - self.states.setTransmitting(False) self.ptt_state = False self.radio_alc = 0.0 @@ -71,6 +70,8 @@ class RF: self.MODE = 0 self.rms_counter = 0 + self.audio_out_queue = queue.Queue() + # Make sure our resampler will work assert (self.AUDIO_SAMPLE_RATE / self.MODEM_SAMPLE_RATE) == codec2.api.FDMDV_OS_48 # type: ignore @@ -160,10 +161,18 @@ class RF: ) self.sd_input_stream.start() + self.sd_output_stream = sd.OutputStream( + channels=1, + dtype="int16", + callback=self.sd_output_audio_callback, + device=out_dev_index, + samplerate=self.AUDIO_SAMPLE_RATE, + blocksize=4800, + ) + self.sd_output_stream.start() + return True - - except Exception as audioerr: self.log.error("[MDM] init: starting pyaudio callback failed", e=audioerr) self.stop_modem() @@ -294,15 +303,12 @@ class RF: txbuffer_out = x # transmit audio - self.transmit_audio(txbuffer_out) - - self.radio.set_ptt(False) - self.event_manager.send_ptt_change(False) - self.states.setTransmitting(False) + self.enqueue_audio_out(txbuffer_out) end_of_transmission = time.time() transmission_time = end_of_transmission - start_of_transmission self.log.debug("[MDM] ON AIR TIME", time=transmission_time) + return True def transmit_add_preamble(self, buffer, freedv): @@ -381,9 +387,9 @@ class RF: ) start_of_transmission = time.time() - txbuffer_out = cw.MorseCodePlayer().text_to_signal("DJ2LS-1") + txbuffer_out = cw.MorseCodePlayer().text_to_signal(self.config['STATION'].mycall) - self.transmit_audio(txbuffer_out) + self.enqueue_audio_out(txbuffer_out) self.radio.set_ptt(False) self.event_manager.send_ptt_change(False) @@ -404,8 +410,10 @@ class RF: self.freedv_datac13_tx = codec2.open_instance(codec2.FREEDV_MODE.datac13.value) - # Low level modem audio transmit - def transmit_audio(self, audio_48k) -> None: + def enqueue_audio_out(self, audio_48k) -> None: + if not self.states.isTransmitting(): + self.states.setTransmitting(True) + self.radio.set_ptt(True) self.event_manager.send_ptt_change(True) @@ -414,5 +422,34 @@ class RF: # we need to wait manually for tci processing self.tci_module.wait_until_transmitted(audio_48k) else: - sd.play(audio_48k, blocksize=4096, blocking=True) + # slice audio data to needed blocklength + block_size = 4800 + pad_length = -len(audio_48k) % block_size + padded_data = np.pad(audio_48k, (0, pad_length), mode='constant') + sliced_audio_data = padded_data.reshape(-1, block_size) + # add each block to audio out queue + for block in sliced_audio_data: + self.audio_out_queue.put(block) + + self.states.transmitting_event.wait() + + self.radio.set_ptt(False) + self.event_manager.send_ptt_change(False) + return + + def sd_output_audio_callback(self, outdata: np.ndarray, frames: int, time, status) -> None: + + try: + if not self.audio_out_queue.empty(): + chunk = self.audio_out_queue.get_nowait() + audio.calculate_fft(chunk, self.fft_queue, self.states) + outdata[:] = chunk.reshape(outdata.shape) + + else: + # Fill with zeros if the queue is empty + self.states.setTransmitting(False) + outdata.fill(0) + except Exception as e: + self.log.warning("[AUDIO STATUS]", status=status, time=time, frames=frames, e=e) + outdata.fill(0) From b58749a8a56df426aeca242919eea60ab568b0f4 Mon Sep 17 00:00:00 2001 From: DJ2LS Date: Tue, 12 Mar 2024 18:39:30 +0100 Subject: [PATCH 05/13] restructured modem part --- modem/config.ini.example | 1 - modem/config.py | 1 - modem/modem.py | 246 +++++++-------------------------------- modem/modulator.py | 150 ++++++++++++++++++++++++ 4 files changed, 192 insertions(+), 206 deletions(-) create mode 100644 modem/modulator.py diff --git a/modem/config.ini.example b/modem/config.ini.example index ffcaa0b0..c8c7dae1 100644 --- a/modem/config.ini.example +++ b/modem/config.ini.example @@ -15,7 +15,6 @@ input_device = 5a1c output_device = bd6c rx_audio_level = 0 tx_audio_level = 0 -enable_auto_tune = False [RIGCTLD] ip = 127.0.0.1 diff --git a/modem/config.py b/modem/config.py index ec8f75c0..fa9926df 100644 --- a/modem/config.py +++ b/modem/config.py @@ -26,7 +26,6 @@ class CONFIG: 'output_device': str, 'rx_audio_level': int, 'tx_audio_level': int, - 'enable_auto_tune': bool, }, 'RADIO': { 'control': str, diff --git a/modem/modem.py b/modem/modem.py index d3e031a8..a7ff27c2 100644 --- a/modem/modem.py +++ b/modem/modem.py @@ -10,9 +10,7 @@ Created on Wed Dec 23 07:04:24 2020 # pylint: disable=import-outside-toplevel import atexit -import ctypes import queue -import threading import time import codec2 import numpy as np @@ -20,9 +18,9 @@ import sounddevice as sd import structlog import tci import cw -from queues import RIGCTLD_COMMAND_QUEUE import audio import demodulator +import modulator TESTMODE = False @@ -44,28 +42,26 @@ class RF: self.audio_input_device = config['AUDIO']['input_device'] self.audio_output_device = config['AUDIO']['output_device'] - self.tx_audio_level = config['AUDIO']['tx_audio_level'] - self.enable_audio_auto_tune = config['AUDIO']['enable_auto_tune'] - self.tx_delay = config['MODEM']['tx_delay'] + self.radiocontrol = config['RADIO']['control'] self.rigctld_ip = config['RIGCTLD']['ip'] self.rigctld_port = config['RIGCTLD']['port'] - - self.ptt_state = False - self.radio_alc = 0.0 - self.tci_ip = config['TCI']['tci_ip'] self.tci_port = config['TCI']['tci_port'] + self.tx_audio_level = config['AUDIO']['tx_audio_level'] + + + self.ptt_state = False self.AUDIO_SAMPLE_RATE = 48000 - self.MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000 + self.modem_sample_rate = codec2.api.FREEDV_FS_8000 # 8192 Let's do some tests with very small chunks for TX #self.AUDIO_FRAMES_PER_BUFFER_TX = 1200 if self.radiocontrol in ["tci"] else 2400 * 2 - # 8 * (self.AUDIO_SAMPLE_RATE/self.MODEM_SAMPLE_RATE) == 48 + # 8 * (self.AUDIO_SAMPLE_RATE/self.modem_sample_rate) == 48 self.AUDIO_CHANNELS = 1 self.MODE = 0 self.rms_counter = 0 @@ -73,7 +69,7 @@ class RF: self.audio_out_queue = queue.Queue() # Make sure our resampler will work - assert (self.AUDIO_SAMPLE_RATE / self.MODEM_SAMPLE_RATE) == codec2.api.FDMDV_OS_48 # type: ignore + assert (self.AUDIO_SAMPLE_RATE / self.modem_sample_rate) == codec2.api.FDMDV_OS_48 # type: ignore self.audio_received_queue = queue.Queue() self.data_queue_received = queue.Queue() @@ -88,6 +84,8 @@ class RF: self.fft_queue ) + self.modulator = modulator.Modulator(self.config) + def tci_tx_callback(self, audio_48k) -> None: @@ -107,8 +105,7 @@ class RF: self.demodulator.start(self.sd_input_stream) atexit.register(self.sd_input_stream.stop) - # Initialize codec2, rig control, and data threads - self.init_codec2() + return True @@ -195,188 +192,7 @@ class RF: return True - def audio_auto_tune(self): - # enable / disable AUDIO TUNE Feature / ALC correction - if self.enable_audio_auto_tune: - if self.radio_alc == 0.0: - self.tx_audio_level = self.tx_audio_level + 20 - elif 0.0 < self.radio_alc <= 0.1: - print("0.0 < self.radio_alc <= 0.1") - self.tx_audio_level = self.tx_audio_level + 2 - self.log.debug("[MDM] AUDIO TUNE", audio_level=str(self.tx_audio_level), - alc_level=str(self.radio_alc)) - elif 0.1 < self.radio_alc < 0.2: - print("0.1 < self.radio_alc < 0.2") - self.tx_audio_level = self.tx_audio_level - self.log.debug("[MDM] AUDIO TUNE", audio_level=str(self.tx_audio_level), - alc_level=str(self.radio_alc)) - elif 0.2 < self.radio_alc < 0.99: - print("0.2 < self.radio_alc < 0.99") - self.tx_audio_level = self.tx_audio_level - 20 - self.log.debug("[MDM] AUDIO TUNE", audio_level=str(self.tx_audio_level), - alc_level=str(self.radio_alc)) - elif 1.0 >= self.radio_alc: - print("1.0 >= self.radio_alc") - self.tx_audio_level = self.tx_audio_level - 40 - self.log.debug("[MDM] AUDIO TUNE", audio_level=str(self.tx_audio_level), - alc_level=str(self.radio_alc)) - else: - self.log.debug("[MDM] AUDIO TUNE", audio_level=str(self.tx_audio_level), - alc_level=str(self.radio_alc)) - def transmit( - self, mode, repeats: int, repeat_delay: int, frames: bytearray - ) -> bool: - """ - - Args: - mode: - repeats: - repeat_delay: - frames: - - """ - if TESTMODE: - return - - - self.demodulator.reset_data_sync() - # get freedv instance by mode - mode_transition = { - codec2.FREEDV_MODE.signalling: self.freedv_datac13_tx, - codec2.FREEDV_MODE.datac0: self.freedv_datac0_tx, - codec2.FREEDV_MODE.datac1: self.freedv_datac1_tx, - codec2.FREEDV_MODE.datac3: self.freedv_datac3_tx, - codec2.FREEDV_MODE.datac4: self.freedv_datac4_tx, - codec2.FREEDV_MODE.datac13: self.freedv_datac13_tx, - } - if mode in mode_transition: - freedv = mode_transition[mode] - else: - print("wrong mode.................") - print(mode) - return False - - # Wait for some other thread that might be transmitting - self.states.waitForTransmission() - self.states.setTransmitting(True) - #self.states.channel_busy_event.wait() - - - start_of_transmission = time.time() - - # Open codec2 instance - self.MODE = mode - - txbuffer = bytes() - - # Add empty data to handle ptt toggle time - if self.tx_delay > 0: - self.transmit_add_silence(txbuffer, self.tx_delay) - - self.log.debug( - "[MDM] TRANSMIT", mode=self.MODE.name, delay=self.tx_delay - ) - - if not isinstance(frames, list): frames = [frames] - for _ in range(repeats): - - # Create modulation for all frames in the list - for frame in frames: - - txbuffer = self.transmit_add_preamble(txbuffer, freedv) - txbuffer = self.transmit_create_frame(txbuffer, freedv, frame) - txbuffer = self.transmit_add_postamble(txbuffer, freedv) - - # Add delay to end of frames - self.transmit_add_silence(txbuffer, repeat_delay) - - # Re-sample back up to 48k (resampler works on np.int16) - x = np.frombuffer(txbuffer, dtype=np.int16) - - self.audio_auto_tune() - x = audio.set_audio_volume(x, self.tx_audio_level) - - if self.radiocontrol not in ["tci"]: - txbuffer_out = self.resampler.resample8_to_48(x) - else: - txbuffer_out = x - - # transmit audio - self.enqueue_audio_out(txbuffer_out) - - end_of_transmission = time.time() - transmission_time = end_of_transmission - start_of_transmission - self.log.debug("[MDM] ON AIR TIME", time=transmission_time) - - return True - - def transmit_add_preamble(self, buffer, freedv): - - # Init buffer for preample - n_tx_preamble_modem_samples = codec2.api.freedv_get_n_tx_preamble_modem_samples( - freedv - ) - mod_out_preamble = ctypes.create_string_buffer(n_tx_preamble_modem_samples * 2) - - # Write preamble to txbuffer - codec2.api.freedv_rawdatapreambletx(freedv, mod_out_preamble) - buffer += bytes(mod_out_preamble) - return buffer - - def transmit_add_postamble(self, buffer, freedv): - # Init buffer for postamble - n_tx_postamble_modem_samples = ( - codec2.api.freedv_get_n_tx_postamble_modem_samples(freedv) - ) - mod_out_postamble = ctypes.create_string_buffer( - n_tx_postamble_modem_samples * 2 - ) - # Write postamble to txbuffer - codec2.api.freedv_rawdatapostambletx(freedv, mod_out_postamble) - # Append postamble to txbuffer - buffer += bytes(mod_out_postamble) - return buffer - - def transmit_add_silence(self, buffer, duration): - data_delay = int(self.MODEM_SAMPLE_RATE * (duration / 1000)) # type: ignore - mod_out_silence = ctypes.create_string_buffer(data_delay * 2) - buffer += bytes(mod_out_silence) - return buffer - - def transmit_create_frame(self, txbuffer, freedv, frame): - # Get number of bytes per frame for mode - bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(freedv) / 8) - payload_bytes_per_frame = bytes_per_frame - 2 - - # Init buffer for data - n_tx_modem_samples = codec2.api.freedv_get_n_tx_modem_samples(freedv) - mod_out = ctypes.create_string_buffer(n_tx_modem_samples * 2) - - # Create buffer for data - # Use this if CRC16 checksum is required (DATAc1-3) - buffer = bytearray(payload_bytes_per_frame) - # Set buffersize to length of data which will be send - buffer[: len(frame)] = frame # type: ignore - - # Create crc for data frame - - # Use the crc function shipped with codec2 - # to avoid CRC algorithm incompatibilities - # Generate CRC16 - crc = ctypes.c_ushort( - codec2.api.freedv_gen_crc16(bytes(buffer), payload_bytes_per_frame) - ) - # Convert crc to 2-byte (16-bit) hex string - crc = crc.value.to_bytes(2, byteorder="big") - # Append CRC to data buffer - buffer += crc - - assert (bytes_per_frame == len(buffer)) - data = (ctypes.c_ubyte * bytes_per_frame).from_buffer_copy(buffer) - # modulate DATA and save it into mod_out pointer - codec2.api.freedv_rawdatatx(freedv, mod_out, data) - txbuffer += bytes(mod_out) - return txbuffer def transmit_morse(self, repeats, repeat_delay, frames): self.states.waitForTransmission() @@ -399,15 +215,7 @@ class RF: transmission_time = end_of_transmission - start_of_transmission self.log.debug("[MDM] ON AIR TIME", time=transmission_time) - def init_codec2(self): - # Open codec2 instances - # INIT TX MODES - here we need all modes. - self.freedv_datac0_tx = codec2.open_instance(codec2.FREEDV_MODE.datac0.value) - self.freedv_datac1_tx = codec2.open_instance(codec2.FREEDV_MODE.datac1.value) - self.freedv_datac3_tx = codec2.open_instance(codec2.FREEDV_MODE.datac3.value) - self.freedv_datac4_tx = codec2.open_instance(codec2.FREEDV_MODE.datac4.value) - self.freedv_datac13_tx = codec2.open_instance(codec2.FREEDV_MODE.datac13.value) def enqueue_audio_out(self, audio_48k) -> None: @@ -453,3 +261,33 @@ class RF: except Exception as e: self.log.warning("[AUDIO STATUS]", status=status, time=time, frames=frames, e=e) outdata.fill(0) + + def transmit( + self, mode, repeats: int, repeat_delay: int, frames: bytearray + ) -> bool: + + self.demodulator.reset_data_sync() + + # Wait for some other thread that might be transmitting + self.states.waitForTransmission() + self.states.setTransmitting(True) + # self.states.channel_busy_event.wait() + + start_of_transmission = time.time() + txbuffer = self.modulator.create_burst(mode, repeats, repeat_delay, frames) + + # Re-sample back up to 48k (resampler works on np.int16) + x = np.frombuffer(txbuffer, dtype=np.int16) + x = audio.set_audio_volume(x, self.tx_audio_level) + + if self.radiocontrol not in ["tci"]: + txbuffer_out = self.resampler.resample8_to_48(x) + else: + txbuffer_out = x + + # transmit audio + self.enqueue_audio_out(txbuffer_out) + + end_of_transmission = time.time() + transmission_time = end_of_transmission - start_of_transmission + self.log.debug("[MDM] ON AIR TIME", time=transmission_time) diff --git a/modem/modulator.py b/modem/modulator.py new file mode 100644 index 00000000..2cfec1b6 --- /dev/null +++ b/modem/modulator.py @@ -0,0 +1,150 @@ +import ctypes +import codec2 +import structlog + + +class Modulator: + log = structlog.get_logger("RF") + + def __init__(self, config): + self.config = config + self.tx_delay = config['MODEM']['tx_delay'] + self.modem_sample_rate = codec2.api.FREEDV_FS_8000 + + # Initialize codec2, rig control, and data threads + self.init_codec2() + + def init_codec2(self): + # Open codec2 instances + + # INIT TX MODES - here we need all modes. + self.freedv_datac0_tx = codec2.open_instance(codec2.FREEDV_MODE.datac0.value) + self.freedv_datac1_tx = codec2.open_instance(codec2.FREEDV_MODE.datac1.value) + self.freedv_datac3_tx = codec2.open_instance(codec2.FREEDV_MODE.datac3.value) + self.freedv_datac4_tx = codec2.open_instance(codec2.FREEDV_MODE.datac4.value) + self.freedv_datac13_tx = codec2.open_instance(codec2.FREEDV_MODE.datac13.value) + + def transmit_add_preamble(self, buffer, freedv): + # Init buffer for preample + n_tx_preamble_modem_samples = codec2.api.freedv_get_n_tx_preamble_modem_samples( + freedv + ) + mod_out_preamble = ctypes.create_string_buffer(n_tx_preamble_modem_samples * 2) + + # Write preamble to txbuffer + codec2.api.freedv_rawdatapreambletx(freedv, mod_out_preamble) + buffer += bytes(mod_out_preamble) + return buffer + + def transmit_add_postamble(self, buffer, freedv): + # Init buffer for postamble + n_tx_postamble_modem_samples = ( + codec2.api.freedv_get_n_tx_postamble_modem_samples(freedv) + ) + mod_out_postamble = ctypes.create_string_buffer( + n_tx_postamble_modem_samples * 2 + ) + # Write postamble to txbuffer + codec2.api.freedv_rawdatapostambletx(freedv, mod_out_postamble) + # Append postamble to txbuffer + buffer += bytes(mod_out_postamble) + return buffer + + def transmit_add_silence(self, buffer, duration): + data_delay = int(self.modem_sample_rate * (duration / 1000)) # type: ignore + mod_out_silence = ctypes.create_string_buffer(data_delay * 2) + buffer += bytes(mod_out_silence) + return buffer + + def transmit_create_frame(self, txbuffer, freedv, frame): + # Get number of bytes per frame for mode + bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(freedv) / 8) + payload_bytes_per_frame = bytes_per_frame - 2 + + # Init buffer for data + n_tx_modem_samples = codec2.api.freedv_get_n_tx_modem_samples(freedv) + mod_out = ctypes.create_string_buffer(n_tx_modem_samples * 2) + + # Create buffer for data + # Use this if CRC16 checksum is required (DATAc1-3) + buffer = bytearray(payload_bytes_per_frame) + # Set buffersize to length of data which will be send + buffer[: len(frame)] = frame # type: ignore + + # Create crc for data frame - + # Use the crc function shipped with codec2 + # to avoid CRC algorithm incompatibilities + # Generate CRC16 + crc = ctypes.c_ushort( + codec2.api.freedv_gen_crc16(bytes(buffer), payload_bytes_per_frame) + ) + # Convert crc to 2-byte (16-bit) hex string + crc = crc.value.to_bytes(2, byteorder="big") + # Append CRC to data buffer + buffer += crc + + assert (bytes_per_frame == len(buffer)) + data = (ctypes.c_ubyte * bytes_per_frame).from_buffer_copy(buffer) + # modulate DATA and save it into mod_out pointer + codec2.api.freedv_rawdatatx(freedv, mod_out, data) + txbuffer += bytes(mod_out) + return txbuffer + + def create_burst( + self, mode, repeats: int, repeat_delay: int, frames: bytearray + ) -> bool: + """ + + Args: + mode: + repeats: + repeat_delay: + frames: + + """ + + + + # get freedv instance by mode + mode_transition = { + codec2.FREEDV_MODE.signalling: self.freedv_datac13_tx, + codec2.FREEDV_MODE.datac0: self.freedv_datac0_tx, + codec2.FREEDV_MODE.datac1: self.freedv_datac1_tx, + codec2.FREEDV_MODE.datac3: self.freedv_datac3_tx, + codec2.FREEDV_MODE.datac4: self.freedv_datac4_tx, + codec2.FREEDV_MODE.datac13: self.freedv_datac13_tx, + } + if mode in mode_transition: + freedv = mode_transition[mode] + else: + print("wrong mode.................") + print(mode) + return False + + + # Open codec2 instance + self.MODE = mode + self.log.debug( + "[MDM] TRANSMIT", mode=self.MODE.name, delay=self.tx_delay + ) + + txbuffer = bytes() + + # Add empty data to handle ptt toggle time + if self.tx_delay > 0: + self.transmit_add_silence(txbuffer, self.tx_delay) + + if not isinstance(frames, list): frames = [frames] + for _ in range(repeats): + + # Create modulation for all frames in the list + for frame in frames: + txbuffer = self.transmit_add_preamble(txbuffer, freedv) + txbuffer = self.transmit_create_frame(txbuffer, freedv, frame) + txbuffer = self.transmit_add_postamble(txbuffer, freedv) + + # Add delay to end of frames + self.transmit_add_silence(txbuffer, repeat_delay) + + return txbuffer + From 7bed6041f375a8a8d51fb8d927268ff596058cdb Mon Sep 17 00:00:00 2001 From: DJ2LS Date: Tue, 12 Mar 2024 19:48:50 +0100 Subject: [PATCH 06/13] restructured modem part --- modem/demodulator.py | 33 ---------------- modem/modem.py | 93 ++++++++++++++++++++++++++++++-------------- modem/server.py | 6 ++- 3 files changed, 67 insertions(+), 65 deletions(-) diff --git a/modem/demodulator.py b/modem/demodulator.py index 8549271f..df5b2de0 100644 --- a/modem/demodulator.py +++ b/modem/demodulator.py @@ -28,7 +28,6 @@ class Demodulator(): def __init__(self, config, audio_rx_q, data_q_rx, states, event_manager, service_queue, fft_queue): self.log = structlog.get_logger("Demodulator") - self.rx_audio_level = config['AUDIO']['rx_audio_level'] self.service_queue = service_queue self.AUDIO_FRAMES_PER_BUFFER_RX = 4800 self.buffer_overflow_counter = [0, 0, 0, 0, 0, 0, 0, 0] @@ -123,38 +122,6 @@ class Demodulator(): ) self.MODE_DICT[mode]['decoding_thread'].start() - def sd_input_audio_callback(self, indata: np.ndarray, frames: int, time, status) -> None: - if status: - self.log.warning("[AUDIO STATUS]", status=status, time=time, frames=frames) - # FIXME on windows input overflows crashing the rx audio stream. Lets restart the server then - if status.input_overflow: - self.service_queue.put("restart") - return - try: - audio_48k = np.frombuffer(indata, dtype=np.int16) - audio_8k = self.resampler.resample48_to_8(audio_48k) - - audio_8k_level_adjusted = audio.set_audio_volume(audio_8k, self.rx_audio_level) - audio.calculate_fft(audio_8k_level_adjusted, self.fft_queue, self.states) - - length_audio_8k_level_adjusted = len(audio_8k_level_adjusted) - # Avoid buffer overflow by filling only if buffer for - # selected datachannel mode is not full - index = 0 - for mode in self.MODE_DICT: - mode_data = self.MODE_DICT[mode] - audiobuffer = mode_data['audio_buffer'] - decode = mode_data['decode'] - index += 1 - if audiobuffer: - if (audiobuffer.nbuffer + length_audio_8k_level_adjusted) > audiobuffer.size: - self.buffer_overflow_counter[index] += 1 - self.event_manager.send_buffer_overflow(self.buffer_overflow_counter) - elif decode: - audiobuffer.push(audio_8k_level_adjusted) - except Exception as e: - self.log.warning("[AUDIO EXCEPTION]", status=status, time=time, frames=frames, e=e) - def get_frequency_offset(self, freedv: ctypes.c_void_p) -> float: """ diff --git a/modem/modem.py b/modem/modem.py index a7ff27c2..9d8ce499 100644 --- a/modem/modem.py +++ b/modem/modem.py @@ -52,6 +52,7 @@ class RF: self.tci_port = config['TCI']['tci_port'] self.tx_audio_level = config['AUDIO']['tx_audio_level'] + self.rx_audio_level = config['AUDIO']['rx_audio_level'] self.ptt_state = False @@ -151,7 +152,7 @@ class RF: self.sd_input_stream = sd.InputStream( channels=1, dtype="int16", - callback=self.demodulator.sd_input_audio_callback, + callback=self.sd_input_audio_callback, device=in_dev_index, samplerate=self.AUDIO_SAMPLE_RATE, blocksize=4800, @@ -205,17 +206,44 @@ class RF: txbuffer_out = cw.MorseCodePlayer().text_to_signal(self.config['STATION'].mycall) + # transmit audio self.enqueue_audio_out(txbuffer_out) - self.radio.set_ptt(False) - self.event_manager.send_ptt_change(False) - - self.states.setTransmitting(False) end_of_transmission = time.time() transmission_time = end_of_transmission - start_of_transmission self.log.debug("[MDM] ON AIR TIME", time=transmission_time) + def transmit( + self, mode, repeats: int, repeat_delay: int, frames: bytearray + ) -> bool: + + self.demodulator.reset_data_sync() + + # Wait for some other thread that might be transmitting + self.states.waitForTransmission() + self.states.setTransmitting(True) + # self.states.channel_busy_event.wait() + + start_of_transmission = time.time() + txbuffer = self.modulator.create_burst(mode, repeats, repeat_delay, frames) + + # Re-sample back up to 48k (resampler works on np.int16) + x = np.frombuffer(txbuffer, dtype=np.int16) + x = audio.set_audio_volume(x, self.tx_audio_level) + + if self.radiocontrol not in ["tci"]: + txbuffer_out = self.resampler.resample8_to_48(x) + else: + txbuffer_out = x + + # transmit audio + self.enqueue_audio_out(txbuffer_out) + + end_of_transmission = time.time() + transmission_time = end_of_transmission - start_of_transmission + self.log.debug("[MDM] ON AIR TIME", time=transmission_time) + def enqueue_audio_out(self, audio_48k) -> None: @@ -262,32 +290,37 @@ class RF: self.log.warning("[AUDIO STATUS]", status=status, time=time, frames=frames, e=e) outdata.fill(0) - def transmit( - self, mode, repeats: int, repeat_delay: int, frames: bytearray - ) -> bool: + def sd_input_audio_callback(self, indata: np.ndarray, frames: int, time, status) -> None: + if status: + self.log.warning("[AUDIO STATUS]", status=status, time=time, frames=frames) + # FIXME on windows input overflows crashing the rx audio stream. Lets restart the server then + if status.input_overflow: + self.service_queue.put("restart") + return + try: + audio_48k = np.frombuffer(indata, dtype=np.int16) + audio_8k = self.resampler.resample48_to_8(audio_48k) - self.demodulator.reset_data_sync() + audio_8k_level_adjusted = audio.set_audio_volume(audio_8k, self.rx_audio_level) - # Wait for some other thread that might be transmitting - self.states.waitForTransmission() - self.states.setTransmitting(True) - # self.states.channel_busy_event.wait() + if not self.states.isTransmitting(): + audio.calculate_fft(audio_8k_level_adjusted, self.fft_queue, self.states) - start_of_transmission = time.time() - txbuffer = self.modulator.create_burst(mode, repeats, repeat_delay, frames) + length_audio_8k_level_adjusted = len(audio_8k_level_adjusted) + # Avoid buffer overflow by filling only if buffer for + # selected datachannel mode is not full + index = 0 + for mode in self.demodulator.MODE_DICT: + mode_data = self.demodulator.MODE_DICT[mode] + audiobuffer = mode_data['audio_buffer'] + decode = mode_data['decode'] + index += 1 + if audiobuffer: + if (audiobuffer.nbuffer + length_audio_8k_level_adjusted) > audiobuffer.size: + self.demodulator.buffer_overflow_counter[index] += 1 + self.event_manager.send_buffer_overflow(self.demodulator.buffer_overflow_counter) + elif decode: + audiobuffer.push(audio_8k_level_adjusted) + except Exception as e: + self.log.warning("[AUDIO EXCEPTION]", status=status, time=time, frames=frames, e=e) - # Re-sample back up to 48k (resampler works on np.int16) - x = np.frombuffer(txbuffer, dtype=np.int16) - x = audio.set_audio_volume(x, self.tx_audio_level) - - if self.radiocontrol not in ["tci"]: - txbuffer_out = self.resampler.resample8_to_48(x) - else: - txbuffer_out = x - - # transmit audio - self.enqueue_audio_out(txbuffer_out) - - end_of_transmission = time.time() - transmission_time = end_of_transmission - start_of_transmission - self.log.debug("[MDM] ON AIR TIME", time=transmission_time) diff --git a/modem/server.py b/modem/server.py index 0df93651..529d6259 100644 --- a/modem/server.py +++ b/modem/server.py @@ -326,11 +326,13 @@ def sock_states(sock): @atexit.register def stop_server(): - app.service_manager.stop_modem() + try: + app.service_manager.stop_modem() + except Exception: + print("Error stopping modem") time.sleep(1) print('Server shutdown...') - if __name__ == "__main__": app.config['SOCK_SERVER_OPTIONS'] = {'ping_interval': 10} # define global MODEM_VERSION From c8fa826e60542aa48ffee7c7c59d18f29cdaa2f6 Mon Sep 17 00:00:00 2001 From: DJ2LS Date: Tue, 12 Mar 2024 19:54:07 +0100 Subject: [PATCH 07/13] fixed tx buffer delay --- modem/modulator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modem/modulator.py b/modem/modulator.py index 2cfec1b6..2ec0bd4f 100644 --- a/modem/modulator.py +++ b/modem/modulator.py @@ -132,7 +132,7 @@ class Modulator: # Add empty data to handle ptt toggle time if self.tx_delay > 0: - self.transmit_add_silence(txbuffer, self.tx_delay) + txbuffer = self.transmit_add_silence(txbuffer, self.tx_delay) if not isinstance(frames, list): frames = [frames] for _ in range(repeats): @@ -144,7 +144,7 @@ class Modulator: txbuffer = self.transmit_add_postamble(txbuffer, freedv) # Add delay to end of frames - self.transmit_add_silence(txbuffer, repeat_delay) + txbuffer = self.transmit_add_silence(txbuffer, repeat_delay) return txbuffer From f5de99a25b6c9560b432bbe57f9b4a907b27151f Mon Sep 17 00:00:00 2001 From: Mashintime Date: Tue, 12 Mar 2024 20:40:35 -0400 Subject: [PATCH 08/13] Fix 18m freq selection --- gui/src/components/dynamic_components.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/src/components/dynamic_components.vue b/gui/src/components/dynamic_components.vue index abd6a3af..6229187f 100644 --- a/gui/src/components/dynamic_components.vue +++ b/gui/src/components/dynamic_components.vue @@ -832,7 +832,7 @@ function quickfill() {
15m
- +
18.106 MHz
EU / US From 4f4c678eacaba0063fb752b6893e43a80340786b Mon Sep 17 00:00:00 2001 From: Mashintime Date: Tue, 12 Mar 2024 21:37:44 -0400 Subject: [PATCH 09/13] Clicking a heard station populates ping textboxs --- gui/src/components/grid/grid_active_broadcasts.vue | 8 ++++++++ gui/src/components/grid/grid_active_broadcasts_vert.vue | 8 ++++++++ gui/src/components/grid/grid_active_heard_stations.vue | 6 +++++- .../components/grid/grid_active_heard_stations_mini.vue | 6 +++++- gui/src/components/grid/grid_ping.vue | 9 +++++++++ 5 files changed, 35 insertions(+), 2 deletions(-) diff --git a/gui/src/components/grid/grid_active_broadcasts.vue b/gui/src/components/grid/grid_active_broadcasts.vue index e4a466c3..08f1a04e 100644 --- a/gui/src/components/grid/grid_active_broadcasts.vue +++ b/gui/src/components/grid/grid_active_broadcasts.vue @@ -21,6 +21,14 @@ function startStopBeacon() { } } var dxcallPing = ref(""); +window.addEventListener( + "stationSelected", + function (eventdata) { + let evt = eventdata; + dxcallPing.value = evt.detail; + }, + false, + );