Merge branch 'main' into pep8_improvements

This commit is contained in:
DJ2LS 2022-05-23 09:46:42 +02:00 committed by GitHub
commit 507e3a5b06
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 3707 additions and 1811 deletions

View file

@ -2,7 +2,7 @@ name: Linux
on: on:
push: push:
tags: tags:
- 'v*' - "v*"
jobs: jobs:
build_linux_release: build_linux_release:
@ -27,8 +27,6 @@ jobs:
with: with:
python-version: 3.8 python-version: 3.8
- name: Install Linux dependencies - name: Install Linux dependencies
if: matrix.os == 'ubuntu-20.04' if: matrix.os == 'ubuntu-20.04'
run: | run: |
@ -44,7 +42,6 @@ jobs:
pip3 install structlog pip3 install structlog
pip3 install sounddevice pip3 install sounddevice
#- name: Build Hamlib Python Binding #- name: Build Hamlib Python Binding
# if: matrix.os == 'ubuntu-latest' # if: matrix.os == 'ubuntu-latest'
# working-directory: tnc # working-directory: tnc
@ -58,19 +55,15 @@ jobs:
# make # make
# make install # make install
- name: Build codec2 Linux - name: Build codec2 Linux
if: matrix.os == 'ubuntu-20.04' if: matrix.os == 'ubuntu-20.04'
working-directory: tnc working-directory: tnc
run: | run: |
git clone https://github.com/drowe67/codec2.git git clone https://github.com/drowe67/codec2.git
cd codec2 && mkdir build_linux && cd build_linux cd codec2 && git checkout master && mkdir build_linux && cd build_linux
cmake ../ cmake ../
make make
- name: Build Linux Daemon - name: Build Linux Daemon
if: matrix.os == 'ubuntu-20.04' if: matrix.os == 'ubuntu-20.04'
working-directory: tnc working-directory: tnc
@ -82,7 +75,6 @@ jobs:
run: | run: |
ls -R ls -R
- name: Compress Linux - name: Compress Linux
shell: bash shell: bash
run: | run: |
@ -96,8 +88,6 @@ jobs:
name: tnc-artifact name: tnc-artifact
path: ./tnc/dist/compressed/* path: ./tnc/dist/compressed/*
- name: Copy TNC to GUI Linux - name: Copy TNC to GUI Linux
if: matrix.os == 'ubuntu-20.04' if: matrix.os == 'ubuntu-20.04'
run: | run: |
@ -124,6 +114,3 @@ jobs:
# release the app after building # release the app after building
release: ${{ startsWith(github.ref, 'refs/tags/v') }} release: ${{ startsWith(github.ref, 'refs/tags/v') }}
args: "-p always" args: "-p always"

View file

@ -2,7 +2,7 @@ name: macOS
on: on:
push: push:
tags: tags:
- 'v*' - "v*"
jobs: jobs:
build_linux_release: build_linux_release:
@ -27,8 +27,6 @@ jobs:
with: with:
python-version: 3.9 python-version: 3.9
- name: Install macOS dependencies - name: Install macOS dependencies
if: matrix.os == 'macos-10.15' if: matrix.os == 'macos-10.15'
run: | run: |
@ -48,17 +46,15 @@ jobs:
brew install portaudio brew install portaudio
pip3 install pyaudio pip3 install pyaudio
- name: Build codec2 macOS - name: Build codec2 macOS
if: matrix.os == 'macos-10.15' if: matrix.os == 'macos-10.15'
working-directory: tnc working-directory: tnc
run: | run: |
git clone https://github.com/drowe67/codec2.git git clone https://github.com/drowe67/codec2.git
cd codec2 && mkdir build_mac && cd build_mac cd codec2 && git checkout master && mkdir build_mac && cd build_mac
cmake ../ cmake ../
make make
- name: Build macOS pyinstaller - name: Build macOS pyinstaller
if: matrix.os == 'macos-10.15' if: matrix.os == 'macos-10.15'
working-directory: tnc working-directory: tnc
@ -70,7 +66,6 @@ jobs:
run: | run: |
ls -R ls -R
- name: Compress - name: Compress
shell: bash shell: bash
run: | run: |
@ -84,9 +79,6 @@ jobs:
name: tnc-artifact name: tnc-artifact
path: ./tnc/dist/compressed/* path: ./tnc/dist/compressed/*
- name: Copy TNC to GUI - name: Copy TNC to GUI
if: matrix.os == 'macos-10.15' if: matrix.os == 'macos-10.15'
run: | run: |
@ -113,6 +105,3 @@ jobs:
# release the app after building # release the app after building
release: ${{ startsWith(github.ref, 'refs/tags/v') }} release: ${{ startsWith(github.ref, 'refs/tags/v') }}
args: "-p always" args: "-p always"

View file

@ -18,13 +18,13 @@ jobs:
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install octave octave-common octave-signal sox python3 python3-pip portaudio19-dev python3-pyaudio sudo apt-get install octave octave-common octave-signal sox python3 python3-pip portaudio19-dev python3-pyaudio
pip3 install psutil crcengine ujson pyserial numpy structlog miniaudio sounddevice pip3 install psutil crcengine ujson pyserial numpy structlog miniaudio sounddevice pytest
- name: Build codec2 - name: Build codec2
shell: bash shell: bash
run: | run: |
git clone https://github.com/drowe67/codec2.git git clone https://github.com/drowe67/codec2.git
cd codec2 && git checkout dr-tnc && git pull cd codec2 && git checkout master # This should be pinned to a release
mkdir -p build_linux && cd build_linux && cmake .. && make mkdir -p build_linux && cd build_linux && cmake .. && make
- name: run ctests - name: run ctests

1
.gitignore vendored
View file

@ -6,3 +6,4 @@ tnc/codec2
**/Testing **/Testing
package-lock.json package-lock.json
.DS_Store

View file

@ -24,16 +24,80 @@ set(TESTFRAMES 3)
add_test(NAME audio_buffer add_test(NAME audio_buffer
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src; COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
export PYTHONPATH=../tnc;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test; cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
python3 test_audiobuffer.py") python3 test_audiobuffer.py")
set_tests_properties(audio_buffer PROPERTIES PASS_REGULAR_EXPRESSION "errors: 0") set_tests_properties(audio_buffer PROPERTIES PASS_REGULAR_EXPRESSION "errors: 0")
add_test(NAME resampler add_test(NAME resampler
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src; COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
export PYTHONPATH=../tnc;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test; cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
python3 t48_8_short.py") python3 test_resample_48_8.py")
set_tests_properties(resampler PROPERTIES PASS_REGULAR_EXPRESSION "PASS") set_tests_properties(resampler PROPERTIES PASS_REGULAR_EXPRESSION "PASS")
add_test(NAME tnc_state_machine
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
export PYTHONPATH=../tnc;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
python3 test_tnc_states.py")
set_tests_properties(tnc_state_machine PROPERTIES PASS_REGULAR_EXPRESSION "errors: 0")
add_test(NAME tnc_irs_iss
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
export PYTHONPATH=../tnc;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
python3 test_tnc.py")
set_tests_properties(tnc_irs_iss PROPERTIES PASS_REGULAR_EXPRESSION "errors: 0")
add_test(NAME helper_routines
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
export PYTHONPATH=../tnc;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
python3 test_helpers.py")
set_tests_properties(helper_routines PROPERTIES PASS_REGULAR_EXPRESSION "errors: 0")
add_test(NAME py_highsnr_stdio_P_P_multi
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
export PYTHONPATH=../tnc;
export BURSTS=${BURSTS};
export FRAMESPERBURST=${FRAMESPERBURST};
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
python3 test_highsnr_stdio_P_P_multi.py")
set_tests_properties(py_highsnr_stdio_P_P_multi PROPERTIES PASS_REGULAR_EXPRESSION "DATAC0: ${BURSTS}/${FRAMESPERBURST} DATAC1: ${BURSTS}/${FRAMESPERBURST} DATAC3: ${BURSTS}/${FRAMESPERBURST}")
add_test(NAME py_highsnr_stdio_P_P_datacx
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
export PYTHONPATH=../tnc;
export BURSTS=${BURSTS};
export FRAMESPERBURST=${FRAMESPERBURST};
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
python3 test_highsnr_stdio_P_P_datacx.py")
set_tests_properties(py_highsnr_stdio_P_P_datacx PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: ${BURSTS} RECEIVED FRAMES: ${FRAMESPERBURST}")
add_test(NAME py_highsnr_stdio_P_C_datacx
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
export PYTHONPATH=../tnc;
export BURSTS=${BURSTS};
export FRAMESPERBURST=${FRAMESPERBURST};
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
python3 test_highsnr_stdio_P_C_datacx.py")
set_tests_properties(py_highsnr_stdio_P_C_datacx PROPERTIES PASS_REGULAR_EXPRESSION "HELLO WORLD")
add_test(NAME py_highsnr_stdio_C_P_datacx
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
export PYTHONPATH=../tnc;
export BURSTS=${BURSTS};
export FRAMESPERBURST=${FRAMESPERBURST};
export TESTFRAMES=${TESTFRAMES};
PATH=$PATH:${CODEC2_BUILD_DIR}/src;
cd ${CMAKE_CURRENT_SOURCE_DIR}/test;
python3 test_highsnr_stdio_C_P_datacx.py")
set_tests_properties(py_highsnr_stdio_C_P_datacx PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: ${BURSTS} RECEIVED FRAMES: ${FRAMESPERBURST}")
add_test(NAME highsnr_stdio_P_C_single add_test(NAME highsnr_stdio_P_C_single
COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src; COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src;
PATH=$PATH:${CODEC2_BUILD_DIR}/src; PATH=$PATH:${CODEC2_BUILD_DIR}/src;
@ -68,7 +132,6 @@ add_test(NAME highsnr_stdio_P_P_multi
python3 test_multimode_rx.py --framesperburst ${FRAMESPERBURST} --bursts ${BURSTS} --timeout 20") python3 test_multimode_rx.py --framesperburst ${FRAMESPERBURST} --bursts ${BURSTS} --timeout 20")
set_tests_properties(highsnr_stdio_P_P_multi PROPERTIES PASS_REGULAR_EXPRESSION "DATAC0: ${BURSTS}/${FRAMESPERBURST} DATAC1: ${BURSTS}/${FRAMESPERBURST} DATAC3: ${BURSTS}/${FRAMESPERBURST}") set_tests_properties(highsnr_stdio_P_P_multi PROPERTIES PASS_REGULAR_EXPRESSION "DATAC0: ${BURSTS}/${FRAMESPERBURST} DATAC1: ${BURSTS}/${FRAMESPERBURST} DATAC3: ${BURSTS}/${FRAMESPERBURST}")
# These tests can't run on GitHub actions as we don't have a virtual sound card # These tests can't run on GitHub actions as we don't have a virtual sound card
if(NOT DEFINED ENV{GITHUB_RUN_ID}) if(NOT DEFINED ENV{GITHUB_RUN_ID})

View file

@ -30,4 +30,3 @@ Download the latest developer release from the releases section, unpack it and j
## Installation ## Installation
Please check the [wiki](https://wiki.freedata.app) for installation instructions Please check the [wiki](https://wiki.freedata.app) for installation instructions

View file

@ -1,6 +1,6 @@
{ {
"name": "FreeDATA", "name": "FreeDATA",
"version": "0.4.0-alpha.8", "version": "0.4.0-alpha.9",
"description": "FreeDATA ", "description": "FreeDATA ",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {

View file

@ -511,8 +511,9 @@ update_chat = function(obj) {
`; `;
var controlarea_receive = ''; var controlarea_receive = '';
} }
} catch { } catch (err) {
console.log("error with database parsing...") console.log("error with database parsing...")
console.log(err)
} }
// CALLSIGN LIST // CALLSIGN LIST
if (!(document.getElementById('chat-' + dxcallsign + '-list'))) { if (!(document.getElementById('chat-' + dxcallsign + '-list'))) {

View file

@ -1,4 +1,52 @@
# Unit Test Menu
The following `CTest` tests cover some TNC functionality and the interface to codec2:
1. Name: `audio_buffer`
Tests the thread safety of the audio buffer routines.
1. Name: `resampler`
Tests FreeDATA audio resampling from 48KHz to 8KHz.
1. Name: `tnc_state_machine`
Tests TNC transitions between states.
1. Name: `helper_routines`
Tests various helper routines.
1. Name: `py_highsnr_stdio_P_P_multi`
Tests a high signal-to-noise ratio (good quality) audio path using multiple codecs. (Pure python.)
1. Name: `py_highsnr_stdio_P_P_datacx`
Tests a high signal-to-noise ratio audio path using multiple individual codecs.
1. Name: `py_highsnr_stdio_P_C_datacx`
Tests a high signal-to-noise ratio audio path using multiple individual codecs.
1. Name: `py_highsnr_stdio_C_P_datacx`
Tests a high signal-to-noise ratio audio path using multiple individual codecs.
1. Name: `highsnr_stdio_P_C_single`
Tests compatibility with FreeDATA's transmit and freedv's raw data receive.
1. Name: `highsnr_stdio_C_P_single`
Tests compatibility with freedv's raw data transmit and FreeDATA's receive.
1. Name: `highsnr_stdio_P_P_single`
Tests a high signal-to-noise ratio audio path using multiple codecs. (Requires POSIX system.)
1. Name: `highsnr_stdio_P_P_multi`
Tests a high signal-to-noise ratio audio path using multiple codecs. (Requires POSIX system.)
The following tests can not currently be run with GitHub's pipeline as they require the ALSA dummy device
kernel module to be installed. They also do not perform reliably. These tests are slowly being
replaced with equivalent pipeline-compatible tests.
1. Name: `highsnr_virtual1_P_P_single_alsa`
Tests a high signal-to-noise ratio audio path using a single codec directly over an ALSA dummy device.
1. Name: `highsnr_virtual2_P_P_single`
Tests a high signal-to-noise ratio audio path using a single codec over an ALSA dummy device.
**Not functional** due to an incompatibility between the two scripts in the way they determine audio devices.
1. Name: `highsnr_virtual3_P_P_multi`
Tests a high signal-to-noise ratio audio path using multiple codecs over an ALSA dummy device.
1. Name: `highsnr_virtual4_P_P_single_callback`
**Not functional** due to an incompatibility between the two scripts in the way they determine audio devices.
1. Name: `highsnr_virtual4_P_P_single_callback_outside`
**Not functional** due to an incompatibility between the two scripts in the way they determine audio devices.
1. Name: `highsnr_virtual5_P_P_multi_callback`
1. Name: `highsnr_virtual5_P_P_multi_callback_outside`
1. Name: `highsnr_ARQ_short`
**Not functional**, it is an obsolete or not yet completed test.
# Instructions # Instructions
1. Install: 1. Install:

View file

@ -1,33 +1,32 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import ctypes
from ctypes import *
import pathlib
import pyaudio
import time
import threading
import argparse import argparse
import sys import ctypes
import pathlib
import threading
import time
#--------------------------------------------GET PARAMETER INPUTS import pyaudio
parser = argparse.ArgumentParser(description='Simons TEST TNC')
parser.add_argument('--bursts', dest="N_BURSTS", default=0, type=int)
parser.add_argument('--frames', dest="N_FRAMES_PER_BURST", default=0, type=int)
parser.add_argument('--delay', dest="DELAY_BETWEEN_BURSTS", default=0, type=int)
parser.add_argument('--txmode', dest="FREEDV_TX_MODE", default=0, type=int)
parser.add_argument('--rxmode', dest="FREEDV_RX_MODE", default=0, type=int)
parser.add_argument('--audiooutput', dest="AUDIO_OUTPUT", default=0, type=int)
parser.add_argument('--audioinput', dest="AUDIO_INPUT", default=0, type=int)
parser.add_argument('--debug', dest="DEBUGGING_MODE", action="store_true")
args = parser.parse_args() # --------------------------------------------GET PARAMETER INPUTS
parser = argparse.ArgumentParser(description="Simons TEST TNC")
parser.add_argument("--bursts", dest="N_BURSTS", default=0, type=int)
parser.add_argument("--frames", dest="N_FRAMES_PER_BURST", default=0, type=int)
parser.add_argument("--delay", dest="DELAY_BETWEEN_BURSTS", default=0, type=int)
parser.add_argument("--txmode", dest="FREEDV_TX_MODE", default=0, type=int)
parser.add_argument("--rxmode", dest="FREEDV_RX_MODE", default=0, type=int)
parser.add_argument("--audiooutput", dest="AUDIO_OUTPUT", default=0, type=int)
parser.add_argument("--audioinput", dest="AUDIO_INPUT", default=0, type=int)
parser.add_argument("--debug", dest="DEBUGGING_MODE", action="store_true")
args, _ = parser.parse_known_args()
N_BURSTS = args.N_BURSTS N_BURSTS = args.N_BURSTS
N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST
DELAY_BETWEEN_BURSTS = args.DELAY_BETWEEN_BURSTS/1000 DELAY_BETWEEN_BURSTS = args.DELAY_BETWEEN_BURSTS / 1000
AUDIO_OUTPUT_DEVICE = args.AUDIO_OUTPUT AUDIO_OUTPUT_DEVICE = args.AUDIO_OUTPUT
AUDIO_INPUT_DEVICE = args.AUDIO_INPUT AUDIO_INPUT_DEVICE = args.AUDIO_INPUT
@ -40,72 +39,77 @@ FREEDV_TX_MODE = args.FREEDV_TX_MODE
FREEDV_RX_MODE = args.FREEDV_RX_MODE FREEDV_RX_MODE = args.FREEDV_RX_MODE
DEBUGGING_MODE = args.DEBUGGING_MODE DEBUGGING_MODE = args.DEBUGGING_MODE
#-------------------------------------------- LOAD FREEDV # -------------------------------------------- LOAD FREEDV
libname = pathlib.Path().absolute() / "codec2/build_linux/src/libcodec2.so" libname = pathlib.Path().absolute() / "codec2/build_linux/src/libcodec2.so"
c_lib = ctypes.CDLL(libname) c_lib = ctypes.CDLL(str(libname))
#--------------------------------------------CREATE PYAUDIO INSTANCE # --------------------------------------------CREATE PYAUDIO INSTANCE
p = pyaudio.PyAudio() p = pyaudio.PyAudio()
#--------------------------------------------GET SUPPORTED SAMPLE RATES FROM SOUND DEVICE # --------------------------------------------GET SUPPORTED SAMPLE RATES FROM SOUND DEVICE
#AUDIO_SAMPLE_RATE_TX = int(p.get_device_info_by_index(AUDIO_OUTPUT_DEVICE)['defaultSampleRate']) # AUDIO_SAMPLE_RATE_TX = int(p.get_device_info_by_index(AUDIO_OUTPUT_DEVICE)['defaultSampleRate'])
#AUDIO_SAMPLE_RATE_RX = int(p.get_device_info_by_index(AUDIO_INPUT_DEVICE)['defaultSampleRate']) # AUDIO_SAMPLE_RATE_RX = int(p.get_device_info_by_index(AUDIO_INPUT_DEVICE)['defaultSampleRate'])
AUDIO_SAMPLE_RATE_TX = 8000 AUDIO_SAMPLE_RATE_TX = 8000
AUDIO_SAMPLE_RATE_RX = 8000 AUDIO_SAMPLE_RATE_RX = 8000
#--------------------------------------------OPEN AUDIO CHANNEL TX # --------------------------------------------OPEN AUDIO CHANNEL TX
stream_tx = p.open(format=pyaudio.paInt16, stream_tx = p.open(
format=pyaudio.paInt16,
channels=1, channels=1,
rate=AUDIO_SAMPLE_RATE_TX, rate=AUDIO_SAMPLE_RATE_TX,
frames_per_buffer=AUDIO_FRAMES_PER_BUFFER, #n_nom_modem_samples frames_per_buffer=AUDIO_FRAMES_PER_BUFFER, # n_nom_modem_samples
output=True, output=True,
output_device_index=AUDIO_OUTPUT_DEVICE, output_device_index=AUDIO_OUTPUT_DEVICE,
) )
stream_rx = p.open(format=pyaudio.paInt16, stream_rx = p.open(
format=pyaudio.paInt16,
channels=1, channels=1,
rate=AUDIO_SAMPLE_RATE_RX, rate=AUDIO_SAMPLE_RATE_RX,
frames_per_buffer=AUDIO_FRAMES_PER_BUFFER, frames_per_buffer=AUDIO_FRAMES_PER_BUFFER,
input=True, input=True,
input_device_index=AUDIO_INPUT_DEVICE, input_device_index=AUDIO_INPUT_DEVICE,
) )
def receive(): def receive():
c_lib.freedv_open.restype = ctypes.POINTER(ctypes.c_ubyte) c_lib.freedv_open.restype = ctypes.POINTER(ctypes.c_ubyte)
freedv = c_lib.freedv_open(FREEDV_RX_MODE) freedv = c_lib.freedv_open(FREEDV_RX_MODE)
bytes_per_frame = int(c_lib.freedv_get_bits_per_modem_frame(freedv)/8) bytes_per_frame = int(c_lib.freedv_get_bits_per_modem_frame(freedv) / 8)
payload_per_frame = bytes_per_frame -2 payload_per_frame = bytes_per_frame - 2
n_nom_modem_samples = c_lib.freedv_get_n_nom_modem_samples(freedv) n_nom_modem_samples = c_lib.freedv_get_n_nom_modem_samples(freedv)
n_tx_modem_samples = c_lib.freedv_get_n_tx_modem_samples(freedv) #get n_tx_modem_samples which defines the size of the modulation object # --> *2 n_tx_modem_samples = c_lib.freedv_get_n_tx_modem_samples(
freedv
) # get n_tx_modem_samples which defines the size of the modulation object # --> *2
bytes_out = (ctypes.c_ubyte * bytes_per_frame) #bytes_per_frame bytes_out = ctypes.c_ubyte * bytes_per_frame # bytes_per_frame
bytes_out = bytes_out() #get pointer from bytes_out bytes_out = bytes_out() # get pointer from bytes_out
total_n_bytes = 0 total_n_bytes = 0
rx_total_frames = 0 rx_total_frames = 0
rx_frames = 0 rx_frames = 0
rx_bursts = 0 rx_bursts = 0
receive = True receive = True
while receive == True: while receive:
time.sleep(0.01) time.sleep(0.01)
nin = c_lib.freedv_nin(freedv) nin = c_lib.freedv_nin(freedv)
nin_converted = int(nin*(AUDIO_SAMPLE_RATE_RX/MODEM_SAMPLE_RATE)) nin_converted = int(nin * (AUDIO_SAMPLE_RATE_RX / MODEM_SAMPLE_RATE))
if DEBUGGING_MODE == True: if DEBUGGING_MODE:
print("-----------------------------") print("-----------------------------")
print("NIN: " + str(nin) + " [ " + str(nin_converted) + " ]") print("NIN: " + str(nin) + " [ " + str(nin_converted) + " ]")
data_in = stream_rx.read(nin_converted, exception_on_overflow = False) data_in = stream_rx.read(nin_converted, exception_on_overflow=False)
data_in = data_in.rstrip(b'\x00') data_in = data_in.rstrip(b"\x00")
c_lib.freedv_rawdatarx.argtype = [ctypes.POINTER(ctypes.c_ubyte), bytes_out, data_in] # check if really neccessary c_lib.freedv_rawdatarx.argtype = [
ctypes.POINTER(ctypes.c_ubyte),
bytes_out,
data_in,
] # check if really neccessary
nbytes = c_lib.freedv_rawdatarx(freedv, bytes_out, data_in) # demodulate audio nbytes = c_lib.freedv_rawdatarx(freedv, bytes_out, data_in) # demodulate audio
total_n_bytes = total_n_bytes + nbytes total_n_bytes = total_n_bytes + nbytes
if DEBUGGING_MODE == True: if DEBUGGING_MODE:
print("SYNC: " + str(c_lib.freedv_get_rx_status(freedv))) print("SYNC: " + str(c_lib.freedv_get_rx_status(freedv)))
if nbytes == bytes_per_frame: if nbytes == bytes_per_frame:
@ -115,79 +119,106 @@ def receive():
if rx_frames == N_FRAMES_PER_BURST: if rx_frames == N_FRAMES_PER_BURST:
rx_frames = 0 rx_frames = 0
rx_bursts = rx_bursts + 1 rx_bursts = rx_bursts + 1
c_lib.freedv_set_sync(freedv,0) c_lib.freedv_set_sync(freedv, 0)
burst = bytes_out[0] burst = bytes_out[0]
n_total_burst = bytes_out[1] n_total_burst = bytes_out[1]
frame = bytes_out[2] frame = bytes_out[2]
n_total_frame = bytes_out[3] n_total_frame = bytes_out[3]
print(
print("RX | PONG | BURST [" + str(burst) + "/" + str(n_total_burst) + "] FRAME [" + str(frame) + "/" + str(n_total_frame) + "]") "RX | PONG | BURST ["
+ str(burst)
+ "/"
+ str(n_total_burst)
+ "] FRAME ["
+ str(frame)
+ "/"
+ str(n_total_frame)
+ "]"
)
print("-----------------------------------------------------------------") print("-----------------------------------------------------------------")
c_lib.freedv_set_sync(freedv,0) c_lib.freedv_set_sync(freedv, 0)
if rx_bursts == N_BURSTS: if rx_bursts == N_BURSTS:
receive = False receive = False
RECEIVE = threading.Thread(target=receive, name="RECEIVE THREAD") RECEIVE = threading.Thread(target=receive, name="RECEIVE THREAD")
RECEIVE.start() RECEIVE.start()
c_lib.freedv_open.restype = ctypes.POINTER(ctypes.c_ubyte) c_lib.freedv_open.restype = ctypes.POINTER(ctypes.c_ubyte)
freedv = c_lib.freedv_open(FREEDV_TX_MODE) freedv = c_lib.freedv_open(FREEDV_TX_MODE)
bytes_per_frame = int(c_lib.freedv_get_bits_per_modem_frame(freedv)/8) bytes_per_frame = int(c_lib.freedv_get_bits_per_modem_frame(freedv) / 8)
payload_per_frame = bytes_per_frame -2 payload_per_frame = bytes_per_frame - 2
n_nom_modem_samples = c_lib.freedv_get_n_nom_modem_samples(freedv) n_nom_modem_samples = c_lib.freedv_get_n_nom_modem_samples(freedv)
n_tx_modem_samples = c_lib.freedv_get_n_tx_modem_samples(freedv) #get n_tx_modem_samples which defines the size of the modulation object # --> *2 n_tx_modem_samples = c_lib.freedv_get_n_tx_modem_samples(
freedv
) # get n_tx_modem_samples which defines the size of the modulation object # --> *2
mod_out = ctypes.c_short * n_tx_modem_samples mod_out = ctypes.c_short * n_tx_modem_samples
mod_out = mod_out() mod_out = mod_out()
mod_out_preamble = ctypes.c_short * (1760*2) #1760 for mode 10,11,12 #4000 for mode 9 mod_out_preamble = ctypes.c_short * (
1760 * 2
) # 1760 for mode 10,11,12 #4000 for mode 9
mod_out_preamble = mod_out_preamble() mod_out_preamble = mod_out_preamble()
print("BURSTS: " + str(N_BURSTS) + " FRAMES: " + str(N_FRAMES_PER_BURST))
print("BURSTS: " + str(N_BURSTS) + " FRAMES: " + str(N_FRAMES_PER_BURST) )
print("-----------------------------------------------------------------") print("-----------------------------------------------------------------")
for i in range(0,N_BURSTS): for i in range(N_BURSTS):
c_lib.freedv_rawdatapreambletx(freedv, mod_out_preamble); c_lib.freedv_rawdatapreambletx(freedv, mod_out_preamble)
txbuffer = bytearray() txbuffer = bytearray()
txbuffer += bytes(mod_out_preamble) txbuffer += bytes(mod_out_preamble)
for n in range(0,N_FRAMES_PER_BURST): for n in range(N_FRAMES_PER_BURST):
data_out = bytearray() data_out = bytearray()
data_out += bytes([i+1]) data_out += bytes([i + 1])
data_out += bytes([N_BURSTS]) data_out += bytes([N_BURSTS])
data_out += bytes([n+1]) data_out += bytes([n + 1])
data_out += bytes([N_FRAMES_PER_BURST]) data_out += bytes([N_FRAMES_PER_BURST])
buffer = bytearray(payload_per_frame) # use this if CRC16 checksum is required ( DATA1-3) buffer = bytearray(
buffer[:len(data_out)] = data_out # set buffersize to length of data which will be send payload_per_frame
) # use this if CRC16 checksum is required ( DATA1-3)
buffer[
: len(data_out)
] = data_out # set buffersize to length of data which will be send
crc = ctypes.c_ushort(c_lib.freedv_gen_crc16(bytes(buffer), payload_per_frame)) # generate CRC16 crc = ctypes.c_ushort(
crc = crc.value.to_bytes(2, byteorder='big') # convert crc to 2 byte hex string c_lib.freedv_gen_crc16(bytes(buffer), payload_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 buffer += crc # append crc16 to buffer
data = (ctypes.c_ubyte * bytes_per_frame).from_buffer_copy(buffer) data = (ctypes.c_ubyte * bytes_per_frame).from_buffer_copy(buffer)
c_lib.freedv_rawdatatx(freedv,mod_out,data) # modulate DATA and safe it into mod_out pointer c_lib.freedv_rawdatatx(
freedv, mod_out, data
) # modulate DATA and safe it into mod_out pointer
txbuffer += bytes(mod_out) txbuffer += bytes(mod_out)
print("TX | PING | BURST [" + str(i+1) + "/" + str(N_BURSTS) + "] FRAME [" + str(n+1) + "/" + str(N_FRAMES_PER_BURST) + "]") print(
"TX | PING | BURST ["
+ str(i + 1)
+ "/"
+ str(N_BURSTS)
+ "] FRAME ["
+ str(n + 1)
+ "/"
+ str(N_FRAMES_PER_BURST)
+ "]"
)
stream_tx.write(bytes(txbuffer)) stream_tx.write(bytes(txbuffer))
ACK_TIMEOUT = time.time() + 3 ACK_TIMEOUT = time.time() + 3
txbuffer = bytearray() txbuffer = bytearray()
#time.sleep(DELAY_BETWEEN_BURSTS) # time.sleep(DELAY_BETWEEN_BURSTS)
# WAIT UNTIL WE RECEIVD AN ACK/DATAC0 FRAME # WAIT UNTIL WE RECEIVD AN ACK/DATAC0 FRAME
while ACK_TIMEOUT >= time.time(): while ACK_TIMEOUT >= time.time():

View file

@ -27,7 +27,7 @@ parser.add_argument('--audioinput', dest="AUDIO_INPUT", default=0, type=int)
parser.add_argument('--audiooutput', dest="AUDIO_OUTPUT", default=0, type=int) parser.add_argument('--audiooutput', dest="AUDIO_OUTPUT", default=0, type=int)
parser.add_argument('--debug', dest="DEBUGGING_MODE", action="store_true") parser.add_argument('--debug', dest="DEBUGGING_MODE", action="store_true")
args = parser.parse_args() args, _ = parser.parse_known_args()
N_BURSTS = args.N_BURSTS N_BURSTS = args.N_BURSTS
N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST

View file

@ -1,100 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Unit test for FreeDV API resampler functions, from
# codec2/unittest/t48_8_short.c - generate a sine wave at 8 KHz,
# upsample to 48 kHz, add an interferer, then downsample back to 8 kHz
#
# You can listen to the output files with:
#
# aplay -f S16_LE in8.raw
# aplay -r 48000 -f S16_LE out48.raw
# aplay -f S16_LE out8.raw
#
# They should sound like clean sine waves
import ctypes
from ctypes import *
import pathlib
import argparse
import sys
sys.path.insert(0,'..')
from tnc import codec2
import numpy as np
# dig some constants out
FDMDV_OS_48 = codec2.api.FDMDV_OS_48
FDMDV_OS_TAPS_48K = codec2.api.FDMDV_OS_TAPS_48K
FDMDV_OS_TAPS_48_8K = codec2.api.FDMDV_OS_TAPS_48_8K
N8 = int(180) # processing buffer size at 8 kHz
N48 = int(N8*FDMDV_OS_48) # processing buffer size at 48 kHz
MEM8 = FDMDV_OS_TAPS_48_8K # 8kHz signal filter memory
MEM48 = FDMDV_OS_TAPS_48K # 48kHz signal filter memory
FRAMES = int(50) # number of frames to test
FS8 = 8000
FS48 = 48000
AMP = 16000 # sine wave amplitude
FTEST8 = 800 # input test frequency at FS=8kHz
FINTER48 = 10000 # interferer frequency at FS=48kHz
# Due to the design of these resamplers, the processing buffer (at 8kHz)
# must be an integer multiple of oversampling ratio
assert N8 % FDMDV_OS_48 == 0
# time indexes, we advance every frame
t = 0
t1 = 0
# output files to listen to/evaluate result
fin8 = open("in8.raw", mode='wb')
f48 = open("out48.raw", mode='wb')
fout8 = open("out8.raw", mode='wb')
resampler = codec2.resampler()
for f in range(FRAMES):
sine_in8k = (AMP*np.cos(2*np.pi*np.arange(t,t+N8)*FTEST8/FS8)).astype(np.int16)
t += N8
sine_in8k.tofile(fin8)
sine_out48k = resampler.resample8_to_48(sine_in8k)
sine_out48k.tofile(f48)
# add interfering sine wave (down sampling filter should remove)
sine_in48k = (sine_out48k + (AMP/2)*np.cos(2*np.pi*np.arange(t1,t1+N48)*FINTER48/FS48)).astype(np.int16)
t1 += N48
sine_out8k = resampler.resample48_to_8(sine_in48k)
sine_out8k.tofile(fout8)
fin8.close()
f48.close()
fout8.close()
# Automated test evaluation --------------------------------------------
# The input and output signals will not be time aligned due to the filter
# delays, so compare the magnitude spectrum
in8k = np.fromfile("in8.raw", dtype=np.int16)
out8k = np.fromfile("out8.raw", dtype=np.int16)
assert len(in8k) == len(out8k)
n = len(in8k)
h = np.hanning(len(in8k))
S1 = np.abs(np.fft.fft(in8k * h))
S2 = np.abs(np.fft.fft(out8k * h))
error = S1-S2
error_energy = np.dot(error,error)
ratio = error_energy/np.dot(S1,S1)
ratio_dB = 10*np.log10(ratio);
print("ratio_dB: %4.2f" % (ratio_dB));
threshdB = -40
if ratio_dB < threshdB:
print("PASS")
else:
print("FAIL")

View file

@ -7,36 +7,64 @@ Created on Wed Dec 23 07:04:24 2020
""" """
import sys import sys
sys.path.insert(0,'..')
sys.path.insert(0,'../tnc')
import data_handler
import argparse
import codec2 import codec2
import data_handler
import modem import modem
import pytest
parser = argparse.ArgumentParser(description='ARQ TEST') import static
parser.add_argument('--mode', dest="FREEDV_MODE", type=str, choices=['datac0', 'datac1', 'datac3'])
parser.add_argument('--framesperburst', dest="N_FRAMES_PER_BURST", default=1, type=int)
args = parser.parse_args()
bytes_out = b'{"dt":"f","fn":"zeit.txt","ft":"text\\/plain","d":"data:text\\/plain;base64,MyBtb2Rlcywgb2huZSBjbGFzcwowLjAwMDk2OTQ4MTE4MDk5MTg0MTcKCjIgbW9kZXMsIG9obmUgY2xhc3MKMC4wMDA5NjY1NDUxODkxMjI1Mzk0CgoxIG1vZGUsIG9obmUgY2xhc3MKMC4wMDA5NjY5NzY1NTU4Nzc4MjA5CgMyBtb2Rlcywgb2huZSBjbGFzcwowLjAwMDk2OTQ4MTE4MDk5MTg0MTcKCjIgbW9kZXMsIG9obmUgY2xhc3MKMC4wMDA5NjY1NDUxODkxMjI1Mzk0CgoxIG1vZGUsIG9obmUgY2xhc3MKMC4wMDA5NjY5NzY1NTU4Nzc4MjA5Cg=MyBtb2Rlcywgb2huZSBjbGFzcwowLjAwMDk2OTQ4MTE4MDk5MTg0MTcKCjIgbW9kZXMsIG9obmUgY2xhc3MKMC4wMDA5NjY1NDUxODkxMjI1Mzk0CgoxIG1vZGUsIG9obmUgY2xhc3MKMC4wMDA5NjY5NzY1NTU4Nzc4MjA5CgMyBtb2Rlcywgb2huZSBjbGFzcwowLjAwMDk2OTQ4MTE4MDk5MTg0MTcKCjIgbW9kZXMsIG9obmUgY2xhc3MKMC4wMDA5NjY1NDUxODkxMjI1Mzk0CgoxIG1vZGUsIG9obmUgY2xhc3MKMC4wMDA5NjY5NzY1NTU4Nzc4MjA5CgMyBtb2Rlcywgb2huZSBjbGFzcwowLjAwMDk2OTQ4MTE4MDk5MTg0MTcKCjIgbW9kZXMsIG9obmUgY2xhc3MKMC4wMDA5NjY1NDUxODkxMjI1Mzk0CgoxIG1vZGUsIG9obmUgY2xhc3MKMC4wMDA5NjY5NzY1NTU4Nzc4MjA5Cg=","crc":"123123123"}' bytes_out = b'{"dt":"f","fn":"zeit.txt","ft":"text\\/plain","d":"data:text\\/plain;base64,MyBtb2Rlcywgb2huZSBjbGFzcwowLjAwMDk2OTQ4MTE4MDk5MTg0MTcKCjIgbW9kZXMsIG9obmUgY2xhc3MKMC4wMDA5NjY1NDUxODkxMjI1Mzk0CgoxIG1vZGUsIG9obmUgY2xhc3MKMC4wMDA5NjY5NzY1NTU4Nzc4MjA5CgMyBtb2Rlcywgb2huZSBjbGFzcwowLjAwMDk2OTQ4MTE4MDk5MTg0MTcKCjIgbW9kZXMsIG9obmUgY2xhc3MKMC4wMDA5NjY1NDUxODkxMjI1Mzk0CgoxIG1vZGUsIG9obmUgY2xhc3MKMC4wMDA5NjY5NzY1NTU4Nzc4MjA5Cg=MyBtb2Rlcywgb2huZSBjbGFzcwowLjAwMDk2OTQ4MTE4MDk5MTg0MTcKCjIgbW9kZXMsIG9obmUgY2xhc3MKMC4wMDA5NjY1NDUxODkxMjI1Mzk0CgoxIG1vZGUsIG9obmUgY2xhc3MKMC4wMDA5NjY5NzY1NTU4Nzc4MjA5CgMyBtb2Rlcywgb2huZSBjbGFzcwowLjAwMDk2OTQ4MTE4MDk5MTg0MTcKCjIgbW9kZXMsIG9obmUgY2xhc3MKMC4wMDA5NjY1NDUxODkxMjI1Mzk0CgoxIG1vZGUsIG9obmUgY2xhc3MKMC4wMDA5NjY5NzY1NTU4Nzc4MjA5CgMyBtb2Rlcywgb2huZSBjbGFzcwowLjAwMDk2OTQ4MTE4MDk5MTg0MTcKCjIgbW9kZXMsIG9obmUgY2xhc3MKMC4wMDA5NjY1NDUxODkxMjI1Mzk0CgoxIG1vZGUsIG9obmUgY2xhc3MKMC4wMDA5NjY5NzY1NTU4Nzc4MjA5Cg=","crc":"123123123"}'
# start data handler
data_handler.DATA()
# start modem @pytest.mark.parametrize("freedv_mode", ["datac0", "datac1", "datac3"])
modem = modem.RF() @pytest.mark.parametrize("n_frames_per_burst", [1, 2, 3])
def test_highsnr_arq_short(freedv_mode: str, n_frames_per_burst: int):
t_mode = t_repeats = t_repeat_delay = 0
t_frames = []
mode = codec2.freedv_get_mode(args.FREEDV_MODE) def t_tx_dummy(mode, repeats, repeat_delay, frames):
print(mode) """Replacement function for transmit"""
n_frames_per_burst = args.N_FRAMES_PER_BURST print(f"t_tx_dummy: In transmit({mode}, {repeats}, {repeat_delay}, {frames})")
nonlocal t_mode, t_repeats, t_repeat_delay, t_frames
t_mode = mode
t_repeats = repeats
t_repeat_delay = repeat_delay
t_frames = frames[:]
static.TRANSMITTING = False
# enable testmode # Enable testmode
data_handler.TESTMODE = True modem.TESTMODE = True
# Set some inner variables so the modules don't throw exceptions.
modem.RXCHANNEL = "/tmp/rxpipe"
modem.TXCHANNEL = "/tmp/txpipe"
data_handler.TESTMODE = True
static.HAMLIB_RADIOCONTROL = "disabled"
# add command to data qeue # start data handler
data_handler.DATA_QUEUE_TRANSMIT.put(['ARQ_FILE', bytes_out, mode, n_frames_per_burst]) data_handler.DATA()
# start modem
t_modem = modem.RF()
# Replace transmit routine with our own, an effective No-Op.
t_modem.transmit = t_tx_dummy
mode = codec2.freedv_get_mode_value_by_name(freedv_mode)
print(mode)
# add command to data qeue
data_handler.DATA_QUEUE_TRANSMIT.put(
["ARQ_FILE", bytes_out, mode, n_frames_per_burst]
)
# This test isn't complete yet, or is obsolete.
assert False
if __name__ == "__main__":
# Run pytest with the current script as the filename.
ecode = pytest.main(["-v", sys.argv[0]])
if ecode == 0:
print("errors: 0")
else:
print(ecode)

View file

@ -3,13 +3,16 @@
# #
# tests audio buffer thread safety # tests audio buffer thread safety
# pylint: disable=global-statement, invalid-name
import sys import sys
sys.path.insert(0,'..')
from tnc import codec2
import threading import threading
import numpy as np
from time import sleep from time import sleep
import codec2
import numpy as np
import pytest
BUFFER_SZ = 1024 BUFFER_SZ = 1024
N_MAX = 100 # write a repeating sequence of 0..N_MAX-1 N_MAX = 100 # write a repeating sequence of 0..N_MAX-1
WRITE_SZ = 10 # different read and write sized buffers WRITE_SZ = 10 # different read and write sized buffers
@ -19,34 +22,44 @@ NTESTS = 10000
running = True running = True
audio_buffer = codec2.audio_buffer(BUFFER_SZ) audio_buffer = codec2.audio_buffer(BUFFER_SZ)
n_write = int(0) n_write = 0
n_read = int(0)
def writer():
def t_writer():
"""
Subprocess to handle writes to the NumPY audio "device."
"""
global n_write global n_write
print("writer starting") print("writer starting")
n = int(0) n = 0
buf = np.zeros(WRITE_SZ, dtype=np.int16) buf = np.zeros(WRITE_SZ, dtype=np.int16)
while running: while running:
nfree = audio_buffer.size - audio_buffer.nbuffer nfree = audio_buffer.size - audio_buffer.nbuffer
if nfree >= WRITE_SZ: if nfree >= WRITE_SZ:
for i in range(0,WRITE_SZ): for index in range(WRITE_SZ):
buf[i] = n; buf[index] = n
n += 1 n += 1
if n == N_MAX: if n == N_MAX:
n = 0 n = 0
n_write += 1 n_write += 1
audio_buffer.push(buf) audio_buffer.push(buf)
x = threading.Thread(target=writer)
x.start()
n_out = int(0) def test_audiobuffer():
errors = int(0) """
for tests in range(1,NTESTS): Test for the audiobuffer
"""
global running
# Start the writer in a new thread.
writer_thread = threading.Thread(target=t_writer)
writer_thread.start()
n_out = n_read = errors = 0
for _ in range(NTESTS):
while audio_buffer.nbuffer < READ_SZ: while audio_buffer.nbuffer < READ_SZ:
sleep(0.001) sleep(0.001)
for i in range(0,READ_SZ): for i in range(READ_SZ):
if audio_buffer.buffer[i] != n_out: if audio_buffer.buffer[i] != n_out:
errors += 1 errors += 1
n_out += 1 n_out += 1
@ -55,5 +68,21 @@ for tests in range(1,NTESTS):
n_read += 1 n_read += 1
audio_buffer.pop(READ_SZ) audio_buffer.pop(READ_SZ)
running = False print(f"n_write: {n_write} n_read: {n_read} errors: {errors} ")
print("n_write: %d n_read: %d errors: %d " % (n_write, n_read, errors))
# Indirectly stop the thread
running = False
sleep(0.1)
assert not writer_thread.is_alive()
assert n_write - n_read < BUFFER_SZ
assert errors == 0
if __name__ == "__main__":
# Run pytest with the current script as the filename.
ecode = pytest.main(["-v", sys.argv[0]])
if ecode == 0:
print("errors: 0")
else:
print(ecode)

View file

@ -6,41 +6,54 @@ Created on Wed Dec 23 07:04:24 2020
@author: DJ2LS @author: DJ2LS
""" """
import ctypes
from ctypes import *
import pathlib
import pyaudio
import sys
import logging
import time
import threading
import sys
import argparse import argparse
import ctypes
import sys
import threading
import time
import numpy as np import numpy as np
sys.path.insert(0,'..') import pyaudio
sys.path.insert(0, "..")
from tnc import codec2 from tnc import codec2
#--------------------------------------------GET PARAMETER INPUTS # --------------------------------------------GET PARAMETER INPUTS
parser = argparse.ArgumentParser(description='FreeDATA audio test') parser = argparse.ArgumentParser(description="FreeDATA audio test")
parser.add_argument('--bursts', dest="N_BURSTS", default=1, type=int) parser.add_argument("--bursts", dest="N_BURSTS", default=1, type=int)
parser.add_argument('--framesperburst', dest="N_FRAMES_PER_BURST", default=1, type=int) parser.add_argument("--framesperburst", dest="N_FRAMES_PER_BURST", default=1, type=int)
parser.add_argument('--audiodev', dest="AUDIO_INPUT_DEVICE", default=-1, type=int, help="audio device number to use") parser.add_argument(
parser.add_argument('--debug', dest="DEBUGGING_MODE", action="store_true") "--audiodev",
parser.add_argument('--list', dest="LIST", action="store_true", help="list audio devices by number and exit") dest="AUDIO_INPUT_DEVICE",
parser.add_argument('--timeout', dest="TIMEOUT", default=10, type=int, help="Timeout (seconds) before test ends") default=-1,
type=int,
help="audio device number to use",
)
parser.add_argument("--debug", dest="DEBUGGING_MODE", action="store_true")
parser.add_argument(
"--list",
dest="LIST",
action="store_true",
help="list audio devices by number and exit",
)
parser.add_argument(
"--timeout",
dest="TIMEOUT",
default=10,
type=int,
help="Timeout (seconds) before test ends",
)
args, _ = parser.parse_known_args()
args = parser.parse_args()
if args.LIST: if args.LIST:
p = pyaudio.PyAudio() p = pyaudio.PyAudio()
for dev in range(0,p.get_device_count()): for dev in range(p.get_device_count()):
print("audiodev: ", dev, p.get_device_info_by_index(dev)["name"]) print("audiodev: ", dev, p.get_device_info_by_index(dev)["name"])
quit() sys.exit()
class Test:
class Test():
def __init__(self): def __init__(self):
self.N_BURSTS = args.N_BURSTS self.N_BURSTS = args.N_BURSTS
self.N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST self.N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST
@ -49,14 +62,15 @@ class Test():
self.TIMEOUT = args.TIMEOUT self.TIMEOUT = args.TIMEOUT
# AUDIO PARAMETERS # AUDIO PARAMETERS
self.AUDIO_FRAMES_PER_BUFFER = 2400*2 # <- consider increasing if you get nread_exceptions > 0 # v-- consider increasing if you get nread_exceptions > 0
self.AUDIO_FRAMES_PER_BUFFER = 2400 * 2
self.MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000 self.MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000
self.AUDIO_SAMPLE_RATE_RX = 48000 self.AUDIO_SAMPLE_RATE_RX = 48000
# make sure our resampler will work # make sure our resampler will work
assert (self.AUDIO_SAMPLE_RATE_RX / self.MODEM_SAMPLE_RATE) == codec2.api.FDMDV_OS_48 assert (
self.AUDIO_SAMPLE_RATE_RX / self.MODEM_SAMPLE_RATE
) == codec2.api.FDMDV_OS_48
# check if we want to use an audio device then do an pyaudio init # check if we want to use an audio device then do an pyaudio init
if self.AUDIO_INPUT_DEVICE != -1: if self.AUDIO_INPUT_DEVICE != -1:
@ -64,50 +78,73 @@ class Test():
# auto search for loopback devices # auto search for loopback devices
if self.AUDIO_INPUT_DEVICE == -2: if self.AUDIO_INPUT_DEVICE == -2:
loopback_list = [] loopback_list = []
for dev in range(0,self.p.get_device_count()): for dev in range(self.p.get_device_count()):
if 'Loopback: PCM' in self.p.get_device_info_by_index(dev)["name"]: if "Loopback: PCM" in self.p.get_device_info_by_index(dev)["name"]:
loopback_list.append(dev) loopback_list.append(dev)
if len(loopback_list) >= 2: if len(loopback_list) >= 2:
self.AUDIO_INPUT_DEVICE = loopback_list[0] #0 = RX 1 = TX # 0 = RX 1 = TX
self.AUDIO_INPUT_DEVICE = loopback_list[0]
print(f"loopback_list rx: {loopback_list}", file=sys.stderr) print(f"loopback_list rx: {loopback_list}", file=sys.stderr)
else: else:
quit() sys.exit()
print(f"AUDIO INPUT DEVICE: {self.AUDIO_INPUT_DEVICE} DEVICE: {self.p.get_device_info_by_index(self.AUDIO_INPUT_DEVICE)['name']} \ print(
AUDIO SAMPLE RATE: {self.AUDIO_SAMPLE_RATE_RX}", file=sys.stderr) f"AUDIO INPUT DEVICE: {self.AUDIO_INPUT_DEVICE} DEVICE: {self.p.get_device_info_by_index(self.AUDIO_INPUT_DEVICE)['name']} \
AUDIO SAMPLE RATE: {self.AUDIO_SAMPLE_RATE_RX}",
file=sys.stderr,
)
self.stream_rx = self.p.open(format=pyaudio.paInt16, self.stream_rx = self.p.open(
format=pyaudio.paInt16,
channels=1, channels=1,
rate=self.AUDIO_SAMPLE_RATE_RX, rate=self.AUDIO_SAMPLE_RATE_RX,
frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER, frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER,
input=True, input=True,
output=False, output=False,
input_device_index=self.AUDIO_INPUT_DEVICE, input_device_index=self.AUDIO_INPUT_DEVICE,
stream_callback=self.callback stream_callback=self.callback,
) )
else:
print("test_callback_multimode_rx: Not written for STDIN usage.")
print("Exiting.")
sys.exit()
# open codec2 instance # open codec2 instance
self.datac0_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC0), c_void_p) self.datac0_freedv = ctypes.cast(
self.datac0_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(self.datac0_freedv)/8) codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC0), ctypes.c_void_p
self.datac0_bytes_out = create_string_buffer(self.datac0_bytes_per_frame) )
codec2.api.freedv_set_frames_per_burst(self.datac0_freedv,self.N_FRAMES_PER_BURST) self.datac0_bytes_per_frame = int(
self.datac0_buffer = codec2.audio_buffer(2*self.AUDIO_FRAMES_PER_BUFFER) codec2.api.freedv_get_bits_per_modem_frame(self.datac0_freedv) / 8
)
self.datac1_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC1), c_void_p) self.datac0_bytes_out = ctypes.create_string_buffer(self.datac0_bytes_per_frame)
self.datac1_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(self.datac1_freedv)/8) codec2.api.freedv_set_frames_per_burst(
self.datac1_bytes_out = create_string_buffer(self.datac1_bytes_per_frame) self.datac0_freedv, self.N_FRAMES_PER_BURST
codec2.api.freedv_set_frames_per_burst(self.datac1_freedv,self.N_FRAMES_PER_BURST) )
self.datac1_buffer = codec2.audio_buffer(2*self.AUDIO_FRAMES_PER_BUFFER) self.datac0_buffer = codec2.audio_buffer(2 * self.AUDIO_FRAMES_PER_BUFFER)
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)
self.datac3_bytes_out = create_string_buffer(self.datac3_bytes_per_frame)
codec2.api.freedv_set_frames_per_burst(self.datac3_freedv,self.N_FRAMES_PER_BURST)
self.datac3_buffer = codec2.audio_buffer(2*self.AUDIO_FRAMES_PER_BUFFER)
self.datac1_freedv = ctypes.cast(
codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC1), ctypes.c_void_p
)
self.datac1_bytes_per_frame = int(
codec2.api.freedv_get_bits_per_modem_frame(self.datac1_freedv) / 8
)
self.datac1_bytes_out = ctypes.create_string_buffer(self.datac1_bytes_per_frame)
codec2.api.freedv_set_frames_per_burst(
self.datac1_freedv, self.N_FRAMES_PER_BURST
)
self.datac1_buffer = codec2.audio_buffer(2 * self.AUDIO_FRAMES_PER_BUFFER)
self.datac3_freedv = ctypes.cast(
codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC3), ctypes.c_void_p
)
self.datac3_bytes_per_frame = int(
codec2.api.freedv_get_bits_per_modem_frame(self.datac3_freedv) / 8
)
self.datac3_bytes_out = ctypes.create_string_buffer(self.datac3_bytes_per_frame)
codec2.api.freedv_set_frames_per_burst(
self.datac3_freedv, self.N_FRAMES_PER_BURST
)
self.datac3_buffer = codec2.audio_buffer(2 * self.AUDIO_FRAMES_PER_BUFFER)
# SET COUNTERS # SET COUNTERS
self.rx_total_frames_datac0 = 0 self.rx_total_frames_datac0 = 0
@ -131,19 +168,18 @@ class Test():
# Copy received 48 kHz to a file. Listen to this file with: # Copy received 48 kHz to a file. Listen to this file with:
# aplay -r 48000 -f S16_LE rx48_callback.raw # aplay -r 48000 -f S16_LE rx48_callback.raw
# Corruption of this file is a good way to detect audio card issues # Corruption of this file is a good way to detect audio card issues
self.frx = open("rx48_callback_multimode.raw", mode='wb') self.frx = open("rx48_callback_multimode.raw", mode="wb")
# initial nin values # initial nin values
self.datac0_nin = codec2.api.freedv_nin(self.datac0_freedv) self.datac0_nin = codec2.api.freedv_nin(self.datac0_freedv)
self.datac1_nin = codec2.api.freedv_nin(self.datac1_freedv) self.datac1_nin = codec2.api.freedv_nin(self.datac1_freedv)
self.datac3_nin = codec2.api.freedv_nin(self.datac3_freedv) self.datac3_nin = codec2.api.freedv_nin(self.datac3_freedv)
self.LOGGER_THREAD = threading.Thread(
self.LOGGER_THREAD = threading.Thread(target=self.print_stats, name="LOGGER_THREAD") target=self.print_stats, name="LOGGER_THREAD"
)
self.LOGGER_THREAD.start() self.LOGGER_THREAD.start()
def callback(self, data_in48k, frame_count, time_info, status): def callback(self, data_in48k, frame_count, time_info, status):
x = np.frombuffer(data_in48k, dtype=np.int16) x = np.frombuffer(data_in48k, dtype=np.int16)
x.tofile(self.frx) x.tofile(self.frx)
@ -153,10 +189,13 @@ class Test():
self.datac1_buffer.push(x) self.datac1_buffer.push(x)
self.datac3_buffer.push(x) self.datac3_buffer.push(x)
while self.datac0_buffer.nbuffer >= self.datac0_nin: while self.datac0_buffer.nbuffer >= self.datac0_nin:
# demodulate audio # demodulate audio
nbytes = codec2.api.freedv_rawdatarx(self.datac0_freedv, self.datac0_bytes_out, self.datac0_buffer.buffer.ctypes) 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_buffer.pop(self.datac0_nin)
self.datac0_nin = codec2.api.freedv_nin(self.datac0_freedv) self.datac0_nin = codec2.api.freedv_nin(self.datac0_freedv)
if nbytes == self.datac0_bytes_per_frame: if nbytes == self.datac0_bytes_per_frame:
@ -167,10 +206,13 @@ class Test():
self.rx_frames_datac0 = 0 self.rx_frames_datac0 = 0
self.rx_bursts_datac0 = self.rx_bursts_datac0 + 1 self.rx_bursts_datac0 = self.rx_bursts_datac0 + 1
while self.datac1_buffer.nbuffer >= self.datac1_nin: while self.datac1_buffer.nbuffer >= self.datac1_nin:
# demodulate audio # demodulate audio
nbytes = codec2.api.freedv_rawdatarx(self.datac1_freedv, self.datac1_bytes_out, self.datac1_buffer.buffer.ctypes) 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_buffer.pop(self.datac1_nin)
self.datac1_nin = codec2.api.freedv_nin(self.datac1_freedv) self.datac1_nin = codec2.api.freedv_nin(self.datac1_freedv)
if nbytes == self.datac1_bytes_per_frame: if nbytes == self.datac1_bytes_per_frame:
@ -181,10 +223,13 @@ class Test():
self.rx_frames_datac1 = 0 self.rx_frames_datac1 = 0
self.rx_bursts_datac1 = self.rx_bursts_datac1 + 1 self.rx_bursts_datac1 = self.rx_bursts_datac1 + 1
while self.datac3_buffer.nbuffer >= self.datac3_nin: while self.datac3_buffer.nbuffer >= self.datac3_nin:
# demodulate audio # demodulate audio
nbytes = codec2.api.freedv_rawdatarx(self.datac3_freedv, self.datac3_bytes_out, self.datac3_buffer.buffer.ctypes) 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_buffer.pop(self.datac3_nin)
self.datac3_nin = codec2.api.freedv_nin(self.datac3_freedv) self.datac3_nin = codec2.api.freedv_nin(self.datac3_freedv)
if nbytes == self.datac3_bytes_per_frame: if nbytes == self.datac3_bytes_per_frame:
@ -195,8 +240,9 @@ class Test():
self.rx_frames_datac3 = 0 self.rx_frames_datac3 = 0
self.rx_bursts_datac3 = self.rx_bursts_datac3 + 1 self.rx_bursts_datac3 = self.rx_bursts_datac3 + 1
if (
if (self.rx_bursts_datac0 and self.rx_bursts_datac1 and self.rx_bursts_datac3) == self.N_BURSTS: self.rx_bursts_datac0 and self.rx_bursts_datac1 and self.rx_bursts_datac3
) == self.N_BURSTS:
self.receive = False self.receive = False
return (None, pyaudio.paContinue) return (None, pyaudio.paContinue)
@ -205,19 +251,39 @@ class Test():
while self.receive: while self.receive:
time.sleep(0.01) time.sleep(0.01)
if self.DEBUGGING_MODE: if self.DEBUGGING_MODE:
self.datac0_rxstatus = codec2.api.freedv_get_rx_status(self.datac0_freedv) self.datac0_rxstatus = codec2.api.freedv_get_rx_status(
self.datac0_rxstatus = codec2.api.rx_sync_flags_to_text[self.datac0_rxstatus] self.datac0_freedv
)
self.datac0_rxstatus = codec2.api.rx_sync_flags_to_text[
self.datac0_rxstatus
]
self.datac1_rxstatus = codec2.api.freedv_get_rx_status(self.datac1_freedv) self.datac1_rxstatus = codec2.api.freedv_get_rx_status(
self.datac1_rxstatus = codec2.api.rx_sync_flags_to_text[self.datac1_rxstatus] self.datac1_freedv
)
self.datac1_rxstatus = codec2.api.rx_sync_flags_to_text[
self.datac1_rxstatus
]
self.datac3_rxstatus = codec2.api.freedv_get_rx_status(self.datac3_freedv) self.datac3_rxstatus = codec2.api.freedv_get_rx_status(
self.datac3_rxstatus = codec2.api.rx_sync_flags_to_text[self.datac3_rxstatus] self.datac3_freedv
)
print("NIN0: %5d RX_STATUS0: %4s NIN1: %5d RX_STATUS1: %4s NIN3: %5d RX_STATUS3: %4s" % \ self.datac3_rxstatus = codec2.api.rx_sync_flags_to_text[
(self.datac0_nin, self.datac0_rxstatus, self.datac1_nin, self.datac1_rxstatus, self.datac3_nin, self.datac3_rxstatus), self.datac3_rxstatus
file=sys.stderr) ]
print(
"NIN0: %5d RX_STATUS0: %4s NIN1: %5d RX_STATUS1: %4s NIN3: %5d RX_STATUS3: %4s"
% (
self.datac0_nin,
self.datac0_rxstatus,
self.datac1_nin,
self.datac1_rxstatus,
self.datac3_nin,
self.datac3_rxstatus,
),
file=sys.stderr,
)
def run_audio(self): def run_audio(self):
try: try:
@ -226,7 +292,6 @@ class Test():
except Exception as e: except Exception as e:
print(f"pyAudio error: {e}", file=sys.stderr) print(f"pyAudio error: {e}", file=sys.stderr)
while self.receive and time.time() < self.timeout: while self.receive and time.time() < self.timeout:
time.sleep(1) time.sleep(1)
@ -235,10 +300,16 @@ class Test():
self.receive = False self.receive = False
if self.nread_exceptions: if self.nread_exceptions:
print("nread_exceptions %d - receive audio lost! Consider increasing Pyaudio frames_per_buffer..." % \ print(
self.nread_exceptions, file=sys.stderr) "nread_exceptions %d - receive audio lost! Consider increasing Pyaudio frames_per_buffer..."
% self.nread_exceptions,
file=sys.stderr,
)
print(f"DATAC0: {self.rx_bursts_datac0}/{self.rx_total_frames_datac0} DATAC1: {self.rx_bursts_datac1}/{self.rx_total_frames_datac1} DATAC3: {self.rx_bursts_datac3}/{self.rx_total_frames_datac3}", file=sys.stderr) print(
f"DATAC0: {self.rx_bursts_datac0}/{self.rx_total_frames_datac0} DATAC1: {self.rx_bursts_datac1}/{self.rx_total_frames_datac1} DATAC3: {self.rx_bursts_datac3}/{self.rx_total_frames_datac3}",
file=sys.stderr,
)
self.frx.close() self.frx.close()
# cloese pyaudio instance # cloese pyaudio instance

