mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2026-09-02 12:59:03 +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:
+10
@@ -133,6 +133,14 @@ set(EXTCPPSRC
|
||||
external/noaaapt_rx/main.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
|
||||
external/shoppingcart_lock/main.cpp
|
||||
external/shoppingcart_lock/shoppingcart_lock.cpp
|
||||
@@ -401,6 +409,8 @@ set(EXTAPPLIST
|
||||
acars_rx
|
||||
wefax_rx
|
||||
noaaapt_rx
|
||||
vor_rx
|
||||
vor_tx
|
||||
shoppingcart_lock
|
||||
ookbrute
|
||||
ook_editor
|
||||
|
||||
+14
@@ -111,6 +111,8 @@ MEMORY
|
||||
ram_external_app_flex_tx (rwx) : org = 0xAE060000, len = 32k
|
||||
ram_external_app_hard_reset (rwx) : org = 0xAE070000, 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
|
||||
@@ -391,6 +393,18 @@ SECTIONS
|
||||
*(*ui*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)
|
||||
{
|
||||
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__
|
||||
Reference in New Issue
Block a user