├── src ├── .gitignore ├── libfec │ ├── CMakeLists.txt │ ├── fec.h │ └── viterbi27_port.c ├── crc.h ├── fmtr-json.h ├── fmtr-text.h ├── output-file.h ├── output-tcp.h ├── output-udp.h ├── output-zmq.h ├── input-file.h ├── input-soapysdr.h ├── output-rdkafka.h ├── fmtr-basestation.h ├── spdu.h ├── metadata.c ├── mpdu.h ├── metadata.h ├── kvargs.h ├── options.h ├── ac_data.h ├── acars.h ├── options.c ├── hfdl.h ├── hfnpdu.h ├── input-helpers.h ├── lpdu.h ├── CMakeModules │ ├── FindLiquidDSP.cmake │ └── FindSQLite3.cmake ├── ac_cache.h ├── config.h.in ├── globals.c ├── cache.h ├── version.cmake ├── fft.h ├── position.h ├── systable.h ├── fft_fftw.c ├── pdu.h ├── globals.h ├── fastddc.h ├── libcsdr_gpl.h ├── input-common.h ├── dumpfile.h ├── statsd.h ├── pthread_barrier.h ├── libcsdr.h ├── block.h ├── kvargs.c ├── crc.c ├── pthread_barrier.c ├── fmtr-text.c ├── fmtr-json.c ├── libcsdr_gpl.c ├── input-common.c ├── fmtr-basestation.c ├── fft.c ├── dumpfile.c ├── input-file.c ├── position.c ├── output-udp.c ├── output-common.h ├── input-helpers.c ├── cache.c ├── statsd.c ├── output-zmq.c ├── util.h ├── libcsdr.c ├── acars.c ├── output-file.c ├── block.c ├── pdu.c ├── output-tcp.c ├── spdu.c ├── ac_data.c └── mpdu.c ├── .gitignore ├── screenshots ├── screenshot.png ├── statistics.png └── vrs_config.png ├── etc ├── dumphfdl ├── dumphfdl.service └── systable.conf ├── extras ├── README.md ├── multitail-dumphfdl.conf ├── hfdlgrep └── log_aggregator.py ├── CMakeLists.txt ├── .github └── workflows │ └── build.yml ├── CHANGELOG.md └── doc └── STATSD_METRICS.md /src/.gitignore: -------------------------------------------------------------------------------- 1 | config.h 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | .cache 3 | compile_commands.json 4 | -------------------------------------------------------------------------------- /screenshots/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/szpajder/dumphfdl/HEAD/screenshots/screenshot.png -------------------------------------------------------------------------------- /screenshots/statistics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/szpajder/dumphfdl/HEAD/screenshots/statistics.png -------------------------------------------------------------------------------- /screenshots/vrs_config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/szpajder/dumphfdl/HEAD/screenshots/vrs_config.png -------------------------------------------------------------------------------- /src/libfec/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_library (fec OBJECT 2 | viterbi27_port.c 3 | ) 4 | target_include_directories(fec PUBLIC "..") 5 | -------------------------------------------------------------------------------- /src/crc.h: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #pragma once 3 | #include 4 | 5 | uint16_t crc16_ccitt(uint8_t *data, uint32_t len, uint16_t crc_init); 6 | -------------------------------------------------------------------------------- /src/fmtr-json.h: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #pragma once 3 | #include "output-common.h" // fmtr_descriptor_t 4 | 5 | extern fmtr_descriptor_t fmtr_DEF_json; 6 | -------------------------------------------------------------------------------- /src/fmtr-text.h: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #pragma once 3 | #include "output-common.h" // fmtr_descriptor_t 4 | 5 | extern fmtr_descriptor_t fmtr_DEF_text; 6 | -------------------------------------------------------------------------------- /src/output-file.h: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #pragma once 3 | #include "output-common.h" // output_descriptor_t 4 | 5 | extern output_descriptor_t out_DEF_file; 6 | -------------------------------------------------------------------------------- /src/output-tcp.h: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #pragma once 3 | 4 | #include "output-common.h" // output_descriptor_t 5 | 6 | extern output_descriptor_t out_DEF_tcp; 7 | -------------------------------------------------------------------------------- /src/output-udp.h: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #pragma once 3 | 4 | #include "output-common.h" // output_descriptor_t 5 | 6 | extern output_descriptor_t out_DEF_udp; 7 | -------------------------------------------------------------------------------- /src/output-zmq.h: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #pragma once 3 | 4 | #include "output-common.h" // output_descriptor_t 5 | 6 | extern output_descriptor_t out_DEF_zmq; 7 | -------------------------------------------------------------------------------- /src/input-file.h: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #pragma once 3 | 4 | #include "input-common.h" // struct input_vtable 5 | 6 | extern struct input_vtable file_input_vtable; 7 | -------------------------------------------------------------------------------- /src/input-soapysdr.h: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #pragma once 3 | #include "input-common.h" // struct input_vtable 4 | 5 | extern struct input_vtable soapysdr_input_vtable; 6 | -------------------------------------------------------------------------------- /src/output-rdkafka.h: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #pragma once 3 | 4 | #include "output-common.h" // output_descriptor_t 5 | 6 | extern output_descriptor_t out_DEF_rdkafka; 7 | -------------------------------------------------------------------------------- /src/fmtr-basestation.h: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #pragma once 3 | #include "output-common.h" // fmtr_descriptor_t 4 | 5 | extern fmtr_descriptor_t fmtr_DEF_basestation; 6 | 7 | -------------------------------------------------------------------------------- /src/spdu.h: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #pragma once 3 | #include 4 | #include // la_list 5 | #include "util.h" // struct octet_string 6 | 7 | la_list *spdu_parse(struct octet_string *pdu, int32_t freq); 8 | -------------------------------------------------------------------------------- /src/metadata.c: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #include "metadata.h" 3 | #include "util.h" // ASSERT 4 | 5 | struct metadata *metadata_copy(struct metadata const *m) { 6 | ASSERT(m); 7 | return m->vtable->copy(m); 8 | } 9 | 10 | void metadata_destroy(struct metadata *m) { 11 | if(m == NULL) { 12 | return; 13 | } 14 | m->vtable->destroy(m); 15 | } 16 | -------------------------------------------------------------------------------- /src/mpdu.h: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #pragma once 3 | #include 4 | #include // struct timeval 5 | #include // la_reasm_ctx 6 | #include // la_list 7 | #include "util.h" // struct octet_string 8 | 9 | la_list *mpdu_parse(struct octet_string *pdu, la_reasm_ctx *reasm_ctx, struct 10 | timeval rx_timestamp, int32_t freq); 11 | -------------------------------------------------------------------------------- /etc/dumphfdl: -------------------------------------------------------------------------------- 1 | # This file is used by the dumphfdl systemd unit. 2 | # - Copy it to /etc/default 3 | # - Uncomment the following line and set your preferred command line options. 4 | #DUMPHFDL_OPTIONS="--output-mpdus --system-table /home/pi/hfdl/systable.conf --system-table-save /home/pi/hfdl/systable.conf.new --prettify-xml --output decoded:text:file:path=/home/pi/hfdl/hfdl.log,rotate=daily --soapysdr driver=airspyhf,serial=1111333355557777 --sample-rate 384000 11184 11306 11312 11318 11321 11348 11387" 5 | -------------------------------------------------------------------------------- /src/metadata.h: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #pragma once 3 | #include // struct timeval 4 | 5 | struct metadata { 6 | struct metadata_vtable *vtable; 7 | struct timeval rx_timestamp; 8 | }; 9 | 10 | struct metadata_vtable { 11 | struct metadata* (*copy)(struct metadata const *); 12 | void (*destroy)(struct metadata *); 13 | }; 14 | 15 | struct metadata *metadata_copy(struct metadata const *m); 16 | void metadata_destroy(struct metadata *m); 17 | -------------------------------------------------------------------------------- /src/kvargs.h: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #pragma once 3 | 4 | #include // ptrdiff_t 5 | 6 | typedef struct kvargs_s kvargs; 7 | 8 | typedef struct { 9 | kvargs *result; 10 | ptrdiff_t err_pos; 11 | int32_t err; 12 | } kvargs_parse_result; 13 | 14 | kvargs *kvargs_new(); 15 | kvargs_parse_result kvargs_from_string(char *string); 16 | char *kvargs_get(kvargs const *kv, char const *key); 17 | char const *kvargs_get_errstr(int32_t err); 18 | void kvargs_destroy(kvargs *kv); 19 | -------------------------------------------------------------------------------- /src/options.h: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #pragma once 3 | #include 4 | 5 | // help text pretty-printing constants and macros 6 | #define USAGE_INDENT_STEP 4 7 | #define USAGE_OPT_NAME_COLWIDTH 48 8 | #define IND(n) (n * USAGE_INDENT_STEP) 9 | 10 | // option name and description to be printed in the help text 11 | typedef struct { 12 | char *name; 13 | char *description; 14 | } option_descr_t; 15 | 16 | void describe_option(char const *name, char const *description, int32_t indent); 17 | -------------------------------------------------------------------------------- /src/ac_data.h: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #pragma once 3 | #include 4 | #include 5 | 6 | typedef struct ac_data ac_data; 7 | 8 | struct ac_data_entry { 9 | char *registration; 10 | char *icaotypecode; 11 | char *operatorflagcode; 12 | char *manufacturer; 13 | char *type; 14 | char *registeredowners; 15 | bool exists; 16 | }; 17 | 18 | ac_data *ac_data_create(char const *bs_db_file); 19 | struct ac_data_entry *ac_data_entry_lookup(ac_data *ac_data, uint32_t icao_address); 20 | void ac_data_destroy(ac_data *ac_data); 21 | -------------------------------------------------------------------------------- /src/acars.h: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #pragma once 3 | 4 | #include 5 | #include // la_proto_node 6 | #include // la_reasm_ctx 7 | #include "pdu.h" // enum hfdl_pdu_direction 8 | #include "position.h" // struct position_info 9 | 10 | la_proto_node *acars_parse(uint8_t *buf, uint32_t len, enum hfdl_pdu_direction direction, 11 | la_reasm_ctx *reasm_ctx, struct timeval rx_timestamp); 12 | struct position_info *acars_position_info_extract(la_proto_node *tree); 13 | -------------------------------------------------------------------------------- /etc/dumphfdl.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=HFDL decoder 3 | Documentation=https://github.com/szpajder/dumphfdl/blob/master/README.md 4 | Wants=network.target 5 | After=network.target 6 | 7 | [Service] 8 | Type=simple 9 | EnvironmentFile=/etc/default/dumphfdl 10 | # If you don't want to run the program as root, then uncomment 11 | # the following line and put a desired user name in it. 12 | # Note that the user must have access to the SDR device. 13 | #User=pi 14 | ExecStart=/usr/local/bin/dumphfdl $DUMPHFDL_OPTIONS 15 | Restart=on-failure 16 | 17 | [Install] 18 | WantedBy=multi-user.target 19 | -------------------------------------------------------------------------------- /src/options.c: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | 3 | #include 4 | #include // fprintf() 5 | #include // strlen 6 | #include "options.h" // USAGE_OPT_NAME_COLWIDTH, USAGE_INDENT_STEP 7 | 8 | void describe_option(char const *name, char const *description, int32_t indent) { 9 | int32_t descr_shiftwidth = USAGE_OPT_NAME_COLWIDTH - (int32_t)strlen(name) - indent * USAGE_INDENT_STEP; 10 | if(descr_shiftwidth < 1) { 11 | descr_shiftwidth = 1; 12 | } 13 | fprintf(stderr, "%*s%s%*s%s\n", IND(indent), "", name, descr_shiftwidth, "", description); 14 | } 15 | 16 | -------------------------------------------------------------------------------- /src/hfdl.h: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #pragma once 3 | #include 4 | #include "block.h" // struct block 5 | 6 | #define SPS 3 7 | #define HFDL_SYMBOL_RATE 1800 8 | #define HFDL_CHANNEL_TRANSITION_BW_HZ 250 9 | 10 | void hfdl_init_globals(void); 11 | struct block *hfdl_channel_create(int32_t sample_rate, int32_t pre_decimation_rate, 12 | float transition_bw, int32_t centerfreq, int32_t frequency); 13 | void hfdl_channel_destroy(struct block *channel_block); 14 | void hfdl_print_summary(void); 15 | int32_t hfdl_nf_stats_thread_start(struct block **channel_block_list, int32_t channel_cnt); 16 | -------------------------------------------------------------------------------- /extras/README.md: -------------------------------------------------------------------------------- 1 | # dumphfdl extras 2 | 3 | - `hfdlgrep` - Perl script for grepping dumphfdl log files. While standard grep displays only matching lines, hfdlgrep shows whole HFDL messages. 4 | 5 | - `log_aggregator.py` - Python script that acts as a ZMQ receiver (server), where several instances of dumphfdl may connect simultaneously. The script aggregates logs received from all dumphfdl instances and writes them to a common log file with optional rotation. Requires `pyzmq` module. Type `./log_aggregator -h` for usage instructions. 6 | 7 | - `multitail-dumphfdl.conf` - an example coloring scheme for dumphfdl log files. To be used with `multitail` program. 8 | -------------------------------------------------------------------------------- /src/hfnpdu.h: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #pragma once 3 | #include 4 | #include // struct timeval 5 | #include // la_proto_node 6 | #include // la_reasm_ctx 7 | #include "pdu.h" // enum hfdl_pdu_direction 8 | #include "position.h" // struct position_info 9 | 10 | la_proto_node *hfnpdu_parse(uint8_t *buf, uint32_t len, enum hfdl_pdu_direction direction, 11 | la_reasm_ctx *reasm_ctx, struct timeval rx_timestamp); 12 | struct position_info *hfnpdu_position_info_extract(la_proto_node *tree); 13 | -------------------------------------------------------------------------------- /src/input-helpers.h: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #pragma once 3 | 4 | #include // size_t 5 | #include // float complex 6 | #include "block.h" // struct circ_buffer 7 | #include "input-common.h" // sample_format, convert_sample_buffer_fun 8 | 9 | size_t get_sample_size(sample_format format); 10 | float get_sample_full_scale_value(sample_format format); 11 | convert_sample_buffer_fun get_sample_converter(sample_format format); 12 | sample_format sample_format_from_string(char const *str); 13 | void complex_samples_produce(struct circ_buffer *circ_buffer, 14 | float complex *samples, size_t num_samples); 15 | -------------------------------------------------------------------------------- /src/lpdu.h: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #pragma once 3 | #include 4 | #include // struct timeval 5 | #include 6 | #include // la_type_descriptor, la_proto_node 7 | #include // la_reasm_ctx 8 | #include "pdu.h" // struct hfdl_pdu_hdr_data 9 | #include "position.h" // struct position_info 10 | 11 | la_proto_node *lpdu_parse(uint8_t *buf, uint32_t len, struct hfdl_pdu_hdr_data 12 | mpdu_header, la_reasm_ctx *reasm_ctx, struct timeval rx_timestamp); 13 | struct position_info *lpdu_position_info_extract(la_proto_node *tree); 14 | -------------------------------------------------------------------------------- /src/CMakeModules/FindLiquidDSP.cmake: -------------------------------------------------------------------------------- 1 | FIND_PATH(LIQUIDDSP_INCLUDE_DIR liquid/liquid.h) 2 | FIND_LIBRARY(LIQUIDDSP_LIBRARIES NAMES liquid) 3 | 4 | IF(LIQUIDDSP_INCLUDE_DIR AND LIQUIDDSP_LIBRARIES) 5 | SET(LIQUIDDSP_FOUND TRUE) 6 | ENDIF(LIQUIDDSP_INCLUDE_DIR AND LIQUIDDSP_LIBRARIES) 7 | 8 | IF(LIQUIDDSP_FOUND) 9 | IF (NOT LiquidDSP_FIND_QUIETLY) 10 | MESSAGE(STATUS "Found liquid includes: ${LIQUIDDSP_INCLUDE_DIR}/liquid/liquid.h") 11 | MESSAGE(STATUS "Found liquid library: ${LIQUIDDSP_LIBRARIES}") 12 | ENDIF (NOT LiquidDSP_FIND_QUIETLY) 13 | ELSE(LIQUIDDSP_FOUND) 14 | IF (LiquidDSP_FIND_REQUIRED) 15 | MESSAGE(FATAL_ERROR "liquid library required but not found") 16 | ENDIF (LiquidDSP_FIND_REQUIRED) 17 | ENDIF(LIQUIDDSP_FOUND) 18 | -------------------------------------------------------------------------------- /src/ac_cache.h: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #pragma once 3 | #include 4 | #include 5 | #include "cache.h" // struct cache_entry 6 | 7 | #define AC_CACHE_TTL_DEFAULT 3600L 8 | #define AC_CACHE_EXPIRATION_INTERVAL 309L 9 | 10 | typedef struct ac_cache ac_cache; 11 | 12 | struct ac_cache_entry { 13 | char *callsign; 14 | uint32_t icao_address; 15 | }; 16 | 17 | ac_cache *ac_cache_create(void); 18 | void ac_cache_entry_create(ac_cache *cache, int32_t freq, 19 | uint8_t id, uint32_t icao_address); 20 | bool ac_cache_entry_delete(ac_cache *cache, int32_t freq, 21 | uint32_t icao_address); 22 | struct ac_cache_entry *ac_cache_entry_lookup(ac_cache *cache, 23 | int32_t freq, uint8_t id); 24 | void ac_cache_destroy(ac_cache *cache); 25 | -------------------------------------------------------------------------------- /src/config.h.in: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #pragma once 3 | 4 | #cmakedefine WITH_SOAPYSDR 5 | #cmakedefine WITH_PROFILING 6 | #cmakedefine WITH_STATSD 7 | #cmakedefine WITH_SQLITE 8 | #cmakedefine WITH_FFTW3F_THREADS 9 | #cmakedefine HAVE_PTHREAD_BARRIERS 10 | #cmakedefine WITH_ZMQ 11 | #cmakedefine WITH_RDKAFKA 12 | #cmakedefine DATADUMPS 13 | #ifdef DATADUMPS 14 | #define COSTAS_DEBUG 15 | #define SYMSYNC_DEBUG 16 | #define CHAN_DEBUG 17 | #define MF_DEBUG 18 | #define AGC_DEBUG 19 | #define EQ_DEBUG 20 | #define CORR_DEBUG 21 | #define DUMP_CONST 22 | #undef DUMP_FFT 23 | #undef FASTDDC_DEBUG 24 | #endif 25 | 26 | #define LIBZMQ_VER_MAJOR_MIN @LIBZMQ_VER_MAJOR_MIN@ 27 | #define LIBZMQ_VER_MINOR_MIN @LIBZMQ_VER_MINOR_MIN@ 28 | #define LIBZMQ_VER_PATCH_MIN @LIBZMQ_VER_PATCH_MIN@ 29 | -------------------------------------------------------------------------------- /src/globals.c: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #include 3 | #include // EXIT_SUCCESS 4 | #include // pthread_mutex_t 5 | #include "globals.h" // dumphfdl_config 6 | #include "systable.h" // systable 7 | #include "ac_cache.h" // ac_cache 8 | #include "ac_data.h" // ac_data 9 | 10 | int32_t do_exit = 0; 11 | int32_t exitcode = EXIT_SUCCESS; 12 | struct dumphfdl_config Config = {0}; 13 | 14 | // Global system table 15 | systable *Systable; 16 | pthread_mutex_t Systable_lock = PTHREAD_MUTEX_INITIALIZER; 17 | 18 | // HFDL ID -> ICAO mapping table 19 | ac_cache *AC_cache; 20 | pthread_mutex_t AC_cache_lock = PTHREAD_MUTEX_INITIALIZER; 21 | 22 | // basestation.sqb cache 23 | ac_data *AC_data; 24 | -------------------------------------------------------------------------------- /src/cache.h: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #pragma once 3 | #include 4 | #include 5 | #include 6 | 7 | struct cache_vtable { 8 | la_hash_func *cache_key_hash; 9 | la_hash_compare_func *cache_key_compare; 10 | void (*cache_key_destroy)(void *key); 11 | void (*cache_entry_data_destroy)(void *entry); 12 | }; 13 | 14 | typedef struct cache cache; 15 | 16 | cache *cache_create(char const *cache_name, struct cache_vtable const *vtable, 17 | time_t ttl, time_t expiration_interval); 18 | bool cache_entry_create(cache *c, void *key, void *value, 19 | time_t created_time); 20 | bool cache_entry_delete(cache *c, void *key); 21 | void *cache_entry_lookup(cache *c, void const *key); 22 | int32_t cache_expire(cache *c, time_t current_timestamp); 23 | void cache_destroy(cache *c); 24 | -------------------------------------------------------------------------------- /src/libfec/fec.h: -------------------------------------------------------------------------------- 1 | /* User include file for libfec 2 | * Copyright 2004, Phil Karn, KA9Q 3 | * May be used under the terms of the GNU Lesser General Public License (LGPL) 4 | */ 5 | 6 | #ifndef _FEC_H_ 7 | #define _FEC_H_ 8 | 9 | /* r=1/2 k=7 convolutional encoder polynomials 10 | * The NASA-DSN convention is to use V27POLYA inverted, then V27POLYB 11 | * The CCSDS/NASA-GSFC convention is to use V27POLYB, then V27POLYA inverted 12 | */ 13 | #define V27POLYA 0x6d 14 | #define V27POLYB 0x4f 15 | 16 | void *create_viterbi27(int len); 17 | void set_viterbi27_polynomial(int polys[2]); 18 | int init_viterbi27(void *vp,int starting_state); 19 | int update_viterbi27_blk(void *vp,unsigned char sym[],int npairs); 20 | int chainback_viterbi27(void *vp, unsigned char *data,unsigned int nbits,unsigned int endstate); 21 | void delete_viterbi27(void *vp); 22 | 23 | #endif /* _FEC_H_ */ 24 | -------------------------------------------------------------------------------- /src/version.cmake: -------------------------------------------------------------------------------- 1 | set (VERSION "char const * const DUMPHFDL_VERSION=\"") 2 | execute_process(COMMAND git describe --always --tags --dirty 3 | OUTPUT_VARIABLE GIT_VERSION 4 | ERROR_QUIET 5 | OUTPUT_STRIP_TRAILING_WHITESPACE) 6 | 7 | if ("${GIT_VERSION}" STREQUAL "") 8 | string(APPEND VERSION "${DUMPHFDL_VERSION}") 9 | elseif("${GIT_VERSION}" MATCHES ".+-g(.+)") 10 | string(APPEND VERSION "${DUMPHFDL_VERSION}-${CMAKE_MATCH_1}") 11 | elseif("${GIT_VERSION}" MATCHES "v(.+)") 12 | string(APPEND VERSION "${CMAKE_MATCH_1}") 13 | else() 14 | string(APPEND VERSION "${DUMPHFDL_VERSION}-${GIT_VERSION}") 15 | endif() 16 | string(APPEND VERSION "\";\n") 17 | 18 | if(EXISTS ${CMAKE_CURRENT_BINARY_DIR}/version.c) 19 | file(READ ${CMAKE_CURRENT_BINARY_DIR}/version.c VERSION_) 20 | else() 21 | set(VERSION_ "") 22 | endif() 23 | 24 | if (NOT "${VERSION}" STREQUAL "${VERSION_}") 25 | file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/version.c "${VERSION}") 26 | endif() 27 | -------------------------------------------------------------------------------- /src/fft.h: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #pragma once 3 | #include 4 | #include 5 | 6 | // FIXME: this should be hidden. 7 | // Need to provide getter function for input pointer (fastddc needs this) 8 | struct fft_plan_s { 9 | int32_t size; 10 | void *input; 11 | void *output; 12 | void *plan; 13 | }; 14 | 15 | #define FFT_THREAD_CNT_DEFAULT 4 16 | 17 | // FIXME: typedef 18 | #define FFT_PLAN_T struct fft_plan_s 19 | 20 | typedef struct fft_thread_ctx_s *fft_thread_ctx_t; 21 | 22 | // fft_fftw.c 23 | void csdr_fft_init(int32_t thread_cnt); 24 | void csdr_fft_destroy(); 25 | FFT_PLAN_T* csdr_make_fft_c2c(int32_t size, float complex *input, 26 | float complex *output, int32_t forward, int32_t benchmark); 27 | void csdr_destroy_fft_c2c(FFT_PLAN_T *plan); 28 | void csdr_fft_execute(FFT_PLAN_T* plan); 29 | 30 | // fft.c 31 | struct block *fft_create(int32_t decimation, float transition_bw); 32 | void fft_destroy(struct block *fft_block); 33 | -------------------------------------------------------------------------------- /src/position.h: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #pragma once 3 | #include 4 | #include 5 | #include // struct tm 6 | #include // la_proto_node 7 | #include "util.h" // struct location 8 | 9 | struct ac_info { 10 | char *flight_id; 11 | uint32_t icao_address; 12 | bool flight_id_present; 13 | bool icao_address_present; 14 | }; 15 | 16 | struct timestamp { 17 | time_t t; 18 | struct tm tm; 19 | // Presence flags for struct tm fields 20 | bool tm_sec_present; 21 | bool tm_min_present; 22 | bool tm_hour_present; 23 | bool tm_date_present; 24 | }; 25 | 26 | struct position { 27 | struct timestamp timestamp; 28 | struct location location; 29 | }; 30 | 31 | struct position_info { 32 | struct ac_info aircraft; 33 | struct position position; 34 | }; 35 | 36 | struct position_info *position_info_create(void); 37 | struct position_info *position_info_extract(la_proto_node *tree); 38 | void position_info_destroy(struct position_info *pos_info); 39 | -------------------------------------------------------------------------------- /src/systable.h: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #pragma once 3 | #include 4 | #include 5 | #include // la_proto_node 6 | 7 | typedef struct systable systable; 8 | 9 | enum systable_err_t { 10 | SYSTABLE_ERR_NONE, 11 | SYSTABLE_ERR_IO, 12 | SYSTABLE_ERR_FILE_PARSE, 13 | SYSTABLE_ERR_VALIDATE 14 | }; 15 | 16 | systable *systable_create(char const *savefile); 17 | bool systable_read_from_file(systable *st, char const *file); 18 | void systable_destroy(systable *st); 19 | 20 | char const *systable_error_text(systable const *st); 21 | enum systable_err_t systable_error_type(systable const *st); 22 | int32_t systable_file_error_line(systable const *st); 23 | 24 | int32_t systable_get_version(systable const *st); 25 | char const *systable_get_station_name(systable const *st, int32_t id); 26 | double systable_get_station_frequency(systable const *st, int32_t gs_id, int32_t freq_id); 27 | bool systable_is_available(systable const *st); 28 | 29 | void systable_store_pdu(systable const *st, int16_t version, uint8_t seq_num, 30 | uint8_t pdu_set_len, uint8_t *buf, uint32_t len); 31 | la_proto_node *systable_process_pdu_set(systable *st); 32 | -------------------------------------------------------------------------------- /extras/multitail-dumphfdl.conf: -------------------------------------------------------------------------------- 1 | colorscheme:dumphfdl 2 | # header coloring 3 | cs_re:cyan:\[ 4 | cs_re:cyan:\] 5 | cs_re_s:blue,,bold: \[([0-9.]+ kHz)\] 6 | cs_re_s:blue,,bold:^\[(....-..-.. ..:..:..(\.[0-9]+)? [A-Z]+)\] 7 | cs_re_s:blue,,bold:\[(.* Hz)\] 8 | cs_re_s:blue,,bold:\[(.* dBFS)\] 9 | cs_re_s:green:\[([2-9][0-9]\.[0-9] dB)\] 10 | cs_re_s:yellow:\[(1[0-9]\.[0-9] dB)\] 11 | cs_re_s:red:\[(-?[0-9]\.[0-9] dB)\] 12 | cs_re_s:green:\[(300 bps)\] 13 | cs_re_s:yellow:\[(600 bps)\] 14 | cs_re_s:yellow,,bold:\[(1200 bps)\] 15 | cs_re_s:red:\[(1800 bps)\] 16 | cs_re_s:green:\[(S)\] 17 | cs_re_s:yellow,,bold:\[(D)\] 18 | # PDU fields 19 | cs_re:yellow,,bold:Downlink .*PDU: 20 | cs_re:yellow:^Uplink .*PDU: 21 | cs_re_s:cyan:AC info: (.*) 22 | cs_re_s:magenta:Flight ID: (.*) 23 | cs_re_s:green:[ct] GS: (.*) 24 | cs_re_s:green:^ +ID: (.*) 25 | cs_re_s:green:^ +GS ID: (.*) 26 | cs_re_s:magenta,,bold:Frequencies in use: (.*) 27 | cs_re_s:magenta,,bold:Heard on: (.*) 28 | # ACARS 29 | cs_re_s:magenta:Reg: ([^ ]+) 30 | cs_re_s:magenta:Reg: .* Flight: (.*) 31 | cs_re:yellow,,bold/underline:WRN/.* 32 | cs_re:yellow,,bold/underline:MPF/.* 33 | cs_re:red,,bold/underline:FLR/.* 34 | # CPDLC 35 | cs_re:yellow:CPDLC Uplink Message: 36 | cs_re:yellow,,bold:CPDLC Downlink Message: 37 | # parsing errors 38 | cs_re:red:^ *-- .* 39 | -------------------------------------------------------------------------------- /src/fft_fftw.c: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #include 3 | #include 4 | #include "fft.h" 5 | #include "util.h" // NEW 6 | #include "config.h" // WITH_FFTW3F_THREADS 7 | 8 | void csdr_fft_init(int32_t thread_cnt) { 9 | #ifdef WITH_FFTW3F_THREADS 10 | fftwf_init_threads(); 11 | fftwf_plan_with_nthreads(thread_cnt); 12 | fprintf(stderr, "Initialized %d FFT threads\n", thread_cnt); 13 | #endif 14 | } 15 | 16 | void csdr_fft_destroy() { 17 | #ifdef WITH_FFTW3F_THREADS 18 | fftwf_cleanup_threads(); 19 | #endif 20 | } 21 | 22 | FFT_PLAN_T* csdr_make_fft_c2c(int32_t size, float complex* input, float complex* output, int32_t forward, int32_t benchmark) { 23 | NEW(FFT_PLAN_T, plan); 24 | // fftwf_complex is binary compatible with float complex 25 | plan->plan = fftwf_plan_dft_1d(size, (fftwf_complex *)input, (fftwf_complex *)output, forward ? FFTW_FORWARD : FFTW_BACKWARD, benchmark ? FFTW_MEASURE : FFTW_ESTIMATE); 26 | plan->size = size; 27 | plan->input = input; 28 | plan->output = output; 29 | return plan; 30 | } 31 | 32 | void csdr_destroy_fft_c2c(FFT_PLAN_T *plan) { 33 | if(plan) { 34 | fftwf_destroy_plan(plan->plan); 35 | XFREE(plan); 36 | } 37 | } 38 | 39 | void csdr_fft_execute(FFT_PLAN_T* plan) { 40 | fftwf_execute(plan->plan); 41 | } 42 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required (VERSION 3.10) 2 | project (dumphfdl C) 3 | 4 | set (DUMPHFDL_VERSION_MAJOR 1) 5 | set (DUMPHFDL_VERSION_MINOR 7) 6 | set (DUMPHFDL_VERSION_PATCH 0) 7 | set (DUMPHFDL_VERSION "${DUMPHFDL_VERSION_MAJOR}.${DUMPHFDL_VERSION_MINOR}.${DUMPHFDL_VERSION_PATCH}") 8 | set (CMAKE_C_STANDARD 11) 9 | set (C_STANDARD_REQUIRED ON) 10 | set (CMAKE_C_EXTENSIONS OFF) 11 | 12 | if(NOT CMAKE_BUILD_TYPE) 13 | set(CMAKE_BUILD_TYPE Release) 14 | message(STATUS "Build type not specified: defaulting to Release") 15 | endif(NOT CMAKE_BUILD_TYPE) 16 | 17 | set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra") 18 | set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -Og -DDEBUG") 19 | 20 | add_subdirectory (src) 21 | 22 | # build a CPack driven installer package 23 | include (InstallRequiredSystemLibraries) 24 | set (CPACK_RESOURCE_FILE_LICENSE 25 | "${CMAKE_CURRENT_SOURCE_DIR}/LICENSE") 26 | set (CPACK_RESOURCE_FILE_README 27 | "${CMAKE_CURRENT_SOURCE_DIR}/README.md") 28 | set (CPACK_PACKAGE_VERSION_MAJOR "${DUMPHFDL_VERSION_MAJOR}") 29 | set (CPACK_PACKAGE_VERSION_MINOR "${DUMPHFDL_VERSION_MINOR}") 30 | set (CPACK_PACKAGE_VERSION_PATCH "${DUMPHFDL_VERSION_PATCH}") 31 | set (CPACK_PACKAGE_CHECKSUM "SHA256") 32 | set (CPACK_STRIP_FILES TRUE) 33 | if (UNIX) 34 | set (CPACK_GENERATOR "TGZ") 35 | elseif (WIN32) 36 | set (CPACK_GENERATOR "ZIP") 37 | endif () 38 | include (CPack) 39 | -------------------------------------------------------------------------------- /src/pdu.h: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #pragma once 3 | #include 4 | #include 5 | #include "metadata.h" // struct metadata 6 | #include "util.h" // struct octet string 7 | 8 | struct hfdl_pdu_metadata { 9 | struct metadata metadata; 10 | int32_t version; 11 | int32_t freq; 12 | int32_t bit_rate; 13 | float freq_err_hz; 14 | float rssi; 15 | float noise_floor; 16 | char slot; // 'S' - single slot frame, 'D' - double slot frame 17 | }; 18 | 19 | enum hfdl_pdu_direction { 20 | UPLINK_PDU = 0, 21 | DOWNLINK_PDU = 1 22 | }; 23 | 24 | // Useful fields extracted from MPDU/SPDU header or PDU metadata 25 | // that need to be passed down below the MPDU layer 26 | struct hfdl_pdu_hdr_data { 27 | int32_t freq; // For ac_cache lookups during LPDU formatting 28 | uint8_t src_id; // GS ID for uplinks, AC ID for downlinks 29 | uint8_t dst_id; // AC ID for uplinks, GS ID for downlinks 30 | enum hfdl_pdu_direction direction; 31 | bool crc_ok; 32 | }; 33 | 34 | void hfdl_pdu_decoder_init(void); 35 | int32_t hfdl_pdu_decoder_start(void *ctx); 36 | void hfdl_pdu_decoder_stop(void); 37 | bool hfdl_pdu_decoder_is_running(void); 38 | bool hfdl_pdu_fcs_check(uint8_t *buf, uint32_t hdr_len); 39 | void pdu_decoder_queue_push(struct metadata *metadata, struct octet_string *pdu, uint32_t flags); 40 | struct metadata *hfdl_pdu_metadata_create(); 41 | -------------------------------------------------------------------------------- /src/globals.h: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #pragma once 3 | 4 | #include 5 | #include 6 | #include 7 | #include "config.h" // DATADUMPS 8 | #include "systable.h" 9 | #include "ac_cache.h" 10 | #include "ac_data.h" 11 | 12 | enum ac_data_details { 13 | AC_DETAILS_NORMAL = 0, 14 | AC_DETAILS_VERBOSE = 1 15 | }; 16 | 17 | // global config 18 | struct dumphfdl_config { 19 | #ifdef DEBUG 20 | uint32_t debug_filter; 21 | #endif 22 | char *station_id; 23 | int32_t output_queue_hwm; 24 | int32_t nf_stats_interval; 25 | int32_t ac_cache_ttl; 26 | enum ac_data_details ac_data_details; 27 | bool utc; 28 | bool milliseconds; 29 | bool output_raw_frames; 30 | bool output_mpdus; 31 | bool output_corrupted_pdus; 32 | bool freq_as_squawk; 33 | bool ac_data_available; 34 | #ifdef DATADUMPS 35 | bool datadumps; 36 | #endif 37 | }; 38 | 39 | #define STATION_ID_LEN_MAX 255 40 | 41 | extern struct dumphfdl_config Config; 42 | extern int32_t do_exit; 43 | extern int32_t exitcode; 44 | 45 | // version.c 46 | extern char const * const DUMPHFDL_VERSION; 47 | 48 | extern systable *Systable; 49 | extern pthread_mutex_t Systable_lock; 50 | #define Systable_lock() do { pthread_mutex_lock(&Systable_lock); } while(0) 51 | #define Systable_unlock() do { pthread_mutex_unlock(&Systable_lock); } while(0) 52 | 53 | extern ac_cache *AC_cache; 54 | extern pthread_mutex_t AC_cache_lock; 55 | #define AC_cache_lock() do { pthread_mutex_lock(&AC_cache_lock); } while(0) 56 | #define AC_cache_unlock() do { pthread_mutex_unlock(&AC_cache_lock); } while(0) 57 | 58 | extern ac_data *AC_data; 59 | -------------------------------------------------------------------------------- /src/fastddc.h: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #pragma once 3 | #include 4 | #include 5 | #include "fft.h" // FFT_PLAN_T 6 | #include "libcsdr_gpl.h" // shift_addition_data_t, decimating_shift_addition_status_t 7 | 8 | typedef struct fastddc_s 9 | { 10 | int32_t pre_decimation; 11 | int32_t post_decimation; 12 | int32_t taps_length; 13 | int32_t taps_min_length; 14 | int32_t overlap_length; //it is taps_length - 1 15 | int32_t fft_size; 16 | int32_t fft_inv_size; 17 | int32_t input_size; 18 | int32_t post_input_size; 19 | float pre_shift; 20 | int32_t startbin; //for pre_shift 21 | int32_t v; //step for pre_shift 22 | int32_t offsetbin; 23 | float post_shift; 24 | int32_t output_scrape; 25 | int32_t scrap; 26 | shift_addition_data_t dsadata; 27 | } fastddc_t; 28 | 29 | typedef struct { 30 | fastddc_t *ddc; 31 | FFT_PLAN_T *inv_plan; 32 | float complex *inv_input, *inv_output; 33 | float complex *filtertaps_fft; 34 | decimating_shift_addition_status_t shift_status; 35 | } fft_channelizer_s; 36 | typedef fft_channelizer_s *fft_channelizer; 37 | 38 | int32_t fastddc_init(fastddc_t *ddc, float transition_bw, int32_t decimation, float shift_rate); 39 | decimating_shift_addition_status_t fastddc_inv_cc(float complex *input, float complex *output, fastddc_t *ddc, FFT_PLAN_T *plan_inverse, float complex *taps_fft, decimating_shift_addition_status_t shift_stat); 40 | void fastddc_print(fastddc_t *ddc, char *source); 41 | void fft_swap_sides(float complex *io, int32_t fft_size); 42 | fft_channelizer fft_channelizer_create(int32_t decimation, float transition_bw, float freq_shift); 43 | void fft_channelizer_destroy(fft_channelizer c); 44 | -------------------------------------------------------------------------------- /src/libcsdr_gpl.h: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of libcsdr. 3 | 4 | Copyright (c) Andras Retzler, HA7ILM 5 | Copyright (c) Warren Pratt, NR0V 6 | Copyright 2006,2010,2012 Free Software Foundation, Inc. 7 | 8 | libcsdr is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | libcsdr is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with libcsdr. If not, see . 20 | */ 21 | 22 | #pragma once 23 | #include 24 | #include 25 | 26 | typedef struct shift_addition_data_s 27 | { 28 | float sindelta; 29 | float cosdelta; 30 | float rate; 31 | } shift_addition_data_t; 32 | 33 | shift_addition_data_t shift_addition_init(float rate); 34 | 35 | typedef struct decimating_shift_addition_status_s 36 | { 37 | int32_t decimation_remain; 38 | float starting_phase; 39 | int32_t output_size; 40 | } decimating_shift_addition_status_t; 41 | 42 | decimating_shift_addition_status_t decimating_shift_addition_cc(float complex 43 | *input, float complex* output, int32_t input_size, shift_addition_data_t d, 44 | int32_t decimation, decimating_shift_addition_status_t s); 45 | shift_addition_data_t decimating_shift_addition_init(float rate, int 46 | decimation); 47 | 48 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: CMake 2 | 3 | on: 4 | [ push, pull_request ] 5 | 6 | env: 7 | BUILD_TYPE: Release 8 | 9 | jobs: 10 | build: 11 | strategy: 12 | matrix: 13 | os: [ ubuntu-24.04, ubuntu-22.04, macos-15, macos-14 ] 14 | runs-on: ${{ matrix.os }} 15 | env: 16 | HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: 1 17 | HOMEBREW_NO_INSTALL_UPGRADE: 1 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@v4 21 | 22 | - name: Install packaged dependencies 23 | run: | 24 | if [[ "$RUNNER_OS" == "Linux" ]]; then sudo apt-get update; fi 25 | if [[ "$RUNNER_OS" == "Linux" ]]; then sudo apt-get install libliquid-dev libglib2.0-dev libfftw3-dev libsoapysdr-dev libconfig++-dev; fi 26 | if [[ "$RUNNER_OS" == "macOS" ]]; then brew update; fi 27 | if [[ "$RUNNER_OS" == "macOS" ]]; then brew install glib liquid-dsp fftw soapysdr libconfig; fi 28 | 29 | - name: Install libacars 30 | run: | 31 | cd "$RUNNER_TEMP" 32 | git clone https://github.com/szpajder/libacars.git 33 | cd libacars 34 | mkdir build 35 | cd build 36 | cmake .. 37 | make -j 38 | sudo make install 39 | if [[ "$RUNNER_OS" == "Linux" ]]; then sudo ldconfig; fi 40 | if [[ "$RUNNER_OS" == "macOS" ]]; then sudo update_dyld_shared_cache; fi 41 | 42 | - name: Configure CMake 43 | run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} 44 | 45 | - name: Build 46 | run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} 47 | 48 | - name: Install 49 | run: sudo cmake --install ${{github.workspace}}/build 50 | 51 | - name: Test run 52 | run: /usr/local/bin/dumphfdl --help 53 | -------------------------------------------------------------------------------- /src/input-common.h: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #pragma once 3 | #include 4 | #include // size_t 5 | #include "config.h" 6 | #include "block.h" // struct block, struct producer 7 | 8 | typedef enum { 9 | INPUT_TYPE_UNDEF, 10 | #ifdef WITH_SOAPYSDR 11 | INPUT_TYPE_SOAPYSDR, 12 | #endif 13 | INPUT_TYPE_FILE, 14 | INPUT_TYPE_MAX 15 | } input_type; 16 | 17 | typedef enum { 18 | SFMT_UNDEF = 0, 19 | SFMT_CU8, 20 | SFMT_CS16, 21 | SFMT_CF32, 22 | SFMT_MAX 23 | } sample_format; 24 | 25 | #define AUTO_GAIN -100 26 | 27 | struct input_cfg { 28 | char *source; 29 | char *gain_elements; 30 | char *antenna; 31 | char *device_settings; 32 | double gain; 33 | double correction; 34 | int32_t sample_rate; 35 | int32_t centerfreq; 36 | int32_t freq_offset; 37 | int32_t read_buffer_size; 38 | input_type type; 39 | sample_format sfmt; 40 | }; 41 | 42 | struct input; // forward declaration 43 | 44 | struct input_vtable { 45 | struct input *(*create)(struct input_cfg *); 46 | int32_t (*init)(struct input *); 47 | void (*destroy)(struct input *); 48 | void* (*rx_thread_routine)(void *); 49 | }; 50 | 51 | typedef void (*convert_sample_buffer_fun)(struct input *, void *, size_t, float complex *); 52 | 53 | struct input { 54 | struct block block; 55 | struct input_vtable *vtable; 56 | struct input_cfg *config; 57 | convert_sample_buffer_fun convert_sample_buffer; 58 | size_t overflow_count; // TODO: replace with statsd 59 | float full_scale; 60 | int32_t bytes_per_sample; 61 | }; 62 | 63 | struct input_cfg *input_cfg_create(); 64 | void input_cfg_destroy(struct input_cfg *cfg); 65 | struct block *input_create(struct input_cfg *cfg); 66 | int32_t input_init(struct block *block); 67 | void input_destroy(struct block *block); 68 | -------------------------------------------------------------------------------- /src/dumpfile.h: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #pragma once 3 | #include 4 | #include 5 | #include 6 | #include "config.h" 7 | 8 | typedef struct dumpfile_rf32 *dumpfile_rf32; 9 | typedef struct dumpfile_cf32 *dumpfile_cf32; 10 | 11 | #ifdef DATADUMPS 12 | 13 | dumpfile_rf32 do_dumpfile_rf32_open(char const *name, float fillval); 14 | void do_dumpfile_rf32_write_value(dumpfile_rf32 f, uint64_t time, float val); 15 | void do_dumpfile_rf32_destroy(dumpfile_rf32 f); 16 | 17 | dumpfile_cf32 do_dumpfile_cf32_open(char const *name, float complex fillval); 18 | void do_dumpfile_cf32_write_value(dumpfile_cf32 f, uint64_t time, 19 | float complex val); 20 | void do_dumpfile_cf32_write_block(dumpfile_cf32 f, uint64_t time, 21 | float complex *buf, size_t len); 22 | void do_dumpfile_cf32_destroy(dumpfile_cf32 f); 23 | 24 | #define dumpfile_rf32_open(name, fillval) do_dumpfile_rf32_open(name, fillval) 25 | #define dumpfile_rf32_write_value(f, time, val) \ 26 | do_dumpfile_rf32_write_value(f, time, val) 27 | #define dumpfile_rf32_destroy(f) do_dumpfile_rf32_destroy(f) 28 | 29 | #define dumpfile_cf32_open(name, fillval) do_dumpfile_cf32_open(name, fillval) 30 | #define dumpfile_cf32_write_value(f, time, val) \ 31 | do_dumpfile_cf32_write_value(f, time, val) 32 | #define dumpfile_cf32_write_block(f, time, buf, len) \ 33 | do_dumpfile_cf32_write_block(f, time, buf, len) 34 | #define dumpfile_cf32_destroy(f) do_dumpfile_cf32_destroy(f) 35 | 36 | #else 37 | 38 | #define dumpfile_rf32_open(name, fillval) NULL 39 | #define dumpfile_rf32_write_value(f, time, val) nop() 40 | #define dumpfile_rf32_destroy(f) nop() 41 | 42 | #define dumpfile_cf32_open(name, fillval) NULL 43 | #define dumpfile_cf32_write_value(f, time, val) nop() 44 | #define dumpfile_cf32_write_block(f, time, buf, len) nop() 45 | #define dumpfile_cf32_destroy(f) nop() 46 | 47 | #endif 48 | -------------------------------------------------------------------------------- /src/statsd.h: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #pragma once 3 | #include 4 | #include // struct timeval 5 | #include // la_msg_dir 6 | #include "config.h" // WITH_STATSD 7 | #include "util.h" // nop() 8 | 9 | #ifdef WITH_STATSD 10 | int32_t statsd_initialize(char *statsd_addr); 11 | void statsd_initialize_counters_per_channel(int32_t freq); 12 | void statsd_initialize_counters_per_msgdir(); 13 | void statsd_initialize_counter_set(char **counter_set); 14 | // Can't have char const * pointers here, because statsd-c-client 15 | // may potentially modify their contents 16 | void statsd_counter_per_channel_increment(int32_t freq, char *counter); 17 | void statsd_timing_delta_per_channel_send(int32_t freq, char *timer, struct timeval ts); 18 | void statsd_counter_per_msgdir_increment(la_msg_dir msg_dir, char *counter); 19 | void statsd_counter_increment(char *counter); 20 | void statsd_gauge_set(char *gauge, size_t value); 21 | void statsd_gauge_per_channel_set(int32_t freq, char *gauge, size_t value); 22 | 23 | #define statsd_increment_per_channel(freq, counter) statsd_counter_per_channel_increment(freq, counter) 24 | #define statsd_timing_delta_per_channel(freq, timer, start) statsd_timing_delta_per_channel_send(freq, timer, start) 25 | #define statsd_increment_per_msgdir(counter, msgdir) statsd_counter_per_msgdir_increment(counter, msgdir) 26 | #define statsd_increment(counter) statsd_counter_increment(counter) 27 | #define statsd_set(gauge, value) statsd_gauge_set(gauge, value) 28 | #define statsd_set_per_channel(freq, gauge, value) statsd_gauge_per_channel_set(freq, gauge, value) 29 | #else 30 | #define statsd_increment_per_channel(freq, counter) nop() 31 | #define statsd_timing_delta_per_channel(freq, timer, start) nop() 32 | #define statsd_increment_per_msgdir(counter, msgdir) nop() 33 | #define statsd_increment(counter) nop() 34 | #define statsd_set(gauge, value) nop() 35 | #define statsd_set_per_channel(freq, gauge, value) nop() 36 | #endif 37 | -------------------------------------------------------------------------------- /src/pthread_barrier.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015, Aleksey Demakov 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions are met: 7 | * 8 | * * Redistributions of source code must retain the above copyright notice, this 9 | * list of conditions and the following disclaimer. 10 | * 11 | * * Redistributions in binary form must reproduce the above copyright notice, 12 | * this list of conditions and the following disclaimer in the documentation 13 | * and/or other materials provided with the distribution. 14 | * 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | */ 26 | 27 | #pragma once 28 | #include 29 | 30 | #ifdef __cplusplus 31 | extern "C" { 32 | #endif 33 | 34 | #if !defined(PTHREAD_BARRIER_SERIAL_THREAD) 35 | #define PTHREAD_BARRIER_SERIAL_THREAD (1) 36 | #endif 37 | 38 | typedef struct { 39 | } pthread_barrierattr_t; 40 | 41 | typedef struct { 42 | pthread_mutex_t mutex; 43 | pthread_cond_t cond; 44 | unsigned int limit; 45 | unsigned int count; 46 | unsigned int phase; 47 | } pthread_barrier_t; 48 | 49 | int pthread_barrier_init(pthread_barrier_t *restrict barrier, 50 | const pthread_barrierattr_t *restrict attr, unsigned int count); 51 | 52 | int pthread_barrier_wait(pthread_barrier_t *barrier); 53 | 54 | #ifdef __cplusplus 55 | } 56 | #endif 57 | -------------------------------------------------------------------------------- /src/CMakeModules/FindSQLite3.cmake: -------------------------------------------------------------------------------- 1 | # Distributed under the OSI-approved BSD 3-Clause License. See accompanying 2 | # file Copyright.txt or https://cmake.org/licensing for details. 3 | 4 | #[=======================================================================[.rst: 5 | FindSQLite3 6 | ----------- 7 | 8 | .. versionadded:: 3.14 9 | 10 | Find the SQLite libraries, v3 11 | 12 | IMPORTED targets 13 | ^^^^^^^^^^^^^^^^ 14 | 15 | This module defines the following :prop_tgt:`IMPORTED` target: 16 | 17 | ``SQLite::SQLite3`` 18 | 19 | Result variables 20 | ^^^^^^^^^^^^^^^^ 21 | 22 | This module will set the following variables if found: 23 | 24 | ``SQLite3_INCLUDE_DIRS`` 25 | where to find sqlite3.h, etc. 26 | ``SQLite3_LIBRARIES`` 27 | the libraries to link against to use SQLite3. 28 | ``SQLite3_VERSION`` 29 | version of the SQLite3 library found 30 | ``SQLite3_FOUND`` 31 | TRUE if found 32 | 33 | #]=======================================================================] 34 | 35 | # Look for the necessary header 36 | find_path(SQLite3_INCLUDE_DIR NAMES sqlite3.h) 37 | mark_as_advanced(SQLite3_INCLUDE_DIR) 38 | 39 | # Look for the necessary library 40 | find_library(SQLite3_LIBRARY NAMES sqlite3 sqlite) 41 | mark_as_advanced(SQLite3_LIBRARY) 42 | 43 | # Extract version information from the header file 44 | if(SQLite3_INCLUDE_DIR) 45 | file(STRINGS ${SQLite3_INCLUDE_DIR}/sqlite3.h _ver_line 46 | REGEX "^#define SQLITE_VERSION *\"[0-9]+\\.[0-9]+\\.[0-9]+\"" 47 | LIMIT_COUNT 1) 48 | string(REGEX MATCH "[0-9]+\\.[0-9]+\\.[0-9]+" 49 | SQLite3_VERSION "${_ver_line}") 50 | unset(_ver_line) 51 | endif() 52 | 53 | include(FindPackageHandleStandardArgs) 54 | find_package_handle_standard_args(SQLite3 55 | REQUIRED_VARS SQLite3_INCLUDE_DIR SQLite3_LIBRARY 56 | VERSION_VAR SQLite3_VERSION) 57 | 58 | # Create the imported target 59 | if(SQLite3_FOUND) 60 | set(SQLite3_INCLUDE_DIRS ${SQLite3_INCLUDE_DIR}) 61 | set(SQLite3_LIBRARIES ${SQLite3_LIBRARY}) 62 | if(NOT TARGET SQLite::SQLite3) 63 | add_library(SQLite::SQLite3 UNKNOWN IMPORTED) 64 | set_target_properties(SQLite::SQLite3 PROPERTIES 65 | IMPORTED_LOCATION "${SQLite3_LIBRARY}" 66 | INTERFACE_INCLUDE_DIRECTORIES "${SQLite3_INCLUDE_DIR}") 67 | endif() 68 | endif() 69 | -------------------------------------------------------------------------------- /src/libcsdr.h: -------------------------------------------------------------------------------- 1 | /* 2 | This software is part of libcsdr, a set of simple DSP routines for 3 | Software Defined Radio. 4 | 5 | Copyright (c) 2014, Andras Retzler 6 | All rights reserved. 7 | 8 | Redistribution and use in source and binary forms, with or without 9 | modification, are permitted provided that the following conditions are met: 10 | * Redistributions of source code must retain the above copyright 11 | notice, this list of conditions and the following disclaimer. 12 | * Redistributions in binary form must reproduce the above copyright 13 | notice, this list of conditions and the following disclaimer in the 14 | documentation and/or other materials provided with the distribution. 15 | * Neither the name of the copyright holder nor the 16 | names of its contributors may be used to endorse or promote products 17 | derived from this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 20 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL ANDRAS RETZLER BE LIABLE FOR ANY 23 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 24 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 26 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | */ 30 | 31 | #pragma once 32 | 33 | #include 34 | #include 35 | 36 | //window 37 | typedef enum window_s 38 | { 39 | WINDOW_BOXCAR, WINDOW_BLACKMAN, WINDOW_HAMMING 40 | } window_t; 41 | 42 | #define WINDOW_DEFAULT WINDOW_HAMMING 43 | 44 | int32_t next_pow2(int32_t x); 45 | int32_t firdes_filter_len(float transition_bw); 46 | void firdes_bandpass_c(float complex *output, int32_t length, float lowcut, float highcut, window_t window); 47 | float compute_filter_relative_transition_bw(int32_t sample_rate, int32_t transition_bw_Hz); 48 | int32_t compute_fft_decimation_rate(int32_t sample_rate, int32_t target_rate); 49 | -------------------------------------------------------------------------------- /src/block.h: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #pragma once 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "config.h" 9 | #ifndef HAVE_PTHREAD_BARRIERS 10 | #include "pthread_barrier.h" 11 | #endif 12 | 13 | enum producer_type { 14 | PRODUCER_NONE = 0, 15 | PRODUCER_SINGLE, 16 | PRODUCER_MULTI, 17 | PRODUCER_MAX 18 | }; 19 | 20 | enum consumer_type { 21 | CONSUMER_NONE = 0, 22 | CONSUMER_SINGLE, 23 | CONSUMER_MULTI, 24 | CONSUMER_MAX 25 | }; 26 | 27 | struct circ_buffer { 28 | cbuffercf buf; 29 | pthread_cond_t *cond; 30 | pthread_mutex_t *mutex; 31 | }; 32 | 33 | struct shared_buffer { 34 | float complex *buf; 35 | pthread_barrier_t *data_ready; 36 | pthread_barrier_t *consumers_ready; 37 | }; 38 | 39 | struct block_connection { 40 | union { 41 | struct circ_buffer circ_buffer; 42 | struct shared_buffer shared_buffer; 43 | }; 44 | uint32_t flags; 45 | }; 46 | 47 | // Block connection flags 48 | #define BLOCK_CONNECTION_SHUTDOWN (1 << 0) 49 | 50 | struct producer { 51 | struct block_connection *out; 52 | size_t max_tu; // maximum transmission unit (samples) 53 | enum producer_type type; 54 | }; 55 | 56 | struct consumer { 57 | struct block_connection *in; 58 | size_t min_ru; // minimum receive unit (samples) 59 | enum consumer_type type; 60 | }; 61 | 62 | struct block { 63 | struct consumer consumer; 64 | struct producer producer; 65 | pthread_t thread; 66 | void *(*thread_routine)(void *); 67 | bool running; 68 | }; 69 | 70 | // block.c 71 | int32_t block_connect_one2one(struct block *source, struct block *sink); 72 | int32_t block_connect_one2many(struct block *source, size_t sink_count, struct block *sinks[sink_count]); 73 | void block_disconnect_one2one(struct block *source, struct block *sink); 74 | void block_disconnect_one2many(struct block *source, size_t sink_count, struct block *sinks[sink_count]); 75 | int32_t block_start(struct block *block); 76 | int32_t block_set_start(size_t block_cnt, struct block *block[block_cnt]); 77 | void block_connection_one2one_shutdown(struct block_connection *connection); 78 | void block_connection_one2many_shutdown(struct block_connection *connection); 79 | bool block_connection_is_shutdown_signaled(struct block_connection *connection); 80 | bool block_is_running(struct block *block); 81 | bool block_set_is_any_running(size_t block_cnt, struct block *blocks[block_cnt]); 82 | -------------------------------------------------------------------------------- /src/kvargs.c: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #include // ptrdiff_t 3 | #include // strsep 4 | #include // la_hash 5 | #include // la_dict 6 | #include "kvargs.h" 7 | #include "util.h" // NEW, XFREE, debug_print 8 | 9 | struct kvargs_s { 10 | la_hash *h; 11 | }; 12 | 13 | #define KV_ERR_NO_ERROR 0 14 | #define KV_ERR_NO_INPUT 1 15 | #define KV_ERR_NO_KEY 2 16 | #define KV_ERR_NO_VALUE 3 17 | 18 | char const *kvargs_get_errstr(int32_t err) { 19 | static la_dict const kvargs_error_strings[] = { 20 | { .id = KV_ERR_NO_ERROR, .val = "success" }, 21 | { .id = KV_ERR_NO_INPUT, .val = "no key-value string given" }, 22 | { .id = KV_ERR_NO_KEY, .val = "no key name given" }, 23 | { .id = KV_ERR_NO_VALUE, .val = "no value given" }, 24 | { .id = 0, .val = NULL } 25 | }; 26 | char const *ret = la_dict_search(kvargs_error_strings, err); 27 | return (ret != NULL ? ret : "unknown error"); 28 | } 29 | 30 | kvargs *kvargs_new() { 31 | NEW(kvargs, kv); 32 | kv->h = la_hash_new(NULL, NULL, la_simple_free, la_simple_free); 33 | return kv; 34 | } 35 | 36 | kvargs_parse_result kvargs_from_string(char *string) { 37 | kvargs *kv = NULL; 38 | int32_t err = 0; 39 | ptrdiff_t err_pos = 0; 40 | 41 | if(string == NULL) { 42 | err = KV_ERR_NO_INPUT; 43 | goto fail; 44 | } 45 | 46 | kv = kvargs_new(); 47 | char *start = string, *kvpair = NULL, *key = NULL; 48 | 49 | do { 50 | kvpair = strsep(&string, ","); 51 | key = strsep(&kvpair, "="); 52 | if(key[0] == '\0') { 53 | err_pos = key - start; 54 | err = KV_ERR_NO_KEY; 55 | goto fail; 56 | } 57 | // kvpair points at the value now 58 | if(kvpair == NULL) { 59 | err_pos = key + strlen(key) - start; 60 | err = KV_ERR_NO_VALUE; 61 | goto fail; 62 | } 63 | debug_print(D_MISC, "key: '%s' val: '%s'\n", key, kvpair); 64 | la_hash_insert(kv->h, strdup(key), strdup(kvpair)); 65 | } while(string != NULL); 66 | 67 | goto end; 68 | 69 | fail: 70 | debug_print(D_MISC, "kvpair error %d at position %td\n", err, err_pos); 71 | kvargs_destroy(kv); 72 | end: 73 | return (kvargs_parse_result){ 74 | .result = kv, 75 | .err_pos = err_pos, 76 | .err = err 77 | }; 78 | } 79 | 80 | char *kvargs_get(kvargs const *kv, char const *key) { 81 | return (char *)la_hash_lookup(kv->h, key); 82 | } 83 | 84 | void kvargs_destroy(kvargs *kv) { 85 | if(kv != NULL) { 86 | la_hash_destroy(kv->h); 87 | XFREE(kv); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/crc.c: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #include 3 | 4 | uint16_t crc16_ccitt(uint8_t *data, uint32_t len, uint16_t crc_init) { 5 | /* CRC-16-CCITT, poly: 0x1021 */ 6 | static uint16_t const crctable[256] = 7 | { 8 | 0x0000, 0x1189, 0x2312, 0x329B, 0x4624, 0x57AD, 0x6536, 0x74BF, 9 | 0x8C48, 0x9DC1, 0xAF5A, 0xBED3, 0xCA6C, 0xDBE5, 0xE97E, 0xF8F7, 10 | 0x1081, 0x0108, 0x3393, 0x221A, 0x56A5, 0x472C, 0x75B7, 0x643E, 11 | 0x9CC9, 0x8D40, 0xBFDB, 0xAE52, 0xDAED, 0xCB64, 0xF9FF, 0xE876, 12 | 0x2102, 0x308B, 0x0210, 0x1399, 0x6726, 0x76AF, 0x4434, 0x55BD, 13 | 0xAD4A, 0xBCC3, 0x8E58, 0x9FD1, 0xEB6E, 0xFAE7, 0xC87C, 0xD9F5, 14 | 0x3183, 0x200A, 0x1291, 0x0318, 0x77A7, 0x662E, 0x54B5, 0x453C, 15 | 0xBDCB, 0xAC42, 0x9ED9, 0x8F50, 0xFBEF, 0xEA66, 0xD8FD, 0xC974, 16 | 0x4204, 0x538D, 0x6116, 0x709F, 0x0420, 0x15A9, 0x2732, 0x36BB, 17 | 0xCE4C, 0xDFC5, 0xED5E, 0xFCD7, 0x8868, 0x99E1, 0xAB7A, 0xBAF3, 18 | 0x5285, 0x430C, 0x7197, 0x601E, 0x14A1, 0x0528, 0x37B3, 0x263A, 19 | 0xDECD, 0xCF44, 0xFDDF, 0xEC56, 0x98E9, 0x8960, 0xBBFB, 0xAA72, 20 | 0x6306, 0x728F, 0x4014, 0x519D, 0x2522, 0x34AB, 0x0630, 0x17B9, 21 | 0xEF4E, 0xFEC7, 0xCC5C, 0xDDD5, 0xA96A, 0xB8E3, 0x8A78, 0x9BF1, 22 | 0x7387, 0x620E, 0x5095, 0x411C, 0x35A3, 0x242A, 0x16B1, 0x0738, 23 | 0xFFCF, 0xEE46, 0xDCDD, 0xCD54, 0xB9EB, 0xA862, 0x9AF9, 0x8B70, 24 | 0x8408, 0x9581, 0xA71A, 0xB693, 0xC22C, 0xD3A5, 0xE13E, 0xF0B7, 25 | 0x0840, 0x19C9, 0x2B52, 0x3ADB, 0x4E64, 0x5FED, 0x6D76, 0x7CFF, 26 | 0x9489, 0x8500, 0xB79B, 0xA612, 0xD2AD, 0xC324, 0xF1BF, 0xE036, 27 | 0x18C1, 0x0948, 0x3BD3, 0x2A5A, 0x5EE5, 0x4F6C, 0x7DF7, 0x6C7E, 28 | 0xA50A, 0xB483, 0x8618, 0x9791, 0xE32E, 0xF2A7, 0xC03C, 0xD1B5, 29 | 0x2942, 0x38CB, 0x0A50, 0x1BD9, 0x6F66, 0x7EEF, 0x4C74, 0x5DFD, 30 | 0xB58B, 0xA402, 0x9699, 0x8710, 0xF3AF, 0xE226, 0xD0BD, 0xC134, 31 | 0x39C3, 0x284A, 0x1AD1, 0x0B58, 0x7FE7, 0x6E6E, 0x5CF5, 0x4D7C, 32 | 0xC60C, 0xD785, 0xE51E, 0xF497, 0x8028, 0x91A1, 0xA33A, 0xB2B3, 33 | 0x4A44, 0x5BCD, 0x6956, 0x78DF, 0x0C60, 0x1DE9, 0x2F72, 0x3EFB, 34 | 0xD68D, 0xC704, 0xF59F, 0xE416, 0x90A9, 0x8120, 0xB3BB, 0xA232, 35 | 0x5AC5, 0x4B4C, 0x79D7, 0x685E, 0x1CE1, 0x0D68, 0x3FF3, 0x2E7A, 36 | 0xE70E, 0xF687, 0xC41C, 0xD595, 0xA12A, 0xB0A3, 0x8238, 0x93B1, 37 | 0x6B46, 0x7ACF, 0x4854, 0x59DD, 0x2D62, 0x3CEB, 0x0E70, 0x1FF9, 38 | 0xF78F, 0xE606, 0xD49D, 0xC514, 0xB1AB, 0xA022, 0x92B9, 0x8330, 39 | 0x7BC7, 0x6A4E, 0x58D5, 0x495C, 0x3DE3, 0x2C6A, 0x1EF1, 0x0F78 40 | }; 41 | 42 | uint16_t crc = crc_init; 43 | while (len-- > 0) { 44 | crc = (crc >> 8) ^ crctable[(crc ^ *data++) & 0xff]; 45 | } 46 | return crc; 47 | } 48 | -------------------------------------------------------------------------------- /src/pthread_barrier.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015, Aleksey Demakov 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions are met: 7 | * 8 | * * Redistributions of source code must retain the above copyright notice, this 9 | * list of conditions and the following disclaimer. 10 | * 11 | * * Redistributions in binary form must reproduce the above copyright notice, 12 | * this list of conditions and the following disclaimer in the documentation 13 | * and/or other materials provided with the distribution. 14 | * 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 | * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | */ 26 | 27 | #include "pthread_barrier.h" 28 | #include 29 | 30 | #define __unused __attribute__((unused)) 31 | 32 | int pthread_barrier_init(pthread_barrier_t *restrict barrier, 33 | const pthread_barrierattr_t *restrict attr __unused, unsigned count) { 34 | if (count == 0) { 35 | errno = EINVAL; 36 | return -1; 37 | } 38 | 39 | if (pthread_mutex_init(&barrier->mutex, 0) < 0) { 40 | return -1; 41 | } 42 | if (pthread_cond_init(&barrier->cond, 0) < 0) { 43 | int errno_save = errno; 44 | pthread_mutex_destroy(&barrier->mutex); 45 | errno = errno_save; 46 | return -1; 47 | } 48 | 49 | barrier->limit = count; 50 | barrier->count = 0; 51 | barrier->phase = 0; 52 | 53 | return 0; 54 | } 55 | 56 | int pthread_barrier_wait(pthread_barrier_t *barrier) { 57 | pthread_mutex_lock(&barrier->mutex); 58 | barrier->count++; 59 | if (barrier->count >= barrier->limit) { 60 | barrier->phase++; 61 | barrier->count = 0; 62 | pthread_cond_broadcast(&barrier->cond); 63 | pthread_mutex_unlock(&barrier->mutex); 64 | return PTHREAD_BARRIER_SERIAL_THREAD; 65 | } else { 66 | unsigned phase = barrier->phase; 67 | do 68 | pthread_cond_wait(&barrier->cond, &barrier->mutex); 69 | while (phase == barrier->phase); 70 | pthread_mutex_unlock(&barrier->mutex); 71 | return 0; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/fmtr-text.c: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #include 3 | #include // round 4 | #include // strftime, gmtime, localtime 5 | #include // struct timeval 6 | #include // la_proto_node 7 | #include // la_vstring 8 | #include "fmtr-text.h" 9 | #include "output-common.h" // fmtr_descriptor_t 10 | #include "util.h" // struct octet_string, Config, EOL 11 | #include "pdu.h" // struct hfdl_pdu_metadata 12 | 13 | static bool fmtr_text_supports_data_type(fmtr_input_type_t type) { 14 | return(type == FMTR_INTYPE_DECODED_FRAME); 15 | } 16 | 17 | static la_vstring *format_timestamp(struct timeval tv) { 18 | int32_t millis = 0; 19 | if(Config.milliseconds == true) { 20 | millis = round(tv.tv_usec / 1000.0); 21 | if(millis > 999) { 22 | millis -= 1000; 23 | tv.tv_sec++; 24 | } 25 | } 26 | struct tm *tmstruct = (Config.utc == true ? gmtime(&tv.tv_sec) : localtime(&tv.tv_sec)); 27 | 28 | char tbuf[30], tzbuf[8]; 29 | strftime(tbuf, sizeof(tbuf), "%F %T", tmstruct); 30 | strftime(tzbuf, sizeof(tzbuf), "%Z", tmstruct); 31 | 32 | la_vstring *vstr = la_vstring_new(); 33 | la_vstring_append_sprintf(vstr, "%s", tbuf); 34 | if(Config.milliseconds == true) { 35 | la_vstring_append_sprintf(vstr, ".%03d", millis); 36 | } 37 | la_vstring_append_sprintf(vstr, " %s", tzbuf); 38 | return vstr; 39 | } 40 | 41 | static struct octet_string *fmtr_text_format_decoded_msg(struct metadata *metadata, la_proto_node *root) { 42 | ASSERT(metadata != NULL); 43 | ASSERT(root != NULL); 44 | 45 | struct hfdl_pdu_metadata *hm = container_of(metadata, struct hfdl_pdu_metadata, 46 | metadata); 47 | la_vstring *timestamp = format_timestamp(metadata->rx_timestamp); 48 | la_vstring *vstr = la_vstring_new(); 49 | 50 | la_vstring_append_sprintf(vstr, "[%s] [%.1f kHz] [%.1f Hz] [%.1f/%.1f dBFS] [%.1f dB] [%d bps] [%c]", 51 | timestamp->str, 52 | (float)hm->freq / 1000.f, 53 | hm->freq_err_hz, 54 | hm->rssi, 55 | hm->noise_floor, 56 | hm->rssi - hm->noise_floor, 57 | hm->bit_rate, 58 | hm->slot 59 | ); 60 | la_vstring_destroy(timestamp, true); 61 | 62 | EOL(vstr); 63 | 64 | vstr = la_proto_tree_format_text(vstr, root); 65 | EOL(vstr); 66 | struct octet_string *ret = octet_string_new(vstr->str, vstr->len); 67 | la_vstring_destroy(vstr, false); 68 | return ret; 69 | } 70 | 71 | fmtr_descriptor_t fmtr_DEF_text = { 72 | .name = "text", 73 | .description = "Human readable text", 74 | .format_decoded_msg = fmtr_text_format_decoded_msg, 75 | .format_raw_msg = NULL, 76 | .supports_data_type = fmtr_text_supports_data_type, 77 | .output_format = OFMT_TEXT, 78 | }; 79 | -------------------------------------------------------------------------------- /src/fmtr-json.c: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #include 3 | #include // la_proto_node 4 | #include // la_vstring 5 | #include 6 | #include "fmtr-json.h" 7 | #include "output-common.h" // fmtr_descriptor_t 8 | #include "util.h" // struct octet_string, Config, EOL 9 | #include "pdu.h" // struct hfdl_pdu_metadata 10 | 11 | // forward declarations 12 | la_type_descriptor const td_DEF_hfdl_message; 13 | 14 | static void hfdl_format_json(la_vstring *vstr, void const *data) { 15 | ASSERT(vstr); 16 | ASSERT(data); 17 | 18 | struct hfdl_pdu_metadata const *m = data; 19 | la_json_object_start(vstr, "app"); 20 | la_json_append_string(vstr, "name", "dumphfdl"); 21 | la_json_append_string(vstr, "ver", DUMPHFDL_VERSION); 22 | la_json_object_end(vstr); 23 | if(Config.station_id != NULL) { 24 | la_json_append_string(vstr, "station", Config.station_id); 25 | } 26 | 27 | la_json_object_start(vstr, "t"); 28 | la_json_append_int64(vstr, "sec", m->metadata.rx_timestamp.tv_sec); 29 | la_json_append_int64(vstr, "usec", m->metadata.rx_timestamp.tv_usec); 30 | la_json_object_end(vstr); 31 | 32 | la_json_append_int64(vstr, "freq", m->freq); 33 | la_json_append_int64(vstr, "bit_rate", m->bit_rate); 34 | la_json_append_double(vstr, "sig_level", m->rssi); 35 | la_json_append_double(vstr, "noise_level", m->noise_floor); 36 | la_json_append_double(vstr, "freq_skew", m->freq_err_hz); 37 | la_json_append_char(vstr, "slot", m->slot); 38 | } 39 | 40 | static bool fmtr_json_supports_data_type(fmtr_input_type_t type) { 41 | return(type == FMTR_INTYPE_DECODED_FRAME); 42 | } 43 | 44 | static struct octet_string *fmtr_json_format_decoded_msg(struct metadata *metadata, la_proto_node *root) { 45 | ASSERT(metadata != NULL); 46 | ASSERT(root != NULL); 47 | 48 | // prepend the metadata node the the tree (and destroy it afterwards) 49 | la_proto_node *hfdl_pdu = la_proto_node_new(); 50 | hfdl_pdu->td = &td_DEF_hfdl_message; 51 | hfdl_pdu->data = metadata; 52 | hfdl_pdu->next = root; 53 | 54 | la_vstring *vstr = la_proto_tree_format_json(NULL, hfdl_pdu); 55 | EOL(vstr); 56 | struct octet_string *ret = octet_string_new(vstr->str, vstr->len); 57 | la_vstring_destroy(vstr, false); 58 | XFREE(hfdl_pdu); 59 | return ret; 60 | } 61 | 62 | la_type_descriptor const td_DEF_hfdl_message = { 63 | .format_text = NULL, 64 | .format_json = hfdl_format_json, 65 | .json_key = "hfdl", 66 | .destroy = NULL 67 | }; 68 | 69 | fmtr_descriptor_t fmtr_DEF_json = { 70 | .name = "json", 71 | .description = "Javascript object notation", 72 | .format_decoded_msg = fmtr_json_format_decoded_msg, 73 | .format_raw_msg = NULL, 74 | .supports_data_type = fmtr_json_supports_data_type, 75 | .output_format = OFMT_JSON 76 | }; 77 | -------------------------------------------------------------------------------- /extras/hfdlgrep: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | # 3 | # hfdlgrep - a simple script for grepping dumphfdl log files. 4 | # Prints whole HFDL messages containing (or not containing) given text string. 5 | # 6 | # Copyright (c) Tomasz Lemiech 7 | 8 | use Getopt::Std; 9 | 10 | my %opts; 11 | 12 | use constant { 13 | DIR_UNKNOWN => 0, 14 | DIR_GND2AIR => 1, 15 | DIR_AIR2GND => 2, 16 | DIR_AIR2AIR => 3, 17 | DIR_GND2GND => 4 18 | }; 19 | 20 | sub usage { 21 | print STDERR "$0 - a simple dumphfdl log file grepper\n\n"; 22 | print STDERR "Usage: $0 [options] [ ...]\n"; 23 | print STDERR <) { 52 | if(/^\[\d{4}-\d\d-\d\d \d\d:\d\d:\d\d[ .]/ && @msg > 0) { 53 | print_if_matches(\@msg, $dir, $print, $dont_print); 54 | $print = $dont_print = 0; @msg = (); $dir = DIR_UNKNOWN; 55 | } 56 | if($dir == DIR_UNKNOWN && /^(Up|Down)link .PDU/) { 57 | if($1 eq "Down") { 58 | $dir = DIR_AIR2GND; 59 | } elsif($1 eq "Up") { 60 | $dir = DIR_GND2AIR; 61 | } 62 | } 63 | if(!$print && !$dont_print && /$regex/) { 64 | if($opts{v}) { 65 | $dont_print = 1; 66 | } else { 67 | $print = 1; 68 | } 69 | } 70 | push @msg, $_; 71 | } 72 | print_if_matches(\@msg, $dir, $print, $dont_print); 73 | } 74 | 75 | 76 | getopts('diuv', \%opts) or usage; 77 | $opts{d} and $opts{u} and die "Options: -d and -u are exclusive\n"; 78 | 79 | @ARGV < 1 and usage; 80 | 81 | my $regex = $opts{i} ? qr/$ARGV[0]/i : qr/$ARGV[0]/; 82 | shift @ARGV; 83 | if(@ARGV) { 84 | my $fh; 85 | for my $fname(@ARGV) { 86 | if($fname =~ /\.(gz|bz2|xz|lzma|zst|zip)$/i) { 87 | require IO::Uncompress::AnyUncompress; 88 | unless($fh = IO::Uncompress::AnyUncompress->new($fname)) { 89 | print STDERR "uncompress($fname): failed to initialize descompressor\n"; 90 | next; 91 | } 92 | } else { 93 | unless(open($fh, "<$fname")) { 94 | print STDERR "open($fname): $!\n"; 95 | next; 96 | } 97 | } 98 | grep_filehandle($regex, $fh); 99 | close($fh); 100 | } 101 | } else { 102 | grep_filehandle($regex, *STDIN); 103 | } 104 | -------------------------------------------------------------------------------- /src/libcsdr_gpl.c: -------------------------------------------------------------------------------- 1 | /* 2 | This file is part of libcsdr. 3 | 4 | Copyright (c) Andras Retzler, HA7ILM 5 | Copyright (c) Warren Pratt, NR0V 6 | Copyright 2006,2010,2012 Free Software Foundation, Inc. 7 | 8 | libcsdr is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | libcsdr is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with libcsdr. If not, see . 20 | 21 | */ 22 | 23 | #include // M_PI 24 | #include "libcsdr_gpl.h" 25 | 26 | shift_addition_data_t shift_addition_init(float rate) 27 | { 28 | rate*=2; 29 | shift_addition_data_t out; 30 | out.sindelta=sin(rate*M_PI); 31 | out.cosdelta=cos(rate*M_PI); 32 | out.rate=rate; 33 | return out; 34 | } 35 | 36 | shift_addition_data_t decimating_shift_addition_init(float rate, int32_t decimation) 37 | { 38 | return shift_addition_init(rate*decimation); 39 | } 40 | 41 | decimating_shift_addition_status_t decimating_shift_addition_cc(float complex *input, float complex* output, int32_t input_size, shift_addition_data_t d, int32_t decimation, decimating_shift_addition_status_t s) 42 | { 43 | //The original idea was taken from wdsp: 44 | //http://svn.tapr.org/repos_sdr_hpsdr/trunk/W5WC/PowerSDR_HPSDR_mRX_PS/Source/wdsp/shift.c 45 | //However, this method introduces noise (from floating point rounding errors), which increases until the end of the buffer. 46 | //fprintf(stderr, "cosd=%g sind=%g\n", d.cosdelta, d.sindelta); 47 | float cosphi=cos(s.starting_phase); 48 | float sinphi=sin(s.starting_phase); 49 | float cosphi_last, sinphi_last; 50 | int32_t i; 51 | int32_t k=0; 52 | for(i=s.decimation_remain;iM_PI) s.starting_phase-=2*M_PI; //@shift_addition_cc: normalize starting_phase 72 | while(s.starting_phase<-M_PI) s.starting_phase+=2*M_PI; 73 | return s; 74 | } 75 | -------------------------------------------------------------------------------- /src/input-common.c: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #include 3 | #include // fprintf 4 | #include "config.h" 5 | #include "util.h" // ASSERT, XCALLOC, NEW, container_of 6 | #include "input-common.h" 7 | #include "input-helpers.h" // get_sample_converter 8 | #include "input-file.h" // file_input_vtable 9 | #ifdef WITH_SOAPYSDR 10 | #include "input-soapysdr.h" // soapysdr_input_vtable 11 | #endif 12 | 13 | static struct input_vtable *input_vtables[] = { 14 | [INPUT_TYPE_FILE] = &file_input_vtable, 15 | #ifdef WITH_SOAPYSDR 16 | [INPUT_TYPE_SOAPYSDR] = &soapysdr_input_vtable, 17 | #endif 18 | [INPUT_TYPE_UNDEF] = NULL 19 | }; 20 | 21 | static struct input_vtable *input_vtable_get(input_type type) { 22 | if(type < INPUT_TYPE_MAX) { 23 | return input_vtables[type]; 24 | } else { 25 | return NULL; 26 | } 27 | } 28 | 29 | struct input_cfg *input_cfg_create() { 30 | NEW(struct input_cfg, cfg); 31 | cfg->centerfreq= -1; 32 | cfg->sample_rate = -1; 33 | cfg->read_buffer_size = -1; 34 | cfg->sfmt = SFMT_UNDEF; 35 | cfg->gain = AUTO_GAIN; 36 | return cfg; 37 | } 38 | 39 | void input_cfg_destroy(struct input_cfg *cfg) { 40 | XFREE(cfg); 41 | } 42 | 43 | struct block *input_create(struct input_cfg *cfg) { 44 | if(cfg == NULL) { 45 | return NULL; 46 | } 47 | struct input_vtable *vtable = input_vtable_get(cfg->type); 48 | if(vtable == NULL) { 49 | return NULL; 50 | } 51 | struct input *input = vtable->create(cfg); 52 | if(input == NULL) { 53 | return NULL; 54 | } 55 | struct producer producer = { .type = PRODUCER_SINGLE, .max_tu = 0 }; 56 | struct consumer consumer = { .type = CONSUMER_NONE }; 57 | input->block.producer = producer; 58 | input->block.consumer = consumer; 59 | input->block.thread_routine = vtable->rx_thread_routine; 60 | input->config = cfg; 61 | input->vtable = vtable; 62 | return &input->block; 63 | } 64 | 65 | int32_t input_init(struct block *block) { 66 | ASSERT(block != NULL); 67 | struct input *input = container_of(block, struct input, block); 68 | int32_t ret = input->vtable->init(input); 69 | if(ret < 0) { 70 | goto end; 71 | } 72 | ASSERT(input->bytes_per_sample > 0); 73 | ASSERT(input->full_scale > 0.f); 74 | ASSERT(block->producer.max_tu > 0); 75 | 76 | // Provide sample converter from the native format to complex float 77 | input->convert_sample_buffer = get_sample_converter(input->config->sfmt); 78 | if(input->convert_sample_buffer == NULL) { 79 | fprintf(stderr, "No sample conversion routine found for sample format %d\n", 80 | input->config->sfmt); 81 | ret = -1; 82 | goto end; 83 | } 84 | // TODO: Lookup converters of other, non-native formats supported by the device 85 | 86 | end: 87 | return ret; 88 | } 89 | 90 | void input_destroy(struct block *block) { 91 | if(block != NULL) { 92 | struct input *input = container_of(block, struct input, block); 93 | ASSERT(input != NULL); 94 | if(input->vtable != NULL && input->vtable->destroy != NULL) { 95 | input->vtable->destroy(input); 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/fmtr-basestation.c: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #include 3 | #include // struct tm, strftime 4 | #include "globals.h" // AC_cache, Config 5 | #include "output-common.h" // fmtr_descriptor_t 6 | #include "position.h" // position_info_* 7 | #include "util.h" // ASSERT, XCALLOC, XFREE, struct octet_string 8 | #include "pdu.h" // struct hfdl_pdu_metadata 9 | 10 | #define POSITION_MAX_AGE 300 // seconds 11 | 12 | static bool fmtr_basestation_supports_data_type(fmtr_input_type_t type) { 13 | return(type == FMTR_INTYPE_DECODED_FRAME); 14 | } 15 | 16 | static char *format_timestamp(struct tm *tm) { 17 | ASSERT(tm); 18 | 19 | size_t bufsize = 30; 20 | char *tbuf = XCALLOC(bufsize, sizeof(char)); 21 | strftime(tbuf, bufsize, "%Y/%m/%d,%T.000", tm); 22 | return tbuf; 23 | } 24 | 25 | static struct octet_string *fmtr_basestation_format_decoded_msg(struct metadata *metadata, 26 | la_proto_node *root) { 27 | UNUSED(metadata); 28 | ASSERT(root != NULL); 29 | 30 | struct position_info *pos_info = position_info_extract(root); 31 | if(pos_info == NULL) { 32 | return NULL; 33 | } 34 | 35 | struct octet_string *result = NULL; 36 | time_t now = time(NULL); 37 | if(pos_info->position.timestamp.t > now) { 38 | debug_print(D_MISC, "pos_info rejected: timestamp %ld is in the future\n", 39 | pos_info->position.timestamp.t); 40 | goto cleanup; 41 | } else if(pos_info->position.timestamp.t + POSITION_MAX_AGE < now) { 42 | debug_print(D_MISC, "pos_info (%f, %f) rejected: timestamp %ld too old\n", 43 | pos_info->position.location.lat, 44 | pos_info->position.location.lon, 45 | pos_info->position.timestamp.t); 46 | goto cleanup; 47 | } 48 | 49 | char *timestamp = format_timestamp(&pos_info->position.timestamp.tm); 50 | la_vstring *vstr = la_vstring_new(); 51 | 52 | int32_t frequency = 0; 53 | if(Config.freq_as_squawk == true) { 54 | struct hfdl_pdu_metadata *hm = container_of(metadata, struct hfdl_pdu_metadata, 55 | metadata); 56 | frequency = hm->freq / 1000; 57 | } 58 | la_vstring_append_sprintf(vstr, "MSG,3,1,1,%06X,1,%s,%s,%s,,,,%f,%f,,%d,,,,0\n", 59 | pos_info->aircraft.icao_address, 60 | timestamp, 61 | timestamp, 62 | pos_info->aircraft.flight_id != NULL ? pos_info->aircraft.flight_id : "", 63 | pos_info->position.location.lat, 64 | pos_info->position.location.lon, 65 | frequency 66 | ); 67 | 68 | XFREE(timestamp); 69 | 70 | result = octet_string_new(vstr->str, vstr->len); 71 | la_vstring_destroy(vstr, false); 72 | cleanup: 73 | position_info_destroy(pos_info); 74 | return result; 75 | } 76 | 77 | fmtr_descriptor_t fmtr_DEF_basestation = { 78 | .name = "basestation", 79 | .description = "Position data in Basestation format (CSV)", 80 | .format_decoded_msg = fmtr_basestation_format_decoded_msg, 81 | .format_raw_msg = NULL, 82 | .supports_data_type = fmtr_basestation_supports_data_type, 83 | .output_format = OFMT_BASESTATION, 84 | }; 85 | -------------------------------------------------------------------------------- /etc/systable.conf: -------------------------------------------------------------------------------- 1 | version = 52; 2 | stations = ( 3 | { 4 | id = 1; 5 | lat = 38.384587; 6 | lon = -121.759647; 7 | frequencies = ( 21934.0, 17919.0, 13276.0, 11327.0, 10081.0, 8927.0, 6559.0, 5508.0 ); 8 | name = "San Francisco, California"; 9 | }, 10 | { 11 | id = 2; 12 | lat = 21.184428; 13 | lon = -157.186846; 14 | frequencies = ( 21937.0, 17919.0, 13324.0, 13312.0, 13276.0, 11348.0, 11312.0, 10027.0, 8936.0, 8912.0, 6565.0, 5514.0 ); 15 | name = "Molokai, Hawaii"; 16 | }, 17 | { 18 | id = 3; 19 | lat = 63.847168; 20 | lon = -22.455754; 21 | frequencies = ( 17985.0, 15025.0, 11184.0, 8977.0, 6712.0, 5720.0, 3900.0 ); 22 | name = "Reykjavik, Iceland"; 23 | }, 24 | { 25 | id = 4; 26 | lat = 40.881922; 27 | lon = -72.63762; 28 | frequencies = ( 21931.0, 17919.0, 13276.0, 11387.0, 8912.0, 6661.0, 5652.0 ); 29 | name = "Riverhead, New York"; 30 | }, 31 | { 32 | id = 5; 33 | lat = -37.015757; 34 | lon = 174.809637; 35 | frequencies = ( 17916.0, 13351.0, 10084.0, 8921.0, 6535.0, 5583.0 ); 36 | name = "Auckland, New Zealand"; 37 | }, 38 | { 39 | id = 6; 40 | lat = 6.937536; 41 | lon = 100.388451; 42 | frequencies = ( 21949.0, 17928.0, 13270.0, 10066.0, 8825.0, 6535.0, 5655.0 ); 43 | name = "Hat Yai, Thailand"; 44 | }, 45 | { 46 | id = 7; 47 | lat = 52.744089; 48 | lon = -8.926752; 49 | frequencies = ( 11384.0, 10081.0, 8942.0, 8843.0, 6532.0, 5547.0, 3455.0, 2998.0 ); 50 | name = "Shannon, Ireland"; 51 | }, 52 | { 53 | id = 8; 54 | lat = -26.129658; 55 | lon = 28.206078; 56 | frequencies = ( 21949.0, 17922.0, 13321.0, 11321.0, 8834.0, 5529.0, 4681.0, 3016.0 ); 57 | name = "Johannesburg, South Africa"; 58 | }, 59 | { 60 | id = 9; 61 | lat = 71.25849; 62 | lon = -156.577447; 63 | frequencies = ( 21937.0, 21928.0, 17934.0, 17919.0, 11354.0, 10093.0, 10027.0, 8936.0, 8927.0, 6646.0, 5544.0, 5538.0, 5529.0, 4687.0, 4654.0, 3497.0, 3007.0, 2992.0, 2944.0 ); 64 | name = "Barrow, Alaska"; 65 | }, 66 | { 67 | id = 10; 68 | lat = 35.032377; 69 | lon = 126.238644; 70 | frequencies = ( 21931.0, 17958.0, 13342.0, 10060.0, 8939.0, 6619.0, 5502.0, 2941.0 ); 71 | name = "Muan, South Korea"; 72 | }, 73 | { 74 | id = 11; 75 | lat = 9.084681; 76 | lon = -79.373969; 77 | frequencies = ( 17901.0, 13264.0, 10063.0, 8894.0, 6589.0, 5589.0 ); 78 | name = "Albrook, Panama"; 79 | }, 80 | { 81 | id = 13; 82 | lat = -17.671199; 83 | lon = -63.157088; 84 | frequencies = ( 21997.0, 17916.0, 13315.0, 11318.0, 8957.0, 6628.0, 4660.0 ); 85 | name = "Santa Cruz, Bolivia"; 86 | }, 87 | { 88 | id = 14; 89 | lat = 56.152603; 90 | lon = 92.583337; 91 | frequencies = ( 21990.0, 17912.0, 13321.0, 10087.0, 8886.0, 6596.0, 5622.0 ); 92 | name = "Krasnoyarsk, Russia"; 93 | }, 94 | { 95 | id = 15; 96 | lat = 26.308529; 97 | lon = 50.472318; 98 | frequencies = ( 21982.0, 17967.0, 13312.0, 10030.0, 8885.0, 6646.0, 5544.0, 2986.0 ); 99 | name = "Al Muharraq, Bahrain"; 100 | }, 101 | { 102 | id = 16; 103 | lat = 13.488833; 104 | lon = 144.828233; 105 | frequencies = ( 21928.0, 17919.0, 13312.0, 11306.0, 8927.0, 6652.0, 5451.0 ); 106 | name = "Agana, Guam"; 107 | }, 108 | { 109 | id = 17; 110 | lat = 27.960945; 111 | lon = -15.405608; 112 | frequencies = ( 21955.0, 17928.0, 13303.0, 11348.0, 8948.0, 6529.0 ); 113 | name = "Canarias, Spain"; 114 | } ); 115 | -------------------------------------------------------------------------------- /extras/log_aggregator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | #SPDX-License-Identifier: GPL-3.0-or-later 3 | # 4 | # Receive logs from multiple sources on ZMQ server socket and write them into a common, optionally rotated file. 5 | # 6 | # (c) Tomasz Lemiech 7 | # 8 | import argparse 9 | import re 10 | import signal 11 | import sys 12 | import time 13 | import zmq 14 | 15 | do_exit = False 16 | 17 | def sighandler(signum, stack_frame): 18 | print(f"Got signal {signum}") 19 | do_exit = True 20 | raise KeyboardInterrupt 21 | 22 | def setup_signals(): 23 | signal.signal(signal.SIGINT, sighandler) 24 | signal.signal(signal.SIGTERM, sighandler) 25 | signal.signal(signal.SIGQUIT, sighandler) 26 | 27 | def out_file_open(self): 28 | if self.rotate is None: 29 | filename = self.filename_prefix 30 | else: 31 | self.current_tm = time.localtime(time.time()) 32 | if self.rotate == 'daily': 33 | fmt = '_%Y%m%d' 34 | elif self.rotate == 'hourly': 35 | fmt = '_%Y%m%d_%H' 36 | timesuffix = time.strftime(fmt, self.current_tm) 37 | filename = self.filename_prefix + timesuffix + self.extension 38 | self.fh = open(filename, mode='a', buffering=1) 39 | 40 | 41 | def out_file_init(self): 42 | if self.output_file == '-': 43 | self.fh = sys.stdout 44 | self.rotate = None 45 | return True 46 | else: 47 | m = re.match("^(.+)(\.[^.]+)$", self.output_file) 48 | if m: 49 | self.filename_prefix = m.group(1) 50 | self.extension = m.group(2) 51 | else: 52 | self.filename_prefix = self.output_file 53 | self.extension = '' 54 | out_file_open(self) 55 | 56 | 57 | def out_file_rotate(self): 58 | now = time.localtime(time.time()) 59 | if (self.rotate == 'daily' and now.tm_mday != self.current_tm.tm_mday) or (self.rotate == 'hourly' and now.tm_hour != self.current_tm.tm_hour): 60 | self.fh.close() 61 | out_file_open(self) 62 | 63 | 64 | if __name__ == '__main__': 65 | parser = argparse.ArgumentParser(description='Receive logs from multiple sources on ZMQ server socket and write them into a common, optionally rotated file') 66 | parser.add_argument('--listen', required=True, help='ZMQ endpoint to listen on (example: tcp://*:4000)') 67 | parser.add_argument('--output-file', default='-', help='Full path to the file where the output should be written (default: stdout)') 68 | parser.add_argument('--rotate', choices=['daily','hourly'], help='Rotate the output file on top of the day or hour, respectively (default: do not rotate)') 69 | self = parser.parse_args() 70 | 71 | try: 72 | out_file_init(self) 73 | except Exception as e: 74 | print(f"Could not open output file: {e}", file=sys.stderr) 75 | sys.exit(1) 76 | 77 | try: 78 | context = zmq.Context() 79 | socket = context.socket(zmq.SUB) 80 | socket.bind(self.listen) 81 | socket.setsockopt_string(zmq.SUBSCRIBE, '') 82 | except Exception as e: 83 | print(f"Could not create ZMQ listener: {e}", file=sys.stderr) 84 | sys.exit(1) 85 | 86 | setup_signals() 87 | 88 | while not do_exit: 89 | try: 90 | string = socket.recv_string() 91 | except KeyboardInterrupt: 92 | break 93 | try: 94 | if self.rotate is not None: 95 | out_file_rotate(self) 96 | except Exception as e: 97 | print(f"Could not rotate output file: {e}", file=sys.stderr) 98 | sys.exit(1) 99 | self.fh.write(string) 100 | 101 | print('Exiting', file=sys.stderr) 102 | 103 | -------------------------------------------------------------------------------- /src/fft.c: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | 3 | #include 4 | #include // memcpy, memmove 5 | #include // pthread_* 6 | #include // cbuffercf_* 7 | #include "config.h" 8 | #ifndef HAVE_PTHREAD_BARRIERS 9 | #include "pthread_barrier.h" 10 | #endif 11 | #include "block.h" // block_* 12 | #include "fastddc.h" // fastddc_t 13 | #include "fft.h" 14 | #include "util.h" // XCALLOC, NEW 15 | 16 | struct fft { 17 | struct block block; 18 | fastddc_t *ddc; 19 | float complex *input; 20 | }; 21 | 22 | static void *fft_thread(void *ctx) { 23 | struct block *block = ctx; 24 | struct fft *fft = container_of(block, struct fft, block); 25 | struct circ_buffer *circ_buffer = &block->consumer.in->circ_buffer; 26 | struct shared_buffer *output = &block->producer.out->shared_buffer; 27 | fastddc_t *ddc = fft->ddc; 28 | float complex *cbuf_read_ptr; 29 | uint32_t samples_read; 30 | float complex *fft_input = fft->input; 31 | 32 | // The plan can't be created in fft_create because the output buffer 33 | // is created by block_connect_one2many() which is called after fft_create(). 34 | FFT_PLAN_T *fwd_plan = csdr_make_fft_c2c(ddc->fft_size, fft_input, output->buf, 1, 0); 35 | 36 | pthread_barrier_wait(output->consumers_ready); // Wait for all consumers to initialize 37 | while(true) { 38 | pthread_mutex_lock(circ_buffer->mutex); 39 | // Check for shutdown signal only when there is no data (or not enough data) in the buffer. 40 | // This causes all the data to be processed and flushed to consumers before shutdown is done. 41 | while(cbuffercf_size(circ_buffer->buf) < (uint32_t)ddc->input_size) { 42 | if(block_connection_is_shutdown_signaled(block->consumer.in)) { 43 | debug_print(D_MISC, "Exiting (ordered shutdown)\n"); 44 | pthread_mutex_unlock(circ_buffer->mutex); 45 | goto shutdown; 46 | } 47 | pthread_cond_wait(circ_buffer->cond, circ_buffer->mutex); 48 | } 49 | memmove(fft_input, fft_input + ddc->input_size, ddc->overlap_length * sizeof(float complex)); 50 | cbuffercf_read(circ_buffer->buf, ddc->input_size, &cbuf_read_ptr, &samples_read); 51 | ASSERT(samples_read == (uint32_t)ddc->input_size); 52 | memcpy(fft_input + ddc->overlap_length, cbuf_read_ptr, 53 | ddc->input_size * sizeof(float complex)); 54 | cbuffercf_release(circ_buffer->buf, ddc->input_size); 55 | pthread_mutex_unlock(circ_buffer->mutex); 56 | 57 | csdr_fft_execute(fwd_plan); 58 | // FIXME: rework fastddc_inv_cc, so that this step is not needed 59 | fft_swap_sides(output->buf, ddc->fft_size); 60 | pthread_barrier_wait(output->data_ready); 61 | pthread_barrier_wait(output->consumers_ready); 62 | } 63 | shutdown: 64 | block_connection_one2many_shutdown(block->producer.out); 65 | csdr_destroy_fft_c2c(fwd_plan); 66 | block->running = false; 67 | return NULL; 68 | } 69 | 70 | struct block *fft_create(int32_t decimation, float transition_bw) { 71 | NEW(struct fft, fft); 72 | NEW(fastddc_t, ddc); 73 | if(fastddc_init(ddc, transition_bw, decimation, 0)) { 74 | fprintf(stderr, "Error in fastddc_init()"); 75 | return NULL; 76 | } 77 | fastddc_print(ddc,"fastddc_fwd_cc"); 78 | fft->ddc = ddc; 79 | fft->input = XCALLOC(ddc->fft_size, sizeof(float complex)); 80 | struct producer producer = { .type = PRODUCER_MULTI, .max_tu = ddc->fft_size }; 81 | struct consumer consumer = { .type = CONSUMER_SINGLE, .min_ru = ddc->fft_size }; 82 | fft->block.producer = producer; 83 | fft->block.consumer = consumer; 84 | fft->block.thread_routine = fft_thread; 85 | return &fft->block; 86 | } 87 | 88 | void fft_destroy(struct block *fft_block) { 89 | if(fft_block != NULL) { 90 | struct fft *fft = container_of(fft_block, struct fft, block); 91 | XFREE(fft->input); 92 | XFREE(fft->ddc); 93 | XFREE(fft); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## Version 1.7.0 (2025-11-02) 4 | 5 | * Added `rdkafka` output driver which allows sending decoded messages (text, 6 | Basestation or JSON format) to Apache Kafka cluster. librdkafka 1.8.0 or 7 | later is a new optional dependency, which is required to use this driver. 8 | Refer to "Apache Kafka networked output support" and "rdkafka" sections in 9 | the README.md file for details on how to use it. (thx @erikbeebe) 10 | 11 | * The program will now exit after 5 consecutive errors returned by SoapySDR 12 | `readStream()` function. This often happens when the communication with the 13 | radio fails (eg. due to USB disconnection). If the problem was temporary in 14 | nature, the program should resume decoding after being restarted by systemd 15 | (provided that the service has been configured with `Restart=on-failure`). 16 | 17 | * The number of FFT threads is now configurable with `--fft-threads ` 18 | command line option. The default value is 4 as before and is suitable for 19 | low power hardware, eg. small single board computers. When running on a high 20 | performance CPU, reducing this to 1 might often result in a slightly lower 21 | overall CPU usage. 22 | 23 | * Fixed compatibility issue with liquid-dsp 1.7.0. 24 | 25 | * Fixed compatibility issue with recent versions of CMake. 26 | 27 | ## Version 1.6.1 (2024-02-03) 28 | 29 | * Compatibility fix for liquid-dsp 1.6.0 and later 30 | 31 | ## Version 1.6.0 (2023-12-14) 32 | 33 | * HFDL system table version updated to 52. 34 | 35 | ## Version 1.5.0 (2023-09-10) 36 | 37 | * Added `--prettify-json` command line option which enables prettification of 38 | JSON payloads in libacars >= 2.2.0. This currently applies to OHMA messages 39 | only. 40 | 41 | * The lifetime of the Aircraft_ID to ICAO hex code mapping cache can now be 42 | changed using `--aircraft-cache-ttl ` option. 43 | 44 | ## Version 1.4.1 (2023-03-26) 45 | 46 | * Fixed a bug which caused incorrect decoding of some uplink MPDUs 47 | containing multiple LPDUs destined to the same aircraft (thx Dick, 48 | acarslogger) 49 | 50 | ## Version 1.4.0 (2022-11-14) 51 | 52 | * Added support for periodic noise floor reporting via Etsy StatsD. This 53 | allows long-term noise floor monitoring and trending on all monitored 54 | HFDL channels. Enable with `--noise-floor-stats-interval ` 55 | command line option (together with `--statsd address:port`, of course). 56 | * Better handling of network errors when producing output to networked 57 | outputs (TCP, UDP, ZMQ). When the message delivery fails, the message 58 | now gets requeued and redelivered when the connection is reestablished. 59 | * A few optimizations resulting in slightly lower CPU usage. 60 | 61 | ## Version 1.3.0 (2022-03-16) 62 | 63 | * Added support for formatting decoded messages as JSON. 64 | 65 | ## Version 1.2.1 (2022-02-26) 66 | 67 | * Fixed a bug that caused manual gain settings to be ineffective on devices 68 | which have auto gain enabled by default. dumphfdl now turns off AGC 69 | explicitly before setting gain elements. 70 | 71 | ## Version 1.2.0 (2021-11-17) 72 | 73 | * Noise floor and signal level estimates are now computed and printed in 74 | message headers. 75 | * Added --freq-as-squawk option which uses squawk field in the Basestation feed 76 | to convey HFDL channel frequency on which the position information has been 77 | received. 78 | * The program can now decode data from I/Q samples piped onto standard input. 79 | This allows interoperation with I/Q data sources like GNURadio or KiwiSDR. 80 | * Added `--read-buffer-size` option for setting input buffer size when decoding 81 | from an I/Q file or from standard input. 82 | * Slightly better sensitivity. 83 | 84 | ## Version 1.1.0 (2021-10-29) 85 | 86 | * Improved sensitivity (less CRC errors) 87 | * Reduced CPU usage 88 | * The program now builds and runs under MacOS 89 | 90 | ## Version 1.0.0 (2021-10-15) 91 | 92 | * First public release 93 | -------------------------------------------------------------------------------- /src/dumpfile.c: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #include 3 | #include // fopen, fwrite, fclose 4 | #include // float complex 5 | #include // NAN 6 | #include // strerror 7 | #include // errno 8 | #include // _exit 9 | #include "config.h" // Config 10 | #include "dumpfile.h" // typedefs 11 | #include "util.h" // NEW, XFREE, ASSERT 12 | 13 | struct dumpfile { 14 | FILE *f; 15 | uint64_t time; 16 | }; 17 | 18 | struct dumpfile_rf32 { 19 | struct dumpfile *df; 20 | float fillval; 21 | }; 22 | 23 | struct dumpfile_cf32 { 24 | struct dumpfile *df; 25 | float complex fillval; 26 | }; 27 | 28 | static struct dumpfile *dumpfile_open(char const *name); 29 | static void dumpfile_write_block(struct dumpfile *df, uint64_t time, 30 | void *buf, size_t nmemb, size_t len, void *fillval); 31 | static void dumpfile_destroy(struct dumpfile *df); 32 | 33 | /********************************** 34 | * Public routines 35 | **********************************/ 36 | 37 | dumpfile_rf32 do_dumpfile_rf32_open(char const *name, float fillval) { 38 | if(Config.datadumps == false) { 39 | return NULL; 40 | } 41 | struct dumpfile *df = dumpfile_open(name); 42 | NEW(struct dumpfile_rf32, df_r32); 43 | df_r32->df = df; 44 | df_r32->fillval = fillval; 45 | return df_r32; 46 | } 47 | 48 | void do_dumpfile_rf32_write_value(dumpfile_rf32 f, uint64_t time, float val) { 49 | if(Config.datadumps == false) { 50 | return; 51 | } 52 | ASSERT(f); 53 | dumpfile_write_block(f->df, time, &val, 1, sizeof(float), &f->fillval); 54 | } 55 | 56 | void do_dumpfile_rf32_destroy(dumpfile_rf32 f) { 57 | if(Config.datadumps == false) { 58 | return; 59 | } 60 | if(f != NULL) { 61 | dumpfile_destroy(f->df); 62 | XFREE(f); 63 | } 64 | } 65 | 66 | dumpfile_cf32 do_dumpfile_cf32_open(char const *name, float complex fillval) { 67 | if(Config.datadumps == false) { 68 | return NULL; 69 | } 70 | struct dumpfile *df = dumpfile_open(name); 71 | NEW(struct dumpfile_cf32, df_c32); 72 | df_c32->df = df; 73 | df_c32->fillval = fillval; 74 | return df_c32; 75 | } 76 | 77 | void do_dumpfile_cf32_write_value(dumpfile_cf32 f, uint64_t time, 78 | float complex val) { 79 | if(Config.datadumps == false) { 80 | return; 81 | } 82 | ASSERT(f); 83 | dumpfile_write_block(f->df, time, &val, 1, sizeof(float complex), &f->fillval); 84 | } 85 | 86 | void do_dumpfile_cf32_write_block(dumpfile_cf32 f, uint64_t time, 87 | float complex *buf, size_t len) { 88 | if(Config.datadumps == false) { 89 | return; 90 | } 91 | ASSERT(f); 92 | dumpfile_write_block(f->df, time, buf, len, sizeof(float complex), &f->fillval); 93 | } 94 | 95 | void do_dumpfile_cf32_destroy(dumpfile_cf32 f) { 96 | if(Config.datadumps == false) { 97 | return; 98 | } 99 | if(f != NULL) { 100 | dumpfile_destroy(f->df); 101 | XFREE(f); 102 | } 103 | } 104 | 105 | /********************************** 106 | * Private routines 107 | **********************************/ 108 | 109 | static struct dumpfile *dumpfile_open(char const *name) { 110 | NEW(struct dumpfile, df); 111 | df->f = fopen(name, "w"); 112 | if(df->f == NULL) { 113 | fprintf(stderr, "Could not open file %s for writing: %s\n", 114 | name, strerror(errno)); 115 | _exit(1); 116 | } 117 | return df; 118 | } 119 | 120 | static void dumpfile_write_block(struct dumpfile *df, uint64_t time, 121 | void *buf, size_t nmemb, size_t len, void *fillval) { 122 | ASSERT(df); 123 | ASSERT(buf); 124 | if(df->time != 0 && df->time < time) { 125 | // Clock discontinuity means there is a hole in the data. 126 | // Fill it with the given fill value 127 | size_t fill_len = time - df->time; 128 | for(size_t i = 0; i < fill_len; i++) { 129 | fwrite(fillval, len, 1, df->f); 130 | } 131 | df->time += fill_len; 132 | } 133 | fwrite(buf, len, nmemb, df->f); 134 | df->time += nmemb; 135 | } 136 | 137 | static void dumpfile_destroy(struct dumpfile *df) { 138 | if(df == NULL) { 139 | return; 140 | } 141 | if(df->f) { 142 | fclose(df->f); 143 | } 144 | XFREE(df); 145 | } 146 | -------------------------------------------------------------------------------- /src/input-file.c: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include // usleep 7 | #include // errno 8 | #include // cbuffercf_* 9 | #include "block.h" // block_* 10 | #include "input-common.h" // input, sample_format, input_vtable 11 | #include "input-helpers.h" // get_sample_full_scale_value, get_sample_size 12 | #include "util.h" // debug_print, ASSERT, XCALLOC 13 | #include "globals.h" // do_exit 14 | 15 | #define INPUT_FILE_BUFSIZE_DEFAULT 320000U 16 | 17 | struct file_input { 18 | struct input input; 19 | FILE *fh; 20 | }; 21 | 22 | struct input *file_input_create(struct input_cfg *cfg) { 23 | UNUSED(cfg); 24 | NEW(struct file_input, file_input); 25 | return &file_input->input; 26 | } 27 | 28 | void file_input_destroy(struct input *input) { 29 | if(input != NULL) { 30 | struct file_input *fi = container_of(input, struct file_input, input); 31 | XFREE(fi); 32 | } 33 | } 34 | 35 | void *file_input_thread(void *ctx) { 36 | ASSERT(ctx); 37 | struct block *block = ctx; 38 | struct input *input = container_of(block, struct input, block); 39 | struct file_input *file_input = container_of(input, struct file_input, input); 40 | struct circ_buffer *circ_buffer = &block->producer.out->circ_buffer; 41 | 42 | ASSERT(file_input->fh != NULL); 43 | ASSERT(input->config->read_buffer_size > 0); 44 | size_t bufsize = input->config->read_buffer_size; 45 | 46 | void *inbuf = XCALLOC(bufsize, sizeof(uint8_t)); 47 | float complex *outbuf = XCALLOC(bufsize / input->bytes_per_sample, 48 | sizeof(float complex)); 49 | size_t space_available, len, samples_read; 50 | do { 51 | len = fread(inbuf, 1, bufsize, file_input->fh); 52 | samples_read = len / input->bytes_per_sample; 53 | while(true) { 54 | pthread_mutex_lock(circ_buffer->mutex); 55 | space_available = cbuffercf_space_available(circ_buffer->buf); 56 | pthread_mutex_unlock(circ_buffer->mutex); 57 | if(space_available * input->bytes_per_sample >= len) { 58 | break; 59 | } 60 | usleep(100000); 61 | } 62 | input->convert_sample_buffer(input, inbuf, len, outbuf); 63 | complex_samples_produce(circ_buffer, outbuf, samples_read); 64 | } while(len > 0 && do_exit == 0); 65 | fclose(file_input->fh); 66 | file_input->fh = NULL; 67 | debug_print(D_MISC, "Shutdown ordered, signaling consumer shutdown\n"); 68 | block_connection_one2one_shutdown(block->producer.out); 69 | do_exit = 1; 70 | block->running = false; 71 | XFREE(inbuf); 72 | XFREE(outbuf); 73 | return NULL; 74 | } 75 | 76 | int32_t file_input_init(struct input *input) { 77 | ASSERT(input != NULL); 78 | struct file_input *file_input = container_of(input, struct file_input, input); 79 | 80 | if(input->config->sfmt == SFMT_UNDEF) { 81 | fprintf(stderr, "Sample format must be specified for file inputs\n"); 82 | return -1; 83 | } 84 | if(input->config->read_buffer_size <= 0) { 85 | input->config->read_buffer_size = INPUT_FILE_BUFSIZE_DEFAULT; 86 | } 87 | if(strcmp(input->config->source, "-") == 0) { 88 | file_input->fh = stdin; 89 | } else { 90 | file_input->fh = fopen(input->config->source, "rb"); 91 | } 92 | if(file_input->fh == NULL) { 93 | fprintf(stderr, "Failed to open input file %s: %s\n", 94 | input->config->source, strerror(errno)); 95 | return -1; 96 | } 97 | 98 | input->full_scale = get_sample_full_scale_value(input->config->sfmt); 99 | input->bytes_per_sample = get_sample_size(input->config->sfmt); 100 | ASSERT(input->bytes_per_sample > 0); 101 | if(input->config->read_buffer_size % input->bytes_per_sample != 0) { 102 | fprintf(stderr, "Invalid --read-buffer-size value " 103 | "(must be a multiple of sample size, which is %d bytes)\n", 104 | input->bytes_per_sample); 105 | return -1; 106 | } 107 | input->block.producer.max_tu = input->config->read_buffer_size / input->bytes_per_sample; 108 | debug_print(D_SDR, "%s: max_tu=%zu\n", 109 | input->config->source, input->block.producer.max_tu); 110 | return 0; 111 | } 112 | 113 | struct input_vtable const file_input_vtable = { 114 | .create = file_input_create, 115 | .init = file_input_init, 116 | .destroy = file_input_destroy, 117 | .rx_thread_routine = file_input_thread 118 | }; 119 | 120 | -------------------------------------------------------------------------------- /doc/STATSD_METRICS.md: -------------------------------------------------------------------------------- 1 | # dumphfdl StatsD metrics 2 | 3 | All metrics described below reside in a common namespace: 4 | 5 | - if receiver site name is not set (ie. `--station-id ` option is not used), then the namespace is `dumphfdl`. 6 | 7 | - if receiver site name is set, then the namespace is `dumphfdl.`. 8 | 9 | Abbreviations used throughout the following sections: 10 | 11 | - MPDU (MAC Protocol Data Unit) - a portion of data occupying a HFDL timeslot (single or double). It consists of a prekey sequence, preamble, training sequences and data fields. It is a container for one or more LPDUs. 12 | 13 | - SPDU (Squitter Protocol Data Unit) - a portion of data occupying a single HFDL timeslot. Only sent by ground stations. An SPDU contains a Squitter message that describes the current state of the network (currently used frequencies). 14 | 15 | - PDU (Protocol Data Unit) - MPDU or SPDU. 16 | 17 | - LPDU (Link Protocol Data Unit) - a portion of data carried inside the MPDU. It encapsulates a HFDL control message (Logon request, Logon confirm, etc) or user data. A single uplink MPDU may contain 0 to 64 LPDUs, while a downlink MPDU may contain 0 to 15 LPDUs. 18 | 19 | ## Per-channel metrics 20 | 21 | In the following list `` is the channel frequency (in Hertz) - for example `11184000`. 22 | 23 | - `.demod.preamble.A2_found` (counter) - number of A2 sequences found. A2 is a pseudo-random bit sequence in the preamble of a HFDL frame. It occurs twice in every preamble. Finding a second occurrence of this sequence is a good indication that a HFDL frame has been found in the input signal. 24 | 25 | - `.demod.preamble.M1_found` (counter) - number of M sequences found. M is a pseudo-random bit sequence in the preamble of a HFDL frame, that indicates the modulation and interleaver type used to encode the frame. This counter is incremented when these parameters have been successfully determined with a reasonable confidence level. 26 | 27 | - `.demod.preamble.errors.M1_not_found` (counter) - incremented when the decoder is unable to determine the modulation and interleaver type for the frame. 28 | 29 | - `.frames.processed` (counter) - number of PDUs processed by the decoder. The following equation holds true for every channel: `frames.processed = frames.good + frame.errors.*`. 30 | 31 | - `.frames.good` (counter) - number of successfully decoded PDUs. The following equation holds true for every channel: `frames.good = frame.dir.air2gnd + frame.dir.gnd2air`. 32 | 33 | - `.frame.errors.bad_fcs` (counter) - number of PDUs which could not be decoded due to a bad Frame Check Sequence (CRC error). 34 | 35 | - `.frame.errors.too_short` (counter) - number of PDUs which could not be decoded due to being unreasonably short. 36 | 37 | - `.frame.dir.air2gnd` (counter) - number of successfully decoded downlink frames (MPDUs). 38 | 39 | - `.frame.dir.gnd2air` (counter) - number of successfully decoded uplink frames (MPDUs and SPDUs). 40 | 41 | - `.lpdus.processed` (counter) - number of LPDUs procssed by the decoder. The following equation holds true for every channel: `lpdus.processed = lpdus.good + lpdu.errors.*`. 42 | 43 | - `.lpdus.good` (counter) - number of successfully decoded LPDUs. 44 | 45 | - `.lpdu.errors.bad_fcs` (counter) - number of LPDUs which could not be decoded due to a bad Frame Check Sequence (CRC error). 46 | 47 | - `.lpdu.errors.too_short` (counter) - number of LPDUs which could not be decoded due to being unreasonably short. 48 | 49 | - `.noise_floor` (gauge) - noise floor level estimate on the given channel. Reported as integer in tenths of dBFS, positive. To convert this to the actual value, multiply it by -0.1, eg. 853 = -85.3 dBFS. This metric is emitted only when enabled with `--noise-floor-stats-interval `. 50 | 51 | ## ACARS reassembly metrics 52 | 53 | - `.acars.reasm.unknown` (counter) 54 | - `.acars.reasm.complete` (counter) 55 | - `.acars.reasm.skipped` (counter) 56 | - `.acars.reasm.duplicate` (counter) 57 | - `.acars.reasm.out_of_seq` (counter) 58 | - `.acars.reasm.invalid_args` (counter) 59 | 60 | These counters are incremented for every decoded ACARS message. Their names correspond to the `Reassembly status` info line printed in the header of each ACARS message. Refer to libacars documentation for details. 61 | 62 | ## Per-cache metrics 63 | 64 | dumphfdl maintains three in-memory caches: 65 | 66 | - `ac_fwd` - per-frequency mappings of aircraft IDs to ICAO addresses 67 | 68 | - `ac_inv` - mappings of ICAO addresses to aircraft IDs (ie. an inverted `ac_fwd` cache) 69 | 70 | - `ac_data` - data retrieved from basestation.sqb database 71 | 72 | Each cache has the following set of metrics: 73 | 74 | - `.entries` (gauge) - number of entries in the cache. Goes up when new entries are created in the cache. Goes down when entries are expired from the cache. 75 | -------------------------------------------------------------------------------- /src/position.c: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #include // fabsf 3 | #include 4 | #include 5 | #include // la_proto_node 6 | #include "position.h" 7 | #include "lpdu.h" // lpdu_position_info_extract 8 | #include "util.h" 9 | 10 | /****************************** 11 | * Forward declarations 12 | ******************************/ 13 | 14 | bool location_is_valid(struct location *loc); 15 | void fixup_timestamp(struct timestamp *ts); 16 | 17 | /****************************** 18 | * Public methods 19 | ******************************/ 20 | 21 | struct position_info *position_info_create(void) { 22 | NEW(struct position_info, pos_info); 23 | return pos_info; 24 | } 25 | 26 | struct position_info *position_info_extract(la_proto_node *tree) { 27 | ASSERT(tree); 28 | 29 | struct position_info *pos_info = lpdu_position_info_extract(tree); 30 | if(pos_info != NULL && location_is_valid(&pos_info->position.location)) { 31 | fixup_timestamp(&pos_info->position.timestamp); 32 | } else { 33 | position_info_destroy(pos_info); 34 | pos_info = NULL; 35 | } 36 | return pos_info; 37 | } 38 | 39 | void position_info_destroy(struct position_info *pos_info) { 40 | XFREE(pos_info); 41 | } 42 | 43 | /**************************************** 44 | * Private variables and methods 45 | ****************************************/ 46 | 47 | static void date_yesterday(struct tm *now, struct tm *result); 48 | 49 | bool location_is_valid(struct location *loc) { 50 | ASSERT(loc); 51 | 52 | bool result = fabs(loc->lat) <= 90.0f; 53 | result &= fabs(loc->lon) <= 180.0f; 54 | debug_print(D_MISC, "location (%f, %f) is %svalid \n", 55 | loc->lat, loc->lon, result ? "" : "in"); 56 | return result; 57 | } 58 | 59 | // Fills in missing struct tm fields. 60 | // Three variants are supported: 61 | // - hours, mins, secs present, day, month, year missing (as in Logon Request/Resume LPDUs) 62 | // - hours, mins present, secs and date missing (as in CPDLC position reports) 63 | // - mins, secs present, hours and date missing (as in ADS-C position reports) 64 | // Missing fields are filled in to get the closest matching timestamp in the past. 65 | void fixup_timestamp(struct timestamp *ts) { 66 | ASSERT(ts); 67 | 68 | struct tm tm_pos = ts->tm; 69 | time_t now = time(NULL); 70 | struct tm tm_now = {0}; 71 | gmtime_r(&now, &tm_now); 72 | 73 | if(!ts->tm_sec_present) { 74 | tm_pos.tm_sec = 0; 75 | ts->tm_sec_present = true; 76 | } 77 | ASSERT(ts->tm_min_present); // Minutes are always present in all message types 78 | if(!ts->tm_hour_present) { 79 | // Assume current hour and go back 1 hour if the resulting wallclock time 80 | // is later than now. 81 | if(tm_pos.tm_min < tm_now.tm_min || 82 | (tm_pos.tm_min == tm_now.tm_min && tm_pos.tm_sec <= tm_now.tm_sec)) { 83 | tm_pos.tm_hour = tm_now.tm_hour; 84 | } else { 85 | tm_pos.tm_hour = tm_now.tm_hour > 0 ? tm_now.tm_hour - 1 : 23; 86 | } 87 | debug_print(D_MISC, "fixup_hour: tm_pos: :%02d:%02d now: %02d:%02d:%02d result: %02d\n", 88 | tm_pos.tm_min, tm_pos.tm_sec, 89 | tm_now.tm_hour, tm_now.tm_min, tm_now.tm_sec, 90 | tm_pos.tm_hour); 91 | ts->tm_hour_present = true; 92 | } 93 | if(!ts->tm_date_present) { 94 | tm_pos.tm_mday = tm_now.tm_mday; 95 | tm_pos.tm_mon = tm_now.tm_mon; 96 | tm_pos.tm_year = tm_now.tm_year; 97 | ts->tm_date_present = true; 98 | } 99 | // Verify if the result is in the past; if not, go back 1 day 100 | time_t t_pos = timegm(&tm_pos); 101 | ASSERT(t_pos != -1); 102 | if(t_pos > now) { 103 | struct tm tm_yesterday = {0}; 104 | date_yesterday(&tm_now, &tm_yesterday); 105 | tm_pos.tm_mday = tm_yesterday.tm_mday; 106 | tm_pos.tm_mon = tm_yesterday.tm_mon; 107 | tm_pos.tm_year = tm_yesterday.tm_year; 108 | t_pos = timegm(&tm_pos); 109 | ASSERT(t_pos != -1); 110 | debug_print(D_MISC, "fixup_date: result: %04d-%02d-%02d \n", 111 | tm_pos.tm_year + 1900, tm_pos.tm_mon + 1, tm_pos.tm_mday); 112 | } 113 | ts->tm = tm_pos; 114 | ts->t = t_pos; 115 | debug_print(D_MISC, "result: %04d-%02d-%02d %02d:%02d:%02d t: %ld\n", 116 | tm_pos.tm_year + 1900, tm_pos.tm_mon + 1, tm_pos.tm_mday, 117 | tm_pos.tm_hour, tm_pos.tm_min, tm_pos.tm_sec, t_pos); 118 | } 119 | 120 | // Computes the date of the previous day 121 | // Only tm_yday, tm_mon and tm_mday fields are valid in the result. 122 | static void date_yesterday(struct tm *now, struct tm *result) { 123 | ASSERT(now); 124 | ASSERT(result); 125 | 126 | time_t day_seconds_elapsed = now->tm_hour * 60 * 60 + now->tm_min * 60 + now->tm_sec; 127 | // Now go back 10 seconds into yesterday and compute the date 128 | time_t t = mktime(now) - day_seconds_elapsed - 10; 129 | gmtime_r(&t, result); 130 | debug_print(D_MISC, "date_now: %04d-%02d-%02d result: %04d-%02d-%02d\n", 131 | now->tm_year + 1900, now->tm_mon + 1, now->tm_mday, 132 | result->tm_year + 1900, result->tm_mon + 1, result->tm_mday); 133 | } 134 | -------------------------------------------------------------------------------- /src/output-udp.c: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #include // fprintf 3 | #include // strdup, strerror 4 | #include // close 5 | #include // errno 6 | #include // socket, connect 7 | #include // socket, connect 8 | #include // getaddrinfo 9 | #include "output-common.h" // output_descriptor_t, output_qentry_t, output_queue_drain 10 | #include "kvargs.h" // kvargs, option_descr_t 11 | #include "util.h" // ASSERT 12 | 13 | typedef struct { 14 | char *address; 15 | char *port; 16 | int sockfd; 17 | } out_udp_ctx_t; 18 | 19 | static bool out_udp_supports_format(output_format_t format) { 20 | return(format == OFMT_TEXT || format == OFMT_BASESTATION || format == OFMT_JSON); 21 | } 22 | 23 | static void *out_udp_configure(kvargs *kv) { 24 | ASSERT(kv != NULL); 25 | NEW(out_udp_ctx_t, cfg); 26 | if(kvargs_get(kv, "address") == NULL) { 27 | fprintf(stderr, "output_udp: IP address not specified\n"); 28 | goto fail; 29 | } 30 | cfg->address = strdup(kvargs_get(kv, "address")); 31 | if(kvargs_get(kv, "port") == NULL) { 32 | fprintf(stderr, "output_udp: UDP port not specified\n"); 33 | goto fail; 34 | } 35 | cfg->port = strdup(kvargs_get(kv, "port")); 36 | return cfg; 37 | fail: 38 | XFREE(cfg); 39 | return NULL; 40 | } 41 | 42 | static int out_udp_init(void *selfptr) { 43 | ASSERT(selfptr != NULL); 44 | out_udp_ctx_t *self = selfptr; 45 | 46 | struct addrinfo hints, *result, *rptr; 47 | memset(&hints, 0, sizeof(struct addrinfo)); 48 | hints.ai_family = AF_UNSPEC; 49 | hints.ai_socktype = SOCK_DGRAM; 50 | hints.ai_flags = 0; 51 | hints.ai_protocol = 0; 52 | int ret = getaddrinfo(self->address, self->port, &hints, &result); 53 | if(ret != 0) { 54 | fprintf(stderr, "output_udp: could not resolve %s: %s\n", self->address, gai_strerror(ret)); 55 | return -1; 56 | } 57 | for (rptr = result; rptr != NULL; rptr = rptr->ai_next) { 58 | self->sockfd = socket(rptr->ai_family, rptr->ai_socktype, rptr->ai_protocol); 59 | if(self->sockfd == -1) { 60 | continue; 61 | } 62 | if(connect(self->sockfd, rptr->ai_addr, rptr->ai_addrlen) != -1) { 63 | break; 64 | } 65 | close(self->sockfd); 66 | self->sockfd = 0; 67 | } 68 | if (rptr == NULL) { 69 | fprintf(stderr, "output_udp: Could not set up UDP socket to %s:%s: all addresses failed\n", 70 | self->address, self->port); 71 | self->sockfd = 0; 72 | return -1; 73 | } 74 | freeaddrinfo(result); 75 | return 0; 76 | } 77 | 78 | static int out_udp_produce_text(out_udp_ctx_t *self, struct metadata *metadata, struct octet_string *msg) { 79 | UNUSED(metadata); 80 | ASSERT(msg != NULL); 81 | ASSERT(self->sockfd != 0); 82 | if(msg->len < 2) { 83 | return 0; 84 | } 85 | if(write(self->sockfd, msg->buf, msg->len) < 0) { 86 | return -1; 87 | } 88 | return 0; 89 | } 90 | 91 | static int out_udp_produce(void *selfptr, output_format_t format, struct metadata *metadata, struct octet_string *msg) { 92 | ASSERT(selfptr != NULL); 93 | out_udp_ctx_t *self = selfptr; 94 | int32_t result = 0; 95 | if(format == OFMT_TEXT || format == OFMT_JSON || format == OFMT_BASESTATION) { 96 | result = out_udp_produce_text(self, metadata, msg); 97 | } 98 | if(result < 0) { 99 | // UDP output is fire-and-forget by definition. 100 | // Return 0 regardless of whether the send succeeded or not, 101 | // but print an error to help with diagnosing common issues, 102 | // (eg. nothing listening on the receiver port). 103 | fprintf(stderr, "output_udp(%s:%s): send error: %s\n", self->address, 104 | self->port, strerror(errno)); 105 | } 106 | return 0; 107 | } 108 | 109 | static void out_udp_handle_shutdown(void *selfptr) { 110 | ASSERT(selfptr != NULL); 111 | out_udp_ctx_t *self = selfptr; 112 | fprintf(stderr, "output_udp(%s:%s): shutting down\n", self->address, self->port); 113 | close(self->sockfd); 114 | } 115 | 116 | static void out_udp_handle_failure(void *selfptr) { 117 | ASSERT(selfptr != NULL); 118 | out_udp_ctx_t *self = selfptr; 119 | fprintf(stderr, "output_udp: can't connect to %s:%s, deactivating output\n", 120 | self->address, self->port); 121 | close(self->sockfd); 122 | } 123 | 124 | static const option_descr_t out_udp_options[] = { 125 | { 126 | .name = "address", 127 | .description = "Destination host name or IP address (required)" 128 | }, 129 | { 130 | .name = "port", 131 | .description = "Destination UDP port (required)" 132 | }, 133 | { 134 | .name = NULL, 135 | .description = NULL 136 | } 137 | }; 138 | 139 | output_descriptor_t out_DEF_udp = { 140 | .name = "udp", 141 | .description = "Output to a remote host via UDP", 142 | .options = out_udp_options, 143 | .supports_format = out_udp_supports_format, 144 | .configure = out_udp_configure, 145 | .init = out_udp_init, 146 | .produce = out_udp_produce, 147 | .handle_shutdown = out_udp_handle_shutdown, 148 | .handle_failure = out_udp_handle_failure 149 | }; 150 | -------------------------------------------------------------------------------- /src/output-common.h: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #pragma once 3 | #include 4 | #include 5 | #include // pthread_t 6 | #include // GAsyncQueue 7 | #include // la_proto_node 8 | #include // la_list 9 | #include "kvargs.h" // kvargs 10 | #include "options.h" // options_descr_t 11 | #include "metadata.h" // struct metadata 12 | 13 | // default output specification - decoded text output to stdout 14 | #define DEFAULT_OUTPUT "decoded:text:file:path=-" 15 | 16 | // output queue high water mark 17 | #define OUTPUT_QUEUE_HWM_DEFAULT 1000 18 | // high water mark disabled 19 | #define OUTPUT_QUEUE_HWM_NONE 0 20 | 21 | // Data type on formatter input 22 | typedef enum { 23 | FMTR_INTYPE_UNKNOWN = 0, 24 | FMTR_INTYPE_DECODED_FRAME = 1, 25 | FMTR_INTYPE_RAW_FRAME = 2 26 | } fmtr_input_type_t; 27 | 28 | // Output formats 29 | typedef enum { 30 | OFMT_UNKNOWN = 0, 31 | OFMT_TEXT = 1, 32 | OFMT_BASESTATION = 2, 33 | OFMT_JSON = 3 34 | } output_format_t; 35 | 36 | typedef struct octet_string* (fmt_decoded_fun_t)(struct metadata *, la_proto_node *); 37 | typedef struct octet_string* (fmt_raw_fun_t)(struct metadata *, struct octet_string *); 38 | typedef bool (intype_check_fun_t)(fmtr_input_type_t); 39 | 40 | // Frame formatter descriptor 41 | typedef struct { 42 | char *name; 43 | char *description; 44 | fmt_decoded_fun_t *format_decoded_msg; 45 | fmt_raw_fun_t *format_raw_msg; 46 | intype_check_fun_t *supports_data_type; 47 | output_format_t output_format; 48 | } fmtr_descriptor_t; 49 | 50 | // Frame formatter instance 51 | typedef struct { 52 | fmtr_descriptor_t *td; // type descriptor of the formatter used 53 | fmtr_input_type_t intype; // what kind of data to pass to the input of this formatter 54 | la_list *outputs; // list of output descriptors where the formatted message should be sent 55 | } fmtr_instance_t; 56 | 57 | typedef bool (output_format_check_fun_t)(output_format_t); 58 | typedef void* (output_configure_fun_t)(kvargs *); 59 | typedef void (output_ctx_destroy_fun_t)(void *); 60 | typedef int32_t (output_init_fun_t)(void *); 61 | typedef int32_t (output_produce_msg_fun_t)(void *, output_format_t, struct metadata *, struct octet_string *); 62 | typedef void (output_shutdown_handler_fun_t)(void *); 63 | typedef void (output_failure_handler_fun_t)(void *); 64 | 65 | // Output descriptor 66 | typedef struct { 67 | char *name; 68 | char *description; 69 | option_descr_t const *options; 70 | output_format_check_fun_t *supports_format; 71 | output_configure_fun_t *configure; 72 | output_ctx_destroy_fun_t *ctx_destroy; 73 | output_init_fun_t *init; 74 | output_produce_msg_fun_t *produce; 75 | output_shutdown_handler_fun_t *handle_shutdown; 76 | output_failure_handler_fun_t *handle_failure; 77 | } output_descriptor_t; 78 | 79 | // Output instance context (passed to the thread routine) 80 | typedef struct { 81 | GAsyncQueue *q; // input queue 82 | void *priv; // output instance context (private) 83 | output_format_t format; // format of the data fed into the output 84 | bool active; // output thread is running 85 | } output_ctx_t; 86 | 87 | // Output instance 88 | typedef struct { 89 | output_descriptor_t *td; // type descriptor of the output 90 | pthread_t *output_thread; // thread of this output instance 91 | output_ctx_t *ctx; // context data for the thread 92 | } output_instance_t; 93 | 94 | // Messages passed via output queues 95 | typedef struct { 96 | struct octet_string *msg; // formatted message 97 | struct metadata *metadata; // opaque message metadata 98 | output_format_t format; // format of the data stored in msg 99 | uint32_t flags; // flags 100 | } output_qentry_t; 101 | 102 | // output queue entry flags 103 | #define OUT_FLAG_ORDERED_SHUTDOWN (1 << 0) 104 | 105 | fmtr_input_type_t fmtr_input_type_from_string(char const *str); 106 | fmtr_descriptor_t *fmtr_descriptor_get(output_format_t fmt); 107 | fmtr_instance_t *fmtr_instance_new(fmtr_descriptor_t *fmttd, fmtr_input_type_t intype); 108 | void fmtr_instance_destroy(fmtr_instance_t *fmtr); 109 | 110 | output_format_t output_format_from_string(char const *str); 111 | output_descriptor_t *output_descriptor_get(char const *output_name); 112 | output_instance_t *output_instance_new(output_descriptor_t *outtd, output_format_t format, void *priv); 113 | void output_instance_destroy(output_instance_t *output); 114 | output_qentry_t *output_qentry_copy(output_qentry_t const *q); 115 | void output_qentry_destroy(output_qentry_t *q); 116 | void output_queue_drain(GAsyncQueue *q); 117 | void *output_thread(void *arg); 118 | void output_queue_push(void *data, void *ctx); 119 | void shutdown_outputs(la_list *fmtr_list); 120 | bool output_thread_is_any_running(la_list *fmtr_list); 121 | 122 | void output_usage(); 123 | -------------------------------------------------------------------------------- /src/input-helpers.c: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #include // SHRT_MAX, SCHAR_MAX, UCHAR_MAX 3 | #include // CMPLXF 4 | #include // strcasecmp() 5 | #include // pthread_* 6 | #include // cbuffercf_* 7 | #include "input-common.h" // struct input 8 | #include "util.h" // ASSERT, debug_print 9 | 10 | static void convert_cf32(struct input *input, void *inbuf, size_t len, 11 | float complex *outbuf) { 12 | if(UNLIKELY(len % input->bytes_per_sample != 0)) { 13 | debug_print(D_SDR, "Warning: buf len %zu is not a multiple of %d, truncating\n", 14 | len, input->bytes_per_sample); 15 | len -= (len % input->bytes_per_sample); 16 | } 17 | if(UNLIKELY(len == 0)) { 18 | return; 19 | } 20 | float *floatbuf = inbuf; 21 | size_t floatbuf_len = len / sizeof(float); 22 | size_t outbuf_idx = 0; 23 | float re = 0.f, im = 0.f; 24 | float const full_scale = input->full_scale; 25 | ASSERT(full_scale > 0.f); 26 | for(size_t i = 0; i < floatbuf_len;) { 27 | re = floatbuf[i++] / full_scale; 28 | im = floatbuf[i++] / full_scale; 29 | outbuf[outbuf_idx++] = CMPLXF(re, im); 30 | } 31 | } 32 | 33 | static void convert_cs16(struct input *input, void *inbuf, size_t len, 34 | float complex *outbuf) { 35 | if(UNLIKELY(len % input->bytes_per_sample != 0)) { 36 | debug_print(D_SDR, "Warning: buf len %zu is not a multiple of %d, truncating\n", 37 | len, input->bytes_per_sample); 38 | len -= (len % input->bytes_per_sample); 39 | } 40 | if(UNLIKELY(len == 0)) { 41 | return; 42 | } 43 | int16_t *shortbuf = inbuf; 44 | size_t shortbuf_len = len / sizeof(int16_t); 45 | size_t outbuf_idx = 0; 46 | float re = 0.f, im = 0.f; 47 | float const full_scale = input->full_scale; 48 | ASSERT(full_scale > 0.f); 49 | for(size_t i = 0; i < shortbuf_len;) { 50 | re = (float)shortbuf[i++] / full_scale; 51 | im = (float)shortbuf[i++] / full_scale; 52 | outbuf[outbuf_idx++] = CMPLXF(re, im); 53 | } 54 | } 55 | 56 | static void convert_cu8(struct input *input, void *inbuf, size_t len, 57 | float complex *outbuf) { 58 | if(UNLIKELY(len % input->bytes_per_sample != 0)) { 59 | debug_print(D_SDR, "Warning: buf len %zu is not a multiple of %d, truncating\n", 60 | len, input->bytes_per_sample); 61 | len -= (len % input->bytes_per_sample); 62 | } 63 | if(UNLIKELY(len == 0)) { 64 | return; 65 | } 66 | uint8_t *bytebuf = inbuf; 67 | size_t bytebuf_len = len / sizeof(uint8_t); 68 | size_t outbuf_idx = 0; 69 | float re = 0.f, im = 0.f; 70 | float const full_scale = input->full_scale; 71 | ASSERT(full_scale > 0.f); 72 | float const shift = input->full_scale / 2.0f; 73 | for(size_t i = 0; i < bytebuf_len;) { 74 | re = (bytebuf[i++] - shift) / full_scale; 75 | im = (bytebuf[i++] - shift) / full_scale; 76 | outbuf[outbuf_idx++] = CMPLXF(re, im); 77 | } 78 | } 79 | 80 | void complex_samples_produce(struct circ_buffer *circ_buffer, 81 | float complex *samples, size_t num_samples) { 82 | pthread_mutex_lock(circ_buffer->mutex); 83 | size_t cbuf_available = cbuffercf_space_available(circ_buffer->buf); 84 | if(cbuf_available < num_samples) { 85 | fprintf(stderr, "Sample buffer overrun (%zu/%zu samples lost)\n", 86 | num_samples - cbuf_available, num_samples); 87 | num_samples = cbuf_available; 88 | } 89 | cbuffercf_write(circ_buffer->buf, samples, num_samples); 90 | pthread_mutex_unlock(circ_buffer->mutex); 91 | pthread_cond_signal(circ_buffer->cond); 92 | } 93 | 94 | struct sample_format_params { 95 | char const *name; 96 | size_t sample_size; // octets per complex sample 97 | float full_scale; // max raw sample value 98 | convert_sample_buffer_fun convert_fun; // sample conversion routine 99 | }; 100 | 101 | static struct sample_format_params const sample_format_params[] = { 102 | [SFMT_UNDEF] = { 103 | .name = "", 104 | .sample_size = 0, 105 | .full_scale = 0.f, 106 | .convert_fun = NULL 107 | }, 108 | [SFMT_CU8] = { 109 | .name = "CU8", 110 | .sample_size = 2 * sizeof(uint8_t), 111 | .full_scale = (float)SCHAR_MAX, 112 | .convert_fun = convert_cu8 113 | }, 114 | [SFMT_CS16] = { 115 | .name = "CS16", 116 | .sample_size = 2 * sizeof(int16_t), 117 | .full_scale = (float)SHRT_MAX + 0.5f, 118 | .convert_fun = convert_cs16 119 | }, 120 | [SFMT_CF32] = { 121 | .name = "CF32", 122 | .sample_size = 2 * sizeof(float), 123 | .full_scale = 1.0f, 124 | .convert_fun = convert_cf32 125 | } 126 | }; 127 | 128 | size_t get_sample_size(sample_format format) { 129 | if(format < SFMT_MAX) { 130 | return sample_format_params[format].sample_size; 131 | } 132 | return 0; 133 | } 134 | 135 | float get_sample_full_scale_value(sample_format format) { 136 | if(format < SFMT_MAX) { 137 | return sample_format_params[format].full_scale; 138 | } 139 | return 0.f; 140 | } 141 | 142 | convert_sample_buffer_fun get_sample_converter(sample_format format) { 143 | return format < SFMT_MAX ? sample_format_params[format].convert_fun : NULL; 144 | } 145 | 146 | sample_format sample_format_from_string(char const *str) { 147 | if(str == NULL) { 148 | return SFMT_UNDEF; 149 | } 150 | for(int32_t i = 0; i < SFMT_MAX; i++) { 151 | if(strcasecmp(sample_format_params[i].name, str) == 0) { 152 | return i; 153 | } 154 | } 155 | return SFMT_UNDEF; 156 | } 157 | -------------------------------------------------------------------------------- /src/cache.c: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #include // strdup 3 | #include // time_t 4 | #include // la_hash_* 5 | #include // la_vstring 6 | #include "util.h" // NEW, debug_print 7 | #include "cache.h" 8 | #include "statsd.h" // statsd_* 9 | 10 | struct cache_entry { 11 | time_t created_time; 12 | void (*data_destroy)(void *data); 13 | void *data; 14 | }; 15 | 16 | struct cache { 17 | struct cache_vtable const *vtable; 18 | la_hash *table; 19 | la_vstring *statsd_metric_name__entry_count; 20 | char *name; 21 | time_t last_expiration_time; 22 | time_t ttl; 23 | time_t expiration_interval; 24 | uint32_t num_entries; 25 | }; 26 | 27 | #define CACHE_DEFAULT_NAME "__default__" 28 | #ifdef WITH_STATSD 29 | #define CACHE_ENTRY_COUNT_ADD(cache, n) do { \ 30 | if((n) < 0 && (cache)->num_entries < (uint32_t)(-(n))) { \ 31 | (cache)->num_entries = 0; \ 32 | } else { \ 33 | (cache)->num_entries += (n); \ 34 | } \ 35 | statsd_set((cache)->statsd_metric_name__entry_count->str, (cache)->num_entries); \ 36 | debug_print(D_CACHE, "%s: num_entries: %u\n", (cache)->name, (cache)->num_entries); \ 37 | } while(0) 38 | #else 39 | #define CACHE_ENTRY_COUNT_ADD(cache, n) nop() 40 | #endif 41 | 42 | 43 | /****************************** 44 | * Forward declarations 45 | ******************************/ 46 | 47 | static bool is_cache_entry_expired(void const *key, void const *value, void *ctx); 48 | static void cache_entry_destroy(void *entry); 49 | 50 | /****************************** 51 | * Public methods 52 | ******************************/ 53 | 54 | cache *cache_create(char const *cache_name, struct cache_vtable const *vtable, 55 | time_t ttl, time_t expiration_interval) { 56 | ASSERT(vtable); 57 | 58 | NEW(struct cache, cache); 59 | cache->vtable = vtable; 60 | cache->ttl = ttl; 61 | cache->expiration_interval = expiration_interval; 62 | cache->table = la_hash_new(vtable->cache_key_hash, vtable->cache_key_compare, 63 | vtable->cache_key_destroy, cache_entry_destroy); 64 | cache->name = strdup(cache_name); 65 | 66 | #ifdef WITH_STATSD 67 | cache->statsd_metric_name__entry_count = la_vstring_new(); 68 | la_vstring_append_sprintf(cache->statsd_metric_name__entry_count, 69 | "cache.%s.entries", cache_name ? cache_name : CACHE_DEFAULT_NAME); 70 | #endif 71 | 72 | cache->last_expiration_time = time(NULL); 73 | return cache; 74 | } 75 | 76 | bool cache_entry_create(cache *c, void *key, void *value, time_t created_time) { 77 | ASSERT(c); 78 | ASSERT(key); 79 | // NULL 'value' ptr is allowed 80 | 81 | NEW(struct cache_entry, entry); 82 | entry->data = value; 83 | entry->created_time = created_time; 84 | entry->data_destroy = c->vtable->cache_entry_data_destroy; 85 | bool result = la_hash_insert(c->table, key, entry); 86 | if(!result) { // increment entry count only when the entry was added, not replaced 87 | CACHE_ENTRY_COUNT_ADD(c, 1); 88 | } 89 | return result; 90 | } 91 | 92 | bool cache_entry_delete(cache *c, void *key) { 93 | ASSERT(c); 94 | ASSERT(key); 95 | 96 | bool result = la_hash_remove(c->table, key); 97 | if(result) { // decrement entry count only when we've actually deleted something 98 | CACHE_ENTRY_COUNT_ADD(c, -1); 99 | } 100 | return result; 101 | } 102 | 103 | void *cache_entry_lookup(cache *c, void const *key) { 104 | ASSERT(c); 105 | ASSERT(key); 106 | 107 | struct cache_entry *e = la_hash_lookup(c->table, key); 108 | if(e == NULL) { 109 | return NULL; 110 | } else if(e->created_time + c->ttl < time(NULL)) { 111 | debug_print(D_CACHE, "%s: key %p: entry expired\n", c->name, key); 112 | return NULL; 113 | } 114 | return e->data; 115 | } 116 | 117 | int32_t cache_expire(cache *c, time_t current_timestamp) { 118 | ASSERT(c); 119 | 120 | int32_t expired_cnt = 0; 121 | if(c->last_expiration_time + c->expiration_interval <= current_timestamp) { 122 | time_t min_created_time = current_timestamp - c->ttl; 123 | expired_cnt = la_hash_foreach_remove(c->table, is_cache_entry_expired, 124 | &min_created_time); 125 | CACHE_ENTRY_COUNT_ADD(c, -expired_cnt); 126 | debug_print(D_CACHE, "%s: last_gc: %ld, current_timestamp: %ld, expired %d cache entries\n", 127 | c->name, c->last_expiration_time, current_timestamp, expired_cnt); 128 | c->last_expiration_time = current_timestamp; 129 | } 130 | return expired_cnt; 131 | } 132 | 133 | void cache_destroy(cache *c) { 134 | if(c != NULL) { 135 | la_hash_destroy(c->table); 136 | la_vstring_destroy(c->statsd_metric_name__entry_count, true); 137 | XFREE(c->name); 138 | XFREE(c); 139 | } 140 | } 141 | 142 | /**************************************** 143 | * Private variables and methods 144 | ****************************************/ 145 | 146 | // Callback for la_hash_foreach_remove 147 | // Used to expire old entries from the cache 148 | static bool is_cache_entry_expired(void const *key, void const *value, void *ctx) { 149 | UNUSED(key); 150 | 151 | struct cache_entry const *entry = value; 152 | time_t min_created_time = *(time_t *)ctx; 153 | return (entry->created_time <= min_created_time); 154 | } 155 | 156 | static void cache_entry_destroy(void *entry) { 157 | if(entry != NULL) { 158 | struct cache_entry *e = entry; 159 | if(e->data_destroy) { 160 | e->data_destroy(e->data); 161 | } 162 | XFREE(e); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/statsd.c: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #include // snprintf 3 | #include 4 | #include 5 | #include 6 | #include // strtok 7 | #include // gettimeofday, struct timeval 8 | #include // statsd_* 9 | #include // la_msg_dir 10 | #include // la_vstring 11 | #include "globals.h" // Config 12 | #include "util.h" // debug_print 13 | 14 | #define STATSD_NAMESPACE "dumphfdl" 15 | static statsd_link *statsd = NULL; 16 | 17 | static char const *counters_per_channel[] = { 18 | "demod.preamble.A2_found", 19 | "demod.preamble.M1_found", 20 | "demod.preamble.errors.M1_not_found", 21 | "frame.errors.bad_fcs", 22 | "frame.errors.too_short", 23 | "frame.dir.air2gnd", 24 | "frame.dir.gnd2air", 25 | "frames.good", 26 | "frames.processed", 27 | "lpdu.errors.bad_fcs", 28 | "lpdu.errors.too_short", 29 | "lpdus.good", 30 | "lpdus.processed", 31 | NULL 32 | }; 33 | 34 | static char const *counters_per_msgdir[] = { 35 | "acars.reasm.unknown", 36 | "acars.reasm.complete", 37 | // "acars.reasm.in_progress", // we report final reasm states only 38 | "acars.reasm.skipped", 39 | "acars.reasm.duplicate", 40 | "acars.reasm.out_of_seq", 41 | "acars.reasm.invalid_args", 42 | NULL 43 | }; 44 | 45 | static char const *msg_dir_labels[] = { 46 | [LA_MSG_DIR_UNKNOWN] = "unknown", 47 | [LA_MSG_DIR_AIR2GND] = "air2gnd", 48 | [LA_MSG_DIR_GND2AIR] = "gnd2air" 49 | }; 50 | 51 | int32_t statsd_initialize(char *statsd_addr) { 52 | ASSERT(statsd_addr != NULL); 53 | char *addr; 54 | char *port; 55 | 56 | if((addr = strtok(statsd_addr, ":")) == NULL) { 57 | return -1; 58 | } 59 | if((port = strtok(NULL, ":")) == NULL) { 60 | return -1; 61 | } 62 | la_vstring *statsd_namespace = la_vstring_new(); 63 | la_vstring_append_sprintf(statsd_namespace, "%s", STATSD_NAMESPACE); 64 | if(Config.station_id != NULL) { 65 | fprintf(stderr, "Using extended statsd namespace %s.%s\n", STATSD_NAMESPACE, Config.station_id); 66 | la_vstring_append_sprintf(statsd_namespace, ".%s", Config.station_id); 67 | } 68 | statsd = statsd_init_with_namespace(addr, atoi(port), statsd_namespace->str); 69 | la_vstring_destroy(statsd_namespace, true); 70 | if(statsd == NULL) { 71 | return -2; 72 | } 73 | return 0; 74 | } 75 | 76 | void statsd_initialize_counters_per_channel(int32_t freq) { 77 | if(statsd == NULL) { 78 | return; 79 | } 80 | char metric[256]; 81 | for(int32_t n = 0; counters_per_channel[n] != NULL; n++) { 82 | snprintf(metric, sizeof(metric), "channels.%d.%s", freq, counters_per_channel[n]); 83 | statsd_count(statsd, metric, 0, 1.0); 84 | } 85 | } 86 | 87 | static void statsd_initialize_counters_for_msg_dir(char const *counters[], la_msg_dir msg_dir) { 88 | char metric[256]; 89 | for(int32_t n = 0; counters[n] != NULL; n++) { 90 | snprintf(metric, sizeof(metric), "%s.%s", counters[n], msg_dir_labels[msg_dir]); 91 | statsd_count(statsd, metric, 0, 1.0); 92 | } 93 | } 94 | 95 | void statsd_initialize_counters_per_msgdir() { 96 | if(statsd == NULL) { 97 | return; 98 | } 99 | statsd_initialize_counters_for_msg_dir(counters_per_msgdir, LA_MSG_DIR_AIR2GND); 100 | statsd_initialize_counters_for_msg_dir(counters_per_msgdir, LA_MSG_DIR_GND2AIR); 101 | } 102 | 103 | void statsd_initialize_counter_set(char **counter_set) { 104 | if(statsd == NULL) { 105 | return; 106 | } 107 | for(int32_t n = 0; counter_set[n] != NULL; n++) { 108 | statsd_count(statsd, counter_set[n], 0, 1.0); 109 | } 110 | } 111 | 112 | void statsd_counter_per_channel_increment(int32_t freq, char *counter) { 113 | if(statsd == NULL) { 114 | return; 115 | } 116 | char metric[256]; 117 | snprintf(metric, sizeof(metric), "channels.%d.%s", freq, counter); 118 | statsd_inc(statsd, metric, 1.0); 119 | } 120 | 121 | void statsd_counter_per_msgdir_increment(la_msg_dir msg_dir, char *counter) { 122 | if(statsd == NULL) { 123 | return; 124 | } 125 | char metric[256]; 126 | snprintf(metric, sizeof(metric), "%s.%s", counter, msg_dir_labels[msg_dir]); 127 | statsd_inc(statsd, metric, 1.0); 128 | } 129 | 130 | void statsd_counter_increment(char *counter) { 131 | if(statsd == NULL) { 132 | return; 133 | } 134 | statsd_inc(statsd, counter, 1.0); 135 | } 136 | 137 | void statsd_gauge_set(char *gauge, size_t value) { 138 | if(statsd == NULL) { 139 | return; 140 | } 141 | statsd_gauge(statsd, gauge, value); 142 | } 143 | 144 | void statsd_gauge_per_channel_set(int32_t freq, char *gauge, size_t value) { 145 | if(statsd == NULL) { 146 | return; 147 | } 148 | char metric[256]; 149 | snprintf(metric, sizeof(metric), "channels.%d.%s", freq, gauge); 150 | statsd_gauge(statsd, metric, value); 151 | } 152 | 153 | void statsd_timing_delta_per_channel_send(int32_t freq, char *timer, struct timeval ts) { 154 | if(statsd == NULL) { 155 | return; 156 | } 157 | char metric[256]; 158 | struct timeval te; 159 | uint32_t tdiff; 160 | gettimeofday(&te, NULL); 161 | if(te.tv_sec < ts.tv_sec || (te.tv_sec == ts.tv_sec && te.tv_usec < ts.tv_usec)) { 162 | debug_print(D_STATS, "timediff is negative: ts.tv_sec=%lu ts.tv_usec=%lu te.tv_sec=%lu te.tv_usec=%lu\n", 163 | ts.tv_sec, ts.tv_usec, te.tv_sec, te.tv_usec); 164 | return; 165 | } 166 | tdiff = ((te.tv_sec - ts.tv_sec) * 1000000UL + te.tv_usec - ts.tv_usec) / 1000; 167 | debug_print(D_STATS, "tdiff: %u ms\n", tdiff); 168 | snprintf(metric, sizeof(metric), "%d.%s", freq, timer); 169 | statsd_timing(statsd, metric, tdiff); 170 | } 171 | -------------------------------------------------------------------------------- /src/output-zmq.c: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #include // fprintf 3 | #include // strdup, strerror 4 | #include // errno 5 | #include // zmq_* 6 | #include "config.h" // LIBZMQ_VER_* 7 | #include "output-common.h" // output_descriptor_t, output_qentry_t, output_queue_drain 8 | #include "kvargs.h" // kvargs 9 | #include "options.h" // option_descr_t 10 | #include "util.h" // ASSERT, NEW 11 | 12 | typedef enum { 13 | ZMQ_MODE_SERVER, 14 | ZMQ_MODE_CLIENT 15 | } out_zmq_mode_t; 16 | 17 | typedef struct { 18 | char *endpoint; 19 | void *zmq_ctx; 20 | void *zmq_sock; 21 | out_zmq_mode_t mode; 22 | } out_zmq_ctx_t; 23 | 24 | static bool out_zmq_supports_format(output_format_t format) { 25 | return(format == OFMT_TEXT || format == OFMT_BASESTATION || format == OFMT_JSON); 26 | } 27 | 28 | static void *out_zmq_configure(kvargs *kv) { 29 | ASSERT(kv != NULL); 30 | NEW(out_zmq_ctx_t, cfg); 31 | 32 | int major, minor, patch; 33 | zmq_version(&major, &minor, &patch); 34 | if((major * 1000000 + minor * 1000 + patch) < (LIBZMQ_VER_MAJOR_MIN * 1000000 + LIBZMQ_VER_MINOR_MIN * 1000 + LIBZMQ_VER_PATCH_MIN)) { 35 | fprintf(stderr, "output_zmq: error: libzmq library version %d.%d.%d is too old; at least %d.%d.%d is required\n", 36 | major, minor, patch, 37 | LIBZMQ_VER_MAJOR_MIN, LIBZMQ_VER_MINOR_MIN, LIBZMQ_VER_PATCH_MIN); 38 | goto fail; 39 | } 40 | if(kvargs_get(kv, "endpoint") == NULL) { 41 | fprintf(stderr, "output_zmq: endpoint not specified\n"); 42 | goto fail; 43 | } 44 | cfg->endpoint = strdup(kvargs_get(kv, "endpoint")); 45 | char *mode = NULL; 46 | if((mode = kvargs_get(kv, "mode")) == NULL) { 47 | fprintf(stderr, "output_zmq: mode not specified\n"); 48 | goto fail; 49 | } 50 | if(!strcmp(mode, "server")) { 51 | cfg->mode = ZMQ_MODE_SERVER; 52 | } else if(!strcmp(mode, "client")) { 53 | cfg->mode = ZMQ_MODE_CLIENT; 54 | } else { 55 | fprintf(stderr, "output_zmq: mode '%s' is invalid; must be either 'client' or 'server'\n", mode); 56 | goto fail; 57 | } 58 | return cfg; 59 | fail: 60 | XFREE(cfg); 61 | return NULL; 62 | } 63 | 64 | static int out_zmq_init(void *selfptr) { 65 | ASSERT(selfptr != NULL); 66 | out_zmq_ctx_t *self = selfptr; 67 | 68 | self->zmq_ctx = zmq_ctx_new(); 69 | if(self->zmq_ctx == NULL) { 70 | fprintf(stderr, "output_zmq(%s): failed to set up ZMQ context\n", self->endpoint); 71 | return -1; 72 | } 73 | self->zmq_sock = zmq_socket(self->zmq_ctx, ZMQ_PUB); 74 | int rc = 0; 75 | if(self->mode == ZMQ_MODE_SERVER) { 76 | rc = zmq_bind(self->zmq_sock, self->endpoint); 77 | } else { // ZMQ_MODE_CLIENT 78 | rc = zmq_connect(self->zmq_sock, self->endpoint); 79 | } 80 | if(rc < 0) { 81 | fprintf(stderr, "output_zmq(%s): %s failed: %s\n", 82 | self->endpoint, self->mode == ZMQ_MODE_SERVER ? "bind" : "connect", 83 | zmq_strerror(errno)); 84 | return -1; 85 | } 86 | rc = zmq_setsockopt(self->zmq_sock, ZMQ_SNDHWM, &Config.output_queue_hwm, 87 | sizeof(Config.output_queue_hwm)); 88 | if(rc < 0) { 89 | fprintf(stderr, "output_zmq(%s): could not set ZMQ_SNDHWM option for socket: %s\n", 90 | self->endpoint, zmq_strerror(errno)); 91 | return -1; 92 | } 93 | return 0; 94 | } 95 | 96 | static int out_zmq_produce_text(out_zmq_ctx_t *self, struct metadata *metadata, struct octet_string *msg) { 97 | UNUSED(metadata); 98 | ASSERT(msg != NULL); 99 | ASSERT(self->zmq_sock != 0); 100 | if(msg->len < 2) { 101 | return 0; 102 | } 103 | if(zmq_send(self->zmq_sock, msg->buf, msg->len, 0) < 0) { 104 | return -1; 105 | } 106 | return 0; 107 | } 108 | 109 | static int out_zmq_produce(void *selfptr, output_format_t format, struct metadata *metadata, struct octet_string *msg) { 110 | ASSERT(selfptr != NULL); 111 | out_zmq_ctx_t *self = selfptr; 112 | int32_t result = 0; 113 | if(format == OFMT_TEXT || format == OFMT_JSON || format == OFMT_BASESTATION) { 114 | result = out_zmq_produce_text(self, metadata, msg); 115 | } 116 | if(result < 0) { 117 | fprintf(stderr, "output_zmq(%s): zmq_send error: %s\n", self->endpoint, zmq_strerror(errno)); 118 | return -1; 119 | } 120 | return 0; 121 | } 122 | 123 | static void out_zmq_handle_shutdown(void *selfptr) { 124 | ASSERT(selfptr != NULL); 125 | out_zmq_ctx_t *self = selfptr; 126 | fprintf(stderr, "output_zmq(%s): shutting down\n", self->endpoint); 127 | zmq_close(self->zmq_sock); 128 | zmq_ctx_destroy(self->zmq_ctx); 129 | } 130 | 131 | static void out_zmq_handle_failure(void *selfptr) { 132 | ASSERT(selfptr != NULL); 133 | out_zmq_ctx_t *self = selfptr; 134 | fprintf(stderr, "output_zmq(%s): could not %s, deactivating output\n", 135 | self->endpoint, self->mode == ZMQ_MODE_SERVER ? "bind" : "connect"); 136 | } 137 | 138 | static const option_descr_t out_zmq_options[] = { 139 | { 140 | .name = "mode", 141 | .description = "Socket mode: client or server (required)" 142 | }, 143 | { 144 | .name= "endpoint", 145 | .description = "Socket endpoint: tcp://address:port (required)" 146 | }, 147 | { 148 | .name = NULL, 149 | .description = NULL 150 | } 151 | }; 152 | 153 | output_descriptor_t out_DEF_zmq = { 154 | .name = "zmq", 155 | .description = "Output to a ZeroMQ publisher socket (as a server or a client)", 156 | .options = out_zmq_options, 157 | .supports_format = out_zmq_supports_format, 158 | .configure = out_zmq_configure, 159 | .init = out_zmq_init, 160 | .produce = out_zmq_produce, 161 | .handle_shutdown = out_zmq_handle_shutdown, 162 | .handle_failure = out_zmq_handle_failure 163 | }; 164 | -------------------------------------------------------------------------------- /src/util.h: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #pragma once 3 | 4 | #include // offsetof 5 | #include // fprintf, stderr 6 | #include // pthread_t, pthread_barrier_t 7 | #include // calloc, realloc 8 | #include // la_proto_node, la_type_descriptor 9 | #include // la_vstring 10 | #include "globals.h" // Config 11 | #include "config.h" 12 | #ifndef HAVE_PTHREAD_BARRIERS 13 | #include "pthread_barrier.h" 14 | #endif 15 | 16 | // debug message classes 17 | #define D_ALL (~0) 18 | #define D_NONE (0) 19 | #define D_SDR (1 << 0) 20 | #define D_DSP (1 << 1) 21 | #define D_DSP_DETAIL (1 << 2) 22 | #define D_FRAME (1 << 3) 23 | #define D_FRAME_DETAIL (1 << 4) 24 | #define D_PROTO (1 << 5) 25 | #define D_PROTO_DETAIL (1 << 6) 26 | #define D_STATS (1 << 7) 27 | #define D_CACHE (1 << 8) 28 | #define D_OUTPUT (1 << 9) 29 | #define D_MISC (1 << 31) 30 | 31 | #define nop() do {} while (0) 32 | 33 | #ifdef __GNUC__ 34 | #define LIKELY(x) (__builtin_expect(!!(x),1)) 35 | #define UNLIKELY(x) (__builtin_expect(!!(x),0)) 36 | #else 37 | #define LIKELY(x) (x) 38 | #define UNLIKELY(x) (x) 39 | #endif 40 | 41 | #ifdef __GNUC__ 42 | #define PRETTY_FUNCTION __PRETTY_FUNCTION__ 43 | #else 44 | #define PRETTY_FUNCTION "" 45 | #endif 46 | 47 | #define ASSERT_se(expr) \ 48 | do { \ 49 | if (UNLIKELY(!(expr))) { \ 50 | fprintf(stderr, "Assertion '%s' failed at %s:%u, function %s(). Aborting.\n", #expr, \ 51 | __FILE__, __LINE__, PRETTY_FUNCTION); \ 52 | abort(); \ 53 | } \ 54 | } while (0) 55 | 56 | #ifdef NDEBUG 57 | #define ASSERT(expr) nop() 58 | #else 59 | #define ASSERT(expr) ASSERT_se(expr) 60 | #endif 61 | 62 | #ifdef DEBUG 63 | #define debug_print(debug_class, fmt, ...) \ 64 | do { \ 65 | if(Config.debug_filter & debug_class) { \ 66 | fprintf(stderr, "%s(): " fmt, __func__, ##__VA_ARGS__); \ 67 | } \ 68 | } while (0) 69 | 70 | #define debug_print_buf_hex(debug_class, buf, len, fmt, ...) \ 71 | do { \ 72 | if(Config.debug_filter & debug_class) { \ 73 | fprintf(stderr, "%s(): " fmt, __func__, ##__VA_ARGS__); \ 74 | fprintf(stderr, "%s(): ", __func__); \ 75 | for(size_t zz = 0; zz < (len); zz++) { \ 76 | fprintf(stderr, "%02x ", buf[zz]); \ 77 | if(zz && (zz+1) % 32 == 0) fprintf(stderr, "\n%s(): ", __func__); \ 78 | } \ 79 | fprintf(stderr, "\n"); \ 80 | } \ 81 | } while(0) 82 | #else 83 | #define debug_print(debug_class, fmt, ...) nop() 84 | #define debug_print_buf_hex(debug_class, buf, len, fmt, ...) nop() 85 | #endif 86 | 87 | #define SAFE_JSON_APPEND_STRING(v, n, val) \ 88 | do { \ 89 | if((val) != NULL) { \ 90 | la_json_append_string((v), (n), (val)); \ 91 | } \ 92 | } while(0) 93 | 94 | #define XCALLOC(nmemb, size) xcalloc((nmemb), (size), __FILE__, __LINE__, __func__) 95 | #define XREALLOC(ptr, size) xrealloc((ptr), (size), __FILE__, __LINE__, __func__) 96 | #define XFREE(ptr) do { free(ptr); ptr = NULL; } while(0) 97 | #define NEW(type, x) type *(x) = XCALLOC(1, sizeof(type)) 98 | #define UNUSED(x) (void)(x) 99 | #define container_of(ptr, type, member) ((type *)((char *)(ptr) - offsetof(type, member))) 100 | #define max(a, b) ((a) > (b) ? (a) : (b)) 101 | #define EOL(x) la_vstring_append_sprintf((x), "%s", "\n") 102 | #define HZ_TO_KHZ(f) ((f) / 1000.0) 103 | 104 | // Stringize a macro with a numeric value 105 | #define _STR(x) #x 106 | #define STR(x) _STR(x) 107 | 108 | // Reverse bit order in a byte (http://graphics.stanford.edu/~seander/bithacks.html#ReverseByteWith64Bits) 109 | #define REVERSE_BYTE(x) (uint8_t)((((x) * 0x80200802ULL) & 0x0884422110ULL) * 0x0101010101ULL >> 32) 110 | 111 | void *xcalloc(size_t nmemb, size_t size, char const *file, int32_t line, char const *func); 112 | void *xrealloc(void *ptr, size_t size, char const *file, int32_t line, char const *func); 113 | int32_t start_thread(pthread_t *pth, void *(*start_routine)(void *), void *thread_ctx); 114 | void stop_thread(pthread_t pth); 115 | int32_t pthread_barrier_create(pthread_barrier_t *barrier, unsigned count); 116 | int32_t pthread_cond_initialize(pthread_cond_t *cond); 117 | int32_t pthread_mutex_initialize(pthread_mutex_t *mutex); 118 | 119 | struct octet_string { 120 | uint8_t *buf; 121 | size_t len; 122 | }; 123 | struct octet_string *octet_string_new(void *buf, size_t len); 124 | struct octet_string *octet_string_copy(struct octet_string const *ostring); 125 | void octet_string_destroy(struct octet_string *ostring); 126 | 127 | void append_hexdump_with_indent(la_vstring *vstr, uint8_t *data, size_t len, int32_t indent); 128 | 129 | la_proto_node *unknown_proto_pdu_new(void *buf, size_t len); 130 | 131 | uint32_t parse_icao_hex(uint8_t const buf[3]); 132 | 133 | #define GS_MAX_FREQ_CNT 20 // Max number of frequencies assigned to a ground station 134 | void freq_list_format_text(la_vstring *vstr, int32_t indent, char const *label, uint8_t gs_id, uint32_t freqs); 135 | void freq_list_format_json(la_vstring *vstr, char const *label, uint8_t gs_id, uint32_t freqs); 136 | void gs_id_format_text(la_vstring *vstr, int32_t indent, char const *label, uint8_t gs_id); 137 | void gs_id_format_json(la_vstring *vstr, char const *label, uint8_t gs_id); 138 | void ac_id_format_text(la_vstring *vstr, int32_t indent, char const *label, int32_t freq, uint8_t ac_id); 139 | void ac_id_format_json(la_vstring *vstr, char const *label, int32_t freq, uint8_t ac_id); 140 | void ac_data_format_text(la_vstring *vstr, int32_t indent, uint32_t addr); 141 | void ac_data_format_json(la_vstring *vstr, char const *label, uint32_t addr); 142 | 143 | struct location { 144 | double lat, lon; 145 | }; 146 | double parse_coordinate(uint32_t c); 147 | -------------------------------------------------------------------------------- /src/libfec/viterbi27_port.c: -------------------------------------------------------------------------------- 1 | /* K=7 r=1/2 Viterbi decoder in portable C 2 | * Copyright Feb 2004, Phil Karn, KA9Q 3 | * May be used under the terms of the GNU Lesser General Public License (LGPL) 4 | */ 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include "fec.h" 10 | 11 | typedef union { unsigned int w[64]; } metric_t; 12 | typedef union { unsigned long w[2];} decision_t; 13 | static union branchtab27 { unsigned char c[32]; } Branchtab27[2] __attribute__ ((aligned(16))); 14 | static int Init = 0; 15 | 16 | /* State info for instance of Viterbi decoder 17 | * Don't change this without also changing references in [mmx|sse|sse2]bfly29.s! 18 | */ 19 | struct v27 { 20 | metric_t metrics1; /* path metric buffer 1 */ 21 | metric_t metrics2; /* path metric buffer 2 */ 22 | decision_t *dp; /* Pointer to current decision */ 23 | metric_t *old_metrics,*new_metrics; /* Pointers to path metrics, swapped on every bit */ 24 | decision_t *decisions; /* Beginning of decisions for block */ 25 | }; 26 | 27 | static unsigned char Partab[256]; 28 | static int P_init; 29 | 30 | // Create 256-entry odd-parity lookup table 31 | static void partab_init(void){ 32 | int i,cnt,ti; 33 | 34 | /* Initialize parity lookup table */ 35 | for(i=0;i<256;i++){ 36 | cnt = 0; 37 | ti = i; 38 | while(ti){ 39 | if(ti & 1) 40 | cnt++; 41 | ti >>= 1; 42 | } 43 | Partab[i] = cnt & 1; 44 | } 45 | P_init=1; 46 | } 47 | 48 | static inline int parityb(unsigned char x){ 49 | extern unsigned char Partab[256]; 50 | extern int P_init; 51 | if(!P_init){ 52 | partab_init(); 53 | } 54 | return Partab[x]; 55 | } 56 | 57 | static inline int parity(int x){ 58 | /* Fold down to one byte */ 59 | x ^= (x >> 16); 60 | x ^= (x >> 8); 61 | return parityb(x); 62 | } 63 | 64 | /* Initialize Viterbi decoder for start of new frame */ 65 | int init_viterbi27(void *p,int starting_state){ 66 | struct v27 *vp = p; 67 | int i; 68 | 69 | if(p == NULL) 70 | return -1; 71 | for(i=0;i<64;i++) 72 | vp->metrics1.w[i] = 63; 73 | 74 | vp->old_metrics = &vp->metrics1; 75 | vp->new_metrics = &vp->metrics2; 76 | vp->dp = vp->decisions; 77 | vp->old_metrics->w[starting_state & 63] = 0; /* Bias known start state */ 78 | return 0; 79 | } 80 | 81 | void set_viterbi27_polynomial(int polys[2]){ 82 | int state; 83 | 84 | for(state=0;state < 32;state++){ 85 | Branchtab27[0].c[state] = (polys[0] < 0) ^ parity((2*state) & abs(polys[0])) ? 255 : 0; 86 | Branchtab27[1].c[state] = (polys[1] < 0) ^ parity((2*state) & abs(polys[1])) ? 255 : 0; 87 | } 88 | Init++; 89 | } 90 | 91 | /* Create a new instance of a Viterbi decoder */ 92 | void *create_viterbi27(int len){ 93 | if(!Init){ 94 | int polys[2] = { V27POLYA, V27POLYB }; 95 | set_viterbi27_polynomial(polys); 96 | } 97 | struct v27 *vp = calloc(1, sizeof(struct v27)); 98 | vp->decisions = calloc(len + 6, sizeof(decision_t)); 99 | init_viterbi27(vp,0); 100 | 101 | return vp; 102 | } 103 | 104 | /* Viterbi chainback */ 105 | int chainback_viterbi27( 106 | void *p, 107 | unsigned char *data, /* Decoded output data */ 108 | unsigned int nbits, /* Number of data bits */ 109 | unsigned int endstate){ /* Terminal encoder state */ 110 | struct v27 *vp = p; 111 | decision_t *d; 112 | 113 | if(p == NULL) 114 | return -1; 115 | d = vp->decisions; 116 | /* Make room beyond the end of the encoder register so we can 117 | * accumulate a full byte of decoded data 118 | */ 119 | endstate %= 64; 120 | endstate <<= 2; 121 | 122 | /* The store into data[] only needs to be done every 8 bits. 123 | * But this avoids a conditional branch, and the writes will 124 | * combine in the cache anyway 125 | */ 126 | d += 6; /* Look past tail */ 127 | while(nbits-- != 0){ 128 | int k; 129 | 130 | k = (d[nbits].w[(endstate>>2)/32] >> ((endstate>>2)%32)) & 1; 131 | data[nbits>>3] = endstate = (endstate >> 1) | (k << 7); 132 | } 133 | return 0; 134 | } 135 | 136 | /* Delete instance of a Viterbi decoder */ 137 | void delete_viterbi27(void *p){ 138 | struct v27 *vp = p; 139 | 140 | if(vp != NULL){ 141 | free(vp->decisions); 142 | free(vp); 143 | } 144 | } 145 | 146 | /* C-language butterfly */ 147 | #define BFLY(i) {\ 148 | unsigned int metric,m0,m1,decision;\ 149 | metric = (Branchtab27[0].c[i] ^ sym0) + (Branchtab27[1].c[i] ^ sym1);\ 150 | m0 = vp->old_metrics->w[i] + metric;\ 151 | m1 = vp->old_metrics->w[i+32] + (510 - metric);\ 152 | decision = (signed int)(m0-m1) > 0;\ 153 | vp->new_metrics->w[2*i] = decision ? m1 : m0;\ 154 | d->w[i/16] |= decision << ((2*i)&31);\ 155 | m0 -= (metric+metric-510);\ 156 | m1 += (metric+metric-510);\ 157 | decision = (signed int)(m0-m1) > 0;\ 158 | vp->new_metrics->w[2*i+1] = decision ? m1 : m0;\ 159 | d->w[i/16] |= decision << ((2*i+1)&31);\ 160 | } 161 | 162 | /* Update decoder with a block of demodulated symbols 163 | * Note that nbits is the number of decoded data bits, not the number 164 | * of symbols! 165 | */ 166 | int update_viterbi27_blk(void *p,unsigned char *syms,int nbits){ 167 | struct v27 *vp = p; 168 | void *tmp; 169 | decision_t *d; 170 | 171 | if(p == NULL) 172 | return -1; 173 | d = (decision_t *)vp->dp; 174 | while(nbits--){ 175 | unsigned char sym0,sym1; 176 | 177 | d->w[0] = d->w[1] = 0; 178 | sym0 = *syms++; 179 | sym1 = *syms++; 180 | 181 | BFLY(0); 182 | BFLY(1); 183 | BFLY(2); 184 | BFLY(3); 185 | BFLY(4); 186 | BFLY(5); 187 | BFLY(6); 188 | BFLY(7); 189 | BFLY(8); 190 | BFLY(9); 191 | BFLY(10); 192 | BFLY(11); 193 | BFLY(12); 194 | BFLY(13); 195 | BFLY(14); 196 | BFLY(15); 197 | BFLY(16); 198 | BFLY(17); 199 | BFLY(18); 200 | BFLY(19); 201 | BFLY(20); 202 | BFLY(21); 203 | BFLY(22); 204 | BFLY(23); 205 | BFLY(24); 206 | BFLY(25); 207 | BFLY(26); 208 | BFLY(27); 209 | BFLY(28); 210 | BFLY(29); 211 | BFLY(30); 212 | BFLY(31); 213 | d++; 214 | /* Swap pointers to old and new metrics */ 215 | tmp = vp->old_metrics; 216 | vp->old_metrics = vp->new_metrics; 217 | vp->new_metrics = tmp; 218 | } 219 | vp->dp = d; 220 | return 0; 221 | } 222 | -------------------------------------------------------------------------------- /src/libcsdr.c: -------------------------------------------------------------------------------- 1 | /* 2 | this software is part of libcsdr, a set of simple dsp routines for 3 | software defined radio. 4 | 5 | copyright (c) 2014, andras retzler 6 | all rights reserved. 7 | 8 | redistribution and use in source and binary forms, with or without 9 | modification, are permitted provided that the following conditions are met: 10 | * redistributions of source code must retain the above copyright 11 | notice, this list of conditions and the following disclaimer. 12 | * redistributions in binary form must reproduce the above copyright 13 | notice, this list of conditions and the following disclaimer in the 14 | documentation and/or other materials provided with the distribution. 15 | * neither the name of the copyright holder nor the 16 | names of its contributors may be used to endorse or promote products 17 | derived from this software without specific prior written permission. 18 | 19 | this software is provided by the copyright holders and contributors "as is" and 20 | any express or implied warranties, including, but not limited to, the implied 21 | warranties of merchantability and fitness for a particular purpose are 22 | disclaimed. in no event shall andras retzler be liable for any 23 | direct, indirect, incidental, special, exemplary, or consequential damages 24 | (including, but not limited to, procurement of substitute goods or services; 25 | loss of use, data, or profits; or business interruption) however caused and 26 | on any theory of liability, whether in contract, strict liability, or tort 27 | (including negligence or otherwise) arising in any way out of the use of this 28 | software, even if advised of the possibility of such damage. 29 | */ 30 | 31 | #include // cos, M_PI 32 | #include 33 | #include "libcsdr.h" // window_t 34 | #include "util.h" // XCALLOC 35 | 36 | int32_t next_pow2(int32_t x) 37 | { 38 | int32_t pow2; 39 | //portability? (31 is the problem) 40 | for(int32_t i=0;i<31;i++) 41 | { 42 | if(x<(pow2=1<2*M_PI) phase-=2*M_PI; //@@firdes_bandpass_c 128 | while(phase<0) phase+=2*M_PI; 129 | output[i] = CMPLXF(cosval*realtaps[i], sinval*realtaps[i]); 130 | //output[i] := realtaps[i] * e^j*w 131 | } 132 | XFREE(realtaps); 133 | } 134 | 135 | float compute_filter_relative_transition_bw(int32_t sample_rate, int32_t transition_bw_Hz) { 136 | ASSERT(sample_rate != 0); 137 | return (float)transition_bw_Hz / (float)sample_rate; 138 | } 139 | 140 | int32_t compute_fft_decimation_rate(int32_t sample_rate, int32_t target_rate) { 141 | ASSERT(sample_rate > 0); 142 | int32_t decimation_rate_int = floorf((float)sample_rate / (float)(target_rate)); 143 | return next_pow2(decimation_rate_int) / 2; 144 | } 145 | 146 | -------------------------------------------------------------------------------- /src/acars.c: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #include // trunc 3 | #include // la_type_descriptor, la_proto_node, LA_MSG_DIR_* 4 | #include // la_reasm_ctx 5 | #include // la_acars_parse 6 | #include // la_adsc_* 7 | #include // la_dict 8 | #include // la_list 9 | #include "config.h" // WITH_STATSD 10 | #include "pdu.h" // enum hfdl_pdu_direction 11 | #include "position.h" // position_info 12 | #include "statsd.h" // statsd_* 13 | #include "util.h" // ASSERT 14 | 15 | /****************************** 16 | * Forward declarations 17 | ******************************/ 18 | 19 | #ifdef WITH_STATSD 20 | static void update_statsd_acars_metrics(la_msg_dir msg_dir, la_proto_node *root); 21 | #endif 22 | static struct position_info *adsc_position_info_extract(la_proto_node *tree); 23 | 24 | /****************************** 25 | * Public methods 26 | ******************************/ 27 | 28 | la_proto_node *acars_parse(uint8_t *buf, uint32_t len, enum hfdl_pdu_direction direction, 29 | la_reasm_ctx *reasm_ctx, struct timeval rx_timestamp) { 30 | ASSERT(buf); 31 | la_proto_node *node = NULL; 32 | if(len > 0 && buf[0] == 1) { // ACARS SOH byte 33 | la_msg_dir msg_dir = (direction == UPLINK_PDU ? LA_MSG_DIR_GND2AIR : LA_MSG_DIR_AIR2GND); 34 | node = la_acars_parse_and_reassemble(buf + 1, len - 1, msg_dir, reasm_ctx, rx_timestamp); 35 | #ifdef WITH_STATSD 36 | update_statsd_acars_metrics(msg_dir, node); 37 | #endif 38 | } 39 | return node; 40 | } 41 | 42 | struct position_info *acars_position_info_extract(la_proto_node *tree) { 43 | ASSERT(tree); 44 | 45 | struct position_info *pos_info = NULL; 46 | pos_info = adsc_position_info_extract(tree); 47 | 48 | return pos_info; 49 | } 50 | 51 | /**************************************** 52 | * Private variables and methods 53 | ****************************************/ 54 | 55 | #ifdef WITH_STATSD 56 | static void update_statsd_acars_metrics(la_msg_dir msg_dir, la_proto_node *root) { 57 | static la_dict const reasm_status_counter_names[] = { 58 | { .id = LA_REASM_UNKNOWN, .val = "acars.reasm.unknown" }, 59 | { .id = LA_REASM_COMPLETE, .val = "acars.reasm.complete" }, 60 | // { .id = LA_REASM_IN_PROGRESS, .val = "acars.reasm.in_progress" }, // report final states only 61 | { .id = LA_REASM_SKIPPED, .val = "acars.reasm.skipped" }, 62 | { .id = LA_REASM_DUPLICATE, .val = "acars.reasm.duplicate" }, 63 | { .id = LA_REASM_FRAG_OUT_OF_SEQUENCE, .val = "acars.reasm.out_of_seq" }, 64 | { .id = LA_REASM_ARGS_INVALID, .val = "acars.reasm.invalid_args" }, 65 | { .id = 0, .val = NULL } 66 | }; 67 | la_proto_node *node = la_proto_tree_find_acars(root); 68 | if(node == NULL) { 69 | return; 70 | } 71 | la_acars_msg *amsg = node->data; 72 | if(amsg->err == true) { 73 | return; 74 | } 75 | char const *metric = la_dict_search(reasm_status_counter_names, amsg->reasm_status); 76 | if(metric == NULL) { 77 | return; 78 | } 79 | // Dropping const on metric is allowed here, because dumphfdl metric names 80 | // do not contain any characters that statsd-c-client library would need to 81 | // sanitize (replace with underscores) 82 | statsd_increment_per_msgdir(msg_dir, (char *)metric); 83 | } 84 | #endif 85 | 86 | static struct position_info *adsc_position_info_extract(la_proto_node *tree) { 87 | ASSERT(tree); 88 | 89 | struct position_info *pos_info = NULL; 90 | la_proto_node *adsc_node = la_proto_tree_find_adsc(tree); 91 | if(adsc_node == NULL) { 92 | return NULL; 93 | } 94 | la_adsc_msg_t *msg = adsc_node->data; 95 | ASSERT(msg != NULL); 96 | if(msg->err == true || msg->tag_list == NULL) { 97 | return NULL; 98 | } 99 | bool position_present = false; 100 | bool icao_address_present = false; 101 | bool flight_id_present = false; 102 | double lat = 0.0, lon = 0.0, timestamp = 0.0; 103 | uint32_t icao_address = 0; 104 | char *flight_id = NULL; 105 | la_adsc_basic_report_t *rpt_tag = NULL; 106 | la_adsc_flight_id_t *flight_id_tag = NULL; 107 | la_adsc_airframe_id_t *airframe_id_tag = NULL; 108 | for(la_list *l = msg->tag_list; l != NULL; l = la_list_next(l)) { 109 | la_adsc_tag_t *tag = l->data; 110 | ASSERT(tag != NULL); 111 | switch(tag->tag) { 112 | // Slightly ugly, but libacars does not have any better API 113 | // for searching / matching tags 114 | case 7: // Basic report 115 | case 9: // Emergency basic report 116 | case 10: // Lateral deviation change event 117 | case 18: // Vertical rate change event 118 | case 19: // Altitude range event 119 | case 20: // Waypoint change event 120 | rpt_tag = tag->data; 121 | ASSERT(rpt_tag != NULL); 122 | lat = rpt_tag->lat; 123 | lon = rpt_tag->lon; 124 | timestamp = rpt_tag->timestamp; 125 | position_present = true; 126 | break; 127 | case 17: // Airframe ID 128 | airframe_id_tag = tag->data; 129 | ASSERT(airframe_id_tag != NULL); 130 | uint8_t *hex = airframe_id_tag->icao_hex; 131 | icao_address = (uint32_t)hex[0] << 16 | (uint32_t)hex[1] << 8 | (uint32_t)hex[2]; 132 | icao_address_present = true; 133 | break; 134 | case 12: // Flight ID 135 | flight_id_tag = tag->data; 136 | ASSERT(flight_id_tag != NULL); 137 | flight_id = flight_id_tag->id; 138 | flight_id_present = true; 139 | break; 140 | default: 141 | break; 142 | } 143 | } 144 | 145 | if(position_present == false) { 146 | debug_print(D_MISC, "No position found\n"); 147 | return NULL; 148 | } 149 | pos_info = position_info_create(); 150 | pos_info->position.location.lat = lat; 151 | pos_info->position.location.lon = lon; 152 | pos_info->position.timestamp.tm.tm_min = trunc(timestamp / 60.0); 153 | pos_info->position.timestamp.tm.tm_sec = trunc(timestamp - 60.0 * trunc(timestamp / 60.0)); 154 | pos_info->position.timestamp.tm_min_present = true; 155 | pos_info->position.timestamp.tm_sec_present = true; 156 | if(icao_address_present) { 157 | pos_info->aircraft.icao_address = icao_address; 158 | pos_info->aircraft.icao_address_present = true; 159 | } 160 | if(flight_id_present) { 161 | pos_info->aircraft.flight_id = flight_id; 162 | pos_info->aircraft.flight_id_present = true; 163 | } 164 | debug_print(D_MISC, "lat: %f lon: %f t: %02d:%02d icao_addr: %06X flight_id: %s\n", 165 | pos_info->position.location.lat, 166 | pos_info->position.location.lon, 167 | pos_info->position.timestamp.tm.tm_min, 168 | pos_info->position.timestamp.tm.tm_sec, 169 | icao_address_present ? pos_info->aircraft.icao_address : 0, 170 | flight_id_present ? pos_info->aircraft.flight_id : "" 171 | ); 172 | return pos_info; 173 | } 174 | -------------------------------------------------------------------------------- /src/output-file.c: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #include // FILE, fprintf, fwrite 3 | #include // strcmp, strdup, strerror 4 | #include // gmtime_r, localtime_r, strftime 5 | #include // errno 6 | #include // htons 7 | #include "output-common.h" // output_descriptor_t, output_qentry_t, output_queue_drain 8 | #include "output-file.h" // OUT_BINARY_FRAME_LEN_OCTETS, OUT_BINARY_FRAME_LEN_MAX 9 | #include "kvargs.h" // kvargs 10 | #include "options.h" // option_descr_t 11 | #include "util.h" // ASSERT, NEW 12 | 13 | typedef enum { 14 | ROT_NONE, 15 | ROT_HOURLY, 16 | ROT_DAILY 17 | } out_file_rotation_mode; 18 | 19 | typedef struct { 20 | FILE *fh; 21 | char *filename_prefix; 22 | char *extension; 23 | size_t prefix_len; 24 | struct tm current_tm; 25 | out_file_rotation_mode rotate; 26 | } out_file_ctx_t; 27 | 28 | static bool out_file_supports_format(output_format_t format) { 29 | return(format == OFMT_TEXT || format == OFMT_BASESTATION || format == OFMT_JSON); 30 | } 31 | 32 | static void *out_file_configure(kvargs *kv) { 33 | ASSERT(kv != NULL); 34 | NEW(out_file_ctx_t, cfg); 35 | if(kvargs_get(kv, "path") == NULL) { 36 | fprintf(stderr, "output_file: path not specified\n"); 37 | goto fail; 38 | } 39 | cfg->filename_prefix = strdup(kvargs_get(kv, "path")); 40 | debug_print(D_OUTPUT, "filename_prefix: %s\n", cfg->filename_prefix); 41 | char *rotate = kvargs_get(kv, "rotate"); 42 | if(rotate != NULL) { 43 | if(!strcmp(rotate, "hourly")) { 44 | cfg->rotate = ROT_HOURLY; 45 | } else if(!strcmp(rotate, "daily")) { 46 | cfg->rotate = ROT_DAILY; 47 | } else { 48 | fprintf(stderr, "output_file: invalid rotation mode: %s\n", rotate); 49 | goto fail; 50 | } 51 | } else { 52 | cfg->rotate = ROT_NONE; 53 | } 54 | return cfg; 55 | fail: 56 | XFREE(cfg); 57 | return NULL; 58 | } 59 | 60 | static void out_file_ctx_destroy(void *ctxptr) { 61 | if(ctxptr != NULL) { 62 | out_file_ctx_t *ctx = ctxptr; 63 | XFREE(ctx->filename_prefix); 64 | XFREE(ctx); 65 | } 66 | } 67 | 68 | static int32_t out_file_open(out_file_ctx_t *self) { 69 | char *filename = NULL; 70 | char *fmt = NULL; 71 | size_t tlen = 0; 72 | 73 | if(self->rotate != ROT_NONE) { 74 | time_t t = time(NULL); 75 | if(Config.utc == true) { 76 | gmtime_r(&t, &self->current_tm); 77 | } else { 78 | localtime_r(&t, &self->current_tm); 79 | } 80 | char suffix[16]; 81 | if(self->rotate == ROT_HOURLY) { 82 | fmt = "_%Y%m%d_%H"; 83 | } else if(self->rotate == ROT_DAILY) { 84 | fmt = "_%Y%m%d"; 85 | } 86 | ASSERT(fmt != NULL); 87 | tlen = strftime(suffix, sizeof(suffix), fmt, &self->current_tm); 88 | if(tlen == 0) { 89 | fprintf(stderr, "open_outfile(): strfime returned 0\n"); 90 | return -1; 91 | } 92 | filename = XCALLOC(self->prefix_len + tlen + 2, sizeof(uint8_t)); 93 | sprintf(filename, "%s%s%s", self->filename_prefix, suffix, self->extension); 94 | } else { 95 | filename = strdup(self->filename_prefix); 96 | } 97 | 98 | if((self->fh = fopen(filename, "a+")) == NULL) { 99 | fprintf(stderr, "Could not open output file %s: %s\n", filename, strerror(errno)); 100 | XFREE(filename); 101 | return -1; 102 | } 103 | XFREE(filename); 104 | return 0; 105 | } 106 | 107 | static int32_t out_file_init(void *selfptr) { 108 | ASSERT(selfptr != NULL); 109 | out_file_ctx_t *self = selfptr; 110 | if(!strcmp(self->filename_prefix, "-")) { 111 | self->fh = stdout; 112 | self->rotate = ROT_NONE; 113 | } else { 114 | self->prefix_len = strlen(self->filename_prefix); 115 | if(self->rotate != ROT_NONE) { 116 | char *basename = strrchr(self->filename_prefix, '/'); 117 | if(basename != NULL) { 118 | basename++; 119 | } else { 120 | basename = self->filename_prefix; 121 | } 122 | char *ext = strrchr(self->filename_prefix, '.'); 123 | if(ext != NULL && (ext <= basename || ext[1] == '\0')) { 124 | ext = NULL; 125 | } 126 | if(ext) { 127 | self->extension = strdup(ext); 128 | *ext = '\0'; 129 | } else { 130 | self->extension = strdup(""); 131 | } 132 | } 133 | return out_file_open(self); 134 | } 135 | return 0; 136 | } 137 | 138 | static int32_t out_file_rotate(out_file_ctx_t *self) { 139 | // FIXME: rotation should be driven by message timestamp, not the current timestamp 140 | struct tm new_tm; 141 | time_t t = time(NULL); 142 | if(Config.utc == true) { 143 | gmtime_r(&t, &new_tm); 144 | } else { 145 | localtime_r(&t, &new_tm); 146 | } 147 | if((self->rotate == ROT_HOURLY && new_tm.tm_hour != self->current_tm.tm_hour) || 148 | (self->rotate == ROT_DAILY && new_tm.tm_mday != self->current_tm.tm_mday)) { 149 | if(self->fh != NULL) { 150 | fclose(self->fh); 151 | self->fh = NULL; 152 | } 153 | return out_file_open(self); 154 | } 155 | return 0; 156 | } 157 | 158 | static void out_file_produce_text(out_file_ctx_t *self, struct metadata *metadata, struct octet_string *msg) { 159 | ASSERT(msg != NULL); 160 | ASSERT(self->fh != NULL); 161 | UNUSED(metadata); 162 | fwrite(msg->buf, sizeof(uint8_t), msg->len, self->fh); 163 | fflush(self->fh); 164 | } 165 | 166 | static int32_t out_file_produce(void *selfptr, output_format_t format, struct metadata *metadata, struct octet_string *msg) { 167 | ASSERT(selfptr != NULL); 168 | out_file_ctx_t *self = selfptr; 169 | if(self->rotate != ROT_NONE && out_file_rotate(self) < 0) { 170 | return -1; 171 | } 172 | if(format == OFMT_TEXT || format == OFMT_JSON || format == OFMT_BASESTATION) { 173 | out_file_produce_text(self, metadata, msg); 174 | } 175 | return 0; 176 | } 177 | 178 | static void out_file_handle_shutdown(void *selfptr) { 179 | ASSERT(selfptr != NULL); 180 | out_file_ctx_t *self = selfptr; 181 | fprintf(stderr, "output_file(%s): shutting down\n", self->filename_prefix); 182 | if(self->fh != NULL) { 183 | fclose(self->fh); 184 | self->fh = NULL; 185 | } 186 | } 187 | 188 | static void out_file_handle_failure(void *selfptr) { 189 | ASSERT(selfptr != NULL); 190 | out_file_ctx_t *self = selfptr; 191 | fprintf(stderr, "output_file: could not write to '%s', deactivating output\n", 192 | self->filename_prefix); 193 | if(self->fh != NULL) { 194 | fclose(self->fh); 195 | self->fh = NULL; 196 | } 197 | } 198 | 199 | static option_descr_t const out_file_options[] = { 200 | { 201 | .name = "path", 202 | .description = "Path to the output file (required)" 203 | }, 204 | { 205 | .name = "rotate", 206 | .description = "How often to start a new file: Accepted values: daily, hourly" 207 | }, 208 | { 209 | .name = NULL, 210 | .description = NULL 211 | } 212 | }; 213 | 214 | output_descriptor_t out_DEF_file = { 215 | .name = "file", 216 | .description = "Output to a file", 217 | .options = out_file_options, 218 | .supports_format = out_file_supports_format, 219 | .configure = out_file_configure, 220 | .ctx_destroy = out_file_ctx_destroy, 221 | .init = out_file_init, 222 | .produce = out_file_produce, 223 | .handle_shutdown = out_file_handle_shutdown, 224 | .handle_failure = out_file_handle_failure 225 | }; 226 | -------------------------------------------------------------------------------- /src/block.c: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include // pthread_* 7 | #include // cbuffercf_* 8 | #include "config.h" 9 | #ifndef HAVE_PTHREAD_BARRIERS 10 | #include "pthread_barrier.h" 11 | #endif 12 | #include "block.h" 13 | #include "util.h" // XCALLOC, pthread_*_initialize, debug_print 14 | 15 | #define BUF_SIZE_PROD_MTU_MULTIPLIER 8 16 | #define BUF_SIZE_CONS_MRU_MULTIPLIER 2 17 | 18 | static int32_t block_circ_buffer_init(struct circ_buffer *buffer, size_t buf_size) { 19 | ASSERT(buffer); 20 | buffer->buf = cbuffercf_create(buf_size); 21 | buffer->cond = XCALLOC(1, sizeof(pthread_cond_t)); 22 | buffer->mutex= XCALLOC(1, sizeof(pthread_mutex_t)); 23 | return pthread_cond_initialize(buffer->cond) || pthread_mutex_initialize(buffer->mutex); 24 | } 25 | 26 | static void block_circ_buffer_destroy(struct circ_buffer *buffer) { 27 | if(buffer != NULL) { 28 | cbuffercf_destroy(buffer->buf); 29 | XFREE(buffer->cond); 30 | XFREE(buffer->mutex); 31 | // No XFREE(buffer) as this is a member of a struct allocated by the caller 32 | } 33 | } 34 | 35 | static int32_t block_shared_buffer_init(struct shared_buffer *buffer, size_t buf_size, size_t thread_cnt) { 36 | ASSERT(buffer); 37 | ASSERT(thread_cnt > 0); 38 | buffer->buf = XCALLOC(buf_size, sizeof(float complex)); 39 | buffer->data_ready = XCALLOC(1, sizeof(pthread_barrier_t)); 40 | buffer->consumers_ready = XCALLOC(1, sizeof(pthread_barrier_t)); 41 | return pthread_barrier_create(buffer->data_ready, thread_cnt) || 42 | pthread_barrier_create(buffer->consumers_ready, thread_cnt); 43 | } 44 | 45 | static void block_shared_buffer_destroy(struct shared_buffer *buffer) { 46 | if(buffer != NULL) { 47 | XFREE(buffer->buf); 48 | XFREE(buffer->data_ready); 49 | XFREE(buffer->consumers_ready); 50 | // No XFREE(buffer) as this is a member of a struct allocated by the caller 51 | } 52 | } 53 | 54 | // Returns the number of successful connections made 55 | int32_t block_connect_one2one(struct block *source, struct block *sink) { 56 | ASSERT(source); 57 | ASSERT(sink); 58 | ASSERT(source->producer.type == PRODUCER_SINGLE); 59 | ASSERT(sink->consumer.type == CONSUMER_SINGLE); 60 | ASSERT(source->producer.max_tu != 0); 61 | 62 | size_t buf_size_by_producer_mtu = BUF_SIZE_PROD_MTU_MULTIPLIER * source->producer.max_tu; 63 | size_t buf_size_by_consumer_mru = BUF_SIZE_CONS_MRU_MULTIPLIER * sink->consumer.min_ru; 64 | size_t buf_size = max(buf_size_by_producer_mtu, buf_size_by_consumer_mru); 65 | debug_print(D_MISC, "producer MTU: %zu consumer MRU: %zu buf_size: %zu\n", 66 | source->producer.max_tu, sink->consumer.min_ru, buf_size); 67 | NEW(struct block_connection, connection); 68 | int32_t ret = 0; 69 | if(block_circ_buffer_init(&connection->circ_buffer, buf_size) != 0) { 70 | goto end; 71 | } 72 | source->producer.out = sink->consumer.in = connection; 73 | ret = 1; 74 | end: 75 | return ret; 76 | } 77 | 78 | void block_disconnect_one2one(struct block *source, struct block *sink) { 79 | ASSERT(source); 80 | ASSERT(sink); 81 | ASSERT(source->producer.type == PRODUCER_SINGLE); 82 | ASSERT(sink->consumer.type == CONSUMER_SINGLE); 83 | if(source->producer.out == sink->consumer.in) { 84 | block_circ_buffer_destroy(&source->producer.out->circ_buffer); 85 | XFREE(source->producer.out); 86 | source->producer.out = sink->consumer.in = NULL; 87 | } 88 | } 89 | 90 | int32_t block_connect_one2many(struct block *source, size_t sink_count, struct block *sinks[sink_count]) { 91 | ASSERT(source); 92 | ASSERT(sinks); 93 | ASSERT(source->producer.type == PRODUCER_MULTI); 94 | ASSERT(source->producer.max_tu != 0); 95 | 96 | size_t max_consumer_mru = 0; 97 | for(size_t i = 0; i < sink_count; i++) { 98 | ASSERT(sinks[i]->consumer.type == CONSUMER_MULTI); 99 | max_consumer_mru = max(max_consumer_mru, sinks[i]->consumer.min_ru); 100 | } 101 | size_t buf_size_by_producer_mtu = BUF_SIZE_PROD_MTU_MULTIPLIER * source->producer.max_tu; 102 | size_t buf_size_by_consumer_mru = BUF_SIZE_CONS_MRU_MULTIPLIER * max_consumer_mru; 103 | size_t buf_size = max(buf_size_by_producer_mtu, buf_size_by_consumer_mru); 104 | debug_print(D_MISC, "producer MTU: %zu max consumer MRU: %zu buf_size: %zu\n", 105 | source->producer.max_tu, max_consumer_mru, buf_size); 106 | 107 | NEW(struct block_connection, connection); 108 | int32_t ret = 0; 109 | // sink_count + 1 to account for the producer when initializing phread_barrier 110 | if(block_shared_buffer_init(&connection->shared_buffer, buf_size, sink_count + 1) != 0) { 111 | goto end; 112 | } 113 | source->producer.out = connection; 114 | for(size_t i = 0; i < sink_count; i++) { 115 | sinks[i]->consumer.in = connection; 116 | ret++; 117 | } 118 | end: 119 | return ret; 120 | } 121 | 122 | void block_disconnect_one2many(struct block *source, size_t sink_count, struct block *sinks[sink_count]) { 123 | ASSERT(source); 124 | ASSERT(sinks); 125 | ASSERT(source->producer.type == PRODUCER_MULTI); 126 | 127 | struct block_connection *connection = source->producer.out; 128 | for(size_t i = 0; i < sink_count; i++) { 129 | if(sinks[i]->consumer.in == connection) { 130 | sinks[i]->consumer.in = NULL; 131 | } 132 | } 133 | block_shared_buffer_destroy(&connection->shared_buffer); 134 | XFREE(connection); 135 | } 136 | 137 | void block_connection_one2one_shutdown(struct block_connection *connection) { 138 | ASSERT(connection); 139 | pthread_mutex_lock(connection->circ_buffer.mutex); 140 | connection->flags |= BLOCK_CONNECTION_SHUTDOWN; 141 | pthread_mutex_unlock(connection->circ_buffer.mutex); 142 | pthread_cond_signal(connection->circ_buffer.cond); 143 | } 144 | 145 | void block_connection_one2many_shutdown(struct block_connection *connection) { 146 | ASSERT(connection); 147 | connection->flags |= BLOCK_CONNECTION_SHUTDOWN; 148 | pthread_barrier_wait(connection->shared_buffer.data_ready); 149 | } 150 | 151 | bool block_connection_is_shutdown_signaled(struct block_connection *connection) { 152 | ASSERT(connection); 153 | return connection->flags & BLOCK_CONNECTION_SHUTDOWN; 154 | } 155 | 156 | // Returns number of blocks successfully started 157 | int32_t block_start(struct block *block) { 158 | ASSERT(block); 159 | ASSERT(block->thread_routine); 160 | int32_t ret = start_thread(&block->thread, block->thread_routine, block); 161 | if(ret == 0) { 162 | block->running = true; 163 | return 1; 164 | } 165 | return 0; 166 | } 167 | 168 | // Returns number of blocks successfully started 169 | int32_t block_set_start(size_t block_cnt, struct block *block[block_cnt]) { 170 | ASSERT(block); 171 | int32_t ret = 0; 172 | for(size_t i = 0; i < block_cnt; i++) { 173 | ASSERT(block[i]->thread_routine); 174 | ret += block_start(block[i]); 175 | } 176 | return ret; 177 | } 178 | 179 | bool block_is_running(struct block *block) { 180 | ASSERT(block); 181 | return block->running; 182 | } 183 | 184 | bool block_set_is_any_running(size_t block_cnt, struct block *blocks[block_cnt]) { 185 | ASSERT(blocks); 186 | for(size_t i = 0; i < block_cnt; i++) { 187 | if(block_is_running(blocks[i])) { 188 | return true; 189 | } 190 | } 191 | return false; 192 | } 193 | 194 | -------------------------------------------------------------------------------- /src/pdu.c: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #include 3 | #include 4 | #include // memcpy 5 | #include // GAsyncQueue, g_async_queue_* 6 | #include // la_proto_tree_destroy() 7 | #include // la_list_* 8 | #include // la_reasm_ctx, la_reasm_ctx_new() 9 | #include "util.h" // NEW, ASSERT, struct octet_string 10 | #include "output-common.h" // output_queue_push, shutdown_outputs 11 | #include "crc.h" // crc16_ccitt 12 | #include "mpdu.h" // mpdu_parse 13 | #include "spdu.h" // spdu_parse 14 | #include "statsd.h" // statsd_* 15 | #include "pdu.h" // struct hfdl_pdu_metadata 16 | 17 | struct hfdl_pdu_qentry { 18 | struct metadata *metadata; 19 | struct octet_string *pdu; 20 | uint32_t flags; 21 | }; 22 | 23 | static GAsyncQueue *pdu_decoder_queue; 24 | static bool pdu_decoder_thread_active = false; 25 | 26 | /****************************** 27 | * Forward declarations 28 | ******************************/ 29 | 30 | static void *pdu_decoder_thread(void *ctx); 31 | static struct metadata_vtable hfdl_pdu_metadata_vtable; 32 | 33 | /****************************** 34 | * Public methods 35 | ******************************/ 36 | 37 | void pdu_decoder_queue_push(struct metadata *metadata, struct octet_string *pdu, uint32_t flags) { 38 | NEW(struct hfdl_pdu_qentry, qentry); 39 | qentry->metadata = metadata; 40 | qentry->pdu = pdu; 41 | qentry->flags = flags; 42 | g_async_queue_push(pdu_decoder_queue, qentry); 43 | } 44 | 45 | void hfdl_pdu_decoder_init(void) { 46 | pdu_decoder_queue = g_async_queue_new(); 47 | } 48 | 49 | int32_t hfdl_pdu_decoder_start(void *ctx) { 50 | pthread_t pdu_th; 51 | int32_t ret = start_thread(&pdu_th, pdu_decoder_thread, ctx); 52 | if(ret == 0) { 53 | pdu_decoder_thread_active = true; 54 | } 55 | return ret; 56 | } 57 | 58 | void hfdl_pdu_decoder_stop(void) { 59 | pdu_decoder_queue_push(NULL, NULL, OUT_FLAG_ORDERED_SHUTDOWN); 60 | } 61 | 62 | bool hfdl_pdu_decoder_is_running(void) { 63 | return pdu_decoder_thread_active; 64 | } 65 | 66 | // Compute FCS over the first hdr_len octets of buf. 67 | // The received FCS value is expected to be stored just after the buffer. 68 | bool hfdl_pdu_fcs_check(uint8_t *buf, uint32_t hdr_len) { 69 | uint16_t fcs_check = buf[hdr_len] | (buf[hdr_len + 1] << 8); 70 | uint16_t fcs_computed = crc16_ccitt(buf, hdr_len, 0xFFFFu) ^ 0xFFFFu; 71 | debug_print(D_PROTO, "FCS: computed: 0x%04x check: 0x%04x\n", 72 | fcs_computed, fcs_check); 73 | if(fcs_check != fcs_computed) { 74 | debug_print(D_PROTO, "FCS check failed\n"); 75 | return false; 76 | } 77 | debug_print(D_PROTO, "FCS check OK\n"); 78 | return true; 79 | } 80 | 81 | struct metadata *hfdl_pdu_metadata_create() { 82 | NEW(struct hfdl_pdu_metadata, m); 83 | m->metadata.vtable = &hfdl_pdu_metadata_vtable; 84 | return &m->metadata; 85 | } 86 | 87 | /**************************************** 88 | * Private variables and methods 89 | ****************************************/ 90 | 91 | static void *pdu_decoder_thread(void *ctx) { 92 | ASSERT(ctx != NULL); 93 | la_list *fmtr_list = ctx; 94 | struct hfdl_pdu_qentry *q = NULL; 95 | la_list *lpdu_list = NULL; 96 | la_reasm_ctx *reasm_ctx = la_reasm_ctx_new(); 97 | enum { 98 | DECODING_NOT_DONE, 99 | DECODING_SUCCESS, 100 | DECODING_FAILURE 101 | } decoding_status; 102 | #define IS_MPDU(buf) ((buf)[0] & 1) 103 | 104 | while(true) { 105 | q = g_async_queue_pop(pdu_decoder_queue); 106 | if(q->flags & OUT_FLAG_ORDERED_SHUTDOWN) { 107 | fprintf(stderr, "Shutting down decoder thread\n"); 108 | shutdown_outputs(fmtr_list); 109 | XFREE(q); 110 | break; 111 | } 112 | ASSERT(q->metadata != NULL); 113 | 114 | fmtr_instance_t *fmtr = NULL; 115 | decoding_status = DECODING_NOT_DONE; 116 | for(la_list *p = fmtr_list; p != NULL; p = la_list_next(p)) { 117 | fmtr = p->data; 118 | if(fmtr->intype == FMTR_INTYPE_DECODED_FRAME) { 119 | // Decode the pdu unless we've done it before 120 | if(decoding_status == DECODING_NOT_DONE) { 121 | struct hfdl_pdu_metadata *hm = container_of(q->metadata, 122 | struct hfdl_pdu_metadata, metadata); 123 | statsd_increment_per_channel(hm->freq, "frames.processed"); 124 | if(IS_MPDU(q->pdu->buf)) { 125 | lpdu_list = mpdu_parse(q->pdu, reasm_ctx, q->metadata->rx_timestamp, hm->freq); 126 | } else { 127 | lpdu_list = spdu_parse(q->pdu, hm->freq); 128 | } 129 | if(lpdu_list != NULL) { 130 | decoding_status = DECODING_SUCCESS; 131 | } else { 132 | decoding_status = DECODING_FAILURE; 133 | } 134 | } 135 | if(decoding_status == DECODING_SUCCESS) { 136 | for(la_list *lpdu = lpdu_list; lpdu != NULL; lpdu = la_list_next(lpdu)) { 137 | ASSERT(lpdu->data != NULL); 138 | struct octet_string *serialized_msg = fmtr->td->format_decoded_msg(q->metadata, lpdu->data); 139 | // First check if the formatter actually returned something. 140 | // A formatter might be suitable only for a particular message type. If this is the case. 141 | // it will return NULL for all messages it cannot handle. 142 | // An example is pp_acars which only deals with ACARS messages. 143 | if(serialized_msg != NULL) { 144 | output_qentry_t qentry = { 145 | .msg = serialized_msg, 146 | .metadata = q->metadata, 147 | .format = fmtr->td->output_format 148 | }; 149 | la_list_foreach(fmtr->outputs, output_queue_push, &qentry); 150 | // output_queue_push makes a copy of serialized_msg, so it's safe to free it now 151 | octet_string_destroy(serialized_msg); 152 | } 153 | } 154 | } 155 | } else if(fmtr->intype == FMTR_INTYPE_RAW_FRAME) { 156 | struct octet_string *serialized_msg = fmtr->td->format_raw_msg(q->metadata, q->pdu); 157 | if(serialized_msg != NULL) { 158 | output_qentry_t qentry = { 159 | .msg = serialized_msg, 160 | .metadata = q->metadata, 161 | .format = fmtr->td->output_format 162 | }; 163 | la_list_foreach(fmtr->outputs, output_queue_push, &qentry); 164 | // output_queue_push makes a copy of serialized_msg, so it's safe to free it now 165 | octet_string_destroy(serialized_msg); 166 | } 167 | } 168 | } 169 | la_list_free_full(lpdu_list, la_proto_tree_destroy); 170 | lpdu_list = NULL; 171 | octet_string_destroy(q->pdu); 172 | XFREE(q->metadata); 173 | XFREE(q); 174 | } 175 | la_reasm_ctx_destroy(reasm_ctx); 176 | pdu_decoder_thread_active = false; 177 | return NULL; 178 | } 179 | 180 | static struct metadata *hfdl_pdu_metadata_copy(struct metadata const *m) { 181 | ASSERT(m != NULL); 182 | struct hfdl_pdu_metadata *hm = container_of(m, struct hfdl_pdu_metadata, metadata); 183 | NEW(struct hfdl_pdu_metadata, copy); 184 | memcpy(copy, hm, sizeof(struct hfdl_pdu_metadata)); 185 | return ©->metadata; 186 | } 187 | 188 | static void hfdl_pdu_metadata_destroy(struct metadata *m) { 189 | if(m == NULL) { 190 | return; 191 | } 192 | struct hfdl_pdu_metadata *hm = container_of(m, struct hfdl_pdu_metadata, metadata); 193 | XFREE(hm); 194 | } 195 | 196 | static struct metadata_vtable hfdl_pdu_metadata_vtable = { 197 | .copy = hfdl_pdu_metadata_copy, 198 | .destroy = hfdl_pdu_metadata_destroy 199 | }; 200 | -------------------------------------------------------------------------------- /src/output-tcp.c: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #include // fprintf 3 | #include // strdup, strerror 4 | #include // close 5 | #include // errno 6 | #include // struct timeval 7 | #include // time_t, time 8 | #include // socket, connect 9 | #include // socket, connect 10 | #include // getaddrinfo 11 | #include "output-common.h" // output_descriptor_t, output_qentry_t, output_queue_drain 12 | #include "kvargs.h" // kvargs, option_descr_t 13 | #include "util.h" // ASSERT 14 | 15 | // Number of seconds to wait before re-establishing the connection 16 | // after the last error. 17 | #define MIN_RECONNECT_INTERVAL 10 18 | // Send socket operations timeout 19 | #define SOCKET_SEND_TIMEOUT 5 20 | 21 | // Forward declarations 22 | static void out_tcp_handle_shutdown(void *selfptr); 23 | 24 | typedef struct { 25 | char *address; 26 | char *port; 27 | time_t next_reconnect_time; 28 | int32_t sockfd; 29 | } out_tcp_ctx_t; 30 | 31 | static bool out_tcp_supports_format(output_format_t format) { 32 | return(format == OFMT_TEXT || format == OFMT_BASESTATION || format == OFMT_JSON); 33 | } 34 | 35 | static void *out_tcp_configure(kvargs *kv) { 36 | ASSERT(kv != NULL); 37 | NEW(out_tcp_ctx_t, cfg); 38 | if(kvargs_get(kv, "address") == NULL) { 39 | fprintf(stderr, "output_tcp: address not specified\n"); 40 | goto fail; 41 | } 42 | cfg->address = strdup(kvargs_get(kv, "address")); 43 | if(kvargs_get(kv, "port") == NULL) { 44 | fprintf(stderr, "output_tcp: port not specified\n"); 45 | goto fail; 46 | } 47 | cfg->port = strdup(kvargs_get(kv, "port")); 48 | return cfg; 49 | fail: 50 | XFREE(cfg); 51 | return NULL; 52 | } 53 | 54 | static void out_tcp_ctx_destroy(void *ctxptr) { 55 | if(ctxptr != NULL) { 56 | out_tcp_ctx_t *ctx = ctxptr; 57 | XFREE(ctx->address); 58 | XFREE(ctx->port); 59 | XFREE(ctx); 60 | } 61 | } 62 | 63 | static int32_t out_tcp_reconnect(void *selfptr) { 64 | ASSERT(selfptr != NULL); 65 | out_tcp_ctx_t *self = selfptr; 66 | 67 | if(self->next_reconnect_time > time(NULL)) { 68 | return -1; 69 | } 70 | static struct timeval const timeout = { .tv_sec = SOCKET_SEND_TIMEOUT, .tv_usec = 0 }; 71 | struct addrinfo hints, *result, *rptr; 72 | memset(&hints, 0, sizeof(struct addrinfo)); 73 | hints.ai_family = AF_UNSPEC; 74 | hints.ai_socktype = SOCK_STREAM; 75 | hints.ai_flags = 0; 76 | hints.ai_protocol = 0; 77 | fprintf(stderr, "output_tcp(%s:%s): connecting...\n", self->address, self->port); 78 | int32_t ret = getaddrinfo(self->address, self->port, &hints, &result); 79 | if(ret != 0) { 80 | fprintf(stderr, "output_tcp(%s:%s): could not resolve address: %s\n", 81 | self->address, self->port, gai_strerror(ret)); 82 | goto fail; 83 | } 84 | for (rptr = result; rptr != NULL; rptr = rptr->ai_next) { 85 | self->sockfd = socket(rptr->ai_family, rptr->ai_socktype, rptr->ai_protocol); 86 | if(self->sockfd == -1) { 87 | continue; 88 | } 89 | if(setsockopt(self->sockfd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout)) < 0) { 90 | fprintf(stderr, "output_tcp(%s:%s): could not set timeout on socket: %s\n", 91 | self->address, self->port, strerror(errno)); 92 | } 93 | 94 | if(connect(self->sockfd, rptr->ai_addr, rptr->ai_addrlen) != -1) { 95 | fprintf(stderr, "output_tcp(%s:%s): connection established\n", 96 | self->address, self->port); 97 | break; 98 | } 99 | close(self->sockfd); 100 | self->sockfd = 0; 101 | } 102 | if (rptr == NULL) { 103 | fprintf(stderr, "output_tcp(%s:%s): could not connect: all addresses failed\n", 104 | self->address, self->port); 105 | self->sockfd = 0; 106 | goto fail; 107 | } 108 | freeaddrinfo(result); 109 | self->next_reconnect_time = 0; 110 | return 0; 111 | fail: 112 | freeaddrinfo(result); 113 | self->next_reconnect_time = time(NULL) + MIN_RECONNECT_INTERVAL; 114 | return -1; 115 | } 116 | 117 | static int32_t out_tcp_init(void *selfptr) { 118 | ASSERT(selfptr != NULL); 119 | out_tcp_ctx_t *self = selfptr; 120 | self->next_reconnect_time = 0; // Force reconnection now 121 | out_tcp_reconnect(selfptr); 122 | // Always return success, even when connection failed - otherwise the output thread would 123 | // declare the error as fatal and disable the output without giving us a change to reestablish 124 | // the connection. 125 | return 0; 126 | } 127 | 128 | static int32_t out_tcp_produce_text(out_tcp_ctx_t *self, struct metadata *metadata, struct octet_string *msg) { 129 | UNUSED(metadata); 130 | ASSERT(msg != NULL); 131 | ASSERT(self->sockfd != 0); 132 | if(msg->len < 1) { 133 | return 0; 134 | } 135 | if(write(self->sockfd, msg->buf, msg->len) < 0) { 136 | return -1; 137 | } 138 | return 0; 139 | } 140 | 141 | static int32_t out_tcp_produce(void *selfptr, output_format_t format, struct metadata *metadata, struct octet_string *msg) { 142 | ASSERT(selfptr != NULL); 143 | out_tcp_ctx_t *self = selfptr; 144 | int32_t result = 0; 145 | if(self->sockfd == 0) { // No connection? 146 | if(out_tcp_reconnect(selfptr) < 0) { 147 | // Return success - this causes the message to be dropped silently. 148 | // Can't requeue it here, because if the problem is permanent, then 149 | // it would prevent the program from shutting down cleanly (shutdown 150 | // message in the output queue would never be processed). 151 | return 0; 152 | } 153 | } 154 | if(format == OFMT_TEXT || format == OFMT_JSON || format == OFMT_BASESTATION) { 155 | result = out_tcp_produce_text(self, metadata, msg); 156 | } 157 | if(result < 0) { 158 | // Possible connection failure. Close it and reschedule an immediate reconnection. 159 | fprintf(stderr, "output_tcp(%s:%s): send error: %s\n", self->address, 160 | self->port, strerror(errno)); 161 | out_tcp_handle_shutdown(selfptr); 162 | self->next_reconnect_time = 0; 163 | // Declare a failure, so that the message gets requeued 164 | return -1; 165 | } 166 | return 0; 167 | } 168 | 169 | static void out_tcp_handle_shutdown(void *selfptr) { 170 | ASSERT(selfptr != NULL); 171 | out_tcp_ctx_t *self = selfptr; 172 | close(self->sockfd); 173 | self->sockfd = 0; 174 | fprintf(stderr, "output_tcp(%s:%s): connection closed\n", self->address, self->port); 175 | } 176 | 177 | static void out_tcp_handle_failure(void *selfptr) { 178 | ASSERT(selfptr != NULL); 179 | out_tcp_ctx_t *self = selfptr; 180 | fprintf(stderr, "output_tcp(%s:%s): could not connect, deactivating output\n", 181 | self->address, self->port); 182 | close(self->sockfd); 183 | self->sockfd = 0; 184 | } 185 | 186 | static const option_descr_t out_tcp_options[] = { 187 | { 188 | .name = "address", 189 | .description = "Destination host name or IP address (required)" 190 | }, 191 | { 192 | .name = "port", 193 | .description = "Destination TCP port (required)" 194 | }, 195 | { 196 | .name = NULL, 197 | .description = NULL 198 | } 199 | }; 200 | 201 | output_descriptor_t out_DEF_tcp = { 202 | .name = "tcp", 203 | .description = "Output to a remote host via TCP", 204 | .options = out_tcp_options, 205 | .supports_format = out_tcp_supports_format, 206 | .configure = out_tcp_configure, 207 | .ctx_destroy = out_tcp_ctx_destroy, 208 | .init = out_tcp_init, 209 | .produce = out_tcp_produce, 210 | .handle_shutdown = out_tcp_handle_shutdown, 211 | .handle_failure = out_tcp_handle_failure 212 | }; 213 | -------------------------------------------------------------------------------- /src/spdu.c: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-2.0-or-later */ 2 | #include 3 | #include // la_proto_node 4 | #include // la_list 5 | #include // la_json_append_* 6 | #include "pdu.h" // struct hfdl_pdu_hdr_data, hfdl_pdu_fcs_check 7 | #include "spdu.h" 8 | #include "statsd.h" // statsd_* 9 | #include "util.h" // NEW, ASSERT, struct octet_string, freq_list_format_text, gs_id_format_text 10 | #include "crc.h" // crc16_ccitt 11 | 12 | #define SPDU_LEN 66 13 | #define GS_STATUS_CNT 3 14 | 15 | struct gs_status { 16 | uint32_t freqs_in_use; 17 | uint8_t id; 18 | bool utc_sync; 19 | }; 20 | 21 | struct hfdl_spdu { 22 | struct octet_string *pdu; 23 | struct hfdl_pdu_hdr_data header; 24 | struct gs_status gs_data[GS_STATUS_CNT]; 25 | uint32_t frame_index; 26 | uint8_t frame_offset; 27 | uint8_t version; 28 | uint8_t change_note; 29 | uint8_t min_priority; 30 | uint8_t systable_version; 31 | bool rls_in_use; 32 | bool iso8208_supported; 33 | }; 34 | 35 | // Forward declarations 36 | la_type_descriptor const proto_DEF_hfdl_spdu; 37 | static void gs_status_format_text(la_vstring *vstr, int32_t indent, struct gs_status const *gs); 38 | static void gs_status_format_json(la_vstring *vstr, struct gs_status const *gs); 39 | 40 | la_list *spdu_parse(struct octet_string *pdu, int32_t freq) { 41 | #ifndef WITH_STATSD 42 | UNUSED(freq); 43 | #endif 44 | ASSERT(pdu); 45 | ASSERT(pdu->buf); 46 | ASSERT(pdu->len > 0); 47 | 48 | la_list *spdu_list = NULL; 49 | NEW(struct hfdl_spdu, spdu); 50 | spdu->pdu = pdu; 51 | la_proto_node *spdu_node = la_proto_node_new(); 52 | spdu_node->data = spdu; 53 | spdu_node->td = &proto_DEF_hfdl_spdu; 54 | 55 | if(pdu->len < SPDU_LEN) { 56 | statsd_increment_per_channel(freq, "frame.errors.too_short"); 57 | debug_print(D_PROTO, "Too short: %zu < %u\n", pdu->len, SPDU_LEN); 58 | goto end; 59 | } 60 | 61 | 62 | if(hfdl_pdu_fcs_check(pdu->buf, 64u)) { 63 | spdu->header.crc_ok = true; 64 | statsd_increment_per_channel(freq, "frames.good"); 65 | } else { 66 | statsd_increment_per_channel(freq, "frame.errors.bad_fcs"); 67 | goto end; 68 | } 69 | uint8_t *buf = pdu->buf; 70 | statsd_increment_per_channel(freq, "frame.dir.gnd2air"); 71 | spdu->header.direction = UPLINK_PDU; 72 | spdu->header.src_id = buf[1] & 0x7F; 73 | 74 | spdu->rls_in_use = buf[0] & 2; 75 | spdu->version = (buf[0] >> 2) & 3; 76 | spdu->iso8208_supported = buf[0] & 0x20; 77 | spdu->change_note = (buf[0] & 0xC0) >> 6; 78 | 79 | spdu->frame_index = buf[2] | ((buf[3] & 0xF) << 8); 80 | spdu->frame_offset = buf[3] >> 4; 81 | 82 | spdu->min_priority = buf[52] & 0xF; 83 | spdu->systable_version = buf[53] | ((buf[54] & 0xF) << 8); 84 | 85 | spdu->gs_data[0].id = spdu->header.src_id; 86 | spdu->gs_data[0].utc_sync = buf[1] & 0x80; 87 | spdu->gs_data[0].freqs_in_use = buf[54] >> 4 | buf[55] << 4 | buf[56] << 12; 88 | debug_print(D_PROTO, "gs_data: id %hhu utc %d freqs_in_use 0x%05x\n", 89 | spdu->gs_data[0].id, spdu->gs_data[0].utc_sync, spdu->gs_data[0].freqs_in_use); 90 | 91 | spdu->gs_data[1].id = buf[57] & 0x7F; 92 | spdu->gs_data[1].utc_sync = buf[57] & 0x80; 93 | spdu->gs_data[1].freqs_in_use = buf[58] | buf[59] << 8 | (buf[60] & 0xF) << 16; 94 | debug_print(D_PROTO, "gs_data: id %hhu utc %d freqs_in_use 0x%05x\n", 95 | spdu->gs_data[1].id, spdu->gs_data[1].utc_sync, spdu->gs_data[1].freqs_in_use); 96 | 97 | spdu->gs_data[2].id = buf[60] >> 4 | (buf[61] & 0x7) << 4; 98 | spdu->gs_data[2].utc_sync = buf[61] & 0x8; 99 | spdu->gs_data[2].freqs_in_use = buf[61] >> 4 | buf[62] << 4 | buf[63] << 12; 100 | debug_print(D_PROTO, "gs_data: id %hhu utc %d freqs_in_use 0x%05x\n", 101 | spdu->gs_data[2].id, spdu->gs_data[2].utc_sync, spdu->gs_data[2].freqs_in_use); 102 | 103 | end: 104 | if(spdu->header.crc_ok || Config.output_corrupted_pdus) { 105 | spdu_list = la_list_append(spdu_list, spdu_node); 106 | } else { 107 | la_proto_tree_destroy(spdu_node); 108 | } 109 | return spdu_list; 110 | } 111 | 112 | static char const *change_note_descr[] = { 113 | [0] = "None", 114 | [1] = "Channel down", 115 | [2] = "Upcoming frequency change", 116 | [3] = "Ground station down" 117 | }; 118 | 119 | static void spdu_format_text(la_vstring *vstr, void const *data, int32_t indent) { 120 | ASSERT(vstr != NULL); 121 | ASSERT(data); 122 | ASSERT(indent >= 0); 123 | 124 | struct hfdl_spdu const *spdu = data; 125 | if(Config.output_raw_frames == true && spdu->pdu->len > 0) { 126 | append_hexdump_with_indent(vstr, spdu->pdu->buf, spdu->pdu->len, indent+1); 127 | } 128 | if(!spdu->header.crc_ok) { 129 | // We say "PDU" rather than "SPDU", because if the CRC check failed the we are 130 | // not even sure whether it's a MPDU or SPDU... 131 | LA_ISPRINTF(vstr, indent, "-- Unparseable PDU (CRC check failed)\n"); 132 | return; 133 | } 134 | 135 | LA_ISPRINTF(vstr, indent, "Uplink SPDU:\n"); 136 | indent++; 137 | gs_id_format_text(vstr, indent, "Src GS", spdu->header.src_id); 138 | LA_ISPRINTF(vstr, indent, "Squitter: ver: %hhu rls: %d iso: %d\n", 139 | spdu->version, spdu->rls_in_use, spdu->iso8208_supported); 140 | indent++; 141 | LA_ISPRINTF(vstr, indent, "Change note: %s\n", change_note_descr[spdu->change_note]); 142 | LA_ISPRINTF(vstr, indent, "TDMA Frame: index: %u offset: %u\n", spdu->frame_index, spdu->frame_offset); 143 | LA_ISPRINTF(vstr, indent, "Minimum priority: %u\n", spdu->min_priority); 144 | LA_ISPRINTF(vstr, indent, "System table version: %u\n", spdu->systable_version); 145 | LA_ISPRINTF(vstr, indent, "Ground station status:\n"); 146 | for(int32_t i = 0; i < GS_STATUS_CNT; i++) { 147 | gs_status_format_text(vstr, indent, &spdu->gs_data[i]); 148 | } 149 | indent--; 150 | } 151 | 152 | static void spdu_format_json(la_vstring *vstr, void const *data) { 153 | ASSERT(vstr != NULL); 154 | ASSERT(data); 155 | 156 | struct hfdl_spdu const *spdu = data; 157 | la_json_append_bool(vstr, "err", !spdu->header.crc_ok); 158 | if(!spdu->header.crc_ok) { 159 | return; 160 | } 161 | gs_id_format_json(vstr, "src", spdu->header.src_id); 162 | la_json_append_int64(vstr, "spdu_version", spdu->version); 163 | la_json_append_bool(vstr, "rls", spdu->rls_in_use); 164 | la_json_append_bool(vstr, "iso", spdu->iso8208_supported); 165 | la_json_append_string(vstr, "change_note", change_note_descr[spdu->change_note]); 166 | la_json_append_int64(vstr, "frame_index", spdu->frame_index); 167 | la_json_append_int64(vstr, "frame_offset", spdu->frame_offset); 168 | la_json_append_int64(vstr, "min_priority", spdu->min_priority); 169 | la_json_append_int64(vstr, "systable_version", spdu->systable_version); 170 | la_json_array_start(vstr, "gs_status"); 171 | for(int32_t i = 0; i < GS_STATUS_CNT; i++) { 172 | gs_status_format_json(vstr, &spdu->gs_data[i]); 173 | } 174 | la_json_array_end(vstr); 175 | } 176 | 177 | static void gs_status_format_text(la_vstring *vstr, int32_t indent, struct gs_status const *gs) { 178 | ASSERT(vstr); 179 | ASSERT(indent >= 0); 180 | ASSERT(gs); 181 | 182 | gs_id_format_text(vstr, indent, "ID", gs->id); 183 | indent++; 184 | LA_ISPRINTF(vstr, indent, "UTC sync: %d\n", gs->utc_sync); 185 | freq_list_format_text(vstr, indent, "Frequencies in use", gs->id, gs->freqs_in_use); 186 | } 187 | 188 | static void gs_status_format_json(la_vstring *vstr, struct gs_status const *gs) { 189 | ASSERT(vstr); 190 | ASSERT(gs); 191 | la_json_object_start(vstr, NULL); 192 | gs_id_format_json(vstr, "gs", gs->id); 193 | la_json_append_bool(vstr, "utc_sync", gs->utc_sync); 194 | freq_list_format_json(vstr, "freqs", gs->id, gs->freqs_in_use); 195 | la_json_object_end(vstr); 196 | } 197 | 198 | la_type_descriptor const proto_DEF_hfdl_spdu = { 199 | .format_text = spdu_format_text, 200 | .format_json = spdu_format_json, 201 | .json_key = "spdu", 202 | .destroy = NULL 203 | }; 204 | -------------------------------------------------------------------------------- /src/ac_data.c: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | 3 | #include 4 | #include 5 | #include "config.h" // WITH_SQLITE 6 | #include "ac_data.h" // struct ac_data_entry 7 | #include "util.h" // UNUSED 8 | 9 | #ifdef WITH_SQLITE 10 | 11 | #include 12 | #include // strdup 13 | #include // time_t, time() 14 | #include 15 | #include "cache.h" // cache_* 16 | #include "statsd.h" // statsd_* 17 | 18 | struct ac_data { 19 | cache *cache; 20 | sqlite3 *db; 21 | sqlite3_stmt *stmt; 22 | }; 23 | 24 | #define AC_DATA_TTL 3600L 25 | #define AC_DATA_EXPIRATION_INTERVAL 305L 26 | #define BS_DB_COLUMNS "Registration,ICAOTypeCode,OperatorFlagCode,Manufacturer,Type,RegisteredOwners" 27 | 28 | /****************************** 29 | * Forward declarations 30 | ******************************/ 31 | 32 | static struct cache_vtable ac_data_vtable; 33 | static struct ac_data_entry *ac_data_entry_fetch_from_db(struct ac_data *ac_data, uint32_t icao_address); 34 | static void ac_data_entry_create(ac_data *ac_data, uint32_t icao_address, struct ac_data_entry *entry); 35 | 36 | // XXX: statsd counters are global across all ac_data objects 37 | // For dumphfdl this is not an issue , since there is only one such object in every program run. 38 | #ifdef WITH_STATSD 39 | static char *ac_data_counters[] = { 40 | "ac_data.db.hits", 41 | "ac_data.db.misses", 42 | "ac_data.db.errors", 43 | NULL 44 | }; 45 | static bool statsd_counters_initialized = false; 46 | #endif 47 | 48 | struct ac_data *ac_data_create(char const *bs_db_file) { 49 | if(bs_db_file == NULL) { 50 | return NULL; 51 | } 52 | NEW(struct ac_data, ac_data); 53 | ac_data->db = NULL; 54 | 55 | int32_t rc = sqlite3_open_v2(bs_db_file, &ac_data->db, SQLITE_OPEN_READONLY, NULL); 56 | if(rc != 0){ 57 | fprintf(stderr, "Can't open database %s: %s\n", bs_db_file, sqlite3_errmsg(ac_data->db)); 58 | goto fail; 59 | } 60 | rc = sqlite3_prepare_v2(ac_data->db, "SELECT " BS_DB_COLUMNS " FROM Aircraft WHERE ModeS = ?", -1, &ac_data->stmt, NULL); 61 | if(rc != SQLITE_OK) { 62 | fprintf(stderr, "%s: could not query Aircraft table: %s\n", bs_db_file, sqlite3_errmsg(ac_data->db)); 63 | goto fail; 64 | } 65 | ac_data->cache = cache_create("ac_data", &ac_data_vtable, AC_DATA_TTL, AC_DATA_EXPIRATION_INTERVAL); 66 | ASSERT(ac_data->cache != NULL); 67 | // XXX: statsd counters are global across all ac_data objects 68 | #ifdef WITH_STATSD 69 | if(statsd_counters_initialized == false) { 70 | statsd_initialize_counter_set(ac_data_counters); 71 | statsd_counters_initialized = true; 72 | } 73 | #endif 74 | if(ac_data_entry_lookup(ac_data, 0) == NULL) { 75 | fprintf(stderr, "%s: test query failed, database is unusable.\n", bs_db_file); 76 | goto fail; 77 | } 78 | fprintf(stderr, "%s: database opened\n", bs_db_file); 79 | return ac_data; 80 | fail: 81 | ac_data_destroy(ac_data); 82 | return NULL; 83 | } 84 | 85 | struct ac_data_entry *ac_data_entry_lookup(ac_data *ac_data, uint32_t icao_address) { 86 | ASSERT(ac_data); 87 | 88 | // Periodic cache expiration 89 | cache_expire(ac_data->cache, time(NULL)); 90 | 91 | struct ac_data_entry *entry = cache_entry_lookup(ac_data->cache, &icao_address); 92 | if(entry != NULL) { 93 | debug_print(D_CACHE, "%06X: %s cache hit\n", icao_address, entry->exists ? "positive" : "negative"); 94 | } else { 95 | entry = ac_data_entry_fetch_from_db(ac_data, icao_address); 96 | if(entry != NULL) { 97 | debug_print(D_CACHE, "%06X: %sfound in BS DB\n", icao_address, entry->exists ? "" : "not "); 98 | ac_data_entry_create(ac_data, icao_address, entry); 99 | } else { 100 | debug_print(D_CACHE, "%06X: BS DB query failure\n", icao_address); 101 | } 102 | } 103 | return entry; 104 | } 105 | 106 | void ac_data_destroy(struct ac_data *ac_data) { 107 | if(ac_data) { 108 | sqlite3_finalize(ac_data->stmt); 109 | sqlite3_close(ac_data->db); 110 | cache_destroy(ac_data->cache); 111 | XFREE(ac_data); 112 | } 113 | } 114 | 115 | static uint32_t uint32_t_hash(void const *key) { 116 | return *(uint32_t *)key; 117 | } 118 | 119 | static bool uint32_t_compare(void const *key1, void const *key2) { 120 | return *(uint32_t *)key1 == *(uint32_t *)key2; 121 | } 122 | 123 | static void ac_data_entry_destroy(void *data) { 124 | if(data != NULL) { 125 | struct ac_data_entry *e = data; 126 | XFREE(e->registration); 127 | XFREE(e->icaotypecode); 128 | XFREE(e->operatorflagcode); 129 | XFREE(e->manufacturer); 130 | XFREE(e->type); 131 | XFREE(e->registeredowners); 132 | XFREE(e); 133 | } 134 | } 135 | 136 | static struct cache_vtable ac_data_vtable = { 137 | .cache_key_hash = uint32_t_hash, 138 | .cache_key_compare = uint32_t_compare, 139 | .cache_key_destroy = la_simple_free, 140 | .cache_entry_data_destroy = ac_data_entry_destroy 141 | }; 142 | 143 | static struct ac_data_entry *ac_data_entry_fetch_from_db(struct ac_data *ac_data, uint32_t icao_address) { 144 | ASSERT(ac_data); 145 | 146 | NEW(struct ac_data_entry, entry); 147 | 148 | char hex_address[7]; 149 | if(snprintf(hex_address, sizeof(hex_address), "%06X", icao_address) != sizeof(hex_address) - 1) { 150 | debug_print(D_CACHE, "could not convert icao_address %u to ICAO hex string - too large?\n", icao_address); 151 | goto fail; 152 | } 153 | 154 | int32_t rc = sqlite3_reset(ac_data->stmt); 155 | if(rc != SQLITE_OK) { 156 | debug_print(D_CACHE, "sqlite3_reset() returned error %d\n", rc); 157 | statsd_increment("ac_data.db.errors"); 158 | goto fail; 159 | } 160 | rc = sqlite3_bind_text(ac_data->stmt, 1, hex_address, -1, SQLITE_STATIC); 161 | if(rc != SQLITE_OK) { 162 | debug_print(D_CACHE, "sqlite3_bind_text('%s') returned error %d\n", hex_address, rc); 163 | statsd_increment("ac_data.db.errors"); 164 | goto fail; 165 | } 166 | rc = sqlite3_step(ac_data->stmt); 167 | if(rc == SQLITE_ROW) { 168 | if(sqlite3_column_count(ac_data->stmt) < 6) { 169 | debug_print(D_CACHE, "%s: not enough columns in the query result\n", hex_address); 170 | goto fail; 171 | } 172 | statsd_increment("ac_data.db.hits"); 173 | char const *field = NULL; 174 | if((field = (char *)sqlite3_column_text(ac_data->stmt, 0)) != NULL) entry->registration = strdup(field); 175 | if((field = (char *)sqlite3_column_text(ac_data->stmt, 1)) != NULL) entry->icaotypecode = strdup(field); 176 | if((field = (char *)sqlite3_column_text(ac_data->stmt, 2)) != NULL) entry->operatorflagcode = strdup(field); 177 | if((field = (char *)sqlite3_column_text(ac_data->stmt, 3)) != NULL) entry->manufacturer = strdup(field); 178 | if((field = (char *)sqlite3_column_text(ac_data->stmt, 4)) != NULL) entry->type = strdup(field); 179 | if((field = (char *)sqlite3_column_text(ac_data->stmt, 5)) != NULL) entry->registeredowners = strdup(field); 180 | entry->exists = true; 181 | } else if(rc == SQLITE_DONE) { 182 | // No rows found 183 | entry->exists = false; 184 | statsd_increment("ac_data.db.misses"); 185 | } else { 186 | debug_print(D_CACHE, "%s: unexpected query return code %d\n", hex_address, rc); 187 | statsd_increment("ac_data.db.errors"); 188 | } 189 | return entry; 190 | fail: 191 | ac_data_entry_destroy(entry); 192 | return NULL; 193 | } 194 | 195 | static void ac_data_entry_create(ac_data *ac_data, uint32_t icao_address, struct ac_data_entry *entry) { 196 | ASSERT(ac_data); 197 | 198 | NEW(uint32_t, key); 199 | *key = icao_address; 200 | cache_entry_create(ac_data->cache, key, entry, time(NULL)); 201 | } 202 | 203 | #else // !WITH_SQLITE 204 | 205 | struct ac_data *ac_data_create(char const *bs_db_file) { 206 | UNUSED(bs_db_file); 207 | return NULL; 208 | } 209 | 210 | struct ac_data_entry *ac_data_entry_lookup(ac_data *ac_data, uint32_t icao_address) { 211 | UNUSED(ac_data); 212 | UNUSED(icao_address); 213 | return NULL; 214 | } 215 | 216 | void ac_data_destroy(ac_data *ac_data) { 217 | UNUSED(ac_data); 218 | } 219 | 220 | #endif // WITH_SQLITE 221 | -------------------------------------------------------------------------------- /src/mpdu.c: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: GPL-3.0-or-later */ 2 | #include 3 | #include // struct timeval 4 | #include // la_type_descriptor, la_proto_node 5 | #include // la_reasm_ctx 6 | #include // la_list 7 | #include // la_json_append_* 8 | #include "pdu.h" // struct hfdl_pdu_hdr_data, hfdl_pdu_fcs_check 9 | #include "lpdu.h" // lpdu_parse 10 | #include "statsd.h" // statsd_* 11 | #include "util.h" // NEW, ASSERT, struct octet_string, {ac,gs}_id_format_text 12 | 13 | struct hfdl_mpdu { 14 | struct octet_string *pdu; 15 | la_list *dst_aircraft; // List of destination aircraft (for multi-LPDU uplinks) 16 | struct hfdl_pdu_hdr_data header; 17 | }; 18 | 19 | struct mpdu_dst { 20 | uint8_t dst_id; 21 | uint8_t lpdu_cnt; 22 | }; 23 | 24 | // Forward declarations 25 | la_type_descriptor const proto_DEF_hfdl_mpdu; 26 | static int32_t parse_lpdu_list(uint8_t *lpdu_len_ptr, uint8_t *data_ptr, 27 | uint8_t *endptr, uint32_t lpdu_cnt, la_list **lpdu_list, 28 | struct hfdl_pdu_hdr_data mpdu_header, la_reasm_ctx *reasm_ctx, 29 | struct timeval rx_timestamp); 30 | 31 | la_list *mpdu_parse(struct octet_string *pdu, la_reasm_ctx *reasm_ctx, 32 | struct timeval rx_timestamp, int32_t freq) { 33 | ASSERT(pdu); 34 | ASSERT(pdu->buf); 35 | ASSERT(pdu->len > 0); 36 | 37 | la_list *result = NULL; 38 | la_list *lpdu_list = NULL; 39 | la_proto_node *mpdu_node = NULL; 40 | struct hfdl_mpdu *mpdu = NULL; 41 | if(Config.output_mpdus) { 42 | mpdu = XCALLOC(1, sizeof(struct hfdl_mpdu)); 43 | mpdu->pdu = pdu; 44 | mpdu_node = la_proto_node_new(); 45 | mpdu_node->data = mpdu; 46 | mpdu_node->td = &proto_DEF_hfdl_mpdu; 47 | } 48 | 49 | struct hfdl_pdu_hdr_data mpdu_header = {0}; 50 | mpdu_header.freq = freq; 51 | uint32_t aircraft_cnt = 0; 52 | uint32_t lpdu_cnt = 0; 53 | uint32_t hdr_len = 0; 54 | uint8_t *buf = pdu->buf; 55 | uint32_t len = pdu->len; 56 | if(pdu->buf[0] & 0x2) { 57 | mpdu_header.direction = DOWNLINK_PDU; 58 | lpdu_cnt = (buf[0] >> 2) & 0xF; 59 | hdr_len = 6 + lpdu_cnt; // header length, not incl. FCS 60 | } else { 61 | mpdu_header.direction = UPLINK_PDU; 62 | aircraft_cnt = ((buf[0] & 0x70) >> 4) + 1; 63 | debug_print(D_PROTO, "aircraft_cnt: %u\n", aircraft_cnt); 64 | hdr_len = 2; // P/NAC/T + UTC/GS ID 65 | for(uint32_t i = 0; i < aircraft_cnt; i++) { 66 | if(len < hdr_len + 2) { 67 | debug_print(D_PROTO, "uplink: too short: %u < %u\n", len, hdr_len + 2); 68 | statsd_increment_per_channel(freq, "frame.errors.too_short"); 69 | goto end; 70 | } 71 | lpdu_cnt = buf[hdr_len+1] >> 4; 72 | hdr_len += 2 + lpdu_cnt; // aircraft_id + NLP/DDR/P + LPDU size octets (one per LPDU) 73 | debug_print(D_PROTO, "uplink: ac %u lpdu_cnt: %u hdr_len: %u\n", i, lpdu_cnt, hdr_len); 74 | } 75 | } 76 | debug_print(D_PROTO, "hdr_len: %u\n", hdr_len); 77 | if(len < hdr_len + 2) { 78 | debug_print(D_PROTO, "Too short: %u < %u\n", len, hdr_len + lpdu_cnt + 2); 79 | statsd_increment_per_channel(freq, "frame.errors.too_short"); 80 | goto end; 81 | } 82 | 83 | if(hfdl_pdu_fcs_check(buf, hdr_len)) { 84 | mpdu_header.crc_ok = true; 85 | statsd_increment_per_channel(freq, "frames.good"); 86 | } else { 87 | statsd_increment_per_channel(freq, "frame.errors.bad_fcs"); 88 | goto end; 89 | } 90 | 91 | uint8_t *dataptr = buf + hdr_len + 2; // First data octet of the first LPDU 92 | if(mpdu_header.direction == DOWNLINK_PDU) { 93 | statsd_increment_per_channel(freq, "frame.dir.air2gnd"); 94 | mpdu_header.src_id = buf[2]; 95 | mpdu_header.dst_id = buf[1] & 0x7f; 96 | uint8_t *hdrptr = buf + 6; // First LPDU size octet 97 | if(parse_lpdu_list(hdrptr, dataptr, buf + len, lpdu_cnt, &lpdu_list, 98 | mpdu_header, reasm_ctx, rx_timestamp) < 0) { 99 | goto end; 100 | } 101 | } else { // UPLINK_PDU 102 | statsd_increment_per_channel(freq, "frame.dir.gnd2air"); 103 | mpdu_header.src_id = buf[1] & 0x7f; 104 | mpdu_header.dst_id = 0; // See comment in mpdu_format_text() 105 | uint8_t *hdrptr = buf + 2; // First AC ID octet 106 | int32_t consumed_octets = 0; 107 | for(uint32_t i = 0; i < aircraft_cnt; i++, hdrptr += lpdu_cnt, dataptr += consumed_octets) { 108 | mpdu_header.dst_id = *hdrptr++; 109 | lpdu_cnt = (*hdrptr++ >> 4) & 0xF; 110 | if(Config.output_mpdus == true) { 111 | NEW(struct mpdu_dst, dst_ac); 112 | dst_ac->dst_id = mpdu_header.dst_id; 113 | dst_ac->lpdu_cnt = lpdu_cnt; 114 | mpdu->dst_aircraft = la_list_append(mpdu->dst_aircraft, dst_ac); 115 | } 116 | if((consumed_octets = parse_lpdu_list(hdrptr, dataptr, buf + len, 117 | lpdu_cnt, &lpdu_list, mpdu_header, 118 | reasm_ctx, rx_timestamp)) < 0) { 119 | goto end; 120 | } 121 | } 122 | } 123 | 124 | end: 125 | if(Config.output_mpdus && (mpdu_header.crc_ok || Config.output_corrupted_pdus)) { 126 | mpdu->header = mpdu_header; 127 | result = la_list_append(result, mpdu_node); 128 | result->next = lpdu_list; 129 | } else { 130 | la_proto_tree_destroy(mpdu_node); 131 | result = lpdu_list; 132 | } 133 | return result; 134 | } 135 | 136 | static int32_t parse_lpdu_list(uint8_t *lpdu_len_ptr, uint8_t *data_ptr, 137 | uint8_t *endptr, uint32_t lpdu_cnt, la_list **lpdu_list, 138 | struct hfdl_pdu_hdr_data mpdu_header, la_reasm_ctx *reasm_ctx, 139 | struct timeval rx_timestamp) { 140 | int32_t consumed_octets = 0; 141 | for(uint32_t j = 0; j < lpdu_cnt; j++) { 142 | uint32_t lpdu_len = *lpdu_len_ptr + 1; 143 | if(data_ptr + lpdu_len <= endptr) { 144 | debug_print(D_PROTO, "lpdu %u/%u: lpdu_len=%u\n", j + 1, lpdu_cnt, lpdu_len); 145 | la_proto_node *node = lpdu_parse(data_ptr, lpdu_len, mpdu_header, reasm_ctx, rx_timestamp); 146 | if(node != NULL) { 147 | *lpdu_list = la_list_append(*lpdu_list, node); 148 | } 149 | data_ptr += lpdu_len; // Move to the next LPDU 150 | consumed_octets += lpdu_len; 151 | lpdu_len_ptr++; 152 | } else { 153 | debug_print(D_PROTO, "lpdu %u/%u truncated: end is %td octets past buffer\n", 154 | j + 1, lpdu_cnt, data_ptr + lpdu_len - endptr); 155 | return -1; 156 | } 157 | } 158 | return consumed_octets; 159 | } 160 | 161 | static void mpdu_format_text(la_vstring *vstr, void const *data, int32_t indent) { 162 | ASSERT(vstr != NULL); 163 | ASSERT(data); 164 | ASSERT(indent >= 0); 165 | 166 | struct hfdl_mpdu const *mpdu = data; 167 | if(Config.output_raw_frames == true) { 168 | append_hexdump_with_indent(vstr, mpdu->pdu->buf, mpdu->pdu->len, indent+1); 169 | } 170 | if(!mpdu->header.crc_ok) { 171 | // We say "PDU" rather than "MPDU", because if the CRC check failed the we are 172 | // not even sure whether it's a MPDU or SPDU... 173 | LA_ISPRINTF(vstr, indent, "-- Unparseable PDU (CRC check failed)\n"); 174 | return; 175 | } 176 | if(mpdu->header.direction == UPLINK_PDU) { 177 | LA_ISPRINTF(vstr, indent, "Uplink MPDU:\n"); 178 | indent++; 179 | gs_id_format_text(vstr, indent, "Src GS", mpdu->header.src_id); 180 | for(la_list *ac = mpdu->dst_aircraft; ac != NULL; ac = la_list_next(ac)) { 181 | struct mpdu_dst *dst = ac->data; 182 | ac_id_format_text(vstr, indent, "Dst AC", mpdu->header.freq, dst->dst_id); 183 | LA_ISPRINTF(vstr, indent+1, "LPDU count: %hhu\n", dst->lpdu_cnt); 184 | } 185 | } else { 186 | LA_ISPRINTF(vstr, indent, "Downlink MPDU:\n"); 187 | indent++; 188 | ac_id_format_text(vstr, indent, "Src AC", mpdu->header.freq, mpdu->header.src_id); 189 | gs_id_format_text(vstr, indent, "Dst GS", mpdu->header.dst_id); 190 | } 191 | } 192 | 193 | static void mpdu_format_json(la_vstring *vstr, void const *data) { 194 | ASSERT(vstr != NULL); 195 | ASSERT(data); 196 | 197 | struct hfdl_mpdu const *mpdu = data; 198 | la_json_append_bool(vstr, "err", !mpdu->header.crc_ok); 199 | if(!mpdu->header.crc_ok) { 200 | return; 201 | } 202 | if(mpdu->header.direction == UPLINK_PDU) { 203 | gs_id_format_json(vstr, "src", mpdu->header.src_id); 204 | la_json_array_start(vstr, "dsts"); 205 | for(la_list *ac = mpdu->dst_aircraft; ac != NULL; ac = la_list_next(ac)) { 206 | struct mpdu_dst *dst = ac->data; 207 | la_json_object_start(vstr, NULL); 208 | ac_id_format_json(vstr, "dst", mpdu->header.freq, dst->dst_id); 209 | la_json_append_int64(vstr, "lpdu_cnt", dst->lpdu_cnt); 210 | la_json_object_end(vstr); 211 | } 212 | la_json_array_end(vstr); 213 | } else { 214 | ac_id_format_json(vstr, "src", mpdu->header.freq, mpdu->header.src_id); 215 | gs_id_format_json(vstr, "dst", mpdu->header.dst_id); 216 | } 217 | } 218 | 219 | static void mpdu_destroy(void *data) { 220 | if(data == NULL) { 221 | return; 222 | } 223 | struct hfdl_mpdu *mpdu = data; 224 | la_list_free(mpdu->dst_aircraft); 225 | XFREE(mpdu); 226 | } 227 | 228 | la_type_descriptor const proto_DEF_hfdl_mpdu = { 229 | .format_text = mpdu_format_text, 230 | .format_json = mpdu_format_json, 231 | .json_key = "mpdu", 232 | .destroy = mpdu_destroy 233 | }; 234 | --------------------------------------------------------------------------------