View file

@ -6,41 +6,52 @@ Created on Wed Dec 23 07:04:24 2020
@author: DJ2LS @author: DJ2LS
""" """
import ctypes
from ctypes import *
import pathlib
import pyaudio
import sys
import logging
import time
import threading
import sys
import argparse import argparse
import ctypes
import sys
import time
import numpy as np import numpy as np
sys.path.insert(0,'..') import pyaudio
sys.path.insert(0, "..")
from tnc import codec2 from tnc import codec2
#--------------------------------------------GET PARAMETER INPUTS # --------------------------------------------GET PARAMETER INPUTS
parser = argparse.ArgumentParser(description='FreeDATA audio test') parser = argparse.ArgumentParser(description="FreeDATA audio test")
parser.add_argument('--bursts', dest="N_BURSTS", default=1, type=int) parser.add_argument("--bursts", dest="N_BURSTS", default=1, type=int)
parser.add_argument('--framesperburst', dest="N_FRAMES_PER_BURST", default=1, type=int) parser.add_argument("--framesperburst", dest="N_FRAMES_PER_BURST", default=1, type=int)
parser.add_argument('--audiodev', dest="AUDIO_INPUT_DEVICE", default=-1, type=int, help="audio device number to use") parser.add_argument(
parser.add_argument('--debug', dest="DEBUGGING_MODE", action="store_true") "--audiodev",
parser.add_argument('--list', dest="LIST", action="store_true", help="list audio devices by number and exit") dest="AUDIO_INPUT_DEVICE",
parser.add_argument('--timeout', dest="TIMEOUT", default=10, type=int, help="Timeout (seconds) before test ends") default=-1,
type=int,
help="audio device number to use",
)
parser.add_argument("--debug", dest="DEBUGGING_MODE", action="store_true")
parser.add_argument(
"--list",
dest="LIST",
action="store_true",
help="list audio devices by number and exit",
)
parser.add_argument(
"--timeout",
dest="TIMEOUT",
default=10,
type=int,
help="Timeout (seconds) before test ends",
)
args, _ = parser.parse_known_args()
args = parser.parse_args()
if args.LIST: if args.LIST:
p = pyaudio.PyAudio() p = pyaudio.PyAudio()
for dev in range(0,p.get_device_count()): for dev in range(p.get_device_count()):
print("audiodev: ", dev, p.get_device_info_by_index(dev)["name"]) print("audiodev: ", dev, p.get_device_info_by_index(dev)["name"])
quit()
class Test:
class Test():
def __init__(self): def __init__(self):
self.N_BURSTS = args.N_BURSTS self.N_BURSTS = args.N_BURSTS
self.N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST self.N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST
@ -49,63 +60,91 @@ class Test():
self.TIMEOUT = args.TIMEOUT self.TIMEOUT = args.TIMEOUT
# AUDIO PARAMETERS # AUDIO PARAMETERS
self.AUDIO_FRAMES_PER_BUFFER = 2400*2 # <- consider increasing if you get nread_exceptions > 0 self.AUDIO_FRAMES_PER_BUFFER = (
2400 * 2
) # <- consider increasing if you get nread_exceptions > 0
self.MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000 self.MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000
self.AUDIO_SAMPLE_RATE_RX = 48000 self.AUDIO_SAMPLE_RATE_RX = 48000
# make sure our resampler will work # make sure our resampler will work
assert (self.AUDIO_SAMPLE_RATE_RX / self.MODEM_SAMPLE_RATE) == codec2.api.FDMDV_OS_48 assert (
self.AUDIO_SAMPLE_RATE_RX / self.MODEM_SAMPLE_RATE
) == codec2.api.FDMDV_OS_48
# check if we want to use an audio device then do an pyaudio init # check if we want to use an audio device then do an pyaudio init
if self.AUDIO_INPUT_DEVICE != -1: if self.AUDIO_INPUT_DEVICE != -1:
self.p = pyaudio.PyAudio() self.p = pyaudio.PyAudio()
# auto search for loopback devices # auto search for loopback devices
if self.AUDIO_INPUT_DEVICE == -2: if self.AUDIO_INPUT_DEVICE == -2:
loopback_list = [] loopback_list = [
for dev in range(0,self.p.get_device_count()): dev
if 'Loopback: PCM' in self.p.get_device_info_by_index(dev)["name"]: for dev in range(self.p.get_device_count())
loopback_list.append(dev) if "Loopback: PCM" in self.p.get_device_info_by_index(dev)["name"]
]
if len(loopback_list) >= 2: if len(loopback_list) >= 2:
self.AUDIO_INPUT_DEVICE = loopback_list[0] #0 = RX 1 = TX self.AUDIO_INPUT_DEVICE = loopback_list[0] # 0 = RX 1 = TX
print(f"loopback_list rx: {loopback_list}", file=sys.stderr) print(f"loopback_list rx: {loopback_list}", file=sys.stderr)
else: else:
quit() sys.exit()
print(f"AUDIO INPUT DEVICE: {self.AUDIO_INPUT_DEVICE} DEVICE: {self.p.get_device_info_by_index(self.AUDIO_INPUT_DEVICE)['name']} \ print(
AUDIO SAMPLE RATE: {self.AUDIO_SAMPLE_RATE_RX}", file=sys.stderr) f"AUDIO INPUT DEVICE: {self.AUDIO_INPUT_DEVICE} DEVICE: {self.p.get_device_info_by_index(self.AUDIO_INPUT_DEVICE)['name']} \
AUDIO SAMPLE RATE: {self.AUDIO_SAMPLE_RATE_RX}",
file=sys.stderr,
)
self.stream_rx = self.p.open(format=pyaudio.paInt16, self.stream_rx = self.p.open(
format=pyaudio.paInt16,
channels=1, channels=1,
rate=self.AUDIO_SAMPLE_RATE_RX, rate=self.AUDIO_SAMPLE_RATE_RX,
frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER, frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER,
input=True, input=True,
output=False, output=False,
input_device_index=self.AUDIO_INPUT_DEVICE, input_device_index=self.AUDIO_INPUT_DEVICE,
stream_callback=self.callback stream_callback=self.callback,
) )
else:
print("test_callback_multimode_rx_outside: Not written for STDIN usage.")
print("Exiting.")
sys.exit()
# open codec2 instance # open codec2 instance
self.datac0_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC0), c_void_p) self.datac0_freedv = ctypes.cast(
self.datac0_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(self.datac0_freedv)/8) codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC0), ctypes.c_void_p
self.datac0_bytes_out = create_string_buffer(self.datac0_bytes_per_frame) )
codec2.api.freedv_set_frames_per_burst(self.datac0_freedv,self.N_FRAMES_PER_BURST) self.datac0_bytes_per_frame = int(
self.datac0_buffer = codec2.audio_buffer(2*self.AUDIO_FRAMES_PER_BUFFER) codec2.api.freedv_get_bits_per_modem_frame(self.datac0_freedv) / 8
)
self.datac1_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC1), c_void_p) self.datac0_bytes_out = ctypes.create_string_buffer(self.datac0_bytes_per_frame)
self.datac1_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(self.datac1_freedv)/8) codec2.api.freedv_set_frames_per_burst(
self.datac1_bytes_out = create_string_buffer(self.datac1_bytes_per_frame) self.datac0_freedv, self.N_FRAMES_PER_BURST
codec2.api.freedv_set_frames_per_burst(self.datac1_freedv,self.N_FRAMES_PER_BURST) )
self.datac1_buffer = codec2.audio_buffer(2*self.AUDIO_FRAMES_PER_BUFFER) self.datac0_buffer = codec2.audio_buffer(2 * self.AUDIO_FRAMES_PER_BUFFER)
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)
self.datac3_bytes_out = create_string_buffer(self.datac3_bytes_per_frame)
codec2.api.freedv_set_frames_per_burst(self.datac3_freedv,self.N_FRAMES_PER_BURST)
self.datac3_buffer = codec2.audio_buffer(2*self.AUDIO_FRAMES_PER_BUFFER)
self.datac1_freedv = ctypes.cast(
codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC1), ctypes.c_void_p
)
self.datac1_bytes_per_frame = int(
codec2.api.freedv_get_bits_per_modem_frame(self.datac1_freedv) / 8
)
self.datac1_bytes_out = ctypes.create_string_buffer(self.datac1_bytes_per_frame)
codec2.api.freedv_set_frames_per_burst(
self.datac1_freedv, self.N_FRAMES_PER_BURST
)
self.datac1_buffer = codec2.audio_buffer(2 * self.AUDIO_FRAMES_PER_BUFFER)
self.datac3_freedv = ctypes.cast(
codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC3), ctypes.c_void_p
)
self.datac3_bytes_per_frame = int(
codec2.api.freedv_get_bits_per_modem_frame(self.datac3_freedv) / 8
)
self.datac3_bytes_out = ctypes.create_string_buffer(self.datac3_bytes_per_frame)
codec2.api.freedv_set_frames_per_burst(
self.datac3_freedv, self.N_FRAMES_PER_BURST
)
self.datac3_buffer = codec2.audio_buffer(2 * self.AUDIO_FRAMES_PER_BUFFER)
# SET COUNTERS # SET COUNTERS
self.rx_total_frames_datac0 = 0 self.rx_total_frames_datac0 = 0
@ -129,15 +168,13 @@ class Test():
# Copy received 48 kHz to a file. Listen to this file with: # Copy received 48 kHz to a file. Listen to this file with:
# aplay -r 48000 -f S16_LE rx48_callback.raw # aplay -r 48000 -f S16_LE rx48_callback.raw
# Corruption of this file is a good way to detect audio card issues # Corruption of this file is a good way to detect audio card issues
self.frx = open("rx48_callback_multimode.raw", mode='wb') self.frx = open("rx48_callback_multimode.raw", mode="wb")
# initial nin values # initial nin values
self.datac0_nin = codec2.api.freedv_nin(self.datac0_freedv) self.datac0_nin = codec2.api.freedv_nin(self.datac0_freedv)
self.datac1_nin = codec2.api.freedv_nin(self.datac1_freedv) self.datac1_nin = codec2.api.freedv_nin(self.datac1_freedv)
self.datac3_nin = codec2.api.freedv_nin(self.datac3_freedv) self.datac3_nin = codec2.api.freedv_nin(self.datac3_freedv)
def callback(self, data_in48k, frame_count, time_info, status): def callback(self, data_in48k, frame_count, time_info, status):
x = np.frombuffer(data_in48k, dtype=np.int16) x = np.frombuffer(data_in48k, dtype=np.int16)
@ -148,26 +185,37 @@ class Test():
self.datac1_buffer.push(x) self.datac1_buffer.push(x)
self.datac3_buffer.push(x) self.datac3_buffer.push(x)
return (None, pyaudio.paContinue) return (None, pyaudio.paContinue)
def print_stats(self): def print_stats(self):
if self.DEBUGGING_MODE: if self.DEBUGGING_MODE:
self.datac0_rxstatus = codec2.api.freedv_get_rx_status(self.datac0_freedv) self.datac0_rxstatus = codec2.api.freedv_get_rx_status(self.datac0_freedv)
self.datac0_rxstatus = codec2.api.rx_sync_flags_to_text[self.datac0_rxstatus] self.datac0_rxstatus = codec2.api.rx_sync_flags_to_text[
self.datac0_rxstatus
]
self.datac1_rxstatus = codec2.api.freedv_get_rx_status(self.datac1_freedv) self.datac1_rxstatus = codec2.api.freedv_get_rx_status(self.datac1_freedv)
self.datac1_rxstatus = codec2.api.rx_sync_flags_to_text[self.datac1_rxstatus] self.datac1_rxstatus = codec2.api.rx_sync_flags_to_text[
self.datac1_rxstatus
]
self.datac3_rxstatus = codec2.api.freedv_get_rx_status(self.datac3_freedv) self.datac3_rxstatus = codec2.api.freedv_get_rx_status(self.datac3_freedv)
self.datac3_rxstatus = codec2.api.rx_sync_flags_to_text[self.datac3_rxstatus] self.datac3_rxstatus = codec2.api.rx_sync_flags_to_text[
self.datac3_rxstatus
print("NIN0: %5d RX_STATUS0: %4s NIN1: %5d RX_STATUS1: %4s NIN3: %5d RX_STATUS3: %4s" % \ ]
(self.datac0_nin, self.datac0_rxstatus, self.datac1_nin, self.datac1_rxstatus, self.datac3_nin, self.datac3_rxstatus),
file=sys.stderr)
print(
"NIN0: %5d RX_STATUS0: %4s NIN1: %5d RX_STATUS1: %4s NIN3: %5d RX_STATUS3: %4s"
% (
self.datac0_nin,
self.datac0_rxstatus,
self.datac1_nin,
self.datac1_rxstatus,
self.datac3_nin,
self.datac3_rxstatus,
),
file=sys.stderr,
)
def run_audio(self): def run_audio(self):
try: try:
@ -176,11 +224,14 @@ class Test():
except Exception as e: except Exception as e:
print(f"pyAudio error: {e}", file=sys.stderr) print(f"pyAudio error: {e}", file=sys.stderr)
while self.receive and time.time() < self.timeout: while self.receive and time.time() < self.timeout:
while self.datac0_buffer.nbuffer >= self.datac0_nin: while self.datac0_buffer.nbuffer >= self.datac0_nin:
# demodulate audio # demodulate audio
nbytes = codec2.api.freedv_rawdatarx(self.datac0_freedv, self.datac0_bytes_out, self.datac0_buffer.buffer.ctypes) 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_buffer.pop(self.datac0_nin)
self.datac0_nin = codec2.api.freedv_nin(self.datac0_freedv) self.datac0_nin = codec2.api.freedv_nin(self.datac0_freedv)
if nbytes == self.datac0_bytes_per_frame: if nbytes == self.datac0_bytes_per_frame:
@ -192,10 +243,13 @@ class Test():
self.rx_bursts_datac0 = self.rx_bursts_datac0 + 1 self.rx_bursts_datac0 = self.rx_bursts_datac0 + 1
self.print_stats() self.print_stats()
while self.datac1_buffer.nbuffer >= self.datac1_nin: while self.datac1_buffer.nbuffer >= self.datac1_nin:
# demodulate audio # demodulate audio
nbytes = codec2.api.freedv_rawdatarx(self.datac1_freedv, self.datac1_bytes_out, self.datac1_buffer.buffer.ctypes) 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_buffer.pop(self.datac1_nin)
self.datac1_nin = codec2.api.freedv_nin(self.datac1_freedv) self.datac1_nin = codec2.api.freedv_nin(self.datac1_freedv)
if nbytes == self.datac1_bytes_per_frame: if nbytes == self.datac1_bytes_per_frame:
@ -209,7 +263,11 @@ class Test():
while self.datac3_buffer.nbuffer >= self.datac3_nin: while self.datac3_buffer.nbuffer >= self.datac3_nin:
# demodulate audio # demodulate audio
nbytes = codec2.api.freedv_rawdatarx(self.datac3_freedv, self.datac3_bytes_out, self.datac3_buffer.buffer.ctypes) 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_buffer.pop(self.datac3_nin)
self.datac3_nin = codec2.api.freedv_nin(self.datac3_freedv) self.datac3_nin = codec2.api.freedv_nin(self.datac3_freedv)
if nbytes == self.datac3_bytes_per_frame: if nbytes == self.datac3_bytes_per_frame:
@ -221,18 +279,27 @@ class Test():
self.rx_bursts_datac3 = self.rx_bursts_datac3 + 1 self.rx_bursts_datac3 = self.rx_bursts_datac3 + 1
self.print_stats() self.print_stats()
if (self.rx_bursts_datac0 and self.rx_bursts_datac1 and self.rx_bursts_datac3) == self.N_BURSTS: if (
self.rx_bursts_datac0
and self.rx_bursts_datac1
and self.rx_bursts_datac3
) == self.N_BURSTS:
self.receive = False self.receive = False
if time.time() >= self.timeout: if time.time() >= self.timeout:
print("TIMEOUT REACHED") print("TIMEOUT REACHED")
if self.nread_exceptions: if self.nread_exceptions:
print("nread_exceptions %d - receive audio lost! Consider increasing Pyaudio frames_per_buffer..." % \ print(
self.nread_exceptions, file=sys.stderr) "nread_exceptions %d - receive audio lost! Consider increasing Pyaudio frames_per_buffer..."
% self.nread_exceptions,
file=sys.stderr,
)
print(f"DATAC0: {self.rx_bursts_datac0}/{self.rx_total_frames_datac0} DATAC1: {self.rx_bursts_datac1}/{self.rx_total_frames_datac1} DATAC3: {self.rx_bursts_datac3}/{self.rx_total_frames_datac3}", file=sys.stderr) print(
f"DATAC0: {self.rx_bursts_datac0}/{self.rx_total_frames_datac0} DATAC1: {self.rx_bursts_datac1}/{self.rx_total_frames_datac1} DATAC3: {self.rx_bursts_datac3}/{self.rx_total_frames_datac3}",
file=sys.stderr,
)
self.frx.close() self.frx.close()
# cloese pyaudio instance # cloese pyaudio instance

