├── .dockerignore ├── .github └── workflows │ └── container.yml ├── .gitignore ├── .travis.yml ├── CMakeLists.txt ├── Dockerfile ├── LICENSE ├── README-Docker.md ├── README.md ├── TESTING.md ├── custom_field_list.json ├── doc ├── horusv2_packet_format.drawio ├── modem_fft.jpg ├── sdrplusplus_overview.jpg └── sdrplusplus_udp_out.jpg ├── docker-compose.yml ├── horus_rx.service ├── horus_rx_dual.service ├── horusdemodlib ├── __init__.py ├── checksums.py ├── decoder.py ├── delegates.py ├── demod.py ├── demodstats.py ├── encoder.py ├── habitat.py ├── horusudp.py ├── payloads.py ├── sondehubamateur.py ├── uploader.py └── utils.py ├── mingw-w64-x86_64.cmake ├── payload_id_list.txt ├── pyproject.toml ├── requirements.txt ├── samples ├── horus_binary_ebno_4.5db.wav ├── horus_v2_100bd.raw ├── horusb_iq_s16.raw ├── rtty_7n1.wav ├── rtty_7n2.wav ├── rtty_8n2.wav └── wenet_sample.c8 ├── scripts ├── docker_dual_4fsk.sh ├── docker_dual_rtty_4fsk.sh ├── docker_ka9q_single.sh ├── docker_single.sh └── docker_soapy_single.sh ├── setup.py ├── src ├── CMakeLists.txt ├── H2064_516_sparse.h ├── H2064_516_sparse_test.h ├── H_128_384_23.c ├── H_128_384_23.h ├── H_256_768_22.c ├── H_256_768_22.h ├── _kiss_fft_guts.h ├── codec2_fdmdv.h ├── comp.h ├── comp_prim.h ├── debug_alloc.h ├── drs232.c ├── drs232_ldpc.c ├── fsk.c ├── fsk.h ├── fsk_demod.c ├── fsk_get_test_bits.c ├── fsk_mod.c ├── fsk_put_test_bits.c ├── golay23.c ├── golay23.h ├── golaydectable.h ├── golayenctable.h ├── horus_api.c ├── horus_api.h ├── horus_demod.c ├── horus_gen_test_bits.c ├── horus_l2.c ├── horus_l2.h ├── kiss_fft.c ├── kiss_fft.h ├── kiss_fftr.c ├── kiss_fftr.h ├── ldpc.c ├── modem_probe.c ├── modem_probe.h ├── modem_stats.c ├── modem_stats.h ├── mpdecode_core.c ├── mpdecode_core.h ├── mpdecode_core_test.c ├── mpdecode_core_test.h ├── octave.c ├── octave.h ├── phi0.c ├── phi0.h └── run_fer_test.py ├── start_dual_4fsk.sh ├── start_dual_rtty_4fsk.sh ├── start_eight_4fsk.sh ├── start_gqrx.sh ├── start_rtlsdr.sh ├── start_triple_4fsk.sh ├── user.cfg.example └── user.env.example /.dockerignore: -------------------------------------------------------------------------------- 1 | samples 2 | 3 | -------------------------------------------------------------------------------- /.github/workflows/container.yml: -------------------------------------------------------------------------------- 1 | name: Container Images 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'testing' 7 | tags: 8 | - 'v*' 9 | pull_request: 10 | workflow_dispatch: 11 | 12 | jobs: 13 | main: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout Repository 17 | uses: actions/checkout@v2 18 | 19 | - name: Calculate Container Metadata 20 | id: meta 21 | uses: docker/metadata-action@v4 22 | with: 23 | images: ghcr.io/${{ github.repository }} 24 | 25 | - name: Setup QEMU 26 | uses: docker/setup-qemu-action@v2 27 | 28 | - name: Setup Buildx 29 | uses: docker/setup-buildx-action@v2 30 | 31 | - name: Login to GitHub Container Registry 32 | if: ${{ github.event_name != 'pull_request' }} 33 | uses: docker/login-action@v2 34 | with: 35 | registry: ghcr.io 36 | username: ${{ github.repository_owner }} 37 | password: ${{ secrets.GITHUB_TOKEN }} 38 | 39 | - name: Build Images 40 | if: ${{ github.event_name == 'pull_request' }} 41 | uses: docker/build-push-action@v3 42 | with: 43 | context: . 44 | platforms: linux/amd64, linux/386, linux/arm64, linux/arm/v6, linux/arm/v7 45 | cache-from: type=registry,ref=ghcr.io/${{ github.repository }}:buildcache 46 | tags: ${{ steps.meta.outputs.tags }} 47 | labels: ${{ steps.meta.outputs.labels }} 48 | 49 | - name: Build and Push Images 50 | if: ${{ github.event_name != 'pull_request' }} 51 | uses: docker/build-push-action@v3 52 | with: 53 | context: . 54 | platforms: linux/amd64, linux/386, linux/arm64, linux/arm/v6, linux/arm/v7 55 | cache-from: type=registry,ref=ghcr.io/${{ github.repository }}:buildcache 56 | cache-to: type=registry,ref=ghcr.io/${{ github.repository }}:buildcache,mode=max 57 | push: true 58 | tags: ${{ steps.meta.outputs.tags }} 59 | labels: ${{ steps.meta.outputs.labels }} 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Compiled Object files 5 | *.slo 6 | *.lo 7 | *.o 8 | *.obj 9 | 10 | # Precompiled Headers 11 | *.gch 12 | *.pch 13 | 14 | # Compiled Dynamic libraries 15 | *.so 16 | *.dylib 17 | *.dll 18 | 19 | # Fortran module files 20 | *.mod 21 | *.smod 22 | 23 | # Compiled Static libraries 24 | *.lai 25 | *.la 26 | *.a 27 | *.lib 28 | 29 | # Executables 30 | *.exe 31 | *.out 32 | *.app 33 | 34 | # Custom config file 35 | user.env 36 | user.cfg 37 | 38 | # IDE 39 | .vscode 40 | .idea 41 | 42 | # cmake 43 | build 44 | 45 | 46 | venv/ 47 | __pycache__/* 48 | *.pyc 49 | *.log -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | env: 2 | sudo: required 3 | language: generic 4 | dist: xenial 5 | os: 6 | - linux 7 | - osx 8 | addons: 9 | apt: 10 | packages: libc6-i386 sox python3-numpy valgrind fonts-freefont-otf libspeexdsp-dev 11 | libsamplerate0-dev portaudio19-dev libasound2-dev libao-dev libgsm1-dev libsndfile-dev 12 | mingw-w64 13 | homebrew: 14 | packages: 15 | - sox 16 | update: true 17 | install: 18 | - export HABLIBDIR=${PWD} 19 | - export MAKEFLAGS=-j2 20 | script: 21 | - cd ${HABLIBDIR} && mkdir -p build_linux && cd build_linux 22 | - cmake $HABLIBDIR && make -j4 23 | - ctest --output-on-failure 24 | - cd ${HABLIBDIR} && mkdir -p build_windows/src && cd build_windows 25 | - if [ "$TRAVIS_OS_NAME" != "osx" ]; then cmake $HABLIBDIR -DCMAKE_TOOLCHAIN_FILE=$HABLIBDIR/mingw-w64-x86_64.cmake && make -j4 ; fi 26 | 27 | before_deploy: 28 | - cd ${HABLIBDIR} 29 | deploy: 30 | provider: releases 31 | api_key: 32 | secure: qkvk9+tNLYwfGaxiPtQBd8KsQeCEUu0FAmMdu3QeUBw4kSp+vferp7cE3Txl4uAR2cbNMszAggbyDNSYFGpubX8YkV/eeCFYrEDpPZQv/VjySTWPjWOYQRXdeQKD0Kjb4+foGfXmvNoAaim3pE54plOin/8e2aI2pu/xcmBpgRvuIb4cGPGOw1hAV1/UC6GKlYenVYYvxcu4leC/YCPPwKCUlrPsajuK6cvBF715aZ70Ugow97IEgo3MVqCBQf0OceeuqcSPRNKXe8yO4zUVyq4y0nZvPDIKwvmw8hCs86fcrtvrhKvDOQYP+3iQigPLoAP80Nq4qD+eJrvVdmpdTZ2MOJfORaBblk6slLeZ/46IL64kBIqZ9MJrKeabLd7WJ0A66UgK9/VswyiephGe5iAZmmOpArheRqAdz7eh0yAOb7l6G5MYhv6nqVvVVJgDdNYnChbsQFkjVgJsyFEYKYAlXbWJH1v5X5e4lHE9ReANSb1VIWOpW9YAliAMAJhKQqJ3LzK14K+xLkecpRwyFgYt652Vewmw79C+iNV8UZYj64NQmYAgw/arhgVHhzitwFm0Gc6GGFZuqtnCjPNWgqsm3i2u/x9tgej6AjjRiI/4vHGROiISJpXLh7zaWRbjKBGL286dECYsFpMi+o7goqDuNYG75UxkSGyHFZKXJ1U= 33 | file: 34 | - build_linux/src/libhorus.so # we might want to cross compile an arm version as well :) 35 | - build_linux/src/libhorus.dylib 36 | - build_windows/src/libhorus.dll 37 | on: 38 | repo: projecthorus/horusdemodlib 39 | tags: true 40 | skip_cleanup: 'true' 41 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.0) 2 | project(horus C) 3 | 4 | include(GNUInstallDirs) 5 | mark_as_advanced(CLEAR 6 | CMAKE_INSTALL_BINDIR 7 | CMAKE_INSTALL_INCLUDEDIR 8 | CMAKE_INSTALL_LIBDIR 9 | ) 10 | 11 | # 12 | # Prevent in-source builds 13 | # If an in-source build is attempted, you will still need to clean up a few 14 | # files manually. 15 | # 16 | set(CMAKE_DISABLE_SOURCE_CHANGES ON) 17 | set(CMAKE_DISABLE_IN_SOURCE_BUILD ON) 18 | if("${CMAKE_SOURCE_DIR}" STREQUAL "${CMAKE_BINARY_DIR}") 19 | message(FATAL_ERROR "In-source builds in ${CMAKE_BINARY_DIR} are not " 20 | "allowed, please remove ./CMakeCache.txt and ./CMakeFiles/, create a " 21 | "separate build directory and run cmake from there.") 22 | endif("${CMAKE_SOURCE_DIR}" STREQUAL "${CMAKE_BINARY_DIR}") 23 | 24 | 25 | # Set project version information. This should probably be done via external 26 | # file at some point. 27 | # 28 | set(HORUS_VERSION_MAJOR 0) 29 | set(HORUS_VERSION_MINOR 2) 30 | # Set to patch level if needed, otherwise leave FALSE. 31 | # Must be positive (non-zero) if set, since 0 == FALSE in CMake. 32 | set(HORUS_VERSION_PATCH FALSE) 33 | set(HORUS_VERSION "${HORUS_VERSION_MAJOR}.${HORUS_VERSION_MINOR}") 34 | # Patch level version bumps should not change API/ABI. 35 | set(SOVERSION "${HORUS_VERSION_MAJOR}.${HORUS_VERSION_MINOR}") 36 | if(HORUS_VERSION_PATCH) 37 | set(HORUS_VERSION "${HORUS_VERSION}.${HORUS_VERSION_PATCH}") 38 | endif() 39 | message(STATUS "Horuslib version: ${HORUS_VERSION}") 40 | 41 | # Set default flags 42 | set(CMAKE_C_FLAGS "-Wall -Wextra -Wno-unused-function -Wno-strict-overflow -O3 -g -I. -MD ${CMAKE_C_FLAGS} -DENABLE_ASSERTIONS") 43 | 44 | # Arch specific stuff here 45 | message(STATUS "Host system arch is: ${CMAKE_SYSTEM_PROCESSOR}") 46 | 47 | 48 | add_subdirectory(src) 49 | 50 | # Ctests ---------------------------------------------------------------------- 51 | 52 | include(CTest) 53 | enable_testing() 54 | 55 | add_test(NAME test_horus_binary 56 | COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; 57 | sox ${CMAKE_CURRENT_SOURCE_DIR}/samples/horus_binary_ebno_4.5db.wav -r 48000 -t raw - | 58 | ./horus_demod -m binary - -" 59 | ) 60 | set_tests_properties(test_horus_binary PROPERTIES PASS_REGULAR_EXPRESSION "1C9A9545") 61 | 62 | add_test(NAME test_horus_binary_v2 63 | COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; 64 | sox -t raw -r 8000 -e signed-integer -b 16 -c 1 ${CMAKE_CURRENT_SOURCE_DIR}/samples/horus_v2_100bd.raw -r 48000 -t raw - | 65 | ./horus_demod -m binary - -" 66 | ) 67 | set_tests_properties(test_horus_binary_v2 PROPERTIES PASS_REGULAR_EXPRESSION "0102030405060708091DBB") 68 | 69 | add_test(NAME test_horus_rtty_7n1 70 | COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; 71 | sox ${CMAKE_CURRENT_SOURCE_DIR}/samples/rtty_7n1.wav -r 48000 -t raw - | 72 | ./horus_demod -c -m rtty7n1 - - | grep OK | wc -l" 73 | ) 74 | set_tests_properties(test_horus_rtty_7n1 PROPERTIES PASS_REGULAR_EXPRESSION "3") 75 | 76 | add_test(NAME test_horus_rtty_7n2 77 | COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; 78 | sox ${CMAKE_CURRENT_SOURCE_DIR}/samples/rtty_7n2.wav -r 48000 -t raw - | 79 | ./horus_demod -c -m rtty - - | grep OK | wc -l" 80 | ) 81 | set_tests_properties(test_horus_rtty_7n2 PROPERTIES PASS_REGULAR_EXPRESSION "6") 82 | 83 | add_test(NAME test_horus_rtty_8n2 84 | COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; 85 | sox ${CMAKE_CURRENT_SOURCE_DIR}/samples/rtty_8n2.wav -r 48000 -t raw - | 86 | ./horus_demod -c --rate=300 -m rtty8n2 - - | grep OK | wc -l" 87 | ) 88 | set_tests_properties(test_horus_rtty_8n2 PROPERTIES PASS_REGULAR_EXPRESSION "4") 89 | 90 | add_test(NAME test_horus_binary_iq 91 | COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; 92 | cat ${CMAKE_CURRENT_SOURCE_DIR}/samples/horusb_iq_s16.raw | 93 | ./horus_demod -q -m binary --fsk_lower=1000 --fsk_upper=20000 - -" 94 | ) 95 | set_tests_properties(test_horus_binary_iq PROPERTIES 96 | PASS_REGULAR_EXPRESSION "000900071E2A000000000000000000000000259A6B14") 97 | 98 | # Wenet - Using Mask estimator (Assuming ~120 kHz tone spacing) 99 | add_test(NAME test_wenet_mask 100 | COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; 101 | cat ${CMAKE_CURRENT_SOURCE_DIR}/samples/wenet_sample.c8 | 102 | ./fsk_demod --cu8 -s --mask=120000 2 921416 115177 - - | 103 | ./drs232_ldpc - - 2>&1 | strings" 104 | ) 105 | set_tests_properties(test_wenet_mask PROPERTIES 106 | PASS_REGULAR_EXPRESSION "packet_errors: 0 PER: 0.000") 107 | 108 | # Using regular frequency estimator, tell freq est to avoid first 100 kHz due to a DC line 109 | add_test(NAME test_wenet_nomask 110 | COMMAND sh -c "cd ${CMAKE_CURRENT_BINARY_DIR}/src; 111 | cat ${CMAKE_CURRENT_SOURCE_DIR}/samples/wenet_sample.c8 | 112 | ./fsk_demod --cu8 -s --fsk_lower 100000 2 921416 115177 - - | 113 | ./drs232_ldpc - - 2>&1 | strings" 114 | ) 115 | set_tests_properties(test_wenet_nomask PROPERTIES 116 | PASS_REGULAR_EXPRESSION "packet_errors: 0 PER: 0.000") 117 | 118 | 119 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:bookworm-slim AS builder 2 | LABEL org.opencontainers.image.authors="sa2kng " 3 | 4 | # ka9q-radio commit: 5 | ARG KA9Q_REF=cc22b5f5e3c26c37df441ebff29eea7d59031afd 6 | 7 | RUN apt-get -y update && apt-get -y upgrade && apt-get -y install --no-install-recommends \ 8 | cmake build-essential ca-certificates git libusb-1.0-0-dev \ 9 | libatlas-base-dev libsoapysdr-dev soapysdr-module-all \ 10 | libairspy-dev libairspyhf-dev libavahi-client-dev libbsd-dev \ 11 | libfftw3-dev libhackrf-dev libiniparser-dev libncurses5-dev \ 12 | libopus-dev librtlsdr-dev libusb-1.0-0-dev libusb-dev \ 13 | portaudio19-dev libasound2-dev libogg-dev uuid-dev rsync unzip && \ 14 | rm -rf /var/lib/apt/lists/* 15 | 16 | # install everything in /target and it will go in to / on destination image. symlink make it easier for builds to find files installed by this. 17 | RUN mkdir -p /target/usr && rm -rf /usr/local && ln -sf /target/usr /usr/local && mkdir /target/etc 18 | 19 | COPY . /horusdemodlib 20 | 21 | RUN cd /horusdemodlib &&\ 22 | cmake -B build -DCMAKE_INSTALL_PREFIX=/target/usr -DCMAKE_BUILD_TYPE=Release &&\ 23 | cmake --build build --target install 24 | 25 | RUN git clone --depth 1 https://github.com/rxseger/rx_tools.git &&\ 26 | cd rx_tools &&\ 27 | cmake -B build -DCMAKE_INSTALL_PREFIX=/target/usr -DCMAKE_BUILD_TYPE=Release &&\ 28 | cmake --build build --target install 29 | 30 | # Compile and install pcmcat and tune from KA9Q-Radio 31 | ADD https://github.com/ka9q/ka9q-radio/archive/$KA9Q_REF.zip /tmp/ka9q-radio.zip 32 | RUN unzip /tmp/ka9q-radio.zip -d /tmp && \ 33 | cd /tmp/ka9q-radio-$KA9Q_REF && \ 34 | make \ 35 | -f Makefile.linux \ 36 | ARCHOPTS= \ 37 | pcmrecord tune && \ 38 | mkdir -p /target/usr/bin/ && \ 39 | cp pcmrecord /target/usr/bin/ && \ 40 | cp tune /target/usr/bin/ && \ 41 | rm -rf /root/ka9q-radio 42 | 43 | COPY scripts/* /target/usr/bin/ 44 | 45 | # to support arm wheels 46 | # RUN echo '[global]\nextra-index-url=https://www.piwheels.org/simple' > /target/etc/pip.conf 47 | 48 | FROM debian:bookworm-slim AS prod 49 | RUN apt-get -y update && apt-get -y upgrade && apt-get -y install --no-install-recommends \ 50 | libusb-1.0-0 \ 51 | python3-venv \ 52 | python3-crcmod \ 53 | python3-dateutil \ 54 | python3-numpy \ 55 | python3-requests \ 56 | python3-pip \ 57 | sox \ 58 | bc \ 59 | procps \ 60 | rtl-sdr \ 61 | libatlas3-base \ 62 | avahi-utils \ 63 | libnss-mdns \ 64 | libbsd0 \ 65 | soapysdr-module-all &&\ 66 | rm -rf /var/lib/apt/lists/* 67 | 68 | RUN pip install --break-system-packages --no-cache-dir --prefer-binary horusdemodlib 69 | 70 | RUN sed -i -e 's/files dns/files mdns4_minimal [NOTFOUND=return] dns/g' /etc/nsswitch.conf 71 | 72 | COPY --from=builder /target / 73 | CMD ["bash"] 74 | -------------------------------------------------------------------------------- /README-Docker.md: -------------------------------------------------------------------------------- 1 | # Guide for running on docker 2 | 3 | ## Host system 4 | This guide will assume an updated installation of Debian bullseye (11), it should work on a lot of different linux OS and architectures.
5 | You will need to install the libraries and supporting sw/fw for your sdr device, including udev rules and blacklists.
6 | Additional software such as soapysdr is not needed on the host, but can certainly be installed.
7 | ```shell 8 | sudo apt install rtl-sdr 9 | echo "blacklist dvb_usb_rtl28xxu" | sudo tee /etc/modprobe.d/blacklist-rtlsdr.conf 10 | sudo modprobe -r dvb_usb_rtl28xxu 11 | ``` 12 | 13 | See the [docker installation](#install-dockerio) at the bottom of this page. 14 | 15 | ## Building the image 16 | If the docker image is not available, or you want to build from your own branch etc. 17 | ```shell 18 | git clone https://github.com/projecthorus/horusdemodlib.git 19 | cd horusdemodlib 20 | docker-compose build 21 | ``` 22 | 23 | ## Configuration 24 | 25 | Start with creating a directory with a name representing the station, this will be shown in several places in the resulting stack. 26 | ```shell 27 | mkdir -p projecthorus 28 | cd projecthorus 29 | wget https://raw.githubusercontent.com/projecthorus/horusdemodlib/master/docker-compose.yml 30 | wget -O user.cfg https://raw.githubusercontent.com/projecthorus/horusdemodlib/master/user.cfg.example 31 | wget -O user.env https://raw.githubusercontent.com/projecthorus/horusdemodlib/master/user.env.example 32 | ``` 33 | 34 | The file `user.env` contains all the station variables (earlier in each script), the DEMODSCRIPT is used by compose.
35 | The `user.cfg` sets the details sent to Sondehub.
36 | Use your favourite editor to configure these: 37 | ```shell 38 | nano user.env 39 | nano user.cfg 40 | ``` 41 | Please note that the values in user.env should not be escaped with quotes or ticks. 42 | 43 | ## Bringing the stack up 44 | 45 | The `docker-compose` (on some systems it's `docker compose` without the hyphen) is the program controlling the creation, updating, building and termination of the stack. 46 | The basic commands you will use is `docker-compose up` and `docker-compose down`. 47 | When you edit the compose file or configuration it will try to figure out what needs to be done to bring the stack in sync of what has changed. 48 | 49 | Starting the stack in foreground (terminate with Ctrl-C): 50 | ```shell 51 | docker-compose up 52 | ``` 53 | 54 | Starting the stack in background: 55 | ```shell 56 | docker-compose up -d 57 | ``` 58 | 59 | Stopping the stack: 60 | ```shell 61 | docker-compose down 62 | ``` 63 | 64 | Updating the images and bringing the stack up: 65 | ```shell 66 | docker-compose pull 67 | docker-compose up -d 68 | ``` 69 | 70 | Over time there will be old images accumulating, these can be removed with `docker image prune -af` 71 | 72 | ## Using SoapySDR with rx_tools 73 | 74 | If you want to use other SDR than rtl_sdr, the docker build includes rx_tools.
75 | Select docker_soapy_single.sh and add the extra argument to select the sdr in SDR_EXTRA: 76 | ```shell 77 | # Script name 78 | DEMODSCRIPT="docker_soapy_single.sh" 79 | SDR_EXTRA="-d driver=rtlsdr" 80 | ``` 81 | 82 | ## Monitoring and maintenance 83 | 84 | Inside each container, the logs are output to stdout, which makes them visible from outside the container in the logs. 85 | Starting to monitor the running stack: 86 | ```shell 87 | docker-compose logs -f 88 | ``` 89 | 90 | If you want to run commands inside the containers, this can be done with the following command: 91 | ````shell 92 | docker-compose exec horusdemod bash 93 | ```` 94 | The container needs to be running. Exit with Ctrl-D or typing `exit`. 95 | 96 | # Install Docker.io 97 | (Or you can install Docker.com engine via the [convenience script](https://docs.docker.com/engine/install/debian/#install-using-the-convenience-script)) 98 | 99 | In Debian bullseye there's already a docker package, so installation is easy: 100 | ```shell 101 | sudo apt install docker.io apparmor 102 | sudo apt -t bullseye-backports install docker-compose 103 | sudo adduser $(whoami) docker 104 | ``` 105 | Re-login for the group permission to take effect. 106 | 107 | The reason for using backports is the version of compose in bullseye is 1.25 and lacks cgroup support, the backport is version 1.27 108 |
If your dist doesn't have backports, enable with this, and try the installation of docker-compose again: 109 | ```shell 110 | echo "deb http://deb.debian.org/debian bullseye-backports main contrib non-free" | sudo tee /etc/apt/sources.list.d/backports.list 111 | suod apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 648ACFD622F3D138 0E98404D386FA1D9 112 | sudo apt update 113 | ``` 114 | If you cannot get a good compose version with your dist, please follow [the official guide](https://docs.docker.com/compose/install/linux/#install-the-plugin-manually). 115 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Project Horus's Telemetry Demodulator Library 2 | 3 | ![Horus Binary Modem FFT](https://github.com/projecthorus/horusdemodlib/raw/master/doc/modem_fft.jpg) 4 | Above: Spectrogram of the Horus Binary 4-FSK modem signal. 5 | 6 | ## What is it? 7 | This repository contains: 8 | * libhorus - A C library containing a high performance 2/4-FSK-demodulator (originally developed as part of the [Codec2](https://github.com/drowe67/codec2) project by [David Rowe](http://rowetel.com)), along with Golay and LDPC forward-error correction algorithms. 9 | * horus_demod - A command-line version of the FSK demodulator. 10 | * horusdemodlib - A Python library which wraps libhorus, and provides additional functions to decode telemetry into formats suitable for uploading to the [Habhub tracker](http://tracker.habhub.org) and other services. 11 | 12 | In particular, this library provides a decoder for the 'Horus Binary' telemetry system, which is the primary tracking system used in [Project Horus's](https://www.areg.org.au/archives/category/activities/project-horus) High-Altitude Balloon launches. 13 | 14 | The modem in this library can also decode the standard UKHAS RTTY telemetry used on many other high-altitude balloon flights. 15 | 16 | **For the latest information on how and why to use this library, please visit the [wiki pages.](https://github.com/projecthorus/horusdemodlib/wiki)** 17 | 18 | **If you're looking for a way to decode telemetry from a Horus Binary (or even an old-school RTTY) High-Altitude Balloon payload, read the [guides available here.](https://github.com/projecthorus/horusdemodlib/wiki#how-do-i-receive-it)** 19 | 20 | ### Authors 21 | Written by: 22 | * Python Library - Mark Jessop 23 | * FSK Modem - [David Rowe](http://rowetel.com) 24 | * FSK Modem Python Wrapper - [XSSFox](https://twitter.com/xssfox) 25 | * LDPC Codes - [Bill Cowley](http://lowsnr.org/) 26 | 27 | ## HorusDemodLib C Library 28 | This contains the demodulator portions of horuslib, which are written in C. 29 | 30 | ### Building 31 | The library can be built and installed using: 32 | 33 | ```console 34 | $ git clone https://github.com/projecthorus/horusdemodlib.git 35 | $ cd horusdemodlib && mkdir build && cd build 36 | $ cmake .. 37 | $ make 38 | $ sudo make install 39 | ``` 40 | 41 | Refer to the [install guide](https://github.com/projecthorus/horusdemodlib/wiki/1.2--Raspberry-Pi-'Headless'-RX-Guide) for a more complete guide, including what dependencies are required. 42 | 43 | ### Testing 44 | Unit tests for the various demodulators can be run using: 45 | 46 | ```console 47 | $ cd build 48 | $ ctest 49 | ``` 50 | 51 | ### Updates 52 | In most cases, you can update this library by running: 53 | ``` 54 | $ git pull 55 | ``` 56 | and then following the build steps above from the `cd horusdemodlib` line. 57 | 58 | 59 | ### API Reference 60 | The main demodulator API is [horus_api.h](https://github.com/projecthorus/horusdemodlib/blob/master/src/horus_api.h). An example of it in use in a C program is available in [horus_demod.c](https://github.com/projecthorus/horusdemodlib/blob/master/src/horus_demod.c) 61 | 62 | A Python wrapper is also available (via the horusdemodlib Python library which is also part of this repository). An example of its use is available [here](https://github.com/projecthorus/horusdemodlib/blob/master/horusdemodlib/demod.py#L379). 63 | 64 | 65 | ## HorusDemodLib Python Library 66 | The horusdemodlib Python library contains decoders for the different Project Horus telemetry formats, including: 67 | * Horus Binary v1 (Legacy 22-byte Golay-encoded format) 68 | * Horus Binary v2 (Golay-encoded 32-byte format) 69 | 70 | It also contains a wrapper around the C library (mentioned above), which contains the Horus modem demodulators. 71 | 72 | The easiest way to install horusdemodlib is via pypi: 73 | ``` 74 | $ pip install horusdemodlib 75 | ``` 76 | 77 | If you want to install directly from this repository, you can run: 78 | ``` 79 | $ pip install -r requirements.txt 80 | $ pip install -e . 81 | ``` 82 | (Note - this has some issues relating to setuptools currently... use pip) 83 | 84 | ### Updating 85 | If you have installed horusdemodlib via pypi, then you can run (from within your venv, if you are using one): 86 | ``` 87 | $ pip install -U horusdemodlib 88 | ``` 89 | This will also install any new dependencies. 90 | 91 | 92 | If you have installed 'directly', then you will need to run: 93 | ``` 94 | $ git stash 95 | $ git pull 96 | $ pip install -r requirements.txt 97 | $ pip install -e . 98 | ``` 99 | (Note - this has some issues relating to setuptools currently... use pip) 100 | 101 | 102 | ## Further Reading 103 | 104 | Here are some links to projects and blog posts that use this code: 105 | 106 | 1. [Horus-GUI](https://github.com/projecthorus/horus-gui) - A cross-platform high-altitude balloon telemetry decoder. 107 | 1. [Testing HAB Telemetry, Horus binary waveform](http://www.rowetel.com/?p=5906) 108 | 1. [Wenet](https://github.com/projecthorus/wenet) - high speed SSTV images from balloons at the edge of space. 109 | 1. [Wenet High speed SSTV images](http://www.rowetel.com/?p=5344) 110 | -------------------------------------------------------------------------------- /TESTING.md: -------------------------------------------------------------------------------- 1 | # Decoder / Encoder Testing Notes 2 | 3 | ## Generating Test Frames 4 | `horus_gen_test_bits` can be used to generate either horus v1 (mode 0) or horus v2 (mode 1) frames, in one-bit-per-byte format. 5 | 6 | ``` 7 | $ ./horus_gen_test_bits 0 1000 > horus_v1_test_frames.bin 8 | ``` 9 | 10 | These can be piped into fsk_mod to produce modulated audio: 11 | ``` 12 | $ ./horus_gen_test_bits 0 100 | ./fsk_mod 4 48000 100 1000 270 - - > horus_v1_test_frames_8khz.raw 13 | ``` 14 | 15 | You can play the frames out your speakers using sox: 16 | ``` 17 | $ ./horus_gen_test_bits 0 100 | ./fsk_mod 4 48000 100 1000 270 - - | play -t raw -r 48000 -e signed-integer -b 16 -c 1 - 18 | ``` 19 | 20 | ... or pipe them straight back into horus_demod and decode them: 21 | ``` 22 | $ ./horus_gen_test_bits 0 100 | ./fsk_mod 4 48000 100 1000 270 - - | ./horus_demod -m binary - - 23 | Using Horus Mode 0. 24 | Generating 100 frames. 25 | 0000000000000000000000000000000000000000B8F6 26 | 0001000000000000000000000000000000000000A728 27 | 0002000000000000000000000000000000000000A75A 28 | 0003000000000000000000000000000000000000B884 29 | ... continues. 30 | ``` 31 | 32 | If we get the cohpsk_ch utility from the codec2 repository, then we can also add noise: 33 | ``` 34 | ./horus_gen_test_bits 0 100 | ./fsk_mod 4 8000 100 1000 270 - - | ./cohpsk_ch - - -24 | sox -t raw -r 8000 -e signed-integer -b 16 -c 1 - -r 48000 -t raw - | ./horus_demod -m binary - - 35 | ``` 36 | In this case, we are adding enough noise that the decoder is barely hanging on. Have a listen to the signal: 37 | ``` 38 | $ ./horus_gen_test_bits 0 100 | ./fsk_mod 4 8000 100 1000 270 - - | ./cohpsk_ch - - -24 | play -t raw -r 8000 -e signed-integer -b 16 -c 1 - 39 | ``` 40 | 41 | Note that we have to use a 8kHz sample rate for cohpsk_ch to work, and hence we use sox to get the audio back into the 48 kHz sample rate expected by horus_demod. -------------------------------------------------------------------------------- /doc/modem_fft.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/horusdemodlib/d668a84059a1b0ec3520c8161c08bd6cad65a3b1/doc/modem_fft.jpg -------------------------------------------------------------------------------- /doc/sdrplusplus_overview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/horusdemodlib/d668a84059a1b0ec3520c8161c08bd6cad65a3b1/doc/sdrplusplus_overview.jpg -------------------------------------------------------------------------------- /doc/sdrplusplus_udp_out.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/horusdemodlib/d668a84059a1b0ec3520c8161c08bd6cad65a3b1/doc/sdrplusplus_udp_out.jpg -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | services: 3 | horusdemod: 4 | build: 5 | context: . 6 | target: prod 7 | image: 'ghcr.io/projecthorus/horusdemodlib:latest' 8 | #read_only: true 9 | device_cgroup_rules: 10 | - 'c 189:* rwm' 11 | env_file: 12 | - './user.env' 13 | command: 'bash -c $${DEMODSCRIPT}' 14 | devices: 15 | - '/dev/bus/usb' 16 | volumes: 17 | - type: 'tmpfs' 18 | target: '/tmp' 19 | - type: 'bind' 20 | source: './user.cfg' 21 | target: '/user.cfg' 22 | -------------------------------------------------------------------------------- /horus_rx.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=horus_rx 3 | Wants=network-online.target 4 | After=network.target network-online.target 5 | 6 | [Service] 7 | ExecStart=/home/pi/horusdemodlib/start_rtlsdr.sh 8 | Restart=always 9 | RestartSec=120 10 | WorkingDirectory=/home/pi/horusdemodlib/ 11 | User=pi 12 | SyslogIdentifier=horus_rx 13 | 14 | [Install] 15 | WantedBy=multi-user.target 16 | -------------------------------------------------------------------------------- /horus_rx_dual.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=horus_rx_dual 3 | Wants=network-online.target 4 | After=network.target network-online.target 5 | 6 | [Service] 7 | ExecStart=/home/pi/horusdemodlib/start_dual_4fsk.sh 8 | Restart=always 9 | RestartSec=120 10 | WorkingDirectory=/home/pi/horusdemodlib/ 11 | User=pi 12 | SyslogIdentifier=horus_rx_dual 13 | 14 | [Install] 15 | WantedBy=multi-user.target 16 | -------------------------------------------------------------------------------- /horusdemodlib/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.3.15" 2 | -------------------------------------------------------------------------------- /horusdemodlib/checksums.py: -------------------------------------------------------------------------------- 1 | # 2 | # HorusLib - Checksumming functions 3 | # 4 | 5 | import crcmod 6 | import logging 7 | import struct 8 | 9 | 10 | 11 | def ukhas_crc(data:bytes) -> str: 12 | """ 13 | Calculate the CRC16 CCITT checksum of *data*. 14 | 15 | (CRC16 CCITT: start 0xFFFF, poly 0x1021) 16 | """ 17 | crc16 = crcmod.predefined.mkCrcFun('crc-ccitt-false') 18 | 19 | return hex(crc16(data))[2:].upper().zfill(4) 20 | 21 | 22 | def check_packet_crc(data:bytes, checksum:str='crc16'): 23 | """ 24 | Attempt to validate a packets checksum, which is assumed to be present 25 | in the last few bytes of the packet. 26 | 27 | Support CRC types: CRC16-CCITT 28 | 29 | """ 30 | 31 | if (checksum == 'crc16') or (checksum == 'CRC16') or (checksum == 'crc16-ccitt') or (checksum == 'CRC16-CCITT'): 32 | # Check we have enough data for a sane CRC16. 33 | if len(data) < 3: 34 | raise ValueError(f"Checksum - Not enough data for CRC16!") 35 | 36 | # Decode the last 2 bytes as a uint16 37 | _packet_checksum = struct.unpack(' 6 | # Released under GNU GPL v3 or later 7 | # 8 | # This utility ingests fsk_demod stats output via stdin, and optionally emits time-averaged modem statistics 9 | # data via UDP. 10 | # 11 | import argparse 12 | import json 13 | import logging 14 | import socket 15 | import sys 16 | import time 17 | import numpy as np 18 | 19 | 20 | class FSKDemodStats(object): 21 | """ 22 | Process modem statistics produced by horus/fsk_demod and provide access to 23 | filtered or instantaneous modem data. 24 | 25 | This class expects the JSON output from horus_demod to be arriving in *realtime*. 26 | The test script below will emulate relatime input based on a file. 27 | """ 28 | 29 | FSK_STATS_FIELDS = ['EbNodB', 'ppm', 'f1_est', 'f2_est', 'samp_fft'] 30 | 31 | 32 | def __init__(self, 33 | averaging_time = 5.0, 34 | peak_hold = False, 35 | decoder_id = "" 36 | ): 37 | """ 38 | 39 | Required Fields: 40 | averaging_time (float): Use the last X seconds of data in calculations. 41 | peak_hold (bool): If true, use a peak-hold SNR metric instead of a mean. 42 | decoder_id (str): A unique ID for this object (suggest use of the SDR device ID) 43 | 44 | """ 45 | 46 | self.averaging_time = float(averaging_time) 47 | self.peak_hold = peak_hold 48 | self.decoder_id = str(decoder_id) 49 | 50 | # Input data stores. 51 | self.in_times = np.array([]) 52 | self.in_snr = np.array([]) 53 | self.in_ppm = np.array([]) 54 | 55 | 56 | # Output State variables. 57 | self.snr = -999.0 58 | self.fest = [0.0,0.0, 0.0,0.0] 59 | self.fest_mean = 0.0 60 | self.fft = [] 61 | self.ppm = 0.0 62 | 63 | 64 | 65 | def update(self, data): 66 | """ 67 | Update the statistics parser with a new set of output from fsk_demod. 68 | This can accept either a string (which will be parsed as JSON), or a dict. 69 | 70 | Required Fields: 71 | data (str, dict): One set of statistics from fsk_demod. 72 | """ 73 | 74 | # Check input type 75 | if type(data) == str: 76 | # Attempt to parse string. 77 | try: 78 | # Clean up any nan entries, which aren't valid JSON. 79 | # For now we just replace these with 0, since they only seem to occur 80 | # in the eye diagram data, which we don't use anyway. 81 | if 'nan' in data: 82 | data = data.replace('nan', '0.0') 83 | 84 | _data = json.loads(data) 85 | except Exception as e: 86 | self.log_error("FSK Demod Stats - %s" % str(e)) 87 | return 88 | elif type(data) == dict: 89 | _data = data 90 | 91 | else: 92 | return 93 | 94 | # Check for required fields in incoming dictionary. 95 | for _field in self.FSK_STATS_FIELDS: 96 | if _field not in _data: 97 | self.log_error("Missing Field %s" % _field) 98 | return 99 | 100 | # Now we can process the data. 101 | _time = time.time() 102 | self.fft = _data['samp_fft'] 103 | self.fest = [0.0,0.0,0.0,0.0] 104 | self.fest[0] = _data['f1_est'] 105 | self.fest[1] = _data['f2_est'] 106 | 107 | if 'f3_est' in _data: 108 | self.fest[2] = _data['f3_est'] 109 | 110 | if 'f4_est' in _data: 111 | self.fest[3] = _data['f4_est'] 112 | else: 113 | self.fest = self.fest[:2] 114 | 115 | self.fest_mean = np.mean(self.fest) 116 | 117 | # Time-series data 118 | self.in_times = np.append(self.in_times, _time) 119 | self.in_snr = np.append(self.in_snr, _data['EbNodB']) 120 | self.in_ppm = np.append(self.in_ppm, _data['ppm']) 121 | 122 | 123 | # Calculate SNR / PPM 124 | _time_range = self.in_times>(_time-self.averaging_time) 125 | # Clip arrays to just the values we want 126 | self.in_ppm = self.in_ppm[_time_range] 127 | self.in_snr = self.in_snr[_time_range] 128 | self.in_times = self.in_times[_time_range] 129 | 130 | # Always just take a mean of the PPM values. 131 | self.ppm = np.mean(self.in_ppm) 132 | 133 | if self.peak_hold: 134 | self.snr = np.max(self.in_snr) 135 | else: 136 | self.snr = np.mean(self.in_snr) 137 | 138 | 139 | def log_debug(self, line): 140 | """ Helper function to log a debug message with a descriptive heading. 141 | Args: 142 | line (str): Message to be logged. 143 | """ 144 | logging.debug("FSK Demod Stats #%s - %s" % (str(self.decoder_id), line)) 145 | 146 | 147 | def log_info(self, line): 148 | """ Helper function to log an informational message with a descriptive heading. 149 | Args: 150 | line (str): Message to be logged. 151 | """ 152 | logging.info("FSK Demod Stats #%s - %s" % (str(self.decoder_id), line)) 153 | 154 | 155 | def log_error(self, line): 156 | """ Helper function to log an error message with a descriptive heading. 157 | Args: 158 | line (str): Message to be logged. 159 | """ 160 | logging.error("FSK Demod Stats #%s - %s" % (str(self.decoder_id), line)) 161 | 162 | 163 | 164 | def send_modem_stats(stats, udp_port=55672): 165 | """ Send a JSON-encoded dictionary to the wenet frontend """ 166 | try: 167 | s = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) 168 | s.settimeout(1) 169 | # Set up socket for broadcast, and allow re-use of the address 170 | s.setsockopt(socket.SOL_SOCKET,socket.SO_BROADCAST,1) 171 | s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 172 | try: 173 | s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) 174 | except: 175 | pass 176 | s.bind(('',udp_port)) 177 | try: 178 | s.sendto(json.dumps(stats).encode('ascii'), ('', udp_port)) 179 | except socket.error: 180 | s.sendto(json.dumps(stats).encode('ascii'), ('127.0.0.1', udp_port)) 181 | 182 | except Exception as e: 183 | logging.error("Error updating GUI with modem status: %s" % str(e)) 184 | 185 | 186 | 187 | if __name__ == "__main__": 188 | # Command line arguments. 189 | parser = argparse.ArgumentParser() 190 | parser.add_argument("-r", "--rate", default=1, type=int, help="Update Rate (Hz). Default: 2 Hz") 191 | parser.add_argument("-p", "--port", default=55672, type=int, help="Output UDP port. Default: 55672") 192 | parser.add_argument("-s", "--source", default='MFSK', help="Source name (must be unique if running multiple decoders). Default: MFSK") 193 | args = parser.parse_args() 194 | 195 | _averaging_time = 1.0/args.rate 196 | 197 | stats_parser = FSKDemodStats(averaging_time=_averaging_time, peak_hold=True) 198 | 199 | 200 | _last_update_time = time.time() 201 | 202 | try: 203 | while True: 204 | data = sys.stdin.readline() 205 | 206 | # An empty line indicates that stdin has been closed. 207 | if data == '': 208 | break 209 | 210 | # Otherwise, feed it to the stats parser. 211 | stats_parser.update(data.rstrip()) 212 | 213 | if (time.time() - _last_update_time) > _averaging_time: 214 | # Send latest modem stats to the Wenet frontend. 215 | _stats = { 216 | 'type': 'MODEM_STATS', 217 | 'source': args.source, 218 | 'snr': stats_parser.snr, 219 | 'ppm': stats_parser.ppm, 220 | 'fft': stats_parser.fft, 221 | 'fest': stats_parser.fest 222 | } 223 | 224 | send_modem_stats(_stats, args.port) 225 | 226 | _last_update_time = time.time() 227 | 228 | except KeyboardInterrupt: 229 | pass 230 | 231 | -------------------------------------------------------------------------------- /horusdemodlib/horusudp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Horus Telemetry GUI - Horus UDP 4 | # 5 | # Mark Jessop 6 | # 7 | import datetime 8 | import json 9 | import logging 10 | import socket 11 | 12 | 13 | def send_payload_summary(telemetry, port=55672, comment="HorusDemodLib"): 14 | """ 15 | Send a payload summary message into the network via UDP broadcast. 16 | 17 | Args: 18 | telemetry (dict): Telemetry dictionary to send. 19 | port (int): UDP port to send to. 20 | 21 | """ 22 | 23 | try: 24 | # Do a few checks before sending. 25 | if telemetry["latitude"] == 0.0 and telemetry["longitude"] == 0.0: 26 | logging.error("Horus UDP - Zero Latitude/Longitude, not sending.") 27 | return 28 | 29 | packet = { 30 | "type": "PAYLOAD_SUMMARY", 31 | "callsign": telemetry["callsign"], 32 | "latitude": telemetry["latitude"], 33 | "longitude": telemetry["longitude"], 34 | "altitude": telemetry["altitude"], 35 | "speed": -1, 36 | "heading": -1, 37 | "time": telemetry["time"], 38 | "comment": comment, 39 | "temp": -1, 40 | "sats": -1, 41 | "batt_voltage": -1, 42 | } 43 | 44 | if 'snr' in telemetry: 45 | packet['snr'] = telemetry['snr'] 46 | 47 | if "f_centre" in telemetry: 48 | packet["frequency"] = telemetry["f_centre"] / 1e6 # Hz -> MHz 49 | 50 | if 'satellites' in telemetry: 51 | packet['sats'] = telemetry['satellites'] 52 | 53 | if 'battery_voltage' in telemetry: 54 | packet['batt_voltage'] = telemetry['battery_voltage'] 55 | 56 | if 'speed' in telemetry: 57 | packet['speed'] = telemetry['speed'] 58 | 59 | # Add in any field names from the custom field section 60 | if "custom_field_names" in telemetry: 61 | for _custom_field_name in telemetry["custom_field_names"]: 62 | if _custom_field_name in telemetry: 63 | packet[_custom_field_name] = telemetry[_custom_field_name] 64 | 65 | # Set up our UDP socket 66 | _s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 67 | _s.settimeout(1) 68 | # Set up socket for broadcast, and allow re-use of the address 69 | _s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) 70 | _s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 71 | # Under OSX we also need to set SO_REUSEPORT to 1 72 | try: 73 | _s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) 74 | except: 75 | pass 76 | 77 | try: 78 | _s.sendto(json.dumps(packet).encode("ascii"), ("", port)) 79 | # Catch any socket errors, that may occur when attempting to send to a broadcast address 80 | # when there is no network connected. In this case, re-try and send to localhost instead. 81 | except socket.error as e: 82 | logging.debug( 83 | "Horus UDP - Send to broadcast address failed, sending to localhost instead." 84 | ) 85 | _s.sendto(json.dumps(packet).encode("ascii"), ("127.0.0.1", port)) 86 | 87 | _s.close() 88 | 89 | except Exception as e: 90 | logging.error("Horus UDP - Error sending Payload Summary: %s" % str(e)) 91 | 92 | 93 | def send_ozimux_message(telemetry, port=55683): 94 | """ 95 | Send an OziMux-compatible message via UDP broadcast, of the form: 96 | 97 | TELEMETRY,HH:MM:SS,lat,lon,alt\n 98 | 99 | """ 100 | try: 101 | _sentence = f"TELEMETRY,{telemetry['time']},{telemetry['latitude']:.5f},{telemetry['longitude']:.5f},{telemetry['altitude']}\n" 102 | except Exception as e: 103 | logging.error(f"OziMux Message - Could not convert telemetry - {str(e)}") 104 | return 105 | 106 | try: 107 | _ozisock = socket.socket(socket.AF_INET,socket.SOCK_DGRAM) 108 | 109 | # Set up socket for broadcast, and allow re-use of the address 110 | _ozisock.setsockopt(socket.SOL_SOCKET,socket.SO_BROADCAST,1) 111 | _ozisock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 112 | # SO_REUSEPORT doesn't work on all platforms, so catch the exception if it fails 113 | try: 114 | _ozisock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) 115 | except: 116 | pass 117 | # Send! 118 | try: 119 | _ozisock.sendto(_sentence.encode('ascii'),('',port)) 120 | except socket.error as e: 121 | logging.warning("OziMux Message - Send to broadcast address failed, sending to localhost instead.") 122 | _ozisock.sendto(_sentence.encode('ascii'),('127.0.0.1',port)) 123 | 124 | _ozisock.close() 125 | logging.debug(f"Sent Telemetry to OziMux ({port}): {_sentence.strip()}") 126 | return _sentence 127 | except Exception as e: 128 | logging.error(f"Failed to send OziMux packet: {str(e)}") 129 | 130 | 131 | if __name__ == "__main__": 132 | # Test script for the above functions 133 | from horusdemodlib.decoder import parse_ukhas_string 134 | from horusdemodlib.checksums import ukhas_crc 135 | # Setup Logging 136 | logging.basicConfig( 137 | format="%(asctime)s %(levelname)s: %(message)s", level=logging.INFO 138 | ) 139 | 140 | sentence = "$$TESTING,1,01:02:03,-34.0,138.0,1000" 141 | crc = ukhas_crc(sentence[2:].encode("ascii")) 142 | sentence = sentence + "*" + crc 143 | print("Sentence: " + sentence) 144 | 145 | _decoded = parse_ukhas_string(sentence) 146 | print(_decoded) 147 | 148 | send_payload_summary(_decoded) 149 | 150 | print(send_ozimux_message(_decoded)) 151 | -------------------------------------------------------------------------------- /horusdemodlib/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # HorusDemodLib - Utils 4 | # 5 | # Because every repository needs a utils.py 6 | # 7 | # Copyright (C) 2025 Mark Jessop 8 | # Released under GNU GPL v3 or later 9 | # 10 | import datetime 11 | import logging 12 | from .delegates import fix_datetime 13 | 14 | 15 | def telem_to_sondehub(telemetry, metadata=None, check_time=True): 16 | """ 17 | Take the output from the HorusDemodLib telemetry decoder, and reformat it 18 | into a dictionary suitable for uploading to SondeHub. 19 | 20 | This function has been broken out here to allow it to be used in other projects, like webhorus. 21 | 22 | Additional metadata should be provided, and should include fields: 23 | "software_name": self.software_name, 24 | "software_version": self.software_version, 25 | "uploader_callsign": self.user_callsign, 26 | "uploader_position": self.user_position, 27 | "uploader_radio": self.user_radio, 28 | "uploader_antenna": self.user_antenna, 29 | "time_received": datetime.datetime.now(datetime.timezone.utc).strftime( 30 | "%Y-%m-%dT%H:%M:%S.%fZ" 31 | ), 32 | """ 33 | 34 | 35 | # Output dictionary 36 | # Start with supplied uploader metadata if we have been provided it. 37 | if metadata: 38 | _output = metadata 39 | else: 40 | _output = {} 41 | 42 | # Mandatory Fields 43 | # Datetime 44 | try: 45 | _datetime = fix_datetime(telemetry['time']) 46 | 47 | # Compare system time and payload time, to look for issues where system time is way out. 48 | _timedelta = abs((_datetime - datetime.datetime.now(datetime.timezone.utc)).total_seconds()) 49 | 50 | if (_timedelta > 3*60) and check_time: 51 | # Greater than 3 minutes time difference. Discard packet in this case. 52 | logging.error("SondeHub Data Reformatter - Payload and Receiver times are offset by more than 3 minutes. Either payload does not have GNSS lock, or your system time is not set correctly. Not uploading.") 53 | return None 54 | 55 | if (_timedelta > 60): 56 | logging.warning("SondeHub Data Reformatter - Payload and Receiver times are offset by more than 1 minute. Either payload does not have GNSS lock, or your system time is not set correctly. Still uploading anyway.") 57 | 58 | _output["datetime"] = _datetime.strftime( 59 | "%Y-%m-%dT%H:%M:%S.%fZ" 60 | ) 61 | except Exception as e: 62 | logging.error( 63 | "SondeHub Data Reformatter - Error converting telemetry datetime to string - %s" % str(e) 64 | ) 65 | logging.debug("SondeHub Data Reformatter - Offending datetime_dt: %s" % str(telemetry["time"])) 66 | return None 67 | 68 | # Callsign - Break if this is an unknown payload ID. 69 | if telemetry["callsign"] == "UNKNOWN_PAYLOAD_ID": 70 | logging.error("SondeHub Data Reformatter - Not uploading telemetry from unknown payload ID. Is your payload ID list old?") 71 | return None 72 | 73 | if '4FSKTEST' in telemetry['callsign']: 74 | logging.warning(f"SondeHub Data Reformatter - Payload ID {telemetry['callsign']} is for testing purposes only, and should not be used on an actual flight. Refer here: https://github.com/projecthorus/horusdemodlib/wiki#how-do-i-transmit-it") 75 | 76 | _output['payload_callsign'] = telemetry["callsign"] 77 | 78 | # Frame Number 79 | _output["frame"] = telemetry["sequence_number"] 80 | 81 | # Position 82 | _output["lat"] = telemetry["latitude"] 83 | _output["lon"] = telemetry["longitude"] 84 | _output["alt"] = telemetry["altitude"] 85 | 86 | # # Optional Fields 87 | if "temperature" in telemetry: 88 | if telemetry["temperature"] > -273.15: 89 | _output["temp"] = telemetry["temperature"] 90 | 91 | if "satellites" in telemetry: 92 | _output["sats"] = telemetry["satellites"] 93 | 94 | if "battery_voltage" in telemetry: 95 | if telemetry["battery_voltage"] >= 0.0: 96 | _output["batt"] = telemetry["battery_voltage"] 97 | 98 | if "speed" in telemetry: 99 | _output["speed"] = telemetry["speed"] 100 | 101 | if "vel_h" in telemetry: 102 | _output["vel_h"] = telemetry["vel_h"] 103 | 104 | if "vel_v" in telemetry: 105 | _output["vel_v"] = telemetry["vel_v"] 106 | 107 | # Handle the additional SNR and frequency estimation if we have it 108 | if "snr" in telemetry: 109 | _output["snr"] = telemetry["snr"] 110 | 111 | if "f_centre" in telemetry: 112 | _output["frequency"] = telemetry["f_centre"] / 1e6 # Hz -> MHz 113 | 114 | if "raw" in telemetry: 115 | _output["raw"] = telemetry["raw"] 116 | 117 | if "modulation" in telemetry: 118 | _output["modulation"] = telemetry["modulation"] 119 | 120 | if "modulation_detail" in telemetry: 121 | _output["modulation_detail"] = telemetry["modulation_detail"] 122 | 123 | if "baud_rate" in telemetry: 124 | _output["baud_rate"] = telemetry["baud_rate"] 125 | 126 | # Add in any field names from the custom field section 127 | if "custom_field_names" in telemetry: 128 | for _custom_field_name in telemetry["custom_field_names"]: 129 | if _custom_field_name in telemetry: 130 | _output[_custom_field_name] = telemetry[_custom_field_name] 131 | 132 | logging.debug(f"SondeHub Data Reformatter - Generated Packet: {str(_output)}") 133 | 134 | return _output 135 | 136 | 137 | if __name__ == "__main__": 138 | # Some simple checks of the telem_to_sondehub function. 139 | 140 | test_inputs = [ 141 | {'packet_format': {'name': 'Horus Binary v2 32 Byte Format', 'length': 32, 'struct': '=0.12"] 20 | build-backend = "poetry.masonry.api" 21 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | crcmod 3 | numpy 4 | python-dateutil 5 | audioop-lts; python_version>='3.13' -------------------------------------------------------------------------------- /samples/horus_binary_ebno_4.5db.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/horusdemodlib/d668a84059a1b0ec3520c8161c08bd6cad65a3b1/samples/horus_binary_ebno_4.5db.wav -------------------------------------------------------------------------------- /samples/horus_v2_100bd.raw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/horusdemodlib/d668a84059a1b0ec3520c8161c08bd6cad65a3b1/samples/horus_v2_100bd.raw -------------------------------------------------------------------------------- /samples/horusb_iq_s16.raw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/horusdemodlib/d668a84059a1b0ec3520c8161c08bd6cad65a3b1/samples/horusb_iq_s16.raw -------------------------------------------------------------------------------- /samples/rtty_7n1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/horusdemodlib/d668a84059a1b0ec3520c8161c08bd6cad65a3b1/samples/rtty_7n1.wav -------------------------------------------------------------------------------- /samples/rtty_7n2.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/horusdemodlib/d668a84059a1b0ec3520c8161c08bd6cad65a3b1/samples/rtty_7n2.wav -------------------------------------------------------------------------------- /samples/rtty_8n2.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/horusdemodlib/d668a84059a1b0ec3520c8161c08bd6cad65a3b1/samples/rtty_8n2.wav -------------------------------------------------------------------------------- /samples/wenet_sample.c8: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/projecthorus/horusdemodlib/d668a84059a1b0ec3520c8161c08bd6cad65a3b1/samples/wenet_sample.c8 -------------------------------------------------------------------------------- /scripts/docker_dual_4fsk.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Dual Horus Binary Decoder Script 4 | # Intended for use with Dual Launches, where both launches have 4FSK payloads closely spaced (~10 kHz) 5 | # 6 | # The SDR is tuned 5 kHz below the Lower 4FSK frequency, and the frequency estimators are set across the two frequencies. 7 | # Modem statistics are sent out via a new 'MODEM_STATS' UDP broadcast message every second. 8 | # 9 | 10 | # Calculate the frequency estimator limits 11 | # Note - these are somewhat hard-coded for this dual-RX application. 12 | MFSK1_LOWER=$(echo "$MFSK1_SIGNAL - $RXBANDWIDTH/2" | bc) 13 | MFSK1_UPPER=$(echo "$MFSK1_SIGNAL + $RXBANDWIDTH/2" | bc) 14 | MFSK1_CENTRE=$(echo "$RXFREQ + $MFSK1_SIGNAL" | bc) 15 | 16 | MFSK2_LOWER=$(echo "$MFSK2_SIGNAL - $RXBANDWIDTH/2" | bc) 17 | MFSK2_UPPER=$(echo "$MFSK2_SIGNAL + $RXBANDWIDTH/2" | bc) 18 | MFSK2_CENTRE=$(echo "$RXFREQ + $MFSK2_SIGNAL" | bc) 19 | 20 | echo "Using SDR Centre Frequency: $RXFREQ Hz." 21 | echo "Using MFSK1 estimation range: $MFSK1_LOWER - $MFSK1_UPPER Hz" 22 | echo "Using MFSK2 estimation range: $MFSK2_LOWER - $MFSK2_UPPER Hz" 23 | 24 | BIAS_SETTING="" 25 | 26 | if [ "$BIAS" = "1" ]; then 27 | echo "Enabling Bias Tee." 28 | BIAS_SETTING=" -T" 29 | fi 30 | 31 | GAIN_SETTING="" 32 | if [ "$GAIN" = "0" ]; then 33 | echo "Using AGC." 34 | GAIN_SETTING="" 35 | else 36 | echo "Using Manual Gain" 37 | GAIN_SETTING=" -g $GAIN" 38 | fi 39 | 40 | STATS_SETTING="" 41 | 42 | if [ "$STATS_OUTPUT" = "1" ]; then 43 | echo "Enabling Modem Statistics." 44 | STATS_SETTING=" --stats=100" 45 | fi 46 | 47 | # Start the receive chain. 48 | # Note that we now pass in the SDR centre frequency ($RXFREQ) and 'target' signal frequency ($MFSK1_CENTRE) 49 | # to enable providing additional metadata to Habitat / Sondehub. 50 | rtl_fm -M raw -F9 -d $SDR_DEVICE -s 48000 -p $PPM $GAIN_SETTING$BIAS_SETTING -f $RXFREQ | tee >($DECODER -q --stats=5 -g -m binary --fsk_lower=$MFSK1_LOWER --fsk_upper=$MFSK1_UPPER - - | python3 -m horusdemodlib.uploader --freq_hz $RXFREQ --freq_target_hz $MFSK1_CENTRE ) >($DECODER -q --stats=5 -g -m binary --fsk_lower=$MFSK2_LOWER --fsk_upper=$MFSK2_UPPER - - | python3 -m horusdemodlib.uploader --freq_hz $RXFREQ ) > /dev/null 51 | -------------------------------------------------------------------------------- /scripts/docker_dual_rtty_4fsk.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Dual RTTY / Horus Binary Decoder Script 4 | # Intended for use on Horus flights, with the following payload frequencies: 5 | # RTTY: 434.650 MHz - Callsign 'HORUS' 6 | # MFSK: 434.660 MHz - Callsign 'HORUSBINARY' 7 | # 8 | # The SDR is tuned 5 kHz below the RTTY frequency, and the frequency estimators are set across the two frequencies. 9 | # Modem statistics are sent out via a new 'MODEM_STATS' UDP broadcast message every second. 10 | # 11 | 12 | # Calculate the frequency estimator limits 13 | # Note - these are somewhat hard-coded for this dual-RX application. 14 | RTTY_LOWER=$(echo "$RTTY_SIGNAL - $RXBANDWIDTH/2" | bc) 15 | RTTY_UPPER=$(echo "$RTTY_SIGNAL + $RXBANDWIDTH/2" | bc) 16 | RTTY_CENTRE=$(echo "$RXFREQ + $RTTY_SIGNAL" | bc) 17 | 18 | MFSK_LOWER=$(echo "$MFSK_SIGNAL - $RXBANDWIDTH/2" | bc) 19 | MFSK_UPPER=$(echo "$MFSK_SIGNAL + $RXBANDWIDTH/2" | bc) 20 | MFSK_CENTRE=$(echo "$RXFREQ + $MFSK_SIGNAL" | bc) 21 | 22 | echo "Using SDR Centre Frequency: $RXFREQ Hz." 23 | echo "Using RTTY estimation range: $RTTY_LOWER - $RTTY_UPPER Hz" 24 | echo "Using MFSK estimation range: $MFSK_LOWER - $MFSK_UPPER Hz" 25 | 26 | BIAS_SETTING="" 27 | 28 | if [ "$BIAS" = "1" ]; then 29 | echo "Enabling Bias Tee." 30 | BIAS_SETTING=" -T" 31 | fi 32 | 33 | GAIN_SETTING="" 34 | if [ "$GAIN" = "0" ]; then 35 | echo "Using AGC." 36 | GAIN_SETTING="" 37 | else 38 | echo "Using Manual Gain" 39 | GAIN_SETTING=" -g $GAIN" 40 | fi 41 | 42 | STATS_SETTING="" 43 | 44 | if [ "$STATS_OUTPUT" = "1" ]; then 45 | echo "Enabling Modem Statistics." 46 | STATS_SETTING=" --stats=100" 47 | fi 48 | 49 | # Start the receive chain. 50 | # Note that we now pass in the SDR centre frequency ($RXFREQ) and 'target' signal frequency ($RTTY_CENTRE / $MFSK_CENTRE) 51 | # to enable providing additional metadata to Habitat / Sondehub. 52 | rtl_fm -M raw -F9 -d $SDR_DEVICE -s 48000 -p $PPM $GAIN_SETTING$BIAS_SETTING -f $RXFREQ | tee >($DECODER -q --stats=5 -g -m RTTY --fsk_lower=$RTTY_LOWER --fsk_upper=$RTTY_UPPER - - | python3 -m horusdemodlib.uploader --rtty --freq_hz $RXFREQ --freq_target_hz $RTTY_CENTRE ) >($DECODER -q --stats=5 -g -m binary --fsk_lower=$MFSK_LOWER --fsk_upper=$MFSK_UPPER - - | python3 -m horusdemodlib.uploader --freq_hz $RXFREQ --freq_target_hz $MFSK_CENTRE ) > /dev/null 53 | -------------------------------------------------------------------------------- /scripts/docker_ka9q_single.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Horus Binary KA9Q-Radio Helper Script 4 | # 5 | # Uses ka9q-radio (pcmrecord) to receive a chunk of spectrum, and passes it into horus_demod. 6 | # 7 | 8 | set -e 9 | set -u 10 | set -o pipefail 11 | set -x 12 | 13 | # trap "exit_rx" EXIT SIGINT SIGTERM 14 | 15 | # Calculate the frequency estimator limits 16 | FSK_LOWER=$(echo "$RXBANDWIDTH / -2" | bc) 17 | FSK_UPPER=$(echo "$RXBANDWIDTH / 2" | bc) 18 | 19 | SSRC=$(($RXFREQ / 1000))01 20 | RADIO=$(echo $SDR_DEVICE | sed 's/-pcm//g') 21 | 22 | exit_rx() { 23 | echo "Exiting..." 24 | echo "Closing channel $SSRC at frequency $RXFREQ" 25 | timeout 2 tune --samprate 48000 --mode iq --low $FSK_LOWER --high $FSK_UPPER --frequency 0 --ssrc $SSRC --radio $RADIO 26 | pkill bash 27 | } 28 | 29 | echo "Using SDR Centre Frequency: $RXFREQ Hz" 30 | echo "Using SSRC: $SSRC" 31 | echo "Using PCM stream: $SDR_DEVICE" 32 | echo "Using FSK estimation range: $FSK_LOWER - $FSK_UPPER Hz" 33 | 34 | # Start the receive chain. 35 | # Note that we now pass in the SDR centre frequency ($RXFREQ) and 'target' signal frequency ($RXFREQ) 36 | # to enable providing additional metadata to Habitat / Sondehub. 37 | echo "Configuring receiver on ka9q-radio" 38 | tune --samprate 48000 --mode iq --low $FSK_LOWER --high $FSK_UPPER --frequency $RXFREQ --ssrc $SSRC --radio $RADIO 39 | 40 | echo "Starting receiver chain" 41 | pcmrecord --ssrc $SSRC --catmode --raw $SDR_DEVICE --timeout 1 | \ 42 | $DECODER -q --stats=5 -g -m binary --fsk_lower=$FSK_LOWER --fsk_upper=$FSK_UPPER - - | \ 43 | python3 -m horusdemodlib.uploader --freq_hz $RXFREQ --freq_target_hz $RXFREQ $@ & 44 | 45 | echo "Started everything, waiting for any failed processes" 46 | 47 | wait -n 48 | exit_rx -------------------------------------------------------------------------------- /scripts/docker_single.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Horus Binary RTLSDR Helper Script 4 | # 5 | # Uses rtl_fm to receive a chunk of spectrum, and passes it into horus_demod. 6 | # 7 | 8 | # Calculate the SDR tuning frequency 9 | SDR_RX_FREQ=$(echo "$RXFREQ - $RXBANDWIDTH/2 - 1000" | bc) 10 | 11 | # Calculate the frequency estimator limits 12 | FSK_LOWER=1000 13 | FSK_UPPER=$(echo "$FSK_LOWER + $RXBANDWIDTH" | bc) 14 | 15 | echo "Using SDR Centre Frequency: $SDR_RX_FREQ Hz." 16 | echo "Using FSK estimation range: $FSK_LOWER - $FSK_UPPER Hz" 17 | 18 | BIAS_SETTING="" 19 | 20 | if [ "$BIAS" = "1" ]; then 21 | echo "Enabling Bias Tee." 22 | BIAS_SETTING=" -T" 23 | fi 24 | 25 | GAIN_SETTING="" 26 | if [ "$GAIN" = "0" ]; then 27 | echo "Using AGC." 28 | GAIN_SETTING="" 29 | else 30 | echo "Using Manual Gain" 31 | GAIN_SETTING=" -g $GAIN" 32 | fi 33 | 34 | # Start the receive chain. 35 | # Note that we now pass in the SDR centre frequency ($SDR_RX_FREQ) and 'target' signal frequency ($RXFREQ) 36 | # to enable providing additional metadata to Habitat / Sondehub. 37 | rtl_fm -M raw -F9 -d $SDR_DEVICE -s 48000 -p $PPM $GAIN_SETTING$BIAS_SETTING -f $SDR_RX_FREQ | $DECODER -q --stats=5 -g -m binary --fsk_lower=$FSK_LOWER --fsk_upper=$FSK_UPPER - - | python3 -m horusdemodlib.uploader --freq_hz $SDR_RX_FREQ --freq_target_hz $RXFREQ $@ 38 | -------------------------------------------------------------------------------- /scripts/docker_soapy_single.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Horus Binary RTLSDR Helper Script 4 | # 5 | # Uses rtl_fm to receive a chunk of spectrum, and passes it into horus_demod. 6 | # 7 | 8 | # Calculate the SDR tuning frequency 9 | SDR_RX_FREQ=$(echo "$RXFREQ - $RXBANDWIDTH/2 - 1000" | bc) 10 | 11 | # Calculate the frequency estimator limits 12 | FSK_LOWER=1000 13 | FSK_UPPER=$(echo "$FSK_LOWER + $RXBANDWIDTH" | bc) 14 | 15 | echo "Using SDR Centre Frequency: $SDR_RX_FREQ Hz." 16 | echo "Using FSK estimation range: $FSK_LOWER - $FSK_UPPER Hz" 17 | 18 | BIAS_SETTING="" 19 | 20 | if [ "$BIAS" = "1" ]; then 21 | echo "Enabling Bias Tee." 22 | BIAS_SETTING=" -T" 23 | fi 24 | 25 | GAIN_SETTING="" 26 | if [ "$GAIN" = "0" ]; then 27 | echo "Using AGC." 28 | GAIN_SETTING="" 29 | else 30 | echo "Using Manual Gain" 31 | GAIN_SETTING=" -g $GAIN" 32 | fi 33 | 34 | # Start the receive chain. 35 | # Note that we now pass in the SDR centre frequency ($SDR_RX_FREQ) and 'target' signal frequency ($RXFREQ) 36 | # to enable providing additional metadata to Habitat / Sondehub. 37 | rx_fm $SDR_EXTRA -M raw -F9 -s 48000 -p $PPM $GAIN_SETTING$BIAS_SETTING -f $SDR_RX_FREQ | $DECODER -q --stats=5 -g -m binary --fsk_lower=$FSK_LOWER --fsk_upper=$FSK_UPPER - - | python3 -m horusdemodlib.uploader --freq_hz $SDR_RX_FREQ --freq_target_hz $RXFREQ $@ 38 | 39 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | from setuptools import setup, find_packages 4 | 5 | regexp = re.compile(r".*__version__ = [\'\"](.*?)[\'\"]", re.S) 6 | 7 | init_file = os.path.join(os.path.dirname(__file__), "horusdemodlib", "__init__.py") 8 | with open(init_file, "r") as f: 9 | module_content = f.read() 10 | match = regexp.match(module_content) 11 | if match: 12 | version = match.group(1) 13 | else: 14 | raise RuntimeError(f"Cannot find __version__ in {init_file}") 15 | 16 | 17 | with open("README.md", "r") as f: 18 | readme = f.read() 19 | 20 | 21 | with open("requirements.txt", "r") as f: 22 | requirements = [] 23 | for line in f.read().split("\n"): 24 | line = line.strip() 25 | if line and not line.startswith("#"): 26 | requirements.append(line) 27 | 28 | 29 | if __name__ == "__main__": 30 | setup( 31 | name="horusdemodlib", 32 | description="Project Horus Telemetry Decoding Library", 33 | long_description=readme, 34 | version=version, 35 | install_requires=requirements, 36 | keywords=["horus modem telemetry radio"], 37 | package_dir={"": "."}, 38 | packages=find_packages("."), 39 | classifiers=[ 40 | "Intended Audience :: Developers", 41 | "Programming Language :: Python :: 3.6", 42 | "Programming Language :: Python :: 3.7", 43 | ] 44 | ) 45 | -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | include_directories(${CMAKE_CURRENT_SOURCE_DIR}) 2 | 3 | set(horus_srcs 4 | fsk.c 5 | kiss_fft.c 6 | kiss_fftr.c 7 | mpdecode_core.c 8 | H_256_768_22.c 9 | H_128_384_23.c 10 | golay23.c 11 | phi0.c 12 | horus_api.c 13 | horus_l2.c 14 | ) 15 | 16 | add_library(horus SHARED ${horus_srcs}) 17 | target_link_libraries(horus m) 18 | set_target_properties(horus PROPERTIES 19 | PUBLIC_HEADER horus_api.h 20 | ) 21 | target_include_directories(horus INTERFACE 22 | $ 23 | $ 24 | ) 25 | install(TARGETS horus EXPORT horus-config 26 | LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} 27 | ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} 28 | RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} 29 | PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/horus 30 | ) 31 | 32 | install(EXPORT horus-config 33 | DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/horus 34 | ) 35 | # Export libhab target for import into build trees of other projects. 36 | export(TARGETS horus 37 | FILE ${CMAKE_BINARY_DIR}/horus.cmake 38 | ) 39 | 40 | add_executable(fsk_mod fsk_mod.c) 41 | target_link_libraries(fsk_mod m horus ${CMAKE_REQUIRED_LIBRARIES}) 42 | 43 | add_executable(fsk_demod fsk_demod.c modem_probe.c octave.c) 44 | target_link_libraries(fsk_demod m horus ${CMAKE_REQUIRED_LIBRARIES}) 45 | 46 | add_executable(fsk_get_test_bits fsk_get_test_bits.c) 47 | target_link_libraries(fsk_get_test_bits) 48 | 49 | add_executable(fsk_put_test_bits fsk_put_test_bits.c) 50 | target_link_libraries(fsk_put_test_bits ${CMAKE_REQUIRED_LIBRARIES}) 51 | 52 | add_executable(drs232 drs232.c) 53 | target_link_libraries(drs232 m horus ${CMAKE_REQUIRED_LIBRARIES}) 54 | 55 | add_executable(drs232_ldpc drs232_ldpc.c) 56 | target_link_libraries(drs232_ldpc m horus ${CMAKE_REQUIRED_LIBRARIES}) 57 | 58 | add_definitions(-DINTERLEAVER -DSCRAMBLER -DRUN_TIME_TABLES) 59 | add_executable(horus_gen_test_bits horus_gen_test_bits.c horus_l2.c) 60 | target_link_libraries(horus_gen_test_bits m horus) 61 | 62 | add_definitions(-DHORUS_L2_RX -DINTERLEAVER -DSCRAMBLER -DRUN_TIME_TABLES) 63 | add_executable(horus_demod horus_demod.c horus_api.c horus_l2.c golay23.c fsk.c kiss_fft.c) 64 | target_link_libraries(horus_demod m horus ${CMAKE_REQUIRED_LIBRARIES}) 65 | 66 | install(TARGETS fsk_mod fsk_demod fsk_get_test_bits fsk_put_test_bits drs232 drs232_ldpc horus_gen_test_bits horus_demod DESTINATION bin) 67 | 68 | -------------------------------------------------------------------------------- /src/H_128_384_23.h: -------------------------------------------------------------------------------- 1 | /* 2 | FILE....: H_128_384_23.h 3 | 4 | Static arrays for LDPC codec H_128_384_23, generated by ldpc_gen_c_h_file.m. 5 | */ 6 | 7 | #define H_128_384_23_NUMBERPARITYBITS 256 8 | #define H_128_384_23_MAX_ROW_WEIGHT 3 9 | #define H_128_384_23_DATA_BYTES 16 10 | #define H_128_384_23_PARITY_BYTES 32 11 | #define H_128_384_23_BITS_PER_PACKET ((H_128_384_23_DATA_BYTES + H_128_384_23_PARITY_BYTES) * 8) 12 | #define H_128_384_23_CODELENGTH 384 13 | #define H_128_384_23_NUMBERROWSHCOLS 128 14 | #define H_128_384_23_MAX_COL_WEIGHT 5 15 | #define H_128_384_23_DEC_TYPE 0 16 | #define H_128_384_23_MAX_ITER 100 17 | #define H_128_384_23_COPRIME 383 18 | 19 | extern const uint16_t H_128_384_23_H_rows[]; 20 | extern const uint16_t H_128_384_23_H_cols[]; 21 | extern const float H_128_384_23_input[]; 22 | extern const char H_128_384_23_detected_data[]; 23 | 24 | -------------------------------------------------------------------------------- /src/H_256_768_22.h: -------------------------------------------------------------------------------- 1 | /* 2 | FILE....: H_256_768_22.h 3 | 4 | Static arrays for LDPC codec H_256_768_22, generated by ldpc_gen_c_h_file.m. 5 | */ 6 | 7 | #define H_256_768_22_NUMBERPARITYBITS 512 8 | #define H_256_768_22_MAX_ROW_WEIGHT 2 9 | #define H_256_768_22_DATA_BYTES 32 10 | #define H_256_768_22_PARITY_BYTES 64 11 | #define H_256_768_22_BITS_PER_PACKET ((H_256_768_22_DATA_BYTES + H_256_768_22_PARITY_BYTES) * 8) 12 | #define H_256_768_22_CODELENGTH 768 13 | #define H_256_768_22_NUMBERROWSHCOLS 256 14 | #define H_256_768_22_MAX_COL_WEIGHT 4 15 | #define H_256_768_22_DEC_TYPE 0 16 | #define H_256_768_22_MAX_ITER 100 17 | #define H_256_768_22_COPRIME 761 18 | 19 | extern const uint16_t H_256_768_22_H_rows[]; 20 | extern const uint16_t H_256_768_22_H_cols[]; 21 | extern const float H_256_768_22_input[]; 22 | extern const char H_256_768_22_detected_data[]; 23 | 24 | -------------------------------------------------------------------------------- /src/_kiss_fft_guts.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2003-2010, Mark Borgerding 3 | 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 10 | * Neither the author nor the names of any contributors may be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 13 | */ 14 | 15 | /* kiss_fft.h 16 | defines kiss_fft_scalar as either short or a float type 17 | and defines 18 | typedef struct { kiss_fft_scalar r; kiss_fft_scalar i; }kiss_fft_cpx; */ 19 | #include "kiss_fft.h" 20 | #include 21 | 22 | #define MAXFACTORS 32 23 | /* e.g. an fft of length 128 has 4 factors 24 | as far as kissfft is concerned 25 | 4*4*4*2 26 | */ 27 | 28 | struct kiss_fft_state{ 29 | int nfft; 30 | int inverse; 31 | int factors[2*MAXFACTORS]; 32 | kiss_fft_cpx twiddles[1]; 33 | }; 34 | 35 | /* 36 | Explanation of macros dealing with complex math: 37 | 38 | C_MUL(m,a,b) : m = a*b 39 | C_FIXDIV( c , div ) : if a fixed point impl., c /= div. noop otherwise 40 | C_SUB( res, a,b) : res = a - b 41 | C_SUBFROM( res , a) : res -= a 42 | C_ADDTO( res , a) : res += a 43 | * */ 44 | #ifdef FIXED_POINT 45 | #if (FIXED_POINT==32) 46 | # define FRACBITS 31 47 | # define SAMPPROD int64_t 48 | #define SAMP_MAX 2147483647 49 | #else 50 | # define FRACBITS 15 51 | # define SAMPPROD int32_t 52 | #define SAMP_MAX 32767 53 | #endif 54 | 55 | #define SAMP_MIN -SAMP_MAX 56 | 57 | #if defined(CHECK_OVERFLOW) 58 | # define CHECK_OVERFLOW_OP(a,op,b) \ 59 | if ( (SAMPPROD)(a) op (SAMPPROD)(b) > SAMP_MAX || (SAMPPROD)(a) op (SAMPPROD)(b) < SAMP_MIN ) { \ 60 | fprintf(stderr,"WARNING:overflow @ " __FILE__ "(%d): (%d " #op" %d) = %ld\n",__LINE__,(a),(b),(SAMPPROD)(a) op (SAMPPROD)(b) ); } 61 | #endif 62 | 63 | 64 | # define smul(a,b) ( (SAMPPROD)(a)*(b) ) 65 | # define sround( x ) (kiss_fft_scalar)( ( (x) + (1<<(FRACBITS-1)) ) >> FRACBITS ) 66 | 67 | # define S_MUL(a,b) sround( smul(a,b) ) 68 | 69 | # define C_MUL(m,a,b) \ 70 | do{ (m).r = sround( smul((a).r,(b).r) - smul((a).i,(b).i) ); \ 71 | (m).i = sround( smul((a).r,(b).i) + smul((a).i,(b).r) ); }while(0) 72 | 73 | # define DIVSCALAR(x,k) \ 74 | (x) = sround( smul( x, SAMP_MAX/k ) ) 75 | 76 | # define C_FIXDIV(c,div) \ 77 | do { DIVSCALAR( (c).r , div); \ 78 | DIVSCALAR( (c).i , div); }while (0) 79 | 80 | # define C_MULBYSCALAR( c, s ) \ 81 | do{ (c).r = sround( smul( (c).r , s ) ) ;\ 82 | (c).i = sround( smul( (c).i , s ) ) ; }while(0) 83 | 84 | #else /* not FIXED_POINT*/ 85 | 86 | # define S_MUL(a,b) ( (a)*(b) ) 87 | #define C_MUL(m,a,b) \ 88 | do{ (m).r = (a).r*(b).r - (a).i*(b).i;\ 89 | (m).i = (a).r*(b).i + (a).i*(b).r; }while(0) 90 | # define C_FIXDIV(c,div) /* NOOP */ 91 | # define C_MULBYSCALAR( c, s ) \ 92 | do{ (c).r *= (s);\ 93 | (c).i *= (s); }while(0) 94 | #endif 95 | 96 | #ifndef CHECK_OVERFLOW_OP 97 | # define CHECK_OVERFLOW_OP(a,op,b) /* noop */ 98 | #endif 99 | 100 | #define C_ADD( res, a,b)\ 101 | do { \ 102 | CHECK_OVERFLOW_OP((a).r,+,(b).r)\ 103 | CHECK_OVERFLOW_OP((a).i,+,(b).i)\ 104 | (res).r=(a).r+(b).r; (res).i=(a).i+(b).i; \ 105 | }while(0) 106 | #define C_SUB( res, a,b)\ 107 | do { \ 108 | CHECK_OVERFLOW_OP((a).r,-,(b).r)\ 109 | CHECK_OVERFLOW_OP((a).i,-,(b).i)\ 110 | (res).r=(a).r-(b).r; (res).i=(a).i-(b).i; \ 111 | }while(0) 112 | #define C_ADDTO( res , a)\ 113 | do { \ 114 | CHECK_OVERFLOW_OP((res).r,+,(a).r)\ 115 | CHECK_OVERFLOW_OP((res).i,+,(a).i)\ 116 | (res).r += (a).r; (res).i += (a).i;\ 117 | }while(0) 118 | 119 | #define C_SUBFROM( res , a)\ 120 | do {\ 121 | CHECK_OVERFLOW_OP((res).r,-,(a).r)\ 122 | CHECK_OVERFLOW_OP((res).i,-,(a).i)\ 123 | (res).r -= (a).r; (res).i -= (a).i; \ 124 | }while(0) 125 | 126 | 127 | #ifdef FIXED_POINT 128 | # define KISS_FFT_COS(phase) floorf(.5+SAMP_MAX * cosf (phase)) 129 | # define KISS_FFT_SIN(phase) floorf(.5+SAMP_MAX * sinf (phase)) 130 | # define HALF_OF(x) ((x)>>1) 131 | #elif defined(USE_SIMD) 132 | # define KISS_FFT_COS(phase) _mm_set1_ps( cosf(phase) ) 133 | # define KISS_FFT_SIN(phase) _mm_set1_ps( sinf(phase) ) 134 | # define HALF_OF(x) ((x)*_mm_set1_ps(.5)) 135 | #else 136 | # define KISS_FFT_COS(phase) (kiss_fft_scalar) cosf(phase) 137 | # define KISS_FFT_SIN(phase) (kiss_fft_scalar) sinf(phase) 138 | # define HALF_OF(x) ((x)*.5) 139 | #endif 140 | 141 | #define kf_cexp(x,phase) \ 142 | do{ \ 143 | (x)->r = KISS_FFT_COS(phase);\ 144 | (x)->i = KISS_FFT_SIN(phase);\ 145 | }while(0) 146 | 147 | 148 | /* a debugging function */ 149 | #define pcpx(c)\ 150 | fprintf(stderr,"%g + %gi\n",(double)((c)->r),(double)((c)->i) ) 151 | 152 | 153 | #ifdef KISS_FFT_USE_ALLOCA 154 | // define this to allow use of alloca instead of malloc for temporary buffers 155 | // Temporary buffers are used in two case: 156 | // 1. FFT sizes that have "bad" factors. i.e. not 2,3 and 5 157 | // 2. "in-place" FFTs. Notice the quotes, since kissfft does not really do an in-place transform. 158 | #include 159 | #define KISS_FFT_TMP_ALLOC(nbytes) alloca(nbytes) 160 | #define KISS_FFT_TMP_FREE(ptr) 161 | #else 162 | #define KISS_FFT_TMP_ALLOC(nbytes) KISS_FFT_MALLOC(nbytes) 163 | #define KISS_FFT_TMP_FREE(ptr) KISS_FFT_FREE(ptr) 164 | #endif 165 | -------------------------------------------------------------------------------- /src/codec2_fdmdv.h: -------------------------------------------------------------------------------- 1 | /*---------------------------------------------------------------------------*\ 2 | 3 | FILE........: codec2_fdmdv.h 4 | AUTHOR......: David Rowe 5 | DATE CREATED: April 14 2012 6 | 7 | A 1400 bit/s (nominal) Frequency Division Multiplexed Digital Voice 8 | (FDMDV) modem. Used for digital audio over HF SSB. See 9 | README_fdmdv.txt for more information, and fdmdv_mod.c and 10 | fdmdv_demod.c for example usage. 11 | 12 | The name codec2_fdmdv.h is used to make it unique when "make 13 | installed". 14 | 15 | References: 16 | 17 | [1] http://n1su.com/fdmdv/FDMDV_Docs_Rel_1_4b.pdf 18 | 19 | \*---------------------------------------------------------------------------*/ 20 | 21 | /* 22 | Copyright (C) 2012 David Rowe 23 | 24 | All rights reserved. 25 | 26 | This program is free software; you can redistribute it and/or modify 27 | it under the terms of the GNU Lesser General Public License version 2.1, as 28 | published by the Free Software Foundation. This program is 29 | distributed in the hope that it will be useful, but WITHOUT ANY 30 | WARRANTY; without even the implied warranty of MERCHANTABILITY or 31 | FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public 32 | License for more details. 33 | 34 | You should have received a copy of the GNU Lesser General Public License 35 | along with this program; if not, see . 36 | */ 37 | 38 | #ifndef __FDMDV__ 39 | #define __FDMDV__ 40 | 41 | #include "comp.h" 42 | #include "modem_stats.h" 43 | 44 | #ifdef __cplusplus 45 | extern "C" { 46 | #endif 47 | 48 | /* set up the calling convention for DLL function import/export for 49 | WIN32 cross compiling */ 50 | 51 | #ifdef __CODEC2_WIN32__ 52 | #ifdef __CODEC2_BUILDING_DLL__ 53 | #define CODEC2_WIN32SUPPORT __declspec(dllexport) __stdcall 54 | #else 55 | #define CODEC2_WIN32SUPPORT __declspec(dllimport) __stdcall 56 | #endif 57 | #else 58 | #define CODEC2_WIN32SUPPORT 59 | #endif 60 | 61 | #define FDMDV_NC 14 /* default number of data carriers */ 62 | #define FDMDV_NC_MAX 20 /* maximum number of data carriers */ 63 | #define FDMDV_BITS_PER_FRAME 28 /* 20ms frames, for nominal 1400 bit/s */ 64 | #define FDMDV_NOM_SAMPLES_PER_FRAME 160 /* modulator output samples/frame and nominal demod samples/frame */ 65 | /* at 8000 Hz sample rate */ 66 | #define FDMDV_MAX_SAMPLES_PER_FRAME 200 /* max demod samples/frame, use this to allocate storage */ 67 | #define FDMDV_SCALE 1000 /* suggested scaling for 16 bit shorts */ 68 | #define FDMDV_FCENTRE 1500 /* Centre frequency, Nc/2 carriers below this, Nc/2 carriers above (Hz) */ 69 | 70 | /* 8 to 48 kHz sample rate conversion */ 71 | 72 | #define FDMDV_OS 2 /* oversampling rate */ 73 | #define FDMDV_OS_TAPS_16K 48 /* number of OS filter taps at 16kHz */ 74 | #define FDMDV_OS_TAPS_8K (FDMDV_OS_TAPS_16K/FDMDV_OS) /* number of OS filter taps at 8kHz */ 75 | 76 | /* FDMDV states and stats structures */ 77 | 78 | struct FDMDV; 79 | 80 | struct FDMDV * fdmdv_create(int Nc); 81 | void fdmdv_destroy(struct FDMDV *fdmdv_state); 82 | void fdmdv_use_old_qpsk_mapping(struct FDMDV *fdmdv_state); 83 | int fdmdv_bits_per_frame(struct FDMDV *fdmdv_state); 84 | float fdmdv_get_fsep(struct FDMDV *fdmdv_state); 85 | void fdmdv_set_fsep(struct FDMDV *fdmdv_state, float fsep); 86 | 87 | void fdmdv_mod(struct FDMDV *fdmdv_state, COMP tx_fdm[], int tx_bits[], int *sync_bit); 88 | void fdmdv_demod(struct FDMDV *fdmdv_state, int rx_bits[], int *reliable_sync_bit, COMP rx_fdm[], int *nin); 89 | 90 | void fdmdv_get_test_bits(struct FDMDV *fdmdv_state, int tx_bits[]); 91 | int fdmdv_error_pattern_size(struct FDMDV *fdmdv_state); 92 | void fdmdv_put_test_bits(struct FDMDV *f, int *sync, short error_pattern[], int *bit_errors, int *ntest_bits, int rx_bits[]); 93 | 94 | void fdmdv_get_demod_stats(struct FDMDV *fdmdv_state, struct MODEM_STATS *stats); 95 | 96 | void fdmdv_8_to_16(float out16k[], float in8k[], int n); 97 | void fdmdv_8_to_16_short(short out16k[], short in8k[], int n); 98 | void fdmdv_16_to_8(float out8k[], float in16k[], int n); 99 | void fdmdv_16_to_8_short(short out8k[], short in16k[], int n); 100 | 101 | void fdmdv_freq_shift(COMP rx_fdm_fcorr[], COMP rx_fdm[], float foff, COMP *foff_phase_rect, int nin); 102 | 103 | /* debug/development function(s) */ 104 | 105 | void fdmdv_dump_osc_mags(struct FDMDV *f); 106 | void fdmdv_simulate_channel(float *sig_pwr_av, COMP samples[], int nin, float target_snr); 107 | 108 | #ifdef __cplusplus 109 | } 110 | #endif 111 | 112 | #endif 113 | 114 | -------------------------------------------------------------------------------- /src/comp.h: -------------------------------------------------------------------------------- 1 | /*---------------------------------------------------------------------------*\ 2 | 3 | FILE........: comp.h 4 | AUTHOR......: David Rowe 5 | DATE CREATED: 24/08/09 6 | 7 | Complex number definition. 8 | 9 | \*---------------------------------------------------------------------------*/ 10 | 11 | /* 12 | Copyright (C) 2009 David Rowe 13 | 14 | All rights reserved. 15 | 16 | This program is free software; you can redistribute it and/or modify 17 | it under the terms of the GNU Lesser General Public License version 2.1, as 18 | published by the Free Software Foundation. This program is 19 | distributed in the hope that it will be useful, but WITHOUT ANY 20 | WARRANTY; without even the implied warranty of MERCHANTABILITY or 21 | FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public 22 | License for more details. 23 | 24 | You should have received a copy of the GNU Lesser General Public License 25 | along with this program; if not, see . 26 | */ 27 | 28 | #ifndef __COMP__ 29 | #define __COMP__ 30 | 31 | /* Complex number */ 32 | 33 | typedef struct { 34 | float real; 35 | float imag; 36 | } COMP; 37 | 38 | #endif 39 | -------------------------------------------------------------------------------- /src/comp_prim.h: -------------------------------------------------------------------------------- 1 | /*---------------------------------------------------------------------------*\ 2 | 3 | FILE........: comp_prim.h 4 | AUTHOR......: David Rowe 5 | DATE CREATED: Marh 2015 6 | 7 | Complex number maths primitives. 8 | 9 | \*---------------------------------------------------------------------------*/ 10 | 11 | /* 12 | Copyright (C) 2015 David Rowe 13 | 14 | All rights reserved. 15 | 16 | This program is free software; you can redistribute it and/or modify 17 | it under the terms of the GNU Lesser General Public License version 2.1, as 18 | published by the Free Software Foundation. This program is 19 | distributed in the hope that it will be useful, but WITHOUT ANY 20 | WARRANTY; without even the implied warranty of MERCHANTABILITY or 21 | FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public 22 | License for more details. 23 | 24 | You should have received a copy of the GNU Lesser General Public License 25 | along with this program; if not, see . 26 | */ 27 | 28 | #ifndef __COMP_PRIM__ 29 | #define __COMP_PRIM__ 30 | 31 | /*---------------------------------------------------------------------------*\ 32 | 33 | FUNCTIONS 34 | 35 | \*---------------------------------------------------------------------------*/ 36 | 37 | inline static COMP cneg(COMP a) 38 | { 39 | COMP res; 40 | 41 | res.real = -a.real; 42 | res.imag = -a.imag; 43 | 44 | return res; 45 | } 46 | 47 | inline static COMP cconj(COMP a) 48 | { 49 | COMP res; 50 | 51 | res.real = a.real; 52 | res.imag = -a.imag; 53 | 54 | return res; 55 | } 56 | 57 | inline static COMP cmult(COMP a, COMP b) 58 | { 59 | COMP res; 60 | 61 | res.real = a.real*b.real - a.imag*b.imag; 62 | res.imag = a.real*b.imag + a.imag*b.real; 63 | 64 | return res; 65 | } 66 | 67 | inline static COMP fcmult(float a, COMP b) 68 | { 69 | COMP res; 70 | 71 | res.real = a*b.real; 72 | res.imag = a*b.imag; 73 | 74 | return res; 75 | } 76 | 77 | inline static COMP cadd(COMP a, COMP b) 78 | { 79 | COMP res; 80 | 81 | res.real = a.real + b.real; 82 | res.imag = a.imag + b.imag; 83 | 84 | return res; 85 | } 86 | 87 | inline static float cabsolute(COMP a) 88 | { 89 | return sqrtf((a.real * a.real) + (a.imag * a.imag) ); 90 | } 91 | 92 | /* 93 | * Euler's formula in a new convenient function 94 | */ 95 | inline static COMP comp_exp_j(float phi){ 96 | COMP res; 97 | res.real = cosf(phi); 98 | res.imag = sinf(phi); 99 | return res; 100 | } 101 | 102 | /* 103 | * Quick and easy complex 0 104 | */ 105 | inline static COMP comp0(){ 106 | COMP res; 107 | res.real = 0; 108 | res.imag = 0; 109 | return res; 110 | } 111 | 112 | /* 113 | * Quick and easy complex subtract 114 | */ 115 | inline static COMP csub(COMP a, COMP b){ 116 | COMP res; 117 | res.real = a.real-b.real; 118 | res.imag = a.imag-b.imag; 119 | return res; 120 | } 121 | 122 | /* 123 | * Compare the magnitude of a and b. if |a|>|b|, return true, otw false. 124 | * This needs no square roots 125 | */ 126 | inline static int comp_mag_gt(COMP a,COMP b){ 127 | return ((a.real*a.real)+(a.imag*a.imag)) > ((b.real*b.real)+(b.imag*b.imag)); 128 | } 129 | 130 | /* 131 | * Normalize a complex number's magnitude to 1 132 | */ 133 | inline static COMP comp_normalize(COMP a){ 134 | COMP b; 135 | float av = cabsolute(a); 136 | b.real = a.real/av; 137 | b.imag = a.imag/av; 138 | return b; 139 | } 140 | 141 | #endif 142 | -------------------------------------------------------------------------------- /src/debug_alloc.h: -------------------------------------------------------------------------------- 1 | /* debug_alloc.h 2 | * 3 | * Some macros which can report on malloc results. 4 | * 5 | * Enable with "-D DEBUG_ALLOC" 6 | */ 7 | 8 | #ifndef DEBUG_ALLOC_H 9 | #define DEBUG_ALLOC_H 10 | 11 | #include 12 | 13 | // Debug calls 14 | 15 | #ifdef CORTEX_M4 16 | extern char * __heap_end; 17 | register char * sp asm ("sp"); 18 | #endif 19 | 20 | static inline void * DEBUG_MALLOC(const char *func, size_t size) { 21 | void *ptr = malloc(size); 22 | fprintf(stderr, "MALLOC: %s %p %d", func, ptr, (int)size); 23 | #ifdef CORTEX_M4 24 | 25 | fprintf(stderr, " : sp %p ", sp); 26 | #endif 27 | if (!ptr) fprintf(stderr, " ** FAILED **"); 28 | fprintf(stderr, "\n"); 29 | return(ptr); 30 | } 31 | 32 | static inline void * DEBUG_CALLOC(const char *func, size_t nmemb, size_t size) { 33 | void *ptr = calloc(nmemb, size); 34 | fprintf(stderr, "CALLOC: %s %p %d %d", func, ptr, (int)nmemb, (int)size); 35 | #ifdef CORTEX_M4 36 | fprintf(stderr, " : sp %p ", sp); 37 | #endif 38 | if (!ptr) fprintf(stderr, " ** FAILED **"); 39 | fprintf(stderr, "\n"); 40 | return(ptr); 41 | } 42 | static inline void DEBUG_FREE(const char *func, void *ptr) { 43 | free(ptr); 44 | fprintf(stderr, "FREE: %s %p\n", func, ptr); 45 | } 46 | 47 | #ifdef DEBUG_ALLOC 48 | #define MALLOC(size) DEBUG_MALLOC(__func__, size) 49 | #define CALLOC(nmemb, size) DEBUG_CALLOC(__func__, nmemb, size) 50 | #define FREE(ptr) DEBUG_FREE(__func__, ptr) 51 | #else //DEBUG_ALLOC 52 | // Default to normal calls 53 | #define MALLOC(size) malloc(size) 54 | 55 | #define CALLOC(nmemb, size) calloc(nmemb, size) 56 | 57 | #define FREE(ptr) free(ptr) 58 | 59 | #endif //DEBUG_ALLOC 60 | 61 | #endif //DEBUG_ALLOC_H 62 | -------------------------------------------------------------------------------- /src/drs232.c: -------------------------------------------------------------------------------- 1 | /*---------------------------------------------------------------------------*\ 2 | 3 | FILE........: drs232.c 4 | AUTHOR......: David Rowe 5 | DATE CREATED: March 2016 6 | 7 | Looks for a unique word in series of bits. When found, deframes a RS232 8 | encoded frame of bytes. Used for high bit rate Horus SSTV reception. 9 | 10 | Frame format: 11 | 12 | 16 bytes 0x55 - 0xabcdef01 UW - 256 bytes of payload - 2 bytes CRC 13 | 14 | Each byte is encoded as a 10 bit RS232 serial word: 15 | 16 | 0 LSB .... MSB 1 17 | 18 | Building: 19 | 20 | $ gcc drs232.c -o drs232 -Wall 21 | 22 | \*---------------------------------------------------------------------------*/ 23 | 24 | 25 | /* 26 | Copyright (C) 2016 David Rowe 27 | 28 | All rights reserved. 29 | 30 | This program is free software; you can redistribute it and/or modify 31 | it under the terms of the GNU Lesser General Public License version 2.1, as 32 | published by the Free Software Foundation. This program is 33 | distributed in the hope that it will be useful, but WITHOUT ANY 34 | WARRANTY; without even the implied warranty of MERCHANTABILITY or 35 | FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public 36 | License for more details. 37 | 38 | You should have received a copy of the GNU Lesser General Public License 39 | along with this program; if not, see . 40 | */ 41 | 42 | #include 43 | #include 44 | #include 45 | #include 46 | #include 47 | #include 48 | 49 | /* states -----------------------------------------------*/ 50 | 51 | #define LOOK_FOR_UW 0 52 | #define COLLECT_PACKET 1 53 | 54 | /* packet parameters */ 55 | 56 | #define UW_BYTES 4 57 | #define UW_BITS 40 58 | #define UW_ALLOWED_ERRORS 5 59 | #define BYTES_PER_PACKET 256 60 | #define CRC_BYTES 2 61 | #define BITS_PER_BYTE 10 62 | #define UNPACKED_PACKET_BYTES ((UW_BYTES+BYTES_PER_PACKET+CRC_BYTES)*BITS_PER_BYTE) 63 | 64 | /* UW pattern we look for, including start/stop bits */ 65 | 66 | uint8_t uw[] = { 67 | /* 0xb 0xa */ 68 | 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 69 | /* 0xd 0xc */ 70 | 0, 1, 0, 1, 1, 0, 0, 1, 1, 1, 71 | /* 0xf 0xe */ 72 | 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 73 | /* 0x1 0x0 */ 74 | 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 75 | }; 76 | 77 | // from http://stackoverflow.com/questions/10564491/function-to-calculate-a-crc16-checksum 78 | 79 | unsigned short gen_crc16(unsigned char* data_p, int length){ 80 | unsigned char x; 81 | unsigned short crc = 0xFFFF; 82 | 83 | while (length--){ 84 | x = crc >> 8 ^ *data_p++; 85 | x ^= x>>4; 86 | crc = (crc << 8) ^ ((unsigned short)(x << 12)) ^ ((unsigned short)(x <<5)) ^ ((unsigned short)x); 87 | } 88 | 89 | return crc; 90 | } 91 | 92 | 93 | int main(int argc, char *argv[]) { 94 | FILE *fin, *fout; 95 | int state, next_state, i, j, k, ind, score, bits_read; 96 | char bit; 97 | uint8_t unpacked_packet[UNPACKED_PACKET_BYTES]; 98 | uint8_t packet[BYTES_PER_PACKET+CRC_BYTES]; 99 | uint8_t abyte; 100 | uint16_t tx_checksum, rx_checksum; 101 | int verbose, packet_errors, packets; 102 | 103 | if (argc < 3) { 104 | fprintf(stderr, "usage: drs232 InputOneBitPerChar OutputPackets [-v[v]]\n"); 105 | exit(1); 106 | } 107 | 108 | if (strcmp(argv[1], "-") == 0) fin = stdin; 109 | else if ( (fin = fopen(argv[1],"rb")) == NULL ) { 110 | fprintf(stderr, "Error opening input file: %s: %s.\n", 111 | argv[1], strerror(errno)); 112 | exit(1); 113 | } 114 | 115 | if (strcmp(argv[2], "-") == 0) fout = stdout; 116 | else if ( (fout = fopen(argv[2],"wb")) == NULL ) { 117 | fprintf(stderr, "Error opening output file: %s: %s.\n", 118 | argv[2], strerror(errno)); 119 | exit(1); 120 | } 121 | 122 | verbose = 0; 123 | if (argc > 3) { 124 | if (strcmp(argv[3], "-v") == 0) { 125 | verbose = 1; 126 | } 127 | if (strcmp(argv[3], "-vv") == 0) { 128 | verbose = 2; 129 | } 130 | } 131 | 132 | state = LOOK_FOR_UW; 133 | for(i=0; i (UW_BITS-UW_ALLOWED_ERRORS)) { 166 | if (verbose == 2) { 167 | fprintf(stderr,"UW found!\n"); 168 | } 169 | ind = UW_BITS; 170 | next_state = COLLECT_PACKET; 171 | } 172 | } 173 | 174 | if (state == COLLECT_PACKET) { 175 | unpacked_packet[ind++] = bit; 176 | 177 | if (ind == UNPACKED_PACKET_BYTES) { 178 | 179 | /* OK we have enough bits, remove RS232 sync bits and pack */ 180 | 181 | for(i=UW_BITS,k=0; i. 26 | */ 27 | 28 | 29 | #ifndef __C2FSK_H 30 | #define __C2FSK_H 31 | 32 | #include 33 | #include "comp.h" 34 | #include "kiss_fftr.h" 35 | #include "modem_stats.h" 36 | 37 | #define MODE_2FSK 2 38 | #define MODE_4FSK 4 39 | 40 | #define MODE_M_MAX 4 41 | 42 | #define FSK_SCALE 16383 43 | 44 | /* default internal parameters */ 45 | #define FSK_DEFAULT_P 8 46 | #define FSK_DEFAULT_NSYM 50 47 | 48 | struct FSK { 49 | /* Static parameters set up by fsk_init */ 50 | int Ndft; /* freq offset est fft */ 51 | int Fs; /* sample freq */ 52 | int N; /* processing buffer size */ 53 | int Rs; /* symbol rate */ 54 | int Ts; /* samples per symbol */ 55 | int Nmem; /* size of extra mem for timing adj */ 56 | int P; /* oversample rate for timing est/adj */ 57 | int Nsym; /* Number of symbols spat out in a processing frame */ 58 | int Nbits; /* Number of bits spat out in a processing frame */ 59 | int f1_tx; /* f1 for modulator */ 60 | int fs_tx; /* Space between TX freqs for modulatosr */ 61 | int mode; /* 2FSK or 4FSK */ 62 | float tc; /* time constant for smoothing FFTs */ 63 | int est_min; /* Minimum frequency for freq. estimator */ 64 | int est_max; /* Maximum frequency for freq. estimaotr */ 65 | int est_space; /* Minimum frequency spacing for freq. estimator */ 66 | float* hann_table; /* Precomputed or runtime computed hann window table */ 67 | 68 | /* Parameters used by demod */ 69 | float* Sf; /* Average of magnitude spectrum */ 70 | COMP phi_c[MODE_M_MAX]; /* phase of each demod local oscillator */ 71 | COMP *f_dc; /* down converted samples */ 72 | 73 | kiss_fft_cfg fft_cfg; /* Config for KISS FFT, used in freq est */ 74 | float norm_rx_timing; /* Normalized RX timing */ 75 | 76 | 77 | /* Parameters used by mod */ 78 | COMP tx_phase_c; /* TX phase, but complex */ 79 | 80 | /* Statistics generated by demod */ 81 | float EbNodB; /* Estimated EbNo in dB */ 82 | float f_est[MODE_M_MAX]; /* Estimated frequencies (peak method) */ 83 | float f2_est[MODE_M_MAX];/* Estimated frequencies (mask method) */ 84 | int freq_est_type; /* which estimator to use */ 85 | float ppm; /* Estimated PPM clock offset */ 86 | 87 | /* Parameters used by mod/demod and driving code */ 88 | int nin; /* Number of samples to feed the next demod cycle */ 89 | int burst_mode; /* enables/disables 'burst' mode */ 90 | int lock_nin; /* locks nin during testing */ 91 | 92 | /* modem statistic struct */ 93 | struct MODEM_STATS *stats; 94 | int normalise_eye; /* enables/disables normalisation of eye diagram */ 95 | }; 96 | 97 | /* 98 | * Create an FSK config/state struct from a set of config parameters 99 | * 100 | * int Fs - Sample frequency 101 | * int Rs - Symbol rate 102 | * int tx_f1 - '0' frequency 103 | * int tx_fs - frequency spacing 104 | */ 105 | struct FSK * fsk_create(int Fs, int Rs, int M, int tx_f1, int tx_fs); 106 | 107 | /* 108 | * Create an FSK config/state struct from a set of config parameters 109 | * 110 | * int Fs - Sample frequency 111 | * int Rs - Symbol rate 112 | * int tx_f1 - '0' frequency 113 | * int tx_fs - frequency spacing 114 | */ 115 | struct FSK * fsk_create_hbr(int Fs, int Rs, int M, int P, int Nsym, int tx_f1, int tx_fs); 116 | 117 | /* 118 | * Set the minimum and maximum frequencies at which the freq. estimator can find tones 119 | */ 120 | void fsk_set_freq_est_limits(struct FSK *fsk,int fmin, int fmax); 121 | 122 | /* 123 | * Clear the estimator states 124 | */ 125 | void fsk_clear_estimators(struct FSK *fsk); 126 | 127 | /* 128 | * Fills MODEM_STATS struct with demod statistics 129 | */ 130 | void fsk_get_demod_stats(struct FSK *fsk, struct MODEM_STATS *stats); 131 | 132 | /* 133 | * Destroy an FSK state struct and free it's memory 134 | * 135 | * struct FSK *fsk - FSK config/state struct to be destroyed 136 | */ 137 | void fsk_destroy(struct FSK *fsk); 138 | 139 | /* 140 | * Modulates Nsym bits into N samples 141 | * 142 | * struct FSK *fsk - FSK config/state struct, set up by fsk_create 143 | * float fsk_out[] - Buffer for N samples of modulated FSK 144 | * uint8_t tx_bits[] - Buffer containing Nbits unpacked bits 145 | */ 146 | void fsk_mod(struct FSK *fsk, float fsk_out[], uint8_t tx_bits[]); 147 | 148 | /* 149 | * Modulates Nsym bits into N samples 150 | * 151 | * struct FSK *fsk - FSK config/state struct, set up by fsk_create 152 | * float fsk_out[] - Buffer for N samples of "voltage" used to modulate an external VCO 153 | * uint8_t tx_bits[] - Buffer containing Nbits unpacked bits 154 | */ 155 | void fsk_mod_ext_vco(struct FSK *fsk, float vco_out[], uint8_t tx_bits[]); 156 | 157 | /* 158 | * Modulates Nsym bits into N complex samples 159 | * 160 | * struct FSK *fsk - FSK config/state struct, set up by fsk_create 161 | * comp fsk_out[] - Buffer for N samples of modulated FSK 162 | * uint8_t tx_bits[] - Buffer containing Nbits unpacked bits 163 | */ 164 | void fsk_mod_c(struct FSK *fsk, COMP fsk_out[], uint8_t tx_bits[]); 165 | 166 | 167 | /* 168 | * Returns the number of samples needed for the next fsk_demod() cycle 169 | * 170 | * struct FSK *fsk - FSK config/state struct, set up by fsk_create 171 | * returns - number of samples to be fed into fsk_demod next cycle 172 | */ 173 | uint32_t fsk_nin(struct FSK *fsk); 174 | 175 | 176 | /* 177 | * Demodulate some number of FSK samples. The number of samples to be 178 | * demodulated can be found by calling fsk_nin(). 179 | * 180 | * struct FSK *fsk - FSK config/state struct, set up by fsk_create 181 | * uint8_t rx_bits[] - Buffer for Nbits unpacked bits to be written 182 | * float fsk_in[] - nin samples of modulated FSK 183 | */ 184 | void fsk_demod(struct FSK *fsk, uint8_t rx_bits[],COMP fsk_in[]); 185 | 186 | /* 187 | * Demodulate some number of FSK samples. The number of samples to be 188 | * demodulated can be found by calling fsk_nin(). 189 | * 190 | * struct FSK *fsk - FSK config/state struct, set up by fsk_create 191 | * float rx_sd[] - Buffer for Nbits soft decision bits to be written 192 | * float fsk_in[] - nin samples of modualted FSK 193 | */ 194 | void fsk_demod_sd(struct FSK *fsk, float rx_sd[], COMP fsk_in[]); 195 | 196 | /* 197 | * " Why not both? " 198 | * Demodulate some number of FSK samples. The number of samples to be 199 | * demodulated can be found by calling fsk_nin(). 200 | * 201 | * struct FSK *fsk - FSK config/state struct, set up by fsk_create 202 | * float rx_bits[] - Buffer for Nbits soft decision bits to be written 203 | * float rx_sd[] - Buffer for Nbits soft decision bits to be written 204 | * float fsk_in[] - nin samples of modualted FSK 205 | */ 206 | void fsk_demod_core(struct FSK *fsk, uint8_t rx_bits[], float rx_sd[], COMP fsk_in[]); 207 | 208 | /* enables/disables normalisation of eye diagram samples */ 209 | 210 | void fsk_stats_normalise_eye(struct FSK *fsk, int normalise_enable); 211 | 212 | /* Set the FSK modem into burst demod mode */ 213 | 214 | void fsk_enable_burst_mode(struct FSK *fsk); 215 | 216 | /* Set freq est algorithm 0: peak 1:mask */ 217 | void fsk_set_freq_est_alg(struct FSK *fsk, int est_type); 218 | 219 | #endif 220 | -------------------------------------------------------------------------------- /src/fsk_get_test_bits.c: -------------------------------------------------------------------------------- 1 | /*---------------------------------------------------------------------------*\ 2 | 3 | FILE........: fsk_get_test_bits.c 4 | AUTHOR......: Brady O'Brien 5 | DATE CREATED: January 2016 6 | 7 | Generates a pseudorandom sequence of bits for testing of fsk_mod and fsk_demod 8 | 9 | \*---------------------------------------------------------------------------*/ 10 | 11 | 12 | /* 13 | Copyright (C) 2016 David Rowe 14 | 15 | All rights reserved. 16 | 17 | This program is free software; you can redistribute it and/or modify 18 | it under the terms of the GNU Lesser General Public License version 2.1, as 19 | published by the Free Software Foundation. This program is 20 | distributed in the hope that it will be useful, but WITHOUT ANY 21 | WARRANTY; without even the implied warranty of MERCHANTABILITY or 22 | FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public 23 | License for more details. 24 | 25 | You should have received a copy of the GNU Lesser General Public License 26 | along with this program; if not, see . 27 | */ 28 | 29 | 30 | #include 31 | #include 32 | #include "fsk.h" 33 | 34 | #define TEST_FRAME_SIZE 100 /* arbitrary chice, repeats after this 35 | many bits, sets frame size for rx 36 | processing */ 37 | 38 | int main(int argc,char *argv[]){ 39 | int bitcnt, framecnt; 40 | int framesize = TEST_FRAME_SIZE; 41 | int i; 42 | FILE *fout; 43 | uint8_t *bitbuf; 44 | 45 | if(argc < 3){ 46 | fprintf(stderr,"usage: %s OutputBitsOnePerByte numBits [framesize]\n",argv[0]); 47 | exit(1); 48 | } 49 | 50 | if (argc == 4){ 51 | framesize = atoi(argv[3]); 52 | fprintf(stderr, "Using custom frame size of %d bits\n", framesize); 53 | } 54 | 55 | /* Extract parameters */ 56 | bitcnt = atoi(argv[2]); 57 | framecnt = bitcnt/framesize; 58 | if (framecnt == 0) { 59 | fprintf(stderr,"Need a minimum of %d bits\n", framesize); 60 | exit(1); 61 | } 62 | 63 | if(strcmp(argv[1],"-")==0){ 64 | fout = stdout; 65 | }else{ 66 | fout = fopen(argv[1],"w"); 67 | } 68 | 69 | if(fout==NULL){ 70 | fprintf(stderr,"Couldn't open output file: %s\n", argv[1]); 71 | goto cleanup; 72 | } 73 | 74 | /* allocate buffers for processing */ 75 | bitbuf = (uint8_t*)alloca(sizeof(uint8_t)*framesize); 76 | 77 | /* Generate buffer of test frame bits from known seed */ 78 | srand(158324); 79 | for(i=0; i. 29 | */ 30 | 31 | #include 32 | #include 33 | #include 34 | #include "fsk.h" 35 | #include "codec2_fdmdv.h" 36 | 37 | int main(int argc,char *argv[]){ 38 | struct FSK *fsk; 39 | int Fs,Rs,f1,fs,M; 40 | int i; 41 | int p, user_p = 0; 42 | FILE *fin,*fout; 43 | uint8_t *bitbuf; 44 | int16_t *rawbuf; 45 | float *modbuf; 46 | 47 | char usage[] = "usage: %s [-p P] Mode SampleFreq SymbolFreq TxFreq1 TxFreqSpace InputOneBitPerCharFile OutputModRawFile\n"; 48 | 49 | int opt; 50 | while ((opt = getopt(argc, argv, "p:")) != -1) { 51 | switch (opt) { 52 | case 'p': 53 | p = atoi(optarg); 54 | user_p = 1; 55 | break; 56 | default: 57 | fprintf(stderr, usage, argv[0]); 58 | exit(1); 59 | } 60 | } 61 | 62 | if (argc<8){ 63 | fprintf(stderr, usage, argv[0]); 64 | exit(1); 65 | } 66 | 67 | /* Extract parameters */ 68 | M = atoi(argv[optind++]); 69 | Fs = atoi(argv[optind++]); 70 | Rs = atoi(argv[optind++]); 71 | f1 = atoi(argv[optind++]); 72 | fs = atoi(argv[optind++]); 73 | 74 | if(strcmp(argv[optind],"-")==0){ 75 | fin = stdin; 76 | }else{ 77 | fin = fopen(argv[optind],"r"); 78 | } 79 | optind++; 80 | 81 | if(strcmp(argv[optind],"-")==0){ 82 | fout = stdout; 83 | }else{ 84 | fout = fopen(argv[optind],"w"); 85 | } 86 | 87 | /* p is not actually used for the modulator, but we need to set it for fsk_create() to be happy */ 88 | if (!user_p) 89 | p = Fs/Rs; 90 | 91 | /* set up FSK */ 92 | fsk = fsk_create_hbr(Fs,Rs,M,p,FSK_DEFAULT_NSYM,f1,fs); 93 | 94 | if(fin==NULL || fout==NULL || fsk==NULL){ 95 | fprintf(stderr,"Couldn't open test vector files\n"); 96 | goto cleanup; 97 | } 98 | 99 | 100 | /* allocate buffers for processing */ 101 | bitbuf = (uint8_t*)malloc(sizeof(uint8_t)*fsk->Nbits); 102 | rawbuf = (int16_t*)malloc(sizeof(int16_t)*fsk->N); 103 | modbuf = (float*)malloc(sizeof(float)*fsk->N); 104 | 105 | /* Modulate! */ 106 | while( fread(bitbuf,sizeof(uint8_t),fsk->Nbits,fin) == fsk->Nbits ){ 107 | fsk_mod(fsk,modbuf,bitbuf); 108 | for(i=0; iN; i++){ 109 | rawbuf[i] = (int16_t)(modbuf[i]*(float)FDMDV_SCALE); 110 | } 111 | fwrite(rawbuf,sizeof(int16_t),fsk->N,fout); 112 | 113 | if(fin == stdin || fout == stdin){ 114 | fflush(fin); 115 | fflush(fout); 116 | } 117 | } 118 | free(bitbuf); 119 | free(rawbuf); 120 | free(modbuf); 121 | 122 | cleanup: 123 | fclose(fin); 124 | fclose(fout); 125 | fsk_destroy(fsk); 126 | exit(0); 127 | } 128 | -------------------------------------------------------------------------------- /src/fsk_put_test_bits.c: -------------------------------------------------------------------------------- 1 | /*---------------------------------------------------------------------------*\ 2 | 3 | FILE........: fsk_get_test_bits.c 4 | AUTHOR......: Brady O'Brien 5 | DATE CREATED: January 2016 6 | 7 | Generates a pseudorandom sequence of bits for testing of fsk_mod and 8 | fsk_demod. 9 | 10 | \*---------------------------------------------------------------------------*/ 11 | 12 | 13 | /* 14 | Copyright (C) 2016 David Rowe 15 | 16 | All rights reserved. 17 | 18 | This program is free software; you can redistribute it and/or modify 19 | it under the terms of the GNU Lesser General Public License version 2.1, as 20 | published by the Free Software Foundation. This program is 21 | distributed in the hope that it will be useful, but WITHOUT ANY 22 | WARRANTY; without even the implied warranty of MERCHANTABILITY or 23 | FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public 24 | License for more details. 25 | 26 | You should have received a copy of the GNU Lesser General Public License 27 | along with this program; if not, see . 28 | */ 29 | 30 | #include 31 | #include 32 | #include 33 | #include "fsk.h" 34 | 35 | #define TEST_FRAME_SIZE 100 /* must match fsk_get_test_bits.c */ 36 | 37 | #define VALID_PACKET_BER_THRESH 0.1 38 | 39 | int main(int argc,char *argv[]){ 40 | int bitcnt,biterr,i,errs,packetcnt; 41 | int framesize = TEST_FRAME_SIZE; 42 | float valid_packet_ber_thresh = VALID_PACKET_BER_THRESH; 43 | int packet_pass_thresh = 0; 44 | float ber_pass_thresh = 0; 45 | FILE *fin; 46 | uint8_t *bitbuf_tx, *bitbuf_rx, abit; 47 | int verbose = 1; 48 | 49 | char usage[] = "usage: %s [-f frameSizeBits] [-t VaildFrameBERThreshold] [-b BERPass] [-p numPacketsPass] InputOneBitPerByte\n"; 50 | 51 | int opt; 52 | while ((opt = getopt(argc, argv, "f:b:p:hqt:")) != -1) { 53 | switch (opt) { 54 | case 't': 55 | valid_packet_ber_thresh = atof(optarg); 56 | break; 57 | case 'b': 58 | ber_pass_thresh = atof(optarg); 59 | break; 60 | case 'p': 61 | packet_pass_thresh = atoi(optarg); 62 | break; 63 | case 'f': 64 | framesize = atoi(optarg); 65 | break; 66 | case 'q': 67 | verbose = 0; 68 | break; 69 | case 'h': 70 | default: 71 | fprintf(stderr, usage, argv[0]); 72 | exit(1); 73 | } 74 | } 75 | if (argc == 1) { 76 | fprintf(stderr, usage, argv[0]); 77 | exit(1); 78 | } 79 | char *fname = argv[optind++]; 80 | if ((strcmp(fname,"-")==0) || (argc<2)){ 81 | fin = stdin; 82 | } else { 83 | fin = fopen(fname,"r"); 84 | } 85 | 86 | if(fin==NULL){ 87 | fprintf(stderr,"Couldn't open input file: %s\n", argv[1]); 88 | exit(1); 89 | } 90 | 91 | /* allocate buffers for processing */ 92 | bitbuf_tx = (uint8_t*)alloca(sizeof(uint8_t)*framesize); 93 | bitbuf_rx = (uint8_t*)alloca(sizeof(uint8_t)*framesize); 94 | 95 | /* Generate known tx frame from known seed */ 96 | srand(158324); 97 | for(i=0; i0){ 106 | 107 | /* update sliding window of input bits */ 108 | 109 | for(i=0; i= packet_pass_thresh) && (ber <= ber_pass_thresh)) { 140 | fprintf(stderr,"PASS\n"); 141 | return 0; 142 | } 143 | else { 144 | fprintf(stderr,"FAIL\n"); 145 | return 1; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/golay23.h: -------------------------------------------------------------------------------- 1 | /*---------------------------------------------------------------------------*\ 2 | 3 | FILE........: golay23.h 4 | AUTHOR......: David Rowe 5 | DATE CREATED: 3 March 2013 6 | 7 | Header file for Golay FEC. 8 | 9 | \*---------------------------------------------------------------------------*/ 10 | 11 | /* 12 | Copyright (C) 2013 David Rowe 13 | 14 | All rights reserved. 15 | 16 | This program is free software; you can redistribute it and/or modify 17 | it under the terms of the GNU Lesser General Public License version 2.1, as 18 | published by the Free Software Foundation. This program is 19 | distributed in the hope that it will be useful, but WITHOUT ANY 20 | WARRANTY; without even the implied warranty of MERCHANTABILITY or 21 | FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public 22 | License for more details. 23 | 24 | You should have received a copy of the GNU Lesser General Public License 25 | along with this program; if not, see . 26 | */ 27 | 28 | #ifndef __GOLAY23__ 29 | #define __GOLAY23__ 30 | 31 | #ifdef __cplusplus 32 | extern "C" { 33 | #endif 34 | 35 | void golay23_init(void); 36 | int golay23_encode(int data); 37 | int golay23_decode(int received_codeword); 38 | int golay23_count_errors(int recd_codeword, int corrected_codeword); 39 | int golay23_syndrome(int c); 40 | 41 | #ifdef __cplusplus 42 | } 43 | #endif 44 | 45 | #endif 46 | -------------------------------------------------------------------------------- /src/horus_api.h: -------------------------------------------------------------------------------- 1 | /*---------------------------------------------------------------------------*\ 2 | 3 | FILE........: horus_api.h 4 | AUTHOR......: David Rowe 5 | DATE CREATED: March 2018 6 | 7 | Library of API functions that implement High Altitude Balloon (HAB) 8 | telemetry modems and protocols. 9 | 10 | \*---------------------------------------------------------------------------*/ 11 | 12 | /* 13 | Copyright (C) 2018 David Rowe 14 | 15 | All rights reserved. 16 | 17 | This program is free software; you can redistribute it and/or modify 18 | it under the terms of the GNU Lesser General Public License version 2.1, as 19 | published by the Free Software Foundation. This program is 20 | distributed in the hope that it will be useful, but WITHOUT ANY 21 | WARRANTY; without even the implied warranty of MERCHANTABILITY or 22 | FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public 23 | License for more details. 24 | 25 | You should have received a copy of the GNU Lesser General Public License 26 | along with this program; if not, see . 27 | */ 28 | 29 | #ifdef __cplusplus 30 | extern "C" { 31 | #endif 32 | 33 | #ifndef __HORUS_API__ 34 | 35 | #include 36 | #include "modem_stats.h" 37 | 38 | /* Horus API Modes */ 39 | #define HORUS_MODE_BINARY_V1 0 // Legacy binary mode 40 | #define HORUS_MODE_BINARY_V2_256BIT 1 // New 256-bit Golay Encoded Mode 41 | #define HORUS_MODE_BINARY_V2_128BIT 2 // New 128-bit Golay Encoded Mode (Not used yet) 42 | #define HORUS_MODE_RTTY_7N1 89 // RTTY Decoding - 7N1 43 | #define HORUS_MODE_RTTY_7N2 90 // RTTY Decoding - 7N2 44 | #define HORUS_MODE_RTTY_8N2 91 // RTTY Decoding - 8N2 45 | 46 | 47 | // Settings for Legacy Horus Binary Mode (Golay (23,12) encoding) 48 | #define HORUS_BINARY_V1_NUM_CODED_BITS 360 49 | #define HORUS_BINARY_V1_NUM_UNCODED_PAYLOAD_BYTES 22 50 | #define HORUS_BINARY_V1_DEFAULT_BAUD 100 51 | #define HORUS_BINARY_V1_DEFAULT_TONE_SPACING 270 // This is the minimum tone spacing possible on the RS41 52 | // reference implementation of this modem. 53 | // Note that mask estimation is turned off by default for 54 | // this mode, and hence this spacing is not used. 55 | 56 | // Settings for Horus Binary 256-bit mode (Golay (23,12) encoding) 57 | #define HORUS_BINARY_V2_256BIT_NUM_CODED_BITS 520 58 | #define HORUS_BINARY_V2_256BIT_NUM_UNCODED_PAYLOAD_BYTES 32 59 | #define HORUS_BINARY_V2_256BIT_DEFAULT_BAUD 100 60 | #define HORUS_BINARY_V2_256BIT_DEFAULT_TONE_SPACING 270 61 | 62 | // Settings for Horus Binary 128-bit mode (Golay (23,12) encoding) - not used yet 63 | #define HORUS_BINARY_V2_128BIT_NUM_CODED_BITS 272 64 | #define HORUS_BINARY_V2_128BIT_NUM_UNCODED_PAYLOAD_BYTES 16 65 | #define HORUS_BINARY_V2_128BIT_DEFAULT_BAUD 100 66 | #define HORUS_BINARY_V2_128BIT_DEFAULT_TONE_SPACING 270 67 | 68 | 69 | #define HORUS_BINARY_V1V2_MAX_BITS HORUS_BINARY_V2_256BIT_NUM_CODED_BITS 70 | #define HORUS_BINARY_V1V2_MAX_UNCODED_BYTES HORUS_BINARY_V2_256BIT_NUM_UNCODED_PAYLOAD_BYTES 71 | 72 | // Not using LDPC any more... 73 | // // Settings for Horus Binary 256-bit mode (LDPC Encoding, r=1/3) 74 | // #define HORUS_BINARY_V2_256BIT_NUM_CODED_BITS (768+32) 75 | // #define HORUS_BINARY_V2_256BIT_NUM_UNCODED_PAYLOAD_BYTES 32 76 | // #define HORUS_BINARY_V2_256BIT_DEFAULT_BAUD 100 77 | // #define HORUS_BINARY_V2_256BIT_DEFAULT_TONE_SPACING 270 78 | 79 | // // Settings for Horus Binary 128-bit mode (LDPC Encoding, r=1/3) 80 | // #define HORUS_BINARY_V2_128BIT_NUM_CODED_BITS (384+32) 81 | // #define HORUS_BINARY_V2_128BIT_NUM_UNCODED_PAYLOAD_BYTES 16 82 | // #define HORUS_BINARY_V2_128BIT_DEFAULT_BAUD 100 83 | // #define HORUS_BINARY_V2_128BIT_DEFAULT_TONE_SPACING 270 84 | 85 | 86 | // Settings for RTTY Decoder 87 | #define HORUS_RTTY_MAX_CHARS 120 88 | #define HORUS_RTTY_7N1_NUM_BITS (HORUS_RTTY_MAX_CHARS*9) 89 | #define HORUS_RTTY_7N2_NUM_BITS (HORUS_RTTY_MAX_CHARS*10) 90 | #define HORUS_RTTY_8N2_NUM_BITS (HORUS_RTTY_MAX_CHARS*11) 91 | #define HORUS_RTTY_DEFAULT_BAUD 100 92 | 93 | struct horus; 94 | struct MODEM_STATS; 95 | 96 | /* 97 | * Create an Horus Demod config/state struct using default mode parameters. 98 | * 99 | * int mode - Horus Mode Type (refer list above) 100 | */ 101 | struct horus *horus_open (int mode); 102 | 103 | /* 104 | * Create an Horus Demod config/state struct with more customizations. 105 | * 106 | * int mode - Horus Mode Type (refer list above) 107 | * int Rs - Symbol Rate (Hz). Set to -1 to use the default value for the mode (refer above) 108 | * int tx_tone_spacing - FSK Tone Spacing, to configure mask estimator. Set to -1 to disable mask estimator. 109 | */ 110 | 111 | struct horus *horus_open_advanced (int mode, int Rs, int tx_tone_spacing); 112 | 113 | /* 114 | * Create an Horus Demod config/state struct with more customizations. 115 | * 116 | * int mode - Horus Mode Type (refer list above) 117 | * int Rs - Symbol Rate (Hz). Set to -1 to use the default value for the mode (refer above) 118 | * int tx_tone_spacing - FSK Tone Spacing, to configure mask estimator. Set to -1 to disable mask estimator. 119 | * int Fs - Sample rate 120 | * int P - Oversamplig rate. (Fs/Rs)%P should equal 0 other the modem will be sad. 121 | */ 122 | 123 | struct horus *horus_open_advanced_sample_rate (int mode, int Rs, int tx_tone_spacing, int Fs, int P); 124 | 125 | 126 | /* 127 | * Close a Horus demodulator struct and free memory. 128 | */ 129 | void horus_close (struct horus *hstates); 130 | 131 | /* call before horus_rx() to determine how many shorts to pass in */ 132 | 133 | uint32_t horus_nin (struct horus *hstates); 134 | 135 | /* 136 | * Demodulate some number of Horus modem samples. The number of samples to be 137 | * demodulated can be found by calling horus_nin(). 138 | * 139 | * Returns 1 if the data in ascii_out[] is valid. 140 | * 141 | * struct horus *hstates - Horus API config/state struct, set up by horus_open / horus_open_advanced 142 | * char ascii_out[] - Buffer for returned packet / text. 143 | * short fsk_in[] - nin samples of modulated FSK. 144 | * int quadrature - Set to 1 if input samples are complex samples. 145 | */ 146 | 147 | int horus_rx (struct horus *hstates, char ascii_out[], short demod_in[], int quadrature); 148 | 149 | /* set verbose level */ 150 | 151 | void horus_set_verbose(struct horus *hstates, int verbose); 152 | 153 | /* functions to get information from API */ 154 | 155 | int horus_get_version (void); 156 | int horus_get_mode (struct horus *hstates); 157 | int horus_get_Fs (struct horus *hstates); 158 | int horus_get_mFSK (struct horus *hstates); 159 | void horus_get_modem_stats (struct horus *hstates, int *sync, float *snr_est); 160 | void horus_get_modem_extended_stats (struct horus *hstates, struct MODEM_STATS *stats); 161 | int horus_crc_ok (struct horus *hstates); 162 | int horus_get_total_payload_bits (struct horus *hstates); 163 | void horus_set_total_payload_bits (struct horus *hstates, int val); 164 | void horus_set_freq_est_limits (struct horus *hstates, float fsk_lower, float fsk_upper); 165 | 166 | /* how much storage you need for demod_in[] and ascii_out[] */ 167 | 168 | int horus_get_max_demod_in (struct horus *hstates); 169 | int horus_get_max_ascii_out_len (struct horus *hstates); 170 | 171 | #endif 172 | 173 | #ifdef __cplusplus 174 | } 175 | #endif 176 | -------------------------------------------------------------------------------- /src/horus_gen_test_bits.c: -------------------------------------------------------------------------------- 1 | /*---------------------------------------------------------------------------*\ 2 | 3 | FILE........: horus_gen_tx_bits.c 4 | AUTHOR......: Mark Jessop 5 | DATE CREATED: May 2020 6 | 7 | Horus dummy packet generation, for use with fsk_demod. 8 | 9 | Build: 10 | gcc horus_gen_test_bits.c horus_l2.c golay23.c -o horus_get_test_bits -Wall -DSCRAMBLER -DINTERLEAVER 11 | 12 | \*---------------------------------------------------------------------------*/ 13 | 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | #include "horus_l2.h" 22 | #include "H_128_384_23.h" 23 | #include "H_256_768_22.h" 24 | 25 | // TODO: Move these packet format definitions to somehwere common. 26 | 27 | /* Horus Mode 0 (Legacy 22-byte) Binary Packet */ 28 | struct TBinaryPacket0 29 | { 30 | uint8_t PayloadID; 31 | uint16_t Counter; 32 | uint8_t Hours; 33 | uint8_t Minutes; 34 | uint8_t Seconds; 35 | float Latitude; 36 | float Longitude; 37 | uint16_t Altitude; 38 | uint8_t Speed; // Speed in Knots (1-255 knots) 39 | uint8_t Sats; 40 | int8_t Temp; // Twos Complement Temp value. 41 | uint8_t BattVoltage; // 0 = 0.5v, 255 = 2.0V, linear steps in-between. 42 | uint16_t Checksum; // CRC16-CCITT Checksum. 43 | } __attribute__ ((packed)); 44 | 45 | /* Horus v2 Mode 1 (32-byte) Binary Packet */ 46 | struct TBinaryPacket1 47 | { 48 | uint16_t PayloadID; 49 | uint16_t Counter; 50 | uint8_t Hours; 51 | uint8_t Minutes; 52 | uint8_t Seconds; 53 | float Latitude; 54 | float Longitude; 55 | uint16_t Altitude; 56 | uint8_t Speed; // Speed in Knots (1-255 knots) 57 | uint8_t Sats; 58 | int8_t Temp; // Twos Complement Temp value. 59 | uint8_t BattVoltage; // 0 = 0.5v, 255 = 2.0V, linear steps in-between. 60 | uint8_t dummy1; // Dummy values for user-configurable section. 61 | float dummy2; // Float 62 | uint8_t dummy3; // battery voltage test 63 | uint8_t dummy4; // divide by 10 64 | uint16_t dummy5; // divide by 100 65 | uint16_t Checksum; // CRC16-CCITT Checksum. 66 | } __attribute__ ((packed)); 67 | 68 | /* Horus v2 Mode 2 (16-byte) Binary Packet (Not currently used) */ 69 | struct TBinaryPacket2 70 | { 71 | uint8_t PayloadID; 72 | uint8_t Counter; 73 | uint16_t BiSeconds; 74 | uint8_t LatitudeMSB; 75 | uint16_t Latitude; 76 | uint8_t LongitudeMSB; 77 | uint16_t Longitude; 78 | uint16_t Altitude; 79 | uint8_t BattVoltage; // 0 = 0.5v, 255 = 2.0V, linear steps in-between. 80 | uint8_t flags; // Dummy values for user-configurable section. 81 | uint16_t Checksum; // CRC16-CCITT Checksum. 82 | } __attribute__ ((packed)); 83 | 84 | 85 | 86 | 87 | int main(int argc,char *argv[]) { 88 | int i, framecnt; 89 | int horus_mode = 0; 90 | 91 | char usage[] = "usage: %s horus_mode numFrames\nMode 0 = Legacy 22-byte Golay FEC\nMode 1 = 32-byte Golay FEC\n"; 92 | 93 | if (argc < 3) { 94 | fprintf(stderr, usage, argv[0]); 95 | exit(1); 96 | } 97 | 98 | horus_mode = atoi(argv[1]); 99 | fprintf(stderr, "Using Horus Mode %d.\n", horus_mode); 100 | 101 | framecnt = atoi(argv[2]); 102 | fprintf(stderr, "Generating %d frames.\n", framecnt); 103 | 104 | if(horus_mode == 0){ 105 | int nbytes = sizeof(struct TBinaryPacket0); 106 | struct TBinaryPacket0 input_payload; 107 | int num_tx_data_bytes = horus_l2_get_num_tx_data_bytes(nbytes); 108 | unsigned char tx[num_tx_data_bytes]; 109 | 110 | uint16_t counter = 0; 111 | 112 | /* all zeros is nastiest sequence for demod before scrambling */ 113 | while(framecnt > 0){ 114 | memset(&input_payload, 0, nbytes); 115 | input_payload.Counter = counter; 116 | input_payload.Checksum = horus_l2_gen_crc16((unsigned char*)&input_payload, nbytes-2); 117 | 118 | horus_l2_encode_tx_packet(tx, (unsigned char*)&input_payload, nbytes); 119 | 120 | int b; 121 | uint8_t tx_bit; 122 | for(i=0; i> (7-b)) & 0x1; /* msb first */ 125 | fwrite(&tx_bit,sizeof(uint8_t),1,stdout); 126 | fflush(stdout); 127 | } 128 | } 129 | framecnt -= 1; 130 | counter += 1; 131 | } 132 | 133 | } else if(horus_mode == 1){ 134 | int nbytes = sizeof(struct TBinaryPacket1); 135 | struct TBinaryPacket1 input_payload; 136 | int num_tx_data_bytes = horus_l2_get_num_tx_data_bytes(nbytes); 137 | unsigned char tx[num_tx_data_bytes]; 138 | 139 | uint16_t counter = 0; 140 | 141 | /* all zeros is nastiest sequence for demod before scrambling */ 142 | while(framecnt > 0){ 143 | memset(&input_payload, 0, nbytes); 144 | input_payload.PayloadID = 256; 145 | input_payload.Hours = 12; 146 | input_payload.Minutes = 34; 147 | input_payload.Seconds = 56; 148 | input_payload.dummy1 = 1; 149 | input_payload.dummy2 = 1.23456789; 150 | input_payload.dummy3 = 200; 151 | input_payload.dummy4 = 123; 152 | input_payload.dummy5 = 1234; 153 | input_payload.Counter = counter; 154 | input_payload.Checksum = horus_l2_gen_crc16((unsigned char*)&input_payload, nbytes-2); 155 | 156 | horus_l2_encode_tx_packet(tx, (unsigned char*)&input_payload, nbytes); 157 | 158 | int b; 159 | uint8_t tx_bit; 160 | for(i=0; i> (7-b)) & 0x1; /* msb first */ 163 | fwrite(&tx_bit,sizeof(uint8_t),1,stdout); 164 | fflush(stdout); 165 | } 166 | } 167 | framecnt -= 1; 168 | counter += 1; 169 | } 170 | // Leaving this in place unless we ever decide to do an LDPC mode. 171 | // } else if(horus_mode == 2){ 172 | // // 16-Byte LDPC Encoded mode. 173 | // int nbytes = sizeof(struct TBinaryPacket2); 174 | // struct TBinaryPacket2 input_payload; 175 | 176 | // // TODO: Add Calculation of expected number of TX bytes based on LDPC code. 177 | // int num_tx_data_bytes = 4 + H_128_384_23_DATA_BYTES + H_128_384_23_PARITY_BYTES; 178 | // unsigned char tx[num_tx_data_bytes]; 179 | 180 | // /* all zeros is nastiest sequence for demod before scrambling */ 181 | // memset(&input_payload, 0, nbytes); 182 | // input_payload.Checksum = horus_l2_gen_crc16((unsigned char*)&input_payload, nbytes-2); 183 | 184 | 185 | // int ldpc_tx_bytes = ldpc_encode_packet(tx, (unsigned char*)&input_payload, 2); 186 | 187 | // int b; 188 | // uint8_t tx_bit; 189 | // while(framecnt > 0){ 190 | // for(i=0; i> (7-b)) & 0x1; /* msb first */ 193 | // fwrite(&tx_bit,sizeof(uint8_t),1,stdout); 194 | // fflush(stdout); 195 | // } 196 | // } 197 | // framecnt -= 1; 198 | // } 199 | } else { 200 | fprintf(stderr, "Unknown Mode!"); 201 | } 202 | 203 | return 0; 204 | } -------------------------------------------------------------------------------- /src/horus_l2.h: -------------------------------------------------------------------------------- 1 | /*---------------------------------------------------------------------------*\ 2 | 3 | FILE........: horus_l2.h 4 | AUTHOR......: David Rowe 5 | DATE CREATED: Dec 2015 6 | 7 | \*---------------------------------------------------------------------------*/ 8 | 9 | #ifndef __HORUS_L2__ 10 | #define __HORUS_L2__ 11 | 12 | int horus_l2_get_num_tx_data_bytes(int num_payload_data_bytes); 13 | 14 | /* call this first */ 15 | 16 | void horus_l2_init(void); 17 | 18 | /* returns number of output bytes in output_tx_data */ 19 | 20 | int horus_l2_encode_tx_packet(unsigned char *output_tx_data, 21 | unsigned char *input_payload_data, 22 | int num_payload_data_bytes); 23 | 24 | void horus_l2_decode_rx_packet(unsigned char *output_payload_data, 25 | unsigned char *input_rx_data, 26 | int num_payload_data_bytes); 27 | 28 | unsigned short horus_l2_gen_crc16(unsigned char* data_p, unsigned char length); 29 | 30 | // int ldpc_encode_packet(uint8_t *buff_mfsk, uint8_t *FSK, int mode); 31 | 32 | // void soft_unscramble(float *in, float* out, int nbits); 33 | // void soft_deinterleave(float *in, float* out, int mode); 34 | // void horus_ldpc_decode(uint8_t *payload, float *sd, int mode); 35 | 36 | #endif 37 | -------------------------------------------------------------------------------- /src/kiss_fft.h: -------------------------------------------------------------------------------- 1 | #ifndef KISS_FFT_H 2 | #define KISS_FFT_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #ifdef __cplusplus 10 | extern "C" { 11 | #endif 12 | 13 | /* 14 | ATTENTION! 15 | If you would like a : 16 | -- a utility that will handle the caching of fft objects 17 | -- real-only (no imaginary time component ) FFT 18 | -- a multi-dimensional FFT 19 | -- a command-line utility to perform ffts 20 | -- a command-line utility to perform fast-convolution filtering 21 | 22 | Then see kfc.h kiss_fftr.h kiss_fftnd.h fftutil.c kiss_fastfir.c 23 | in the tools/ directory. 24 | */ 25 | 26 | #ifdef USE_SIMD 27 | # include 28 | # define kiss_fft_scalar __m128 29 | #define KISS_FFT_MALLOC(nbytes) _mm_malloc(nbytes,16) 30 | #define KISS_FFT_FREE _mm_free 31 | #else 32 | #define KISS_FFT_MALLOC malloc 33 | #define KISS_FFT_FREE free 34 | #endif 35 | 36 | 37 | #ifdef FIXED_POINT 38 | #include 39 | # if (FIXED_POINT == 32) 40 | # define kiss_fft_scalar int32_t 41 | # else 42 | # define kiss_fft_scalar int16_t 43 | # endif 44 | #else 45 | # ifndef kiss_fft_scalar 46 | /* default is float */ 47 | # define kiss_fft_scalar float 48 | # endif 49 | #endif 50 | 51 | typedef struct { 52 | kiss_fft_scalar r; 53 | kiss_fft_scalar i; 54 | }kiss_fft_cpx; 55 | 56 | typedef struct kiss_fft_state* kiss_fft_cfg; 57 | 58 | /* 59 | * kiss_fft_alloc 60 | * 61 | * Initialize a FFT (or IFFT) algorithm's cfg/state buffer. 62 | * 63 | * typical usage: kiss_fft_cfg mycfg=kiss_fft_alloc(1024,0,NULL,NULL); 64 | * 65 | * The return value from fft_alloc is a cfg buffer used internally 66 | * by the fft routine or NULL. 67 | * 68 | * If lenmem is NULL, then kiss_fft_alloc will allocate a cfg buffer using malloc. 69 | * The returned value should be free()d when done to avoid memory leaks. 70 | * 71 | * The state can be placed in a user supplied buffer 'mem': 72 | * If lenmem is not NULL and mem is not NULL and *lenmem is large enough, 73 | * then the function places the cfg in mem and the size used in *lenmem 74 | * and returns mem. 75 | * 76 | * If lenmem is not NULL and ( mem is NULL or *lenmem is not large enough), 77 | * then the function returns NULL and places the minimum cfg 78 | * buffer size in *lenmem. 79 | * */ 80 | 81 | kiss_fft_cfg kiss_fft_alloc(int nfft,int inverse_fft,void * mem,size_t * lenmem); 82 | 83 | /* 84 | * kiss_fft(cfg,in_out_buf) 85 | * 86 | * Perform an FFT on a complex input buffer. 87 | * for a forward FFT, 88 | * fin should be f[0] , f[1] , ... ,f[nfft-1] 89 | * fout will be F[0] , F[1] , ... ,F[nfft-1] 90 | * Note that each element is complex and can be accessed like 91 | f[k].r and f[k].i 92 | * */ 93 | void kiss_fft(kiss_fft_cfg cfg,const kiss_fft_cpx *fin,kiss_fft_cpx *fout); 94 | 95 | /* 96 | A more generic version of the above function. It reads its input from every Nth sample. 97 | * */ 98 | void kiss_fft_stride(kiss_fft_cfg cfg,const kiss_fft_cpx *fin,kiss_fft_cpx *fout,int fin_stride); 99 | 100 | /* If kiss_fft_alloc allocated a buffer, it is one contiguous 101 | buffer and can be simply free()d when no longer needed*/ 102 | #define kiss_fft_free free 103 | 104 | /* 105 | Cleans up some memory that gets managed internally. Not necessary to call, but it might clean up 106 | your compiler output to call this before you exit. 107 | */ 108 | void kiss_fft_cleanup(void); 109 | 110 | 111 | /* 112 | * Returns the smallest integer k, such that k>=n and k has only "fast" factors (2,3,5) 113 | */ 114 | int kiss_fft_next_fast_size(int n); 115 | 116 | /* for real ffts, we need an even size */ 117 | #define kiss_fftr_next_fast_size_real(n) \ 118 | (kiss_fft_next_fast_size( ((n)+1)>>1)<<1) 119 | 120 | #ifdef __cplusplus 121 | } 122 | #endif 123 | 124 | #endif 125 | -------------------------------------------------------------------------------- /src/kiss_fftr.c: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2003-2004, Mark Borgerding 3 | 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 10 | * Neither the author nor the names of any contributors may be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 13 | */ 14 | 15 | #include "kiss_fftr.h" 16 | #include "_kiss_fft_guts.h" 17 | #include "assert.h" 18 | 19 | struct kiss_fftr_state{ 20 | kiss_fft_cfg substate; 21 | kiss_fft_cpx * tmpbuf; 22 | kiss_fft_cpx * super_twiddles; 23 | #ifdef USE_SIMD 24 | void * pad; 25 | #endif 26 | }; 27 | 28 | kiss_fftr_cfg kiss_fftr_alloc(int nfft,int inverse_fft,void * mem,size_t * lenmem) 29 | { 30 | int i; 31 | kiss_fftr_cfg st = NULL; 32 | size_t subsize, memneeded; 33 | 34 | if (nfft & 1) { 35 | fprintf(stderr,"Real FFT optimization must be even.\n"); 36 | return NULL; 37 | } 38 | nfft >>= 1; 39 | 40 | kiss_fft_alloc (nfft, inverse_fft, NULL, &subsize); 41 | memneeded = sizeof(struct kiss_fftr_state) + subsize + sizeof(kiss_fft_cpx) * ( nfft * 3 / 2); 42 | 43 | if (lenmem == NULL) { 44 | st = (kiss_fftr_cfg) KISS_FFT_MALLOC (memneeded); 45 | } else { 46 | if (*lenmem >= memneeded) 47 | st = (kiss_fftr_cfg) mem; 48 | *lenmem = memneeded; 49 | } 50 | if (!st) 51 | return NULL; 52 | 53 | st->substate = (kiss_fft_cfg) (st + 1); /*just beyond kiss_fftr_state struct */ 54 | st->tmpbuf = (kiss_fft_cpx *) (((char *) st->substate) + subsize); 55 | st->super_twiddles = st->tmpbuf + nfft; 56 | kiss_fft_alloc(nfft, inverse_fft, st->substate, &subsize); 57 | 58 | for (i = 0; i < nfft/2; ++i) { 59 | float phase = 60 | -3.14159265358979323846264338327 * ((float) (i+1) / nfft + .5); 61 | if (inverse_fft) 62 | phase *= -1; 63 | kf_cexp (st->super_twiddles+i,phase); 64 | } 65 | return st; 66 | } 67 | 68 | void kiss_fftr(kiss_fftr_cfg st,const kiss_fft_scalar *timedata,kiss_fft_cpx *freqdata) 69 | { 70 | /* input buffer timedata is stored row-wise */ 71 | int k,ncfft; 72 | kiss_fft_cpx fpnk,fpk,f1k,f2k,tw,tdc; 73 | 74 | assert(st->substate->inverse==0); 75 | 76 | ncfft = st->substate->nfft; 77 | 78 | /*perform the parallel fft of two real signals packed in real,imag*/ 79 | kiss_fft( st->substate , (const kiss_fft_cpx*)timedata, st->tmpbuf ); 80 | /* The real part of the DC element of the frequency spectrum in st->tmpbuf 81 | * contains the sum of the even-numbered elements of the input time sequence 82 | * The imag part is the sum of the odd-numbered elements 83 | * 84 | * The sum of tdc.r and tdc.i is the sum of the input time sequence. 85 | * yielding DC of input time sequence 86 | * The difference of tdc.r - tdc.i is the sum of the input (dot product) [1,-1,1,-1... 87 | * yielding Nyquist bin of input time sequence 88 | */ 89 | 90 | tdc.r = st->tmpbuf[0].r; 91 | tdc.i = st->tmpbuf[0].i; 92 | C_FIXDIV(tdc,2); 93 | CHECK_OVERFLOW_OP(tdc.r ,+, tdc.i); 94 | CHECK_OVERFLOW_OP(tdc.r ,-, tdc.i); 95 | freqdata[0].r = tdc.r + tdc.i; 96 | freqdata[ncfft].r = tdc.r - tdc.i; 97 | #ifdef USE_SIMD 98 | freqdata[ncfft].i = freqdata[0].i = _mm_set1_ps(0); 99 | #else 100 | freqdata[ncfft].i = freqdata[0].i = 0; 101 | #endif 102 | 103 | for ( k=1;k <= ncfft/2 ; ++k ) { 104 | fpk = st->tmpbuf[k]; 105 | fpnk.r = st->tmpbuf[ncfft-k].r; 106 | fpnk.i = - st->tmpbuf[ncfft-k].i; 107 | C_FIXDIV(fpk,2); 108 | C_FIXDIV(fpnk,2); 109 | 110 | C_ADD( f1k, fpk , fpnk ); 111 | C_SUB( f2k, fpk , fpnk ); 112 | C_MUL( tw , f2k , st->super_twiddles[k-1]); 113 | 114 | freqdata[k].r = HALF_OF(f1k.r + tw.r); 115 | freqdata[k].i = HALF_OF(f1k.i + tw.i); 116 | freqdata[ncfft-k].r = HALF_OF(f1k.r - tw.r); 117 | freqdata[ncfft-k].i = HALF_OF(tw.i - f1k.i); 118 | } 119 | } 120 | 121 | void kiss_fftri(kiss_fftr_cfg st,const kiss_fft_cpx *freqdata,kiss_fft_scalar *timedata) 122 | { 123 | /* input buffer timedata is stored row-wise */ 124 | int k, ncfft; 125 | 126 | assert(st->substate->inverse == 1); 127 | 128 | ncfft = st->substate->nfft; 129 | 130 | st->tmpbuf[0].r = freqdata[0].r + freqdata[ncfft].r; 131 | st->tmpbuf[0].i = freqdata[0].r - freqdata[ncfft].r; 132 | C_FIXDIV(st->tmpbuf[0],2); 133 | 134 | for (k = 1; k <= ncfft / 2; ++k) { 135 | kiss_fft_cpx fk, fnkc, fek, fok, tmp; 136 | fk = freqdata[k]; 137 | fnkc.r = freqdata[ncfft - k].r; 138 | fnkc.i = -freqdata[ncfft - k].i; 139 | C_FIXDIV( fk , 2 ); 140 | C_FIXDIV( fnkc , 2 ); 141 | 142 | C_ADD (fek, fk, fnkc); 143 | C_SUB (tmp, fk, fnkc); 144 | C_MUL (fok, tmp, st->super_twiddles[k-1]); 145 | C_ADD (st->tmpbuf[k], fek, fok); 146 | C_SUB (st->tmpbuf[ncfft - k], fek, fok); 147 | #ifdef USE_SIMD 148 | st->tmpbuf[ncfft - k].i *= _mm_set1_ps(-1.0); 149 | #else 150 | st->tmpbuf[ncfft - k].i *= -1; 151 | #endif 152 | } 153 | kiss_fft (st->substate, st->tmpbuf, (kiss_fft_cpx *) timedata); 154 | } 155 | -------------------------------------------------------------------------------- /src/kiss_fftr.h: -------------------------------------------------------------------------------- 1 | #ifndef KISS_FTR_H 2 | #define KISS_FTR_H 3 | 4 | #include "kiss_fft.h" 5 | #ifdef __cplusplus 6 | extern "C" { 7 | #endif 8 | 9 | 10 | /* 11 | 12 | Real optimized version can save about 45% cpu time vs. complex fft of a real seq. 13 | 14 | 15 | 16 | */ 17 | 18 | typedef struct kiss_fftr_state *kiss_fftr_cfg; 19 | 20 | 21 | kiss_fftr_cfg kiss_fftr_alloc(int nfft,int inverse_fft,void * mem, size_t * lenmem); 22 | /* 23 | nfft must be even 24 | 25 | If you don't care to allocate space, use mem = lenmem = NULL 26 | */ 27 | 28 | 29 | void kiss_fftr(kiss_fftr_cfg cfg,const kiss_fft_scalar *timedata,kiss_fft_cpx *freqdata); 30 | /* 31 | input timedata has nfft scalar points 32 | output freqdata has nfft/2+1 complex points 33 | */ 34 | 35 | void kiss_fftri(kiss_fftr_cfg cfg,const kiss_fft_cpx *freqdata,kiss_fft_scalar *timedata); 36 | /* 37 | input freqdata has nfft/2+1 complex points 38 | output timedata has nfft scalar points 39 | */ 40 | 41 | #define kiss_fftr_free free 42 | 43 | #ifdef __cplusplus 44 | } 45 | #endif 46 | #endif 47 | -------------------------------------------------------------------------------- /src/ldpc.c: -------------------------------------------------------------------------------- 1 | /* ldpc interface to decoder 2 | * 3 | * It is expected that the switch to ldpc will give a 60% speed improvement 4 | * over golay code, with no loss of performance over white noise - the use of 5 | * soft-bit detection and longer codewords compensating for the expected 2dB loss 6 | * from reducing the number of parity bits. 7 | * 8 | * Golay code can reliably correct a 10% BER, equivalent to a 20% loss of signal 9 | * during deep fading. It is not clear how well ldpc will cope with deep fading, 10 | * but the shorter packers are bound to be more badly affected. 11 | */ 12 | 13 | 14 | #include 15 | #include "math.h" 16 | #include "string.h" 17 | #include "mpdecode_core.h" 18 | #include "horus_l2.h" 19 | #include "H_128_384_23.h" 20 | #include "H_256_768_22.h" 21 | 22 | 23 | #define MAX_ITER 20 24 | 25 | /* Scramble and interleave are 8bit lsb, but bitstream is sent msb */ 26 | #define LSB2MSB(X) (X + 7 - 2 * (X & 7) ) 27 | 28 | /* Invert bits - ldpc expects negative floats for high hits */ 29 | void soft_unscramble(float *in, float* out, int nbits) { 30 | int i, ibit; 31 | uint16_t scrambler = 0x4a80; /* init additive scrambler at start of every frame */ 32 | uint16_t scrambler_out; 33 | 34 | for ( i = 0; i < nbits; i++ ) { 35 | scrambler_out = ( (scrambler >> 1) ^ scrambler) & 0x1; 36 | 37 | /* modify i-th bit by xor-ing with scrambler output sequence */ 38 | ibit = LSB2MSB(i); 39 | if ( scrambler_out ) { 40 | out[ibit] = in[ibit]; 41 | } else { 42 | out[ibit] = -in[ibit]; 43 | } 44 | 45 | scrambler >>= 1; 46 | scrambler |= scrambler_out << 14; 47 | } 48 | } 49 | 50 | // soft bit deinterleave 51 | void soft_deinterleave(float *in, float* out, int mode) { 52 | int n, i, j, bits_per_packet, coprime; 53 | 54 | if (mode == 1) { 55 | // 256_768 56 | bits_per_packet = H_256_768_22_BITS_PER_PACKET; 57 | coprime = H_256_768_22_COPRIME; 58 | } else { 59 | bits_per_packet = H_128_384_23_BITS_PER_PACKET; 60 | coprime = H_128_384_23_COPRIME; 61 | } 62 | 63 | 64 | for ( n = 0; n < bits_per_packet; n++ ) { 65 | i = LSB2MSB(n); 66 | j = LSB2MSB( (coprime * n) % bits_per_packet); 67 | out[i] = in[j]; 68 | } 69 | } 70 | 71 | // // packed bit deinterleave - same as Golay version , but different Coprime 72 | // void bitwise_deinterleave(uint8_t *inout, int nbytes) 73 | // { 74 | // uint16_t nbits = (uint16_t)nbytes*8; 75 | // uint32_t i, j, ibit, ibyte, ishift, jbyte, jshift; 76 | // uint8_t out[nbytes]; 77 | 78 | // memset(out, 0, nbytes); 79 | // for(j = 0; j < nbits; j++) { 80 | // i = (COPRIME * j) % nbits; 81 | 82 | // /* read bit i */ 83 | // ibyte = i>>3; 84 | // ishift = i&7; 85 | // ibit = (inout[ibyte] >> ishift) & 0x1; 86 | 87 | // /* write bit i to bit j position */ 88 | // jbyte = j>>3; 89 | // jshift = j&7; 90 | // out[jbyte] |= ibit << jshift; 91 | // } 92 | 93 | // memcpy(inout, out, nbytes); 94 | // } 95 | 96 | // /* Compare detected bits to corrected bits */ 97 | // void ldpc_errors( const uint8_t *outbytes, uint8_t *rx_bytes ) { 98 | // int length = DATA_BYTES + PARITY_BYTES; 99 | // uint8_t temp[length]; 100 | // int i, percentage, count = 0; 101 | // memcpy(temp, rx_bytes, length); 102 | 103 | // scramble(temp, length); // use scrambler from Golay code 104 | // bitwise_deinterleave(temp, length); 105 | 106 | // // count bits changed during error correction 107 | // for(i = 0; i < BITS_PER_PACKET; i++) { 108 | // int x, y, offset, shift; 109 | 110 | // shift = i & 7; 111 | // offset = i >> 3; 112 | // x = temp[offset] >> shift; 113 | // y = outbytes[offset] >> shift; 114 | // count += (x ^ y) & 1; 115 | // } 116 | 117 | // // scale errors against a maximum of 20% BER 118 | // percentage = (count * 5 * 100) / BITS_PER_PACKET; 119 | // if (percentage > 100) 120 | // percentage = 100; 121 | // set_error_count( percentage ); 122 | // } 123 | 124 | /* LDPC decode */ 125 | void horus_ldpc_decode(uint8_t *payload, float *sd, int mode) { 126 | float sum, mean, sumsq, estEsN0, x; 127 | int bits_per_packet; 128 | 129 | if(mode == 1){ 130 | bits_per_packet = H_256_768_22_BITS_PER_PACKET; 131 | } else { 132 | bits_per_packet = H_128_384_23_BITS_PER_PACKET; 133 | } 134 | 135 | float llr[bits_per_packet]; 136 | float temp[bits_per_packet]; 137 | uint8_t outbits[bits_per_packet]; 138 | 139 | int b, i, parityCC; 140 | struct LDPC ldpc; 141 | 142 | /* normalise bitstream to log-like */ 143 | sum = 0.0; 144 | for ( i = 0; i < bits_per_packet; i++ ) 145 | sum += fabs(sd[i]); 146 | mean = sum / bits_per_packet; 147 | 148 | sumsq = 0.0; 149 | for ( i = 0; i < bits_per_packet; i++ ) { 150 | x = fabs(sd[i]) / mean - 1.0; 151 | sumsq += x * x; 152 | } 153 | estEsN0 = 2.0 * bits_per_packet / (sumsq + 1.0e-3) / mean; 154 | for ( i = 0; i < bits_per_packet; i++ ) 155 | llr[i] = estEsN0 * sd[i]; 156 | 157 | /* reverse whitening and re-order bits */ 158 | soft_unscramble(llr, temp, bits_per_packet); 159 | soft_deinterleave(temp, llr, mode); 160 | 161 | /* correct errors */ 162 | if (mode == 1){ 163 | // 32-byte mode H_256_768_22 164 | ldpc.max_iter = H_256_768_22_MAX_ITER; 165 | ldpc.dec_type = 0; 166 | ldpc.q_scale_factor = 1; 167 | ldpc.r_scale_factor = 1; 168 | ldpc.CodeLength = H_256_768_22_CODELENGTH; 169 | ldpc.NumberParityBits = H_256_768_22_NUMBERPARITYBITS; 170 | ldpc.NumberRowsHcols = H_256_768_22_NUMBERROWSHCOLS; 171 | ldpc.max_row_weight = H_256_768_22_MAX_ROW_WEIGHT; 172 | ldpc.max_col_weight = H_256_768_22_MAX_COL_WEIGHT; 173 | ldpc.H_rows = (uint16_t *)H_256_768_22_H_rows; 174 | ldpc.H_cols = (uint16_t *)H_256_768_22_H_cols; 175 | } else { 176 | // 16-byte mode 177 | ldpc.max_iter = H_128_384_23_MAX_ITER; 178 | ldpc.dec_type = 0; 179 | ldpc.q_scale_factor = 1; 180 | ldpc.r_scale_factor = 1; 181 | ldpc.CodeLength = H_128_384_23_CODELENGTH; 182 | ldpc.NumberParityBits = H_128_384_23_NUMBERPARITYBITS; 183 | ldpc.NumberRowsHcols = H_128_384_23_NUMBERROWSHCOLS; 184 | ldpc.max_row_weight = H_128_384_23_MAX_ROW_WEIGHT; 185 | ldpc.max_col_weight = H_128_384_23_MAX_COL_WEIGHT; 186 | ldpc.H_rows = (uint16_t *)H_128_384_23_H_rows; 187 | ldpc.H_cols = (uint16_t *)H_128_384_23_H_cols; 188 | } 189 | 190 | i = run_ldpc_decoder(&ldpc, outbits, llr, &parityCC); 191 | 192 | /* convert MSB bits to a packet of bytes */ 193 | for (b = 0; b < (bits_per_packet/8); b++) { 194 | uint8_t rxbyte = 0; 195 | for(i=0; i<8; i++) 196 | rxbyte |= outbits[b*8+i] << (7 - i); 197 | payload[b] = rxbyte; 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/modem_probe.c: -------------------------------------------------------------------------------- 1 | /*---------------------------------------------------------------------------*\ 2 | 3 | FILE........: modem_probe.c 4 | AUTHOR......: Brady O'Brien 5 | DATE CREATED: 9 January 2016 6 | 7 | Library to easily extract debug traces from modems during development and 8 | verification 9 | 10 | \*---------------------------------------------------------------------------*/ 11 | 12 | /* 13 | Copyright (C) 2016 David Rowe 14 | 15 | All rights reserved. 16 | 17 | This program is free software; you can redistribute it and/or modify 18 | it under the terms of the GNU Lesser General Public License version 2.1, as 19 | published by the Free Software Foundation. This program is 20 | distributed in the hope that it will be useful, but WITHOUT ANY 21 | WARRANTY; without even the implied warranty of MERCHANTABILITY or 22 | FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public 23 | License for more details. 24 | 25 | You should have received a copy of the GNU Lesser General Public License 26 | along with this program; if not, see . 27 | */ 28 | 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include "comp.h" 34 | #include "octave.h" 35 | 36 | #define TRACE_I 1 37 | #define TRACE_F 2 38 | #define TRACE_C 3 39 | 40 | 41 | typedef struct probe_trace_info_s probe_trace_info; 42 | typedef struct datlink_s datlink; 43 | 44 | struct datlink_s{ 45 | void * data; 46 | size_t len; 47 | datlink * next; 48 | }; 49 | 50 | struct probe_trace_info_s{ 51 | int type; 52 | char name[255]; 53 | datlink * data; 54 | datlink * last; 55 | probe_trace_info *next; 56 | }; 57 | 58 | static char *run = NULL; 59 | static char *mod = NULL; 60 | static probe_trace_info *first_trace = NULL; 61 | 62 | /* Init the probing library */ 63 | void modem_probe_init_int(char *modname, char *runname){ 64 | mod = malloc((strlen(modname)+1)*sizeof(char)); 65 | run = malloc((strlen(runname)+1)*sizeof(char)); 66 | strcpy(run,runname); 67 | strcpy(mod,modname); 68 | } 69 | 70 | /* 71 | * Gather the data stored in the linked list into a single blob, 72 | * freeing links and buffers as it goes 73 | */ 74 | void * gather_data(datlink * d,size_t * len){ 75 | size_t size = 0; 76 | datlink * cur = d; 77 | datlink * next; 78 | while(cur!=NULL){ 79 | size += d->len; 80 | cur = cur->next; 81 | } 82 | cur = d; 83 | size_t i = 0; 84 | void * newbuf = malloc(size); 85 | 86 | while(cur!=NULL){ 87 | memcpy(newbuf+i,cur->data,cur->len); 88 | i += cur->len; 89 | free(cur->data); 90 | next = cur->next; 91 | free(cur); 92 | cur = next; 93 | } 94 | *len = size; 95 | return newbuf; 96 | } 97 | 98 | /* Dump all of the traces into a nice octave-able dump file */ 99 | void modem_probe_close_int(){ 100 | if(run==NULL) 101 | return; 102 | 103 | probe_trace_info *cur,*next; 104 | cur = first_trace; 105 | FILE * dumpfile = fopen(run,"w"); 106 | void * dbuf; 107 | size_t len; 108 | 109 | while(cur != NULL){ 110 | dbuf = gather_data(cur->data,&len); 111 | switch(cur->type){ 112 | case TRACE_I: 113 | octave_save_int(dumpfile,cur->name,(int32_t*)dbuf,1,len/sizeof(int32_t)); 114 | break; 115 | case TRACE_F: 116 | octave_save_float(dumpfile,cur->name,(float*)dbuf,1,len/sizeof(float),10); 117 | break; 118 | case TRACE_C: 119 | octave_save_complex(dumpfile,cur->name,(COMP*)dbuf,1,len/sizeof(COMP),10); 120 | break; 121 | } 122 | next = cur->next; 123 | free(cur); 124 | free(dbuf); 125 | cur = next; 126 | } 127 | 128 | fclose(dumpfile); 129 | free(run); 130 | free(mod); 131 | } 132 | 133 | /* Look up or create a trace by name */ 134 | probe_trace_info * modem_probe_get_trace(char * tracename){ 135 | probe_trace_info *cur,*npti; 136 | 137 | /* Make sure probe session is open */ 138 | if(run==NULL) 139 | return NULL; 140 | 141 | cur = first_trace; 142 | /* Walk through list, find trace with matching name */ 143 | while(cur != NULL){ 144 | /* We got one! */ 145 | if(strcmp( cur->name, tracename) == 0){ 146 | return cur; 147 | } 148 | cur = cur->next; 149 | } 150 | /* None found, open a new trace */ 151 | 152 | npti = (probe_trace_info *) malloc(sizeof(probe_trace_info)); 153 | npti->next = first_trace; 154 | npti->data = NULL; 155 | npti->last = NULL; 156 | strcpy(npti->name,tracename); 157 | first_trace = npti; 158 | 159 | return npti; 160 | 161 | } 162 | 163 | 164 | void modem_probe_samp_i_int(char * tracename,int32_t samp[],size_t cnt){ 165 | probe_trace_info *pti; 166 | datlink *ndat; 167 | 168 | pti = modem_probe_get_trace(tracename); 169 | if(pti == NULL) 170 | return; 171 | 172 | pti->type = TRACE_I; 173 | 174 | ndat = (datlink*) malloc(sizeof(datlink)); 175 | ndat->data = malloc(sizeof(int32_t)*cnt); 176 | 177 | ndat->len = cnt*sizeof(int32_t); 178 | ndat->next = NULL; 179 | memcpy(ndat->data,(void*)&(samp[0]),sizeof(int32_t)*cnt); 180 | 181 | if(pti->last!=NULL){ 182 | pti->last->next = ndat; 183 | pti->last = ndat; 184 | } else { 185 | pti->data = ndat; 186 | pti->last = ndat; 187 | } 188 | 189 | } 190 | 191 | void modem_probe_samp_f_int(char * tracename,float samp[],size_t cnt){ 192 | probe_trace_info *pti; 193 | datlink *ndat; 194 | 195 | pti = modem_probe_get_trace(tracename); 196 | if(pti == NULL) 197 | return; 198 | 199 | pti->type = TRACE_F; 200 | 201 | ndat = (datlink*) malloc(sizeof(datlink)); 202 | ndat->data = malloc(sizeof(float)*cnt); 203 | 204 | ndat->len = cnt*sizeof(float); 205 | ndat->next = NULL; 206 | memcpy(ndat->data,(void*)&(samp[0]),sizeof(float)*cnt); 207 | 208 | if(pti->last!=NULL){ 209 | pti->last->next = ndat; 210 | pti->last = ndat; 211 | } else { 212 | pti->data = ndat; 213 | pti->last = ndat; 214 | } 215 | } 216 | 217 | void modem_probe_samp_c_int(char * tracename,COMP samp[],size_t cnt){ 218 | probe_trace_info *pti; 219 | datlink *ndat; 220 | 221 | pti = modem_probe_get_trace(tracename); 222 | if(pti == NULL) 223 | return; 224 | 225 | pti->type = TRACE_C; 226 | 227 | ndat = (datlink*) malloc(sizeof(datlink)); 228 | ndat->data = malloc(sizeof(COMP)*cnt); 229 | 230 | ndat->len = cnt*sizeof(COMP); 231 | ndat->next = NULL; 232 | memcpy(ndat->data,(void*)&(samp[0]),sizeof(COMP)*cnt); 233 | 234 | if(pti->last!=NULL){ 235 | pti->last->next = ndat; 236 | pti->last = ndat; 237 | } else { 238 | pti->data = ndat; 239 | pti->last = ndat; 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /src/modem_probe.h: -------------------------------------------------------------------------------- 1 | /*---------------------------------------------------------------------------*\ 2 | 3 | FILE........: modem_probe.h 4 | AUTHOR......: Brady O'Brien 5 | DATE CREATED: 9 January 2016 6 | 7 | Library to easily extract debug traces from modems during development 8 | 9 | \*---------------------------------------------------------------------------*/ 10 | 11 | /* 12 | Copyright (C) 2016 David Rowe 13 | 14 | All rights reserved. 15 | 16 | This program is free software; you can redistribute it and/or modify 17 | it under the terms of the GNU Lesser General Public License version 2.1, as 18 | published by the Free Software Foundation. This program is 19 | distributed in the hope that it will be useful, but WITHOUT ANY 20 | WARRANTY; without even the implied warranty of MERCHANTABILITY or 21 | FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public 22 | License for more details. 23 | 24 | You should have received a copy of the GNU Lesser General Public License 25 | along with this program; if not, see . 26 | */ 27 | 28 | #ifndef __MODEMPROBE_H 29 | #define __MODEMPROBE_H 30 | 31 | #include 32 | #include 33 | #include 34 | #include "comp.h" 35 | 36 | #ifdef MODEMPROBE_ENABLE 37 | 38 | /* Internal functions */ 39 | void modem_probe_init_int(char *modname, char *runname); 40 | void modem_probe_close_int(); 41 | 42 | void modem_probe_samp_i_int(char * tracename,int samp[],size_t cnt); 43 | void modem_probe_samp_f_int(char * tracename,float samp[],size_t cnt); 44 | void modem_probe_samp_c_int(char * tracename,COMP samp[],size_t cnt); 45 | 46 | /* 47 | * Init the probe library. 48 | * char *modname - Name of the modem under test 49 | * char *runname - Name/path of the file data is dumped to 50 | */ 51 | static inline void modem_probe_init(char *modname,char *runname){ 52 | modem_probe_init_int(modname,runname); 53 | } 54 | 55 | /* 56 | * Dump traces to a file and clean up 57 | */ 58 | static inline void modem_probe_close(){ 59 | modem_probe_close_int(); 60 | } 61 | 62 | /* 63 | * Save some number of int samples to a named trace 64 | * char *tracename - name of trace being saved to 65 | * int samp[] - int samples 66 | * size_t cnt - how many samples to save 67 | */ 68 | static inline void modem_probe_samp_i(char *tracename,int samp[],size_t cnt){ 69 | modem_probe_samp_i_int(tracename,samp,cnt); 70 | } 71 | 72 | /* 73 | * Save some number of float samples to a named trace 74 | * char *tracename - name of trace being saved to 75 | * float samp[] - int samples 76 | * size_t cnt - how many samples to save 77 | */ 78 | static inline void modem_probe_samp_f(char *tracename,float samp[],size_t cnt){ 79 | modem_probe_samp_f_int(tracename,samp,cnt); 80 | } 81 | 82 | /* 83 | * Save some number of complex samples to a named trace 84 | * char *tracename - name of trace being saved to 85 | * COMP samp[] - int samples 86 | * size_t cnt - how many samples to save 87 | */ 88 | static inline void modem_probe_samp_c(char *tracename,COMP samp[],size_t cnt){ 89 | modem_probe_samp_c_int(tracename,samp,cnt); 90 | } 91 | 92 | /* 93 | * Save some number of complex samples to a named trace 94 | * char *tracename - name of trace being saved to 95 | * float complex samp[] - int samples 96 | * size_t cnt - how many samples to save 97 | */ 98 | static inline void modem_probe_samp_cft(char *tracename,complex float samp[],size_t cnt){ 99 | modem_probe_samp_c_int(tracename,(COMP*)samp,cnt); 100 | } 101 | 102 | #else 103 | 104 | static inline void modem_probe_init(char *modname,char *runname){ 105 | return; 106 | } 107 | 108 | static inline void modem_probe_close(){ 109 | return; 110 | } 111 | 112 | static inline void modem_probe_samp_i(char *name,int samp[],size_t sampcnt){ 113 | return; 114 | } 115 | 116 | static inline void modem_probe_samp_f(char *name,float samp[],size_t cnt){ 117 | return; 118 | } 119 | 120 | static inline void modem_probe_samp_c(char *name,COMP samp[],size_t cnt){ 121 | return; 122 | } 123 | 124 | static inline void modem_probe_samp_cft(char *name,complex float samp[],size_t cnt){ 125 | return; 126 | } 127 | 128 | #endif 129 | 130 | #endif 131 | -------------------------------------------------------------------------------- /src/modem_stats.c: -------------------------------------------------------------------------------- 1 | /*---------------------------------------------------------------------------*\ 2 | 3 | FILE........: modem_stats.c 4 | AUTHOR......: David Rowe 5 | DATE CREATED: June 2015 6 | 7 | Common functions for returning demod stats from fdmdv and cohpsk modems. 8 | 9 | \*---------------------------------------------------------------------------*/ 10 | 11 | /* 12 | Copyright (C) 2015 David Rowe 13 | 14 | All rights reserved. 15 | 16 | This program is free software; you can redistribute it and/or modify 17 | it under the terms of the GNU Lesser General Public License version 2.1, as 18 | published by the Free Software Foundation. This program is 19 | distributed in the hope that it will be useful, but WITHOUT ANY 20 | WARRANTY; without even the implied warranty of MERCHANTABILITY or 21 | FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public 22 | License for more details. 23 | 24 | You should have received a copy of the GNU Lesser General Public License 25 | along with this program; if not, see . 26 | */ 27 | 28 | #include 29 | #include 30 | #include "modem_stats.h" 31 | #include "codec2_fdmdv.h" 32 | 33 | void modem_stats_open(struct MODEM_STATS *f) 34 | { 35 | int i; 36 | 37 | /* zero out all the stats */ 38 | 39 | memset(f, 0, sizeof(struct MODEM_STATS)); 40 | 41 | /* init the FFT */ 42 | 43 | #ifndef __EMBEDDED__ 44 | for(i=0; i<2*MODEM_STATS_NSPEC; i++) 45 | f->fft_buf[i] = 0.0; 46 | f->fft_cfg = kiss_fft_alloc (2*MODEM_STATS_NSPEC, 0, NULL, NULL); 47 | assert(f->fft_cfg != NULL); 48 | #endif 49 | } 50 | 51 | void modem_stats_close(struct MODEM_STATS *f) 52 | { 53 | #ifndef __EMBEDDED__ 54 | KISS_FFT_FREE(f->fft_cfg); 55 | #endif 56 | } 57 | 58 | /*---------------------------------------------------------------------------*\ 59 | 60 | FUNCTION....: modem_stats_get_rx_spectrum() 61 | AUTHOR......: David Rowe 62 | DATE CREATED: 9 June 2012 63 | 64 | Returns the MODEM_STATS_NSPEC point magnitude spectrum of the rx signal in 65 | dB. The spectral samples are scaled so that 0dB is the peak, a good 66 | range for plotting is 0 to -40dB. 67 | 68 | Note only the real part of the complex input signal is used at 69 | present. A complex variable is used for input for compatability 70 | with the other rx signal procesing. 71 | 72 | Successive calls can be used to build up a waterfall or spectrogram 73 | plot, by mapping the received levels to colours. 74 | 75 | The time-frequency resolution of the spectrum can be adjusted by varying 76 | MODEM_STATS_NSPEC. Note that a 2* MODEM_STATS_NSPEC size FFT is reqd to get 77 | MODEM_STATS_NSPEC output points. MODEM_STATS_NSPEC must be a power of 2. 78 | 79 | See octave/tget_spec.m for a demo real time spectral display using 80 | Octave. This demo averages the output over time to get a smoother 81 | display: 82 | 83 | av = 0.9*av + 0.1*mag_dB 84 | 85 | \*---------------------------------------------------------------------------*/ 86 | 87 | #ifndef __EMBEDDED__ 88 | void modem_stats_get_rx_spectrum(struct MODEM_STATS *f, float mag_spec_dB[], COMP rx_fdm[], int nin) 89 | { 90 | int i,j; 91 | COMP fft_in[2*MODEM_STATS_NSPEC]; 92 | COMP fft_out[2*MODEM_STATS_NSPEC]; 93 | float full_scale_dB; 94 | 95 | /* update buffer of input samples */ 96 | 97 | for(i=0; i<2*MODEM_STATS_NSPEC-nin; i++) 98 | f->fft_buf[i] = f->fft_buf[i+nin]; 99 | for(j=0; jfft_buf[i] = rx_fdm[j].real; 101 | assert(i == 2*MODEM_STATS_NSPEC); 102 | 103 | /* window and FFT */ 104 | 105 | for(i=0; i<2*MODEM_STATS_NSPEC; i++) { 106 | fft_in[i].real = f->fft_buf[i] * (0.5 - 0.5*cosf((float)i*2.0*M_PI/(2*MODEM_STATS_NSPEC))); 107 | fft_in[i].imag = 0.0; 108 | } 109 | 110 | kiss_fft(f->fft_cfg, (kiss_fft_cpx *)fft_in, (kiss_fft_cpx *)fft_out); 111 | 112 | /* FFT scales up a signal of level 1 FDMDV_NSPEC */ 113 | 114 | full_scale_dB = 20*log10(MODEM_STATS_NSPEC*FDMDV_SCALE); 115 | 116 | /* scale and convert to dB */ 117 | 118 | for(i=0; i. 26 | */ 27 | 28 | #ifndef __MODEM_STATS__ 29 | #define __MODEM_STATS__ 30 | 31 | #include "comp.h" 32 | #include "kiss_fft.h" 33 | 34 | #ifdef __cplusplus 35 | extern "C" { 36 | #endif 37 | 38 | #define MODEM_STATS_NC_MAX 50 39 | #define MODEM_STATS_NR_MAX 8 40 | #define MODEM_STATS_ET_MAX 8 41 | #define MODEM_STATS_EYE_IND_MAX 160 42 | #define MODEM_STATS_NSPEC 512 43 | #define MODEM_STATS_MAX_F_HZ 4000 44 | #define MODEM_STATS_MAX_F_EST 4 45 | 46 | struct MODEM_STATS { 47 | int Nc; 48 | float snr_est; /* estimated SNR of rx signal in dB (3 kHz noise BW) */ 49 | #ifndef __EMBEDDED__ 50 | COMP rx_symbols[MODEM_STATS_NR_MAX][MODEM_STATS_NC_MAX+1]; 51 | /* latest received symbols, for scatter plot */ 52 | #endif 53 | int nr; /* number of rows in rx_symbols */ 54 | int sync; /* demod sync state */ 55 | float foff; /* estimated freq offset in Hz */ 56 | float rx_timing; /* estimated optimum timing offset in samples */ 57 | float clock_offset; /* Estimated tx/rx sample clock offset in ppm */ 58 | float sync_metric; /* number between 0 and 1 indicating quality of sync */ 59 | 60 | /* eye diagram traces */ 61 | /* Eye diagram plot -- first dim is trace number, second is the trace idx */ 62 | #ifndef __EMBEDDED__ 63 | float rx_eye[MODEM_STATS_ET_MAX][MODEM_STATS_EYE_IND_MAX]; 64 | int neyetr; /* How many eye traces are plotted */ 65 | int neyesamp; /* How many samples in the eye diagram */ 66 | 67 | /* optional for FSK modems - est tone freqs */ 68 | 69 | float f_est[MODEM_STATS_MAX_F_EST]; 70 | #endif 71 | 72 | /* Buf for FFT/waterfall */ 73 | 74 | #ifndef __EMBEDDED__ 75 | float fft_buf[2*MODEM_STATS_NSPEC]; 76 | kiss_fft_cfg fft_cfg; 77 | #endif 78 | }; 79 | 80 | void modem_stats_open(struct MODEM_STATS *f); 81 | void modem_stats_close(struct MODEM_STATS *f); 82 | void modem_stats_get_rx_spectrum(struct MODEM_STATS *f, float mag_spec_dB[], COMP rx_fdm[], int nin); 83 | 84 | #ifdef __cplusplus 85 | } 86 | #endif 87 | 88 | #endif 89 | 90 | -------------------------------------------------------------------------------- /src/mpdecode_core.h: -------------------------------------------------------------------------------- 1 | /* 2 | FILE...: mpdecode_core.h 3 | AUTHOR.: David Rowe 4 | CREATED: Sep 2016 5 | 6 | C-callable core functions for MpDecode, so they can be used for 7 | Octave and C programs. Also some convenience functions to help use 8 | the C-callable LDPC decoder in C programs. 9 | */ 10 | 11 | #ifndef __MPDECODE_CORE__ 12 | #define __MPDECODE_CORE__ 13 | 14 | #include 15 | 16 | #include "comp.h" 17 | 18 | struct LDPC { 19 | int max_iter; 20 | int dec_type; 21 | int q_scale_factor; 22 | int r_scale_factor; 23 | int CodeLength; 24 | int NumberParityBits; 25 | int NumberRowsHcols; 26 | int max_row_weight; 27 | int max_col_weight; 28 | 29 | /* these two are fixed to code params */ 30 | int ldpc_data_bits_per_frame; 31 | int ldpc_coded_bits_per_frame; 32 | 33 | /* these three may vary if we don't use all data bits in code */ 34 | int data_bits_per_frame; 35 | int coded_bits_per_frame; 36 | int coded_syms_per_frame; 37 | 38 | uint16_t *H_rows; 39 | uint16_t *H_cols; 40 | }; 41 | 42 | void encode(struct LDPC *ldpc, unsigned char ibits[], unsigned char pbits[]); 43 | 44 | int run_ldpc_decoder(struct LDPC *ldpc, uint8_t out_char[], float input[], int *parityCheckCount); 45 | 46 | void sd_to_llr(float llr[], double sd[], int n); 47 | void Demod2D(float symbol_likelihood[], COMP r[], COMP S_matrix[], float EsNo, float fading[], float mean_amp, int number_symbols); 48 | void Somap(float bit_likelihood[], float symbol_likelihood[], int number_symbols); 49 | void symbols_to_llrs(float llr[], COMP rx_qpsk_symbols[], float rx_amps[], float EsNo, float mean_amp, int nsyms); 50 | 51 | void ldpc_print_info(struct LDPC *ldpc); 52 | 53 | 54 | #endif 55 | -------------------------------------------------------------------------------- /src/mpdecode_core_test.h: -------------------------------------------------------------------------------- 1 | /* 2 | FILE...: mpdecode_core.h 3 | AUTHOR.: David Rowe 4 | CREATED: Sep 2016 5 | 6 | C-callable core functions for MpDecode, so they can be used for 7 | Octave and C programs. Also some convenience functions to help use 8 | the C-callable LDPC decoder in C programs. 9 | */ 10 | 11 | #ifndef __MPDECODE_CORE__ 12 | #define __MPDECODE_CORE__ 13 | 14 | #include 15 | 16 | #include "comp.h" 17 | 18 | struct LDPC { 19 | int max_iter; 20 | int dec_type; 21 | int q_scale_factor; 22 | int r_scale_factor; 23 | int CodeLength; 24 | int NumberParityBits; 25 | int NumberRowsHcols; 26 | int max_row_weight; 27 | int max_col_weight; 28 | int data_bits_per_frame; 29 | int coded_bits_per_frame; 30 | int coded_syms_per_frame; 31 | uint16_t *H_rows; 32 | uint16_t *H_cols; 33 | }; 34 | 35 | extern void ldpc_init(struct LDPC *ldpc, int *size_common); 36 | extern void ldpc_free_mem(struct LDPC *ldpc); 37 | 38 | extern void encode(struct LDPC *ldpc, unsigned char ibits[], unsigned char pbits[]); 39 | 40 | int run_ldpc_decoder(struct LDPC *ldpc, uint8_t out_char[], float input[], int *parityCheckCount); 41 | extern void ldpc_dump_nodes(struct LDPC *ldpc); 42 | 43 | extern void sd_to_llr(float llr[], double sd[], int n); 44 | 45 | extern void Demod2D(float symbol_likelihood[], COMP r[], COMP S_matrix[], float EsNo, float fading[], float mean_amp, int number_symbols); 46 | extern void Somap(float bit_likelihood[], float symbol_likelihood[], int number_symbols); 47 | extern void symbols_to_llrs(float llr[], COMP rx_qpsk_symbols[], float rx_amps[], float EsNo, float mean_amp, int nsyms); 48 | 49 | #endif 50 | -------------------------------------------------------------------------------- /src/octave.c: -------------------------------------------------------------------------------- 1 | /*---------------------------------------------------------------------------*\ 2 | 3 | FILE........: octave.c 4 | AUTHOR......: David Rowe 5 | DATE CREATED: April 28 2012 6 | 7 | Functions to save C arrays in GNU Octave matrix format. The output text 8 | file can be directly read into Octave using "load filename". 9 | 10 | \*---------------------------------------------------------------------------*/ 11 | 12 | 13 | /* 14 | Copyright (C) 2012 David Rowe 15 | 16 | All rights reserved. 17 | 18 | This program is free software; you can redistribute it and/or modify 19 | it under the terms of the GNU Lesser General Public License version 2.1, as 20 | published by the Free Software Foundation. This program is 21 | distributed in the hope that it will be useful, but WITHOUT ANY 22 | WARRANTY; without even the implied warranty of MERCHANTABILITY or 23 | FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public 24 | License for more details. 25 | 26 | You should have received a copy of the GNU Lesser General Public License 27 | along with this program; if not, see . 28 | */ 29 | 30 | #include 31 | #include 32 | 33 | #include "octave.h" 34 | 35 | #ifdef ARM_MATH_CM4 36 | #include "Trace.h" 37 | #endif 38 | 39 | #define OCTAVE_BUFSIZE 2048 40 | 41 | 42 | void flush_buffer(FILE* f, char* buffer,size_t* buf_idx_ptr) 43 | { 44 | #ifdef ARM_MATH_CM4 45 | trace_write(buffer,*buf_idx_ptr); 46 | #else 47 | fwrite(buffer,*buf_idx_ptr,1,f); 48 | #endif 49 | *buf_idx_ptr = 0; 50 | } 51 | 52 | void handle_buffer(FILE* f, char* buffer,const size_t max_buf, size_t* buf_idx_ptr, size_t l) 53 | { 54 | *buf_idx_ptr += l; 55 | if (*buf_idx_ptr > max_buf - 64) 56 | { 57 | flush_buffer(f, buffer,buf_idx_ptr); 58 | } 59 | } 60 | 61 | signed int printf_buffer(FILE* f, char* buffer,const size_t max_buf, size_t* buf_idx_ptr, const char *pFormat, ...) 62 | { 63 | va_list ap; 64 | signed int rc; 65 | 66 | va_start(ap, pFormat); 67 | rc = vsnprintf(&buffer[*buf_idx_ptr], max_buf - *buf_idx_ptr, pFormat, ap); 68 | va_end(ap); 69 | if (rc>0) 70 | { 71 | handle_buffer(f, buffer,max_buf,buf_idx_ptr,rc); 72 | } 73 | return rc; 74 | } 75 | 76 | 77 | void printf_header(FILE* f, char* buffer,const size_t max_buf, size_t* buf_idx_ptr, const char *name, const char *dtype, int rows, int cols, int isFloat) 78 | { 79 | #ifdef ARM_MATH_CM4 80 | printf_buffer(f, buffer, OCTAVE_BUFSIZE, buf_idx_ptr, "# hex: %s\n", isFloat?"true":"false"); 81 | #endif 82 | printf_buffer(f, buffer, OCTAVE_BUFSIZE, buf_idx_ptr, "# name: %s\n", name); 83 | printf_buffer(f, buffer, OCTAVE_BUFSIZE, buf_idx_ptr, "# type: %s\n",dtype); 84 | printf_buffer(f, buffer, OCTAVE_BUFSIZE, buf_idx_ptr, "# rows: %d\n", rows); 85 | printf_buffer(f, buffer, OCTAVE_BUFSIZE, buf_idx_ptr, "# columns: %d\n", cols); 86 | } 87 | void octave_save_int(FILE *f, char name[], int data[], int rows, int cols) 88 | { 89 | int r,c; 90 | char buffer[OCTAVE_BUFSIZE]; 91 | size_t buf_idx = 0; 92 | 93 | printf_header(f, buffer, OCTAVE_BUFSIZE, &buf_idx, name, "matrix", rows, cols, 0); 94 | 95 | for(r=0; r. 28 | */ 29 | 30 | #ifndef __OCTAVE__ 31 | #define __OCTAVE__ 32 | 33 | #include "comp.h" 34 | 35 | void octave_save_int(FILE *f, char name[], int data[], int rows, int cols); 36 | void octave_save_float(FILE *f, char name[], float data[], int rows, int cols, int col_len); 37 | void octave_save_complex(FILE *f, char name[], COMP data[], int rows, int cols, int col_len); 38 | 39 | #endif 40 | -------------------------------------------------------------------------------- /src/phi0.c: -------------------------------------------------------------------------------- 1 | 2 | // phi0.c 3 | // 4 | // An approximation of the function 5 | // 6 | // This file is generated by the gen_phi0 script, from codec2 7 | // https://github.com/drowe67/codec2/blob/master/script/gen_phi0 8 | // Any changes should be made to that file, not this one 9 | 10 | #include 11 | 12 | #define SI16(f) ((int32_t)(f * (1<<16))) 13 | 14 | float phi0( float xf ) { 15 | 16 | int32_t x = SI16(xf); 17 | 18 | if (x >= SI16(10.0f)) return(0.0f); 19 | else { 20 | if (x >= SI16(5.0f)) { 21 | int i = 19 - (x >> 15); 22 | switch (i) { 23 | case 0: return(0.000116589f); // (9.5) 24 | case 1: return(0.000192223f); // (9.0) 25 | case 2: return(0.000316923f); // (8.5) 26 | case 3: return(0.000522517f); // (8.0) 27 | case 4: return(0.000861485f); // (7.5) 28 | case 5: return(0.001420349f); // (7.0) 29 | case 6: return(0.002341760f); // (6.5) 30 | case 7: return(0.003860913f); // (6.0) 31 | case 8: return(0.006365583f); // (5.5) 32 | case 9: return(0.010495133f); // (5.0) 33 | } 34 | } 35 | else { 36 | if (x >= SI16(1.0f)) { 37 | int i = 79 - (x >> 12); 38 | switch (i) { 39 | case 0: return(0.013903889f); // (4.9375) 40 | case 1: return(0.014800644f); // (4.8750) 41 | case 2: return(0.015755242f); // (4.8125) 42 | case 3: return(0.016771414f); // (4.7500) 43 | case 4: return(0.017853133f); // (4.6875) 44 | case 5: return(0.019004629f); // (4.6250) 45 | case 6: return(0.020230403f); // (4.5625) 46 | case 7: return(0.021535250f); // (4.5000) 47 | case 8: return(0.022924272f); // (4.4375) 48 | case 9: return(0.024402903f); // (4.3750) 49 | case 10: return(0.025976926f); // (4.3125) 50 | case 11: return(0.027652501f); // (4.2500) 51 | case 12: return(0.029436184f); // (4.1875) 52 | case 13: return(0.031334956f); // (4.1250) 53 | case 14: return(0.033356250f); // (4.0625) 54 | case 15: return(0.035507982f); // (4.0000) 55 | case 16: return(0.037798579f); // (3.9375) 56 | case 17: return(0.040237016f); // (3.8750) 57 | case 18: return(0.042832850f); // (3.8125) 58 | case 19: return(0.045596260f); // (3.7500) 59 | case 20: return(0.048538086f); // (3.6875) 60 | case 21: return(0.051669874f); // (3.6250) 61 | case 22: return(0.055003924f); // (3.5625) 62 | case 23: return(0.058553339f); // (3.5000) 63 | case 24: return(0.062332076f); // (3.4375) 64 | case 25: return(0.066355011f); // (3.3750) 65 | case 26: return(0.070637993f); // (3.3125) 66 | case 27: return(0.075197917f); // (3.2500) 67 | case 28: return(0.080052790f); // (3.1875) 68 | case 29: return(0.085221814f); // (3.1250) 69 | case 30: return(0.090725463f); // (3.0625) 70 | case 31: return(0.096585578f); // (3.0000) 71 | case 32: return(0.102825462f); // (2.9375) 72 | case 33: return(0.109469985f); // (2.8750) 73 | case 34: return(0.116545700f); // (2.8125) 74 | case 35: return(0.124080967f); // (2.7500) 75 | case 36: return(0.132106091f); // (2.6875) 76 | case 37: return(0.140653466f); // (2.6250) 77 | case 38: return(0.149757747f); // (2.5625) 78 | case 39: return(0.159456024f); // (2.5000) 79 | case 40: return(0.169788027f); // (2.4375) 80 | case 41: return(0.180796343f); // (2.3750) 81 | case 42: return(0.192526667f); // (2.3125) 82 | case 43: return(0.205028078f); // (2.2500) 83 | case 44: return(0.218353351f); // (2.1875) 84 | case 45: return(0.232559308f); // (2.1250) 85 | case 46: return(0.247707218f); // (2.0625) 86 | case 47: return(0.263863255f); // (2.0000) 87 | case 48: return(0.281099022f); // (1.9375) 88 | case 49: return(0.299492155f); // (1.8750) 89 | case 50: return(0.319127030f); // (1.8125) 90 | case 51: return(0.340095582f); // (1.7500) 91 | case 52: return(0.362498271f); // (1.6875) 92 | case 53: return(0.386445235f); // (1.6250) 93 | case 54: return(0.412057648f); // (1.5625) 94 | case 55: return(0.439469363f); // (1.5000) 95 | case 56: return(0.468828902f); // (1.4375) 96 | case 57: return(0.500301872f); // (1.3750) 97 | case 58: return(0.534073947f); // (1.3125) 98 | case 59: return(0.570354566f); // (1.2500) 99 | case 60: return(0.609381573f); // (1.1875) 100 | case 61: return(0.651427083f); // (1.1250) 101 | case 62: return(0.696805010f); // (1.0625) 102 | case 63: return(0.745880827f); // (1.0000) 103 | } 104 | } 105 | else { 106 | if (x > SI16(0.007812f)) { 107 | if (x > SI16(0.088388f)) { 108 | if (x > SI16(0.250000f)) { 109 | if (x > SI16(0.500000f)) { 110 | if (x > SI16(0.707107f)) { 111 | return(0.922449644f); 112 | } else { 113 | return(1.241248638f); 114 | } 115 | } else { 116 | if (x > SI16(0.353553f)) { 117 | return(1.573515241f); 118 | } else { 119 | return(1.912825912f); 120 | } 121 | } 122 | } else { 123 | if (x > SI16(0.125000f)) { 124 | if (x > SI16(0.176777f)) { 125 | return(2.255740095f); 126 | } else { 127 | return(2.600476919f); 128 | } 129 | } else { 130 | return(2.946130351f); 131 | } 132 | } 133 | } else { 134 | if (x > SI16(0.022097f)) { 135 | if (x > SI16(0.044194f)) { 136 | if (x > SI16(0.062500f)) { 137 | return(3.292243417f); 138 | } else { 139 | return(3.638586634f); 140 | } 141 | } else { 142 | if (x > SI16(0.031250f)) { 143 | return(3.985045009f); 144 | } else { 145 | return(4.331560985f); 146 | } 147 | } 148 | } else { 149 | if (x > SI16(0.011049f)) { 150 | if (x > SI16(0.015625f)) { 151 | return(4.678105767f); 152 | } else { 153 | return(5.024664952f); 154 | } 155 | } else { 156 | return(5.371231340f); 157 | } 158 | } 159 | } 160 | } else { 161 | if (x > SI16(0.000691f)) { 162 | if (x > SI16(0.001953f)) { 163 | if (x > SI16(0.003906f)) { 164 | if (x > SI16(0.005524f)) { 165 | return(5.717801329f); 166 | } else { 167 | return(6.064373119f); 168 | } 169 | } else { 170 | if (x > SI16(0.002762f)) { 171 | return(6.410945809f); 172 | } else { 173 | return(6.757518949f); 174 | } 175 | } 176 | } else { 177 | if (x > SI16(0.000977f)) { 178 | if (x > SI16(0.001381f)) { 179 | return(7.104092314f); 180 | } else { 181 | return(7.450665792f); 182 | } 183 | } else { 184 | return(7.797239326f); 185 | } 186 | } 187 | } else { 188 | if (x > SI16(0.000173f)) { 189 | if (x > SI16(0.000345f)) { 190 | if (x > SI16(0.000488f)) { 191 | return(8.143812888f); 192 | } else { 193 | return(8.490386464f); 194 | } 195 | } else { 196 | if (x > SI16(0.000244f)) { 197 | return(8.836960047f); 198 | } else { 199 | return(9.183533634f); 200 | } 201 | } 202 | } else { 203 | if (x > SI16(0.000086f)) { 204 | if (x > SI16(0.000122f)) { 205 | return(9.530107222f); 206 | } else { 207 | return(9.876680812f); 208 | } 209 | } else { 210 | return(10.000000000f); 211 | } 212 | } 213 | } 214 | } 215 | } 216 | } 217 | } 218 | return(10.0f); 219 | } 220 | -------------------------------------------------------------------------------- /src/phi0.h: -------------------------------------------------------------------------------- 1 | // phi0.h 2 | #ifndef PHI0_H 3 | #define PHI0_H 4 | 5 | extern float phi0( float xf ); 6 | 7 | #endif 8 | -------------------------------------------------------------------------------- /start_dual_4fsk.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Dual Horus Binary Decoder Script 4 | # Intended for use with Dual Launches, where both launches have 4FSK payloads closely spaced (~10 kHz) 5 | # 6 | # The SDR is tuned 5 kHz below the Lower 4FSK frequency, and the frequency estimators are set across the two frequencies. 7 | # Modem statistics are sent out via a new 'MODEM_STATS' UDP broadcast message every second. 8 | # 9 | 10 | # Change directory to the horusdemodlib directory. 11 | # If running as a different user, you will need to change this line 12 | cd /home/pi/horusdemodlib/ 13 | 14 | # Receive requency, in Hz. This is the frequency the SDR is tuned to. 15 | RXFREQ=434195000 16 | 17 | # Where in the passband we expect to find the Lower Horus Binary (MFSK) signal, in Hz. 18 | # For this example, this is on 434.290 MHz, so with a SDR frequency of 434.195 MHz, 19 | # we expect to find the signal at approx +5 kHz. 20 | # Note that the signal must be located ABOVE the centre frequency of the receiver. 21 | MFSK1_SIGNAL=5000 22 | 23 | # Where in the receiver passband we expect to find the higher Horus Binary (MFSK) signal, in Hz. 24 | # In this example, our second frequency is at 434.210 MHz, so with a SDR frequency of 434.195 MHz, 25 | # we expect to find the signal at approx +15 kHz. 26 | MFSK2_SIGNAL=15000 27 | 28 | # Frequency estimator bandwidth. The wider the bandwidth, the more drift and frequency error the modem can tolerate, 29 | # but the higher the chance that the modem will lock on to a strong spurious signal. 30 | RXBANDWIDTH=5000 31 | 32 | # RTLSDR Device Selection 33 | # If you want to use a specific RTLSDR, you can change this setting to match the 34 | # device identifier of your SDR (use rtl_test to get a list) 35 | SDR_DEVICE=0 36 | 37 | # Receiver Gain. Set this to 0 to use automatic gain control, otherwise if running a 38 | # preamplifier, you may want to experiment with different gain settings to optimize 39 | # your receiver setup. 40 | # You can find what gain range is valid for your RTLSDR by running: rtl_test 41 | GAIN=0 42 | 43 | # Bias Tee Enable (1) or Disable (0) 44 | # NOTE: This uses the -T bias-tee option which is only available on recent versions 45 | # of rtl-sdr. Check if your version has this option by running rtl_fm --help and looking 46 | # for it in the option list. 47 | # If not, you may need to uninstall that version, and then compile from source: https://github.com/osmocom/rtl-sdr 48 | BIAS=0 49 | 50 | # Receiver PPM offset 51 | PPM=0 52 | 53 | 54 | 55 | 56 | # Check that the horus_demod decoder has been compiled. 57 | DECODER=./build/src/horus_demod 58 | if [ -f "$DECODER" ]; then 59 | echo "Found horus_demod." 60 | else 61 | echo "ERROR - $DECODER does not exist - have you compiled it yet?" 62 | exit 1 63 | fi 64 | 65 | # Check that bc is available on the system path. 66 | if echo "1+1" | bc > /dev/null; then 67 | echo "Found bc." 68 | else 69 | echo "ERROR - Cannot find bc - Did you install it?" 70 | exit 1 71 | fi 72 | 73 | # Use a local venv if it exists 74 | VENV_DIR=venv 75 | if [ -d "$VENV_DIR" ]; then 76 | echo "Entering venv." 77 | source $VENV_DIR/bin/activate 78 | fi 79 | 80 | 81 | # Calculate the frequency estimator limits 82 | # Note - these are somewhat hard-coded for this dual-RX application. 83 | MFSK1_LOWER=$(echo "$MFSK1_SIGNAL - $RXBANDWIDTH/2" | bc) 84 | MFSK1_UPPER=$(echo "$MFSK1_SIGNAL + $RXBANDWIDTH/2" | bc) 85 | MFSK1_CENTRE=$(echo "$RXFREQ + $MFSK1_SIGNAL" | bc) 86 | 87 | MFSK2_LOWER=$(echo "$MFSK2_SIGNAL - $RXBANDWIDTH/2" | bc) 88 | MFSK2_UPPER=$(echo "$MFSK2_SIGNAL + $RXBANDWIDTH/2" | bc) 89 | MFSK2_CENTRE=$(echo "$RXFREQ + $MFSK2_SIGNAL" | bc) 90 | 91 | echo "Using SDR Centre Frequency: $RXFREQ Hz." 92 | echo "Using MFSK1 estimation range: $MFSK1_LOWER - $MFSK1_UPPER Hz" 93 | echo "Using MFSK2 estimation range: $MFSK2_LOWER - $MFSK2_UPPER Hz" 94 | 95 | BIAS_SETTING="" 96 | 97 | if [ "$BIAS" = "1" ]; then 98 | echo "Enabling Bias Tee." 99 | BIAS_SETTING=" -T" 100 | fi 101 | 102 | GAIN_SETTING="" 103 | if [ "$GAIN" = "0" ]; then 104 | echo "Using AGC." 105 | GAIN_SETTING="" 106 | else 107 | echo "Using Manual Gain" 108 | GAIN_SETTING=" -g $GAIN" 109 | fi 110 | 111 | STATS_SETTING="" 112 | 113 | if [ "$STATS_OUTPUT" = "1" ]; then 114 | echo "Enabling Modem Statistics." 115 | STATS_SETTING=" --stats=100" 116 | fi 117 | 118 | # Start the receive chain. 119 | # Note that we now pass in the SDR centre frequency ($RXFREQ) and 'target' signal frequency ($MFSK1_CENTRE) 120 | # to enable providing additional metadata to Habitat / Sondehub. 121 | rtl_fm -M raw -F9 -d $SDR_DEVICE -s 48000 -p $PPM $GAIN_SETTING$BIAS_SETTING -f $RXFREQ | tee >($DECODER -q --stats=5 -g -m binary --fsk_lower=$MFSK1_LOWER --fsk_upper=$MFSK1_UPPER - - | python -m horusdemodlib.uploader --freq_hz $RXFREQ --freq_target_hz $MFSK1_CENTRE ) >($DECODER -q --stats=5 -g -m binary --fsk_lower=$MFSK2_LOWER --fsk_upper=$MFSK2_UPPER - - | python -m horusdemodlib.uploader --freq_hz $RXFREQ ) > /dev/null 122 | -------------------------------------------------------------------------------- /start_dual_rtty_4fsk.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Dual RTTY / Horus Binary Decoder Script 4 | # Intended for use on Horus flights, with the following payload frequencies: 5 | # RTTY: 434.650 MHz - Callsign 'HORUS' 6 | # MFSK: 434.660 MHz - Callsign 'HORUSBINARY' 7 | # 8 | # The SDR is tuned 5 kHz below the RTTY frequency, and the frequency estimators are set across the two frequencies. 9 | # Modem statistics are sent out via a new 'MODEM_STATS' UDP broadcast message every second. 10 | # 11 | 12 | # Change directory to the horusdemodlib directory. 13 | # If running as a different user, you will need to change this line 14 | cd /home/pi/horusdemodlib/ 15 | 16 | # Receive requency, in Hz. This is the frequency the SDR is tuned to. 17 | RXFREQ=434645000 18 | 19 | # Where in the passband we expect to find the RTTY signal, in Hz. 20 | # For Horus flights, this is on 434.650 MHz, so with a SDR frequency of 434.645 MHz, 21 | # we expect to find the RTTY signal at approx +5 kHz. 22 | # Note that the signal must be located ABOVE the centre frequency of the receiver. 23 | RTTY_SIGNAL=5000 24 | 25 | # Where in the receiver passband we expect to find the Horus Binary (MFSK) signal, in Hz. 26 | # For Horus flights, this is on 434.660 MHz, so with a SDR frequency of 434.645 MHz, 27 | # we expect to find the RTTY signal at approx +15 kHz. 28 | MFSK_SIGNAL=15000 29 | 30 | # Frequency estimator bandwidth. The wider the bandwidth, the more drift and frequency error the modem can tolerate, 31 | # but the higher the chance that the modem will lock on to a strong spurious signal. 32 | RXBANDWIDTH=8000 33 | 34 | # RTLSDR Device Selection 35 | # If you want to use a specific RTLSDR, you can change this setting to match the 36 | # device identifier of your SDR (use rtl_test to get a list) 37 | SDR_DEVICE=0 38 | 39 | # Receiver Gain. Set this to 0 to use automatic gain control, otherwise if running a 40 | # preamplifier, you may want to experiment with different gain settings to optimize 41 | # your receiver setup. 42 | # You can find what gain range is valid for your RTLSDR by running: rtl_test 43 | GAIN=0 44 | 45 | # Bias Tee Enable (1) or Disable (0) 46 | # NOTE: This uses the -T bias-tee option which is only available on recent versions 47 | # of rtl-sdr. Check if your version has this option by running rtl_fm --help and looking 48 | # for it in the option list. 49 | # If not, you may need to uninstall that version, and then compile from source: https://github.com/osmocom/rtl-sdr 50 | BIAS=0 51 | 52 | # Receiver PPM offset 53 | PPM=0 54 | 55 | 56 | 57 | 58 | # Check that the horus_demod decoder has been compiled. 59 | DECODER=./build/src/horus_demod 60 | if [ -f "$DECODER" ]; then 61 | echo "Found horus_demod." 62 | else 63 | echo "ERROR - $DECODER does not exist - have you compiled it yet?" 64 | exit 1 65 | fi 66 | 67 | # Check that bc is available on the system path. 68 | if echo "1+1" | bc > /dev/null; then 69 | echo "Found bc." 70 | else 71 | echo "ERROR - Cannot find bc - Did you install it?" 72 | exit 1 73 | fi 74 | 75 | # Use a local venv if it exists 76 | VENV_DIR=venv 77 | if [ -d "$VENV_DIR" ]; then 78 | echo "Entering venv." 79 | source $VENV_DIR/bin/activate 80 | fi 81 | 82 | 83 | # Calculate the frequency estimator limits 84 | # Note - these are somewhat hard-coded for this dual-RX application. 85 | RTTY_LOWER=$(echo "$RTTY_SIGNAL - $RXBANDWIDTH/2" | bc) 86 | RTTY_UPPER=$(echo "$RTTY_SIGNAL + $RXBANDWIDTH/2" | bc) 87 | RTTY_CENTRE=$(echo "$RXFREQ + $RTTY_SIGNAL" | bc) 88 | 89 | MFSK_LOWER=$(echo "$MFSK_SIGNAL - $RXBANDWIDTH/2" | bc) 90 | MFSK_UPPER=$(echo "$MFSK_SIGNAL + $RXBANDWIDTH/2" | bc) 91 | MFSK_CENTRE=$(echo "$RXFREQ + $MFSK_SIGNAL" | bc) 92 | 93 | echo "Using SDR Centre Frequency: $RXFREQ Hz." 94 | echo "Using RTTY estimation range: $RTTY_LOWER - $RTTY_UPPER Hz" 95 | echo "Using MFSK estimation range: $MFSK_LOWER - $MFSK_UPPER Hz" 96 | 97 | BIAS_SETTING="" 98 | 99 | if [ "$BIAS" = "1" ]; then 100 | echo "Enabling Bias Tee." 101 | BIAS_SETTING=" -T" 102 | fi 103 | 104 | GAIN_SETTING="" 105 | if [ "$GAIN" = "0" ]; then 106 | echo "Using AGC." 107 | GAIN_SETTING="" 108 | else 109 | echo "Using Manual Gain" 110 | GAIN_SETTING=" -g $GAIN" 111 | fi 112 | 113 | STATS_SETTING="" 114 | 115 | if [ "$STATS_OUTPUT" = "1" ]; then 116 | echo "Enabling Modem Statistics." 117 | STATS_SETTING=" --stats=100" 118 | fi 119 | 120 | # Start the receive chain. 121 | # Note that we now pass in the SDR centre frequency ($RXFREQ) and 'target' signal frequency ($RTTY_CENTRE / $MFSK_CENTRE) 122 | # to enable providing additional metadata to Habitat / Sondehub. 123 | rtl_fm -M raw -F9 -d $SDR_DEVICE -s 48000 -p $PPM $GAIN_SETTING$BIAS_SETTING -f $RXFREQ | tee >($DECODER -q --stats=5 -g -m RTTY --fsk_lower=$RTTY_LOWER --fsk_upper=$RTTY_UPPER - - | python -m horusdemodlib.uploader --rtty --freq_hz $RXFREQ --freq_target_hz $RTTY_CENTRE ) >($DECODER -q --stats=5 -g -m binary --fsk_lower=$MFSK_LOWER --fsk_upper=$MFSK_UPPER - - | python -m horusdemodlib.uploader --freq_hz $RXFREQ --freq_target_hz $MFSK_CENTRE ) > /dev/null 124 | -------------------------------------------------------------------------------- /start_eight_4fsk.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Horus Binary *Eight-way* Decoder Script 4 | # Intended for situations with up to eight payloads in the air, spaced 5 kHz apart. 5 | # NOTE - Ensure your horus_demod build is from newer than ~5th April 2024, else this 6 | # will not work correctly! 7 | # 8 | # Note: Don't try this with DFM17's. They have significant frequency drift and may 9 | # interfere with adjoining payloads. 10 | # 11 | # The *centre* frequency of the SDR Receiver, in Hz. 12 | # Trackers should be programmed at 5khz intervals, starting 2.5khz above, and below the center 13 | # frequency. 14 | # In this example we set the center frequency to 432.622.500. Tracker frequencies should 15 | # be programmed at 432.605.000 - 432.640.000 at 5khz intervals 16 | 17 | # Center Frequency 18 | RXFREQ=432622500 19 | 20 | 21 | # Change directory to the horusdemodlib directory. 22 | # If running as a different user, you will need to change this line 23 | cd /home/pi/horusdemodlib/ 24 | 25 | # Where to find the first signal - in this case at 432.605 MHz, so -17500 Hz below the centre. 26 | MFSK1_SIGNAL=-17500 27 | 28 | # Where to find the second signal - in this case at 432.610 MHz, so -12500 Hz below the centre. 29 | MFSK2_SIGNAL=-12500 30 | 31 | # Where to find the third signal - in this case at 432.615 MHz, so -7500 Hz below the centre. 32 | MFSK3_SIGNAL=-7500 33 | 34 | # Where to find the fourth signal - in this case at 432.620 MHz, so -2500 Hz below the centre. 35 | MFSK4_SIGNAL=-2500 36 | 37 | # Where to find the fifth signal - in this case at 432.625 MHz, so 2500 Hz above the centre. 38 | MFSK5_SIGNAL=2500 39 | 40 | # Where to find the sixth signal - in this case at 432.630 MHz, so 7500 Hz above the centre. 41 | MFSK6_SIGNAL=7500 42 | 43 | # Where to find the seventh signal - in this case at 432.635 MHz, so 12500 Hz above the centre. 44 | MFSK7_SIGNAL=12500 45 | 46 | # Where to find the eighth signal - in this case at 432.640 MHz, so 17500 Hz above the centre. 47 | MFSK8_SIGNAL=17500 48 | 49 | # Frequency estimator bandwidth. The wider the bandwidth, the more drift and frequency error the modem can tolerate, 50 | # but the higher the chance that the modem will lock on to a strong spurious signal. 51 | RXBANDWIDTH=5000 52 | 53 | # RTLSDR Device Selection 54 | # If you want to use a specific RTLSDR, you can change this setting to match the 55 | # device identifier of your SDR (use rtl_test to get a list) 56 | SDR_DEVICE=0 57 | 58 | # Receiver Gain. Set this to 0 to use automatic gain control, otherwise if running a 59 | # preamplifier, you may want to experiment with different gain settings to optimize 60 | # your receiver setup. 61 | # You can find what gain range is valid for your RTLSDR by running: rtl_test 62 | GAIN=0 63 | 64 | # Bias Tee Enable (1) or Disable (0) 65 | # NOTE: This uses the -T bias-tee option which is only available on recent versions 66 | # of rtl-sdr. Check if your version has this option by running rtl_fm --help and looking 67 | # for it in the option list. 68 | # If not, you may need to uninstall that version, and then compile from source: https://github.com/osmocom/rtl-sdr 69 | BIAS=0 70 | 71 | # Receiver PPM offset 72 | PPM=0 73 | 74 | # Check that the horus_demod decoder has been compiled. 75 | DECODER=./build/src/horus_demod 76 | if [ -f "$DECODER" ]; then 77 | echo "Found horus_demod." 78 | else 79 | echo "ERROR - $DECODER does not exist - have you compiled it yet?" 80 | exit 1 81 | fi 82 | 83 | # Check that bc is available on the system path. 84 | if echo "1+1" | bc > /dev/null; then 85 | echo "Found bc." 86 | else 87 | echo "ERROR - Cannot find bc - Did you install it?" 88 | exit 1 89 | fi 90 | 91 | # Use a local venv if it exists 92 | VENV_DIR=venv 93 | if [ -d "$VENV_DIR" ]; then 94 | echo "Entering venv." 95 | source $VENV_DIR/bin/activate 96 | fi 97 | 98 | 99 | # Calculate the frequency estimator limits for each decoder 100 | MFSK1_LOWER=$(echo "$MFSK1_SIGNAL - $RXBANDWIDTH/2" | bc) 101 | MFSK1_UPPER=$(echo "$MFSK1_SIGNAL + $RXBANDWIDTH/2" | bc) 102 | MFSK1_CENTRE=$(echo "$RXFREQ + $MFSK1_SIGNAL" | bc) 103 | 104 | MFSK2_LOWER=$(echo "$MFSK2_SIGNAL - $RXBANDWIDTH/2" | bc) 105 | MFSK2_UPPER=$(echo "$MFSK2_SIGNAL + $RXBANDWIDTH/2" | bc) 106 | MFSK2_CENTRE=$(echo "$RXFREQ + $MFSK2_SIGNAL" | bc) 107 | 108 | MFSK3_LOWER=$(echo "$MFSK3_SIGNAL - $RXBANDWIDTH/2" | bc) 109 | MFSK3_UPPER=$(echo "$MFSK3_SIGNAL + $RXBANDWIDTH/2" | bc) 110 | MFSK3_CENTRE=$(echo "$RXFREQ + $MFSK3_SIGNAL" | bc) 111 | 112 | MFSK4_LOWER=$(echo "$MFSK4_SIGNAL - $RXBANDWIDTH/2" | bc) 113 | MFSK4_UPPER=$(echo "$MFSK4_SIGNAL + $RXBANDWIDTH/2" | bc) 114 | MFSK4_CENTRE=$(echo "$RXFREQ + $MFSK4_SIGNAL" | bc) 115 | 116 | MFSK5_LOWER=$(echo "$MFSK5_SIGNAL - $RXBANDWIDTH/2" | bc) 117 | MFSK5_UPPER=$(echo "$MFSK5_SIGNAL + $RXBANDWIDTH/2" | bc) 118 | MFSK5_CENTRE=$(echo "$RXFREQ + $MFSK5_SIGNAL" | bc) 119 | 120 | MFSK6_LOWER=$(echo "$MFSK6_SIGNAL - $RXBANDWIDTH/2" | bc) 121 | MFSK6_UPPER=$(echo "$MFSK6_SIGNAL + $RXBANDWIDTH/2" | bc) 122 | MFSK6_CENTRE=$(echo "$RXFREQ + $MFSK6_SIGNAL" | bc) 123 | 124 | MFSK7_LOWER=$(echo "$MFSK7_SIGNAL - $RXBANDWIDTH/2" | bc) 125 | MFSK7_UPPER=$(echo "$MFSK7_SIGNAL + $RXBANDWIDTH/2" | bc) 126 | MFSK7_CENTRE=$(echo "$RXFREQ + $MFSK6_SIGNAL" | bc) 127 | 128 | MFSK8_LOWER=$(echo "$MFSK8_SIGNAL - $RXBANDWIDTH/2" | bc) 129 | MFSK8_UPPER=$(echo "$MFSK8_SIGNAL + $RXBANDWIDTH/2" | bc) 130 | MFSK8_CENTRE=$(echo "$RXFREQ + $MFSK6_SIGNAL" | bc) 131 | 132 | echo "Using SDR Centre Frequency: $RXFREQ Hz." 133 | echo "Using MFSK1 estimation range: $MFSK1_LOWER - $MFSK1_UPPER Hz" 134 | echo "Using MFSK2 estimation range: $MFSK2_LOWER - $MFSK2_UPPER Hz" 135 | echo "Using MFSK3 estimation range: $MFSK3_LOWER - $MFSK3_UPPER Hz" 136 | echo "Using MFSK4 estimation range: $MFSK4_LOWER - $MFSK4_UPPER Hz" 137 | echo "Using MFSK5 estimation range: $MFSK5_LOWER - $MFSK5_UPPER Hz" 138 | echo "Using MFSK6 estimation range: $MFSK6_LOWER - $MFSK6_UPPER Hz" 139 | echo "Using MFSK7 estimation range: $MFSK7_LOWER - $MFSK7_UPPER Hz" 140 | echo "Using MFSK8 estimation range: $MFSK8_LOWER - $MFSK8_UPPER Hz" 141 | 142 | BIAS_SETTING="" 143 | 144 | if [ "$BIAS" = "1" ]; then 145 | echo "Enabling Bias Tee." 146 | BIAS_SETTING=" -T" 147 | fi 148 | 149 | GAIN_SETTING="" 150 | if [ "$GAIN" = "0" ]; then 151 | echo "Using AGC." 152 | GAIN_SETTING="" 153 | else 154 | echo "Using Manual Gain" 155 | GAIN_SETTING=" -g $GAIN" 156 | fi 157 | 158 | STATS_SETTING="" 159 | 160 | if [ "$STATS_OUTPUT" = "1" ]; then 161 | echo "Enabling Modem Statistics." 162 | STATS_SETTING=" --stats=100" 163 | fi 164 | 165 | # Start the receive chain. 166 | # Note that we now pass in the SDR centre frequency ($RXFREQ) and 'target' signal frequency ($MFSK1_CENTRE) 167 | # to enable providing additional metadata to SondeHub 168 | rtl_fm -M raw -F9 -d $SDR_DEVICE -s 48000 -p $PPM $GAIN_SETTING$BIAS_SETTING -f $RXFREQ \ 169 | | tee >($DECODER -q --stats=5 -g -m binary --fsk_lower=$MFSK1_LOWER --fsk_upper=$MFSK1_UPPER - - | python -m horusdemodlib.uploader --freq_hz $RXFREQ --freq_target_hz $MFSK1_CENTRE ) \ 170 | | tee >($DECODER -q --stats=5 -g -m binary --fsk_lower=$MFSK2_LOWER --fsk_upper=$MFSK2_UPPER - - | python -m horusdemodlib.uploader --freq_hz $RXFREQ --freq_target_hz $MFSK2_CENTRE ) \ 171 | | tee >($DECODER -q --stats=5 -g -m binary --fsk_lower=$MFSK3_LOWER --fsk_upper=$MFSK3_UPPER - - | python -m horusdemodlib.uploader --freq_hz $RXFREQ --freq_target_hz $MFSK3_CENTRE ) \ 172 | | tee >($DECODER -q --stats=5 -g -m binary --fsk_lower=$MFSK4_LOWER --fsk_upper=$MFSK4_UPPER - - | python -m horusdemodlib.uploader --freq_hz $RXFREQ --freq_target_hz $MFSK4_CENTRE ) \ 173 | | tee >($DECODER -q --stats=5 -g -m binary --fsk_lower=$MFSK5_LOWER --fsk_upper=$MFSK5_UPPER - - | python -m horusdemodlib.uploader --freq_hz $RXFREQ --freq_target_hz $MFSK5_CENTRE ) \ 174 | | tee >($DECODER -q --stats=5 -g -m binary --fsk_lower=$MFSK6_LOWER --fsk_upper=$MFSK6_UPPER - - | python -m horusdemodlib.uploader --freq_hz $RXFREQ --freq_target_hz $MFSK6_CENTRE ) \ 175 | | tee >($DECODER -q --stats=5 -g -m binary --fsk_lower=$MFSK7_LOWER --fsk_upper=$MFSK7_UPPER - - | python -m horusdemodlib.uploader --freq_hz $RXFREQ --freq_target_hz $MFSK7_CENTRE ) \ 176 | >($DECODER -q --stats=5 -g -m binary --fsk_lower=$MFSK8_LOWER --fsk_upper=$MFSK8_UPPER - - | python -m horusdemodlib.uploader --freq_hz $RXFREQ --freq_target_hz $MFSK8_CENTRE ) > /dev/null 177 | 178 | -------------------------------------------------------------------------------- /start_gqrx.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Horus Binary GQRX Helper Script 4 | # 5 | # Accepts data from GQRX's UDP output, and passes it into horus_demod. 6 | # 7 | 8 | # Decoder mode. 9 | # Can be: 'binary', 'rtty', '256bit' or '128bit' 10 | MODE="binary" 11 | 12 | # Check that the horus_demod decoder has been compiled. 13 | DECODER=./build/src/horus_demod 14 | if [ -f "$DECODER" ]; then 15 | echo "Found horus_demod." 16 | else 17 | echo "ERROR - $DECODER does not exist - have you compiled it yet?" 18 | exit 1 19 | fi 20 | 21 | # Use a local venv if it exists 22 | VENV_DIR=venv 23 | if [ -d "$VENV_DIR" ]; then 24 | echo "Entering venv." 25 | source $VENV_DIR/bin/activate 26 | fi 27 | 28 | 29 | if [[ $OSTYPE == darwin* ]]; then 30 | # OSX's netcat utility uses a different, incompatible syntax. Sigh. 31 | nc -l -u localhost 7355 | $DECODER -m $MODE --stats=5 -g --fsk_lower=100 --fsk_upper=20000 - - | python -m horusdemodlib.uploader $@ 32 | else 33 | # Start up! 34 | nc -l -u -p 7355 localhost | $DECODER -m $MODE --stats=5 -g --fsk_lower=100 --fsk_upper=20000 - - | python -m horusdemodlib.uploader $@ 35 | fi 36 | -------------------------------------------------------------------------------- /start_rtlsdr.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Horus Binary RTLSDR Helper Script 4 | # 5 | # Uses rtl_fm to receive a chunk of spectrum, and passes it into horus_demod. 6 | # 7 | 8 | 9 | # Change directory to the horusdemodlib directory. 10 | # If running as a different user, you will need to change this line 11 | cd /home/pi/horusdemodlib/ 12 | 13 | 14 | # Receive *centre* frequency, in Hz 15 | # Note: The SDR will be tuned to RXBANDWIDTH/2 below this frequency. 16 | RXFREQ=434200000 17 | 18 | 19 | # RTLSDR Device Selection 20 | # If you want to use a specific RTLSDR, you can change this setting to match the 21 | # device identifier of your SDR (use rtl_test to get a list) 22 | SDR_DEVICE=0 23 | 24 | # Receiver Gain. Set this to 0 to use automatic gain control, otherwise if running a 25 | # preamplifier, you may want to experiment with different gain settings to optimize 26 | # your receiver setup. 27 | # You can find what gain range is valid for your RTLSDR by running: rtl_test 28 | GAIN=0 29 | 30 | # Bias Tee Enable (1) or Disable (0) 31 | BIAS=0 32 | 33 | # Receiver PPM offset 34 | PPM=0 35 | 36 | # Frequency estimator bandwidth. The wider the bandwidth, the more drift and frequency error the modem can tolerate, 37 | # but the higher the chance that the modem will lock on to a strong spurious signal. 38 | # Note: The SDR will be tuned to RXFREQ-RXBANDWIDTH/2, and the estimator set to look at 0-RXBANDWIDTH Hz. 39 | RXBANDWIDTH=10000 40 | 41 | # Enable (1) or disable (0) modem statistics output. 42 | # If enabled, modem statistics are written to stats.txt, and can be observed 43 | # during decoding by running: tail -f stats.txt | python fskstats.py 44 | STATS_OUTPUT=0 45 | 46 | 47 | # Check that the horus_demod decoder has been compiled. 48 | DECODER=./build/src/horus_demod 49 | if [ -f "$DECODER" ]; then 50 | echo "Found horus_demod." 51 | else 52 | echo "ERROR - $DECODER does not exist - have you compiled it yet?" 53 | exit 1 54 | fi 55 | 56 | # Check that bc is available on the system path. 57 | if echo "1+1" | bc > /dev/null; then 58 | echo "Found bc." 59 | else 60 | echo "ERROR - Cannot find bc - Did you install it?" 61 | exit 1 62 | fi 63 | 64 | # Use a local venv if it exists 65 | VENV_DIR=venv 66 | if [ -d "$VENV_DIR" ]; then 67 | echo "Entering venv." 68 | source $VENV_DIR/bin/activate 69 | fi 70 | 71 | # Calculate the SDR tuning frequency 72 | SDR_RX_FREQ=$(echo "$RXFREQ - $RXBANDWIDTH/2 - 1000" | bc) 73 | 74 | # Calculate the frequency estimator limits 75 | FSK_LOWER=1000 76 | FSK_UPPER=$(echo "$FSK_LOWER + $RXBANDWIDTH" | bc) 77 | 78 | echo "Using SDR Centre Frequency: $SDR_RX_FREQ Hz." 79 | echo "Using FSK estimation range: $FSK_LOWER - $FSK_UPPER Hz" 80 | 81 | BIAS_SETTING="" 82 | 83 | if [ "$BIAS" = "1" ]; then 84 | echo "Enabling Bias Tee." 85 | BIAS_SETTING=" -T" 86 | fi 87 | 88 | GAIN_SETTING="" 89 | if [ "$GAIN" = "0" ]; then 90 | echo "Using AGC." 91 | GAIN_SETTING="" 92 | else 93 | echo "Using Manual Gain" 94 | GAIN_SETTING=" -g $GAIN" 95 | fi 96 | 97 | # Start the receive chain. 98 | # Note that we now pass in the SDR centre frequency ($SDR_RX_FREQ) and 'target' signal frequency ($RXFREQ) 99 | # to enable providing additional metadata to Habitat / Sondehub. 100 | rtl_fm -M raw -F9 -d $SDR_DEVICE -s 48000 -p $PPM $GAIN_SETTING$BIAS_SETTING -f $SDR_RX_FREQ | $DECODER -q --stats=5 -g -m binary --fsk_lower=$FSK_LOWER --fsk_upper=$FSK_UPPER - - | python -m horusdemodlib.uploader --freq_hz $SDR_RX_FREQ --freq_target_hz $RXFREQ $@ 101 | -------------------------------------------------------------------------------- /start_triple_4fsk.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Horus Binary *Triple* Decoder Script 4 | # Intended for situations with three payloads in the air, spaced ~10 kHz apart. 5 | # NOTE - Ensure your horus_demod build is from newer than ~5th April 2024, else this 6 | # will not work correctly! 7 | # 8 | # It also possible to extend this approach further to handle as many transmissions 9 | # as can be safely fitted into the receiver passband (+/- 24 kHz). Spacings of 10 | # 5 kHz between transmissions is possible with frequency stable transmitters (e.g. RS41s) 11 | # Don't try this with DFM17's, which drift a lot! 12 | # 13 | 14 | # Change directory to the horusdemodlib directory. 15 | # If running as a different user, you will need to change this line 16 | cd /home/pi/horusdemodlib/ 17 | 18 | # The following settings are an example for a situation where there are three transmitters in the air, 19 | # on: 434.190 MHz, 434.200 MHz, and 434.210 MHz. 20 | 21 | # The *centre* frequency of the SDR Receiver, in Hz. 22 | # It's recommended that this not be tuned directly on top of one of the signals we want to receive, 23 | # as sometimes we can get a DC spike which can affect the demodulators. 24 | # In this example the receiver has been tuned in the middle of 2 of the signals, at 434.195 MHz. 25 | RXFREQ=434195000 26 | 27 | # Where to find the first signal - in this case at 434.190 MHz, so -5000 Hz below the centre. 28 | MFSK1_SIGNAL=-5000 29 | 30 | # Where to find the second signal - in this case at 434.200 MHz, so 5000 Hz above the centre. 31 | MFSK2_SIGNAL=5000 32 | 33 | # Where to find the third signal - in this case at 434.210 MHz, so 15000 Hz above the centre. 34 | MFSK3_SIGNAL=15000 35 | 36 | # Frequency estimator bandwidth. The wider the bandwidth, the more drift and frequency error the modem can tolerate, 37 | # but the higher the chance that the modem will lock on to a strong spurious signal. 38 | RXBANDWIDTH=8000 39 | 40 | # RTLSDR Device Selection 41 | # If you want to use a specific RTLSDR, you can change this setting to match the 42 | # device identifier of your SDR (use rtl_test to get a list) 43 | SDR_DEVICE=0 44 | 45 | # Receiver Gain. Set this to 0 to use automatic gain control, otherwise if running a 46 | # preamplifier, you may want to experiment with different gain settings to optimize 47 | # your receiver setup. 48 | # You can find what gain range is valid for your RTLSDR by running: rtl_test 49 | GAIN=0 50 | 51 | # Bias Tee Enable (1) or Disable (0) 52 | # NOTE: This uses the -T bias-tee option which is only available on recent versions 53 | # of rtl-sdr. Check if your version has this option by running rtl_fm --help and looking 54 | # for it in the option list. 55 | # If not, you may need to uninstall that version, and then compile from source: https://github.com/osmocom/rtl-sdr 56 | BIAS=0 57 | 58 | # Receiver PPM offset 59 | PPM=0 60 | 61 | 62 | 63 | 64 | # Check that the horus_demod decoder has been compiled. 65 | DECODER=./build/src/horus_demod 66 | if [ -f "$DECODER" ]; then 67 | echo "Found horus_demod." 68 | else 69 | echo "ERROR - $DECODER does not exist - have you compiled it yet?" 70 | exit 1 71 | fi 72 | 73 | # Check that bc is available on the system path. 74 | if echo "1+1" | bc > /dev/null; then 75 | echo "Found bc." 76 | else 77 | echo "ERROR - Cannot find bc - Did you install it?" 78 | exit 1 79 | fi 80 | 81 | # Use a local venv if it exists 82 | VENV_DIR=venv 83 | if [ -d "$VENV_DIR" ]; then 84 | echo "Entering venv." 85 | source $VENV_DIR/bin/activate 86 | fi 87 | 88 | 89 | # Calculate the frequency estimator limits for each decoder 90 | MFSK1_LOWER=$(echo "$MFSK1_SIGNAL - $RXBANDWIDTH/2" | bc) 91 | MFSK1_UPPER=$(echo "$MFSK1_SIGNAL + $RXBANDWIDTH/2" | bc) 92 | MFSK1_CENTRE=$(echo "$RXFREQ + $MFSK1_SIGNAL" | bc) 93 | 94 | MFSK2_LOWER=$(echo "$MFSK2_SIGNAL - $RXBANDWIDTH/2" | bc) 95 | MFSK2_UPPER=$(echo "$MFSK2_SIGNAL + $RXBANDWIDTH/2" | bc) 96 | MFSK2_CENTRE=$(echo "$RXFREQ + $MFSK2_SIGNAL" | bc) 97 | 98 | MFSK3_LOWER=$(echo "$MFSK3_SIGNAL - $RXBANDWIDTH/2" | bc) 99 | MFSK3_UPPER=$(echo "$MFSK3_SIGNAL + $RXBANDWIDTH/2" | bc) 100 | MFSK3_CENTRE=$(echo "$RXFREQ + $MFSK3_SIGNAL" | bc) 101 | 102 | echo "Using SDR Centre Frequency: $RXFREQ Hz." 103 | echo "Using MFSK1 estimation range: $MFSK1_LOWER - $MFSK1_UPPER Hz" 104 | echo "Using MFSK2 estimation range: $MFSK2_LOWER - $MFSK2_UPPER Hz" 105 | echo "Using MFSK3 estimation range: $MFSK3_LOWER - $MFSK3_UPPER Hz" 106 | 107 | BIAS_SETTING="" 108 | 109 | if [ "$BIAS" = "1" ]; then 110 | echo "Enabling Bias Tee." 111 | BIAS_SETTING=" -T" 112 | fi 113 | 114 | GAIN_SETTING="" 115 | if [ "$GAIN" = "0" ]; then 116 | echo "Using AGC." 117 | GAIN_SETTING="" 118 | else 119 | echo "Using Manual Gain" 120 | GAIN_SETTING=" -g $GAIN" 121 | fi 122 | 123 | STATS_SETTING="" 124 | 125 | if [ "$STATS_OUTPUT" = "1" ]; then 126 | echo "Enabling Modem Statistics." 127 | STATS_SETTING=" --stats=100" 128 | fi 129 | 130 | # Start the receive chain. 131 | # Note that we now pass in the SDR centre frequency ($RXFREQ) and 'target' signal frequency ($MFSK1_CENTRE) 132 | # to enable providing additional metadata to SondeHub 133 | rtl_fm -M raw -F9 -d $SDR_DEVICE -s 48000 -p $PPM $GAIN_SETTING$BIAS_SETTING -f $RXFREQ \ 134 | | tee >($DECODER -q --stats=5 -g -m binary --fsk_lower=$MFSK1_LOWER --fsk_upper=$MFSK1_UPPER - - | python -m horusdemodlib.uploader --freq_hz $RXFREQ --freq_target_hz $MFSK1_CENTRE ) \ 135 | | tee >($DECODER -q --stats=5 -g -m binary --fsk_lower=$MFSK3_LOWER --fsk_upper=$MFSK3_UPPER - - | python -m horusdemodlib.uploader --freq_hz $RXFREQ --freq_target_hz $MFSK3_CENTRE ) \ 136 | >($DECODER -q --stats=5 -g -m binary --fsk_lower=$MFSK2_LOWER --fsk_upper=$MFSK2_UPPER - - | python -m horusdemodlib.uploader --freq_hz $RXFREQ ) > /dev/null -------------------------------------------------------------------------------- /user.cfg.example: -------------------------------------------------------------------------------- 1 | # 2 | # Horus Binary Uploader Example Configuration File 3 | # 4 | 5 | [user] 6 | # Your callsign - used when uploading to the SondeHub-Amateur Tracker 7 | callsign = YOUR_CALL_HERE 8 | 9 | # Your station latitude/longitude, which will show up on tracker.habhub.org. 10 | # These values must be in Decimal Degree format. 11 | # Leave the lat/lon at 0.0 if you do not wish your station plotted on the map, 12 | # or if you are uploading your position via other means (i.e. using chasemapper) 13 | station_lat = 0.0 14 | station_lon = 0.0 15 | # Radio/Antenna descriptions. 16 | # An optional short description of your radio/antenna setup. 17 | radio_comment = HorusDemodLib + Your Radio Description Here 18 | antenna_comment = Your Antenna Description Here 19 | 20 | 21 | [horus_udp] 22 | # Horus-UDP Message Output port. This is the preferred output for use with mapping systems 23 | # such as ChaseMapper. 24 | summary_port = 55672 25 | 26 | # OziMux UDP Broadcast port, for use with other mapping systems. 27 | ozimux_port = 55683 -------------------------------------------------------------------------------- /user.env.example: -------------------------------------------------------------------------------- 1 | ### General SDR settings ### 2 | 3 | # RTLSDR Device Selection 4 | # If you want to use a specific RTLSDR, you can change this setting to match the 5 | # device identifier of your SDR (use rtl_test to get a list) 6 | SDR_DEVICE=0 7 | 8 | # Receiver Gain. Set this to 0 to use automatic gain control, otherwise if running a 9 | # preamplifier, you may want to experiment with different gain settings to optimize 10 | # your receiver setup. 11 | # You can find what gain range is valid for your RTLSDR by running: rtl_test 12 | GAIN=0 13 | 14 | # Bias Tee Enable (1) or Disable (0) 15 | BIAS=0 16 | 17 | # Receiver PPM offset 18 | PPM=0 19 | 20 | # Enable (1) or disable (0) modem statistics output. 21 | # If enabled, modem statistics are written to stats.txt, and can be observed 22 | # during decoding by running: tail -f stats.txt | python fskstats.py 23 | STATS_OUTPUT=0 24 | 25 | # Select decoder to tun 26 | DECODER=horus_demod 27 | 28 | # For use with SoapySDR via rx_tools 29 | #SDR_EXTRA="-d driver=rtlsdr" 30 | 31 | ########################################################## 32 | ### NOTE: Only uncomment one of the settings sections! ### 33 | ########################################################## 34 | 35 | ### Single 4FSK settings ### 36 | 37 | # Script name 38 | DEMODSCRIPT="docker_single.sh" 39 | 40 | # Receive *centre* frequency, in Hz 41 | # Note: The SDR will be tuned to RXBANDWIDTH/2 below this frequency. 42 | RXFREQ=434200000 43 | 44 | # Frequency estimator bandwidth. The wider the bandwidth, the more drift and frequency error the modem can tolerate, 45 | # but the higher the chance that the modem will lock on to a strong spurious signal. 46 | # Note: The SDR will be tuned to RXFREQ-RXBANDWIDTH/2, and the estimator set to look at 0-RXBANDWIDTH Hz. 47 | RXBANDWIDTH=10000 48 | 49 | 50 | 51 | ### Dual 4FSK settings ### 52 | 53 | # Script name 54 | #DEMODSCRIPT="docker_dual_4fsk.sh" 55 | 56 | # Receive requency, in Hz. This is the frequency the SDR is tuned to. 57 | #RXFREQ=434195000 58 | 59 | # Frequency estimator bandwidth. The wider the bandwidth, the more drift and frequency error the modem can tolerate, 60 | # but the higher the chance that the modem will lock on to a strong spurious signal. 61 | #RXBANDWIDTH=5000 62 | 63 | # Where in the passband we expect to find the Lower Horus Binary (MFSK) signal, in Hz. 64 | # For this example, this is on 434.290 MHz, so with a SDR frequency of 434.195 MHz, 65 | # we expect to find the signal at approx +5 kHz. 66 | # Note that the signal must be located ABOVE the centre frequency of the receiver. 67 | #MFSK1_SIGNAL=5000 68 | 69 | # Where in the receiver passband we expect to find the higher Horus Binary (MFSK) signal, in Hz. 70 | # In this example, our second frequency is at 434.210 MHz, so with a SDR frequency of 434.195 MHz, 71 | # we expect to find the signal at approx +15 kHz. 72 | #MFSK2_SIGNAL=15000 73 | 74 | 75 | 76 | ## Dual RTTY 4FSK settings ### 77 | 78 | # Script name 79 | #DEMODSCRIPT="docker_dual_rtty_4fsk.sh" 80 | 81 | # Receive requency, in Hz. This is the frequency the SDR is tuned to. 82 | #RXFREQ=434645000 83 | 84 | # Frequency estimator bandwidth. The wider the bandwidth, the more drift and frequency error the modem can tolerate, 85 | # but the higher the chance that the modem will lock on to a strong spurious signal. 86 | #RXBANDWIDTH=8000 87 | 88 | # Where in the passband we expect to find the RTTY signal, in Hz. 89 | # For Horus flights, this is on 434.650 MHz, so with a SDR frequency of 434.645 MHz, 90 | # we expect to find the RTTY signal at approx +5 kHz. 91 | # Note that the signal must be located ABOVE the centre frequency of the receiver. 92 | #RTTY_SIGNAL=5000 93 | 94 | # Where in the receiver passband we expect to find the Horus Binary (MFSK) signal, in Hz. 95 | # For Horus flights, this is on 434.660 MHz, so with a SDR frequency of 434.645 MHz, 96 | # we expect to find the RTTY signal at approx +15 kHz. 97 | #MFSK_SIGNAL=15000 98 | 99 | --------------------------------------------------------------------------------