2020-12-23 16:48:54 +00:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
"""
|
|
|
|
Created on Wed Dec 23 07:04:24 2020
|
|
|
|
|
|
|
|
@author: DJ2LS
|
|
|
|
"""
|
2021-09-25 13:24:25 +00:00
|
|
|
import sys
|
2020-12-23 16:48:54 +00:00
|
|
|
import ctypes
|
|
|
|
from ctypes import *
|
|
|
|
import pathlib
|
2021-09-25 13:24:25 +00:00
|
|
|
#import asyncio
|
2021-11-18 18:40:22 +00:00
|
|
|
import logging, structlog, log_handler
|
2021-01-06 12:17:17 +00:00
|
|
|
import time
|
|
|
|
import threading
|
2021-09-10 16:56:33 +00:00
|
|
|
import atexit
|
2021-09-25 13:24:25 +00:00
|
|
|
import numpy as np
|
2021-01-06 12:17:17 +00:00
|
|
|
import helpers
|
2020-12-23 16:48:54 +00:00
|
|
|
import static
|
2021-02-24 13:22:28 +00:00
|
|
|
import data_handler
|
2021-12-29 19:54:54 +00:00
|
|
|
|
2021-11-18 18:40:22 +00:00
|
|
|
import re
|
2021-12-20 14:38:43 +00:00
|
|
|
import queue
|
2021-12-19 18:45:08 +00:00
|
|
|
import codec2
|
2021-12-25 16:05:38 +00:00
|
|
|
import rig
|
2021-12-19 18:45:08 +00:00
|
|
|
|
2021-11-19 13:52:58 +00:00
|
|
|
# option for testing miniaudio instead of audioop for sample rate conversion
|
|
|
|
#import miniaudio
|
|
|
|
|
|
|
|
|
2021-11-07 11:17:23 +00:00
|
|
|
####################################################
|
|
|
|
# https://stackoverflow.com/questions/7088672/pyaudio-working-but-spits-out-error-messages-each-time
|
|
|
|
# https://github.com/DJ2LS/FreeDATA/issues/22
|
|
|
|
# we need to have a look at this if we want to run this on Windows and MacOS !
|
|
|
|
# Currently it seems, this is a Linux-only problem
|
|
|
|
|
|
|
|
from ctypes import *
|
|
|
|
from contextlib import contextmanager
|
|
|
|
import pyaudio
|
|
|
|
|
|
|
|
ERROR_HANDLER_FUNC = CFUNCTYPE(None, c_char_p, c_int, c_char_p, c_int, c_char_p)
|
|
|
|
|
|
|
|
def py_error_handler(filename, line, function, err, fmt):
|
|
|
|
pass
|
|
|
|
|
|
|
|
c_error_handler = ERROR_HANDLER_FUNC(py_error_handler)
|
2021-09-25 13:24:25 +00:00
|
|
|
|
2021-11-07 11:17:23 +00:00
|
|
|
@contextmanager
|
|
|
|
def noalsaerr():
|
|
|
|
asound = cdll.LoadLibrary('libasound.so')
|
|
|
|
asound.snd_lib_error_set_handler(c_error_handler)
|
|
|
|
yield
|
|
|
|
asound.snd_lib_error_set_handler(None)
|
|
|
|
|
|
|
|
# with noalsaerr():
|
|
|
|
# p = pyaudio.PyAudio()
|
|
|
|
######################################################
|
2021-09-26 15:51:11 +00:00
|
|
|
|
2021-07-10 21:27:33 +00:00
|
|
|
|
2021-08-06 20:09:16 +00:00
|
|
|
MODEM_STATS_NR_MAX = 320
|
|
|
|
MODEM_STATS_NC_MAX = 51
|
|
|
|
|
2021-09-25 13:24:25 +00:00
|
|
|
|
2021-08-06 20:09:16 +00:00
|
|
|
class MODEMSTATS(ctypes.Structure):
|
2021-09-25 13:24:25 +00:00
|
|
|
_fields_ = [
|
|
|
|
("Nc", ctypes.c_int),
|
|
|
|
("snr_est", ctypes.c_float),
|
|
|
|
("rx_symbols", (ctypes.c_float * MODEM_STATS_NR_MAX)*MODEM_STATS_NC_MAX),
|
|
|
|
("nr", ctypes.c_int),
|
|
|
|
("sync", ctypes.c_int),
|
|
|
|
("foff", ctypes.c_float),
|
|
|
|
("rx_timing", ctypes.c_float),
|
|
|
|
("clock_offset", ctypes.c_float),
|
|
|
|
("sync_metric", ctypes.c_float),
|
|
|
|
("pre", ctypes.c_int),
|
|
|
|
("post", ctypes.c_int),
|
|
|
|
("uw_fails", ctypes.c_int),
|
|
|
|
]
|
2021-08-06 20:09:16 +00:00
|
|
|
|
|
|
|
|
2020-12-23 16:48:54 +00:00
|
|
|
class RF():
|
2021-03-12 13:14:36 +00:00
|
|
|
|
2021-09-25 13:24:25 +00:00
|
|
|
def __init__(self):
|
2021-10-03 14:31:34 +00:00
|
|
|
self.AUDIO_SAMPLE_RATE_RX = 48000
|
|
|
|
self.AUDIO_SAMPLE_RATE_TX = 48000
|
2021-12-19 19:31:53 +00:00
|
|
|
self.MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000
|
|
|
|
self.AUDIO_FRAMES_PER_BUFFER_RX = 2400*2 #8192
|
2021-12-22 11:48:49 +00:00
|
|
|
self.AUDIO_FRAMES_PER_BUFFER_TX = 2400 #8192 Lets to some tests with very small chunks for TX
|
2021-10-03 14:31:34 +00:00
|
|
|
self.AUDIO_CHUNKS = 48 #8 * (self.AUDIO_SAMPLE_RATE_RX/self.MODEM_SAMPLE_RATE) #48
|
|
|
|
self.AUDIO_CHANNELS = 1
|
|
|
|
|
2021-12-19 19:31:53 +00:00
|
|
|
# make sure our resampler will work
|
|
|
|
assert (self.AUDIO_SAMPLE_RATE_RX / self.MODEM_SAMPLE_RATE) == codec2.api.FDMDV_OS_48
|
|
|
|
|
2021-12-19 18:45:08 +00:00
|
|
|
# small hack for initializing codec2 via codec2.py module
|
|
|
|
# TODO: we need to change the entire modem module to integrate codec2 module
|
|
|
|
self.c_lib = codec2.api
|
2021-12-19 19:31:53 +00:00
|
|
|
self.resampler = codec2.resampler()
|
2021-12-19 18:45:08 +00:00
|
|
|
|
2021-12-20 14:38:43 +00:00
|
|
|
# init FIFO queue to store received frames in
|
|
|
|
self.dataqueue = queue.Queue()
|
2021-12-05 19:11:38 +00:00
|
|
|
|
2021-12-22 11:48:49 +00:00
|
|
|
# init FIFO queue to store modulation out in
|
|
|
|
self.modoutqueue = queue.Queue()
|
|
|
|
|
2021-12-26 14:43:47 +00:00
|
|
|
# define fft_data buffer
|
|
|
|
self.fft_data = bytes()
|
|
|
|
|
2021-12-20 14:38:43 +00:00
|
|
|
# open codec2 instance
|
|
|
|
self.datac0_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC0), c_void_p)
|
|
|
|
self.datac0_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(self.datac0_freedv)/8)
|
2021-12-20 15:21:09 +00:00
|
|
|
self.datac0_payload_per_frame = self.datac0_bytes_per_frame -2
|
|
|
|
self.datac0_n_nom_modem_samples = self.c_lib.freedv_get_n_nom_modem_samples(self.datac0_freedv)
|
|
|
|
self.datac0_n_tx_modem_samples = self.c_lib.freedv_get_n_tx_modem_samples(self.datac0_freedv)
|
|
|
|
self.datac0_n_tx_preamble_modem_samples = self.c_lib.freedv_get_n_tx_preamble_modem_samples(self.datac0_freedv)
|
|
|
|
self.datac0_n_tx_postamble_modem_samples = self.c_lib.freedv_get_n_tx_postamble_modem_samples(self.datac0_freedv)
|
|
|
|
self.datac0_bytes_out = create_string_buffer(self.datac0_bytes_per_frame)
|
2021-12-20 14:38:43 +00:00
|
|
|
codec2.api.freedv_set_frames_per_burst(self.datac0_freedv,1)
|
|
|
|
self.datac0_buffer = codec2.audio_buffer(2*self.AUDIO_FRAMES_PER_BUFFER_RX)
|
|
|
|
|
|
|
|
self.datac1_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC1), c_void_p)
|
|
|
|
self.datac1_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(self.datac1_freedv)/8)
|
2021-12-20 15:21:09 +00:00
|
|
|
self.datac1_bytes_out = create_string_buffer(self.datac1_bytes_per_frame)
|
2021-12-20 14:38:43 +00:00
|
|
|
codec2.api.freedv_set_frames_per_burst(self.datac1_freedv,1)
|
|
|
|
self.datac1_buffer = codec2.audio_buffer(2*self.AUDIO_FRAMES_PER_BUFFER_RX)
|
|
|
|
|
|
|
|
self.datac3_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC3), c_void_p)
|
|
|
|
self.datac3_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(self.datac3_freedv)/8)
|
2021-12-20 15:21:09 +00:00
|
|
|
self.datac3_bytes_out = create_string_buffer(self.datac3_bytes_per_frame)
|
2021-12-20 14:38:43 +00:00
|
|
|
codec2.api.freedv_set_frames_per_burst(self.datac3_freedv,1)
|
|
|
|
self.datac3_buffer = codec2.audio_buffer(2*self.AUDIO_FRAMES_PER_BUFFER_RX)
|
|
|
|
|
|
|
|
# initial nin values
|
|
|
|
self.datac0_nin = codec2.api.freedv_nin(self.datac0_freedv)
|
|
|
|
self.datac1_nin = codec2.api.freedv_nin(self.datac1_freedv)
|
|
|
|
self.datac3_nin = codec2.api.freedv_nin(self.datac3_freedv)
|
|
|
|
|
|
|
|
# --------------------------------------------CREATE PYAUDIO INSTANCE
|
2021-11-07 15:12:19 +00:00
|
|
|
try:
|
|
|
|
# we need to "try" this, because sometimes libasound.so isn't in the default place
|
|
|
|
# try to supress error messages
|
|
|
|
with noalsaerr(): # https://github.com/DJ2LS/FreeDATA/issues/22
|
|
|
|
self.p = pyaudio.PyAudio()
|
|
|
|
# else do it the default way
|
|
|
|
except:
|
2021-11-07 11:17:23 +00:00
|
|
|
self.p = pyaudio.PyAudio()
|
2021-09-10 16:56:33 +00:00
|
|
|
atexit.register(self.p.terminate)
|
2021-12-20 14:38:43 +00:00
|
|
|
|
|
|
|
# --------------------------------------------OPEN RX AUDIO CHANNEL
|
2021-12-19 18:45:08 +00:00
|
|
|
# optional auto selection of loopback device if using in testmode
|
|
|
|
if static.AUDIO_INPUT_DEVICE == -2:
|
|
|
|
loopback_list = []
|
|
|
|
for dev in range(0,self.p.get_device_count()):
|
|
|
|
if 'Loopback: PCM' in self.p.get_device_info_by_index(dev)["name"]:
|
|
|
|
loopback_list.append(dev)
|
|
|
|
if len(loopback_list) >= 2:
|
2021-12-22 11:48:49 +00:00
|
|
|
static.AUDIO_INPUT_DEVICE = loopback_list[0] #0 = RX
|
|
|
|
static.AUDIO_OUTPUT_DEVICE = loopback_list[1] #1 = TX
|
2021-12-19 18:45:08 +00:00
|
|
|
print(f"loopback_list rx: {loopback_list}", file=sys.stderr)
|
2021-12-20 14:38:43 +00:00
|
|
|
|
2021-12-22 11:48:49 +00:00
|
|
|
self.audio_stream = self.p.open(format=pyaudio.paInt16,
|
2021-10-03 14:31:34 +00:00
|
|
|
channels=self.AUDIO_CHANNELS,
|
|
|
|
rate=self.AUDIO_SAMPLE_RATE_RX,
|
2021-12-05 19:11:38 +00:00
|
|
|
frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER_RX,
|
2021-03-12 13:14:36 +00:00
|
|
|
input=True,
|
2021-12-22 11:48:49 +00:00
|
|
|
output=True,
|
2021-12-20 14:38:43 +00:00
|
|
|
input_device_index=static.AUDIO_INPUT_DEVICE,
|
2021-12-22 11:48:49 +00:00
|
|
|
output_device_index=static.AUDIO_OUTPUT_DEVICE,
|
2021-12-25 16:05:38 +00:00
|
|
|
stream_callback=self.audio_callback
|
2021-03-12 13:14:36 +00:00
|
|
|
)
|
|
|
|
|
2021-12-25 16:05:38 +00:00
|
|
|
|
2021-12-26 15:16:25 +00:00
|
|
|
# --------------------------------------------INIT AND OPEN HAMLIB
|
|
|
|
self.hamlib = rig.radio()
|
2021-12-27 12:23:23 +00:00
|
|
|
self.hamlib.open_rig(devicename=static.HAMLIB_DEVICE_NAME, deviceport=static.HAMLIB_DEVICE_PORT, hamlib_ptt_type=static.HAMLIB_PTT_TYPE, serialspeed=static.HAMLIB_SERIAL_SPEED, pttport=static.HAMLIB_PTT_PORT, data_bits=static.HAMLIB_DATA_BITS, stop_bits=static.HAMLIB_STOP_BITS, handshake=static.HAMLIB_HANDSHAKE)
|
2021-12-22 11:48:49 +00:00
|
|
|
|
2021-03-12 13:14:36 +00:00
|
|
|
# --------------------------------------------START DECODER THREAD
|
2021-12-26 14:43:47 +00:00
|
|
|
|
2021-12-26 14:25:35 +00:00
|
|
|
FFT_THREAD = threading.Thread(target=self.calculate_fft, name="FFT_THREAD")
|
|
|
|
FFT_THREAD.start()
|
|
|
|
|
2021-12-22 11:48:49 +00:00
|
|
|
AUDIO_THREAD = threading.Thread(target=self.audio, name="AUDIO_THREAD")
|
|
|
|
AUDIO_THREAD.start()
|
2021-03-12 13:14:36 +00:00
|
|
|
|
2021-12-25 16:05:38 +00:00
|
|
|
HAMLIB_THREAD = threading.Thread(target=self.update_rig_data, name="HAMLIB_THREAD")
|
|
|
|
HAMLIB_THREAD.start()
|
|
|
|
|
2021-12-20 14:38:43 +00:00
|
|
|
WORKER_THREAD = threading.Thread(target=self.worker, name="WORKER_THREAD")
|
|
|
|
WORKER_THREAD.start()
|
2021-12-25 16:05:38 +00:00
|
|
|
|
2021-09-25 13:24:25 +00:00
|
|
|
|
2021-12-20 14:38:43 +00:00
|
|
|
# --------------------------------------------------------------------------------------------------------
|
2021-12-25 16:05:38 +00:00
|
|
|
def audio_callback(self, data_in48k, frame_count, time_info, status):
|
2021-12-20 14:38:43 +00:00
|
|
|
|
|
|
|
x = np.frombuffer(data_in48k, dtype=np.int16)
|
|
|
|
x = self.resampler.resample48_to_8(x)
|
2021-09-25 13:24:25 +00:00
|
|
|
|
2021-12-20 14:38:43 +00:00
|
|
|
self.datac0_buffer.push(x)
|
|
|
|
self.datac1_buffer.push(x)
|
|
|
|
self.datac3_buffer.push(x)
|
2021-12-26 15:50:31 +00:00
|
|
|
self.fft_data += bytes(x)
|
2021-12-20 15:21:09 +00:00
|
|
|
|
2021-12-22 11:48:49 +00:00
|
|
|
if self.modoutqueue.empty():
|
|
|
|
data_out48k = bytes(self.AUDIO_FRAMES_PER_BUFFER_TX*2*2)
|
|
|
|
else:
|
|
|
|
data_out48k = self.modoutqueue.get()
|
|
|
|
|
|
|
|
return (data_out48k, pyaudio.paContinue)
|
2021-09-25 13:24:25 +00:00
|
|
|
|
2021-11-07 19:31:26 +00:00
|
|
|
# --------------------------------------------------------------------------------------------------------
|
2021-03-12 13:14:36 +00:00
|
|
|
|
2021-12-22 11:48:49 +00:00
|
|
|
|
2021-12-26 15:50:31 +00:00
|
|
|
def transmit(self, mode, repeats, repeat_delay, frames):
|
2021-12-22 11:48:49 +00:00
|
|
|
|
|
|
|
# open codec2 instance
|
2021-12-26 09:06:07 +00:00
|
|
|
#self.MODE = codec2.freedv_get_mode_value_by_name(mode)
|
|
|
|
self.MODE = mode
|
2021-12-22 11:48:49 +00:00
|
|
|
freedv = cast(codec2.api.freedv_open(self.MODE), c_void_p)
|
|
|
|
|
|
|
|
# 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 = create_string_buffer(n_tx_modem_samples * 2)
|
|
|
|
|
|
|
|
# init buffer for preample
|
|
|
|
n_tx_preamble_modem_samples = codec2.api.freedv_get_n_tx_preamble_modem_samples(freedv)
|
|
|
|
mod_out_preamble = create_string_buffer(n_tx_preamble_modem_samples * 2)
|
|
|
|
|
|
|
|
# init buffer for postamble
|
|
|
|
n_tx_postamble_modem_samples = codec2.api.freedv_get_n_tx_postamble_modem_samples(freedv)
|
|
|
|
mod_out_postamble = create_string_buffer(n_tx_postamble_modem_samples * 2)
|
|
|
|
|
2021-12-25 16:05:38 +00:00
|
|
|
# add empty data to handle ptt toggle time
|
2021-12-29 19:54:54 +00:00
|
|
|
data_delay_mseconds = 0 #miliseconds
|
|
|
|
data_delay = int(self.MODEM_SAMPLE_RATE*(data_delay_mseconds/1000))
|
2021-12-25 16:05:38 +00:00
|
|
|
mod_out_silence = create_string_buffer(data_delay*2)
|
|
|
|
txbuffer = bytes(mod_out_silence)
|
2021-12-22 11:48:49 +00:00
|
|
|
|
2021-12-25 16:05:38 +00:00
|
|
|
for i in range(1,repeats+1):
|
2021-12-22 11:48:49 +00:00
|
|
|
# write preamble to txbuffer
|
|
|
|
codec2.api.freedv_rawdatapreambletx(freedv, mod_out_preamble)
|
2022-01-04 10:55:55 +00:00
|
|
|
time.sleep(0.05)
|
2021-12-25 16:05:38 +00:00
|
|
|
txbuffer += bytes(mod_out_preamble)
|
2022-01-02 23:27:05 +00:00
|
|
|
|
2021-12-26 17:41:43 +00:00
|
|
|
|
2021-12-22 11:48:49 +00:00
|
|
|
# create modulaton for n frames in list
|
|
|
|
for n in range(0,len(frames)):
|
|
|
|
|
|
|
|
# create buffer for data
|
|
|
|
buffer = bytearray(payload_bytes_per_frame) # use this if CRC16 checksum is required ( DATA1-3)
|
|
|
|
buffer[:len(frames[n])] = frames[n] # set buffersize to length of data which will be send
|
|
|
|
|
|
|
|
# create crc for data frame - we are using the crc function shipped with codec2 to avoid
|
|
|
|
# crc algorithm incompatibilities
|
|
|
|
crc = ctypes.c_ushort(codec2.api.freedv_gen_crc16(bytes(buffer), payload_bytes_per_frame)) # generate CRC16
|
|
|
|
crc = crc.value.to_bytes(2, byteorder='big') # convert crc to 2 byte hex string
|
|
|
|
buffer += crc # append crc16 to buffer
|
|
|
|
|
|
|
|
data = (ctypes.c_ubyte * bytes_per_frame).from_buffer_copy(buffer)
|
|
|
|
codec2.api.freedv_rawdatatx(freedv,mod_out,data) # modulate DATA and save it into mod_out pointer
|
2022-01-04 10:55:55 +00:00
|
|
|
time.sleep(0.05)
|
2021-12-22 11:48:49 +00:00
|
|
|
txbuffer += bytes(mod_out)
|
|
|
|
|
|
|
|
|
|
|
|
# append postamble to txbuffer
|
|
|
|
codec2.api.freedv_rawdatapostambletx(freedv, mod_out_postamble)
|
|
|
|
txbuffer += bytes(mod_out_postamble)
|
2022-01-04 10:55:55 +00:00
|
|
|
time.sleep(0.05)
|
2021-12-25 16:05:38 +00:00
|
|
|
# add delay to end of frames
|
|
|
|
samples_delay = int(self.MODEM_SAMPLE_RATE*(repeat_delay/1000))
|
|
|
|
mod_out_silence = create_string_buffer(samples_delay*2)
|
|
|
|
txbuffer += bytes(mod_out_silence)
|
2022-01-04 10:55:55 +00:00
|
|
|
#time.sleep(0.05)
|
2021-12-25 16:05:38 +00:00
|
|
|
|
2021-12-22 11:48:49 +00:00
|
|
|
# resample up to 48k (resampler works on np.int16)
|
|
|
|
x = np.frombuffer(txbuffer, dtype=np.int16)
|
|
|
|
txbuffer_48k = self.resampler.resample8_to_48(x)
|
|
|
|
|
|
|
|
# split modualted audio to chunks
|
|
|
|
#https://newbedev.com/how-to-split-a-byte-string-into-separate-bytes-in-python
|
|
|
|
txbuffer_48k = bytes(txbuffer_48k)
|
|
|
|
chunk = [txbuffer_48k[i:i+self.AUDIO_FRAMES_PER_BUFFER_RX*2] for i in range(0, len(txbuffer_48k), self.AUDIO_FRAMES_PER_BUFFER_RX*2)]
|
|
|
|
# add modulated chunks to fifo buffer
|
|
|
|
for c in chunk:
|
|
|
|
# if data is shorter than the expcected audio frames per buffer we need to append 0
|
|
|
|
# to prevent the callback from stucking/crashing
|
|
|
|
if len(c) < self.AUDIO_FRAMES_PER_BUFFER_RX*2:
|
|
|
|
c += bytes(self.AUDIO_FRAMES_PER_BUFFER_RX*2 - len(c))
|
|
|
|
self.modoutqueue.put(c)
|
|
|
|
|
2021-12-26 09:06:07 +00:00
|
|
|
# maybe we need to toggle PTT before craeting modulation because of queue processing
|
2021-12-25 16:05:38 +00:00
|
|
|
static.PTT_STATE = self.hamlib.set_ptt(True)
|
|
|
|
while not self.modoutqueue.empty():
|
2021-12-22 11:48:49 +00:00
|
|
|
pass
|
2021-12-25 16:05:38 +00:00
|
|
|
static.PTT_STATE = self.hamlib.set_ptt(False)
|
2021-12-26 09:06:07 +00:00
|
|
|
|
2021-12-22 11:48:49 +00:00
|
|
|
self.c_lib.freedv_close(freedv)
|
|
|
|
return True
|
|
|
|
|
|
|
|
def audio(self):
|
2021-12-20 14:38:43 +00:00
|
|
|
try:
|
|
|
|
print(f"starting pyaudio callback", file=sys.stderr)
|
2021-12-22 11:48:49 +00:00
|
|
|
self.audio_stream.start_stream()
|
2021-12-20 14:38:43 +00:00
|
|
|
except Exception as e:
|
|
|
|
print(f"pyAudio error: {e}", file=sys.stderr)
|
|
|
|
|
|
|
|
|
2021-12-22 11:48:49 +00:00
|
|
|
while self.audio_stream.is_active():
|
|
|
|
while self.datac0_buffer.nbuffer >= self.datac0_nin:
|
|
|
|
# demodulate audio
|
|
|
|
nbytes = codec2.api.freedv_rawdatarx(self.datac0_freedv, self.datac0_bytes_out, self.datac0_buffer.buffer.ctypes)
|
|
|
|
self.datac0_buffer.pop(self.datac0_nin)
|
|
|
|
self.datac0_nin = codec2.api.freedv_nin(self.datac0_freedv)
|
|
|
|
if nbytes == self.datac0_bytes_per_frame:
|
|
|
|
self.dataqueue.put([self.datac0_bytes_out, self.datac0_freedv ,self.datac0_bytes_per_frame])
|
|
|
|
self.get_scatter(self.datac0_freedv)
|
|
|
|
self.calculate_snr(self.datac0_freedv)
|
|
|
|
|
|
|
|
while self.datac1_buffer.nbuffer >= self.datac1_nin:
|
|
|
|
# demodulate audio
|
|
|
|
nbytes = codec2.api.freedv_rawdatarx(self.datac1_freedv, self.datac1_bytes_out, self.datac1_buffer.buffer.ctypes)
|
|
|
|
self.datac1_buffer.pop(self.datac1_nin)
|
|
|
|
self.datac1_nin = codec2.api.freedv_nin(self.datac1_freedv)
|
|
|
|
if nbytes == self.datac1_bytes_per_frame:
|
|
|
|
self.dataqueue.put([self.datac1_bytes_out, self.datac1_freedv ,self.datac1_bytes_per_frame])
|
|
|
|
self.get_scatter(self.datac1_freedv)
|
|
|
|
self.calculate_snr(self.datac1_freedv)
|
|
|
|
|
|
|
|
while self.datac3_buffer.nbuffer >= self.datac3_nin:
|
|
|
|
# demodulate audio
|
|
|
|
nbytes = codec2.api.freedv_rawdatarx(self.datac3_freedv, self.datac3_bytes_out, self.datac3_buffer.buffer.ctypes)
|
|
|
|
self.datac3_buffer.pop(self.datac3_nin)
|
|
|
|
self.datac3_nin = codec2.api.freedv_nin(self.datac3_freedv)
|
|
|
|
if nbytes == self.datac3_bytes_per_frame:
|
|
|
|
self.dataqueue.put([self.datac3_bytes_out, self.datac3_freedv ,self.datac3_bytes_per_frame])
|
|
|
|
self.get_scatter(self.datac3_freedv)
|
|
|
|
self.calculate_snr(self.datac3_freedv)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2021-12-20 14:38:43 +00:00
|
|
|
|
|
|
|
# worker for FIFO queue for processing received frames
|
|
|
|
def worker(self):
|
|
|
|
while True:
|
2021-12-29 19:54:54 +00:00
|
|
|
time.sleep(0.01)
|
2021-12-20 14:38:43 +00:00
|
|
|
data = self.dataqueue.get()
|
2022-01-04 12:09:15 +00:00
|
|
|
# data[0] = bytes_out
|
|
|
|
# data[1] = freedv session
|
|
|
|
# data[2] = bytes_per_frame
|
2021-12-20 14:38:43 +00:00
|
|
|
self.process_data(data[0], data[1], data[2])
|
|
|
|
self.dataqueue.task_done()
|
|
|
|
|
|
|
|
|
2021-11-07 19:31:26 +00:00
|
|
|
# forward data only if broadcast or we are the receiver
|
|
|
|
# bytes_out[1:2] == callsign check for signalling frames,
|
|
|
|
# bytes_out[6:7] == callsign check for data frames,
|
2021-12-20 14:38:43 +00:00
|
|
|
# bytes_out[1:2] == b'\x01' --> broadcasts like CQ with n frames per_burst = 1
|
2021-11-07 19:31:26 +00:00
|
|
|
# we could also create an own function, which returns True.
|
2021-09-26 15:51:11 +00:00
|
|
|
def process_data(self, bytes_out, freedv, bytes_per_frame):
|
2021-09-25 13:24:25 +00:00
|
|
|
|
2021-12-29 19:54:54 +00:00
|
|
|
if bytes(bytes_out[1:2]) == static.MYCALLSIGN_CRC8 or bytes(bytes_out[3:4]) == static.MYCALLSIGN_CRC8 or bytes(bytes_out[1:2]) == b'\x01':
|
2021-09-25 13:24:25 +00:00
|
|
|
|
|
|
|
# CHECK IF FRAMETYPE IS BETWEEN 10 and 50 ------------------------
|
|
|
|
frametype = int.from_bytes(bytes(bytes_out[:1]), "big")
|
|
|
|
frame = frametype - 10
|
|
|
|
n_frames_per_burst = int.from_bytes(bytes(bytes_out[1:2]), "big")
|
2021-12-25 16:05:38 +00:00
|
|
|
#self.c_lib.freedv_set_frames_per_burst(freedv, n_frames_per_burst);
|
2021-09-25 13:24:25 +00:00
|
|
|
|
2021-10-24 12:44:55 +00:00
|
|
|
#frequency_offset = self.get_frequency_offset(freedv)
|
|
|
|
#print("Freq-Offset: " + str(frequency_offset))
|
|
|
|
|
2021-09-25 13:24:25 +00:00
|
|
|
if 50 >= frametype >= 10:
|
|
|
|
|
2022-01-04 12:09:15 +00:00
|
|
|
# get snr of received data
|
2022-01-04 12:10:59 +00:00
|
|
|
snr = self.calculate_snr(freedv)
|
2022-01-04 13:16:50 +00:00
|
|
|
print(f"SNR - {snr}")
|
2021-12-25 16:05:38 +00:00
|
|
|
# send payload data to arq checker without CRC16
|
2022-01-04 12:09:15 +00:00
|
|
|
data_handler.arq_data_received(bytes(bytes_out[:-2]), bytes_per_frame, snr)
|
2021-09-25 13:24:25 +00:00
|
|
|
|
2022-01-04 10:55:55 +00:00
|
|
|
# if we received the last frame of a burst or the last remaining rpt frame, do a modem unsync
|
|
|
|
if static.RX_BURST_BUFFER.count(None) <= 1 or (frame+1) == n_frames_per_burst:
|
|
|
|
logging.debug(f"LAST FRAME OF BURST --> UNSYNC {frame+1}/{n_frames_per_burst}")
|
2021-12-25 16:05:38 +00:00
|
|
|
self.c_lib.freedv_set_sync(freedv, 0)
|
2021-09-25 13:24:25 +00:00
|
|
|
|
|
|
|
|
|
|
|
# BURST ACK
|
|
|
|
elif frametype == 60:
|
|
|
|
logging.debug("ACK RECEIVED....")
|
2022-01-04 13:16:50 +00:00
|
|
|
|
|
|
|
data_handler.burst_ack_received(bytes_out[:-2])
|
2021-09-25 13:24:25 +00:00
|
|
|
|
|
|
|
# FRAME ACK
|
|
|
|
elif frametype == 61:
|
|
|
|
logging.debug("FRAME ACK RECEIVED....")
|
|
|
|
data_handler.frame_ack_received()
|
|
|
|
|
|
|
|
# FRAME RPT
|
|
|
|
elif frametype == 62:
|
|
|
|
logging.debug("REPEAT REQUEST RECEIVED....")
|
|
|
|
data_handler.burst_rpt_received(bytes_out[:-2])
|
|
|
|
|
2022-01-04 11:11:21 +00:00
|
|
|
# FRAME NACK
|
|
|
|
elif frametype == 63:
|
|
|
|
logging.debug("FRAME NOT ACK RECEIVED....")
|
|
|
|
data_handler.frame_nack_received(bytes_out[:-2])
|
|
|
|
|
2021-09-25 13:24:25 +00:00
|
|
|
# CQ FRAME
|
|
|
|
elif frametype == 200:
|
|
|
|
logging.debug("CQ RECEIVED....")
|
|
|
|
data_handler.received_cq(bytes_out[:-2])
|
|
|
|
|
|
|
|
# PING FRAME
|
|
|
|
elif frametype == 210:
|
|
|
|
logging.debug("PING RECEIVED....")
|
2021-10-13 17:39:46 +00:00
|
|
|
frequency_offset = self.get_frequency_offset(freedv)
|
2021-10-24 12:44:55 +00:00
|
|
|
#print("Freq-Offset: " + str(frequency_offset))
|
2021-10-13 17:39:46 +00:00
|
|
|
data_handler.received_ping(bytes_out[:-2], frequency_offset)
|
|
|
|
|
2021-09-25 13:24:25 +00:00
|
|
|
|
|
|
|
# PING ACK
|
|
|
|
elif frametype == 211:
|
|
|
|
logging.debug("PING ACK RECEIVED....")
|
2021-10-13 17:39:46 +00:00
|
|
|
# early detection of frequency offset
|
2021-10-24 12:44:55 +00:00
|
|
|
#frequency_offset = int.from_bytes(bytes(bytes_out[9:11]), "big", signed=True)
|
|
|
|
#print("Freq-Offset: " + str(frequency_offset))
|
|
|
|
#current_frequency = self.my_rig.get_freq()
|
|
|
|
#corrected_frequency = current_frequency + frequency_offset
|
2021-10-13 18:43:38 +00:00
|
|
|
# temporarely disabled this feature, beacuse it may cause some confusion.
|
|
|
|
# we also have problems if we are operating at band bordes like 7.000Mhz
|
|
|
|
# If we get a corrected frequency less 7.000 Mhz, Ham Radio devices will not transmit...
|
|
|
|
#self.my_rig.set_vfo(Hamlib.RIG_VFO_A)
|
|
|
|
#self.my_rig.set_freq(Hamlib.RIG_VFO_A, corrected_frequency)
|
2021-09-25 13:24:25 +00:00
|
|
|
data_handler.received_ping_ack(bytes_out[:-2])
|
|
|
|
|
|
|
|
# ARQ FILE TRANSFER RECEIVED!
|
|
|
|
elif frametype == 225:
|
2021-12-26 09:06:07 +00:00
|
|
|
logging.debug("ARQ arq_received_data_channel_opener")
|
2021-09-25 13:24:25 +00:00
|
|
|
data_handler.arq_received_data_channel_opener(bytes_out[:-2])
|
2022-01-02 22:42:56 +00:00
|
|
|
|
2021-09-25 13:24:25 +00:00
|
|
|
# ARQ CHANNEL IS OPENED
|
|
|
|
elif frametype == 226:
|
2021-12-26 09:06:07 +00:00
|
|
|
logging.debug("ARQ arq_received_channel_is_open")
|
2021-09-25 13:24:25 +00:00
|
|
|
data_handler.arq_received_channel_is_open(bytes_out[:-2])
|
|
|
|
|
|
|
|
# ARQ CONNECT ACK / KEEP ALIVE
|
2022-01-04 11:11:21 +00:00
|
|
|
# this is outdated and we may remove it
|
2021-09-25 13:24:25 +00:00
|
|
|
elif frametype == 230:
|
|
|
|
logging.debug("BEACON RECEIVED")
|
|
|
|
data_handler.received_beacon(bytes_out[:-2])
|
|
|
|
|
2022-01-04 11:11:21 +00:00
|
|
|
# TESTFRAMES
|
2021-12-20 14:38:43 +00:00
|
|
|
elif frametype == 255:
|
|
|
|
structlog.get_logger("structlog").debug("TESTFRAME RECEIVED", frame=bytes_out[:])
|
|
|
|
|
|
|
|
|
2021-09-25 13:24:25 +00:00
|
|
|
else:
|
2021-11-18 18:40:22 +00:00
|
|
|
structlog.get_logger("structlog").warning("[TNC] ARQ - other frame type", frametype=frametype)
|
2021-09-25 13:24:25 +00:00
|
|
|
|
2021-12-26 15:50:31 +00:00
|
|
|
|
2021-09-25 13:24:25 +00:00
|
|
|
# DO UNSYNC AFTER LAST BURST by checking the frame nums against the total frames per burst
|
2022-01-04 11:11:21 +00:00
|
|
|
# this should be changed to a timeout based unsync
|
2021-09-25 13:24:25 +00:00
|
|
|
if frame == n_frames_per_burst:
|
2021-12-05 19:11:38 +00:00
|
|
|
logging.info("LAST FRAME ---> UNSYNC")
|
2021-09-25 13:24:25 +00:00
|
|
|
self.c_lib.freedv_set_sync(freedv, 0) # FORCE UNSYNC
|
2021-12-26 15:50:31 +00:00
|
|
|
|
2021-03-12 13:14:36 +00:00
|
|
|
|
2021-09-23 15:49:45 +00:00
|
|
|
else:
|
2021-09-25 13:24:25 +00:00
|
|
|
# for debugging purposes to receive all data
|
2021-12-05 19:11:38 +00:00
|
|
|
structlog.get_logger("structlog").debug("[TNC] Unknown frame received", frame=bytes_out[:-2])
|
2021-09-25 13:24:25 +00:00
|
|
|
|
2021-10-13 17:39:46 +00:00
|
|
|
|
|
|
|
def get_frequency_offset(self, freedv):
|
|
|
|
modemStats = MODEMSTATS()
|
|
|
|
self.c_lib.freedv_get_modem_extended_stats.restype = None
|
|
|
|
self.c_lib.freedv_get_modem_extended_stats(freedv, ctypes.byref(modemStats))
|
|
|
|
offset = round(modemStats.foff) * (-1)
|
2021-10-24 12:44:55 +00:00
|
|
|
static.FREQ_OFFSET = offset
|
2021-10-13 18:43:38 +00:00
|
|
|
return offset
|
|
|
|
|
2021-10-13 17:39:46 +00:00
|
|
|
|
2021-08-06 20:09:16 +00:00
|
|
|
def get_scatter(self, freedv):
|
|
|
|
modemStats = MODEMSTATS()
|
|
|
|
self.c_lib.freedv_get_modem_extended_stats.restype = None
|
2021-09-26 15:51:11 +00:00
|
|
|
self.c_lib.freedv_get_modem_extended_stats(freedv, ctypes.byref(modemStats))
|
2021-09-25 13:24:25 +00:00
|
|
|
|
2021-08-06 20:09:16 +00:00
|
|
|
scatterdata = []
|
2021-09-30 19:49:22 +00:00
|
|
|
scatterdata_small = []
|
2021-08-06 20:09:16 +00:00
|
|
|
for i in range(MODEM_STATS_NC_MAX):
|
2021-09-25 13:24:25 +00:00
|
|
|
for j in range(MODEM_STATS_NR_MAX):
|
|
|
|
# check if odd or not to get every 2nd item for x
|
|
|
|
if (j % 2) == 0:
|
2021-10-01 17:05:43 +00:00
|
|
|
xsymbols = round(modemStats.rx_symbols[i][j]/1000)
|
|
|
|
ysymbols = round(modemStats.rx_symbols[i][j+1]/1000)
|
2021-08-07 09:14:49 +00:00
|
|
|
# check if value 0.0 or has real data
|
2021-08-06 20:09:16 +00:00
|
|
|
if xsymbols != 0.0 and ysymbols != 0.0:
|
2021-09-25 13:24:25 +00:00
|
|
|
scatterdata.append({"x": xsymbols, "y": ysymbols})
|
|
|
|
|
2021-08-07 09:14:49 +00:00
|
|
|
# only append scatter data if new data arrived
|
2021-09-30 19:49:22 +00:00
|
|
|
if 150 > len(scatterdata) > 0:
|
2021-09-25 13:24:25 +00:00
|
|
|
static.SCATTER = scatterdata
|
2021-09-30 19:49:22 +00:00
|
|
|
else:
|
2021-10-01 17:05:43 +00:00
|
|
|
# only take every tenth data point
|
2021-09-30 19:49:22 +00:00
|
|
|
scatterdata_small = scatterdata[::10]
|
|
|
|
static.SCATTER = scatterdata_small
|
2021-09-25 13:24:25 +00:00
|
|
|
|
2021-12-25 16:05:38 +00:00
|
|
|
|
2021-03-16 15:37:23 +00:00
|
|
|
def calculate_snr(self, freedv):
|
2021-09-25 13:24:25 +00:00
|
|
|
|
2021-03-16 15:37:23 +00:00
|
|
|
modem_stats_snr = c_float()
|
|
|
|
modem_stats_sync = c_int()
|
2021-09-25 13:24:25 +00:00
|
|
|
|
|
|
|
self.c_lib.freedv_get_modem_stats(freedv, byref(
|
|
|
|
modem_stats_sync), byref(modem_stats_snr))
|
2021-03-16 15:37:23 +00:00
|
|
|
modem_stats_snr = modem_stats_snr.value
|
2022-01-04 13:16:50 +00:00
|
|
|
|
2021-03-19 14:28:55 +00:00
|
|
|
try:
|
2021-09-25 13:24:25 +00:00
|
|
|
static.SNR = round(modem_stats_snr, 1)
|
2022-01-04 13:16:50 +00:00
|
|
|
return static.SNR
|
2021-03-19 14:28:55 +00:00
|
|
|
except:
|
|
|
|
static.SNR = 0
|
2022-01-04 13:16:50 +00:00
|
|
|
return static.SNR
|
2021-12-25 16:05:38 +00:00
|
|
|
|
|
|
|
def update_rig_data(self):
|
2021-09-24 15:16:38 +00:00
|
|
|
while True:
|
2021-12-26 15:16:25 +00:00
|
|
|
time.sleep(0.5)
|
2021-12-26 14:25:35 +00:00
|
|
|
#(static.HAMLIB_FREQUENCY, static.HAMLIB_MODE, static.HAMLIB_BANDWITH, static.PTT_STATE) = self.hamlib.get_rig_data()
|
|
|
|
static.HAMLIB_FREQUENCY = self.hamlib.get_frequency()
|
|
|
|
static.HAMLIB_MODE = self.hamlib.get_mode()
|
|
|
|
static.HAMLIB_BANDWITH = self.hamlib.get_bandwith()
|
2021-12-25 16:05:38 +00:00
|
|
|
|
2021-09-24 15:16:38 +00:00
|
|
|
def calculate_fft(self):
|
|
|
|
while True:
|
|
|
|
time.sleep(0.01)
|
|
|
|
# WE NEED TO OPTIMIZE THIS!
|
2021-10-03 14:31:34 +00:00
|
|
|
if len(self.fft_data) >= 1024:
|
|
|
|
data_in = self.fft_data
|
|
|
|
self.fft_data = bytes()
|
|
|
|
|
|
|
|
# https://gist.github.com/ZWMiller/53232427efc5088007cab6feee7c6e4c
|
|
|
|
audio_data = np.fromstring(data_in, np.int16)
|
|
|
|
# Fast Fourier Transform, 10*log10(abs) is to scale it to dB
|
|
|
|
# and make sure it's not imaginary
|
|
|
|
|
|
|
|
try:
|
|
|
|
fftarray = np.fft.rfft(audio_data)
|
2021-10-05 19:03:15 +00:00
|
|
|
|
2021-10-03 14:31:34 +00:00
|
|
|
# set value 0 to 1 to avoid division by zero
|
|
|
|
fftarray[fftarray == 0] = 1
|
|
|
|
dfft = 10.*np.log10(abs(fftarray))
|
2021-10-05 19:03:15 +00:00
|
|
|
# round data to decrease size
|
|
|
|
dfft = np.around(dfft, 1)
|
2021-10-03 14:31:34 +00:00
|
|
|
dfftlist = dfft.tolist()
|
2021-12-26 15:50:31 +00:00
|
|
|
|
|
|
|
static.FFT = dfftlist[0:320] #200 --> bandwith 3000
|
2021-10-03 14:31:34 +00:00
|
|
|
except:
|
2021-11-18 18:40:22 +00:00
|
|
|
|
|
|
|
structlog.get_logger("structlog").debug("[TNC] Setting fft=0")
|
2021-10-03 14:31:34 +00:00
|
|
|
# else 0
|
2021-12-26 15:50:31 +00:00
|
|
|
static.FFT = [0] * 320
|
2021-10-03 14:31:34 +00:00
|
|
|
else:
|
|
|
|
pass
|
2021-12-29 19:54:54 +00:00
|
|
|
|
|
|
|
def get_bytes_per_frame(self, mode):
|
|
|
|
freedv = cast(codec2.api.freedv_open(mode), c_void_p)
|
|
|
|
|
|
|
|
# get number of bytes per frame for mode
|
|
|
|
return int(codec2.api.freedv_get_bits_per_modem_frame(freedv)/8)
|