View file

@ -6,106 +6,130 @@ Created on Wed Dec 23 07:04:24 2020
@author: DJ2LS @author: DJ2LS
""" """
import ctypes
from ctypes import *
import pathlib
import pyaudio
import sys
import logging
import time
import threading
import sys
import argparse import argparse
import ctypes
import queue import queue
import sys
import time
import numpy as np import numpy as np
sys.path.insert(0,'..') import pyaudio
sys.path.insert(0, "..")
from tnc import codec2 from tnc import codec2
#--------------------------------------------GET PARAMETER INPUTS # --------------------------------------------GET PARAMETER INPUTS
parser = argparse.ArgumentParser(description='FreeDATA audio test') parser = argparse.ArgumentParser(description="FreeDATA audio test")
parser.add_argument('--bursts', dest="N_BURSTS", default=1, type=int) parser.add_argument("--bursts", dest="N_BURSTS", default=1, type=int)
parser.add_argument('--framesperburst', dest="N_FRAMES_PER_BURST", default=1, type=int) parser.add_argument("--framesperburst", dest="N_FRAMES_PER_BURST", default=1, type=int)
parser.add_argument('--delay', dest="DELAY_BETWEEN_BURSTS", default=500, type=int) parser.add_argument("--delay", dest="DELAY_BETWEEN_BURSTS", default=500, type=int)
parser.add_argument('--audiodev', dest="AUDIO_OUTPUT_DEVICE", default=-1, type=int, help="audio output device number to use") parser.add_argument(
parser.add_argument('--list', dest="LIST", action="store_true", help="list audio devices by number and exit") "--audiodev",
parser.add_argument('--testframes', dest="TESTFRAMES", action="store_true", default=False, help="generate testframes") dest="AUDIO_OUTPUT_DEVICE",
default=-1,
type=int,
help="audio output device number to use",
)
parser.add_argument(
"--list",
dest="LIST",
action="store_true",
help="list audio devices by number and exit",
)
parser.add_argument(
"--testframes",
dest="TESTFRAMES",
action="store_true",
default=False,
help="generate testframes",
)
args = parser.parse_args() args, _ = parser.parse_known_args()
if args.LIST: if args.LIST:
p = pyaudio.PyAudio() p = pyaudio.PyAudio()
for dev in range(0,p.get_device_count()): for dev in range(p.get_device_count()):
print("audiodev: ", dev, p.get_device_info_by_index(dev)["name"]) print("audiodev: ", dev, p.get_device_info_by_index(dev)["name"])
quit() sys.exit()
class Test:
class Test():
def __init__(self): def __init__(self):
self.dataqueue = queue.Queue() self.dataqueue = queue.Queue()
self.N_BURSTS = args.N_BURSTS self.N_BURSTS = args.N_BURSTS
self.N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST self.N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST
self.AUDIO_OUTPUT_DEVICE = args.AUDIO_OUTPUT_DEVICE self.AUDIO_OUTPUT_DEVICE = args.AUDIO_OUTPUT_DEVICE
self.DELAY_BETWEEN_BURSTS = args.DELAY_BETWEEN_BURSTS/1000 self.DELAY_BETWEEN_BURSTS = args.DELAY_BETWEEN_BURSTS / 1000
# AUDIO PARAMETERS # AUDIO PARAMETERS
self.AUDIO_FRAMES_PER_BUFFER = 2400 # <- consider increasing if you get nread_exceptions > 0 # v-- consider increasing if you get nread_exceptions > 0
self.AUDIO_FRAMES_PER_BUFFER = 2400
self.MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000 self.MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000
self.AUDIO_SAMPLE_RATE_TX = 48000 self.AUDIO_SAMPLE_RATE_TX = 48000
# make sure our resampler will work # make sure our resampler will work
assert (self.AUDIO_SAMPLE_RATE_TX / self.MODEM_SAMPLE_RATE) == codec2.api.FDMDV_OS_48 assert (
self.AUDIO_SAMPLE_RATE_TX / self.MODEM_SAMPLE_RATE
) == codec2.api.FDMDV_OS_48
self.transmit = True self.transmit = True
self.resampler = codec2.resampler() self.resampler = codec2.resampler()
# check if we want to use an audio device then do an pyaudio init # check if we want to use an audio device then do an pyaudio init
if self.AUDIO_OUTPUT_DEVICE != -1: if self.AUDIO_OUTPUT_DEVICE != -1:
self.p = pyaudio.PyAudio() self.p = pyaudio.PyAudio()
# auto search for loopback devices # auto search for loopback devices
if self.AUDIO_OUTPUT_DEVICE == -2: if self.AUDIO_OUTPUT_DEVICE == -2:
loopback_list = [] loopback_list = [
for dev in range(0,self.p.get_device_count()): dev
if 'Loopback: PCM' in self.p.get_device_info_by_index(dev)["name"]: for dev in range(self.p.get_device_count())
loopback_list.append(dev) if "Loopback: PCM" in self.p.get_device_info_by_index(dev)["name"]
]
if len(loopback_list) >= 2: if len(loopback_list) >= 2:
self.AUDIO_OUTPUT_DEVICE = loopback_list[0] #0 = RX 1 = TX self.AUDIO_OUTPUT_DEVICE = loopback_list[0] # 0 = RX 1 = TX
print(f"loopback_list rx: {loopback_list}", file=sys.stderr) print(f"loopback_list rx: {loopback_list}", file=sys.stderr)
else: else:
quit() sys.exit()
print(f"AUDIO OUTPUT DEVICE: {self.AUDIO_OUTPUT_DEVICE} DEVICE: {self.p.get_device_info_by_index(self.AUDIO_OUTPUT_DEVICE)['name']} \ print(
AUDIO SAMPLE RATE: {self.AUDIO_SAMPLE_RATE_TX}", file=sys.stderr) f"AUDIO OUTPUT DEVICE: {self.AUDIO_OUTPUT_DEVICE} "
f"DEVICE: {self.p.get_device_info_by_index(self.AUDIO_OUTPUT_DEVICE)['name']} "
f"AUDIO SAMPLE RATE: {self.AUDIO_SAMPLE_RATE_TX}",
file=sys.stderr,
)
self.stream_tx = self.p.open(format=pyaudio.paInt16, self.stream_tx = self.p.open(
format=pyaudio.paInt16,
channels=1, channels=1,
rate=self.AUDIO_SAMPLE_RATE_TX, rate=self.AUDIO_SAMPLE_RATE_TX,
frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER, frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER,
input=False, input=False,
output=True, output=True,
output_device_index=self.AUDIO_OUTPUT_DEVICE, output_device_index=self.AUDIO_OUTPUT_DEVICE,
stream_callback=self.callback stream_callback=self.callback,
) )
else:
print("test_callback_multimode_tx: Not written for STDOUT usage.")
print("Exiting.")
sys.exit()
# Copy received 48 kHz to a file. Listen to this file with: # Copy received 48 kHz to a file. Listen to this file with:
# aplay -r 48000 -f S16_LE rx48_callback.raw # aplay -r 48000 -f S16_LE rx48_callback.raw
# Corruption of this file is a good way to detect audio card issues # Corruption of this file is a good way to detect audio card issues
self.ftx = open("tx48_callback.raw", mode='wb') self.ftx = open("tx48_callback.raw", mode="wb")
# data binary string # data binary string
if args.TESTFRAMES: if args.TESTFRAMES:
self.data_out = bytearray(14) self.data_out = bytearray(14)
self.data_out[:1] = bytes([255]) self.data_out[:1] = bytes([255])
self.data_out[1:2] = bytes([1]) self.data_out[1:2] = bytes([1])
self.data_out[2:] = b'HELLO WORLD' self.data_out[2:] = b"HELLO WORLD"
else: else:
self.data_out = b'HELLO WORLD!' self.data_out = b"HELLO WORLD!"
def callback(self, data_in48k, frame_count, time_info, status): def callback(self, data_in48k, frame_count, time_info, status):
@ -133,55 +157,76 @@ class Test():
def create_modulation(self): def create_modulation(self):
modes = [codec2.api.FREEDV_MODE_DATAC0, codec2.api.FREEDV_MODE_DATAC1, codec2.api.FREEDV_MODE_DATAC3] modes = [
codec2.api.FREEDV_MODE_DATAC0,
codec2.api.FREEDV_MODE_DATAC1,
codec2.api.FREEDV_MODE_DATAC3,
]
for m in modes: for m in modes:
freedv = cast(codec2.api.freedv_open(m), c_void_p) freedv = ctypes.cast(codec2.api.freedv_open(m), ctypes.c_void_p)
n_tx_modem_samples = codec2.api.freedv_get_n_tx_modem_samples(freedv) n_tx_modem_samples = codec2.api.freedv_get_n_tx_modem_samples(freedv)
mod_out = create_string_buffer(2*n_tx_modem_samples) mod_out = ctypes.create_string_buffer(2 * n_tx_modem_samples)
n_tx_preamble_modem_samples = codec2.api.freedv_get_n_tx_preamble_modem_samples(freedv) n_tx_preamble_modem_samples = (
mod_out_preamble = create_string_buffer(2*n_tx_preamble_modem_samples) codec2.api.freedv_get_n_tx_preamble_modem_samples(freedv)
)
mod_out_preamble = ctypes.create_string_buffer(
2 * n_tx_preamble_modem_samples
)
n_tx_postamble_modem_samples = codec2.api.freedv_get_n_tx_postamble_modem_samples(freedv) n_tx_postamble_modem_samples = (
mod_out_postamble = create_string_buffer(2*n_tx_postamble_modem_samples) codec2.api.freedv_get_n_tx_postamble_modem_samples(freedv)
)
mod_out_postamble = ctypes.create_string_buffer(
2 * n_tx_postamble_modem_samples
)
bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(freedv) / 8) bytes_per_frame = int(
codec2.api.freedv_get_bits_per_modem_frame(freedv) / 8
)
payload_per_frame = bytes_per_frame - 2 payload_per_frame = bytes_per_frame - 2
buffer = bytearray(payload_per_frame) buffer = bytearray(payload_per_frame)
# set buffersize to length of data which will be send # set buffersize to length of data which will be send
buffer[:len(self.data_out)] = self.data_out buffer[: len(self.data_out)] = self.data_out
crc = ctypes.c_ushort(codec2.api.freedv_gen_crc16(bytes(buffer), payload_per_frame)) # generate CRC16 crc = ctypes.c_ushort(
codec2.api.freedv_gen_crc16(bytes(buffer), payload_per_frame)
) # generate CRC16
# convert crc to 2 byte hex string # convert crc to 2 byte hex string
crc = crc.value.to_bytes(2, byteorder='big') crc = crc.value.to_bytes(2, byteorder="big")
buffer += crc # append crc16 to buffer buffer += crc # append crc16 to buffer
data = (ctypes.c_ubyte * bytes_per_frame).from_buffer_copy(buffer) data = (ctypes.c_ubyte * bytes_per_frame).from_buffer_copy(buffer)
for i in range(1,self.N_BURSTS+1): for i in range(1, self.N_BURSTS + 1):
# write preamble to txbuffer # write preamble to txbuffer
codec2.api.freedv_rawdatapreambletx(freedv, mod_out_preamble) codec2.api.freedv_rawdatapreambletx(freedv, mod_out_preamble)
txbuffer = bytes(mod_out_preamble) txbuffer = bytes(mod_out_preamble)
# create modulaton for N = FRAMESPERBURST and append it to txbuffer # create modulaton for N = FRAMESPERBURST and append it to txbuffer
for n in range(1,self.N_FRAMES_PER_BURST+1): for n in range(1, self.N_FRAMES_PER_BURST + 1):
data = (ctypes.c_ubyte * bytes_per_frame).from_buffer_copy(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 codec2.api.freedv_rawdatatx(
freedv, mod_out, data
) # modulate DATA and save it into mod_out pointer
txbuffer += bytes(mod_out) txbuffer += bytes(mod_out)
print(f"GENERATING TX BURST: {i}/{self.N_BURSTS} FRAME: {n}/{self.N_FRAMES_PER_BURST}", file=sys.stderr) print(
f"GENERATING TX BURST: {i}/{self.N_BURSTS} FRAME: {n}/{self.N_FRAMES_PER_BURST}",
file=sys.stderr,
)
# append postamble to txbuffer # append postamble to txbuffer
codec2.api.freedv_rawdatapostambletx(freedv, mod_out_postamble) codec2.api.freedv_rawdatapostambletx(freedv, mod_out_postamble)
txbuffer += bytes(mod_out_postamble) txbuffer += bytes(mod_out_postamble)
# append a delay between bursts as audio silence # append a delay between bursts as audio silence
samples_delay = int(self.MODEM_SAMPLE_RATE*self.DELAY_BETWEEN_BURSTS) samples_delay = int(self.MODEM_SAMPLE_RATE * self.DELAY_BETWEEN_BURSTS)
mod_out_silence = create_string_buffer(samples_delay*2) mod_out_silence = ctypes.create_string_buffer(samples_delay * 2)
txbuffer += bytes(mod_out_silence) txbuffer += bytes(mod_out_silence)
# resample up to 48k (resampler works on np.int16) # resample up to 48k (resampler works on np.int16)
@ -189,20 +234,23 @@ class Test():
txbuffer_48k = self.resampler.resample8_to_48(x) txbuffer_48k = self.resampler.resample8_to_48(x)
# split modulated audio to chunks # split modulated audio to chunks
#https://newbedev.com/how-to-split-a-byte-string-into-separate-bytes-in-python # https://newbedev.com/how-to-split-a-byte-string-into-separate-bytes-in-python
txbuffer_48k = bytes(txbuffer_48k) txbuffer_48k = bytes(txbuffer_48k)
chunk = [txbuffer_48k[i:i+self.AUDIO_FRAMES_PER_BUFFER*2] for i in range(0, len(txbuffer_48k), self.AUDIO_FRAMES_PER_BUFFER*2)] chunk = [
txbuffer_48k[i : i + self.AUDIO_FRAMES_PER_BUFFER * 2]
for i in range(
0, len(txbuffer_48k), self.AUDIO_FRAMES_PER_BUFFER * 2
)
]
# add modulated chunks to fifo buffer # add modulated chunks to fifo buffer
for c in chunk: for c in chunk:
# if data is shorter than the expcected audio frames per buffer we need to append 0 # if data is shorter than the expcected audio frames per buffer we need to append 0
# to prevent the callback from stucking/crashing # to prevent the callback from stucking/crashing
if len(c) < self.AUDIO_FRAMES_PER_BUFFER*2: if len(c) < self.AUDIO_FRAMES_PER_BUFFER * 2:
c += bytes(self.AUDIO_FRAMES_PER_BUFFER*2 - len(c)) c += bytes(self.AUDIO_FRAMES_PER_BUFFER * 2 - len(c))
self.dataqueue.put(c) self.dataqueue.put(c)
test = Test() test = Test()
test.create_modulation() test.create_modulation()
test.run_audio() test.run_audio()

View file

@ -6,42 +6,56 @@ Created on Wed Dec 23 07:04:24 2020
@author: DJ2LS @author: DJ2LS
""" """
import ctypes
from ctypes import *
import pathlib
import pyaudio
import sys
import logging
import time
import threading
import sys
import argparse import argparse
import ctypes
import sys
import time
import numpy as np import numpy as np
sys.path.insert(0,'..') import pyaudio
sys.path.insert(0, "..")
from tnc import codec2 from tnc import codec2
#--------------------------------------------GET PARAMETER INPUTS # --------------------------------------------GET PARAMETER INPUTS
parser = argparse.ArgumentParser(description='FreeDATA audio test') parser = argparse.ArgumentParser(description="FreeDATA audio test")
parser.add_argument('--bursts', dest="N_BURSTS", default=1, type=int) parser.add_argument("--bursts", dest="N_BURSTS", default=1, type=int)
parser.add_argument('--framesperburst', dest="N_FRAMES_PER_BURST", default=1, type=int) parser.add_argument("--framesperburst", dest="N_FRAMES_PER_BURST", default=1, type=int)
parser.add_argument('--mode', dest="FREEDV_MODE", type=str, choices=['datac0', 'datac1', 'datac3']) parser.add_argument(
parser.add_argument('--audiodev', dest="AUDIO_INPUT_DEVICE", default=-1, type=int, "--mode", dest="FREEDV_MODE", type=str, choices=["datac0", "datac1", "datac3"]
help="audio device number to use, use -2 to automatically select a loopback device") )
parser.add_argument('--debug', dest="DEBUGGING_MODE", action="store_true") parser.add_argument(
parser.add_argument('--timeout', dest="TIMEOUT", default=10, type=int, help="Timeout (seconds) before test ends") "--audiodev",
parser.add_argument('--list', dest="LIST", action="store_true", help="list audio devices by number and exit") dest="AUDIO_INPUT_DEVICE",
default=-1,
type=int,
help="audio device number to use, use -2 to automatically select a loopback device",
)
parser.add_argument("--debug", dest="DEBUGGING_MODE", action="store_true")
parser.add_argument(
"--timeout",
dest="TIMEOUT",
default=10,
type=int,
help="Timeout (seconds) before test ends",
)
parser.add_argument(
"--list",
dest="LIST",
action="store_true",
help="list audio devices by number and exit",
)
args = parser.parse_args() args, _ = parser.parse_known_args()
if args.LIST: if args.LIST:
p = pyaudio.PyAudio() p = pyaudio.PyAudio()
for dev in range(0,p.get_device_count()): for dev in range(p.get_device_count()):
print("audiodev: ", dev, p.get_device_info_by_index(dev)["name"]) print("audiodev: ", dev, p.get_device_info_by_index(dev)["name"])
quit() sys.exit()
class Test:
class Test():
def __init__(self): def __init__(self):
self.N_BURSTS = args.N_BURSTS self.N_BURSTS = args.N_BURSTS
self.N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST self.N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST
@ -51,12 +65,16 @@ class Test():
self.TIMEOUT = args.TIMEOUT self.TIMEOUT = args.TIMEOUT
# AUDIO PARAMETERS # AUDIO PARAMETERS
self.AUDIO_FRAMES_PER_BUFFER = 2400*2 # <- consider increasing if you get nread_exceptions > 0 self.AUDIO_FRAMES_PER_BUFFER = (
2400 * 2
) # <- consider increasing if you get nread_exceptions > 0
self.MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000 self.MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000
self.AUDIO_SAMPLE_RATE_RX = 48000 self.AUDIO_SAMPLE_RATE_RX = 48000
# make sure our resampler will work # make sure our resampler will work
assert (self.AUDIO_SAMPLE_RATE_RX / self.MODEM_SAMPLE_RATE) == codec2.api.FDMDV_OS_48 assert (
self.AUDIO_SAMPLE_RATE_RX / self.MODEM_SAMPLE_RATE
) == codec2.api.FDMDV_OS_48
# check if we want to use an audio device then do an pyaudio init # check if we want to use an audio device then do an pyaudio init
if self.AUDIO_INPUT_DEVICE != -1: if self.AUDIO_INPUT_DEVICE != -1:
@ -64,37 +82,43 @@ class Test():
# auto search for loopback devices # auto search for loopback devices
if self.AUDIO_INPUT_DEVICE == -2: if self.AUDIO_INPUT_DEVICE == -2:
loopback_list = [] loopback_list = []
for dev in range(0,self.p.get_device_count()): for dev in range(self.p.get_device_count()):
if 'Loopback: PCM' in self.p.get_device_info_by_index(dev)["name"]: if "Loopback: PCM" in self.p.get_device_info_by_index(dev)["name"]:
loopback_list.append(dev) loopback_list.append(dev)
if len(loopback_list) >= 2: if len(loopback_list) >= 2:
self.AUDIO_INPUT_DEVICE = loopback_list[0] #0 = RX 1 = TX self.AUDIO_INPUT_DEVICE = loopback_list[0] # 0 = RX 1 = TX
print(f"loopback_list rx: {loopback_list}", file=sys.stderr) print(f"loopback_list rx: {loopback_list}", file=sys.stderr)
else: else:
quit() sys.exit()
print(f"AUDIO INPUT DEVICE: {self.AUDIO_INPUT_DEVICE} DEVICE: {self.p.get_device_info_by_index(self.AUDIO_INPUT_DEVICE)['name']} \ print(
AUDIO SAMPLE RATE: {self.AUDIO_SAMPLE_RATE_RX}", file=sys.stderr) f"AUDIO INPUT DEVICE: {self.AUDIO_INPUT_DEVICE} DEVICE: {self.p.get_device_info_by_index(self.AUDIO_INPUT_DEVICE)['name']} \
AUDIO SAMPLE RATE: {self.AUDIO_SAMPLE_RATE_RX}",
file=sys.stderr,
)
self.stream_rx = self.p.open(format=pyaudio.paInt16, self.stream_rx = self.p.open(
format=pyaudio.paInt16,
channels=1, channels=1,
rate=self.AUDIO_SAMPLE_RATE_RX, rate=self.AUDIO_SAMPLE_RATE_RX,
frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER, frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER,
input=True, input=True,
output=False, output=False,
input_device_index=self.AUDIO_INPUT_DEVICE, input_device_index=self.AUDIO_INPUT_DEVICE,
stream_callback=self.callback stream_callback=self.callback,
) )
# open codec2 instance # open codec2 instance
self.freedv = cast(codec2.api.freedv_open(self.MODE), c_void_p) self.freedv = ctypes.cast(codec2.api.freedv_open(self.MODE), ctypes.c_void_p)
# get number of bytes per frame for mode # get number of bytes per frame for mode
self.bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(self.freedv)/8) self.bytes_per_frame = int(
codec2.api.freedv_get_bits_per_modem_frame(self.freedv) / 8
)
self.bytes_out = create_string_buffer(self.bytes_per_frame) self.bytes_out = ctypes.create_string_buffer(self.bytes_per_frame)
codec2.api.freedv_set_frames_per_burst(self.freedv,self.N_FRAMES_PER_BURST) codec2.api.freedv_set_frames_per_burst(self.freedv, self.N_FRAMES_PER_BURST)
self.total_n_bytes = 0 self.total_n_bytes = 0
self.rx_total_frames = 0 self.rx_total_frames = 0
@ -104,13 +128,13 @@ class Test():
self.nread_exceptions = 0 self.nread_exceptions = 0
self.timeout = time.time() + self.TIMEOUT self.timeout = time.time() + self.TIMEOUT
self.receive = True self.receive = True
self.audio_buffer = codec2.audio_buffer(self.AUDIO_FRAMES_PER_BUFFER*2) self.audio_buffer = codec2.audio_buffer(self.AUDIO_FRAMES_PER_BUFFER * 2)
self.resampler = codec2.resampler() self.resampler = codec2.resampler()
# Copy received 48 kHz to a file. Listen to this file with: # Copy received 48 kHz to a file. Listen to this file with:
# aplay -r 48000 -f S16_LE rx48_callback.raw # aplay -r 48000 -f S16_LE rx48_callback.raw
# Corruption of this file is a good way to detect audio card issues # Corruption of this file is a good way to detect audio card issues
self.frx = open("rx48_callback.raw", mode='wb') self.frx = open("rx48_callback.raw", mode="wb")
def callback(self, data_in48k, frame_count, time_info, status): def callback(self, data_in48k, frame_count, time_info, status):
@ -119,13 +143,14 @@ class Test():
x = self.resampler.resample48_to_8(x) x = self.resampler.resample48_to_8(x)
self.audio_buffer.push(x) self.audio_buffer.push(x)
# when we have enough samples call FreeDV Rx # when we have enough samples call FreeDV Rx
nin = codec2.api.freedv_nin(self.freedv) nin = codec2.api.freedv_nin(self.freedv)
while self.audio_buffer.nbuffer >= nin: while self.audio_buffer.nbuffer >= nin:
# demodulate audio # demodulate audio
nbytes = codec2.api.freedv_rawdatarx(self.freedv, self.bytes_out, self.audio_buffer.buffer.ctypes) nbytes = codec2.api.freedv_rawdatarx(
self.freedv, self.bytes_out, self.audio_buffer.buffer.ctypes
)
self.audio_buffer.pop(nin) self.audio_buffer.pop(nin)
# call me on every loop! # call me on every loop!
@ -136,8 +161,11 @@ class Test():
self.rx_errors = self.rx_errors + 1 self.rx_errors = self.rx_errors + 1
if self.DEBUGGING_MODE: if self.DEBUGGING_MODE:
rx_status = codec2.api.rx_sync_flags_to_text[rx_status] rx_status = codec2.api.rx_sync_flags_to_text[rx_status]
print("nin: %5d rx_status: %4s naudio_buffer: %4d" % \ print(
(nin,rx_status,self.audio_buffer.nbuffer), file=sys.stderr) "nin: %5d rx_status: %4s naudio_buffer: %4d"
% (nin, rx_status, self.audio_buffer.nbuffer),
file=sys.stderr,
)
if nbytes: if nbytes:
self.total_n_bytes = self.total_n_bytes + nbytes self.total_n_bytes = self.total_n_bytes + nbytes
@ -162,7 +190,6 @@ class Test():
except Exception as e: except Exception as e:
print(f"pyAudio error: {e}", file=sys.stderr) print(f"pyAudio error: {e}", file=sys.stderr)
while self.receive and time.time() < self.timeout: while self.receive and time.time() < self.timeout:
time.sleep(1) time.sleep(1)
@ -170,9 +197,15 @@ class Test():
print("TIMEOUT REACHED") print("TIMEOUT REACHED")
if self.nread_exceptions: if self.nread_exceptions:
print("nread_exceptions %d - receive audio lost! Consider increasing Pyaudio frames_per_buffer..." % \ print(
self.nread_exceptions, file=sys.stderr) "nread_exceptions %d - receive audio lost! Consider increasing Pyaudio frames_per_buffer..."
print(f"RECEIVED BURSTS: {self.rx_bursts} RECEIVED FRAMES: {self.rx_total_frames} RX_ERRORS: {self.rx_errors}", file=sys.stderr) % self.nread_exceptions,
file=sys.stderr,
)
print(
f"RECEIVED BURSTS: {self.rx_bursts} RECEIVED FRAMES: {self.rx_total_frames} RX_ERRORS: {self.rx_errors}",
file=sys.stderr,
)
self.frx.close() self.frx.close()
# cloese pyaudio instance # cloese pyaudio instance

View file

@ -6,42 +6,56 @@ Created on Wed Dec 23 07:04:24 2020
@author: DJ2LS @author: DJ2LS
""" """
import ctypes
from ctypes import *
import pathlib
import pyaudio
import sys
import logging
import time
import threading
import sys
import argparse import argparse
import ctypes
import sys
import time
import numpy as np import numpy as np
sys.path.insert(0,'..') import pyaudio
sys.path.insert(0, "..")
from tnc import codec2 from tnc import codec2
#--------------------------------------------GET PARAMETER INPUTS # --------------------------------------------GET PARAMETER INPUTS
parser = argparse.ArgumentParser(description='FreeDATA audio test') parser = argparse.ArgumentParser(description="FreeDATA audio test")
parser.add_argument('--bursts', dest="N_BURSTS", default=1, type=int) parser.add_argument("--bursts", dest="N_BURSTS", default=1, type=int)
parser.add_argument('--framesperburst', dest="N_FRAMES_PER_BURST", default=1, type=int) parser.add_argument("--framesperburst", dest="N_FRAMES_PER_BURST", default=1, type=int)
parser.add_argument('--mode', dest="FREEDV_MODE", type=str, choices=['datac0', 'datac1', 'datac3']) parser.add_argument(
parser.add_argument('--audiodev', dest="AUDIO_INPUT_DEVICE", default=-1, type=int, "--mode", dest="FREEDV_MODE", type=str, choices=["datac0", "datac1", "datac3"]
help="audio device number to use, use -2 to automatically select a loopback device") )
parser.add_argument('--debug', dest="DEBUGGING_MODE", action="store_true") parser.add_argument(
parser.add_argument('--timeout', dest="TIMEOUT", default=10, type=int, help="Timeout (seconds) before test ends") "--audiodev",
parser.add_argument('--list', dest="LIST", action="store_true", help="list audio devices by number and exit") dest="AUDIO_INPUT_DEVICE",
default=-1,
type=int,
help="audio device number to use, use -2 to automatically select a loopback device",
)
parser.add_argument("--debug", dest="DEBUGGING_MODE", action="store_true")
parser.add_argument(
"--timeout",
dest="TIMEOUT",
default=10,
type=int,
help="Timeout (seconds) before test ends",
)
parser.add_argument(
"--list",
dest="LIST",
action="store_true",
help="list audio devices by number and exit",
)
args = parser.parse_args() args, _ = parser.parse_known_args()
if args.LIST: if args.LIST:
p = pyaudio.PyAudio() p = pyaudio.PyAudio()
for dev in range(0,p.get_device_count()): for dev in range(p.get_device_count()):
print("audiodev: ", dev, p.get_device_info_by_index(dev)["name"]) print("audiodev: ", dev, p.get_device_info_by_index(dev)["name"])
quit() sys.exit()
class Test:
class Test():
def __init__(self): def __init__(self):
self.N_BURSTS = args.N_BURSTS self.N_BURSTS = args.N_BURSTS
self.N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST self.N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST
@ -51,12 +65,16 @@ class Test():
self.TIMEOUT = args.TIMEOUT self.TIMEOUT = args.TIMEOUT
# AUDIO PARAMETERS # AUDIO PARAMETERS
self.AUDIO_FRAMES_PER_BUFFER = 2400*2 # <- consider increasing if you get nread_exceptions > 0 self.AUDIO_FRAMES_PER_BUFFER = (
2400 * 2
) # <- consider increasing if you get nread_exceptions > 0
self.MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000 self.MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000
self.AUDIO_SAMPLE_RATE_RX = 48000 self.AUDIO_SAMPLE_RATE_RX = 48000
# make sure our resampler will work # make sure our resampler will work
assert (self.AUDIO_SAMPLE_RATE_RX / self.MODEM_SAMPLE_RATE) == codec2.api.FDMDV_OS_48 assert (
self.AUDIO_SAMPLE_RATE_RX / self.MODEM_SAMPLE_RATE
) == codec2.api.FDMDV_OS_48
# check if we want to use an audio device then do an pyaudio init # check if we want to use an audio device then do an pyaudio init
if self.AUDIO_INPUT_DEVICE != -1: if self.AUDIO_INPUT_DEVICE != -1:
@ -64,37 +82,43 @@ class Test():
# auto search for loopback devices # auto search for loopback devices
if self.AUDIO_INPUT_DEVICE == -2: if self.AUDIO_INPUT_DEVICE == -2:
loopback_list = [] loopback_list = []
for dev in range(0,self.p.get_device_count()): for dev in range(self.p.get_device_count()):
if 'Loopback: PCM' in self.p.get_device_info_by_index(dev)["name"]: if "Loopback: PCM" in self.p.get_device_info_by_index(dev)["name"]:
loopback_list.append(dev) loopback_list.append(dev)
if len(loopback_list) >= 2: if len(loopback_list) >= 2:
self.AUDIO_INPUT_DEVICE = loopback_list[0] #0 = RX 1 = TX self.AUDIO_INPUT_DEVICE = loopback_list[0] # 0 = RX 1 = TX
print(f"loopback_list rx: {loopback_list}", file=sys.stderr) print(f"loopback_list rx: {loopback_list}", file=sys.stderr)
else: else:
quit() sys.exit()
print(f"AUDIO INPUT DEVICE: {self.AUDIO_INPUT_DEVICE} DEVICE: {self.p.get_device_info_by_index(self.AUDIO_INPUT_DEVICE)['name']} \ print(
AUDIO SAMPLE RATE: {self.AUDIO_SAMPLE_RATE_RX}", file=sys.stderr) f"AUDIO INPUT DEVICE: {self.AUDIO_INPUT_DEVICE} DEVICE: {self.p.get_device_info_by_index(self.AUDIO_INPUT_DEVICE)['name']} \
AUDIO SAMPLE RATE: {self.AUDIO_SAMPLE_RATE_RX}",
file=sys.stderr,
)
self.stream_rx = self.p.open(format=pyaudio.paInt16, self.stream_rx = self.p.open(
format=pyaudio.paInt16,
channels=1, channels=1,
rate=self.AUDIO_SAMPLE_RATE_RX, rate=self.AUDIO_SAMPLE_RATE_RX,
frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER, frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER,
input=True, input=True,
output=False, output=False,
input_device_index=self.AUDIO_INPUT_DEVICE, input_device_index=self.AUDIO_INPUT_DEVICE,
stream_callback=self.callback stream_callback=self.callback,
) )
# open codec2 instance # open codec2 instance
self.freedv = cast(codec2.api.freedv_open(self.MODE), c_void_p) self.freedv = ctypes.cast(codec2.api.freedv_open(self.MODE), ctypes.c_void_p)
# get number of bytes per frame for mode # get number of bytes per frame for mode
self.bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(self.freedv)/8) self.bytes_per_frame = int(
codec2.api.freedv_get_bits_per_modem_frame(self.freedv) / 8
)
self.bytes_out = create_string_buffer(self.bytes_per_frame * 2) self.bytes_out = ctypes.create_string_buffer(self.bytes_per_frame * 2)
codec2.api.freedv_set_frames_per_burst(self.freedv,self.N_FRAMES_PER_BURST) codec2.api.freedv_set_frames_per_burst(self.freedv, self.N_FRAMES_PER_BURST)
self.total_n_bytes = 0 self.total_n_bytes = 0
self.rx_total_frames = 0 self.rx_total_frames = 0
@ -104,13 +128,13 @@ class Test():
self.nread_exceptions = 0 self.nread_exceptions = 0
self.timeout = time.time() + self.TIMEOUT self.timeout = time.time() + self.TIMEOUT
self.receive = True self.receive = True
self.audio_buffer = codec2.audio_buffer(self.AUDIO_FRAMES_PER_BUFFER*2) self.audio_buffer = codec2.audio_buffer(self.AUDIO_FRAMES_PER_BUFFER * 2)
self.resampler = codec2.resampler() self.resampler = codec2.resampler()
# Copy received 48 kHz to a file. Listen to this file with: # Copy received 48 kHz to a file. Listen to this file with:
# aplay -r 48000 -f S16_LE rx48_callback.raw # aplay -r 48000 -f S16_LE rx48_callback.raw
# Corruption of this file is a good way to detect audio card issues # Corruption of this file is a good way to detect audio card issues
self.frx = open("rx48_callback.raw", mode='wb') self.frx = open("rx48_callback.raw", mode="wb")
def callback(self, data_in48k, frame_count, time_info, status): def callback(self, data_in48k, frame_count, time_info, status):
@ -128,15 +152,16 @@ class Test():
except Exception as e: except Exception as e:
print(f"pyAudio error: {e}", file=sys.stderr) print(f"pyAudio error: {e}", file=sys.stderr)
while self.receive and time.time() < self.timeout: while self.receive and time.time() < self.timeout:
#time.sleep(1) # time.sleep(1)
# when we have enough samples call FreeDV Rx # when we have enough samples call FreeDV Rx
nin = codec2.api.freedv_nin(self.freedv) nin = codec2.api.freedv_nin(self.freedv)
while self.audio_buffer.nbuffer >= nin: while self.audio_buffer.nbuffer >= nin:
# demodulate audio # demodulate audio
nbytes = codec2.api.freedv_rawdatarx(self.freedv, self.bytes_out, self.audio_buffer.buffer.ctypes) nbytes = codec2.api.freedv_rawdatarx(
self.freedv, self.bytes_out, self.audio_buffer.buffer.ctypes
)
self.audio_buffer.pop(nin) self.audio_buffer.pop(nin)
# call me on every loop! # call me on every loop!
@ -147,8 +172,11 @@ class Test():
self.rx_errors = self.rx_errors + 1 self.rx_errors = self.rx_errors + 1
if self.DEBUGGING_MODE: if self.DEBUGGING_MODE:
rx_status = codec2.api.rx_sync_flags_to_text[rx_status] rx_status = codec2.api.rx_sync_flags_to_text[rx_status]
print("nin: %5d rx_status: %4s naudio_buffer: %4d" % \ print(
(nin,rx_status,self.audio_buffer.nbuffer), file=sys.stderr) "nin: %5d rx_status: %4s naudio_buffer: %4d"
% (nin, rx_status, self.audio_buffer.nbuffer),
file=sys.stderr,
)
if nbytes: if nbytes:
self.total_n_bytes = self.total_n_bytes + nbytes self.total_n_bytes = self.total_n_bytes + nbytes
@ -167,9 +195,15 @@ class Test():
print("TIMEOUT REACHED") print("TIMEOUT REACHED")
if self.nread_exceptions: if self.nread_exceptions:
print("nread_exceptions %d - receive audio lost! Consider increasing Pyaudio frames_per_buffer..." % \ print(
self.nread_exceptions, file=sys.stderr) "nread_exceptions %d - receive audio lost! Consider increasing Pyaudio frames_per_buffer..."
print(f"RECEIVED BURSTS: {self.rx_bursts} RECEIVED FRAMES: {self.rx_total_frames} RX_ERRORS: {self.rx_errors}", file=sys.stderr) % self.nread_exceptions,
file=sys.stderr,
)
print(
f"RECEIVED BURSTS: {self.rx_bursts} RECEIVED FRAMES: {self.rx_total_frames} RX_ERRORS: {self.rx_errors}",
file=sys.stderr,
)
self.frx.close() self.frx.close()
# cloese pyaudio instance # cloese pyaudio instance

