Files
Luca b5d84fcc4d Airband VOR receiver and trasmitter (#3244)
* First test

* Reorder includes for consistency and fix string dereference in VorCdiIndicator

* Add memory section and linker script entry for vor_rx application

* Testing VOR TX

* Moving VOR TX to SD

* Code refactoring to align with guidelines

* Add TODO comment to verify radial sign convention in vor_tx_config

* Remove unnecessary set_dirty() calls in VorRxView methods

* Update Course Deviation Indicator label and adjust UI element positions

* Update VorTxView to run prepared image from memory map

* Update build condition in nightly release workflow to include workflow_dispatch event

* Reverting action changes

* Adjust Course Deviation Indicator layout and refine painting logic

* Refactor VorRxView UI elements for improved layout and clarity

* Add calibration field and update radial calculation logic in VorRxView

* Enhance VOR processing: add renormalization to phase oscillator, improve signal handling, and update configuration parameters

* Implement radial smoothing and TO/FROM state handling in VorRxView; update VOR processing logic

* Setting comments inline

* Refactor VorCdiIndicator and VorRxView: change normalize_signed_degrees to use int32_t, update radial smoothing to use fixed point arithmetic to reduce flash usage

* Update external app address end to 0xAE0B0000

* Update set_vor_tx_config to include enabled parameter for consistency

* Refactor VorRx to omit decim_2 stage and update comments for clarity on VOR processing

* Add RSSI, Channel, and Audio fields to VorRxView to mimic existing apps

* Refactor VorRxView labels and status messages

* Update VorRxView calibration field range to allow full circle input

* Completing Ident transmission logic

* Fixing TX gain and moving log label

* Refactor VOR phase calculations and update radial offset handling for accurate bearing representation

* Add low-pass filters to strip 9960 Hz subcarrier from audio output
2026-07-04 08:49:21 +02:00

151 lines
5.7 KiB
C++

/*
* Copyright (C) 2014 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2026 PortaPack Mayhem
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#ifndef __PROC_VOR_RX_H__
#define __PROC_VOR_RX_H__
#include "baseband_processor.hpp"
#include "baseband_thread.hpp"
#include "rssi_thread.hpp"
#include "dsp_decimate.hpp"
#include "dsp_demodulate.hpp"
#include "dsp_iir.hpp"
#include "audio_compressor.hpp"
#include "audio_output.hpp"
#include "spectrum_collector.hpp"
#include <array>
#include <cstdint>
#include <cmath>
// Standalone VOR receiver baseband.
// Demodulates the AM channel at 48 kHz (so the 9960 Hz subcarrier stays
// representable) and decodes the 30 Hz reference vs. variable phase to derive
// the radial.
class VorRx : public BasebandProcessor {
public:
VorRx();
void execute(const buffer_c8_t& buffer) override;
void on_message(const Message* const message) override;
private:
static constexpr size_t baseband_fs = 3072000;
// VOR keeps the channel at 48 kHz instead of the AM audio 12 kHz, so the
// 9960 Hz subcarrier stays below Nyquist. The standard AM decim_2 stage is
// therefore omitted (its ~4.5 kHz low-pass would remove the subcarrier).
static constexpr size_t channel_filter_decimation_factor = 1;
std::array<complex16_t, 512> dst{};
const buffer_c16_t dst_buffer{
dst.data(),
dst.size()};
std::array<float, 32> audio{};
const buffer_f32_t audio_buffer{
audio.data(),
audio.size()};
dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0{};
dsp::decimate::FIRC16xR16x32Decim8 decim_1{};
dsp::decimate::FIRAndDecimateComplex channel_filter{};
int32_t channel_filter_low_f = 0;
int32_t channel_filter_high_f = 0;
int32_t channel_filter_transition = 0;
bool configured{false};
bool vor_enabled{false};
static constexpr float vor_reference_hz{30.0f};
static constexpr float vor_subcarrier_hz{9960.0f};
// 100 ms at 48 kHz (exactly 3 cycles of 30 Hz)
static constexpr uint32_t vor_window_samples{4800};
// One-pole low-pass to isolate the +/-480 Hz FM subcarrier baseband
// after quadrature down-conversion (corner ~600 Hz at 48 kHz).
static constexpr float vor_sub_lp_alpha{0.9245f};
// Fixed phase lag the receive chain adds to the recovered 30 Hz reference
// tone, expressed in degrees at 30 Hz. It comes almost entirely from the
// one-pole subcarrier low-pass above (~2.75 deg of lag at 30 Hz for
// alpha = 0.9245) plus a ~0.1 deg half-sample delay in the FM
// differentiator. Because the reference tone is delayed, the raw radial
// (reference - variable) reads low by this amount, so we add it back.
// Value verified by a TX->RX loopback (proc_vor_tx fed through this
// decoder): every transmitted radial decoded ~3.2 deg low before this
// correction, and on-bearing afterwards.
static constexpr float vor_ref_phase_lag_deg{3.2f};
struct PhaseOscillator {
float sin_v{0.0f};
float cos_v{1.0f};
float step_sin{0.0f};
float step_cos{1.0f};
void configure(float frequency_hz, float sample_rate_hz);
float cosine() const { return cos_v; }
float sine() const { return sin_v; }
void advance();
void renormalize();
} vor_reference_osc{}, vor_subcarrier_osc{};
// 30 Hz DFT accumulators for the variable (AM) and reference (FM) tones.
float vor_ref_i{0.0f};
float vor_ref_q{0.0f};
float vor_var_i{0.0f};
float vor_var_q{0.0f};
float vor_dc_sum{0.0f};
// Subcarrier quadrature down-converter / FM-demodulator state.
float vor_sub_lp_i{0.0f};
float vor_sub_lp_q{0.0f};
float vor_sub_prev_i{0.0f};
float vor_sub_prev_q{0.0f};
uint32_t vor_sample_count{0};
dsp::demodulate::AM demod_am{};
// Two cascaded 2nd-order Butterworth low-passes (4th-order total) that strip
// the 9960 Hz FM subcarrier out of the audio before it reaches the speaker.
// The channel is kept wide (48 kHz) so the subcarrier survives for the
// radial decode, so it must be filtered out of the audible path here.
IIRBiquadFilter audio_lpf_1{};
IIRBiquadFilter audio_lpf_2{};
FeedForwardCompressor audio_compressor{};
AudioOutput audio_output{};
SpectrumCollector channel_spectrum{};
/* NB: Threads should be the last members in the class definition. */
#ifdef PRALINE
BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive,
/*auto_start*/ false};
RSSIThread rssi_thread{/*auto_start*/ false};
#else
BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive};
RSSIThread rssi_thread{};
#endif
void configure(const AMConfigureMessage& message);
void configure_vor(const VorRxConfigureMessage& message);
void process_vor_metrics(const buffer_f32_t& audio);
void send_vor_status(uint16_t phase_deg, uint16_t radial_deg, uint16_t ref_level, uint16_t var_level, uint8_t quality, bool valid, bool to_from);
static uint16_t normalize_degrees(float degrees);
};
#endif /*__PROC_VOR_RX_H__*/