Compare commits

...

33 Commits

Author SHA1 Message Date
Copilot 367eaf54c0 Fix GeoPos DMS edge carry rollback at clamped degree limits (#3284)
* Initial plan

* Fix geomap DMS wrap carry at clamped bounds

Co-authored-by: gullradriel <3157857+gullradriel@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: gullradriel <3157857+gullradriel@users.noreply.github.com>
2026-08-08 00:40:35 +02:00
qwer123 de30cbfe1b fix ADSB RX weak signal on pro (#3282) 2026-08-05 10:09:55 +02:00
未来方舟 a4d91f12ef Fix waterfall designer crash and also some linker script fixes (#3281)
* init

* _
2026-08-01 12:57:45 +02:00
gullradriel c4f32ac436 update submodule (#3277) 2026-07-25 18:54:33 +02:00
Frederic BORRY efe214167d Added sn0ren's SPECTRUM BMP files (#3276) 2026-07-24 18:37:55 +08:00
Copilot 483a100100 Fix ADSB altitude integer overflow and add on-ground indicator (#3275)
* Initial plan

* Fix ADSB altitude integer overflow and add on-ground indicator

- Fix display overflow: use to_string_dec_int (signed) instead of
  to_string_dec_uint (unsigned) for altitude display - per ICAO Annex 10
  Vol IV 3.1.2.6.5.4, altitude = 25N-1000 can be as low as -1000ft
- Remove incorrect altitude clamp (altitude < 0 => 0) in DF 0/4/20 decoder
- Add on_ground flag to AircraftRecentEntry; set from TC 5-8 (surface
  position) messages, cleared on TC 9-22 (airborne position) messages
- Display 'GND' in altitude column when aircraft is on ground
- Clear altitude to 0 when on_ground is set to prevent stale data in map
  color calculation

Closes #3274

* Revert on-ground indicator; keep only the overflow and clamp fixes

Remove on_ground field, TC 5-8 handling, and "GND" display per review
feedback. Only keep the two minimal bug fixes:
- Signed altitude display (to_string_dec_int vs to_string_dec_uint)
- Remove erroneous altitude < 0 clamp; update comment to show the math
2026-07-23 01:16:21 +02:00
Pezsma 8561ff32b3 Gpio modify v4 (#3266)
* Refactor GPIO handling for RFFC5072: update pin mappings and simplify initialization

* Refactor GPIO handling: replace portapack::gpio_dfu with dfu_button and clean up unused GPIOs

* Refactor MAX283x GPIO handling: update pin mappings and simplify initialization

* Refactor GPIO handling: update MAX2831 shutdown pin configuration

* Refactor GPIO handling: add MAX2831 RXHP and RXTX control, update related configurations

* Refactor MAX2831 frequency setting to include reference divider parameter and improve validation checks

* Refactor MAX2831 frequency setting to remove reference divider parameter and improve frequency validation

* copilot
2026-07-10 21:37:41 +02:00
gullradriel 87eb8e5863 update submodule (#3267) 2026-07-09 23:11:39 +02:00
Frederic BORRY d11669f711 EPIRB TX SGB support (#3263)
* Update BEACONS.TXT
Added test beacon with 3 bch1 errors and 2 bch2 errors to test multiple bit error correction.
* Fist step to SGB format
* Added SGB to manual mode.
* SGB support
- Added SGB frame to beacons file
- Fixed hex display for SGB
- Fixed frame type option display
- Fixed self-test mode activation logic
- Fixed PRN for self-test mode
2026-07-09 10:36:26 +02:00
Totoo 7513cd46fc Pause on the looking glass (#3261) 2026-07-09 09:43:45 +02:00
Totoo d671455f79 Added tetra rx ext app (#3257) 2026-07-09 09:10:08 +02:00
Totoo 42122a739c remove const temp measurement from core to the ext app. we don't need it (#3259) 2026-07-08 10:09:53 +08:00
未来方舟 03fe508aae fix looking glass frequency cast error (#3253)
* fix looking glass frequency cast error

* add useless protecter back
2026-07-08 10:08:24 +08:00
未来方舟 e3eafbeb66 Add benchmark function for mbt (#3255) 2026-07-07 11:32:00 +02:00
未来方舟 e3b3fa2fc8 fine tune app search ui (#3256) 2026-07-07 11:06:31 +02:00
未来方舟 5f0ae22f66 remove my dumb nicknames in files (#3254) 2026-07-07 11:04:34 +02:00
Totoo f9dd28f31d fix dfu button (#3252) 2026-07-07 00:31:44 +02:00
Matej Sochan b99cec5111 Feature/signal hunter (#3245) 2026-07-06 11:05:49 +08:00
Pezsma fe2ba80a10 refactor: simplify audio volume handling in AudioVolumeField (#3246) 2026-07-04 08:53:28 +02:00
Luca b5d84fcc4d Airband VOR receiver and trasmitter (#3244)
* First test

* Reorder includes for consistency and fix string dereference in VorCdiIndicator

* Add memory section and linker script entry for vor_rx application

* Testing VOR TX

* Moving VOR TX to SD

* Code refactoring to align with guidelines

* Add TODO comment to verify radial sign convention in vor_tx_config

* Remove unnecessary set_dirty() calls in VorRxView methods

* Update Course Deviation Indicator label and adjust UI element positions

* Update VorTxView to run prepared image from memory map

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

* Reverting action changes

* Adjust Course Deviation Indicator layout and refine painting logic

* Refactor VorRxView UI elements for improved layout and clarity

* Add calibration field and update radial calculation logic in VorRxView

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

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

* Setting comments inline

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

* Update external app address end to 0xAE0B0000

* Update set_vor_tx_config to include enabled parameter for consistency

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

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

* Refactor VorRxView labels and status messages

* Update VorRxView calibration field range to allow full circle input

* Completing Ident transmission logic

* Fixing TX gain and moving log label

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

* Add low-pass filters to strip 9960 Hz subcarrier from audio output
2026-07-04 08:49:21 +02:00
Sandbox 7481aefe9f customizable name attack option "CUSTOM" for the BLESPAM App (#3239) 2026-07-02 16:14:17 +02:00
未来方舟 6b641c8a5d You can search app now (#3241) 2026-07-02 10:19:53 +02:00
Pezsma 2ad34bbc09 Gpio modify v3 (#3238)
* Refactor GPIO mappings and definitions for improved clarity and functionality

- Updated SCU_ARRAY_SIZE definitions in pal_lld.h for PRALINE and non-PRALINE configurations.
- Modified pin mappings in gpio.hpp to reflect new hardware configurations, including additional control pins for RF and audio components.
- Removed obsolete GPIO definitions from hackrf_gpio.hpp and streamlined the code for better maintainability.
- Added new GPIO definitions for mix bypass, amplifier control, and other RF-related functionalities to enhance the system's capabilities.

* Refactor GPIO mappings and update SCU_ARRAY_SIZE for LPC43xx platform

- Updated SCU_ARRAY_SIZE to 80 for PRALINE configuration and 58 for others.
- Added new GPIO mappings for auxiliary power control, antenna bias, and R9 clock enable signals in gpio.hpp.
- Removed commented-out PPS output mapping in hackrf_gpio.hpp.
- Adjusted GPIO definitions for R9 clock signals to ensure proper functionality.

* Refactor RF path initialization and configuration for improved clarity and functionality

* Refactor RF path configuration and GPIO mappings for improved clarity and functionality

* Potential fix for pull request finding

* comment

* aux power polarrity

* Fix formatting of pin_aux_power_enable declaration
2026-07-02 10:05:34 +02:00
Totoo 9c74a1f392 fix mbt build on rare errors (#3242) 2026-07-01 14:42:25 +02:00
gullradriel abadf35e06 Lna investigations and fix (#3240)
* The LNA gain field 'L' lives in LPF_VGA_2 (reg 6), not RXRF_2 (reg 2). RXRF_2 only holds LNAgain_SPI_EN. The configure_rx_gain() function wrote 'L' into lpf_vga_2 but marked RXRF_2 dirty => the LNA value only got flushed incidentally because the following VGA write marks LPF_VGA_2 dirty. Drop the stray RXRF_2 dirty flag and rely on the LPF_VGA_2 flag, which carries both L and VGA.

* The baseband build defined IS_H1_R9 as the compile-time constant 0, on the (now incorrect) assumption that detected_platform() isn't linked. The sd_over_usb image is the only baseband target that compiles the IS_H1_R9-gated upstream files (rf_path.c, si5351c.c, sgpio.c, platform_gpio.c, platform_scu.c), and it *does* link platform_detect.c and call detect_hardware_platform() at startup. Forcing IS_H1_R9=0 dead-stripped every 'if (IS_H1_R9)' branch, so a real HackRF One r9 was set up with the OG clock/GPIO/RF-path config in that image.
Define IS_H1_R9 as the runtime '(detected_platform()==BOARD_ID_HACKRF1_R9)', mirroring upstream platform-detect.cmake for HACKRF_ONE. All usages are '#ifdef IS_H1_R9' + 'if (IS_H1_R9)' (no arithmetic '#if'), so a function-call expression is safe, single-quoted -D with no internal spaces keeps it intact through the shell-split DDEFS blob.

---------

Co-authored-by: gullradriel <gullradriel@users.noreply.github.com>
2026-07-01 08:47:07 +02:00
Totoo 288e87537a MAX 17055 sleep bug fixes. 2026-06-25 09:08:44 +02:00
gullradriel d25899de10 Upgrade subm plus warning fix (#3235) 2026-06-25 08:46:36 +02:00
Totoo 8a16ec829c fix clock source change on praline (#3233)
* fix clock source change on praline

* whoops
2026-06-22 16:50:16 +02:00
Totoo f6d6e3be4c Fix battery display bug, and the flash time (#3232)
* battfix

* por again, but nicer
2026-06-22 16:20:49 +02:00
Pezsma c16bac24c7 Add RSSI input configuration for VGA output measurement (#3231) 2026-06-21 11:17:09 +02:00
Payalo64 4e6a2cc934 Add 3.3GHz Analog FPV Channels to FPV_ANALOG.TXT (#3230) 2026-06-20 18:28:02 +02:00
Pezsma cafe62564e Gpio modify (#3229)
* Refactor GPIO configuration for PRALINE and non-PRALINE setups

- Updated PinMap structure to include gpio_mode for better flexibility.
- Added new GPIO mappings for SGPIO pins with appropriate configurations.
- Commented out unused GPIO definitions in hackrf_gpio.hpp to improve code clarity.
- Adjusted GPIO initialization for control pins to utilize the new PinMap structure.
- Ensured compatibility for both PRALINE and non-PRALINE configurations by using preprocessor directives.

* Refactor GPIO handling and remove LED abstraction

- Updated GPIO class to support logical polarity, enabling/disabling features based on their configured state.
- Replaced direct GPIO manipulation in power control functions with new GPIO methods for better readability and maintainability.
- Removed the LED class and its associated functionality, as it was deemed unnecessary for the current implementation.
- Adjusted GPIO initialization for various components, ensuring correct polarity settings for VAA and power enable pins.
- Cleaned up unused includes and commented-out code in hackrf_gpio.hpp.

* Refactor GPIO LED control methods to use setActive() and setInactive() for improved clarity

* Refactor GPIO control methods to use setActive() and setInactive() for improved clarity and consistency

* copilot

* Update GPIO control logic and pin definitions for clarity and consistency

* Refactor GPIO methods for improved naming consistency and clarity
2026-06-19 07:48:55 +02:00
gullradriel 960e5b8329 Upgrade subm plus warning fix (#3227)
* fix compilation warning
* update submodule
2026-06-17 11:34:00 +02:00
155 changed files with 8122 additions and 1918 deletions
+1
View File
@@ -83,3 +83,4 @@ venv/
# 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
/firmware/tools/bitmap.hpp
tmp/*
+6 -2
View File
@@ -239,7 +239,6 @@ set(CPPSRC
serializer.cpp
spectrum_color_lut.cpp
string_format.cpp
temperature_logger.cpp
theme.cpp
touch.cpp
tone_key.cpp
@@ -494,7 +493,10 @@ add_executable(${PROJECT_NAME}.elf ${CSRC} ${CPPSRC} ${ASMSRC})
target_link_options(${PROJECT_NAME}.elf PRIVATE
"LINKER:--defsym,LD_FLASH_SIZE=${FLASH_BYTES_LIMIT_SIZE}"
)
set_target_properties(${PROJECT_NAME}.elf PROPERTIES LINK_DEPENDS ${LDSCRIPT})
# NB: LPC43xx_M0.ld does INCLUDE external.ld, so that one has to be listed too -
# otherwise editing the external app section rules silently does not relink.
set_target_properties(${PROJECT_NAME}.elf PROPERTIES
LINK_DEPENDS "${LDSCRIPT};${CMAKE_CURRENT_SOURCE_DIR}/external/external.ld")
add_definitions(${DEFS})
include_directories(. ${INCDIR})
link_directories(${LLIBDIR})
@@ -513,3 +515,5 @@ add_custom_target(
${PROJECT_NAME}
DEPENDS ${PROJECT_NAME}.bin
)
add_dependencies(${PROJECT_NAME} baseband)
+14 -3
View File
@@ -75,7 +75,7 @@ void RecentEntriesTable<AircraftRecentEntries>::draw(
uint8_t firstcolwidth = columns.at(0).second;
ipc.resize(firstcolwidth, ' '); // Make sure this is always match the first column's width that is dynamic.
entry_string += ipc + to_string_dec_uint((unsigned int)(entry.pos.altitude / 100), 4);
entry_string += ipc + to_string_dec_int((int32_t)(entry.pos.altitude / 100), 4);
if (entry.velo.type == SPD_IAS && entry.pos.alt_valid) { // IAS can be converted to TAS
// It is generally accepted that for every thousand feet of altitude,
@@ -532,6 +532,18 @@ ADSBRxView::ADSBRxView(NavigationView& nav) {
logger = std::make_unique<ADSBLogger>();
logger->append(logs_dir / u"ADSB.TXT");
/* First run only: start from the configuration that is known to receive
* ADS-B on this hardware -- LNA 32, VGA 32, RF amp ON. The first two are
* already ReceiverModel's defaults; the amp is not, and running without it
* costs about 14 dB, which is the difference between a busy list and an
* empty one. Once the user has saved settings for this app their choice
* wins, so this only sets the starting point.
* Going through the field rather than receiver_model keeps the displayed
* value in step: RFAmpField snapshots rf_amp() in its own constructor,
* which has already run by the time we get here. */
if (!settings_.loaded())
field_rf_amp.set_value(1);
receiver_model.enable();
baseband::set_adsb();
@@ -668,9 +680,8 @@ void ADSBRxView::on_frame(const ADSBFrameMessage* message) {
(raw_data[3] & 15);
// The final altitude is due to the resulting number multiplied by 25, minus 1000.
// altitude = 25N - 1000 (ft); minimum is -1000 ft when N=0 (Q=1, M=0 per ICAO Annex 10).
altitude = 25 * n - 1000;
if (altitude < 0)
altitude = 0;
} // else N is an 11 bit Gillham coded altitude
}
+2 -2
View File
@@ -3176,9 +3176,9 @@ void RFFC5072StatusView::refresh_status() {
// Blink LED
/*for (int i = 0; i < 3; i++) {
hackrf::one::led_rx.on();
hackrf::one::led_rx.setActive();
chThdSleepMilliseconds(100);
hackrf::one::led_rx.off();
hackrf::one::led_rx.setInactive();
chThdSleepMilliseconds(100);
}*/
} else {
@@ -168,7 +168,7 @@ bool FlashUtilityView::flash_firmware(std::filesystem::path::string_type path) {
{0, 0, portapack::display.width(), portapack::display.height()},
Theme::getInstance()->bg_darkest->background);
painter.draw_string({12, 24}, this->nav_.style(), "This will take 15 seconds.");
painter.draw_string({12, 24}, this->nav_.style(), "This will take 15-45 sec.");
painter.draw_string({12, 64}, this->nav_.style(), "Please wait while LED RX");
painter.draw_string({12, 84}, this->nav_.style(), "is on and TX is flashing.");
painter.draw_string({12, 124}, this->nav_.style(), "Device will then restart.");
@@ -328,8 +328,8 @@ void GlassView::update_min(int32_t v) {
min_size = search_span;
if (min_size < 1)
min_size = 1;
if (v > 7200 - min_size) {
v = 7200 - min_size;
if (v > LOOKING_GLASS_MAX_FREQ_MHZ - min_size) {
v = LOOKING_GLASS_MAX_FREQ_MHZ - min_size;
}
if (v > (field_frequency_max.value() - min_size))
field_frequency_max.set_value(v + min_size, false);
@@ -348,6 +348,9 @@ void GlassView::update_max(int32_t v) {
if (v < min_size) {
v = min_size;
}
if (v > LOOKING_GLASS_MAX_FREQ_MHZ) {
v = LOOKING_GLASS_MAX_FREQ_MHZ;
}
if (v < (field_frequency_min.value() + min_size))
field_frequency_min.set_value(v - min_size, false);
if (locked_range)
@@ -398,7 +401,7 @@ GlassView::GlassView(
&button_jump,
&button_rst,
&button_rssi,
&freq_stats});
&freq_stats, &button_play});
load_presets(); // Load available presets from TXT files (or default).
preset_index = clip<uint8_t>(preset_index, 0, presets_db.size());
@@ -554,6 +557,21 @@ GlassView::GlassView(
reset_live_view();
};
button_play.on_select = [this](ImageButton&) {
paused = !paused;
if (paused) {
button_play.set_foreground(Theme::getInstance()->fg_red->foreground);
button_play.set_background(Theme::getInstance()->fg_red->background);
baseband::spectrum_streaming_stop();
button_play.set_bitmap(&bitmap_play);
} else {
button_play.set_foreground(Theme::getInstance()->fg_green->foreground);
button_play.set_background(Theme::getInstance()->fg_green->background);
baseband::spectrum_streaming_start();
button_play.set_bitmap(&bitmap_stop);
}
};
display.scroll_set_area(109, screen_height - 1); // Restart scroll on the correct coordinates
// trigger:
@@ -630,7 +648,7 @@ void GlassView::load_presets() {
parse_int(cols[1], entry.max);
entry.label = trimr(cols[2]);
if (entry.min == 0 || entry.max == 0 || entry.min >= entry.max)
if (entry.max == 0 || entry.min >= entry.max)
continue; // Invalid line.
presets_db.emplace_back(std::move(entry));
@@ -42,6 +42,7 @@ namespace ui {
#define LOOKING_GLASS_SLICE_WIDTH_MAX 20000000
#define LOOKING_GLASS_MAX_SAMPLERATE 20000000
#define MHZ_DIV 1000000
#define LOOKING_GLASS_MAX_FREQ_MHZ 7250 // HackRF upper tuning limit (band_high top), in MHz
// blanked DC (16 centered bins ignored ) and top left and right (2 bins ignored on each side )
#define LOOKING_GLASS_FASTSCAN 0
@@ -129,6 +130,8 @@ class GlassView : public View {
void populate_presets();
void launch_audio(rf::Frequency center_freq);
bool paused{false};
rf::Frequency search_span{0};
rf::Frequency f_center{0};
rf::Frequency f_center_ini{0};
@@ -179,14 +182,14 @@ class GlassView : public View {
NumberField field_frequency_min{
{UI_POS_X(4), UI_POS_Y(0)},
4,
{0, 7199},
{0, LOOKING_GLASS_MAX_FREQ_MHZ - 1},
1, // number of steps by encoder delta
' '};
NumberField field_frequency_max{
{UI_POS_X(13), UI_POS_Y(0)},
4,
{1, 7200},
{1, LOOKING_GLASS_MAX_FREQ_MHZ},
1, // number of steps by encoder delta
' '};
@@ -221,6 +224,12 @@ class GlassView : public View {
{UI_POS_X_RIGHT(8), UI_POS_Y(2.25), UI_POS_WIDTH(8), UI_POS_HEIGHT(0.5)},
""};
ImageButton button_play{
{UI_POS_X_RIGHT(2), UI_POS_Y(3.25) - 2, UI_POS_WIDTH(2), UI_POS_HEIGHT(0.5)},
&bitmap_stop,
Theme::getInstance()->fg_green->foreground,
Theme::getInstance()->fg_green->background};
TextField field_marker{
{UI_POS_X(12), UI_POS_Y(3), UI_POS_WIDTH(9), UI_POS_HEIGHT(1)},
""};
+6 -2
View File
@@ -6,7 +6,7 @@
* Copyright (C) 2024 Mark Thompson
* Copyright (C) 2024 u-foka
* Copyright (C) 2024 HTotoo
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
* copyleft 2024 zxkmm
*
* This file is part of PortaPack.
*
@@ -1174,13 +1174,17 @@ SetBatteryView::SetBatteryView(NavigationView& nav) {
pmem::set_battery_replaceable(checkbox_battery_replaceable.value());
battery::BatteryManagement::set_calc_override(checkbox_overridebatt.value());
i2cdev::I2cDev_MAX17055* dev = (i2cdev::I2cDev_MAX17055*)i2cdev::I2CDevManager::get_dev_by_model(I2C_DEVMDL::I2CDEVMDL_MAX17055);
if (dev) dev->resetChangedFlag();
if ((((uint32_t)field_battcap.value() != pmem::battery_cap_mah()) || (!pmem::battery_cap_valid())) || (dev && dev->getIsBattChanged())) {
pmem::set_battery_cap_mah(field_battcap.value());
if (dev) dev->resetChangedFlag();
if (dev && !dev->reInit()) {
nav.display_modal("Error", "Battery gauge re-init failed");
return;
}
if (dev) {
dev->resetChangedFlag();
dev->update(); // resend the new data
}
}
send_system_refresh();
nav.pop();
+1 -1
View File
@@ -5,7 +5,7 @@
* Copyright (C) 2023 Kyle Reed
* Copyright (C) 2024 Mark Thompson
* Copyright (C) 2024 u-foka
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
* copyleft 2024 zxkmm
*
* This file is part of PortaPack.
*
+18
View File
@@ -361,6 +361,16 @@ void set_noaaapt_config() {
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() {
const FlexConfigureMessage message{};
send_message(&message);
@@ -445,6 +455,14 @@ void set_p25tx_data(const uint8_t* dibits, uint16_t frame_length) {
send_message(&msg);
}
void set_hunter_config(uint32_t threshold, uint32_t hangtime_ms, bool start) {
HunterConfigMessage message{};
message.energy_threshold = threshold;
message.hangtime_ms = hangtime_ms;
message.start = start;
send_message(&message);
}
static bool baseband_image_running = false;
bool is_image_running() {
+3
View File
@@ -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_wefax_config(uint8_t lpm, uint8_t ioc);
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_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
@@ -120,6 +122,7 @@ void set_rtty_config(RTTYDataMessage& message);
void set_epirb_tx_config(EPIRBTXDataMessage& message);
void set_epirb_rx_config(EPIRBRXConfig& message);
void set_p25tx_data(const uint8_t* dibits, uint16_t frame_length);
void set_hunter_config(uint32_t threshold, uint32_t hangtime_ms, bool start);
void request_roger_beep();
void request_rssi_beep();
+64 -49
View File
@@ -25,6 +25,8 @@
#include "portapack_io.hpp"
#include "portapack.hpp"
#include "lpc43xx.inc"
#include "hackrf_hal.hpp"
using namespace hackrf::one;
@@ -417,7 +419,11 @@ std::string ClockManager::get_freq() {
to_string_dec_uint((reference.frequency % 1000000) / 100, 4, '0') + " MHz";
}
static void portapack_tcxo_enable() {
void ClockManager::portapack_tcxo_enable() {
#ifdef PRALINE
gpio_control::clkin_ctrl.setActive();
set_p1_control(P1_Function::P22_ClkIn);
#endif
portapack::io.reference_oscillator(true);
/* Delay >10ms at 96MHz clock speed for reference oscillator to start. */
@@ -426,7 +432,10 @@ static void portapack_tcxo_enable() {
while (delay--);
}
static void portapack_tcxo_disable() {
void ClockManager::portapack_tcxo_disable() {
#ifdef PRALINE
gpio_control::clkin_ctrl.setInactive();
#endif
portapack::io.reference_oscillator(false);
}
@@ -434,11 +443,6 @@ static void portapack_tcxo_disable() {
using namespace hackrf::one;
void ClockManager::init_clock_generator() {
if (hackrf_r9) {
gpio_r9_mcu_clk_en.output();
gpio_r9_mcu_clk_en.write(1);
}
clock_generator.reset();
clock_generator.set_crystal_internal_load_capacitance(CrystalInternalLoadCapacitance::XTAL_CL_8pF);
clock_generator.enable_fanout();
@@ -676,7 +680,7 @@ ClockManager::Reference ClockManager::choose_reference() {
}
#else
if (hackrf_r9) {
gpio_r9_clkin_en.write(1);
gpio_control::r9_clkin_en.setActive();
volatile uint32_t delay = 240000 + 24000;
while (delay--);
}
@@ -691,7 +695,7 @@ ClockManager::Reference ClockManager::choose_reference() {
}
if (hackrf_r9) {
gpio_r9_clkin_en.write(0);
gpio_control::r9_clkin_en.setInactive();
}
#endif
@@ -903,12 +907,24 @@ void ClockManager::set_sampling_frequency(const uint32_t frequency) {
// Set FPGA RX decimation register
fpga_debug_register_write(FPGA_REG_DECIM, n);
/* RX Mode: Register 3 is FPGA_REG_RX_DIGITAL_GAIN.
* We shift up by (3 * n) to compensate for CIC bit-growth.
/* No RX digital-gain register is written here.
*
* Register 0x03 used to be programmed with (3 * n + 2) as a "CIC
* bit-growth" renormalisation. The gateware has no such register: the
* RX decimator is a chain of unity-gain half-band FIRs selected by
* rx_decim (fpga/top/standard.py), and 0x03 is rx_pstep, whose top two
* bits are the quarter-rate shift. Writing a gain here silently
* cancelled the shift that set_tuning_frequency() had programmed, which
* left the analogue passband offset with no matching rotation.
*
* The shift depends on the AFE rate we just chose, so re-apply it after
* the rate change. ReceiverModel::update_sampling_rate() calls
* update_tuning_frequency() straight after this, which does exactly
* that; the write below only keeps the register consistent in between.
*/
uint8_t ds = (3 * n);
ds += 2;
fpga_debug_register_write(FPGA_REG_RX_DIGITAL_GAIN, ds);
fpga_debug_register_write(
FPGA_REG_RX_PSTEP,
(radio::debug::get_cached_quarter_shift() & 0b11) << FPGA_RX_QUARTER_SHIFT_SHIFT);
// Re-enable FPGA processing with clean state ===
fpga_debug_register_write(1, 0x01);
@@ -1178,8 +1194,7 @@ void ClockManager::enable_clock_output(bool enable) {
}
#else
if (hackrf_r9) {
gpio_r9_clkout_en.output();
gpio_r9_clkout_en.write(enable);
gpio_control::r9_clkout_en.setState(enable);
// NOTE: RETURNING HERE IF HACKRF_R9 TO PREVENT CLK2 FROM BEING DISABLED OR FREQ MODIFIED SINCE CLK2 ON R9 IS
// USED FOR BOTH CLKOUT AND FOR THE MCU_CLOCK (== GP_CLKIN) WHICH OTHER LP43XX CLOCKS CURRENTLY RELY ON.
@@ -1211,47 +1226,47 @@ void ClockManager::enable_clock_output(bool enable) {
#ifdef PRALINE
void ClockManager::set_p1_control(P1_Function func) {
// Truth table based on P1_Control.csv (L=clear, H=set)
// Truth table based on P1_Control.csv (L=setInactive, H=setActive)
switch (func) {
case P1_Function::TriggerIn:
gpio_control::p1_ctrl2.clear();
gpio_control::p1_ctrl1.clear();
gpio_control::p1_ctrl0.clear();
gpio_control::p1_ctrl2.setInactive();
gpio_control::p1_ctrl1.setInactive();
gpio_control::p1_ctrl0.setInactive();
break;
case P1_Function::AuxClk1:
gpio_control::p1_ctrl2.clear();
gpio_control::p1_ctrl1.clear();
gpio_control::p1_ctrl0.set();
gpio_control::p1_ctrl2.setInactive();
gpio_control::p1_ctrl1.setInactive();
gpio_control::p1_ctrl0.setActive();
break;
case P1_Function::ClkIn:
gpio_control::p1_ctrl2.clear();
gpio_control::p1_ctrl1.set();
gpio_control::p1_ctrl0.clear();
gpio_control::p1_ctrl2.setInactive();
gpio_control::p1_ctrl1.setActive();
gpio_control::p1_ctrl0.setInactive();
break;
case P1_Function::TriggerOut:
gpio_control::p1_ctrl2.clear();
gpio_control::p1_ctrl1.set();
gpio_control::p1_ctrl0.set();
gpio_control::p1_ctrl2.setInactive();
gpio_control::p1_ctrl1.setActive();
gpio_control::p1_ctrl0.setActive();
break;
case P1_Function::P22_ClkIn:
gpio_control::p1_ctrl2.set();
gpio_control::p1_ctrl1.clear();
gpio_control::p1_ctrl0.clear();
gpio_control::p1_ctrl2.setActive();
gpio_control::p1_ctrl1.setInactive();
gpio_control::p1_ctrl0.setInactive();
break;
case P1_Function::P2_5:
gpio_control::p1_ctrl2.set();
gpio_control::p1_ctrl1.clear();
gpio_control::p1_ctrl0.set();
gpio_control::p1_ctrl2.setActive();
gpio_control::p1_ctrl1.setInactive();
gpio_control::p1_ctrl0.setActive();
break;
case P1_Function::NotConnected:
gpio_control::p1_ctrl2.set();
gpio_control::p1_ctrl1.set();
gpio_control::p1_ctrl0.clear();
gpio_control::p1_ctrl2.setActive();
gpio_control::p1_ctrl1.setActive();
gpio_control::p1_ctrl0.setInactive();
break;
case P1_Function::AuxClk2:
gpio_control::p1_ctrl2.set();
gpio_control::p1_ctrl1.set();
gpio_control::p1_ctrl0.set();
gpio_control::p1_ctrl2.setActive();
gpio_control::p1_ctrl1.setActive();
gpio_control::p1_ctrl0.setActive();
break;
}
}
@@ -1259,20 +1274,20 @@ void ClockManager::set_p1_control(P1_Function func) {
void ClockManager::set_p2_control(P2_Function func) {
// Ensure all P2 control pins are configured as outputs
// Truth table based on P2_Control.csv (L=clear, H=set)
// Truth table based on P2_Control.csv (L=setInactive, H=setActive)
switch (func) {
case P2_Function::Clk3:
// CTRL0 is 'X' (don't care) according to CSV, we default it to Low (clear)
gpio_control::p2_ctrl1.clear();
gpio_control::p2_ctrl0.clear();
// CTRL0 is 'X' (don't care) according to CSV, we default it to Low (setInactive)
gpio_control::p2_ctrl1.setInactive();
gpio_control::p2_ctrl0.setInactive();
break;
case P2_Function::TriggerIn:
gpio_control::p2_ctrl1.set();
gpio_control::p2_ctrl0.clear();
gpio_control::p2_ctrl1.setActive();
gpio_control::p2_ctrl0.setInactive();
break;
case P2_Function::TriggerOut:
gpio_control::p2_ctrl1.set();
gpio_control::p2_ctrl0.set();
gpio_control::p2_ctrl1.setActive();
gpio_control::p2_ctrl0.setActive();
break;
}
}
+3
View File
@@ -116,6 +116,9 @@ class ClockManager {
void enable_clock_output(bool enable);
void portapack_tcxo_enable();
void portapack_tcxo_disable();
private:
I2C& i2c0;
si5351::Si5351& clock_generator;
+19 -15
View File
@@ -24,6 +24,11 @@
#include "hackrf_gpio.hpp"
#include "portapack_hal.hpp"
#include "board.h"
#include "gpio.hpp"
using namespace gpio_control;
void config_mode_blink_until_dfu();
void config_mode_set() {
@@ -56,7 +61,6 @@ uint32_t blink_patterns[] = {
void config_mode_run() {
configure_pins_portapack();
portapack::gpio_dfu.input();
portapack::persistent_memory::cache::init();
if (hackrf_r9) {
@@ -75,14 +79,14 @@ void config_mode_run() {
config_mode_blink_until_dfu();
}
auto last_dfu_btn = portapack::gpio_dfu.read();
auto last_dfu_btn = dfu_button.read();
int32_t counter = 0;
int8_t blink_pattern_value = portapack::persistent_memory::config_cpld() +
(portapack::persistent_memory::config_disable_external_tcxo() ? 5 : 0);
while (true) {
auto dfu_btn = portapack::gpio_dfu.read();
auto dfu_btn = dfu_button.read();
auto dfu_clicked = last_dfu_btn == true && dfu_btn == false;
last_dfu_btn = dfu_btn;
@@ -103,16 +107,16 @@ void config_mode_run() {
auto tx_value = ((tx_blink_pattern >> ((counter >> 0) & 31)) & 0x1) == 0x1;
if (tx_value) {
hackrf::one::led_tx.on();
led_tx.setActive();
} else {
hackrf::one::led_tx.off();
led_tx.setInactive();
}
auto rx_value = ((rx_blink_pattern >> ((counter >> 0) & 31)) & 0x1) == 0x1;
if (rx_value) {
hackrf::one::led_rx.on();
led_rx.setActive();
} else {
hackrf::one::led_rx.off();
led_rx.setInactive();
}
chThdSleepMilliseconds(100);
@@ -122,17 +126,17 @@ void config_mode_run() {
void config_mode_blink_until_dfu() {
while (true) {
hackrf::one::led_tx.on();
hackrf::one::led_rx.on();
hackrf::one::led_usb.on();
led_tx.setActive();
led_rx.setActive();
led_usb.setActive();
chThdSleepMilliseconds(10);
hackrf::one::led_tx.off();
hackrf::one::led_rx.off();
hackrf::one::led_usb.off();
led_tx.setInactive();
led_rx.setInactive();
led_usb.setInactive();
chThdSleepMilliseconds(115);
auto dfu_btn = portapack::gpio_dfu.read();
auto dfu_btn = dfu_button.read();
if (dfu_btn)
break;
}
@@ -140,7 +144,7 @@ void config_mode_blink_until_dfu() {
while (true) {
chThdSleepMilliseconds(10);
auto dfu_btn = portapack::gpio_dfu.read();
auto dfu_btn = dfu_button.read();
if (!dfu_btn)
break;
}
+5 -2
View File
@@ -33,6 +33,9 @@
#include "irq_controls.hpp"
#include "file_path.hpp"
#include "gpio.hpp"
using namespace gpio_control;
using namespace ui;
#define DEBUG_LOG_FILE "debug_log.txt"
@@ -134,9 +137,9 @@ void draw_line(int32_t y_offset, const char* label, regarm_t value) {
}
void runtime_error(uint8_t source) {
LED led = (source == CORTEX_M0) ? hackrf::one::led_rx : hackrf::one::led_tx;
const auto& led = (source == CORTEX_M0) ? led_rx : led_tx;
led.off();
led.setInactive();
// wait for DFU button release if pressed, so we don't immediately jump into stack dump
while (swizzled_switches() & (1 << (int)Switch::Dfu));
+12 -16
View File
@@ -37,8 +37,8 @@
#include "ch.h"
#include "hackrf_gpio.hpp"
using namespace hackrf::one;
#include "gpio.hpp"
using namespace gpio_control;
#include "irq_rtc.hpp"
@@ -56,8 +56,6 @@ using namespace hackrf::one;
#include "ui_navigation.hpp"
#include "gpio.hpp"
static int delayed_error = 0;
extern "C" {
@@ -229,8 +227,8 @@ void EventDispatcher::charge_deep_sleep(const bool sleep) {
LPC_ADC0->CR &= ~(1 << 21);
LPC_ADC1->CR &= ~(1 << 21);
led_rx.off();
led_usb.off();
led_rx.setInactive();
led_usb.setInactive();
rtc_wakeup_init();
NVIC_EnableIRQ(I2C0_OR_I2C1_IRQn);
@@ -247,21 +245,21 @@ void EventDispatcher::charge_deep_sleep(const bool sleep) {
if (is_full) {
// Case 1: Battery full (All LEDs off)
led_rx.off();
led_tx.off();
led_rx.setInactive();
led_tx.setInactive();
} else if ((voltage < 4150 && current < 10) || valid_mask == 0) {
// Case 2: Not full but low current draw (<10mA) -> Charging error
led_tx.on(); // LED indicates error/idle
led_rx.off();
led_tx.setActive(); // LED indicates error/idle
led_rx.setInactive();
} else {
// Case 3: Actively charging
led_rx.on(); // LED indicates charging
led_tx.off();
led_rx.setActive(); // LED indicates charging
led_tx.setInactive();
}
} else {
// Case 4: Battery IC not detected -> Error or H2 or older, so don't show that as an error.
led_tx.on();
led_rx.on();
led_tx.setActive();
led_rx.setActive();
}
// Shut down I2C and power down the APB bus for sleep
@@ -402,8 +400,6 @@ void EventDispatcher::handle_local_queue() {
void EventDispatcher::handle_rtc_tick() {
sd_card::poll_inserted();
portapack::temperature_logger.second_tick();
const auto backlight_timer = portapack::persistent_memory::config_backlight_timer();
if (backlight_timer.timeout_enabled()) {
if (portapack::bl_tick_counter == backlight_timer.timeout_seconds())
@@ -1,5 +1,5 @@
/*
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
* copyleft 2024 zxkmm
*
* This file is part of PortaPack.
*
@@ -1,5 +1,5 @@
/*
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
* copyleft 2024 zxkmm
*
* This file is part of PortaPack.
*
+66 -4
View File
@@ -294,11 +294,11 @@ void BLESpamView::createWindowsPacket() {
std::copy(res.begin(), res.end(), advertisementData);
}
void BLESpamView::createNameSpamPacket() {
const char* names[] = {"PortaHack", "PwnBt", "iSpam", "GenericFoodVagon", "SignalSnoop", "ByteBandit", "RadioRogue", "RadioRebel", "ByteBlast"};
const char* name = names[rand() % 9]; //"PortaHack";
// Builds a "Complete Local Name" HID advertisement for the given name.
// Shared by NameSpam (built-in list) and Custom (SD card list) modes.
void BLESpamView::buildNamePacket(const char* name) {
uint8_t name_len = strlen(name);
if (name_len > 19) name_len = 19; // clamp so the hex string fits advertisementData[63]
uint8_t size = 12 + name_len;
uint8_t i = 0;
@@ -327,6 +327,65 @@ void BLESpamView::createNameSpamPacket() {
std::copy(res.begin(), res.end(), advertisementData);
}
void BLESpamView::createNameSpamPacket() {
const char* names[] = {"PortaHack", "PwnBt", "iSpam", "GenericFoodVagon", "SignalSnoop", "ByteBandit", "RadioRogue", "RadioRebel", "ByteBlast"};
buildNamePacket(names[rand() % 9]);
}
// Opens /BLESPAM/NAME.txt once and keeps it open for the session.
void BLESpamView::open_custom_file() {
custom_loaded_ = true; // attempt only once
auto open_error = custom_file_.open(u"/BLESPAM/NAME.txt");
custom_file_valid_ = !open_error; // false -> caller falls back to NameSpam
}
// Reads the next line into custom_name_ (clamped to 19 chars), advancing the
// file position. At end of file it seeks back to the start to cycle. Returns
// false if the file holds no usable name. Only one line is ever held in RAM.
bool BLESpamView::read_next_custom_name() {
if (!custom_file_valid_) return false;
custom_name_.clear();
bool wrapped = false;
char c;
while (true) {
auto read_result = custom_file_.read(&c, 1);
if (read_result.is_error()) return false;
if (read_result.value() == 0) {
// end of file
if (!custom_name_.empty()) break;
// use the final line
if (wrapped) return false;
// no usable name in file
if (custom_file_.seek(0).is_error()) return false;
// loop back to the first line
wrapped = true;
continue;
}
if (c == '\n' || c == '\r') {
if (!custom_name_.empty()) break; // got a complete line
continue; // skip blank lines / CRLF pairs
}
if (custom_name_.size() < 19) // clamp; keep scanning rest of long line
custom_name_.push_back(c);
}
return true;
}
void BLESpamView::createCustomPacket() {
if (!custom_loaded_) open_custom_file();
if (!read_next_custom_name()) { // no file / unreadable / empty -> fall back
createNameSpamPacket();
return;
}
buildNamePacket(custom_name_.c_str());
}
char BLESpamView::randomNameChar() {
int randVal = rand() % 62; // 26 uppercase + 26 lowercase + 10 digits
if (randVal < 26) {
@@ -622,6 +681,9 @@ void BLESpamView::createPacket(ATK_TYPE attackType) {
case ATK_NAMERANDOM:
createNameRandomPacket();
break;
case ATK_CUSTOM:
createCustomPacket();
break;
case ATK_ALL_SAFE:
createAnyPacket(true);
break;
+14 -2
View File
@@ -39,6 +39,7 @@
#include "radio_state.hpp"
#include "log_file.hpp"
#include "utility.hpp"
#include "file.hpp"
using namespace ui;
@@ -52,6 +53,7 @@ enum ATK_TYPE {
ATK_SAMSUNG,
ATK_NAMESPAM,
ATK_NAMERANDOM,
ATK_CUSTOM,
ATK_ALL_SAFE,
ATK_ALL
};
@@ -131,8 +133,9 @@ class BLESpamView : public View {
{"Samsung", 4},
{"NameSpam", 5},
{"NameRandom", 6},
{"All-Safe", 7},
{"All", 8}}};
{"NameCustom", 7},
{"All-Safe", 8},
{"All", 9}}};
bool is_running{false};
@@ -144,6 +147,11 @@ class BLESpamView : public View {
bool randomMac{true};
bool randomDev{true};
File custom_file_{}; // kept open while in Custom mode
std::string custom_name_{}; // holds only the current name (clamped)
bool custom_loaded_{false}; // true once an open attempt has been made
bool custom_file_valid_{false}; // true if /BLESPAM/NAME.txt opened successfully
uint8_t channel_number = 37;
char mac[13] = "010203040407";
char advertisementData[63] = {"03032CFE06162CFED5A59E020AB4\0"};
@@ -156,7 +164,11 @@ class BLESpamView : public View {
void createIosPacket(bool crash);
void createSamsungPacket();
void createWindowsPacket();
void buildNamePacket(const char* name); // shared name advertisement builder (NameSpam / Custom)
void createNameSpamPacket();
void createCustomPacket();
void open_custom_file(); // open /BLESPAM/NAME.txt once (one name per line)
bool read_next_custom_name(); // read next line into custom_name_, looping at EOF
void createNameRandomPacket();
void createAnyPacket(bool safe);
void createPacket(ATK_TYPE attackType);
+198 -24
View File
@@ -143,30 +143,6 @@ static void push_bits(uint8_t* buf, int& pos, uint64_t v, int n) {
set_bit(buf, pos++, (v >> i) & 1);
}
/**
* Convert a beacon to hex string representation
* @param frame the frame to convert
* @param start true for the first half of the frame, false for the second half
* @return the hex string representation of the specieid half of the frame
*/
std::string beacon_to_hex_string(const uint8_t* frame, bool start) {
static const char hex[] = "0123456789ABCDEF";
std::string out;
out.resize(18);
int offset = start ? 0 : 9;
for (int i = 0; i < 9; i++) {
uint8_t b = frame[offset + i];
out[i * 2] = hex[b >> 4];
out[i * 2 + 1] = hex[b & 0x0F];
}
return out;
}
/**
* Generate a beacon in the provided buffer
* @param frame the buffer to generate the frame in
@@ -351,6 +327,204 @@ size_t generate_beacon(uint8_t* frame, const BeaconParams& params) {
return 18;
}
/**
* Convert a beacon hex string representation (generic range)
* @param frame the frame data
* @param offset_bytes starting byte offset
* @param count_bytes number of bytes to convert
* @return the hex string representation
*/
std::string beacon_to_hex_string_range(const uint8_t* frame, int offset_bytes, int count_bytes) {
static const char hex[] = "0123456789ABCDEF";
std::string out;
out.resize(count_bytes * 2);
for (int i = 0; i < count_bytes; i++) {
uint8_t b = frame[offset_bytes + i];
out[i * 2] = hex[b >> 4];
out[i * 2 + 1] = hex[b & 0x0F];
}
return out;
}
// ---------------------------------------------------------------------------
// SGB (Second Generation Beacon) — T.018 Rev 7, March 2021
// ---------------------------------------------------------------------------
// Reference values from T.018 canonical test vector (Appendix B.1)
#define SGB_TAC_NUMBER 230 // TAC number (16 bits, 065535)
#define SGB_SERIAL_NUMBER 573 // Serial number within TAC (14 bits, 016383)
#define SGB_HOMING_DEVICE 1 // 1 = beacon has 121.5 / 243 MHz homing device
#define SGB_RLS_FUNCTION 0 // 0 = no Return Link Service function
// BCH(250,202) generator polynomial — lower 48 bits (without leading X^48 term)
// Full g(x): X^48+X^47+X^46+X^42+X^41+X^40+X^39+X^38+X^37+X^35+X^33+X^32+X^31
// +X^26+X^24+X^23+X^22+X^20+X^19+X^18+X^17+X^16+X^13+X^12+X^11+X^10
// +X^7+X^4+X^2+X+1
// Binary MSB-first: 1110001111110101110000101110111110011110010010111 (T.018 App. B.1)
#define SGB_BCH_G_LOWER UINT64_C(0xC7EB85DF3C97)
/**
* Encode latitude for SGB GNSS location protocol (T.018 Appendix C)
* Pushes 23 bits: 1-bit N/S flag + 7-bit degrees + 15-bit decimal fraction
* @param frame the frame buffer
* @param pos current bit position (modified in-place)
* @param south true if southern hemisphere
* @param lat_mag absolute latitude in decimal degrees (0..90)
*/
static void push_sgb_latitude(uint8_t* frame, int& pos, bool south, float lat_mag) {
push_bits(frame, pos, south ? 1 : 0, 1);
uint32_t deg = (uint32_t)lat_mag;
if (deg > 90) deg = 90;
float frac = lat_mag - (float)deg;
uint32_t frac_n = (uint32_t)(frac * 32768.0f + 0.5f);
if (frac_n >= 32768) {
frac_n = 0;
if (deg < 90) deg++;
}
push_bits(frame, pos, deg, 7);
push_bits(frame, pos, frac_n, 15);
}
/**
* Encode longitude for SGB GNSS location protocol (T.018 Appendix C)
* Pushes 24 bits: 1-bit E/W flag + 8-bit degrees + 15-bit decimal fraction
* @param frame the frame buffer
* @param pos current bit position (modified in-place)
* @param west true if western hemisphere
* @param lon_mag absolute longitude in decimal degrees (0..180)
*/
static void push_sgb_longitude(uint8_t* frame, int& pos, bool west, float lon_mag) {
push_bits(frame, pos, west ? 1 : 0, 1);
uint32_t deg = (uint32_t)lon_mag;
if (deg > 180) deg = 180;
float frac = lon_mag - (float)deg;
uint32_t frac_n = (uint32_t)(frac * 32768.0f + 0.5f);
if (frac_n >= 32768) {
frac_n = 0;
if (deg < 180) deg++;
}
push_bits(frame, pos, deg, 8);
push_bits(frame, pos, frac_n, 15);
}
/**
* Compute BCH(250,202) parity using LFSR method (T.018 Appendix B.1)
* Reads 202 message bits from buffer positions 6..207 and writes
* 48 BCH parity bits to buffer positions 208..255.
* @param frame the 32-byte SGB buffer (message already written at bits 6..207)
*/
static void compute_sgb_bch(uint8_t* frame) {
uint64_t reg = 0;
for (int i = 0; i < 202; i++) {
int bp = i + 6; // buffer bit position (6 = start of message after padding)
bool msg_bit = (frame[bp >> 3] >> (7 - (bp & 7))) & 1;
bool feedback = ((reg >> 47) & 1) ^ (msg_bit ? 1u : 0u);
reg = (reg << 1) & UINT64_C(0xFFFFFFFFFFFF);
if (feedback) reg ^= SGB_BCH_G_LOWER;
}
// Append 48-bit remainder at buffer positions 208..255
int pos = 208;
push_bits(frame, pos, reg, 48);
}
/**
* Map BeaconType to SGB 3-bit beacon type code (T.018 Table 3.1, bits 138-140)
* 000=ELT, 001=EPIRB, 010=PLB
*/
static uint8_t sgb_beacon_type_code(BeaconType t) {
switch (t) {
case BeaconType::EPIRB:
return 0b001;
case BeaconType::PLB:
return 0b010;
default:
case BeaconType::ELT:
return 0b000;
}
}
/**
* Generate a Second Generation Beacon (SGB / T.018) frame.
* The 250-bit frame is stored in 32 bytes with 6 leading padding bits
* (bits 0..4 = 0, bit 5 = 1).
* The rotating field is Type 0 G.008 Objective Requirements (Table 3.3),
* with elapsed_hours and time_last_loc_min updated from elapsed_s.
* @param frame 32-byte output buffer
* @param params beacon parameters
* @param elapsed_s seconds elapsed since beacon activation (drives rotating field)
* @return 32 (size of the generated frame in bytes)
*/
size_t generate_sgb_beacon(uint8_t* frame, const BeaconParams& params, uint32_t elapsed_s) {
memset(frame, 0, 32);
// 6 padding bits at start of 32-byte buffer: [0,0,0,0,1,0] (5th bit (1 based index as per specification) = 1 to indicate self-test mode)
set_bit(frame, 4, 1);
// Message bits start at buffer bit position 6 (= SGB message bit 1)
int pos = 6;
// Bits 1-16: TAC number (16 bits) — T.018 Table 3.1
push_bits(frame, pos, SGB_TAC_NUMBER, 16);
// Bits 17-30: Serial number within TAC (14 bits)
push_bits(frame, pos, SGB_SERIAL_NUMBER, 14);
// Bits 31-40: Country code (10 bits, ITU MID)
push_bits(frame, pos, params.country & 0x3FFU, 10);
// Bit 41: Status of homing device (1 = has 121.5/243 MHz homing device)
push_bits(frame, pos, params.has_121_5 ? 1 : 0, 1);
// Bit 42: RLS function flag (0 = no RLS)
push_bits(frame, pos, SGB_RLS_FUNCTION, 1);
// Bit 43: Test protocol flag
push_bits(frame, pos, params.is_test ? 1 : 0, 1);
// Bits 44-66: Encoded GNSS latitude (23 bits) — T.018 Appendix C
float lat_mag = params.location.latitude < 0.0f ? -params.location.latitude : params.location.latitude;
push_sgb_latitude(frame, pos, params.location.south, lat_mag);
// Bits 67-90: Encoded GNSS longitude (24 bits)
float lon_mag = params.location.longitude < 0.0f ? -params.location.longitude : params.location.longitude;
push_sgb_longitude(frame, pos, params.location.west, lon_mag);
// Bits 91-137: Vessel ID (47 bits = 3-bit type selector + 44-bit body)
// Type 000 = No aircraft/maritime identity; body all zeros
push_bits(frame, pos, 0, 47);
// Bits 138-140: Beacon type (3 bits)
push_bits(frame, pos, sgb_beacon_type_code(params.type), 3);
// Bits 141-154: Spare (14 bits, all 1s in normal operation)
push_bits(frame, pos, 0x3FFFU, 14);
// Bits 155-202: Rotating Field Type 0 — G.008 Objective Requirements (Table 3.3)
// Bits 155-158: Identifier (4 bits = 0000)
push_bits(frame, pos, 0b0000U, 4);
// Bits 159-164: Elapsed time since activation (6 bits, 0-63 h, truncated at 63)
uint32_t elapsed_h = elapsed_s / 3600;
if (elapsed_h > 63) elapsed_h = 63;
push_bits(frame, pos, elapsed_h, 6);
// Bits 165-175: Time from last encoded location (11 bits, 0-2046 min; 2047 = no fix)
uint32_t loc_age_min = elapsed_s / 60;
if (loc_age_min > 2046) loc_age_min = 2046;
push_bits(frame, pos, loc_age_min, 11);
// Bits 176-185: Altitude of encoded location (10 bits; 0x3FF = no altitude)
push_bits(frame, pos, 0x3FFU, 10);
// Bits 186-193: Dilution of Precision (4-bit HDOP + 4-bit VDOP; 0 = best HDOP, 0 = best VDOP)
push_bits(frame, pos, 0b00000000U, 8);
// Bits 194-195: Activation notification (00 = manual by user)
push_bits(frame, pos, 0b00U, 2);
// Bits 196-198: Remaining battery capacity (101 = >75%)
push_bits(frame, pos, 0b101U, 3);
// Bits 199-200: GNSS status (10 = 3D fix)
push_bits(frame, pos, 0b10U, 2);
// Bits 201-202: Spare (00)
push_bits(frame, pos, 0b00U, 2);
// pos == 208: 6 padding + 202 message bits consumed
// Bits 203-250: BCH(250,202) parity (48 bits) computed and written at positions 208-255
compute_sgb_bch(frame);
return 32;
}
} // namespace ui::external_app::epirb_tx
#endif /*__BEACON_H__*/
+63 -14
View File
@@ -67,12 +67,17 @@ static uint8_t hexToByte(char high, char low) {
return (hexval(high) << 4) | hexval(low);
}
std::string EPIRBTXAppView::frame_to_hex_string(bool start) {
return beacon_to_hex_string(epirb_tx_message.data, start);
std::string EPIRBTXAppView::frame_to_hex_string_range(int offset_bytes, int count_bytes) {
return beacon_to_hex_string_range(epirb_tx_message.data, offset_bytes, count_bytes);
}
void EPIRBTXAppView::generate_frame(BeaconParams params) {
epirb_tx_message.data_len = generate_beacon(epirb_tx_message.data, params);
if (format_sgb) {
uint32_t elapsed_s = sgb_start_time > 0 ? (chTimeNow() - sgb_start_time) / 1000 : 0;
epirb_tx_message.data_len = generate_sgb_beacon(epirb_tx_message.data, params, elapsed_s);
} else {
epirb_tx_message.data_len = generate_beacon(epirb_tx_message.data, params);
}
}
void EPIRBTXAppView::on_timer() {
@@ -125,7 +130,7 @@ void EPIRBTXAppView::update_am_transmission() {
if (am_enabled && transmitting && !transmitting_bpsk) {
// Start am transmission
// Restore am frequency
epirb_tx_message.mode_bpsk = false;
epirb_tx_message.mode_406 = false;
transmitter_model.set_target_frequency(am_frequency);
// Send config to baseband
baseband::set_epirb_tx_config(epirb_tx_message);
@@ -146,10 +151,18 @@ void EPIRBTXAppView::update_frame(bool updateConfig) {
file_mode_ui->text_description.set(beacon.description.substr(0, max_text_width_ext));
file_mode_ui->text_description_end.set(beacon.description.size() > max_text_width_ext ? "-" + beacon.description.substr(max_text_width_ext, max_text_width_ext + max_text_width_ext - 1) : "");
// Udapte frame content on display
text_frame.set(beacon.frame.substr(0, 18));
text_frame_end.set(beacon.frame.size() > 18 ? beacon.frame.substr(18, 36) : "");
bool is_fgb = beacon.frame.size() <= BEACON_HEXA_SIZE_FGB;
if (is_fgb) {
text_frame.set(beacon.frame.substr(0, BEACON_HEXA_SPLIT_FGB));
text_frame_end.set(beacon.frame.size() > BEACON_HEXA_SPLIT_FGB ? beacon.frame.substr(BEACON_HEXA_SPLIT_FGB, BEACON_HEXA_SPLIT_FGB) : "");
text_frame_sgb_end.set("");
} else {
text_frame.set(" " + beacon.frame.substr(1, BEACON_HEXA_SPLIT_SGB - 1));
text_frame_end.set(beacon.frame.size() > BEACON_HEXA_SPLIT_SGB ? beacon.frame.substr(BEACON_HEXA_SPLIT_SGB, BEACON_HEXA_SPLIT_SGB) : "");
text_frame_sgb_end.set(beacon.frame.size() > 2 * BEACON_HEXA_SPLIT_SGB ? beacon.frame.substr(2 * BEACON_HEXA_SPLIT_SGB, BEACON_HEXA_SPLIT_SGB) : "");
}
// Prepare tx configuration
epirb_tx_message.data_len = std::min<size_t>((beacon.frame.size() / 2), 18);
epirb_tx_message.data_len = std::min<size_t>((beacon.frame.size() / 2), BEACON_SIZE_SGB);
for (uint8_t i = 0; i < epirb_tx_message.data_len; i++) {
epirb_tx_message.data[i] = hexToByte(
beacon.frame[2 * i],
@@ -159,8 +172,17 @@ void EPIRBTXAppView::update_frame(bool updateConfig) {
// In manual mode, generate frame content for current beacon params
generate_frame(beacon_params);
// Update frame content on display
text_frame.set(frame_to_hex_string(true));
text_frame_end.set(frame_to_hex_string(false));
if (format_sgb) {
// SGB: 32 bytes across 3 lines (BEACON_HEXA_SPLIT_SGB = 22 hex chars = 11 bytes)
// Remove first nibble and replace it with a space to match COSPAS hexadecimal representation specification
text_frame.set(" " + frame_to_hex_string_range(0, BEACON_HEXA_SPLIT_SGB / 2).substr(1));
text_frame_end.set(frame_to_hex_string_range(BEACON_HEXA_SPLIT_SGB / 2, BEACON_HEXA_SPLIT_SGB / 2));
text_frame_sgb_end.set(frame_to_hex_string_range(BEACON_HEXA_SPLIT_SGB, (BEACON_HEXA_SPLIT_SGB / 2) - 1));
} else {
text_frame.set(frame_to_hex_string_range(0, BEACON_HEXA_SPLIT_FGB / 2));
text_frame_end.set(frame_to_hex_string_range(BEACON_HEXA_SPLIT_FGB / 2, BEACON_HEXA_SPLIT_FGB / 2));
text_frame_sgb_end.set("");
}
}
if (updateConfig && send_on_change && loop) {
// Need to update config / send new beacon
@@ -176,14 +198,14 @@ void EPIRBTXAppView::update_frame(bool updateConfig) {
}
void EPIRBTXAppView::update_config() {
if (!epirb_tx_message.mode_bpsk) {
if (!epirb_tx_message.mode_406) {
// Previously in AM mode => restore bpsk frequency
transmitter_model.set_target_frequency(bpsk_frequency);
// Update displayed frequency
tx_view.on_show();
}
// Set mode to bpsk
epirb_tx_message.mode_bpsk = true;
epirb_tx_message.mode_406 = true;
transmitting_bpsk = true;
// Set pre/post count
epirb_tx_message.pre_count = (160 * TONES_SAMPLERATE) / 1000; // 160 ms carrier (COSPAS spec.)
@@ -198,6 +220,13 @@ void EPIRBTXAppView::set_tx_button_state(bool active) {
}
void EPIRBTXAppView::start_tx() {
if (format_sgb && !mode_file) {
// Track start time for the SGB rotating field elapsed-time counter
if (!transmitting) sgb_start_time = chTimeNow();
// update_frame regenerates the SGB frame with current elapsed time and refreshes display
update_frame(false);
set_dirty();
}
last_frame_time = chTimeNow();
update_config();
loop = loop_enabled;
@@ -221,7 +250,7 @@ void EPIRBTXAppView::on_tx_progress(const uint32_t progress, const bool done) {
transmitting_bpsk = false;
if (am_enabled) {
// BPSK frame sent, switch back to 121.5 AM signal
epirb_tx_message.mode_bpsk = false;
epirb_tx_message.mode_406 = false;
// Start am transmission
update_am_transmission();
} else {
@@ -310,6 +339,7 @@ void EPIRBTXAppView::update_mode() {
options_beacon_type.hidden(mode_file);
options_beacon_protocol.hidden(mode_file);
options_beacon_country.hidden(mode_file);
options_format.hidden(mode_file);
text_field_beacon_locator.hidden(mode_file);
}
@@ -323,6 +353,7 @@ EPIRBTXAppView::EPIRBTXAppView(
&text_beacon_type,
&options_beacon_type,
&options_beacon_protocol,
&options_format,
&text_beacon_country,
&options_beacon_country,
&checkbox_beacon_internal,
@@ -335,6 +366,7 @@ EPIRBTXAppView::EPIRBTXAppView(
&text_field_beacon_locator,
&text_frame,
&text_frame_end,
&text_frame_sgb_end,
&text_timeout,
&checkbox_loop,
&field_delay,
@@ -376,6 +408,7 @@ EPIRBTXAppView::EPIRBTXAppView(
init_from_locator(beacon_params.location);
update_mode();
update_location();
options_format.set_by_value(format_sgb ? 1 : 0);
options_mode.on_change = [this](size_t, OptionsField::value_t value) {
mode_file = (((BeaconMode)value) == BeaconMode::FILE);
@@ -405,6 +438,13 @@ EPIRBTXAppView::EPIRBTXAppView(
set_dirty();
};
options_format.on_change = [this](size_t, OptionsField::value_t v) {
format_sgb = (v == 1);
sgb_start_time = 0; // reset elapsed counter when format changes
update_frame();
set_dirty();
};
options_am_channel.on_change = [this](size_t, OptionsField::value_t v) {
bool is_real = false;
switch ((AmChannel)v) {
@@ -458,6 +498,9 @@ EPIRBTXAppView::EPIRBTXAppView(
case BpskChannel::O:
bpsk_frequency = BPSK_FREQUENCY_O;
break;
case BpskChannel::SGB:
bpsk_frequency = BPSK_FREQUENCY_SGB;
break;
default:
v = (uint8_t)BpskChannel::HAM;
// fallthrough
@@ -590,6 +633,9 @@ EPIRBTXAppView::EPIRBTXAppView(
case BPSK_FREQUENCY_O:
bpsk_channel = (uint8_t)BpskChannel::O;
break;
case BPSK_FREQUENCY_SGB:
bpsk_channel = (uint8_t)BpskChannel::SGB;
break;
default:
bpsk_channel = (uint8_t)BpskChannel::MANUAL;
manual_bpsk_frequency = bpsk_frequency;
@@ -642,11 +688,14 @@ void EPIRBTXAppView::load_beacons() {
Beacon beacon{};
beacon.title = trim(cols[0]);
beacon.description = trim(cols[1]);
// Make sure frame is not longer tha 18 bytes / 36 hex character
beacon.frame = trim(cols[2]).substr(0, 36);
// 1G uses up to 36 hex chars, 2G uses 64 hex chars (250 bits rounded to 32 bytes).
beacon.frame = trim(cols[2]).substr(0, BEACON_HEXA_SIZE_SGB);
size_t size = beacon.frame.size();
if (size <= 0)
continue; // Invalid line.
if ((size % 2) != 0) {
beacon.frame = "0" + beacon.frame; // Pad with leading 0 to make it even length
}
// Beacon is valid, add it to the list
beacons.emplace_back(std::move(beacon));
}
+27 -6
View File
@@ -33,9 +33,12 @@
#include "message.hpp"
#include "tonesets.hpp"
#define BEACON_HEXA_SIZE 36
#define BEACON_HEXA_HALF_SIZE 18
#define BEACON_SIZE 18
#define BEACON_SIZE_FGB 18
#define BEACON_SIZE_SGB 32
#define BEACON_HEXA_SIZE_FGB BEACON_SIZE_FGB * 2
#define BEACON_HEXA_SIZE_SGB BEACON_SIZE_SGB * 2
#define BEACON_HEXA_SPLIT_FGB 18
#define BEACON_HEXA_SPLIT_SGB 22
#define AM_TEST_FREQUENCY 121375000
#define AM_REAL_FREQUENCY 121500000
@@ -49,6 +52,7 @@
#define BPSK_FREQUENCY_K 406052000
#define BPSK_FREQUENCY_N 406061000
#define BPSK_FREQUENCY_O 406064000
#define BPSK_FREQUENCY_SGB 406050000
namespace ui::external_app::epirb_tx {
@@ -85,7 +89,8 @@ enum class BpskChannel {
K = 6,
N = 7,
O = 8,
MANUAL = 10
MANUAL = 10,
SGB = 100
};
struct Location {
@@ -129,7 +134,7 @@ class EPIRBTXAppView : public View {
void on_timer();
void load_beacons();
void set_tx_button_state(bool active);
std::string frame_to_hex_string(bool start);
std::string frame_to_hex_string_range(int offset_bytes, int count_bytes);
void generate_frame(BeaconParams params);
void update_frame(bool updateConfig = true);
void update_bpsk_frequency();
@@ -214,6 +219,7 @@ class EPIRBTXAppView : public View {
{"country"sv, &beacon_country},
{"internal"sv, &beacon_internal},
{"locator"sv, &locator},
{"sgb"sv, &format_sgb},
}};
// Time of the last sent frame
@@ -224,6 +230,10 @@ class EPIRBTXAppView : public View {
bool transmitting_bpsk{false};
// True when currently looping on sending beacons
bool loop{false};
// True when SGB (Second Generation Beacon) format is selected
bool format_sgb{false};
// System time (ms) when the current SGB transmission session started
uint32_t sgb_start_time{0};
// Current EPIRBTXDataMessage for baseband
EPIRBTXDataMessage epirb_tx_message{};
@@ -302,10 +312,17 @@ class EPIRBTXAppView : public View {
{"PLB", (uint8_t)BeaconType::PLB}}};
OptionsField options_beacon_protocol{
{UI_POS_X(9 + 7), UI_POS_Y(1)},
30,
8,
{{"User", (uint8_t)BeaconProtocol::USER},
{"Standard", (uint8_t)BeaconProtocol::STANDARD},
{"National", (uint8_t)BeaconProtocol::NATIONAL}}};
// Format selector (FGB / SGB) — manual mode only, same row as type/protocol
OptionsField options_format{
{UI_POS_X_RIGHT(4), UI_POS_Y(1)},
4,
{{"FGB", 0},
{"SGB", 1}}};
OptionsField options_beacon_country{
{UI_POS_X(9), UI_POS_Y(2)},
7,
@@ -337,6 +354,9 @@ class EPIRBTXAppView : public View {
Text text_frame_end{
{UI_POS_X(6), UI_POS_Y(7), UI_POS_WIDTH_REMAINING(6), UI_POS_DEFAULT_HEIGHT},
""};
Text text_frame_sgb_end{
{UI_POS_X(6), UI_POS_Y(8), UI_POS_WIDTH_REMAINING(6), UI_POS_DEFAULT_HEIGHT},
""};
Text text_timeout{
{UI_POS_X(14), UI_POS_Y(10), UI_POS_WIDTH(2), UI_POS_DEFAULT_HEIGHT},
@@ -389,6 +409,7 @@ class EPIRBTXAppView : public View {
{"406.052 MHz (K)", (uint8_t)BpskChannel::K},
{"406.061 MHz (N)", (uint8_t)BpskChannel::N},
{"406.064 MHz (O)", (uint8_t)BpskChannel::O},
{"406.050 MHz (SGB)", (uint8_t)BpskChannel::SGB},
{"Manual", (uint8_t)BpskChannel::MANUAL}}};
// Transmitter view
+28
View File
@@ -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
@@ -161,6 +169,7 @@ set(EXTCPPSRC
#mcu_temperature 112
external/mcu_temperature/main.cpp
external/mcu_temperature/mcu_temperature.cpp
external/mcu_temperature/temperature_logger.cpp
#fmradio 640
external/fmradio/main.cpp
@@ -367,6 +376,21 @@ set(EXTCPPSRC
external/secplustx/main.cpp
external/secplustx/ui_secplustx.cpp
external/secplustx/secplustx.cpp
#signal_hunter
external/signal_hunter/main.cpp
external/signal_hunter/ui_signal_hunter.cpp
#tetra rx
external/tetra_rx/main.cpp
external/tetra_rx/ui_tetra_rx.cpp
external/tetra_rx/tetra_crc.cpp
external/tetra_rx/tetra_descrambler.cpp
external/tetra_rx/tetra_interleave.cpp
external/tetra_rx/tetra_rcpc.cpp
external/tetra_rx/tetra_viterbi.cpp
)
set(EXTAPPLIST
@@ -401,6 +425,8 @@ set(EXTAPPLIST
acars_rx
wefax_rx
noaaapt_rx
vor_rx
vor_tx
shoppingcart_lock
ookbrute
ook_editor
@@ -457,6 +483,8 @@ set(EXTAPPLIST
two_tone_rx
hard_reset
secplustx
signal_hunter
tetra_rx
)
# sdusb has type conflicts with PRALINE (HackRF Pro) - add only for non-PRALINE builds
+119 -88
View File
@@ -111,6 +111,10 @@ 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
ram_external_app_signal_hunter (rwx) : org = 0xAE0B0000, len = 32k
ram_external_app_tetra_rx (rwx) : org = 0xAE0C0000, len = 32k
}
SECTIONS
@@ -118,528 +122,555 @@ SECTIONS
.external_app_afsk_rx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_afsk_rx.application_information));
*(*ui*external_app*afsk_rx*);
*/external/afsk_rx/*(*ui*external_app*afsk_rx*);
} > ram_external_app_afsk_rx
.external_app_calculator : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_calculator.application_information));
*(*ui*external_app*calculator*);
*/external/calculator/*(*ui*external_app*calculator*);
} > ram_external_app_calculator
.external_app_font_viewer : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_font_viewer.application_information));
*(*ui*external_app*font_viewer*);
*/external/font_viewer/*(*ui*external_app*font_viewer*);
} > ram_external_app_font_viewer
.external_app_blespam : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_blespam.application_information));
*(*ui*external_app*blespam*);
*/external/blespam/*(*ui*external_app*blespam*);
} > ram_external_app_blespam
.external_app_analogtv : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_analogtv.application_information));
*(*ui*external_app*analogtv*);
*/external/analogtv/*(*ui*external_app*analogtv*);
} > ram_external_app_analogtv
.external_app_nrf_rx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_nrf_rx.application_information));
*(*ui*external_app*nrf_rx*);
*/external/nrf_rx/*(*ui*external_app*nrf_rx*);
} > ram_external_app_nrf_rx
.external_app_coasterp : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_coasterp.application_information));
*(*ui*external_app*coasterp*);
*/external/coasterp/*(*ui*external_app*coasterp*);
} > ram_external_app_coasterp
.external_app_lge : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_lge.application_information));
*(*ui*external_app*lge*);
*/external/lge/*(*ui*external_app*lge*);
} > ram_external_app_lge
.external_app_lcr : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_lcr.application_information));
*(*ui*external_app*lcr*);
*/external/lcr/*(*ui*external_app*lcr*);
} > ram_external_app_lcr
.external_app_jammer : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_jammer.application_information));
*(*ui*external_app*jammer*);
*/external/jammer/*(*ui*external_app*jammer*);
} > ram_external_app_jammer
.external_app_gpssim : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_gpssim.application_information));
*(*ui*external_app*gpssim*);
*/external/gpssim/*(*ui*external_app*gpssim*);
} > ram_external_app_gpssim
.external_app_spainter : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_spainter.application_information));
*(*ui*external_app*spainter*);
*/external/spainter/*(*ui*external_app*spainter*);
} > ram_external_app_spainter
.external_app_keyfob : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_keyfob.application_information));
*(*ui*external_app*keyfob*);
*/external/keyfob/*(*ui*external_app*keyfob*);
} > ram_external_app_keyfob
.external_app_tetris : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_tetris.application_information));
*(*ui*external_app*tetris*);
*/external/tetris/*(*ui*external_app*tetris*);
} > ram_external_app_tetris
.external_app_extsensors : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_extsensors.application_information));
*(*ui*external_app*extsensors*);
*/external/extsensors/*(*ui*external_app*extsensors*);
} > ram_external_app_extsensors
.external_app_foxhunt_rx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_foxhunt_rx.application_information));
*(*ui*external_app*foxhunt_rx*);
*/external/foxhunt/*(*ui*external_app*foxhunt_rx*);
} > ram_external_app_foxhunt_rx
.external_app_audio_test : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_audio_test.application_information));
*(*ui*external_app*audio_test*);
*/external/audio_test/*(*ui*external_app*audio_test*);
} > ram_external_app_audio_test
.external_app_wardrivemap : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_wardrivemap.application_information));
*(*ui*external_app*wardrivemap*);
*/external/wardrivemap/*(*ui*external_app*wardrivemap*);
} > ram_external_app_wardrivemap
.external_app_tpmsrx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_tpmsrx.application_information));
*(*ui*external_app*tpmsrx*);
*/external/tpmsrx/*(*ui*external_app*tpmsrx*);
} > ram_external_app_tpmsrx
.external_app_tpmstx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_tpmstx.application_information));
*(*ui*external_app*tpmstx*);
*/external/tpmstx/*(*ui*external_app*tpmstx*);
} > ram_external_app_tpmstx
.external_app_protoview : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_protoview.application_information));
*(*ui*external_app*protoview*);
*/external/protoview/*(*ui*external_app*protoview*);
} > ram_external_app_protoview
.external_app_adsbtx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_adsbtx.application_information));
*(*ui*external_app*adsbtx*);
*/external/adsbtx/*(*ui*external_app*adsbtx*);
} > ram_external_app_adsbtx
.external_app_morse_tx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_morse_tx.application_information));
*(*ui*external_app*morse_tx*);
*/external/morse_tx/*(*ui*external_app*morse_tx*);
} > ram_external_app_morse_tx
.external_app_sstvtx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_sstvtx.application_information));
*(*ui*external_app*sstvtx*);
*/external/sstvtx/*(*ui*external_app*sstvtx*);
} > ram_external_app_sstvtx
.external_app_random_password : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_random_password.application_information));
*(*ui*external_app*random_password*);
*/external/random_password/*(*ui*external_app*random_password*);
} > ram_external_app_random_password
.external_app_acars_rx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_acars_rx.application_information));
*(*ui*external_app*acars_rx*);
*/external/acars_rx/*(*ui*external_app*acars_rx*);
} > ram_external_app_acars_rx
.external_app_shoppingcart_lock : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_shoppingcart_lock.application_information));
*(*ui*external_app*shoppingcart_lock*);
*/external/shoppingcart_lock/*(*ui*external_app*shoppingcart_lock*);
} > ram_external_app_shoppingcart_lock
.external_app_cvs_spam : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_cvs_spam.application_information));
*(*ui*external_app*cvs_spam*);
*/external/cvs_spam/*(*ui*external_app*cvs_spam*);
} > ram_external_app_cvs_spam
.external_app_ookbrute : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_ookbrute.application_information));
*(*ui*external_app*ookbrute*);
*/external/ookbrute/*(*ui*external_app*ookbrute*);
} > ram_external_app_ookbrute
.external_app_ook_editor : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_ook_editor.application_information));
*(*ui*external_app*ook_editor*);
*/external/ook_editor/*(*ui*external_app*ook_editor*);
} > ram_external_app_ook_editor
.external_app_flippertx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_flippertx.application_information));
*(*ui*external_app*flippertx*);
*/external/flippertx/*(*ui*external_app*flippertx*);
} > ram_external_app_flippertx
.external_app_remote : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_remote.application_information));
*(*ui*external_app*remote*);
*/external/remote/*(*ui*external_app*remote*);
} > ram_external_app_remote
.external_app_mcu_temperature : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_mcu_temperature.application_information));
*(*ui*external_app*mcu_temperature*);
*/external/mcu_temperature/*(*ui*external_app*mcu_temperature*);
} > ram_external_app_mcu_temperature
.external_app_fmradio : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_fmradio.application_information));
*(*ui*external_app*fmradio*);
*/external/fmradio/*(*ui*external_app*fmradio*);
} > ram_external_app_fmradio
.external_app_tuner : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_tuner.application_information));
*(*ui*external_app*tuner*);
*/external/tuner/*(*ui*external_app*tuner*);
} > ram_external_app_tuner
.external_app_metronome : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_metronome.application_information));
*(*ui*external_app*metronome*);
*/external/metronome/*(*ui*external_app*metronome*);
} > ram_external_app_metronome
.external_app_app_manager : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_app_manager.application_information));
*(*ui*external_app*app_manager*);
*/external/app_manager/*(*ui*external_app*app_manager*);
} > ram_external_app_app_manager
.external_app_hopper : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_hopper.application_information));
*(*ui*external_app*hopper*);
*/external/hopper/*(*ui*external_app*hopper*);
} > ram_external_app_hopper
.external_app_antenna_length : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_antenna_length.application_information));
*(*ui*external_app*antenna_length*);
*/external/antenna_length/*(*ui*external_app*antenna_length*);
} > ram_external_app_antenna_length
.external_app_view_wav : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_view_wav.application_information));
*(*ui*external_app*view_wav*);
*/external/wav_view/*(*ui*external_app*view_wav*);
} > ram_external_app_view_wav
.external_app_sd_wipe : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_sd_wipe.application_information));
*(*ui*external_app*sd_wipe*);
*/external/sd_wipe/*(*ui*external_app*sd_wipe*);
} > ram_external_app_sd_wipe
.external_app_playlist_editor : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_playlist_editor.application_information));
*(*ui*external_app*playlist_editor*);
*/external/playlist_editor/*(*ui*external_app*playlist_editor*);
} > ram_external_app_playlist_editor
.external_app_snake : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_snake.application_information));
*(*ui*external_app*snake*);
*/external/snake/*(*ui*external_app*snake*);
} > ram_external_app_snake
.external_app_stopwatch : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_stopwatch.application_information));
*(*ui*external_app*stopwatch*);
*/external/stopwatch/*(*ui*external_app*stopwatch*);
} > ram_external_app_stopwatch
.external_app_wefax_rx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_wefax_rx.application_information));
*(*ui*external_app*wefax_rx*);
*/external/wefax_rx/*(*ui*external_app*wefax_rx*);
} > ram_external_app_wefax_rx
.external_app_noaaapt_rx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_noaaapt_rx.application_information));
*(*ui*external_app*noaaapt_rx*);
*/external/noaaapt_rx/*(*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));
*/external/vor_rx/*(*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));
*/external/vor_tx/*(*ui*external_app*vor_tx*);
} > ram_external_app_vor_tx
.external_app_breakout : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_breakout.application_information));
*(*ui*external_app*breakout*);
*/external/breakout/*(*ui*external_app*breakout*);
} > ram_external_app_breakout
.external_app_doom : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_doom.application_information));
*(*ui*external_app*doom*);
*/external/doom/*(*ui*external_app*doom*);
} > ram_external_app_doom
.external_app_debug_pmem : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_debug_pmem.application_information));
*(*ui*external_app*debug_pmem*);
*/external/debug_pmem/*(*ui*external_app*debug_pmem*);
} > ram_external_app_debug_pmem
.external_app_scanner : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_scanner.application_information));
*(*ui*external_app*scanner*);
*/external/scanner/*(*ui*external_app*scanner*);
} > ram_external_app_scanner
.external_app_level : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_level.application_information));
*(*ui*external_app*level*);
*/external/level/*(*ui*external_app*level*);
} > ram_external_app_level
.external_app_gfxeq : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_gfxeq.application_information));
*(*ui*external_app*gfxeq*);
*/external/gfxeq/*(*ui*external_app*gfxeq*);
} > ram_external_app_gfxeq
.external_app_waterfall_designer : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_waterfall_designer.application_information));
*(*ui*external_app*waterfall_designer*);
*/external/waterfall_designer/*(*ui*external_app*waterfall_designer*);
} > ram_external_app_waterfall_designer
.external_app_detector_rx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_detector_rx.application_information));
*(*ui*external_app*detector_rx*);
*/external/detector_rx/*(*ui*external_app*detector_rx*);
} > ram_external_app_detector_rx
.external_app_dinogame : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_dinogame.application_information));
*(*ui*external_app*dinogame*);
*/external/dinogame/*(*ui*external_app*dinogame*);
} > ram_external_app_dinogame
.external_app_spaceinv : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_spaceinv.application_information));
*(*ui*external_app*spaceinv*);
*/external/spaceinv/*(*ui*external_app*spaceinv*);
} > ram_external_app_spaceinv
.external_app_blackjack : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_blackjack.application_information));
*(*ui*external_app*blackjack*);
*/external/blackjack/*(*ui*external_app*blackjack*);
} > ram_external_app_blackjack
.external_app_battleship : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_battleship.application_information));
*(*ui*external_app*battleship*);
*/external/battleship/*(*ui*external_app*battleship*);
} > ram_external_app_battleship
.external_app_ert : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_ert.application_information));
*(*ui*external_app*ert*);
*/external/ert/*(*ui*external_app*ert*);
} > ram_external_app_ert
.external_app_epirb_rx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_epirb_rx.application_information));
*(*ui*external_app*epirb_rx*);
*/external/epirb_rx/*(*ui*external_app*epirb_rx*);
} > ram_external_app_epirb_rx
.external_app_soundboard : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_soundboard.application_information));
*(*ui*external_app*soundboard*);
*/external/soundboard/*(*ui*external_app*soundboard*);
} > ram_external_app_soundboard
.external_app_game2048 : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_game2048.application_information));
*(*ui*external_app*game2048*);
*/external/game2048/*(*ui*external_app*game2048*);
} > ram_external_app_game2048
.external_app_bht_tx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_bht_tx.application_information));
*(*ui*external_app*bht_tx*);
*/external/bht_tx/*(*ui*external_app*bht_tx*);
} > ram_external_app_bht_tx
.external_app_morse_practice : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_morse_practice.application_information));
*(*ui*external_app*morse_practice*);
*/external/morse_practice/*(*ui*external_app*morse_practice*);
} > ram_external_app_morse_practice
.external_app_adult_toys_controller : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_adult_toys_controller.application_information));
*(*ui*external_app*adult_toys_controller*);
*/external/adult_toys_controller/*(*ui*external_app*adult_toys_controller*);
} > ram_external_app_adult_toys_controller
.external_app_flex_rx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_flex_rx.application_information));
*(*ui*external_app*flex_rx*);
*/external/flex_rx/*(*ui*external_app*flex_rx*);
} > ram_external_app_flex_rx
.external_app_sstvrx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_sstvrx.application_information));
*(*ui*external_app*sstvrx*);
*/external/sstvrx/*(*ui*external_app*sstvrx*);
} > ram_external_app_sstvrx
.external_app_subcarrx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_subcarrx.application_information));
*(*ui*external_app*subcarrx*);
*/external/subcarrx/*(*ui*external_app*subcarrx*);
} > ram_external_app_subcarrx
.external_app_siggen : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_siggen.application_information));
*(*ui*external_app*siggen*);
*/external/siggen/*(*ui*external_app*siggen*);
} > ram_external_app_siggen
.external_app_sdusb : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_sdusb.application_information));
*(*ui*external_app*sdusb*);
*/external/sdusb/*(*ui*external_app*sdusb*);
} > ram_external_app_sdusb
.external_app_morse_radio : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_morse_radio.application_information));
*(*ui*external_app*morse_radio*);
*/external/morse_radio/*(*ui*external_app*morse_radio*);
} > ram_external_app_morse_radio
.external_app_morseradiotx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_morseradiotx.application_information));
*(*ui*external_app*morseradiotx*);
*/external/morseradiotx/*(*ui*external_app*morseradiotx*);
} > ram_external_app_morseradiotx
.external_app_keeloqtx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_keeloqtx.application_information));
*(*ui*external_app*keeloqtx*);
*/external/keeloqtx/*(*ui*external_app*keeloqtx*);
} > ram_external_app_keeloqtx
.external_app_rtty_rx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_rtty_rx.application_information));
*(*ui*external_app*rtty_rx*);
*/external/rtty_rx/*(*ui*external_app*rtty_rx*);
} > ram_external_app_rtty_rx
.external_app_rtty_tx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_rtty_tx.application_information));
*(*ui*external_app*rtty_tx*);
*/external/rtty_tx/*(*ui*external_app*rtty_tx*);
} > ram_external_app_rtty_tx
.external_app_pocsag_tx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_pocsag_tx.application_information));
*(*ui*external_app*pocsag_tx*);
*/external/pocsag_tx/*(*ui*external_app*pocsag_tx*);
} > ram_external_app_pocsag_tx
.external_app_time_sink : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_time_sink.application_information));
*(*ui*external_app*time_sink*);
*/external/time_sink/*(*ui*external_app*time_sink*);
} > ram_external_app_time_sink
.external_app_same_tx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_same_tx.application_information));
*(*ui*external_app*same_tx*);
*/external/same_tx/*(*ui*external_app*same_tx*);
} > ram_external_app_same_tx
.external_app_kiss_tnc : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_kiss_tnc.application_information));
*(*ui*external_app*kiss_tnc*);
*/external/kiss_tnc/*(*ui*external_app*kiss_tnc*);
} > ram_external_app_kiss_tnc
.external_app_mdc_tx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_mdc_tx.application_information));
*(*ui*external_app*mdc_tx*);
*/external/mdc_tx/*(*ui*external_app*mdc_tx*);
} > ram_external_app_mdc_tx
.external_app_epirb_tx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_epirb_tx.application_information));
*(*ui*external_app*epirb_tx*);
*/external/epirb_tx/*(*ui*external_app*epirb_tx*);
} > ram_external_app_epirb_tx
.external_app_fpv_detect : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_fpv_detect.application_information));
*(*ui*external_app*fpv_detect*);
*/external/fpv_detect/*(*ui*external_app*fpv_detect*);
} > ram_external_app_fpv_detect
.external_app_p25_tx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_p25_tx.application_information));
*(*ui*external_app*p25_tx*);
*/external/p25_tx/*(*ui*external_app*p25_tx*);
} > ram_external_app_p25_tx
.external_app_two_tone_pager : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_two_tone_pager.application_information));
*(*ui*external_app*two_tone_pager*);
*/external/two_tone_pager/*(*ui*external_app*two_tone_pager*);
} > ram_external_app_two_tone_pager
.external_app_two_tone_rx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_two_tone_rx.application_information));
*(*ui*external_app*two_tone_rx*);
*/external/two_tone_rx/*(*ui*external_app*two_tone_rx*);
} > ram_external_app_two_tone_rx
.external_app_flex_tx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_flex_tx.application_information));
*(*ui*external_app*flex_tx*);
*/external/flex_tx/*(*ui*external_app*flex_tx*);
} > ram_external_app_flex_tx
.external_app_hard_reset : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_hard_reset.application_information));
*(*ui*external_app*hard_reset*);
*/external/hard_reset/*(*ui*external_app*hard_reset*);
} > ram_external_app_hard_reset
.external_app_secplustx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_secplustx.application_information));
*(*ui*external_app*secplustx*);
*/external/secplustx/*(*ui*external_app*secplustx*);
} > ram_external_app_secplustx
.external_app_signal_hunter : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_signal_hunter.application_information));
*/external/signal_hunter/*(*ui*external_app*signal_hunter*);
} > ram_external_app_signal_hunter
.external_app_tetra_rx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_tetra_rx.application_information));
*/external/tetra_rx/*(*ui*external_app*tetra_rx*);
} > ram_external_app_tetra_rx
}
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2023 Mark Thompson
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
* copyleft 2024 zxkmm
*
* This file is part of PortaPack.
*
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2023 Mark Thompson
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
* copyleft 2024 zxkmm
*
* This file is part of PortaPack.
*
+1 -1
View File
@@ -1,5 +1,5 @@
/*
* copyleft 2025 zxkmm AKA zix aka sommermorgentraum
* copyleft 2025 zxkmm
*
* This file is part of PortaPack.
*
+1 -1
View File
@@ -1,5 +1,5 @@
/*
* copyleft 2025 zxkmm AKA zix aka sommermorgentraum
* copyleft 2025 zxkmm
*
* This file is part of PortaPack.
*
+1 -1
View File
@@ -2,7 +2,7 @@
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2016 Furrtek
* Copyright (C) 2020 euquiq
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
* copyleft 2024 zxkmm
*
* This file is part of PortaPack.
*
+1 -1
View File
@@ -2,7 +2,7 @@
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2016 Furrtek
* Copyright (C) 2020 euquiq
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
* copyleft 2024 zxkmm
*
* This file is part of PortaPack.
*
@@ -8,14 +8,12 @@ using namespace portapack;
namespace ui::external_app::mcu_temperature {
void McuTemperatureWidget::paint(Painter& painter) {
const auto logger = portapack::temperature_logger;
const auto rect = screen_rect();
const Color color_background{0, 0, 64};
const Color color_foreground = Theme::getInstance()->fg_green->foreground;
const Color color_reticle{128, 128, 128};
const auto graph_width = static_cast<int>(logger.capacity()) * bar_width;
const auto graph_width = static_cast<int>(temperature_logger.capacity()) * bar_width;
const Rect graph_rect{
rect.left() + (rect.width() - graph_width) / 2, rect.top() + 8,
graph_width, rect.height()};
@@ -25,7 +23,7 @@ void McuTemperatureWidget::paint(Painter& painter) {
painter.draw_rectangle(frame_rect, color_reticle);
painter.fill_rectangle(graph_rect, color_background);
const auto history = logger.history();
const auto history = temperature_logger.history();
for (size_t i = 0; i < history.size(); i++) {
const Coord x = graph_rect.right() - (history.size() - i) * bar_width;
const auto sample = history[i];
@@ -32,7 +32,7 @@
#include "portapack.hpp"
#include "memory_map.hpp"
#include "irq_controls.hpp"
#include "temperature_logger.hpp"
#include <functional>
#include <utility>
@@ -43,14 +43,25 @@ class McuTemperatureWidget : public Widget {
explicit McuTemperatureWidget(
Rect parent_rect)
: Widget{parent_rect} {
signal_token_tick_second = rtc_time::signal_tick_second += [this]() {
this->on_tick_second();
};
}
~McuTemperatureWidget() {
rtc_time::signal_tick_second -= signal_token_tick_second;
}
void on_tick_second() {
temperature_logger.second_tick();
set_dirty();
}
void paint(Painter& painter) override;
private:
TemperatureLogger temperature_logger{};
using sample_t = uint32_t;
using temperature_t = int32_t;
SignalToken signal_token_tick_second{};
temperature_t temperature(const sample_t sensor_value) const;
Coord screen_y(const temperature_t temperature, const Rect& screen_rect) const;
@@ -25,6 +25,8 @@
#include <algorithm>
namespace ui::external_app::mcu_temperature {
void TemperatureLogger::second_tick() {
sample_phase++;
if (sample_phase >= sample_interval) {
@@ -67,3 +69,5 @@ void TemperatureLogger::push_sample(const TemperatureLogger::sample_t sample) {
samples_count++;
sample_phase = 0;
}
} // namespace ui::external_app::mcu_temperature
@@ -27,6 +27,8 @@
#include <array>
#include <vector>
namespace ui::external_app::mcu_temperature {
class TemperatureLogger {
public:
using sample_t = int8_t;
@@ -49,4 +51,6 @@ class TemperatureLogger {
void push_sample(const sample_t sample);
};
} // namespace ui::external_app::mcu_temperature
#endif /*__TEMPERATURE_LOGGER_H__*/
+1 -1
View File
@@ -1,5 +1,5 @@
/*
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
* copyleft 2024 zxkmm
*
* This file is part of PortaPack.
*
+1 -1
View File
@@ -1,5 +1,5 @@
/*
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
* copyleft 2024 zxkmm
*
* This file is part of PortaPack.
*
+1 -1
View File
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2024 Samir Sánchez Garnica @sasaga92
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
* copyleft 2024 zxkmm
*
* This file is part of PortaPack.
+1 -1
View File
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2024 Samir Sánchez Garnica @sasaga92
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
* copyleft 2024 zxkmm
*
* This file is part of PortaPack.
*
+1 -1
View File
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2024 HTotoo
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
* copyleft 2024 zxkmm
*
* This file is part of PortaPack.
*
+1 -1
View File
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2024 HTotoo
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
* copyleft 2024 zxkmm
*
* This file is part of PortaPack.
*
@@ -1,5 +1,5 @@
/*
* copyleft 2025 zxkmm AKA zix aka sommermorgentraum
* copyleft 2025 zxkmm
*
* This file is part of PortaPack.
*
@@ -1,5 +1,5 @@
/*
* copyleft 2025 zxkmm AKA zix aka sommermorgentraum
* copyleft 2025 zxkmm
*
* This file is part of PortaPack.
*
+48
View File
@@ -0,0 +1,48 @@
/*
* Copyright (C) 2026 Matej Sochan
*
* 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 "ui.hpp"
#include "ui_signal_hunter.hpp"
#include "ui_navigation.hpp"
#include "external_app.hpp"
namespace ui::external_app::signal_hunter {
void initialize_app(ui::NavigationView& nav) {
nav.push<SignalHunterAppView>();
}
} // namespace ui::external_app::signal_hunter
extern "C" {
__attribute__((section(".external_app.app_signal_hunter.application_information"), used)) application_information_t _application_information_signal_hunter = {
/*.memory_location = */ (uint8_t*)0x00000000,
/*.externalAppEntry = */ ui::external_app::signal_hunter::initialize_app,
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
/*.app_name = */ "SigHunter",
/*.bitmap_data = */ {0x80, 0x01, 0x80, 0x01, 0xE0, 0x07, 0xF0, 0x0F, 0x98, 0x19, 0x8C, 0x31, 0x84, 0x21, 0xFF, 0xFF, 0xFF, 0xFF, 0x84, 0x21, 0x8C, 0x31, 0x98, 0x19, 0xF0, 0x0F, 0xE0, 0x07, 0x80, 0x01, 0x80, 0x01},
/*.icon_color = */ ui::Color::green().v,
/*.menu_location = */ app_location_t::RX,
/*.desired_menu_position = */ -1,
/*.m4_app_tag = */ {'H', 'U', 'N', 'T'},
/*.m4_app_offset = */ 0x00000000,
};
}
@@ -0,0 +1,403 @@
/*
* Copyright (C) 2026 Matej Sochan
*
* 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 "ui_signal_hunter.hpp"
#include "receiver_model.hpp"
#include "baseband_api.hpp"
#include "ui_fileman.hpp"
#include "freqman_db.hpp"
#include "string_format.hpp"
#include "io_file.hpp"
#include "rtc_time.hpp"
using namespace portapack;
namespace ui::external_app::signal_hunter {
// --- TAB 1: Main View ---
HunterMainView::HunterMainView(Rect parent_rect, SignalHunterAppView& parent)
: View(parent_rect), parent_app(parent) {
add_children({&text_current_freq, &text_status, &button_start_stop, &text_hits});
update_frequency(433920000);
button_start_stop.on_select = [this](Button&) {
parent_app.is_hunting = !parent_app.is_hunting;
if (parent_app.is_hunting) {
button_start_stop.set_text("STOP");
// Set initial frequency based on set mode (single freq / freq hopping)
if (parent_app.freq_hop_mode && !parent_app.frequency_list.empty()) {
parent_app.current_freq_index = 0;
auto freq = parent_app.frequency_list[0];
receiver_model.set_target_frequency(freq);
update_frequency(freq);
} else {
receiver_model.set_target_frequency(parent_app.single_frequency);
update_frequency(parent_app.single_frequency);
}
update_status("HUNTING...", Theme::getInstance()->fg_green);
parent_app.send_hunter_config(true);
} else {
button_start_stop.set_text("START");
update_status("IDLE", Theme::getInstance()->fg_light);
set_recording_state(false);
parent_app.send_hunter_config(false);
}
};
}
void HunterMainView::on_show() {
if (!parent_app.freq_hop_mode) {
current_freq_ = receiver_model.target_frequency();
}
update_frequency(current_freq_); // force repaint
}
void HunterMainView::focus() {
if (!parent_app.freq_hop_mode) {
current_freq_ = receiver_model.target_frequency();
}
update_frequency(current_freq_);
button_start_stop.focus();
}
void HunterMainView::set_recording_state(bool recording) {
if (recording)
text_current_freq.set_style(Theme::getInstance()->fg_red);
else
text_current_freq.set_style(Theme::getInstance()->fg_light);
text_current_freq.set_dirty(); // force ui update
update_frequency(current_freq_); // repaint with the new color
}
void HunterMainView::update_frequency(rf::Frequency freq) {
current_freq_ = freq;
text_current_freq.set(to_string_dec_uint(freq) + " Hz");
}
void HunterMainView::update_status(const std::string& status, const Style* style) {
text_status.set(status);
text_status.set_style(style);
}
void HunterMainView::update_hits(uint32_t hits) {
text_hits.set("Hits: " + to_string_dec_uint(hits));
}
// --- TAB 2: Freqs View ---
HunterFreqsView::HunterFreqsView(Rect parent_rect, SignalHunterAppView& parent)
: View(parent_rect), parent_app(parent) {
add_children({&button_load_file,
&button_clear,
&labels,
&field_dwell,
&text_loaded_info});
field_dwell.set_value(parent_app.hop_dwell_ms);
field_dwell.on_change = [this](int32_t v) { parent_app.hop_dwell_ms = v; };
button_load_file.on_select = [this](Button&) {
auto* view = parent_app.get_nav().push<FileLoadView>(".TXT");
if (view) {
view->on_changed = [this](std::filesystem::path path) {
parent_app.frequency_list.clear();
parent_app.current_freq_index = 0;
FreqmanDB db{};
db.open(path);
for (const auto& entry : db) {
if (entry.frequency_a > 0)
parent_app.frequency_list.push_back(entry.frequency_a);
}
parent_app.freqman_file = path.stem().string();
// Switch to HOP mode if a file was loaded
if (!parent_app.frequency_list.empty())
parent_app.freq_hop_mode = true;
update_list_count();
// Update the mode display in the Config tab
if (parent_app.get_config_view()) {
parent_app.get_config_view()->update_mode_display();
}
};
}
};
button_clear.on_select = [this](Button&) {
parent_app.frequency_list.clear();
parent_app.current_freq_index = 0;
parent_app.freq_hop_mode = false;
update_list_count();
// Update the mode display in the Config tab
if (parent_app.get_config_view()) {
parent_app.get_config_view()->update_mode_display();
}
};
}
void HunterFreqsView::update_list_count() {
text_loaded_info.set("Loaded: " +
to_string_dec_uint(parent_app.frequency_list.size()) + " freqs");
}
void HunterFreqsView::focus() {
button_load_file.focus();
}
// --- TAB 3: Config View ---
HunterConfigView::HunterConfigView(Rect parent_rect, SignalHunterAppView& parent)
: View(parent_rect), parent_app(parent), field_single_freq{{UI_POS_X(2), UI_POS_Y(4)}, parent.get_nav()} {
add_children({&button_mode,
&field_single_freq,
&labels,
&field_threshold,
&field_hang_time,
&text_info_config});
button_mode.on_select = [this](Button&) {
parent_app.freq_hop_mode = !parent_app.freq_hop_mode;
update_mode_display();
};
field_single_freq.set_value(parent_app.single_frequency);
field_threshold.set_value(parent_app.energy_threshold);
field_threshold.on_change = [this](int32_t v) { parent_app.energy_threshold = v; };
field_hang_time.set_value(parent_app.hangtime_ms);
field_hang_time.on_change = [this](int32_t v) { parent_app.hangtime_ms = v; };
update_mode_display();
}
void HunterConfigView::on_show() {
auto current = receiver_model.target_frequency();
parent_app.single_frequency = current;
field_single_freq.set_value(current);
}
void HunterConfigView::update_mode_display() {
if (parent_app.freq_hop_mode)
button_mode.set_text("MODE: HOP (" +
to_string_dec_uint(parent_app.frequency_list.size()) + " freqs)");
else
button_mode.set_text("MODE: SINGLE");
}
void HunterConfigView::focus() {
field_threshold.focus();
}
// --- Main App View ---
SignalHunterAppView::SignalHunterAppView(ui::NavigationView& nav)
: frequency_list{},
nav_(nav) {
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
receiver_model.set_target_frequency(433920000);
receiver_model.set_baseband_bandwidth(1750000);
receiver_model.set_sampling_rate(2000000);
receiver_model.enable();
view_main = std::make_unique<HunterMainView>(content_rect, *this);
view_freqs = std::make_unique<HunterFreqsView>(content_rect, *this);
view_config = std::make_unique<HunterConfigView>(content_rect, *this);
tab_view = std::make_unique<TabView>(std::initializer_list<TabView::TabDef>{
{"Target", Color::cyan(), view_main.get()},
{"Freqs", Color::green(), view_freqs.get()},
{"Config", Color::yellow(), view_config.get()}});
tab_view->set_parent_rect(view_rect);
add_children({&field_lna,
&field_vga,
&field_rf_amp,
&rssi,
tab_view.get(),
view_main.get(),
view_freqs.get(),
view_config.get()});
view_freqs->hidden(true);
view_config->hidden(true);
using namespace app_settings;
settings_ = std::make_unique<SettingsManager>(
"ext_signal_hunter", Mode::RX,
SettingBindings{
{"threshold"sv, &energy_threshold},
{"hangtime_ms"sv, &hangtime_ms},
{"hop_dwell_ms"sv, &hop_dwell_ms},
{"single_freq"sv, &single_frequency},
{"file"sv, &freqman_file}});
}
SignalHunterAppView::~SignalHunterAppView() {
if (!freq_hop_mode) {
single_frequency = receiver_model.target_frequency();
}
// Safely shutdown recording
stop_recording();
receiver_model.disable();
baseband::shutdown();
}
void SignalHunterAppView::focus() {
if (tab_view)
tab_view->focus();
}
// TIMER LOGIC: Based on display refresh rate
void SignalHunterAppView::on_frame_sync() {
// Timer running only if, HUNTING, in HOPPING mode, not recording + we have the freqs list
if (is_hunting && freq_hop_mode && !capture_thread && !frequency_list.empty()) {
hop_timer_ms += 17; // 1 frame in a 60 Hz display ~ 16.6 ms
// if Dwell Time has passed, change frequency
if (hop_timer_ms >= hop_dwell_ms) {
hop_timer_ms = 0;
current_freq_index = (current_freq_index + 1) % frequency_list.size();
auto next_freq = frequency_list[current_freq_index];
receiver_model.set_target_frequency(next_freq);
if (view_main) {
view_main->update_frequency(next_freq);
}
}
} else {
hop_timer_ms = 0; // if not hunting / recording -> timer = 0
}
}
// Recording control logic via M0 <-> M4 IPC
void SignalHunterAppView::send_hunter_config(bool start) {
// Send config to baseband processor via IPC mechanism
baseband::set_hunter_config(energy_threshold, hangtime_ms, start);
if (!start) {
stop_recording();
}
}
void SignalHunterAppView::on_hunter_trigger(const HunterTriggerMessage* /*message*/) {
// M4 has detected signal above threshold - start I/Q capture
if (!capture_thread && is_hunting) {
trigger_hits++;
view_main->update_hits(trigger_hits);
view_main->update_status("RECORDING!", Theme::getInstance()->fg_red);
view_main->set_recording_state(true);
start_recording();
}
}
void SignalHunterAppView::on_hunter_stop(const HunterStopMessage*) {
if (capture_thread) {
stop_recording();
view_main->set_recording_state(false);
if (is_hunting) {
if (freq_hop_mode && !frequency_list.empty()) {
// Next frequency
current_freq_index = (current_freq_index + 1) % frequency_list.size();
auto next_freq = frequency_list[current_freq_index];
receiver_model.set_target_frequency(next_freq);
view_main->update_frequency(next_freq);
// Reset the timer
hop_timer_ms = 0;
}
view_main->update_status("HUNTING...", Theme::getInstance()->fg_green);
}
}
}
void SignalHunterAppView::start_recording() {
rtc::RTC datetime{};
rtc_time::now(datetime);
// Generate timestamped filename for capture session
const std::string capture_filename = "HNT_" +
to_string_dec_uint(datetime.year(), 4, '0') +
to_string_dec_uint(datetime.month(), 2, '0') +
to_string_dec_uint(datetime.day(), 2, '0') + "T" +
to_string_dec_uint(datetime.hour(), 2, '0') +
to_string_dec_uint(datetime.minute(), 2, '0') +
to_string_dec_uint(datetime.second(), 2, '0');
// Use FileWriter for direct raw I/Q dump to avoid M0 CPU bottlenecks.
// FileConvertWriter is heavier
auto writer = std::make_unique<FileWriter>();
auto create_error = writer->create(std::filesystem::path(u"/CAPTURES") / (capture_filename + ".C16"));
if (create_error.is_valid()) {
if (view_main) view_main->update_status("SD ERROR", Theme::getInstance()->fg_red);
send_hunter_config(false);
is_hunting = false;
if (view_main) view_main->set_start_button_text("START");
return;
}
current_capture_filename = capture_filename;
capture_thread = std::make_unique<CaptureThread>(
std::move(writer),
4096,
4,
[]() {},
[](File::Error) {});
if (view_main) view_main->update_status("RECORDING!", Theme::getInstance()->fg_red);
}
void SignalHunterAppView::stop_recording() {
// Stop I/Q capture and release SD card for metadata write
capture_thread.reset();
// Generate metadata file after capture completes
if (!current_capture_filename.empty()) {
File txt_file;
auto txt_error = txt_file.create(std::filesystem::path(u"/CAPTURES") / (current_capture_filename + ".TXT"));
// is_valid() returns true only if error is present (counter-intuitive)
if (!txt_error.is_valid()) {
std::string metadata =
"center_frequency=" + to_string_dec_uint(receiver_model.target_frequency()) + "\n" +
// Divide main sample rate by decimation factor (8) to match C16 sample rate
"sample_rate=" + to_string_dec_uint(receiver_model.sampling_rate() / 8) + "\n";
txt_file.write(metadata.c_str(), metadata.length());
}
// Clear filename for next capture cycle
current_capture_filename.clear();
}
}
} // namespace ui::external_app::signal_hunter
@@ -0,0 +1,192 @@
/*
* Copyright (C) 2026 Matej Sochan
*
* 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 __UI_SIGNAL_HUNTER_H__
#define __UI_SIGNAL_HUNTER_H__
#include "app_settings.hpp"
#include "rf_path.hpp"
#include "ui_widget.hpp"
#include "ui_navigation.hpp"
#include "ui_receiver.hpp"
#include "ui_tabview.hpp"
#include "ui_freq_field.hpp"
#include "capture_thread.hpp"
#include "message.hpp"
#include <vector>
#include <memory>
namespace ui::external_app::signal_hunter {
class SignalHunterAppView;
// --- TAB 1: Main View ---
class HunterMainView : public View {
public:
HunterMainView(Rect parent_rect, SignalHunterAppView& parent);
void on_show() override;
void focus() override;
void update_status(const std::string& status, const Style* style);
void update_hits(uint32_t hits);
void update_frequency(rf::Frequency freq);
void set_recording_state(bool recording);
void set_start_button_text(const std::string& text) { button_start_stop.set_text(text); }
private:
SignalHunterAppView& parent_app;
rf::Frequency current_freq_{433920000};
Text text_current_freq{{UI_POS_X_CENTER(14), UI_POS_Y(3), 112, 16}, ""};
Text text_status{{UI_POS_X_CENTER(12), UI_POS_Y(6), 96, 16}, "IDLE"};
Button button_start_stop{{UI_POS_X_CENTER(10), UI_POS_Y(8), 80, 32}, "START"};
Text text_hits{{UI_POS_X_CENTER(10), UI_POS_Y(11), 80, 16}, "Hits: 0"};
};
// --- TAB 2: Freqs View ---
class HunterFreqsView : public View {
public:
HunterFreqsView(Rect parent_rect, SignalHunterAppView& parent);
void focus() override;
void update_list_count();
private:
SignalHunterAppView& parent_app;
Button button_load_file{{UI_POS_X(2), UI_POS_Y(1), 80, 28}, "LOAD FILE"};
Button button_clear{{UI_POS_X_RIGHT(12), UI_POS_Y(1), 80, 28}, "CLEAR"};
Labels labels{
{{UI_POS_X(2), UI_POS_Y(4)}, "Dwell Time (ms):", Color::light_grey()}};
NumberField field_dwell{{UI_POS_X_RIGHT(8), UI_POS_Y(4)}, 4, {10, 9999}, 10, ' '};
Text text_loaded_info{{UI_POS_X(2), UI_POS_Y(6), 208, 16}, "Loaded: 0 freqs"};
};
// --- TAB 3: Config View ---
class HunterConfigView : public View {
public:
HunterConfigView(Rect parent_rect, SignalHunterAppView& parent);
void update_mode_display();
void focus() override;
void on_show() override;
private:
SignalHunterAppView& parent_app;
Button button_mode{
{UI_POS_X(2), UI_POS_Y(2), 208, 28},
"MODE: SINGLE"};
RxFrequencyField field_single_freq; // UI_POS_Y(4)
Labels labels{
{{UI_POS_X(2), UI_POS_Y(6)}, "Energy Threshold:", Color::light_grey()},
{{UI_POS_X(2), UI_POS_Y(8)}, "Hang-Time (ms):", Color::light_grey()}};
NumberField field_threshold{{UI_POS_X_RIGHT(8), UI_POS_Y(6)}, 5, {100, 99999}, 100, ' '};
NumberField field_hang_time{{UI_POS_X_RIGHT(8), UI_POS_Y(8)}, 4, {10, 5000}, 10, ' '};
Text text_info_config{
{UI_POS_X(2), UI_POS_Y(10), 208, 16},
"Restart HUNT after change"};
};
// --- MAIN APP VIEW ---
class SignalHunterAppView final : public ui::View {
public:
SignalHunterAppView(ui::NavigationView& nav);
~SignalHunterAppView();
void focus() override;
std::string title() const override { return "SignalHunter"; }
ui::NavigationView& get_nav() { return nav_; }
// Application state shared across all views
std::vector<rf::Frequency> frequency_list;
uint32_t current_freq_index{0};
uint32_t energy_threshold{5000};
uint32_t hangtime_ms{500};
uint32_t hop_dwell_ms{200};
std::string freqman_file{"TARGETS"};
bool is_hunting{false};
uint32_t trigger_hits{0};
rf::Frequency single_frequency{433920000};
bool freq_hop_mode{false};
void send_hunter_config(bool start);
HunterMainView* get_main_view() { return view_main.get(); }
HunterConfigView* get_config_view() { return view_config.get(); }
private:
ui::NavigationView& nav_;
std::unique_ptr<app_settings::SettingsManager> settings_{};
std::unique_ptr<HunterMainView> view_main{};
std::unique_ptr<HunterFreqsView> view_freqs{};
std::unique_ptr<HunterConfigView> view_config{};
std::unique_ptr<TabView> tab_view{};
std::string current_capture_filename{};
std::unique_ptr<CaptureThread> capture_thread{};
ui::LNAGainField field_lna{{UI_POS_X(0), UI_POS_Y(0)}};
ui::VGAGainField field_vga{{UI_POS_X(7), UI_POS_Y(0)}};
ui::RFAmpField field_rf_amp{{UI_POS_X(14), UI_POS_Y(0)}};
ui::RSSI rssi{{UI_POS_X(0), UI_POS_Y(1), UI_POS_MAXWIDTH, 4}};
static constexpr Dim tab_bar_h = 20;
Rect view_rect{
0, UI_POS_Y(2) + 4, UI_POS_MAXWIDTH,
screen_height - (UI_POS_Y(2) + 4) - UI_POS_HEIGHT(1)};
Rect content_rect{
0, UI_POS_Y(2) + 4 + tab_bar_h, UI_POS_MAXWIDTH,
screen_height - (UI_POS_Y(2) + 4 + tab_bar_h) - UI_POS_HEIGHT(1)};
void on_hunter_trigger(const HunterTriggerMessage* message);
void on_hunter_stop(const HunterStopMessage* message);
void start_recording();
void stop_recording();
// timer variables
uint32_t hop_timer_ms{0};
void on_frame_sync();
MessageHandlerRegistration message_handler_frame_sync{
Message::ID::DisplayFrameSync,
[this](const Message* const) {
this->on_frame_sync();
}};
MessageHandlerRegistration message_handler_trigger{
Message::ID::HunterTrigger,
[this](const Message* const p) {
this->on_hunter_trigger(static_cast<const HunterTriggerMessage*>(p));
}};
MessageHandlerRegistration message_handler_stop{
Message::ID::HunterStop,
[this](const Message* const p) {
this->on_hunter_stop(static_cast<const HunterStopMessage*>(p));
}};
};
} // namespace ui::external_app::signal_hunter
#endif /*__UI_SIGNAL_HUNTER_H__*/
+1 -1
View File
@@ -1,6 +1,6 @@
/*
* Copyright 2025 Mark Thompson
* copyleft 2025 zxkmm AKA zix aka sommermorgentraum
* copyleft 2025 zxkmm
*
* This file is part of PortaPack.
*
+1 -1
View File
@@ -1,6 +1,6 @@
/*
* Copyright 2025 Mark Thompson
* copyleft 2025 zxkmm AKA zix aka sommermorgentraum
* copyleft 2025 zxkmm
*
* This file is part of PortaPack.
*
+81
View File
@@ -0,0 +1,81 @@
/*
* Copyright (C) 2026 HTotoo
*
* 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 "ui_tetra_rx.hpp"
#include "ui_navigation.hpp"
#include "external_app.hpp"
namespace ui::external_app::tetra_rx {
void initialize_app(ui::NavigationView& nav) {
nav.push<TetraRxView>();
}
} // namespace ui::external_app::tetra_rx
extern "C" {
__attribute__((section(".external_app.app_tetra_rx.application_information"), used)) application_information_t _application_information_tetra_rx = {
/*.memory_location = */ (uint8_t*)0x00000000,
/*.externalAppEntry = */ ui::external_app::tetra_rx::initialize_app,
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
/*.app_name = */ "Tetra",
/*.bitmap_data = */ {
0xE0,
0x0F,
0x18,
0x38,
0xE4,
0x67,
0x7E,
0xCE,
0xC7,
0xCC,
0x00,
0x00,
0xFF,
0x4F,
0xBA,
0xB2,
0x9A,
0xEE,
0xBA,
0xB2,
0x00,
0x00,
0x3B,
0xE3,
0x73,
0x7E,
0xC6,
0x27,
0x1C,
0x18,
0xF0,
0x07,
},
/*.icon_color = */ ui::Color::orange().v,
/*.menu_location = */ app_location_t::RX,
/*.desired_menu_position = */ -1,
/*.m4_app_tag = portapack::spi_flash::image_tag_tetrarx*/ {'P', 'T', 'E', 'T'},
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
};
}
+45
View File
@@ -0,0 +1,45 @@
#pragma once
#include <stdint.h>
#include <stddef.h>
namespace ui::external_app::tetra_rx {
class BitVector {
public:
BitVector(uint8_t* p, size_t bits)
: data_(p),
bits_(bits) {
}
inline bool get(size_t bit) const {
return (data_[bit >> 3] >>
(7 - (bit & 7))) &
1;
}
inline void set(size_t bit, bool value) {
if (value)
data_[bit >> 3] |=
1 << (7 - (bit & 7));
else
data_[bit >> 3] &=
~(1 << (7 - (bit & 7)));
}
inline void xor_bit(size_t bit, bool value) {
if (value)
data_[bit >> 3] ^=
1 << (7 - (bit & 7));
}
inline size_t size() const {
return bits_;
}
private:
uint8_t* data_;
size_t bits_;
};
} // namespace ui::external_app::tetra_rx
+33
View File
@@ -0,0 +1,33 @@
#include "tetra_crc.hpp"
namespace ui::external_app::tetra_rx {
uint16_t CRC::crc16_itut_bits(
const uint8_t* bits,
uint32_t n_bits,
uint16_t init) {
uint16_t crc = init;
for (uint32_t i = 0; i < n_bits; i++) {
uint8_t bit =
(bits[i >> 3] >>
(7 - (i & 7))) &
1;
crc ^= uint16_t(bit) << 15;
if (crc & 0x8000) {
crc =
(crc << 1) ^
0x1021;
} else {
crc <<= 1;
}
crc &= 0xffff;
}
return crc;
}
} // namespace ui::external_app::tetra_rx
+17
View File
@@ -0,0 +1,17 @@
#pragma once
#include <stdint.h>
namespace ui::external_app::tetra_rx {
class CRC {
public:
static constexpr uint16_t CRC_OK = 0x1d0f;
static uint16_t crc16_itut_bits(
const uint8_t* bits,
uint32_t n_bits,
uint16_t init = 0xffff);
};
} // namespace ui::external_app::tetra_rx
@@ -0,0 +1,60 @@
#include "tetra_descrambler.hpp"
namespace ui::external_app::tetra_rx {
static inline uint8_t lfsr_step(uint32_t& s) {
uint8_t b = 0;
b ^= (s >> (32 - 32)) & 1;
b ^= (s >> (32 - 26)) & 1;
b ^= (s >> (32 - 23)) & 1;
b ^= (s >> (32 - 22)) & 1;
b ^= (s >> (32 - 16)) & 1;
b ^= (s >> (32 - 12)) & 1;
b ^= (s >> (32 - 11)) & 1;
b ^= (s >> (32 - 10)) & 1;
b ^= (s >> (32 - 8)) & 1;
b ^= (s >> (32 - 7)) & 1;
b ^= (s >> (32 - 5)) & 1;
b ^= (s >> (32 - 4)) & 1;
b ^= (s >> (32 - 2)) & 1;
b ^= (s >> (32 - 1)) & 1;
s = (s >> 1) | (uint32_t(b) << 31);
return b;
}
void Descrambler::generate(
uint32_t init,
uint8_t* seq,
size_t bits) {
uint32_t s = init;
for (size_t i = 0; i < bits; i++)
seq[i] = lfsr_step(s);
}
void Descrambler::descramble(
BitVector& bits,
uint32_t init) {
uint32_t s = init;
for (size_t i = 0; i < bits.size(); i++)
bits.xor_bit(i, lfsr_step(s));
}
uint32_t Descrambler::dmo_scramb_init(
uint32_t mni,
uint32_t src) {
uint32_t init =
(src & 0xFFFFFF) |
((mni & 0x3F) << 24);
init <<= 2;
init |= 3;
return init;
}
} // namespace ui::external_app::tetra_rx
@@ -0,0 +1,26 @@
#pragma once
#include <stdint.h>
#include "tetra_bits.hpp"
namespace ui::external_app::tetra_rx {
class Descrambler {
public:
static constexpr uint32_t SCRAMB_INIT_BSCH = 0x00000003;
static void descramble(
BitVector& bits,
uint32_t init = SCRAMB_INIT_BSCH);
static void generate(
uint32_t init,
uint8_t* sequence,
size_t bits);
static uint32_t dmo_scramb_init(
uint32_t mni,
uint32_t src);
};
} // namespace ui::external_app::tetra_rx
@@ -0,0 +1,21 @@
#include "tetra_interleave.hpp"
namespace ui::external_app::tetra_rx {
void Interleave::block_deinterleave(
BitVector& in,
BitVector& out,
uint32_t K,
uint32_t a) {
for (uint32_t i = 1; i <= K; i++) {
uint32_t k =
1 +
((a * i) % K);
out.set(
i - 1,
in.get(k - 1));
}
}
} // namespace ui::external_app::tetra_rx
@@ -0,0 +1,16 @@
#pragma once
#include "tetra_bits.hpp"
namespace ui::external_app::tetra_rx {
class Interleave {
public:
static void block_deinterleave(
BitVector& in,
BitVector& out,
uint32_t K,
uint32_t a);
};
} // namespace ui::external_app::tetra_rx
+24
View File
@@ -0,0 +1,24 @@
#include "tetra_rcpc.hpp"
#include <string.h>
namespace ui::external_app::tetra_rx {
static constexpr uint8_t P_RATE_2_3[3] = {1, 2, 5};
static constexpr uint32_t T_RATE_2_3 = 3;
static constexpr uint32_t PERIOD = 8;
void RCPC::depuncture_2_3(BitVector& in, uint8_t* out, uint32_t in_bits) {
const uint32_t mother_bits = ((in_bits + T_RATE_2_3 - 1) / T_RATE_2_3) * PERIOD + PERIOD;
memset(out, ERASED, mother_bits);
for (uint32_t j = 1; j <= in_bits; j++) {
uint32_t i = j;
uint32_t k = PERIOD * ((i - 1) / T_RATE_2_3);
k += P_RATE_2_3[(i - 1) % T_RATE_2_3];
if (k >= 1 && k <= mother_bits) {
out[k - 1] = in.get(j - 1);
}
}
}
} // namespace ui::external_app::tetra_rx
+18
View File
@@ -0,0 +1,18 @@
#pragma once
#include <stdint.h>
#include "tetra_bits.hpp"
namespace ui::external_app::tetra_rx {
class RCPC {
public:
static constexpr uint8_t ERASED = 0xff;
static void depuncture_2_3(
BitVector& in,
uint8_t* out,
uint32_t in_bits);
};
} // namespace ui::external_app::tetra_rx
@@ -0,0 +1,73 @@
#include "tetra_viterbi.hpp"
#include <string.h>
namespace ui::external_app::tetra_rx {
int Viterbi::decode_cch(
const uint8_t* mother,
uint32_t n_info,
uint8_t* bits,
uint8_t* trace_buffer) { // Puffer by caller
// Store these on stack, to avoid flash space usage
const uint8_t next_state[16][2] = {
{0, 1}, {2, 3}, {4, 5}, {6, 7}, {8, 9}, {10, 11}, {12, 13}, {14, 15}, {0, 1}, {2, 3}, {4, 5}, {6, 7}, {8, 9}, {10, 11}, {12, 13}, {14, 15}};
const uint8_t next_output[16][2] = {
{0, 15}, {11, 4}, {6, 9}, {13, 2}, {5, 10}, {14, 1}, {3, 12}, {8, 7}, {15, 0}, {4, 11}, {9, 6}, {2, 13}, {10, 5}, {1, 14}, {12, 3}, {7, 8}};
const uint16_t INF = 0x3fff;
uint16_t cost[16];
uint16_t new_cost[16];
// Initialization
for (int i = 0; i < 16; i++) cost[i] = INF;
cost[0] = 0;
for (uint32_t step = 0; step < n_info; step++) {
for (int i = 0; i < 16; i++) new_cost[i] = INF;
const uint8_t* rx = &mother[step * 4];
for (int prev = 0; prev < 16; prev++) {
if (cost[prev] >= INF) continue;
for (int bit = 0; bit < 2; bit++) {
const uint8_t pattern = next_output[prev][bit];
const uint8_t next = next_state[prev][bit];
uint16_t d = 0;
for (int b = 0; b < 4; b++) {
if (rx[b] != 0xff && rx[b] != ((pattern >> (3 - b)) & 1)) d++;
}
const uint16_t total = cost[prev] + d;
if (total < new_cost[next]) {
new_cost[next] = total;
trace_buffer[step * 16 + next] = (prev << 1) | bit;
}
}
}
memcpy(cost, new_cost, sizeof(cost));
}
// Traceback
uint8_t best_state = 0;
for (int i = 1; i < 16; i++) {
if (cost[i] < cost[best_state]) best_state = i;
}
uint8_t state = best_state;
memset(bits, 0, (n_info + 7) >> 3);
for (int step = (int)n_info - 1; step >= 0; step--) {
uint8_t packed = trace_buffer[step * 16 + state];
uint8_t prev = (packed >> 1) & 0x0F;
uint8_t bit = packed & 0x01;
if (bit) bits[step >> 3] |= (1 << (7 - (step & 7)));
state = prev;
}
return cost[best_state];
}
} // namespace ui::external_app::tetra_rx
@@ -0,0 +1,17 @@
#pragma once
#include <stdint.h>
#include <string.h>
namespace ui::external_app::tetra_rx {
class Viterbi {
public:
int decode_cch(
const uint8_t* mother,
uint32_t n_info,
uint8_t* bits,
uint8_t* trace_buffer);
};
} // namespace ui::external_app::tetra_rx
+354
View File
@@ -0,0 +1,354 @@
#include "ui_tetra_rx.hpp"
#include "rtc_time.hpp"
#include "baseband_api.hpp"
#include "string_format.hpp"
#include "portapack_persistent_memory.hpp"
#include "file_path.hpp"
#include "tetra_crc.hpp"
#include "tetra_descrambler.hpp"
#include "tetra_interleave.hpp"
#include "tetra_rcpc.hpp"
#include "tetra_viterbi.hpp"
#include <cstring>
using namespace portapack;
using namespace ui;
namespace ui::external_app::tetra_rx {
template <size_t OutBytes>
bool TetraChannelDecoder::decode_block(
const std::array<uint8_t, 63>& burst,
size_t offset,
size_t len_t5,
size_t type1_bits,
size_t type2_bits,
size_t interleave_a,
std::array<uint8_t, OutBytes>& out,
uint16_t& crc,
int& cost,
uint32_t scramb_init) {
std::array<uint8_t, 64> t5{};
std::array<uint8_t, 64> t3{};
std::array<uint8_t, 1200> mother{};
std::array<uint8_t, 64> type2{};
out.fill(0);
for (size_t i = 0; i < len_t5; i++) {
set_bit(t5, i, get_bit(burst, offset + i));
}
BitVector t5_bits{t5.data(), len_t5};
Descrambler::descramble(t5_bits, scramb_init);
BitVector t3_bits{t3.data(), len_t5};
Interleave::block_deinterleave(t5_bits, t3_bits, len_t5, interleave_a);
RCPC::depuncture_2_3(t3_bits, mother.data(), len_t5);
Viterbi viterbi;
cost = viterbi.decode_cch(mother.data(), type2_bits, type2.data(), trace_buffer);
crc = CRC::crc16_itut_bits(type2.data(), type1_bits + 16);
for (size_t i = 0; i < type1_bits; i++) {
set_bit(out, i, (type2[i >> 3] >> (7 - (i & 7))) & 1);
}
return crc == CRC::CRC_OK;
}
TetraChannelDecoder::Result TetraChannelDecoder::decode_dnb(const TetraDnbMessage& message) {
Result result{};
result.inverted = message.inverted;
if (!network_synced)
return result;
std::array<uint8_t, 63> buffer{};
std::copy(message.payload.begin(), message.payload.end(), buffer.begin());
uint32_t tmo_dnb_seed = (((uint32_t)last_mcc << 20) | ((uint32_t)last_mnc << 6) | (uint32_t)last_bcc) << 2 | 3;
// 1. DNB Full slot (SCH/F)
bool ok = decode_block(buffer, 0, SCH_F_LEN_BITS, SCH_F_TYPE1_BITS, SCH_F_TYPE2_BITS,
SCH_F_INTERLEAVE_A, result.payload, result.crc, result.cost, tmo_dnb_seed);
// 2. SCH/HD Block 1
if (!ok) {
ok = decode_block(buffer, 0, BLK2_LEN_BITS, SB2_TYPE1_BITS, SB2_TYPE2_BITS,
SB2_INTERLEAVE_A, result.payload, result.crc, result.cost, tmo_dnb_seed);
}
// 3. SCH/HD Block 2
if (!ok) {
ok = decode_block(buffer, 216, BLK2_LEN_BITS, SB2_TYPE1_BITS, SB2_TYPE2_BITS,
SB2_INTERLEAVE_A, result.payload, result.crc, result.cost, tmo_dnb_seed);
}
if (ok) {
result.type = Result::Type::Dnb;
result.is_ok = true;
parse_mac_pdu(result);
}
return result;
}
TetraChannelDecoder::Result TetraChannelDecoder::decode_burst(const TetraBurstMessage& message) {
Result result{};
result.sync_errors = message.sync_errors;
result.inverted = message.inverted;
// 1. SCH/S
bool ok = decode_block(
message.payload, BLK1_OFFSET_BITS, BLK1_LEN_BITS,
SB1_TYPE1_BITS, SB1_TYPE2_BITS, SB1_INTERLEAVE_A,
result.payload, result.crc, result.cost,
Descrambler::SCRAMB_INIT_BSCH);
if (!ok) return result;
result.type = Result::Type::Sync;
result.is_ok = true;
parse_sync_pdu(result);
uint16_t current_mcc = result.mcc;
uint16_t current_mnc = result.mnc;
uint8_t current_bcc = result.bcc;
uint8_t current_enc = result.encryption;
// 2. SCH/H
uint16_t h_crc = 0;
int h_cost = 0;
ok = decode_block(
message.payload, TMO_BLK2_OFFSET_BITS, BLK2_LEN_BITS,
SB2_TYPE1_BITS, SB2_TYPE2_BITS, SB2_INTERLEAVE_A,
result.payload, h_crc, h_cost,
Descrambler::SCRAMB_INIT_BSCH);
if (ok) {
result.type = Result::Type::SyncFull;
result.crc = h_crc;
result.cost = h_cost;
result.mcc = current_mcc;
result.mnc = current_mnc;
result.bcc = current_bcc;
result.encryption = current_enc;
parse_mac_pdu(result);
} else {
result.encryption = current_enc;
}
return result;
}
uint32_t TetraChannelDecoder::read_bits(const uint8_t* bytes, size_t bit, size_t count) const {
uint32_t value = 0;
for (size_t i = 0; i < count; i++) {
value <<= 1;
value |= (bytes[(bit + i) >> 3] >> (7 - ((bit + i) & 7))) & 1;
}
return value;
}
void TetraChannelDecoder::parse_sync_pdu(Result& result) {
// Sys 4 bit, then BCC
result.bcc = read_bits(result.payload.data(), 4, 6);
result.timeslot = read_bits(result.payload.data(), 10, 2);
result.frame_number = read_bits(result.payload.data(), 12, 5);
result.encryption = read_bits(result.payload.data(), 30, 1);
result.mcc = read_bits(result.payload.data(), 31, 10);
result.mnc = read_bits(result.payload.data(), 41, 14);
last_mcc = result.mcc;
last_mnc = result.mnc;
last_bcc = result.bcc;
network_synced = true;
}
void TetraChannelDecoder::parse_mac_pdu(Result& result) {
result.pdu_type = read_bits(result.payload.data(), 0, 2);
if (result.pdu_type == 0) { // MAC-RESOURCE
result.encryption = read_bits(result.payload.data(), 4, 2);
uint8_t length_ind = read_bits(result.payload.data(), 7, 6);
if (result.encryption == 0 && length_ind > 0 && length_ind < 62) {
size_t offset = 13;
uint8_t addr_type = read_bits(result.payload.data(), offset, 3);
offset += 3;
if (addr_type == 1) {
result.calling_ssi = read_bits(result.payload.data(), offset, 24);
offset += 24;
} else if (addr_type == 2) {
offset += 10;
} else if (addr_type == 3 || addr_type == 4) {
result.calling_ssi = read_bits(result.payload.data(), offset, 24);
offset += 24;
} else if (addr_type == 5) {
offset += 34;
} else if (addr_type == 6) {
offset += 30;
} else if (addr_type == 7) {
offset += 34;
}
// skip optional fields
if (read_bits(result.payload.data(), offset++, 1)) offset += 4; // Power control
if (read_bits(result.payload.data(), offset++, 1)) offset += 8; // Slot grant
uint8_t chan_alloc = read_bits(result.payload.data(), offset++, 1);
if (chan_alloc == 0) {
uint8_t llc_type = read_bits(result.payload.data(), offset, 4);
offset += 4;
// LLC header skipped
if (llc_type == 0 || llc_type == 1)
offset += 2;
else if (llc_type == 2 || llc_type == 3 || llc_type == 6 || llc_type == 7)
offset += 1;
uint8_t mle_type = read_bits(result.payload.data(), offset, 3);
offset += 3;
// 1 = CMCE
if (mle_type == 1) {
result.cmce_type = read_bits(result.payload.data(), offset, 5);
offset += 5;
result.call_id = read_bits(result.payload.data(), offset, 14);
}
}
}
} else if (result.pdu_type == 2) { // SYSINFO/BCAST
uint8_t bcast_type = read_bits(result.payload.data(), 2, 2);
if (bcast_type == 0) {
result.la = read_bits(result.payload.data(), 82, 14);
result.encryption = read_bits(result.payload.data(), 122, 1);
}
}
}
TetraRxView::TetraRxView(NavigationView& nav)
: nav_{nav} {
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
add_children({&rssi, &channel, &field_rf_amp, &field_lna, &field_vga,
&field_frequency, &text_mcc, &text_la, &text_pdu,
&text_mnc, &text_ts, &text_fn, &text_bcc, &text_enc, &text_debug, &console});
field_frequency.set_step(25000);
receiver_model.set_modulation(ReceiverModel::Mode::NarrowbandFMAudio);
receiver_model.set_sampling_rate(3072000);
receiver_model.set_baseband_bandwidth(1750000);
receiver_model.set_squelch_level(0);
receiver_model.enable();
}
void TetraRxView::focus() {
field_frequency.focus();
}
void TetraRxView::on_data_tetra(const TetraBurstMessage& message) {
packet_count++;
auto result = decoder.decode_burst(message);
if (result.type == TetraChannelDecoder::Result::Type::Sync ||
result.type == TetraChannelDecoder::Result::Type::SyncFull) {
valid_count++;
text_mcc.set("MCC: " + to_string_dec_uint(result.mcc));
text_mnc.set("MNC: " + to_string_dec_uint(result.mnc));
text_bcc.set("BCC: " + to_string_dec_uint(result.bcc));
text_ts.set("TS: " + to_string_dec_uint(result.timeslot));
text_fn.set("FN: " + to_string_dec_uint(result.frame_number));
std::string enc_str = (result.encryption != 0) ? "Encrypted" : "Clear";
text_enc.set("ENC: " + enc_str);
if (result.type == TetraChannelDecoder::Result::Type::SyncFull) {
h_valid_count++;
if (result.la != 0xffff) text_la.set("LA: " + to_string_dec_uint(result.la));
text_pdu.set("PDU: " + decoder.get_pdu_name(result.pdu_type));
}
}
text_debug.set(
"Syn: " + to_string_dec_uint(packet_count) +
", V: " + to_string_dec_uint(valid_count) +
", H: " + to_string_dec_uint(h_valid_count) +
", E:" + to_string_dec_uint(result.sync_errors));
}
void TetraRxView::on_data_dnb(const TetraDnbMessage& message) {
dnb_count++;
auto result = decoder.decode_dnb(message);
if (result.is_ok && result.type == TetraChannelDecoder::Result::Type::Dnb) {
if (result.la != 0xffff) text_la.set("LA: " + to_string_dec_uint(result.la));
text_pdu.set("PDU: " + decoder.get_pdu_name(result.pdu_type));
std::string enc_str = (result.encryption != 0) ? "Encrypted" : "Clear";
text_enc.set("ENC: " + enc_str);
// if (rate_limiter++ < 2) console.writeln("DNB: " + decoder.get_pdu_name(result.pdu_type));
if (result.cmce_type != 0xff) {
std::string msg = "";
if (result.cmce_type == 7)
msg = "SETUP";
else if (result.cmce_type == 1)
msg = "CALL PROCEEDING";
else if (result.cmce_type == 0)
msg = "ALERTING";
else if (result.cmce_type == 2)
msg = "CONNECT";
else if (result.cmce_type == 3)
msg = "CONNECT ACK";
else if (result.cmce_type == 11)
msg = "TX GRANTED";
else if (result.cmce_type == 9)
msg = "TX CEASED";
else if (result.cmce_type == 13)
msg = "TX WAIT";
else if (result.cmce_type == 10)
msg = "TX CONTINUE";
else if (result.cmce_type == 12)
msg = "TX INTERRUPT";
else if (result.cmce_type == 4)
msg = "DISCONNECT";
else if (result.cmce_type == 6)
msg = "RELEASE";
else if (result.cmce_type == 5)
msg = "INFO";
else if (result.cmce_type == 8)
msg = "STATUS";
else if (result.cmce_type == 14)
msg = "SDS DATA";
else if (result.cmce_type == 15)
msg = "FACILITY";
else if (result.cmce_type == 16)
msg = "CALL RESTORE";
else if (result.cmce_type == 31)
msg = "NOT SUPPORTED";
else
msg = "CMCE:" + to_string_dec_uint(result.cmce_type);
console.writeln(msg);
if (result.call_id != 0xffff) {
console.writeln("ID:" + to_string_dec_uint(result.call_id) + " | SSI:" + to_string_dec_uint(result.calling_ssi));
}
}
}
}
void TetraRxView::on_timer() {
rate_limiter = 0;
}
TetraRxView::~TetraRxView() {
receiver_model.disable();
baseband::shutdown();
}
} // namespace ui::external_app::tetra_rx
+192
View File
@@ -0,0 +1,192 @@
#ifndef __UI_TETRA_RX_H__
#define __UI_TETRA_RX_H__
#include "ui.hpp"
#include "ui_language.hpp"
#include "ui_navigation.hpp"
#include "ui_receiver.hpp"
#include "ui_freq_field.hpp"
#include "app_settings.hpp"
#include "radio_state.hpp"
#include "log_file.hpp"
#include "utility.hpp"
#include "message.hpp"
#include <array>
#include <string>
using namespace ui;
namespace ui::external_app::tetra_rx {
class TetraChannelDecoder {
public:
struct Result {
enum class Type { None,
Sync,
SyncFull,
Dnb };
Type type{Type::None};
bool is_ok{false};
uint8_t sync_errors{0};
bool inverted{false};
uint16_t crc{0};
int cost{0};
uint8_t timeslot{0xff};
uint8_t frame_number{0xff};
uint16_t mcc{0xffff};
uint16_t mnc{0xffff};
uint8_t bcc{0xff};
uint8_t pdu_type{0xff};
uint16_t la{0xffff};
uint8_t encryption{0xff};
uint8_t cmce_type{0xff};
uint16_t call_id{0xffff};
uint32_t calling_ssi{0};
std::array<uint8_t, 34> payload{};
};
std::string get_pdu_name(uint8_t pdu_type) {
switch (pdu_type) {
case 0:
return "MAC-RESOURCE";
case 1:
return "MAC-FRAG";
case 2:
return "SYSINFO/BCAST";
case 3:
return "MAC-U-SIGNAL";
default:
return "UNK_" + std::to_string(pdu_type);
}
}
Result decode_burst(const TetraBurstMessage& message);
Result decode_dnb(const TetraDnbMessage& message);
private:
static constexpr size_t BLK1_OFFSET_BITS = 94;
static constexpr size_t BLK1_LEN_BITS = 120;
static constexpr size_t TMO_BLK2_OFFSET_BITS = 282;
static constexpr size_t BLK2_LEN_BITS = 216;
static constexpr size_t SB1_TYPE1_BITS = 60;
static constexpr size_t SB1_TYPE2_BITS = 80;
static constexpr size_t SB1_INTERLEAVE_A = 11;
static constexpr size_t SB2_TYPE1_BITS = 124;
static constexpr size_t SB2_TYPE2_BITS = 144;
static constexpr size_t SB2_INTERLEAVE_A = 101;
static constexpr size_t SCH_F_LEN_BITS = 432;
static constexpr size_t SCH_F_TYPE1_BITS = 268;
static constexpr size_t SCH_F_TYPE2_BITS = 288;
static constexpr size_t SCH_F_INTERLEAVE_A = 103;
uint16_t last_mcc{0};
uint16_t last_mnc{0};
uint8_t last_bcc{0};
bool network_synced{false};
// Viterbi buffer
uint8_t trace_buffer[9216];
template <size_t OutBytes>
bool decode_block(const std::array<uint8_t, 63>& burst,
size_t offset,
size_t len_t5,
size_t type1_bits,
size_t type2_bits,
size_t interleave_a,
std::array<uint8_t, OutBytes>& out,
uint16_t& crc,
int& cost,
uint32_t scramb_init);
void parse_sync_pdu(Result& result);
void parse_mac_pdu(Result& result);
uint32_t read_bits(const uint8_t* bytes, size_t bit, size_t count) const;
template <size_t N>
uint8_t get_bit(const std::array<uint8_t, N>& arr, size_t bit_idx) const {
return (arr[bit_idx / 8] >> (7 - (bit_idx % 8))) & 0x01;
}
template <size_t N>
void set_bit(std::array<uint8_t, N>& arr, size_t bit_idx, uint8_t val) const {
if (val)
arr[bit_idx / 8] |= (1 << (7 - (bit_idx % 8)));
else
arr[bit_idx / 8] &= ~(1 << (7 - (bit_idx % 8)));
}
};
class TetraRxView : public View {
public:
TetraRxView(NavigationView& nav);
~TetraRxView();
void focus() override;
std::string title() const override { return "Tetra RX"; };
private:
NavigationView& nav_;
RxRadioState radio_state_{};
app_settings::SettingsManager settings_{"rx_tetra", app_settings::Mode::RX};
uint32_t packet_count{0};
uint32_t valid_count{0};
uint32_t h_valid_count{0};
uint32_t dnb_count{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)}};
RSSI rssi{{UI_POS_X_RIGHT(9), UI_POS_Y(0), UI_POS_WIDTH(9), 4}};
Channel channel{{UI_POS_X_RIGHT(9), UI_POS_Y(0) + 5, UI_POS_WIDTH(9), 4}};
Text text_mcc{{UI_POS_X(0), UI_POS_Y(1), UI_POS_WIDTH(15), UI_POS_HEIGHT(1)}, "MCC: ---"};
Text text_mnc{{UI_POS_X(15), UI_POS_Y(1), UI_POS_WIDTH(15), UI_POS_HEIGHT(1)}, "MNC: ---"};
Text text_ts{{UI_POS_X(0), UI_POS_Y(2), UI_POS_WIDTH(15), UI_POS_HEIGHT(1)}, "TS: -"}; // time slot
Text text_fn{{UI_POS_X(15), UI_POS_Y(2), UI_POS_WIDTH(15), UI_POS_HEIGHT(1)}, "FN: -"}; // frame number
Text text_bcc{{UI_POS_X(0), UI_POS_Y(3), UI_POS_WIDTH(15), UI_POS_HEIGHT(1)}, "BCC: -"}; // Base Station Color Code
Text text_enc{{UI_POS_X(15), UI_POS_Y(3), UI_POS_WIDTH(15), UI_POS_HEIGHT(1)}, "ENC: -"}; // encoded or not
Text text_la{{UI_POS_X(0), UI_POS_Y(4), UI_POS_WIDTH(15), UI_POS_HEIGHT(1)}, "LA: ----"}; // Location Area
Text text_pdu{{UI_POS_X(15), UI_POS_Y(4), UI_POS_WIDTH(15), UI_POS_HEIGHT(1)}, "PDU: ----"}; // pdu content type
Text text_debug{{UI_POS_X(0), UI_POS_Y(5), UI_POS_MAXWIDTH, UI_POS_HEIGHT(1)}, "Syn: 0, V: 0, H: 0, E:0"}; // sync, valid, h_valid, errors corrected in top part
Console console{{UI_POS_X(0), UI_POS_Y(6) + 8, UI_POS_MAXWIDTH, screen_height - (UI_POS_Y(7) + 8)}};
TetraChannelDecoder decoder{};
void on_data_tetra(const TetraBurstMessage&);
void on_data_dnb(const TetraDnbMessage&);
uint8_t rate_limiter{0}; // limiter, to keep the device responsive
void on_timer();
MessageHandlerRegistration message_handler_frame_sync{
Message::ID::DisplayFrameSync,
[this](const Message* const) {
this->on_timer();
}};
MessageHandlerRegistration message_handler_packet{
Message::ID::TetraBsch,
[this](Message* const p) {
this->on_data_tetra(*static_cast<const TetraBurstMessage*>(p));
}};
MessageHandlerRegistration message_handler_dnb{
Message::ID::TetraDnb,
[this](Message* const p) {
this->on_data_dnb(*static_cast<const TetraDnbMessage*>(p));
}};
};
} // namespace ui::external_app::tetra_rx
#endif
+1 -1
View File
@@ -1,5 +1,5 @@
/*
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
* copyleft 2024 zxkmm
*
* This file is part of PortaPack.
*
+1 -1
View File
@@ -1,5 +1,5 @@
/*
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
* copyleft 2024 zxkmm
*
* This file is part of PortaPack.
*
+85
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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__
@@ -28,11 +28,47 @@
#include "ui_fileman.hpp"
#include "file_reader.hpp"
#include "ui_textentry.hpp"
#include "convert.hpp"
#include <algorithm>
using namespace portapack;
namespace ui::external_app::waterfall_designer {
namespace {
/* Parses an "index,R,G,B" profile line.
* NB: std::stoi() must not be used here - the firmware is built with
* -fno-exceptions, so any malformed field in a hand-edited profile makes it
* call std::terminate() instead of throwing. parse_int() is what gradient.cpp
* uses to read the very same file format. */
bool parse_level(std::string_view line, uint8_t& index, uint8_t& r, uint8_t& g, uint8_t& b) {
// Comments are skipped by Gradient::load_file() too; parse_int() ignores
// trailing junk, so "#0,0,0,0" would otherwise read back as a real level.
if (line.empty() || line.front() == '#') return false;
auto cols = split_string(line, ',');
if (cols.size() != 4) return false;
uint8_t* const out[4] = {&index, &r, &g, &b};
for (size_t i = 0; i < 4; i++) {
int32_t value;
if (!parse_int(cols[i], value) || value < 0 || value > 255) return false;
*out[i] = static_cast<uint8_t>(value);
}
return true;
}
bool is_color_level(const std::string& line) {
uint8_t index, r, g, b;
return parse_level(line, index, r, g, b);
}
} // namespace
WaterfallDesignerView::WaterfallDesignerView(NavigationView& nav)
: nav_{nav} {
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
@@ -152,7 +188,11 @@ WaterfallDesignerView::WaterfallDesignerView(
}
WaterfallDesignerView::~WaterfallDesignerView() {
if (!if_apply_setting) restore_current_profile();
if (!if_apply_setting)
restore_current_profile();
else if (profile_backed_up_)
delete_file(waterfalls_dir / u"wtf_des_bk.bk"); // kept the edit, drop the backup
receiver_model.disable();
baseband::shutdown();
}
@@ -176,35 +216,68 @@ void WaterfallDesignerView::on_open_profile() {
auto open_view = nav_.push<FileLoadView>(".txt");
open_view->push_dir(waterfalls_dir);
open_view->on_changed = [this](std::filesystem::path new_file_path) {
on_profile_changed(new_file_path);
// NB: FileLoadView calls this and then pops, same as text_prompt does.
// Defer for the same two reasons - see on_create_new_profile().
pending_profile_path = std::move(new_file_path);
nav_.set_on_pop([this]() {
on_profile_changed(pending_profile_path);
});
};
}
void WaterfallDesignerView::on_profile_changed(std::filesystem::path new_profile_path) {
current_profile_path = new_profile_path;
bool WaterfallDesignerView::read_profile_file(const std::filesystem::path& path) {
File profile_file;
if (profile_file.open(path)) return false;
profile_levels.clear();
File playlist_file;
auto error = playlist_file.open(new_profile_path.string());
if (error) return;
menu_view.clear();
auto reader = FileLineReader(playlist_file);
auto reader = FileLineReader(profile_file);
for (const auto& line : reader) {
// remove empty lines
if (line == "\n" || line == "\r\n" || line == "\r") continue;
profile_levels.push_back(line);
auto entry = line;
// Strip the whole line terminator; the old code only dropped one char,
// so CRLF files kept a stray '\r' in every entry.
while (!entry.empty() && (entry.back() == '\n' || entry.back() == '\r'))
entry.pop_back();
if (entry.empty()) continue;
profile_levels.push_back(std::move(entry));
}
for (auto& line : profile_levels) {
// remove line end \n etc
if (line.length() > 0 && (line[line.length() - 1] == '\n' || line[line.length() - 1] == '\r')) {
line = line.substr(0, line.length() - 1);
}
return true;
}
bool WaterfallDesignerView::write_profile_file(const std::filesystem::path& path) {
File profile_file;
if (profile_file.open(path, false, true)) return false;
// FA_OPEN_ALWAYS leaves the position at end-of-file, so rewind before
// truncating or the existing contents are kept and appended to.
if (profile_file.seek(0).is_error()) return false;
if (profile_file.truncate().is_error()) return false;
for (const auto& entry : profile_levels)
if (profile_file.write_line(entry)) return false;
/* ~File() calls f_close(), which flushes, but its result is unreachable.
* Sync explicitly so a full or yanked card is reported to the caller
* instead of being announced as a successful save. */
return !profile_file.sync();
}
void WaterfallDesignerView::on_profile_changed(const std::filesystem::path& new_profile_path) {
// read_profile_file() only touches profile_levels once the open succeeded,
// so a failure here leaves the previously loaded profile intact.
if (!read_profile_file(new_profile_path)) {
nav_.display_modal("Err", "open err");
return;
}
current_profile_path = new_profile_path;
highlighted_index_ = 0;
button_save.hidden(false);
button_add_level.hidden(false);
button_remove_level.hidden(false);
@@ -219,53 +292,52 @@ void WaterfallDesignerView::refresh_menu_view() {
menu_view.clear();
for (const auto& line : profile_levels) {
if (line.length() == 0 || line[0] == '#') {
uint8_t index, r, g, b;
// index,R,G,B - anything else (comment, header, garbage) is shown greyed.
if (parse_level(line, index, r, g, b)) {
menu_view.add_item({line,
ui::Color(r, g, b),
&bitmap_icon_cwgen,
[this](KeyEvent) {
button_remove_level.focus();
}});
} else {
menu_view.add_item({line,
ui::Color::grey(),
&bitmap_icon_notepad,
[this](KeyEvent) {
button_add_level.focus();
}});
} else {
// index,R,G,B
size_t pos = 0;
size_t next_pos = 0;
// pass index
next_pos = line.find(',', pos);
if (next_pos == std::string::npos) continue;
pos = next_pos + 1;
// r
next_pos = line.find(',', pos);
if (next_pos == std::string::npos) continue;
uint8_t r = static_cast<uint8_t>(std::stoi(line.substr(pos, next_pos - pos)));
pos = next_pos + 1;
// g
next_pos = line.find(',', pos);
if (next_pos == std::string::npos) continue;
uint8_t g = static_cast<uint8_t>(std::stoi(line.substr(pos, next_pos - pos)));
pos = next_pos + 1;
// b
uint8_t b = static_cast<uint8_t>(std::stoi(line.substr(pos)));
ui::Color color = ui::Color(r, g, b);
menu_view.add_item({line,
color,
&bitmap_icon_cwgen,
[this](KeyEvent) {
button_remove_level.focus();
}});
}
}
// menu_view.clear() resets its own highlight to 0 but does not fire
// on_highlight, so highlighted_index_ has to be re-synced by hand or the
// edit/remove buttons act on a different row than the one the user sees.
if (highlighted_index_ >= profile_levels.size())
highlighted_index_ = profile_levels.empty() ? 0 : profile_levels.size() - 1;
if (!profile_levels.empty())
menu_view.set_highlighted(highlighted_index_);
set_dirty();
}
void WaterfallDesignerView::on_apply_current_to_wtf() {
std::filesystem::path system_read_path = "waterfall.txt";
copy_file(current_profile_path, system_read_path);
/* Take the backup lazily, right before the first overwrite of the live
* gradient. Doing it in the constructor meant copy_file() (~1.8kB of stack)
* ran underneath run_external_app(), which holds a 776 byte frame with a
* File of its own - about 3.6kB of the 4kB M0 process stack. It also did SD
* I/O even when the user just opened the app and backed straight out. */
backup_current_profile();
/* NB: this used to copy_file(current_profile_path, "waterfall.txt"), which
* costs ~1.8kB of stack (two FILs + a 512 byte block buffer) out of the 4kB
* M0 process stack, on top of whatever UI callback we are nested under.
* profile_levels is the authoritative copy of the profile anyway, so write
* it straight out through a single File instead. */
if (!write_profile_file(u"waterfall.txt")) return;
waterfall.load_gradient();
@@ -281,71 +353,82 @@ void WaterfallDesignerView::on_save_profile() {
return;
}
File profile_file;
auto error = profile_file.open(current_profile_path.string(), false, false);
if (error) {
nav_.display_modal("Err", "open err");
// NB: not just "open err" any more - this now also covers a failed
// truncate/write/flush, i.e. the file may be partially written.
if (!write_profile_file(current_profile_path)) {
nav_.display_modal("Err", "write failed");
return;
}
// clear file
profile_file.seek(0);
profile_file.truncate();
// write new data
for (const auto& entry : profile_levels) {
profile_file.write_line(entry);
}
nav_.display_modal("Save", "Saved profile\n" + current_profile_path.string());
}
void WaterfallDesignerView::on_add_level() {
if (highlighted_index_ >= profile_levels.size()) return;
if (profile_levels[highlighted_index_].empty()) return;
if (profile_levels[highlighted_index_][0] == '#') return;
if (profile_levels[highlighted_index_].find(',') == std::string::npos) return;
size_t insert_pos = highlighted_index_;
std::string new_entry = "0,128,128,128";
profile_levels.insert(profile_levels.begin() + insert_pos, new_entry);
if (current_profile_path.empty()) return;
/* A freshly created profile has no rows at all, and the old guard
* (highlighted_index_ >= size()) then refused with no feedback - so the
* button looked dead on exactly the file you just made. Seed instead. */
size_t insert_pos = std::min<size_t>(highlighted_index_, profile_levels.size());
// Don't push a level above a comment/header row; drop it after instead.
if (insert_pos < profile_levels.size() && !is_color_level(profile_levels[insert_pos]))
insert_pos++;
profile_levels.insert(profile_levels.begin() + insert_pos, "0,128,128,128");
highlighted_index_ = insert_pos;
refresh_menu_view();
on_edit_color();
}
void WaterfallDesignerView::on_remove_level() {
if (highlighted_index_ >= profile_levels.size()) return;
if (profile_levels[highlighted_index_].empty()) return;
if (profile_levels[highlighted_index_][0] == '#') return;
if (profile_levels[highlighted_index_].find(',') == std::string::npos) return;
if (!is_color_level(profile_levels[highlighted_index_])) return;
profile_levels.erase(profile_levels.begin() + highlighted_index_);
refresh_menu_view();
}
void WaterfallDesignerView::on_edit_color() {
if (highlighted_index_ >= profile_levels.size()) return;
if (profile_levels[highlighted_index_].empty()) return;
if (profile_levels[highlighted_index_][0] == '#') return;
if (profile_levels[highlighted_index_].find(',') == std::string::npos) return;
if (!is_color_level(profile_levels[highlighted_index_])) return;
auto color_picker_view = nav_.push<WaterfallDesignerColorPickerView>(profile_levels[highlighted_index_]);
color_picker_view->on_save = [this](std::string new_color) {
profile_levels[highlighted_index_] = new_color;
// Capture the row by value: highlighted_index_ can move (or the list can
// shrink) while the picker is up, and the old code wrote to whatever
// highlighted_index_ happened to be at save time with no bounds check.
const size_t index = highlighted_index_;
auto color_picker_view = nav_.push<WaterfallDesignerColorPickerView>(profile_levels[index]);
color_picker_view->on_save = [this, index](std::string new_color) {
if (index >= profile_levels.size()) return;
profile_levels[index] = std::move(new_color);
refresh_menu_view();
on_apply_current_to_wtf();
};
}
void WaterfallDesignerView::backup_current_profile() {
std::filesystem::path curren_wtf_path = "waterfall.txt";
std::filesystem::path backup_path = waterfalls_dir / "wtf_des_bk.bk";
copy_file(curren_wtf_path, backup_path);
if (backup_attempted_) return;
backup_attempted_ = true;
std::filesystem::path current_wtf_path = u"waterfall.txt";
std::filesystem::path backup_path = waterfalls_dir / u"wtf_des_bk.bk";
if (!file_exists(current_wtf_path)) return;
profile_backed_up_ = copy_file(current_wtf_path, backup_path).ok();
}
void WaterfallDesignerView::restore_current_profile() {
std::filesystem::path backup_path = waterfalls_dir / "wtf_des_bk.bk";
std::filesystem::path put_back_path = "waterfall.txt";
copy_file(backup_path, put_back_path);
// Only touch waterfall.txt if we actually took a backup on entry; otherwise
// this would silently do nothing and leave the edited gradient installed.
if (!profile_backed_up_) return;
std::filesystem::path backup_path = waterfalls_dir / u"wtf_des_bk.bk";
copy_file(backup_path, u"waterfall.txt");
delete_file(backup_path);
}
@@ -365,16 +448,35 @@ void WaterfallDesignerView::on_create_new_profile() {
buffer += ".txt";
}
File new_file;
auto error = new_file.create(waterfalls_dir / buffer);
if (error) {
nav_.display_modal("Err", "create file err");
auto new_path = waterfalls_dir / buffer;
bool created;
{
// Scoped so the FIL is closed (and its 512 byte sector cache is
// off the stack) before anything re-opens the same file - _FS_LOCK
// is 0, so FatFs will not stop us from double-opening it.
File new_file;
created = !new_file.create(new_path);
}
/* NB: everything below has to run *after* the keyboard view is gone.
* text_prompt()'s caller invokes this handler and then pops, so:
* - pushing a modal from here would pop the modal, not the keyboard;
* - and running on_profile_changed() inline stacks its File plus the
* gradient write under the alphanum/button/dispatch frames, which
* is what blows the 4kB M0 process stack ("Stack Overflow" guru).
* set_on_pop() defers it to just after the pop, at a shallow depth. */
if (!created) {
nav_.set_on_pop([this]() {
nav_.display_modal("Err", "create file err");
});
return;
}
profile_levels.clear();
current_profile_path = waterfalls_dir / buffer;
on_profile_changed(current_profile_path);
pending_profile_path = new_path;
nav_.set_on_pop([this]() {
on_profile_changed(pending_profile_path);
});
});
}
@@ -392,32 +494,7 @@ WaterfallDesignerColorPickerView::WaterfallDesignerColorPickerView(NavigationVie
progressbar.set_max(UINT8_MAX);
size_t pos = 0;
size_t next_pos = 0;
// index
next_pos = color_str.find(',', pos);
if (next_pos != std::string::npos) {
index_ = static_cast<uint8_t>(std::stoi(color_str.substr(pos, next_pos - pos)));
pos = next_pos + 1;
}
// r
next_pos = color_str.find(',', pos);
if (next_pos != std::string::npos) {
red_ = static_cast<uint8_t>(std::stoi(color_str.substr(pos, next_pos - pos)));
pos = next_pos + 1;
}
// g
next_pos = color_str.find(',', pos);
if (next_pos != std::string::npos) {
green_ = static_cast<uint8_t>(std::stoi(color_str.substr(pos, next_pos - pos)));
pos = next_pos + 1;
}
// b
blue_ = static_cast<uint8_t>(std::stoi(color_str.substr(pos)));
parse_level(color_str_, index_, red_, green_, blue_);
field_red.set_value(red_);
field_green.set_value(green_);
@@ -466,19 +543,12 @@ void WaterfallDesignerColorPickerView::update_color_index() {
green_ = static_cast<uint8_t>(field_green.value());
blue_ = static_cast<uint8_t>(field_blue.value());
const Rect preview_rect{screen_width - 48, 1 * 16, 40, 40};
Painter painter_instance_2;
painter_instance_2.fill_rectangle(
{preview_rect.left(), preview_rect.top(), preview_rect.width(), preview_rect.height()},
ui::Color(red_, green_, blue_));
// Repaint through the normal dirty pass instead of driving the display from
// inside an on_change handler; paint() below already draws the swatch.
set_dirty();
}
void WaterfallDesignerColorPickerView::paint(Painter& painter) {
// this is not duplicated code.
// because need to display color when enter,
// but it is too early to call update_color() in the constructor.
const Rect preview_rect{screen_width - 48, 1 * 16, 40, 40};
painter.fill_rectangle(
@@ -487,14 +557,12 @@ void WaterfallDesignerColorPickerView::paint(Painter& painter) {
}
std::string WaterfallDesignerColorPickerView::build_color_str() {
size_t index_pos = color_str_.find(',');
if (index_pos != std::string::npos) {
return color_str_.substr(0, index_pos + 1) +
to_string_dec_uint(red_) + "," +
to_string_dec_uint(green_) + "," +
to_string_dec_uint(blue_);
}
return to_string_dec_uint(index_) + "," + to_string_dec_uint(red_) + "," + to_string_dec_uint(green_) + "," + to_string_dec_uint(blue_);
// Always emit index_ - the old version kept the index substring from the
// input line, so edits made with field_index were silently discarded.
return to_string_dec_uint(index_) + "," +
to_string_dec_uint(red_) + "," +
to_string_dec_uint(green_) + "," +
to_string_dec_uint(blue_);
}
} /* namespace ui::external_app::waterfall_designer */
@@ -217,18 +217,31 @@ class WaterfallDesignerView : public View {
void restore_current_profile();
void on_create_new_profile();
void on_open_profile();
void on_profile_changed(std::filesystem::path new_profile_path);
void on_profile_changed(const std::filesystem::path& new_profile_path);
void on_save_profile();
void on_add_level();
void on_remove_level();
void on_edit_color();
/* Each of these owns the only File in its call chain. Keeping them as
* separate frames matters: a File carries a 512 byte FIL sector cache
* (_FS_TINY is 0) and the M0 process stack is 4kB, so if read_profile_file
* merged into on_profile_changed its 784 byte frame would stay live across
* the copy_file() in on_apply_current_to_wtf() - 3.6kB of 4kB instead of
* 2.8kB. GCC does not inline them at -O2 today; noinline just pins that so
* a heuristic change can't quietly eat the headroom. */
__attribute__((noinline)) bool read_profile_file(const std::filesystem::path& path);
__attribute__((noinline)) bool write_profile_file(const std::filesystem::path& path);
void refresh_menu_view();
void on_apply_current_to_wtf(); // will restore if didn't apple, when distruct
void on_apply_setting(); // apply set
bool if_apply_setting{false};
bool backup_attempted_{false};
bool profile_backed_up_{false};
std::filesystem::path pending_profile_path{}; // set by the New dialog, consumed on_pop
/*NB:
this works as:
each time you change color, it apply as file realtime
+85 -22
View File
@@ -28,10 +28,15 @@
#include "max2831.hpp"
#include "portapack_adc.hpp"
#include "hackrf_hal.hpp"
#include "hackrf_gpio.hpp"
using namespace hackrf::one;
#include "gpio.hpp"
using namespace gpio_control;
#include "ch.h"
#include "hal.h"
@@ -151,7 +156,7 @@ void MAX2831::set_mode(const Mode mode) {
* RX: ENABLE=1, RXTX=0
* TX: ENABLE=1, RXTX=1
*
* Note: gpio_max2831_rx_enable is the RXTX mode select pin.
* Note: max2831_rxtx_enable is the RXTX mode select pin.
* RXTX=0 selects RX, RXTX=1 selects TX.
*/
@@ -174,26 +179,26 @@ void MAX2831::set_mode(const Mode mode) {
switch (mode) {
default:
case Mode::Shutdown:
gpio_max2831_rx_enable.write(0); /* RXTX=0 */
gpio_max283x_enable.write(0); /* ENABLE=0 */
max2831_rxtx_enable.setInactive(); /* RXTX=0 */
max283x_enable.setInactive(); /* ENABLE=0 */
set_rssi_mux(0);
break;
case Mode::Standby:
gpio_max2831_rx_enable.write(1); /* RXTX=1 */
gpio_max283x_enable.write(0); /* ENABLE=0 */
max2831_rxtx_enable.setActive(); /* RXTX=1 */
max283x_enable.setInactive(); /* ENABLE=0 */
set_rssi_mux(0);
break;
case Mode::Transmit:
case Mode::Tx_Calibration:
gpio_max2831_rx_enable.write(1); /* RXTX=1 for TX */
gpio_max283x_enable.write(1); /* ENABLE=1 */
max2831_rxtx_enable.setActive(); /* RXTX=1 for TX */
max283x_enable.setActive(); /* ENABLE=1 */
set_rssi_mux(2); // transmit power
break;
case Mode::Receive:
case Mode::Rx_Calibration:
gpio_max2831_rx_enable.write(0); /* RXTX=0 for RX */
gpio_max283x_enable.write(1); /* ENABLE=1 */
set_rssi_mux(1); // RSSI
max2831_rxtx_enable.setInactive(); /* RXTX=0 for RX */
max283x_enable.setActive(); /* ENABLE=1 */
set_rssi_mux(1); // RSSI
break;
}
@@ -227,8 +232,15 @@ void MAX2831::set_lna_gain(const int_fast8_t db) {
} else {
gain_val = REG11_LNA_GAIN_M33;
}
max2831_rxhp.setActive(); // Fast DC offset compensation
set_reg_field(11, REG11_LNA_GAIN_MASK, gain_val);
flush_reg(11);
chThdSleepMicroseconds(2);
max2831_rxhp.setInactive();
}
void MAX2831::set_vga_gain(const int_fast8_t db) {
@@ -237,10 +249,18 @@ void MAX2831::set_vga_gain(const int_fast8_t db) {
if ((db & 0x1) || db > 62) {
return; /* Invalid: must be even and <= 62 */
}
int_fast8_t db_clipped = std::max(0, std::min(62, (int)db));
uint16_t value = (db_clipped >> 1) & 0x1f;
max2831_rxhp.setActive(); // Fast DC offset compensation
set_reg_field(11, REG11_RXVGA_GAIN_MASK, value);
flush_reg(11);
chThdSleepMicroseconds(2);
max2831_rxhp.setInactive();
}
/*
@@ -347,9 +367,9 @@ void MAX2831::set_lpf_rf_bandwidth_rx(const uint32_t bandwidth_minimum) {
* external Anti-Aliasing (AA) filter on pin P1_14 to prevent aliasing.
*/
if (actual_bw <= 1750000) {
gpio_control::aa_en.set(); // Enable external narrow AA filter
gpio_control::aa_en.setActive(); // Enable external narrow AA filter
} else {
gpio_control::aa_en.clear(); // Disable external AA filter for wideband operations
gpio_control::aa_en.setInactive(); // Disable external AA filter for wideband operations
}
_desired_lpf_bw = actual_bw;
@@ -365,9 +385,9 @@ void MAX2831::set_lpf_rf_bandwidth_rx(const uint32_t bandwidth_minimum) {
void MAX2831::set_lpf_rf_bandwidth_tx(const uint32_t bandwidth_minimum) {
_desired_lpf_bw = bandwidth_minimum;
#ifdef PRALINE
gpio_control::aa_en.clear(); // Disable external AA filter for wideband operations
#endif
gpio_control::aa_en.setInactive(); // Disable external AA filter for wideband operations
if (_mode == Mode::Transmit || _mode == Mode::Tx_Calibration) {
set_lpf_bandwidth_internal(bandwidth_minimum);
}
@@ -461,13 +481,56 @@ void MAX2831::set_rx_buff_vcm(const size_t v) {
}
int8_t MAX2831::temp_sense() {
/* MAX2831 temperature sensor can be read via RSSI MUX.
* This would require:
* 1. Switch RSSI_MUX to temperature mode
* 2. Read the ADC
* 3. Switch back to RSSI mode
* For now, return a placeholder value. */
return 25; /* Room temperature placeholder */
bool not_in_rx = (_mode != Mode::Receive && _mode != Mode::Rx_Calibration);
if (not_in_rx) {
max2831_rxtx_enable.setInactive();
max283x_enable.setActive();
chThdSleepMilliseconds(1);
}
set_rssi_mux(3);
chThdSleepMilliseconds(5);
uint32_t saved_cr = LPC_ADC1->CR;
uint32_t cr_temp = saved_cr;
cr_temp &= ~0xFF;
cr_temp |= (1 << portapack::adc1_rssi_input);
if (((cr_temp >> 8) & 0xFF) == 0) {
cr_temp |= (0xFF << 8);
}
cr_temp &= ~(1 << 16);
cr_temp |= (1 << 21);
cr_temp &= ~(7 << 24);
cr_temp |= (1 << 24);
LPC_ADC1->CR = cr_temp;
uint32_t val;
while (((val = LPC_ADC1->DR[portapack::adc1_rssi_input]) & (1UL << 31)) == 0) {
__asm__("nop");
}
uint32_t adc_raw = (val >> 6) & 0x3FF;
LPC_ADC1->CR = saved_cr;
if (not_in_rx) {
set_mode(_mode);
} else {
set_rssi_mux(1);
}
float v_adc = (adc_raw / 1023.0f) * 3.3f;
constexpr float V_25_CELSIUS = 1.185f;
constexpr float SLOPE = 0.003f;
float temp = 25.0f + ((v_adc - V_25_CELSIUS) / SLOPE);
return static_cast<int8_t>(std::max(-128.0f, std::min(127.0f, temp)));
}
reg_t MAX2831::read(const address_t reg_num) {
+8 -19
View File
@@ -19,12 +19,17 @@
* Boston, MA 02110-1301, USA.
*/
#ifndef PRALINE // not praline
#include "max2837.hpp"
#include "hackrf_hal.hpp"
#include "hackrf_gpio.hpp"
using namespace hackrf::one;
#include "gpio.hpp"
using namespace gpio_control;
#include "ch.h"
#include "hal.h"
@@ -99,12 +104,6 @@ constexpr uint32_t pll_factor = 1.0 / (4.0 / 3.0 / reference_frequency) + 0.5;
void MAX2837::init() {
set_mode(Mode::Shutdown);
#ifndef PRALINE
gpio_max283x_enable.output();
gpio_max2837_rxenable.output();
gpio_max2837_txenable.output();
#endif
_map.r.tx_gain.TXVGA_GAIN_SPI_EN = 1;
_map.r.tx_gain.TXVGA_GAIN_MSB_SPI_EN = 1;
_map.r.tx_gain.TXVGA_GAIN_SPI = 0x00;
@@ -159,12 +158,6 @@ void MAX2837::set_tx_LO_iq_phase_calibration(const size_t v) {
// TX calibration , Logic pins , ENABLE, RXENABLE, TXENABLE = 1,0,1 (5dec), and Reg address 16, D1 (CAL mode 1):DO (CHIP ENABLE 1)
set_mode(Mode::Tx_Calibration); // write to ram 3 LOGIC Pins .
#ifndef PRALINE
gpio_max283x_enable.output();
gpio_max2837_rxenable.output();
gpio_max2837_txenable.output();
#endif
_map.r.spi_en.CAL_SPI = 1; // Register Settings reg address 16, D1 (CAL mode 1)
_map.r.spi_en.EN_SPI = 1; // Register Settings reg address 16, DO (CHIP ENABLE 1)
flush_one(Register::SPI_EN);
@@ -214,7 +207,7 @@ void MAX2837::set_mode(const Mode mode) { // We set up the 3 Logic Pins ENABLE,
_mode = mode;
Mask mask = mode_mask(mode);
gpio_max283x_enable.write(toUType(mask) & toUType(Mask::Enable));
max283x_enable.write(toUType(mask) & toUType(Mask::Enable));
gpio_max2837_rxenable.write(toUType(mask) & toUType(Mask::RxEnable));
gpio_max2837_txenable.write(toUType(mask) & toUType(Mask::TxEnable));
}
@@ -352,12 +345,6 @@ void MAX2837::set_rx_LO_iq_phase_calibration(const size_t v) {
// RX calibration , Logic pins , ENABLE, RXENABLE, TXENABLE = 1,1,0 (3dec), and Reg address 16, D1 (CAL mode 1):DO (CHIP ENABLE 1)
set_mode(Mode::Rx_Calibration); // write to ram 3 LOGIC Pins .
#ifndef PRALINE
gpio_max283x_enable.output();
gpio_max2837_rxenable.output();
gpio_max2837_txenable.output();
#endif
_map.r.spi_en.CAL_SPI = 1; // Register Settings reg address 16, D1 (CAL mode 1)
_map.r.spi_en.EN_SPI = 1; // Register Settings reg address 16, DO (CHIP ENABLE 1)
flush_one(Register::SPI_EN);
@@ -419,3 +406,5 @@ int8_t MAX2837::temp_sense() {
}
} // namespace max2837
#endif // not PRALINE
+10 -19
View File
@@ -19,6 +19,7 @@
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#ifndef PRALINE // not praline
#include "max2839.hpp"
@@ -26,6 +27,9 @@
#include "hackrf_gpio.hpp"
using namespace hackrf::one;
#include "gpio.hpp"
using namespace gpio_control;
#include "ch.h"
#include "hal.h"
@@ -100,11 +104,6 @@ static int_fast8_t requested_rx_vga_gain = 0;
void MAX2839::init() {
set_mode(Mode::Shutdown);
#ifndef PRALINE
gpio_max283x_enable.output();
gpio_max2839_rxtx.output();
#endif
_map.r.rxrf_1.MIMOmode = 1; /* enable RXINB */
_map.r.pa_drv.TXVGA_GAIN_SPI_EN = 1;
@@ -131,6 +130,8 @@ void MAX2839::init() {
_map.r.rssi_vga.RSSI_MODE = 1; /* RSSI independent of RXHP */
_map.r.rssi_vga.RSSI_INPUT = 0; /* Measure RSSI at VGA Output (stronger signal) */
/*
* There are two LNA band settings, but we only use one of them.
* Switching to the other one doesn't make the overall spectrum any
@@ -152,11 +153,6 @@ void MAX2839::set_tx_LO_iq_phase_calibration(const size_t v) {
// TX calibration , 2 x Logic pins , ENABLE, RXENABLE = 1,0, (2dec), and Reg address 16, D1 (CAL mode 1):DO (CHIP ENABLE 1)
set_mode(Mode::Tx_Calibration); // write to ram 3 LOGIC Pins .
#ifndef PRALINE
gpio_max283x_enable.output(); // max2839 has only 2 x pins + regs to decide mode.
gpio_max2839_rxtx.output(); // Here is combined rx & tx pin in one port.
#endif
_map.r.spi_en.CAL_SPI = 1; // Register Settings reg address 16, D1 (CAL mode 1)
_map.r.spi_en.EN_SPI = 1; // Register Settings reg address 16, DO (CHIP ENABLE 1)
flush_one(Register::SPI_EN);
@@ -205,7 +201,7 @@ void MAX2839::set_mode(const Mode mode) {
_mode = mode;
Mask mask = mode_mask(mode);
gpio_max283x_enable.write(toUType(mask) & toUType(Mask::Enable));
max283x_enable.write(toUType(mask) & toUType(Mask::Enable));
gpio_max2839_rxtx.write(toUType(mask) & toUType(Mask::RxTx));
}
@@ -305,9 +301,8 @@ void MAX2839::configure_rx_gain() {
}
_map.r.lpf_vga_2.L = lna::gain_ordinal(lna_gain);
_dirty[Register::RXRF_2] = 1;
_map.r.lpf_vga_2.VGA = vga::gain_ordinal(vga_gain);
_dirty[Register::LPF_VGA_2] = 1;
_dirty[Register::LPF_VGA_2] = 1; /* both L (LNA) and VGA live in LPF_VGA_2 */
flush();
}
@@ -394,11 +389,6 @@ void MAX2839::set_rx_LO_iq_phase_calibration(const size_t v) {
// RX calibration , Logic pins , ENABLE, RXENABLE, TXENABLE = 1,1,0 (3dec), and Reg address 16, D1 (CAL mode 1):DO (CHIP ENABLE 1)
set_mode(Mode::Rx_Calibration); // write to ram 3 LOGIC Pins .
#ifndef PRALINE
gpio_max283x_enable.output(); // max2839 has only 2 x pins + regs to decide mode.
gpio_max2839_rxtx.output(); // Here is combined rx & tx pin in one port.
#endif
_map.r.spi_en.CAL_SPI = 1; // Register Settings reg address 16, D1 (CAL mode 1)
_map.r.spi_en.EN_SPI = 1; // Register Settings reg address 16, DO (CHIP ENABLE 1)
flush_one(Register::SPI_EN);
@@ -445,4 +435,5 @@ int8_t MAX2839::temp_sense() {
return std::min(127, (int)(value * 4.31 - 40)); // reg value is 0 to 31; possible return range is -40 C to 127 C
}
} // namespace max2839
} // namespace max2839
#endif // not PRALINE
+9 -7
View File
@@ -26,9 +26,13 @@
#include "utility.hpp"
#include "hackrf_hal.hpp"
#include "hackrf_gpio.hpp"
using namespace hackrf::one;
#include "gpio.hpp"
using namespace gpio_control;
#include "hal.h"
#ifdef PRALINE
@@ -176,13 +180,11 @@ struct SynthConfig {
void RFFC507x::init() {
#ifdef PRALINE
gpio_control::rf5072_mix_en.set(); // RF5072_MIX_EN
rf5072_mix_en.setActive(); // RF5072_MIX_EN
#endif
gpio_rffc5072_resetx.set();
#ifndef PRALINE
gpio_rffc5072_resetx.output();
#endif
rffc5072_resetx.setInactive();
reset();
_bus.init();
@@ -195,9 +197,9 @@ void RFFC507x::reset() {
/* TODO: Is RESETB pin ignored if sdi_ctrl.sipin=1? Programming guide
* description of sdi_ctrl.sipin suggests the pin is not ignored.
*/
gpio_rffc5072_resetx.clear();
rffc5072_resetx.setActive();
halPolledDelay(ticks_during_reset);
gpio_rffc5072_resetx.set();
rffc5072_resetx.setInactive();
halPolledDelay(ticks_after_reset);
}
+13 -19
View File
@@ -23,50 +23,44 @@
#include "utility.hpp"
#include "hackrf_gpio.hpp"
using namespace hackrf::one;
#include "gpio.hpp"
using namespace gpio_control;
namespace rffc507x {
namespace spi {
void SPI::init() {
gpio_rffc5072_select.set();
gpio_rffc5072_clock.clear();
#ifndef PRALINE
gpio_rffc5072_select.output();
gpio_rffc5072_clock.output();
#endif
gpio_rffc5072_data.input();
gpio_rffc5072_data.clear();
rffc5072_select.setInactive();
rffc5072_clock.setInactive();
rffc5072_sdata.input();
rffc5072_sdata.setInactive();
}
inline void SPI::select(const bool active) {
gpio_rffc5072_select.write(!active);
rffc5072_select.setState(active);
}
inline void SPI::direction_out() {
gpio_rffc5072_data.output();
rffc5072_sdata.output();
}
inline void SPI::direction_in() {
gpio_rffc5072_data.input();
rffc5072_sdata.input();
}
inline void SPI::write_bit(const bit_t value) {
gpio_rffc5072_data.write(value);
rffc5072_sdata.write(value);
}
inline bit_t SPI::read_bit() {
return gpio_rffc5072_data.read() & 1;
return rffc5072_sdata.read() & 1;
}
inline bit_t SPI::transfer_bit(const bit_t bit_out) {
gpio_rffc5072_clock.clear();
rffc5072_clock.setInactive();
write_bit(bit_out);
const bit_t bit_in = read_bit();
gpio_rffc5072_clock.set();
rffc5072_clock.setActive();
return bit_in;
}
+1 -1
View File
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* copyleft 2023 zxkmm AKA zix aka sommermorgentraum
* copyleft 2023 zxkmm
* Copyright (C) 2023 u-foka
*
* This file is part of PortaPack.
+1 -1
View File
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* copyleft 2023 zxkmm AKA zix aka sommermorgentraum
* copyleft 2023 zxkmm
* Copyright (C) 2023 u-foka
*
* This file is part of PortaPack.
-1
View File
@@ -132,7 +132,6 @@ Continuous (Fox-oring)
#include "spi_image.hpp"
#include "debug.hpp"
#include "led.hpp"
#include "gcc.hpp"
+1 -5
View File
@@ -57,13 +57,13 @@ using asahi_kasei::ak4951::AK4951;
#include "battery.hpp"
#include "gpio.hpp"
using namespace gpio_control;
extern "C" {
#include "platform_detect.h"
#ifdef PRALINE
#include "fpga_bridge.h"
#include "board.h"
#endif
}
@@ -74,11 +74,9 @@ const char* init_error = nullptr;
portapack::IO io{
portapack::gpio_dir,
portapack::gpio_lcd_rdx,
portapack::gpio_lcd_wrx,
portapack::gpio_io_stbx,
portapack::gpio_addr,
portapack::gpio_lcd_te,
portapack::gpio_dfu,
};
portapack::BacklightCAT4004 backlight_cat4004;
@@ -102,8 +100,6 @@ AK4951 audio_codec_ak4951{i2c0, 0x12};
ReceiverModel receiver_model;
TransmitterModel transmitter_model;
TemperatureLogger temperature_logger;
bool antenna_bias{false};
uint32_t bl_tick_counter{0};
uint16_t touch_threshold{32};
-3
View File
@@ -36,7 +36,6 @@
#include "radio.hpp"
#include "clock_manager.hpp"
#include "temperature_logger.hpp"
#include "theme.hpp"
/* TODO: This would be better as a class to add
@@ -69,8 +68,6 @@ extern TransmitterModel transmitter_model;
extern uint32_t bl_tick_counter;
extern uint16_t touch_threshold;
extern TemperatureLogger temperature_logger;
/* Get or set the antenna_bias flag.
* NB: Does not actually update the radio state. */
void set_antenna_bias(const bool v);
+69 -44
View File
@@ -53,6 +53,9 @@ using namespace hackrf::one;
#include "baseband_api.hpp"
#include "hal.h" // For LPC_SGPIO
#include "gpio.hpp"
using namespace gpio_control;
#include <array>
/* Direct access to the radio. Setting values incorrectly can damage
@@ -73,8 +76,8 @@ static constexpr uint32_t ssp_scr(
/* MAX2831 uses 9-bit SPI transfers */
static constexpr SPIConfig ssp_config_max283x = {
.end_cb = NULL,
.ssport = gpio_max283x_select.port(),
.sspad = gpio_max283x_select.pad(),
.ssport = map_max283x_select.gpio_port,
.sspad = map_max283x_select.gpio_pad,
.cr0 =
CR0_CLOCKRATE(ssp_scr(ssp1_pclk_f, ssp1_cpsr, max283x_spi_f) + 3) | CR0_FRFSPI | CR0_DSS9BIT,
.cpsr = ssp1_cpsr,
@@ -92,8 +95,8 @@ void set_rx_buff_vcm(const size_t v) {
/* MAX2837/MAX2839 use 16-bit SPI transfers */
static constexpr SPIConfig ssp_config_max283x = {
.end_cb = NULL,
.ssport = gpio_max283x_select.port(),
.sspad = gpio_max283x_select.pad(),
.ssport = map_max283x_select.gpio_port,
.sspad = map_max283x_select.gpio_pad,
.cr0 =
CR0_CLOCKRATE(ssp_scr(ssp1_pclk_f, ssp1_cpsr, max283x_spi_f) + 3) | CR0_FRFSPI | CR0_DSS16BIT,
.cpsr = ssp1_cpsr,
@@ -141,6 +144,10 @@ static rf::Direction cached_direction = rf::Direction::Receive;
static bool cached_rf_amp = false;
static int_fast8_t cached_lna_gain = 0;
static int_fast8_t cached_vga_gain = 0;
/* FPGA quarter-rate shift mode currently programmed, in gateware encoding
* (0b00 none / 0b11 up / 0b01 down). The baseband filter width depends on it,
* so ReceiverModel reads it back through get_quarter_shift(). */
static uint8_t cached_quarter_shift = 0;
#endif
void init() {
@@ -148,10 +155,6 @@ void init() {
/* PRALINE uses MAX2831 transceiver */
second_if = (max283x::MAX283x*)&second_if_max2831;
#else
if (hackrf_r9) {
gpio_r9_not_ant_pwr.write(1);
gpio_r9_not_ant_pwr.output();
}
second_if = hackrf_r9
? (max283x::MAX283x*)&second_if_max2839
: (max283x::MAX283x*)&second_if_max2837;
@@ -183,18 +186,15 @@ void init() {
fpga_set_mode(FPGA_MODE_RX);
// These FPGA registers control DC_BLOCK, Q-Inv, QUARTER SHIFT, and Decimation.
fpga_debug_register_write(FPGA_REG_CTRL, FPGA_CTRL_DC_BLOCK_EN); // DC_BLOCK=1, QUARTER_SHIFT=0, Q_INVERT=0
fpga_debug_register_write(FPGA_REG_DECIM, 0x00); // RX_DECIM=No Decim
// RX Mode: Register 3 is RX Digital Gain. Start with 0dB (no shift).
fpga_debug_register_write(FPGA_REG_RX_DIGITAL_GAIN, FPGA_RX_DEFAULT_DIGITAL_GAIN);
/* RX Mode: Initialize DC Block parameters to standard Praline values.
* 0x04 Width and 0x08 Adapt Rate are typical for 40MHz stability.
*/
fpga_debug_register_write(FPGA_REG_RX_DC_BLOCK_WIDTH, FPGA_RX_DEFAULT_DC_WIDTH);
fpga_debug_register_write(FPGA_REG_RX_DC_ADAPT_RATE, FPGA_RX_DEFAULT_ADAPT_RATE);
/* Boot register state, matching fpga_init() in hackrf/firmware/common/fpga.c:
* DC block on, no PRBS, no external trigger, no quarter shift, TX NCO off.
* The decimation ratio and the quarter shift are programmed later by
* ClockManager::set_sampling_frequency() and set_tuning_frequency(). */
fpga_debug_register_write(FPGA_REG_CTRL, FPGA_CTRL_DC_BLOCK_EN);
fpga_debug_register_write(FPGA_REG_DECIM, 0x00); // RX_DECIM = no decimation
fpga_debug_register_write(FPGA_REG_RX_PSTEP, 0x00); // quarter shift off
fpga_debug_register_write(FPGA_REG_TX_CONTROL, 0x00);
cached_quarter_shift = 0;
ssp1_arbiter.invalidate();
chThdSleepMilliseconds(10); // Let FPGA registers settle
@@ -225,13 +225,13 @@ void set_direction(const rf::Direction new_direction) {
fpga_debug_register_write(FPGA_REG_TX_PHASE_STEP, 0x00);
} else {
fpga_set_mode(FPGA_MODE_RX);
// RX Mode: Ensure NCO is disabled and reset digital gain
fpga_debug_register_write(FPGA_REG_RX_DIGITAL_GAIN, FPGA_RX_DEFAULT_DIGITAL_GAIN);
/* RX Mode: Initialize DC Block parameters to standard Praline values.
* 0x04 Width and 0x08 Adapt Rate are typical for 40MHz stability.
*/
fpga_debug_register_write(FPGA_REG_RX_DC_BLOCK_WIDTH, FPGA_RX_DEFAULT_DC_WIDTH);
fpga_debug_register_write(FPGA_REG_RX_DC_ADAPT_RATE, FPGA_RX_DEFAULT_ADAPT_RATE);
/* RX Mode: DC block on, TX NCO off. The quarter shift is re-applied by
* set_tuning_frequency(); clear it here so a stale TX/RX transition
* cannot leave a rotation programmed with no matching LO offset. */
fpga_debug_register_write(FPGA_REG_CTRL, FPGA_CTRL_DC_BLOCK_EN);
fpga_debug_register_write(FPGA_REG_TX_CONTROL, 0x00);
fpga_debug_register_write(FPGA_REG_RX_PSTEP, 0x00);
cached_quarter_shift = 0;
}
#endif
@@ -280,9 +280,9 @@ void set_direction(const rf::Direction new_direction) {
baseband_codec.set_mode((direction == rf::Direction::Transmit) ? max5864::Mode::Transmit : max5864::Mode::Receive);
if (direction == rf::Direction::Receive)
led_rx.on();
led_rx.setActive();
else
led_tx.on();
led_tx.setActive();
}
bool set_tuning_frequency(const rf::Frequency frequency) {
@@ -310,7 +310,19 @@ bool set_tuning_frequency(const rf::Frequency frequency) {
final_frequency = final_frequency + portapack::persistent_memory::config_freq_rx_correction();
}
#ifdef PRALINE
/* The PRALINE tuning tables offset the analogue passband by a quarter of
* the ADC sample rate and have the FPGA rotate it back to DC, so the
* planner needs to know the AFE rate. See tuning.cpp. */
const uint32_t afe_rate = portapack::clock_manager.get_sampling_frequency()
<< portapack::clock_manager.get_resampling_n();
const auto tuning_config = tuning::config::create(
final_frequency,
afe_rate,
direction == rf::Direction::Transmit);
#else
const auto tuning_config = tuning::config::create(final_frequency);
#endif
if (tuning_config.is_valid()) {
first_if.disable();
@@ -340,6 +352,22 @@ bool set_tuning_frequency(const rf::Frequency frequency) {
LPC_GPIO->CLR[0] = (1 << 13); // SGPIO12 = 0 (Q normal)
}
/* Program the FPGA's quarter-rate shift to match the offset the tuning
* table just applied to the analogue centre frequency. The gateware
* (hackrf/firmware/fpga/top/standard.py) takes both bits from the top
* of register 0x03 (rx_pstep):
* rx_pstep[6] -> quarter_shift.enable
* rx_pstep[7] -> quarter_shift.up
* which is exactly fpga_set_rx_quarter_shift_mode() in
* hackrf/firmware/common/fpga.c: write (mode & 0b11) << 6.
*
* These two settings MUST be programmed together. Tuning off-centre
* without the rotation puts the signal outside the decimation filter's
* passband and it disappears entirely; rotating without the offset
* moves the wanted signal off DC by the same amount. */
cached_quarter_shift = tuning_config.quarter_shift;
fpga_debug_register_write(FPGA_REG_RX_PSTEP, (cached_quarter_shift & 0b11) << 6);
ssp1_arbiter.invalidate();
#else
baseband_cpld.set_invert(mixer_invert ^ baseband_invert);
@@ -395,7 +423,7 @@ void set_antenna_bias(const bool on) {
rf_path.set_ant_bias(on);
#else
if (hackrf_r9) {
gpio_r9_not_ant_pwr.write(on ? 0 : 1);
rf_path.set_ant_bias(on);
} else {
first_if.set_gpo1(on ? 0 : 1);
}
@@ -442,8 +470,8 @@ void disable() {
first_if.disable();
set_rf_amp(false);
led_rx.off();
led_tx.off();
led_rx.setInactive();
led_tx.setInactive();
}
#ifdef PRALINE
@@ -470,6 +498,10 @@ int_fast8_t get_cached_lna_gain() {
int_fast8_t get_cached_vga_gain() {
return cached_vga_gain;
}
uint8_t get_cached_quarter_shift() {
return cached_quarter_shift;
}
#endif
namespace first_if {
@@ -571,18 +603,11 @@ void register_write(const size_t register_number, uint32_t value) {
void init() {
fpga_set_mode(FPGA_MODE_RX);
// These FPGA registers control DC_BLOCK, Q-Inv, QUARTER SHIFT, and Decimation.
fpga_debug_register_write(FPGA_REG_CTRL, FPGA_CTRL_DC_BLOCK_EN); // DC_BLOCK=1, QUARTER_SHIFT=0, Q_INVERT=0
fpga_debug_register_write(FPGA_REG_DECIM, 0x00); // RX_DECIM=No Decim
// RX Mode: Register 3 is RX Digital Gain. Start with 0dB (no shift).
fpga_debug_register_write(FPGA_REG_RX_DIGITAL_GAIN, FPGA_RX_DEFAULT_DIGITAL_GAIN);
/* RX Mode: Initialize DC Block parameters to standard Praline values.
* 0x04 Width and 0x08 Adapt Rate are typical for 40MHz stability.
*/
fpga_debug_register_write(FPGA_REG_RX_DC_BLOCK_WIDTH, FPGA_RX_DEFAULT_DC_WIDTH);
fpga_debug_register_write(FPGA_REG_RX_DC_ADAPT_RATE, FPGA_RX_DEFAULT_ADAPT_RATE);
/* Same boot state as fpga_init() in hackrf/firmware/common/fpga.c. */
fpga_debug_register_write(FPGA_REG_CTRL, FPGA_CTRL_DC_BLOCK_EN);
fpga_debug_register_write(FPGA_REG_DECIM, 0x00); // RX_DECIM = no decimation
fpga_debug_register_write(FPGA_REG_RX_PSTEP, 0x00); // quarter shift off
fpga_debug_register_write(FPGA_REG_TX_CONTROL, 0x00);
ssp1_arbiter.invalidate(); // Force arbiter to reconfigure on next transfer
}
+4
View File
@@ -126,6 +126,10 @@ rf::Direction get_cached_direction();
bool get_cached_rf_amp();
int_fast8_t get_cached_lna_gain();
int_fast8_t get_cached_vga_gain();
/* FPGA RX quarter-rate shift currently programmed, in gateware encoding:
* 0b00 none, 0b11 up, 0b01 down. */
uint8_t get_cached_quarter_shift();
#endif
namespace sgpio {
+37 -31
View File
@@ -244,10 +244,24 @@ void ReceiverModel::enable() {
radio::set_direction(rf::Direction::Receive);
#ifdef PRALINE
/* Anchor the Common Mode Voltage (VCM) to 1.2V.
* This stabilizes the electrical floor of the I/Q signals.
*/
radio::set_rx_buff_vcm(1);
/* MAX2831 RX IQ common-mode voltage (register 15).
*
* 0 = 1.1 V, 1 = 1.2 V, 2 = 1.3 V, 3 = 1.45 V.
*
* The reference firmware leaves this alone: max2831.c's default register
* table has reg 15 = 0x0145 (1.1 V) and the line that would raise it is
* commented out ("maximum rx output common-mode voltage"). Mayhem used to
* force 1.2 V here on the theory that it "stabilises the electrical floor
* of the I/Q signals" -- plausible, but never measured, and it was the
* last remaining RF-path setting where this branch disagreed with the
* configuration that is proven to receive ADS-B on this board.
*
* Set to 0 to match the reference (writing 1.1 V is a no-op against the
* power-on default), or back to 1 to restore the old Mayhem behaviour.
* If reception measurably worsens, put it back to 1 and say so -- neither
* value has been verified on hardware. */
#define PRALINE_RX_IQ_VCM 0
radio::set_rx_buff_vcm(PRALINE_RX_IQ_VCM);
#endif
update_tuning_frequency();
@@ -326,47 +340,39 @@ void ReceiverModel::update_baseband_bandwidth() {
if (enabled_) {
#ifdef PRALINE
/*
* PRALINE LPF bandwidth calculation from GSG hackrf_usb radio.c
* PRALINE LPF bandwidth, ported from auto_bandwidth() in
* hackrf/firmware/common/radio.c:
*
* The LPF should be set to capture the desired signal bandwidth
* while the FPGA decimation filter handles anti-aliasing.
* bb_bandwidth = sample_rate * 3 / 4
* lpf_bandwidth = bb_bandwidth + offset_hz * 2
*
* For most modes: LPF = (output_sample_rate * 3) / 8
* For quarter-shift: add offset for shifted spectrum
* where offset_hz is the quarter-rate shift, i.e. afe_rate / 4 when a
* shift is in use. The doubling is because the wanted signal sits
* offset from the analogue centre, so the analogue filter has to stay
* open out to that offset on the far side too.
*
* Note: MAX2831 minimum LPF is 11.6 MHz, so for narrow sample rates
* the hardware limit applies and FPGA filter does the real work.
* The previous version used /8 in both places and read the shift from
* bits 2-3 of FPGA register 1, which do not exist in the gateware, so
* it always took the no-shift branch. At the ADS-B rate that asked for
* 750 kHz, which is below the MAX2831's 1.75 MHz floor and therefore
* also switched in the external narrowband AA filter
* (MAX2831::set_lpf_rf_bandwidth_rx), squeezing the RX path shut. The
* reference asks for 17.5 MHz at the same rate and the AA filter stays
* out of circuit.
*/
uint32_t sample_rate = sampling_rate();
uint8_t resampling_n = portapack::clock_manager.get_resampling_n();
uint32_t afe_rate = sample_rate << resampling_n;
// Base LPF: enough to capture desired bandwidth
uint32_t lpf_bandwidth = (sample_rate * 3) / 8;
// Check if quarter-shift is enabled (FPGA register 1, bits 2-3)
uint32_t fpga_ctrl = radio::debug::fpga::register_read(1);
uint8_t quarter_shift = (fpga_ctrl >> 2) & 0x03;
uint32_t lpf_bandwidth = (sample_rate * 3) / 4;
const uint8_t quarter_shift = radio::debug::get_cached_quarter_shift();
if (quarter_shift != 0) {
// Quarter-shift moves spectrum by AFE_rate/4, need wider LPF
uint32_t offset = afe_rate / 8;
const uint32_t offset = afe_rate / 4;
lpf_bandwidth += offset * 2;
}
// For best anti-alias performance, also consider AFE Nyquist
// If our calculated LPF is below MAX2831 minimum, it doesn't matter
// But if we can set LPF to just below AFE Nyquist, that's optimal
uint32_t afe_nyquist = afe_rate / 2;
// Use the larger of: signal bandwidth requirement OR Nyquist protection
// (but MAX2831 driver will clamp to its available settings anyway)
if (lpf_bandwidth < afe_nyquist) {
// Set LPF close to Nyquist for maximum alias rejection
lpf_bandwidth = (afe_nyquist * 9) / 10; // 90% of Nyquist
}
radio::set_baseband_filter_bandwidth_rx(lpf_bandwidth);
#else
radio::set_baseband_filter_bandwidth_rx(baseband_bandwidth());
+63 -249
View File
@@ -21,221 +21,26 @@
#include "rf_path.hpp"
#include "platform.hpp"
#include <array>
#include <initializer_list>
#include "hackrf_gpio.hpp"
using namespace hackrf::one;
#include "utility.hpp"
#include "gpio.hpp"
using namespace gpio_control;
namespace rf {
namespace path {
namespace {
#ifdef PRALINE
/* PRALINE uses a simplified RF path with only 5 control signals.
* The RF path architecture is completely different from HackRF One.
*/
struct PralineConfig {
bool tx_en;
bool mix_bypass; // RF path mixer bypass (GPIO3[2])
bool lpf_en;
bool rf_amp_en;
bool ant_bias_en_n; // Inverted: 0 = bias enabled
void apply() const {
gpio_tx_enable.write(tx_en);
gpio_mix_bypass.write(mix_bypass); // Control RF path mixer
gpio_lpf_enable.write(lpf_en);
gpio_rf_amp_enable.write(rf_amp_en);
gpio_ant_bias_disable.write(ant_bias_en_n);
}
};
#else
/* HackRF One uses 11 GPIOs for RF path control */
using GPIOs = std::array<GPIO, 11>;
/* TODO: ARM GCC 4.8 2014q3 doesn't like this array inside struct Config.
* No idea why.
*/
constexpr GPIOs gpios{
gpio_mix_bypass,
gpio_not_mix_bypass,
gpio_tx_mix_bp,
gpio_rx_mix_bp,
gpio_hp,
gpio_lp,
gpio_amp_bypass,
gpio_tx_amp,
gpio_not_tx_amp_pwr,
gpio_rx_amp,
gpio_not_rx_amp_pwr,
};
/* HackRF One Config struct - not used on PRALINE */
struct Config {
using base_type = uint16_t;
union {
struct {
bool tx : 1;
bool rx : 1;
bool mix_bypass : 1;
bool not_mix_bypass : 1;
bool tx_mix_bp : 1;
bool rx_mix_bp : 1;
bool hp : 1;
bool lp : 1;
bool amp_bypass : 1;
bool tx_amp : 1;
bool not_tx_amp : 1;
bool rx_amp : 1;
bool not_rx_amp : 1;
};
base_type w;
};
constexpr Config(
const Direction direction,
const Band band,
const bool amplify)
: tx(direction == Direction::Transmit),
rx(direction == Direction::Receive),
mix_bypass(band == Band::Mid),
not_mix_bypass(band != Band::Mid),
tx_mix_bp((direction == Direction::Transmit) && (band == Band::Mid)),
rx_mix_bp((direction == Direction::Receive) && (band == Band::Mid)),
hp(band == Band::High),
lp(band == Band::Low),
amp_bypass(!amplify),
tx_amp((direction == Direction::Transmit) && amplify),
not_tx_amp(!tx_amp),
rx_amp((direction == Direction::Receive) && amplify),
not_rx_amp(!rx_amp) {
}
constexpr Config()
: Config(Direction::Receive, Band::Mid, false) {
}
constexpr Config(
const base_type w)
: w(w) {
}
constexpr Config operator^(const Config& r) const {
return w ^ r.w;
}
constexpr Config operator&(const Config& r) const {
return w & r.w;
}
constexpr bool operator[](const size_t n) const {
return (w >> n) & 1;
}
static void gpio_init() {
if (hackrf_r9) {
gpio_r9_rx.output();
} else {
gpio_og_tx.output();
gpio_og_rx.output();
}
for (auto gpio : gpios) {
gpio.output();
}
}
void apply() const {
/* NOTE: Assumes order in gpios[] and Config bitfield match,
* after the 'tx' and 'rx' fields which are handled specially. */
for (size_t n = 0; n < gpios.size() + 2; n++) {
bool value = (*this)[n];
switch (n) {
case 0:
if (!hackrf_r9) {
gpio_og_tx.write(value);
}
break;
case 1:
if (hackrf_r9) {
gpio_r9_rx.write(value);
} else {
gpio_og_rx.write(value);
}
break;
default:
gpios[n - 2].write(value);
break;
}
}
}
};
/* HackRF One config table - not used on PRALINE */
using ConfigAmp = std::array<Config, 2>;
using ConfigDirection = std::array<ConfigAmp, 2>;
using ConfigBand = std::array<ConfigDirection, 3>;
constexpr ConfigAmp config_amp(
const Direction direction,
const Band band) {
return {{
Config(direction, band, false),
Config(direction, band, true),
}};
}
constexpr ConfigDirection config_rx_tx(
const Band band) {
return {
config_amp(Direction::Receive, band),
config_amp(Direction::Transmit, band),
};
}
constexpr ConfigBand config_band() {
return {
config_rx_tx(Band::Low),
config_rx_tx(Band::Mid),
config_rx_tx(Band::High),
};
}
constexpr ConfigBand config_table = config_band();
static_assert(sizeof(config_table) == sizeof(Config::base_type) * 3 * 2 * 2, "rf path config table unexpected size");
constexpr Config get_config(
const Direction direction,
const Band band,
const bool amplify) {
return config_table[toUType(band)][toUType(direction)][amplify ? 1 : 0];
}
#endif /* PRALINE */
} /* namespace */
void Path::init() {
/* Set safe initial default states */
direction = Direction::Receive;
rf_amp_en = false;
ant_bias_en = false;
#ifdef PRALINE
/* Set safe initial state: RX mode, mixer enabled, LPF on, amp off, no bias */
PralineConfig config = {
.tx_en = false,
.mix_bypass = false, // RF path mixer bypass (GPIO3[2])
.lpf_en = true, // LPF on for low band
.rf_amp_en = false, // Amp off
.ant_bias_en_n = true // Bias off (inverted)
};
config.apply();
band = Band::Low;
#else
update();
Config::gpio_init();
band = Band::Mid;
#endif
update();
}
void Path::set_direction(const Direction new_direction) {
@@ -245,73 +50,82 @@ void Path::set_direction(const Direction new_direction) {
void Path::set_band(const Band new_band) {
band = new_band;
_band = new_band;
update();
}
void Path::set_rf_amp(const bool new_rf_amp) {
rf_amp = new_rf_amp;
rf_amp_en = new_rf_amp;
update();
}
void Path::set_ant_bias(const bool new_ant_bias) {
ant_bias = new_ant_bias;
ant_bias_en = new_ant_bias;
update();
}
bool Path::get_ant_bias() const {
return ant_bias;
return ant_bias_en;
}
void Path::update() {
/* 0 ^ 0 => 0 & 0 = 0 ^ 0 = 0 (no change)
* 0 ^ 1 => 1 & 0 = 0 ^ 0 = 0 (ignore change to 1)
* 1 ^ 0 => 1 & 1 = 1 ^ 1 = 0 (allow change to 0)
* 1 ^ 1 => 0 & 1 = 0 ^ 1 = 1 (no change) */
const bool is_tx = (direction == Direction::Transmit);
#ifdef PRALINE
/* PRALINE RF path control:
* - tx_en: 1 for TX, 0 for RX
* - mix_bypass: 0 to enable RF path mixer bypass (GPIO3[2])
* - lpf_en: 1 for low band (< 2.4 GHz), 0 for high band
* - rf_amp_en: 1 to enable RF amplifier
* - ant_bias_en_n: 0 to enable antenna bias (inverted)
*/
// const Config changed = _config ^ config_next;
// const Config turned_off = _config & changed;
// PRALINE specific RF path control directly applied to pins.
// Active-low pin inversion is handled internally inside setState().
PralineConfig config;
tx_enable.setState(is_tx);
/* In transition, ignore the bits that are turning on. So this transition phase
* only turns off signals. It doesn't turn on signals.
*/
// const Config transition_config = _config ^ turned_off;
// update_signals(transition_config);
// On the PRALINE board, the mixer is used ONLY on the Low band.
// Since setState() internally handles the active-low (MIX_ENABLE_N) hardware inversion,
// we simply pass 'true' to enable the mixer on Low band, and 'false' for Mid/High bands.
config.tx_en = (direction == Direction::Transmit);
mix_bypass.setState(band == Band::Low);
// RF path mixer bypass: 0=enabled, 1=bypassed
config.mix_bypass = (band == Band::Mid);
/* Move to the final state by turning on required signals. */
/* LPF for low band */
config.lpf_en = (band == Band::Low);
/* RF amp when amplification requested */
config.rf_amp_en = rf_amp;
/* Antenna bias */
config.ant_bias_en_n = !ant_bias;
config.apply();
lpf.setState(band == Band::Low);
rf_amp_enable.setState(rf_amp_en);
ant_bias.setState(ant_bias_en);
#else
/* HackRF One RF path control */
const auto config = get_config(direction, band, rf_amp);
config.apply();
const bool is_rx = (direction == Direction::Receive);
// HackRF One (OG & R9) RF path control
const bool mix_bypass_en = (band == Band::Mid);
const bool amplify = rf_amp_en;
// Primary TX/RX routing switches
if (!hackrf_r9) {
og_tx.setState(is_tx);
}
if (hackrf_r9) {
r9_rx.setState(is_rx); // Single pin handles directional switching on R9
} else {
og_rx.setState(is_rx);
}
// RF path switch configuration matrix
rx_mix_bypass.setState(mix_bypass_en);
tx_mix_bp.setState(is_tx && mix_bypass_en);
rx_mix_bp.setState(is_rx && mix_bypass_en);
hpf.setState(band == Band::High);
lpf.setState(band == Band::Low);
amp_bypass.setState(!amplify);
tx_amp.setState(is_tx && amplify);
rx_amp.setState(is_rx && amplify);
tx_mix_bypass.setState(mix_bypass_en);
tx_amp_pwr.setState(is_tx && amplify);
rx_amp_pwr.setState(is_rx && amplify);
if (hackrf_r9) {
ant_bias.setState(ant_bias_en);
}
#endif
}
} // namespace path
} // namespace rf
} // namespace rf
+8 -11
View File
@@ -41,15 +41,15 @@ namespace path {
#ifdef PRALINE
/* PRALINE: MAX2831 direct path is 2320-2740 MHz */
constexpr FrequencyRange band_low{0, 2320'000'000};
constexpr FrequencyRange band_high{2740'000'000, 7250'000'000};
constexpr FrequencyRange band_mid{band_low.maximum, band_high.minimum};
constexpr Frequency TRANSITION = 2320'000'000;
#else
/* HackRF One: Original band boundaries */
constexpr FrequencyRange band_low{0, 2170'000'000};
constexpr Frequency TRANSITION = 2170'000'000;
#endif
constexpr FrequencyRange band_low{0, TRANSITION};
constexpr FrequencyRange band_high{2740'000'000, 7250'000'000};
constexpr FrequencyRange band_mid{band_low.maximum, band_high.minimum};
#endif
enum class Band {
/* Zero-based, used as index into frequency_bands table */
@@ -61,23 +61,20 @@ enum class Band {
class Path {
public:
void init();
void set_direction(const Direction direction);
void set_band(const Band band);
void set_rf_amp(const bool rf_amp);
void set_ant_bias(const bool ant_bias);
bool get_ant_bias() const;
Band get_band() const { return _band; } //_band is used solely for debugging purposes.
Band get_band() const { return band; }
private:
Direction direction{Direction::Receive};
Band band{Band::Mid};
bool rf_amp{false};
bool ant_bias{false};
bool rf_amp_en{false};
bool ant_bias_en{false};
void update();
Band _band{Band::Mid}; //_band is solely used of debugging purposes
};
} // namespace path
+3 -1
View File
@@ -26,7 +26,9 @@
#include "audio.hpp"
#include "baseband_api.hpp"
#include "event_m0.hpp"
#include "hackrf_gpio.hpp"
#include "gpio.hpp"
using namespace gpio_control;
#include "portapack.hpp"
#include "portapack_persistent_memory.hpp"
#include "radio.hpp"
+332 -99
View File
@@ -27,8 +27,8 @@ namespace tuning {
namespace config {
// Forward declarations
Config low_band(const rf::Frequency target_frequency);
Config mid_band(const rf::Frequency target_frequency);
Config low_band(const rf::Frequency target_frequency, const uint32_t afe_rate, const bool transmit);
Config mid_band(const rf::Frequency target_frequency, const uint32_t afe_rate, const bool transmit);
Config high_band(const rf::Frequency target_frequency);
#ifdef PRALINE
@@ -36,133 +36,366 @@ Config high_band(const rf::Frequency target_frequency);
* PRALINE Tuning Configuration
* ============================
*
* Reference: hackrf_usb/common/tune_config.h praline_tune_config_rx[]
* These tables are copied verbatim from the reference firmware,
* hackrf/firmware/common/tune_config.h (praline_tune_config_rx /
* praline_tune_config_tx), and the selection and offset maths below reproduce
* hackrf/firmware/common/radio.c radio_update_frequency() /
* analog_from_digital_rf() / compute_offset().
*
* The hackrf_usb firmware uses a table-driven approach where each entry
* specifies:
* - rf_range_end_mhz: Upper frequency limit for this config
* - if_mhz: IF frequency (what MAX2831 tunes to)
* - high_lo: true = high-side injection, false = low-side
* - shift: FPGA quarter-shift mode (not implemented in Mayhem yet)
* Each entry gives, for target frequencies up to rf_range_end_mhz:
* if_mhz the IF the MAX2831 tunes to (0 = mixer bypassed, IF = RF)
* high_lo true -> LO = IF + analogue RF (mixer inverts the spectrum)
* false -> LO = IF - analogue RF (no inversion)
* shift the FPGA quarter-rate shift mode used for this entry
*
* Key insight: The IF frequency varies to keep the RFFC5072 VCO in a
* safe operating range (ideally 3500-5000 MHz, avoiding extremes).
* The quarter-rate shift is the part Mayhem was previously missing. RX entries
* deliberately place the analogue passband a quarter of the ADC rate away from
* the requested frequency (+8 MHz at the usual 32 Msps AFE rate) so that the
* wanted signal never sits on the DC offset / LO leakage, and then ask the FPGA
* to rotate it back down to DC. Both halves have to be programmed together:
* - tuning to target + offset without asking the FPGA to rotate leaves the
* signal 8 MHz out and the decimation filter deletes it;
* - tuning to target with no offset (what Mayhem did) parks the signal on DC,
* under the LO leakage and the gateware's adaptive DC block.
*
* RFFC5072 VCO calculation:
* High-side injection: LO = IF + RF, VCO = LO × lodiv
* Low-side injection: LO = IF - RF, VCO = LO × lodiv
* Where lodiv = 2 for frequencies where VCO > 2700 MHz
*
* From hackrf_usb tune_config_rx (simplified):
* 0-2100 MHz: IF=2375, high_lo=true VCO = (2375+RF)×2
* 2105-2115: IF=2375, high_lo=false VCO = (2375-RF)×2
* 2115-2130: IF=2425, high_lo=false VCO = (2425-RF)×2
* ... (more entries for fine-grained control)
* 2320-2580: IF=0 (bypass mode, no mixer)
* 2580+: High-pass mode
* "up" and "down" are named for the direction the FPGA rotates, so
* FPGA_QUARTER_SHIFT_MODE_UP means the analogue centre is placed ABOVE the
* requested frequency, and DOWN below it (radio.c analog_from_digital_rf()).
*/
// Simplified tune_config lookup for Mayhem
// Returns the IF frequency in Hz for a given target frequency
constexpr rf::Frequency praline_get_if_frequency(const rf::Frequency target_frequency) {
const uint32_t freq_mhz = target_frequency / 1'000'000;
namespace {
// Based on hackrf_usb tune_config_rx table
if (freq_mhz < 2100) {
// Most low-band frequencies: use 2375 MHz IF
// This keeps VCO around 4750-4950 MHz for FM band
return 2375'000'000;
} else if (freq_mhz < 2320) {
// Transition zone: use varying IF to avoid VCO edges
// These frequencies are tricky - near MAX2831 minimum
// Use 2425 MHz to give some margin
return 2425'000'000;
} else {
// Bypass mode or high-band - IF not used for mixer
return 0;
struct PralineTuneConfig {
uint16_t rf_range_end_mhz;
uint16_t if_mhz;
bool high_lo;
uint8_t shift; /* 0b00 none, 0b11 up, 0b01 down */
};
/* The tables below are kept column-aligned to match the reference source, so
* they are exempt from reformatting. */
// clang-format off
/* tuning table optimized for RX */
constexpr PralineTuneConfig praline_tune_config_rx[] = {
{ 0, 2360, true, 0b00},
{ 50, 2320, true, 0b11},
{ 100, 2320, true, 0b01},
{ 140, 2320, true, 0b11},
{ 406, 2560, true, 0b11},
{ 511, 2380, true, 0b11},
{ 578, 2560, true, 0b01},
{ 741, 2340, true, 0b11},
{ 861, 2560, true, 0b01},
{ 921, 2560, true, 0b11},
{ 1049, 2340, true, 0b01},
{ 1169, 2380, true, 0b11},
{ 1360, 2340, true, 0b11},
{ 1544, 2560, true, 0b01},
{ 1675, 2560, true, 0b11},
{ 1992, 2380, true, 0b01},
{ 2070, 2340, true, 0b01},
{ 2150, 2360, true, 0b01},
{ 2168, 2560, false, 0b11},
{ 2185, 2580, false, 0b11},
{ 2202, 2580, false, 0b01},
{ 2205, 2520, false, 0b11},
{ 2216, 2560, false, 0b11},
{ 2223, 2540, false, 0b11},
{ 2234, 2580, false, 0b11},
{ 2240, 2560, false, 0b11},
{ 2251, 2580, false, 0b01},
{ 2258, 2580, false, 0b11},
{ 2265, 2540, false, 0b01},
{ 2271, 2580, false, 0b11},
{ 2273, 2560, false, 0b11},
{ 2275, 2580, false, 0b01},
{ 2280, 2500, false, 0b01},
{ 2284, 2540, false, 0b11},
{ 2289, 2580, false, 0b01},
{ 2293, 2540, false, 0b01},
{ 2298, 2520, false, 0b01},
{ 2300, 2580, false, 0b11},
{ 2302, 2540, false, 0b01},
{ 2309, 2560, false, 0b01},
{ 2311, 2580, false, 0b01},
{ 2314, 2540, false, 0b11},
{ 2315, 2540, false, 0b01},
{ 2320, 2580, false, 0b11},
{ 2380, 0, false, 0b11},
{ 2440, 0, false, 0b01},
{ 2500, 0, false, 0b11},
{ 2580, 0, false, 0b01},
{ 2583, 2360, false, 0b11},
{ 2584, 2380, false, 0b11},
{ 2587, 2340, false, 0b11},
{ 2593, 2340, false, 0b01},
{ 2607, 2340, false, 0b11},
{ 2609, 2360, false, 0b11},
{ 2615, 2360, false, 0b01},
{ 2627, 2340, false, 0b01},
{ 2629, 2360, false, 0b01},
{ 2631, 2380, false, 0b11},
{ 2644, 2340, false, 0b11},
{ 2649, 2380, false, 0b11},
{ 2651, 2380, false, 0b01},
{ 2654, 2500, false, 0b11},
{ 2665, 2360, false, 0b11},
{ 2669, 2380, false, 0b01},
{ 2672, 2360, false, 0b01},
{ 2682, 2340, false, 0b11},
{ 2687, 2380, false, 0b11},
{ 2692, 2340, false, 0b11},
{ 2695, 2500, false, 0b11},
{ 2705, 2360, false, 0b11},
{ 2707, 2380, false, 0b01},
{ 2712, 2340, false, 0b01},
{ 2717, 2520, false, 0b11},
{ 2728, 2380, false, 0b11},
{ 2730, 2560, false, 0b11},
{ 2734, 2500, false, 0b11},
{ 2758, 2340, false, 0b11},
{ 2780, 2360, false, 0b11},
{ 2787, 2520, false, 0b11},
{ 2802, 2380, false, 0b11},
{ 2809, 2540, false, 0b11},
{ 2822, 2380, false, 0b01},
{ 2831, 2560, false, 0b11},
{ 2854, 2340, false, 0b11},
{ 2875, 2360, false, 0b11},
{ 2898, 2380, false, 0b11},
{ 2918, 2380, false, 0b01},
{ 2936, 2520, false, 0b01},
{ 2944, 2380, false, 0b01},
{ 2959, 2560, false, 0b11},
{ 2976, 2340, false, 0b11},
{ 2985, 2500, false, 0b01},
{ 3003, 2340, false, 0b11},
{ 3009, 2540, false, 0b11},
{ 3027, 2380, false, 0b11},
{ 3034, 2560, false, 0b11},
{ 3050, 2380, false, 0b01},
{ 3069, 2500, false, 0b11},
{ 3094, 2520, false, 0b11},
{ 3119, 2540, false, 0b11},
{ 3144, 2560, false, 0b11},
{ 3169, 2560, false, 0b01},
{ 3180, 2500, false, 0b11},
{ 3204, 2340, false, 0b11},
{ 3232, 2360, false, 0b11},
{ 3292, 2340, false, 0b01},
{ 3340, 2380, false, 0b01},
{ 3369, 2340, false, 0b11},
{ 3399, 2360, false, 0b11},
{ 3429, 2380, false, 0b11},
{ 3464, 2500, false, 0b11},
{ 3489, 2520, false, 0b11},
{ 3512, 2540, false, 0b11},
{ 3551, 2500, false, 0b01},
{ 3582, 2540, false, 0b11},
{ 3611, 2560, false, 0b11},
{ 3639, 2520, false, 0b11},
{ 3729, 2340, false, 0b11},
{ 3817, 2380, false, 0b01},
{ 3942, 2360, false, 0b01},
{ 4049, 2540, false, 0b11},
{ 4134, 2500, false, 0b01},
{ 4194, 2560, false, 0b11},
{ 4353, 2520, false, 0b11},
{ 4449, 2360, false, 0b01},
{ 4562, 2500, false, 0b11},
{ 4672, 2560, false, 0b11},
{ 4769, 2540, false, 0b11},
{ 4849, 2560, false, 0b01},
{ 4889, 2560, false, 0b11},
{ 4929, 2560, false, 0b11},
{ 4969, 2560, false, 0b11},
{ 5009, 2560, false, 0b11},
{ 5049, 2560, false, 0b11},
{ 5092, 2360, false, 0b11},
{ 5209, 2340, false, 0b01},
{ 5298, 2380, false, 0b01},
{ 5468, 2340, false, 0b01},
{ 5582, 2520, false, 0b11},
{ 5702, 2340, false, 0b11},
{ 5888, 2520, false, 0b01},
{ 6092, 2340, false, 0b01},
{ 6240, 2560, false, 0b11},
{ 6609, 2340, false, 0b11},
{ 6752, 2380, false, 0b01},
{ 6930, 2520, false, 0b01},
{ 7000, 2560, false, 0b11},
{ 7070, 2560, false, 0b01},
{ 7251, 2580, false, 0b01},
{ 0, 0, false, 0b00},
};
/* tuning table optimized for TX */
constexpr PralineTuneConfig praline_tune_config_tx[] = {
{ 2100, 2375, true, 0b00},
{ 2105, 2375, false, 0b00},
{ 2115, 2425, false, 0b00},
{ 2130, 2375, false, 0b00},
{ 2150, 2425, false, 0b00},
{ 2160, 2475, false, 0b00},
{ 2175, 2425, false, 0b00},
{ 2190, 2475, false, 0b00},
{ 2195, 2425, false, 0b00},
{ 2210, 2375, false, 0b00},
{ 2248, 2425, false, 0b00},
{ 2265, 2525, false, 0b00},
{ 2300, 2425, false, 0b00},
{ 2320, 2525, false, 0b00},
{ 2580, 0, false, 0b00},
{ 3000, 2325, false, 0b00},
{ 3140, 2375, false, 0b00},
{ 3200, 2425, false, 0b00},
{ 3280, 2375, false, 0b00},
{ 3340, 2425, false, 0b00},
{ 3420, 2475, false, 0b00},
{ 3480, 2525, false, 0b00},
{ 3500, 2475, false, 0b00},
{ 3595, 2425, false, 0b00},
{ 3625, 2375, false, 0b00},
{ 3670, 2475, false, 0b00},
{ 3710, 2425, false, 0b00},
{ 3760, 2525, false, 0b00},
{ 3790, 2475, false, 0b00},
{ 3860, 2425, false, 0b00},
{ 3915, 2375, false, 0b00},
{ 4000, 2425, false, 0b00},
{ 4055, 2375, false, 0b00},
{ 4125, 2425, false, 0b00},
{ 4700, 2375, false, 0b00},
{ 4800, 2425, false, 0b00},
{ 5000, 2375, false, 0b00},
{ 5260, 2475, false, 0b00},
{ 5465, 2525, false, 0b00},
{ 5560, 2375, false, 0b00},
{ 5720, 2425, false, 0b00},
{ 5860, 2475, false, 0b00},
{ 5970, 2575, false, 0b00},
{ 6000, 2375, false, 0b00},
{ 6500, 2325, false, 0b00},
{ 6750, 2375, false, 0b00},
{ 6850, 2425, false, 0b00},
{ 6950, 2475, false, 0b00},
{ 7000, 2525, false, 0b00},
{ 7251, 2575, false, 0b00},
{ 0, 0, false, 0b00},
};
// clang-format on
/* radio.c select_tune_config(): first entry whose range end is above the
* requested frequency. The list is terminated by an all-zero entry, which is
* also what a frequency past the end of the table lands on. */
const PralineTuneConfig* select_tune_config(const rf::Frequency target_frequency, const bool transmit) {
const PralineTuneConfig* entry = transmit ? praline_tune_config_tx : praline_tune_config_rx;
const uint32_t freq_mhz = static_cast<uint32_t>(target_frequency / 1'000'000);
while ((entry->rf_range_end_mhz != 0) || (entry->if_mhz != 0)) {
if ((target_frequency == 0) || (entry->rf_range_end_mhz > freq_mhz))
break;
entry++;
}
return entry;
}
// Returns true for high-side injection, false for low-side
constexpr bool praline_use_high_side_injection(const rf::Frequency target_frequency) {
const uint32_t freq_mhz = target_frequency / 1'000'000;
// Based on hackrf_usb tune_config_rx table
if (freq_mhz < 2100) {
// Standard low-band: high-side injection
// LO = IF + RF, mixer inverts spectrum
return true;
} else if (freq_mhz < 2105) {
// Narrow transition: still high-side
return true;
} else if (freq_mhz < 2320) {
// Near MAX2831 minimum: use low-side injection
// LO = IF - RF, no spectrum inversion
return false;
} else {
// Bypass/high-band - doesn't matter, mixer bypassed
return false;
}
/* radio.c compute_offset(): a quarter of the AFE (ADC) sample rate, or zero if
* no shift is in use or the AFE rate isn't known yet. */
constexpr uint32_t quarter_shift_offset(const uint8_t shift, const uint32_t afe_rate) {
return (shift == 0) ? 0 : (afe_rate / 4);
}
/* radio.c analog_from_digital_rf(). */
rf::Frequency analog_from_digital_rf(const rf::Frequency target_frequency, const uint8_t shift, const uint32_t afe_rate) {
const rf::Frequency offset = quarter_shift_offset(shift, afe_rate);
if (shift == 0b11)
return target_frequency + offset;
if (shift == 0b01)
return (offset > target_frequency) ? (offset - target_frequency)
: (target_frequency - offset);
return target_frequency;
}
} // namespace
#endif // PRALINE
// Low band <2170 Mhz (HackRF One) or <2320 MHz (PRALINE):
constexpr rf::Frequency low_band_second_lo_frequency(const rf::Frequency target_frequency) {
Config low_band(const rf::Frequency target_frequency, const uint32_t afe_rate, const bool transmit) {
#ifdef PRALINE
// Use the tune_config lookup for PRALINE
return praline_get_if_frequency(target_frequency);
#else
return 2650'000'000 - (target_frequency / 7);
#endif
}
const PralineTuneConfig* entry = select_tune_config(target_frequency, transmit);
Config low_band(const rf::Frequency target_frequency) {
const rf::Frequency second_lo_frequency = low_band_second_lo_frequency(target_frequency);
/* Past the end of the table: no usable configuration. */
if ((entry->rf_range_end_mhz == 0) && (entry->if_mhz == 0))
return {};
#ifdef PRALINE
/* afe_rate == 0 means the caller doesn't know the ADC rate, so no LO offset
* is applied. The FPGA rotation has to be dropped with it: rotating without
* the matching offset moves the wanted signal off DC by afe_rate / 4. */
const uint8_t shift = (afe_rate == 0) ? 0 : entry->shift;
const rf::Frequency analog_rf = analog_from_digital_rf(target_frequency, shift, afe_rate);
/* if_mhz == 0 means the mixer is bypassed and the transceiver tunes the RF
* directly; there is no first LO in that case. */
const rf::Frequency second_lo_frequency =
(entry->if_mhz == 0) ? analog_rf : (static_cast<rf::Frequency>(entry->if_mhz) * 1'000'000);
if (entry->if_mhz == 0)
return {0, second_lo_frequency, rf::path::Band::Low, false, shift};
/* The low band always runs through the low-pass image-reject filter, so
* the spectrum is inverted exactly when the first LO ends up above the IF,
* i.e. for high-side injection. This is hackrf_usb.c radio_changed():
* invert = (img_reject == RF_PATH_FILTER_LOW_PASS) && (freq_lo > freq_if)
*/
rf::Frequency first_lo_frequency;
bool mixer_invert;
if (praline_use_high_side_injection(target_frequency)) {
// High-side injection: LO = IF + RF
first_lo_frequency = second_lo_frequency + target_frequency;
if (entry->high_lo) {
first_lo_frequency = second_lo_frequency + analog_rf;
mixer_invert = true;
} else {
// Low-side injection: LO = IF - RF
first_lo_frequency = second_lo_frequency - target_frequency;
first_lo_frequency = second_lo_frequency - analog_rf;
mixer_invert = false;
}
return {first_lo_frequency, second_lo_frequency, rf::path::Band::Low, mixer_invert};
return {first_lo_frequency, second_lo_frequency, rf::path::Band::Low, mixer_invert, shift};
#else
(void)afe_rate;
(void)transmit;
const rf::Frequency second_lo_frequency = 2650'000'000 - (target_frequency / 7);
const rf::Frequency first_lo_frequency = target_frequency + second_lo_frequency;
const bool mixer_invert = true;
return {first_lo_frequency, second_lo_frequency, rf::path::Band::Low, mixer_invert};
#endif
}
// Mid band 2170-2740 Mhz (HackRF One) or 2320-2580 MHz (PRALINE):
Config mid_band(const rf::Frequency target_frequency) {
// Mid band 2170-2740 Mhz (HackRF One) or 2320-2740 MHz (PRALINE):
Config mid_band(const rf::Frequency target_frequency, const uint32_t afe_rate, const bool transmit) {
#ifdef PRALINE
// For Praline with MAX2831 (2.3-2.6 GHz range)
// Frequencies 2170-2300 MHz need upconversion since they're below MAX2831 minimum
if (target_frequency < 2300'000'000) {
// Treat as low band - need mixer
return low_band(target_frequency);
}
// Frequencies 2300-2600 MHz can go direct (no RFFC5072)
else if (target_frequency <= 2600'000'000) {
const rf::Frequency second_lo_frequency = target_frequency;
const rf::Frequency first_lo_frequency = 0;
const bool mixer_invert = false;
return {first_lo_frequency, second_lo_frequency, rf::path::Band::Mid, mixer_invert};
}
// Frequencies 2600-2740 MHz need downconversion since they're above MAX2831 maximum
else {
// Treat as high band
return high_band(target_frequency);
/* radio.c select_img_reject() / tuning.c: on PRALINE the MAX2831 tunes
* direct (mixer bypassed) from 2320 to 2580 MHz. band_mid starts at
* TRANSITION = 2320 MHz, so everything below that already went to
* low_band(). */
if (target_frequency <= 2580'000'000) {
const PralineTuneConfig* entry = select_tune_config(target_frequency, transmit);
const uint8_t shift = (afe_rate == 0) ? 0 : entry->shift;
const rf::Frequency analog_rf = analog_from_digital_rf(target_frequency, shift, afe_rate);
/* Mixer bypassed: no first LO, the MAX2831 tunes the (offset)
* analogue RF directly and the FPGA rotates it back. */
return {0, analog_rf, rf::path::Band::Mid, false, shift};
}
/* 2580-2740 MHz: above the bypass window, downconvert. */
return high_band(target_frequency);
#else
(void)afe_rate;
(void)transmit;
const rf::Frequency second_lo_frequency = target_frequency;
const rf::Frequency first_lo_frequency = 0;
const bool mixer_invert = false;
@@ -203,12 +436,12 @@ Config high_band(const rf::Frequency target_frequency) {
return {first_lo_frequency, second_lo_frequency, rf::path::Band::High, mixer_invert};
}
Config create(const rf::Frequency target_frequency) {
Config create(const rf::Frequency target_frequency, const uint32_t afe_rate, const bool transmit) {
/* TODO: This is some lame code. */
if (rf::path::band_low.contains(target_frequency)) {
return low_band(target_frequency);
return low_band(target_frequency, afe_rate, transmit);
} else if (rf::path::band_mid.contains(target_frequency)) {
return mid_band(target_frequency);
return mid_band(target_frequency, afe_rate, transmit);
} else if (rf::path::band_high.contains(target_frequency)) {
return high_band(target_frequency);
} else {
+21 -4
View File
@@ -33,18 +33,21 @@ struct Config {
: first_lo_frequency(0),
second_lo_frequency(0),
rf_path_band(rf::path::Band::Mid),
mixer_invert(false) {
mixer_invert(false),
quarter_shift(0) {
}
constexpr Config(
rf::Frequency first_lo_frequency,
rf::Frequency second_lo_frequency,
rf::path::Band rf_path_band,
bool mixer_invert)
bool mixer_invert,
uint8_t quarter_shift = 0)
: first_lo_frequency(first_lo_frequency),
second_lo_frequency(second_lo_frequency),
rf_path_band(rf_path_band),
mixer_invert(mixer_invert) {
mixer_invert(mixer_invert),
quarter_shift(quarter_shift) {
}
bool is_valid() const {
@@ -55,9 +58,23 @@ struct Config {
const rf::Frequency second_lo_frequency;
const rf::path::Band rf_path_band;
const bool mixer_invert;
/* PRALINE only: FPGA RX quarter-rate shift mode, in the encoding the
* gateware expects in the top two bits of register 0x03 (rx_pstep):
* 0b00 = none, 0b11 = up, 0b01 = down.
* Matches fpga_quarter_shift_mode_t in hackrf/firmware/common/fpga.h.
* Always 0 on HackRF One (no FPGA). */
const uint8_t quarter_shift;
};
Config create(const rf::Frequency target_frequency);
/* afe_rate is the ADC sample rate in Hz (output rate << decimation), needed on
* PRALINE to work out how far off centre the quarter-rate shift places the
* analogue passband. Pass 0 (or leave defaulted) to disable the shift.
* transmit selects the TX tuning table. Both are ignored on HackRF One. */
Config create(
const rf::Frequency target_frequency,
const uint32_t afe_rate = 0,
const bool transmit = false);
} /* namespace config */
} /* namespace tuning */
+1 -1
View File
@@ -4,7 +4,7 @@
* Copyright (C) 2019 Elia Yehuda (z4ziggy)
* Copyright (C) 2023 Mark Thompson
* Copyright (C) 2024 u-foka
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
* copyleft 2024 zxkmm
*
* This file is part of PortaPack.
*
+18 -2
View File
@@ -83,19 +83,35 @@ GeoPos::GeoPos(
field_lon_seconds.on_change = changed_fn;
const auto wrapped_lat_seconds = [this](int32_t v) {
const auto old_minutes = field_lat_minutes.value();
field_lat_minutes.on_encoder(v);
if (field_lat_minutes.value() == old_minutes) {
field_lat_seconds.set_value((v > 0) ? 59 : 0);
}
};
const auto wrapped_lat_minutes = [this](int32_t v) {
field_lat_degrees.on_encoder((field_lat_degrees.value() >= 0) ? v : -v);
const auto old_degrees = field_lat_degrees.value();
field_lat_degrees.on_encoder((old_degrees >= 0) ? v : -v);
if (field_lat_degrees.value() == old_degrees) {
field_lat_minutes.set_value((v > 0) ? 59 : 0);
}
};
const auto wrapped_lon_seconds = [this](int32_t v) {
const auto old_minutes = field_lon_minutes.value();
field_lon_minutes.on_encoder(v);
if (field_lon_minutes.value() == old_minutes) {
field_lon_seconds.set_value((v > 0) ? 59 : 0);
}
};
const auto wrapped_lon_minutes = [this](int32_t v) {
field_lon_degrees.on_encoder((field_lon_degrees.value() >= 0) ? v : -v);
const auto old_degrees = field_lon_degrees.value();
field_lon_degrees.on_encoder((old_degrees >= 0) ? v : -v);
if (field_lon_degrees.value() == old_degrees) {
field_lon_minutes.set_value((v > 0) ? 59 : 0);
}
};
field_lat_seconds.on_wrap = wrapped_lat_seconds;
+4 -2
View File
@@ -344,7 +344,9 @@ bool MenuView::on_key(const KeyEvent key) {
}
[[fallthrough]];
case KeyEvent::Select:
if (menu_items[highlighted_item].on_select) {
// NB: a MenuView can legitimately be empty (e.g. after clear()) while
// still holding focus; indexing menu_items unchecked faults there.
if (highlighted_item < menu_items.size() && menu_items[highlighted_item].on_select) {
menu_items[highlighted_item].on_select(key);
}
return true;
@@ -364,7 +366,7 @@ bool MenuView::on_keyboard(const KeyboardEvent key) {
if (key == '-') return set_highlighted(highlighted_item - 1);
if (key == '+') return set_highlighted(highlighted_item + 1);
if (key == 10) {
if (menu_items[highlighted_item].on_select) {
if (highlighted_item < menu_items.size() && menu_items[highlighted_item].on_select) {
menu_items[highlighted_item].on_select(KeyEvent::Right);
}
return true;
+2 -10
View File
@@ -26,10 +26,7 @@
#include "string_format.hpp"
#include "ui_receiver.hpp"
#include "ui_freqman.hpp"
#ifndef PRALINE
#include "audio.hpp"
#endif
using namespace portapack;
@@ -595,18 +592,13 @@ AudioVolumeField::AudioVolumeField(
/* fill char */ ' '} {
set_value(receiver_model.normalized_headphone_volume());
#ifdef PRALINE
on_change = [](int32_t v) {
receiver_model.set_normalized_headphone_volume(v);
#else
on_change = [](int32_t vol) {
// don't call receiver model, because this widget shuld be able to handle volume settinsg from any app, regardless of the receiver model's enables state. like the tx apps should be able to set volume too.
// this is identical to the receiver model's method
// Don't call receiver_model here so volume can be adjusted even when ReceiverModel is disabled (e.g. TX apps/games).
// The dB mapping mirrors ReceiverModel::set_normalized_headphone_volume().
uint8_t v = clip<uint8_t>(vol, 0, 99);
auto new_volume = volume_t::decibel(v - 99) + audio::headphone::volume_range().max;
persistent_memory::set_headphone_volume(new_volume);
audio::headphone::set_volume(new_volume);
#endif
};
}
+1 -1
View File
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* copyleft 2025 zxkmm AKA zix aka sommermorgentraum
* copyleft 2025 zxkmm
*
* This file is part of PortaPack.
*
+1 -1
View File
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* copyleft 2025 zxkmm AKA zix aka sommermorgentraum
* copyleft 2025 zxkmm
*
* This file is part of PortaPack.
*
+151 -1
View File
@@ -2,7 +2,7 @@
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2016 Furrtek
* Copyright (C) 2024 u-foka
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
* copyleft 2024 zxkmm
*
* This file is part of PortaPack.
*
@@ -49,6 +49,7 @@
#include "ui_recon.hpp"
#include "ui_search.hpp"
#include "ui_settings.hpp"
#include "ui_textentry.hpp"
#include "ui_sonde.hpp"
#include "ui_ss_viewer.hpp"
// #include "ui_test.hpp"
@@ -139,6 +140,115 @@ bool NavigationView::StartAppByName(const char* name) {
return false;
}
/* App search ***********************************************************/
namespace {
// ASCII, allocation-free, case-insensitive lowering. Kept local so this TU
// doesn't need to include <cctype> just for one small comparison.
char ascii_lower(char c) {
return (c >= 'A' && c <= 'Z') ? static_cast<char>(c - 'A' + 'a') : c;
}
// True when 'needle' occurs anywhere in 'haystack', case-insensitive.
bool name_contains(const char* haystack, const std::string& needle) {
const size_t n = needle.size();
if (n == 0)
return false;
for (size_t i = 0; haystack[i] != '\0'; ++i) {
size_t j = 0;
while (j < n && haystack[i + j] != '\0' &&
ascii_lower(haystack[i + j]) == ascii_lower(needle[j]))
++j;
if (j == n)
return true;
}
return false;
}
} // namespace
void NavigationView::start_app_search() {
app_search_query_.clear();
app_search_committed_ = false;
// Reuse the shared keyboard view; it is destroyed as soon as the user
// confirms or cancels, so no search UI ever lingers in RAM.
text_prompt(*this, app_search_query_, 20, ENTER_KEYBOARD_MODE_ALPHA,
[this](std::string& query) {
// Runs while the keyboard is still alive: only record that a
// query was confirmed; results are built once it has popped.
app_search_committed_ = !query.empty();
});
// Defer building/showing the results until the keyboard view has popped.
set_on_pop([this]() { open_app_search_results(); });
}
void NavigationView::open_app_search_results() {
if (!app_search_committed_)
return; // user cancelled with Back, or the query was empty
app_search_committed_ = false;
std::vector<AppSearchEntry> matches;
// Internal apps: match on the name shown in the menus.
for (const auto& app : appList) {
if (app.displayName != nullptr && app.viewFactory != nullptr &&
name_contains(app.displayName, app_search_query_))
matches.push_back({app.displayName, app.viewFactory, {}});
}
// External (.ppma) and standalone (.ppmp) apps on the SD card. The
// enumerator hands back pointers to short-lived buffers, so the names are
// copied into owned strings here. Match on both the friendly name and the
// file (call) name. module_included=false: PPmod apps can't be launched by
// path, so they're intentionally excluded.
ExternalItemsMenuLoader::load_all_external_items_callback(
[this, &matches](AppInfoConsole& info) {
if (name_contains(info.appFriendlyName, app_search_query_) ||
name_contains(info.appCallName, app_search_query_))
matches.push_back({info.appFriendlyName, nullptr, info.appCallName});
},
false);
if (matches.empty()) {
display_modal("Search", "No matching app found.");
return;
}
push<AppSearchResultsView>(std::move(matches));
}
void NavigationView::launch_search_entry(ViewFactoryBase* factory, std::string call_name) {
// Same close-then-open contract as StartAppByName: drop the search UI
// (freeing the results view and the very button that called us) before
// starting the target, so only one app is ever resident. Everything used
// below is a by-value argument or this resident NavigationView.
home(false);
if (factory != nullptr) {
push_view(factory->produce(*this));
return;
}
// External/standalone: AppInfoConsole doesn't record which kind it is, so
// try both extensions the way handle_autostart() does.
std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t> conv;
std::string appwithpath = "/" + apps_dir.string() + "/" + call_name + ".ppma";
std::filesystem::path pth = conv.from_bytes(appwithpath.c_str());
if (ExternalItemsMenuLoader::run_external_app(*this, pth))
return;
appwithpath = "/" + apps_dir.string() + "/" + call_name + ".ppmp";
pth = conv.from_bytes(appwithpath.c_str());
if (ExternalItemsMenuLoader::run_standalone_app(*this, pth))
return;
display_modal("Search", "Failed to start:\n" + call_name);
}
/* StatusTray ************************************************************/
StatusTray::StatusTray(Point pos)
@@ -561,6 +671,7 @@ InformationView::InformationView(
NavigationView& nav)
: nav_(nav) {
add_children({&backdrop,
&search_icon,
&version,
&ltime});
@@ -604,6 +715,19 @@ bool InformationView::firmware_checksum_error() {
return fw_checksum_error;
}
bool InformationView::on_touch(const TouchEvent event) {
// The whole info bar is one touch target. Capture on Start so the End
// event is delivered here, then act on release like a normal button tap.
switch (event.type) {
case TouchEvent::Type::End:
if (nav_.is_valid())
nav_.start_app_search();
return true;
default:
return true;
}
}
/* Navigation ************************************************************/
bool NavigationView::is_top() const {
@@ -873,6 +997,32 @@ void GamesMenuView::on_populate() {
add_external_items(nav_, app_location_t::GAMES, *this, return_icon ? 1 : 0);
}
/* AppSearchResultsView *************************************************/
AppSearchResultsView::AppSearchResultsView(NavigationView& nav, std::vector<AppSearchEntry>&& entries)
: nav_(nav), entries_(std::move(entries)) {
set_max_rows(2); // wider buttons: app names need the room
}
void AppSearchResultsView::on_populate() {
for (size_t i = 0; i < entries_.size(); ++i) {
const auto& entry = entries_[i];
// Internal apps are green, external/standalone orange, matching the
// "yellow-ish external" convention used elsewhere. No icon: the cheap
// enumerator doesn't decode external bitmaps.
add_item({entry.display,
entry.factory ? Color::green() : Color::orange(),
nullptr,
[this, i]() {
// Read the target off entries_ and pass it by value: the
// call below frees this view via home(), so nothing after
// it may touch 'this' or its members.
nav_.launch_search_entry(entries_[i].factory, entries_[i].call_name);
}},
true);
}
}
/* SystemMenuView ********************************************************/
void SystemMenuView::hackrf_mode(NavigationView& nav) {
+55 -2
View File
@@ -2,7 +2,7 @@
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2016 Furrtek
* Copyright (C) 2024 u-foka
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
* copyleft 2024 zxkmm
*
* This file is part of PortaPack.
*
@@ -86,6 +86,14 @@ struct AppInfoConsole {
const app_location_t appLocation;
};
// One row in the app-search results list. Strings are owned copies: the
// external-app enumerator hands back pointers to short-lived buffers.
struct AppSearchEntry {
std::string display; // friendly name shown to the user
ViewFactoryBase* factory; // internal app factory, or nullptr for external/standalone
std::string call_name; // external/standalone file name (no extension); unused when internal
};
class NavigationView : public View {
public:
std::function<void(const View&)> on_view_changed{};
@@ -138,12 +146,34 @@ class NavigationView : public View {
bool StartAppByName(const char* name); // Starts a View (app) by name stored in appListFC. This is to start apps from console
void handle_autostart();
// Opens a keyboard; once confirmed, shows a pick-list of every internal,
// external (.ppma) and standalone (.ppmp) app whose name matches the query.
// Reuses the transient text-entry view, so the only steady-state cost is
// the two members below; the keyboard and the results list are allocated
// only while a search is in progress.
void start_app_search();
// Launches one search result and drops the search UI first, so only one app
// is ever resident. 'factory' non-null selects an internal app; otherwise
// 'call_name' is an external/standalone app file name (no extension).
// Arguments are taken by value on purpose: home() below frees the results
// view (and the button that called this), so nothing here may touch it.
void launch_search_entry(ViewFactoryBase* factory, std::string call_name);
private:
struct ViewState {
std::unique_ptr<View> view;
std::function<void()> on_pop;
};
// Builds and pushes the results list. Runs from the keyboard view's on_pop,
// i.e. after that view is fully destroyed, so pushing here can't be
// clobbered by the keyboard's own trailing pop().
void open_app_search_results();
std::string app_search_query_{}; // keyboard buffer, owned across its lifetime
bool app_search_committed_{false}; // true only when the user confirmed a non-empty query
std::vector<ViewState> view_stack{};
Widget* view() const;
@@ -329,6 +359,9 @@ class InformationView : public View {
void refresh();
bool firmware_checksum_error();
// Whole info bar is a touch target that opens the app search.
bool on_touch(const TouchEvent event) override;
private:
// static constexpr auto version_string = "v1.4.4"; // This is commented out as we are now setting the version via ENV (VERSION_STRING=v1.0.0)
NavigationView& nav_;
@@ -337,8 +370,15 @@ class InformationView : public View {
{0, 0, screen_width, 16},
Theme::getInstance()->bg_darker->background};
// Visual hint that the info bar starts an app search when tapped.
Image search_icon{
{2, 0, 16, 16},
&bitmap_icon_search,
Theme::getInstance()->fg_light->foreground,
Theme::getInstance()->bg_darker->background};
Text version{
{2, 0, 11 * 8, 16},
{18, 0, 9 * 8, 16}, // 9 chars keeps room for the "FLASH ERR" indicator; right edge unchanged from before the icon
VERSION_STRING};
LiveDateTime ltime{
@@ -422,6 +462,19 @@ class SystemMenuView : public BtnGridView {
void hackrf_mode(NavigationView& nav);
};
// Pick-list of apps matching an app-search query. Reuses the button grid so it
// behaves (and launches on tap) exactly like the normal app menus.
class AppSearchResultsView : public BtnGridView {
public:
AppSearchResultsView(NavigationView& nav, std::vector<AppSearchEntry>&& entries);
std::string title() const override { return "Search"; };
private:
NavigationView& nav_;
std::vector<AppSearchEntry> entries_;
void on_populate() override;
};
class SystemView : public View {
public:
SystemView(
+1 -1
View File
@@ -1,5 +1,5 @@
/*
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
* copyleft 2024 zxkmm
* Copyright (C) 2024 HTotoo
*
* This file is part of PortaPack.
+1 -1
View File
@@ -1,5 +1,5 @@
/*
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
* copyleft 2024 zxkmm
* Copyright (C) 2024 HTotoo
*
* This file is part of PortaPack.

Some files were not shown because too many files have changed in this diff Show More