View file

@ -6,43 +6,58 @@ Created on Wed Dec 23 07:04:24 2020
@author: DJ2LS @author: DJ2LS
""" """
import ctypes
from ctypes import *
import pathlib
import pyaudio
import sys
import logging
import time
import threading
import sys
import argparse import argparse
import ctypes
import queue import queue
import sys
import time
import numpy as np import numpy as np
sys.path.insert(0,'..') import pyaudio
sys.path.insert(0, "..")
from tnc import codec2 from tnc import codec2
#--------------------------------------------GET PARAMETER INPUTS # --------------------------------------------GET PARAMETER INPUTS
parser = argparse.ArgumentParser(description='FreeDATA audio test') parser = argparse.ArgumentParser(description="FreeDATA audio test")
parser.add_argument('--bursts', dest="N_BURSTS", default=1, type=int) parser.add_argument("--bursts", dest="N_BURSTS", default=1, type=int)
parser.add_argument('--framesperburst', dest="N_FRAMES_PER_BURST", default=1, type=int) parser.add_argument("--framesperburst", dest="N_FRAMES_PER_BURST", default=1, type=int)
parser.add_argument('--delay', dest="DELAY_BETWEEN_BURSTS", default=500, type=int) parser.add_argument("--delay", dest="DELAY_BETWEEN_BURSTS", default=500, type=int)
parser.add_argument('--mode', dest="FREEDV_MODE", type=str, choices=['datac0', 'datac1', 'datac3']) parser.add_argument(
parser.add_argument('--audiodev', dest="AUDIO_OUTPUT_DEVICE", default=-1, type=int, "--mode", dest="FREEDV_MODE", type=str, choices=["datac0", "datac1", "datac3"]
help="audio device number to use, use -2 to automatically select a loopback device") )
parser.add_argument('--list', dest="LIST", action="store_true", help="list audio devices by number and exit") parser.add_argument(
parser.add_argument('--testframes', dest="TESTFRAMES", action="store_true", default=False, help="generate testframes") "--audiodev",
dest="AUDIO_OUTPUT_DEVICE",
default=-1,
type=int,
help="audio device number to use, use -2 to automatically select a loopback device",
)
parser.add_argument(
"--list",
dest="LIST",
action="store_true",
help="list audio devices by number and exit",
)
parser.add_argument(
"--testframes",
dest="TESTFRAMES",
action="store_true",
default=False,
help="generate testframes",
)
args = parser.parse_args() args, _ = parser.parse_known_args()
if args.LIST: if args.LIST:
p = pyaudio.PyAudio() p = pyaudio.PyAudio()
for dev in range(0,p.get_device_count()): for dev in range(p.get_device_count()):
print("audiodev: ", dev, p.get_device_info_by_index(dev)["name"]) print("audiodev: ", dev, p.get_device_info_by_index(dev)["name"])
quit() sys.exit()
class Test:
class Test():
def __init__(self): def __init__(self):
self.dataqueue = queue.Queue() self.dataqueue = queue.Queue()
@ -50,76 +65,81 @@ class Test():
self.N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST self.N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST
self.AUDIO_OUTPUT_DEVICE = args.AUDIO_OUTPUT_DEVICE self.AUDIO_OUTPUT_DEVICE = args.AUDIO_OUTPUT_DEVICE
self.MODE = codec2.FREEDV_MODE[args.FREEDV_MODE].value self.MODE = codec2.FREEDV_MODE[args.FREEDV_MODE].value
self.DELAY_BETWEEN_BURSTS = args.DELAY_BETWEEN_BURSTS/1000 self.DELAY_BETWEEN_BURSTS = args.DELAY_BETWEEN_BURSTS / 1000
# AUDIO PARAMETERS # AUDIO PARAMETERS
self.AUDIO_FRAMES_PER_BUFFER = 2400 # <- consider increasing if you get nread_exceptions > 0 # v-- consider increasing if you get nread_exceptions > 0
self.AUDIO_FRAMES_PER_BUFFER = 2400
self.MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000 self.MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000
self.AUDIO_SAMPLE_RATE_TX = 48000 self.AUDIO_SAMPLE_RATE_TX = 48000
# make sure our resampler will work # make sure our resampler will work
assert (self.AUDIO_SAMPLE_RATE_TX / self.MODEM_SAMPLE_RATE) == codec2.api.FDMDV_OS_48 assert (
self.AUDIO_SAMPLE_RATE_TX / self.MODEM_SAMPLE_RATE
) == codec2.api.FDMDV_OS_48
self.transmit = True self.transmit = True
self.resampler = codec2.resampler() self.resampler = codec2.resampler()
# check if we want to use an audio device then do an pyaudio init # check if we want to use an audio device then do an pyaudio init
if self.AUDIO_OUTPUT_DEVICE != -1: if self.AUDIO_OUTPUT_DEVICE != -1:
self.p = pyaudio.PyAudio() self.p = pyaudio.PyAudio()
# auto search for loopback devices # auto search for loopback devices
if self.AUDIO_OUTPUT_DEVICE == -2: if self.AUDIO_OUTPUT_DEVICE == -2:
loopback_list = [] loopback_list = []
for dev in range(0,self.p.get_device_count()): for dev in range( self.p.get_device_count()):
if 'Loopback: PCM' in self.p.get_device_info_by_index(dev)["name"]: if "Loopback: PCM" in self.p.get_device_info_by_index(dev)["name"]:
loopback_list.append(dev) loopback_list.append(dev)
if len(loopback_list) >= 2: if len(loopback_list) >= 2:
self.AUDIO_OUTPUT_DEVICE = loopback_list[0] #0 = RX 1 = TX self.AUDIO_OUTPUT_DEVICE = loopback_list[0] # 0 = RX 1 = TX
print(f"loopback_list rx: {loopback_list}", file=sys.stderr) print(f"loopback_list rx: {loopback_list}", file=sys.stderr)
else: else:
quit() sys.exit()
print(f"AUDIO OUTPUT DEVICE: {self.AUDIO_OUTPUT_DEVICE} DEVICE: {self.p.get_device_info_by_index(self.AUDIO_OUTPUT_DEVICE)['name']} \ print(
AUDIO SAMPLE RATE: {self.AUDIO_SAMPLE_RATE_TX}", file=sys.stderr) f"AUDIO OUTPUT DEVICE: {self.AUDIO_OUTPUT_DEVICE} DEVICE: {self.p.get_device_info_by_index(self.AUDIO_OUTPUT_DEVICE)['name']} \
AUDIO SAMPLE RATE: {self.AUDIO_SAMPLE_RATE_TX}",
file=sys.stderr,
)
self.stream_tx = self.p.open(format=pyaudio.paInt16, self.stream_tx = self.p.open(
format=pyaudio.paInt16,
channels=1, channels=1,
rate=self.AUDIO_SAMPLE_RATE_TX, rate=self.AUDIO_SAMPLE_RATE_TX,
frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER, frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER,
input=False, input=False,
output=True, output=True,
output_device_index=self.AUDIO_OUTPUT_DEVICE, output_device_index=self.AUDIO_OUTPUT_DEVICE,
stream_callback=self.callback stream_callback=self.callback,
) )
# open codec2 instance # open codec2 instance
self.freedv = cast(codec2.api.freedv_open(self.MODE), c_void_p) self.freedv = ctypes.cast(codec2.api.freedv_open(self.MODE), ctypes.c_void_p)
# get number of bytes per frame for mode # get number of bytes per frame for mode
self.bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(self.freedv)/8) self.bytes_per_frame = int(
codec2.api.freedv_get_bits_per_modem_frame(self.freedv) / 8
)
self.bytes_out = create_string_buffer(self.bytes_per_frame) self.bytes_out = ctypes.create_string_buffer(self.bytes_per_frame)
codec2.api.freedv_set_frames_per_burst(self.freedv,self.N_FRAMES_PER_BURST)
codec2.api.freedv_set_frames_per_burst(self.freedv, self.N_FRAMES_PER_BURST)
# Copy received 48 kHz to a file. Listen to this file with: # Copy received 48 kHz to a file. Listen to this file with:
# aplay -r 48000 -f S16_LE rx48_callback.raw # aplay -r 48000 -f S16_LE rx48_callback.raw
# Corruption of this file is a good way to detect audio card issues # Corruption of this file is a good way to detect audio card issues
self.ftx = open("tx48_callback.raw", mode='wb') self.ftx = open("tx48_callback.raw", mode="wb")
# data binary string # data binary string
if args.TESTFRAMES: if args.TESTFRAMES:
self.data_out = bytearray(14) self.data_out = bytearray(14)
self.data_out[:1] = bytes([255]) self.data_out[:1] = bytes([255])
self.data_out[1:2] = bytes([1]) self.data_out[1:2] = bytes([1])
self.data_out[2:] = b'HELLO WORLD' self.data_out[2:] = b"HELLO WORLD"
else: else:
self.data_out = b'HELLO WORLD!' self.data_out = b"HELLO WORLD!"
def callback(self, data_in48k, frame_count, time_info, status): def callback(self, data_in48k, frame_count, time_info, status):
@ -139,8 +159,6 @@ class Test():
sheeps = sheeps + 1 sheeps = sheeps + 1
print(f"counting sheeps...{sheeps}") print(f"counting sheeps...{sheeps}")
self.ftx.close() self.ftx.close()
# close pyaudio instance # close pyaudio instance
@ -150,80 +168,103 @@ class Test():
def create_modulation(self): def create_modulation(self):
# open codec2 instance # open codec2 instance
freedv = cast(codec2.api.freedv_open(self.MODE), c_void_p) freedv = ctypes.cast(codec2.api.freedv_open(self.MODE), ctypes.c_void_p)
# get number of bytes per frame for mode # get number of bytes per frame for mode
bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(freedv)/8) bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(freedv) / 8)
payload_bytes_per_frame = bytes_per_frame -2 payload_bytes_per_frame = bytes_per_frame - 2
# init buffer for data # init buffer for data
n_tx_modem_samples = codec2.api.freedv_get_n_tx_modem_samples(freedv) n_tx_modem_samples = codec2.api.freedv_get_n_tx_modem_samples(freedv)
mod_out = create_string_buffer(n_tx_modem_samples * 2) mod_out = ctypes.create_string_buffer(n_tx_modem_samples * 2)
# init buffer for preample # init buffer for preample
n_tx_preamble_modem_samples = codec2.api.freedv_get_n_tx_preamble_modem_samples(freedv) n_tx_preamble_modem_samples = codec2.api.freedv_get_n_tx_preamble_modem_samples(
mod_out_preamble = create_string_buffer(n_tx_preamble_modem_samples * 2) freedv
)
mod_out_preamble = ctypes.create_string_buffer(n_tx_preamble_modem_samples * 2)
# init buffer for postamble # init buffer for postamble
n_tx_postamble_modem_samples = codec2.api.freedv_get_n_tx_postamble_modem_samples(freedv) n_tx_postamble_modem_samples = (
mod_out_postamble = create_string_buffer(n_tx_postamble_modem_samples * 2) codec2.api.freedv_get_n_tx_postamble_modem_samples(freedv)
)
mod_out_postamble = ctypes.create_string_buffer(n_tx_postamble_modem_samples * 2)
# create buffer for data # create buffer for data
buffer = bytearray(payload_bytes_per_frame) # use this if CRC16 checksum is required ( DATA1-3) buffer = bytearray(
buffer[:len(self.data_out)] = self.data_out # set buffersize to length of data which will be send payload_bytes_per_frame
) # use this if CRC16 checksum is required ( DATA1-3)
buffer[
: len(self.data_out)
] = self.data_out # 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 # create crc for data frame - we are using the crc function shipped with codec2 to avoid
# crc algorithm incompatibilities # crc algorithm incompatibilities
crc = ctypes.c_ushort(codec2.api.freedv_gen_crc16(bytes(buffer), payload_bytes_per_frame)) # generate CRC16 crc = ctypes.c_ushort(
crc = crc.value.to_bytes(2, byteorder='big') # convert crc to 2 byte hex string 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 buffer += crc # append crc16 to buffer
print(f"TOTAL BURSTS: {self.N_BURSTS} TOTAL FRAMES_PER_BURST: {self.N_FRAMES_PER_BURST}", file=sys.stderr) print(
f"TOTAL BURSTS: {self.N_BURSTS} TOTAL FRAMES_PER_BURST: {self.N_FRAMES_PER_BURST}",
file=sys.stderr,
)
for i in range(1,self.N_BURSTS+1): for i in range(1, self.N_BURSTS + 1):
# write preamble to txbuffer # write preamble to txbuffer
codec2.api.freedv_rawdatapreambletx(freedv, mod_out_preamble) codec2.api.freedv_rawdatapreambletx(freedv, mod_out_preamble)
txbuffer = bytes(mod_out_preamble) txbuffer = bytes(mod_out_preamble)
# create modulaton for N = FRAMESPERBURST and append it to txbuffer # create modulaton for N = FRAMESPERBURST and append it to txbuffer
for n in range(1,self.N_FRAMES_PER_BURST+1): for n in range(1, self.N_FRAMES_PER_BURST + 1):
data = (ctypes.c_ubyte * bytes_per_frame).from_buffer_copy(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 codec2.api.freedv_rawdatatx(
freedv, mod_out, data
) # modulate DATA and save it into mod_out pointer
txbuffer += bytes(mod_out) txbuffer += bytes(mod_out)
print(f" GENERATING TX BURST: {i}/{self.N_BURSTS} FRAME: {n}/{self.N_FRAMES_PER_BURST}", file=sys.stderr) print(
f" GENERATING TX BURST: {i}/{self.N_BURSTS} FRAME: {n}/{self.N_FRAMES_PER_BURST}",
file=sys.stderr,
)
# append postamble to txbuffer # append postamble to txbuffer
codec2.api.freedv_rawdatapostambletx(freedv, mod_out_postamble) codec2.api.freedv_rawdatapostambletx(freedv, mod_out_postamble)
txbuffer += bytes(mod_out_postamble) txbuffer += bytes(mod_out_postamble)
# append a delay between bursts as audio silence # append a delay between bursts as audio silence
samples_delay = int(self.MODEM_SAMPLE_RATE*self.DELAY_BETWEEN_BURSTS) samples_delay = int(self.MODEM_SAMPLE_RATE * self.DELAY_BETWEEN_BURSTS)
mod_out_silence = create_string_buffer(samples_delay*2) mod_out_silence = ctypes.create_string_buffer(samples_delay * 2)
txbuffer += bytes(mod_out_silence) txbuffer += bytes(mod_out_silence)
print(f"samples_delay: {samples_delay} DELAY_BETWEEN_BURSTS: {self.DELAY_BETWEEN_BURSTS}", file=sys.stderr) print(
f"samples_delay: {samples_delay} DELAY_BETWEEN_BURSTS: {self.DELAY_BETWEEN_BURSTS}",
file=sys.stderr,
)
# resample up to 48k (resampler works on np.int16) # resample up to 48k (resampler works on np.int16)
x = np.frombuffer(txbuffer, dtype=np.int16) x = np.frombuffer(txbuffer, dtype=np.int16)
txbuffer_48k = self.resampler.resample8_to_48(x) txbuffer_48k = self.resampler.resample8_to_48(x)
# split modualted audio to chunks # split modualted audio to chunks
#https://newbedev.com/how-to-split-a-byte-string-into-separate-bytes-in-python # https://newbedev.com/how-to-split-a-byte-string-into-separate-bytes-in-python
txbuffer_48k = bytes(txbuffer_48k) txbuffer_48k = bytes(txbuffer_48k)
chunk = [txbuffer_48k[i:i+self.AUDIO_FRAMES_PER_BUFFER*2] for i in range(0, len(txbuffer_48k), self.AUDIO_FRAMES_PER_BUFFER*2)] chunk = [
txbuffer_48k[i : i + self.AUDIO_FRAMES_PER_BUFFER * 2]
for i in range( len(txbuffer_48k), self.AUDIO_FRAMES_PER_BUFFER * 2)
]
# add modulated chunks to fifo buffer # add modulated chunks to fifo buffer
for c in chunk: for c in chunk:
# if data is shorter than the expcected audio frames per buffer we need to append 0 # if data is shorter than the expcected audio frames per buffer we need to append 0
# to prevent the callback from stucking/crashing # to prevent the callback from stucking/crashing
if len(c) < self.AUDIO_FRAMES_PER_BUFFER*2: if len(c) < self.AUDIO_FRAMES_PER_BUFFER * 2:
c += bytes(self.AUDIO_FRAMES_PER_BUFFER*2 - len(c)) c += bytes(self.AUDIO_FRAMES_PER_BUFFER * 2 - len(c))
self.dataqueue.put(c) self.dataqueue.put(c)
test = Test() test = Test()
test.create_modulation() test.create_modulation()
test.run_audio() test.run_audio()

90
test/test_helpers.py Normal file
View file

@ -0,0 +1,90 @@
"""
Tests for the FreeDATA TNC state machine.
"""
import sys
import helpers
import pytest
import static
@pytest.mark.parametrize("callsign", ["AA1AA", "DE2DE", "E4AWQ-4"])
def test_check_callsign(callsign: str):
"""
Execute test to demonstrate how to create and verify callsign checksums.
"""
static.SSID_LIST = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
t_callsign_bytes = helpers.callsign_to_bytes(callsign)
t_callsign = helpers.bytes_to_callsign(t_callsign_bytes)
t_callsign_crc = helpers.get_crc_24(t_callsign)
dxcallsign_bytes = helpers.callsign_to_bytes("ZZ9ZZA-0")
dxcallsign = helpers.bytes_to_callsign(dxcallsign_bytes)
dxcallsign_crc = helpers.get_crc_24(dxcallsign)
assert helpers.check_callsign(t_callsign, t_callsign_crc)[0] is True
assert helpers.check_callsign(t_callsign, dxcallsign_crc)[0] is False
@pytest.mark.parametrize("callsign", ["AA1AA-2", "DE2DE-0", "E4AWQ-4"])
def test_callsign_to_bytes(callsign: str):
"""
Execute test to demonsrate symmetry when converting callsigns to and from byte arrays.
"""
static.SSID_LIST = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
t_callsign_crc = helpers.get_crc_24(bytes(callsign, "UTF-8"))
t_callsign_bytes = helpers.callsign_to_bytes(callsign)
t_callsign = helpers.bytes_to_callsign(t_callsign_bytes)
assert helpers.check_callsign(t_callsign, t_callsign_crc)[0] is True
assert helpers.check_callsign(t_callsign, t_callsign_crc)[1] == bytes(
callsign, "UTF-8"
)
@pytest.mark.parametrize("callsign", ["AA1AA-2", "DE2DE-0", "e4awq-4"])
def test_encode_callsign(callsign: str):
"""
Execute test to demonsrate symmetry when encoding and decoding callsigns.
"""
callenc = helpers.encode_call(callsign)
calldec = helpers.decode_call(callenc)
assert callsign.upper() != calldec
@pytest.mark.parametrize("gridsq", ["EM98dc", "DE01GG", "EF42sW"])
def test_encode_grid(gridsq: str):
"""
Execute test to demonsrate symmetry when encoding and decoding grid squares.
"""
gridenc = helpers.encode_grid(gridsq)
griddec = helpers.decode_grid(gridenc)
assert gridsq.upper() == griddec
@pytest.mark.parametrize("gridsq", ["SM98dc", "DE01GZ", "EV42sY"])
@pytest.mark.xfail(reason="Invalid gridsquare provided")
def test_invalid_grid(gridsq: str):
"""
Execute test to demonsrate symmetry when encoding and decoding grid squares.
"""
gridenc = helpers.encode_grid(gridsq)
griddec = helpers.decode_grid(gridenc)
assert gridsq.upper() != griddec
if __name__ == "__main__":
# Run pytest with the current script as the filename.
ecode = pytest.main(["-v", sys.argv[0]])
if ecode == 0:
print("errors: 0")
else:
print(ecode)

View file

@ -0,0 +1,123 @@
"""
Tests a high signal-to-noise ratio path with codec2 data formats using codec2 to transmit.
"""
# pylint: disable=global-statement, invalid-name, unused-import
import os
import subprocess
import sys
import pytest
try:
BURSTS = int(os.environ["BURSTS"])
FRAMESPERBURST = int(os.environ["FRAMESPERBURST"])
TESTFRAMES = int(os.environ["TESTFRAMES"])
except KeyError:
BURSTS = 1
FRAMESPERBURST = 1
TESTFRAMES = 3
@pytest.mark.parametrize("bursts", [BURSTS, 2, 3])
@pytest.mark.parametrize("frames_per_burst", [FRAMESPERBURST, 2, 3])
@pytest.mark.parametrize("testframes", [TESTFRAMES, 2, 1])
@pytest.mark.parametrize("mode", ["datac0", "datac1", "datac3"])
def test_HighSNR_P_P_DATACx(
bursts: int, frames_per_burst: int, testframes: int, mode: str
):
"""
Test a high signal-to-noise ratio path with DATAC0.
:param bursts: Number of bursts
:type bursts: str
:param frames_per_burst: Number of frames transmitted per burst
:type frames_per_burst: str
:param testframes: Number of test frames to transmit
:type testframes: str
"""
tx_side = "freedv_data_raw_tx"
# Facilitate running from main directory as well as inside test/
rx_side = "test_rx.py"
if os.path.exists("test") and os.path.exists(os.path.join("test", rx_side)):
rx_side = os.path.join("test", rx_side)
os.environ["PYTHONPATH"] += ":."
with subprocess.Popen(
args=[
tx_side,
mode,
"--testframes",
f"{testframes}",
"--bursts",
f"{bursts}",
"--framesperburst",
f"{frames_per_burst}",
"/dev/zero",
"-",
],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
) as transmit:
with subprocess.Popen(
args=[
"sox",
"-t",
".s16",
"-r",
"8000",
"-c",
"1",
"-",
"-t",
".s16",
"-r",
"48000",
"-c",
"1",
"-",
],
stdin=transmit.stdout,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
) as sox_filter:
with subprocess.Popen(
args=[
"python3",
rx_side,
"--mode",
mode,
"--framesperburst",
str(frames_per_burst),
"--bursts",
str(bursts),
],
stdin=sox_filter.stdout,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
) as receive:
assert receive.stdout
lastline = "".join(
[
str(line, "UTF-8")
for line in receive.stdout.readlines()
if "RECEIVED " in str(line, "UTF-8")
]
)
assert f"RECEIVED BURSTS: {bursts}" in lastline
assert f"RECEIVED FRAMES: {frames_per_burst * bursts}" in lastline
assert "RX_ERRORS: 0" in lastline
print(lastline)
if __name__ == "__main__":
# Run pytest with the current script as the filename.
ecode = pytest.main(["-v", "-s", sys.argv[0]])
if ecode == 0:
print("errors: 0")
else:
print(ecode)

View file

@ -0,0 +1,126 @@
"""
Tests a high signal-to-noise ratio path with codec2 data formats using codec2 to receive.
"""
# pylint: disable=global-statement, invalid-name, unused-import
import os
import subprocess
import sys
import pytest
try:
BURSTS = int(os.environ["BURSTS"])
FRAMESPERBURST = int(os.environ["FRAMESPERBURST"])
TESTFRAMES = int(os.environ["TESTFRAMES"])
except KeyError:
BURSTS = 1
FRAMESPERBURST = 1
TESTFRAMES = 3
@pytest.mark.parametrize("bursts", [BURSTS, 2, 3])
@pytest.mark.parametrize("frames_per_burst", [FRAMESPERBURST, 2, 3])
@pytest.mark.parametrize("mode", ["datac0", "datac1", "datac3"])
def test_HighSNR_P_P_DATACx(bursts: int, frames_per_burst: int, mode: str):
"""
Test a high signal-to-noise ratio path with DATAC0.
:param bursts: Number of bursts
:type bursts: str
:param frames_per_burst: Number of frames transmitted per burst
:type frames_per_burst: str
:param testframes: Number of test frames to transmit
:type testframes: str
"""
# Facilitate running from main directory as well as inside test/
tx_side = "test_tx.py"
if os.path.exists("test") and os.path.exists(os.path.join("test", tx_side)):
tx_side = os.path.join("test", tx_side)
os.environ["PYTHONPATH"] += ":."
rx_side = "freedv_data_raw_rx"
with subprocess.Popen(
args=[
"python3",
tx_side,
"--mode",
mode,
"--delay",
"500",
"--framesperburst",
f"{frames_per_burst}",
"--bursts",
f"{bursts}",
],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
) as transmit:
with subprocess.Popen(
args=[
"sox",
"-t",
".s16",
"-r",
"48000",
"-c",
"1",
"-",
"-t",
".s16",
"-r",
"8000",
"-c",
"1",
"-",
],
stdin=transmit.stdout,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
) as sox_filter:
with subprocess.Popen(
args=[
rx_side,
mode,
"-",
"-",
"--framesperburst",
str(frames_per_burst),
],
stdin=sox_filter.stdout,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
) as receive:
with subprocess.Popen(
args=[
"hexdump",
"-C",
],
stdin=receive.stdout,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
) as hexdump:
assert hexdump.stdout
lastline = "".join(
[
str(line, "UTF-8")
for line in hexdump.stdout.readlines()
if "HELLO" in str(line, "UTF-8")
]
)
assert "HELLO WORLD!" in lastline
print(lastline)
if __name__ == "__main__":
# Run pytest with the current script as the filename.
ecode = pytest.main(["-v", "-s", sys.argv[0]])
if ecode == 0:
print("errors: 0")
else:
print(ecode)

View file

@ -0,0 +1,97 @@
"""
Tests a high signal-to-noise ratio path with codec2 data formats.
"""
# pylint: disable=global-statement, invalid-name, unused-import
import os
import subprocess
import sys
import pytest
try:
BURSTS = int(os.environ["BURSTS"])
FRAMESPERBURST = int(os.environ["FRAMESPERBURST"])
TESTFRAMES = int(os.environ["TESTFRAMES"])
except KeyError:
BURSTS = 1
FRAMESPERBURST = 1
TESTFRAMES = 3
@pytest.mark.parametrize("bursts", [BURSTS, 2, 3])
@pytest.mark.parametrize("frames_per_burst", [FRAMESPERBURST, 2, 3])
@pytest.mark.parametrize("mode", ["datac0", "datac1", "datac3"])
def test_HighSNR_P_P_DATACx(bursts: int, frames_per_burst: int, mode: str):
"""
Test a high signal-to-noise ratio path with Codec2 modes.
:param bursts: Number of bursts
:type bursts: str
:param frames_per_burst: Number of frames transmitted per burst
:type frames_per_burst: str
"""
# Facilitate running from main directory as well as inside test/
tx_side = "test_tx.py"
rx_side = "test_rx.py"
if os.path.exists("test") and os.path.exists(os.path.join("test", tx_side)):
tx_side = os.path.join("test", tx_side)
rx_side = os.path.join("test", rx_side)
os.environ["PYTHONPATH"] += ":."
with subprocess.Popen(
args=[
"python3",
tx_side,
"--mode",
mode,
"--delay",
"500",
"--framesperburst",
str(frames_per_burst),
"--bursts",
str(bursts),
],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
) as transmit:
with subprocess.Popen(
args=[
"python3",
rx_side,
"--mode",
mode,
"--framesperburst",
str(frames_per_burst),
"--bursts",
str(bursts),
"--timeout",
"20",
],
stdin=transmit.stdout,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
) as receive:
assert receive.stdout
lastline = "".join(
[
str(line, "UTF-8")
for line in receive.stdout.readlines()
if "RECEIVED " in str(line, "UTF-8")
]
)
assert f"RECEIVED BURSTS: {bursts}" in lastline
assert f"RECEIVED FRAMES: {frames_per_burst * bursts}" in lastline
assert "RX_ERRORS: 0" in lastline
print(lastline)
if __name__ == "__main__":
# Run pytest with the current script as the filename.
ecode = pytest.main(["-v", "-s", sys.argv[0]])
if ecode == 0:
print("errors: 0")
else:
print(ecode)

View file

@ -0,0 +1,92 @@
"""
Tests a high signal-to-noise ratio path with multiple codec2 data formats.
"""
# pylint: disable=global-statement, invalid-name, unused-import
import os
import subprocess
import sys
import pytest
try:
BURSTS = int(os.environ["BURSTS"])
FRAMESPERBURST = int(os.environ["FRAMESPERBURST"])
TESTFRAMES = int(os.environ["TESTFRAMES"])
except KeyError:
BURSTS = 1
FRAMESPERBURST = 1
TESTFRAMES = 3
@pytest.mark.parametrize("bursts", [BURSTS, 2, 3])
@pytest.mark.parametrize("frames_per_burst", [FRAMESPERBURST, 2, 3])
def test_HighSNR_P_P_Multi(bursts: int, frames_per_burst: int):
"""
Test a high signal-to-noise ratio path with DATAC0, DATAC1 and DATAC3.
:param bursts: Number of bursts
:type bursts: int
:param frames_per_burst: Number of frames transmitted per burst
:type frames_per_burst: int
"""
# Facilitate running from main directory as well as inside test/
tx_side = "test_multimode_tx.py"
rx_side = "test_multimode_rx.py"
if os.path.exists("test") and os.path.exists(os.path.join("test", tx_side)):
tx_side = os.path.join("test", tx_side)
rx_side = os.path.join("test", rx_side)
os.environ["PYTHONPATH"] += ":."
with subprocess.Popen(
args=[
"python3",
tx_side,
"--delay",
"500",
"--framesperburst",
str(frames_per_burst),
"--bursts",
str(bursts),
],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
) as transmit:
with subprocess.Popen(
args=[
"python3",
rx_side,
"--framesperburst",
str(frames_per_burst),
"--bursts",
str(bursts),
"--timeout",
"20",
],
stdin=transmit.stdout,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
) as receive:
assert receive.stdout
lastline = "".join(
[
str(line, "UTF-8")
for line in receive.stdout.readlines()
if "DATAC" in str(line, "UTF-8")
]
)
assert f"DATAC0: {bursts}/{frames_per_burst * bursts}" in lastline
assert f"DATAC1: {bursts}/{frames_per_burst * bursts}" in lastline
assert f"DATAC3: {bursts}/{frames_per_burst * bursts}" in lastline
print(lastline)
if __name__ == "__main__":
# Run pytest with the current script as the filename.
ecode = pytest.main(["-v", "-s", sys.argv[0]])
if ecode == 0:
print("errors: 0")
else:
print(ecode)

160
test/test_modem.py Normal file
View file

@ -0,0 +1,160 @@
"""
Tests for the FreeDATA modem.
"""
import multiprocessing
import sys
import time
import pytest
sys.path.insert(0, "..")
sys.path.insert(0, "../tnc")
import helpers
import modem
import static
def print_frame(data: bytearray):
"""
Pretty-print the provided frame.
:param data: Frame to be output
:type data: bytearray
"""
print(f"Type : {int(data[0])}")
print(f"DXCRC : {bytes(data[1:4])}")
print(f"CallCRC: {bytes(data[4:7])}")
print(f"Call : {helpers.bytes_to_callsign(data[7:13])}")
def t_create_frame(frame_type: int, mycall: str, dxcall: str) -> bytearray:
"""
Generate the requested frame.
:param frame_type: The numerical type of the desired frame.
:type frame_type: int
:param mycall: Callsign of the near station
:type mycall: str
:param dxcall: Callsign of the far station
:type dxcall: str
:return: Bytearray of the requested frame
:rtype: bytearray
"""
mycallsign_bytes = helpers.callsign_to_bytes(mycall)
mycallsign = helpers.bytes_to_callsign(mycallsign_bytes)
mycallsign_crc = helpers.get_crc_24(mycallsign)
dxcallsign_bytes = helpers.callsign_to_bytes(dxcall)
dxcallsign = helpers.bytes_to_callsign(dxcallsign_bytes)
dxcallsign_crc = helpers.get_crc_24(dxcallsign)
frame = bytearray(14)
frame[:1] = bytes([frame_type])
frame[1:4] = dxcallsign_crc
frame[4:7] = mycallsign_crc
frame[7:13] = mycallsign_bytes
return frame
def t_create_session_close(mycall: str, dxcall: str) -> bytearray:
"""
Generate the session_close frame.
:param mycall: Callsign of the near station
:type mycall: str
:param dxcall: Callsign of the far station
:type dxcall: str
:return: Bytearray of the requested frame
:rtype: bytearray
"""
return t_create_frame(223, mycall, dxcall)
def t_create_start_session(mycall: str, dxcall: str) -> bytearray:
"""
Generate the create_session frame.
:param mycall: Callsign of the near station
:type mycall: str
:param dxcall: Callsign of the far station
:type dxcall: str
:return: Bytearray of the requested frame
:rtype: bytearray
"""
return t_create_frame(221, mycall, dxcall)
def t_tsh_dummy():
"""Replacement function for transmit"""
print("In t_tsh_dummy")
def t_modem():
"""
Execute test to validate that receiving a session open frame sets the correct machine
state.
"""
t_mode = t_repeats = t_repeat_delay = 0
t_frames = []
# enable testmode
modem.TESTMODE = True
modem.RXCHANNEL = "/tmp/hfchannel1"
modem.TXCHANNEL = "/tmp/hfchannel2"
static.HAMLIB_RADIOCONTROL = "disabled"
def t_tx_dummy(mode, repeats, repeat_delay, frames):
"""Replacement function for transmit"""
print(f"t_tx_dummy: In transmit({mode}, {repeats}, {repeat_delay}, {frames})")
nonlocal t_mode, t_repeats, t_repeat_delay, t_frames
t_mode = mode
t_repeats = repeats
t_repeat_delay = repeat_delay
t_frames = frames[:]
static.TRANSMITTING = False
# Create the modem
local_modem = modem.RF()
# Replace transmit routine with our own, an effective No-Op.
local_modem.transmit = t_tx_dummy
txbuffer = t_create_start_session("AA9AA", "DC2EJ")
# Start the transmission
static.TRANSMITTING = True
modem.MODEM_TRANSMIT_QUEUE.put([14, 5, 250, txbuffer])
while static.TRANSMITTING:
time.sleep(0.1)
# Check that the contents were transferred correctly.
assert t_mode == 14
assert t_repeats == 5
assert t_repeat_delay == 250
assert t_frames == txbuffer
def test_modem_queue():
proc = multiprocessing.Process(target=t_modem, args=())
# print("Starting threads.")
proc.start()
time.sleep(0.5)
# print("Terminating threads.")
proc.terminate()
proc.join()
# print(f"\n{proc.exitcode=}")
assert proc.exitcode == 0
if __name__ == "__main__":
# Run pytest with the current script as the filename.
ecode = pytest.main(["-v", "-s", sys.argv[0]])
if ecode == 0:
print("errors: 0")
else:
print(ecode)

View file

@ -1,236 +1,242 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import pyaudio
import audioop
import time
import argparse import argparse
import sys
import ctypes import ctypes
from ctypes import * import sys
import pathlib import time
sys.path.insert(0,'..') from typing import List
from tnc import codec2
import numpy as np import numpy as np
import pyaudio
#--------------------------------------------GET PARAMETER INPUTS sys.path.insert(0, "..")
parser = argparse.ArgumentParser(description='Simons TEST TNC') from tnc import codec2
parser.add_argument('--bursts', dest="N_BURSTS", default=1, type=int)
parser.add_argument('--framesperburst', dest="N_FRAMES_PER_BURST", default=1, type=int)
parser.add_argument('--audiodev', dest="AUDIO_INPUT_DEVICE", default=-1, type=int, help="audio device number to use")
parser.add_argument('--debug', dest="DEBUGGING_MODE", action="store_true")
parser.add_argument('--list', dest="LIST", action="store_true", help="list audio devices by number and exit")
parser.add_argument('--timeout', dest="TIMEOUT", default=10, type=int, help="Timeout (seconds) before test ends")
args = parser.parse_args()
if args.LIST: def test_mm_rx():
p = pyaudio.PyAudio() # AUDIO PARAMETERS
for dev in range(0,p.get_device_count()): AUDIO_FRAMES_PER_BUFFER = 2400 * 2
print("audiodev: ", dev, p.get_device_info_by_index(dev)["name"]) MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000
quit() AUDIO_SAMPLE_RATE_RX = 48000
# make sure our resampler will work
assert (AUDIO_SAMPLE_RATE_RX / MODEM_SAMPLE_RATE) == codec2.api.FDMDV_OS_48
N_BURSTS = args.N_BURSTS # SET COUNTERS
N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST rx_bursts_datac = [0, 0, 0]
AUDIO_INPUT_DEVICE = args.AUDIO_INPUT_DEVICE rx_frames_datac = [0, 0, 0]
DEBUGGING_MODE = args.DEBUGGING_MODE rx_total_frames_datac = [0, 0, 0]
TIMEOUT = args.TIMEOUT
# AUDIO PARAMETERS # time meassurement
AUDIO_FRAMES_PER_BUFFER = 2400*2 time_end_datac = [0.0, 0.0, 0.0]
MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000 time_needed_datac = [0.0, 0.0, 0.0]
AUDIO_SAMPLE_RATE_RX = 48000 time_start_datac = [0.0, 0.0, 0.0]
# make sure our resampler will work
assert (AUDIO_SAMPLE_RATE_RX / MODEM_SAMPLE_RATE) == codec2.api.FDMDV_OS_48
# SET COUNTERS datac_buffer: List[codec2.audio_buffer] = []
rx_total_frames_datac0 = 0 datac_bytes_out: List[ctypes.Array] = []
rx_frames_datac0 = 0 datac_bytes_per_frame = []
rx_bursts_datac0 = 0 datac_freedv: List[ctypes.c_void_p] = []
rx_total_frames_datac1 = 0 args = parse_arguments()
rx_frames_datac1 = 0
rx_bursts_datac1 = 0
rx_total_frames_datac3 = 0 if args.LIST:
rx_frames_datac3 = 0 p_audio = pyaudio.PyAudio()
rx_bursts_datac3 = 0 for dev in range(p_audio.get_device_count()):
print("audiodev: ", dev, p_audio.get_device_info_by_index(dev)["name"])
sys.exit()
# time meassurement N_BURSTS = args.N_BURSTS
time_start_datac0 = 0 N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST
time_end_datac0 = 0 AUDIO_INPUT_DEVICE = args.AUDIO_INPUT_DEVICE
time_start_datac1 = 0 DEBUGGING_MODE = args.DEBUGGING_MODE
time_end_datac1 = 0 MAX_TIME = args.TIMEOUT
time_start_datac3 = 0
time_end_datac3 = 0
time_needed_datac0 = 0
time_needed_datac1 = 0
time_needed_datac3 = 0
# open codec2 instance # open codec2 instances
datac0_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC0), c_void_p) for idx in range(3):
datac0_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(datac0_freedv)/8) datac_freedv.append(
datac0_bytes_out = create_string_buffer(datac0_bytes_per_frame) ctypes.cast(
codec2.api.freedv_set_frames_per_burst(datac0_freedv,N_FRAMES_PER_BURST) codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC0), ctypes.c_void_p
datac0_buffer = codec2.audio_buffer(2*AUDIO_FRAMES_PER_BUFFER) )
)
datac_bytes_per_frame.append(
int(codec2.api.freedv_get_bits_per_modem_frame(datac_freedv[idx]) / 8)
)
datac_bytes_out.append(ctypes.create_string_buffer(datac_bytes_per_frame[idx]))
codec2.api.freedv_set_frames_per_burst(datac_freedv[idx], N_FRAMES_PER_BURST)
datac_buffer.append(codec2.audio_buffer(2 * AUDIO_FRAMES_PER_BUFFER))
datac1_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC1), c_void_p) resampler = codec2.resampler()
datac1_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(datac1_freedv)/8)
datac1_bytes_out = create_string_buffer(datac1_bytes_per_frame)
codec2.api.freedv_set_frames_per_burst(datac1_freedv,N_FRAMES_PER_BURST)
datac1_buffer = codec2.audio_buffer(2*AUDIO_FRAMES_PER_BUFFER)
datac3_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC3), c_void_p) # check if we want to use an audio device then do an pyaudio init
datac3_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(datac3_freedv)/8) if AUDIO_INPUT_DEVICE != -1:
datac3_bytes_out = create_string_buffer(datac3_bytes_per_frame) p_audio = pyaudio.PyAudio()
codec2.api.freedv_set_frames_per_burst(datac3_freedv,N_FRAMES_PER_BURST)
datac3_buffer = codec2.audio_buffer(2*AUDIO_FRAMES_PER_BUFFER)
resampler = codec2.resampler()
# check if we want to use an audio device then do an pyaudio init
if AUDIO_INPUT_DEVICE != -1:
p = pyaudio.PyAudio()
# auto search for loopback devices # auto search for loopback devices
if AUDIO_INPUT_DEVICE == -2: if AUDIO_INPUT_DEVICE == -2:
loopback_list = [] loopback_list = [
for dev in range(0,p.get_device_count()): dev
if 'Loopback: PCM' in p.get_device_info_by_index(dev)["name"]: for dev in range(p_audio.get_device_count())
loopback_list.append(dev) if "Loopback: PCM" in p_audio.get_device_info_by_index(dev)["name"]
]
if len(loopback_list) >= 2: if len(loopback_list) >= 2:
AUDIO_INPUT_DEVICE = loopback_list[0] #0 = RX 1 = TX AUDIO_INPUT_DEVICE = loopback_list[0] # 0 = RX 1 = TX
print(f"loopback_list rx: {loopback_list}", file=sys.stderr) print(f"loopback_list rx: {loopback_list}", file=sys.stderr)
else: else:
quit() sys.exit()
print(f"AUDIO INPUT DEVICE: {AUDIO_INPUT_DEVICE} DEVICE: {p.get_device_info_by_index(AUDIO_INPUT_DEVICE)['name']} AUDIO SAMPLE RATE: {AUDIO_SAMPLE_RATE_RX}", file=sys.stderr) print(
stream_rx = p.open(format=pyaudio.paInt16, f"AUDIO INPUT DEVICE: {AUDIO_INPUT_DEVICE} "
f"DEVICE: {p_audio.get_device_info_by_index(AUDIO_INPUT_DEVICE)['name']} "
f"AUDIO SAMPLE RATE: {AUDIO_SAMPLE_RATE_RX}",
file=sys.stderr,
)
stream_rx = p_audio.open(
format=pyaudio.paInt16,
channels=1, channels=1,
rate=AUDIO_SAMPLE_RATE_RX, rate=AUDIO_SAMPLE_RATE_RX,
frames_per_buffer=AUDIO_FRAMES_PER_BUFFER, frames_per_buffer=AUDIO_FRAMES_PER_BUFFER,
input=True, input=True,
input_device_index=AUDIO_INPUT_DEVICE input_device_index=AUDIO_INPUT_DEVICE,
) )
timeout = time.time() + MAX_TIME
print(time.time(), MAX_TIME, timeout)
receive = True
nread_exceptions = 0
timeout = time.time() + TIMEOUT # initial nin values
print(time.time(),TIMEOUT, timeout) datac_nin = [0, 0, 0]
receive = True for idx in range(3):
nread_exceptions = 0 datac_nin[idx] = codec2.api.freedv_nin(datac_freedv[idx])
# initial nin values def print_stats(time_datac0, time_datac1, time_datac3):
datac0_nin = codec2.api.freedv_nin(datac0_freedv) if not DEBUGGING_MODE:
datac1_nin = codec2.api.freedv_nin(datac1_freedv) return
datac3_nin = codec2.api.freedv_nin(datac3_freedv)
def print_stats(time_needed_datac0, time_needed_datac1, time_needed_datac3): time_datac = [time_datac0, time_datac1, time_datac3]
if DEBUGGING_MODE: datac_rxstatus = ["", "", ""]
datac0_rxstatus = codec2.api.freedv_get_rx_status(datac0_freedv) for idx in range(3):
datac0_rxstatus = codec2.api.rx_sync_flags_to_text[datac0_rxstatus] datac_rxstatus[idx] = codec2.api.rx_sync_flags_to_text[
codec2.api.freedv_get_rx_status(datac_freedv[idx])
]
datac1_rxstatus = codec2.api.freedv_get_rx_status(datac1_freedv) text_out = ""
datac1_rxstatus = codec2.api.rx_sync_flags_to_text[datac1_rxstatus] for idx in range(3):
text_out += f"NIN{idx}: {datac_nin[idx]:5d} "
text_out += f"RX_STATUS{idx}: {datac_rxstatus[idx]:4s} "
text_out += f"TIME: {round(time_datac[idx], 4):.4f} | "
text_out = text_out.rstrip(" ").rstrip("|").rstrip(" ")
print(text_out, file=sys.stderr)
datac3_rxstatus = codec2.api.freedv_get_rx_status(datac3_freedv) while receive and time.time() < timeout:
datac3_rxstatus = codec2.api.rx_sync_flags_to_text[datac3_rxstatus]
print("NIN0: %5d RX_STATUS0: %4s TIME: %4s | NIN1: %5d RX_STATUS1: %4s TIME: %4s | NIN3: %5d RX_STATUS3: %4s TIME: %4s" % \
(datac0_nin, datac0_rxstatus, round(time_needed_datac0, 4), datac1_nin, datac1_rxstatus, round(time_needed_datac1, 4) ,datac3_nin, datac3_rxstatus, round(time_needed_datac3, 4)),
file=sys.stderr)
while receive and time.time() < timeout:
if AUDIO_INPUT_DEVICE != -1: if AUDIO_INPUT_DEVICE != -1:
try: try:
data_in48k = stream_rx.read(AUDIO_FRAMES_PER_BUFFER, exception_on_overflow = True) data_in48k = stream_rx.read(
AUDIO_FRAMES_PER_BUFFER, exception_on_overflow=True
)
except OSError as err: except OSError as err:
print(err, file=sys.stderr) print(err, file=sys.stderr)
if str(err).find("Input overflowed") != -1: if "Input overflowed" in str(err):
nread_exceptions += 1 nread_exceptions += 1
if str(err).find("Stream closed") != -1: if "Stream closed" in str(err):
print("Ending....") print("Ending....")
receive = False receive = False
else: else:
data_in48k = sys.stdin.buffer.read(AUDIO_FRAMES_PER_BUFFER*2) data_in48k = sys.stdin.buffer.read(AUDIO_FRAMES_PER_BUFFER * 2)
# insert samples in buffer # insert samples in buffer
x = np.frombuffer(data_in48k, dtype=np.int16) audio_buffer = np.frombuffer(data_in48k, dtype=np.int16)
if len(x) != AUDIO_FRAMES_PER_BUFFER: if len(audio_buffer) != AUDIO_FRAMES_PER_BUFFER:
print("len(x)",len(x)) print("len(x)", len(audio_buffer))
receive = False receive = False
x = resampler.resample48_to_8(x) audio_buffer = resampler.resample48_to_8(audio_buffer)
datac0_buffer.push(x) for idx in range(3):
datac1_buffer.push(x) datac_buffer[idx].push(audio_buffer)
datac3_buffer.push(x) while datac_buffer[idx].nbuffer >= datac_nin[idx]:
print_something = False
while datac0_buffer.nbuffer >= datac0_nin:
# demodulate audio # demodulate audio
time_start_datac0 = time.time() time_start_datac[idx] = time.time()
nbytes = codec2.api.freedv_rawdatarx(datac0_freedv, datac0_bytes_out, datac0_buffer.buffer.ctypes) nbytes = codec2.api.freedv_rawdatarx(
time_end_datac0 = time.time() datac_freedv[idx],
datac0_buffer.pop(datac0_nin) datac_bytes_out[idx],
datac0_nin = codec2.api.freedv_nin(datac0_freedv) datac_buffer[idx].buffer.ctypes,
if nbytes == datac0_bytes_per_frame: )
rx_total_frames_datac0 = rx_total_frames_datac0 + 1 time_end_datac[idx] = time.time()
rx_frames_datac0 = rx_frames_datac0 + 1 datac_buffer[idx].pop(datac_nin[idx])
datac_nin[idx] = codec2.api.freedv_nin(datac_freedv[idx])
if nbytes == datac_bytes_per_frame[idx]:
rx_total_frames_datac[idx] += 1
rx_frames_datac[idx] += 1
if rx_frames_datac0 == N_FRAMES_PER_BURST: if rx_frames_datac[idx] == N_FRAMES_PER_BURST:
rx_frames_datac0 = 0 rx_frames_datac[idx] = 0
rx_bursts_datac0 = rx_bursts_datac0 + 1 rx_bursts_datac[idx] += 1
time_needed_datac0 = time_end_datac0 - time_start_datac0 time_needed_datac[idx] = time_end_datac[idx] - time_start_datac[idx]
print_stats(time_needed_datac0, time_needed_datac1, time_needed_datac3) print_stats(
time_needed_datac[0], time_needed_datac[1], time_needed_datac[2]
)
while datac1_buffer.nbuffer >= datac1_nin: if (
# demodulate audio rx_bursts_datac[0] == N_BURSTS
time_start_datac1 = time.time() and rx_bursts_datac[1] == N_BURSTS
nbytes = codec2.api.freedv_rawdatarx(datac1_freedv, datac1_bytes_out, datac1_buffer.buffer.ctypes) and rx_bursts_datac[2] == N_BURSTS
time_end_datac1 = time.time() ):
datac1_buffer.pop(datac1_nin)
datac1_nin = codec2.api.freedv_nin(datac1_freedv)
if nbytes == datac1_bytes_per_frame:
rx_total_frames_datac1 = rx_total_frames_datac1 + 1
rx_frames_datac1 = rx_frames_datac1 + 1
if rx_frames_datac1 == N_FRAMES_PER_BURST:
rx_frames_datac1 = 0
rx_bursts_datac1 = rx_bursts_datac1 + 1
time_needed_datac1 = time_end_datac1 - time_start_datac1
print_stats(time_needed_datac0, time_needed_datac1, time_needed_datac3)
while datac3_buffer.nbuffer >= datac3_nin:
# demodulate audio
time_start_datac3 = time.time()
nbytes = codec2.api.freedv_rawdatarx(datac3_freedv, datac3_bytes_out, datac3_buffer.buffer.ctypes)
time_end_datac3 = time.time()
datac3_buffer.pop(datac3_nin)
datac3_nin = codec2.api.freedv_nin(datac3_freedv)
if nbytes == datac3_bytes_per_frame:
rx_total_frames_datac3 = rx_total_frames_datac3 + 1
rx_frames_datac3 = rx_frames_datac3 + 1
if rx_frames_datac3 == N_FRAMES_PER_BURST:
rx_frames_datac3 = 0
rx_bursts_datac3 = rx_bursts_datac3 + 1
time_needed_datac3 = time_end_datac3 - time_start_datac3
print_stats(time_needed_datac0, time_needed_datac1, time_needed_datac3)
if rx_bursts_datac0 == N_BURSTS and rx_bursts_datac1 == N_BURSTS and rx_bursts_datac3 == N_BURSTS:
receive = False receive = False
if nread_exceptions: if nread_exceptions:
print("nread_exceptions %d - receive audio lost! Consider increasing Pyaudio frames_per_buffer..." % \ print(
nread_exceptions, file=sys.stderr) f"nread_exceptions {nread_exceptions:d} - receive audio lost! "
# INFO IF WE REACHED TIMEOUT "Consider increasing Pyaudio frames_per_buffer...",
if time.time() > timeout: file=sys.stderr,
print(f"TIMEOUT REACHED", file=sys.stderr) )
# INFO IF WE REACHED TIMEOUT
if time.time() > timeout:
print("TIMEOUT REACHED", file=sys.stderr)
print(
f"DATAC0: {rx_bursts_datac[0]}/{rx_total_frames_datac[0]} "
f"DATAC1: {rx_bursts_datac[1]}/{rx_total_frames_datac[1]} "
f"DATAC3: {rx_bursts_datac[2]}/{rx_total_frames_datac[2]}",
file=sys.stderr,
)
if AUDIO_INPUT_DEVICE != -1:
print(f"DATAC0: {rx_bursts_datac0}/{rx_total_frames_datac0} DATAC1: {rx_bursts_datac1}/{rx_total_frames_datac1} DATAC3: {rx_bursts_datac3}/{rx_total_frames_datac3}", file=sys.stderr)
if AUDIO_INPUT_DEVICE != -1:
stream_rx.close() stream_rx.close()
p.terminate() p_audio.terminate()
def parse_arguments():
# --------------------------------------------GET PARAMETER INPUTS
parser = argparse.ArgumentParser(description="Simons TEST TNC")
parser.add_argument("--bursts", dest="N_BURSTS", default=1, type=int)
parser.add_argument(
"--framesperburst", dest="N_FRAMES_PER_BURST", default=1, type=int
)
parser.add_argument(
"--audiodev",
dest="AUDIO_INPUT_DEVICE",
default=-1,
type=int,
help="audio device number to use",
)
parser.add_argument("--debug", dest="DEBUGGING_MODE", action="store_true")
parser.add_argument(
"--list",
dest="LIST",
action="store_true",
help="list audio devices by number and exit",
)
parser.add_argument(
"--timeout",
dest="TIMEOUT",
default=10,
type=int,
help="Timeout (seconds) before test ends",
)
args, _ = parser.parse_known_args()
return args
if __name__ == "__main__":
test_mm_rx()

