diff --git a/.github/workflows/build-project-linux.yml b/.github/workflows/build-project-linux.yml index 05eaa4cf..9e35b81a 100644 --- a/.github/workflows/build-project-linux.yml +++ b/.github/workflows/build-project-linux.yml @@ -2,7 +2,7 @@ name: Linux on: push: tags: - - 'v*' + - "v*" jobs: build_linux_release: @@ -20,15 +20,13 @@ jobs: - name: Checkout code uses: actions/checkout@v2 with: - ref: main + ref: main - name: Set up Python 3.8 uses: actions/setup-python@v2 with: python-version: 3.8 - - - name: Install Linux dependencies if: matrix.os == 'ubuntu-20.04' run: | @@ -44,7 +42,6 @@ jobs: pip3 install structlog pip3 install sounddevice - #- name: Build Hamlib Python Binding # if: matrix.os == 'ubuntu-latest' # working-directory: tnc @@ -58,19 +55,15 @@ jobs: # make # make install - - - - name: Build codec2 Linux if: matrix.os == 'ubuntu-20.04' working-directory: tnc run: | 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 ../ make - - name: Build Linux Daemon if: matrix.os == 'ubuntu-20.04' working-directory: tnc @@ -82,7 +75,6 @@ jobs: run: | ls -R - - name: Compress Linux shell: bash run: | @@ -96,8 +88,6 @@ jobs: name: tnc-artifact path: ./tnc/dist/compressed/* - - - name: Copy TNC to GUI Linux if: matrix.os == 'ubuntu-20.04' run: | @@ -124,6 +114,3 @@ jobs: # release the app after building release: ${{ startsWith(github.ref, 'refs/tags/v') }} args: "-p always" - - - diff --git a/.github/workflows/build-project-mac.yml b/.github/workflows/build-project-mac.yml index 85570d42..f7cf24a1 100644 --- a/.github/workflows/build-project-mac.yml +++ b/.github/workflows/build-project-mac.yml @@ -2,7 +2,7 @@ name: macOS on: push: tags: - - 'v*' + - "v*" jobs: build_linux_release: @@ -20,15 +20,13 @@ jobs: - name: Checkout code uses: actions/checkout@v2 with: - ref: main + ref: main - name: Set up Python 3.9 uses: actions/setup-python@v2 with: python-version: 3.9 - - - name: Install macOS dependencies if: matrix.os == 'macos-10.15' run: | @@ -45,20 +43,18 @@ jobs: - name: Install Portaudio if: matrix.os == 'macos-10.15' run: | - brew install portaudio - pip3 install pyaudio - + brew install portaudio + pip3 install pyaudio - name: Build codec2 macOS if: matrix.os == 'macos-10.15' working-directory: tnc run: | 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 ../ make - - name: Build macOS pyinstaller if: matrix.os == 'macos-10.15' working-directory: tnc @@ -70,7 +66,6 @@ jobs: run: | ls -R - - name: Compress shell: bash run: | @@ -84,9 +79,6 @@ jobs: name: tnc-artifact path: ./tnc/dist/compressed/* - - - - name: Copy TNC to GUI if: matrix.os == 'macos-10.15' run: | @@ -113,6 +105,3 @@ jobs: # release the app after building release: ${{ startsWith(github.ref, 'refs/tags/v') }} args: "-p always" - - - diff --git a/.github/workflows/ctest.yml b/.github/workflows/ctest.yml index 0c7984ed..8721a823 100644 --- a/.github/workflows/ctest.yml +++ b/.github/workflows/ctest.yml @@ -11,26 +11,26 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v2 - - name: Install packages - shell: bash - run: | - sudo apt-get update - 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 + - name: Install packages + shell: bash + run: | + sudo apt-get update + 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 pytest - - name: Build codec2 - shell: bash - run: | - git clone https://github.com/drowe67/codec2.git - cd codec2 && git checkout dr-tnc && git pull - mkdir -p build_linux && cd build_linux && cmake .. && make + - name: Build codec2 + shell: bash + run: | + git clone https://github.com/drowe67/codec2.git + cd codec2 && git checkout master # This should be pinned to a release + mkdir -p build_linux && cd build_linux && cmake .. && make - - name: run ctests - shell: bash - working-directory: ${{github.workspace}} - run: | - mkdir build && cd build - cmake -DCODEC2_BUILD_DIR=$GITHUB_WORKSPACE/codec2/build_linux .. - ctest --output-on-failure + - name: run ctests + shell: bash + working-directory: ${{github.workspace}} + run: | + mkdir build && cd build + cmake -DCODEC2_BUILD_DIR=$GITHUB_WORKSPACE/codec2/build_linux .. + ctest --output-on-failure diff --git a/.gitignore b/.gitignore index e569bfe4..9a5c5568 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ tnc/codec2 **/Testing package-lock.json +.DS_Store diff --git a/CMakeLists.txt b/CMakeLists.txt index 185ca2d8..cdd5ad80 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,16 +24,80 @@ set(TESTFRAMES 3) add_test(NAME audio_buffer COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src; + export PYTHONPATH=../tnc; cd ${CMAKE_CURRENT_SOURCE_DIR}/test; python3 test_audiobuffer.py") set_tests_properties(audio_buffer PROPERTIES PASS_REGULAR_EXPRESSION "errors: 0") - + add_test(NAME resampler COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src; + export PYTHONPATH=../tnc; 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") +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 COMMAND sh -c "export LD_LIBRARY_PATH=${CODEC2_BUILD_DIR}/src; PATH=$PATH:${CODEC2_BUILD_DIR}/src; @@ -59,7 +123,7 @@ add_test(NAME highsnr_stdio_P_P_single python3 test_tx.py --mode datac0 --delay 500 --framesperburst ${FRAMESPERBURST} --bursts ${BURSTS} | python3 test_rx.py --debug --mode datac0 --framesperburst ${FRAMESPERBURST} --bursts ${BURSTS}") set_tests_properties(highsnr_stdio_P_P_single PROPERTIES PASS_REGULAR_EXPRESSION "RECEIVED BURSTS: ${BURSTS} RECEIVED FRAMES: ${FRAMESPERBURST}") - + add_test(NAME highsnr_stdio_P_P_multi COMMAND sh -c "export LD_LIBRARY_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") 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 if(NOT DEFINED ENV{GITHUB_RUN_ID}) @@ -127,7 +190,7 @@ add_test(NAME highsnr_virtual5_P_P_multi_callback_outside cd ${CMAKE_CURRENT_SOURCE_DIR}/test; ./test_virtual4b.sh") set_tests_properties(highsnr_virtual5_P_P_multi_callback_outside PROPERTIES PASS_REGULAR_EXPRESSION "DATAC0: 2/4 DATAC1: 2/4 DATAC3: 2/4") - + # ARQ test short add_test(NAME highsnr_ARQ_short @@ -135,11 +198,11 @@ add_test(NAME highsnr_ARQ_short PATH=$PATH:${CODEC2_BUILD_DIR}/src; cd ${CMAKE_CURRENT_SOURCE_DIR}/test; python3 test_arq_short.py") - + set_tests_properties(highsnr_ARQ_short PROPERTIES PASS_REGULAR_EXPRESSION "ARQ | TX | DATA TRANSMITTED!") - - - - + + + + endif() - + diff --git a/README.md b/README.md index 16a74adf..755a8c57 100644 --- a/README.md +++ b/README.md @@ -30,4 +30,3 @@ Download the latest developer release from the releases section, unpack it and j ## Installation Please check the [wiki](https://wiki.freedata.app) for installation instructions - diff --git a/gui/package.json b/gui/package.json index 293fedd6..7e4e4898 100644 --- a/gui/package.json +++ b/gui/package.json @@ -1,6 +1,6 @@ { "name": "FreeDATA", - "version": "0.4.0-alpha.8", + "version": "0.4.0-alpha.9", "description": "FreeDATA ", "main": "main.js", "scripts": { diff --git a/gui/preload-chat.js b/gui/preload-chat.js index 0511767b..6c79730c 100644 --- a/gui/preload-chat.js +++ b/gui/preload-chat.js @@ -511,8 +511,9 @@ update_chat = function(obj) { `; var controlarea_receive = ''; } - } catch { + } catch (err) { console.log("error with database parsing...") + console.log(err) } // CALLSIGN LIST if (!(document.getElementById('chat-' + dxcallsign + '-list'))) { diff --git a/test/README.md b/test/README.md index 81a9453e..8e0a1a6c 100644 --- a/test/README.md +++ b/test/README.md @@ -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 1. Install: @@ -58,7 +106,7 @@ The virtual audio devices are great for testing, but they are also a little bit 1. Create virtual audio devices. Note: This command needs to be run again after every reboot ``` - sudo modprobe snd-aloop index=1,2 enable=1,1 pcm_substreams=1,1 id=CHAT1,CHAT2 + sudo modprobe snd-aloop index=1,2 enable=1,1 pcm_substreams=1,1 id=CHAT1,CHAT2 ``` 1. Check if devices have been created @@ -81,7 +129,7 @@ The virtual audio devices are great for testing, but they are also a little bit Sub-Geräte: 1/1 Sub-Gerät #0: subdevice #0 ``` - + 1. Determine the audio device number you would like to use: ``` python3 test_rx.py --list diff --git a/test/ping.py b/test/ping.py index 1c8f4ef7..f63b5b7d 100644 --- a/test/ping.py +++ b/test/ping.py @@ -1,113 +1,117 @@ - #!/usr/bin/env python3 +#!/usr/bin/env python3 # -*- coding: utf-8 -*- -import ctypes -from ctypes import * -import pathlib -import pyaudio -import time -import threading import argparse -import sys +import ctypes +import pathlib +import threading +import time -#--------------------------------------------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") +import pyaudio -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_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_INPUT_DEVICE = args.AUDIO_INPUT # 1024 good for mode 6 -AUDIO_FRAMES_PER_BUFFER = 2048 +AUDIO_FRAMES_PER_BUFFER = 2048 MODEM_SAMPLE_RATE = 8000 FREEDV_TX_MODE = args.FREEDV_TX_MODE FREEDV_RX_MODE = args.FREEDV_RX_MODE DEBUGGING_MODE = args.DEBUGGING_MODE - #-------------------------------------------- LOAD FREEDV +# -------------------------------------------- LOAD FREEDV 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() - #--------------------------------------------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_RX = int(p.get_device_info_by_index(AUDIO_INPUT_DEVICE)['defaultSampleRate']) +# --------------------------------------------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_RX = int(p.get_device_info_by_index(AUDIO_INPUT_DEVICE)['defaultSampleRate']) AUDIO_SAMPLE_RATE_TX = 8000 -AUDIO_SAMPLE_RATE_RX = 8000 - #--------------------------------------------OPEN AUDIO CHANNEL TX +AUDIO_SAMPLE_RATE_RX = 8000 +# --------------------------------------------OPEN AUDIO CHANNEL TX + +stream_tx = p.open( + format=pyaudio.paInt16, + channels=1, + rate=AUDIO_SAMPLE_RATE_TX, + frames_per_buffer=AUDIO_FRAMES_PER_BUFFER, # n_nom_modem_samples + output=True, + output_device_index=AUDIO_OUTPUT_DEVICE, +) + +stream_rx = p.open( + format=pyaudio.paInt16, + channels=1, + rate=AUDIO_SAMPLE_RATE_RX, + frames_per_buffer=AUDIO_FRAMES_PER_BUFFER, + input=True, + input_device_index=AUDIO_INPUT_DEVICE, +) -stream_tx = p.open(format=pyaudio.paInt16, - channels=1, - rate=AUDIO_SAMPLE_RATE_TX, - frames_per_buffer=AUDIO_FRAMES_PER_BUFFER, #n_nom_modem_samples - output=True, - output_device_index=AUDIO_OUTPUT_DEVICE, - ) - -stream_rx = p.open(format=pyaudio.paInt16, - channels=1, - rate=AUDIO_SAMPLE_RATE_RX, - frames_per_buffer=AUDIO_FRAMES_PER_BUFFER, - input=True, - input_device_index=AUDIO_INPUT_DEVICE, - ) - - - - def receive(): c_lib.freedv_open.restype = ctypes.POINTER(ctypes.c_ubyte) freedv = c_lib.freedv_open(FREEDV_RX_MODE) - bytes_per_frame = int(c_lib.freedv_get_bits_per_modem_frame(freedv)/8) - payload_per_frame = bytes_per_frame -2 + bytes_per_frame = int(c_lib.freedv_get_bits_per_modem_frame(freedv) / 8) + payload_per_frame = bytes_per_frame - 2 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 = bytes_out() # get pointer from bytes_out - bytes_out = (ctypes.c_ubyte * bytes_per_frame) #bytes_per_frame - bytes_out = bytes_out() #get pointer from bytes_out - total_n_bytes = 0 rx_total_frames = 0 rx_frames = 0 rx_bursts = 0 receive = True - while receive == True: + while receive: time.sleep(0.01) - + nin = c_lib.freedv_nin(freedv) - nin_converted = int(nin*(AUDIO_SAMPLE_RATE_RX/MODEM_SAMPLE_RATE)) - if DEBUGGING_MODE == True: + nin_converted = int(nin * (AUDIO_SAMPLE_RATE_RX / MODEM_SAMPLE_RATE)) + if DEBUGGING_MODE: print("-----------------------------") print("NIN: " + str(nin) + " [ " + str(nin_converted) + " ]") - - data_in = stream_rx.read(nin_converted, exception_on_overflow = False) - 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 - nbytes = c_lib.freedv_rawdatarx(freedv, bytes_out, data_in) # demodulate audio + + data_in = stream_rx.read(nin_converted, exception_on_overflow=False) + 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 + nbytes = c_lib.freedv_rawdatarx(freedv, bytes_out, data_in) # demodulate audio total_n_bytes = total_n_bytes + nbytes - if DEBUGGING_MODE == True: + if DEBUGGING_MODE: print("SYNC: " + str(c_lib.freedv_get_rx_status(freedv))) - + if nbytes == bytes_per_frame: rx_total_frames = rx_total_frames + 1 rx_frames = rx_frames + 1 @@ -115,84 +119,111 @@ def receive(): if rx_frames == N_FRAMES_PER_BURST: rx_frames = 0 rx_bursts = rx_bursts + 1 - c_lib.freedv_set_sync(freedv,0) - - + c_lib.freedv_set_sync(freedv, 0) + burst = bytes_out[0] n_total_burst = bytes_out[1] frame = bytes_out[2] n_total_frame = bytes_out[3] - - - print("RX | PONG | BURST [" + str(burst) + "/" + str(n_total_burst) + "] FRAME [" + str(frame) + "/" + str(n_total_frame) + "]") + + print( + "RX | PONG | BURST [" + + str(burst) + + "/" + + str(n_total_burst) + + "] FRAME [" + + str(frame) + + "/" + + str(n_total_frame) + + "]" + ) print("-----------------------------------------------------------------") - c_lib.freedv_set_sync(freedv,0) - - + c_lib.freedv_set_sync(freedv, 0) + if rx_bursts == N_BURSTS: - receive = False + receive = False - RECEIVE = threading.Thread(target=receive, name="RECEIVE THREAD") -RECEIVE.start() - +RECEIVE.start() + c_lib.freedv_open.restype = ctypes.POINTER(ctypes.c_ubyte) freedv = c_lib.freedv_open(FREEDV_TX_MODE) -bytes_per_frame = int(c_lib.freedv_get_bits_per_modem_frame(freedv)/8) -payload_per_frame = bytes_per_frame -2 +bytes_per_frame = int(c_lib.freedv_get_bits_per_modem_frame(freedv) / 8) +payload_per_frame = bytes_per_frame - 2 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 = 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() - -print("BURSTS: " + str(N_BURSTS) + " FRAMES: " + str(N_FRAMES_PER_BURST) ) +print("BURSTS: " + str(N_BURSTS) + " FRAMES: " + str(N_FRAMES_PER_BURST)) print("-----------------------------------------------------------------") -for i in range(0,N_BURSTS): - - c_lib.freedv_rawdatapreambletx(freedv, mod_out_preamble); - +for i in range(N_BURSTS): + + c_lib.freedv_rawdatapreambletx(freedv, mod_out_preamble) + txbuffer = bytearray() txbuffer += bytes(mod_out_preamble) - for n in range(0,N_FRAMES_PER_BURST): - - data_out = bytearray() - data_out += bytes([i+1]) - data_out += bytes([N_BURSTS]) - data_out += bytes([n+1]) - data_out += bytes([N_FRAMES_PER_BURST]) - - buffer = bytearray(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 + for n in range(N_FRAMES_PER_BURST): + + data_out = bytearray() + data_out += bytes([i + 1]) + data_out += bytes([N_BURSTS]) + data_out += bytes([n + 1]) + data_out += bytes([N_FRAMES_PER_BURST]) + + buffer = bytearray( + 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 = crc.value.to_bytes(2, byteorder="big") # convert crc to 2 byte hex string + buffer += crc # append crc16 to buffer - crc = ctypes.c_ushort(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 - 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) - - 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)) ACK_TIMEOUT = time.time() + 3 txbuffer = bytearray() - #time.sleep(DELAY_BETWEEN_BURSTS) + # time.sleep(DELAY_BETWEEN_BURSTS) # WAIT UNTIL WE RECEIVD AN ACK/DATAC0 FRAME while ACK_TIMEOUT >= time.time(): time.sleep(0.01) - + time.sleep(1) stream_tx.close() diff --git a/test/pong.py b/test/pong.py index 04033b2a..c9cb4860 100644 --- a/test/pong.py +++ b/test/pong.py @@ -17,17 +17,17 @@ import threading import sys import argparse -#--------------------------------------------GET PARAMETER INPUTS +#--------------------------------------------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('--txmode', dest="FREEDV_TX_MODE", default=0, type=int) -parser.add_argument('--rxmode', dest="FREEDV_RX_MODE", default=0, type=int) -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('--debug', dest="DEBUGGING_MODE", action="store_true") +parser.add_argument('--rxmode', dest="FREEDV_RX_MODE", default=0, type=int) +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('--debug', dest="DEBUGGING_MODE", action="store_true") -args = parser.parse_args() +args, _ = parser.parse_known_args() N_BURSTS = args.N_BURSTS N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST @@ -41,20 +41,20 @@ FREEDV_RX_MODE = args.FREEDV_RX_MODE DEBUGGING_MODE = args.DEBUGGING_MODE # 1024 good for mode 6 -AUDIO_FRAMES_PER_BUFFER = 2048 +AUDIO_FRAMES_PER_BUFFER = 2048 MODEM_SAMPLE_RATE = 8000 - - #-------------------------------------------- LOAD FREEDV + + #-------------------------------------------- LOAD FREEDV libname = pathlib.Path().absolute() / "codec2/build_linux/src/libcodec2.so" c_lib = ctypes.CDLL(libname) - #--------------------------------------------CREATE PYAUDIO INSTANCE + #--------------------------------------------CREATE PYAUDIO INSTANCE p = pyaudio.PyAudio() #--------------------------------------------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_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_RX = 8000 +AUDIO_SAMPLE_RATE_RX = 8000 #--------------------------------------------OPEN AUDIO CHANNEL RX stream_tx = p.open(format=pyaudio.paInt16, @@ -62,22 +62,22 @@ stream_tx = p.open(format=pyaudio.paInt16, rate=AUDIO_SAMPLE_RATE_TX, frames_per_buffer=AUDIO_FRAMES_PER_BUFFER, #n_nom_modem_samples output=True, - output_device_index=AUDIO_OUTPUT_DEVICE, - ) - -stream_rx = p.open(format=pyaudio.paInt16, + output_device_index=AUDIO_OUTPUT_DEVICE, + ) + +stream_rx = p.open(format=pyaudio.paInt16, channels=1, rate=AUDIO_SAMPLE_RATE_RX, frames_per_buffer=AUDIO_FRAMES_PER_BUFFER, input=True, input_device_index=AUDIO_INPUT_DEVICE, - ) + ) - # GENERAL PARAMETERS + # GENERAL PARAMETERS c_lib.freedv_open.restype = ctypes.POINTER(ctypes.c_ubyte) - - + + def send_pong(burst,n_total_burst,frame,n_total_frame): data_out = bytearray() @@ -85,47 +85,47 @@ def send_pong(burst,n_total_burst,frame,n_total_frame): data_out[1:2] = bytes([n_total_burst]) data_out[2:3] = bytes([frame]) data_out[4:5] = bytes([n_total_frame]) - - - + + + c_lib.freedv_open.restype = ctypes.POINTER(ctypes.c_ubyte) freedv = c_lib.freedv_open(FREEDV_TX_MODE) bytes_per_frame = int(c_lib.freedv_get_bits_per_modem_frame(freedv)/8) payload_per_frame = bytes_per_frame -2 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 - + mod_out = ctypes.c_short * n_tx_modem_samples 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 = mod_out_preamble() - + buffer = bytearray(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 = 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 + c_lib.freedv_rawdatapreambletx(freedv, mod_out_preamble); txbuffer = bytearray() txbuffer += bytes(mod_out_preamble) - + 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) stream_tx.write(bytes(txbuffer)) - - txbuffer = bytearray() - + + txbuffer = bytearray() + # DATA CHANNEL INITIALISATION freedv = c_lib.freedv_open(FREEDV_RX_MODE) bytes_per_frame = int(c_lib.freedv_get_bits_per_modem_frame(freedv)/8) -n_max_modem_samples = c_lib.freedv_get_n_max_modem_samples(freedv) +n_max_modem_samples = c_lib.freedv_get_n_max_modem_samples(freedv) bytes_out = (ctypes.c_ubyte * bytes_per_frame) #bytes_per_frame bytes_out = bytes_out() #get pointer from bytes_out @@ -136,31 +136,31 @@ while receive == True: time.sleep(0.01) data_in = b'' - + nin = c_lib.freedv_nin(freedv) nin_converted = int(nin*(AUDIO_SAMPLE_RATE_RX/MODEM_SAMPLE_RATE)) if DEBUGGING_MODE == True: print("-----------------------------") 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') - - 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 if DEBUGGING_MODE == True: print("SYNC: " + str(c_lib.freedv_get_rx_status(freedv))) - + if nbytes == bytes_per_frame: - + burst = bytes_out[0] n_total_burst = bytes_out[1] frame = bytes_out[2] n_total_frame = bytes_out[3] print("RX | BURST [" + str(burst) + "/" + str(n_total_burst) + "] FRAME [" + str(frame) + "/" + str(n_total_frame) + "] >>> SENDING PONG") - + TRANSMIT_PONG = threading.Thread(target=send_pong, args=[burst,n_total_burst,frame,n_total_frame], name="SEND PONG") TRANSMIT_PONG.start() - + c_lib.freedv_set_sync(freedv,0) diff --git a/test/t48_8_short.py b/test/t48_8_short.py deleted file mode 100644 index 480062b8..00000000 --- a/test/t48_8_short.py +++ /dev/null @@ -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") diff --git a/test/test_arq_short.py b/test/test_arq_short.py index fe17cdcb..78305a32 100644 --- a/test/test_arq_short.py +++ b/test/test_arq_short.py @@ -7,36 +7,64 @@ Created on Wed Dec 23 07:04:24 2020 """ import sys -sys.path.insert(0,'..') -sys.path.insert(0,'../tnc') -import data_handler -import argparse + import codec2 +import data_handler import modem - -parser = argparse.ArgumentParser(description='ARQ TEST') -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() - +import pytest +import static 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 -modem = modem.RF() - -mode = codec2.freedv_get_mode(args.FREEDV_MODE) -print(mode) -n_frames_per_burst = args.N_FRAMES_PER_BURST -# enable testmode -data_handler.TESTMODE = True +@pytest.mark.parametrize("freedv_mode", ["datac0", "datac1", "datac3"]) +@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 = [] -# add command to data qeue -data_handler.DATA_QUEUE_TRANSMIT.put(['ARQ_FILE', bytes_out, mode, n_frames_per_burst]) + 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 + # Enable testmode + 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" + # start data handler + 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) diff --git a/test/test_audiobuffer.py b/test/test_audiobuffer.py index 9ef333ed..94003105 100644 --- a/test/test_audiobuffer.py +++ b/test/test_audiobuffer.py @@ -3,57 +3,86 @@ # # tests audio buffer thread safety +# pylint: disable=global-statement, invalid-name + import sys -sys.path.insert(0,'..') -from tnc import codec2 import threading -import numpy as np from time import sleep +import codec2 +import numpy as np +import pytest + BUFFER_SZ = 1024 -N_MAX = 100 # write a repeating sequence of 0..N_MAX-1 -WRITE_SZ = 10 # different read and write sized buffers -READ_SZ = 8 -NTESTS = 10000 +N_MAX = 100 # write a repeating sequence of 0..N_MAX-1 +WRITE_SZ = 10 # different read and write sized buffers +READ_SZ = 8 +NTESTS = 10000 running = True audio_buffer = codec2.audio_buffer(BUFFER_SZ) -n_write = int(0) -n_read = int(0) +n_write = 0 -def writer(): + +def t_writer(): + """ + Subprocess to handle writes to the NumPY audio "device." + """ global n_write print("writer starting") - n = int(0) + n = 0 buf = np.zeros(WRITE_SZ, dtype=np.int16) while running: nfree = audio_buffer.size - audio_buffer.nbuffer if nfree >= WRITE_SZ: - for i in range(0,WRITE_SZ): - buf[i] = n; + for index in range(WRITE_SZ): + buf[index] = n n += 1 if n == N_MAX: n = 0 n_write += 1 audio_buffer.push(buf) - -x = threading.Thread(target=writer) -x.start() -n_out = int(0) -errors = int(0) -for tests in range(1,NTESTS): - while audio_buffer.nbuffer < READ_SZ: - sleep(0.001) - for i in range(0,READ_SZ): - if audio_buffer.buffer[i] != n_out: - errors += 1 - n_out += 1 - if n_out == N_MAX: - n_out = 0 - n_read += 1 - audio_buffer.pop(READ_SZ) -running = False -print("n_write: %d n_read: %d errors: %d " % (n_write, n_read, errors)) +def test_audiobuffer(): + """ + 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: + sleep(0.001) + for i in range(READ_SZ): + if audio_buffer.buffer[i] != n_out: + errors += 1 + n_out += 1 + if n_out == N_MAX: + n_out = 0 + n_read += 1 + audio_buffer.pop(READ_SZ) + + print(f"n_write: {n_write} n_read: {n_read} errors: {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) diff --git a/test/test_callback_multimode_rx.py b/test/test_callback_multimode_rx.py index cc58e6a8..12c671f5 100644 --- a/test/test_callback_multimode_rx.py +++ b/test/test_callback_multimode_rx.py @@ -6,41 +6,54 @@ Created on Wed Dec 23 07:04:24 2020 @author: DJ2LS """ -import ctypes -from ctypes import * -import pathlib -import pyaudio -import sys -import logging -import time -import threading -import sys import argparse +import ctypes +import sys +import threading +import time + import numpy as np -sys.path.insert(0,'..') +import pyaudio + +sys.path.insert(0, "..") from tnc import codec2 -#--------------------------------------------GET PARAMETER INPUTS -parser = argparse.ArgumentParser(description='FreeDATA audio 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('--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") +# --------------------------------------------GET PARAMETER INPUTS +parser = argparse.ArgumentParser(description="FreeDATA audio 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( + "--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() +args, _ = parser.parse_known_args() if args.LIST: 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"]) - quit() + sys.exit() - -class Test(): +class Test: def __init__(self): self.N_BURSTS = args.N_BURSTS self.N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST @@ -49,65 +62,89 @@ class Test(): self.TIMEOUT = args.TIMEOUT # 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.AUDIO_SAMPLE_RATE_RX = 48000 - - # 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 - if self.AUDIO_INPUT_DEVICE != -1: + if self.AUDIO_INPUT_DEVICE != -1: self.p = pyaudio.PyAudio() # auto search for loopback devices if self.AUDIO_INPUT_DEVICE == -2: loopback_list = [] - for dev in range(0,self.p.get_device_count()): - if 'Loopback: PCM' in self.p.get_device_info_by_index(dev)["name"]: + for dev in range(self.p.get_device_count()): + if "Loopback: PCM" in self.p.get_device_info_by_index(dev)["name"]: loopback_list.append(dev) if len(loopback_list) >= 2: - 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) else: - quit() - - print(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, - channels=1, - rate=self.AUDIO_SAMPLE_RATE_RX, - frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER, - input=True, - output=False, - input_device_index=self.AUDIO_INPUT_DEVICE, - stream_callback=self.callback - ) + sys.exit() + print( + 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, + channels=1, + rate=self.AUDIO_SAMPLE_RATE_RX, + frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER, + input=True, + output=False, + input_device_index=self.AUDIO_INPUT_DEVICE, + stream_callback=self.callback, + ) + else: + print("test_callback_multimode_rx: Not written for STDIN usage.") + print("Exiting.") + sys.exit() - # open codec2 instance - self.datac0_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC0), c_void_p) - self.datac0_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(self.datac0_freedv)/8) - 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_buffer = codec2.audio_buffer(2*self.AUDIO_FRAMES_PER_BUFFER) - - self.datac1_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC1), c_void_p) - self.datac1_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(self.datac1_freedv)/8) - self.datac1_bytes_out = 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 = 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) + # open codec2 instance + self.datac0_freedv = ctypes.cast( + codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC0), ctypes.c_void_p + ) + self.datac0_bytes_per_frame = int( + codec2.api.freedv_get_bits_per_modem_frame(self.datac0_freedv) / 8 + ) + self.datac0_bytes_out = ctypes.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_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 self.rx_total_frames_datac0 = 0 @@ -127,36 +164,38 @@ class Test(): self.timeout = time.time() + self.TIMEOUT self.receive = True self.resampler = codec2.resampler() - + # Copy received 48 kHz to a file. Listen to this file with: # aplay -r 48000 -f S16_LE rx48_callback.raw # Corruption of this file is a good way to detect audio card issues - self.frx = open("rx48_callback_multimode.raw", mode='wb') - - - # initial nin values - self.datac0_nin = codec2.api.freedv_nin(self.datac0_freedv) - self.datac1_nin = codec2.api.freedv_nin(self.datac1_freedv) + self.frx = open("rx48_callback_multimode.raw", mode="wb") + + # initial nin values + self.datac0_nin = codec2.api.freedv_nin(self.datac0_freedv) + self.datac1_nin = codec2.api.freedv_nin(self.datac1_freedv) self.datac3_nin = codec2.api.freedv_nin(self.datac3_freedv) - - self.LOGGER_THREAD = threading.Thread(target=self.print_stats, name="LOGGER_THREAD") + self.LOGGER_THREAD = threading.Thread( + target=self.print_stats, name="LOGGER_THREAD" + ) self.LOGGER_THREAD.start() - - + def callback(self, data_in48k, frame_count, time_info, status): x = np.frombuffer(data_in48k, dtype=np.int16) x.tofile(self.frx) - x = self.resampler.resample48_to_8(x) + x = self.resampler.resample48_to_8(x) self.datac0_buffer.push(x) self.datac1_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 - 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_nin = codec2.api.freedv_nin(self.datac0_freedv) if nbytes == self.datac0_bytes_per_frame: @@ -167,10 +206,13 @@ class Test(): self.rx_frames_datac0 = 0 self.rx_bursts_datac0 = self.rx_bursts_datac0 + 1 - while self.datac1_buffer.nbuffer >= self.datac1_nin: # demodulate audio - nbytes = codec2.api.freedv_rawdatarx(self.datac1_freedv, self.datac1_bytes_out, self.datac1_buffer.buffer.ctypes) + nbytes = codec2.api.freedv_rawdatarx( + self.datac1_freedv, + self.datac1_bytes_out, + self.datac1_buffer.buffer.ctypes, + ) self.datac1_buffer.pop(self.datac1_nin) self.datac1_nin = codec2.api.freedv_nin(self.datac1_freedv) if nbytes == self.datac1_bytes_per_frame: @@ -181,10 +223,13 @@ class Test(): self.rx_frames_datac1 = 0 self.rx_bursts_datac1 = self.rx_bursts_datac1 + 1 - while self.datac3_buffer.nbuffer >= self.datac3_nin: - # demodulate audio - nbytes = codec2.api.freedv_rawdatarx(self.datac3_freedv, self.datac3_bytes_out, self.datac3_buffer.buffer.ctypes) + # demodulate audio + nbytes = codec2.api.freedv_rawdatarx( + self.datac3_freedv, + self.datac3_bytes_out, + self.datac3_buffer.buffer.ctypes, + ) self.datac3_buffer.pop(self.datac3_nin) self.datac3_nin = codec2.api.freedv_nin(self.datac3_freedv) if nbytes == self.datac3_bytes_per_frame: @@ -193,39 +238,59 @@ class Test(): if self.rx_frames_datac3 == self.N_FRAMES_PER_BURST: self.rx_frames_datac3 = 0 - self.rx_bursts_datac3 = self.rx_bursts_datac3 + 1 + self.rx_bursts_datac3 = self.rx_bursts_datac3 + 1 - - 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 - + return (None, pyaudio.paContinue) - + def print_stats(self): while self.receive: time.sleep(0.01) if self.DEBUGGING_MODE: - 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.freedv_get_rx_status( + 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.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.rx_sync_flags_to_text[self.datac3_rxstatus] + 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.datac3_rxstatus = codec2.api.freedv_get_rx_status( + self.datac3_freedv + ) + 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): - try: + try: print(f"starting pyaudio callback", file=sys.stderr) self.stream_rx.start_stream() 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: time.sleep(1) @@ -233,18 +298,24 @@ class Test(): if time.time() >= self.timeout and self.stream_rx.is_active(): print("TIMEOUT REACHED") self.receive = False - - if self.nread_exceptions: - print("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) + if self.nread_exceptions: + print( + "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, + ) self.frx.close() # cloese pyaudio instance self.stream_rx.close() self.p.terminate() - - + + test = Test() -test.run_audio() +test.run_audio() diff --git a/test/test_callback_multimode_rx_outside.py b/test/test_callback_multimode_rx_outside.py index 43226610..cd704a81 100644 --- a/test/test_callback_multimode_rx_outside.py +++ b/test/test_callback_multimode_rx_outside.py @@ -6,41 +6,52 @@ Created on Wed Dec 23 07:04:24 2020 @author: DJ2LS """ -import ctypes -from ctypes import * -import pathlib -import pyaudio -import sys -import logging -import time -import threading -import sys import argparse +import ctypes +import sys +import time + import numpy as np -sys.path.insert(0,'..') +import pyaudio + +sys.path.insert(0, "..") from tnc import codec2 -#--------------------------------------------GET PARAMETER INPUTS -parser = argparse.ArgumentParser(description='FreeDATA audio 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('--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") +# --------------------------------------------GET PARAMETER INPUTS +parser = argparse.ArgumentParser(description="FreeDATA audio 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( + "--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() +args, _ = parser.parse_known_args() if args.LIST: 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"]) - quit() - -class Test(): +class Test: def __init__(self): self.N_BURSTS = args.N_BURSTS self.N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST @@ -49,63 +60,91 @@ class Test(): self.TIMEOUT = args.TIMEOUT # 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.AUDIO_SAMPLE_RATE_RX = 48000 # 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 - if self.AUDIO_INPUT_DEVICE != -1: + if self.AUDIO_INPUT_DEVICE != -1: self.p = pyaudio.PyAudio() # auto search for loopback devices if self.AUDIO_INPUT_DEVICE == -2: - loopback_list = [] - for dev in range(0,self.p.get_device_count()): - if 'Loopback: PCM' in self.p.get_device_info_by_index(dev)["name"]: - loopback_list.append(dev) + loopback_list = [ + dev + for dev in range(self.p.get_device_count()) + if "Loopback: PCM" in self.p.get_device_info_by_index(dev)["name"] + ] + 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) else: - quit() - - print(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, - channels=1, - rate=self.AUDIO_SAMPLE_RATE_RX, - frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER, - input=True, - output=False, - input_device_index=self.AUDIO_INPUT_DEVICE, - stream_callback=self.callback - ) + sys.exit() + print( + 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, + channels=1, + rate=self.AUDIO_SAMPLE_RATE_RX, + frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER, + input=True, + output=False, + input_device_index=self.AUDIO_INPUT_DEVICE, + stream_callback=self.callback, + ) + else: + print("test_callback_multimode_rx_outside: Not written for STDIN usage.") + print("Exiting.") + sys.exit() - # open codec2 instance - self.datac0_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC0), c_void_p) - self.datac0_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(self.datac0_freedv)/8) - 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_buffer = codec2.audio_buffer(2*self.AUDIO_FRAMES_PER_BUFFER) - - self.datac1_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC1), c_void_p) - self.datac1_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(self.datac1_freedv)/8) - self.datac1_bytes_out = 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 = 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) + # open codec2 instance + self.datac0_freedv = ctypes.cast( + codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC0), ctypes.c_void_p + ) + self.datac0_bytes_per_frame = int( + codec2.api.freedv_get_bits_per_modem_frame(self.datac0_freedv) / 8 + ) + self.datac0_bytes_out = ctypes.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_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 self.rx_total_frames_datac0 = 0 @@ -125,62 +164,74 @@ class Test(): self.timeout = time.time() + self.TIMEOUT self.receive = True self.resampler = codec2.resampler() - + # Copy received 48 kHz to a file. Listen to this file with: # aplay -r 48000 -f S16_LE rx48_callback.raw # Corruption of this file is a good way to detect audio card issues - self.frx = open("rx48_callback_multimode.raw", mode='wb') - - - # initial nin values - self.datac0_nin = codec2.api.freedv_nin(self.datac0_freedv) - self.datac1_nin = codec2.api.freedv_nin(self.datac1_freedv) + self.frx = open("rx48_callback_multimode.raw", mode="wb") + + # initial nin values + self.datac0_nin = codec2.api.freedv_nin(self.datac0_freedv) + self.datac1_nin = codec2.api.freedv_nin(self.datac1_freedv) self.datac3_nin = codec2.api.freedv_nin(self.datac3_freedv) - - + def callback(self, data_in48k, frame_count, time_info, status): - + x = np.frombuffer(data_in48k, dtype=np.int16) x.tofile(self.frx) - x = self.resampler.resample48_to_8(x) + x = self.resampler.resample48_to_8(x) self.datac0_buffer.push(x) self.datac1_buffer.push(x) self.datac3_buffer.push(x) - - - return (None, pyaudio.paContinue) - + def print_stats(self): if self.DEBUGGING_MODE: 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.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.rx_sync_flags_to_text[self.datac3_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.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): - try: + try: print(f"starting pyaudio callback", file=sys.stderr) self.stream_rx.start_stream() 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.datac0_buffer.nbuffer >= self.datac0_nin: + while self.datac0_buffer.nbuffer >= self.datac0_nin: # demodulate audio - nbytes = codec2.api.freedv_rawdatarx(self.datac0_freedv, self.datac0_bytes_out, self.datac0_buffer.buffer.ctypes) + nbytes = codec2.api.freedv_rawdatarx( + self.datac0_freedv, + self.datac0_bytes_out, + self.datac0_buffer.buffer.ctypes, + ) self.datac0_buffer.pop(self.datac0_nin) self.datac0_nin = codec2.api.freedv_nin(self.datac0_freedv) if nbytes == self.datac0_bytes_per_frame: @@ -192,10 +243,13 @@ class Test(): self.rx_bursts_datac0 = self.rx_bursts_datac0 + 1 self.print_stats() - while self.datac1_buffer.nbuffer >= self.datac1_nin: # demodulate audio - nbytes = codec2.api.freedv_rawdatarx(self.datac1_freedv, self.datac1_bytes_out, self.datac1_buffer.buffer.ctypes) + nbytes = codec2.api.freedv_rawdatarx( + self.datac1_freedv, + self.datac1_bytes_out, + self.datac1_buffer.buffer.ctypes, + ) self.datac1_buffer.pop(self.datac1_nin) self.datac1_nin = codec2.api.freedv_nin(self.datac1_freedv) if nbytes == self.datac1_bytes_per_frame: @@ -206,10 +260,14 @@ class Test(): self.rx_frames_datac1 = 0 self.rx_bursts_datac1 = self.rx_bursts_datac1 + 1 self.print_stats() - + while self.datac3_buffer.nbuffer >= self.datac3_nin: - # demodulate audio - nbytes = codec2.api.freedv_rawdatarx(self.datac3_freedv, self.datac3_bytes_out, self.datac3_buffer.buffer.ctypes) + # demodulate audio + nbytes = codec2.api.freedv_rawdatarx( + self.datac3_freedv, + self.datac3_bytes_out, + self.datac3_buffer.buffer.ctypes, + ) self.datac3_buffer.pop(self.datac3_nin) self.datac3_nin = codec2.api.freedv_nin(self.datac3_freedv) if nbytes == self.datac3_bytes_per_frame: @@ -218,27 +276,36 @@ class Test(): if self.rx_frames_datac3 == self.N_FRAMES_PER_BURST: self.rx_frames_datac3 = 0 - self.rx_bursts_datac3 = self.rx_bursts_datac3 + 1 + self.rx_bursts_datac3 = self.rx_bursts_datac3 + 1 self.print_stats() - if (self.rx_bursts_datac0 and self.rx_bursts_datac1 and self.rx_bursts_datac3) == self.N_BURSTS: - self.receive = False - + if ( + self.rx_bursts_datac0 + and self.rx_bursts_datac1 + and self.rx_bursts_datac3 + ) == self.N_BURSTS: + self.receive = False if time.time() >= self.timeout: print("TIMEOUT REACHED") - - if self.nread_exceptions: - print("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) + if self.nread_exceptions: + print( + "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, + ) self.frx.close() # cloese pyaudio instance self.stream_rx.close() self.p.terminate() - - + + test = Test() -test.run_audio() +test.run_audio() diff --git a/test/test_callback_multimode_tx.py b/test/test_callback_multimode_tx.py index f620a6e4..bc80bd21 100644 --- a/test/test_callback_multimode_tx.py +++ b/test/test_callback_multimode_tx.py @@ -6,182 +6,227 @@ Created on Wed Dec 23 07:04:24 2020 @author: DJ2LS """ -import ctypes -from ctypes import * -import pathlib -import pyaudio -import sys -import logging -import time -import threading -import sys import argparse +import ctypes import queue +import sys +import time + import numpy as np -sys.path.insert(0,'..') +import pyaudio + +sys.path.insert(0, "..") from tnc import codec2 -#--------------------------------------------GET PARAMETER INPUTS -parser = argparse.ArgumentParser(description='FreeDATA audio 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") -parser.add_argument('--testframes', dest="TESTFRAMES", action="store_true", default=False, help="generate testframes") +# --------------------------------------------GET PARAMETER INPUTS +parser = argparse.ArgumentParser(description="FreeDATA audio 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", +) +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: 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"]) - quit() + sys.exit() - -class Test(): +class Test: def __init__(self): self.dataqueue = queue.Queue() self.N_BURSTS = args.N_BURSTS self.N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST 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 - 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.AUDIO_SAMPLE_RATE_TX = 48000 # 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.resampler = codec2.resampler() - - + # 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() # auto search for loopback devices if self.AUDIO_OUTPUT_DEVICE == -2: - loopback_list = [] - for dev in range(0,self.p.get_device_count()): - if 'Loopback: PCM' in self.p.get_device_info_by_index(dev)["name"]: - loopback_list.append(dev) + loopback_list = [ + dev + for dev in range(self.p.get_device_count()) + if "Loopback: PCM" in self.p.get_device_info_by_index(dev)["name"] + ] + 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) else: - quit() - - print(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, - channels=1, - rate=self.AUDIO_SAMPLE_RATE_TX, - frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER, - input=False, - output=True, - output_device_index=self.AUDIO_OUTPUT_DEVICE, - stream_callback=self.callback - ) + sys.exit() + + print( + 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, + channels=1, + rate=self.AUDIO_SAMPLE_RATE_TX, + frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER, + input=False, + output=True, + output_device_index=self.AUDIO_OUTPUT_DEVICE, + 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: # aplay -r 48000 -f S16_LE rx48_callback.raw # 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 if args.TESTFRAMES: self.data_out = bytearray(14) - self.data_out[:1] = bytes([255]) - self.data_out[1:2] = bytes([1]) - self.data_out[2:] = b'HELLO WORLD' - - else: - self.data_out = b'HELLO WORLD!' + self.data_out[:1] = bytes([255]) + self.data_out[1:2] = bytes([1]) + self.data_out[2:] = b"HELLO WORLD" + + else: + self.data_out = b"HELLO WORLD!" - def callback(self, data_in48k, frame_count, time_info, status): - + data_out48k = self.dataqueue.get() return (data_out48k, pyaudio.paContinue) def run_audio(self): - try: + try: print(f"starting pyaudio callback", file=sys.stderr) self.stream_tx.start_stream() except Exception as e: - print(f"pyAudio error: {e}", file=sys.stderr) - + print(f"pyAudio error: {e}", file=sys.stderr) + sheeps = 0 while self.transmit: time.sleep(1) sheeps = sheeps + 1 print(f"counting sheeps...{sheeps}") - + self.ftx.close() # close pyaudio instance self.stream_tx.close() self.p.terminate() - + 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: - - freedv = cast(codec2.api.freedv_open(m), c_void_p) - - n_tx_modem_samples = codec2.api.freedv_get_n_tx_modem_samples(freedv) - mod_out = create_string_buffer(2*n_tx_modem_samples) - - n_tx_preamble_modem_samples = codec2.api.freedv_get_n_tx_preamble_modem_samples(freedv) - mod_out_preamble = create_string_buffer(2*n_tx_preamble_modem_samples) - n_tx_postamble_modem_samples = codec2.api.freedv_get_n_tx_postamble_modem_samples(freedv) - mod_out_postamble = create_string_buffer(2*n_tx_postamble_modem_samples) - - bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(freedv) / 8) + 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) + 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) + ) + 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) + ) + 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 + ) payload_per_frame = bytes_per_frame - 2 buffer = bytearray(payload_per_frame) # 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 - crc = crc.value.to_bytes(2, byteorder='big') - buffer += crc # append crc16 to buffer + 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) - - for i in range(1,self.N_BURSTS+1): + + for i in range(1, self.N_BURSTS + 1): # write preamble to txbuffer codec2.api.freedv_rawdatapreambletx(freedv, mod_out_preamble) txbuffer = bytes(mod_out_preamble) # 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) - 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) - 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) txbuffer += bytes(mod_out_postamble) # append a delay between bursts as audio silence - samples_delay = int(self.MODEM_SAMPLE_RATE*self.DELAY_BETWEEN_BURSTS) - mod_out_silence = create_string_buffer(samples_delay*2) + samples_delay = int(self.MODEM_SAMPLE_RATE * self.DELAY_BETWEEN_BURSTS) + mod_out_silence = ctypes.create_string_buffer(samples_delay * 2) txbuffer += bytes(mod_out_silence) # resample up to 48k (resampler works on np.int16) @@ -189,20 +234,23 @@ class Test(): txbuffer_48k = self.resampler.resample8_to_48(x) # 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) - 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 for c in chunk: # if data is shorter than the expcected audio frames per buffer we need to append 0 # to prevent the callback from stucking/crashing - if len(c) < self.AUDIO_FRAMES_PER_BUFFER*2: - c += bytes(self.AUDIO_FRAMES_PER_BUFFER*2 - len(c)) + if len(c) < self.AUDIO_FRAMES_PER_BUFFER * 2: + c += bytes(self.AUDIO_FRAMES_PER_BUFFER * 2 - len(c)) self.dataqueue.put(c) - - test = Test() test.create_modulation() -test.run_audio() +test.run_audio() diff --git a/test/test_callback_rx.py b/test/test_callback_rx.py index 1ea86e4a..ffbb6c02 100644 --- a/test/test_callback_rx.py +++ b/test/test_callback_rx.py @@ -6,42 +6,56 @@ Created on Wed Dec 23 07:04:24 2020 @author: DJ2LS """ -import ctypes -from ctypes import * -import pathlib -import pyaudio -import sys -import logging -import time -import threading -import sys import argparse +import ctypes +import sys +import time + import numpy as np -sys.path.insert(0,'..') +import pyaudio + +sys.path.insert(0, "..") from tnc import codec2 -#--------------------------------------------GET PARAMETER INPUTS -parser = argparse.ArgumentParser(description='FreeDATA audio 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('--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") +# --------------------------------------------GET PARAMETER INPUTS +parser = argparse.ArgumentParser(description="FreeDATA audio 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( + "--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() +args, _ = parser.parse_known_args() if args.LIST: 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"]) - quit() + sys.exit() - -class Test(): +class Test: def __init__(self): self.N_BURSTS = args.N_BURSTS self.N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST @@ -51,51 +65,61 @@ class Test(): self.TIMEOUT = args.TIMEOUT # 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.AUDIO_SAMPLE_RATE_RX = 48000 # 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 - if self.AUDIO_INPUT_DEVICE != -1: + if self.AUDIO_INPUT_DEVICE != -1: self.p = pyaudio.PyAudio() # auto search for loopback devices if self.AUDIO_INPUT_DEVICE == -2: loopback_list = [] - for dev in range(0,self.p.get_device_count()): - if 'Loopback: PCM' in self.p.get_device_info_by_index(dev)["name"]: + for dev in range(self.p.get_device_count()): + if "Loopback: PCM" in self.p.get_device_info_by_index(dev)["name"]: loopback_list.append(dev) if len(loopback_list) >= 2: - 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) else: - quit() - - print(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, - channels=1, - rate=self.AUDIO_SAMPLE_RATE_RX, - frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER, - input=True, - output=False, - input_device_index=self.AUDIO_INPUT_DEVICE, - stream_callback=self.callback - ) + sys.exit() + + print( + 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, + channels=1, + rate=self.AUDIO_SAMPLE_RATE_RX, + frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER, + input=True, + output=False, + input_device_index=self.AUDIO_INPUT_DEVICE, + stream_callback=self.callback, + ) + + # open codec2 instance + self.freedv = ctypes.cast(codec2.api.freedv_open(self.MODE), ctypes.c_void_p) - # open codec2 instance - self.freedv = cast(codec2.api.freedv_open(self.MODE), c_void_p) - # 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_out = create_string_buffer(self.bytes_per_frame) - - codec2.api.freedv_set_frames_per_burst(self.freedv,self.N_FRAMES_PER_BURST) - + self.bytes_per_frame = int( + codec2.api.freedv_get_bits_per_modem_frame(self.freedv) / 8 + ) + + 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) + self.total_n_bytes = 0 self.rx_total_frames = 0 self.rx_frames = 0 @@ -104,44 +128,48 @@ class Test(): self.nread_exceptions = 0 self.timeout = time.time() + self.TIMEOUT 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() - + # Copy received 48 kHz to a file. Listen to this file with: # aplay -r 48000 -f S16_LE rx48_callback.raw # 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): - + x = np.frombuffer(data_in48k, dtype=np.int16) x.tofile(self.frx) - x = self.resampler.resample48_to_8(x) + x = self.resampler.resample48_to_8(x) self.audio_buffer.push(x) - # when we have enough samples call FreeDV Rx nin = codec2.api.freedv_nin(self.freedv) while self.audio_buffer.nbuffer >= nin: # 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) - + # call me on every loop! nin = codec2.api.freedv_nin(self.freedv) - - rx_status = codec2.api.freedv_get_rx_status(self.freedv) + + rx_status = codec2.api.freedv_get_rx_status(self.freedv) if rx_status & codec2.api.FREEDV_RX_BIT_ERRORS: self.rx_errors = self.rx_errors + 1 - if self.DEBUGGING_MODE: - rx_status = codec2.api.rx_sync_flags_to_text[rx_status] - print("nin: %5d rx_status: %4s naudio_buffer: %4d" % \ - (nin,rx_status,self.audio_buffer.nbuffer), file=sys.stderr) + if self.DEBUGGING_MODE: + rx_status = codec2.api.rx_sync_flags_to_text[rx_status] + print( + "nin: %5d rx_status: %4s naudio_buffer: %4d" + % (nin, rx_status, self.audio_buffer.nbuffer), + file=sys.stderr, + ) if nbytes: self.total_n_bytes = self.total_n_bytes + nbytes - + if nbytes == self.bytes_per_frame: self.rx_total_frames = self.rx_total_frames + 1 self.rx_frames = self.rx_frames + 1 @@ -149,36 +177,41 @@ class Test(): if self.rx_frames == self.N_FRAMES_PER_BURST: self.rx_frames = 0 self.rx_bursts = self.rx_bursts + 1 - + if self.rx_bursts == self.N_BURSTS: self.receive = False - + return (None, pyaudio.paContinue) def run_audio(self): - try: + try: print(f"starting pyaudio callback", file=sys.stderr) self.stream_rx.start_stream() 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: time.sleep(1) if time.time() >= self.timeout: print("TIMEOUT REACHED") - + if self.nread_exceptions: - print("nread_exceptions %d - receive audio lost! Consider increasing Pyaudio frames_per_buffer..." % \ - 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) + print( + "nread_exceptions %d - receive audio lost! Consider increasing Pyaudio frames_per_buffer..." + % 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() # cloese pyaudio instance self.stream_rx.close() self.p.terminate() - - + + test = Test() -test.run_audio() +test.run_audio() diff --git a/test/test_callback_rx_outside.py b/test/test_callback_rx_outside.py index cf908ca4..a006c740 100644 --- a/test/test_callback_rx_outside.py +++ b/test/test_callback_rx_outside.py @@ -6,42 +6,56 @@ Created on Wed Dec 23 07:04:24 2020 @author: DJ2LS """ -import ctypes -from ctypes import * -import pathlib -import pyaudio -import sys -import logging -import time -import threading -import sys import argparse +import ctypes +import sys +import time + import numpy as np -sys.path.insert(0,'..') +import pyaudio + +sys.path.insert(0, "..") from tnc import codec2 -#--------------------------------------------GET PARAMETER INPUTS -parser = argparse.ArgumentParser(description='FreeDATA audio 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('--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") +# --------------------------------------------GET PARAMETER INPUTS +parser = argparse.ArgumentParser(description="FreeDATA audio 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( + "--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() +args, _ = parser.parse_known_args() if args.LIST: 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"]) - quit() + sys.exit() - -class Test(): +class Test: def __init__(self): self.N_BURSTS = args.N_BURSTS self.N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST @@ -51,51 +65,61 @@ class Test(): self.TIMEOUT = args.TIMEOUT # 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.AUDIO_SAMPLE_RATE_RX = 48000 # 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 - if self.AUDIO_INPUT_DEVICE != -1: + if self.AUDIO_INPUT_DEVICE != -1: self.p = pyaudio.PyAudio() # auto search for loopback devices if self.AUDIO_INPUT_DEVICE == -2: loopback_list = [] - for dev in range(0,self.p.get_device_count()): - if 'Loopback: PCM' in self.p.get_device_info_by_index(dev)["name"]: + for dev in range(self.p.get_device_count()): + if "Loopback: PCM" in self.p.get_device_info_by_index(dev)["name"]: loopback_list.append(dev) if len(loopback_list) >= 2: - 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) else: - quit() - - print(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, - channels=1, - rate=self.AUDIO_SAMPLE_RATE_RX, - frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER, - input=True, - output=False, - input_device_index=self.AUDIO_INPUT_DEVICE, - stream_callback=self.callback - ) + sys.exit() + + print( + 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, + channels=1, + rate=self.AUDIO_SAMPLE_RATE_RX, + frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER, + input=True, + output=False, + input_device_index=self.AUDIO_INPUT_DEVICE, + stream_callback=self.callback, + ) + + # open codec2 instance + self.freedv = ctypes.cast(codec2.api.freedv_open(self.MODE), ctypes.c_void_p) - # open codec2 instance - self.freedv = cast(codec2.api.freedv_open(self.MODE), c_void_p) - # 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_out = create_string_buffer(self.bytes_per_frame * 2) - - codec2.api.freedv_set_frames_per_burst(self.freedv,self.N_FRAMES_PER_BURST) - + self.bytes_per_frame = int( + codec2.api.freedv_get_bits_per_modem_frame(self.freedv) / 8 + ) + + 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) + self.total_n_bytes = 0 self.rx_total_frames = 0 self.rx_frames = 0 @@ -104,55 +128,59 @@ class Test(): self.nread_exceptions = 0 self.timeout = time.time() + self.TIMEOUT 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() - + # Copy received 48 kHz to a file. Listen to this file with: # aplay -r 48000 -f S16_LE rx48_callback.raw # 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): - + x = np.frombuffer(data_in48k, dtype=np.int16) x.tofile(self.frx) - x = self.resampler.resample48_to_8(x) + x = self.resampler.resample48_to_8(x) self.audio_buffer.push(x) return (None, pyaudio.paContinue) def run_audio(self): - try: + try: print(f"starting pyaudio callback", file=sys.stderr) self.stream_rx.start_stream() 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: - #time.sleep(1) + # time.sleep(1) # when we have enough samples call FreeDV Rx nin = codec2.api.freedv_nin(self.freedv) while self.audio_buffer.nbuffer >= nin: # 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) - + # call me on every loop! nin = codec2.api.freedv_nin(self.freedv) - - rx_status = codec2.api.freedv_get_rx_status(self.freedv) + + rx_status = codec2.api.freedv_get_rx_status(self.freedv) if rx_status & codec2.api.FREEDV_RX_BIT_ERRORS: self.rx_errors = self.rx_errors + 1 - if self.DEBUGGING_MODE: - rx_status = codec2.api.rx_sync_flags_to_text[rx_status] - print("nin: %5d rx_status: %4s naudio_buffer: %4d" % \ - (nin,rx_status,self.audio_buffer.nbuffer), file=sys.stderr) + if self.DEBUGGING_MODE: + rx_status = codec2.api.rx_sync_flags_to_text[rx_status] + print( + "nin: %5d rx_status: %4s naudio_buffer: %4d" + % (nin, rx_status, self.audio_buffer.nbuffer), + file=sys.stderr, + ) if nbytes: self.total_n_bytes = self.total_n_bytes + nbytes - + if nbytes == self.bytes_per_frame: self.rx_total_frames = self.rx_total_frames + 1 self.rx_frames = self.rx_frames + 1 @@ -160,22 +188,28 @@ class Test(): if self.rx_frames == self.N_FRAMES_PER_BURST: self.rx_frames = 0 self.rx_bursts = self.rx_bursts + 1 - + if self.rx_bursts == self.N_BURSTS: self.receive = False if time.time() >= self.timeout: print("TIMEOUT REACHED") - + if self.nread_exceptions: - print("nread_exceptions %d - receive audio lost! Consider increasing Pyaudio frames_per_buffer..." % \ - 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) + print( + "nread_exceptions %d - receive audio lost! Consider increasing Pyaudio frames_per_buffer..." + % 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() # cloese pyaudio instance self.stream_rx.close() self.p.terminate() - - + + test = Test() -test.run_audio() +test.run_audio() diff --git a/test/test_callback_tx.py b/test/test_callback_tx.py index 5c0c07fe..f30d7bdf 100644 --- a/test/test_callback_tx.py +++ b/test/test_callback_tx.py @@ -6,43 +6,58 @@ Created on Wed Dec 23 07:04:24 2020 @author: DJ2LS """ -import ctypes -from ctypes import * -import pathlib -import pyaudio -import sys -import logging -import time -import threading -import sys + import argparse +import ctypes import queue +import sys +import time + import numpy as np -sys.path.insert(0,'..') +import pyaudio + +sys.path.insert(0, "..") from tnc import codec2 -#--------------------------------------------GET PARAMETER INPUTS -parser = argparse.ArgumentParser(description='FreeDATA audio 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('--mode', dest="FREEDV_MODE", type=str, choices=['datac0', 'datac1', 'datac3']) -parser.add_argument('--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") +# --------------------------------------------GET PARAMETER INPUTS +parser = argparse.ArgumentParser(description="FreeDATA audio 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( + "--mode", dest="FREEDV_MODE", type=str, choices=["datac0", "datac1", "datac3"] +) +parser.add_argument( + "--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: 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"]) - quit() + sys.exit() - -class Test(): +class Test: def __init__(self): self.dataqueue = queue.Queue() @@ -50,180 +65,206 @@ class Test(): self.N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST self.AUDIO_OUTPUT_DEVICE = args.AUDIO_OUTPUT_DEVICE 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 - 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.AUDIO_SAMPLE_RATE_TX = 48000 # 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.resampler = codec2.resampler() - - + # 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() # auto search for loopback devices if self.AUDIO_OUTPUT_DEVICE == -2: loopback_list = [] - for dev in range(0,self.p.get_device_count()): - if 'Loopback: PCM' in self.p.get_device_info_by_index(dev)["name"]: + for dev in range( self.p.get_device_count()): + if "Loopback: PCM" in self.p.get_device_info_by_index(dev)["name"]: loopback_list.append(dev) if len(loopback_list) >= 2: - 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) else: - quit() - - print(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, - channels=1, - rate=self.AUDIO_SAMPLE_RATE_TX, - frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER, - input=False, - output=True, - output_device_index=self.AUDIO_OUTPUT_DEVICE, - stream_callback=self.callback - ) + sys.exit() + + print( + 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, + channels=1, + rate=self.AUDIO_SAMPLE_RATE_TX, + frames_per_buffer=self.AUDIO_FRAMES_PER_BUFFER, + input=False, + output=True, + output_device_index=self.AUDIO_OUTPUT_DEVICE, + stream_callback=self.callback, + ) + + # open codec2 instance + self.freedv = ctypes.cast(codec2.api.freedv_open(self.MODE), ctypes.c_void_p) - # open codec2 instance - self.freedv = cast(codec2.api.freedv_open(self.MODE), c_void_p) - # 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_out = create_string_buffer(self.bytes_per_frame) - - codec2.api.freedv_set_frames_per_burst(self.freedv,self.N_FRAMES_PER_BURST) - - + self.bytes_per_frame = int( + codec2.api.freedv_get_bits_per_modem_frame(self.freedv) / 8 + ) + + 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) + # Copy received 48 kHz to a file. Listen to this file with: # aplay -r 48000 -f S16_LE rx48_callback.raw # 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 if args.TESTFRAMES: self.data_out = bytearray(14) - self.data_out[:1] = bytes([255]) - self.data_out[1:2] = bytes([1]) - self.data_out[2:] = b'HELLO WORLD' - - else: - self.data_out = b'HELLO WORLD!' + self.data_out[:1] = bytes([255]) + self.data_out[1:2] = bytes([1]) + self.data_out[2:] = b"HELLO WORLD" + + else: + self.data_out = b"HELLO WORLD!" - def callback(self, data_in48k, frame_count, time_info, status): - + data_out48k = self.dataqueue.get() return (data_out48k, pyaudio.paContinue) def run_audio(self): - try: + try: print(f"starting pyaudio callback", file=sys.stderr) self.stream_tx.start_stream() except Exception as e: - print(f"pyAudio error: {e}", file=sys.stderr) - + print(f"pyAudio error: {e}", file=sys.stderr) + sheeps = 0 while self.transmit: time.sleep(1) sheeps = sheeps + 1 print(f"counting sheeps...{sheeps}") - - self.ftx.close() # close pyaudio instance self.stream_tx.close() self.p.terminate() - + def create_modulation(self): - - # open codec2 instance - freedv = cast(codec2.api.freedv_open(self.MODE), c_void_p) + + # open codec2 instance + freedv = ctypes.cast(codec2.api.freedv_open(self.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 + bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(freedv) / 8) + payload_bytes_per_frame = bytes_per_frame - 2 # init buffer for data n_tx_modem_samples = codec2.api.freedv_get_n_tx_modem_samples(freedv) - mod_out = create_string_buffer(n_tx_modem_samples * 2) + 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 = create_string_buffer(n_tx_preamble_modem_samples * 2) + 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) # init buffer for postamble - n_tx_postamble_modem_samples = codec2.api.freedv_get_n_tx_postamble_modem_samples(freedv) - mod_out_postamble = create_string_buffer(n_tx_postamble_modem_samples * 2) + 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) # create buffer for data - buffer = bytearray(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 + buffer = bytearray( + 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 = 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 + 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: {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 codec2.api.freedv_rawdatapreambletx(freedv, mod_out_preamble) txbuffer = bytes(mod_out_preamble) # 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) - 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) - - print(f" GENERATING TX BURST: {i}/{self.N_BURSTS} FRAME: {n}/{self.N_FRAMES_PER_BURST}", file=sys.stderr) - - # append postamble to txbuffer + + print( + f" GENERATING TX BURST: {i}/{self.N_BURSTS} FRAME: {n}/{self.N_FRAMES_PER_BURST}", + file=sys.stderr, + ) + + # append postamble to txbuffer codec2.api.freedv_rawdatapostambletx(freedv, mod_out_postamble) txbuffer += bytes(mod_out_postamble) # append a delay between bursts as audio silence - samples_delay = int(self.MODEM_SAMPLE_RATE*self.DELAY_BETWEEN_BURSTS) - mod_out_silence = create_string_buffer(samples_delay*2) + samples_delay = int(self.MODEM_SAMPLE_RATE * self.DELAY_BETWEEN_BURSTS) + mod_out_silence = ctypes.create_string_buffer(samples_delay * 2) 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) x = np.frombuffer(txbuffer, dtype=np.int16) txbuffer_48k = self.resampler.resample8_to_48(x) # split modualted audio to chunks - #https://newbedev.com/how-to-split-a-byte-string-into-separate-bytes-in-python + # https://newbedev.com/how-to-split-a-byte-string-into-separate-bytes-in-python txbuffer_48k = bytes(txbuffer_48k) - chunk = [txbuffer_48k[i:i+self.AUDIO_FRAMES_PER_BUFFER*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 for c in chunk: # if data is shorter than the expcected audio frames per buffer we need to append 0 # to prevent the callback from stucking/crashing - if len(c) < self.AUDIO_FRAMES_PER_BUFFER*2: - c += bytes(self.AUDIO_FRAMES_PER_BUFFER*2 - len(c)) + if len(c) < self.AUDIO_FRAMES_PER_BUFFER * 2: + c += bytes(self.AUDIO_FRAMES_PER_BUFFER * 2 - len(c)) self.dataqueue.put(c) - - + test = Test() test.create_modulation() -test.run_audio() +test.run_audio() diff --git a/test/test_helpers.py b/test/test_helpers.py new file mode 100644 index 00000000..e33eb798 --- /dev/null +++ b/test/test_helpers.py @@ -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) diff --git a/test/test_highsnr_stdio_C_P_datacx.py b/test/test_highsnr_stdio_C_P_datacx.py new file mode 100644 index 00000000..2acce1b9 --- /dev/null +++ b/test/test_highsnr_stdio_C_P_datacx.py @@ -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) diff --git a/test/test_highsnr_stdio_P_C_datacx.py b/test/test_highsnr_stdio_P_C_datacx.py new file mode 100644 index 00000000..feecad37 --- /dev/null +++ b/test/test_highsnr_stdio_P_C_datacx.py @@ -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) diff --git a/test/test_highsnr_stdio_P_P_datacx.py b/test/test_highsnr_stdio_P_P_datacx.py new file mode 100644 index 00000000..7f652bf8 --- /dev/null +++ b/test/test_highsnr_stdio_P_P_datacx.py @@ -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) diff --git a/test/test_highsnr_stdio_P_P_multi.py b/test/test_highsnr_stdio_P_P_multi.py new file mode 100644 index 00000000..84ddd7f7 --- /dev/null +++ b/test/test_highsnr_stdio_P_P_multi.py @@ -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) diff --git a/test/test_modem.py b/test/test_modem.py new file mode 100644 index 00000000..ef5eb6ae --- /dev/null +++ b/test/test_modem.py @@ -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) diff --git a/test/test_multimode_rx.py b/test/test_multimode_rx.py index 6e380688..3b58b306 100755 --- a/test/test_multimode_rx.py +++ b/test/test_multimode_rx.py @@ -1,236 +1,242 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -import pyaudio -import audioop -import time import argparse -import sys import ctypes -from ctypes import * -import pathlib -sys.path.insert(0,'..') -from tnc import codec2 +import sys +import time +from typing import List + import numpy as np +import pyaudio -#--------------------------------------------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") +sys.path.insert(0, "..") +from tnc import codec2 -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() +def test_mm_rx(): + # AUDIO PARAMETERS + AUDIO_FRAMES_PER_BUFFER = 2400 * 2 + MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000 + 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 -N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST -AUDIO_INPUT_DEVICE = args.AUDIO_INPUT_DEVICE -DEBUGGING_MODE = args.DEBUGGING_MODE -TIMEOUT = args.TIMEOUT + # SET COUNTERS + rx_bursts_datac = [0, 0, 0] + rx_frames_datac = [0, 0, 0] + rx_total_frames_datac = [0, 0, 0] -# AUDIO PARAMETERS -AUDIO_FRAMES_PER_BUFFER = 2400*2 -MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000 -AUDIO_SAMPLE_RATE_RX = 48000 -# make sure our resampler will work -assert (AUDIO_SAMPLE_RATE_RX / MODEM_SAMPLE_RATE) == codec2.api.FDMDV_OS_48 + # time meassurement + time_end_datac = [0.0, 0.0, 0.0] + time_needed_datac = [0.0, 0.0, 0.0] + time_start_datac = [0.0, 0.0, 0.0] -# SET COUNTERS -rx_total_frames_datac0 = 0 -rx_frames_datac0 = 0 -rx_bursts_datac0 = 0 + datac_buffer: List[codec2.audio_buffer] = [] + datac_bytes_out: List[ctypes.Array] = [] + datac_bytes_per_frame = [] + datac_freedv: List[ctypes.c_void_p] = [] -rx_total_frames_datac1 = 0 -rx_frames_datac1 = 0 -rx_bursts_datac1 = 0 + args = parse_arguments() -rx_total_frames_datac3 = 0 -rx_frames_datac3 = 0 -rx_bursts_datac3 = 0 + 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() -# time meassurement -time_start_datac0 = 0 -time_end_datac0 = 0 -time_start_datac1 = 0 -time_end_datac1 = 0 -time_start_datac3 = 0 -time_end_datac3 = 0 -time_needed_datac0 = 0 -time_needed_datac1 = 0 -time_needed_datac3 = 0 + N_BURSTS = args.N_BURSTS + N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST + AUDIO_INPUT_DEVICE = args.AUDIO_INPUT_DEVICE + DEBUGGING_MODE = args.DEBUGGING_MODE + MAX_TIME = args.TIMEOUT -# open codec2 instance -datac0_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC0), c_void_p) -datac0_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(datac0_freedv)/8) -datac0_bytes_out = create_string_buffer(datac0_bytes_per_frame) -codec2.api.freedv_set_frames_per_burst(datac0_freedv,N_FRAMES_PER_BURST) -datac0_buffer = codec2.audio_buffer(2*AUDIO_FRAMES_PER_BUFFER) + # open codec2 instances + for idx in range(3): + datac_freedv.append( + ctypes.cast( + codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC0), ctypes.c_void_p + ) + ) + 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) -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) + resampler = codec2.resampler() -datac3_freedv = cast(codec2.api.freedv_open(codec2.api.FREEDV_MODE_DATAC3), c_void_p) -datac3_bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(datac3_freedv)/8) -datac3_bytes_out = create_string_buffer(datac3_bytes_per_frame) -codec2.api.freedv_set_frames_per_burst(datac3_freedv,N_FRAMES_PER_BURST) -datac3_buffer = codec2.audio_buffer(2*AUDIO_FRAMES_PER_BUFFER) + # check if we want to use an audio device then do an pyaudio init + if AUDIO_INPUT_DEVICE != -1: + p_audio = pyaudio.PyAudio() + # auto search for loopback devices + if AUDIO_INPUT_DEVICE == -2: + loopback_list = [ + dev + for dev in range(p_audio.get_device_count()) + if "Loopback: PCM" in p_audio.get_device_info_by_index(dev)["name"] + ] -resampler = codec2.resampler() + if len(loopback_list) >= 2: + AUDIO_INPUT_DEVICE = loopback_list[0] # 0 = RX 1 = TX + print(f"loopback_list rx: {loopback_list}", file=sys.stderr) + else: + sys.exit() -# 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 - if AUDIO_INPUT_DEVICE == -2: - loopback_list = [] - for dev in range(0,p.get_device_count()): - if 'Loopback: PCM' in p.get_device_info_by_index(dev)["name"]: - loopback_list.append(dev) - if len(loopback_list) >= 2: - AUDIO_INPUT_DEVICE = loopback_list[0] #0 = RX 1 = TX - print(f"loopback_list rx: {loopback_list}", file=sys.stderr) + print( + 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, + rate=AUDIO_SAMPLE_RATE_RX, + frames_per_buffer=AUDIO_FRAMES_PER_BUFFER, + input=True, + input_device_index=AUDIO_INPUT_DEVICE, + ) + + timeout = time.time() + MAX_TIME + print(time.time(), MAX_TIME, timeout) + receive = True + nread_exceptions = 0 + + # initial nin values + datac_nin = [0, 0, 0] + for idx in range(3): + datac_nin[idx] = codec2.api.freedv_nin(datac_freedv[idx]) + + def print_stats(time_datac0, time_datac1, time_datac3): + if not DEBUGGING_MODE: + return + + time_datac = [time_datac0, time_datac1, time_datac3] + datac_rxstatus = ["", "", ""] + for idx in range(3): + datac_rxstatus[idx] = codec2.api.rx_sync_flags_to_text[ + codec2.api.freedv_get_rx_status(datac_freedv[idx]) + ] + + text_out = "" + 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) + + while receive and time.time() < timeout: + if AUDIO_INPUT_DEVICE != -1: + try: + data_in48k = stream_rx.read( + AUDIO_FRAMES_PER_BUFFER, exception_on_overflow=True + ) + except OSError as err: + print(err, file=sys.stderr) + if "Input overflowed" in str(err): + nread_exceptions += 1 + if "Stream closed" in str(err): + print("Ending....") + receive = False else: - quit() - - 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) - stream_rx = p.open(format=pyaudio.paInt16, - channels=1, - rate=AUDIO_SAMPLE_RATE_RX, - frames_per_buffer=AUDIO_FRAMES_PER_BUFFER, - input=True, - input_device_index=AUDIO_INPUT_DEVICE - ) - + data_in48k = sys.stdin.buffer.read(AUDIO_FRAMES_PER_BUFFER * 2) -timeout = time.time() + TIMEOUT -print(time.time(),TIMEOUT, timeout) -receive = True -nread_exceptions = 0 + # insert samples in buffer + audio_buffer = np.frombuffer(data_in48k, dtype=np.int16) + if len(audio_buffer) != AUDIO_FRAMES_PER_BUFFER: + print("len(x)", len(audio_buffer)) + receive = False + audio_buffer = resampler.resample48_to_8(audio_buffer) -# initial nin values -datac0_nin = codec2.api.freedv_nin(datac0_freedv) -datac1_nin = codec2.api.freedv_nin(datac1_freedv) -datac3_nin = codec2.api.freedv_nin(datac3_freedv) + for idx in range(3): + datac_buffer[idx].push(audio_buffer) + while datac_buffer[idx].nbuffer >= datac_nin[idx]: + # demodulate audio + time_start_datac[idx] = time.time() + nbytes = codec2.api.freedv_rawdatarx( + datac_freedv[idx], + datac_bytes_out[idx], + datac_buffer[idx].buffer.ctypes, + ) + time_end_datac[idx] = time.time() + 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 -def print_stats(time_needed_datac0, time_needed_datac1, time_needed_datac3): - if DEBUGGING_MODE: - datac0_rxstatus = codec2.api.freedv_get_rx_status(datac0_freedv) - datac0_rxstatus = codec2.api.rx_sync_flags_to_text[datac0_rxstatus] + if rx_frames_datac[idx] == N_FRAMES_PER_BURST: + rx_frames_datac[idx] = 0 + rx_bursts_datac[idx] += 1 + time_needed_datac[idx] = time_end_datac[idx] - time_start_datac[idx] + print_stats( + time_needed_datac[0], time_needed_datac[1], time_needed_datac[2] + ) - datac1_rxstatus = codec2.api.freedv_get_rx_status(datac1_freedv) - datac1_rxstatus = codec2.api.rx_sync_flags_to_text[datac1_rxstatus] - - datac3_rxstatus = codec2.api.freedv_get_rx_status(datac3_freedv) - datac3_rxstatus = codec2.api.rx_sync_flags_to_text[datac3_rxstatus] + if ( + rx_bursts_datac[0] == N_BURSTS + and rx_bursts_datac[1] == N_BURSTS + and rx_bursts_datac[2] == N_BURSTS + ): + receive = False - 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) + if nread_exceptions: + print( + f"nread_exceptions {nread_exceptions:d} - receive audio lost! " + "Consider increasing Pyaudio frames_per_buffer...", + file=sys.stderr, + ) + # INFO IF WE REACHED TIMEOUT + if time.time() > timeout: + print("TIMEOUT REACHED", file=sys.stderr) -while receive and time.time() < timeout: - if AUDIO_INPUT_DEVICE != -1: - try: - data_in48k = stream_rx.read(AUDIO_FRAMES_PER_BUFFER, exception_on_overflow = True) - except OSError as err: - print(err, file=sys.stderr) - if str(err).find("Input overflowed") != -1: - nread_exceptions += 1 - if str(err).find("Stream closed") != -1: - print("Ending....") - receive = False - else: - data_in48k = sys.stdin.buffer.read(AUDIO_FRAMES_PER_BUFFER*2) - - # insert samples in buffer - x = np.frombuffer(data_in48k, dtype=np.int16) - if len(x) != AUDIO_FRAMES_PER_BUFFER: - print("len(x)",len(x)) - receive = False - x = resampler.resample48_to_8(x) - - datac0_buffer.push(x) - datac1_buffer.push(x) - datac3_buffer.push(x) - print_something = False - - while datac0_buffer.nbuffer >= datac0_nin: - # demodulate audio - time_start_datac0 = time.time() - nbytes = codec2.api.freedv_rawdatarx(datac0_freedv, datac0_bytes_out, datac0_buffer.buffer.ctypes) - time_end_datac0 = time.time() - datac0_buffer.pop(datac0_nin) - datac0_nin = codec2.api.freedv_nin(datac0_freedv) - if nbytes == datac0_bytes_per_frame: - rx_total_frames_datac0 = rx_total_frames_datac0 + 1 - rx_frames_datac0 = rx_frames_datac0 + 1 + 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 rx_frames_datac0 == N_FRAMES_PER_BURST: - rx_frames_datac0 = 0 - rx_bursts_datac0 = rx_bursts_datac0 + 1 - time_needed_datac0 = time_end_datac0 - time_start_datac0 - print_stats(time_needed_datac0, time_needed_datac1, time_needed_datac3) - - while datac1_buffer.nbuffer >= datac1_nin: - # demodulate audio - time_start_datac1 = time.time() - nbytes = codec2.api.freedv_rawdatarx(datac1_freedv, datac1_bytes_out, datac1_buffer.buffer.ctypes) - 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 - -if nread_exceptions: - print("nread_exceptions %d - receive audio lost! Consider increasing Pyaudio frames_per_buffer..." % \ - nread_exceptions, file=sys.stderr) -# INFO IF WE REACHED TIMEOUT -if time.time() > timeout: - print(f"TIMEOUT REACHED", file=sys.stderr) - + if AUDIO_INPUT_DEVICE != -1: + stream_rx.close() + 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 -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() - p.terminate() +if __name__ == "__main__": + test_mm_rx() diff --git a/test/test_multimode_tx.py b/test/test_multimode_tx.py index 1aaa74ec..dd4ef8b1 100644 --- a/test/test_multimode_tx.py +++ b/test/test_multimode_tx.py @@ -2,150 +2,187 @@ # -*- coding: utf-8 -*- -import ctypes -from ctypes import * -import pathlib -import pyaudio -import time -import threading -import audioop import argparse +import ctypes import sys -sys.path.insert(0,'..') +import time + +import numpy as np +import pyaudio + +sys.path.insert(0, "..") from tnc import codec2 -import numpy as np - -# 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_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 -AUDIO_FRAMES_PER_BUFFER = 2400 -MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000 -AUDIO_SAMPLE_RATE_TX = 48000 -assert (AUDIO_SAMPLE_RATE_TX % MODEM_SAMPLE_RATE) == 0 +def test_mm_tx(): + # AUDIO PARAMETERS + AUDIO_FRAMES_PER_BUFFER = 2400 + MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000 + AUDIO_SAMPLE_RATE_TX = 48000 + assert (AUDIO_SAMPLE_RATE_TX % MODEM_SAMPLE_RATE) == 0 -if AUDIO_OUTPUT_DEVICE != -1: - p = pyaudio.PyAudio() - # auto search for loopback devices - if AUDIO_OUTPUT_DEVICE == -2: - loopback_list = [] - for dev in range(0,p.get_device_count()): - if 'Loopback: PCM' in p.get_device_info_by_index(dev)["name"]: - loopback_list.append(dev) - if len(loopback_list) >= 2: - AUDIO_OUTPUT_DEVICE = loopback_list[1] #0 = RX 1 = TX - print(f"loopback_list tx: {loopback_list}", file=sys.stderr) - else: - quit() - # pyaudio init - stream_tx = p.open(format=pyaudio.paInt16, - channels=1, - rate=AUDIO_SAMPLE_RATE_TX, - frames_per_buffer=AUDIO_FRAMES_PER_BUFFER, #n_nom_modem_samples - output=True, - output_device_index=AUDIO_OUTPUT_DEVICE, - ) + args = parse_arguments() -resampler = codec2.resampler() -modes = [codec2.api.FREEDV_MODE_DATAC0, codec2.api.FREEDV_MODE_DATAC1, codec2.api.FREEDV_MODE_DATAC3] -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) - mod_out = create_string_buffer(2*n_tx_modem_samples) - - n_tx_preamble_modem_samples = codec2.api.freedv_get_n_tx_preamble_modem_samples(freedv) - mod_out_preamble = create_string_buffer(2*n_tx_preamble_modem_samples) + 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_tx_postamble_modem_samples = codec2.api.freedv_get_n_tx_postamble_modem_samples(freedv) - mod_out_postamble = create_string_buffer(2*n_tx_postamble_modem_samples) - - bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(freedv) / 8) - payload_per_frame = bytes_per_frame - 2 + 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 - - # data binary string - data_out = b'HELLO WORLD!' - - buffer = bytearray(payload_per_frame) - # set buffersize to length of data which will be send - buffer[:len(data_out)] = data_out + resampler = codec2.resampler() - crc = ctypes.c_ushort(codec2.api.freedv_gen_crc16(bytes(buffer), payload_per_frame)) # generate CRC16 - # 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) - - for i in range(1,N_BURSTS+1): + # Data binary string + data_out = b"HELLO WORLD!" - # write preamble to txbuffer - codec2.api.freedv_rawdatapreambletx(freedv, mod_out_preamble) - txbuffer = bytes(mod_out_preamble) + modes = [ + codec2.api.FREEDV_MODE_DATAC0, + codec2.api.FREEDV_MODE_DATAC1, + codec2.api.FREEDV_MODE_DATAC3, + ] - # create modulaton for N = FRAMESPERBURST and append it to txbuffer - for n in range(1,N_FRAMES_PER_BURST+1): + if AUDIO_OUTPUT_DEVICE != -1: + p_audio = pyaudio.PyAudio() + # Auto search for loopback devices + if AUDIO_OUTPUT_DEVICE == -2: + loopback_list = [ + dev + for dev in range(p_audio.get_device_count()) + if "Loopback: PCM" in p_audio.get_device_info_by_index(dev)["name"] + ] - 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 + if len(loopback_list) >= 2: + AUDIO_OUTPUT_DEVICE = loopback_list[1] # 0 = RX 1 = TX + print(f"loopback_list tx: {loopback_list}", file=sys.stderr) + else: + sys.exit() - txbuffer += bytes(mod_out) - print(f"TX BURST: {i}/{N_BURSTS} FRAME: {n}/{N_FRAMES_PER_BURST}", file=sys.stderr) + # pyaudio init + stream_tx = p_audio.open( + format=pyaudio.paInt16, + channels=1, + rate=AUDIO_SAMPLE_RATE_TX, + frames_per_buffer=AUDIO_FRAMES_PER_BUFFER, # n_nom_modem_samples + output=True, + output_device_index=AUDIO_OUTPUT_DEVICE, + ) - # append postamble to txbuffer - codec2.api.freedv_rawdatapostambletx(freedv, mod_out_postamble) - txbuffer += bytes(mod_out_postamble) + for mode in modes: + freedv = ctypes.cast(codec2.api.freedv_open(mode), ctypes.c_void_p) - # append a delay between bursts as audio silence - samples_delay = int(MODEM_SAMPLE_RATE*DELAY_BETWEEN_BURSTS) - mod_out_silence = create_string_buffer(samples_delay*2) - txbuffer += bytes(mod_out_silence) + n_tx_modem_samples = codec2.api.freedv_get_n_tx_modem_samples(freedv) + mod_out = ctypes.create_string_buffer(2 * n_tx_modem_samples) - # resample up to 48k (resampler works on np.int16) - x = np.frombuffer(txbuffer, dtype=np.int16) - txbuffer_48k = resampler.resample8_to_48(x) + 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) - # check if we want to use an audio device or stdout - if AUDIO_OUTPUT_DEVICE != -1: - stream_tx.write(txbuffer_48k.tobytes()) - else: - # this test needs a lot of time, so we are having a look at times... - starttime = time.time() - - # print data to terminal for piping the output to other programs - sys.stdout.buffer.write(txbuffer_48k) - sys.stdout.flush() + 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 + ) - # and at least print the needed time to see which time we needed - timeneeded = time.time()-starttime - #print(f"time: {timeneeded} buffer: {len(txbuffer)}", file=sys.stderr) - - -# and at last check if we had an opened pyaudio instance and close it -if AUDIO_OUTPUT_DEVICE != -1: - time.sleep(stream_tx.get_output_latency()) - stream_tx.stop_stream() - stream_tx.close() - p.terminate() - + bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(freedv) / 8) + payload_per_frame = bytes_per_frame - 2 + buffer = bytearray(payload_per_frame) + # Set buffersize to length of data which will be send + buffer[: len(data_out)] = data_out + + # Generate CRC16 + crc = ctypes.c_ushort( + codec2.api.freedv_gen_crc16(bytes(buffer), payload_per_frame) + ) + # 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) + + for brst in range(1, N_BURSTS + 1): + # Write preamble to txbuffer + codec2.api.freedv_rawdatapreambletx(freedv, mod_out_preamble) + txbuffer = bytes(mod_out_preamble) + + # Create modulaton for N = FRAMESPERBURST and append it to txbuffer + for frm in range(1, N_FRAMES_PER_BURST + 1): + data = (ctypes.c_ubyte * bytes_per_frame).from_buffer_copy(buffer) + # Modulate DATA and save it into mod_out pointer + codec2.api.freedv_rawdatatx(freedv, mod_out, data) + + txbuffer += bytes(mod_out) + print( + f"TX BURST: {brst}/{N_BURSTS} FRAME: {frm}/{N_FRAMES_PER_BURST}", + file=sys.stderr, + ) + + # Append postamble to txbuffer + codec2.api.freedv_rawdatapostambletx(freedv, mod_out_postamble) + txbuffer += bytes(mod_out_postamble) + + # Append a delay between bursts as audio silence + samples_delay = int(MODEM_SAMPLE_RATE * DELAY_BETWEEN_BURSTS) + mod_out_silence = ctypes.create_string_buffer(samples_delay * 2) + txbuffer += bytes(mod_out_silence) + + # Resample up to 48k (resampler works on np.int16) + audio_buffer = np.frombuffer(txbuffer, dtype=np.int16) + txbuffer_48k = resampler.resample8_to_48(audio_buffer) + + # Check if we want to use an audio device or stdout + if AUDIO_OUTPUT_DEVICE != -1: + stream_tx.write(txbuffer_48k.tobytes()) + else: + # This test needs a lot of time, so we are having a look at times... + starttime = time.time() + + # Print data to terminal for piping the output to other programs + sys.stdout.buffer.write(txbuffer_48k) + sys.stdout.flush() + + # and at least print the needed time to see which time we needed + timeneeded = time.time() - starttime + # print(f"time: {timeneeded} buffer: {len(txbuffer)}", file=sys.stderr) + + # and at last check if we had an opened pyaudio instance and close it + if AUDIO_OUTPUT_DEVICE != -1: + time.sleep(stream_tx.get_output_latency()) + stream_tx.stop_stream() + stream_tx.close() + 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() diff --git a/test/test_pa.py b/test/test_pa.py index 6d417f9e..f587600b 100644 --- a/test/test_pa.py +++ b/test/test_pa.py @@ -3,37 +3,46 @@ # # Throw away test program to help understand the care and feeding of PyAudio -import pyaudio import numpy as np +import pyaudio CHUNK = 1024 -FS48 = 48000 +FS48 = 48000 FTEST = 800 -AMP = 16000 +AMP = 16000 -# 1. play sine wave out of default sound device -p = pyaudio.PyAudio() -stream = p.open(format=pyaudio.paInt16, - channels=1, - rate=FS48, - frames_per_buffer=CHUNK, - output=True -) +def test_pa(): + # 1. play sine wave out of default sound device -f48 = open("out48.raw", mode='wb') -t = 0; -for f in range(50): - sine_48k = (AMP*np.cos(2*np.pi*np.arange(t,t+CHUNK)*FTEST/FS48)).astype(np.int16) - t += CHUNK - sine_48k.tofile(f48) - stream.write(sine_48k.tobytes()) - sil_48k = np.zeros(CHUNK, dtype=np.int16) -for f in range(50): - sil_48k.tofile(f48) - stream.write(sil_48k) - -stream.stop_stream() -stream.close() -p.terminate() -f48.close() + p_audio = pyaudio.PyAudio() + stream = p_audio.open( + format=pyaudio.paInt16, + channels=1, + rate=FS48, + frames_per_buffer=CHUNK, + output=True, + ) + + with open("out48.raw", mode="wb") as f48: + temp = 0 + for _ in range(50): + sine_48k = ( + AMP * np.cos(2 * np.pi * np.arange(temp, temp + CHUNK) * FTEST / FS48) + ).astype(np.int16) + temp += CHUNK + sine_48k.tofile(f48) + stream.write(sine_48k.tobytes()) + sil_48k = np.zeros(CHUNK, dtype=np.int16) + + for _ in range(50): + sil_48k.tofile(f48) + stream.write(sil_48k) + + stream.stop_stream() + stream.close() + p_audio.terminate() + + +if __name__ == "__main__": + test_pa() diff --git a/test/test_resample_48_8.py b/test/test_resample_48_8.py new file mode 100644 index 00000000..a7c07663 --- /dev/null +++ b/test/test_resample_48_8.py @@ -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") diff --git a/test/test_rx.py b/test/test_rx.py index 6acdf095..36057ab3 100644 --- a/test/test_rx.py +++ b/test/test_rx.py @@ -6,201 +6,242 @@ Created on Wed Dec 23 07:04:24 2020 @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 ctypes +import sys +import time + import numpy as np -sys.path.insert(0,'..') +import sounddevice as sd + +sys.path.insert(0, "..") from tnc import codec2 -#--------------------------------------------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") +def test_rx(): + args = parse_arguments() -args = parser.parse_args() + if args.LIST: -if args.LIST: - - devices = sd.query_devices(device=None, kind=None) - index = 0 - for device in devices: - print(f"{index} {device['name']}") - index += 1 - sd._terminate() - quit() - -N_BURSTS = args.N_BURSTS -N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST -AUDIO_INPUT_DEVICE = args.AUDIO_INPUT_DEVICE -MODE = codec2.FREEDV_MODE[args.FREEDV_MODE].value -DEBUGGING_MODE = args.DEBUGGING_MODE -TIMEOUT = args.TIMEOUT - -# AUDIO PARAMETERS -AUDIO_FRAMES_PER_BUFFER = 2400*2 # <- consider increasing if you get nread_exceptions > 0 -MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000 -AUDIO_SAMPLE_RATE_RX = 48000 - -# make sure our resampler will work -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 -if AUDIO_INPUT_DEVICE != -1: - # auto search for loopback devices - if AUDIO_INPUT_DEVICE == -2: - loopback_list = [] - devices = sd.query_devices(device=None, kind=None) index = 0 - for device in devices: - if 'Loopback: PCM' in device['name']: - print(index) - loopback_list.append(index) + print(f"{index} {device['name']}") index += 1 - - if len(loopback_list) >= 1: - AUDIO_INPUT_DEVICE = loopback_list[0] #0 = RX 1 = TX - print(f"loopback_list tx: {loopback_list}", file=sys.stderr) - else: - print("not enough audio loopback devices ready...") - print("you should wait about 30 seconds...") + sd._terminate() + sys.exit() - sd._terminate() - quit() - print(f"AUDIO INPUT DEVICE: {AUDIO_INPUT_DEVICE}", file=sys.stderr) + N_BURSTS = args.N_BURSTS + N_FRAMES_PER_BURST = args.N_FRAMES_PER_BURST + AUDIO_INPUT_DEVICE = args.AUDIO_INPUT_DEVICE + MODE = codec2.FREEDV_MODE[args.FREEDV_MODE].value + DEBUGGING_MODE = args.DEBUGGING_MODE + MAX_TIME = args.TIMEOUT - # audio stream init - stream_rx = sd.RawStream(channels=1, dtype='int16', device=AUDIO_INPUT_DEVICE, samplerate = AUDIO_SAMPLE_RATE_RX, blocksize=4800) - stream_rx.start() - -# ---------------------------------------------------------------- - -# DATA CHANNEL INITIALISATION + # AUDIO PARAMETERS + # v-- consider increasing if you get nread_exceptions > 0 + AUDIO_FRAMES_PER_BUFFER = 2400 * 2 + MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000 + AUDIO_SAMPLE_RATE_RX = 48000 -# open codec2 instance -freedv = cast(codec2.api.freedv_open(MODE), c_void_p) + # make sure our resampler will work + assert (AUDIO_SAMPLE_RATE_RX / MODEM_SAMPLE_RATE) == codec2.api.FDMDV_OS_48 -# 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 - -n_max_modem_samples = codec2.api.freedv_get_n_max_modem_samples(freedv) -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() + TIMEOUT -receive = True -audio_buffer = codec2.audio_buffer(AUDIO_FRAMES_PER_BUFFER*2) -resampler = codec2.resampler() - -# time meassurement -time_start = 0 -time_end = 0 - -# Copy received 48 kHz to a file. Listen to this file with: -# aplay -r 48000 -f S16_LE rx48.raw -# Corruption of this file is a good way to detect audio card issues -frx = open("rx48.raw", mode='wb') - -# initial number of samples we need -nin = codec2.api.freedv_nin(freedv) -while receive and time.time() < timeout: + # check if we want to use an audio device then do an pyaudio init if AUDIO_INPUT_DEVICE != -1: - try: - #data_in48k = stream_rx.read(AUDIO_FRAMES_PER_BUFFER, exception_on_overflow = True) - data_in48k, overflowed = stream_rx.read(AUDIO_FRAMES_PER_BUFFER) - except OSError as err: - print(err, file=sys.stderr) - #if str(err).find("Input overflowed") != -1: - # nread_exceptions += 1 - #if str(err).find("Stream closed") != -1: - # print("Ending...") - # receive = False - else: - data_in48k = sys.stdin.buffer.read(AUDIO_FRAMES_PER_BUFFER*2) - - # insert samples in buffer - x = np.frombuffer(data_in48k, dtype=np.int16) - #print(x) - #x = data_in48k - x.tofile(frx) - if len(x) != AUDIO_FRAMES_PER_BUFFER: - receive = False - x = resampler.resample48_to_8(x) - audio_buffer.push(x) - - # when we have enough samples call FreeDV Rx - while audio_buffer.nbuffer >= nin: - # start time measurement - time_start = time.time() - # demodulate audio - nbytes = codec2.api.freedv_rawdatarx(freedv, bytes_out, audio_buffer.buffer.ctypes) - time_end = time.time() - - audio_buffer.pop(nin) - - # call me on every loop! - nin = codec2.api.freedv_nin(freedv) - - rx_status = codec2.api.freedv_get_rx_status(freedv) - if rx_status & codec2.api.FREEDV_RX_BIT_ERRORS: - rx_errors = rx_errors + 1 - if DEBUGGING_MODE: - rx_status = codec2.api.rx_sync_flags_to_text[rx_status] - time_needed = time_end - time_start + # auto search for loopback devices + if AUDIO_INPUT_DEVICE == -2: + loopback_list = [] - print("nin: %5d rx_status: %4s naudio_buffer: %4d time: %4s" % \ - (nin,rx_status,audio_buffer.nbuffer, time_needed), file=sys.stderr) + devices = sd.query_devices(device=None, kind=None) - if nbytes: - total_n_bytes = total_n_bytes + nbytes - - if nbytes == bytes_per_frame: - rx_total_frames = rx_total_frames + 1 - rx_frames = rx_frames + 1 + for index, device in enumerate(devices): + if "Loopback: PCM" in device["name"]: + print(index) + loopback_list.append(index) - if rx_frames == N_FRAMES_PER_BURST: - rx_frames = 0 - rx_bursts = rx_bursts + 1 - - if rx_bursts == N_BURSTS: - receive = False - - if time.time() >= timeout: - print("TIMEOUT REACHED") - -if nread_exceptions: - print("nread_exceptions %d - receive audio lost! Consider increasing Pyaudio frames_per_buffer..." % \ - nread_exceptions, file=sys.stderr) -print(f"RECEIVED BURSTS: {rx_bursts} RECEIVED FRAMES: {rx_total_frames} RX_ERRORS: {rx_errors}", file=sys.stderr) -frx.close() - - -# and at last check if we had an opened audio instance and close it -if AUDIO_INPUT_DEVICE != -1: - sd._terminate() + if loopback_list: + # 0 = RX 1 = TX + AUDIO_INPUT_DEVICE = loopback_list[0] + print(f"loopback_list tx: {loopback_list}", file=sys.stderr) + else: + print("not enough audio loopback devices ready...") + print("you should wait about 30 seconds...") + + sd._terminate() + sys.exit() + print(f"AUDIO INPUT DEVICE: {AUDIO_INPUT_DEVICE}", file=sys.stderr) + + # audio stream init + stream_rx = sd.RawStream( + channels=1, + dtype="int16", + device=AUDIO_INPUT_DEVICE, + samplerate=AUDIO_SAMPLE_RATE_RX, + blocksize=4800, + ) + stream_rx.start() + + # ---------------------------------------------------------------- + # DATA CHANNEL INITIALISATION + + # 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 + + n_max_modem_samples = codec2.api.freedv_get_n_max_modem_samples(freedv) + bytes_out = ctypes.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() + + # time meassurement + time_start = 0 + time_end = 0 + + # Copy received 48 kHz to a file. Listen to this file with: + # aplay -r 48000 -f S16_LE rx48.raw + # Corruption of this file is a good way to detect audio card issues + 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: + try: + # data_in48k = stream_rx.read(AUDIO_FRAMES_PER_BUFFER, exception_on_overflow = True) + data_in48k, overflowed = stream_rx.read(AUDIO_FRAMES_PER_BUFFER) + except OSError as err: + print(err, file=sys.stderr) + # if str(err).find("Input overflowed") != -1: + # nread_exceptions += 1 + # if str(err).find("Stream closed") != -1: + # print("Ending...") + # receive = False + else: + data_in48k = sys.stdin.buffer.read(AUDIO_FRAMES_PER_BUFFER * 2) + + # insert samples in buffer + x = np.frombuffer(data_in48k, dtype=np.int16) + # print(x) + # x = data_in48k + x.tofile(frx) + if len(x) != AUDIO_FRAMES_PER_BUFFER: + receive = False + x = resampler.resample48_to_8(x) + audio_buffer.push(x) + + # when we have enough samples call FreeDV Rx + while audio_buffer.nbuffer >= nin: + # start time measurement + time_start = time.time() + # demodulate audio + nbytes = codec2.api.freedv_rawdatarx( + freedv, bytes_out, audio_buffer.buffer.ctypes + ) + time_end = time.time() + + audio_buffer.pop(nin) + + # call me on every loop! + nin = codec2.api.freedv_nin(freedv) + + rx_status = codec2.api.freedv_get_rx_status(freedv) + if rx_status & codec2.api.FREEDV_RX_BIT_ERRORS: + rx_errors = rx_errors + 1 + if DEBUGGING_MODE: + rx_status = codec2.api.rx_sync_flags_to_text[rx_status] + time_needed = time_end - time_start + + print( + 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: + total_n_bytes += nbytes + + if nbytes == bytes_per_frame: + rx_total_frames += 1 + rx_frames += 1 + + if rx_frames == N_FRAMES_PER_BURST: + rx_frames = 0 + rx_bursts += 1 + + if rx_bursts == N_BURSTS: + receive = False + + if time.time() >= timeout: + print("TIMEOUT REACHED") + + if nread_exceptions: + print( + f"nread_exceptions {nread_exceptions:d} - receive audio lost! " + "Consider increasing Pyaudio frames_per_buffer...", + 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 + if AUDIO_INPUT_DEVICE != -1: + 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() diff --git a/test/test_tnc.py b/test/test_tnc.py new file mode 100755 index 00000000..44bc04fa --- /dev/null +++ b/test/test_tnc.py @@ -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) diff --git a/test/test_tnc_IRS.py b/test/test_tnc_IRS.py new file mode 100644 index 00000000..b1aba6e0 --- /dev/null +++ b/test/test_tnc_IRS.py @@ -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) diff --git a/test/test_tnc_ISS.py b/test/test_tnc_ISS.py new file mode 100644 index 00000000..303f3fd9 --- /dev/null +++ b/test/test_tnc_ISS.py @@ -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) diff --git a/test/test_tnc_states.py b/test/test_tnc_states.py new file mode 100644 index 00000000..b9384be0 --- /dev/null +++ b/test/test_tnc_states.py @@ -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) diff --git a/test/test_tx.py b/test/test_tx.py index 1b33ab53..2855a927 100644 --- a/test/test_tx.py +++ b/test/test_tx.py @@ -2,174 +2,217 @@ # -*- coding: utf-8 -*- -import ctypes -from ctypes import * -import pathlib -import sounddevice as sd -import time import argparse +import ctypes import sys -sys.path.insert(0,'..') -from tnc import codec2 + import numpy as np +import sounddevice as sd -# 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") +sys.path.insert(0, "..") +from tnc import codec2 -args = parser.parse_args() +def test_tx(): + args = parse_arguments() -if args.LIST: - - devices = sd.query_devices(device=None, kind=None) - index = 0 - for device in devices: - print(f"{index} {device['name']}") - index += 1 - sd._terminate() - 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 - -MODE = codec2.FREEDV_MODE[args.FREEDV_MODE].value - -# AUDIO PARAMETERS -AUDIO_FRAMES_PER_BUFFER = 2400 -MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000 -AUDIO_SAMPLE_RATE_TX = 48000 -assert (AUDIO_SAMPLE_RATE_TX % MODEM_SAMPLE_RATE) == 0 - -# check if we want to use an audio device then do an pyaudio init -if AUDIO_OUTPUT_DEVICE != -1: - # auto search for loopback devices - if AUDIO_OUTPUT_DEVICE == -2: - loopback_list = [] - + if args.LIST: devices = sd.query_devices(device=None, kind=None) - index = 0 - - for device in devices: - if 'Loopback: PCM' in device['name']: - print(index) - loopback_list.append(index) - index += 1 - - if len(loopback_list) >= 1: - AUDIO_OUTPUT_DEVICE = loopback_list[len(loopback_list)-1] #0 = RX 1 = TX - print(f"loopback_list tx: {loopback_list}", file=sys.stderr) - else: - print("not enough audio loopback devices ready...") - print("you should wait about 30 seconds...") - sd._terminate() - quit() - print(f"AUDIO OUTPUT DEVICE: {AUDIO_OUTPUT_DEVICE}", file=sys.stderr) - # audio stream init - stream_tx = sd.RawStream(channels=1, dtype='int16', device=(0, AUDIO_OUTPUT_DEVICE), samplerate = AUDIO_SAMPLE_RATE_TX, blocksize=4800) -resampler = codec2.resampler() + for index, device in enumerate(devices): + print(f"{index} {device['name']}") + sd._terminate() + sys.exit() -# data binary string -if args.TESTFRAMES: - data_out = bytearray(14) - data_out[:1] = bytes([255]) - data_out[1:2] = bytes([1]) - data_out[2:] = b'HELLO WORLD' - -else: - data_out = b'HELLO WORLD!' + 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 + MODE = codec2.FREEDV_MODE[args.FREEDV_MODE].value -# ---------------------------------------------------------------- + # AUDIO PARAMETERS + AUDIO_FRAMES_PER_BUFFER = 2400 + MODEM_SAMPLE_RATE = codec2.api.FREEDV_FS_8000 + AUDIO_SAMPLE_RATE_TX = 48000 + assert (AUDIO_SAMPLE_RATE_TX % MODEM_SAMPLE_RATE) == 0 - - -# open codec2 instance -freedv = cast(codec2.api.freedv_open(MODE), c_void_p) - -# get number of bytes per frame for mode -bytes_per_frame = int(codec2.api.freedv_get_bits_per_modem_frame(freedv)/8) -payload_bytes_per_frame = bytes_per_frame -2 - -# init buffer for data -n_tx_modem_samples = codec2.api.freedv_get_n_tx_modem_samples(freedv) -mod_out = create_string_buffer(n_tx_modem_samples * 2) - -# init buffer for preample -n_tx_preamble_modem_samples = codec2.api.freedv_get_n_tx_preamble_modem_samples(freedv) -mod_out_preamble = create_string_buffer(n_tx_preamble_modem_samples * 2) - -# init buffer for postamble -n_tx_postamble_modem_samples = codec2.api.freedv_get_n_tx_postamble_modem_samples(freedv) -mod_out_postamble = create_string_buffer(n_tx_postamble_modem_samples * 2) - - -# 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) - txbuffer = bytes(mod_out_preamble) - - # create modulaton for N = FRAMESPERBURST and append it to txbuffer - for n in range(1,N_FRAMES_PER_BURST+1): - - 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 - - txbuffer += bytes(mod_out) - - print(f"TX BURST: {i}/{N_BURSTS} FRAME: {n}/{N_FRAMES_PER_BURST}", file=sys.stderr) - - # append postamble to txbuffer - codec2.api.freedv_rawdatapostambletx(freedv, mod_out_postamble) - txbuffer += bytes(mod_out_postamble) - - # append a delay between bursts as audio silence - samples_delay = int(MODEM_SAMPLE_RATE*DELAY_BETWEEN_BURSTS) - mod_out_silence = create_string_buffer(samples_delay*2) - txbuffer += bytes(mod_out_silence) - #print(f"samples_delay: {samples_delay} DELAY_BETWEEN_BURSTS: {DELAY_BETWEEN_BURSTS}", file=sys.stderr) - - # resample up to 48k (resampler works on np.int16) - x = np.frombuffer(txbuffer, dtype=np.int16) - txbuffer_48k = resampler.resample8_to_48(x) - - # check if we want to use an audio device or stdout + # check if we want to use an audio device then do an pyaudio init if AUDIO_OUTPUT_DEVICE != -1: - stream_tx.start() - stream_tx.write(txbuffer_48k) + # auto search for loopback devices + if AUDIO_OUTPUT_DEVICE == -2: + loopback_list = [] + + devices = sd.query_devices(device=None, kind=None) + + for index, device in enumerate(devices): + if "Loopback: PCM" in device["name"]: + print(index) + loopback_list.append(index) + + if loopback_list: + # 0 = RX 1 = TX + AUDIO_OUTPUT_DEVICE = loopback_list[-1] + print(f"loopback_list tx: {loopback_list}", file=sys.stderr) + else: + print("not enough audio loopback devices ready...") + print("you should wait about 30 seconds...") + sd._terminate() + sys.exit() + print(f"AUDIO OUTPUT DEVICE: {AUDIO_OUTPUT_DEVICE}", file=sys.stderr) + + # audio stream init + stream_tx = sd.RawStream( + channels=1, + dtype="int16", + device=(0, AUDIO_OUTPUT_DEVICE), + samplerate=AUDIO_SAMPLE_RATE_TX, + blocksize=4800, + ) + + resampler = codec2.resampler() + + # data binary string + if args.TESTFRAMES: + data_out = bytearray(14) + data_out[:1] = bytes([255]) + data_out[1:2] = bytes([1]) + data_out[2:] = b"HELLO WORLD" else: - # print data to terminal for piping the output to other programs - sys.stdout.buffer.write(txbuffer_48k) - sys.stdout.flush() + 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) + + # Init buffer for postamble + n_tx_postamble_modem_samples = codec2.api.freedv_get_n_tx_postamble_modem_samples( + freedv + ) + mod_out_postamble = ctypes.create_string_buffer(n_tx_postamble_modem_samples * 2) + + # Create buffer for data + # Use this if CRC16 checksum is required (DATA1-3) + buffer = bytearray(payload_bytes_per_frame) + # set buffersize to length of data which will be send + buffer[: len(data_out)] = data_out + + # Create CRC for data frame - we are using the CRC function shipped with codec2 to avoid + # CRC algorithm incompatibilities + # generate CRC16 + crc = ctypes.c_ushort( + codec2.api.freedv_gen_crc16(bytes(buffer), payload_bytes_per_frame) + ) + 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 brst in range(1, N_BURSTS + 1): + # Write preamble to txbuffer + codec2.api.freedv_rawdatapreambletx(freedv, mod_out_preamble) + txbuffer = bytes(mod_out_preamble) + + # Create modulaton for N = FRAMESPERBURST and append it to txbuffer + for frm in range(1, N_FRAMES_PER_BURST + 1): + data = (ctypes.c_ubyte * bytes_per_frame).from_buffer_copy(buffer) + # Modulate DATA and save it into mod_out pointer + codec2.api.freedv_rawdatatx(freedv, mod_out, data) + + txbuffer += bytes(mod_out) + + print( + f"TX BURST: {brst}/{N_BURSTS} FRAME: {frm}/{N_FRAMES_PER_BURST}", + file=sys.stderr, + ) + + # Append postamble to txbuffer + codec2.api.freedv_rawdatapostambletx(freedv, mod_out_postamble) + txbuffer += bytes(mod_out_postamble) + + # Append a delay between bursts as audio silence + samples_delay = int(MODEM_SAMPLE_RATE * DELAY_BETWEEN_BURSTS) + mod_out_silence = ctypes.create_string_buffer(samples_delay * 2) + txbuffer += bytes(mod_out_silence) + # print(f"samples_delay: {samples_delay} DELAY_BETWEEN_BURSTS: {DELAY_BETWEEN_BURSTS}", file=sys.stderr) + + # Resample up to 48k (resampler works on np.int16) + np_buffer = np.frombuffer(txbuffer, dtype=np.int16) + txbuffer_48k = resampler.resample8_to_48(np_buffer) + + # Check if we want to use an audio device or stdout + if AUDIO_OUTPUT_DEVICE != -1: + stream_tx.start() + stream_tx.write(txbuffer_48k) + else: + # Print data to terminal for piping the output to other programs + sys.stdout.buffer.write(txbuffer_48k) + sys.stdout.flush() + + # and at last check if we had an opened audio instance and close it + if AUDIO_OUTPUT_DEVICE != -1: + sd._terminate() -# and at last check if we had an opened audio instance and close it -if AUDIO_OUTPUT_DEVICE != -1: - 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() diff --git a/tnc/daemon.py b/tnc/daemon.py index efc30f5e..2c6e05ec 100755 --- a/tnc/daemon.py +++ b/tnc/daemon.py @@ -191,7 +191,7 @@ class DAEMON: options.append('--radiocontrol') options.append(data[13]) - if data[13] != 'rigctld': + if data[13] == 'rigctld': options.append('--rigctld_ip') options.append(data[14]) diff --git a/tnc/data_handler.py b/tnc/data_handler.py index 74a9578e..56a109c4 100644 --- a/tnc/data_handler.py +++ b/tnc/data_handler.py @@ -33,10 +33,11 @@ DATA_QUEUE_TRANSMIT = queue.Queue() DATA_QUEUE_RECEIVED = queue.Queue() -class DATA(): +class DATA: """ Terminal Node Controller for FreeDATA """ + 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_received = DATA_QUEUE_RECEIVED @@ -52,13 +53,15 @@ class DATA(): 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.burst_ack_snr = 0 # SNR from received ack frames - self.burst_ack = False # if we received an acknowledge frame for a burst - self.data_frame_ack_received = False # if we received an acknowledge frame for a data frame - self.rpt_request_received = False # if we received an request for repeater frames - self.rpt_request_buffer = [] # requested frames, saved in a list - self.rx_start_of_transmission = 0 # time of transmission start + + 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 = False # if we received an acknowledge frame for a burst + self.data_frame_ack_received = False # if we received an acknowledge frame for a data frame + self.rpt_request_received = False # if we received an request for repeater frames + self.rpt_request_buffer = [] # requested frames, saved in a list + 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_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.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: 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: 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 static.ARQ_SPEED_LEVEL = self.speed_level @@ -98,7 +103,9 @@ class DATA(): 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_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]) # ARQ MANUAL MODE TRANSMISSION - elif 230 <= frametype <= 240 : + elif 230 <= frametype <= 240: structlog.get_logger("structlog").debug("[TNC] ARQ manual mode") self.arq_received_data_channel_opener(bytes_out[:-2]) @@ -365,7 +372,7 @@ class DATA(): # set n frames per burst to modem # this is an idea, so it's not getting lost.... # 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. @@ -443,15 +450,13 @@ class DATA(): self.arq_file_transfer = True - RX_PAYLOAD_PER_MODEM_FRAME = bytes_per_frame - 2 # payload per moden frame - static.TNC_STATE = 'BUSY' static.ARQ_STATE = True static.INFO.append("ARQ;RECEIVING") self.data_channel_last_received = int(time.time()) # get some important data from the frame - RX_N_FRAME_OF_BURST = int.from_bytes(bytes(data_in[:1]), "big") - 10 # get number of burst frame + RX_N_FRAME_OF_BURST = int.from_bytes(bytes(data_in[:1]), "big") - 10 # get number of burst frame RX_N_FRAMES_PER_BURST = int.from_bytes(bytes(data_in[1:2]), "big") # get number of bursts from received frame # The RX burst buffer needs to have a fixed length filled with "None". @@ -465,7 +470,8 @@ class DATA(): 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" # 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 = 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 # 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) @@ -500,16 +506,18 @@ class DATA(): if get_position >= 0: static.RX_FRAME_BUFFER = static.RX_FRAME_BUFFER[:search_position + get_position] 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: static.RX_FRAME_BUFFER += temp_burst_buffer structlog.get_logger("structlog").debug("[TNC] ARQ | RX | appending data to buffer") # lets check if we didnt receive a BOF and EOF yet to avoid sending ack frames if we already received all data if (not self.rx_frame_bof_received and - not self.rx_frame_eof_received and - data_in.find(self.data_frame_eof) < 0): + not self.rx_frame_eof_received and + data_in.find(self.data_frame_eof) < 0): self.frame_received_counter += 1 if self.frame_received_counter >= 2: @@ -532,29 +540,31 @@ class DATA(): # calculate statistics 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, # 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 # 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.calculate_transfer_rate_rx(self.rx_start_of_transmission, len(static.RX_FRAME_BUFFER)) # Should never reach this point 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. - # 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 bof_position = static.RX_FRAME_BUFFER.find(self.data_frame_bof) 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 - 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 static.TOTAL_BYTES = frame_length 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)) 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}") self.rx_frame_bof_received = True self.rx_frame_eof_received = True # 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 data_frame_crc = payload[:4] # 0:4 4bytes # Get the data frame length @@ -603,36 +614,43 @@ class DATA(): # Re-code data_frame in base64, UTF-8 for JSON UI communication. base64_data = base64.b64encode(data_frame).decode("utf-8") 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) structlog.get_logger("structlog").debug("[TNC] arq_data_received:", jsondata=jsondata) sock.SOCKET_QUEUE.put(json_data_out) 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) # update our statistics AFTER the frame ACK self.calculate_transfer_rate_rx(self.rx_start_of_transmission, len(static.RX_FRAME_BUFFER)) 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: 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") self.send_burst_nack_frame(snr) # update session timeout - 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 # And finally we do a cleanup of our buffers and states # do cleanup only when not in testmode if not TESTMODE: 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: @@ -645,7 +663,7 @@ class DATA(): """ self.arq_file_transfer = True - 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 TX_N_SENT_BYTES = 0 # already sent bytes per data frame @@ -665,7 +683,8 @@ class DATA(): frame_total_size = len(data_out).to_bytes(4, byteorder='big') 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) 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 + frame_total_size + compression_factor + data_out + self.data_frame_eof - #initial bufferposition is 0 + # initial bufferposition is 0 bufferposition = bufferposition_end = 0 # iterate through data out buffer @@ -730,10 +749,11 @@ class DATA(): static.ARQ_SPEED_LEVEL = 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_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 = [] @@ -752,8 +772,8 @@ class DATA(): # normal behavior if bufferposition_end <= len(data_out): - frame = data_out[bufferposition:bufferposition_end] - frame = arqheader + frame + frame = data_out[bufferposition:bufferposition_end] + frame = arqheader + frame # this point shouldnt reached that often elif bufferposition > len(data_out): @@ -762,19 +782,20 @@ class DATA(): # the last bytes of a frame else: 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 # append frame to tempbuffer for transmission tempbuffer.append(frame) 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 # this is not that nice, we could improve this somehow 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 while static.TRANSMITTING: @@ -788,14 +809,15 @@ class DATA(): ''' # burstacktimeout = time.time() + BURST_ACK_TIMEOUT_SECONDS + 100 while (static.ARQ_STATE and not - (self.burst_ack or self.burst_nack or - self.rpt_request_received or self.data_frame_ack_received)): + (self.burst_ack or self.burst_nack or + self.rpt_request_received or self.data_frame_ack_received)): time.sleep(0.01) # once we received a burst ack, reset its state and break the RETRIES loop if self.burst_ack: - self.burst_ack = False # reset ack state - self.tx_n_retry_of_burst = 0 # reset retries + + self.burst_ack = False # reset ack state + self.tx_n_retry_of_burst = 0 # reset retries break # break retry loop if self.burst_nack: @@ -816,7 +838,9 @@ class DATA(): self.calculate_transfer_rate_tx(tx_start_of_transmission, bufferposition_end, len(data_out)) # 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 bufferposition = bufferposition_end @@ -824,7 +848,8 @@ class DATA(): # update stats 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) sock.SOCKET_QUEUE.put(json_data_out) @@ -832,19 +857,25 @@ class DATA(): if self.data_frame_ack_received: 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) 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: 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) 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() # and last but not least doing a state cleanup @@ -856,7 +887,7 @@ class DATA(): sys.exit(0) # signalling frames received - def burst_ack_received(self, data_in:bytes): + def burst_ack_received(self, data_in: bytes): """ Args: @@ -872,11 +903,12 @@ class DATA(): # only process data if we are in ARQ and BUSY 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.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.speed_level= int.from_bytes(bytes(data_in[6:7]), "big") + 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[7:8]), "big") + self.speed_level = int.from_bytes(bytes(data_in[8:9]), "big") static.ARQ_SPEED_LEVEL = self.speed_level structlog.get_logger("structlog").debug("[TNC] burst_ack_received:", speed_level=self.speed_level) # print(self.speed_level) @@ -886,7 +918,7 @@ class DATA(): self.n_retries_per_burst = 0 # signalling frames received - def burst_nack_received(self, data_in:bytes): + def burst_nack_received(self, data_in: bytes): """ Args: @@ -902,11 +934,12 @@ class DATA(): # only process data if we are in ARQ and BUSY 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.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.speed_level= int.from_bytes(bytes(data_in[6:7]), "big") + 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[7:8]), "big") + self.speed_level = int.from_bytes(bytes(data_in[8:9]), "big") static.ARQ_SPEED_LEVEL = self.speed_level self.burst_nack_counter += 1 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 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_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.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 - 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: @@ -930,17 +964,19 @@ class DATA(): 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") - 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) 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 if not TESTMODE: self.arq_cleanup() - def burst_rpt_received(self, data_in:bytes): + def burst_rpt_received(self, data_in: bytes): """ Args: @@ -951,10 +987,11 @@ class DATA(): """ # only process data if we are in ARQ and BUSY state 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.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.rpt_request_buffer = [] missing_area = bytes(data_in[3:12]) # 1:9 @@ -978,7 +1015,9 @@ class DATA(): """ # TODO: we need to check this, maybe placing it to class init 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) @@ -1014,8 +1053,11 @@ class DATA(): while not static.ARQ_SESSION: time.sleep(0.01) - 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) + 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) self.enqueue_frame_for_tx(connection_frame) @@ -1035,7 +1077,7 @@ class DATA(): self.close_session() return False - def received_session_opener(self, data_in:bytes): + def received_session_opener(self, data_in: bytes): """ Args: @@ -1052,8 +1094,11 @@ class DATA(): static.DXCALLSIGN_CRC = bytes(data_in[4:7]) 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) - structlog.get_logger("structlog").info("[TNC] SESSION [" + str(self.mycallsign, 'utf-8') + "]>>|<<[" + str(static.DXCALLSIGN, 'utf-8') + "]", state=static.ARQ_SESSION_STATE) + 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] SESSION [" + str(self.mycallsign, 'utf-8') + "]>>|<<[" + str(static.DXCALLSIGN, 'utf-8') + "]", + state=static.ARQ_SESSION_STATE) static.ARQ_SESSION = True static.TNC_STATE = 'BUSY' @@ -1062,8 +1107,11 @@ class DATA(): def close_session(self): """ Close the ARQ session """ static.ARQ_SESSION_STATE = 'disconnecting' - 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] SESSION [" + str(self.mycallsign, 'utf-8') + "]<>[" + str(static.DXCALLSIGN, 'utf-8') + "]", state=static.ARQ_SESSION_STATE) + 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] SESSION [" + str(self.mycallsign, 'utf-8') + "]<>[" + str(static.DXCALLSIGN, 'utf-8') + "]", + state=static.ARQ_SESSION_STATE) static.INFO.append("ARQ;SESSION;CLOSE") self.IS_ARQ_SESSION_MASTER = False static.ARQ_SESSION = False @@ -1072,7 +1120,7 @@ class DATA(): 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 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])) if _valid_crc: static.ARQ_SESSION_STATE = 'disconnected' - 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] SESSION [" + str(self.mycallsign, 'utf-8') + "]<>[" + str(static.DXCALLSIGN, 'utf-8') + "]", state=static.ARQ_SESSION_STATE) + 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] SESSION [" + str(self.mycallsign, 'utf-8') + "]<>[" + str(static.DXCALLSIGN, 'utf-8') + "]", + state=static.ARQ_SESSION_STATE) static.INFO.append("ARQ;SESSION;CLOSE") self.IS_ARQ_SESSION_MASTER = False @@ -1100,14 +1151,14 @@ class DATA(): # static.TNC_STATE = 'BUSY' # static.ARQ_SESSION_STATE = 'connected' - connection_frame = bytearray(14) - connection_frame[:1] = bytes([222]) + connection_frame = bytearray(14) + connection_frame[:1] = bytes([222]) connection_frame[1:4] = static.DXCALLSIGN_CRC connection_frame[4:7] = static.MYCALLSIGN_CRC self.enqueue_frame_for_tx(connection_frame) - def received_session_heartbeat(self, data_in:bytes): + def received_session_heartbeat(self, data_in: bytes): """ Args: @@ -1120,9 +1171,10 @@ class DATA(): _valid_crc, _ = helpers.check_callsign(static.DXCALLSIGN, bytes(data_in[4:7])) if _valid_crc: 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 static.ARQ_SESSION = True static.ARQ_SESSION_STATE = 'connected' @@ -1134,7 +1186,8 @@ class DATA(): # ############################################################################################################ # 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: @@ -1173,9 +1226,9 @@ class DATA(): if static.ARQ_STATE: self.arq_transmit(data_out, mode, n_frames_per_burst) 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: @@ -1209,9 +1262,12 @@ class DATA(): while not static.ARQ_STATE: 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") - 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) @@ -1227,12 +1283,19 @@ class DATA(): if attempt == self.data_channel_max_retries: 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) 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 if not TESTMODE: self.arq_cleanup() @@ -1241,9 +1304,9 @@ class DATA(): # open_session frame and can still hear us. self.close_session() 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: @@ -1278,7 +1341,8 @@ class DATA(): # updated modes we are listening to 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 valid, mycallsign = helpers.check_callsign(self.mycallsign, data_in[1:4]) @@ -1288,7 +1352,9 @@ class DATA(): self.arq_cleanup() 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.TNC_STATE = 'BUSY' @@ -1312,12 +1378,17 @@ class DATA(): 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 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 Args: @@ -1344,9 +1415,13 @@ class DATA(): self.speed_level = len(self.mode_list) - 1 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 static.ARQ_STATE = True @@ -1355,11 +1430,12 @@ class DATA(): static.TNC_STATE = 'IDLE' static.ARQ_STATE = False 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() # ---------- PING - def transmit_ping(self, dxcallsign:bytes): + def transmit_ping(self, dxcallsign: bytes): """ Funktion for controlling pings Args: @@ -1372,7 +1448,8 @@ class DATA(): static.DXCALLSIGN_CRC = helpers.get_crc_24(static.DXCALLSIGN) 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[:1] = bytes([210]) @@ -1386,7 +1463,7 @@ class DATA(): else: 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 @@ -1398,7 +1475,8 @@ class DATA(): """ static.DXCALLSIGN_CRC = bytes(data_in[4:7]) 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") @@ -1410,7 +1488,9 @@ class DATA(): # print("ping not for me...") 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[:1] = bytes([211]) @@ -1424,7 +1504,7 @@ class DATA(): else: 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 Args: @@ -1436,15 +1516,20 @@ class DATA(): static.DXCALLSIGN_CRC = bytes(data_in[4:7]) 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) 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") - 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' def stop_transmission(self): @@ -1478,9 +1563,10 @@ class DATA(): # ----------- BROADCASTS def run_beacon(self): """ - Controlling funktion for running a beacon + Controlling function for running a beacon Args: - self: + + self: arq class Returns: @@ -1494,8 +1580,8 @@ class DATA(): structlog.get_logger("structlog").info("[TNC] Sending beacon!", interval=self.beacon_interval) beacon_frame = bytearray(14) - beacon_frame[:1] = bytes([250]) - beacon_frame[1:7] = helpers.callsign_to_bytes(self.mycallsign) + beacon_frame[:1] = bytes([250]) + beacon_frame[1:7] = helpers.callsign_to_bytes(self.mycallsign) beacon_frame[9:13] = static.MYGRID[:4] structlog.get_logger("structlog").info("[TNC] ENABLE FSK", state=static.ENABLE_FSK) @@ -1512,7 +1598,7 @@ class DATA(): structlog.get_logger("structlog").debug("[TNC] run_beacon: ", exception=e) # print(e) - def received_beacon(self, data_in:bytes): + def received_beacon(self, data_in: bytes): """ Called if we received a beacon Args: @@ -1525,19 +1611,23 @@ class DATA(): dxcallsign = helpers.bytes_to_callsign(bytes(data_in[1:7])) 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) sock.SOCKET_QUEUE.put(json_data_out) static.INFO.append("BEACON;RECEIVING") - structlog.get_logger("structlog").info("[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) + structlog.get_logger("structlog").info( + "[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): """ Transmit a CQ Args: - Nothing + self Returns: Nothing @@ -1558,7 +1648,7 @@ class DATA(): else: 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 Args: @@ -1573,8 +1663,10 @@ class DATA(): # print(dxcallsign) dxgrid = bytes(helpers.decode_grid(data_in[7:11]), "utf-8") static.INFO.append("CQ;RECEIVING") - structlog.get_logger("structlog").info("[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) + structlog.get_logger("structlog").info( + "[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: self.transmit_qrv() @@ -1583,7 +1675,7 @@ class DATA(): """ Called when we send a QRV frame Args: - data_in:bytes: + self Returns: Nothing @@ -1608,7 +1700,7 @@ class DATA(): else: 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 Args: @@ -1621,16 +1713,20 @@ class DATA(): dxcallsign = helpers.bytes_to_callsign(bytes(data_in[1:7])) 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) sock.SOCKET_QUEUE.put(json_data_out) static.INFO.append("QRV;RECEIVING") - structlog.get_logger("structlog").info("[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) + structlog.get_logger("structlog").info( + "[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 - 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 Args: @@ -1645,7 +1741,8 @@ class DATA(): try: if static.TOTAL_BYTES == 0: 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 @@ -1672,13 +1769,14 @@ class DATA(): """ # reset ARQ statistics static.ARQ_BYTES_PER_MINUTE_BURST = 0 - static.ARQ_BYTES_PER_MINUTE = 0 - static.ARQ_BITS_PER_SECOND_BURST = 0 - static.ARQ_BITS_PER_SECOND = 0 - static.ARQ_TRANSMISSION_PERCENT = 0 - static.TOTAL_BYTES = 0 + static.ARQ_BYTES_PER_MINUTE = 0 + static.ARQ_BITS_PER_SECOND_BURST = 0 + static.ARQ_BITS_PER_SECOND = 0 + static.ARQ_TRANSMISSION_PERCENT = 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 Args: @@ -1728,7 +1826,7 @@ class DATA(): self.data_frame_ack_received = False static.RX_BURST_BUFFER = [] static.RX_FRAME_BUFFER = b'' - self.burst_ack_snr= 255 + self.burst_ack_snr = 255 # reset modem receiving state to reduce cpu load modem.RECEIVE_DATAC1 = False @@ -1760,7 +1858,7 @@ class DATA(): static.BEACON_PAUSE = False - def arq_reset_ack(self,state:bool): + def arq_reset_ack(self, state: bool): """ Funktion for resetting acknowledge states Args: @@ -1824,14 +1922,18 @@ class DATA(): DATA BURST """ # 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 - + # 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(): # print((self.data_channel_last_received + self.time_list[self.speed_level])-time.time()) pass 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.burst_nack_counter += 1 if self.burst_nack_counter >= 2: @@ -1856,7 +1958,6 @@ class DATA(): self.stop_transmission() self.arq_cleanup() - def data_channel_keep_alive_watchdog(self): """ watchdog which checks if we are running into a connection timeout @@ -1871,7 +1972,8 @@ class DATA(): # pass else: self.data_channel_last_received = 0 - structlog.get_logger("structlog").info("[TNC] DATA [" + str(self.mycallsign, 'utf-8') + "]<>[" + str(static.DXCALLSIGN, 'utf-8') + "]") + structlog.get_logger("structlog").info( + "[TNC] DATA [" + str(self.mycallsign, 'utf-8') + "]<>[" + str(static.DXCALLSIGN, 'utf-8') + "]") static.INFO.append("ARQ;RECEIVING;FAILED") if not TESTMODE: self.arq_cleanup() @@ -1885,7 +1987,9 @@ class DATA(): if self.arq_session_last_received + self.arq_session_timeout > time.time(): time.sleep(0.01) else: - structlog.get_logger("structlog").info("[TNC] SESSION [" + str(self.mycallsign, 'utf-8') + "]<>[" + str(static.DXCALLSIGN, 'utf-8') + "]") + structlog.get_logger("structlog").info( + "[TNC] SESSION [" + str(self.mycallsign, 'utf-8') + "]<>[" + str(static.DXCALLSIGN, + 'utf-8') + "]") static.INFO.append("ARQ;SESSION;TIMEOUT") self.close_session() @@ -1901,4 +2005,4 @@ class DATA(): time.sleep(2) 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)]]) diff --git a/tnc/modem.py b/tnc/modem.py index 15d58e82..74eba890 100644 --- a/tnc/modem.py +++ b/tnc/modem.py @@ -647,8 +647,8 @@ class RF: snr = round(modem_stats_snr, 1) 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 except Exception as e: structlog.get_logger("structlog").error(f"[MDM] calculate_snr: Exception: {e}") diff --git a/tnc/rigctld.py b/tnc/rigctld.py index 0b9e4f43..244b37ce 100644 --- a/tnc/rigctld.py +++ b/tnc/rigctld.py @@ -51,7 +51,7 @@ class radio(): """ self.hostname = rigctld_ip self.port = int(rigctld_port) - + if self.connect(): logging.debug("Rigctl intialized") return True