diff --git a/tnc/modem.py b/tnc/modem.py index d8338d47..996bcd1b 100644 --- a/tnc/modem.py +++ b/tnc/modem.py @@ -26,7 +26,8 @@ import static import structlog import ujson as json import tci -from queues import DATA_QUEUE_RECEIVED, MODEM_RECEIVED_QUEUE, MODEM_TRANSMIT_QUEUE, RIGCTLD_COMMAND_QUEUE, AUDIO_RECEIVED_QUEUE, AUDIO_TRANSMIT_QUEUE +from queues import DATA_QUEUE_RECEIVED, MODEM_RECEIVED_QUEUE, MODEM_TRANSMIT_QUEUE, RIGCTLD_COMMAND_QUEUE, \ + AUDIO_RECEIVED_QUEUE, AUDIO_TRANSMIT_QUEUE TESTMODE = False RXCHANNEL = "" @@ -40,7 +41,6 @@ RECEIVE_SIG1 = False RECEIVE_DATAC1 = False RECEIVE_DATAC3 = False - # state buffer SIG0_DATAC0_STATE = [] SIG1_DATAC0_STATE = [] @@ -49,6 +49,7 @@ DAT0_DATAC3_STATE = [] FSK_LDPC0_STATE = [] FSK_LDPC1_STATE = [] + class RF: """Class to encapsulate interactions between the audio device and codec2""" @@ -62,10 +63,10 @@ class RF: self.AUDIO_SAMPLE_RATE_RX = 48000 self.AUDIO_SAMPLE_RATE_TX = 48000 self.MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000 + self.AUDIO_FRAMES_PER_BUFFER_RX = 2400 * 2 # 8192 # 8192 Let's do some tests with very small chunks for TX - self.AUDIO_FRAMES_PER_BUFFER_TX = 2400 * 2 - + self.AUDIO_FRAMES_PER_BUFFER_TX = 1200 if static.AUDIO_ENABLE_TCI else 2400 * 2 # 8 * (self.AUDIO_SAMPLE_RATE_RX/self.MODEM_SAMPLE_RATE) == 48 self.AUDIO_CHANNELS = 1 self.MODE = 0 @@ -89,7 +90,6 @@ class RF: self.audio_received_queue = AUDIO_RECEIVED_QUEUE self.audio_transmit_queue = AUDIO_TRANSMIT_QUEUE - # Init FIFO queue to store modulation out in self.modoutqueue = deque() @@ -188,10 +188,12 @@ class RF: # placeholder area for processing audio via TCI # https://github.com/maksimus1210/TCI self.log.warning("[MDM] [TCI] Not yet fully implemented", ip=static.TCI_IP, port=static.TCI_PORT) + # we are trying this by simulating an audio stream Object like with mkfifo class Object: """An object for simulating audio stream""" active = True + self.stream = Object() # lets init TCI module @@ -254,14 +256,17 @@ class RF: sys.exit(1) elif static.HAMLIB_RADIOCONTROL == "rigctld": import rigctld as rig + elif static.AUDIO_ENABLE_TCI: + self.radio = self.tci_module else: import rigdummy as rig - self.hamlib = rig.radio() - self.hamlib.open_rig( - rigctld_ip=static.HAMLIB_RIGCTLD_IP, - rigctld_port=static.HAMLIB_RIGCTLD_PORT, - ) + if not static.AUDIO_ENABLE_TCI: + self.radio = rig.radio() + self.radio.open_rig( + rigctld_ip=static.HAMLIB_RIGCTLD_IP, + rigctld_port=static.HAMLIB_RIGCTLD_PORT, + ) # --------------------------------------------START DECODER THREAD if static.ENABLE_FFT: @@ -302,7 +307,6 @@ class RF: ) audio_thread_dat0_datac3.start() - hamlib_thread = threading.Thread( target=self.update_rig_data, name="HAMLIB_THREAD", daemon=True ) @@ -332,11 +336,14 @@ class RF: while True: threading.Event().wait(0.01) - - # -----write if len(self.modoutqueue) > 0 and not self.mod_out_locked: - data_out48k = self.modoutqueue.popleft() - self.tci_module.push_audio(data_out48k) + static.PTT_STATE = self.radio.set_ptt(True) + jsondata = {"ptt": "True"} + data_out = json.dumps(jsondata) + sock.SOCKET_QUEUE.put(data_out) + + data_out = self.modoutqueue.popleft() + self.tci_module.push_audio(data_out) def tci_rx_callback(self) -> None: """ @@ -351,7 +358,7 @@ class RF: x = self.audio_received_queue.get() x = np.frombuffer(x, dtype=np.int16) - #x = self.resampler.resample48_to_8(x) + # x = self.resampler.resample48_to_8(x) self.fft_data = x @@ -370,8 +377,6 @@ class RF: ): data_buffer.push(x) - - def mkfifo_read_callback(self) -> None: """ Support testing by reading the audio data from a pipe and @@ -469,7 +474,7 @@ class RF: if not static.PTT_STATE: # TODO: Moved to this place for testing # Maybe we can avoid moments of silence before transmitting - static.PTT_STATE = self.hamlib.set_ptt(True) + static.PTT_STATE = self.radio.set_ptt(True) jsondata = {"ptt": "True"} data_out = json.dumps(jsondata) sock.SOCKET_QUEUE.put(data_out) @@ -528,7 +533,7 @@ class RF: start_of_transmission = time.time() # TODO: Moved ptt toggle some steps before audio is ready for testing # Toggle ptt early to save some time and send ptt state via socket - # static.PTT_STATE = self.hamlib.set_ptt(True) + # static.PTT_STATE = self.radio.set_ptt(True) # jsondata = {"ptt": "True"} # data_out = json.dumps(jsondata) # sock.SOCKET_QUEUE.put(data_out) @@ -632,24 +637,32 @@ class RF: elif 0.0 < static.HAMLIB_ALC <= 0.1: print("0.0 < static.HAMLIB_ALC <= 0.1") static.TX_AUDIO_LEVEL = static.TX_AUDIO_LEVEL + 2 - self.log.debug("[MDM] AUDIO TUNE", audio_level=str(static.TX_AUDIO_LEVEL), alc_level=str(static.HAMLIB_ALC)) + self.log.debug("[MDM] AUDIO TUNE", audio_level=str(static.TX_AUDIO_LEVEL), + alc_level=str(static.HAMLIB_ALC)) elif 0.1 < static.HAMLIB_ALC < 0.2: print("0.1 < static.HAMLIB_ALC < 0.2") static.TX_AUDIO_LEVEL = static.TX_AUDIO_LEVEL - self.log.debug("[MDM] AUDIO TUNE", audio_level=str(static.TX_AUDIO_LEVEL), alc_level=str(static.HAMLIB_ALC)) + self.log.debug("[MDM] AUDIO TUNE", audio_level=str(static.TX_AUDIO_LEVEL), + alc_level=str(static.HAMLIB_ALC)) elif 0.2 < static.HAMLIB_ALC < 0.99: print("0.2 < static.HAMLIB_ALC < 0.99") static.TX_AUDIO_LEVEL = static.TX_AUDIO_LEVEL - 20 - self.log.debug("[MDM] AUDIO TUNE", audio_level=str(static.TX_AUDIO_LEVEL), alc_level=str(static.HAMLIB_ALC)) - elif 1.0 >=static.HAMLIB_ALC: + self.log.debug("[MDM] AUDIO TUNE", audio_level=str(static.TX_AUDIO_LEVEL), + alc_level=str(static.HAMLIB_ALC)) + elif 1.0 >= static.HAMLIB_ALC: print("1.0 >= static.HAMLIB_ALC") static.TX_AUDIO_LEVEL = static.TX_AUDIO_LEVEL - 40 - self.log.debug("[MDM] AUDIO TUNE", audio_level=str(static.TX_AUDIO_LEVEL), alc_level=str(static.HAMLIB_ALC)) + self.log.debug("[MDM] AUDIO TUNE", audio_level=str(static.TX_AUDIO_LEVEL), + alc_level=str(static.HAMLIB_ALC)) else: - self.log.debug("[MDM] AUDIO TUNE", audio_level=str(static.TX_AUDIO_LEVEL), alc_level=str(static.HAMLIB_ALC)) + self.log.debug("[MDM] AUDIO TUNE", audio_level=str(static.TX_AUDIO_LEVEL), + alc_level=str(static.HAMLIB_ALC)) x = set_audio_volume(x, static.TX_AUDIO_LEVEL) - txbuffer_48k = self.resampler.resample8_to_48(x) + if not static.AUDIO_ENABLE_TCI: + txbuffer_out = self.resampler.resample8_to_48(x) + else: + txbuffer_out = x # Explicitly lock our usage of mod_out_queue if needed # This could avoid audio problems on slower CPU @@ -660,8 +673,8 @@ class RF: # ------------------------------- chunk_length = self.AUDIO_FRAMES_PER_BUFFER_TX # 4800 chunk = [ - txbuffer_48k[i: i + chunk_length] - for i in range(0, len(txbuffer_48k), chunk_length) + txbuffer_out[i: i + chunk_length] + for i in range(0, len(txbuffer_out), chunk_length) ] for c in chunk: # Pad the chunk, if needed @@ -670,18 +683,36 @@ class RF: delta_zeros = np.zeros(delta, dtype=np.int16) c = np.append(c, delta_zeros) # self.log.debug("[MDM] mod out shorter than audio buffer", delta=delta) - self.modoutqueue.append(c) # Release our mod_out_lock, so we can use the queue self.mod_out_locked = False - while self.modoutqueue: + # we need to wait manually for tci processing + if static.AUDIO_ENABLE_TCI: + duration = len(txbuffer_out) / 8000 + timestamp_to_sleep = time.time() + duration + self.log.debug("[MDM] TCI calculated duration", duration=duration) + tci_timeout_reached = False + #while time.time() < timestamp_to_sleep: + # threading.Event().wait(0.01) + else: + timestamp_to_sleep = time.time() + # set tci timeout reached to True for overriding if not used + tci_timeout_reached = True + + while self.modoutqueue or not tci_timeout_reached: + if static.AUDIO_ENABLE_TCI: + if time.time() < timestamp_to_sleep: + tci_timeout_reached = False + else: + tci_timeout_reached = True + threading.Event().wait(0.01) # if we're transmitting FreeDATA signals, reset channel busy state static.CHANNEL_BUSY = False - static.PTT_STATE = self.hamlib.set_ptt(False) + static.PTT_STATE = self.radio.set_ptt(False) # Push ptt state to socket stream jsondata = {"ptt": "False"} @@ -1051,10 +1082,10 @@ class RF: cmd = RIGCTLD_COMMAND_QUEUE.get() if cmd[0] == "set_frequency": # [1] = Frequency - self.hamlib.set_frequency(cmd[1]) + self.radio.set_frequency(cmd[1]) if cmd[0] == "set_mode": # [1] = Mode - self.hamlib.set_mode(cmd[1]) + self.radio.set_mode(cmd[1]) def update_rig_data(self) -> None: """ @@ -1067,22 +1098,22 @@ class RF: while True: # this looks weird, but is necessary for avoiding rigctld packet colission sock threading.Event().wait(0.25) - static.HAMLIB_FREQUENCY = self.hamlib.get_frequency() + static.HAMLIB_FREQUENCY = self.radio.get_frequency() threading.Event().wait(0.1) - static.HAMLIB_MODE = self.hamlib.get_mode() + static.HAMLIB_MODE = self.radio.get_mode() threading.Event().wait(0.1) - static.HAMLIB_BANDWIDTH = self.hamlib.get_bandwidth() + static.HAMLIB_BANDWIDTH = self.radio.get_bandwidth() threading.Event().wait(0.1) - static.HAMLIB_STATUS = self.hamlib.get_status() + static.HAMLIB_STATUS = self.radio.get_status() threading.Event().wait(0.1) if static.TRANSMITTING: - static.HAMLIB_ALC = self.hamlib.get_alc() + static.HAMLIB_ALC = self.radio.get_alc() threading.Event().wait(0.1) - #static.HAMLIB_RF = self.hamlib.get_level() - #threading.Event().wait(0.1) - static.HAMLIB_STRENGTH = self.hamlib.get_strength() + # static.HAMLIB_RF = self.radio.get_level() + # threading.Event().wait(0.1) + static.HAMLIB_STRENGTH = self.radio.get_strength() - #print(f"ALC: {static.HAMLIB_ALC}, RF: {static.HAMLIB_RF}, STRENGTH: {static.HAMLIB_STRENGTH}") + # print(f"ALC: {static.HAMLIB_ALC}, RF: {static.HAMLIB_RF}, STRENGTH: {static.HAMLIB_STRENGTH}") def calculate_fft(self) -> None: """ diff --git a/tnc/static.py b/tnc/static.py index 5d1af6cb..7d48fc98 100644 --- a/tnc/static.py +++ b/tnc/static.py @@ -11,7 +11,7 @@ Not nice, suggestions are appreciated :-) import subprocess from enum import Enum -VERSION = "0.8.0-alpha.4" +VERSION = "0.8.0-alpha.4-TCI-exp" ENABLE_EXPLORER = False ENABLE_STATS = False diff --git a/tnc/tci.py b/tnc/tci.py index d1ee0699..6739d9a1 100644 --- a/tnc/tci.py +++ b/tnc/tci.py @@ -4,14 +4,10 @@ import structlog import threading import websocket +import numpy as np +import time from queues import AUDIO_TRANSMIT_QUEUE, AUDIO_RECEIVED_QUEUE -""" -trx:0,true; -trx:0,false; - -""" - class TCI: def __init__(self, hostname='127.0.0.1', port=50001): # websocket.enableTrace(True) @@ -32,6 +28,25 @@ class TCI: ) tci_thread.start() + # flag if we're receiving a tx_chrono + self.tx_chrono = False + + # audio related parameters, will be updated by tx chrono + self.sample_rate = None + self.format = None + self.codec = None + self.audio_length = None + self.crc = None + self.channel = None + + self.frequency = None + self.bandwidth = None + self.mode = None + self.alc = None + self.meter = None + self.level = None + self.ptt = None + def connect(self): self.log.info( "[TCI] Starting TCI thread!", ip=self.hostname, port=self.port @@ -45,17 +60,49 @@ class TCI: ) self.ws.run_forever(reconnect=5) # Set dispatcher to automatic reconnection, 5 second reconnect delay if con> - #rel.signal(2, rel.abort) # Keyboard Interrupt - #rel.dispatch() + # rel.signal(2, rel.abort) # Keyboard Interrupt + # rel.dispatch() def on_message(self, ws, message): + + # ready message + # we need to wait until radio is ready before we can push commands if message == "ready;": self.ws.send('audio_samplerate:8000;') self.ws.send('audio_stream_channels:1;') - self.ws.send('AUDIO_STREAM_SAMPLE_TYPE:int16;') - self.ws.send('AUDIO_STREAM_SAMPLES:1200;') + self.ws.send('audio_stream_sample_type:int16;') + self.ws.send('audio_stream_samples:1200;') self.ws.send('audio_start:0;') + # tx chrono frame + if len(message) in {64}: + receiver = message[:4] + sample_rate = int.from_bytes(message[4:8], "little") + format = int.from_bytes(message[8:12], "little") + codec = int.from_bytes(message[12:16], "little") + crc = int.from_bytes(message[16:20], "little") + audio_length = int.from_bytes(message[20:24], "little") + type = int.from_bytes(message[24:28], "little") + channel = int.from_bytes(message[28:32], "little") + reserved1 = int.from_bytes(message[32:36], "little") + reserved2 = int.from_bytes(message[36:40], "little") + reserved3 = int.from_bytes(message[40:44], "little") + reserved4 = int.from_bytes(message[44:48], "little") + reserved5 = int.from_bytes(message[48:52], "little") + reserved6 = int.from_bytes(message[52:56], "little") + reserved7 = int.from_bytes(message[56:60], "little") + reserved8 = int.from_bytes(message[60:64], "little") + if type == 3: + self.tx_chrono = True + + self.sample_rate = sample_rate + self.format = format + self.codec = codec + self.audio_length = audio_length + self.channel = channel + self.crc = crc + + # audio frame if len(message) in {576, 2464, 4160}: # audio received receiver = message[:4] @@ -77,6 +124,36 @@ class TCI: audio_data = message[64:] self.audio_received_queue.put(audio_data) + + if len(message)< 64: + # find frequency + if bytes(message, "utf-8").startswith(b"vfo:0,0,"): + splitted_message = message.split("vfo:0,0,") + self.frequency = splitted_message[1][:-1] + + # find mode + if bytes(message, "utf-8").startswith(b"modulation:0,"): + splitted_message = message.split("modulation:0,") + self.mode = splitted_message[1][:-1] + + # find ptt + #if bytes(message, "utf-8").startswith(b"trx:0,"): + # splitted_message = message.split("trx:0,") + # self.ptt = splitted_message[1][:-1] + + # find bandwidth + #if message.startswith("rx_filter_band:0,"): + # splitted_message = message.split("rx_filter_band:0,") + # bandwidths = splitted_message[1] + # splitted_bandwidths = bandwidths.split(",") + # lower_bandwidth = int(splitted_bandwidths[0]) + # upper_bandwidth = int(splitted_bandwidths[1][:-1]) + # self.bandwidth = upper_bandwidth - lower_bandwidth + + + + + def on_error(self, error): self.log.error( "[TCI] Error FreeDATA to TCI rig!", ip=self.hostname, port=self.port, e=error @@ -84,15 +161,160 @@ class TCI: def on_close(self, ws, close_status_code, close_msg): self.log.warning( - "[TCI] Closed FreeDATA to TCI connection!", ip=self.hostname, port=self.port, statu=close_status_code, msg=close_msg + "[TCI] Closed FreeDATA to TCI connection!", ip=self.hostname, port=self.port, statu=close_status_code, + msg=close_msg ) def on_open(self, ws): + self.ws = ws self.log.info( "[TCI] Connected FreeDATA to TCI rig!", ip=self.hostname, port=self.port ) - self.log.info( "[TCI] Init...", ip=self.hostname, port=self.port ) + + def push_audio(self, data_out): + #print(data_out) + + """ + # audio[:4] = receiver.to_bytes(4,byteorder='little', signed=False) + audio[4:8] = sample_rate.to_bytes(4, byteorder='little', signed=False) + audio[8:12] = format.to_bytes(4, byteorder='little', signed=False) + audio[12:16] = codec.to_bytes(4, byteorder='little', signed=False) + audio[16:20] = crc.to_bytes(4, byteorder='little', signed=False) + audio[20:24] = audio_length.to_bytes(4, byteorder='little', signed=False) + audio[24:28] = int(2).to_bytes(4, byteorder='little', signed=True) + audio[28:32] = channel.to_bytes(4, byteorder='little', signed=False) + audio[32:36] = reserved1.to_bytes(4, byteorder='little', signed=False) + audio[36:40] = reserved2.to_bytes(4, byteorder='little', signed=False) + audio[40:44] = reserved3.to_bytes(4, byteorder='little', signed=False) + audio[44:48] = reserved4.to_bytes(4, byteorder='little', signed=False) + audio[48:52] = reserved5.to_bytes(4, byteorder='little', signed=False) + audio[52:56] = reserved6.to_bytes(4, byteorder='little', signed=False) + audio[56:60] = reserved7.to_bytes(4, byteorder='little', signed=False) + audio[60:64] = reserved8.to_bytes(4, byteorder='little', signed=False) + """ + + while not self.tx_chrono: + time.sleep(0.01) + + #print(len(data_out)) + #print(self.sample_rate) + #print(self.audio_length) + #print(self.channel) + #print(self.crc) + #print(self.codec) + #print(self.tx_chrono) + + if self.tx_chrono: + #print("#############") + #print(len(data_out)) + #print(len(bytes(data_out))) + #print("-------------") + audio = bytearray(4096 + 64) + + audio[64:64 + len(bytes(data_out))] = bytes(data_out) + audio[4:8] = self.sample_rate.to_bytes(4, byteorder='little', signed=False) + # audio[8:12] = format.to_bytes(4,byteorder='little', signed=False) + audio[12:16] = self.codec.to_bytes(4, byteorder='little', signed=False) + audio[16:20] = self.crc.to_bytes(4, byteorder='little', signed=False) + audio[20:24] = self.audio_length.to_bytes(4, byteorder='little', signed=False) + audio[24:28] = int(2).to_bytes(4, byteorder='little', signed=False) + audio[28:32] = self.channel.to_bytes(4, byteorder='little', signed=False) + # audio[32:36] = reserved1.to_bytes(4,byteorder='little', signed=False) + # audio[36:40] = reserved2.to_bytes(4,byteorder='little', signed=False) + # audio[40:44] = reserved3.to_bytes(4,byteorder='little', signed=False) + # audio[44:48] = reserved4.to_bytes(4,byteorder='little', signed=False) + # audio[48:52] = reserved5.to_bytes(4,byteorder='little', signed=False) + # audio[52:56] = reserved6.to_bytes(4,byteorder='little', signed=False) + # audio[56:60] = reserved7.to_bytes(4,byteorder='little', signed=False) + + self.ws.send(audio, websocket.ABNF.OPCODE_BINARY) + + def set_ptt(self, state): + if state: + self.ws.send('trx:0,true,tci;') + else: + + self.ws.send('trx:0,false;') + self.tx_chrono = False + + def get_frequency(self): + """ """ + self.ws.send('VFO:0,0;') + return self.frequency + + def get_mode(self): + """ """ + self.ws.send('MODULATION:0;') + return self.mode + + def get_level(self): + """ """ + return self.level + + def get_alc(self): + """ """ + return self.alc + + def get_meter(self): + """ """ + return self.meter + + def get_bandwidth(self): + """ """ + return self.bandwidth + + def get_strength(self): + """ """ + return None + + def set_bandwidth(self): + """ """ + return None + + def set_mode(self, mode): + """ + + Args: + mode: + + Returns: + + """ + self.ws.send(f'MODULATION:0,{str(mode)};') + return None + + def set_frequency(self, frequency): + """ + + Args: + frequency: + + Returns: + + """ + self.ws.send(f'VFO:0,0,{str(frequency)};') + return None + + def get_status(self): + """ + + Args: + mode: + + Returns: + + """ + return "connected" + + def get_ptt(self): + """ """ + self.ws.send('trx:0;') + return self.ptt + + def close_rig(self): + """ """ + return