View file

@ -2,150 +2,187 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import ctypes
from ctypes import *
import pathlib
import pyaudio
import time
import threading
import audioop
import argparse import argparse
import ctypes
import sys import sys
sys.path.insert(0,'..') import time
from tnc import codec2
import numpy as np import numpy as np
import pyaudio
# GET PARAMETER INPUTS sys.path.insert(0, "..")
parser = argparse.ArgumentParser(description='FreeDATA TEST') from tnc import codec2
parser.add_argument('--bursts', dest="N_BURSTS", default=1, type=int)
parser.add_argument('--framesperburst', dest="N_FRAMES_PER_BURST", default=1, type=int)
parser.add_argument('--delay', dest="DELAY_BETWEEN_BURSTS", default=500, type=int)
parser.add_argument('--audiodev', dest="AUDIO_OUTPUT_DEVICE", default=-1, type=int, help="audio output device number to use")
parser.add_argument('--list', dest="LIST", action="store_true", help="list audio devices by number and exit")
args = parser.parse_args()
if args.LIST:
p = pyaudio.PyAudio()
for dev in range(0,p.get_device_count()):
print("audiodev: ", dev, p.get_device_info_by_index(dev)["name"])
quit()
N_BURSTS = args.N_BURSTS
N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST
DELAY_BETWEEN_BURSTS = args.DELAY_BETWEEN_BURSTS/1000
AUDIO_OUTPUT_DEVICE = args.AUDIO_OUTPUT_DEVICE
# AUDIO PARAMETERS def test_mm_tx():
AUDIO_FRAMES_PER_BUFFER = 2400 # AUDIO PARAMETERS
MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000 AUDIO_FRAMES_PER_BUFFER = 2400
AUDIO_SAMPLE_RATE_TX = 48000 MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000
assert (AUDIO_SAMPLE_RATE_TX % MODEM_SAMPLE_RATE) == 0 AUDIO_SAMPLE_RATE_TX = 48000
assert (AUDIO_SAMPLE_RATE_TX % MODEM_SAMPLE_RATE) == 0
if AUDIO_OUTPUT_DEVICE != -1: args = parse_arguments()
p = pyaudio.PyAudio()
# auto search for loopback devices if args.LIST:
p_audio = pyaudio.PyAudio()
for dev in range(p_audio.get_device_count()):
print("audiodev: ", dev, p_audio.get_device_info_by_index(dev)["name"])
sys.exit()
N_BURSTS = args.N_BURSTS
N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST
DELAY_BETWEEN_BURSTS = args.DELAY_BETWEEN_BURSTS / 1000
AUDIO_OUTPUT_DEVICE = args.AUDIO_OUTPUT_DEVICE
resampler = codec2.resampler()
# Data binary string
data_out = b"HELLO WORLD!"
modes = [
codec2.api.FREEDV_MODE_DATAC0,
codec2.api.FREEDV_MODE_DATAC1,
codec2.api.FREEDV_MODE_DATAC3,
]
if AUDIO_OUTPUT_DEVICE != -1:
p_audio = pyaudio.PyAudio()
# Auto search for loopback devices
if AUDIO_OUTPUT_DEVICE == -2: if AUDIO_OUTPUT_DEVICE == -2:
loopback_list = [] loopback_list = [
for dev in range(0,p.get_device_count()): dev
if 'Loopback: PCM' in p.get_device_info_by_index(dev)["name"]: for dev in range(p_audio.get_device_count())
loopback_list.append(dev) if "Loopback: PCM" in p_audio.get_device_info_by_index(dev)["name"]
]
if len(loopback_list) >= 2: if len(loopback_list) >= 2:
AUDIO_OUTPUT_DEVICE = loopback_list[1] #0 = RX 1 = TX AUDIO_OUTPUT_DEVICE = loopback_list[1] # 0 = RX 1 = TX
print(f"loopback_list tx: {loopback_list}", file=sys.stderr) print(f"loopback_list tx: {loopback_list}", file=sys.stderr)
else: else:
quit() sys.exit()
# pyaudio init # pyaudio init
stream_tx = p.open(format=pyaudio.paInt16, stream_tx = p_audio.open(
format=pyaudio.paInt16,
channels=1, channels=1,
rate=AUDIO_SAMPLE_RATE_TX, rate=AUDIO_SAMPLE_RATE_TX,
frames_per_buffer=AUDIO_FRAMES_PER_BUFFER, #n_nom_modem_samples frames_per_buffer=AUDIO_FRAMES_PER_BUFFER, # n_nom_modem_samples
output=True, output=True,
output_device_index=AUDIO_OUTPUT_DEVICE, output_device_index=AUDIO_OUTPUT_DEVICE,
) )
resampler = codec2.resampler() for mode in modes:
modes = [codec2.api.FREEDV_MODE_DATAC0, codec2.api.FREEDV_MODE_DATAC1, codec2.api.FREEDV_MODE_DATAC3] freedv = ctypes.cast(codec2.api.freedv_open(mode), ctypes.c_void_p)
for m in modes:
freedv = cast(codec2.api.freedv_open(m), c_void_p)
n_tx_modem_samples = codec2.api.freedv_get_n_tx_modem_samples(freedv) n_tx_modem_samples = codec2.api.freedv_get_n_tx_modem_samples(freedv)
mod_out = create_string_buffer(2*n_tx_modem_samples) mod_out = ctypes.create_string_buffer(2 * n_tx_modem_samples)
n_tx_preamble_modem_samples = codec2.api.freedv_get_n_tx_preamble_modem_samples(freedv) n_tx_preamble_modem_samples = codec2.api.freedv_get_n_tx_preamble_modem_samples(
mod_out_preamble = create_string_buffer(2*n_tx_preamble_modem_samples) freedv
)
mod_out_preamble = ctypes.create_string_buffer(2 * n_tx_preamble_modem_samples)
n_tx_postamble_modem_samples = codec2.api.freedv_get_n_tx_postamble_modem_samples(freedv) n_tx_postamble_modem_samples = (
mod_out_postamble = create_string_buffer(2*n_tx_postamble_modem_samples) codec2.api.freedv_get_n_tx_postamble_modem_samples(freedv)
)
mod_out_postamble = ctypes.create_string_buffer(
2 * n_tx_postamble_modem_samples
)
bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(freedv) / 8) bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(freedv) / 8)
payload_per_frame = bytes_per_frame - 2 payload_per_frame = bytes_per_frame - 2
# data binary string
data_out = b'HELLO WORLD!'
buffer = bytearray(payload_per_frame) buffer = bytearray(payload_per_frame)
# set buffersize to length of data which will be send # Set buffersize to length of data which will be send
buffer[:len(data_out)] = data_out buffer[: len(data_out)] = data_out
crc = ctypes.c_ushort(codec2.api.freedv_gen_crc16(bytes(buffer), payload_per_frame)) # generate CRC16 # Generate CRC16
# convert crc to 2 byte hex string crc = ctypes.c_ushort(
crc = crc.value.to_bytes(2, byteorder='big') codec2.api.freedv_gen_crc16(bytes(buffer), payload_per_frame)
buffer += crc # append crc16 to buffer )
# Convert CRC to 2 byte hex string
crc = crc.value.to_bytes(2, byteorder="big")
buffer += crc # Append crc16 to buffer
data = (ctypes.c_ubyte * bytes_per_frame).from_buffer_copy(buffer) data = (ctypes.c_ubyte * bytes_per_frame).from_buffer_copy(buffer)
for i in range(1,N_BURSTS+1): for brst in range(1, N_BURSTS + 1):
# Write preamble to txbuffer
# write preamble to txbuffer
codec2.api.freedv_rawdatapreambletx(freedv, mod_out_preamble) codec2.api.freedv_rawdatapreambletx(freedv, mod_out_preamble)
txbuffer = bytes(mod_out_preamble) txbuffer = bytes(mod_out_preamble)
# create modulaton for N = FRAMESPERBURST and append it to txbuffer # Create modulaton for N = FRAMESPERBURST and append it to txbuffer
for n in range(1,N_FRAMES_PER_BURST+1): for frm in range(1, N_FRAMES_PER_BURST + 1):
data = (ctypes.c_ubyte * bytes_per_frame).from_buffer_copy(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 # Modulate DATA and save it into mod_out pointer
codec2.api.freedv_rawdatatx(freedv, mod_out, data)
txbuffer += bytes(mod_out) txbuffer += bytes(mod_out)
print(f"TX BURST: {i}/{N_BURSTS} FRAME: {n}/{N_FRAMES_PER_BURST}", file=sys.stderr) print(
f"TX BURST: {brst}/{N_BURSTS} FRAME: {frm}/{N_FRAMES_PER_BURST}",
file=sys.stderr,
)
# append postamble to txbuffer # Append postamble to txbuffer
codec2.api.freedv_rawdatapostambletx(freedv, mod_out_postamble) codec2.api.freedv_rawdatapostambletx(freedv, mod_out_postamble)
txbuffer += bytes(mod_out_postamble) txbuffer += bytes(mod_out_postamble)
# append a delay between bursts as audio silence # Append a delay between bursts as audio silence
samples_delay = int(MODEM_SAMPLE_RATE*DELAY_BETWEEN_BURSTS) samples_delay = int(MODEM_SAMPLE_RATE * DELAY_BETWEEN_BURSTS)
mod_out_silence = create_string_buffer(samples_delay*2) mod_out_silence = ctypes.create_string_buffer(samples_delay * 2)
txbuffer += bytes(mod_out_silence) txbuffer += bytes(mod_out_silence)
# resample up to 48k (resampler works on np.int16) # Resample up to 48k (resampler works on np.int16)
x = np.frombuffer(txbuffer, dtype=np.int16) audio_buffer = np.frombuffer(txbuffer, dtype=np.int16)
txbuffer_48k = resampler.resample8_to_48(x) txbuffer_48k = resampler.resample8_to_48(audio_buffer)
# check if we want to use an audio device or stdout # Check if we want to use an audio device or stdout
if AUDIO_OUTPUT_DEVICE != -1: if AUDIO_OUTPUT_DEVICE != -1:
stream_tx.write(txbuffer_48k.tobytes()) stream_tx.write(txbuffer_48k.tobytes())
else: else:
# this test needs a lot of time, so we are having a look at times... # This test needs a lot of time, so we are having a look at times...
starttime = time.time() starttime = time.time()
# print data to terminal for piping the output to other programs # Print data to terminal for piping the output to other programs
sys.stdout.buffer.write(txbuffer_48k) sys.stdout.buffer.write(txbuffer_48k)
sys.stdout.flush() sys.stdout.flush()
# and at least print the needed time to see which time we needed # and at least print the needed time to see which time we needed
timeneeded = time.time()-starttime timeneeded = time.time() - starttime
#print(f"time: {timeneeded} buffer: {len(txbuffer)}", file=sys.stderr) # print(f"time: {timeneeded} buffer: {len(txbuffer)}", file=sys.stderr)
# and at last check if we had an opened pyaudio instance and close it
# and at last check if we had an opened pyaudio instance and close it if AUDIO_OUTPUT_DEVICE != -1:
if AUDIO_OUTPUT_DEVICE != -1:
time.sleep(stream_tx.get_output_latency()) time.sleep(stream_tx.get_output_latency())
stream_tx.stop_stream() stream_tx.stop_stream()
stream_tx.close() stream_tx.close()
p.terminate() p_audio.terminate()
def parse_arguments():
# GET PARAMETER INPUTS
parser = argparse.ArgumentParser(description="FreeDATA TEST")
parser.add_argument("--bursts", dest="N_BURSTS", default=1, type=int)
parser.add_argument(
"--framesperburst", dest="N_FRAMES_PER_BURST", default=1, type=int
)
parser.add_argument("--delay", dest="DELAY_BETWEEN_BURSTS", default=500, type=int)
parser.add_argument(
"--audiodev",
dest="AUDIO_OUTPUT_DEVICE",
default=-1,
type=int,
help="audio output device number to use",
)
parser.add_argument(
"--list",
dest="LIST",
action="store_true",
help="list audio devices by number and exit",
)
args, _ = parser.parse_known_args()
return args
if __name__ == "__main__":
test_mm_tx()

View file

@ -3,37 +3,46 @@
# #
# Throw away test program to help understand the care and feeding of PyAudio # Throw away test program to help understand the care and feeding of PyAudio
import pyaudio
import numpy as np import numpy as np
import pyaudio
CHUNK = 1024 CHUNK = 1024
FS48 = 48000 FS48 = 48000
FTEST = 800 FTEST = 800
AMP = 16000 AMP = 16000
# 1. play sine wave out of default sound device
p = pyaudio.PyAudio() def test_pa():
stream = p.open(format=pyaudio.paInt16, # 1. play sine wave out of default sound device
p_audio = pyaudio.PyAudio()
stream = p_audio.open(
format=pyaudio.paInt16,
channels=1, channels=1,
rate=FS48, rate=FS48,
frames_per_buffer=CHUNK, frames_per_buffer=CHUNK,
output=True output=True,
) )
f48 = open("out48.raw", mode='wb') with open("out48.raw", mode="wb") as f48:
t = 0; temp = 0
for f in range(50): for _ in range(50):
sine_48k = (AMP*np.cos(2*np.pi*np.arange(t,t+CHUNK)*FTEST/FS48)).astype(np.int16) sine_48k = (
t += CHUNK AMP * np.cos(2 * np.pi * np.arange(temp, temp + CHUNK) * FTEST / FS48)
).astype(np.int16)
temp += CHUNK
sine_48k.tofile(f48) sine_48k.tofile(f48)
stream.write(sine_48k.tobytes()) stream.write(sine_48k.tobytes())
sil_48k = np.zeros(CHUNK, dtype=np.int16) sil_48k = np.zeros(CHUNK, dtype=np.int16)
for f in range(50):
for _ in range(50):
sil_48k.tofile(f48) sil_48k.tofile(f48)
stream.write(sil_48k) stream.write(sil_48k)
stream.stop_stream() stream.stop_stream()
stream.close() stream.close()
p.terminate() p_audio.terminate()
f48.close()
if __name__ == "__main__":
test_pa()

120
test/test_resample_48_8.py Normal file
View file

@ -0,0 +1,120 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Unit test for FreeDV API resampler functions, from
# codec2/unittest/t48_8_short.c - generate a sine wave at 8 KHz,
# upsample to 48 kHz, add an interferer, then downsample back to 8 kHz
#
# You can listen to the output files with:
#
# aplay -f S16_LE in8.raw
# aplay -r 48000 -f S16_LE out48.raw
# aplay -f S16_LE out8.raw
#
# They should sound like clean sine waves
# pylint: disable=global-statement, invalid-name, unused-import
import os
import sys
import codec2
import numpy as np
import pytest
# dig some constants out
FDMDV_OS_48 = codec2.api.FDMDV_OS_48
FDMDV_OS_TAPS_48K = codec2.api.FDMDV_OS_TAPS_48K
FDMDV_OS_TAPS_48_8K = codec2.api.FDMDV_OS_TAPS_48_8K
N8 = 180 # processing buffer size at 8 kHz
N48 = N8 * FDMDV_OS_48 # processing buffer size at 48 kHz
MEM8 = FDMDV_OS_TAPS_48_8K # 8kHz signal filter memory
MEM48 = FDMDV_OS_TAPS_48K # 48kHz signal filter memory
FRAMES = 50 # number of frames to test
FS8 = 8000
FS48 = 48000
AMP = 16000 # sine wave amplitude
FTEST8 = 800 # input test frequency at FS=8kHz
FINTER48 = 10000 # interferer frequency at FS=48kHz
# Due to the design of these resamplers, the processing buffer (at 8kHz)
# must be an integer multiple of oversampling ratio
assert N8 % FDMDV_OS_48 == 0
def test_resampler():
"""
Test for the codec2 audio resampling routine
"""
# time indexes, we advance every frame
t = 0
t1 = 0
# output files to listen to/evaluate result
with open("in8.raw", mode="wb") as fin8:
with open("out48.raw", mode="wb") as f48:
with open("out8.raw", mode="wb") as fout8:
resampler = codec2.resampler()
# Generate FRAMES of a sine wave
for _ in range(FRAMES):
# Primary sine wave, which the down-sampling filter should retain.
sine_in8k = (
AMP * np.cos(2 * np.pi * np.arange(t, t + N8) * FTEST8 / FS8)
).astype(np.int16)
t += N8
sine_in8k.tofile(fin8)
sine_out48k = resampler.resample8_to_48(sine_in8k)
sine_out48k.tofile(f48)
# Add an interfering sine wave, which the down-sampling filter should (mostly) remove
sine_in48k = (
sine_out48k
+ (AMP / 2)
* np.cos(2 * np.pi * np.arange(t1, t1 + N48) * FINTER48 / FS48)
).astype(np.int16)
t1 += N48
sine_out8k = resampler.resample48_to_8(sine_in48k)
sine_out8k.tofile(fout8)
# os.unlink("out48.raw")
# Automated test evaluation --------------------------------------------
# The input and output signals will not be time aligned due to the filter
# delays, so compare the magnitude spectrum
# Read the raw audio files
in8k = np.fromfile("in8.raw", dtype=np.int16)
out8k = np.fromfile("out8.raw", dtype=np.int16)
assert len(in8k) == len(out8k)
# os.unlink("in8.raw")
# os.unlink("out8.raw")
# Apply hanning filter to raw input data samples
h = np.hanning(len(in8k))
S1 = np.abs(np.fft.fft(in8k * h))
S2 = np.abs(np.fft.fft(out8k * h))
# Calculate the ratio between signal and noise (error energy).
error = S1 - S2
error_energy = np.dot(error, error)
ratio = error_energy / np.dot(S1, S1)
ratio_dB = 10 * np.log10(ratio)
# Establish -40.0 as the noise ratio ceiling
threshdB = -40.0
print(f"ratio_dB: {ratio_dB:4.2}" % (ratio_dB))
assert ratio_dB < threshdB
if __name__ == "__main__":
# Run pytest with the current script as the filename.
ecode = pytest.main(["-v", sys.argv[0]])
if ecode == 0:
print("PASS")
else:
print("FAIL")

View file

@ -6,35 +6,22 @@ Created on Wed Dec 23 07:04:24 2020
@author: DJ2LS @author: DJ2LS
""" """
import ctypes
from ctypes import *
import pathlib
import sounddevice as sd
import sys
import logging
import time
import threading
import sys
import argparse import argparse
import ctypes
import sys
import time
import numpy as np import numpy as np
sys.path.insert(0,'..') import sounddevice as sd
sys.path.insert(0, "..")
from tnc import codec2 from tnc import codec2
#--------------------------------------------GET PARAMETER INPUTS def test_rx():
parser = argparse.ArgumentParser(description='Simons TEST TNC') args = parse_arguments()
parser.add_argument('--bursts', dest="N_BURSTS", default=1, type=int)
parser.add_argument('--framesperburst', dest="N_FRAMES_PER_BURST", default=1, type=int)
parser.add_argument('--mode', dest="FREEDV_MODE", type=str, choices=['datac0', 'datac1', 'datac3'])
parser.add_argument('--audiodev', dest="AUDIO_INPUT_DEVICE", default=-1, type=int,
help="audio device number to use, use -2 to automatically select a loopback device")
parser.add_argument('--debug', dest="DEBUGGING_MODE", action="store_true")
parser.add_argument('--timeout', dest="TIMEOUT", default=10, type=int, help="Timeout (seconds) before test ends")
parser.add_argument('--list', dest="LIST", action="store_true", help="list audio devices by number and exit")
args = parser.parse_args() if args.LIST:
if args.LIST:
devices = sd.query_devices(device=None, kind=None) devices = sd.query_devices(device=None, kind=None)
index = 0 index = 0
@ -42,110 +29,115 @@ if args.LIST:
print(f"{index} {device['name']}") print(f"{index} {device['name']}")
index += 1 index += 1
sd._terminate() sd._terminate()
quit() sys.exit()
N_BURSTS = args.N_BURSTS N_BURSTS = args.N_BURSTS
N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST
AUDIO_INPUT_DEVICE = args.AUDIO_INPUT_DEVICE AUDIO_INPUT_DEVICE = args.AUDIO_INPUT_DEVICE
MODE = codec2.FREEDV_MODE[args.FREEDV_MODE].value MODE = codec2.FREEDV_MODE[args.FREEDV_MODE].value
DEBUGGING_MODE = args.DEBUGGING_MODE DEBUGGING_MODE = args.DEBUGGING_MODE
TIMEOUT = args.TIMEOUT MAX_TIME = args.TIMEOUT
# AUDIO PARAMETERS # AUDIO PARAMETERS
AUDIO_FRAMES_PER_BUFFER = 2400*2 # <- consider increasing if you get nread_exceptions > 0 # v-- consider increasing if you get nread_exceptions > 0
MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000 AUDIO_FRAMES_PER_BUFFER = 2400 * 2
AUDIO_SAMPLE_RATE_RX = 48000 MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000
AUDIO_SAMPLE_RATE_RX = 48000
# make sure our resampler will work # make sure our resampler will work
assert (AUDIO_SAMPLE_RATE_RX / MODEM_SAMPLE_RATE) == codec2.api.FDMDV_OS_48 assert (AUDIO_SAMPLE_RATE_RX / MODEM_SAMPLE_RATE) == codec2.api.FDMDV_OS_48
# check if we want to use an audio device then do an pyaudio init # check if we want to use an audio device then do an pyaudio init
if AUDIO_INPUT_DEVICE != -1: if AUDIO_INPUT_DEVICE != -1:
# auto search for loopback devices # auto search for loopback devices
if AUDIO_INPUT_DEVICE == -2: if AUDIO_INPUT_DEVICE == -2:
loopback_list = [] loopback_list = []
devices = sd.query_devices(device=None, kind=None) devices = sd.query_devices(device=None, kind=None)
index = 0
for device in devices: for index, device in enumerate(devices):
if 'Loopback: PCM' in device['name']: if "Loopback: PCM" in device["name"]:
print(index) print(index)
loopback_list.append(index) loopback_list.append(index)
index += 1
if len(loopback_list) >= 1: if loopback_list:
AUDIO_INPUT_DEVICE = loopback_list[0] #0 = RX 1 = TX # 0 = RX 1 = TX
AUDIO_INPUT_DEVICE = loopback_list[0]
print(f"loopback_list tx: {loopback_list}", file=sys.stderr) print(f"loopback_list tx: {loopback_list}", file=sys.stderr)
else: else:
print("not enough audio loopback devices ready...") print("not enough audio loopback devices ready...")
print("you should wait about 30 seconds...") print("you should wait about 30 seconds...")
sd._terminate() sd._terminate()
quit() sys.exit()
print(f"AUDIO INPUT DEVICE: {AUDIO_INPUT_DEVICE}", file=sys.stderr) print(f"AUDIO INPUT DEVICE: {AUDIO_INPUT_DEVICE}", file=sys.stderr)
# audio stream init # audio stream init
stream_rx = sd.RawStream(channels=1, dtype='int16', device=AUDIO_INPUT_DEVICE, samplerate = AUDIO_SAMPLE_RATE_RX, blocksize=4800) stream_rx = sd.RawStream(
channels=1,
dtype="int16",
device=AUDIO_INPUT_DEVICE,
samplerate=AUDIO_SAMPLE_RATE_RX,
blocksize=4800,
)
stream_rx.start() stream_rx.start()
# ---------------------------------------------------------------- # ----------------------------------------------------------------
# DATA CHANNEL INITIALISATION
# DATA CHANNEL INITIALISATION # open codec2 instance
freedv = ctypes.cast(codec2.api.freedv_open(MODE), ctypes.c_void_p)
# open codec2 instance # get number of bytes per frame for mode
freedv = cast(codec2.api.freedv_open(MODE), c_void_p) bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(freedv) / 8)
payload_bytes_per_frame = bytes_per_frame - 2
# get number of bytes per frame for mode n_max_modem_samples = codec2.api.freedv_get_n_max_modem_samples(freedv)
bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(freedv)/8) bytes_out = ctypes.create_string_buffer(bytes_per_frame)
payload_bytes_per_frame = bytes_per_frame -2
n_max_modem_samples = codec2.api.freedv_get_n_max_modem_samples(freedv) codec2.api.freedv_set_frames_per_burst(freedv, N_FRAMES_PER_BURST)
bytes_out = create_string_buffer(bytes_per_frame)
codec2.api.freedv_set_frames_per_burst(freedv,N_FRAMES_PER_BURST) total_n_bytes = 0
rx_total_frames = 0
rx_frames = 0
rx_bursts = 0
rx_errors = 0
nread_exceptions = 0
timeout = time.time() + MAX_TIME
receive = True
audio_buffer = codec2.audio_buffer(AUDIO_FRAMES_PER_BUFFER * 2)
resampler = codec2.resampler()
total_n_bytes = 0 # time meassurement
rx_total_frames = 0 time_start = 0
rx_frames = 0 time_end = 0
rx_bursts = 0
rx_errors = 0
nread_exceptions = 0
timeout = time.time() + TIMEOUT
receive = True
audio_buffer = codec2.audio_buffer(AUDIO_FRAMES_PER_BUFFER*2)
resampler = codec2.resampler()
# time meassurement # Copy received 48 kHz to a file. Listen to this file with:
time_start = 0 # aplay -r 48000 -f S16_LE rx48.raw
time_end = 0 # Corruption of this file is a good way to detect audio card issues
frx = open("rx48.raw", mode="wb")
# Copy received 48 kHz to a file. Listen to this file with: # initial number of samples we need
# aplay -r 48000 -f S16_LE rx48.raw nin = codec2.api.freedv_nin(freedv)
# Corruption of this file is a good way to detect audio card issues while receive and time.time() < timeout:
frx = open("rx48.raw", mode='wb')
# initial number of samples we need
nin = codec2.api.freedv_nin(freedv)
while receive and time.time() < timeout:
if AUDIO_INPUT_DEVICE != -1: if AUDIO_INPUT_DEVICE != -1:
try: try:
#data_in48k = stream_rx.read(AUDIO_FRAMES_PER_BUFFER, exception_on_overflow = True) # data_in48k = stream_rx.read(AUDIO_FRAMES_PER_BUFFER, exception_on_overflow = True)
data_in48k, overflowed = stream_rx.read(AUDIO_FRAMES_PER_BUFFER) data_in48k, overflowed = stream_rx.read(AUDIO_FRAMES_PER_BUFFER)
except OSError as err: except OSError as err:
print(err, file=sys.stderr) print(err, file=sys.stderr)
#if str(err).find("Input overflowed") != -1: # if str(err).find("Input overflowed") != -1:
# nread_exceptions += 1 # nread_exceptions += 1
#if str(err).find("Stream closed") != -1: # if str(err).find("Stream closed") != -1:
# print("Ending...") # print("Ending...")
# receive = False # receive = False
else: else:
data_in48k = sys.stdin.buffer.read(AUDIO_FRAMES_PER_BUFFER*2) data_in48k = sys.stdin.buffer.read(AUDIO_FRAMES_PER_BUFFER * 2)
# insert samples in buffer # insert samples in buffer
x = np.frombuffer(data_in48k, dtype=np.int16) x = np.frombuffer(data_in48k, dtype=np.int16)
#print(x) # print(x)
#x = data_in48k # x = data_in48k
x.tofile(frx) x.tofile(frx)
if len(x) != AUDIO_FRAMES_PER_BUFFER: if len(x) != AUDIO_FRAMES_PER_BUFFER:
receive = False receive = False
@ -157,7 +149,9 @@ while receive and time.time() < timeout:
# start time measurement # start time measurement
time_start = time.time() time_start = time.time()
# demodulate audio # demodulate audio
nbytes = codec2.api.freedv_rawdatarx(freedv, bytes_out, audio_buffer.buffer.ctypes) nbytes = codec2.api.freedv_rawdatarx(
freedv, bytes_out, audio_buffer.buffer.ctypes
)
time_end = time.time() time_end = time.time()
audio_buffer.pop(nin) audio_buffer.pop(nin)
@ -172,19 +166,22 @@ while receive and time.time() < timeout:
rx_status = codec2.api.rx_sync_flags_to_text[rx_status] rx_status = codec2.api.rx_sync_flags_to_text[rx_status]
time_needed = time_end - time_start time_needed = time_end - time_start
print("nin: %5d rx_status: %4s naudio_buffer: %4d time: %4s" % \ print(
(nin,rx_status,audio_buffer.nbuffer, time_needed), file=sys.stderr) f"nin: {nin:5d} rx_status: {rx_status:4s} "
f"naudio_buffer: {audio_buffer.nbuffer:4d} time: {time_needed:4f}",
file=sys.stderr,
)
if nbytes: if nbytes:
total_n_bytes = total_n_bytes + nbytes total_n_bytes += nbytes
if nbytes == bytes_per_frame: if nbytes == bytes_per_frame:
rx_total_frames = rx_total_frames + 1 rx_total_frames += 1
rx_frames = rx_frames + 1 rx_frames += 1
if rx_frames == N_FRAMES_PER_BURST: if rx_frames == N_FRAMES_PER_BURST:
rx_frames = 0 rx_frames = 0
rx_bursts = rx_bursts + 1 rx_bursts += 1
if rx_bursts == N_BURSTS: if rx_bursts == N_BURSTS:
receive = False receive = False
@ -192,15 +189,59 @@ while receive and time.time() < timeout:
if time.time() >= timeout: if time.time() >= timeout:
print("TIMEOUT REACHED") print("TIMEOUT REACHED")
if nread_exceptions: if nread_exceptions:
print("nread_exceptions %d - receive audio lost! Consider increasing Pyaudio frames_per_buffer..." % \ print(
nread_exceptions, file=sys.stderr) f"nread_exceptions {nread_exceptions:d} - receive audio lost! "
print(f"RECEIVED BURSTS: {rx_bursts} RECEIVED FRAMES: {rx_total_frames} RX_ERRORS: {rx_errors}", file=sys.stderr) "Consider increasing Pyaudio frames_per_buffer...",
frx.close() file=sys.stderr,
)
print(
f"RECEIVED BURSTS: {rx_bursts} "
f"RECEIVED FRAMES: {rx_total_frames} "
f"RX_ERRORS: {rx_errors}",
file=sys.stderr,
)
frx.close()
# and at last check if we had an opened audio instance and close it
# and at last check if we had an opened audio instance and close it if AUDIO_INPUT_DEVICE != -1:
if AUDIO_INPUT_DEVICE != -1:
sd._terminate() sd._terminate()
def parse_arguments():
# --------------------------------------------GET PARAMETER INPUTS
parser = argparse.ArgumentParser(description="Simons TEST TNC")
parser.add_argument("--bursts", dest="N_BURSTS", default=1, type=int)
parser.add_argument(
"--framesperburst", dest="N_FRAMES_PER_BURST", default=1, type=int
)
parser.add_argument(
"--mode", dest="FREEDV_MODE", type=str, choices=["datac0", "datac1", "datac3"]
)
parser.add_argument(
"--audiodev",
dest="AUDIO_INPUT_DEVICE",
default=-1,
type=int,
help="audio device number to use, use -2 to automatically select a loopback device",
)
parser.add_argument("--debug", dest="DEBUGGING_MODE", action="store_true")
parser.add_argument(
"--timeout",
dest="TIMEOUT",
default=10,
type=int,
help="Timeout (seconds) before test ends",
)
parser.add_argument(
"--list",
dest="LIST",
action="store_true",
help="list audio devices by number and exit",
)
args, _ = parser.parse_known_args()
return args
if __name__ == "__main__":
test_rx()

