Compare commits

...

31 Commits

Author SHA1 Message Date
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
Totoo f5a5e8e5e8 Map improv. (#3226) 2026-06-16 17:54:32 +02:00
Synray d8a1e2530a Add Security+ 2.0 TX App (#3217)
* Add U64 BoundSetting
* Add secplus-tx
* Add secplus license
* Update icon
* UI update
* Add filesystem::path BoundSetting
* Add file UI
* Rename file
* Add save button
* Fix warnings, add integer conversions to UI macros
* UI update
* Copilot suggestions, name edit UI
* Fix buffer length
* Style update
* Avoid recursion in Widget::style()
* Apply suggestions from code review
* Move secplustx.cpp to external
* Shorten title
* Space optimizations, remove FilePath setting
* Remove assertions
* Change namespace
* Change title to match
2026-06-15 19:28:47 +02:00
Totoo b84a424875 MBT update 2026-06-13 09:50:25 +08:00
154 changed files with 7501 additions and 1467 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/*
+2 -1
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
@@ -513,3 +512,5 @@ add_custom_target(
${PROJECT_NAME}
DEPENDS ${PROJECT_NAME}.bin
)
add_dependencies(${PROJECT_NAME} baseband)
+8 -3
View File
@@ -50,6 +50,9 @@ void BoundSetting::parse(std::string_view value) {
case SettingType::I64:
parse_int(value, as<int64_t>());
break;
case SettingType::U64:
parse_int(value, as<uint64_t>());
break;
case SettingType::I32:
parse_int(value, as<int32_t>());
break;
@@ -87,6 +90,9 @@ void BoundSetting::write(File& file) const {
case SettingType::I64:
file.write(to_string_dec_int(as<int64_t>(), buffer, length), length);
break;
case SettingType::U64:
file.write(to_string_dec_uint(as<uint64_t>(), buffer, length), length);
break;
case SettingType::I32:
file.write(to_string_dec_int(as<int32_t>(), buffer, length), length);
break;
@@ -97,9 +103,8 @@ void BoundSetting::write(File& file) const {
file.write(to_string_dec_uint(as<uint8_t>(), buffer, length), length);
break;
case SettingType::String: {
const auto& str = as<std::string>();
file.write(str.data(), str.length());
break;
file.write_line(as<std::string>());
return;
}
case SettingType::Bool:
file.write(as<bool>() ? "1" : "0", 1);
+4
View File
@@ -48,6 +48,7 @@ class BoundSetting {
/* The type of bound setting. */
enum class SettingType : uint8_t {
I64,
U64,
I32,
U32,
U8,
@@ -60,6 +61,9 @@ class BoundSetting {
BoundSetting(std::string_view name, int64_t* target)
: name_{name}, target_{target}, type_{SettingType::I64} {}
BoundSetting(std::string_view name, uint64_t* target)
: name_{name}, target_{target}, type_{SettingType::U64} {}
BoundSetting(std::string_view name, int32_t* target)
: name_{name}, target_{target}, type_{SettingType::I32} {}
+2 -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,
@@ -668,9 +668,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();
+47 -44
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
@@ -1178,8 +1182,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 +1214,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 +1262,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
+35 -1
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
@@ -327,7 +336,7 @@ set(EXTCPPSRC
external/rtty_tx/ui_rtty_tx.cpp
external/rtty_tx/baudot.cpp
#pocsag_tx
#pocsag_tx
external/pocsag_tx/main.cpp
external/pocsag_tx/ui_pocsag_tx.cpp
@@ -362,6 +371,26 @@ set(EXTCPPSRC
#hard_reset
external/hard_reset/main.cpp
external/hard_reset/ui_hard_reset.cpp
#secplustx
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
@@ -396,6 +425,8 @@ set(EXTAPPLIST
acars_rx
wefax_rx
noaaapt_rx
vor_rx
vor_tx
shoppingcart_lock
ookbrute
ook_editor
@@ -451,6 +482,9 @@ set(EXTAPPLIST
two_tone_pager
two_tone_rx
hard_reset
secplustx
signal_hunter
tetra_rx
)
# sdusb has type conflicts with PRALINE (HackRF Pro) - add only for non-PRALINE builds
+38
View File
@@ -110,6 +110,11 @@ MEMORY
ram_external_app_two_tone_rx (rwx) : org = 0xAE050000, len = 32k
ram_external_app_flex_tx (rwx) : org = 0xAE060000, len = 32k
ram_external_app_hard_reset (rwx) : org = 0xAE070000, len = 32k
ram_external_app_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
@@ -390,6 +395,18 @@ SECTIONS
*(*ui*external_app*noaaapt_rx*);
} > ram_external_app_noaaapt_rx
.external_app_vor_rx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_vor_rx.application_information));
*(*ui*external_app*vor_rx*);
} > ram_external_app_vor_rx
.external_app_vor_tx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_vor_tx.application_information));
*(*ui*external_app*vor_tx*);
} > ram_external_app_vor_tx
.external_app_breakout : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_breakout.application_information));
@@ -635,4 +652,25 @@ SECTIONS
KEEP(*(.external_app.app_hard_reset.application_information));
*(*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*);
} > ram_external_app_secplustx
.external_app_signal_hunter : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_signal_hunter.application_information));
*(*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));
*(*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.
*
+81
View File
@@ -0,0 +1,81 @@
/*
* Copyright (C) 2026 Synray
*
* 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_navigation.hpp"
#include "ui_secplustx.hpp"
#include "external_app.hpp"
namespace ui::external_app::ui_secplustx {
void initialize_app(ui::NavigationView& nav) {
nav.push<SecplusTXView>();
}
} // namespace ui::external_app::ui_secplustx
extern "C" {
__attribute__((section(".external_app.app_secplustx.application_information"), used)) application_information_t _application_information_secplustx = {
/*.memory_location = */ (uint8_t*)0x00000000,
/*.externalAppEntry = */ ui::external_app::ui_secplustx::initialize_app,
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
/*.app_name = */ "Security+",
/*.bitmap_data = */ {
0b00100000,
0b00000000,
0b00100000,
0b00100000,
0b00100000,
0b01110000,
0b00100000,
0b00100000,
0b11100000,
0b00000111,
0b11110000,
0b00001111,
0b00110000,
0b00001100,
0b00110000,
0b00001100,
0b11110000,
0b00001111,
0b11110000,
0b00001111,
0b01110000,
0b00001101,
0b10110000,
0b00001110,
0b01110000,
0b00001101,
0b10110000,
0b00001110,
0b11110000,
0b00001111,
0b11100000,
0b00000111,
},
/*.icon_color = */ ui::Color::yellow().v,
/*.menu_location = */ app_location_t::TX,
/*.desired_menu_position = */ -1,
/*.m4_app_tag = portapack::spi_flash::image_tag_ook */ {'P', 'O', 'O', 'K'},
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
};
}
+150
View File
@@ -0,0 +1,150 @@
/*
* Copyright 2022 Clayton Smith (argilo@gmail.com)
*
* This file is part of secplus.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
*/
#include "secplustx.hpp"
namespace ui::external_app::ui_secplustx {
static void v2_calc_parity(const uint64_t fixed, uint32_t* data) {
uint32_t parity = (fixed >> 32) & 0xf;
int8_t offset;
*data &= 0xffff0fff;
for (offset = 0; offset < 32; offset += 4) {
parity ^= ((*data >> offset) & 0xf);
}
*data |= (parity << 12);
}
static void encode_v2_rolling(const uint32_t rolling,
uint32_t* rolling_halves) {
uint32_t rolling_reversed = 0;
int8_t i, half;
for (i = 0; i < 28; i++) {
rolling_reversed |= ((rolling >> i) & 1) << (28 - i - 1);
}
rolling_halves[0] = 0;
rolling_halves[1] = 0;
for (half = 0; half < 2; half++) {
for (i = 0; i < 8; i += 2) {
rolling_halves[half] |= rolling_reversed % 3 << i;
rolling_reversed /= 3;
}
}
for (half = 0; half < 2; half++) {
for (i = 10; i < 18; i += 2) {
rolling_halves[half] |= rolling_reversed % 3 << i;
rolling_reversed /= 3;
}
}
rolling_halves[0] |= (rolling_reversed % 3) << 8;
rolling_reversed /= 3;
rolling_halves[1] |= (rolling_reversed % 3) << 8;
}
static const int8_t ORDER[16] = {9, 33, 6, -1, 24, 18, 36, -1,
24, 36, 6, -1, -1, -1, -1, -1};
static const int8_t INVERT[16] = {6, 2, 1, -1, 7, 5, 3, -1,
4, 0, 5, -1, -1, -1, -1, -1};
static void v2_scramble(const uint32_t* parts, const uint8_t frame_type, uint8_t* packet_half) {
const int8_t order = ORDER[packet_half[0] >> 4];
const int8_t invert = INVERT[packet_half[0] & 0xf];
int8_t i;
uint8_t out_offset = 10;
int8_t end;
uint32_t parts_permuted[3];
end = (frame_type == 0 ? 5 : 8);
for (i = 1; i < end; i++) {
packet_half[i] = 0;
}
parts_permuted[0] =
(invert & 4) ? ~parts[(order >> 4) & 3] : parts[(order >> 4) & 3];
parts_permuted[1] =
(invert & 2) ? ~parts[(order >> 2) & 3] : parts[(order >> 2) & 3];
parts_permuted[2] = (invert & 1) ? ~parts[order & 3] : parts[order & 3];
end = (frame_type == 0 ? 8 : 0);
for (i = 18 - 1; i >= end; i--) {
packet_half[out_offset >> 3] |= ((parts_permuted[0] >> i) & 1)
<< (7 - (out_offset % 8));
out_offset++;
packet_half[out_offset >> 3] |= ((parts_permuted[1] >> i) & 1)
<< (7 - (out_offset % 8));
out_offset++;
packet_half[out_offset >> 3] |= ((parts_permuted[2] >> i) & 1)
<< (7 - (out_offset % 8));
out_offset++;
}
}
static void encode_v2_half_parts(const uint32_t rolling, const uint32_t fixed, const uint16_t data, const uint8_t frame_type, uint8_t* packet_half) {
uint32_t parts[3];
parts[0] = ((fixed >> 10) << 8) | (data >> 8);
parts[1] = ((fixed & 0x3ff) << 8) | (data & 0xff);
parts[2] = rolling;
packet_half[0] = (uint8_t)rolling;
v2_scramble(parts, frame_type, packet_half);
}
static int8_t v2_check_limits(const uint32_t rolling, const uint64_t fixed) {
if ((rolling >> 28) != 0) {
return -1;
}
if ((fixed >> 40) != 0) {
return -1;
}
return 0;
}
static void encode_v2_half(const uint32_t rolling, const uint32_t fixed, const uint16_t data, const uint8_t frame_type, uint8_t* packet_half) {
encode_v2_half_parts(rolling, fixed, data, frame_type, packet_half);
/* shift indicator two bits to the right */
packet_half[1] |= (packet_half[0] & 0x3) << 6;
packet_half[0] >>= 2;
/* set frame type */
packet_half[0] |= (frame_type << 6);
}
int8_t encode_v2(const uint32_t rolling, const uint64_t fixed, uint32_t data, const uint8_t frame_type, uint8_t* packet1, uint8_t* packet2) {
int8_t err = 0;
uint32_t rolling_halves[2];
err = v2_check_limits(rolling, fixed);
if (err < 0) {
return err;
}
encode_v2_rolling(rolling, rolling_halves);
v2_calc_parity(fixed, &data);
encode_v2_half(rolling_halves[0], fixed >> 20, data >> 16, frame_type,
packet1);
encode_v2_half(rolling_halves[1], fixed & 0xfffff, data & 0xffff, frame_type,
packet2);
return 0;
}
}; // namespace ui::external_app::ui_secplustx
+19
View File
@@ -0,0 +1,19 @@
/*
* Copyright 2022 Clayton Smith (argilo@gmail.com)
*
* This file is part of secplus.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
*/
#ifndef __SECPLUSTX__
#define __SECPLUSTX__
#include <cstdint>
namespace ui::external_app::ui_secplustx {
int8_t encode_v2(const uint32_t rolling, const uint64_t fixed, uint32_t data, const uint8_t frame_type, uint8_t* packet1, uint8_t* packet2);
};
#endif
+227
View File
@@ -0,0 +1,227 @@
#include "ui_secplustx.hpp"
#include <string_view>
#include <string>
#include <utility>
#include <vector>
#include "secplustx.hpp"
#include "ui_fileman.hpp"
#include "file_reader.hpp"
#include "baseband_api.hpp"
#include "string_format.hpp"
#include "optional.hpp"
using namespace portapack;
using namespace ui;
namespace fs = std::filesystem;
namespace ui::external_app::ui_secplustx {
static constexpr uint8_t secplus_repeat_count = 3;
static constexpr float secplus_sample_rate = OOK_SAMPLERATE / 4000.0f;
Optional<SecplusData> SecplusTXView::read_secplus_file(const fs::path& file_path) {
File file{};
if (auto error = file.open(file_path)) return {};
std::string raw = *FileLineReader{file}.begin();
std::vector chunks = split_string(raw, ';');
if (chunks.empty()) return {};
SecplusData data{};
data.version = static_cast<SecplusVersion>(std::strtoul(chunks[0].data(), NULL, 10));
switch (data.version) {
case SecplusVersion::V1:
return {}; // unimplemented
case SecplusVersion::V2: {
if (chunks.size() != 6) return {};
data.name = std::string{chunks[1]};
if (data.name.empty()) data.name = "Remote";
data.has_data = std::strtoul(chunks[2].data(), NULL, 10);
data.fixed_code = std::strtoull(chunks[3].data(), NULL, 16);
data.rolling_code = std::strtoul(chunks[4].data(), NULL, 16);
data.data = std::strtoul(chunks[5].data(), NULL, 16);
return data;
}
default:
return {};
}
}
bool SecplusTXView::write_secplus_file(const fs::path& file_path, const SecplusData& data) {
ensure_directory(secplus_dir);
File file{};
if (auto error = file.create(file_path)) {
return false;
}
switch (data.version) {
case SecplusVersion::V1:
return false; // unimplemented
case SecplusVersion::V2: {
std::string formatted = to_string_dec_uint(static_cast<uint8_t>(data.version));
formatted += ';';
formatted += data.name;
formatted += ';';
formatted += to_string_dec_uint(data.has_data);
formatted += ';';
formatted += to_string_hex(data.fixed_code);
formatted += ';';
formatted += to_string_hex(data.rolling_code);
formatted += ';';
formatted += to_string_hex(data.data);
file.write_line(formatted);
file.close();
return true;
}
default:
return false;
}
}
SecplusTXView::SecplusTXView(NavigationView& nav)
: nav_{nav} {
if (!fs::file_exists(file_path)) write_secplus_file(file_path, data);
baseband::run_image(portapack::spi_flash::image_tag_ook);
add_children({&button_open,
&button_save,
&field_name,
&labels,
&field_fixed,
&field_rolling,
&has_data,
&field_data,
&learn_mode,
&autosave,
&progressbar,
&tx_view});
button_open.on_select = [this](const Button&) {
ensure_directory(secplus_dir);
auto open_view = nav_.push<FileLoadView>(".SECPLUS");
open_view->push_dir(secplus_dir);
open_view->on_changed = [this](fs::path path) { file_path = path.string(); };
nav_.set_on_pop([this]() { reload_data(); });
};
button_save.on_select = [this](const Button&) { write_secplus_file(file_path, data); };
field_name.on_select = [this, &nav](TextField&) {
buffer = data.name;
text_prompt(nav_, buffer, field_name.parent_rect().width() / UI_POS_DEFAULT_WIDTH, ENTER_KEYBOARD_MODE_ALPHA, [this](std::string& new_name) {
data.name = new_name;
field_name.set_text(new_name);
save_data();
});
};
field_fixed.on_change = [this](SymField& field) { data.fixed_code = field.to_integer(); };
field_rolling.on_change = [this](SymField& field) { data.rolling_code = field.to_integer(); };
field_data.on_change = [this](SymField& field) { data.data = field.to_integer(); };
has_data.on_select = [this](Checkbox& checkbox, bool value) {
data.has_data = value;
// fade if inactive, otherwise use default style
const Style* new_style = value ? &style() : Theme::getInstance()->fg_medium;
checkbox.set_style(new_style);
field_data.set_style(new_style);
field_data.set_focusable(value);
};
tx_view.on_edit_frequency = [this]() {
auto new_view = nav_.push<FrequencyKeypadView>(transmitter_model.target_frequency());
new_view->on_changed = [](rf::Frequency f) {
transmitter_model.set_target_frequency(f);
};
};
tx_view.on_start = [this]() { start_tx(); };
tx_view.on_stop = [this]() {
baseband::kill_ook();
stop_tx();
};
autosave.set_value(true);
reload_data();
}
void SecplusTXView::save_data() {
if (autosave.value()) write_secplus_file(file_path, data);
}
void SecplusTXView::reload_data() {
if (auto result = read_secplus_file(file_path)) data = std::move(*result);
// always update data, use default values if read failed
field_name.set_text(data.name);
has_data.set_value(data.has_data);
field_rolling.set_value(data.rolling_code);
field_fixed.set_value(data.fixed_code);
field_data.set_value(data.data);
}
void SecplusTXView::start_tx() {
uint8_t packet1[8]{};
uint8_t packet2[8]{};
size_t bitstream_length = 0;
constexpr uint8_t packet1_indicator = 0b00;
constexpr uint8_t packet2_indicator = 0b01;
auto encode_packet = [&bitstream_length](uint8_t indicator, auto& packet, bool has_data) {
constexpr uint32_t preamble = 0b0000000000000000'1111;
constexpr uint32_t blank_size = 24;
auto manchester_encode = [&bitstream_length](auto& x, uint32_t size) {
for (uint32_t i = 0; i < size; ++i) {
bool bit = (x >> (size - 1 - i)) & 1;
encoders::bitstream_append(bitstream_length, 2, bit ? 0b01 : 0b10);
}
};
manchester_encode(preamble, 20);
manchester_encode(indicator, 2);
for (uint8_t byte = 0; byte < (has_data ? 8 : 5); ++byte) manchester_encode(packet[byte], 8);
encoders::bitstream_append(bitstream_length, blank_size, 0);
};
if (encode_v2(data.rolling_code, data.fixed_code, data.data, data.has_data, packet1, packet2) < 0) {
nav_.display_modal("Error", "Invalid rolling/fixed code");
return;
}
encode_packet(packet1_indicator, packet1, has_data.value());
encode_packet(packet2_indicator, packet2, has_data.value());
if (!learn_mode.value()) {
field_rolling.set_value(++data.rolling_code);
field_rolling.set_dirty();
}
progressbar.set_max(secplus_repeat_count);
progressbar.set_value(1);
tx_view.set_transmitting(true);
transmitter_model.enable();
baseband::set_ook_data(
bitstream_length,
secplus_sample_rate,
secplus_repeat_count,
0);
}
void SecplusTXView::stop_tx() {
transmitter_model.disable();
progressbar.set_value(0);
tx_view.set_transmitting(false);
}
void SecplusTXView::on_tx_progress(uint32_t progress, bool done) {
progressbar.set_value(progress + 1);
if (done) {
stop_tx();
save_data();
}
}
SecplusTXView::~SecplusTXView() {
transmitter_model.disable();
baseband::shutdown();
save_data();
}
} // namespace ui::external_app::ui_secplustx
@@ -0,0 +1,96 @@
#ifndef __UI_SECPLUSTX__
#define __UI_SECPLUSTX__
#include <cstdint>
#include <string>
#include "ui.hpp"
#include "file.hpp"
#include "ui_transmitter.hpp"
#include "transmitter_model.hpp"
#include "file_path.hpp"
#include "app_settings.hpp"
#include "radio_state.hpp"
#include "encoders.hpp"
#include "string_format.hpp"
namespace ui::external_app::ui_secplustx {
enum class SecplusVersion : uint8_t {
V1,
V2,
};
struct SecplusData {
SecplusVersion version;
std::string name;
bool has_data;
uint64_t fixed_code;
uint32_t rolling_code;
uint32_t data;
};
class SecplusTXView : public View {
public:
void focus() override { button_open.focus(); }
SecplusTXView(NavigationView& nav);
~SecplusTXView();
std::string title() const override {
return "Security+";
}
private:
std::string file_path = (secplus_dir / "DEFAULT.SECPLUS").string();
SecplusData data{SecplusVersion::V2, "Remote", false, 0, 0, 0};
std::string buffer{};
NavigationView& nav_;
TxRadioState radio_state_{
315000000,
1750000,
OOK_SAMPLERATE};
app_settings::SettingsManager settings_{"tx_secplus", app_settings::Mode::TX, {{"file_path"sv, &file_path}}};
Button button_open{{UI_POS_X(1), UI_POS_Y(1), screen_width / 2 - UI_POS_X(1), UI_POS_HEIGHT(2)}, "Open"};
Button button_save{{screen_width / 2, UI_POS_Y(1), screen_width / 2 - UI_POS_X(1), UI_POS_HEIGHT(2)}, "Save"};
// remote data
TextField field_name{{UI_POS_X(1), UI_POS_Y(3), UI_POS_WIDTH_REMAINING(2), UI_POS_HEIGHT(1)}, "Remote"};
Labels labels{
{{UI_POS_X(1), UI_POS_Y(4)}, "Fixed:", Theme::getInstance()->fg_medium->foreground},
{{UI_POS_X(1), UI_POS_Y(5)}, "Rolling:", Theme::getInstance()->fg_medium->foreground}};
SymField field_fixed{{UI_POS_X(10), UI_POS_Y(4)}, 10, SymField::Type::Hex, true};
SymField field_rolling{{UI_POS_X(10), UI_POS_Y(5)}, 7, SymField::Type::Hex, true};
Checkbox has_data{{UI_POS_X(1), UI_POS_Y(6)}, 5, "Data:", true};
SymField field_data{{UI_POS_X(10), UI_POS_Y(6)}, 8, SymField::Type::Hex, true};
// options
Checkbox learn_mode{{UI_POS_X(1), UI_POS_Y(7)}, 5, "Learn", true};
Checkbox autosave{{UI_POS_X(1), UI_POS_Y(8)}, 8, "Autosave", true};
ProgressBar progressbar{{UI_POS_X(2), UI_POS_Y_BOTTOM(5.75), UI_POS_WIDTH_REMAINING(4), UI_POS_HEIGHT(1)}};
TransmitterView tx_view{UI_POS_Y_BOTTOM(4), 1000000, 0};
Optional<SecplusData> read_secplus_file(const std::filesystem::path& file_path);
bool write_secplus_file(const std::filesystem::path& file_path, const SecplusData& data);
void save_data();
void reload_data();
void start_tx();
void stop_tx();
void on_tx_progress(uint32_t progress, bool done);
MessageHandlerRegistration message_handler_tx_progress{
Message::ID::TXProgress,
[this](const Message* const p) {
const auto message = *reinterpret_cast<const TXProgressMessage*>(p);
this->on_tx_progress(message.progress, message.done);
}};
};
} // namespace ui::external_app::ui_secplustx
#endif
+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__
+1
View File
@@ -57,4 +57,5 @@ const std::filesystem::path macaddress_dir = u"MACADDRESS";
const std::filesystem::path splash_dot_bmp = u"/splash.bmp";
const std::filesystem::path keeloq_keys_dir = u"KEELOQKEYS";
const std::filesystem::path keeloq_remotes_dir = u"KEELOQREMOTES";
const std::filesystem::path secplus_dir = u"SECPLUS";
const std::filesystem::path epirb_dir = u"EPIRB";
+1
View File
@@ -58,6 +58,7 @@ extern const std::filesystem::path waterfalls_dir;
extern const std::filesystem::path macaddress_dir;
extern const std::filesystem::path keeloq_keys_dir;
extern const std::filesystem::path keeloq_remotes_dir;
extern const std::filesystem::path secplus_dir;
extern const std::filesystem::path epirb_dir;
extern const std::filesystem::path splash_dot_bmp;
+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);
+12 -13
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,
@@ -148,10 +151,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;
@@ -280,9 +279,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) {
@@ -395,7 +394,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 +441,8 @@ void disable() {
first_if.disable();
set_rf_amp(false);
led_rx.off();
led_tx.off();
led_rx.setInactive();
led_tx.setInactive();
}
#ifdef PRALINE
+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"
+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.
*
+67 -25
View File
@@ -237,26 +237,69 @@ void GeoMap::map_read_line_bin(ui::Color* buffer, uint16_t pixels) {
if (map_zoom == 1) {
map_file.read(buffer, pixels << 1);
} else if (map_zoom > 1) {
map_file.read(buffer, (pixels / map_zoom) << 1);
// Zoom in: Expand each pixel to "map_zoom" number of pixels.
// Future TODO: Add dithering to smooth out the pixelation.
// As long as MOD(width,map_zoom)==0 then we don't need to check buffer overflow case when stretching last pixel;
// For 240 width, than means no check is needed for map_zoom values up to 6.
// (Rectangle height must also divide evenly into map_zoom or we get black lines at end of screen)
// Note that zooming in results in a map offset of (1/map_zoom) pixels to the right & downward directions (see zoom_pixel_offset).
for (int i = (width / map_zoom) - 1; i >= 0; i--) {
for (int j = 0; j < map_zoom; j++) {
buffer[(i * map_zoom) + j] = buffer[i];
// Calculate how many source pixels we actually need from the file
uint16_t src_pixels_needed = (pixels + map_zoom - 1) / map_zoom;
if (src_pixels_needed == 0) src_pixels_needed = 1;
// Position the source data at the very END of the buffer.
// This allows us to overwrite the buffer from left-to-right safely.
uint16_t src_offset = pixels - src_pixels_needed;
// Read directly into the tail of the target buffer. Zero extra RAM needed.
map_file.read(&buffer[src_offset], src_pixels_needed << 1);
// Process forwards. Because `dst` grows by 1 and the source index grows by 1/map_zoom,
// `dst` will never catch up to overwrite a source pixel before we are done with it.
for (uint16_t dst = 0; dst < pixels; ++dst) {
uint16_t i0 = dst / map_zoom;
uint16_t i1 = i0 + 1;
// Clamp the right-hand pixel to avoid reading out of bounds on the last iteration
if (i1 >= src_pixels_needed) {
i1 = src_pixels_needed - 1;
}
// 'frac' represents the integer distance from the left pixel (0 to map_zoom - 1)
uint16_t frac = dst % map_zoom;
// Fetch colors from the tail of our buffer
ui::Color c0 = buffer[src_offset + i0];
ui::Color c1 = buffer[src_offset + i1];
if (frac == 0) {
// Exact pixel match, bypass the math entirely for a speed boost
buffer[dst] = c0;
} else {
uint32_t inv_frac = map_zoom - frac;
uint8_t r = ((uint32_t)c0.r() * inv_frac + (uint32_t)c1.r() * frac) / map_zoom;
uint8_t g = ((uint32_t)c0.g() * inv_frac + (uint32_t)c1.g() * frac) / map_zoom;
uint8_t b = ((uint32_t)c0.b() * inv_frac + (uint32_t)c1.b() * frac) / map_zoom;
buffer[dst] = ui::Color(r, g, b);
}
}
} else {
ui::Color zoom_out_buffer[(pixels * (-map_zoom))];
map_file.read(zoom_out_buffer, (pixels * (-map_zoom)) << 1);
// Zoom out: Collapse each group of "-map_zoom" pixels into one pixel.
// Future TODO: Average each group of pixels (in both X & Y directions if possible).
for (int i = 0; i < width; i++) {
buffer[i] = zoom_out_buffer[i * (-map_zoom)];
const int skip = -map_zoom;
const int MAX_BUFFER_ELEMENTS = 256;
// Fixed-size local array avoids VLA crashes.
ui::Color zoom_out_buffer[MAX_BUFFER_ELEMENTS];
const int total_elements_needed = pixels * skip;
// Use the default size, but strictly cap it at 256 to protect the stack
const int chunk_size = total_elements_needed < MAX_BUFFER_ELEMENTS ? total_elements_needed : MAX_BUFFER_ELEMENTS;
int target_i = 0;
int current_file_offset = 0;
// Sequentially read through the required portion of the file in chunks
while (target_i < width && current_file_offset < total_elements_needed) {
// Calculate how many pixels we can read in this specific pass
int read_size = chunk_size < (total_elements_needed - current_file_offset) ? chunk_size : (total_elements_needed - current_file_offset);
// Read the chunk (<< 1 converts pixel count to byte count)
map_file.read(zoom_out_buffer, read_size << 1);
// Determine where the first valid "zoomed" pixel is located inside this specific chunk
int first_valid_index = 0;
int offset_modulo = current_file_offset % skip;
if (offset_modulo != 0) {
first_valid_index = skip - offset_modulo;
}
// Extract only the pixels we need, skipping the rest
for (int j = first_valid_index; j < read_size; j += skip) {
if (target_i < width) {
buffer[target_i] = zoom_out_buffer[j];
target_i++;
}
}
current_file_offset += read_size;
}
}
}
@@ -415,8 +458,7 @@ bool GeoMap::draw_osm_file(int zoom, int tile_x, int tile_y, int relative_x, int
return true;
}
BMPFile bmp{};
bmp.open("/OSM/" + to_string_dec_int(zoom) + "/" + to_string_dec_int(tile_x) + "/" + to_string_dec_int(tile_y) + ".bmp", true);
BMPFile* bmp = bmp_cache.get(zoom, tile_x, tile_y);
// 1. Define the source and destination areas, starting with the full tile.
int src_x = 0;
int src_y = 0;
@@ -449,7 +491,7 @@ bool GeoMap::draw_osm_file(int zoom, int tile_x, int tile_y, int relative_x, int
return true;
}
if (!bmp.is_loaded()) {
if (!bmp || !bmp->is_loaded()) {
// Draw an error rectangle using the calculated clipped dimensions
ui::Rect error_rect{{dest_x + r.left(), dest_y + r.top()}, {clip_w, clip_h}};
display.fill_rectangle(error_rect, Theme::getInstance()->bg_darkest->background);
@@ -458,20 +500,20 @@ bool GeoMap::draw_osm_file(int zoom, int tile_x, int tile_y, int relative_x, int
map_line_buffer.resize(clip_w);
if (bmp.is_bottomup()) {
if (bmp->is_bottomup()) {
for (int y = clip_h - 1; y >= 0; --y) {
int source_row = src_y + y;
int dest_row = dest_y + y;
bmp.seek(src_x, source_row);
bmp.read_next_px_cnt(map_line_buffer.data(), clip_w, false);
bmp->seek(src_x, source_row);
bmp->read_next_px_cnt(map_line_buffer.data(), clip_w, false);
display.draw_pixels({dest_x + r.left(), dest_row + r.top(), clip_w, 1}, map_line_buffer);
}
} else {
for (int y = 0; y < clip_h; ++y) {
int source_row = src_y + y;
int dest_row = dest_y + y;
bmp.seek(src_x, source_row);
bmp.read_next_px_cnt(map_line_buffer.data(), clip_w, false);
bmp->seek(src_x, source_row);
bmp->read_next_px_cnt(map_line_buffer.data(), clip_w, false);
display.draw_pixels({dest_x + r.left(), dest_row + r.top(), clip_w, 1}, map_line_buffer);
}
}
+101 -2
View File
@@ -30,13 +30,16 @@
#include "bmpfile.hpp"
#include "mathdef.hpp"
#include <array>
#include <cstdio>
#include <inttypes.h>
#include "portapack.hpp"
namespace ui {
#define MAX_MAP_ZOOM_IN 4000
#define MAX_MAP_ZOOM_OUT 10
#define MAP_ZOOM_RESOLUTION_LIMIT 5 // Max zoom-in to show map; rect height & width must divide into this evenly
#define MAX_MAP_ZOOM_OUT 15
#define MAP_ZOOM_RESOLUTION_LIMIT 10 // Max zoom-in to show map;
#define INVALID_LAT_LON 200
#define INVALID_ANGLE 400
@@ -74,6 +77,101 @@ struct GeoMarker {
}
};
class BMPFileCache {
public:
static constexpr uint8_t SlotsCount = 9;
BMPFileCache() {
}
~BMPFileCache() {
clear();
}
BMPFile* get(const int32_t z, const int32_t x, const int32_t y) {
// Cache hit.
for (auto& slot : slots_) {
if (slot.used && slot.x == x && slot.y == y && slot.z == z) {
slot.last_used = next_stamp();
return &slot.bmp;
}
}
// Select free slot first, otherwise LRU slot.
Slot* target = nullptr;
uint16_t oldest = 0xFFFFu;
for (auto& slot : slots_) {
if (!slot.used) {
target = &slot;
break;
}
if (slot.last_used < oldest) {
oldest = slot.last_used;
target = &slot;
}
}
if (!target) {
return nullptr;
}
if (target->used) {
target->bmp.close();
target->used = false;
}
// OSM tile path convention: <base>/<z>/<x>/<y>.bmp
char path_buffer[64];
snprintf(path_buffer, sizeof(path_buffer), "/OSM/%" PRId32 "/%" PRId32 "/%" PRId32 ".bmp", z, x, y);
if (!target->bmp.open(path_buffer, true)) {
target->bmp.close();
return nullptr;
}
target->x = x;
target->y = y;
target->z = z;
target->last_used = next_stamp();
target->used = true;
return &target->bmp;
}
void clear() {
for (auto& slot : slots_) {
if (slot.used) {
slot.bmp.close();
slot.used = false;
}
}
}
private:
struct Slot {
BMPFile bmp{};
int32_t x{};
int32_t y{};
int32_t z{};
uint16_t last_used{};
bool used{false};
};
uint16_t next_stamp() {
if (++stamp_ == 0) {
uint16_t v = 1;
for (auto& slot : slots_) {
if (slot.used) {
slot.last_used = v++;
}
}
stamp_ = v;
}
return stamp_;
}
std::array<Slot, SlotsCount> slots_{};
uint16_t stamp_{0};
};
class GeoPos : public View {
public:
enum alt_unit {
@@ -276,6 +374,7 @@ class GeoMap : public Widget {
bool hide_center_marker_{false};
GeoMapMode mode_{};
File map_file{};
BMPFileCache bmp_cache{};
bool map_opened{};
bool map_visible{};
uint16_t map_width{}, map_height{};
+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) {

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