mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2026-07-26 10:38:52 +00:00
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
This commit is contained in:
@@ -83,3 +83,4 @@ venv/
|
|||||||
# generated bitmap arr file
|
# generated bitmap arr file
|
||||||
# TODO: generate bitmap during build, since we use python during build anyway, lemme know if this is a bad idea @zxkmm
|
# TODO: generate bitmap during build, since we use python during build anyway, lemme know if this is a bad idea @zxkmm
|
||||||
/firmware/tools/bitmap.hpp
|
/firmware/tools/bitmap.hpp
|
||||||
|
tmp/*
|
||||||
|
|||||||
@@ -361,6 +361,16 @@ void set_noaaapt_config() {
|
|||||||
send_message(&message);
|
send_message(&message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void set_vor_config(bool enabled) {
|
||||||
|
const VorRxConfigureMessage message{enabled};
|
||||||
|
send_message(&message);
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_vor_tx_config(uint16_t radial_deg, bool ident_enabled, const std::string& ident, bool enabled) {
|
||||||
|
const VorTxConfigureMessage message{radial_deg, ident_enabled, ident.c_str(), enabled};
|
||||||
|
send_message(&message);
|
||||||
|
}
|
||||||
|
|
||||||
void set_flex_config() {
|
void set_flex_config() {
|
||||||
const FlexConfigureMessage message{};
|
const FlexConfigureMessage message{};
|
||||||
send_message(&message);
|
send_message(&message);
|
||||||
|
|||||||
@@ -113,6 +113,8 @@ void set_morsetx_config(uint8_t mode, uint32_t tone, float fm_delta);
|
|||||||
void set_morsetx_key(bool key_down);
|
void set_morsetx_key(bool key_down);
|
||||||
void set_wefax_config(uint8_t lpm, uint8_t ioc);
|
void set_wefax_config(uint8_t lpm, uint8_t ioc);
|
||||||
void set_noaaapt_config();
|
void set_noaaapt_config();
|
||||||
|
void set_vor_config(bool enabled = true);
|
||||||
|
void set_vor_tx_config(uint16_t radial_deg, bool ident_enabled = true, const std::string& ident = "", bool enabled = true);
|
||||||
void set_flex_config();
|
void set_flex_config();
|
||||||
void set_bitstream_config(uint32_t deviation, uint8_t mode); // mode 0 for am, 1 for 2fsk
|
void set_bitstream_config(uint32_t deviation, uint8_t mode); // mode 0 for am, 1 for 2fsk
|
||||||
void set_rtty_config(uint16_t baud, uint16_t shift, uint8_t* payload = nullptr, uint16_t payload_length = 0); // baud*100
|
void set_rtty_config(uint16_t baud, uint16_t shift, uint8_t* payload = nullptr, uint16_t payload_length = 0); // baud*100
|
||||||
|
|||||||
+10
@@ -133,6 +133,14 @@ set(EXTCPPSRC
|
|||||||
external/noaaapt_rx/main.cpp
|
external/noaaapt_rx/main.cpp
|
||||||
external/noaaapt_rx/ui_noaaapt_rx.cpp
|
external/noaaapt_rx/ui_noaaapt_rx.cpp
|
||||||
|
|
||||||
|
#vor_rx
|
||||||
|
external/vor_rx/main.cpp
|
||||||
|
external/vor_rx/ui_vor_rx.cpp
|
||||||
|
|
||||||
|
#vor_tx
|
||||||
|
external/vor_tx/main.cpp
|
||||||
|
external/vor_tx/ui_vor_tx.cpp
|
||||||
|
|
||||||
#shoppingcart_lock 272 bytes
|
#shoppingcart_lock 272 bytes
|
||||||
external/shoppingcart_lock/main.cpp
|
external/shoppingcart_lock/main.cpp
|
||||||
external/shoppingcart_lock/shoppingcart_lock.cpp
|
external/shoppingcart_lock/shoppingcart_lock.cpp
|
||||||
@@ -401,6 +409,8 @@ set(EXTAPPLIST
|
|||||||
acars_rx
|
acars_rx
|
||||||
wefax_rx
|
wefax_rx
|
||||||
noaaapt_rx
|
noaaapt_rx
|
||||||
|
vor_rx
|
||||||
|
vor_tx
|
||||||
shoppingcart_lock
|
shoppingcart_lock
|
||||||
ookbrute
|
ookbrute
|
||||||
ook_editor
|
ook_editor
|
||||||
|
|||||||
+14
@@ -111,6 +111,8 @@ MEMORY
|
|||||||
ram_external_app_flex_tx (rwx) : org = 0xAE060000, len = 32k
|
ram_external_app_flex_tx (rwx) : org = 0xAE060000, len = 32k
|
||||||
ram_external_app_hard_reset (rwx) : org = 0xAE070000, len = 32k
|
ram_external_app_hard_reset (rwx) : org = 0xAE070000, len = 32k
|
||||||
ram_external_app_secplustx (rwx) : org = 0xAE080000, len = 32k
|
ram_external_app_secplustx (rwx) : org = 0xAE080000, len = 32k
|
||||||
|
ram_external_app_vor_rx (rwx) : org = 0xAE090000, len = 32k
|
||||||
|
ram_external_app_vor_tx (rwx) : org = 0xAE0A0000, len = 32k
|
||||||
}
|
}
|
||||||
|
|
||||||
SECTIONS
|
SECTIONS
|
||||||
@@ -391,6 +393,18 @@ SECTIONS
|
|||||||
*(*ui*external_app*noaaapt_rx*);
|
*(*ui*external_app*noaaapt_rx*);
|
||||||
} > ram_external_app_noaaapt_rx
|
} > ram_external_app_noaaapt_rx
|
||||||
|
|
||||||
|
.external_app_vor_rx : ALIGN(4) SUBALIGN(4)
|
||||||
|
{
|
||||||
|
KEEP(*(.external_app.app_vor_rx.application_information));
|
||||||
|
*(*ui*external_app*vor_rx*);
|
||||||
|
} > ram_external_app_vor_rx
|
||||||
|
|
||||||
|
.external_app_vor_tx : ALIGN(4) SUBALIGN(4)
|
||||||
|
{
|
||||||
|
KEEP(*(.external_app.app_vor_tx.application_information));
|
||||||
|
*(*ui*external_app*vor_tx*);
|
||||||
|
} > ram_external_app_vor_tx
|
||||||
|
|
||||||
.external_app_breakout : ALIGN(4) SUBALIGN(4)
|
.external_app_breakout : ALIGN(4) SUBALIGN(4)
|
||||||
{
|
{
|
||||||
KEEP(*(.external_app.app_breakout.application_information));
|
KEEP(*(.external_app.app_breakout.application_information));
|
||||||
|
|||||||
+85
@@ -0,0 +1,85 @@
|
|||||||
|
/*
|
||||||
|
* 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; if not, write to
|
||||||
|
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||||
|
* Boston, MA 02110-1301, USA.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "ui.hpp"
|
||||||
|
#include "ui_vor_rx.hpp"
|
||||||
|
#include "ui_navigation.hpp"
|
||||||
|
#include "external_app.hpp"
|
||||||
|
|
||||||
|
namespace ui::external_app::vor_rx {
|
||||||
|
|
||||||
|
void initialize_app(ui::NavigationView& nav) {
|
||||||
|
nav.push<VorRxView>();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ui::external_app::vor_rx
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
|
||||||
|
__attribute__((section(".external_app.app_vor_rx.application_information"), used)) application_information_t _application_information_vor_rx = {
|
||||||
|
/*.memory_location = */ (uint8_t*)0x00000000,
|
||||||
|
/*.externalAppEntry = */ ui::external_app::vor_rx::initialize_app,
|
||||||
|
/*.header_version = */ CURRENT_HEADER_VERSION,
|
||||||
|
/*.app_version = */ VERSION_MD5,
|
||||||
|
|
||||||
|
/*.app_name = */ "VOR RX",
|
||||||
|
/*.bitmap_data = */ {
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x80,
|
||||||
|
0x01,
|
||||||
|
0xC0,
|
||||||
|
0x03,
|
||||||
|
0xE0,
|
||||||
|
0x07,
|
||||||
|
0xF0,
|
||||||
|
0x0F,
|
||||||
|
0xF8,
|
||||||
|
0x1F,
|
||||||
|
0xF8,
|
||||||
|
0x1F,
|
||||||
|
0xF0,
|
||||||
|
0x0F,
|
||||||
|
0xE0,
|
||||||
|
0x07,
|
||||||
|
0xC0,
|
||||||
|
0x03,
|
||||||
|
0x80,
|
||||||
|
0x01,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
},
|
||||||
|
/*.icon_color = */ ui::Color::orange().v,
|
||||||
|
/*.menu_location = */ app_location_t::RX,
|
||||||
|
/*.desired_menu_position = */ -1,
|
||||||
|
|
||||||
|
/*.m4_app_tag = portapack::spi_flash::image_tag_vor_rx */ {'P', 'V', 'R', 'X'},
|
||||||
|
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
|
||||||
|
};
|
||||||
|
}
|
||||||
+341
@@ -0,0 +1,341 @@
|
|||||||
|
/*
|
||||||
|
* 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; if not, write to
|
||||||
|
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||||
|
* Boston, MA 02110-1301, USA.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "ui_vor_rx.hpp"
|
||||||
|
|
||||||
|
#include "audio.hpp"
|
||||||
|
#include "string_format.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
using namespace portapack;
|
||||||
|
using namespace ui;
|
||||||
|
|
||||||
|
namespace ui::external_app::vor_rx {
|
||||||
|
|
||||||
|
void VorLogger::write_header() {
|
||||||
|
log_file.write_raw("Time;Course;Radial;Deviation;Quality;Flag;");
|
||||||
|
}
|
||||||
|
|
||||||
|
void VorLogger::log_status(const VorRxStatusDataMessage& message, uint16_t course_deg) {
|
||||||
|
const int16_t deviation = static_cast<int16_t>((static_cast<int32_t>(message.radial_deg) - static_cast<int32_t>(course_deg) + 540) % 360 - 180);
|
||||||
|
std::string row = ";";
|
||||||
|
row += to_string_dec_uint(course_deg);
|
||||||
|
row += ";" + to_string_dec_uint(message.radial_deg);
|
||||||
|
row += ";" + std::to_string(deviation);
|
||||||
|
row += ";" + to_string_dec_uint(message.quality);
|
||||||
|
row += ";" + std::string(message.valid ? (message.to_from ? "TO" : "FROM") : "--");
|
||||||
|
log_file.write_entry(rtc_time::now(), row);
|
||||||
|
}
|
||||||
|
|
||||||
|
VorCdiIndicator::VorCdiIndicator(Point position)
|
||||||
|
: Widget{{position, {screen_width, 32}}} {
|
||||||
|
}
|
||||||
|
|
||||||
|
void VorCdiIndicator::set_course(uint16_t course_deg) {
|
||||||
|
if (course_deg_ != course_deg) {
|
||||||
|
course_deg_ = course_deg;
|
||||||
|
set_dirty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VorCdiIndicator::set_radial(uint16_t radial_deg) {
|
||||||
|
if (radial_deg_ != radial_deg) {
|
||||||
|
radial_deg_ = radial_deg;
|
||||||
|
set_dirty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VorCdiIndicator::set_valid(bool valid) {
|
||||||
|
if (valid_ != valid) {
|
||||||
|
valid_ = valid;
|
||||||
|
set_dirty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t VorCdiIndicator::normalize_signed_degrees(int32_t degrees) {
|
||||||
|
while (degrees <= -180) {
|
||||||
|
degrees += 360;
|
||||||
|
}
|
||||||
|
while (degrees > 180) {
|
||||||
|
degrees -= 360;
|
||||||
|
}
|
||||||
|
return degrees;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VorCdiIndicator::paint(Painter& painter) {
|
||||||
|
const auto r = screen_rect();
|
||||||
|
const auto theme = Theme::getInstance();
|
||||||
|
const auto line_color = valid_ ? theme->fg_light->foreground : theme->bg_medium->foreground;
|
||||||
|
const auto needle_color = valid_ ? theme->fg_red->foreground : theme->bg_medium->foreground;
|
||||||
|
|
||||||
|
// Clear background so old needle positions don't linger.
|
||||||
|
painter.fill_rectangle(r, theme->bg_darkest->background);
|
||||||
|
|
||||||
|
const auto center_x = static_cast<Coord>(r.left() + r.width() / 2);
|
||||||
|
const auto center_y = static_cast<Coord>(r.top() + r.height() / 2);
|
||||||
|
|
||||||
|
// Horizontal scale line.
|
||||||
|
painter.draw_hline({static_cast<Coord>(r.left() + 8), center_y}, r.width() - 16, line_color);
|
||||||
|
|
||||||
|
// Evenly spaced tick marks, longer at center.
|
||||||
|
constexpr int32_t tick_spacing = 40;
|
||||||
|
for (int32_t tick = -2; tick <= 2; ++tick) {
|
||||||
|
const auto x = static_cast<Coord>(center_x + tick * tick_spacing);
|
||||||
|
const auto len = (tick == 0) ? 14 : 8;
|
||||||
|
painter.draw_vline({x, static_cast<Coord>(center_y - len / 2)}, len, line_color);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deviation needle, clamped to +/-10 degrees full-scale.
|
||||||
|
const int32_t deviation = normalize_signed_degrees(static_cast<int32_t>(radial_deg_) - static_cast<int32_t>(course_deg_));
|
||||||
|
const int32_t scaled = std::max<int32_t>(-10, std::min<int32_t>(10, deviation));
|
||||||
|
// +/-10 deg full-scale maps to +/-2 ticks (2 * tick_spacing); the division
|
||||||
|
// is exact for the clamped range so no rounding is needed.
|
||||||
|
const int16_t needle_offset = static_cast<int16_t>((scaled * (2 * tick_spacing)) / 10);
|
||||||
|
const auto needle_x = static_cast<Coord>(center_x + needle_offset);
|
||||||
|
painter.draw_vline({needle_x, static_cast<Coord>(r.top() + 4)}, r.height() - 8, needle_color);
|
||||||
|
}
|
||||||
|
|
||||||
|
VorRxView::VorRxView(NavigationView& nav)
|
||||||
|
: nav_{nav} {
|
||||||
|
add_children({&field_rf_amp,
|
||||||
|
&field_lna,
|
||||||
|
&field_vga,
|
||||||
|
&field_volume,
|
||||||
|
&field_frequency,
|
||||||
|
&rssi,
|
||||||
|
&channel,
|
||||||
|
&audio,
|
||||||
|
&labels,
|
||||||
|
&text_status,
|
||||||
|
&text_band,
|
||||||
|
&text_next,
|
||||||
|
&field_course,
|
||||||
|
&text_course_unit,
|
||||||
|
&field_calibration,
|
||||||
|
&text_calib_unit,
|
||||||
|
&text_radial,
|
||||||
|
&text_flag,
|
||||||
|
&text_cdi_title,
|
||||||
|
&cdi_indicator,
|
||||||
|
&check_log,
|
||||||
|
&button_start_stop});
|
||||||
|
|
||||||
|
field_frequency.set_step(8333);
|
||||||
|
if (field_frequency.value() == 0) {
|
||||||
|
field_frequency.set_value(110'000'000);
|
||||||
|
}
|
||||||
|
|
||||||
|
field_course.set_value(0);
|
||||||
|
field_course.on_change = [this](int32_t) {
|
||||||
|
update_cdi();
|
||||||
|
};
|
||||||
|
|
||||||
|
field_calibration.set_value(0);
|
||||||
|
field_calibration.on_change = [this](int32_t) {
|
||||||
|
refresh_radial();
|
||||||
|
};
|
||||||
|
|
||||||
|
logger = std::make_unique<VorLogger>();
|
||||||
|
check_log.set_value(logging_);
|
||||||
|
check_log.on_select = [this](Checkbox&, bool v) {
|
||||||
|
logging_ = v;
|
||||||
|
update_logging();
|
||||||
|
};
|
||||||
|
update_logging();
|
||||||
|
|
||||||
|
button_start_stop.on_select = [this](Button&) {
|
||||||
|
if (running_) {
|
||||||
|
stop_receiver();
|
||||||
|
} else {
|
||||||
|
start_receiver();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
start_receiver();
|
||||||
|
}
|
||||||
|
|
||||||
|
VorRxView::~VorRxView() {
|
||||||
|
stop_receiver();
|
||||||
|
}
|
||||||
|
|
||||||
|
void VorRxView::focus() {
|
||||||
|
field_frequency.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
void VorRxView::start_receiver() {
|
||||||
|
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
|
||||||
|
baseband::set_vor_config(true);
|
||||||
|
|
||||||
|
radial_filter_valid_ = false;
|
||||||
|
flag_state_ = 0;
|
||||||
|
|
||||||
|
receiver_model.set_hidden_offset(0);
|
||||||
|
receiver_model.set_modulation(ReceiverModel::Mode::AMAudio);
|
||||||
|
receiver_model.set_frequency_step(8333);
|
||||||
|
receiver_model.set_sampling_rate(3072000);
|
||||||
|
receiver_model.set_baseband_bandwidth(1750000);
|
||||||
|
receiver_model.enable();
|
||||||
|
|
||||||
|
// enable() applies the AM configuration, which forces the audio codec to
|
||||||
|
// 12 kHz. The VOR baseband keeps its channel at 48 kHz (so the 9960 Hz
|
||||||
|
// subcarrier stays representable), so re-assert 48 kHz audio afterwards.
|
||||||
|
audio::set_rate(audio::Rate::Hz_48000);
|
||||||
|
audio::output::start();
|
||||||
|
|
||||||
|
running_ = true;
|
||||||
|
update_status();
|
||||||
|
}
|
||||||
|
|
||||||
|
void VorRxView::stop_receiver() {
|
||||||
|
if (!running_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
running_ = false;
|
||||||
|
baseband::set_vor_config(false);
|
||||||
|
receiver_model.disable();
|
||||||
|
baseband::shutdown();
|
||||||
|
audio::output::stop();
|
||||||
|
update_status();
|
||||||
|
}
|
||||||
|
|
||||||
|
void VorRxView::update_status() {
|
||||||
|
text_status.set(running_ ? "Running" : "Idle");
|
||||||
|
button_start_stop.set_text(running_ ? "Stop" : "Start");
|
||||||
|
}
|
||||||
|
|
||||||
|
void VorRxView::update_logging() {
|
||||||
|
if (logger && logging_) {
|
||||||
|
logger->append(logs_dir / (std::string("VOR_") + to_string_timestamp(rtc_time::now()) + ".CSV"));
|
||||||
|
logger->write_header();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VorRxView::on_vor_status(const VorRxStatusDataMessage& message) {
|
||||||
|
if (!running_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
last_radial_deg_ = message.radial_deg;
|
||||||
|
last_valid_ = message.valid;
|
||||||
|
have_status_ = true;
|
||||||
|
|
||||||
|
if (message.valid) {
|
||||||
|
last_radial_deg_ = smooth_radial(message.radial_deg);
|
||||||
|
} else {
|
||||||
|
// Drop the filter history so a fresh lock doesn't drag from stale data.
|
||||||
|
radial_filter_valid_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
text_next.set(message.valid ? "Locked" : "Searching");
|
||||||
|
refresh_radial();
|
||||||
|
if (logger && logging_) {
|
||||||
|
VorRxStatusDataMessage calibrated = message;
|
||||||
|
calibrated.radial_deg = calibrated_radial(last_radial_deg_);
|
||||||
|
calibrated.to_from = (flag_state_ == 2);
|
||||||
|
logger->log_status(calibrated, field_course.value());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t VorRxView::smooth_radial(uint16_t radial_deg) {
|
||||||
|
// Wrapped exponential moving average. At ~10 updates/s blending 20% of each
|
||||||
|
// new reading (alpha = 0.8) gives a ~0.5 s time constant while cutting the
|
||||||
|
// jitter by ~3x. Stepping along the shortest arc in 1/64 deg fixed point
|
||||||
|
// handles the 0/360 deg wrap without any trig (which would otherwise pull
|
||||||
|
// newlib's float sin/cos/atan2 argument-reduction code into flash ROM).
|
||||||
|
constexpr int32_t scale = 64;
|
||||||
|
constexpr int32_t full_circle = 360 * scale;
|
||||||
|
const int32_t target = static_cast<int32_t>(radial_deg) * scale;
|
||||||
|
|
||||||
|
if (!radial_filter_valid_) {
|
||||||
|
radial_smoothed_fp_ = target;
|
||||||
|
radial_filter_valid_ = true;
|
||||||
|
} else {
|
||||||
|
int32_t diff = (target - radial_smoothed_fp_) % full_circle;
|
||||||
|
if (diff < -full_circle / 2) {
|
||||||
|
diff += full_circle;
|
||||||
|
} else if (diff > full_circle / 2) {
|
||||||
|
diff -= full_circle;
|
||||||
|
}
|
||||||
|
// 20% step toward the new reading (1 - alpha).
|
||||||
|
radial_smoothed_fp_ += diff / 5;
|
||||||
|
radial_smoothed_fp_ %= full_circle;
|
||||||
|
if (radial_smoothed_fp_ < 0) {
|
||||||
|
radial_smoothed_fp_ += full_circle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return static_cast<uint16_t>((radial_smoothed_fp_ + scale / 2) / scale) % 360;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t VorRxView::calibrated_radial(uint16_t radial_deg) const {
|
||||||
|
int32_t value = (static_cast<int32_t>(radial_deg) + field_calibration.value()) % 360;
|
||||||
|
if (value < 0) {
|
||||||
|
value += 360;
|
||||||
|
}
|
||||||
|
return static_cast<uint16_t>(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void VorRxView::refresh_radial() {
|
||||||
|
if (!have_status_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint16_t radial = calibrated_radial(last_radial_deg_);
|
||||||
|
text_radial.set(to_string_dec_uint(radial, 3) + " deg");
|
||||||
|
text_flag.set(to_from_label(radial));
|
||||||
|
cdi_indicator.set_radial(radial);
|
||||||
|
cdi_indicator.set_valid(last_valid_);
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* VorRxView::to_from_label(uint16_t radial_deg) {
|
||||||
|
if (!last_valid_) {
|
||||||
|
flag_state_ = 0;
|
||||||
|
return "--";
|
||||||
|
}
|
||||||
|
|
||||||
|
// TO/FROM is set by the selected OBS course relative to the received radial,
|
||||||
|
// not by the radial alone. Take the difference wrapped to [-180, 180]: the
|
||||||
|
// switch is at the 90 deg abeam point. |diff| < 90 means the selected course
|
||||||
|
// leads away from the station (FROM); |diff| > 90 means toward it (TO).
|
||||||
|
int32_t diff = (static_cast<int32_t>(radial_deg) - field_course.value() + 540) % 360 - 180;
|
||||||
|
const int32_t adiff = (diff < 0) ? -diff : diff;
|
||||||
|
|
||||||
|
// Hysteresis: hold the current flag within a +/-5 deg dead zone around the
|
||||||
|
// 90 deg boundary so noise near abeam doesn't rapidly toggle TO/FROM.
|
||||||
|
if (adiff < 85) {
|
||||||
|
flag_state_ = 1; // FROM
|
||||||
|
} else if (adiff > 95) {
|
||||||
|
flag_state_ = 2; // TO
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flag_state_ == 1) return "FROM";
|
||||||
|
if (flag_state_ == 2) return "TO";
|
||||||
|
return "--"; // abeam / ambiguous
|
||||||
|
}
|
||||||
|
|
||||||
|
void VorRxView::update_cdi() {
|
||||||
|
cdi_indicator.set_course(field_course.value());
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ui::external_app::vor_rx
|
||||||
+200
@@ -0,0 +1,200 @@
|
|||||||
|
/*
|
||||||
|
* 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; if not, write to
|
||||||
|
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||||
|
* Boston, MA 02110-1301, USA.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef __UI_VOR_RX_H__
|
||||||
|
#define __UI_VOR_RX_H__
|
||||||
|
|
||||||
|
#include "audio.hpp"
|
||||||
|
#include "baseband_api.hpp"
|
||||||
|
#include "file_path.hpp"
|
||||||
|
#include "log_file.hpp"
|
||||||
|
#include "rtc_time.hpp"
|
||||||
|
#include "portapack.hpp"
|
||||||
|
#include "ui.hpp"
|
||||||
|
#include "ui_audio.hpp"
|
||||||
|
#include "ui_channel.hpp"
|
||||||
|
#include "ui_freq_field.hpp"
|
||||||
|
#include "ui_navigation.hpp"
|
||||||
|
#include "ui_receiver.hpp"
|
||||||
|
#include "ui_rssi.hpp"
|
||||||
|
|
||||||
|
namespace ui::external_app::vor_rx {
|
||||||
|
|
||||||
|
class VorLogger {
|
||||||
|
public:
|
||||||
|
Optional<File::Error> append(const std::filesystem::path& filename) {
|
||||||
|
return log_file.append(filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
void write_header();
|
||||||
|
void log_status(const VorRxStatusDataMessage& message, uint16_t course_deg);
|
||||||
|
|
||||||
|
private:
|
||||||
|
LogFile log_file{};
|
||||||
|
};
|
||||||
|
|
||||||
|
class VorCdiIndicator : public Widget {
|
||||||
|
public:
|
||||||
|
VorCdiIndicator(Point position);
|
||||||
|
|
||||||
|
void set_course(uint16_t course_deg);
|
||||||
|
void set_radial(uint16_t radial_deg);
|
||||||
|
void set_valid(bool valid);
|
||||||
|
|
||||||
|
void paint(Painter& painter) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
static int32_t normalize_signed_degrees(int32_t degrees);
|
||||||
|
|
||||||
|
uint16_t course_deg_{0};
|
||||||
|
uint16_t radial_deg_{0};
|
||||||
|
bool valid_{false};
|
||||||
|
};
|
||||||
|
|
||||||
|
class VorRxView : public View {
|
||||||
|
public:
|
||||||
|
VorRxView(NavigationView& nav);
|
||||||
|
~VorRxView();
|
||||||
|
|
||||||
|
void focus() override;
|
||||||
|
|
||||||
|
std::string title() const override { return "VOR RX"; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
void start_receiver();
|
||||||
|
void stop_receiver();
|
||||||
|
void update_status();
|
||||||
|
void on_vor_status(const VorRxStatusDataMessage& message);
|
||||||
|
void update_cdi();
|
||||||
|
void refresh_radial();
|
||||||
|
uint16_t calibrated_radial(uint16_t radial_deg) const;
|
||||||
|
uint16_t smooth_radial(uint16_t radial_deg);
|
||||||
|
const char* to_from_label(uint16_t radial_deg);
|
||||||
|
void update_logging();
|
||||||
|
|
||||||
|
NavigationView& nav_;
|
||||||
|
bool running_{false};
|
||||||
|
bool logging_{false};
|
||||||
|
bool have_status_{false};
|
||||||
|
uint16_t last_radial_deg_{0};
|
||||||
|
bool last_valid_{false};
|
||||||
|
uint8_t flag_state_{0}; // TO/FROM hysteresis: 0=unknown, 1=FROM, 2=TO
|
||||||
|
// Circular exponential moving average of the radial. Each 100 ms estimate
|
||||||
|
// is noisy (~10 deg std even when locked), so blend it along the shortest
|
||||||
|
// arc in 1/64 deg fixed point to average correctly across the 0/360 deg
|
||||||
|
// wrap without trig.
|
||||||
|
bool radial_filter_valid_{false};
|
||||||
|
int32_t radial_smoothed_fp_{0};
|
||||||
|
|
||||||
|
RxFrequencyField field_frequency{
|
||||||
|
{UI_POS_X(0), UI_POS_Y(0)},
|
||||||
|
nav_};
|
||||||
|
RFAmpField field_rf_amp{
|
||||||
|
{UI_POS_X(13), UI_POS_Y(0)}};
|
||||||
|
LNAGainField field_lna{
|
||||||
|
{UI_POS_X(15), UI_POS_Y(0)}};
|
||||||
|
VGAGainField field_vga{
|
||||||
|
{UI_POS_X(18), UI_POS_Y(0)}};
|
||||||
|
AudioVolumeField field_volume{
|
||||||
|
{screen_width - 2 * 8, UI_POS_Y(0)}};
|
||||||
|
|
||||||
|
RSSI rssi{
|
||||||
|
{UI_POS_X(21), 0, UI_POS_WIDTH_REMAINING(21) - UI_POS_WIDTH(2), 4}};
|
||||||
|
Channel channel{
|
||||||
|
{UI_POS_X(21), 5, UI_POS_WIDTH_REMAINING(21) - UI_POS_WIDTH(2), 4}};
|
||||||
|
Audio audio{
|
||||||
|
{UI_POS_X(21), 10, UI_POS_WIDTH_REMAINING(21) - UI_POS_WIDTH(2), 4}};
|
||||||
|
|
||||||
|
NumberField field_course{
|
||||||
|
{UI_POS_X(14), UI_POS_Y(4)},
|
||||||
|
3,
|
||||||
|
{0, 359},
|
||||||
|
1,
|
||||||
|
'0'};
|
||||||
|
|
||||||
|
NumberField field_calibration{
|
||||||
|
{UI_POS_X(14), UI_POS_Y(7)},
|
||||||
|
4,
|
||||||
|
{-359, 359},
|
||||||
|
1,
|
||||||
|
' '};
|
||||||
|
|
||||||
|
Labels labels{
|
||||||
|
{{UI_POS_X(0), UI_POS_Y(1)}, "Status:", Theme::getInstance()->fg_light->foreground},
|
||||||
|
{{UI_POS_X(0), UI_POS_Y(2)}, "VOR Band:", Theme::getInstance()->fg_light->foreground},
|
||||||
|
{{UI_POS_X(0), UI_POS_Y(3)}, "Decoder:", Theme::getInstance()->fg_light->foreground},
|
||||||
|
{{UI_POS_X(0), UI_POS_Y(4)}, "Course (OBS):", Theme::getInstance()->fg_light->foreground},
|
||||||
|
{{UI_POS_X(0), UI_POS_Y(5)}, "Rec. Radial:", Theme::getInstance()->fg_light->foreground},
|
||||||
|
{{UI_POS_X(0), UI_POS_Y(6)}, "Flag:", Theme::getInstance()->fg_light->foreground},
|
||||||
|
{{UI_POS_X(0), UI_POS_Y(7)}, "Calibration:", Theme::getInstance()->fg_light->foreground}};
|
||||||
|
|
||||||
|
Text text_status{
|
||||||
|
{UI_POS_X(14), UI_POS_Y(1), UI_POS_WIDTH_REMAINING(14), UI_POS_HEIGHT(1)},
|
||||||
|
"Idle"};
|
||||||
|
Text text_band{
|
||||||
|
{UI_POS_X(14), UI_POS_Y(2), UI_POS_WIDTH_REMAINING(14), UI_POS_HEIGHT(1)},
|
||||||
|
"108.00-117.95MHz"};
|
||||||
|
Text text_next{
|
||||||
|
{UI_POS_X(14), UI_POS_Y(3), UI_POS_WIDTH_REMAINING(14), UI_POS_HEIGHT(1)},
|
||||||
|
"Pending"};
|
||||||
|
Text text_course_unit{
|
||||||
|
{UI_POS_X(18), UI_POS_Y(4), UI_POS_WIDTH_REMAINING(18), UI_POS_HEIGHT(1)},
|
||||||
|
"deg"};
|
||||||
|
Text text_radial{
|
||||||
|
{UI_POS_X(14), UI_POS_Y(5), UI_POS_WIDTH_REMAINING(14), UI_POS_HEIGHT(1)},
|
||||||
|
"--"};
|
||||||
|
Text text_flag{
|
||||||
|
{UI_POS_X(14), UI_POS_Y(6), UI_POS_WIDTH_REMAINING(14), UI_POS_HEIGHT(1)},
|
||||||
|
"--"};
|
||||||
|
Text text_calib_unit{
|
||||||
|
{UI_POS_X(19), UI_POS_Y(7), UI_POS_WIDTH_REMAINING(19), UI_POS_HEIGHT(1)},
|
||||||
|
"deg"};
|
||||||
|
|
||||||
|
Text text_cdi_title{
|
||||||
|
{UI_POS_X(0), UI_POS_Y(9), UI_POS_WIDTH_REMAINING(0), UI_POS_HEIGHT(1)},
|
||||||
|
"Course Deviation Indicator"};
|
||||||
|
|
||||||
|
VorCdiIndicator cdi_indicator{
|
||||||
|
{UI_POS_X(0), UI_POS_Y(10)}};
|
||||||
|
|
||||||
|
std::unique_ptr<VorLogger> logger{};
|
||||||
|
|
||||||
|
Button button_start_stop{
|
||||||
|
{UI_POS_X(9), UI_POS_Y(14), UI_POS_WIDTH(12), UI_POS_HEIGHT(2)},
|
||||||
|
"Start"};
|
||||||
|
|
||||||
|
Checkbox check_log{
|
||||||
|
{UI_POS_X(7), UI_POS_Y(17)},
|
||||||
|
3,
|
||||||
|
"LOG to SD Card",
|
||||||
|
true};
|
||||||
|
|
||||||
|
MessageHandlerRegistration message_handler_vor_status{
|
||||||
|
Message::ID::VorRxStatusData,
|
||||||
|
[this](const Message* p) {
|
||||||
|
const auto message = *reinterpret_cast<const VorRxStatusDataMessage*>(p);
|
||||||
|
on_vor_status(message);
|
||||||
|
}};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ui::external_app::vor_rx
|
||||||
|
|
||||||
|
#endif // __UI_VOR_RX_H__
|
||||||
+85
@@ -0,0 +1,85 @@
|
|||||||
|
/*
|
||||||
|
* 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; if not, write to
|
||||||
|
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||||
|
* Boston, MA 02110-1301, USA.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "ui.hpp"
|
||||||
|
#include "ui_vor_tx.hpp"
|
||||||
|
#include "ui_navigation.hpp"
|
||||||
|
#include "external_app.hpp"
|
||||||
|
|
||||||
|
namespace ui::external_app::vor_tx {
|
||||||
|
|
||||||
|
void initialize_app(ui::NavigationView& nav) {
|
||||||
|
nav.push<VorTxView>();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ui::external_app::vor_tx
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
|
||||||
|
__attribute__((section(".external_app.app_vor_tx.application_information"), used)) application_information_t _application_information_vor_tx = {
|
||||||
|
/*.memory_location = */ (uint8_t*)0x00000000,
|
||||||
|
/*.externalAppEntry = */ ui::external_app::vor_tx::initialize_app,
|
||||||
|
/*.header_version = */ CURRENT_HEADER_VERSION,
|
||||||
|
/*.app_version = */ VERSION_MD5,
|
||||||
|
|
||||||
|
/*.app_name = */ "VOR TX",
|
||||||
|
/*.bitmap_data = */ {
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x80,
|
||||||
|
0x01,
|
||||||
|
0xC0,
|
||||||
|
0x03,
|
||||||
|
0xE0,
|
||||||
|
0x07,
|
||||||
|
0xF0,
|
||||||
|
0x0F,
|
||||||
|
0xF8,
|
||||||
|
0x1F,
|
||||||
|
0xF8,
|
||||||
|
0x1F,
|
||||||
|
0xF0,
|
||||||
|
0x0F,
|
||||||
|
0xE0,
|
||||||
|
0x07,
|
||||||
|
0xC0,
|
||||||
|
0x03,
|
||||||
|
0x80,
|
||||||
|
0x01,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
0x00,
|
||||||
|
},
|
||||||
|
/*.icon_color = */ ui::Color::green().v,
|
||||||
|
/*.menu_location = */ app_location_t::TX,
|
||||||
|
/*.desired_menu_position = */ -1,
|
||||||
|
|
||||||
|
/*.m4_app_tag = portapack::spi_flash::image_tag_vor_tx */ {'P', 'V', 'T', 'X'},
|
||||||
|
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
|
||||||
|
};
|
||||||
|
}
|
||||||
+133
@@ -0,0 +1,133 @@
|
|||||||
|
/*
|
||||||
|
* 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; if not, write to
|
||||||
|
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||||
|
* Boston, MA 02110-1301, USA.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "ui_vor_tx.hpp"
|
||||||
|
|
||||||
|
#include "string_format.hpp"
|
||||||
|
#include "ui_textentry.hpp"
|
||||||
|
|
||||||
|
using namespace portapack;
|
||||||
|
using namespace ui;
|
||||||
|
|
||||||
|
namespace ui::external_app::vor_tx {
|
||||||
|
|
||||||
|
VorTxView::VorTxView(NavigationView& nav)
|
||||||
|
: nav_{nav} {
|
||||||
|
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
|
||||||
|
|
||||||
|
add_children({&labels,
|
||||||
|
&field_radial,
|
||||||
|
&text_radial_unit,
|
||||||
|
&check_ident,
|
||||||
|
&button_ident_text,
|
||||||
|
&text_status,
|
||||||
|
&tx_view});
|
||||||
|
|
||||||
|
field_radial.set_value(radial_);
|
||||||
|
field_radial.on_change = [this](int32_t v) {
|
||||||
|
radial_ = v;
|
||||||
|
if (transmitting_) {
|
||||||
|
update_config();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
check_ident.set_value(ident_);
|
||||||
|
check_ident.on_select = [this](Checkbox&, bool v) {
|
||||||
|
ident_ = v;
|
||||||
|
if (transmitting_) {
|
||||||
|
update_config();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
button_ident_text.set_text(ident_text_);
|
||||||
|
button_ident_text.on_select = [this](Button&) {
|
||||||
|
text_prompt(nav_, ident_text_, 7, ENTER_KEYBOARD_MODE_ALPHA, [this](std::string&) {
|
||||||
|
button_ident_text.set_text(ident_text_);
|
||||||
|
if (transmitting_) {
|
||||||
|
update_config();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
tx_view.on_edit_frequency = [this, &nav]() {
|
||||||
|
auto new_view = nav.push<FrequencyKeypadView>(transmitter_model.target_frequency());
|
||||||
|
new_view->on_changed = [this](rf::Frequency f) {
|
||||||
|
transmitter_model.set_target_frequency(f);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
tx_view.on_start = [this]() {
|
||||||
|
if (tx_acknowledged_) {
|
||||||
|
start_tx();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
nav_.display_modal(
|
||||||
|
"TX WARNING",
|
||||||
|
"VOR is an aviation nav\nband! TX only into a\ndummy load.",
|
||||||
|
YESNO,
|
||||||
|
[this](bool choice) {
|
||||||
|
if (choice) {
|
||||||
|
tx_acknowledged_ = true;
|
||||||
|
start_tx();
|
||||||
|
} else {
|
||||||
|
tx_view.set_transmitting(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
tx_view.on_stop = [this]() {
|
||||||
|
stop_tx();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
VorTxView::~VorTxView() {
|
||||||
|
transmitter_model.disable();
|
||||||
|
baseband::shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
void VorTxView::focus() {
|
||||||
|
tx_view.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
void VorTxView::update_config() {
|
||||||
|
baseband::set_vor_tx_config(static_cast<uint16_t>(radial_), ident_, ident_text_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void VorTxView::start_tx() {
|
||||||
|
transmitter_model.set_sampling_rate(1536000);
|
||||||
|
transmitter_model.set_baseband_bandwidth(1750000);
|
||||||
|
transmitter_model.enable();
|
||||||
|
update_config();
|
||||||
|
transmitting_ = true;
|
||||||
|
tx_view.set_transmitting(true);
|
||||||
|
text_status.set("TX radial " + to_string_dec_uint(radial_));
|
||||||
|
set_dirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
void VorTxView::stop_tx() {
|
||||||
|
transmitter_model.disable();
|
||||||
|
transmitting_ = false;
|
||||||
|
tx_view.set_transmitting(false);
|
||||||
|
text_status.set("Idle");
|
||||||
|
set_dirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ui::external_app::vor_tx
|
||||||
+113
@@ -0,0 +1,113 @@
|
|||||||
|
/*
|
||||||
|
* 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; if not, write to
|
||||||
|
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||||
|
* Boston, MA 02110-1301, USA.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef __UI_VOR_TX_H__
|
||||||
|
#define __UI_VOR_TX_H__
|
||||||
|
|
||||||
|
#include "app_settings.hpp"
|
||||||
|
#include "baseband_api.hpp"
|
||||||
|
#include "portapack.hpp"
|
||||||
|
#include "radio_state.hpp"
|
||||||
|
#include "ui.hpp"
|
||||||
|
#include "ui_navigation.hpp"
|
||||||
|
#include "ui_receiver.hpp"
|
||||||
|
#include "ui_transmitter.hpp"
|
||||||
|
|
||||||
|
namespace ui::external_app::vor_tx {
|
||||||
|
|
||||||
|
class VorTxView : public View {
|
||||||
|
public:
|
||||||
|
VorTxView(NavigationView& nav);
|
||||||
|
~VorTxView();
|
||||||
|
|
||||||
|
void focus() override;
|
||||||
|
|
||||||
|
std::string title() const override { return "VOR TX"; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
void start_tx();
|
||||||
|
void stop_tx();
|
||||||
|
void update_config();
|
||||||
|
|
||||||
|
NavigationView& nav_;
|
||||||
|
bool transmitting_{false};
|
||||||
|
bool tx_acknowledged_{false};
|
||||||
|
|
||||||
|
uint32_t radial_{0};
|
||||||
|
bool ident_{true};
|
||||||
|
std::string ident_text_{"VOR"};
|
||||||
|
|
||||||
|
TxRadioState radio_state_{
|
||||||
|
113'500'000 /* frequency */,
|
||||||
|
1750000 /* bandwidth */,
|
||||||
|
1536000 /* sampling rate */
|
||||||
|
};
|
||||||
|
|
||||||
|
app_settings::SettingsManager settings_{
|
||||||
|
"tx_vor",
|
||||||
|
app_settings::Mode::TX,
|
||||||
|
{
|
||||||
|
{"radial"sv, &radial_},
|
||||||
|
{"ident"sv, &ident_},
|
||||||
|
{"ident_text"sv, &ident_text_},
|
||||||
|
}};
|
||||||
|
|
||||||
|
Labels labels{
|
||||||
|
{{UI_POS_X(0), UI_POS_Y(1)}, "Radial:", Theme::getInstance()->fg_light->foreground},
|
||||||
|
{{UI_POS_X(0), UI_POS_Y(5)}, "Ident:", Theme::getInstance()->fg_light->foreground},
|
||||||
|
{{UI_POS_X(0), UI_POS_Y(8)}, "Status:", Theme::getInstance()->fg_light->foreground},
|
||||||
|
{{UI_POS_X(0), UI_POS_Y(10)}, "Lab use: set any freq &", Theme::getInstance()->fg_light->foreground},
|
||||||
|
{{UI_POS_X(0), UI_POS_Y(11)}, "TX into a dummy load.", Theme::getInstance()->fg_light->foreground}};
|
||||||
|
|
||||||
|
NumberField field_radial{
|
||||||
|
{UI_POS_X(8), UI_POS_Y(1)},
|
||||||
|
3,
|
||||||
|
{0, 359},
|
||||||
|
1,
|
||||||
|
'0'};
|
||||||
|
|
||||||
|
Text text_radial_unit{
|
||||||
|
{UI_POS_X(12), UI_POS_Y(1), UI_POS_WIDTH(4), UI_POS_HEIGHT(1)},
|
||||||
|
"deg"};
|
||||||
|
|
||||||
|
Checkbox check_ident{
|
||||||
|
{UI_POS_X(0), UI_POS_Y(3)},
|
||||||
|
13,
|
||||||
|
"CW identifier",
|
||||||
|
true};
|
||||||
|
|
||||||
|
Button button_ident_text{
|
||||||
|
{UI_POS_X(8), UI_POS_Y(5), UI_POS_WIDTH(10), UI_POS_HEIGHT(2)},
|
||||||
|
"VOR"};
|
||||||
|
|
||||||
|
Text text_status{
|
||||||
|
{UI_POS_X(8), UI_POS_Y(8), UI_POS_WIDTH_REMAINING(8), UI_POS_HEIGHT(1)},
|
||||||
|
"Idle"};
|
||||||
|
|
||||||
|
TransmitterView tx_view{
|
||||||
|
(int16_t)UI_POS_Y_BOTTOM(4),
|
||||||
|
8333,
|
||||||
|
1750000};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ui::external_app::vor_tx
|
||||||
|
|
||||||
|
#endif // __UI_VOR_TX_H__
|
||||||
@@ -516,6 +516,20 @@ DeclareTargets(PFUT flash_utility)
|
|||||||
set(add_to_firmware FALSE)
|
set(add_to_firmware FALSE)
|
||||||
set(MODE_FLAGS "-O3")
|
set(MODE_FLAGS "-O3")
|
||||||
|
|
||||||
|
### VOR TX
|
||||||
|
|
||||||
|
set(MODE_CPPSRC
|
||||||
|
proc_vor_tx.cpp
|
||||||
|
)
|
||||||
|
DeclareTargets(PVTX vor_tx)
|
||||||
|
|
||||||
|
### VOR RX
|
||||||
|
|
||||||
|
set(MODE_CPPSRC
|
||||||
|
proc_vor_rx.cpp
|
||||||
|
)
|
||||||
|
DeclareTargets(PVRX vor_rx)
|
||||||
|
|
||||||
### FLEX RX
|
### FLEX RX
|
||||||
|
|
||||||
set(MODE_CPPSRC
|
set(MODE_CPPSRC
|
||||||
|
|||||||
@@ -0,0 +1,272 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "proc_vor_rx.hpp"
|
||||||
|
|
||||||
|
#include "audio_output.hpp"
|
||||||
|
#include "audio_dma.hpp"
|
||||||
|
#include "dsp_iir_config.hpp"
|
||||||
|
#include "portapack_shared_memory.hpp"
|
||||||
|
|
||||||
|
#include "event_m4.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr float kPi = 3.14159265358979323846f;
|
||||||
|
}
|
||||||
|
|
||||||
|
VorRx::VorRx() {
|
||||||
|
channel_spectrum.set_decimation_factor(1);
|
||||||
|
|
||||||
|
baseband_thread.start();
|
||||||
|
rssi_thread.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
void VorRx::execute(const buffer_c8_t& buffer) {
|
||||||
|
if (!configured) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto decim_0_out = decim_0.execute(buffer, dst_buffer);
|
||||||
|
const auto decim_1_out = decim_1.execute(decim_0_out, dst_buffer);
|
||||||
|
|
||||||
|
channel_spectrum.feed(decim_1_out, channel_filter_low_f, channel_filter_high_f, channel_filter_transition);
|
||||||
|
|
||||||
|
// No decim_2 stage: the standard AM decim_2 filter is a ~4.5 kHz low-pass
|
||||||
|
// that would attenuate the 9960 Hz VOR subcarrier by ~60 dB and destroy the
|
||||||
|
// reference phase. The channel stays at 48 kHz so the subcarrier survives.
|
||||||
|
const auto channel_out = channel_filter.execute(decim_1_out, dst_buffer);
|
||||||
|
|
||||||
|
feed_channel_stats(channel_out);
|
||||||
|
|
||||||
|
auto audio = demod_am.execute(channel_out, audio_buffer);
|
||||||
|
if (vor_enabled) {
|
||||||
|
process_vor_metrics(audio);
|
||||||
|
// Strip the 9960 Hz subcarrier from the audible signal (the metrics
|
||||||
|
// above still ran on the unfiltered audio so the decode is unaffected).
|
||||||
|
audio_lpf_1.execute_in_place(audio);
|
||||||
|
audio_lpf_2.execute_in_place(audio);
|
||||||
|
}
|
||||||
|
audio_compressor.execute_in_place(audio);
|
||||||
|
audio_output.write(audio);
|
||||||
|
}
|
||||||
|
|
||||||
|
void VorRx::on_message(const Message* const message) {
|
||||||
|
switch (message->id) {
|
||||||
|
case Message::ID::UpdateSpectrum:
|
||||||
|
case Message::ID::SpectrumStreamingConfig:
|
||||||
|
channel_spectrum.on_message(message);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Message::ID::VorRxConfigure:
|
||||||
|
configure_vor(*reinterpret_cast<const VorRxConfigureMessage*>(message));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Message::ID::AMConfigure:
|
||||||
|
configure(*reinterpret_cast<const AMConfigureMessage*>(message));
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VorRx::configure(const AMConfigureMessage& message) {
|
||||||
|
decim_0.configure(message.decim_0_filter.taps);
|
||||||
|
decim_1.configure(message.decim_1_filter.taps);
|
||||||
|
channel_filter.configure(message.channel_filter.taps, channel_filter_decimation_factor);
|
||||||
|
|
||||||
|
constexpr size_t decim_0_output_fs = baseband_fs / decim_0.decimation_factor;
|
||||||
|
constexpr size_t decim_1_output_fs = decim_0_output_fs / decim_1.decimation_factor;
|
||||||
|
constexpr size_t channel_filter_input_fs = decim_1_output_fs;
|
||||||
|
|
||||||
|
channel_filter_low_f = message.channel_filter.low_frequency_normalized * channel_filter_input_fs;
|
||||||
|
channel_filter_high_f = message.channel_filter.high_frequency_normalized * channel_filter_input_fs;
|
||||||
|
channel_filter_transition = message.channel_filter.transition_normalized * channel_filter_input_fs;
|
||||||
|
|
||||||
|
channel_spectrum.set_decimation_factor(message.channel_spectrum_decimation_factor);
|
||||||
|
audio_output.configure(message.audio_hpf_lpf_config);
|
||||||
|
|
||||||
|
configured = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VorRx::PhaseOscillator::configure(float frequency_hz, float sample_rate_hz) {
|
||||||
|
const float step = 2.0f * kPi * frequency_hz / sample_rate_hz;
|
||||||
|
step_sin = sinf(step);
|
||||||
|
step_cos = cosf(step);
|
||||||
|
sin_v = 0.0f;
|
||||||
|
cos_v = 1.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VorRx::PhaseOscillator::advance() {
|
||||||
|
const float next_cos = (cos_v * step_cos) - (sin_v * step_sin);
|
||||||
|
const float next_sin = (sin_v * step_cos) + (cos_v * step_sin);
|
||||||
|
cos_v = next_cos;
|
||||||
|
sin_v = next_sin;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VorRx::PhaseOscillator::renormalize() {
|
||||||
|
// The recursive rotator slowly drifts in amplitude over long runs;
|
||||||
|
// rescale to the unit circle so accumulated tone levels stay meaningful.
|
||||||
|
const float mag = sqrtf((cos_v * cos_v) + (sin_v * sin_v));
|
||||||
|
if (mag > 0.0f) {
|
||||||
|
const float inv = 1.0f / mag;
|
||||||
|
cos_v *= inv;
|
||||||
|
sin_v *= inv;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VorRx::configure_vor(const VorRxConfigureMessage& message) {
|
||||||
|
vor_enabled = message.enabled;
|
||||||
|
vor_reference_osc.configure(vor_reference_hz, 48000.0f);
|
||||||
|
vor_subcarrier_osc.configure(vor_subcarrier_hz, 48000.0f);
|
||||||
|
audio_lpf_1.configure(audio_48k_lpf_3200hz_config);
|
||||||
|
audio_lpf_2.configure(audio_48k_lpf_3200hz_config);
|
||||||
|
vor_ref_i = 0.0f;
|
||||||
|
vor_ref_q = 0.0f;
|
||||||
|
vor_var_i = 0.0f;
|
||||||
|
vor_var_q = 0.0f;
|
||||||
|
vor_dc_sum = 0.0f;
|
||||||
|
vor_sub_lp_i = 0.0f;
|
||||||
|
vor_sub_lp_q = 0.0f;
|
||||||
|
vor_sub_prev_i = 0.0f;
|
||||||
|
vor_sub_prev_q = 0.0f;
|
||||||
|
vor_sample_count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t VorRx::normalize_degrees(float degrees) {
|
||||||
|
while (degrees < 0.0f) {
|
||||||
|
degrees += 360.0f;
|
||||||
|
}
|
||||||
|
while (degrees >= 360.0f) {
|
||||||
|
degrees -= 360.0f;
|
||||||
|
}
|
||||||
|
return static_cast<uint16_t>(degrees + 0.5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
void VorRx::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) {
|
||||||
|
const VorRxStatusDataMessage message{phase_deg, radial_deg, ref_level, var_level, quality, valid, to_from};
|
||||||
|
shared_memory.application_queue.push(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
void VorRx::process_vor_metrics(const buffer_f32_t& audio) {
|
||||||
|
for (size_t i = 0; i < audio.count; ++i) {
|
||||||
|
const float sample = audio.p[i];
|
||||||
|
const float ref_cos = vor_reference_osc.cosine();
|
||||||
|
const float ref_sin = vor_reference_osc.sine();
|
||||||
|
const float sub_cos = vor_subcarrier_osc.cosine();
|
||||||
|
const float sub_sin = vor_subcarrier_osc.sine();
|
||||||
|
|
||||||
|
// Variable signal: 30 Hz amplitude modulation of the carrier envelope.
|
||||||
|
// Correlate the envelope against a local 30 Hz tone (DC rejected because
|
||||||
|
// the window spans an exact integer number of 30 Hz cycles).
|
||||||
|
vor_dc_sum += sample;
|
||||||
|
vor_var_i += sample * ref_cos;
|
||||||
|
vor_var_q -= sample * ref_sin;
|
||||||
|
|
||||||
|
// Reference signal: 30 Hz FM on the 9960 Hz subcarrier. Quadrature
|
||||||
|
// down-convert the subcarrier, low-pass to its +/-480 Hz baseband, then
|
||||||
|
// FM-demodulate (phase difference between successive samples).
|
||||||
|
const float sc_i = sample * sub_cos;
|
||||||
|
const float sc_q = -sample * sub_sin;
|
||||||
|
vor_sub_lp_i += (1.0f - vor_sub_lp_alpha) * (sc_i - vor_sub_lp_i);
|
||||||
|
vor_sub_lp_q += (1.0f - vor_sub_lp_alpha) * (sc_q - vor_sub_lp_q);
|
||||||
|
|
||||||
|
const float num = (vor_sub_lp_q * vor_sub_prev_i) - (vor_sub_lp_i * vor_sub_prev_q);
|
||||||
|
const float den = (vor_sub_lp_i * vor_sub_prev_i) + (vor_sub_lp_q * vor_sub_prev_q);
|
||||||
|
const float fm = atan2f(num, den);
|
||||||
|
vor_sub_prev_i = vor_sub_lp_i;
|
||||||
|
vor_sub_prev_q = vor_sub_lp_q;
|
||||||
|
|
||||||
|
vor_ref_i += fm * ref_cos;
|
||||||
|
vor_ref_q -= fm * ref_sin;
|
||||||
|
|
||||||
|
vor_reference_osc.advance();
|
||||||
|
vor_subcarrier_osc.advance();
|
||||||
|
|
||||||
|
++vor_sample_count;
|
||||||
|
if (vor_sample_count < vor_window_samples) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float inv_n = 1.0f / static_cast<float>(vor_window_samples);
|
||||||
|
const float variable_phase = atan2f(vor_var_q, vor_var_i);
|
||||||
|
const float reference_phase = atan2f(vor_ref_q, vor_ref_i);
|
||||||
|
// Real VOR radials increase clockwise, i.e. the variable tone lags the
|
||||||
|
// reference by the bearing. The decoder therefore reports
|
||||||
|
// (reference - variable); the opposite order mirrors every bearing to
|
||||||
|
// 360 - true. Add back the receive-chain phase lag (see
|
||||||
|
// vor_ref_phase_lag_deg): with this subtraction order the delayed
|
||||||
|
// reference makes the raw radial read low, so the correction is added.
|
||||||
|
float phase_diff = ((reference_phase - variable_phase) * 180.0f / kPi) + vor_ref_phase_lag_deg;
|
||||||
|
while (phase_diff < 0.0f) {
|
||||||
|
phase_diff += 360.0f;
|
||||||
|
}
|
||||||
|
while (phase_diff >= 360.0f) {
|
||||||
|
phase_diff -= 360.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scale-invariant lock metrics (independent of RF/AGC level):
|
||||||
|
// - var_ratio: 30 Hz AM depth vs. carrier DC (~0.30 for a real VOR).
|
||||||
|
// - ref_amp: FM-demodulated 30 Hz amplitude (phase-only, so it does
|
||||||
|
// not depend on signal amplitude, ~0.065 when locked).
|
||||||
|
const float dc_level = vor_dc_sum * inv_n;
|
||||||
|
const float var_amp = 2.0f * sqrtf((vor_var_i * vor_var_i) + (vor_var_q * vor_var_q)) * inv_n;
|
||||||
|
const float ref_amp = 2.0f * sqrtf((vor_ref_i * vor_ref_i) + (vor_ref_q * vor_ref_q)) * inv_n;
|
||||||
|
const float var_ratio = (dc_level > 0.0f) ? (var_amp / dc_level) : 0.0f;
|
||||||
|
|
||||||
|
const auto phase_deg = normalize_degrees(phase_diff);
|
||||||
|
const auto radial_deg = phase_deg;
|
||||||
|
const auto ref_level = static_cast<uint16_t>(std::min(65535.0f, ref_amp * 10000.0f));
|
||||||
|
const auto var_level = static_cast<uint16_t>(std::min(65535.0f, var_ratio * 1000.0f));
|
||||||
|
const bool valid = (ref_amp > 0.03f) && (var_ratio > 0.15f);
|
||||||
|
// TO/FROM depends on the operator-selected OBS course, which only the
|
||||||
|
// application knows, so it is derived there (see VorRxView). Emit a
|
||||||
|
// stable placeholder here rather than a radial-only guess.
|
||||||
|
const bool to_from = false;
|
||||||
|
uint8_t quality = 0;
|
||||||
|
if (valid) {
|
||||||
|
const float q = (ref_amp / 0.065f) * 100.0f;
|
||||||
|
quality = static_cast<uint8_t>(std::min(100.0f, std::max(0.0f, q)));
|
||||||
|
}
|
||||||
|
|
||||||
|
send_vor_status(phase_deg, radial_deg, ref_level, var_level, quality, valid, to_from);
|
||||||
|
|
||||||
|
vor_reference_osc.renormalize();
|
||||||
|
vor_subcarrier_osc.renormalize();
|
||||||
|
vor_sample_count = 0;
|
||||||
|
vor_ref_i = 0.0f;
|
||||||
|
vor_ref_q = 0.0f;
|
||||||
|
vor_var_i = 0.0f;
|
||||||
|
vor_var_q = 0.0f;
|
||||||
|
vor_dc_sum = 0.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
audio::dma::init_audio_out();
|
||||||
|
|
||||||
|
EventDispatcher event_dispatcher{std::make_unique<VorRx>()};
|
||||||
|
event_dispatcher.run();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
/*
|
||||||
|
* 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__*/
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
/*
|
||||||
|
* 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; if not, write to
|
||||||
|
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||||
|
* Boston, MA 02110-1301, USA.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "proc_vor_tx.hpp"
|
||||||
|
|
||||||
|
#include "morse.hpp"
|
||||||
|
#include "sine_table_int8.hpp"
|
||||||
|
#include "event_m4.hpp"
|
||||||
|
|
||||||
|
void VorTxProcessor::execute(const buffer_c8_t& buffer) {
|
||||||
|
for (size_t i = 0; i < buffer.count; i++) {
|
||||||
|
if (!configured) {
|
||||||
|
buffer.p[i] = {0, 0};
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 30 Hz reference and variable tones (same frequency, offset by the radial).
|
||||||
|
const int32_t ref_sine = sine_table_i8[(phase_30 >> 24) & 0xFF];
|
||||||
|
const int32_t var_sine = sine_table_i8[((phase_30 - radial_offset) >> 24) & 0xFF];
|
||||||
|
phase_30 += delta_30;
|
||||||
|
|
||||||
|
// 9960 Hz subcarrier, FM-modulated +/- 480 Hz by the 30 Hz reference tone.
|
||||||
|
const int32_t sub_inc = static_cast<int32_t>(delta_sub) + ((ref_sine * static_cast<int32_t>(delta_480)) >> 7);
|
||||||
|
phase_sub += static_cast<uint32_t>(sub_inc);
|
||||||
|
const int32_t sub_sine = sine_table_i8[(phase_sub >> 24) & 0xFF];
|
||||||
|
|
||||||
|
// Optional 1020 Hz identification tone.
|
||||||
|
int32_t id_sine = 0;
|
||||||
|
if (ident_enabled && ident_tone_active()) {
|
||||||
|
id_sine = sine_table_i8[(phase_id >> 24) & 0xFF];
|
||||||
|
phase_id += delta_1020;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DSB-AM envelope (carrier kept at LO -> Q = 0).
|
||||||
|
int32_t env = carrier_level + (((var_sine * var_depth) + (sub_sine * sub_depth) + (id_sine * id_depth)) >> 7);
|
||||||
|
if (env < 0) env = 0;
|
||||||
|
if (env > 127) env = 127;
|
||||||
|
|
||||||
|
buffer.p[i] = {static_cast<int8_t>(env), 0};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VorTxProcessor::vor_tx_config(const VorTxConfigureMessage& message) {
|
||||||
|
// Real VOR radials increase clockwise: the variable tone lags the 30 Hz
|
||||||
|
// reference by the bearing, so the offset is subtracted (see the matching
|
||||||
|
// (reference - variable) decode in proc_vor_rx). A TX->RX loopback with this
|
||||||
|
// convention still decodes a transmitted radial of N degrees as N degrees,
|
||||||
|
// and it now matches real ground-station VOR bearings.
|
||||||
|
radial_offset = static_cast<uint32_t>(static_cast<uint64_t>(message.radial_deg % 360) * phase_period / 360);
|
||||||
|
ident_enabled = message.ident_enabled;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < sizeof(ident_text); ++i)
|
||||||
|
ident_text[i] = message.ident_text[i];
|
||||||
|
ident_text[sizeof(ident_text) - 1] = '\0';
|
||||||
|
build_ident_schedule();
|
||||||
|
|
||||||
|
configured = message.enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render the ident text into a repeating sequence of keyed-on / keyed-off
|
||||||
|
// sample runs using the shared ITU Morse table. Timing is derived from the
|
||||||
|
// standard "dot = 1200 / wpm" relation; letters are separated by a 3-unit gap,
|
||||||
|
// words by 7 units, and a trailing gap pads each cycle to a fixed 10 s period.
|
||||||
|
void VorTxProcessor::build_ident_schedule() {
|
||||||
|
ident_segment_count = 0;
|
||||||
|
ident_index = 0;
|
||||||
|
ident_sample_counter = 0;
|
||||||
|
|
||||||
|
const uint32_t dot_samples = static_cast<uint32_t>(1200ULL * baseband_fs / (ident_wpm * 1000ULL));
|
||||||
|
|
||||||
|
uint32_t total_samples = 0;
|
||||||
|
auto push_samples = [&](bool on, uint32_t samples) {
|
||||||
|
if (ident_segment_count >= max_ident_segments) return;
|
||||||
|
ident_segments[ident_segment_count++] = {on, samples};
|
||||||
|
total_samples += samples;
|
||||||
|
};
|
||||||
|
auto push = [&](bool on, uint32_t units) {
|
||||||
|
push_samples(on, units * dot_samples);
|
||||||
|
};
|
||||||
|
|
||||||
|
bool any = false;
|
||||||
|
for (size_t n = 0; n < sizeof(ident_text) && ident_text[n]; ++n) {
|
||||||
|
char ch = ident_text[n];
|
||||||
|
if (ch >= 'a' && ch <= 'z') ch -= 32;
|
||||||
|
|
||||||
|
uint16_t code = 0;
|
||||||
|
if (ch >= '!' && ch <= '_') code = morse::morse_ITU[ch - '!'];
|
||||||
|
|
||||||
|
if (!code) {
|
||||||
|
// Space or unsupported character: word gap between symbols.
|
||||||
|
if (any) push(false, MORSE_WORD_SPACE);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint16_t code_size = code & 7;
|
||||||
|
for (uint16_t c = 0; c < code_size; ++c) {
|
||||||
|
const bool dash = ((code << c) & 0x8000) != 0;
|
||||||
|
push(true, dash ? MORSE_DASH : MORSE_DOT);
|
||||||
|
if (c + 1 < code_size)
|
||||||
|
push(false, MORSE_SYMBOL_SPACE);
|
||||||
|
}
|
||||||
|
push(false, MORSE_LETTER_SPACE);
|
||||||
|
any = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!any) {
|
||||||
|
// No sendable characters: disable keying entirely.
|
||||||
|
ident_segment_count = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pad the trailing silence so ident-start to ident-start is a full 10 s.
|
||||||
|
// If the keyed sequence is somehow longer than the period, fall back to a
|
||||||
|
// one word-space gap so repeats never run back-to-back.
|
||||||
|
const uint32_t min_gap = MORSE_WORD_SPACE * dot_samples;
|
||||||
|
const uint32_t gap = (ident_period_samples > total_samples + min_gap)
|
||||||
|
? ident_period_samples - total_samples
|
||||||
|
: min_gap;
|
||||||
|
push_samples(false, gap);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advances the keying schedule by one sample and returns whether the ident
|
||||||
|
// tone should be sounding right now. Returns false when there is nothing to
|
||||||
|
// send so the caller leaves the ident tone off.
|
||||||
|
bool VorTxProcessor::ident_tone_active() {
|
||||||
|
if (ident_segment_count == 0) return false;
|
||||||
|
|
||||||
|
if (ident_sample_counter == 0)
|
||||||
|
ident_sample_counter = ident_segments[ident_index].length;
|
||||||
|
|
||||||
|
const bool on = ident_segments[ident_index].on;
|
||||||
|
|
||||||
|
if (ident_sample_counter == 0 || --ident_sample_counter == 0) {
|
||||||
|
++ident_index;
|
||||||
|
if (ident_index >= ident_segment_count) ident_index = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return on;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VorTxProcessor::on_message(const Message* const msg) {
|
||||||
|
if (msg->id == Message::ID::VorTxConfigure) {
|
||||||
|
vor_tx_config(*reinterpret_cast<const VorTxConfigureMessage*>(msg));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
EventDispatcher event_dispatcher{std::make_unique<VorTxProcessor>()};
|
||||||
|
event_dispatcher.run();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
/*
|
||||||
|
* 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; if not, write to
|
||||||
|
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||||
|
* Boston, MA 02110-1301, USA.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef __PROC_VOR_TX_H__
|
||||||
|
#define __PROC_VOR_TX_H__
|
||||||
|
|
||||||
|
#include "baseband_processor.hpp"
|
||||||
|
#include "baseband_thread.hpp"
|
||||||
|
|
||||||
|
// Generates a conventional VOR composite signal as DSB-AM (carrier at LO, Q = 0):
|
||||||
|
// - 30 Hz "variable" tone, amplitude-modulated directly (phase = transmitted radial)
|
||||||
|
// - 9960 Hz subcarrier, FM-modulated (+/- 480 Hz) by a 30 Hz "reference" tone
|
||||||
|
// - optional 1020 Hz identification tone
|
||||||
|
// The radial encoded is the phase difference between the variable and reference 30 Hz tones.
|
||||||
|
class VorTxProcessor : public BasebandProcessor {
|
||||||
|
public:
|
||||||
|
void execute(const buffer_c8_t& buffer) override;
|
||||||
|
void on_message(const Message* const msg) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
static constexpr size_t baseband_fs = 1536000;
|
||||||
|
static constexpr uint64_t phase_period = 1ULL << 32;
|
||||||
|
// Per-sample phase increments (top byte indexes the 256-entry sine table).
|
||||||
|
static constexpr uint32_t delta_30 = static_cast<uint32_t>(30ULL * phase_period / baseband_fs);
|
||||||
|
static constexpr uint32_t delta_sub = static_cast<uint32_t>(9960ULL * phase_period / baseband_fs);
|
||||||
|
static constexpr uint32_t delta_1020 = static_cast<uint32_t>(1020ULL * phase_period / baseband_fs);
|
||||||
|
static constexpr uint32_t delta_480 = static_cast<uint32_t>(480ULL * phase_period / baseband_fs);
|
||||||
|
|
||||||
|
// AM levels (int8 scale). carrier_level + sum of contributions must stay <= 127.
|
||||||
|
static constexpr int32_t carrier_level = 60;
|
||||||
|
static constexpr int32_t var_depth = 18; // ~30% of carrier
|
||||||
|
static constexpr int32_t sub_depth = 18; // ~30% of carrier
|
||||||
|
static constexpr int32_t id_depth = 6; // ~10% of carrier
|
||||||
|
|
||||||
|
// CW identifier keying. Real VOR stations key the 1020 Hz ident tone in
|
||||||
|
// Morse at ~7 words per minute, so the schedule below is expressed in
|
||||||
|
// "dot" time units and rendered to on/off sample runs at config time.
|
||||||
|
static constexpr uint32_t ident_wpm = 7;
|
||||||
|
static constexpr size_t max_ident_segments = 128;
|
||||||
|
|
||||||
|
// VOR ident is transmitted at least every 10 s; repeat the keyed identifier
|
||||||
|
// on a fixed 10 s cycle (ident start to next ident start).
|
||||||
|
static constexpr uint32_t ident_period_samples = 10 * baseband_fs;
|
||||||
|
|
||||||
|
struct IdentSegment {
|
||||||
|
bool on;
|
||||||
|
uint32_t length; // duration in samples
|
||||||
|
};
|
||||||
|
|
||||||
|
uint32_t phase_30{0};
|
||||||
|
uint32_t phase_sub{0};
|
||||||
|
uint32_t phase_id{0};
|
||||||
|
uint32_t radial_offset{0};
|
||||||
|
|
||||||
|
bool ident_enabled{true};
|
||||||
|
bool configured{false};
|
||||||
|
|
||||||
|
char ident_text[8]{};
|
||||||
|
IdentSegment ident_segments[max_ident_segments]{};
|
||||||
|
size_t ident_segment_count{0};
|
||||||
|
size_t ident_index{0};
|
||||||
|
uint32_t ident_sample_counter{0};
|
||||||
|
|
||||||
|
void vor_tx_config(const VorTxConfigureMessage& message);
|
||||||
|
void build_ident_schedule();
|
||||||
|
bool ident_tone_active();
|
||||||
|
|
||||||
|
/* NB: Threads should be the last members in the class definition. */
|
||||||
|
BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Transmit};
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif /* __PROC_VOR_TX_H__ */
|
||||||
@@ -119,4 +119,12 @@ constexpr iir_biquad_config_t apt_audio_12k_lpf_2000hz_config{
|
|||||||
{0.15505103f, 0.31010205f, 0.15505103f},
|
{0.15505103f, 0.31010205f, 0.15505103f},
|
||||||
{1.00000000f, -0.6202041f, 0.24040821f}};
|
{1.00000000f, -0.6202041f, 0.24040821f}};
|
||||||
|
|
||||||
|
// scipy.signal.butter(2, 3200 / 24000.0, 'low', analog=False)
|
||||||
|
// VOR RX audio low-pass. Removes the 9960 Hz FM subcarrier (the annoying
|
||||||
|
// high-pitch whine) while passing voice and the 1020 Hz ident tone. Cascade
|
||||||
|
// two of these sections for a 4th-order roll-off (~44 dB at 9960 Hz).
|
||||||
|
constexpr iir_biquad_config_t audio_48k_lpf_3200hz_config{
|
||||||
|
{0.03357181f, 0.06714362f, 0.03357181f},
|
||||||
|
{1.00000000f, -1.41898265f, 0.55326989f}};
|
||||||
|
|
||||||
#endif /*__DSP_IIR_CONFIG_H__*/
|
#endif /*__DSP_IIR_CONFIG_H__*/
|
||||||
|
|||||||
@@ -162,6 +162,9 @@ class Message {
|
|||||||
ToneDetectConfig = 104,
|
ToneDetectConfig = 104,
|
||||||
FlexTosend = 105,
|
FlexTosend = 105,
|
||||||
EPIRBRXConfig = 106,
|
EPIRBRXConfig = 106,
|
||||||
|
VorRxConfigure = 107,
|
||||||
|
VorRxStatusData = 108,
|
||||||
|
VorTxConfigure = 109,
|
||||||
MAX
|
MAX
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1899,6 +1902,68 @@ class ToneDetectConfigureMessage : public Message {
|
|||||||
uint32_t ctcss_freq_x10{0}; // CTCSS frequency × 10 (e.g. 1000 = 100.0 Hz); 0 = None
|
uint32_t ctcss_freq_x10{0}; // CTCSS frequency × 10 (e.g. 1000 = 100.0 Hz); 0 = None
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class VorRxConfigureMessage : public Message {
|
||||||
|
public:
|
||||||
|
constexpr VorRxConfigureMessage(bool enabled = true)
|
||||||
|
: Message{ID::VorRxConfigure},
|
||||||
|
enabled{enabled} {
|
||||||
|
}
|
||||||
|
|
||||||
|
bool enabled{true};
|
||||||
|
};
|
||||||
|
|
||||||
|
class VorRxStatusDataMessage : public Message {
|
||||||
|
public:
|
||||||
|
constexpr VorRxStatusDataMessage(
|
||||||
|
uint16_t phase_deg = 0,
|
||||||
|
uint16_t radial_deg = 0,
|
||||||
|
uint16_t reference_level = 0,
|
||||||
|
uint16_t variable_level = 0,
|
||||||
|
uint8_t quality = 0,
|
||||||
|
bool valid = false,
|
||||||
|
bool to_from = false)
|
||||||
|
: Message{ID::VorRxStatusData},
|
||||||
|
phase_deg{phase_deg},
|
||||||
|
radial_deg{radial_deg},
|
||||||
|
reference_level{reference_level},
|
||||||
|
variable_level{variable_level},
|
||||||
|
quality{quality},
|
||||||
|
valid{valid},
|
||||||
|
to_from{to_from} {
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t phase_deg{0};
|
||||||
|
uint16_t radial_deg{0};
|
||||||
|
uint16_t reference_level{0};
|
||||||
|
uint16_t variable_level{0};
|
||||||
|
uint8_t quality{0};
|
||||||
|
bool valid{false};
|
||||||
|
bool to_from{false};
|
||||||
|
};
|
||||||
|
|
||||||
|
class VorTxConfigureMessage : public Message {
|
||||||
|
public:
|
||||||
|
VorTxConfigureMessage(
|
||||||
|
uint16_t radial_deg = 0,
|
||||||
|
bool ident_enabled = true,
|
||||||
|
const char* ident = "",
|
||||||
|
bool enabled = true)
|
||||||
|
: Message{ID::VorTxConfigure},
|
||||||
|
radial_deg{radial_deg},
|
||||||
|
ident_enabled{ident_enabled},
|
||||||
|
enabled{enabled} {
|
||||||
|
size_t i = 0;
|
||||||
|
for (; ident && ident[i] && i < sizeof(ident_text) - 1; ++i)
|
||||||
|
ident_text[i] = ident[i];
|
||||||
|
ident_text[i] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t radial_deg{0};
|
||||||
|
bool ident_enabled{true};
|
||||||
|
bool enabled{true};
|
||||||
|
char ident_text[8]{}; // CW identifier, null-terminated (max 7 chars)
|
||||||
|
};
|
||||||
|
|
||||||
class FlexTosendMessage : public Message {
|
class FlexTosendMessage : public Message {
|
||||||
public:
|
public:
|
||||||
constexpr FlexTosendMessage(
|
constexpr FlexTosendMessage(
|
||||||
|
|||||||
@@ -129,6 +129,8 @@ constexpr image_tag_t image_tag_noaaapt_rx{'P', 'N', 'O', 'A'};
|
|||||||
constexpr image_tag_t image_tag_sstv_rx{'P', 'S', 'R', 'X'};
|
constexpr image_tag_t image_tag_sstv_rx{'P', 'S', 'R', 'X'};
|
||||||
constexpr image_tag_t image_tag_morse{'P', 'M', 'R', 'S'};
|
constexpr image_tag_t image_tag_morse{'P', 'M', 'R', 'S'};
|
||||||
constexpr image_tag_t image_tag_morsetx{'P', 'M', 'R', 'T'};
|
constexpr image_tag_t image_tag_morsetx{'P', 'M', 'R', 'T'};
|
||||||
|
constexpr image_tag_t image_tag_vor_tx{'P', 'V', 'T', 'X'};
|
||||||
|
constexpr image_tag_t image_tag_vor_rx{'P', 'V', 'R', 'X'};
|
||||||
constexpr image_tag_t image_tag_rttyrx{'P', 'R', 'T', 'R'};
|
constexpr image_tag_t image_tag_rttyrx{'P', 'R', 'T', 'R'};
|
||||||
constexpr image_tag_t image_tag_rttytx{'P', 'R', 'T', 'T'};
|
constexpr image_tag_t image_tag_rttytx{'P', 'R', 'T', 'T'};
|
||||||
constexpr image_tag_t image_tag_tonedetect{'P', 'T', 'N', 'E'};
|
constexpr image_tag_t image_tag_tonedetect{'P', 'T', 'N', 'E'};
|
||||||
|
|||||||
@@ -24,4 +24,4 @@
|
|||||||
# external app address ranges below must match those in linker file "external.ld"
|
# external app address ranges below must match those in linker file "external.ld"
|
||||||
maximum_application_size = 32*1024
|
maximum_application_size = 32*1024
|
||||||
external_apps_address_start = 0xADB00000
|
external_apps_address_start = 0xADB00000
|
||||||
external_apps_address_end = 0xAE090000
|
external_apps_address_end = 0xAE0B0000
|
||||||
|
|||||||
Reference in New Issue
Block a user