54
test/test_tnc.py Executable file
View file

@ -0,0 +1,54 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import multiprocessing
import os
import sys
import time
import pytest
# pylint: disable=wrong-import-position
sys.path.insert(0, "..")
sys.path.insert(0, "../tnc")
sys.path.insert(0, "test")
import test_tnc_IRS as irs
import test_tnc_ISS as iss
# These do not update static.INFO.
# "CONNECT", "SEND_TEST_FRAME"
@pytest.mark.parametrize("command", ["CQ", "PING", "BEACON"])
def test_tnc(command):
iss_proc = multiprocessing.Process(target=iss.t_arq_iss, args=[command])
irs_proc = multiprocessing.Process(target=irs.t_arq_irs, args=[command])
# print("Starting threads.")
iss_proc.start()
irs_proc.start()
time.sleep(12)
# print("Terminating threads.")
irs_proc.terminate()
iss_proc.terminate()
irs_proc.join()
iss_proc.join()
for idx in range(2):
try:
os.unlink(f"/tmp/hfchannel{idx+1}")
except FileNotFoundError as fnfe:
print(f"Unlinking pipe: {fnfe}")
assert iss_proc.exitcode == 0, f"Transmit side failed test. {iss_proc}"
assert irs_proc.exitcode == 0, f"Receive side failed test. {irs_proc}"
if __name__ == "__main__":
# Run pytest with the current script as the filename.
ecode = pytest.main(["-s", "-v", sys.argv[0]])
if ecode == 0:
print("errors: 0")
else:
print(ecode)

81
test/test_tnc_IRS.py Normal file
View file

@ -0,0 +1,81 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Wed Dec 23 07:04:24 2020
@author: DJ2LS
"""
import os
import sys
import time
sys.path.insert(0, "..")
sys.path.insert(0, "../tnc")
import data_handler
import helpers
import modem
import static
IRS_original_arq_cleanup = object
MESSAGE: str
def irs_arq_cleanup():
"""Replacement for modem.arq_cleanup to detect when to exit process."""
if "TRANSMISSION;STOPPED" in static.INFO:
print(f"{static.INFO=}")
time.sleep(2)
# sys.exit does not terminate threads.
# pylint: disable=protected-access
if f"{MESSAGE};RECEIVING" not in static.INFO:
print(f"{MESSAGE} was not received.")
os._exit(1)
os._exit(0)
IRS_original_arq_cleanup()
def t_arq_irs(*args):
# pylint: disable=global-statement
global IRS_original_arq_cleanup, MESSAGE
MESSAGE = args[0]
# enable testmode
data_handler.TESTMODE = True
modem.TESTMODE = True
modem.RXCHANNEL = "/tmp/hfchannel2"
modem.TXCHANNEL = "/tmp/hfchannel1"
static.HAMLIB_RADIOCONTROL = "disabled"
static.RESPOND_TO_CQ = True
mycallsign = bytes("DN2LS-2", "utf-8")
mycallsign = helpers.callsign_to_bytes(mycallsign)
static.MYCALLSIGN = helpers.bytes_to_callsign(mycallsign)
static.MYCALLSIGN_CRC = helpers.get_crc_24(static.MYCALLSIGN)
static.MYGRID = bytes("AA12aa", "utf-8")
static.SSID_LIST = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
# start data handler
tnc = data_handler.DATA()
# Inject a way to exit the TNC infinite loop
IRS_original_arq_cleanup = tnc.arq_cleanup
tnc.arq_cleanup = irs_arq_cleanup
# start modem
t_modem = modem.RF()
# Set timeout
timeout = time.time() + 10
while time.time() < timeout:
time.sleep(0.1)
assert not "TIMEOUT!"
if __name__ == "__main__":
print("This cannot be run as an application.")
sys.exit(1)

118
test/test_tnc_ISS.py Normal file
View file

@ -0,0 +1,118 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Wed Dec 23 07:04:24 2020
@author: DJ2LS
"""
import os
import sys
import time
sys.path.insert(0, "..")
sys.path.insert(0, "../tnc")
import data_handler
import helpers
import modem
import static
ISS_original_arq_cleanup = object
MESSAGE: str
def iss_arq_cleanup():
"""Replacement for modem.arq_cleanup to detect when to exit process."""
if "TRANSMISSION;STOPPED" in static.INFO:
print(f"{static.INFO=}")
time.sleep(1)
# sys.exit does not terminate threads.
# pylint: disable=protected-access
if f"{MESSAGE};SENDING" not in static.INFO:
print(f"{MESSAGE} was not sent.")
os._exit(1)
os._exit(0)
ISS_original_arq_cleanup()
def t_arq_iss(*args):
# pylint: disable=global-statement
global ISS_original_arq_cleanup, MESSAGE
MESSAGE = args[0]
# enable testmode
data_handler.TESTMODE = True
modem.TESTMODE = True
modem.RXCHANNEL = "/tmp/hfchannel1"
modem.TXCHANNEL = "/tmp/hfchannel2"
static.HAMLIB_RADIOCONTROL = "disabled"
mycallsign = bytes("DJ2LS-2", "utf-8")
mycallsign = helpers.callsign_to_bytes(mycallsign)
static.MYCALLSIGN = helpers.bytes_to_callsign(mycallsign)
static.MYCALLSIGN_CRC = helpers.get_crc_24(static.MYCALLSIGN)
static.MYGRID = bytes("AA12aa", "utf-8")
static.SSID_LIST = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
dxcallsign = b"DN2LS-0"
dxcallsign = helpers.callsign_to_bytes(dxcallsign)
dxcallsign = helpers.bytes_to_callsign(dxcallsign)
static.DXCALLSIGN = dxcallsign
static.DXCALLSIGN_CRC = helpers.get_crc_24(static.DXCALLSIGN)
bytes_out = b'{"dt":"f","fn":"zeit.txt","ft":"text\\/plain","d":"data:text\\/plain;base64,MyBtb2Rlcywgb2huZSBjbGFzcwowLjAwMDk2OTQ4MTE4MDk5MTg0MTcKCjIgbW9kZXMsIG9obmUgY2xhc3MKMC4wMDA5NjY1NDUxODkxMjI1Mzk0CgoxIG1vZGUsIG9obmUgY2xhc3MKMC4wMDA5NjY5NzY1NTU4Nzc4MjA5CgMyBtb2Rlcywgb2huZSBjbGFzcwowLjAwMDk2OTQ4MTE4MDk5MTg0MTcKCjIgbW9kZXMsIG9obmUgY2xhc3MKMC4wMDA5NjY1NDUxODkxMjI1Mzk0CgoxIG1vZGUsIG9obmUgY2xhc3MKMC4wMDA5NjY5NzY1NTU4Nzc4MjA5Cg=MyBtb2Rlcywgb2huZSBjbGFzcwowLjAwMDk2OTQ4MTE4MDk5MTg0MTcKCjIgbW9kZXMsIG9obmUgY2xhc3MKMC4wMDA5NjY1NDUxODkxMjI1Mzk0CgoxIG1vZGUsIG9obmUgY2xhc3MKMC4wMDA5NjY5NzY1NTU4Nzc4MjA5CgMyBtb2Rlcywgb2huZSBjbGFzcwowLjAwMDk2OTQ4MTE4MDk5MTg0MTcKCjIgbW9kZXMsIG9obmUgY2xhc3MKMC4wMDA5NjY1NDUxODkxMjI1Mzk0CgoxIG1vZGUsIG9obmUgY2xhc3MKMC4wMDA5NjY5NzY1NTU4Nzc4MjA5CgMyBtb2Rlcywgb2huZSBjbGFzcwowLjAwMDk2OTQ4MTE4MDk5MTg0MTcKCjIgbW9kZXMsIG9obmUgY2xhc3MKMC4wMDA5NjY1NDUxODkxMjI1Mzk0CgoxIG1vZGUsIG9obmUgY2xhc3MKMC4wMDA5NjY5NzY1NTU4Nzc4MjA5Cg=","crc":"123123123"}'
# start data handler
tnc = data_handler.DATA()
# Inject a way to exit the TNC infinite loop
ISS_original_arq_cleanup = tnc.arq_cleanup
tnc.arq_cleanup = iss_arq_cleanup
# start modem
t_modem = modem.RF()
# mode = codec2.freedv_get_mode_value_by_name(FREEDV_MODE)
# n_frames_per_burst = N_FRAMES_PER_BURST
# add command to data qeue
"""
elif data[0] == 'ARQ_RAW':
# [0] ARQ_RAW
# [1] DATA_OUT bytes
# [2] MODE int
# [3] N_FRAMES_PER_BURST int
# [4] self.transmission_uuid str
# [5] mycallsign with ssid
"""
# data_handler.DATA_QUEUE_TRANSMIT.put(['ARQ_RAW', bytes_out, 255, n_frames_per_burst, '123', b'DJ2LS-0'])
# for _ in range(4):
if MESSAGE in ["BEACON"]:
data_handler.DATA_QUEUE_TRANSMIT.put([MESSAGE, 5, True])
elif MESSAGE in ["PING", "CONNECT"]:
data_handler.DATA_QUEUE_TRANSMIT.put([MESSAGE, dxcallsign])
else:
data_handler.DATA_QUEUE_TRANSMIT.put([MESSAGE])
time.sleep(1.5)
# for i in range(4):
# data_handler.DATA_QUEUE_TRANSMIT.put(['PING', b'DN2LS-2'])
data_handler.DATA_QUEUE_TRANSMIT.put(["STOP"])
# Set timeout
timeout = time.time() + 10
while time.time() < timeout:
time.sleep(0.1)
assert not "TIMEOUT!"
if __name__ == "__main__":
print("This cannot be run as an application.")
sys.exit(-1)

225
test/test_tnc_states.py Normal file
View file

@ -0,0 +1,225 @@
"""
Tests for the FreeDATA TNC state machine.
"""
import sys
import pytest
# pylint: disable=wrong-import-position
sys.path.insert(0, "..")
sys.path.insert(0, "../tnc")
import data_handler
import helpers
import static
def print_frame(data: bytearray):
"""
Pretty-print the provided frame.
:param data: Frame to be output
:type data: bytearray
"""
print(f"Type : {int(data[0])}")
print(f"DXCRC : {bytes(data[1:4])}")
print(f"CallCRC: {bytes(data[4:7])}")
print(f"Call : {helpers.bytes_to_callsign(data[7:13])}")
def t_create_frame(frame_type: int, mycall: str, dxcall: str) -> bytearray:
"""
Generate the requested frame.
:param frame_type: The numerical type of the desired frame.
:type frame_type: int
:param mycall: Callsign of the near station
:type mycall: str
:param dxcall: Callsign of the far station
:type dxcall: str
:return: Bytearray of the requested frame
:rtype: bytearray
"""
mycallsign_bytes = helpers.callsign_to_bytes(mycall)
mycallsign = helpers.bytes_to_callsign(mycallsign_bytes)
mycallsign_crc = helpers.get_crc_24(mycallsign)
dxcallsign_bytes = helpers.callsign_to_bytes(dxcall)
dxcallsign = helpers.bytes_to_callsign(dxcallsign_bytes)
dxcallsign_crc = helpers.get_crc_24(dxcallsign)
frame = bytearray(14)
frame[:1] = bytes([frame_type])
frame[1:4] = dxcallsign_crc
frame[4:7] = mycallsign_crc
frame[7:13] = mycallsign_bytes
return frame
def t_create_session_close(mycall: str, dxcall: str) -> bytearray:
"""
Generate the session_close frame.
:param mycall: Callsign of the near station
:type mycall: str
:param dxcall: Callsign of the far station
:type dxcall: str
:return: Bytearray of the requested frame
:rtype: bytearray
"""
return t_create_frame(223, mycall, dxcall)
def t_create_start_session(mycall: str, dxcall: str) -> bytearray:
"""
Generate the create_session frame.
:param mycall: Callsign of the near station
:type mycall: str
:param dxcall: Callsign of the far station
:type dxcall: str
:return: Bytearray of the requested frame
:rtype: bytearray
"""
return t_create_frame(221, mycall, dxcall)
def t_tsh_dummy():
"""Replacement function for transmit_session_heartbeat"""
print("In transmit_session_heartbeat")
@pytest.mark.parametrize("mycall", ["AA1AA-2", "DE2DE-0", "M4AWQ-4"])
@pytest.mark.parametrize("dxcall", ["AA9AA-1", "DE2ED-0", "F6QWE-3"])
def test_valid_disconnect(mycall: str, dxcall: str):
"""
Execute test to validate that receiving a session open frame sets the correct machine
state.
:param mycall: Callsign of the near station
:type mycall: str
:param dxcall: Callsign of the far station
:type dxcall: str
:return: Bytearray of the requested frame
:rtype: bytearray
"""
# Set the SSIDs we'll use for this test.
static.SSID_LIST = [0, 1, 2, 3, 4]
# Setup the static parameters for the connection.
mycallsign_bytes = helpers.callsign_to_bytes(mycall)
mycallsign = helpers.bytes_to_callsign(mycallsign_bytes)
static.MYCALLSIGN = mycallsign
static.MYCALLSIGN_CRC = helpers.get_crc_24(mycallsign)
dxcallsign_bytes = helpers.callsign_to_bytes(dxcall)
dxcallsign = helpers.bytes_to_callsign(dxcallsign_bytes)
static.DXCALLSIGN = dxcallsign
static.DXCALLSIGN_CRC = helpers.get_crc_24(dxcallsign)
# Create the TNC
tnc = data_handler.DATA()
# Replace the heartbeat transmit routine with our own, a No-Op.
tnc.transmit_session_heartbeat = t_tsh_dummy
# Create packet to be 'received' by this station.
create_frame = t_create_start_session(mycall=dxcall, dxcall=mycall)
print_frame(create_frame)
tnc.received_session_opener(create_frame)
assert static.ARQ_SESSION is True
assert static.TNC_STATE == "BUSY"
assert static.ARQ_SESSION_STATE == "connecting"
# Create packet to be 'received' by this station.
close_frame = t_create_session_close(mycall=dxcall, dxcall=mycall)
print_frame(close_frame)
tnc.received_session_close(close_frame)
assert helpers.callsign_to_bytes(static.MYCALLSIGN) == mycallsign_bytes
assert helpers.callsign_to_bytes(static.DXCALLSIGN) == dxcallsign_bytes
assert static.ARQ_SESSION is False
assert static.TNC_STATE == "IDLE"
assert static.ARQ_SESSION_STATE == "disconnected"
@pytest.mark.parametrize("mycall", ["AA1AA-2", "DE2DE-0", "E4AWQ-4"])
@pytest.mark.parametrize("dxcall", ["AA9AA-1", "DE2ED-0", "F6QWE-3"])
def test_foreign_disconnect(mycall: str, dxcall: str):
"""
Execute test to validate that receiving a session open frame sets the correct machine
state.
:param mycall: Callsign of the near station
:type mycall: str
:param dxcall: Callsign of the far station
:type dxcall: str
:return: Bytearray of the requested frame
:rtype: bytearray
"""
# Setup the static parameters for the connection.
mycallsign_bytes = helpers.callsign_to_bytes(mycall)
mycallsign = helpers.bytes_to_callsign(mycallsign_bytes)
static.MYCALLSIGN = mycallsign
static.MYCALLSIGN_CRC = helpers.get_crc_24(mycallsign)
dxcallsign_bytes = helpers.callsign_to_bytes(dxcall)
dxcallsign = helpers.bytes_to_callsign(dxcallsign_bytes)
static.DXCALLSIGN = dxcallsign
static.DXCALLSIGN_CRC = helpers.get_crc_24(dxcallsign)
# Create the TNC
tnc = data_handler.DATA()
# Replace the heartbeat transmit routine with a No-Op.
tnc.transmit_session_heartbeat = t_tsh_dummy
# Create frame to be 'received' by this station.
create_frame = t_create_start_session(mycall=dxcall, dxcall=mycall)
print_frame(create_frame)
tnc.received_session_opener(create_frame)
assert helpers.callsign_to_bytes(static.MYCALLSIGN) == mycallsign_bytes
assert helpers.callsign_to_bytes(static.DXCALLSIGN) == dxcallsign_bytes
assert static.ARQ_SESSION is True
assert static.TNC_STATE == "BUSY"
assert static.ARQ_SESSION_STATE == "connecting"
# Set up a frame from a non-associated station.
foreigncall_bytes = helpers.callsign_to_bytes("ZZ0ZZ-0")
foreigncall = helpers.bytes_to_callsign(foreigncall_bytes)
close_frame = t_create_session_close("ZZ0ZZ-0", "ZZ0ZZ-0")
print_frame(close_frame)
assert (
helpers.check_callsign(static.DXCALLSIGN, bytes(close_frame[4:7]))[0] is False
)
assert helpers.check_callsign(foreigncall, bytes(close_frame[4:7]))[0] is True
# Send the non-associated session close frame to the TNC
tnc.received_session_close(close_frame)
assert helpers.callsign_to_bytes(static.MYCALLSIGN) == helpers.callsign_to_bytes(
mycall
)
assert helpers.callsign_to_bytes(static.DXCALLSIGN) == helpers.callsign_to_bytes(
dxcall
)
assert static.ARQ_SESSION is True
assert static.TNC_STATE == "BUSY"
assert static.ARQ_SESSION_STATE == "connecting"
if __name__ == "__main__":
# Run pytest with the current script as the filename.
ecode = pytest.main(["-v", sys.argv[0]])
if ecode == 0:
print("errors: 0")
else:
print(ecode)

View file

