Compare commits

..

26 Commits

Author SHA1 Message Date
Matej Sochan 6dadefe86f Bug/big frequency (#3247)
* fix(ui): BigFrequency ignores set_dirty() when frequency is unchanged

paint() gated its entire body behind a frequency-changed check (_previous_frequency cache), so calling set_dirty() without changing the value (e.g. forcing a repaint after a style/theme change) silently did nothing. Moved the change check into set() instead, so paint() always redraws when called and set_dirty() behaves as expected.

* accidental deletion
2026-08-19 05:57:38 +02:00
Copilot ea40e3a46d Remove dead _previous_frequency state from BigFrequency widget (#3294)
* Initial plan

* Remove unused _previous_frequency member from BigFrequency and remove paint() guard

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

* Move frequency dirty-check to set() to avoid redundant repaints

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

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: gullradriel <3157857+gullradriel@users.noreply.github.com>
2026-08-19 10:15:39 +08:00
rollorentner 86584feb16 Add Superrollo (GW60/HCS361) TX and RX (#3290)
keeloqtx: Superrollo 67-bit transmit mode with rolling counter.
subghzd: GW60 receive decoder (reports as KeeLoq; manufacturer key read from KEELOQKEYS/MFCODES). The manufacturer key is not included; add a Superrollo entry to MFCODES to enable TX/RX.
2026-08-16 20:10:04 +02:00
Bernd Herzog 47c94dbf26 SD Over USB for the Hackrf Pro (#3291)
* enabled sd over usb compilation
* updated hackrf pro usb stack
* fixed hackrf one code path
* fixed hackrf pro code path
* improved performance
* fixed sd card clock
* refactoring
* formatted code
2026-08-16 20:08:19 +02:00
Oleg Belousov b048b8f4f1 Feature/sliding freq (#3280)
* Add sliding-frequency audio receiver tuning
* Move ADS-B receiver to external app
* Move AIS receiver to external app
* Move APRS receiver to external app
* Move APRS transmitter to external app
* Isolate filtered spectrum collection from WFM
* Fixed the issues #3280
* fpga_bridge.c: init registers per reference fpga_init, add quarter-shift mode setter
* radio.hpp: expose cached FPGA quarter-rate shift
* radio.cpp: program FPGA quarter-rate shift together with tuning offset
* tuning.hpp: pass AFE rate and direction to tuning config, add quarter_shift field
* tuning.cpp: import full PRALINE RX/TX tuning tables with quarter-shift offsets
* clock_manager: stop writing bogus RX digital gain, keep quarter shift across rate changes
* receiver_model: port LPF bandwidth from reference auto_bandwidth, use cached quarter shift
* adsb_rx: enable RF amp by default on first run
* ui_geomap.hpp: add hemisphere fields, degrees become magnitude
* ui_geomap: use hemisphere selector for lat/lon sign, fix minute/second wrap carry
* ui_menu: guard select against empty menu
* waterfall_designer: header updates for profile file handling
* waterfall_designer: exception-free profile parsing, CRLF handling, deferred nav callbacks, backup cleanup
* external.ld: scope app section globs to their own object directories
* CMakeLists: relink when external.ld changes
* tools: add external app symbol placement checker
* tools: add guru meditation address lookup script
* tools: add per-function stack usage report script
Co-authored-by: gullradriel <gullradriel@users.noreply.github.com>
2026-08-08 21:47:54 +02:00
MsfPablo 9067e007b6 Fix GeoPos losing the sign of coordinates between 0 and -1 (#3283)
GeoPos stored a coordinate's sign only in its degrees NumberField, which
is an int32_t and therefore has no negative zero. Any coordinate whose
integer degrees part is 0 but which is negative (i.e. strictly between
0.0 and -1.0) could not be represented at all:

  - set_lat()/set_lon() passed the raw float to the degrees field, so
    -0.2933 truncated to 0 and the sign was gone before it reached the
    widget.
  - lat()/lon() then decided the sign with `field_lon_degrees.value() < 0`,
    which is false for 0, so the value came back positive.

The reporter's airport is at longitude -0.2933, which was impossible to
enter in the ADSB Tx app.

Give each coordinate an explicit hemisphere field (N/S and E/W) and make
the degrees field an unsigned magnitude:

  - The hemisphere OptionsField is the single source of the sign, so
    "negative with zero degrees" is now representable.
  - set_lat()/set_lon() derive the hemisphere from the sign of the input
    and feed the fields the magnitude.
  - lat()/lon() read the hemisphere instead of inferring the sign from
    the degrees value.
  - The minutes on_wrap handlers no longer need to flip the carry
    direction based on the degrees sign, since degrees is now a
    magnitude; the carry is the same in both hemispheres.
  - The degrees fields no longer loop, so carrying below 0 clamps at 0
    instead of wrapping round to 90/180.

The hemisphere indicator occupies the column that the 4-wide signed
degrees field used for its minus sign, so the row layout, the degree
symbol and the decimal readout all stay where they were.

Fixes #3234
2026-08-08 21:18:18 +02:00
Copilot 367eaf54c0 Fix GeoPos DMS edge carry rollback at clamped degree limits (#3284)
* Initial plan

* Fix geomap DMS wrap carry at clamped bounds

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

---------

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

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

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

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

Closes #3274

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

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

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

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

* Refactor GPIO handling: update MAX2831 shutdown pin configuration

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

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

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

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

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

* Reorder includes for consistency and fix string dereference in VorCdiIndicator

* Add memory section and linker script entry for vor_rx application

* Testing VOR TX

* Moving VOR TX to SD

* Code refactoring to align with guidelines

* Add TODO comment to verify radial sign convention in vor_tx_config

* Remove unnecessary set_dirty() calls in VorRxView methods

* Update Course Deviation Indicator label and adjust UI element positions

* Update VorTxView to run prepared image from memory map

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

* Reverting action changes

* Adjust Course Deviation Indicator layout and refine painting logic

* Refactor VorRxView UI elements for improved layout and clarity

* Add calibration field and update radial calculation logic in VorRxView

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

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

* Setting comments inline

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

* Update external app address end to 0xAE0B0000

* Update set_vor_tx_config to include enabled parameter for consistency

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

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

* Refactor VorRxView labels and status messages

* Update VorRxView calibration field range to allow full circle input

* Completing Ident transmission logic

* Fixing TX gain and moving log label

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

* Add low-pass filters to strip 9960 Hz subcarrier from audio output
2026-07-04 08:49:21 +02:00
184 changed files with 10668 additions and 2535 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/*
+5 -7
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
@@ -280,16 +279,12 @@ set(CPPSRC
ui/ui_tone_key.cpp
ui/ui_transmitter.cpp
ui/ui_bmpview.cpp
apps/ais_app.cpp
apps/analog_audio_app.cpp
apps/ble_rx_app.cpp
apps/ble_tx_app.cpp
apps/capture_app.cpp
apps/pocsag_app.cpp
apps/ui_about_simple.cpp
apps/ui_adsb_rx.cpp
apps/ui_aprs_rx.cpp
apps/ui_aprs_tx.cpp
apps/ui_battinfo.cpp
apps/ui_bmp_file_viewer.cpp
apps/ui_btle_rx.cpp
@@ -494,7 +489,10 @@ add_executable(${PROJECT_NAME}.elf ${CSRC} ${CPPSRC} ${ASMSRC})
target_link_options(${PROJECT_NAME}.elf PRIVATE
"LINKER:--defsym,LD_FLASH_SIZE=${FLASH_BYTES_LIMIT_SIZE}"
)
set_target_properties(${PROJECT_NAME}.elf PROPERTIES LINK_DEPENDS ${LDSCRIPT})
# NB: LPC43xx_M0.ld does INCLUDE external.ld, so that one has to be listed too -
# otherwise editing the external app section rules silently does not relink.
set_target_properties(${PROJECT_NAME}.elf PROPERTIES
LINK_DEPENDS "${LDSCRIPT};${CMAKE_CURRENT_SOURCE_DIR}/external/external.ld")
add_definitions(${DEFS})
include_directories(. ${INCDIR})
link_directories(${LLIBDIR})
@@ -514,4 +512,4 @@ add_custom_target(
DEPENDS ${PROJECT_NAME}.bin
)
add_dependencies(${PROJECT_NAME} baseband)
add_dependencies(${PROJECT_NAME} baseband)
+74 -4
View File
@@ -280,6 +280,12 @@ AnalogAudioView::AnalogAudioView(
field_frequency.on_show_options = [this]() {
this->on_show_options_frequency();
};
field_frequency.changing = [this](rf::Frequency frequency) {
return this->on_frequency_changed(frequency);
};
field_frequency.entered = [this](rf::Frequency frequency) {
this->set_frequency_absolute(frequency);
};
field_lna.on_show_options = [this]() {
this->on_show_options_rf_gain();
@@ -308,8 +314,10 @@ AnalogAudioView::AnalogAudioView(
};
waterfall.on_select = [this](int32_t offset) {
field_frequency.set_value(receiver_model.target_frequency() + offset);
field_frequency.set_value(
field_frequency.value() + offset * receiver_model.frequency_step());
};
waterfall.set_live_tuning(true);
#ifdef PRALINE
button_pro.on_select = [this](Button&) { this->on_show_options_praline(); };
@@ -319,6 +327,7 @@ AnalogAudioView::AnalogAudioView(
// This call starts the correct baseband image to run
// and sets the radio up as necessary for the given modulation.
sliding_center_frequency = receiver_model.target_frequency();
on_modulation_changed(modulation);
}
@@ -328,7 +337,7 @@ AnalogAudioView::AnalogAudioView(
: AnalogAudioView(nav) {
// Settings to override when launched from another app (versus from AppSettings .ini file)
// TODO: Which other settings make sense to override?
field_frequency.set_value(override.frequency_app_override);
set_frequency_absolute(override.frequency_app_override);
on_frequency_step_changed(override.frequency_step);
options_modulation.set_by_value(toUType(override.mode));
}
@@ -573,10 +582,14 @@ void AnalogAudioView::update_modulation(ReceiverModel::Mode modulation) {
receiver_model.set_sampling_rate(is_wideband_spectrum_mode ? spec_bw : 3072000);
receiver_model.set_baseband_bandwidth(is_wideband_spectrum_mode ? spec_bw / 2 : 1750000);
receiver_model.set_hidden_offset(modulation == ReceiverModel::Mode::AMAudioFMApt ? -2200 : 0); // wefax needs to be shifted, see wefax rx app.
reset_sliding_frequency(modulation);
receiver_model.enable();
if (sliding_enabled) {
baseband::set_audio_ddc_frequency(0);
}
// TODO: This doesn't belong here! There's a better way.
size_t sampling_rate = 0;
switch (modulation) {
@@ -610,7 +623,64 @@ void AnalogAudioView::handle_coded_squelch(uint32_t value) {
}
void AnalogAudioView::on_freqchg(int64_t freq) {
field_frequency.set_value(freq);
set_frequency_absolute(freq);
}
void AnalogAudioView::set_frequency_absolute(rf::Frequency frequency) {
if (!sliding_enabled) {
field_frequency.set_value(frequency);
return;
}
sliding_center_frequency = frequency;
/* set_value() does not call on_change when the displayed frequency is
* already equal, so reset the hardware and DDC explicitly in that case. */
if (field_frequency.value() == frequency) {
receiver_model.set_target_frequency_with_hidden_offset(frequency, 0);
baseband::set_audio_ddc_frequency(0);
} else {
field_frequency.set_value(frequency);
}
}
int32_t AnalogAudioView::sliding_limit() const {
const bool zoom_x2 =
receiver_model.modulation() == ReceiverModel::Mode::AMAudio &&
previous_zoom != 0;
return zoom_x2 ? sliding_limit_zoom_x2 : sliding_limit_zoom_x1;
}
void AnalogAudioView::reset_sliding_frequency(ReceiverModel::Mode modulation) {
sliding_enabled =
modulation == ReceiverModel::Mode::AMAudio ||
modulation == ReceiverModel::Mode::NarrowbandFMAudio;
sliding_center_frequency = receiver_model.target_frequency();
/* AMFM keeps its existing Wefax offset; sliding applies to AM and NFM. */
receiver_model.set_hidden_offset(
modulation == ReceiverModel::Mode::AMAudioFMApt ? -2200 : 0);
}
bool AnalogAudioView::on_frequency_changed(rf::Frequency frequency) {
if (!sliding_enabled)
return false;
const auto limit = sliding_limit();
int64_t offset = frequency - sliding_center_frequency;
if (offset > limit) {
sliding_center_frequency = frequency - limit;
offset = limit;
} else if (offset < -limit) {
sliding_center_frequency = frequency + limit;
offset = -limit;
}
/* Store the displayed frequency and retune the hardware centre atomically. */
receiver_model.set_target_frequency_with_hidden_offset(
frequency, sliding_center_frequency - frequency);
baseband::set_audio_ddc_frequency(static_cast<int32_t>(offset));
return true;
}
#ifdef PRALINE
@@ -269,6 +269,10 @@ class AnalogAudioView : public View {
uint8_t zoom_factor_amfm{0}; // initial zoom factor in AMFM mode
uint8_t previous_AM_mode_option{0}; // GUI 5 AM modes : (0..4 ) (DSB9K, DSB6K, USB,LSB, CW). Used to select proper FIR filter (0..11) AM mode + offset 0 (zoom+1) or +6 (if zoom+2)
uint8_t previous_zoom{0}; // GUI ZOOM+1, ZOOM+2 , equivalent to two values offset 0 (zoom+1) or +6 (if zoom+2)
static constexpr int32_t sliding_limit_zoom_x1 = 50000;
static constexpr int32_t sliding_limit_zoom_x2 = 30000;
rf::Frequency sliding_center_frequency{0};
bool sliding_enabled{false};
app_settings::SettingsManager settings_{
"rx_audio",
@@ -354,6 +358,10 @@ class AnalogAudioView : public View {
void handle_coded_squelch(uint32_t value);
void on_freqchg(int64_t freq);
bool on_frequency_changed(rf::Frequency frequency);
void set_frequency_absolute(rf::Frequency frequency);
int32_t sliding_limit() const;
void reset_sliding_frequency(ReceiverModel::Mode modulation);
MessageHandlerRegistration message_handler_coded_squelch{
Message::ID::CodedSquelch,
@@ -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)},
""};
+1 -1
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.
*
+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.
*
+4 -2
View File
@@ -74,7 +74,7 @@ void SubGhzDRecentEntryDetailView::update_data() {
console.writeln("Action: 0x" + to_string_hex(func_code));
}
if (entry_.sensorType == FPS_KEELOQ) {
if (entry_.sensorType == FPS_KEELOQ || entry_.sensorType == FPS_SUPERROLLO) {
console.writeln("Fix: " + to_string_hex(fix));
console.writeln("Encrypted: " + to_string_hex(encrypted));
console.writeln("Manufacturer: " + mf_name);
@@ -243,6 +243,8 @@ const char* SubGhzDView::getSensorTypeName(FPROTO_SUBGHZD_SENSOR type) {
return "Ido 11x";
case FPS_INTERTECHNOV3:
return "InterTehcno v3";
case FPS_SUPERROLLO:
return "Superrollo";
case FPS_KEELOQ:
return "KeeLoq";
case FPS_KINGGATESSTYLO4K:
@@ -671,7 +673,7 @@ void SubGhzDRecentEntryDetailView::parseProtocol() {
return;
}
if (entry_.sensorType == FPS_KEELOQ) {
if (entry_.sensorType == FPS_KEELOQ || entry_.sensorType == FPS_SUPERROLLO) {
uint64_t data_rev = FProtoGeneral::subghz_protocol_blocks_reverse_key(entry_.data, 64);
btn = data_rev >> 60;
+27 -8
View File
@@ -32,10 +32,6 @@
#include "core_control.hpp"
/* Set true to enable additional checks to ensure
* M4 and M0 are synchronized before passing messages. */
static constexpr bool enforce_core_sync = true;
/* Set true to enable check for baseband messages getting stuck.
* This implies the baseband thread is not dequeuing and has probably stalled.
* NB: This check adds a small amout of overhead to the message sending code
@@ -343,6 +339,11 @@ void set_spectrum(
send_message(&message);
}
void set_audio_ddc_frequency(int32_t frequency) {
const AudioDDCConfigMessage message{frequency};
send_message(&message);
}
void set_time_sink(
const size_t sampling_rate,
const size_t trigger) {
@@ -361,6 +362,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,13 +456,21 @@ 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() {
return baseband_image_running;
}
void run_image(const spi_flash::image_tag_t image_tag) {
void run_image(const spi_flash::image_tag_t image_tag, bool enforce_core_sync) {
if (baseband_image_running) {
chDbgPanic("BBRunning");
}
@@ -464,7 +483,7 @@ void run_image(const spi_flash::image_tag_t image_tag) {
creg::m4txevent::enable();
if constexpr (enforce_core_sync) {
if (enforce_core_sync) {
// Wait up to 3 seconds for baseband to start handling events.
auto count = 3'000u;
while (!shared_memory.baseband_ready && --count)
@@ -475,7 +494,7 @@ void run_image(const spi_flash::image_tag_t image_tag) {
}
}
void run_prepared_image(const uint32_t m4_code) {
void run_prepared_image(const uint32_t m4_code, bool enforce_core_sync) {
if (baseband_image_running) {
chDbgPanic("BBRunning");
}
@@ -488,7 +507,7 @@ void run_prepared_image(const uint32_t m4_code) {
creg::m4txevent::enable();
if constexpr (enforce_core_sync) {
if (enforce_core_sync) {
// Wait up to 3 seconds for baseband to start handling events.
auto count = 3'000u;
while (!shared_memory.baseband_ready && --count)
+6 -2
View File
@@ -100,6 +100,7 @@ void set_rds_data(const uint16_t message_length);
void set_spectrum(
const size_t sampling_rate,
const size_t trigger);
void set_audio_ddc_frequency(int32_t frequency);
void set_time_sink(
const size_t sampling_rate,
const size_t trigger);
@@ -113,6 +114,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 +123,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();
@@ -127,8 +131,8 @@ void request_beep_stop();
void request_audio_beep(uint32_t freq, uint32_t sample_rate, uint32_t duration_ms);
bool is_image_running();
void run_image(const portapack::spi_flash::image_tag_t image_tag);
void run_prepared_image(const uint32_t m4_code);
void run_image(const portapack::spi_flash::image_tag_t image_tag, bool enforce_core_sync = true);
void run_prepared_image(const uint32_t m4_code, bool enforce_core_sync = true);
void shutdown();
void spectrum_streaming_start();
+17 -5
View File
@@ -907,12 +907,24 @@ void ClockManager::set_sampling_frequency(const uint32_t frequency) {
// Set FPGA RX decimation register
fpga_debug_register_write(FPGA_REG_DECIM, n);
/* RX Mode: Register 3 is FPGA_REG_RX_DIGITAL_GAIN.
* We shift up by (3 * n) to compensate for CIC bit-growth.
/* No RX digital-gain register is written here.
*
* Register 0x03 used to be programmed with (3 * n + 2) as a "CIC
* bit-growth" renormalisation. The gateware has no such register: the
* RX decimator is a chain of unity-gain half-band FIRs selected by
* rx_decim (fpga/top/standard.py), and 0x03 is rx_pstep, whose top two
* bits are the quarter-rate shift. Writing a gain here silently
* cancelled the shift that set_tuning_frequency() had programmed, which
* left the analogue passband offset with no matching rotation.
*
* The shift depends on the AFE rate we just chose, so re-apply it after
* the rate change. ReceiverModel::update_sampling_rate() calls
* update_tuning_frequency() straight after this, which does exactly
* that; the write below only keeps the register consistent in between.
*/
uint8_t ds = (3 * n);
ds += 2;
fpga_debug_register_write(FPGA_REG_RX_DIGITAL_GAIN, ds);
fpga_debug_register_write(
FPGA_REG_RX_PSTEP,
(radio::debug::get_cached_quarter_shift() & 0b11) << FPGA_RX_QUARTER_SHIFT_SHIFT);
// Re-enable FPGA processing with clean state ===
fpga_debug_register_write(1, 0x01);
+4 -5
View File
@@ -61,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) {
@@ -80,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;
@@ -137,7 +136,7 @@ void config_mode_blink_until_dfu() {
led_usb.setInactive();
chThdSleepMilliseconds(115);
auto dfu_btn = portapack::gpio_dfu.read();
auto dfu_btn = dfu_button.read();
if (dfu_btn)
break;
}
@@ -145,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;
}
-2
View File
@@ -400,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())
+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; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#include "ui_adsb_rx.hpp"
#include "ui_navigation.hpp"
#include "external_app.hpp"
namespace ui::external_app::adsbrx {
void initialize_app(ui::NavigationView& nav) {
nav.push<ADSBRxView>();
}
} // namespace ui::external_app::adsbrx
extern "C" {
__attribute__((section(".external_app.app_adsbrx.application_information"), used)) application_information_t _application_information_adsbrx = {
/*.memory_location = */ (uint8_t*)0x00000000,
/*.externalAppEntry = */ ui::external_app::adsbrx::initialize_app,
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
/*.app_name = */ "ADS-B",
/*.bitmap_data = */ {
0x80,
0x01,
0xC0,
0x03,
0xC0,
0x03,
0xC0,
0x03,
0xC0,
0x03,
0xE0,
0x07,
0xF8,
0x1F,
0xFE,
0x7F,
0xFF,
0xFF,
0xFF,
0xFF,
0xC0,
0x03,
0xC0,
0x03,
0xC0,
0x03,
0xE0,
0x07,
0xF0,
0x0F,
0xF8,
0x1F,
},
/*.icon_color = */ ui::Color::green().v,
/*.menu_location = */ app_location_t::RX,
/*.desired_menu_position = */ -1,
/*.m4_app_tag = portapack::spi_flash::image_tag_adsbrx */ {'P', 'A', 'D', 'R'},
/*.m4_app_offset = */ 0x00000000,
};
} // extern "C"
@@ -38,7 +38,7 @@ using namespace portapack;
namespace pmem = portapack::persistent_memory;
namespace ui {
namespace ui::external_app::adsbrx {
static const char speed_type_msg[][6] = {" Spd:", " IAS:", " TAS:"};
@@ -46,13 +46,19 @@ static std::string get_map_tag(const AircraftRecentEntry& entry) {
return trimr(entry.callsign.empty() ? entry.icao_str : entry.callsign);
}
} // namespace ui::external_app::adsbrx
namespace ui {
template <>
void RecentEntriesTable<AircraftRecentEntries>::draw(
void RecentEntriesTable<external_app::adsbrx::AircraftRecentEntries>::draw(
const Entry& entry,
const Rect& target_rect,
Painter& painter,
const Style& style,
RecentEntriesColumns& columns) {
using namespace external_app::adsbrx;
Color target_color;
std::string entry_string;
@@ -75,7 +81,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,
@@ -107,6 +113,10 @@ void RecentEntriesTable<AircraftRecentEntries>::draw(
bitmap_target, target_color, style.background);
}
} // namespace ui
namespace ui::external_app::adsbrx {
/* ADSBLogger ********************************************/
void ADSBLogger::log(const ADSBLogEntry& log_entry) {
@@ -532,6 +542,18 @@ ADSBRxView::ADSBRxView(NavigationView& nav) {
logger = std::make_unique<ADSBLogger>();
logger->append(logs_dir / u"ADSB.TXT");
/* First run only: start from the configuration that is known to receive
* ADS-B on this hardware -- LNA 32, VGA 32, RF amp ON. The first two are
* already ReceiverModel's defaults; the amp is not, and running without it
* costs about 14 dB, which is the difference between a busy list and an
* empty one. Once the user has saved settings for this app their choice
* wins, so this only sets the starting point.
* Going through the field rather than receiver_model keeps the displayed
* value in step: RFAmpField snapshots rf_amp() in its own constructor,
* which has already run by the time we get here. */
if (!settings_.loaded())
field_rf_amp.set_value(1);
receiver_model.enable();
baseband::set_adsb();
@@ -668,9 +690,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
}
@@ -817,4 +838,4 @@ void ADSBRxView::remove_expired_entries() {
recent.erase(it.base(), recent.end());
}
} /* namespace ui */
} // namespace ui::external_app::adsbrx
@@ -39,7 +39,7 @@
using namespace adsb;
namespace ui {
namespace ui::external_app::adsbrx {
#define AIRCRAFT_ID_L 1 // aircraft ID message type (lowest type id)
#define AIRCRAFT_ID_H 4 // aircraft ID message type (highest type id)
@@ -491,4 +491,4 @@ class ADSBRxView : public View {
}};
};
} /* namespace ui */
} // namespace ui::external_app::adsbrx
@@ -35,7 +35,8 @@ using namespace portapack;
namespace pmem = portapack::persistent_memory;
namespace ais {
namespace ui::external_app::ais_rx {
namespace format {
static std::string latlon_abs_normalized(const int32_t normalized, const char suffixes[2]) {
@@ -47,7 +48,7 @@ static std::string latlon_abs_normalized(const int32_t normalized, const char su
return to_string_dec_uint(degrees) + "." + to_string_dec_uint(fraction, 6, '0') + suffix;
}
static std::string latlon(const Latitude latitude, const Longitude longitude) {
static std::string latlon(const ais::Latitude latitude, const ais::Longitude longitude) {
if (latitude.is_valid() && longitude.is_valid()) {
return latlon_abs_normalized(latitude.normalized(), "SN") + " " + latlon_abs_normalized(longitude.normalized(), "WE");
} else if (latitude.is_not_available() && longitude.is_not_available()) {
@@ -128,7 +129,7 @@ static std::string navigational_status(const unsigned int value) {
}
}
static std::string rate_of_turn(const RateOfTurn value) {
static std::string rate_of_turn(const ais::RateOfTurn value) {
switch (value) {
case -128:
return "not available";
@@ -149,7 +150,7 @@ static std::string rate_of_turn(const RateOfTurn value) {
}
}
static std::string speed_over_ground(const SpeedOverGround value) {
static std::string speed_over_ground(const ais::SpeedOverGround value) {
if (value == 1023) {
return "not available";
} else if (value == 1022) {
@@ -159,7 +160,7 @@ static std::string speed_over_ground(const SpeedOverGround value) {
}
}
static std::string course_over_ground(const CourseOverGround value) {
static std::string course_over_ground(const ais::CourseOverGround value) {
if (value > 3600) {
return "invalid";
} else if (value == 3600) {
@@ -169,7 +170,7 @@ static std::string course_over_ground(const CourseOverGround value) {
}
}
static std::string true_heading(const TrueHeading value) {
static std::string true_heading(const ais::TrueHeading value) {
if (value == 511) {
return "not available";
} else if (value > 359) {
@@ -180,7 +181,6 @@ static std::string true_heading(const TrueHeading value) {
}
} /* namespace format */
} /* namespace ais */
void AISLogger::on_packet(const ais::Packet& packet) {
// TODO: Unstuff here, not in baseband!
@@ -265,25 +265,33 @@ void AISRecentEntry::update(const ais::Packet& packet) {
}
}
} // namespace ui::external_app::ais_rx
namespace ui {
template <>
void RecentEntriesTable<AISRecentEntries>::draw(
void RecentEntriesTable<external_app::ais_rx::AISRecentEntries>::draw(
const Entry& entry,
const Rect& target_rect,
Painter& painter,
const Style& style,
RecentEntriesColumns&) {
std::string line = ais::format::mmsi(entry.mmsi) + " ";
using namespace external_app::ais_rx;
std::string line = format::mmsi(entry.mmsi) + " ";
if (!entry.name.empty()) {
line += ais::format::text(entry.name);
line += format::text(entry.name);
} else {
line += ais::format::text(entry.call_sign);
line += format::text(entry.call_sign);
}
line.resize(target_rect.width() / 8, ' ');
painter.draw_string(target_rect.location(), style, line);
}
} // namespace ui
namespace ui::external_app::ais_rx {
AISRecentEntryDetailView::AISRecentEntryDetailView(NavigationView& nav) {
add_children({
&button_done,
@@ -298,12 +306,12 @@ AISRecentEntryDetailView::AISRecentEntryDetailView(NavigationView& nav) {
button_see_map.on_select = [this, &nav](Button&) {
geomap_view = nav.push<GeoMapView>(
ais::format::text(entry_.name),
format::text(entry_.name),
0,
GeoPos::alt_unit::METERS,
GeoPos::spd_unit::KNOTS,
ais::format::latlon_float(entry_.last_position.latitude.normalized()),
ais::format::latlon_float(entry_.last_position.longitude.normalized()),
format::latlon_float(entry_.last_position.latitude.normalized()),
format::latlon_float(entry_.last_position.longitude.normalized()),
entry_.last_position.true_heading,
[this]() {
send_updates = false;
@@ -324,14 +332,14 @@ AISRecentEntryDetailView& AISRecentEntryDetailView::operator=(const AISRecentEnt
void AISRecentEntryDetailView::update_position() {
if (send_updates)
geomap_view->update_position(ais::format::latlon_float(entry_.last_position.latitude.normalized()), ais::format::latlon_float(entry_.last_position.longitude.normalized()), (float)entry_.last_position.true_heading, 0, entry_.last_position.speed_over_ground > 1022 ? 0 : entry_.last_position.speed_over_ground / 10);
geomap_view->update_position(format::latlon_float(entry_.last_position.latitude.normalized()), format::latlon_float(entry_.last_position.longitude.normalized()), (float)entry_.last_position.true_heading, 0, entry_.last_position.speed_over_ground > 1022 ? 0 : entry_.last_position.speed_over_ground / 10);
}
bool AISRecentEntryDetailView::add_map_marker(const AISRecentEntry& entry) {
if (geomap_view && send_updates) {
GeoMarker marker{};
marker.lon = ais::format::latlon_float(entry.last_position.longitude.normalized());
marker.lat = ais::format::latlon_float(entry.last_position.latitude.normalized());
marker.lon = format::latlon_float(entry.last_position.longitude.normalized());
marker.lat = format::latlon_float(entry.last_position.latitude.normalized());
marker.angle = entry.last_position.true_heading;
marker.tag = entry.call_sign.empty() ? to_string_dec_uint(entry.mmsi) : entry.call_sign;
auto markerStored = geomap_view->store_marker(marker);
@@ -377,18 +385,18 @@ void AISRecentEntryDetailView::paint(Painter& painter) {
auto field_rect = Rect{rect.left(), rect.top() + 16, rect.width(), 16};
field_rect = draw_field(painter, field_rect, s, "MMSI", ais::format::mmsi(entry_.mmsi));
field_rect = draw_field(painter, field_rect, s, "Ctry", ais::format::mid(entry_.mmsi));
field_rect = draw_field(painter, field_rect, s, "Name", ais::format::text(entry_.name));
field_rect = draw_field(painter, field_rect, s, "Call", ais::format::text(entry_.call_sign));
field_rect = draw_field(painter, field_rect, s, "Dest", ais::format::text(entry_.destination));
field_rect = draw_field(painter, field_rect, s, "MMSI", format::mmsi(entry_.mmsi));
field_rect = draw_field(painter, field_rect, s, "Ctry", format::mid(entry_.mmsi));
field_rect = draw_field(painter, field_rect, s, "Name", format::text(entry_.name));
field_rect = draw_field(painter, field_rect, s, "Call", format::text(entry_.call_sign));
field_rect = draw_field(painter, field_rect, s, "Dest", format::text(entry_.destination));
field_rect = draw_field(painter, field_rect, s, "Last", to_string_datetime(entry_.last_position.timestamp));
field_rect = draw_field(painter, field_rect, s, "Pos ", ais::format::latlon(entry_.last_position.latitude, entry_.last_position.longitude));
field_rect = draw_field(painter, field_rect, s, "Stat", ais::format::navigational_status(entry_.navigational_status));
field_rect = draw_field(painter, field_rect, s, "RoT ", ais::format::rate_of_turn(entry_.last_position.rate_of_turn));
field_rect = draw_field(painter, field_rect, s, "SoG ", ais::format::speed_over_ground(entry_.last_position.speed_over_ground));
field_rect = draw_field(painter, field_rect, s, "CoG ", ais::format::course_over_ground(entry_.last_position.course_over_ground));
field_rect = draw_field(painter, field_rect, s, "Head", ais::format::true_heading(entry_.last_position.true_heading));
field_rect = draw_field(painter, field_rect, s, "Pos ", format::latlon(entry_.last_position.latitude, entry_.last_position.longitude));
field_rect = draw_field(painter, field_rect, s, "Stat", format::navigational_status(entry_.navigational_status));
field_rect = draw_field(painter, field_rect, s, "RoT ", format::rate_of_turn(entry_.last_position.rate_of_turn));
field_rect = draw_field(painter, field_rect, s, "SoG ", format::speed_over_ground(entry_.last_position.speed_over_ground));
field_rect = draw_field(painter, field_rect, s, "CoG ", format::course_over_ground(entry_.last_position.course_over_ground));
field_rect = draw_field(painter, field_rect, s, "Head", format::true_heading(entry_.last_position.true_heading));
field_rect = draw_field(painter, field_rect, s, "Rx #", to_string_dec_uint(entry_.received_count));
}
@@ -504,4 +512,4 @@ void AISAppView::on_show_detail(const AISRecentEntry& entry) {
recent_entry_detail_view.update_map_markers(recent);
}
} /* namespace ui */
} // namespace ui::external_app::ais_rx
@@ -50,6 +50,8 @@ using namespace lpc43xx;
#include "recent_entries.hpp"
namespace ui::external_app::ais_rx {
struct AISPosition {
rtc::RTC timestamp{};
ais::Latitude latitude{};
@@ -109,8 +111,6 @@ class AISLogger {
LogFile log_file{};
};
namespace ui {
using AISRecentEntriesView = RecentEntriesView<AISRecentEntries>;
class AISRecentEntryDetailView : public View {
@@ -239,6 +239,6 @@ class AISAppView : public View {
void on_tick_second();
};
} /* namespace ui */
} // namespace ui::external_app::ais_rx
#endif /*__AIS_APP_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; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#include "ais_app.hpp"
#include "ui_navigation.hpp"
#include "external_app.hpp"
namespace ui::external_app::ais_rx {
void initialize_app(ui::NavigationView& nav) {
nav.push<AISAppView>();
}
} // namespace ui::external_app::ais_rx
extern "C" {
__attribute__((section(".external_app.app_ais_rx.application_information"), used)) application_information_t _application_information_ais_rx = {
/*.memory_location = */ (uint8_t*)0x00000000,
/*.externalAppEntry = */ ui::external_app::ais_rx::initialize_app,
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
/*.app_name = */ "AIS Boats",
/*.bitmap_data = */ {
0x00,
0x01,
0x80,
0x01,
0xC0,
0x01,
0xC0,
0x0D,
0xE0,
0x3D,
0xF0,
0x3D,
0xF8,
0x7D,
0xFC,
0x7D,
0xFC,
0x7D,
0xFE,
0x7D,
0xFF,
0x7D,
0x00,
0x00,
0xF8,
0x7F,
0xF8,
0x3F,
0xF0,
0x0F,
0x00,
0x00,
},
/*.icon_color = */ ui::Color::green().v,
/*.menu_location = */ app_location_t::RX,
/*.desired_menu_position = */ -1,
/*.m4_app_tag = portapack::spi_flash::image_tag_ais */ {'P', 'A', 'I', 'S'},
/*.m4_app_offset = */ 0x00000000,
};
} // extern "C"
@@ -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.
*
+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; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#include "ui_aprs_rx.hpp"
#include "ui_navigation.hpp"
#include "external_app.hpp"
namespace ui::external_app::aprs_rx {
void initialize_app(ui::NavigationView& nav) {
nav.push<APRSRXView>();
}
} // namespace ui::external_app::aprs_rx
extern "C" {
__attribute__((section(".external_app.app_aprs_rx.application_information"), used)) application_information_t _application_information_aprs_rx = {
/*.memory_location = */ (uint8_t*)0x00000000,
/*.externalAppEntry = */ ui::external_app::aprs_rx::initialize_app,
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
/*.app_name = */ "APRS",
/*.bitmap_data = */ {
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0xF0,
0x0F,
0x4C,
0x32,
0xFE,
0x7F,
0x25,
0xA4,
0x25,
0xA4,
0xFF,
0xFF,
0x25,
0xA4,
0x25,
0xA4,
0xFE,
0x7F,
0x4C,
0x32,
0xF0,
0x0F,
0x00,
0x00,
0x00,
0x00,
},
/*.icon_color = */ ui::Color::green().v,
/*.menu_location = */ app_location_t::RX,
/*.desired_menu_position = */ -1,
/*.m4_app_tag = portapack::spi_flash::image_tag_aprs_rx */ {'P', 'A', 'P', 'R'},
/*.m4_app_offset = */ 0x00000000,
};
} // extern "C"
@@ -31,19 +31,25 @@
using namespace portapack;
namespace ui::external_app::aprs_rx {
void APRSLogger::log_raw_data(const std::string& data) {
log_file.write_entry(data);
}
} // namespace ui::external_app::aprs_rx
namespace ui {
template <>
void RecentEntriesTable<APRSRecentEntries>::draw(
void RecentEntriesTable<external_app::aprs_rx::APRSRecentEntries>::draw(
const Entry& entry,
const Rect& target_rect,
Painter& painter,
const Style& style,
RecentEntriesColumns& columns) {
using namespace external_app::aprs_rx;
Color target_color;
// auto entry_age = entry.age;
@@ -68,6 +74,10 @@ void RecentEntriesTable<APRSRecentEntries>::draw(
}
}
} // namespace ui
namespace ui::external_app::aprs_rx {
void APRSRxView::focus() {
options_region.focus();
}
@@ -402,4 +412,4 @@ void APRSRXView::focus() {
APRSRXView::~APRSRXView() {
}
} /* namespace ui */
} // namespace ui::external_app::aprs_rx
@@ -38,6 +38,8 @@
#include "utility.hpp"
#include "file_path.hpp"
namespace ui::external_app::aprs_rx {
class APRSLogger {
public:
Optional<File::Error> append(const std::filesystem::path& filename) {
@@ -50,8 +52,6 @@ class APRSLogger {
LogFile log_file{};
};
namespace ui {
struct APRSRecentEntry {
using Key = uint64_t;
@@ -290,6 +290,6 @@ class APRSRXView : public View {
}};
};
} /* namespace ui */
} // namespace ui::external_app::aprs_rx
#endif /*__UI_APRS_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; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#include "ui_aprs_tx.hpp"
#include "ui_navigation.hpp"
#include "external_app.hpp"
namespace ui::external_app::aprs_tx {
void initialize_app(ui::NavigationView& nav) {
nav.push<APRSTXView>();
}
} // namespace ui::external_app::aprs_tx
extern "C" {
__attribute__((section(".external_app.app_aprs_tx.application_information"), used)) application_information_t _application_information_aprs_tx = {
/*.memory_location = */ (uint8_t*)0x00000000,
/*.externalAppEntry = */ ui::external_app::aprs_tx::initialize_app,
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
/*.app_name = */ "APRS TX",
/*.bitmap_data = */ {
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0xF0,
0x0F,
0x4C,
0x32,
0xFE,
0x7F,
0x25,
0xA4,
0x25,
0xA4,
0xFF,
0xFF,
0x25,
0xA4,
0x25,
0xA4,
0xFE,
0x7F,
0x4C,
0x32,
0xF0,
0x0F,
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_afsk */ {'P', 'A', 'F', 'T'},
/*.m4_app_offset = */ 0x00000000,
};
} // extern "C"
@@ -37,7 +37,7 @@
using namespace aprs;
using namespace portapack;
namespace ui {
namespace ui::external_app::aprs_tx {
void APRSTXView::focus() {
tx_view.focus();
@@ -242,4 +242,4 @@ APRSTXView::APRSTXView(NavigationView& nav) {
// process_coordinates(last_lat, last_lon); //don't load last, so won't confuse users
}
} /* namespace ui */
} // namespace ui::external_app::aprs_tx
@@ -33,7 +33,7 @@
#include "radio_state.hpp"
#include "portapack.hpp"
namespace ui {
namespace ui::external_app::aprs_tx {
class APRSTXView : public View {
public:
@@ -161,4 +161,4 @@ class APRSTXView : public View {
}};
};
} /* namespace ui */
} // namespace ui::external_app::aprs_tx
+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
+53 -10
View File
@@ -133,6 +133,14 @@ set(EXTCPPSRC
external/noaaapt_rx/main.cpp
external/noaaapt_rx/ui_noaaapt_rx.cpp
#vor_rx
external/vor_rx/main.cpp
external/vor_rx/ui_vor_rx.cpp
#vor_tx
external/vor_tx/main.cpp
external/vor_tx/ui_vor_tx.cpp
#shoppingcart_lock 272 bytes
external/shoppingcart_lock/main.cpp
external/shoppingcart_lock/shoppingcart_lock.cpp
@@ -161,6 +169,7 @@ set(EXTCPPSRC
#mcu_temperature 112
external/mcu_temperature/main.cpp
external/mcu_temperature/mcu_temperature.cpp
external/mcu_temperature/temperature_logger.cpp
#fmradio 640
external/fmradio/main.cpp
@@ -367,6 +376,41 @@ set(EXTCPPSRC
external/secplustx/main.cpp
external/secplustx/ui_secplustx.cpp
external/secplustx/secplustx.cpp
#signal_hunter
external/signal_hunter/main.cpp
external/signal_hunter/ui_signal_hunter.cpp
#tetra rx
external/tetra_rx/main.cpp
external/tetra_rx/ui_tetra_rx.cpp
external/tetra_rx/tetra_crc.cpp
external/tetra_rx/tetra_descrambler.cpp
external/tetra_rx/tetra_interleave.cpp
external/tetra_rx/tetra_rcpc.cpp
external/tetra_rx/tetra_viterbi.cpp
#adsb rx
external/adsbrx/main.cpp
external/adsbrx/ui_adsb_rx.cpp
#ais rx
external/ais_rx/main.cpp
external/ais_rx/ais_app.cpp
#aprs rx
external/aprs_rx/main.cpp
external/aprs_rx/ui_aprs_rx.cpp
#aprs tx
external/aprs_tx/main.cpp
external/aprs_tx/ui_aprs_tx.cpp
#sd over usb
external/sdusb/main.cpp
external/sdusb/ui_sd_over_usb.cpp
)
set(EXTAPPLIST
@@ -401,6 +445,8 @@ set(EXTAPPLIST
acars_rx
wefax_rx
noaaapt_rx
vor_rx
vor_tx
shoppingcart_lock
ookbrute
ook_editor
@@ -457,14 +503,11 @@ set(EXTAPPLIST
two_tone_rx
hard_reset
secplustx
signal_hunter
tetra_rx
adsbrx
ais_rx
aprs_rx
aprs_tx
sdusb
)
# sdusb has type conflicts with PRALINE (HackRF Pro) - add only for non-PRALINE builds
if(NOT BOARD STREQUAL "PRALINE")
list(APPEND EXTCPPSRC
external/sdusb/main.cpp
external/sdusb/ui_sd_over_usb.cpp
)
list(APPEND EXTAPPLIST sdusb)
endif()
+147 -88
View File
@@ -111,6 +111,14 @@ MEMORY
ram_external_app_flex_tx (rwx) : org = 0xAE060000, len = 32k
ram_external_app_hard_reset (rwx) : org = 0xAE070000, len = 32k
ram_external_app_secplustx (rwx) : org = 0xAE080000, len = 32k
ram_external_app_vor_rx (rwx) : org = 0xAE090000, len = 32k
ram_external_app_vor_tx (rwx) : org = 0xAE0A0000, len = 32k
ram_external_app_signal_hunter (rwx) : org = 0xAE0B0000, len = 32k
ram_external_app_tetra_rx (rwx) : org = 0xAE0C0000, len = 32k
ram_external_app_adsbrx (rwx) : org = 0xAE0D0000, len = 32k
ram_external_app_ais_rx (rwx) : org = 0xAE0E0000, len = 32k
ram_external_app_aprs_rx (rwx) : org = 0xAE0F0000, len = 32k
ram_external_app_aprs_tx (rwx) : org = 0xAE100000, len = 32k
}
SECTIONS
@@ -118,528 +126,579 @@ SECTIONS
.external_app_afsk_rx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_afsk_rx.application_information));
*(*ui*external_app*afsk_rx*);
*/external/afsk_rx/*(*ui*external_app*afsk_rx*);
} > ram_external_app_afsk_rx
.external_app_calculator : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_calculator.application_information));
*(*ui*external_app*calculator*);
*/external/calculator/*(*ui*external_app*calculator*);
} > ram_external_app_calculator
.external_app_font_viewer : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_font_viewer.application_information));
*(*ui*external_app*font_viewer*);
*/external/font_viewer/*(*ui*external_app*font_viewer*);
} > ram_external_app_font_viewer
.external_app_blespam : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_blespam.application_information));
*(*ui*external_app*blespam*);
*/external/blespam/*(*ui*external_app*blespam*);
} > ram_external_app_blespam
.external_app_analogtv : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_analogtv.application_information));
*(*ui*external_app*analogtv*);
*/external/analogtv/*(*ui*external_app*analogtv*);
} > ram_external_app_analogtv
.external_app_nrf_rx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_nrf_rx.application_information));
*(*ui*external_app*nrf_rx*);
*/external/nrf_rx/*(*ui*external_app*nrf_rx*);
} > ram_external_app_nrf_rx
.external_app_coasterp : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_coasterp.application_information));
*(*ui*external_app*coasterp*);
*/external/coasterp/*(*ui*external_app*coasterp*);
} > ram_external_app_coasterp
.external_app_lge : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_lge.application_information));
*(*ui*external_app*lge*);
*/external/lge/*(*ui*external_app*lge*);
} > ram_external_app_lge
.external_app_lcr : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_lcr.application_information));
*(*ui*external_app*lcr*);
*/external/lcr/*(*ui*external_app*lcr*);
} > ram_external_app_lcr
.external_app_jammer : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_jammer.application_information));
*(*ui*external_app*jammer*);
*/external/jammer/*(*ui*external_app*jammer*);
} > ram_external_app_jammer
.external_app_gpssim : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_gpssim.application_information));
*(*ui*external_app*gpssim*);
*/external/gpssim/*(*ui*external_app*gpssim*);
} > ram_external_app_gpssim
.external_app_spainter : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_spainter.application_information));
*(*ui*external_app*spainter*);
*/external/spainter/*(*ui*external_app*spainter*);
} > ram_external_app_spainter
.external_app_keyfob : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_keyfob.application_information));
*(*ui*external_app*keyfob*);
*/external/keyfob/*(*ui*external_app*keyfob*);
} > ram_external_app_keyfob
.external_app_tetris : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_tetris.application_information));
*(*ui*external_app*tetris*);
*/external/tetris/*(*ui*external_app*tetris*);
} > ram_external_app_tetris
.external_app_extsensors : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_extsensors.application_information));
*(*ui*external_app*extsensors*);
*/external/extsensors/*(*ui*external_app*extsensors*);
} > ram_external_app_extsensors
.external_app_foxhunt_rx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_foxhunt_rx.application_information));
*(*ui*external_app*foxhunt_rx*);
*/external/foxhunt/*(*ui*external_app*foxhunt_rx*);
} > ram_external_app_foxhunt_rx
.external_app_audio_test : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_audio_test.application_information));
*(*ui*external_app*audio_test*);
*/external/audio_test/*(*ui*external_app*audio_test*);
} > ram_external_app_audio_test
.external_app_wardrivemap : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_wardrivemap.application_information));
*(*ui*external_app*wardrivemap*);
*/external/wardrivemap/*(*ui*external_app*wardrivemap*);
} > ram_external_app_wardrivemap
.external_app_tpmsrx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_tpmsrx.application_information));
*(*ui*external_app*tpmsrx*);
*/external/tpmsrx/*(*ui*external_app*tpmsrx*);
} > ram_external_app_tpmsrx
.external_app_tpmstx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_tpmstx.application_information));
*(*ui*external_app*tpmstx*);
*/external/tpmstx/*(*ui*external_app*tpmstx*);
} > ram_external_app_tpmstx
.external_app_protoview : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_protoview.application_information));
*(*ui*external_app*protoview*);
*/external/protoview/*(*ui*external_app*protoview*);
} > ram_external_app_protoview
.external_app_adsbtx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_adsbtx.application_information));
*(*ui*external_app*adsbtx*);
*/external/adsbtx/*(*ui*external_app*adsbtx*);
} > ram_external_app_adsbtx
.external_app_morse_tx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_morse_tx.application_information));
*(*ui*external_app*morse_tx*);
*/external/morse_tx/*(*ui*external_app*morse_tx*);
} > ram_external_app_morse_tx
.external_app_sstvtx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_sstvtx.application_information));
*(*ui*external_app*sstvtx*);
*/external/sstvtx/*(*ui*external_app*sstvtx*);
} > ram_external_app_sstvtx
.external_app_random_password : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_random_password.application_information));
*(*ui*external_app*random_password*);
*/external/random_password/*(*ui*external_app*random_password*);
} > ram_external_app_random_password
.external_app_acars_rx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_acars_rx.application_information));
*(*ui*external_app*acars_rx*);
*/external/acars_rx/*(*ui*external_app*acars_rx*);
} > ram_external_app_acars_rx
.external_app_shoppingcart_lock : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_shoppingcart_lock.application_information));
*(*ui*external_app*shoppingcart_lock*);
*/external/shoppingcart_lock/*(*ui*external_app*shoppingcart_lock*);
} > ram_external_app_shoppingcart_lock
.external_app_cvs_spam : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_cvs_spam.application_information));
*(*ui*external_app*cvs_spam*);
*/external/cvs_spam/*(*ui*external_app*cvs_spam*);
} > ram_external_app_cvs_spam
.external_app_ookbrute : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_ookbrute.application_information));
*(*ui*external_app*ookbrute*);
*/external/ookbrute/*(*ui*external_app*ookbrute*);
} > ram_external_app_ookbrute
.external_app_ook_editor : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_ook_editor.application_information));
*(*ui*external_app*ook_editor*);
*/external/ook_editor/*(*ui*external_app*ook_editor*);
} > ram_external_app_ook_editor
.external_app_flippertx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_flippertx.application_information));
*(*ui*external_app*flippertx*);
*/external/flippertx/*(*ui*external_app*flippertx*);
} > ram_external_app_flippertx
.external_app_remote : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_remote.application_information));
*(*ui*external_app*remote*);
*/external/remote/*(*ui*external_app*remote*);
} > ram_external_app_remote
.external_app_mcu_temperature : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_mcu_temperature.application_information));
*(*ui*external_app*mcu_temperature*);
*/external/mcu_temperature/*(*ui*external_app*mcu_temperature*);
} > ram_external_app_mcu_temperature
.external_app_fmradio : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_fmradio.application_information));
*(*ui*external_app*fmradio*);
*/external/fmradio/*(*ui*external_app*fmradio*);
} > ram_external_app_fmradio
.external_app_tuner : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_tuner.application_information));
*(*ui*external_app*tuner*);
*/external/tuner/*(*ui*external_app*tuner*);
} > ram_external_app_tuner
.external_app_metronome : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_metronome.application_information));
*(*ui*external_app*metronome*);
*/external/metronome/*(*ui*external_app*metronome*);
} > ram_external_app_metronome
.external_app_app_manager : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_app_manager.application_information));
*(*ui*external_app*app_manager*);
*/external/app_manager/*(*ui*external_app*app_manager*);
} > ram_external_app_app_manager
.external_app_hopper : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_hopper.application_information));
*(*ui*external_app*hopper*);
*/external/hopper/*(*ui*external_app*hopper*);
} > ram_external_app_hopper
.external_app_antenna_length : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_antenna_length.application_information));
*(*ui*external_app*antenna_length*);
*/external/antenna_length/*(*ui*external_app*antenna_length*);
} > ram_external_app_antenna_length
.external_app_view_wav : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_view_wav.application_information));
*(*ui*external_app*view_wav*);
*/external/wav_view/*(*ui*external_app*view_wav*);
} > ram_external_app_view_wav
.external_app_sd_wipe : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_sd_wipe.application_information));
*(*ui*external_app*sd_wipe*);
*/external/sd_wipe/*(*ui*external_app*sd_wipe*);
} > ram_external_app_sd_wipe
.external_app_playlist_editor : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_playlist_editor.application_information));
*(*ui*external_app*playlist_editor*);
*/external/playlist_editor/*(*ui*external_app*playlist_editor*);
} > ram_external_app_playlist_editor
.external_app_snake : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_snake.application_information));
*(*ui*external_app*snake*);
*/external/snake/*(*ui*external_app*snake*);
} > ram_external_app_snake
.external_app_stopwatch : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_stopwatch.application_information));
*(*ui*external_app*stopwatch*);
*/external/stopwatch/*(*ui*external_app*stopwatch*);
} > ram_external_app_stopwatch
.external_app_wefax_rx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_wefax_rx.application_information));
*(*ui*external_app*wefax_rx*);
*/external/wefax_rx/*(*ui*external_app*wefax_rx*);
} > ram_external_app_wefax_rx
.external_app_noaaapt_rx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_noaaapt_rx.application_information));
*(*ui*external_app*noaaapt_rx*);
*/external/noaaapt_rx/*(*ui*external_app*noaaapt_rx*);
} > ram_external_app_noaaapt_rx
.external_app_vor_rx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_vor_rx.application_information));
*/external/vor_rx/*(*ui*external_app*vor_rx*);
} > ram_external_app_vor_rx
.external_app_vor_tx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_vor_tx.application_information));
*/external/vor_tx/*(*ui*external_app*vor_tx*);
} > ram_external_app_vor_tx
.external_app_breakout : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_breakout.application_information));
*(*ui*external_app*breakout*);
*/external/breakout/*(*ui*external_app*breakout*);
} > ram_external_app_breakout
.external_app_doom : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_doom.application_information));
*(*ui*external_app*doom*);
*/external/doom/*(*ui*external_app*doom*);
} > ram_external_app_doom
.external_app_debug_pmem : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_debug_pmem.application_information));
*(*ui*external_app*debug_pmem*);
*/external/debug_pmem/*(*ui*external_app*debug_pmem*);
} > ram_external_app_debug_pmem
.external_app_scanner : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_scanner.application_information));
*(*ui*external_app*scanner*);
*/external/scanner/*(*ui*external_app*scanner*);
} > ram_external_app_scanner
.external_app_level : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_level.application_information));
*(*ui*external_app*level*);
*/external/level/*(*ui*external_app*level*);
} > ram_external_app_level
.external_app_gfxeq : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_gfxeq.application_information));
*(*ui*external_app*gfxeq*);
*/external/gfxeq/*(*ui*external_app*gfxeq*);
} > ram_external_app_gfxeq
.external_app_waterfall_designer : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_waterfall_designer.application_information));
*(*ui*external_app*waterfall_designer*);
*/external/waterfall_designer/*(*ui*external_app*waterfall_designer*);
} > ram_external_app_waterfall_designer
.external_app_detector_rx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_detector_rx.application_information));
*(*ui*external_app*detector_rx*);
*/external/detector_rx/*(*ui*external_app*detector_rx*);
} > ram_external_app_detector_rx
.external_app_dinogame : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_dinogame.application_information));
*(*ui*external_app*dinogame*);
*/external/dinogame/*(*ui*external_app*dinogame*);
} > ram_external_app_dinogame
.external_app_spaceinv : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_spaceinv.application_information));
*(*ui*external_app*spaceinv*);
*/external/spaceinv/*(*ui*external_app*spaceinv*);
} > ram_external_app_spaceinv
.external_app_blackjack : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_blackjack.application_information));
*(*ui*external_app*blackjack*);
*/external/blackjack/*(*ui*external_app*blackjack*);
} > ram_external_app_blackjack
.external_app_battleship : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_battleship.application_information));
*(*ui*external_app*battleship*);
*/external/battleship/*(*ui*external_app*battleship*);
} > ram_external_app_battleship
.external_app_ert : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_ert.application_information));
*(*ui*external_app*ert*);
*/external/ert/*(*ui*external_app*ert*);
} > ram_external_app_ert
.external_app_epirb_rx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_epirb_rx.application_information));
*(*ui*external_app*epirb_rx*);
*/external/epirb_rx/*(*ui*external_app*epirb_rx*);
} > ram_external_app_epirb_rx
.external_app_soundboard : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_soundboard.application_information));
*(*ui*external_app*soundboard*);
*/external/soundboard/*(*ui*external_app*soundboard*);
} > ram_external_app_soundboard
.external_app_game2048 : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_game2048.application_information));
*(*ui*external_app*game2048*);
*/external/game2048/*(*ui*external_app*game2048*);
} > ram_external_app_game2048
.external_app_bht_tx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_bht_tx.application_information));
*(*ui*external_app*bht_tx*);
*/external/bht_tx/*(*ui*external_app*bht_tx*);
} > ram_external_app_bht_tx
.external_app_morse_practice : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_morse_practice.application_information));
*(*ui*external_app*morse_practice*);
*/external/morse_practice/*(*ui*external_app*morse_practice*);
} > ram_external_app_morse_practice
.external_app_adult_toys_controller : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_adult_toys_controller.application_information));
*(*ui*external_app*adult_toys_controller*);
*/external/adult_toys_controller/*(*ui*external_app*adult_toys_controller*);
} > ram_external_app_adult_toys_controller
.external_app_flex_rx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_flex_rx.application_information));
*(*ui*external_app*flex_rx*);
*/external/flex_rx/*(*ui*external_app*flex_rx*);
} > ram_external_app_flex_rx
.external_app_sstvrx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_sstvrx.application_information));
*(*ui*external_app*sstvrx*);
*/external/sstvrx/*(*ui*external_app*sstvrx*);
} > ram_external_app_sstvrx
.external_app_subcarrx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_subcarrx.application_information));
*(*ui*external_app*subcarrx*);
*/external/subcarrx/*(*ui*external_app*subcarrx*);
} > ram_external_app_subcarrx
.external_app_siggen : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_siggen.application_information));
*(*ui*external_app*siggen*);
*/external/siggen/*(*ui*external_app*siggen*);
} > ram_external_app_siggen
.external_app_sdusb : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_sdusb.application_information));
*(*ui*external_app*sdusb*);
*/external/sdusb/*(*ui*external_app*sdusb*);
} > ram_external_app_sdusb
.external_app_morse_radio : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_morse_radio.application_information));
*(*ui*external_app*morse_radio*);
*/external/morse_radio/*(*ui*external_app*morse_radio*);
} > ram_external_app_morse_radio
.external_app_morseradiotx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_morseradiotx.application_information));
*(*ui*external_app*morseradiotx*);
*/external/morseradiotx/*(*ui*external_app*morseradiotx*);
} > ram_external_app_morseradiotx
.external_app_keeloqtx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_keeloqtx.application_information));
*(*ui*external_app*keeloqtx*);
*/external/keeloqtx/*(*ui*external_app*keeloqtx*);
} > ram_external_app_keeloqtx
.external_app_rtty_rx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_rtty_rx.application_information));
*(*ui*external_app*rtty_rx*);
*/external/rtty_rx/*(*ui*external_app*rtty_rx*);
} > ram_external_app_rtty_rx
.external_app_rtty_tx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_rtty_tx.application_information));
*(*ui*external_app*rtty_tx*);
*/external/rtty_tx/*(*ui*external_app*rtty_tx*);
} > ram_external_app_rtty_tx
.external_app_pocsag_tx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_pocsag_tx.application_information));
*(*ui*external_app*pocsag_tx*);
*/external/pocsag_tx/*(*ui*external_app*pocsag_tx*);
} > ram_external_app_pocsag_tx
.external_app_time_sink : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_time_sink.application_information));
*(*ui*external_app*time_sink*);
*/external/time_sink/*(*ui*external_app*time_sink*);
} > ram_external_app_time_sink
.external_app_same_tx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_same_tx.application_information));
*(*ui*external_app*same_tx*);
*/external/same_tx/*(*ui*external_app*same_tx*);
} > ram_external_app_same_tx
.external_app_kiss_tnc : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_kiss_tnc.application_information));
*(*ui*external_app*kiss_tnc*);
*/external/kiss_tnc/*(*ui*external_app*kiss_tnc*);
} > ram_external_app_kiss_tnc
.external_app_mdc_tx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_mdc_tx.application_information));
*(*ui*external_app*mdc_tx*);
*/external/mdc_tx/*(*ui*external_app*mdc_tx*);
} > ram_external_app_mdc_tx
.external_app_epirb_tx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_epirb_tx.application_information));
*(*ui*external_app*epirb_tx*);
*/external/epirb_tx/*(*ui*external_app*epirb_tx*);
} > ram_external_app_epirb_tx
.external_app_fpv_detect : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_fpv_detect.application_information));
*(*ui*external_app*fpv_detect*);
*/external/fpv_detect/*(*ui*external_app*fpv_detect*);
} > ram_external_app_fpv_detect
.external_app_p25_tx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_p25_tx.application_information));
*(*ui*external_app*p25_tx*);
*/external/p25_tx/*(*ui*external_app*p25_tx*);
} > ram_external_app_p25_tx
.external_app_two_tone_pager : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_two_tone_pager.application_information));
*(*ui*external_app*two_tone_pager*);
*/external/two_tone_pager/*(*ui*external_app*two_tone_pager*);
} > ram_external_app_two_tone_pager
.external_app_two_tone_rx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_two_tone_rx.application_information));
*(*ui*external_app*two_tone_rx*);
*/external/two_tone_rx/*(*ui*external_app*two_tone_rx*);
} > ram_external_app_two_tone_rx
.external_app_flex_tx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_flex_tx.application_information));
*(*ui*external_app*flex_tx*);
*/external/flex_tx/*(*ui*external_app*flex_tx*);
} > ram_external_app_flex_tx
.external_app_hard_reset : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_hard_reset.application_information));
*(*ui*external_app*hard_reset*);
*/external/hard_reset/*(*ui*external_app*hard_reset*);
} > ram_external_app_hard_reset
.external_app_secplustx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_secplustx.application_information));
*(*ui*external_app*secplustx*);
*/external/secplustx/*(*ui*external_app*secplustx*);
} > ram_external_app_secplustx
.external_app_signal_hunter : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_signal_hunter.application_information));
*/external/signal_hunter/*(*ui*external_app*signal_hunter*);
} > ram_external_app_signal_hunter
.external_app_tetra_rx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_tetra_rx.application_information));
*/external/tetra_rx/*(*ui*external_app*tetra_rx*);
} > ram_external_app_tetra_rx
.external_app_adsbrx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_adsbrx.application_information));
*(*ui*external_app*adsbrx*);
} > ram_external_app_adsbrx
.external_app_ais_rx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_ais_rx.application_information));
*(*ui*external_app*ais_rx*);
} > ram_external_app_ais_rx
.external_app_aprs_rx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_aprs_rx.application_information));
*(*ui*external_app*aprs_rx*);
} > ram_external_app_aprs_rx
.external_app_aprs_tx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_aprs_tx.application_information));
*(*ui*external_app*aprs_tx*);
} > ram_external_app_aprs_tx
}
@@ -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.
*
+38 -2
View File
@@ -34,7 +34,7 @@ void KeeloqTXView::update_hop() {
hop = data.btn << 28 | (apri_serial & 0xFFF) << 16 | data.counter;
} else if (
data.mf_name == "DTM_Neo" || data.mf_name == "FAAC_RC,XT" || data.mf_name == "Mutanco_Mutancode" || data.mf_name == "Came_Space" || data.mf_name == "Genius_Bravo" || data.mf_name == "GSN" || data.mf_name == "Rosh" || data.mf_name == "Rossi" || data.mf_name == "Peccinin" || data.mf_name == "Steelmate" || data.mf_name == "Cardin_S449") {
data.mf_name == "DTM_Neo" || data.mf_name == "FAAC_RC,XT" || data.mf_name == "Mutanco_Mutancode" || data.mf_name == "Came_Space" || data.mf_name == "Genius_Bravo" || data.mf_name == "GSN" || data.mf_name == "Rosh" || data.mf_name == "Rossi" || data.mf_name == "Peccinin" || data.mf_name == "Steelmate" || data.mf_name == "Cardin_S449" || data.mf_name == "Superrollo") {
hop = data.btn << 28 | (data.serial & 0xFFF) << 16 | data.counter;
} else if (
data.mf_name == "NICE_Smilo" || data.mf_name == "NICE_MHOUSE" || data.mf_name == "JCM_Tech") {
@@ -75,6 +75,12 @@ void KeeloqTXView::update_payload() {
}
}
if (data.mf_name == "Superrollo") {
text_payload.set(to_string_hex((uint64_t)(uint32_t)encrypt));
encode_data_gw60((uint32_t)encrypt);
return;
}
payload = (uint64_t)fix << 32 | encrypt;
uint64_t preview_payload = FProtoGeneral::subghz_protocol_blocks_reverse_key(payload, 64);
@@ -83,6 +89,34 @@ void KeeloqTXView::update_payload() {
encode_data();
}
// Superrollo GW60 (HCS361) 67-bit OOK frame at Te=450us.
void KeeloqTXView::encode_data_gw60(uint32_t hop_enc) {
int bits[67];
for (uint32_t i = 0; i < 32; i++) bits[i] = (hop_enc >> i) & 1;
for (uint32_t i = 0; i < 28; i++) bits[32 + i] = (data.serial >> i) & 1;
for (uint32_t i = 0; i < 4; i++) bits[60 + i] = (data.btn >> i) & 1;
bits[64] = 1;
int crc0 = 0, crc1 = 0;
for (uint32_t i = 0; i < 65; i++) {
int new_crc1 = crc0 ^ bits[i];
int new_crc0 = new_crc1 ^ crc1;
crc0 = new_crc0 & 1;
crc1 = new_crc1 & 1;
}
bits[65] = crc0;
bits[66] = crc1;
std::string s{};
for (int i = 0; i < 9; i++) s += "100";
s += "11111111110000000000";
for (int i = 0; i < 67; i++) s += keeloq_fragments[bits[i]];
for (int i = 0; i < 18; i++) s += "0";
encoded_data = s;
pause_duration = 0;
}
void KeeloqTXView::encode_data() {
std::string fragments{};
@@ -206,9 +240,11 @@ void KeeloqTXView::start_tx() {
transmitter_model.enable();
const double te_us = (data.mf_name == "Superrollo") ? 450.0 : 400.0;
baseband::set_ook_data(
bitstream_length,
OOK_SAMPLERATE * (400.0 / 1000000.0),
OOK_SAMPLERATE * (te_us / 1000000.0),
repeat,
pause_duration);
}
@@ -106,6 +106,7 @@ class KeeloqTXView : public View {
std::string encoded_data{};
void encode_data();
void encode_data_gw60(uint32_t hop_enc);
uint32_t repeat = 4;
uint32_t pause_duration = 0;
@@ -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.
*
+1 -1
View File
@@ -46,7 +46,7 @@ SdOverUsbView::SdOverUsbView(NavigationView& nav)
sdcStop(&SDCD1);
portapack::shutdown(true, false);
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
baseband::run_prepared_image(portapack::memory::map::m4_code.base(), false);
m0_halt();
/* will not return*/
};
+48
View File
@@ -0,0 +1,48 @@
/*
* Copyright (C) 2026 Matej Sochan
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#include "ui.hpp"
#include "ui_signal_hunter.hpp"
#include "ui_navigation.hpp"
#include "external_app.hpp"
namespace ui::external_app::signal_hunter {
void initialize_app(ui::NavigationView& nav) {
nav.push<SignalHunterAppView>();
}
} // namespace ui::external_app::signal_hunter
extern "C" {
__attribute__((section(".external_app.app_signal_hunter.application_information"), used)) application_information_t _application_information_signal_hunter = {
/*.memory_location = */ (uint8_t*)0x00000000,
/*.externalAppEntry = */ ui::external_app::signal_hunter::initialize_app,
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
/*.app_name = */ "SigHunter",
/*.bitmap_data = */ {0x80, 0x01, 0x80, 0x01, 0xE0, 0x07, 0xF0, 0x0F, 0x98, 0x19, 0x8C, 0x31, 0x84, 0x21, 0xFF, 0xFF, 0xFF, 0xFF, 0x84, 0x21, 0x8C, 0x31, 0x98, 0x19, 0xF0, 0x0F, 0xE0, 0x07, 0x80, 0x01, 0x80, 0x01},
/*.icon_color = */ ui::Color::green().v,
/*.menu_location = */ app_location_t::RX,
/*.desired_menu_position = */ -1,
/*.m4_app_tag = */ {'H', 'U', 'N', 'T'},
/*.m4_app_offset = */ 0x00000000,
};
}
@@ -0,0 +1,403 @@
/*
* Copyright (C) 2026 Matej Sochan
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#include "ui_signal_hunter.hpp"
#include "receiver_model.hpp"
#include "baseband_api.hpp"
#include "ui_fileman.hpp"
#include "freqman_db.hpp"
#include "string_format.hpp"
#include "io_file.hpp"
#include "rtc_time.hpp"
using namespace portapack;
namespace ui::external_app::signal_hunter {
// --- TAB 1: Main View ---
HunterMainView::HunterMainView(Rect parent_rect, SignalHunterAppView& parent)
: View(parent_rect), parent_app(parent) {
add_children({&text_current_freq, &text_status, &button_start_stop, &text_hits});
update_frequency(433920000);
button_start_stop.on_select = [this](Button&) {
parent_app.is_hunting = !parent_app.is_hunting;
if (parent_app.is_hunting) {
button_start_stop.set_text("STOP");
// Set initial frequency based on set mode (single freq / freq hopping)
if (parent_app.freq_hop_mode && !parent_app.frequency_list.empty()) {
parent_app.current_freq_index = 0;
auto freq = parent_app.frequency_list[0];
receiver_model.set_target_frequency(freq);
update_frequency(freq);
} else {
receiver_model.set_target_frequency(parent_app.single_frequency);
update_frequency(parent_app.single_frequency);
}
update_status("HUNTING...", Theme::getInstance()->fg_green);
parent_app.send_hunter_config(true);
} else {
button_start_stop.set_text("START");
update_status("IDLE", Theme::getInstance()->fg_light);
set_recording_state(false);
parent_app.send_hunter_config(false);
}
};
}
void HunterMainView::on_show() {
if (!parent_app.freq_hop_mode) {
current_freq_ = receiver_model.target_frequency();
}
update_frequency(current_freq_); // force repaint
}
void HunterMainView::focus() {
if (!parent_app.freq_hop_mode) {
current_freq_ = receiver_model.target_frequency();
}
update_frequency(current_freq_);
button_start_stop.focus();
}
void HunterMainView::set_recording_state(bool recording) {
if (recording)
text_current_freq.set_style(Theme::getInstance()->fg_red);
else
text_current_freq.set_style(Theme::getInstance()->fg_light);
text_current_freq.set_dirty(); // force ui update
update_frequency(current_freq_); // repaint with the new color
}
void HunterMainView::update_frequency(rf::Frequency freq) {
current_freq_ = freq;
text_current_freq.set(to_string_dec_uint(freq) + " Hz");
}
void HunterMainView::update_status(const std::string& status, const Style* style) {
text_status.set(status);
text_status.set_style(style);
}
void HunterMainView::update_hits(uint32_t hits) {
text_hits.set("Hits: " + to_string_dec_uint(hits));
}
// --- TAB 2: Freqs View ---
HunterFreqsView::HunterFreqsView(Rect parent_rect, SignalHunterAppView& parent)
: View(parent_rect), parent_app(parent) {
add_children({&button_load_file,
&button_clear,
&labels,
&field_dwell,
&text_loaded_info});
field_dwell.set_value(parent_app.hop_dwell_ms);
field_dwell.on_change = [this](int32_t v) { parent_app.hop_dwell_ms = v; };
button_load_file.on_select = [this](Button&) {
auto* view = parent_app.get_nav().push<FileLoadView>(".TXT");
if (view) {
view->on_changed = [this](std::filesystem::path path) {
parent_app.frequency_list.clear();
parent_app.current_freq_index = 0;
FreqmanDB db{};
db.open(path);
for (const auto& entry : db) {
if (entry.frequency_a > 0)
parent_app.frequency_list.push_back(entry.frequency_a);
}
parent_app.freqman_file = path.stem().string();
// Switch to HOP mode if a file was loaded
if (!parent_app.frequency_list.empty())
parent_app.freq_hop_mode = true;
update_list_count();
// Update the mode display in the Config tab
if (parent_app.get_config_view()) {
parent_app.get_config_view()->update_mode_display();
}
};
}
};
button_clear.on_select = [this](Button&) {
parent_app.frequency_list.clear();
parent_app.current_freq_index = 0;
parent_app.freq_hop_mode = false;
update_list_count();
// Update the mode display in the Config tab
if (parent_app.get_config_view()) {
parent_app.get_config_view()->update_mode_display();
}
};
}
void HunterFreqsView::update_list_count() {
text_loaded_info.set("Loaded: " +
to_string_dec_uint(parent_app.frequency_list.size()) + " freqs");
}
void HunterFreqsView::focus() {
button_load_file.focus();
}
// --- TAB 3: Config View ---
HunterConfigView::HunterConfigView(Rect parent_rect, SignalHunterAppView& parent)
: View(parent_rect), parent_app(parent), field_single_freq{{UI_POS_X(2), UI_POS_Y(4)}, parent.get_nav()} {
add_children({&button_mode,
&field_single_freq,
&labels,
&field_threshold,
&field_hang_time,
&text_info_config});
button_mode.on_select = [this](Button&) {
parent_app.freq_hop_mode = !parent_app.freq_hop_mode;
update_mode_display();
};
field_single_freq.set_value(parent_app.single_frequency);
field_threshold.set_value(parent_app.energy_threshold);
field_threshold.on_change = [this](int32_t v) { parent_app.energy_threshold = v; };
field_hang_time.set_value(parent_app.hangtime_ms);
field_hang_time.on_change = [this](int32_t v) { parent_app.hangtime_ms = v; };
update_mode_display();
}
void HunterConfigView::on_show() {
auto current = receiver_model.target_frequency();
parent_app.single_frequency = current;
field_single_freq.set_value(current);
}
void HunterConfigView::update_mode_display() {
if (parent_app.freq_hop_mode)
button_mode.set_text("MODE: HOP (" +
to_string_dec_uint(parent_app.frequency_list.size()) + " freqs)");
else
button_mode.set_text("MODE: SINGLE");
}
void HunterConfigView::focus() {
field_threshold.focus();
}
// --- Main App View ---
SignalHunterAppView::SignalHunterAppView(ui::NavigationView& nav)
: frequency_list{},
nav_(nav) {
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
receiver_model.set_target_frequency(433920000);
receiver_model.set_baseband_bandwidth(1750000);
receiver_model.set_sampling_rate(2000000);
receiver_model.enable();
view_main = std::make_unique<HunterMainView>(content_rect, *this);
view_freqs = std::make_unique<HunterFreqsView>(content_rect, *this);
view_config = std::make_unique<HunterConfigView>(content_rect, *this);
tab_view = std::make_unique<TabView>(std::initializer_list<TabView::TabDef>{
{"Target", Color::cyan(), view_main.get()},
{"Freqs", Color::green(), view_freqs.get()},
{"Config", Color::yellow(), view_config.get()}});
tab_view->set_parent_rect(view_rect);
add_children({&field_lna,
&field_vga,
&field_rf_amp,
&rssi,
tab_view.get(),
view_main.get(),
view_freqs.get(),
view_config.get()});
view_freqs->hidden(true);
view_config->hidden(true);
using namespace app_settings;
settings_ = std::make_unique<SettingsManager>(
"ext_signal_hunter", Mode::RX,
SettingBindings{
{"threshold"sv, &energy_threshold},
{"hangtime_ms"sv, &hangtime_ms},
{"hop_dwell_ms"sv, &hop_dwell_ms},
{"single_freq"sv, &single_frequency},
{"file"sv, &freqman_file}});
}
SignalHunterAppView::~SignalHunterAppView() {
if (!freq_hop_mode) {
single_frequency = receiver_model.target_frequency();
}
// Safely shutdown recording
stop_recording();
receiver_model.disable();
baseband::shutdown();
}
void SignalHunterAppView::focus() {
if (tab_view)
tab_view->focus();
}
// TIMER LOGIC: Based on display refresh rate
void SignalHunterAppView::on_frame_sync() {
// Timer running only if, HUNTING, in HOPPING mode, not recording + we have the freqs list
if (is_hunting && freq_hop_mode && !capture_thread && !frequency_list.empty()) {
hop_timer_ms += 17; // 1 frame in a 60 Hz display ~ 16.6 ms
// if Dwell Time has passed, change frequency
if (hop_timer_ms >= hop_dwell_ms) {
hop_timer_ms = 0;
current_freq_index = (current_freq_index + 1) % frequency_list.size();
auto next_freq = frequency_list[current_freq_index];
receiver_model.set_target_frequency(next_freq);
if (view_main) {
view_main->update_frequency(next_freq);
}
}
} else {
hop_timer_ms = 0; // if not hunting / recording -> timer = 0
}
}
// Recording control logic via M0 <-> M4 IPC
void SignalHunterAppView::send_hunter_config(bool start) {
// Send config to baseband processor via IPC mechanism
baseband::set_hunter_config(energy_threshold, hangtime_ms, start);
if (!start) {
stop_recording();
}
}
void SignalHunterAppView::on_hunter_trigger(const HunterTriggerMessage* /*message*/) {
// M4 has detected signal above threshold - start I/Q capture
if (!capture_thread && is_hunting) {
trigger_hits++;
view_main->update_hits(trigger_hits);
view_main->update_status("RECORDING!", Theme::getInstance()->fg_red);
view_main->set_recording_state(true);
start_recording();
}
}
void SignalHunterAppView::on_hunter_stop(const HunterStopMessage*) {
if (capture_thread) {
stop_recording();
view_main->set_recording_state(false);
if (is_hunting) {
if (freq_hop_mode && !frequency_list.empty()) {
// Next frequency
current_freq_index = (current_freq_index + 1) % frequency_list.size();
auto next_freq = frequency_list[current_freq_index];
receiver_model.set_target_frequency(next_freq);
view_main->update_frequency(next_freq);
// Reset the timer
hop_timer_ms = 0;
}
view_main->update_status("HUNTING...", Theme::getInstance()->fg_green);
}
}
}
void SignalHunterAppView::start_recording() {
rtc::RTC datetime{};
rtc_time::now(datetime);
// Generate timestamped filename for capture session
const std::string capture_filename = "HNT_" +
to_string_dec_uint(datetime.year(), 4, '0') +
to_string_dec_uint(datetime.month(), 2, '0') +
to_string_dec_uint(datetime.day(), 2, '0') + "T" +
to_string_dec_uint(datetime.hour(), 2, '0') +
to_string_dec_uint(datetime.minute(), 2, '0') +
to_string_dec_uint(datetime.second(), 2, '0');
// Use FileWriter for direct raw I/Q dump to avoid M0 CPU bottlenecks.
// FileConvertWriter is heavier
auto writer = std::make_unique<FileWriter>();
auto create_error = writer->create(std::filesystem::path(u"/CAPTURES") / (capture_filename + ".C16"));
if (create_error.is_valid()) {
if (view_main) view_main->update_status("SD ERROR", Theme::getInstance()->fg_red);
send_hunter_config(false);
is_hunting = false;
if (view_main) view_main->set_start_button_text("START");
return;
}
current_capture_filename = capture_filename;
capture_thread = std::make_unique<CaptureThread>(
std::move(writer),
4096,
4,
[]() {},
[](File::Error) {});
if (view_main) view_main->update_status("RECORDING!", Theme::getInstance()->fg_red);
}
void SignalHunterAppView::stop_recording() {
// Stop I/Q capture and release SD card for metadata write
capture_thread.reset();
// Generate metadata file after capture completes
if (!current_capture_filename.empty()) {
File txt_file;
auto txt_error = txt_file.create(std::filesystem::path(u"/CAPTURES") / (current_capture_filename + ".TXT"));
// is_valid() returns true only if error is present (counter-intuitive)
if (!txt_error.is_valid()) {
std::string metadata =
"center_frequency=" + to_string_dec_uint(receiver_model.target_frequency()) + "\n" +
// Divide main sample rate by decimation factor (8) to match C16 sample rate
"sample_rate=" + to_string_dec_uint(receiver_model.sampling_rate() / 8) + "\n";
txt_file.write(metadata.c_str(), metadata.length());
}
// Clear filename for next capture cycle
current_capture_filename.clear();
}
}
} // namespace ui::external_app::signal_hunter
@@ -0,0 +1,192 @@
/*
* Copyright (C) 2026 Matej Sochan
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#ifndef __UI_SIGNAL_HUNTER_H__
#define __UI_SIGNAL_HUNTER_H__
#include "app_settings.hpp"
#include "rf_path.hpp"
#include "ui_widget.hpp"
#include "ui_navigation.hpp"
#include "ui_receiver.hpp"
#include "ui_tabview.hpp"
#include "ui_freq_field.hpp"
#include "capture_thread.hpp"
#include "message.hpp"
#include <vector>
#include <memory>
namespace ui::external_app::signal_hunter {
class SignalHunterAppView;
// --- TAB 1: Main View ---
class HunterMainView : public View {
public:
HunterMainView(Rect parent_rect, SignalHunterAppView& parent);
void on_show() override;
void focus() override;
void update_status(const std::string& status, const Style* style);
void update_hits(uint32_t hits);
void update_frequency(rf::Frequency freq);
void set_recording_state(bool recording);
void set_start_button_text(const std::string& text) { button_start_stop.set_text(text); }
private:
SignalHunterAppView& parent_app;
rf::Frequency current_freq_{433920000};
Text text_current_freq{{UI_POS_X_CENTER(14), UI_POS_Y(3), 112, 16}, ""};
Text text_status{{UI_POS_X_CENTER(12), UI_POS_Y(6), 96, 16}, "IDLE"};
Button button_start_stop{{UI_POS_X_CENTER(10), UI_POS_Y(8), 80, 32}, "START"};
Text text_hits{{UI_POS_X_CENTER(10), UI_POS_Y(11), 80, 16}, "Hits: 0"};
};
// --- TAB 2: Freqs View ---
class HunterFreqsView : public View {
public:
HunterFreqsView(Rect parent_rect, SignalHunterAppView& parent);
void focus() override;
void update_list_count();
private:
SignalHunterAppView& parent_app;
Button button_load_file{{UI_POS_X(2), UI_POS_Y(1), 80, 28}, "LOAD FILE"};
Button button_clear{{UI_POS_X_RIGHT(12), UI_POS_Y(1), 80, 28}, "CLEAR"};
Labels labels{
{{UI_POS_X(2), UI_POS_Y(4)}, "Dwell Time (ms):", Color::light_grey()}};
NumberField field_dwell{{UI_POS_X_RIGHT(8), UI_POS_Y(4)}, 4, {10, 9999}, 10, ' '};
Text text_loaded_info{{UI_POS_X(2), UI_POS_Y(6), 208, 16}, "Loaded: 0 freqs"};
};
// --- TAB 3: Config View ---
class HunterConfigView : public View {
public:
HunterConfigView(Rect parent_rect, SignalHunterAppView& parent);
void update_mode_display();
void focus() override;
void on_show() override;
private:
SignalHunterAppView& parent_app;
Button button_mode{
{UI_POS_X(2), UI_POS_Y(2), 208, 28},
"MODE: SINGLE"};
RxFrequencyField field_single_freq; // UI_POS_Y(4)
Labels labels{
{{UI_POS_X(2), UI_POS_Y(6)}, "Energy Threshold:", Color::light_grey()},
{{UI_POS_X(2), UI_POS_Y(8)}, "Hang-Time (ms):", Color::light_grey()}};
NumberField field_threshold{{UI_POS_X_RIGHT(8), UI_POS_Y(6)}, 5, {100, 99999}, 100, ' '};
NumberField field_hang_time{{UI_POS_X_RIGHT(8), UI_POS_Y(8)}, 4, {10, 5000}, 10, ' '};
Text text_info_config{
{UI_POS_X(2), UI_POS_Y(10), 208, 16},
"Restart HUNT after change"};
};
// --- MAIN APP VIEW ---
class SignalHunterAppView final : public ui::View {
public:
SignalHunterAppView(ui::NavigationView& nav);
~SignalHunterAppView();
void focus() override;
std::string title() const override { return "SignalHunter"; }
ui::NavigationView& get_nav() { return nav_; }
// Application state shared across all views
std::vector<rf::Frequency> frequency_list;
uint32_t current_freq_index{0};
uint32_t energy_threshold{5000};
uint32_t hangtime_ms{500};
uint32_t hop_dwell_ms{200};
std::string freqman_file{"TARGETS"};
bool is_hunting{false};
uint32_t trigger_hits{0};
rf::Frequency single_frequency{433920000};
bool freq_hop_mode{false};
void send_hunter_config(bool start);
HunterMainView* get_main_view() { return view_main.get(); }
HunterConfigView* get_config_view() { return view_config.get(); }
private:
ui::NavigationView& nav_;
std::unique_ptr<app_settings::SettingsManager> settings_{};
std::unique_ptr<HunterMainView> view_main{};
std::unique_ptr<HunterFreqsView> view_freqs{};
std::unique_ptr<HunterConfigView> view_config{};
std::unique_ptr<TabView> tab_view{};
std::string current_capture_filename{};
std::unique_ptr<CaptureThread> capture_thread{};
ui::LNAGainField field_lna{{UI_POS_X(0), UI_POS_Y(0)}};
ui::VGAGainField field_vga{{UI_POS_X(7), UI_POS_Y(0)}};
ui::RFAmpField field_rf_amp{{UI_POS_X(14), UI_POS_Y(0)}};
ui::RSSI rssi{{UI_POS_X(0), UI_POS_Y(1), UI_POS_MAXWIDTH, 4}};
static constexpr Dim tab_bar_h = 20;
Rect view_rect{
0, UI_POS_Y(2) + 4, UI_POS_MAXWIDTH,
screen_height - (UI_POS_Y(2) + 4) - UI_POS_HEIGHT(1)};
Rect content_rect{
0, UI_POS_Y(2) + 4 + tab_bar_h, UI_POS_MAXWIDTH,
screen_height - (UI_POS_Y(2) + 4 + tab_bar_h) - UI_POS_HEIGHT(1)};
void on_hunter_trigger(const HunterTriggerMessage* message);
void on_hunter_stop(const HunterStopMessage* message);
void start_recording();
void stop_recording();
// timer variables
uint32_t hop_timer_ms{0};
void on_frame_sync();
MessageHandlerRegistration message_handler_frame_sync{
Message::ID::DisplayFrameSync,
[this](const Message* const) {
this->on_frame_sync();
}};
MessageHandlerRegistration message_handler_trigger{
Message::ID::HunterTrigger,
[this](const Message* const p) {
this->on_hunter_trigger(static_cast<const HunterTriggerMessage*>(p));
}};
MessageHandlerRegistration message_handler_stop{
Message::ID::HunterStop,
[this](const Message* const p) {
this->on_hunter_stop(static_cast<const HunterStopMessage*>(p));
}};
};
} // namespace ui::external_app::signal_hunter
#endif /*__UI_SIGNAL_HUNTER_H__*/
+1 -1
View File
@@ -1,6 +1,6 @@
/*
* Copyright 2025 Mark Thompson
* copyleft 2025 zxkmm AKA zix aka sommermorgentraum
* copyleft 2025 zxkmm
*
* This file is part of PortaPack.
*
+1 -1
View File
@@ -1,6 +1,6 @@
/*
* Copyright 2025 Mark Thompson
* copyleft 2025 zxkmm AKA zix aka sommermorgentraum
* copyleft 2025 zxkmm
*
* This file is part of PortaPack.
*
+81
View File
@@ -0,0 +1,81 @@
/*
* Copyright (C) 2026 HTotoo
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#include "ui_tetra_rx.hpp"
#include "ui_navigation.hpp"
#include "external_app.hpp"
namespace ui::external_app::tetra_rx {
void initialize_app(ui::NavigationView& nav) {
nav.push<TetraRxView>();
}
} // namespace ui::external_app::tetra_rx
extern "C" {
__attribute__((section(".external_app.app_tetra_rx.application_information"), used)) application_information_t _application_information_tetra_rx = {
/*.memory_location = */ (uint8_t*)0x00000000,
/*.externalAppEntry = */ ui::external_app::tetra_rx::initialize_app,
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
/*.app_name = */ "Tetra",
/*.bitmap_data = */ {
0xE0,
0x0F,
0x18,
0x38,
0xE4,
0x67,
0x7E,
0xCE,
0xC7,
0xCC,
0x00,
0x00,
0xFF,
0x4F,
0xBA,
0xB2,
0x9A,
0xEE,
0xBA,
0xB2,
0x00,
0x00,
0x3B,
0xE3,
0x73,
0x7E,
0xC6,
0x27,
0x1C,
0x18,
0xF0,
0x07,
},
/*.icon_color = */ ui::Color::orange().v,
/*.menu_location = */ app_location_t::RX,
/*.desired_menu_position = */ -1,
/*.m4_app_tag = portapack::spi_flash::image_tag_tetrarx*/ {'P', 'T', 'E', 'T'},
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
};
}
+45
View File
@@ -0,0 +1,45 @@
#pragma once
#include <stdint.h>
#include <stddef.h>
namespace ui::external_app::tetra_rx {
class BitVector {
public:
BitVector(uint8_t* p, size_t bits)
: data_(p),
bits_(bits) {
}
inline bool get(size_t bit) const {
return (data_[bit >> 3] >>
(7 - (bit & 7))) &
1;
}
inline void set(size_t bit, bool value) {
if (value)
data_[bit >> 3] |=
1 << (7 - (bit & 7));
else
data_[bit >> 3] &=
~(1 << (7 - (bit & 7)));
}
inline void xor_bit(size_t bit, bool value) {
if (value)
data_[bit >> 3] ^=
1 << (7 - (bit & 7));
}
inline size_t size() const {
return bits_;
}
private:
uint8_t* data_;
size_t bits_;
};
} // namespace ui::external_app::tetra_rx
+33
View File
@@ -0,0 +1,33 @@
#include "tetra_crc.hpp"
namespace ui::external_app::tetra_rx {
uint16_t CRC::crc16_itut_bits(
const uint8_t* bits,
uint32_t n_bits,
uint16_t init) {
uint16_t crc = init;
for (uint32_t i = 0; i < n_bits; i++) {
uint8_t bit =
(bits[i >> 3] >>
(7 - (i & 7))) &
1;
crc ^= uint16_t(bit) << 15;
if (crc & 0x8000) {
crc =
(crc << 1) ^
0x1021;
} else {
crc <<= 1;
}
crc &= 0xffff;
}
return crc;
}
} // namespace ui::external_app::tetra_rx
+17
View File
@@ -0,0 +1,17 @@
#pragma once
#include <stdint.h>
namespace ui::external_app::tetra_rx {
class CRC {
public:
static constexpr uint16_t CRC_OK = 0x1d0f;
static uint16_t crc16_itut_bits(
const uint8_t* bits,
uint32_t n_bits,
uint16_t init = 0xffff);
};
} // namespace ui::external_app::tetra_rx
@@ -0,0 +1,60 @@
#include "tetra_descrambler.hpp"
namespace ui::external_app::tetra_rx {
static inline uint8_t lfsr_step(uint32_t& s) {
uint8_t b = 0;
b ^= (s >> (32 - 32)) & 1;
b ^= (s >> (32 - 26)) & 1;
b ^= (s >> (32 - 23)) & 1;
b ^= (s >> (32 - 22)) & 1;
b ^= (s >> (32 - 16)) & 1;
b ^= (s >> (32 - 12)) & 1;
b ^= (s >> (32 - 11)) & 1;
b ^= (s >> (32 - 10)) & 1;
b ^= (s >> (32 - 8)) & 1;
b ^= (s >> (32 - 7)) & 1;
b ^= (s >> (32 - 5)) & 1;
b ^= (s >> (32 - 4)) & 1;
b ^= (s >> (32 - 2)) & 1;
b ^= (s >> (32 - 1)) & 1;
s = (s >> 1) | (uint32_t(b) << 31);
return b;
}
void Descrambler::generate(
uint32_t init,
uint8_t* seq,
size_t bits) {
uint32_t s = init;
for (size_t i = 0; i < bits; i++)
seq[i] = lfsr_step(s);
}
void Descrambler::descramble(
BitVector& bits,
uint32_t init) {
uint32_t s = init;
for (size_t i = 0; i < bits.size(); i++)
bits.xor_bit(i, lfsr_step(s));
}
uint32_t Descrambler::dmo_scramb_init(
uint32_t mni,
uint32_t src) {
uint32_t init =
(src & 0xFFFFFF) |
((mni & 0x3F) << 24);
init <<= 2;
init |= 3;
return init;
}
} // namespace ui::external_app::tetra_rx
@@ -0,0 +1,26 @@
#pragma once
#include <stdint.h>
#include "tetra_bits.hpp"
namespace ui::external_app::tetra_rx {
class Descrambler {
public:
static constexpr uint32_t SCRAMB_INIT_BSCH = 0x00000003;
static void descramble(
BitVector& bits,
uint32_t init = SCRAMB_INIT_BSCH);
static void generate(
uint32_t init,
uint8_t* sequence,
size_t bits);
static uint32_t dmo_scramb_init(
uint32_t mni,
uint32_t src);
};
} // namespace ui::external_app::tetra_rx
@@ -0,0 +1,21 @@
#include "tetra_interleave.hpp"
namespace ui::external_app::tetra_rx {
void Interleave::block_deinterleave(
BitVector& in,
BitVector& out,
uint32_t K,
uint32_t a) {
for (uint32_t i = 1; i <= K; i++) {
uint32_t k =
1 +
((a * i) % K);
out.set(
i - 1,
in.get(k - 1));
}
}
} // namespace ui::external_app::tetra_rx
@@ -0,0 +1,16 @@
#pragma once
#include "tetra_bits.hpp"
namespace ui::external_app::tetra_rx {
class Interleave {
public:
static void block_deinterleave(
BitVector& in,
BitVector& out,
uint32_t K,
uint32_t a);
};
} // namespace ui::external_app::tetra_rx
+24
View File
@@ -0,0 +1,24 @@
#include "tetra_rcpc.hpp"
#include <string.h>
namespace ui::external_app::tetra_rx {
static constexpr uint8_t P_RATE_2_3[3] = {1, 2, 5};
static constexpr uint32_t T_RATE_2_3 = 3;
static constexpr uint32_t PERIOD = 8;
void RCPC::depuncture_2_3(BitVector& in, uint8_t* out, uint32_t in_bits) {
const uint32_t mother_bits = ((in_bits + T_RATE_2_3 - 1) / T_RATE_2_3) * PERIOD + PERIOD;
memset(out, ERASED, mother_bits);
for (uint32_t j = 1; j <= in_bits; j++) {
uint32_t i = j;
uint32_t k = PERIOD * ((i - 1) / T_RATE_2_3);
k += P_RATE_2_3[(i - 1) % T_RATE_2_3];
if (k >= 1 && k <= mother_bits) {
out[k - 1] = in.get(j - 1);
}
}
}
} // namespace ui::external_app::tetra_rx
+18
View File
@@ -0,0 +1,18 @@
#pragma once
#include <stdint.h>
#include "tetra_bits.hpp"
namespace ui::external_app::tetra_rx {
class RCPC {
public:
static constexpr uint8_t ERASED = 0xff;
static void depuncture_2_3(
BitVector& in,
uint8_t* out,
uint32_t in_bits);
};
} // namespace ui::external_app::tetra_rx
@@ -0,0 +1,73 @@
#include "tetra_viterbi.hpp"
#include <string.h>
namespace ui::external_app::tetra_rx {
int Viterbi::decode_cch(
const uint8_t* mother,
uint32_t n_info,
uint8_t* bits,
uint8_t* trace_buffer) { // Puffer by caller
// Store these on stack, to avoid flash space usage
const uint8_t next_state[16][2] = {
{0, 1}, {2, 3}, {4, 5}, {6, 7}, {8, 9}, {10, 11}, {12, 13}, {14, 15}, {0, 1}, {2, 3}, {4, 5}, {6, 7}, {8, 9}, {10, 11}, {12, 13}, {14, 15}};
const uint8_t next_output[16][2] = {
{0, 15}, {11, 4}, {6, 9}, {13, 2}, {5, 10}, {14, 1}, {3, 12}, {8, 7}, {15, 0}, {4, 11}, {9, 6}, {2, 13}, {10, 5}, {1, 14}, {12, 3}, {7, 8}};
const uint16_t INF = 0x3fff;
uint16_t cost[16];
uint16_t new_cost[16];
// Initialization
for (int i = 0; i < 16; i++) cost[i] = INF;
cost[0] = 0;
for (uint32_t step = 0; step < n_info; step++) {
for (int i = 0; i < 16; i++) new_cost[i] = INF;
const uint8_t* rx = &mother[step * 4];
for (int prev = 0; prev < 16; prev++) {
if (cost[prev] >= INF) continue;
for (int bit = 0; bit < 2; bit++) {
const uint8_t pattern = next_output[prev][bit];
const uint8_t next = next_state[prev][bit];
uint16_t d = 0;
for (int b = 0; b < 4; b++) {
if (rx[b] != 0xff && rx[b] != ((pattern >> (3 - b)) & 1)) d++;
}
const uint16_t total = cost[prev] + d;
if (total < new_cost[next]) {
new_cost[next] = total;
trace_buffer[step * 16 + next] = (prev << 1) | bit;
}
}
}
memcpy(cost, new_cost, sizeof(cost));
}
// Traceback
uint8_t best_state = 0;
for (int i = 1; i < 16; i++) {
if (cost[i] < cost[best_state]) best_state = i;
}
uint8_t state = best_state;
memset(bits, 0, (n_info + 7) >> 3);
for (int step = (int)n_info - 1; step >= 0; step--) {
uint8_t packed = trace_buffer[step * 16 + state];
uint8_t prev = (packed >> 1) & 0x0F;
uint8_t bit = packed & 0x01;
if (bit) bits[step >> 3] |= (1 << (7 - (step & 7)));
state = prev;
}
return cost[best_state];
}
} // namespace ui::external_app::tetra_rx
@@ -0,0 +1,17 @@
#pragma once
#include <stdint.h>
#include <string.h>
namespace ui::external_app::tetra_rx {
class Viterbi {
public:
int decode_cch(
const uint8_t* mother,
uint32_t n_info,
uint8_t* bits,
uint8_t* trace_buffer);
};
} // namespace ui::external_app::tetra_rx
+354
View File
@@ -0,0 +1,354 @@
#include "ui_tetra_rx.hpp"
#include "rtc_time.hpp"
#include "baseband_api.hpp"
#include "string_format.hpp"
#include "portapack_persistent_memory.hpp"
#include "file_path.hpp"
#include "tetra_crc.hpp"
#include "tetra_descrambler.hpp"
#include "tetra_interleave.hpp"
#include "tetra_rcpc.hpp"
#include "tetra_viterbi.hpp"
#include <cstring>
using namespace portapack;
using namespace ui;
namespace ui::external_app::tetra_rx {
template <size_t OutBytes>
bool TetraChannelDecoder::decode_block(
const std::array<uint8_t, 63>& burst,
size_t offset,
size_t len_t5,
size_t type1_bits,
size_t type2_bits,
size_t interleave_a,
std::array<uint8_t, OutBytes>& out,
uint16_t& crc,
int& cost,
uint32_t scramb_init) {
std::array<uint8_t, 64> t5{};
std::array<uint8_t, 64> t3{};
std::array<uint8_t, 1200> mother{};
std::array<uint8_t, 64> type2{};
out.fill(0);
for (size_t i = 0; i < len_t5; i++) {
set_bit(t5, i, get_bit(burst, offset + i));
}
BitVector t5_bits{t5.data(), len_t5};
Descrambler::descramble(t5_bits, scramb_init);
BitVector t3_bits{t3.data(), len_t5};
Interleave::block_deinterleave(t5_bits, t3_bits, len_t5, interleave_a);
RCPC::depuncture_2_3(t3_bits, mother.data(), len_t5);
Viterbi viterbi;
cost = viterbi.decode_cch(mother.data(), type2_bits, type2.data(), trace_buffer);
crc = CRC::crc16_itut_bits(type2.data(), type1_bits + 16);
for (size_t i = 0; i < type1_bits; i++) {
set_bit(out, i, (type2[i >> 3] >> (7 - (i & 7))) & 1);
}
return crc == CRC::CRC_OK;
}
TetraChannelDecoder::Result TetraChannelDecoder::decode_dnb(const TetraDnbMessage& message) {
Result result{};
result.inverted = message.inverted;
if (!network_synced)
return result;
std::array<uint8_t, 63> buffer{};
std::copy(message.payload.begin(), message.payload.end(), buffer.begin());
uint32_t tmo_dnb_seed = (((uint32_t)last_mcc << 20) | ((uint32_t)last_mnc << 6) | (uint32_t)last_bcc) << 2 | 3;
// 1. DNB Full slot (SCH/F)
bool ok = decode_block(buffer, 0, SCH_F_LEN_BITS, SCH_F_TYPE1_BITS, SCH_F_TYPE2_BITS,
SCH_F_INTERLEAVE_A, result.payload, result.crc, result.cost, tmo_dnb_seed);
// 2. SCH/HD Block 1
if (!ok) {
ok = decode_block(buffer, 0, BLK2_LEN_BITS, SB2_TYPE1_BITS, SB2_TYPE2_BITS,
SB2_INTERLEAVE_A, result.payload, result.crc, result.cost, tmo_dnb_seed);
}
// 3. SCH/HD Block 2
if (!ok) {
ok = decode_block(buffer, 216, BLK2_LEN_BITS, SB2_TYPE1_BITS, SB2_TYPE2_BITS,
SB2_INTERLEAVE_A, result.payload, result.crc, result.cost, tmo_dnb_seed);
}
if (ok) {
result.type = Result::Type::Dnb;
result.is_ok = true;
parse_mac_pdu(result);
}
return result;
}
TetraChannelDecoder::Result TetraChannelDecoder::decode_burst(const TetraBurstMessage& message) {
Result result{};
result.sync_errors = message.sync_errors;
result.inverted = message.inverted;
// 1. SCH/S
bool ok = decode_block(
message.payload, BLK1_OFFSET_BITS, BLK1_LEN_BITS,
SB1_TYPE1_BITS, SB1_TYPE2_BITS, SB1_INTERLEAVE_A,
result.payload, result.crc, result.cost,
Descrambler::SCRAMB_INIT_BSCH);
if (!ok) return result;
result.type = Result::Type::Sync;
result.is_ok = true;
parse_sync_pdu(result);
uint16_t current_mcc = result.mcc;
uint16_t current_mnc = result.mnc;
uint8_t current_bcc = result.bcc;
uint8_t current_enc = result.encryption;
// 2. SCH/H
uint16_t h_crc = 0;
int h_cost = 0;
ok = decode_block(
message.payload, TMO_BLK2_OFFSET_BITS, BLK2_LEN_BITS,
SB2_TYPE1_BITS, SB2_TYPE2_BITS, SB2_INTERLEAVE_A,
result.payload, h_crc, h_cost,
Descrambler::SCRAMB_INIT_BSCH);
if (ok) {
result.type = Result::Type::SyncFull;
result.crc = h_crc;
result.cost = h_cost;
result.mcc = current_mcc;
result.mnc = current_mnc;
result.bcc = current_bcc;
result.encryption = current_enc;
parse_mac_pdu(result);
} else {
result.encryption = current_enc;
}
return result;
}
uint32_t TetraChannelDecoder::read_bits(const uint8_t* bytes, size_t bit, size_t count) const {
uint32_t value = 0;
for (size_t i = 0; i < count; i++) {
value <<= 1;
value |= (bytes[(bit + i) >> 3] >> (7 - ((bit + i) & 7))) & 1;
}
return value;
}
void TetraChannelDecoder::parse_sync_pdu(Result& result) {
// Sys 4 bit, then BCC
result.bcc = read_bits(result.payload.data(), 4, 6);
result.timeslot = read_bits(result.payload.data(), 10, 2);
result.frame_number = read_bits(result.payload.data(), 12, 5);
result.encryption = read_bits(result.payload.data(), 30, 1);
result.mcc = read_bits(result.payload.data(), 31, 10);
result.mnc = read_bits(result.payload.data(), 41, 14);
last_mcc = result.mcc;
last_mnc = result.mnc;
last_bcc = result.bcc;
network_synced = true;
}
void TetraChannelDecoder::parse_mac_pdu(Result& result) {
result.pdu_type = read_bits(result.payload.data(), 0, 2);
if (result.pdu_type == 0) { // MAC-RESOURCE
result.encryption = read_bits(result.payload.data(), 4, 2);
uint8_t length_ind = read_bits(result.payload.data(), 7, 6);
if (result.encryption == 0 && length_ind > 0 && length_ind < 62) {
size_t offset = 13;
uint8_t addr_type = read_bits(result.payload.data(), offset, 3);
offset += 3;
if (addr_type == 1) {
result.calling_ssi = read_bits(result.payload.data(), offset, 24);
offset += 24;
} else if (addr_type == 2) {
offset += 10;
} else if (addr_type == 3 || addr_type == 4) {
result.calling_ssi = read_bits(result.payload.data(), offset, 24);
offset += 24;
} else if (addr_type == 5) {
offset += 34;
} else if (addr_type == 6) {
offset += 30;
} else if (addr_type == 7) {
offset += 34;
}
// skip optional fields
if (read_bits(result.payload.data(), offset++, 1)) offset += 4; // Power control
if (read_bits(result.payload.data(), offset++, 1)) offset += 8; // Slot grant
uint8_t chan_alloc = read_bits(result.payload.data(), offset++, 1);
if (chan_alloc == 0) {
uint8_t llc_type = read_bits(result.payload.data(), offset, 4);
offset += 4;
// LLC header skipped
if (llc_type == 0 || llc_type == 1)
offset += 2;
else if (llc_type == 2 || llc_type == 3 || llc_type == 6 || llc_type == 7)
offset += 1;
uint8_t mle_type = read_bits(result.payload.data(), offset, 3);
offset += 3;
// 1 = CMCE
if (mle_type == 1) {
result.cmce_type = read_bits(result.payload.data(), offset, 5);
offset += 5;
result.call_id = read_bits(result.payload.data(), offset, 14);
}
}
}
} else if (result.pdu_type == 2) { // SYSINFO/BCAST
uint8_t bcast_type = read_bits(result.payload.data(), 2, 2);
if (bcast_type == 0) {
result.la = read_bits(result.payload.data(), 82, 14);
result.encryption = read_bits(result.payload.data(), 122, 1);
}
}
}
TetraRxView::TetraRxView(NavigationView& nav)
: nav_{nav} {
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
add_children({&rssi, &channel, &field_rf_amp, &field_lna, &field_vga,
&field_frequency, &text_mcc, &text_la, &text_pdu,
&text_mnc, &text_ts, &text_fn, &text_bcc, &text_enc, &text_debug, &console});
field_frequency.set_step(25000);
receiver_model.set_modulation(ReceiverModel::Mode::NarrowbandFMAudio);
receiver_model.set_sampling_rate(3072000);
receiver_model.set_baseband_bandwidth(1750000);
receiver_model.set_squelch_level(0);
receiver_model.enable();
}
void TetraRxView::focus() {
field_frequency.focus();
}
void TetraRxView::on_data_tetra(const TetraBurstMessage& message) {
packet_count++;
auto result = decoder.decode_burst(message);
if (result.type == TetraChannelDecoder::Result::Type::Sync ||
result.type == TetraChannelDecoder::Result::Type::SyncFull) {
valid_count++;
text_mcc.set("MCC: " + to_string_dec_uint(result.mcc));
text_mnc.set("MNC: " + to_string_dec_uint(result.mnc));
text_bcc.set("BCC: " + to_string_dec_uint(result.bcc));
text_ts.set("TS: " + to_string_dec_uint(result.timeslot));
text_fn.set("FN: " + to_string_dec_uint(result.frame_number));
std::string enc_str = (result.encryption != 0) ? "Encrypted" : "Clear";
text_enc.set("ENC: " + enc_str);
if (result.type == TetraChannelDecoder::Result::Type::SyncFull) {
h_valid_count++;
if (result.la != 0xffff) text_la.set("LA: " + to_string_dec_uint(result.la));
text_pdu.set("PDU: " + decoder.get_pdu_name(result.pdu_type));
}
}
text_debug.set(
"Syn: " + to_string_dec_uint(packet_count) +
", V: " + to_string_dec_uint(valid_count) +
", H: " + to_string_dec_uint(h_valid_count) +
", E:" + to_string_dec_uint(result.sync_errors));
}
void TetraRxView::on_data_dnb(const TetraDnbMessage& message) {
dnb_count++;
auto result = decoder.decode_dnb(message);
if (result.is_ok && result.type == TetraChannelDecoder::Result::Type::Dnb) {
if (result.la != 0xffff) text_la.set("LA: " + to_string_dec_uint(result.la));
text_pdu.set("PDU: " + decoder.get_pdu_name(result.pdu_type));
std::string enc_str = (result.encryption != 0) ? "Encrypted" : "Clear";
text_enc.set("ENC: " + enc_str);
// if (rate_limiter++ < 2) console.writeln("DNB: " + decoder.get_pdu_name(result.pdu_type));
if (result.cmce_type != 0xff) {
std::string msg = "";
if (result.cmce_type == 7)
msg = "SETUP";
else if (result.cmce_type == 1)
msg = "CALL PROCEEDING";
else if (result.cmce_type == 0)
msg = "ALERTING";
else if (result.cmce_type == 2)
msg = "CONNECT";
else if (result.cmce_type == 3)
msg = "CONNECT ACK";
else if (result.cmce_type == 11)
msg = "TX GRANTED";
else if (result.cmce_type == 9)
msg = "TX CEASED";
else if (result.cmce_type == 13)
msg = "TX WAIT";
else if (result.cmce_type == 10)
msg = "TX CONTINUE";
else if (result.cmce_type == 12)
msg = "TX INTERRUPT";
else if (result.cmce_type == 4)
msg = "DISCONNECT";
else if (result.cmce_type == 6)
msg = "RELEASE";
else if (result.cmce_type == 5)
msg = "INFO";
else if (result.cmce_type == 8)
msg = "STATUS";
else if (result.cmce_type == 14)
msg = "SDS DATA";
else if (result.cmce_type == 15)
msg = "FACILITY";
else if (result.cmce_type == 16)
msg = "CALL RESTORE";
else if (result.cmce_type == 31)
msg = "NOT SUPPORTED";
else
msg = "CMCE:" + to_string_dec_uint(result.cmce_type);
console.writeln(msg);
if (result.call_id != 0xffff) {
console.writeln("ID:" + to_string_dec_uint(result.call_id) + " | SSI:" + to_string_dec_uint(result.calling_ssi));
}
}
}
}
void TetraRxView::on_timer() {
rate_limiter = 0;
}
TetraRxView::~TetraRxView() {
receiver_model.disable();
baseband::shutdown();
}
} // namespace ui::external_app::tetra_rx
+192
View File
@@ -0,0 +1,192 @@
#ifndef __UI_TETRA_RX_H__
#define __UI_TETRA_RX_H__
#include "ui.hpp"
#include "ui_language.hpp"
#include "ui_navigation.hpp"
#include "ui_receiver.hpp"
#include "ui_freq_field.hpp"
#include "app_settings.hpp"
#include "radio_state.hpp"
#include "log_file.hpp"
#include "utility.hpp"
#include "message.hpp"
#include <array>
#include <string>
using namespace ui;
namespace ui::external_app::tetra_rx {
class TetraChannelDecoder {
public:
struct Result {
enum class Type { None,
Sync,
SyncFull,
Dnb };
Type type{Type::None};
bool is_ok{false};
uint8_t sync_errors{0};
bool inverted{false};
uint16_t crc{0};
int cost{0};
uint8_t timeslot{0xff};
uint8_t frame_number{0xff};
uint16_t mcc{0xffff};
uint16_t mnc{0xffff};
uint8_t bcc{0xff};
uint8_t pdu_type{0xff};
uint16_t la{0xffff};
uint8_t encryption{0xff};
uint8_t cmce_type{0xff};
uint16_t call_id{0xffff};
uint32_t calling_ssi{0};
std::array<uint8_t, 34> payload{};
};
std::string get_pdu_name(uint8_t pdu_type) {
switch (pdu_type) {
case 0:
return "MAC-RESOURCE";
case 1:
return "MAC-FRAG";
case 2:
return "SYSINFO/BCAST";
case 3:
return "MAC-U-SIGNAL";
default:
return "UNK_" + std::to_string(pdu_type);
}
}
Result decode_burst(const TetraBurstMessage& message);
Result decode_dnb(const TetraDnbMessage& message);
private:
static constexpr size_t BLK1_OFFSET_BITS = 94;
static constexpr size_t BLK1_LEN_BITS = 120;
static constexpr size_t TMO_BLK2_OFFSET_BITS = 282;
static constexpr size_t BLK2_LEN_BITS = 216;
static constexpr size_t SB1_TYPE1_BITS = 60;
static constexpr size_t SB1_TYPE2_BITS = 80;
static constexpr size_t SB1_INTERLEAVE_A = 11;
static constexpr size_t SB2_TYPE1_BITS = 124;
static constexpr size_t SB2_TYPE2_BITS = 144;
static constexpr size_t SB2_INTERLEAVE_A = 101;
static constexpr size_t SCH_F_LEN_BITS = 432;
static constexpr size_t SCH_F_TYPE1_BITS = 268;
static constexpr size_t SCH_F_TYPE2_BITS = 288;
static constexpr size_t SCH_F_INTERLEAVE_A = 103;
uint16_t last_mcc{0};
uint16_t last_mnc{0};
uint8_t last_bcc{0};
bool network_synced{false};
// Viterbi buffer
uint8_t trace_buffer[9216];
template <size_t OutBytes>
bool decode_block(const std::array<uint8_t, 63>& burst,
size_t offset,
size_t len_t5,
size_t type1_bits,
size_t type2_bits,
size_t interleave_a,
std::array<uint8_t, OutBytes>& out,
uint16_t& crc,
int& cost,
uint32_t scramb_init);
void parse_sync_pdu(Result& result);
void parse_mac_pdu(Result& result);
uint32_t read_bits(const uint8_t* bytes, size_t bit, size_t count) const;
template <size_t N>
uint8_t get_bit(const std::array<uint8_t, N>& arr, size_t bit_idx) const {
return (arr[bit_idx / 8] >> (7 - (bit_idx % 8))) & 0x01;
}
template <size_t N>
void set_bit(std::array<uint8_t, N>& arr, size_t bit_idx, uint8_t val) const {
if (val)
arr[bit_idx / 8] |= (1 << (7 - (bit_idx % 8)));
else
arr[bit_idx / 8] &= ~(1 << (7 - (bit_idx % 8)));
}
};
class TetraRxView : public View {
public:
TetraRxView(NavigationView& nav);
~TetraRxView();
void focus() override;
std::string title() const override { return "Tetra RX"; };
private:
NavigationView& nav_;
RxRadioState radio_state_{};
app_settings::SettingsManager settings_{"rx_tetra", app_settings::Mode::RX};
uint32_t packet_count{0};
uint32_t valid_count{0};
uint32_t h_valid_count{0};
uint32_t dnb_count{0};
RxFrequencyField field_frequency{{UI_POS_X(0), UI_POS_Y(0)}, nav_};
RFAmpField field_rf_amp{{UI_POS_X(13), UI_POS_Y(0)}};
LNAGainField field_lna{{UI_POS_X(15), UI_POS_Y(0)}};
VGAGainField field_vga{{UI_POS_X(18), UI_POS_Y(0)}};
RSSI rssi{{UI_POS_X_RIGHT(9), UI_POS_Y(0), UI_POS_WIDTH(9), 4}};
Channel channel{{UI_POS_X_RIGHT(9), UI_POS_Y(0) + 5, UI_POS_WIDTH(9), 4}};
Text text_mcc{{UI_POS_X(0), UI_POS_Y(1), UI_POS_WIDTH(15), UI_POS_HEIGHT(1)}, "MCC: ---"};
Text text_mnc{{UI_POS_X(15), UI_POS_Y(1), UI_POS_WIDTH(15), UI_POS_HEIGHT(1)}, "MNC: ---"};
Text text_ts{{UI_POS_X(0), UI_POS_Y(2), UI_POS_WIDTH(15), UI_POS_HEIGHT(1)}, "TS: -"}; // time slot
Text text_fn{{UI_POS_X(15), UI_POS_Y(2), UI_POS_WIDTH(15), UI_POS_HEIGHT(1)}, "FN: -"}; // frame number
Text text_bcc{{UI_POS_X(0), UI_POS_Y(3), UI_POS_WIDTH(15), UI_POS_HEIGHT(1)}, "BCC: -"}; // Base Station Color Code
Text text_enc{{UI_POS_X(15), UI_POS_Y(3), UI_POS_WIDTH(15), UI_POS_HEIGHT(1)}, "ENC: -"}; // encoded or not
Text text_la{{UI_POS_X(0), UI_POS_Y(4), UI_POS_WIDTH(15), UI_POS_HEIGHT(1)}, "LA: ----"}; // Location Area
Text text_pdu{{UI_POS_X(15), UI_POS_Y(4), UI_POS_WIDTH(15), UI_POS_HEIGHT(1)}, "PDU: ----"}; // pdu content type
Text text_debug{{UI_POS_X(0), UI_POS_Y(5), UI_POS_MAXWIDTH, UI_POS_HEIGHT(1)}, "Syn: 0, V: 0, H: 0, E:0"}; // sync, valid, h_valid, errors corrected in top part
Console console{{UI_POS_X(0), UI_POS_Y(6) + 8, UI_POS_MAXWIDTH, screen_height - (UI_POS_Y(7) + 8)}};
TetraChannelDecoder decoder{};
void on_data_tetra(const TetraBurstMessage&);
void on_data_dnb(const TetraDnbMessage&);
uint8_t rate_limiter{0}; // limiter, to keep the device responsive
void on_timer();
MessageHandlerRegistration message_handler_frame_sync{
Message::ID::DisplayFrameSync,
[this](const Message* const) {
this->on_timer();
}};
MessageHandlerRegistration message_handler_packet{
Message::ID::TetraBsch,
[this](Message* const p) {
this->on_data_tetra(*static_cast<const TetraBurstMessage*>(p));
}};
MessageHandlerRegistration message_handler_dnb{
Message::ID::TetraDnb,
[this](Message* const p) {
this->on_data_dnb(*static_cast<const TetraDnbMessage*>(p));
}};
};
} // namespace ui::external_app::tetra_rx
#endif
+1 -1
View File
@@ -1,5 +1,5 @@
/*
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
* copyleft 2024 zxkmm
*
* This file is part of PortaPack.
*
+1 -1
View File
@@ -1,5 +1,5 @@
/*
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
* copyleft 2024 zxkmm
*
* This file is part of PortaPack.
*
+85
View File
@@ -0,0 +1,85 @@
/*
* Copyright (C) 2026 PortaPack Mayhem
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#include "ui.hpp"
#include "ui_vor_rx.hpp"
#include "ui_navigation.hpp"
#include "external_app.hpp"
namespace ui::external_app::vor_rx {
void initialize_app(ui::NavigationView& nav) {
nav.push<VorRxView>();
}
} // namespace ui::external_app::vor_rx
extern "C" {
__attribute__((section(".external_app.app_vor_rx.application_information"), used)) application_information_t _application_information_vor_rx = {
/*.memory_location = */ (uint8_t*)0x00000000,
/*.externalAppEntry = */ ui::external_app::vor_rx::initialize_app,
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
/*.app_name = */ "VOR RX",
/*.bitmap_data = */ {
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x80,
0x01,
0xC0,
0x03,
0xE0,
0x07,
0xF0,
0x0F,
0xF8,
0x1F,
0xF8,
0x1F,
0xF0,
0x0F,
0xE0,
0x07,
0xC0,
0x03,
0x80,
0x01,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
},
/*.icon_color = */ ui::Color::orange().v,
/*.menu_location = */ app_location_t::RX,
/*.desired_menu_position = */ -1,
/*.m4_app_tag = portapack::spi_flash::image_tag_vor_rx */ {'P', 'V', 'R', 'X'},
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
};
}
+341
View File
@@ -0,0 +1,341 @@
/*
* Copyright (C) 2026 PortaPack Mayhem
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#include "ui_vor_rx.hpp"
#include "audio.hpp"
#include "string_format.hpp"
#include <algorithm>
#include <cstdint>
using namespace portapack;
using namespace ui;
namespace ui::external_app::vor_rx {
void VorLogger::write_header() {
log_file.write_raw("Time;Course;Radial;Deviation;Quality;Flag;");
}
void VorLogger::log_status(const VorRxStatusDataMessage& message, uint16_t course_deg) {
const int16_t deviation = static_cast<int16_t>((static_cast<int32_t>(message.radial_deg) - static_cast<int32_t>(course_deg) + 540) % 360 - 180);
std::string row = ";";
row += to_string_dec_uint(course_deg);
row += ";" + to_string_dec_uint(message.radial_deg);
row += ";" + std::to_string(deviation);
row += ";" + to_string_dec_uint(message.quality);
row += ";" + std::string(message.valid ? (message.to_from ? "TO" : "FROM") : "--");
log_file.write_entry(rtc_time::now(), row);
}
VorCdiIndicator::VorCdiIndicator(Point position)
: Widget{{position, {screen_width, 32}}} {
}
void VorCdiIndicator::set_course(uint16_t course_deg) {
if (course_deg_ != course_deg) {
course_deg_ = course_deg;
set_dirty();
}
}
void VorCdiIndicator::set_radial(uint16_t radial_deg) {
if (radial_deg_ != radial_deg) {
radial_deg_ = radial_deg;
set_dirty();
}
}
void VorCdiIndicator::set_valid(bool valid) {
if (valid_ != valid) {
valid_ = valid;
set_dirty();
}
}
int32_t VorCdiIndicator::normalize_signed_degrees(int32_t degrees) {
while (degrees <= -180) {
degrees += 360;
}
while (degrees > 180) {
degrees -= 360;
}
return degrees;
}
void VorCdiIndicator::paint(Painter& painter) {
const auto r = screen_rect();
const auto theme = Theme::getInstance();
const auto line_color = valid_ ? theme->fg_light->foreground : theme->bg_medium->foreground;
const auto needle_color = valid_ ? theme->fg_red->foreground : theme->bg_medium->foreground;
// Clear background so old needle positions don't linger.
painter.fill_rectangle(r, theme->bg_darkest->background);
const auto center_x = static_cast<Coord>(r.left() + r.width() / 2);
const auto center_y = static_cast<Coord>(r.top() + r.height() / 2);
// Horizontal scale line.
painter.draw_hline({static_cast<Coord>(r.left() + 8), center_y}, r.width() - 16, line_color);
// Evenly spaced tick marks, longer at center.
constexpr int32_t tick_spacing = 40;
for (int32_t tick = -2; tick <= 2; ++tick) {
const auto x = static_cast<Coord>(center_x + tick * tick_spacing);
const auto len = (tick == 0) ? 14 : 8;
painter.draw_vline({x, static_cast<Coord>(center_y - len / 2)}, len, line_color);
}
// Deviation needle, clamped to +/-10 degrees full-scale.
const int32_t deviation = normalize_signed_degrees(static_cast<int32_t>(radial_deg_) - static_cast<int32_t>(course_deg_));
const int32_t scaled = std::max<int32_t>(-10, std::min<int32_t>(10, deviation));
// +/-10 deg full-scale maps to +/-2 ticks (2 * tick_spacing); the division
// is exact for the clamped range so no rounding is needed.
const int16_t needle_offset = static_cast<int16_t>((scaled * (2 * tick_spacing)) / 10);
const auto needle_x = static_cast<Coord>(center_x + needle_offset);
painter.draw_vline({needle_x, static_cast<Coord>(r.top() + 4)}, r.height() - 8, needle_color);
}
VorRxView::VorRxView(NavigationView& nav)
: nav_{nav} {
add_children({&field_rf_amp,
&field_lna,
&field_vga,
&field_volume,
&field_frequency,
&rssi,
&channel,
&audio,
&labels,
&text_status,
&text_band,
&text_next,
&field_course,
&text_course_unit,
&field_calibration,
&text_calib_unit,
&text_radial,
&text_flag,
&text_cdi_title,
&cdi_indicator,
&check_log,
&button_start_stop});
field_frequency.set_step(8333);
if (field_frequency.value() == 0) {
field_frequency.set_value(110'000'000);
}
field_course.set_value(0);
field_course.on_change = [this](int32_t) {
update_cdi();
};
field_calibration.set_value(0);
field_calibration.on_change = [this](int32_t) {
refresh_radial();
};
logger = std::make_unique<VorLogger>();
check_log.set_value(logging_);
check_log.on_select = [this](Checkbox&, bool v) {
logging_ = v;
update_logging();
};
update_logging();
button_start_stop.on_select = [this](Button&) {
if (running_) {
stop_receiver();
} else {
start_receiver();
}
};
start_receiver();
}
VorRxView::~VorRxView() {
stop_receiver();
}
void VorRxView::focus() {
field_frequency.focus();
}
void VorRxView::start_receiver() {
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
baseband::set_vor_config(true);
radial_filter_valid_ = false;
flag_state_ = 0;
receiver_model.set_hidden_offset(0);
receiver_model.set_modulation(ReceiverModel::Mode::AMAudio);
receiver_model.set_frequency_step(8333);
receiver_model.set_sampling_rate(3072000);
receiver_model.set_baseband_bandwidth(1750000);
receiver_model.enable();
// enable() applies the AM configuration, which forces the audio codec to
// 12 kHz. The VOR baseband keeps its channel at 48 kHz (so the 9960 Hz
// subcarrier stays representable), so re-assert 48 kHz audio afterwards.
audio::set_rate(audio::Rate::Hz_48000);
audio::output::start();
running_ = true;
update_status();
}
void VorRxView::stop_receiver() {
if (!running_) {
return;
}
running_ = false;
baseband::set_vor_config(false);
receiver_model.disable();
baseband::shutdown();
audio::output::stop();
update_status();
}
void VorRxView::update_status() {
text_status.set(running_ ? "Running" : "Idle");
button_start_stop.set_text(running_ ? "Stop" : "Start");
}
void VorRxView::update_logging() {
if (logger && logging_) {
logger->append(logs_dir / (std::string("VOR_") + to_string_timestamp(rtc_time::now()) + ".CSV"));
logger->write_header();
}
}
void VorRxView::on_vor_status(const VorRxStatusDataMessage& message) {
if (!running_) {
return;
}
last_radial_deg_ = message.radial_deg;
last_valid_ = message.valid;
have_status_ = true;
if (message.valid) {
last_radial_deg_ = smooth_radial(message.radial_deg);
} else {
// Drop the filter history so a fresh lock doesn't drag from stale data.
radial_filter_valid_ = false;
}
text_next.set(message.valid ? "Locked" : "Searching");
refresh_radial();
if (logger && logging_) {
VorRxStatusDataMessage calibrated = message;
calibrated.radial_deg = calibrated_radial(last_radial_deg_);
calibrated.to_from = (flag_state_ == 2);
logger->log_status(calibrated, field_course.value());
}
}
uint16_t VorRxView::smooth_radial(uint16_t radial_deg) {
// Wrapped exponential moving average. At ~10 updates/s blending 20% of each
// new reading (alpha = 0.8) gives a ~0.5 s time constant while cutting the
// jitter by ~3x. Stepping along the shortest arc in 1/64 deg fixed point
// handles the 0/360 deg wrap without any trig (which would otherwise pull
// newlib's float sin/cos/atan2 argument-reduction code into flash ROM).
constexpr int32_t scale = 64;
constexpr int32_t full_circle = 360 * scale;
const int32_t target = static_cast<int32_t>(radial_deg) * scale;
if (!radial_filter_valid_) {
radial_smoothed_fp_ = target;
radial_filter_valid_ = true;
} else {
int32_t diff = (target - radial_smoothed_fp_) % full_circle;
if (diff < -full_circle / 2) {
diff += full_circle;
} else if (diff > full_circle / 2) {
diff -= full_circle;
}
// 20% step toward the new reading (1 - alpha).
radial_smoothed_fp_ += diff / 5;
radial_smoothed_fp_ %= full_circle;
if (radial_smoothed_fp_ < 0) {
radial_smoothed_fp_ += full_circle;
}
}
return static_cast<uint16_t>((radial_smoothed_fp_ + scale / 2) / scale) % 360;
}
uint16_t VorRxView::calibrated_radial(uint16_t radial_deg) const {
int32_t value = (static_cast<int32_t>(radial_deg) + field_calibration.value()) % 360;
if (value < 0) {
value += 360;
}
return static_cast<uint16_t>(value);
}
void VorRxView::refresh_radial() {
if (!have_status_) {
return;
}
const uint16_t radial = calibrated_radial(last_radial_deg_);
text_radial.set(to_string_dec_uint(radial, 3) + " deg");
text_flag.set(to_from_label(radial));
cdi_indicator.set_radial(radial);
cdi_indicator.set_valid(last_valid_);
}
const char* VorRxView::to_from_label(uint16_t radial_deg) {
if (!last_valid_) {
flag_state_ = 0;
return "--";
}
// TO/FROM is set by the selected OBS course relative to the received radial,
// not by the radial alone. Take the difference wrapped to [-180, 180]: the
// switch is at the 90 deg abeam point. |diff| < 90 means the selected course
// leads away from the station (FROM); |diff| > 90 means toward it (TO).
int32_t diff = (static_cast<int32_t>(radial_deg) - field_course.value() + 540) % 360 - 180;
const int32_t adiff = (diff < 0) ? -diff : diff;
// Hysteresis: hold the current flag within a +/-5 deg dead zone around the
// 90 deg boundary so noise near abeam doesn't rapidly toggle TO/FROM.
if (adiff < 85) {
flag_state_ = 1; // FROM
} else if (adiff > 95) {
flag_state_ = 2; // TO
}
if (flag_state_ == 1) return "FROM";
if (flag_state_ == 2) return "TO";
return "--"; // abeam / ambiguous
}
void VorRxView::update_cdi() {
cdi_indicator.set_course(field_course.value());
}
} // namespace ui::external_app::vor_rx
+200
View File
@@ -0,0 +1,200 @@
/*
* Copyright (C) 2026 PortaPack Mayhem
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#ifndef __UI_VOR_RX_H__
#define __UI_VOR_RX_H__
#include "audio.hpp"
#include "baseband_api.hpp"
#include "file_path.hpp"
#include "log_file.hpp"
#include "rtc_time.hpp"
#include "portapack.hpp"
#include "ui.hpp"
#include "ui_audio.hpp"
#include "ui_channel.hpp"
#include "ui_freq_field.hpp"
#include "ui_navigation.hpp"
#include "ui_receiver.hpp"
#include "ui_rssi.hpp"
namespace ui::external_app::vor_rx {
class VorLogger {
public:
Optional<File::Error> append(const std::filesystem::path& filename) {
return log_file.append(filename);
}
void write_header();
void log_status(const VorRxStatusDataMessage& message, uint16_t course_deg);
private:
LogFile log_file{};
};
class VorCdiIndicator : public Widget {
public:
VorCdiIndicator(Point position);
void set_course(uint16_t course_deg);
void set_radial(uint16_t radial_deg);
void set_valid(bool valid);
void paint(Painter& painter) override;
private:
static int32_t normalize_signed_degrees(int32_t degrees);
uint16_t course_deg_{0};
uint16_t radial_deg_{0};
bool valid_{false};
};
class VorRxView : public View {
public:
VorRxView(NavigationView& nav);
~VorRxView();
void focus() override;
std::string title() const override { return "VOR RX"; }
private:
void start_receiver();
void stop_receiver();
void update_status();
void on_vor_status(const VorRxStatusDataMessage& message);
void update_cdi();
void refresh_radial();
uint16_t calibrated_radial(uint16_t radial_deg) const;
uint16_t smooth_radial(uint16_t radial_deg);
const char* to_from_label(uint16_t radial_deg);
void update_logging();
NavigationView& nav_;
bool running_{false};
bool logging_{false};
bool have_status_{false};
uint16_t last_radial_deg_{0};
bool last_valid_{false};
uint8_t flag_state_{0}; // TO/FROM hysteresis: 0=unknown, 1=FROM, 2=TO
// Circular exponential moving average of the radial. Each 100 ms estimate
// is noisy (~10 deg std even when locked), so blend it along the shortest
// arc in 1/64 deg fixed point to average correctly across the 0/360 deg
// wrap without trig.
bool radial_filter_valid_{false};
int32_t radial_smoothed_fp_{0};
RxFrequencyField field_frequency{
{UI_POS_X(0), UI_POS_Y(0)},
nav_};
RFAmpField field_rf_amp{
{UI_POS_X(13), UI_POS_Y(0)}};
LNAGainField field_lna{
{UI_POS_X(15), UI_POS_Y(0)}};
VGAGainField field_vga{
{UI_POS_X(18), UI_POS_Y(0)}};
AudioVolumeField field_volume{
{screen_width - 2 * 8, UI_POS_Y(0)}};
RSSI rssi{
{UI_POS_X(21), 0, UI_POS_WIDTH_REMAINING(21) - UI_POS_WIDTH(2), 4}};
Channel channel{
{UI_POS_X(21), 5, UI_POS_WIDTH_REMAINING(21) - UI_POS_WIDTH(2), 4}};
Audio audio{
{UI_POS_X(21), 10, UI_POS_WIDTH_REMAINING(21) - UI_POS_WIDTH(2), 4}};
NumberField field_course{
{UI_POS_X(14), UI_POS_Y(4)},
3,
{0, 359},
1,
'0'};
NumberField field_calibration{
{UI_POS_X(14), UI_POS_Y(7)},
4,
{-359, 359},
1,
' '};
Labels labels{
{{UI_POS_X(0), UI_POS_Y(1)}, "Status:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(2)}, "VOR Band:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(3)}, "Decoder:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(4)}, "Course (OBS):", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(5)}, "Rec. Radial:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(6)}, "Flag:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(7)}, "Calibration:", Theme::getInstance()->fg_light->foreground}};
Text text_status{
{UI_POS_X(14), UI_POS_Y(1), UI_POS_WIDTH_REMAINING(14), UI_POS_HEIGHT(1)},
"Idle"};
Text text_band{
{UI_POS_X(14), UI_POS_Y(2), UI_POS_WIDTH_REMAINING(14), UI_POS_HEIGHT(1)},
"108.00-117.95MHz"};
Text text_next{
{UI_POS_X(14), UI_POS_Y(3), UI_POS_WIDTH_REMAINING(14), UI_POS_HEIGHT(1)},
"Pending"};
Text text_course_unit{
{UI_POS_X(18), UI_POS_Y(4), UI_POS_WIDTH_REMAINING(18), UI_POS_HEIGHT(1)},
"deg"};
Text text_radial{
{UI_POS_X(14), UI_POS_Y(5), UI_POS_WIDTH_REMAINING(14), UI_POS_HEIGHT(1)},
"--"};
Text text_flag{
{UI_POS_X(14), UI_POS_Y(6), UI_POS_WIDTH_REMAINING(14), UI_POS_HEIGHT(1)},
"--"};
Text text_calib_unit{
{UI_POS_X(19), UI_POS_Y(7), UI_POS_WIDTH_REMAINING(19), UI_POS_HEIGHT(1)},
"deg"};
Text text_cdi_title{
{UI_POS_X(0), UI_POS_Y(9), UI_POS_WIDTH_REMAINING(0), UI_POS_HEIGHT(1)},
"Course Deviation Indicator"};
VorCdiIndicator cdi_indicator{
{UI_POS_X(0), UI_POS_Y(10)}};
std::unique_ptr<VorLogger> logger{};
Button button_start_stop{
{UI_POS_X(9), UI_POS_Y(14), UI_POS_WIDTH(12), UI_POS_HEIGHT(2)},
"Start"};
Checkbox check_log{
{UI_POS_X(7), UI_POS_Y(17)},
3,
"LOG to SD Card",
true};
MessageHandlerRegistration message_handler_vor_status{
Message::ID::VorRxStatusData,
[this](const Message* p) {
const auto message = *reinterpret_cast<const VorRxStatusDataMessage*>(p);
on_vor_status(message);
}};
};
} // namespace ui::external_app::vor_rx
#endif // __UI_VOR_RX_H__
+85
View File
@@ -0,0 +1,85 @@
/*
* Copyright (C) 2026 PortaPack Mayhem
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#include "ui.hpp"
#include "ui_vor_tx.hpp"
#include "ui_navigation.hpp"
#include "external_app.hpp"
namespace ui::external_app::vor_tx {
void initialize_app(ui::NavigationView& nav) {
nav.push<VorTxView>();
}
} // namespace ui::external_app::vor_tx
extern "C" {
__attribute__((section(".external_app.app_vor_tx.application_information"), used)) application_information_t _application_information_vor_tx = {
/*.memory_location = */ (uint8_t*)0x00000000,
/*.externalAppEntry = */ ui::external_app::vor_tx::initialize_app,
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
/*.app_name = */ "VOR TX",
/*.bitmap_data = */ {
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x80,
0x01,
0xC0,
0x03,
0xE0,
0x07,
0xF0,
0x0F,
0xF8,
0x1F,
0xF8,
0x1F,
0xF0,
0x0F,
0xE0,
0x07,
0xC0,
0x03,
0x80,
0x01,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
},
/*.icon_color = */ ui::Color::green().v,
/*.menu_location = */ app_location_t::TX,
/*.desired_menu_position = */ -1,
/*.m4_app_tag = portapack::spi_flash::image_tag_vor_tx */ {'P', 'V', 'T', 'X'},
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
};
}
+133
View File
@@ -0,0 +1,133 @@
/*
* Copyright (C) 2026 PortaPack Mayhem
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#include "ui_vor_tx.hpp"
#include "string_format.hpp"
#include "ui_textentry.hpp"
using namespace portapack;
using namespace ui;
namespace ui::external_app::vor_tx {
VorTxView::VorTxView(NavigationView& nav)
: nav_{nav} {
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
add_children({&labels,
&field_radial,
&text_radial_unit,
&check_ident,
&button_ident_text,
&text_status,
&tx_view});
field_radial.set_value(radial_);
field_radial.on_change = [this](int32_t v) {
radial_ = v;
if (transmitting_) {
update_config();
}
};
check_ident.set_value(ident_);
check_ident.on_select = [this](Checkbox&, bool v) {
ident_ = v;
if (transmitting_) {
update_config();
}
};
button_ident_text.set_text(ident_text_);
button_ident_text.on_select = [this](Button&) {
text_prompt(nav_, ident_text_, 7, ENTER_KEYBOARD_MODE_ALPHA, [this](std::string&) {
button_ident_text.set_text(ident_text_);
if (transmitting_) {
update_config();
}
});
};
tx_view.on_edit_frequency = [this, &nav]() {
auto new_view = nav.push<FrequencyKeypadView>(transmitter_model.target_frequency());
new_view->on_changed = [this](rf::Frequency f) {
transmitter_model.set_target_frequency(f);
};
};
tx_view.on_start = [this]() {
if (tx_acknowledged_) {
start_tx();
return;
}
nav_.display_modal(
"TX WARNING",
"VOR is an aviation nav\nband! TX only into a\ndummy load.",
YESNO,
[this](bool choice) {
if (choice) {
tx_acknowledged_ = true;
start_tx();
} else {
tx_view.set_transmitting(false);
}
});
};
tx_view.on_stop = [this]() {
stop_tx();
};
}
VorTxView::~VorTxView() {
transmitter_model.disable();
baseband::shutdown();
}
void VorTxView::focus() {
tx_view.focus();
}
void VorTxView::update_config() {
baseband::set_vor_tx_config(static_cast<uint16_t>(radial_), ident_, ident_text_);
}
void VorTxView::start_tx() {
transmitter_model.set_sampling_rate(1536000);
transmitter_model.set_baseband_bandwidth(1750000);
transmitter_model.enable();
update_config();
transmitting_ = true;
tx_view.set_transmitting(true);
text_status.set("TX radial " + to_string_dec_uint(radial_));
set_dirty();
}
void VorTxView::stop_tx() {
transmitter_model.disable();
transmitting_ = false;
tx_view.set_transmitting(false);
text_status.set("Idle");
set_dirty();
}
} // namespace ui::external_app::vor_tx
+113
View File
@@ -0,0 +1,113 @@
/*
* Copyright (C) 2026 PortaPack Mayhem
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#ifndef __UI_VOR_TX_H__
#define __UI_VOR_TX_H__
#include "app_settings.hpp"
#include "baseband_api.hpp"
#include "portapack.hpp"
#include "radio_state.hpp"
#include "ui.hpp"
#include "ui_navigation.hpp"
#include "ui_receiver.hpp"
#include "ui_transmitter.hpp"
namespace ui::external_app::vor_tx {
class VorTxView : public View {
public:
VorTxView(NavigationView& nav);
~VorTxView();
void focus() override;
std::string title() const override { return "VOR TX"; }
private:
void start_tx();
void stop_tx();
void update_config();
NavigationView& nav_;
bool transmitting_{false};
bool tx_acknowledged_{false};
uint32_t radial_{0};
bool ident_{true};
std::string ident_text_{"VOR"};
TxRadioState radio_state_{
113'500'000 /* frequency */,
1750000 /* bandwidth */,
1536000 /* sampling rate */
};
app_settings::SettingsManager settings_{
"tx_vor",
app_settings::Mode::TX,
{
{"radial"sv, &radial_},
{"ident"sv, &ident_},
{"ident_text"sv, &ident_text_},
}};
Labels labels{
{{UI_POS_X(0), UI_POS_Y(1)}, "Radial:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(5)}, "Ident:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(8)}, "Status:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(10)}, "Lab use: set any freq &", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(11)}, "TX into a dummy load.", Theme::getInstance()->fg_light->foreground}};
NumberField field_radial{
{UI_POS_X(8), UI_POS_Y(1)},
3,
{0, 359},
1,
'0'};
Text text_radial_unit{
{UI_POS_X(12), UI_POS_Y(1), UI_POS_WIDTH(4), UI_POS_HEIGHT(1)},
"deg"};
Checkbox check_ident{
{UI_POS_X(0), UI_POS_Y(3)},
13,
"CW identifier",
true};
Button button_ident_text{
{UI_POS_X(8), UI_POS_Y(5), UI_POS_WIDTH(10), UI_POS_HEIGHT(2)},
"VOR"};
Text text_status{
{UI_POS_X(8), UI_POS_Y(8), UI_POS_WIDTH_REMAINING(8), UI_POS_HEIGHT(1)},
"Idle"};
TransmitterView tx_view{
(int16_t)UI_POS_Y_BOTTOM(4),
8333,
1750000};
};
} // namespace ui::external_app::vor_tx
#endif // __UI_VOR_TX_H__
@@ -28,11 +28,47 @@
#include "ui_fileman.hpp"
#include "file_reader.hpp"
#include "ui_textentry.hpp"
#include "convert.hpp"
#include <algorithm>
using namespace portapack;
namespace ui::external_app::waterfall_designer {
namespace {
/* Parses an "index,R,G,B" profile line.
* NB: std::stoi() must not be used here - the firmware is built with
* -fno-exceptions, so any malformed field in a hand-edited profile makes it
* call std::terminate() instead of throwing. parse_int() is what gradient.cpp
* uses to read the very same file format. */
bool parse_level(std::string_view line, uint8_t& index, uint8_t& r, uint8_t& g, uint8_t& b) {
// Comments are skipped by Gradient::load_file() too; parse_int() ignores
// trailing junk, so "#0,0,0,0" would otherwise read back as a real level.
if (line.empty() || line.front() == '#') return false;
auto cols = split_string(line, ',');
if (cols.size() != 4) return false;
uint8_t* const out[4] = {&index, &r, &g, &b};
for (size_t i = 0; i < 4; i++) {
int32_t value;
if (!parse_int(cols[i], value) || value < 0 || value > 255) return false;
*out[i] = static_cast<uint8_t>(value);
}
return true;
}
bool is_color_level(const std::string& line) {
uint8_t index, r, g, b;
return parse_level(line, index, r, g, b);
}
} // namespace
WaterfallDesignerView::WaterfallDesignerView(NavigationView& nav)
: nav_{nav} {
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
@@ -152,7 +188,11 @@ WaterfallDesignerView::WaterfallDesignerView(
}
WaterfallDesignerView::~WaterfallDesignerView() {
if (!if_apply_setting) restore_current_profile();
if (!if_apply_setting)
restore_current_profile();
else if (profile_backed_up_)
delete_file(waterfalls_dir / u"wtf_des_bk.bk"); // kept the edit, drop the backup
receiver_model.disable();
baseband::shutdown();
}
@@ -176,35 +216,68 @@ void WaterfallDesignerView::on_open_profile() {
auto open_view = nav_.push<FileLoadView>(".txt");
open_view->push_dir(waterfalls_dir);
open_view->on_changed = [this](std::filesystem::path new_file_path) {
on_profile_changed(new_file_path);
// NB: FileLoadView calls this and then pops, same as text_prompt does.
// Defer for the same two reasons - see on_create_new_profile().
pending_profile_path = std::move(new_file_path);
nav_.set_on_pop([this]() {
on_profile_changed(pending_profile_path);
});
};
}
void WaterfallDesignerView::on_profile_changed(std::filesystem::path new_profile_path) {
current_profile_path = new_profile_path;
bool WaterfallDesignerView::read_profile_file(const std::filesystem::path& path) {
File profile_file;
if (profile_file.open(path)) return false;
profile_levels.clear();
File playlist_file;
auto error = playlist_file.open(new_profile_path.string());
if (error) return;
menu_view.clear();
auto reader = FileLineReader(playlist_file);
auto reader = FileLineReader(profile_file);
for (const auto& line : reader) {
// remove empty lines
if (line == "\n" || line == "\r\n" || line == "\r") continue;
profile_levels.push_back(line);
auto entry = line;
// Strip the whole line terminator; the old code only dropped one char,
// so CRLF files kept a stray '\r' in every entry.
while (!entry.empty() && (entry.back() == '\n' || entry.back() == '\r'))
entry.pop_back();
if (entry.empty()) continue;
profile_levels.push_back(std::move(entry));
}
for (auto& line : profile_levels) {
// remove line end \n etc
if (line.length() > 0 && (line[line.length() - 1] == '\n' || line[line.length() - 1] == '\r')) {
line = line.substr(0, line.length() - 1);
}
return true;
}
bool WaterfallDesignerView::write_profile_file(const std::filesystem::path& path) {
File profile_file;
if (profile_file.open(path, false, true)) return false;
// FA_OPEN_ALWAYS leaves the position at end-of-file, so rewind before
// truncating or the existing contents are kept and appended to.
if (profile_file.seek(0).is_error()) return false;
if (profile_file.truncate().is_error()) return false;
for (const auto& entry : profile_levels)
if (profile_file.write_line(entry)) return false;
/* ~File() calls f_close(), which flushes, but its result is unreachable.
* Sync explicitly so a full or yanked card is reported to the caller
* instead of being announced as a successful save. */
return !profile_file.sync();
}
void WaterfallDesignerView::on_profile_changed(const std::filesystem::path& new_profile_path) {
// read_profile_file() only touches profile_levels once the open succeeded,
// so a failure here leaves the previously loaded profile intact.
if (!read_profile_file(new_profile_path)) {
nav_.display_modal("Err", "open err");
return;
}
current_profile_path = new_profile_path;
highlighted_index_ = 0;
button_save.hidden(false);
button_add_level.hidden(false);
button_remove_level.hidden(false);
@@ -219,53 +292,52 @@ void WaterfallDesignerView::refresh_menu_view() {
menu_view.clear();
for (const auto& line : profile_levels) {
if (line.length() == 0 || line[0] == '#') {
uint8_t index, r, g, b;
// index,R,G,B - anything else (comment, header, garbage) is shown greyed.
if (parse_level(line, index, r, g, b)) {
menu_view.add_item({line,
ui::Color(r, g, b),
&bitmap_icon_cwgen,
[this](KeyEvent) {
button_remove_level.focus();
}});
} else {
menu_view.add_item({line,
ui::Color::grey(),
&bitmap_icon_notepad,
[this](KeyEvent) {
button_add_level.focus();
}});
} else {
// index,R,G,B
size_t pos = 0;
size_t next_pos = 0;
// pass index
next_pos = line.find(',', pos);
if (next_pos == std::string::npos) continue;
pos = next_pos + 1;
// r
next_pos = line.find(',', pos);
if (next_pos == std::string::npos) continue;
uint8_t r = static_cast<uint8_t>(std::stoi(line.substr(pos, next_pos - pos)));
pos = next_pos + 1;
// g
next_pos = line.find(',', pos);
if (next_pos == std::string::npos) continue;
uint8_t g = static_cast<uint8_t>(std::stoi(line.substr(pos, next_pos - pos)));
pos = next_pos + 1;
// b
uint8_t b = static_cast<uint8_t>(std::stoi(line.substr(pos)));
ui::Color color = ui::Color(r, g, b);
menu_view.add_item({line,
color,
&bitmap_icon_cwgen,
[this](KeyEvent) {
button_remove_level.focus();
}});
}
}
// menu_view.clear() resets its own highlight to 0 but does not fire
// on_highlight, so highlighted_index_ has to be re-synced by hand or the
// edit/remove buttons act on a different row than the one the user sees.
if (highlighted_index_ >= profile_levels.size())
highlighted_index_ = profile_levels.empty() ? 0 : profile_levels.size() - 1;
if (!profile_levels.empty())
menu_view.set_highlighted(highlighted_index_);
set_dirty();
}
void WaterfallDesignerView::on_apply_current_to_wtf() {
std::filesystem::path system_read_path = "waterfall.txt";
copy_file(current_profile_path, system_read_path);
/* Take the backup lazily, right before the first overwrite of the live
* gradient. Doing it in the constructor meant copy_file() (~1.8kB of stack)
* ran underneath run_external_app(), which holds a 776 byte frame with a
* File of its own - about 3.6kB of the 4kB M0 process stack. It also did SD
* I/O even when the user just opened the app and backed straight out. */
backup_current_profile();
/* NB: this used to copy_file(current_profile_path, "waterfall.txt"), which
* costs ~1.8kB of stack (two FILs + a 512 byte block buffer) out of the 4kB
* M0 process stack, on top of whatever UI callback we are nested under.
* profile_levels is the authoritative copy of the profile anyway, so write
* it straight out through a single File instead. */
if (!write_profile_file(u"waterfall.txt")) return;
waterfall.load_gradient();
@@ -281,71 +353,82 @@ void WaterfallDesignerView::on_save_profile() {
return;
}
File profile_file;
auto error = profile_file.open(current_profile_path.string(), false, false);
if (error) {
nav_.display_modal("Err", "open err");
// NB: not just "open err" any more - this now also covers a failed
// truncate/write/flush, i.e. the file may be partially written.
if (!write_profile_file(current_profile_path)) {
nav_.display_modal("Err", "write failed");
return;
}
// clear file
profile_file.seek(0);
profile_file.truncate();
// write new data
for (const auto& entry : profile_levels) {
profile_file.write_line(entry);
}
nav_.display_modal("Save", "Saved profile\n" + current_profile_path.string());
}
void WaterfallDesignerView::on_add_level() {
if (highlighted_index_ >= profile_levels.size()) return;
if (profile_levels[highlighted_index_].empty()) return;
if (profile_levels[highlighted_index_][0] == '#') return;
if (profile_levels[highlighted_index_].find(',') == std::string::npos) return;
size_t insert_pos = highlighted_index_;
std::string new_entry = "0,128,128,128";
profile_levels.insert(profile_levels.begin() + insert_pos, new_entry);
if (current_profile_path.empty()) return;
/* A freshly created profile has no rows at all, and the old guard
* (highlighted_index_ >= size()) then refused with no feedback - so the
* button looked dead on exactly the file you just made. Seed instead. */
size_t insert_pos = std::min<size_t>(highlighted_index_, profile_levels.size());
// Don't push a level above a comment/header row; drop it after instead.
if (insert_pos < profile_levels.size() && !is_color_level(profile_levels[insert_pos]))
insert_pos++;
profile_levels.insert(profile_levels.begin() + insert_pos, "0,128,128,128");
highlighted_index_ = insert_pos;
refresh_menu_view();
on_edit_color();
}
void WaterfallDesignerView::on_remove_level() {
if (highlighted_index_ >= profile_levels.size()) return;
if (profile_levels[highlighted_index_].empty()) return;
if (profile_levels[highlighted_index_][0] == '#') return;
if (profile_levels[highlighted_index_].find(',') == std::string::npos) return;
if (!is_color_level(profile_levels[highlighted_index_])) return;
profile_levels.erase(profile_levels.begin() + highlighted_index_);
refresh_menu_view();
}
void WaterfallDesignerView::on_edit_color() {
if (highlighted_index_ >= profile_levels.size()) return;
if (profile_levels[highlighted_index_].empty()) return;
if (profile_levels[highlighted_index_][0] == '#') return;
if (profile_levels[highlighted_index_].find(',') == std::string::npos) return;
if (!is_color_level(profile_levels[highlighted_index_])) return;
auto color_picker_view = nav_.push<WaterfallDesignerColorPickerView>(profile_levels[highlighted_index_]);
color_picker_view->on_save = [this](std::string new_color) {
profile_levels[highlighted_index_] = new_color;
// Capture the row by value: highlighted_index_ can move (or the list can
// shrink) while the picker is up, and the old code wrote to whatever
// highlighted_index_ happened to be at save time with no bounds check.
const size_t index = highlighted_index_;
auto color_picker_view = nav_.push<WaterfallDesignerColorPickerView>(profile_levels[index]);
color_picker_view->on_save = [this, index](std::string new_color) {
if (index >= profile_levels.size()) return;
profile_levels[index] = std::move(new_color);
refresh_menu_view();
on_apply_current_to_wtf();
};
}
void WaterfallDesignerView::backup_current_profile() {
std::filesystem::path curren_wtf_path = "waterfall.txt";
std::filesystem::path backup_path = waterfalls_dir / "wtf_des_bk.bk";
copy_file(curren_wtf_path, backup_path);
if (backup_attempted_) return;
backup_attempted_ = true;
std::filesystem::path current_wtf_path = u"waterfall.txt";
std::filesystem::path backup_path = waterfalls_dir / u"wtf_des_bk.bk";
if (!file_exists(current_wtf_path)) return;
profile_backed_up_ = copy_file(current_wtf_path, backup_path).ok();
}
void WaterfallDesignerView::restore_current_profile() {
std::filesystem::path backup_path = waterfalls_dir / "wtf_des_bk.bk";
std::filesystem::path put_back_path = "waterfall.txt";
copy_file(backup_path, put_back_path);
// Only touch waterfall.txt if we actually took a backup on entry; otherwise
// this would silently do nothing and leave the edited gradient installed.
if (!profile_backed_up_) return;
std::filesystem::path backup_path = waterfalls_dir / u"wtf_des_bk.bk";
copy_file(backup_path, u"waterfall.txt");
delete_file(backup_path);
}
@@ -365,16 +448,35 @@ void WaterfallDesignerView::on_create_new_profile() {
buffer += ".txt";
}
File new_file;
auto error = new_file.create(waterfalls_dir / buffer);
if (error) {
nav_.display_modal("Err", "create file err");
auto new_path = waterfalls_dir / buffer;
bool created;
{
// Scoped so the FIL is closed (and its 512 byte sector cache is
// off the stack) before anything re-opens the same file - _FS_LOCK
// is 0, so FatFs will not stop us from double-opening it.
File new_file;
created = !new_file.create(new_path);
}
/* NB: everything below has to run *after* the keyboard view is gone.
* text_prompt()'s caller invokes this handler and then pops, so:
* - pushing a modal from here would pop the modal, not the keyboard;
* - and running on_profile_changed() inline stacks its File plus the
* gradient write under the alphanum/button/dispatch frames, which
* is what blows the 4kB M0 process stack ("Stack Overflow" guru).
* set_on_pop() defers it to just after the pop, at a shallow depth. */
if (!created) {
nav_.set_on_pop([this]() {
nav_.display_modal("Err", "create file err");
});
return;
}
profile_levels.clear();
current_profile_path = waterfalls_dir / buffer;
on_profile_changed(current_profile_path);
pending_profile_path = new_path;
nav_.set_on_pop([this]() {
on_profile_changed(pending_profile_path);
});
});
}
@@ -392,32 +494,7 @@ WaterfallDesignerColorPickerView::WaterfallDesignerColorPickerView(NavigationVie
progressbar.set_max(UINT8_MAX);
size_t pos = 0;
size_t next_pos = 0;
// index
next_pos = color_str.find(',', pos);
if (next_pos != std::string::npos) {
index_ = static_cast<uint8_t>(std::stoi(color_str.substr(pos, next_pos - pos)));
pos = next_pos + 1;
}
// r
next_pos = color_str.find(',', pos);
if (next_pos != std::string::npos) {
red_ = static_cast<uint8_t>(std::stoi(color_str.substr(pos, next_pos - pos)));
pos = next_pos + 1;
}
// g
next_pos = color_str.find(',', pos);
if (next_pos != std::string::npos) {
green_ = static_cast<uint8_t>(std::stoi(color_str.substr(pos, next_pos - pos)));
pos = next_pos + 1;
}
// b
blue_ = static_cast<uint8_t>(std::stoi(color_str.substr(pos)));
parse_level(color_str_, index_, red_, green_, blue_);
field_red.set_value(red_);
field_green.set_value(green_);
@@ -466,19 +543,12 @@ void WaterfallDesignerColorPickerView::update_color_index() {
green_ = static_cast<uint8_t>(field_green.value());
blue_ = static_cast<uint8_t>(field_blue.value());
const Rect preview_rect{screen_width - 48, 1 * 16, 40, 40};
Painter painter_instance_2;
painter_instance_2.fill_rectangle(
{preview_rect.left(), preview_rect.top(), preview_rect.width(), preview_rect.height()},
ui::Color(red_, green_, blue_));
// Repaint through the normal dirty pass instead of driving the display from
// inside an on_change handler; paint() below already draws the swatch.
set_dirty();
}
void WaterfallDesignerColorPickerView::paint(Painter& painter) {
// this is not duplicated code.
// because need to display color when enter,
// but it is too early to call update_color() in the constructor.
const Rect preview_rect{screen_width - 48, 1 * 16, 40, 40};
painter.fill_rectangle(
@@ -487,14 +557,12 @@ void WaterfallDesignerColorPickerView::paint(Painter& painter) {
}
std::string WaterfallDesignerColorPickerView::build_color_str() {
size_t index_pos = color_str_.find(',');
if (index_pos != std::string::npos) {
return color_str_.substr(0, index_pos + 1) +
to_string_dec_uint(red_) + "," +
to_string_dec_uint(green_) + "," +
to_string_dec_uint(blue_);
}
return to_string_dec_uint(index_) + "," + to_string_dec_uint(red_) + "," + to_string_dec_uint(green_) + "," + to_string_dec_uint(blue_);
// Always emit index_ - the old version kept the index substring from the
// input line, so edits made with field_index were silently discarded.
return to_string_dec_uint(index_) + "," +
to_string_dec_uint(red_) + "," +
to_string_dec_uint(green_) + "," +
to_string_dec_uint(blue_);
}
} /* namespace ui::external_app::waterfall_designer */
@@ -217,18 +217,31 @@ class WaterfallDesignerView : public View {
void restore_current_profile();
void on_create_new_profile();
void on_open_profile();
void on_profile_changed(std::filesystem::path new_profile_path);
void on_profile_changed(const std::filesystem::path& new_profile_path);
void on_save_profile();
void on_add_level();
void on_remove_level();
void on_edit_color();
/* Each of these owns the only File in its call chain. Keeping them as
* separate frames matters: a File carries a 512 byte FIL sector cache
* (_FS_TINY is 0) and the M0 process stack is 4kB, so if read_profile_file
* merged into on_profile_changed its 784 byte frame would stay live across
* the copy_file() in on_apply_current_to_wtf() - 3.6kB of 4kB instead of
* 2.8kB. GCC does not inline them at -O2 today; noinline just pins that so
* a heuristic change can't quietly eat the headroom. */
__attribute__((noinline)) bool read_profile_file(const std::filesystem::path& path);
__attribute__((noinline)) bool write_profile_file(const std::filesystem::path& path);
void refresh_menu_view();
void on_apply_current_to_wtf(); // will restore if didn't apple, when distruct
void on_apply_setting(); // apply set
bool if_apply_setting{false};
bool backup_attempted_{false};
bool profile_backed_up_{false};
std::filesystem::path pending_profile_path{}; // set by the New dialog, consumed on_pop
/*NB:
this works as:
each time you change color, it apply as file realtime
+83 -20
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();
}
/*
@@ -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
+7 -17
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;
@@ -154,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);
@@ -207,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));
}
@@ -395,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);
@@ -446,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.setActive(); // 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 -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);
+61 -35
View File
@@ -76,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,
@@ -95,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,
@@ -144,6 +144,10 @@ static rf::Direction cached_direction = rf::Direction::Receive;
static bool cached_rf_amp = false;
static int_fast8_t cached_lna_gain = 0;
static int_fast8_t cached_vga_gain = 0;
/* FPGA quarter-rate shift mode currently programmed, in gateware encoding
* (0b00 none / 0b11 up / 0b01 down). The baseband filter width depends on it,
* so ReceiverModel reads it back through get_quarter_shift(). */
static uint8_t cached_quarter_shift = 0;
#endif
void init() {
@@ -182,18 +186,15 @@ void init() {
fpga_set_mode(FPGA_MODE_RX);
// These FPGA registers control DC_BLOCK, Q-Inv, QUARTER SHIFT, and Decimation.
fpga_debug_register_write(FPGA_REG_CTRL, FPGA_CTRL_DC_BLOCK_EN); // DC_BLOCK=1, QUARTER_SHIFT=0, Q_INVERT=0
fpga_debug_register_write(FPGA_REG_DECIM, 0x00); // RX_DECIM=No Decim
// RX Mode: Register 3 is RX Digital Gain. Start with 0dB (no shift).
fpga_debug_register_write(FPGA_REG_RX_DIGITAL_GAIN, FPGA_RX_DEFAULT_DIGITAL_GAIN);
/* RX Mode: Initialize DC Block parameters to standard Praline values.
* 0x04 Width and 0x08 Adapt Rate are typical for 40MHz stability.
*/
fpga_debug_register_write(FPGA_REG_RX_DC_BLOCK_WIDTH, FPGA_RX_DEFAULT_DC_WIDTH);
fpga_debug_register_write(FPGA_REG_RX_DC_ADAPT_RATE, FPGA_RX_DEFAULT_ADAPT_RATE);
/* Boot register state, matching fpga_init() in hackrf/firmware/common/fpga.c:
* DC block on, no PRBS, no external trigger, no quarter shift, TX NCO off.
* The decimation ratio and the quarter shift are programmed later by
* ClockManager::set_sampling_frequency() and set_tuning_frequency(). */
fpga_debug_register_write(FPGA_REG_CTRL, FPGA_CTRL_DC_BLOCK_EN);
fpga_debug_register_write(FPGA_REG_DECIM, 0x00); // RX_DECIM = no decimation
fpga_debug_register_write(FPGA_REG_RX_PSTEP, 0x00); // quarter shift off
fpga_debug_register_write(FPGA_REG_TX_CONTROL, 0x00);
cached_quarter_shift = 0;
ssp1_arbiter.invalidate();
chThdSleepMilliseconds(10); // Let FPGA registers settle
@@ -224,13 +225,13 @@ void set_direction(const rf::Direction new_direction) {
fpga_debug_register_write(FPGA_REG_TX_PHASE_STEP, 0x00);
} else {
fpga_set_mode(FPGA_MODE_RX);
// RX Mode: Ensure NCO is disabled and reset digital gain
fpga_debug_register_write(FPGA_REG_RX_DIGITAL_GAIN, FPGA_RX_DEFAULT_DIGITAL_GAIN);
/* RX Mode: Initialize DC Block parameters to standard Praline values.
* 0x04 Width and 0x08 Adapt Rate are typical for 40MHz stability.
*/
fpga_debug_register_write(FPGA_REG_RX_DC_BLOCK_WIDTH, FPGA_RX_DEFAULT_DC_WIDTH);
fpga_debug_register_write(FPGA_REG_RX_DC_ADAPT_RATE, FPGA_RX_DEFAULT_ADAPT_RATE);
/* RX Mode: DC block on, TX NCO off. The quarter shift is re-applied by
* set_tuning_frequency(); clear it here so a stale TX/RX transition
* cannot leave a rotation programmed with no matching LO offset. */
fpga_debug_register_write(FPGA_REG_CTRL, FPGA_CTRL_DC_BLOCK_EN);
fpga_debug_register_write(FPGA_REG_TX_CONTROL, 0x00);
fpga_debug_register_write(FPGA_REG_RX_PSTEP, 0x00);
cached_quarter_shift = 0;
}
#endif
@@ -309,7 +310,19 @@ bool set_tuning_frequency(const rf::Frequency frequency) {
final_frequency = final_frequency + portapack::persistent_memory::config_freq_rx_correction();
}
#ifdef PRALINE
/* The PRALINE tuning tables offset the analogue passband by a quarter of
* the ADC sample rate and have the FPGA rotate it back to DC, so the
* planner needs to know the AFE rate. See tuning.cpp. */
const uint32_t afe_rate = portapack::clock_manager.get_sampling_frequency()
<< portapack::clock_manager.get_resampling_n();
const auto tuning_config = tuning::config::create(
final_frequency,
afe_rate,
direction == rf::Direction::Transmit);
#else
const auto tuning_config = tuning::config::create(final_frequency);
#endif
if (tuning_config.is_valid()) {
first_if.disable();
@@ -339,6 +352,22 @@ bool set_tuning_frequency(const rf::Frequency frequency) {
LPC_GPIO->CLR[0] = (1 << 13); // SGPIO12 = 0 (Q normal)
}
/* Program the FPGA's quarter-rate shift to match the offset the tuning
* table just applied to the analogue centre frequency. The gateware
* (hackrf/firmware/fpga/top/standard.py) takes both bits from the top
* of register 0x03 (rx_pstep):
* rx_pstep[6] -> quarter_shift.enable
* rx_pstep[7] -> quarter_shift.up
* which is exactly fpga_set_rx_quarter_shift_mode() in
* hackrf/firmware/common/fpga.c: write (mode & 0b11) << 6.
*
* These two settings MUST be programmed together. Tuning off-centre
* without the rotation puts the signal outside the decimation filter's
* passband and it disappears entirely; rotating without the offset
* moves the wanted signal off DC by the same amount. */
cached_quarter_shift = tuning_config.quarter_shift;
fpga_debug_register_write(FPGA_REG_RX_PSTEP, (cached_quarter_shift & 0b11) << 6);
ssp1_arbiter.invalidate();
#else
baseband_cpld.set_invert(mixer_invert ^ baseband_invert);
@@ -469,6 +498,10 @@ int_fast8_t get_cached_lna_gain() {
int_fast8_t get_cached_vga_gain() {
return cached_vga_gain;
}
uint8_t get_cached_quarter_shift() {
return cached_quarter_shift;
}
#endif
namespace first_if {
@@ -570,18 +603,11 @@ void register_write(const size_t register_number, uint32_t value) {
void init() {
fpga_set_mode(FPGA_MODE_RX);
// These FPGA registers control DC_BLOCK, Q-Inv, QUARTER SHIFT, and Decimation.
fpga_debug_register_write(FPGA_REG_CTRL, FPGA_CTRL_DC_BLOCK_EN); // DC_BLOCK=1, QUARTER_SHIFT=0, Q_INVERT=0
fpga_debug_register_write(FPGA_REG_DECIM, 0x00); // RX_DECIM=No Decim
// RX Mode: Register 3 is RX Digital Gain. Start with 0dB (no shift).
fpga_debug_register_write(FPGA_REG_RX_DIGITAL_GAIN, FPGA_RX_DEFAULT_DIGITAL_GAIN);
/* RX Mode: Initialize DC Block parameters to standard Praline values.
* 0x04 Width and 0x08 Adapt Rate are typical for 40MHz stability.
*/
fpga_debug_register_write(FPGA_REG_RX_DC_BLOCK_WIDTH, FPGA_RX_DEFAULT_DC_WIDTH);
fpga_debug_register_write(FPGA_REG_RX_DC_ADAPT_RATE, FPGA_RX_DEFAULT_ADAPT_RATE);
/* Same boot state as fpga_init() in hackrf/firmware/common/fpga.c. */
fpga_debug_register_write(FPGA_REG_CTRL, FPGA_CTRL_DC_BLOCK_EN);
fpga_debug_register_write(FPGA_REG_DECIM, 0x00); // RX_DECIM = no decimation
fpga_debug_register_write(FPGA_REG_RX_PSTEP, 0x00); // quarter shift off
fpga_debug_register_write(FPGA_REG_TX_CONTROL, 0x00);
ssp1_arbiter.invalidate(); // Force arbiter to reconfigure on next transfer
}
+4
View File
@@ -126,6 +126,10 @@ rf::Direction get_cached_direction();
bool get_cached_rf_amp();
int_fast8_t get_cached_lna_gain();
int_fast8_t get_cached_vga_gain();
/* FPGA RX quarter-rate shift currently programmed, in gateware encoding:
* 0b00 none, 0b11 up, 0b01 down. */
uint8_t get_cached_quarter_shift();
#endif
namespace sgpio {
+46 -31
View File
@@ -88,6 +88,15 @@ void ReceiverModel::set_target_frequency(rf::Frequency f) {
update_tuning_frequency();
}
void ReceiverModel::set_target_frequency_with_hidden_offset(
rf::Frequency f,
rf::Frequency offset) {
persistent_memory::set_target_frequency(f);
settings_.frequency_app_override = f;
hidden_offset = offset;
update_tuning_frequency();
}
uint32_t ReceiverModel::baseband_bandwidth() const {
return settings_.baseband_bandwidth;
}
@@ -244,10 +253,24 @@ void ReceiverModel::enable() {
radio::set_direction(rf::Direction::Receive);
#ifdef PRALINE
/* Anchor the Common Mode Voltage (VCM) to 1.2V.
* This stabilizes the electrical floor of the I/Q signals.
*/
radio::set_rx_buff_vcm(1);
/* MAX2831 RX IQ common-mode voltage (register 15).
*
* 0 = 1.1 V, 1 = 1.2 V, 2 = 1.3 V, 3 = 1.45 V.
*
* The reference firmware leaves this alone: max2831.c's default register
* table has reg 15 = 0x0145 (1.1 V) and the line that would raise it is
* commented out ("maximum rx output common-mode voltage"). Mayhem used to
* force 1.2 V here on the theory that it "stabilises the electrical floor
* of the I/Q signals" -- plausible, but never measured, and it was the
* last remaining RF-path setting where this branch disagreed with the
* configuration that is proven to receive ADS-B on this board.
*
* Set to 0 to match the reference (writing 1.1 V is a no-op against the
* power-on default), or back to 1 to restore the old Mayhem behaviour.
* If reception measurably worsens, put it back to 1 and say so -- neither
* value has been verified on hardware. */
#define PRALINE_RX_IQ_VCM 0
radio::set_rx_buff_vcm(PRALINE_RX_IQ_VCM);
#endif
update_tuning_frequency();
@@ -326,47 +349,39 @@ void ReceiverModel::update_baseband_bandwidth() {
if (enabled_) {
#ifdef PRALINE
/*
* PRALINE LPF bandwidth calculation from GSG hackrf_usb radio.c
* PRALINE LPF bandwidth, ported from auto_bandwidth() in
* hackrf/firmware/common/radio.c:
*
* The LPF should be set to capture the desired signal bandwidth
* while the FPGA decimation filter handles anti-aliasing.
* bb_bandwidth = sample_rate * 3 / 4
* lpf_bandwidth = bb_bandwidth + offset_hz * 2
*
* For most modes: LPF = (output_sample_rate * 3) / 8
* For quarter-shift: add offset for shifted spectrum
* where offset_hz is the quarter-rate shift, i.e. afe_rate / 4 when a
* shift is in use. The doubling is because the wanted signal sits
* offset from the analogue centre, so the analogue filter has to stay
* open out to that offset on the far side too.
*
* Note: MAX2831 minimum LPF is 11.6 MHz, so for narrow sample rates
* the hardware limit applies and FPGA filter does the real work.
* The previous version used /8 in both places and read the shift from
* bits 2-3 of FPGA register 1, which do not exist in the gateware, so
* it always took the no-shift branch. At the ADS-B rate that asked for
* 750 kHz, which is below the MAX2831's 1.75 MHz floor and therefore
* also switched in the external narrowband AA filter
* (MAX2831::set_lpf_rf_bandwidth_rx), squeezing the RX path shut. The
* reference asks for 17.5 MHz at the same rate and the AA filter stays
* out of circuit.
*/
uint32_t sample_rate = sampling_rate();
uint8_t resampling_n = portapack::clock_manager.get_resampling_n();
uint32_t afe_rate = sample_rate << resampling_n;
// Base LPF: enough to capture desired bandwidth
uint32_t lpf_bandwidth = (sample_rate * 3) / 8;
// Check if quarter-shift is enabled (FPGA register 1, bits 2-3)
uint32_t fpga_ctrl = radio::debug::fpga::register_read(1);
uint8_t quarter_shift = (fpga_ctrl >> 2) & 0x03;
uint32_t lpf_bandwidth = (sample_rate * 3) / 4;
const uint8_t quarter_shift = radio::debug::get_cached_quarter_shift();
if (quarter_shift != 0) {
// Quarter-shift moves spectrum by AFE_rate/4, need wider LPF
uint32_t offset = afe_rate / 8;
const uint32_t offset = afe_rate / 4;
lpf_bandwidth += offset * 2;
}
// For best anti-alias performance, also consider AFE Nyquist
// If our calculated LPF is below MAX2831 minimum, it doesn't matter
// But if we can set LPF to just below AFE Nyquist, that's optimal
uint32_t afe_nyquist = afe_rate / 2;
// Use the larger of: signal bandwidth requirement OR Nyquist protection
// (but MAX2831 driver will clamp to its available settings anyway)
if (lpf_bandwidth < afe_nyquist) {
// Set LPF close to Nyquist for maximum alias rejection
lpf_bandwidth = (afe_nyquist * 9) / 10; // 90% of Nyquist
}
radio::set_baseband_filter_bandwidth_rx(lpf_bandwidth);
#else
radio::set_baseband_filter_bandwidth_rx(baseband_bandwidth());
+1
View File
@@ -66,6 +66,7 @@ class ReceiverModel {
/* The frequency to receive (no offset). */
rf::Frequency target_frequency() const;
void set_target_frequency(rf::Frequency f);
void set_target_frequency_with_hidden_offset(rf::Frequency f, rf::Frequency offset);
uint32_t baseband_bandwidth() const;
void set_baseband_bandwidth(uint32_t v);
+332 -99
View File
@@ -27,8 +27,8 @@ namespace tuning {
namespace config {
// Forward declarations
Config low_band(const rf::Frequency target_frequency);
Config mid_band(const rf::Frequency target_frequency);
Config low_band(const rf::Frequency target_frequency, const uint32_t afe_rate, const bool transmit);
Config mid_band(const rf::Frequency target_frequency, const uint32_t afe_rate, const bool transmit);
Config high_band(const rf::Frequency target_frequency);
#ifdef PRALINE
@@ -36,133 +36,366 @@ Config high_band(const rf::Frequency target_frequency);
* PRALINE Tuning Configuration
* ============================
*
* Reference: hackrf_usb/common/tune_config.h praline_tune_config_rx[]
* These tables are copied verbatim from the reference firmware,
* hackrf/firmware/common/tune_config.h (praline_tune_config_rx /
* praline_tune_config_tx), and the selection and offset maths below reproduce
* hackrf/firmware/common/radio.c radio_update_frequency() /
* analog_from_digital_rf() / compute_offset().
*
* The hackrf_usb firmware uses a table-driven approach where each entry
* specifies:
* - rf_range_end_mhz: Upper frequency limit for this config
* - if_mhz: IF frequency (what MAX2831 tunes to)
* - high_lo: true = high-side injection, false = low-side
* - shift: FPGA quarter-shift mode (not implemented in Mayhem yet)
* Each entry gives, for target frequencies up to rf_range_end_mhz:
* if_mhz the IF the MAX2831 tunes to (0 = mixer bypassed, IF = RF)
* high_lo true -> LO = IF + analogue RF (mixer inverts the spectrum)
* false -> LO = IF - analogue RF (no inversion)
* shift the FPGA quarter-rate shift mode used for this entry
*
* Key insight: The IF frequency varies to keep the RFFC5072 VCO in a
* safe operating range (ideally 3500-5000 MHz, avoiding extremes).
* The quarter-rate shift is the part Mayhem was previously missing. RX entries
* deliberately place the analogue passband a quarter of the ADC rate away from
* the requested frequency (+8 MHz at the usual 32 Msps AFE rate) so that the
* wanted signal never sits on the DC offset / LO leakage, and then ask the FPGA
* to rotate it back down to DC. Both halves have to be programmed together:
* - tuning to target + offset without asking the FPGA to rotate leaves the
* signal 8 MHz out and the decimation filter deletes it;
* - tuning to target with no offset (what Mayhem did) parks the signal on DC,
* under the LO leakage and the gateware's adaptive DC block.
*
* RFFC5072 VCO calculation:
* High-side injection: LO = IF + RF, VCO = LO × lodiv
* Low-side injection: LO = IF - RF, VCO = LO × lodiv
* Where lodiv = 2 for frequencies where VCO > 2700 MHz
*
* From hackrf_usb tune_config_rx (simplified):
* 0-2100 MHz: IF=2375, high_lo=true VCO = (2375+RF)×2
* 2105-2115: IF=2375, high_lo=false VCO = (2375-RF)×2
* 2115-2130: IF=2425, high_lo=false VCO = (2425-RF)×2
* ... (more entries for fine-grained control)
* 2320-2580: IF=0 (bypass mode, no mixer)
* 2580+: High-pass mode
* "up" and "down" are named for the direction the FPGA rotates, so
* FPGA_QUARTER_SHIFT_MODE_UP means the analogue centre is placed ABOVE the
* requested frequency, and DOWN below it (radio.c analog_from_digital_rf()).
*/
// Simplified tune_config lookup for Mayhem
// Returns the IF frequency in Hz for a given target frequency
constexpr rf::Frequency praline_get_if_frequency(const rf::Frequency target_frequency) {
const uint32_t freq_mhz = target_frequency / 1'000'000;
namespace {
// Based on hackrf_usb tune_config_rx table
if (freq_mhz < 2100) {
// Most low-band frequencies: use 2375 MHz IF
// This keeps VCO around 4750-4950 MHz for FM band
return 2375'000'000;
} else if (freq_mhz < 2320) {
// Transition zone: use varying IF to avoid VCO edges
// These frequencies are tricky - near MAX2831 minimum
// Use 2425 MHz to give some margin
return 2425'000'000;
} else {
// Bypass mode or high-band - IF not used for mixer
return 0;
struct PralineTuneConfig {
uint16_t rf_range_end_mhz;
uint16_t if_mhz;
bool high_lo;
uint8_t shift; /* 0b00 none, 0b11 up, 0b01 down */
};
/* The tables below are kept column-aligned to match the reference source, so
* they are exempt from reformatting. */
// clang-format off
/* tuning table optimized for RX */
constexpr PralineTuneConfig praline_tune_config_rx[] = {
{ 0, 2360, true, 0b00},
{ 50, 2320, true, 0b11},
{ 100, 2320, true, 0b01},
{ 140, 2320, true, 0b11},
{ 406, 2560, true, 0b11},
{ 511, 2380, true, 0b11},
{ 578, 2560, true, 0b01},
{ 741, 2340, true, 0b11},
{ 861, 2560, true, 0b01},
{ 921, 2560, true, 0b11},
{ 1049, 2340, true, 0b01},
{ 1169, 2380, true, 0b11},
{ 1360, 2340, true, 0b11},
{ 1544, 2560, true, 0b01},
{ 1675, 2560, true, 0b11},
{ 1992, 2380, true, 0b01},
{ 2070, 2340, true, 0b01},
{ 2150, 2360, true, 0b01},
{ 2168, 2560, false, 0b11},
{ 2185, 2580, false, 0b11},
{ 2202, 2580, false, 0b01},
{ 2205, 2520, false, 0b11},
{ 2216, 2560, false, 0b11},
{ 2223, 2540, false, 0b11},
{ 2234, 2580, false, 0b11},
{ 2240, 2560, false, 0b11},
{ 2251, 2580, false, 0b01},
{ 2258, 2580, false, 0b11},
{ 2265, 2540, false, 0b01},
{ 2271, 2580, false, 0b11},
{ 2273, 2560, false, 0b11},
{ 2275, 2580, false, 0b01},
{ 2280, 2500, false, 0b01},
{ 2284, 2540, false, 0b11},
{ 2289, 2580, false, 0b01},
{ 2293, 2540, false, 0b01},
{ 2298, 2520, false, 0b01},
{ 2300, 2580, false, 0b11},
{ 2302, 2540, false, 0b01},
{ 2309, 2560, false, 0b01},
{ 2311, 2580, false, 0b01},
{ 2314, 2540, false, 0b11},
{ 2315, 2540, false, 0b01},
{ 2320, 2580, false, 0b11},
{ 2380, 0, false, 0b11},
{ 2440, 0, false, 0b01},
{ 2500, 0, false, 0b11},
{ 2580, 0, false, 0b01},
{ 2583, 2360, false, 0b11},
{ 2584, 2380, false, 0b11},
{ 2587, 2340, false, 0b11},
{ 2593, 2340, false, 0b01},
{ 2607, 2340, false, 0b11},
{ 2609, 2360, false, 0b11},
{ 2615, 2360, false, 0b01},
{ 2627, 2340, false, 0b01},
{ 2629, 2360, false, 0b01},
{ 2631, 2380, false, 0b11},
{ 2644, 2340, false, 0b11},
{ 2649, 2380, false, 0b11},
{ 2651, 2380, false, 0b01},
{ 2654, 2500, false, 0b11},
{ 2665, 2360, false, 0b11},
{ 2669, 2380, false, 0b01},
{ 2672, 2360, false, 0b01},
{ 2682, 2340, false, 0b11},
{ 2687, 2380, false, 0b11},
{ 2692, 2340, false, 0b11},
{ 2695, 2500, false, 0b11},
{ 2705, 2360, false, 0b11},
{ 2707, 2380, false, 0b01},
{ 2712, 2340, false, 0b01},
{ 2717, 2520, false, 0b11},
{ 2728, 2380, false, 0b11},
{ 2730, 2560, false, 0b11},
{ 2734, 2500, false, 0b11},
{ 2758, 2340, false, 0b11},
{ 2780, 2360, false, 0b11},
{ 2787, 2520, false, 0b11},
{ 2802, 2380, false, 0b11},
{ 2809, 2540, false, 0b11},
{ 2822, 2380, false, 0b01},
{ 2831, 2560, false, 0b11},
{ 2854, 2340, false, 0b11},
{ 2875, 2360, false, 0b11},
{ 2898, 2380, false, 0b11},
{ 2918, 2380, false, 0b01},
{ 2936, 2520, false, 0b01},
{ 2944, 2380, false, 0b01},
{ 2959, 2560, false, 0b11},
{ 2976, 2340, false, 0b11},
{ 2985, 2500, false, 0b01},
{ 3003, 2340, false, 0b11},
{ 3009, 2540, false, 0b11},
{ 3027, 2380, false, 0b11},
{ 3034, 2560, false, 0b11},
{ 3050, 2380, false, 0b01},
{ 3069, 2500, false, 0b11},
{ 3094, 2520, false, 0b11},
{ 3119, 2540, false, 0b11},
{ 3144, 2560, false, 0b11},
{ 3169, 2560, false, 0b01},
{ 3180, 2500, false, 0b11},
{ 3204, 2340, false, 0b11},
{ 3232, 2360, false, 0b11},
{ 3292, 2340, false, 0b01},
{ 3340, 2380, false, 0b01},
{ 3369, 2340, false, 0b11},
{ 3399, 2360, false, 0b11},
{ 3429, 2380, false, 0b11},
{ 3464, 2500, false, 0b11},
{ 3489, 2520, false, 0b11},
{ 3512, 2540, false, 0b11},
{ 3551, 2500, false, 0b01},
{ 3582, 2540, false, 0b11},
{ 3611, 2560, false, 0b11},
{ 3639, 2520, false, 0b11},
{ 3729, 2340, false, 0b11},
{ 3817, 2380, false, 0b01},
{ 3942, 2360, false, 0b01},
{ 4049, 2540, false, 0b11},
{ 4134, 2500, false, 0b01},
{ 4194, 2560, false, 0b11},
{ 4353, 2520, false, 0b11},
{ 4449, 2360, false, 0b01},
{ 4562, 2500, false, 0b11},
{ 4672, 2560, false, 0b11},
{ 4769, 2540, false, 0b11},
{ 4849, 2560, false, 0b01},
{ 4889, 2560, false, 0b11},
{ 4929, 2560, false, 0b11},
{ 4969, 2560, false, 0b11},
{ 5009, 2560, false, 0b11},
{ 5049, 2560, false, 0b11},
{ 5092, 2360, false, 0b11},
{ 5209, 2340, false, 0b01},
{ 5298, 2380, false, 0b01},
{ 5468, 2340, false, 0b01},
{ 5582, 2520, false, 0b11},
{ 5702, 2340, false, 0b11},
{ 5888, 2520, false, 0b01},
{ 6092, 2340, false, 0b01},
{ 6240, 2560, false, 0b11},
{ 6609, 2340, false, 0b11},
{ 6752, 2380, false, 0b01},
{ 6930, 2520, false, 0b01},
{ 7000, 2560, false, 0b11},
{ 7070, 2560, false, 0b01},
{ 7251, 2580, false, 0b01},
{ 0, 0, false, 0b00},
};
/* tuning table optimized for TX */
constexpr PralineTuneConfig praline_tune_config_tx[] = {
{ 2100, 2375, true, 0b00},
{ 2105, 2375, false, 0b00},
{ 2115, 2425, false, 0b00},
{ 2130, 2375, false, 0b00},
{ 2150, 2425, false, 0b00},
{ 2160, 2475, false, 0b00},
{ 2175, 2425, false, 0b00},
{ 2190, 2475, false, 0b00},
{ 2195, 2425, false, 0b00},
{ 2210, 2375, false, 0b00},
{ 2248, 2425, false, 0b00},
{ 2265, 2525, false, 0b00},
{ 2300, 2425, false, 0b00},
{ 2320, 2525, false, 0b00},
{ 2580, 0, false, 0b00},
{ 3000, 2325, false, 0b00},
{ 3140, 2375, false, 0b00},
{ 3200, 2425, false, 0b00},
{ 3280, 2375, false, 0b00},
{ 3340, 2425, false, 0b00},
{ 3420, 2475, false, 0b00},
{ 3480, 2525, false, 0b00},
{ 3500, 2475, false, 0b00},
{ 3595, 2425, false, 0b00},
{ 3625, 2375, false, 0b00},
{ 3670, 2475, false, 0b00},
{ 3710, 2425, false, 0b00},
{ 3760, 2525, false, 0b00},
{ 3790, 2475, false, 0b00},
{ 3860, 2425, false, 0b00},
{ 3915, 2375, false, 0b00},
{ 4000, 2425, false, 0b00},
{ 4055, 2375, false, 0b00},
{ 4125, 2425, false, 0b00},
{ 4700, 2375, false, 0b00},
{ 4800, 2425, false, 0b00},
{ 5000, 2375, false, 0b00},
{ 5260, 2475, false, 0b00},
{ 5465, 2525, false, 0b00},
{ 5560, 2375, false, 0b00},
{ 5720, 2425, false, 0b00},
{ 5860, 2475, false, 0b00},
{ 5970, 2575, false, 0b00},
{ 6000, 2375, false, 0b00},
{ 6500, 2325, false, 0b00},
{ 6750, 2375, false, 0b00},
{ 6850, 2425, false, 0b00},
{ 6950, 2475, false, 0b00},
{ 7000, 2525, false, 0b00},
{ 7251, 2575, false, 0b00},
{ 0, 0, false, 0b00},
};
// clang-format on
/* radio.c select_tune_config(): first entry whose range end is above the
* requested frequency. The list is terminated by an all-zero entry, which is
* also what a frequency past the end of the table lands on. */
const PralineTuneConfig* select_tune_config(const rf::Frequency target_frequency, const bool transmit) {
const PralineTuneConfig* entry = transmit ? praline_tune_config_tx : praline_tune_config_rx;
const uint32_t freq_mhz = static_cast<uint32_t>(target_frequency / 1'000'000);
while ((entry->rf_range_end_mhz != 0) || (entry->if_mhz != 0)) {
if ((target_frequency == 0) || (entry->rf_range_end_mhz > freq_mhz))
break;
entry++;
}
return entry;
}
// Returns true for high-side injection, false for low-side
constexpr bool praline_use_high_side_injection(const rf::Frequency target_frequency) {
const uint32_t freq_mhz = target_frequency / 1'000'000;
// Based on hackrf_usb tune_config_rx table
if (freq_mhz < 2100) {
// Standard low-band: high-side injection
// LO = IF + RF, mixer inverts spectrum
return true;
} else if (freq_mhz < 2105) {
// Narrow transition: still high-side
return true;
} else if (freq_mhz < 2320) {
// Near MAX2831 minimum: use low-side injection
// LO = IF - RF, no spectrum inversion
return false;
} else {
// Bypass/high-band - doesn't matter, mixer bypassed
return false;
}
/* radio.c compute_offset(): a quarter of the AFE (ADC) sample rate, or zero if
* no shift is in use or the AFE rate isn't known yet. */
constexpr uint32_t quarter_shift_offset(const uint8_t shift, const uint32_t afe_rate) {
return (shift == 0) ? 0 : (afe_rate / 4);
}
/* radio.c analog_from_digital_rf(). */
rf::Frequency analog_from_digital_rf(const rf::Frequency target_frequency, const uint8_t shift, const uint32_t afe_rate) {
const rf::Frequency offset = quarter_shift_offset(shift, afe_rate);
if (shift == 0b11)
return target_frequency + offset;
if (shift == 0b01)
return (offset > target_frequency) ? (offset - target_frequency)
: (target_frequency - offset);
return target_frequency;
}
} // namespace
#endif // PRALINE
// Low band <2170 Mhz (HackRF One) or <2320 MHz (PRALINE):
constexpr rf::Frequency low_band_second_lo_frequency(const rf::Frequency target_frequency) {
Config low_band(const rf::Frequency target_frequency, const uint32_t afe_rate, const bool transmit) {
#ifdef PRALINE
// Use the tune_config lookup for PRALINE
return praline_get_if_frequency(target_frequency);
#else
return 2650'000'000 - (target_frequency / 7);
#endif
}
const PralineTuneConfig* entry = select_tune_config(target_frequency, transmit);
Config low_band(const rf::Frequency target_frequency) {
const rf::Frequency second_lo_frequency = low_band_second_lo_frequency(target_frequency);
/* Past the end of the table: no usable configuration. */
if ((entry->rf_range_end_mhz == 0) && (entry->if_mhz == 0))
return {};
#ifdef PRALINE
/* afe_rate == 0 means the caller doesn't know the ADC rate, so no LO offset
* is applied. The FPGA rotation has to be dropped with it: rotating without
* the matching offset moves the wanted signal off DC by afe_rate / 4. */
const uint8_t shift = (afe_rate == 0) ? 0 : entry->shift;
const rf::Frequency analog_rf = analog_from_digital_rf(target_frequency, shift, afe_rate);
/* if_mhz == 0 means the mixer is bypassed and the transceiver tunes the RF
* directly; there is no first LO in that case. */
const rf::Frequency second_lo_frequency =
(entry->if_mhz == 0) ? analog_rf : (static_cast<rf::Frequency>(entry->if_mhz) * 1'000'000);
if (entry->if_mhz == 0)
return {0, second_lo_frequency, rf::path::Band::Low, false, shift};
/* The low band always runs through the low-pass image-reject filter, so
* the spectrum is inverted exactly when the first LO ends up above the IF,
* i.e. for high-side injection. This is hackrf_usb.c radio_changed():
* invert = (img_reject == RF_PATH_FILTER_LOW_PASS) && (freq_lo > freq_if)
*/
rf::Frequency first_lo_frequency;
bool mixer_invert;
if (praline_use_high_side_injection(target_frequency)) {
// High-side injection: LO = IF + RF
first_lo_frequency = second_lo_frequency + target_frequency;
if (entry->high_lo) {
first_lo_frequency = second_lo_frequency + analog_rf;
mixer_invert = true;
} else {
// Low-side injection: LO = IF - RF
first_lo_frequency = second_lo_frequency - target_frequency;
first_lo_frequency = second_lo_frequency - analog_rf;
mixer_invert = false;
}
return {first_lo_frequency, second_lo_frequency, rf::path::Band::Low, mixer_invert};
return {first_lo_frequency, second_lo_frequency, rf::path::Band::Low, mixer_invert, shift};
#else
(void)afe_rate;
(void)transmit;
const rf::Frequency second_lo_frequency = 2650'000'000 - (target_frequency / 7);
const rf::Frequency first_lo_frequency = target_frequency + second_lo_frequency;
const bool mixer_invert = true;
return {first_lo_frequency, second_lo_frequency, rf::path::Band::Low, mixer_invert};
#endif
}
// Mid band 2170-2740 Mhz (HackRF One) or 2320-2580 MHz (PRALINE):
Config mid_band(const rf::Frequency target_frequency) {
// Mid band 2170-2740 Mhz (HackRF One) or 2320-2740 MHz (PRALINE):
Config mid_band(const rf::Frequency target_frequency, const uint32_t afe_rate, const bool transmit) {
#ifdef PRALINE
// For Praline with MAX2831 (2.3-2.6 GHz range)
// Frequencies 2170-2300 MHz need upconversion since they're below MAX2831 minimum
if (target_frequency < 2300'000'000) {
// Treat as low band - need mixer
return low_band(target_frequency);
}
// Frequencies 2300-2600 MHz can go direct (no RFFC5072)
else if (target_frequency <= 2600'000'000) {
const rf::Frequency second_lo_frequency = target_frequency;
const rf::Frequency first_lo_frequency = 0;
const bool mixer_invert = false;
return {first_lo_frequency, second_lo_frequency, rf::path::Band::Mid, mixer_invert};
}
// Frequencies 2600-2740 MHz need downconversion since they're above MAX2831 maximum
else {
// Treat as high band
return high_band(target_frequency);
/* radio.c select_img_reject() / tuning.c: on PRALINE the MAX2831 tunes
* direct (mixer bypassed) from 2320 to 2580 MHz. band_mid starts at
* TRANSITION = 2320 MHz, so everything below that already went to
* low_band(). */
if (target_frequency <= 2580'000'000) {
const PralineTuneConfig* entry = select_tune_config(target_frequency, transmit);
const uint8_t shift = (afe_rate == 0) ? 0 : entry->shift;
const rf::Frequency analog_rf = analog_from_digital_rf(target_frequency, shift, afe_rate);
/* Mixer bypassed: no first LO, the MAX2831 tunes the (offset)
* analogue RF directly and the FPGA rotates it back. */
return {0, analog_rf, rf::path::Band::Mid, false, shift};
}
/* 2580-2740 MHz: above the bypass window, downconvert. */
return high_band(target_frequency);
#else
(void)afe_rate;
(void)transmit;
const rf::Frequency second_lo_frequency = target_frequency;
const rf::Frequency first_lo_frequency = 0;
const bool mixer_invert = false;
@@ -203,12 +436,12 @@ Config high_band(const rf::Frequency target_frequency) {
return {first_lo_frequency, second_lo_frequency, rf::path::Band::High, mixer_invert};
}
Config create(const rf::Frequency target_frequency) {
Config create(const rf::Frequency target_frequency, const uint32_t afe_rate, const bool transmit) {
/* TODO: This is some lame code. */
if (rf::path::band_low.contains(target_frequency)) {
return low_band(target_frequency);
return low_band(target_frequency, afe_rate, transmit);
} else if (rf::path::band_mid.contains(target_frequency)) {
return mid_band(target_frequency);
return mid_band(target_frequency, afe_rate, transmit);
} else if (rf::path::band_high.contains(target_frequency)) {
return high_band(target_frequency);
} else {
+21 -4
View File
@@ -33,18 +33,21 @@ struct Config {
: first_lo_frequency(0),
second_lo_frequency(0),
rf_path_band(rf::path::Band::Mid),
mixer_invert(false) {
mixer_invert(false),
quarter_shift(0) {
}
constexpr Config(
rf::Frequency first_lo_frequency,
rf::Frequency second_lo_frequency,
rf::path::Band rf_path_band,
bool mixer_invert)
bool mixer_invert,
uint8_t quarter_shift = 0)
: first_lo_frequency(first_lo_frequency),
second_lo_frequency(second_lo_frequency),
rf_path_band(rf_path_band),
mixer_invert(mixer_invert) {
mixer_invert(mixer_invert),
quarter_shift(quarter_shift) {
}
bool is_valid() const {
@@ -55,9 +58,23 @@ struct Config {
const rf::Frequency second_lo_frequency;
const rf::path::Band rf_path_band;
const bool mixer_invert;
/* PRALINE only: FPGA RX quarter-rate shift mode, in the encoding the
* gateware expects in the top two bits of register 0x03 (rx_pstep):
* 0b00 = none, 0b11 = up, 0b01 = down.
* Matches fpga_quarter_shift_mode_t in hackrf/firmware/common/fpga.h.
* Always 0 on HackRF One (no FPGA). */
const uint8_t quarter_shift;
};
Config create(const rf::Frequency target_frequency);
/* afe_rate is the ADC sample rate in Hz (output rate << decimation), needed on
* PRALINE to work out how far off centre the quarter-rate shift places the
* analogue passband. Pass 0 (or leave defaulted) to disable the shift.
* transmit selects the TX tuning table. Both are ignored on HackRF One. */
Config create(
const rf::Frequency target_frequency,
const uint32_t afe_rate = 0,
const bool transmit = false);
} /* namespace config */
} /* namespace tuning */
+1 -1
View File
@@ -4,7 +4,7 @@
* Copyright (C) 2019 Elia Yehuda (z4ziggy)
* Copyright (C) 2023 Mark Thompson
* Copyright (C) 2024 u-foka
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
* copyleft 2024 zxkmm
*
* This file is part of PortaPack.
*
+9 -3
View File
@@ -39,6 +39,8 @@ class BoundFrequencyField : public FrequencyField {
public:
decltype(FrequencyField::on_change) updated{};
std::function<bool(rf::Frequency)> changing{};
std::function<void(rf::Frequency)> entered{};
BoundFrequencyField(Point parent_pos, NavigationView& nav)
: FrequencyField(parent_pos) {
@@ -47,7 +49,8 @@ class BoundFrequencyField : public FrequencyField {
set_value(model->target_frequency());
on_change = [this](rf::Frequency f) {
model->set_target_frequency(f);
if (!changing || !changing(f))
model->set_target_frequency(f);
if (updated)
updated(f);
};
@@ -57,7 +60,10 @@ class BoundFrequencyField : public FrequencyField {
on_edit_shown();
auto freq_view = nav.push<FrequencyKeypadView>(model->target_frequency());
freq_view->on_changed = [this](rf::Frequency f) {
set_value(f);
if (entered)
entered(f);
else
set_value(f);
};
nav.set_on_pop([this]() {
if (on_edit_hidden)
@@ -76,4 +82,4 @@ using TxFrequencyField = BoundFrequencyField<TransmitterModel, &portapack::trans
} // namespace ui
#endif // __UI_FREQ_FIELD_H__
#endif // __UI_FREQ_FIELD_H__

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