@ -2,174 +2,217 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import ctypes
from ctypes import *
import pathlib
import sounddevice as sd
import time
import argparse import argparse
import ctypes
import sys import sys
sys.path.insert(0,'..')
from tnc import codec2
import numpy as np import numpy as np
import sounddevice as sd
# GET PARAMETER INPUTS sys.path.insert(0, "..")
parser = argparse.ArgumentParser(description='Simons TEST TNC') from tnc import codec2
parser.add_argument('--bursts', dest="N_BURSTS", default=1, type=int)
parser.add_argument('--framesperburst', dest="N_FRAMES_PER_BURST", default=1, type=int)
parser.add_argument('--delay', dest="DELAY_BETWEEN_BURSTS", default=500, type=int,
help="delay between bursts in ms")
parser.add_argument('--mode', dest="FREEDV_MODE", type=str, choices=['datac0', 'datac1', 'datac3'])
parser.add_argument('--audiodev', dest="AUDIO_OUTPUT_DEVICE", default=-1, type=int,
help="audio output device number to use, use -2 to automatically select a loopback device")
parser.add_argument('--list', dest="LIST", action="store_true", help="list audio devices by number and exit")
parser.add_argument('--testframes', dest="TESTFRAMES", action="store_true", default=False, help="list audio devices by number and exit")
args = parser.parse_args() def test_tx():
args = parse_arguments()
if args.LIST:
if args.LIST:
devices = sd.query_devices(device=None, kind=None) devices = sd.query_devices(device=None, kind=None)
index = 0
for device in devices: for index, device in enumerate(devices):
print(f"{index} {device['name']}") print(f"{index} {device['name']}")
index += 1
sd._terminate() sd._terminate()
quit() sys.exit()
N_BURSTS = args.N_BURSTS N_BURSTS = args.N_BURSTS
N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST
DELAY_BETWEEN_BURSTS = args.DELAY_BETWEEN_BURSTS/1000 DELAY_BETWEEN_BURSTS = args.DELAY_BETWEEN_BURSTS / 1000
AUDIO_OUTPUT_DEVICE = args.AUDIO_OUTPUT_DEVICE AUDIO_OUTPUT_DEVICE = args.AUDIO_OUTPUT_DEVICE
MODE = codec2.FREEDV_MODE[args.FREEDV_MODE].value MODE = codec2.FREEDV_MODE[args.FREEDV_MODE].value
# AUDIO PARAMETERS # AUDIO PARAMETERS
AUDIO_FRAMES_PER_BUFFER = 2400 AUDIO_FRAMES_PER_BUFFER = 2400
MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000 MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000
AUDIO_SAMPLE_RATE_TX = 48000 AUDIO_SAMPLE_RATE_TX = 48000
assert (AUDIO_SAMPLE_RATE_TX % MODEM_SAMPLE_RATE) == 0 assert (AUDIO_SAMPLE_RATE_TX % MODEM_SAMPLE_RATE) == 0
# check if we want to use an audio device then do an pyaudio init # check if we want to use an audio device then do an pyaudio init
if AUDIO_OUTPUT_DEVICE != -1: if AUDIO_OUTPUT_DEVICE != -1:
# auto search for loopback devices # auto search for loopback devices
if AUDIO_OUTPUT_DEVICE == -2: if AUDIO_OUTPUT_DEVICE == -2:
loopback_list = [] loopback_list = []
devices = sd.query_devices(device=None, kind=None) devices = sd.query_devices(device=None, kind=None)
index = 0
for device in devices: for index, device in enumerate(devices):
if 'Loopback: PCM' in device['name']: if "Loopback: PCM" in device["name"]:
print(index) print(index)
loopback_list.append(index) loopback_list.append(index)
index += 1
if len(loopback_list) >= 1: if loopback_list:
AUDIO_OUTPUT_DEVICE = loopback_list[len(loopback_list)-1] #0 = RX 1 = TX # 0 = RX 1 = TX
AUDIO_OUTPUT_DEVICE = loopback_list[-1]
print(f"loopback_list tx: {loopback_list}", file=sys.stderr) print(f"loopback_list tx: {loopback_list}", file=sys.stderr)
else: else:
print("not enough audio loopback devices ready...") print("not enough audio loopback devices ready...")
print("you should wait about 30 seconds...") print("you should wait about 30 seconds...")
sd._terminate() sd._terminate()
quit() sys.exit()
print(f"AUDIO OUTPUT DEVICE: {AUDIO_OUTPUT_DEVICE}", file=sys.stderr) print(f"AUDIO OUTPUT DEVICE: {AUDIO_OUTPUT_DEVICE}", file=sys.stderr)
# audio stream init # audio stream init
stream_tx = sd.RawStream(channels=1, dtype='int16', device=(0, AUDIO_OUTPUT_DEVICE), samplerate = AUDIO_SAMPLE_RATE_TX, blocksize=4800) stream_tx = sd.RawStream(
resampler = codec2.resampler() channels=1,
dtype="int16",
device=(0, AUDIO_OUTPUT_DEVICE),
samplerate=AUDIO_SAMPLE_RATE_TX,
blocksize=4800,
)
# data binary string resampler = codec2.resampler()
if args.TESTFRAMES:
# data binary string
if args.TESTFRAMES:
data_out = bytearray(14) data_out = bytearray(14)
data_out[:1] = bytes([255]) data_out[:1] = bytes([255])
data_out[1:2] = bytes([1]) data_out[1:2] = bytes([1])
data_out[2:] = b'HELLO WORLD' data_out[2:] = b"HELLO WORLD"
else:
data_out = b"HELLO WORLD!"
else: # ----------------------------------------------------------------
data_out = b'HELLO WORLD!'
# Open codec2 instance
freedv = ctypes.cast(codec2.api.freedv_open(MODE), ctypes.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 = ctypes.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 = ctypes.create_string_buffer(n_tx_preamble_modem_samples * 2)
# open codec2 instance # Init buffer for postamble
freedv = cast(codec2.api.freedv_open(MODE), c_void_p) 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)
# get number of bytes per frame for mode # Create buffer for data
bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(freedv)/8) # Use this if CRC16 checksum is required (DATA1-3)
payload_bytes_per_frame = bytes_per_frame -2 buffer = bytearray(payload_bytes_per_frame)
# set buffersize to length of data which will be send
buffer[: len(data_out)] = data_out
# init buffer for data # Create CRC for data frame - we are using the CRC function shipped with codec2 to avoid
n_tx_modem_samples = codec2.api.freedv_get_n_tx_modem_samples(freedv) # CRC algorithm incompatibilities
mod_out = create_string_buffer(n_tx_modem_samples * 2) # generate CRC16
crc = ctypes.c_ushort(
codec2.api.freedv_gen_crc16(bytes(buffer), payload_bytes_per_frame)
)
crc = crc.value.to_bytes(2, byteorder="big") # convert crc to 2 byte hex string
buffer += crc # append crc16 to buffer
# init buffer for preample print(
n_tx_preamble_modem_samples = codec2.api.freedv_get_n_tx_preamble_modem_samples(freedv) f"TOTAL BURSTS: {N_BURSTS} TOTAL FRAMES_PER_BURST: {N_FRAMES_PER_BURST}",
mod_out_preamble = create_string_buffer(n_tx_preamble_modem_samples * 2) file=sys.stderr,
)
# init buffer for postamble for brst in range(1, N_BURSTS + 1):
n_tx_postamble_modem_samples = codec2.api.freedv_get_n_tx_postamble_modem_samples(freedv) # Write preamble to txbuffer
mod_out_postamble = create_string_buffer(n_tx_postamble_modem_samples * 2)
# create buffer for data
buffer = bytearray(payload_bytes_per_frame) # use this if CRC16 checksum is required ( DATA1-3)
buffer[:len(data_out)] = data_out # 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
print(f"TOTAL BURSTS: {N_BURSTS} TOTAL FRAMES_PER_BURST: {N_FRAMES_PER_BURST}", file=sys.stderr)
for i in range(1,N_BURSTS+1):
# write preamble to txbuffer
codec2.api.freedv_rawdatapreambletx(freedv, mod_out_preamble) codec2.api.freedv_rawdatapreambletx(freedv, mod_out_preamble)
txbuffer = bytes(mod_out_preamble) txbuffer = bytes(mod_out_preamble)
# create modulaton for N = FRAMESPERBURST and append it to txbuffer # Create modulaton for N = FRAMESPERBURST and append it to txbuffer
for n in range(1,N_FRAMES_PER_BURST+1): for frm in range(1, N_FRAMES_PER_BURST + 1):
data = (ctypes.c_ubyte * bytes_per_frame).from_buffer_copy(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 # Modulate DATA and save it into mod_out pointer
codec2.api.freedv_rawdatatx(freedv, mod_out, data)
txbuffer += bytes(mod_out) txbuffer += bytes(mod_out)
print(f"TX BURST: {i}/{N_BURSTS} FRAME: {n}/{N_FRAMES_PER_BURST}", file=sys.stderr) print(
f"TX BURST: {brst}/{N_BURSTS} FRAME: {frm}/{N_FRAMES_PER_BURST}",
file=sys.stderr,
)
# append postamble to txbuffer # Append postamble to txbuffer
codec2.api.freedv_rawdatapostambletx(freedv, mod_out_postamble) codec2.api.freedv_rawdatapostambletx(freedv, mod_out_postamble)
txbuffer += bytes(mod_out_postamble) txbuffer += bytes(mod_out_postamble)
# append a delay between bursts as audio silence # Append a delay between bursts as audio silence
samples_delay = int(MODEM_SAMPLE_RATE*DELAY_BETWEEN_BURSTS) samples_delay = int(MODEM_SAMPLE_RATE * DELAY_BETWEEN_BURSTS)
mod_out_silence = create_string_buffer(samples_delay*2) mod_out_silence = ctypes.create_string_buffer(samples_delay * 2)
txbuffer += bytes(mod_out_silence) txbuffer += bytes(mod_out_silence)
#print(f"samples_delay: {samples_delay} DELAY_BETWEEN_BURSTS: {DELAY_BETWEEN_BURSTS}", file=sys.stderr) # print(f"samples_delay: {samples_delay} DELAY_BETWEEN_BURSTS: {DELAY_BETWEEN_BURSTS}", file=sys.stderr)
# resample up to 48k (resampler works on np.int16) # Resample up to 48k (resampler works on np.int16)
x = np.frombuffer(txbuffer, dtype=np.int16) np_buffer = np.frombuffer(txbuffer, dtype=np.int16)
txbuffer_48k = resampler.resample8_to_48(x) txbuffer_48k = resampler.resample8_to_48(np_buffer)
# check if we want to use an audio device or stdout # Check if we want to use an audio device or stdout
if AUDIO_OUTPUT_DEVICE != -1: if AUDIO_OUTPUT_DEVICE != -1:
stream_tx.start() stream_tx.start()
stream_tx.write(txbuffer_48k) stream_tx.write(txbuffer_48k)
else: else:
# print data to terminal for piping the output to other programs # Print data to terminal for piping the output to other programs
sys.stdout.buffer.write(txbuffer_48k) sys.stdout.buffer.write(txbuffer_48k)
sys.stdout.flush() sys.stdout.flush()
# and at last check if we had an opened audio instance and close it
# and at last check if we had an opened audio instance and close it if AUDIO_OUTPUT_DEVICE != -1:
if AUDIO_OUTPUT_DEVICE != -1:
sd._terminate() sd._terminate()
def parse_arguments():
# GET PARAMETER INPUTS
parser = argparse.ArgumentParser(description="Simons TEST TNC")
parser.add_argument("--bursts", dest="N_BURSTS", default=1, type=int)
parser.add_argument(
"--framesperburst", dest="N_FRAMES_PER_BURST", default=1, type=int
)
parser.add_argument(
"--delay",
dest="DELAY_BETWEEN_BURSTS",
default=500,
type=int,
help="delay between bursts in ms",
)
parser.add_argument(
"--mode", dest="FREEDV_MODE", type=str, choices=["datac0", "datac1", "datac3"]
)
parser.add_argument(
"--audiodev",
dest="AUDIO_OUTPUT_DEVICE",
default=-1,
type=int,
help="audio output device number to use, use -2 to automatically select a loopback device",
)
parser.add_argument(
"--list",
dest="LIST",
action="store_true",
help="list audio devices by number and exit",
)
parser.add_argument(
"--testframes",
dest="TESTFRAMES",
action="store_true",
default=False,
help="list audio devices by number and exit",
)
args, _ = parser.parse_known_args()
return args
if __name__ == "__main__":
test_tx()

View file

@ -191,7 +191,7 @@ class DAEMON:
options.append('--radiocontrol') options.append('--radiocontrol')
options.append(data[13]) options.append(data[13])
if data[13] != 'rigctld': if data[13] == 'rigctld':
options.append('--rigctld_ip') options.append('--rigctld_ip')
options.append(data[14]) options.append(data[14])

View file

@ -33,10 +33,11 @@ DATA_QUEUE_TRANSMIT = queue.Queue()
DATA_QUEUE_RECEIVED = queue.Queue() DATA_QUEUE_RECEIVED = queue.Queue()
class DATA(): class DATA:
""" Terminal Node Controller for FreeDATA """ """ Terminal Node Controller for FreeDATA """
def __init__(self): def __init__(self):
self.mycallsign = static.MYCALLSIGN # initial callsign. Will be overwritten later self.mycallsign = static.MYCALLSIGN # initial call sign. Will be overwritten later
self.data_queue_transmit = DATA_QUEUE_TRANSMIT self.data_queue_transmit = DATA_QUEUE_TRANSMIT
self.data_queue_received = DATA_QUEUE_RECEIVED self.data_queue_received = DATA_QUEUE_RECEIVED
@ -52,6 +53,7 @@ class DATA():
self.received_mycall_crc = b'' # Received my callsign crc if we received a crc for another ssid self.received_mycall_crc = b'' # Received my callsign crc if we received a crc for another ssid
self.data_channel_last_received = 0.0 # time of last "live sign" of a frame self.data_channel_last_received = 0.0 # time of last "live sign" of a frame
self.burst_ack_snr = 0 # SNR from received ack frames self.burst_ack_snr = 0 # SNR from received ack frames
self.burst_ack = False # if we received an acknowledge frame for a burst self.burst_ack = False # if we received an acknowledge frame for a burst
@ -59,6 +61,7 @@ class DATA():
self.rpt_request_received = False # if we received an request for repeater frames self.rpt_request_received = False # if we received an request for repeater frames
self.rpt_request_buffer = [] # requested frames, saved in a list self.rpt_request_buffer = [] # requested frames, saved in a list
self.rx_start_of_transmission = 0 # time of transmission start self.rx_start_of_transmission = 0 # time of transmission start
self.data_frame_bof = b'BOF' # 2 bytes for the BOF End of File indicator in a data frame self.data_frame_bof = b'BOF' # 2 bytes for the BOF End of File indicator in a data frame
self.data_frame_eof = b'EOF' # 2 bytes for the EOF End of File indicator in a data frame self.data_frame_eof = b'EOF' # 2 bytes for the EOF End of File indicator in a data frame
@ -73,17 +76,19 @@ class DATA():
self.mode_list_low_bw = [14, 12] self.mode_list_low_bw = [14, 12]
self.time_list_low_bw = [3, 7] self.time_list_low_bw = [3, 7]
self.mode_list_high_bw = [14, 12, 10] # 201 = FSK mode list of available modes, each mode will be used 2times per speed level
self.time_list_high_bw = [3, 7, 8, 30] # list for time to wait for correspinding mode in seconds
# mode list for selecting between low bandwith ( 500Hz ) and normal modes with higher bandwith self.mode_list_high_bw = [14, 12, 10] # mode list of available modes,each mode will be used 2 times per level
self.time_list_high_bw = [3, 7, 8, 30] # list for time to wait for corresponding mode in seconds
# mode list for selecting between low bandwidth ( 500Hz ) and normal modes with higher bandwidth
if static.LOW_BANDWITH_MODE: if static.LOW_BANDWITH_MODE:
self.mode_list = self.mode_list_low_bw # mode list of available modes, each mode will be used 2times per speed level self.mode_list = self.mode_list_low_bw # mode list of available modes, each mode will be used 2times per speed level
self.time_list = self.time_list_low_bw # list for time to wait for correspinding mode in seconds
self.time_list = self.time_list_low_bw # list for time to wait for corresponding mode in seconds
else: else:
self.mode_list = self.mode_list_high_bw # mode list of available modes, each mode will be used 2times per speed level self.mode_list = self.mode_list_high_bw # mode list of available modes, each mode will be used 2times per speed level
self.time_list = self.time_list_high_bw # list for time to wait for correspinding mode in seconds self.time_list = self.time_list_high_bw # list for time to wait for corresponding mode in seconds
self.speed_level = len(self.mode_list) - 1 # speed level for selecting mode self.speed_level = len(self.mode_list) - 1 # speed level for selecting mode
static.ARQ_SPEED_LEVEL = self.speed_level static.ARQ_SPEED_LEVEL = self.speed_level
@ -98,7 +103,9 @@ class DATA():
self.transmission_timeout = 360 # transmission timeout in seconds self.transmission_timeout = 360 # transmission timeout in seconds
worker_thread_transmit = threading.Thread(target=self.worker_transmit, name="worker thread transmit", daemon=True)
worker_thread_transmit = threading.Thread(target=self.worker_transmit, name="worker thread transmit",
daemon=True)
worker_thread_transmit.start() worker_thread_transmit.start()
worker_thread_receive = threading.Thread(target=self.worker_receive, name="worker thread receive", daemon=True) worker_thread_receive = threading.Thread(target=self.worker_receive, name="worker thread receive", daemon=True)
@ -284,7 +291,7 @@ class DATA():
self.arq_received_channel_is_open(bytes_out[:-2]) self.arq_received_channel_is_open(bytes_out[:-2])
# ARQ MANUAL MODE TRANSMISSION # ARQ MANUAL MODE TRANSMISSION
elif 230 <= frametype <= 240 : elif 230 <= frametype <= 240:
structlog.get_logger("structlog").debug("[TNC] ARQ manual mode") structlog.get_logger("structlog").debug("[TNC] ARQ manual mode")
self.arq_received_data_channel_opener(bytes_out[:-2]) self.arq_received_data_channel_opener(bytes_out[:-2])
@ -365,7 +372,7 @@ class DATA():
# set n frames per burst to modem # set n frames per burst to modem
# this is an idea, so it's not getting lost.... # this is an idea, so it's not getting lost....
# we need to work on this # we need to work on this
codec2.api.freedv_set_frames_per_burst(freedv,len(missing_frames)) codec2.api.freedv_set_frames_per_burst(freedv, len(missing_frames))
# TODO: Trim `missing_frames` bytesarray to [7:13] (6) frames, if it's larger. # TODO: Trim `missing_frames` bytesarray to [7:13] (6) frames, if it's larger.
@ -443,8 +450,6 @@ class DATA():
self.arq_file_transfer = True self.arq_file_transfer = True
RX_PAYLOAD_PER_MODEM_FRAME = bytes_per_frame - 2 # payload per moden frame
static.TNC_STATE = 'BUSY' static.TNC_STATE = 'BUSY'
static.ARQ_STATE = True static.ARQ_STATE = True
static.INFO.append("ARQ;RECEIVING") static.INFO.append("ARQ;RECEIVING")
@ -465,7 +470,8 @@ class DATA():
structlog.get_logger("structlog").debug("[TNC] static.RX_BURST_BUFFER", buffer=static.RX_BURST_BUFFER) structlog.get_logger("structlog").debug("[TNC] static.RX_BURST_BUFFER", buffer=static.RX_BURST_BUFFER)
helpers.add_to_heard_stations(static.DXCALLSIGN,static.DXGRID, 'DATA-CHANNEL', snr, static.FREQ_OFFSET, static.HAMLIB_FREQUENCY) helpers.add_to_heard_stations(static.DXCALLSIGN, static.DXGRID, 'DATA-CHANNEL', snr, static.FREQ_OFFSET,
static.HAMLIB_FREQUENCY)
# Check if we received all frames in the burst by checking if burst buffer has no more "Nones" # Check if we received all frames in the burst by checking if burst buffer has no more "Nones"
# This is the ideal case because we received all data # This is the ideal case because we received all data
@ -492,7 +498,7 @@ class DATA():
# search_area --> area where we want to search # search_area --> area where we want to search
search_area = 510 search_area = 510
search_position = len(static.RX_FRAME_BUFFER)-search_area search_position = len(static.RX_FRAME_BUFFER) - search_area
# find position of data. returns -1 if nothing found in area else >= 0 # find position of data. returns -1 if nothing found in area else >= 0
# we are beginning from the end, so if data exists twice or more, only the last one should be replaced # we are beginning from the end, so if data exists twice or more, only the last one should be replaced
get_position = static.RX_FRAME_BUFFER[search_position:].rfind(temp_burst_buffer) get_position = static.RX_FRAME_BUFFER[search_position:].rfind(temp_burst_buffer)
@ -500,8 +506,10 @@ class DATA():
if get_position >= 0: if get_position >= 0:
static.RX_FRAME_BUFFER = static.RX_FRAME_BUFFER[:search_position + get_position] static.RX_FRAME_BUFFER = static.RX_FRAME_BUFFER[:search_position + get_position]
static.RX_FRAME_BUFFER += temp_burst_buffer static.RX_FRAME_BUFFER += temp_burst_buffer
structlog.get_logger("structlog").warning("[TNC] ARQ | RX | replacing existing buffer data", area=search_area, pos=get_position)
# if we don't find data n this range, we really have new data and going to replace it structlog.get_logger("structlog").warning("[TNC] ARQ | RX | replacing existing buffer data",
area=search_area, pos=get_position)
# if we dont find data n this range, we really have new data and going to replace it
else: else:
static.RX_FRAME_BUFFER += temp_burst_buffer static.RX_FRAME_BUFFER += temp_burst_buffer
structlog.get_logger("structlog").debug("[TNC] ARQ | RX | appending data to buffer") structlog.get_logger("structlog").debug("[TNC] ARQ | RX | appending data to buffer")
@ -532,29 +540,31 @@ class DATA():
# calculate statistics # calculate statistics
self.calculate_transfer_rate_rx(self.rx_start_of_transmission, len(static.RX_FRAME_BUFFER)) self.calculate_transfer_rate_rx(self.rx_start_of_transmission, len(static.RX_FRAME_BUFFER))
elif RX_N_FRAME_OF_BURST == RX_N_FRAMES_PER_BURST -1: elif RX_N_FRAME_OF_BURST == RX_N_FRAMES_PER_BURST - 1:
# We have "Nones" in our rx buffer, # We have "Nones" in our rx buffer,
# Check if we received last frame of burst - this is an indicator for missed frames. # Check if we received last frame of burst - this is an indicator for missed frames.
# With this way of doing this, we always MUST receive the last frame of a burst otherwise the entire # With this way of doing this, we always MUST receive the last frame of a burst otherwise the entire
# burst is lost # burst is lost
structlog.get_logger("structlog").debug("[TNC] all frames in burst received:", frame=RX_N_FRAME_OF_BURST, frames=RX_N_FRAMES_PER_BURST) structlog.get_logger("structlog").debug("[TNC] all frames in burst received:", frame=RX_N_FRAME_OF_BURST,
frames=RX_N_FRAMES_PER_BURST)
self.send_retransmit_request_frame(freedv) self.send_retransmit_request_frame(freedv)
self.calculate_transfer_rate_rx(self.rx_start_of_transmission, len(static.RX_FRAME_BUFFER)) self.calculate_transfer_rate_rx(self.rx_start_of_transmission, len(static.RX_FRAME_BUFFER))
# Should never reach this point # Should never reach this point
else: else:
structlog.get_logger("structlog").error("[TNC] data_handler: Should not reach this point...", frame=RX_N_FRAME_OF_BURST, frames=RX_N_FRAMES_PER_BURST) structlog.get_logger("structlog").error("[TNC] data_handler: Should not reach this point...",
frame=RX_N_FRAME_OF_BURST, frames=RX_N_FRAMES_PER_BURST)
# We have a BOF and EOF flag in our data. If we received both we received our frame. # We have a BOF and EOF flag in our data. If we received both we received our frame.
# In case of loosing data but we received already a BOF and EOF we need to make sure, we # In case of loosing data, but we received already a BOF and EOF we need to make sure, we
# received the complete last burst by checking it for Nones # received the complete last burst by checking it for Nones
bof_position = static.RX_FRAME_BUFFER.find(self.data_frame_bof) bof_position = static.RX_FRAME_BUFFER.find(self.data_frame_bof)
eof_position = static.RX_FRAME_BUFFER.find(self.data_frame_eof) eof_position = static.RX_FRAME_BUFFER.find(self.data_frame_eof)
# get total bytes per transmission information as soon we recevied a frame with a BOF # get total bytes per transmission information as soon we recevied a frame with a BOF
if bof_position >=0:
payload = static.RX_FRAME_BUFFER[bof_position+len(self.data_frame_bof):eof_position] if bof_position >= 0:
payload = static.RX_FRAME_BUFFER[bof_position + len(self.data_frame_bof):eof_position]
frame_length = int.from_bytes(payload[4:8], "big") # 4:8 4bytes frame_length = int.from_bytes(payload[4:8], "big") # 4:8 4bytes
static.TOTAL_BYTES = frame_length static.TOTAL_BYTES = frame_length
compression_factor = int.from_bytes(payload[8:9], "big") # 4:8 4bytes compression_factor = int.from_bytes(payload[8:9], "big") # 4:8 4bytes
@ -563,13 +573,14 @@ class DATA():
self.calculate_transfer_rate_rx(self.rx_start_of_transmission, len(static.RX_FRAME_BUFFER)) self.calculate_transfer_rate_rx(self.rx_start_of_transmission, len(static.RX_FRAME_BUFFER))
if bof_position >= 0 and eof_position > 0 and None not in static.RX_BURST_BUFFER: if bof_position >= 0 and eof_position > 0 and None not in static.RX_BURST_BUFFER:
structlog.get_logger("structlog").debug("[TNC] arq_data_received:", bof_position=bof_position, eof_position=eof_position) structlog.get_logger("structlog").debug("[TNC] arq_data_received:", bof_position=bof_position,
eof_position=eof_position)
# print(f"bof_position {bof_position} / eof_position {eof_position}") # print(f"bof_position {bof_position} / eof_position {eof_position}")
self.rx_frame_bof_received = True self.rx_frame_bof_received = True
self.rx_frame_eof_received = True self.rx_frame_eof_received = True
# Extract raw data from buffer # Extract raw data from buffer
payload = static.RX_FRAME_BUFFER[bof_position+len(self.data_frame_bof):eof_position] payload = static.RX_FRAME_BUFFER[bof_position + len(self.data_frame_bof):eof_position]
# Get the data frame crc # Get the data frame crc
data_frame_crc = payload[:4] # 0:4 4bytes data_frame_crc = payload[:4] # 0:4 4bytes
# Get the data frame length # Get the data frame length
@ -603,23 +614,30 @@ class DATA():
# Re-code data_frame in base64, UTF-8 for JSON UI communication. # Re-code data_frame in base64, UTF-8 for JSON UI communication.
base64_data = base64.b64encode(data_frame).decode("utf-8") base64_data = base64.b64encode(data_frame).decode("utf-8")
static.RX_BUFFER.append([uniqueid, timestamp, static.DXCALLSIGN, static.DXGRID, base64_data]) static.RX_BUFFER.append([uniqueid, timestamp, static.DXCALLSIGN, static.DXGRID, base64_data])
jsondata = {"arq":"received", "uuid" : uniqueid, "timestamp": timestamp, "mycallsign" : str(mycallsign, 'utf-8'), "dxcallsign": str(static.DXCALLSIGN, 'utf-8'), "dxgrid": str(static.DXGRID, 'utf-8'), "data": base64_data} jsondata = {"arq": "received", "uuid": uniqueid, "timestamp": timestamp,
"mycallsign": str(mycallsign, 'utf-8'), "dxcallsign": str(static.DXCALLSIGN, 'utf-8'),
"dxgrid": str(static.DXGRID, 'utf-8'), "data": base64_data}
json_data_out = json.dumps(jsondata) json_data_out = json.dumps(jsondata)
structlog.get_logger("structlog").debug("[TNC] arq_data_received:", jsondata=jsondata) structlog.get_logger("structlog").debug("[TNC] arq_data_received:", jsondata=jsondata)
sock.SOCKET_QUEUE.put(json_data_out) sock.SOCKET_QUEUE.put(json_data_out)
static.INFO.append("ARQ;RECEIVING;SUCCESS") static.INFO.append("ARQ;RECEIVING;SUCCESS")
structlog.get_logger("structlog").info("[TNC] ARQ | RX | SENDING DATA FRAME ACK", snr=snr, crc=data_frame_crc.hex()) structlog.get_logger("structlog").info("[TNC] ARQ | RX | SENDING DATA FRAME ACK", snr=snr,
crc=data_frame_crc.hex())
self.send_data_ack_frame(snr) self.send_data_ack_frame(snr)
# update our statistics AFTER the frame ACK # update our statistics AFTER the frame ACK
self.calculate_transfer_rate_rx(self.rx_start_of_transmission, len(static.RX_FRAME_BUFFER)) self.calculate_transfer_rate_rx(self.rx_start_of_transmission, len(static.RX_FRAME_BUFFER))
structlog.get_logger("structlog").info("[TNC] | RX | DATACHANNEL [" + structlog.get_logger("structlog").info("[TNC] | RX | DATACHANNEL [" +
str(self.mycallsign, 'utf-8') + "]<< >>[" + str(static.DXCALLSIGN, 'utf-8') + "]", snr=snr) str(self.mycallsign, 'utf-8') + "]<< >>[" + str(
static.DXCALLSIGN, 'utf-8') + "]", snr=snr)
else: else:
static.INFO.append("ARQ;RECEIVING;FAILED") static.INFO.append("ARQ;RECEIVING;FAILED")
structlog.get_logger("structlog").warning("[TNC] ARQ | RX | DATA FRAME NOT SUCESSFULLY RECEIVED!", e="wrong crc", expected=data_frame_crc, received=data_frame_crc_received, overflows=static.BUFFER_OVERFLOW_COUNTER) structlog.get_logger("structlog").warning("[TNC] ARQ | RX | DATA FRAME NOT SUCESSFULLY RECEIVED!",
e="wrong crc", expected=data_frame_crc,
received=data_frame_crc_received,
overflows=static.BUFFER_OVERFLOW_COUNTER)
structlog.get_logger("structlog").info("[TNC] ARQ | RX | Sending NACK") structlog.get_logger("structlog").info("[TNC] ARQ | RX | Sending NACK")
self.send_burst_nack_frame(snr) self.send_burst_nack_frame(snr)
@ -632,7 +650,7 @@ class DATA():
if not TESTMODE: if not TESTMODE:
self.arq_cleanup() self.arq_cleanup()
def arq_transmit(self, data_out:bytes, mode:int, n_frames_per_burst:int): def arq_transmit(self, data_out: bytes, mode: int, n_frames_per_burst: int):
""" """
Args: Args:
@ -665,7 +683,8 @@ class DATA():
frame_total_size = len(data_out).to_bytes(4, byteorder='big') frame_total_size = len(data_out).to_bytes(4, byteorder='big')
static.INFO.append("ARQ;TRANSMITTING") static.INFO.append("ARQ;TRANSMITTING")
jsondata = {"arq":"transmission", "status" :"transmitting", "uuid" : self.transmission_uuid, "percent" : static.ARQ_TRANSMISSION_PERCENT, "bytesperminute" : static.ARQ_BYTES_PER_MINUTE} jsondata = {"arq": "transmission", "status": "transmitting", "uuid": self.transmission_uuid,
"percent": static.ARQ_TRANSMISSION_PERCENT, "bytesperminute": static.ARQ_BYTES_PER_MINUTE}
json_data_out = json.dumps(jsondata) json_data_out = json.dumps(jsondata)
sock.SOCKET_QUEUE.put(json_data_out) sock.SOCKET_QUEUE.put(json_data_out)
@ -690,7 +709,7 @@ class DATA():
# data_out = self.data_frame_bof + frame_payload_crc + data_out + self.data_frame_eof # data_out = self.data_frame_bof + frame_payload_crc + data_out + self.data_frame_eof
data_out = self.data_frame_bof + frame_payload_crc + frame_total_size + compression_factor + data_out + self.data_frame_eof data_out = self.data_frame_bof + frame_payload_crc + frame_total_size + compression_factor + data_out + self.data_frame_eof
#initial bufferposition is 0 # initial bufferposition is 0
bufferposition = bufferposition_end = 0 bufferposition = bufferposition_end = 0
# iterate through data out buffer # iterate through data out buffer
@ -730,10 +749,11 @@ class DATA():
static.ARQ_SPEED_LEVEL = self.speed_level static.ARQ_SPEED_LEVEL = self.speed_level
data_mode = self.mode_list[self.speed_level] data_mode = self.mode_list[self.speed_level]
structlog.get_logger("structlog").debug("[TNC] Speed-level:", level=self.speed_level, retry=self.tx_n_retry_of_burst, mode=data_mode) structlog.get_logger("structlog").debug("[TNC] Speed-level:", level=self.speed_level,
retry=self.tx_n_retry_of_burst, mode=data_mode)
# payload information # payload information
payload_per_frame = modem.get_bytes_per_frame(data_mode) -2 payload_per_frame = modem.get_bytes_per_frame(data_mode) - 2
# tempbuffer list for storing our data frames # tempbuffer list for storing our data frames
tempbuffer = [] tempbuffer = []
@ -762,19 +782,20 @@ class DATA():
# the last bytes of a frame # the last bytes of a frame
else: else:
extended_data_out = data_out[bufferposition:] extended_data_out = data_out[bufferposition:]
extended_data_out += bytes([0]) * (payload_per_frame-len(extended_data_out)-len(arqheader)) extended_data_out += bytes([0]) * (payload_per_frame - len(extended_data_out) - len(arqheader))
frame = arqheader + extended_data_out frame = arqheader + extended_data_out
# append frame to tempbuffer for transmission # append frame to tempbuffer for transmission
tempbuffer.append(frame) tempbuffer.append(frame)
structlog.get_logger("structlog").debug("[TNC] tempbuffer:", tempbuffer=tempbuffer) structlog.get_logger("structlog").debug("[TNC] tempbuffer:", tempbuffer=tempbuffer)
structlog.get_logger("structlog").info("[TNC] ARQ | TX | FRAMES", mode=data_mode, fpb=TX_N_FRAMES_PER_BURST, retry=self.tx_n_retry_of_burst) structlog.get_logger("structlog").info("[TNC] ARQ | TX | FRAMES", mode=data_mode,
fpb=TX_N_FRAMES_PER_BURST, retry=self.tx_n_retry_of_burst)
# we need to set our TRANSMITTING flag before we are adding an object the transmit queue # we need to set our TRANSMITTING flag before we are adding an object the transmit queue
# this is not that nice, we could improve this somehow # this is not that nice, we could improve this somehow
static.TRANSMITTING = True static.TRANSMITTING = True
modem.MODEM_TRANSMIT_QUEUE.put([data_mode,1,0,tempbuffer]) modem.MODEM_TRANSMIT_QUEUE.put([data_mode, 1, 0, tempbuffer])
# wait while transmitting # wait while transmitting
while static.TRANSMITTING: while static.TRANSMITTING:
@ -794,6 +815,7 @@ class DATA():
# once we received a burst ack, reset its state and break the RETRIES loop # once we received a burst ack, reset its state and break the RETRIES loop
if self.burst_ack: if self.burst_ack:
self.burst_ack = False # reset ack state self.burst_ack = False # reset ack state
self.tx_n_retry_of_burst = 0 # reset retries self.tx_n_retry_of_burst = 0 # reset retries
break # break retry loop break # break retry loop
@ -816,7 +838,9 @@ class DATA():
self.calculate_transfer_rate_tx(tx_start_of_transmission, bufferposition_end, len(data_out)) self.calculate_transfer_rate_tx(tx_start_of_transmission, bufferposition_end, len(data_out))
# NEXT ATTEMPT # NEXT ATTEMPT
structlog.get_logger("structlog").debug("[TNC] ATTEMPT:", retry=self.tx_n_retry_of_burst, maxretries=TX_N_MAX_RETRIES_PER_BURST, overflows=static.BUFFER_OVERFLOW_COUNTER) structlog.get_logger("structlog").debug("[TNC] ATTEMPT:", retry=self.tx_n_retry_of_burst,
maxretries=TX_N_MAX_RETRIES_PER_BURST,
overflows=static.BUFFER_OVERFLOW_COUNTER)
# update buffer position # update buffer position
bufferposition = bufferposition_end bufferposition = bufferposition_end
@ -824,7 +848,8 @@ class DATA():
# update stats # update stats
self.calculate_transfer_rate_tx(tx_start_of_transmission, bufferposition_end, len(data_out)) self.calculate_transfer_rate_tx(tx_start_of_transmission, bufferposition_end, len(data_out))
jsondata = {"arq":"transmission", "status" :"transmitting", "uuid" : self.transmission_uuid, "percent" : static.ARQ_TRANSMISSION_PERCENT, "bytesperminute" : static.ARQ_BYTES_PER_MINUTE} jsondata = {"arq": "transmission", "status": "transmitting", "uuid": self.transmission_uuid,
"percent": static.ARQ_TRANSMISSION_PERCENT, "bytesperminute": static.ARQ_BYTES_PER_MINUTE}
json_data_out = json.dumps(jsondata) json_data_out = json.dumps(jsondata)
sock.SOCKET_QUEUE.put(json_data_out) sock.SOCKET_QUEUE.put(json_data_out)
@ -832,19 +857,25 @@ class DATA():
if self.data_frame_ack_received: if self.data_frame_ack_received:
static.INFO.append("ARQ;TRANSMITTING;SUCCESS") static.INFO.append("ARQ;TRANSMITTING;SUCCESS")
jsondata = {"arq":"transmission", "status" :"success", "uuid" : self.transmission_uuid, "percent" : static.ARQ_TRANSMISSION_PERCENT, "bytesperminute" : static.ARQ_BYTES_PER_MINUTE} jsondata = {"arq": "transmission", "status": "success", "uuid": self.transmission_uuid,
"percent": static.ARQ_TRANSMISSION_PERCENT, "bytesperminute": static.ARQ_BYTES_PER_MINUTE}
json_data_out = json.dumps(jsondata) json_data_out = json.dumps(jsondata)
sock.SOCKET_QUEUE.put(json_data_out) sock.SOCKET_QUEUE.put(json_data_out)
structlog.get_logger("structlog").info("[TNC] ARQ | TX | DATA TRANSMITTED!", BytesPerMinute=static.ARQ_BYTES_PER_MINUTE, BitsPerSecond=static.ARQ_BITS_PER_SECOND, overflows=static.BUFFER_OVERFLOW_COUNTER) structlog.get_logger("structlog").info("[TNC] ARQ | TX | DATA TRANSMITTED!",
BytesPerMinute=static.ARQ_BYTES_PER_MINUTE,
BitsPerSecond=static.ARQ_BITS_PER_SECOND,
overflows=static.BUFFER_OVERFLOW_COUNTER)
else: else:
static.INFO.append("ARQ;TRANSMITTING;FAILED") static.INFO.append("ARQ;TRANSMITTING;FAILED")
jsondata = {"arq":"transmission", "status" :"failed", "uuid" : self.transmission_uuid, "percent" : static.ARQ_TRANSMISSION_PERCENT, "bytesperminute" : static.ARQ_BYTES_PER_MINUTE} jsondata = {"arq": "transmission", "status": "failed", "uuid": self.transmission_uuid,
"percent": static.ARQ_TRANSMISSION_PERCENT, "bytesperminute": static.ARQ_BYTES_PER_MINUTE}
json_data_out = json.dumps(jsondata) json_data_out = json.dumps(jsondata)
sock.SOCKET_QUEUE.put(json_data_out) sock.SOCKET_QUEUE.put(json_data_out)
structlog.get_logger("structlog").info("[TNC] ARQ | TX | TRANSMISSION FAILED OR TIME OUT!", overflows=static.BUFFER_OVERFLOW_COUNTER) structlog.get_logger("structlog").info("[TNC] ARQ | TX | TRANSMISSION FAILED OR TIME OUT!",
overflows=static.BUFFER_OVERFLOW_COUNTER)
self.stop_transmission() self.stop_transmission()
# and last but not least doing a state cleanup # and last but not least doing a state cleanup
@ -856,7 +887,7 @@ class DATA():
sys.exit(0) sys.exit(0)
# signalling frames received # signalling frames received
def burst_ack_received(self, data_in:bytes): def burst_ack_received(self, data_in: bytes):
""" """
Args: Args:
@ -872,11 +903,12 @@ class DATA():
# only process data if we are in ARQ and BUSY state # only process data if we are in ARQ and BUSY state
if static.ARQ_STATE: if static.ARQ_STATE:
helpers.add_to_heard_stations(static.DXCALLSIGN,static.DXGRID, 'DATA-CHANNEL', static.SNR, static.FREQ_OFFSET, static.HAMLIB_FREQUENCY) helpers.add_to_heard_stations(static.DXCALLSIGN, static.DXGRID, 'DATA-CHANNEL', static.SNR,
static.FREQ_OFFSET, static.HAMLIB_FREQUENCY)
self.burst_ack = True # Force data loops of TNC to stop and continue with next frame self.burst_ack = True # Force data loops of TNC to stop and continue with next frame
self.data_channel_last_received = int(time.time()) # we need to update our timeout timestamp self.data_channel_last_received = int(time.time()) # we need to update our timeout timestamp
self.burst_ack_snr= int.from_bytes(bytes(data_in[5:6]), "big") self.burst_ack_snr = int.from_bytes(bytes(data_in[7:8]), "big")
self.speed_level= int.from_bytes(bytes(data_in[6:7]), "big") self.speed_level = int.from_bytes(bytes(data_in[8:9]), "big")
static.ARQ_SPEED_LEVEL = self.speed_level static.ARQ_SPEED_LEVEL = self.speed_level
structlog.get_logger("structlog").debug("[TNC] burst_ack_received:", speed_level=self.speed_level) structlog.get_logger("structlog").debug("[TNC] burst_ack_received:", speed_level=self.speed_level)
# print(self.speed_level) # print(self.speed_level)
@ -886,7 +918,7 @@ class DATA():
self.n_retries_per_burst = 0 self.n_retries_per_burst = 0
# signalling frames received # signalling frames received
def burst_nack_received(self, data_in:bytes): def burst_nack_received(self, data_in: bytes):
""" """
Args: Args:
@ -902,11 +934,12 @@ class DATA():
# only process data if we are in ARQ and BUSY state # only process data if we are in ARQ and BUSY state
if static.ARQ_STATE: if static.ARQ_STATE:
helpers.add_to_heard_stations(static.DXCALLSIGN,static.DXGRID, 'DATA-CHANNEL', static.SNR, static.FREQ_OFFSET, static.HAMLIB_FREQUENCY) helpers.add_to_heard_stations(static.DXCALLSIGN, static.DXGRID, 'DATA-CHANNEL', static.SNR,
static.FREQ_OFFSET, static.HAMLIB_FREQUENCY)
self.burst_nack = True # Force data loops of TNC to stop and continue with next frame self.burst_nack = True # Force data loops of TNC to stop and continue with next frame
self.data_channel_last_received = int(time.time()) # we need to update our timeout timestamp self.data_channel_last_received = int(time.time()) # we need to update our timeout timestamp
self.burst_ack_snr= int.from_bytes(bytes(data_in[5:6]), "big") self.burst_ack_snr = int.from_bytes(bytes(data_in[7:8]), "big")
self.speed_level= int.from_bytes(bytes(data_in[6:7]), "big") self.speed_level = int.from_bytes(bytes(data_in[8:9]), "big")
static.ARQ_SPEED_LEVEL = self.speed_level static.ARQ_SPEED_LEVEL = self.speed_level
self.burst_nack_counter += 1 self.burst_nack_counter += 1
structlog.get_logger("structlog").debug("[TNC] burst_nack_received:", speed_level=self.speed_level) structlog.get_logger("structlog").debug("[TNC] burst_nack_received:", speed_level=self.speed_level)
@ -916,12 +949,13 @@ class DATA():
""" """ """ """
# only process data if we are in ARQ and BUSY state # only process data if we are in ARQ and BUSY state
if static.ARQ_STATE: if static.ARQ_STATE:
helpers.add_to_heard_stations(static.DXCALLSIGN,static.DXGRID, 'DATA-CHANNEL', static.SNR, static.FREQ_OFFSET, static.HAMLIB_FREQUENCY) helpers.add_to_heard_stations(static.DXCALLSIGN, static.DXGRID, 'DATA-CHANNEL', static.SNR,
static.FREQ_OFFSET, static.HAMLIB_FREQUENCY)
self.data_frame_ack_received = True # Force data loops of TNC to stop and continue with next frame self.data_frame_ack_received = True # Force data loops of TNC to stop and continue with next frame
self.data_channel_last_received = int(time.time()) # we need to update our timeout timestamp self.data_channel_last_received = int(time.time()) # we need to update our timeout timestamp
self.arq_session_last_received = int(time.time()) # we need to update our timeout timestamp self.arq_session_last_received = int(time.time()) # we need to update our timeout timestamp
def frame_nack_received(self, data_in:bytes): # pylint: disable=unused-argument def frame_nack_received(self, data_in: bytes): # pylint: disable=unused-argument
""" """
Args: Args:
@ -930,9 +964,11 @@ class DATA():
Returns: Returns:
""" """
helpers.add_to_heard_stations(static.DXCALLSIGN,static.DXGRID, 'DATA-CHANNEL', static.SNR, static.FREQ_OFFSET, static.HAMLIB_FREQUENCY) helpers.add_to_heard_stations(static.DXCALLSIGN, static.DXGRID, 'DATA-CHANNEL', static.SNR, static.FREQ_OFFSET,
static.HAMLIB_FREQUENCY)
static.INFO.append("ARQ;TRANSMITTING;FAILED") static.INFO.append("ARQ;TRANSMITTING;FAILED")
jsondata = {"arq":"transmission", "status" : "failed", "uuid" : self.transmission_uuid, "percent" : static.ARQ_TRANSMISSION_PERCENT, "bytesperminute" : static.ARQ_BYTES_PER_MINUTE} jsondata = {"arq": "transmission", "status": "failed", "uuid": self.transmission_uuid,
"percent": static.ARQ_TRANSMISSION_PERCENT, "bytesperminute": static.ARQ_BYTES_PER_MINUTE}
json_data_out = json.dumps(jsondata) json_data_out = json.dumps(jsondata)
sock.SOCKET_QUEUE.put(json_data_out) sock.SOCKET_QUEUE.put(json_data_out)
self.arq_session_last_received = int(time.time()) # we need to update our timeout timestamp self.arq_session_last_received = int(time.time()) # we need to update our timeout timestamp
@ -940,7 +976,7 @@ class DATA():
if not TESTMODE: if not TESTMODE:
self.arq_cleanup() self.arq_cleanup()
def burst_rpt_received(self, data_in:bytes): def burst_rpt_received(self, data_in: bytes):
""" """
Args: Args:
@ -951,7 +987,8 @@ class DATA():
""" """
# only process data if we are in ARQ and BUSY state # only process data if we are in ARQ and BUSY state
if static.ARQ_STATE and static.TNC_STATE == 'BUSY': if static.ARQ_STATE and static.TNC_STATE == 'BUSY':
helpers.add_to_heard_stations(static.DXCALLSIGN,static.DXGRID, 'DATA-CHANNEL', static.SNR, static.FREQ_OFFSET, static.HAMLIB_FREQUENCY) helpers.add_to_heard_stations(static.DXCALLSIGN, static.DXGRID, 'DATA-CHANNEL', static.SNR,
static.FREQ_OFFSET, static.HAMLIB_FREQUENCY)
self.rpt_request_received = True self.rpt_request_received = True
self.data_channel_last_received = int(time.time()) # we need to update our timeout timestamp self.data_channel_last_received = int(time.time()) # we need to update our timeout timestamp
@ -978,7 +1015,9 @@ class DATA():
""" """
# TODO: we need to check this, maybe placing it to class init # TODO: we need to check this, maybe placing it to class init
self.datachannel_timeout = False self.datachannel_timeout = False
structlog.get_logger("structlog").info("[TNC] SESSION [" + str(self.mycallsign, 'utf-8') + "]>> <<[" + str(static.DXCALLSIGN, 'utf-8') + "]", state=static.ARQ_SESSION_STATE) structlog.get_logger("structlog").info(
"[TNC] SESSION [" + str(self.mycallsign, 'utf-8') + "]>> <<[" + str(static.DXCALLSIGN, 'utf-8') + "]",
state=static.ARQ_SESSION_STATE)
self.open_session(callsign) self.open_session(callsign)
@ -1014,8 +1053,11 @@ class DATA():
while not static.ARQ_SESSION: while not static.ARQ_SESSION:
time.sleep(0.01) time.sleep(0.01)
for attempt in range(1,self.session_connect_max_retries+1): for attempt in range(1, self.session_connect_max_retries + 1):
structlog.get_logger("structlog").info("[TNC] SESSION [" + str(self.mycallsign, 'utf-8') + "]>>?<<[" + str(static.DXCALLSIGN, 'utf-8') + "]", a=attempt, state=static.ARQ_SESSION_STATE) structlog.get_logger("structlog").info(
"[TNC] SESSION [" + str(self.mycallsign, 'utf-8') + "]>>?<<[" + str(static.DXCALLSIGN,
'utf-8') + "]", a=attempt,
state=static.ARQ_SESSION_STATE)
self.enqueue_frame_for_tx(connection_frame) self.enqueue_frame_for_tx(connection_frame)
@ -1035,7 +1077,7 @@ class DATA():
self.close_session() self.close_session()
return False return False
def received_session_opener(self, data_in:bytes): def received_session_opener(self, data_in: bytes):
""" """
Args: Args:
@ -1052,8 +1094,11 @@ class DATA():
static.DXCALLSIGN_CRC = bytes(data_in[4:7]) static.DXCALLSIGN_CRC = bytes(data_in[4:7])
static.DXCALLSIGN = helpers.bytes_to_callsign(bytes(data_in[7:13])) static.DXCALLSIGN = helpers.bytes_to_callsign(bytes(data_in[7:13]))
helpers.add_to_heard_stations(static.DXCALLSIGN,static.DXGRID, 'DATA-CHANNEL', static.SNR, static.FREQ_OFFSET, static.HAMLIB_FREQUENCY) helpers.add_to_heard_stations(static.DXCALLSIGN, static.DXGRID, 'DATA-CHANNEL', static.SNR, static.FREQ_OFFSET,
structlog.get_logger("structlog").info("[TNC] SESSION [" + str(self.mycallsign, 'utf-8') + "]>>|<<[" + str(static.DXCALLSIGN, 'utf-8') + "]", state=static.ARQ_SESSION_STATE) static.HAMLIB_FREQUENCY)
structlog.get_logger("structlog").info(
"[TNC] SESSION [" + str(self.mycallsign, 'utf-8') + "]>>|<<[" + str(static.DXCALLSIGN, 'utf-8') + "]",
state=static.ARQ_SESSION_STATE)
static.ARQ_SESSION = True static.ARQ_SESSION = True
static.TNC_STATE = 'BUSY' static.TNC_STATE = 'BUSY'
@ -1062,8 +1107,11 @@ class DATA():
def close_session(self): def close_session(self):
""" Close the ARQ session """ """ Close the ARQ session """
static.ARQ_SESSION_STATE = 'disconnecting' static.ARQ_SESSION_STATE = 'disconnecting'
helpers.add_to_heard_stations(static.DXCALLSIGN,static.DXGRID, 'DATA-CHANNEL', static.SNR, static.FREQ_OFFSET, static.HAMLIB_FREQUENCY) helpers.add_to_heard_stations(static.DXCALLSIGN, static.DXGRID, 'DATA-CHANNEL', static.SNR, static.FREQ_OFFSET,
structlog.get_logger("structlog").info("[TNC] SESSION [" + str(self.mycallsign, 'utf-8') + "]<<X>>[" + str(static.DXCALLSIGN, 'utf-8') + "]", state=static.ARQ_SESSION_STATE) static.HAMLIB_FREQUENCY)
structlog.get_logger("structlog").info(
"[TNC] SESSION [" + str(self.mycallsign, 'utf-8') + "]<<X>>[" + str(static.DXCALLSIGN, 'utf-8') + "]",
state=static.ARQ_SESSION_STATE)
static.INFO.append("ARQ;SESSION;CLOSE") static.INFO.append("ARQ;SESSION;CLOSE")
self.IS_ARQ_SESSION_MASTER = False self.IS_ARQ_SESSION_MASTER = False
static.ARQ_SESSION = False static.ARQ_SESSION = False
@ -1072,7 +1120,7 @@ class DATA():
self.send_disconnect_frame() self.send_disconnect_frame()
def received_session_close(self, data_in:bytes): def received_session_close(self, data_in: bytes):
""" """
Closes the session when a close session frame is received and Closes the session when a close session frame is received and
the DXCALLSIGN_CRC matches the remote station participating in the session. the DXCALLSIGN_CRC matches the remote station participating in the session.
@ -1086,8 +1134,11 @@ class DATA():
_valid_crc, _ = helpers.check_callsign(static.DXCALLSIGN, bytes(data_in[4:7])) _valid_crc, _ = helpers.check_callsign(static.DXCALLSIGN, bytes(data_in[4:7]))
if _valid_crc: if _valid_crc:
static.ARQ_SESSION_STATE = 'disconnected' static.ARQ_SESSION_STATE = 'disconnected'
helpers.add_to_heard_stations(static.DXCALLSIGN,static.DXGRID, 'DATA-CHANNEL', static.SNR, static.FREQ_OFFSET, static.HAMLIB_FREQUENCY) helpers.add_to_heard_stations(static.DXCALLSIGN, static.DXGRID, 'DATA-CHANNEL', static.SNR,
structlog.get_logger("structlog").info("[TNC] SESSION [" + str(self.mycallsign, 'utf-8') + "]<<X>>[" + str(static.DXCALLSIGN, 'utf-8') + "]", state=static.ARQ_SESSION_STATE) static.FREQ_OFFSET, static.HAMLIB_FREQUENCY)
structlog.get_logger("structlog").info(
"[TNC] SESSION [" + str(self.mycallsign, 'utf-8') + "]<<X>>[" + str(static.DXCALLSIGN, 'utf-8') + "]",
state=static.ARQ_SESSION_STATE)
static.INFO.append("ARQ;SESSION;CLOSE") static.INFO.append("ARQ;SESSION;CLOSE")
self.IS_ARQ_SESSION_MASTER = False self.IS_ARQ_SESSION_MASTER = False
@ -1107,7 +1158,7 @@ class DATA():
self.enqueue_frame_for_tx(connection_frame) self.enqueue_frame_for_tx(connection_frame)
def received_session_heartbeat(self, data_in:bytes): def received_session_heartbeat(self, data_in: bytes):
""" """
Args: Args:
@ -1120,7 +1171,8 @@ class DATA():
_valid_crc, _ = helpers.check_callsign(static.DXCALLSIGN, bytes(data_in[4:7])) _valid_crc, _ = helpers.check_callsign(static.DXCALLSIGN, bytes(data_in[4:7]))
if _valid_crc: if _valid_crc:
structlog.get_logger("structlog").debug("[TNC] Received session heartbeat") structlog.get_logger("structlog").debug("[TNC] Received session heartbeat")
helpers.add_to_heard_stations(static.DXCALLSIGN, static.DXGRID, 'SESSION-HB', static.SNR, static.FREQ_OFFSET, static.HAMLIB_FREQUENCY) helpers.add_to_heard_stations(static.DXCALLSIGN, static.DXGRID, 'SESSION-HB', static.SNR,
static.FREQ_OFFSET, static.HAMLIB_FREQUENCY)
self.arq_session_last_received = int(time.time()) # we need to update our timeout timestamp self.arq_session_last_received = int(time.time()) # we need to update our timeout timestamp
@ -1134,7 +1186,8 @@ class DATA():
# ############################################################################################################ # ############################################################################################################
# ARQ DATA CHANNEL HANDLER # ARQ DATA CHANNEL HANDLER
# ############################################################################################################ # ############################################################################################################
def open_dc_and_transmit(self, data_out:bytes, mode:int, n_frames_per_burst:int, transmission_uuid:str, mycallsign): def open_dc_and_transmit(self, data_out: bytes, mode: int, n_frames_per_burst: int, transmission_uuid: str,
mycallsign):
""" """
Args: Args:
@ -1175,7 +1228,7 @@ class DATA():
else: else:
return False return False
def arq_open_data_channel(self, mode:int, n_frames_per_burst:int, mycallsign): def arq_open_data_channel(self, mode: int, n_frames_per_burst: int, mycallsign):
""" """
Args: Args:
@ -1209,9 +1262,12 @@ class DATA():
while not static.ARQ_STATE: while not static.ARQ_STATE:
time.sleep(0.01) time.sleep(0.01)
for attempt in range(1,self.data_channel_max_retries+1): for attempt in range(1, self.data_channel_max_retries + 1):
static.INFO.append("DATACHANNEL;OPENING") static.INFO.append("DATACHANNEL;OPENING")
structlog.get_logger("structlog").info("[TNC] ARQ | DATA | TX | [" + str(mycallsign, 'utf-8') + "]>> <<[" + str(static.DXCALLSIGN, 'utf-8') + "]", attempt=f"{str(attempt)}/{str(self.data_channel_max_retries)}") structlog.get_logger("structlog").info(
"[TNC] ARQ | DATA | TX | [" + str(mycallsign, 'utf-8') + "]>> <<[" + str(static.DXCALLSIGN,
'utf-8') + "]",
attempt=f"{str(attempt)}/{str(self.data_channel_max_retries)}")
self.enqueue_frame_for_tx(connection_frame) self.enqueue_frame_for_tx(connection_frame)
@ -1227,12 +1283,19 @@ class DATA():
if attempt == self.data_channel_max_retries: if attempt == self.data_channel_max_retries:
static.INFO.append("DATACHANNEL;FAILED") static.INFO.append("DATACHANNEL;FAILED")
structlog.get_logger("structlog").debug("[TNC] arq_open_data_channel:", transmission_uuid=self.transmission_uuid)
jsondata = {"arq":"transmission", "status" :"failed", "uuid" : self.transmission_uuid, "percent" : static.ARQ_TRANSMISSION_PERCENT, "bytesperminute" : static.ARQ_BYTES_PER_MINUTE} structlog.get_logger("structlog").debug("[TNC] arq_open_data_channel:",
transmission_uuid=self.transmission_uuid)
# print(self.transmission_uuid)
jsondata = {"arq": "transmission", "status": "failed", "uuid": self.transmission_uuid,
"percent": static.ARQ_TRANSMISSION_PERCENT,
"bytesperminute": static.ARQ_BYTES_PER_MINUTE}
json_data_out = json.dumps(jsondata) json_data_out = json.dumps(jsondata)
sock.SOCKET_QUEUE.put(json_data_out) sock.SOCKET_QUEUE.put(json_data_out)
structlog.get_logger("structlog").warning("[TNC] ARQ | TX | DATA [" + str(mycallsign, 'utf-8') + "]>>X<<[" + str(static.DXCALLSIGN, 'utf-8') + "]") structlog.get_logger("structlog").warning(
"[TNC] ARQ | TX | DATA [" + str(mycallsign, 'utf-8') + "]>>X<<[" + str(static.DXCALLSIGN,
'utf-8') + "]")
self.datachannel_timeout = True self.datachannel_timeout = True
if not TESTMODE: if not TESTMODE:
self.arq_cleanup() self.arq_cleanup()
@ -1241,9 +1304,9 @@ class DATA():
# open_session frame and can still hear us. # open_session frame and can still hear us.
self.close_session() self.close_session()
return False return False
#sys.exit() # close thread and so connection attempts # sys.exit() # close thread and so connection attempts
def arq_received_data_channel_opener(self, data_in:bytes): def arq_received_data_channel_opener(self, data_in: bytes):
""" """
Args: Args:
@ -1278,7 +1341,8 @@ class DATA():
# updated modes we are listening to # updated modes we are listening to
self.set_listening_modes(self.mode_list[self.speed_level]) self.set_listening_modes(self.mode_list[self.speed_level])
helpers.add_to_heard_stations(static.DXCALLSIGN,static.DXGRID, 'DATA-CHANNEL', static.SNR, static.FREQ_OFFSET, static.HAMLIB_FREQUENCY) helpers.add_to_heard_stations(static.DXCALLSIGN, static.DXGRID, 'DATA-CHANNEL', static.SNR, static.FREQ_OFFSET,
static.HAMLIB_FREQUENCY)
# check if callsign ssid override # check if callsign ssid override
valid, mycallsign = helpers.check_callsign(self.mycallsign, data_in[1:4]) valid, mycallsign = helpers.check_callsign(self.mycallsign, data_in[1:4])
@ -1288,7 +1352,9 @@ class DATA():
self.arq_cleanup() self.arq_cleanup()
return return
structlog.get_logger("structlog").info("[TNC] ARQ | DATA | RX | [" + str(mycallsign, 'utf-8') + "]>> <<[" + str(static.DXCALLSIGN, 'utf-8') + "]", bandwith="wide") structlog.get_logger("structlog").info(
"[TNC] ARQ | DATA | RX | [" + str(mycallsign, 'utf-8') + "]>> <<[" + str(static.DXCALLSIGN, 'utf-8') + "]",
bandwith="wide")
static.ARQ_STATE = True static.ARQ_STATE = True
static.TNC_STATE = 'BUSY' static.TNC_STATE = 'BUSY'
@ -1312,12 +1378,17 @@ class DATA():
self.enqueue_frame_for_tx(connection_frame) self.enqueue_frame_for_tx(connection_frame)
structlog.get_logger("structlog").info("[TNC] ARQ | DATA | RX | [" + str(mycallsign, 'utf-8') + "]>>|<<[" + str(static.DXCALLSIGN, 'utf-8') + "]", bandwith="wide", snr=static.SNR) structlog.get_logger("structlog").info(
"[TNC] ARQ | DATA | RX | [" + str(mycallsign, 'utf-8') + "]>>|<<[" + str(static.DXCALLSIGN, 'utf-8') + "]",
bandwith="wide", snr=static.SNR)
# set start of transmission for our statistics # set start of transmission for our statistics
self.rx_start_of_transmission = time.time() self.rx_start_of_transmission = time.time()
def arq_received_channel_is_open(self, data_in:bytes): # reset our data channel watchdog
self.data_channel_last_received = int(time.time())
def arq_received_channel_is_open(self, data_in: bytes):
""" """
Called if we received a data channel opener Called if we received a data channel opener
Args: Args:
@ -1344,9 +1415,13 @@ class DATA():
self.speed_level = len(self.mode_list) - 1 self.speed_level = len(self.mode_list) - 1
structlog.get_logger("structlog").debug("[TNC] high bandwidth mode", modes=self.mode_list) structlog.get_logger("structlog").debug("[TNC] high bandwidth mode", modes=self.mode_list)
helpers.add_to_heard_stations(static.DXCALLSIGN, static.DXGRID, 'DATA-CHANNEL', static.SNR, static.FREQ_OFFSET, static.HAMLIB_FREQUENCY) helpers.add_to_heard_stations(static.DXCALLSIGN, static.DXGRID, 'DATA-CHANNEL', static.SNR,
static.FREQ_OFFSET, static.HAMLIB_FREQUENCY)
structlog.get_logger("structlog").info("[TNC] ARQ | DATA | TX | [" + str(self.mycallsign, 'utf-8') + "]>>|<<[" + str(static.DXCALLSIGN, 'utf-8') + "]", snr=static.SNR) structlog.get_logger("structlog").info(
"[TNC] ARQ | DATA | TX | [" + str(self.mycallsign, 'utf-8') + "]>>|<<[" + str(static.DXCALLSIGN,
'utf-8') + "]",
snr=static.SNR)
# as soon as we set ARQ_STATE to DATA, transmission starts # as soon as we set ARQ_STATE to DATA, transmission starts
static.ARQ_STATE = True static.ARQ_STATE = True
@ -1355,11 +1430,12 @@ class DATA():
static.TNC_STATE = 'IDLE' static.TNC_STATE = 'IDLE'
static.ARQ_STATE = False static.ARQ_STATE = False
static.INFO.append("PROTOCOL;VERSION_MISMATCH") static.INFO.append("PROTOCOL;VERSION_MISMATCH")
structlog.get_logger("structlog").warning("[TNC] protocol version mismatch:", received=protocol_version, own=static.ARQ_PROTOCOL_VERSION) structlog.get_logger("structlog").warning("[TNC] protocol version mismatch:", received=protocol_version,
own=static.ARQ_PROTOCOL_VERSION)
self.arq_cleanup() self.arq_cleanup()
# ---------- PING # ---------- PING
def transmit_ping(self, dxcallsign:bytes): def transmit_ping(self, dxcallsign: bytes):
""" """
Funktion for controlling pings Funktion for controlling pings
Args: Args:
@ -1372,7 +1448,8 @@ class DATA():
static.DXCALLSIGN_CRC = helpers.get_crc_24(static.DXCALLSIGN) static.DXCALLSIGN_CRC = helpers.get_crc_24(static.DXCALLSIGN)
static.INFO.append("PING;SENDING") static.INFO.append("PING;SENDING")
structlog.get_logger("structlog").info("[TNC] PING REQ [" + str(self.mycallsign, 'utf-8') + "] >>> [" + str(static.DXCALLSIGN, 'utf-8') + "]" ) structlog.get_logger("structlog").info(
"[TNC] PING REQ [" + str(self.mycallsign, 'utf-8') + "] >>> [" + str(static.DXCALLSIGN, 'utf-8') + "]")
ping_frame = bytearray(14) ping_frame = bytearray(14)
ping_frame[:1] = bytes([210]) ping_frame[:1] = bytes([210])
@ -1386,7 +1463,7 @@ class DATA():
else: else:
self.enqueue_frame_for_tx(ping_frame) self.enqueue_frame_for_tx(ping_frame)
def received_ping(self, data_in:bytes): def received_ping(self, data_in: bytes):
""" """
Called if we received a ping Called if we received a ping
@ -1398,7 +1475,8 @@ class DATA():
""" """
static.DXCALLSIGN_CRC = bytes(data_in[4:7]) static.DXCALLSIGN_CRC = bytes(data_in[4:7])
static.DXCALLSIGN = helpers.bytes_to_callsign(bytes(data_in[7:13])) static.DXCALLSIGN = helpers.bytes_to_callsign(bytes(data_in[7:13]))
helpers.add_to_heard_stations(static.DXCALLSIGN, static.DXGRID, 'PING', static.SNR, static.FREQ_OFFSET, static.HAMLIB_FREQUENCY) helpers.add_to_heard_stations(static.DXCALLSIGN, static.DXGRID, 'PING', static.SNR, static.FREQ_OFFSET,
static.HAMLIB_FREQUENCY)
static.INFO.append("PING;RECEIVING") static.INFO.append("PING;RECEIVING")
@ -1410,7 +1488,9 @@ class DATA():
# print("ping not for me...") # print("ping not for me...")
return return
structlog.get_logger("structlog").info("[TNC] PING REQ [" + str(mycallsign, 'utf-8') + "] <<< [" + str(static.DXCALLSIGN, 'utf-8') + "]", snr=static.SNR ) structlog.get_logger("structlog").info(
"[TNC] PING REQ [" + str(mycallsign, 'utf-8') + "] <<< [" + str(static.DXCALLSIGN, 'utf-8') + "]",
snr=static.SNR)
ping_frame = bytearray(14) ping_frame = bytearray(14)
ping_frame[:1] = bytes([211]) ping_frame[:1] = bytes([211])
@ -1424,7 +1504,7 @@ class DATA():
else: else:
self.enqueue_frame_for_tx(ping_frame) self.enqueue_frame_for_tx(ping_frame)
def received_ping_ack(self, data_in:bytes): def received_ping_ack(self, data_in: bytes):
""" """
Called if a PING ack has been received Called if a PING ack has been received
Args: Args:
@ -1436,15 +1516,20 @@ class DATA():
static.DXCALLSIGN_CRC = bytes(data_in[4:7]) static.DXCALLSIGN_CRC = bytes(data_in[4:7])
static.DXGRID = bytes(data_in[7:13]).rstrip(b'\x00') static.DXGRID = bytes(data_in[7:13]).rstrip(b'\x00')
jsondata = {"type" : "ping", "status" : "ack", "uuid" : str(uuid.uuid4()), "timestamp": int(time.time()), "mycallsign" : str(self.mycallsign, 'utf-8'), "dxcallsign": str(static.DXCALLSIGN, 'utf-8'), "dxgrid": str(static.DXGRID, 'utf-8'), "snr": str(static.SNR)} jsondata = {"type": "ping", "status": "ack", "uuid": str(uuid.uuid4()), "timestamp": int(time.time()),
"mycallsign": str(self.mycallsign, 'utf-8'), "dxcallsign": str(static.DXCALLSIGN, 'utf-8'),
"dxgrid": str(static.DXGRID, 'utf-8'), "snr": str(static.SNR)}
json_data_out = json.dumps(jsondata) json_data_out = json.dumps(jsondata)
sock.SOCKET_QUEUE.put(json_data_out) sock.SOCKET_QUEUE.put(json_data_out)
helpers.add_to_heard_stations(static.DXCALLSIGN, static.DXGRID, 'PING-ACK', static.SNR, static.FREQ_OFFSET, static.HAMLIB_FREQUENCY) helpers.add_to_heard_stations(static.DXCALLSIGN, static.DXGRID, 'PING-ACK', static.SNR, static.FREQ_OFFSET,
static.HAMLIB_FREQUENCY)
static.INFO.append("PING;RECEIVEDACK") static.INFO.append("PING;RECEIVEDACK")
structlog.get_logger("structlog").info("[TNC] PING ACK [" + str(self.mycallsign, 'utf-8') + "] >|< [" + str(static.DXCALLSIGN, 'utf-8') + "]", snr=static.SNR ) structlog.get_logger("structlog").info(
"[TNC] PING ACK [" + str(self.mycallsign, 'utf-8') + "] >|< [" + str(static.DXCALLSIGN, 'utf-8') + "]",
snr=static.SNR)
static.TNC_STATE = 'IDLE' static.TNC_STATE = 'IDLE'
def stop_transmission(self): def stop_transmission(self):
@ -1478,9 +1563,10 @@ class DATA():
# ----------- BROADCASTS # ----------- BROADCASTS
def run_beacon(self): def run_beacon(self):
""" """
Controlling funktion for running a beacon Controlling function for running a beacon
Args: Args:
self:
self: arq class
Returns: Returns:
@ -1512,7 +1598,7 @@ class DATA():
structlog.get_logger("structlog").debug("[TNC] run_beacon: ", exception=e) structlog.get_logger("structlog").debug("[TNC] run_beacon: ", exception=e)
# print(e) # print(e)
def received_beacon(self, data_in:bytes): def received_beacon(self, data_in: bytes):
""" """
Called if we received a beacon Called if we received a beacon
Args: Args:
@ -1525,19 +1611,23 @@ class DATA():
dxcallsign = helpers.bytes_to_callsign(bytes(data_in[1:7])) dxcallsign = helpers.bytes_to_callsign(bytes(data_in[1:7]))
dxgrid = bytes(data_in[9:13]).rstrip(b'\x00') dxgrid = bytes(data_in[9:13]).rstrip(b'\x00')
jsondata = {"type" : "beacon", "status" : "received", "uuid" : str(uuid.uuid4()), "timestamp": int(time.time()), "mycallsign" : str(self.mycallsign, 'utf-8'), "dxcallsign": str(dxcallsign, 'utf-8'), "dxgrid": str(dxgrid, 'utf-8'), "snr": str(static.SNR)} jsondata = {"type": "beacon", "status": "received", "uuid": str(uuid.uuid4()), "timestamp": int(time.time()),
"mycallsign": str(self.mycallsign, 'utf-8'), "dxcallsign": str(dxcallsign, 'utf-8'),
"dxgrid": str(dxgrid, 'utf-8'), "snr": str(static.SNR)}
json_data_out = json.dumps(jsondata) json_data_out = json.dumps(jsondata)
sock.SOCKET_QUEUE.put(json_data_out) sock.SOCKET_QUEUE.put(json_data_out)
static.INFO.append("BEACON;RECEIVING") static.INFO.append("BEACON;RECEIVING")
structlog.get_logger("structlog").info("[TNC] BEACON RCVD [" + str(dxcallsign, 'utf-8') + "]["+ str(dxgrid, 'utf-8') +"] ", snr=static.SNR) structlog.get_logger("structlog").info(
helpers.add_to_heard_stations(dxcallsign,dxgrid, 'BEACON', static.SNR, static.FREQ_OFFSET, static.HAMLIB_FREQUENCY) "[TNC] BEACON RCVD [" + str(dxcallsign, 'utf-8') + "][" + str(dxgrid, 'utf-8') + "] ", snr=static.SNR)
helpers.add_to_heard_stations(dxcallsign, dxgrid, 'BEACON', static.SNR, static.FREQ_OFFSET,
static.HAMLIB_FREQUENCY)
def transmit_cq(self): def transmit_cq(self):
""" """
Transmit a CQ Transmit a CQ
Args: Args:
Nothing self
Returns: Returns:
Nothing Nothing
@ -1558,7 +1648,7 @@ class DATA():
else: else:
self.enqueue_frame_for_tx(cq_frame) self.enqueue_frame_for_tx(cq_frame)
def received_cq(self, data_in:bytes): def received_cq(self, data_in: bytes):
""" """
Called when we receive a CQ frame Called when we receive a CQ frame
Args: Args:
@ -1573,8 +1663,10 @@ class DATA():
# print(dxcallsign) # print(dxcallsign)
dxgrid = bytes(helpers.decode_grid(data_in[7:11]), "utf-8") dxgrid = bytes(helpers.decode_grid(data_in[7:11]), "utf-8")
static.INFO.append("CQ;RECEIVING") static.INFO.append("CQ;RECEIVING")
structlog.get_logger("structlog").info("[TNC] CQ RCVD [" + str(dxcallsign, 'utf-8') + "]["+ str(dxgrid, 'utf-8') +"] ", snr=static.SNR) structlog.get_logger("structlog").info(
helpers.add_to_heard_stations(dxcallsign, dxgrid, 'CQ CQ CQ', static.SNR, static.FREQ_OFFSET, static.HAMLIB_FREQUENCY) "[TNC] CQ RCVD [" + str(dxcallsign, 'utf-8') + "][" + str(dxgrid, 'utf-8') + "] ", snr=static.SNR)
helpers.add_to_heard_stations(dxcallsign, dxgrid, 'CQ CQ CQ', static.SNR, static.FREQ_OFFSET,
static.HAMLIB_FREQUENCY)
if static.RESPOND_TO_CQ: if static.RESPOND_TO_CQ:
self.transmit_qrv() self.transmit_qrv()
@ -1583,7 +1675,7 @@ class DATA():
""" """
Called when we send a QRV frame Called when we send a QRV frame
Args: Args:
data_in:bytes: self
Returns: Returns:
Nothing Nothing
@ -1608,7 +1700,7 @@ class DATA():
else: else:
self.enqueue_frame_for_tx(qrv_frame) self.enqueue_frame_for_tx(qrv_frame)
def received_qrv(self, data_in:bytes): def received_qrv(self, data_in: bytes):
""" """
Called when we receive a QRV frame Called when we receive a QRV frame
Args: Args:
@ -1621,16 +1713,20 @@ class DATA():
dxcallsign = helpers.bytes_to_callsign(bytes(data_in[1:7])) dxcallsign = helpers.bytes_to_callsign(bytes(data_in[1:7]))
dxgrid = bytes(helpers.decode_grid(data_in[7:11]), "utf-8") dxgrid = bytes(helpers.decode_grid(data_in[7:11]), "utf-8")
jsondata = {"type" : "qrv", "status" : "received", "uuid" : str(uuid.uuid4()), "timestamp": int(time.time()), "mycallsign" : str(self.mycallsign, 'utf-8'), "dxcallsign": str(dxcallsign, 'utf-8'), "dxgrid": str(dxgrid, 'utf-8'), "snr": str(static.SNR)} jsondata = {"type": "qrv", "status": "received", "uuid": str(uuid.uuid4()), "timestamp": int(time.time()),
"mycallsign": str(self.mycallsign, 'utf-8'), "dxcallsign": str(dxcallsign, 'utf-8'),
"dxgrid": str(dxgrid, 'utf-8'), "snr": str(static.SNR)}
json_data_out = json.dumps(jsondata) json_data_out = json.dumps(jsondata)
sock.SOCKET_QUEUE.put(json_data_out) sock.SOCKET_QUEUE.put(json_data_out)
static.INFO.append("QRV;RECEIVING") static.INFO.append("QRV;RECEIVING")
structlog.get_logger("structlog").info("[TNC] QRV RCVD [" + str(dxcallsign, 'utf-8') + "]["+ str(dxgrid, 'utf-8') +"] ", snr=static.SNR) structlog.get_logger("structlog").info(
helpers.add_to_heard_stations(dxcallsign,dxgrid, 'QRV', static.SNR, static.FREQ_OFFSET, static.HAMLIB_FREQUENCY) "[TNC] QRV RCVD [" + str(dxcallsign, 'utf-8') + "][" + str(dxgrid, 'utf-8') + "] ", snr=static.SNR)
helpers.add_to_heard_stations(dxcallsign, dxgrid, 'QRV', static.SNR, static.FREQ_OFFSET,
static.HAMLIB_FREQUENCY)
# ------------ CALUCLATE TRANSFER RATES # ------------ CALUCLATE TRANSFER RATES
def calculate_transfer_rate_rx(self, rx_start_of_transmission:float, receivedbytes:int) -> list: def calculate_transfer_rate_rx(self, rx_start_of_transmission: float, receivedbytes: int) -> list:
""" """
Calculate transfer rate for received data Calculate transfer rate for received data
Args: Args:
@ -1645,7 +1741,8 @@ class DATA():
try: try:
if static.TOTAL_BYTES == 0: if static.TOTAL_BYTES == 0:
static.TOTAL_BYTES = 1 static.TOTAL_BYTES = 1
static.ARQ_TRANSMISSION_PERCENT = min(int((receivedbytes*static.ARQ_COMPRESSION_FACTOR / (static.TOTAL_BYTES)) * 100), 100) static.ARQ_TRANSMISSION_PERCENT = min(
int((receivedbytes * static.ARQ_COMPRESSION_FACTOR / (static.TOTAL_BYTES)) * 100), 100)
transmissiontime = time.time() - self.rx_start_of_transmission transmissiontime = time.time() - self.rx_start_of_transmission
@ -1678,7 +1775,8 @@ class DATA():
static.ARQ_TRANSMISSION_PERCENT = 0 static.ARQ_TRANSMISSION_PERCENT = 0
static.TOTAL_BYTES = 0 static.TOTAL_BYTES = 0
def calculate_transfer_rate_tx(self, tx_start_of_transmission:float, sentbytes:int, tx_buffer_length:int) -> list: def calculate_transfer_rate_tx(self, tx_start_of_transmission: float, sentbytes: int,
tx_buffer_length: int) -> list:
""" """
Calculate transfer rate for transmission Calculate transfer rate for transmission
Args: Args:
@ -1728,7 +1826,7 @@ class DATA():
self.data_frame_ack_received = False self.data_frame_ack_received = False
static.RX_BURST_BUFFER = [] static.RX_BURST_BUFFER = []
static.RX_FRAME_BUFFER = b'' static.RX_FRAME_BUFFER = b''
self.burst_ack_snr= 255 self.burst_ack_snr = 255
# reset modem receiving state to reduce cpu load # reset modem receiving state to reduce cpu load
modem.RECEIVE_DATAC1 = False modem.RECEIVE_DATAC1 = False
@ -1760,7 +1858,7 @@ class DATA():
static.BEACON_PAUSE = False static.BEACON_PAUSE = False
def arq_reset_ack(self,state:bool): def arq_reset_ack(self, state: bool):
""" """
Funktion for resetting acknowledge states Funktion for resetting acknowledge states
Args: Args:
@ -1824,14 +1922,18 @@ class DATA():
DATA BURST DATA BURST
""" """
# IRS SIDE # IRS SIDE
if not static.ARQ_STATE or static.ARQ_SESSION_STATE != 'connected' or static.TNC_STATE != 'BUSY' or not self.is_IRS: # TODO: We need to redesign this part for cleaner state handling
# return only if not ARQ STATE and not ARQ SESSION STATE as they are different use cases
if not static.ARQ_STATE and static.ARQ_SESSION_STATE != 'connected' or not self.is_IRS:
return return
# we want to reach this state only if connected ( == return above not called )
if self.data_channel_last_received + self.time_list[self.speed_level] > time.time(): if self.data_channel_last_received + self.time_list[self.speed_level] > time.time():
# print((self.data_channel_last_received + self.time_list[self.speed_level])-time.time()) # print((self.data_channel_last_received + self.time_list[self.speed_level])-time.time())
pass pass
else: else:
structlog.get_logger("structlog").warning("[TNC] Frame timeout", attempt=self.n_retries_per_burst, max_attempts=self.rx_n_max_retries_per_burst, speed_level=self.speed_level) structlog.get_logger("structlog").warning("[TNC] Frame timeout", attempt=self.n_retries_per_burst,
max_attempts=self.rx_n_max_retries_per_burst,
speed_level=self.speed_level)
self.frame_received_counter = 0 self.frame_received_counter = 0
self.burst_nack_counter += 1 self.burst_nack_counter += 1
if self.burst_nack_counter >= 2: if self.burst_nack_counter >= 2:
@ -1856,7 +1958,6 @@ class DATA():
self.stop_transmission() self.stop_transmission()
self.arq_cleanup() self.arq_cleanup()
def data_channel_keep_alive_watchdog(self): def data_channel_keep_alive_watchdog(self):
""" """
watchdog which checks if we are running into a connection timeout watchdog which checks if we are running into a connection timeout
@ -1871,7 +1972,8 @@ class DATA():
# pass # pass
else: else:
self.data_channel_last_received = 0 self.data_channel_last_received = 0
structlog.get_logger("structlog").info("[TNC] DATA [" + str(self.mycallsign, 'utf-8') + "]<<T>>[" + str(static.DXCALLSIGN, 'utf-8') + "]") structlog.get_logger("structlog").info(
"[TNC] DATA [" + str(self.mycallsign, 'utf-8') + "]<<T>>[" + str(static.DXCALLSIGN, 'utf-8') + "]")
static.INFO.append("ARQ;RECEIVING;FAILED") static.INFO.append("ARQ;RECEIVING;FAILED")
if not TESTMODE: if not TESTMODE:
self.arq_cleanup() self.arq_cleanup()
@ -1885,7 +1987,9 @@ class DATA():
if self.arq_session_last_received + self.arq_session_timeout > time.time(): if self.arq_session_last_received + self.arq_session_timeout > time.time():
time.sleep(0.01) time.sleep(0.01)
else: else:
structlog.get_logger("structlog").info("[TNC] SESSION [" + str(self.mycallsign, 'utf-8') + "]<<T>>[" + str(static.DXCALLSIGN, 'utf-8') + "]") structlog.get_logger("structlog").info(
"[TNC] SESSION [" + str(self.mycallsign, 'utf-8') + "]<<T>>[" + str(static.DXCALLSIGN,
'utf-8') + "]")
static.INFO.append("ARQ;SESSION;TIMEOUT") static.INFO.append("ARQ;SESSION;TIMEOUT")
self.close_session() self.close_session()
@ -1901,4 +2005,4 @@ class DATA():
time.sleep(2) time.sleep(2)
def send_test_frame(self): def send_test_frame(self):
modem.MODEM_TRANSMIT_QUEUE.put([12,1,0,[bytearray(126)]]) modem.MODEM_TRANSMIT_QUEUE.put([12, 1, 0, [bytearray(126)]])

View file

@ -620,8 +620,8 @@ class RF():
snr = round(modem_stats_snr, 1) snr = round(modem_stats_snr, 1)
structlog.get_logger("structlog").info("[MDM] calculate_snr: ", snr=snr) structlog.get_logger("structlog").info("[MDM] calculate_snr: ", snr=snr)
# print(snr) # static.SNR = np.clip(snr, 0, 255) # limit to max value of 255
static.SNR = np.clip(snr, 0, 255) # limit to max value of 255 static.SNR = np.clip(snr, -128, 128) # limit to max value of -128/128 as a possible fix of #188
return static.SNR return static.SNR
except Exception as e: except Exception as e:
structlog.get_logger("structlog").error(f"[MDM] calculate_snr: Exception: {e}") structlog.get_logger("structlog").error(f"[MDM] calculate_snr: Exception: {e}")