Compare commits

...

73 Commits

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

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

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

Closes #3274

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

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

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

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

* Refactor GPIO handling: update MAX2831 shutdown pin configuration

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

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

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

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

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

* Reorder includes for consistency and fix string dereference in VorCdiIndicator

* Add memory section and linker script entry for vor_rx application

* Testing VOR TX

* Moving VOR TX to SD

* Code refactoring to align with guidelines

* Add TODO comment to verify radial sign convention in vor_tx_config

* Remove unnecessary set_dirty() calls in VorRxView methods

* Update Course Deviation Indicator label and adjust UI element positions

* Update VorTxView to run prepared image from memory map

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

* Reverting action changes

* Adjust Course Deviation Indicator layout and refine painting logic

* Refactor VorRxView UI elements for improved layout and clarity

* Add calibration field and update radial calculation logic in VorRxView

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

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

* Setting comments inline

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

* Update external app address end to 0xAE0B0000

* Update set_vor_tx_config to include enabled parameter for consistency

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

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

* Refactor VorRxView labels and status messages

* Update VorRxView calibration field range to allow full circle input

* Completing Ident transmission logic

* Fixing TX gain and moving log label

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

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

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

* Refactor GPIO mappings and update SCU_ARRAY_SIZE for LPC43xx platform

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

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

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

* Potential fix for pull request finding

* comment

* aux power polarrity

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

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

---------

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

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

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

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

* Refactor GPIO handling and remove LED abstraction

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

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

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

* copilot

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

* Refactor GPIO methods for improved naming consistency and clarity
2026-06-19 07:48:55 +02:00
gullradriel 960e5b8329 Upgrade subm plus warning fix (#3227)
* fix compilation warning
* update submodule
2026-06-17 11:34:00 +02:00
Totoo f5a5e8e5e8 Map improv. (#3226) 2026-06-16 17:54:32 +02:00
Synray d8a1e2530a Add Security+ 2.0 TX App (#3217)
* Add U64 BoundSetting
* Add secplus-tx
* Add secplus license
* Update icon
* UI update
* Add filesystem::path BoundSetting
* Add file UI
* Rename file
* Add save button
* Fix warnings, add integer conversions to UI macros
* UI update
* Copilot suggestions, name edit UI
* Fix buffer length
* Style update
* Avoid recursion in Widget::style()
* Apply suggestions from code review
* Move secplustx.cpp to external
* Shorten title
* Space optimizations, remove FilePath setting
* Remove assertions
* Change namespace
* Change title to match
2026-06-15 19:28:47 +02:00
Totoo b84a424875 MBT update 2026-06-13 09:50:25 +08:00
gullradriel 62d0e598d4 Update submodule (#3222)
* update submodule to latest GSG modifications
* build: fix delay function
* update submodule to include praline fix
* fix dep to rebuild/reinclude hackrf_usb_ram.bin
2026-06-12 22:15:44 +02:00
Synray a93f2ff583 Remove sine table lookup from proc_ook (#3201) 2026-06-12 16:26:13 +02:00
Totoo e2586ceb22 proper fix for flasher (#3223) 2026-06-12 16:25:43 +02:00
未来方舟 5a2b0cdc2a build tool and agent.md (#2993)
* init

* note

* detect arch

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix typo

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update mbt

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* protect rm -rf

* remove wrong ai code

* Update mbt

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* add agent file

* remove empty file

* agent.md rewrite

* agent.md rewrite

* fix on ubuntu

* err check

* add make

* add python3-yaml

* add binutils

* fix the build order to avoid some compile bugs

* mg

* remove trash

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: gullradriel <3157857+gullradriel@users.noreply.github.com>
Co-authored-by: HTotoo <ttotoo@gmail.com>
2026-06-12 16:52:13 +08:00
Totoo f852900cd0 Epirbrx sizefix (#3221) 2026-06-11 17:18:05 +02:00
Pezsma 9ab367f2e0 Add praline gpio (#3218)
* Implement antenna bias control for Praline and update GPIO configurations
* Add GPIO power management functions and update board initialization for Praline
* Refactor power management functions for Praline and update GPIO configurations
* Update GPIO configurations for Praline and enable power control pins. AA_EN enabled
* Refactor GPIO configuration for Praline: update array size definitions and clean up comments
* Add P1 and P2 control functions for PRALINE
* Introduced P1_Function and P2_Function enums in ClockManager to manage multiplexer control.
* mplemented set_p1_control and set_p2_control methods for configuring P1 and P2 control pins based on specified functions.
* Updated clock_manager.hpp and clock_manager.cpp to include new functionality.
* Modified MAX2837 and MAX2839 initialization to conditionally configure GPIO based on PRALINE.
* Adjusted RFFC507x and RFFC507x_SPI initialization to include GPIO setup for PRALINE.
- Refactored RF path initialization to remove unnecessary GPIO setup.
- Updated board configuration for PRALINE to reflect new GPIO settings and pin configurations.
- Cleaned up FPGA bridge code by removing unused pin configuration functions.
- Adjusted SCU array size in pal_lld.h for PRALINE.
- Enhanced hackrf_gpio.hpp to define multiplexer control pins for PRALINE.
- Modified LED setup to accommodate active-low configuration for PRALINE.

* Refactor GPIO and power control functions

- Removed unused power control function declarations from board.h.
- Enhanced gpio.hpp with new pin setup functions and GPIO control structures for PRALINE.
- Consolidated multiplexer control pin definitions into gpio_control namespace.
- Moved power control function implementations to gpio.cpp, including detailed VAA power management logic.
- Updated power control functions to handle both PRALINE and HackRF R9 configurations.
- Cleaned up hackrf_gpio.hpp by removing redundant multiplexer control definitions.

* Refactor GPIO control for PRALINE: update MAX2831 and RFFC507x configurations, add new GPIO mappings

* Refactor GPIO configurations for PRALINE: update anti-aliasing filter pin mapping, enhance LED setup logic, and add placeholder GPIO entries.

* Refactor GPIO configurations for PRALINE: update LED mappings and remove unused anti-aliasing filter GPIO entry.
2026-06-11 13:24:42 +02:00
Arne Luehrs c7a0846645 EPIRB RX: add receiver-overload indicator on channel bar (#3212)
* EPIRB RX: add receiver-overload indicator on channel bar

Tint the channel-power bar red when the front-end overloads (ADC near
full scale). EPIRB demodulation relies on the constant-envelope carrier
and the +/-1.1 rad biphase phase jumps; ADC clipping distorts both, so a
visible cue to reduce RF-amp/LNA/VGA gain is useful.

Implemented in the shared ui::Channel widget via an opt-in
set_overload_threshold(): when the last ChannelStatistics max_db reaches
the threshold the bar is drawn red instead of blue. Default threshold is
above the 0 dBFS ceiling, so all existing users are unaffected. The
EPIRB view enables it at -3 dBFS.

Done in the widget (main firmware) rather than the EPIRB app because the
epirb_rx external app is already at the 32 KB flash limit; the app only
pays for a single setter call.
2026-06-11 11:10:23 +02:00
Arne Luehrs 569bcaad22 Epirb afc wide capture (#3213)
* EPIRB RX: widen AFC capture range to +/-5 kHz

The AFC estimate was only applied after carrier lock, so carrier
acquisition ran on raw phase deltas and could only tolerate offsets of
a few hundred Hz before the 0.6/0.7/1.6 rad detection thresholds (on
the 12-sample accumulator) broke down.

Track the carrier offset continuously in the IDLE state with a
first-order loop (AFC_TRACK_ALPHA), so the de-biased accumulator
self-centers for any offset within the discriminator Nyquist (~24 kHz)
*before* the thresholds run. ALPHA = 0.005 pulls a +/-5 kHz offset
under the 0.6 rad lock threshold in ~11 ms, well inside the 160 ms
preamble / 80 ms stability window. The IDLE rise-detect threshold is
also made symmetric (fabsf) now that the bias is removed.

Also add the missing <cstdint> include to test_convert.cpp so the
application_test suite compiles under the current toolchain.

Verified: baseband_epirb_rx.elf builds (flash 53%, RAM 12%) and
baseband_test passes.
2026-06-11 11:01:54 +02:00
Totoo 6b5955e0b5 Enhance battery detection and configuration handling (#3197)
* indicate batt changed event
* added sleep config, and use it.
* reinit if new batt detected.
* more detect
* repl batt setting
* disabled some things
* copilot fixes
2026-06-11 10:52:20 +02:00
Synray 9042d1c433 Use UI_ macros in looking glass (#3208) 2026-06-09 20:46:35 +02:00
未来方舟 378b2808ba Looking Glass frequency list now supports muiti file loads (#3215) 2026-06-08 15:22:29 +02:00
Erwin Ried 5b363b79aa Update links and images in README.md 2026-05-31 22:56:20 +02:00
Brumi-2021 0d51787099 Clean_gcc_14_2_1_compiler_errors (#3206)
* Clean_gcc_14_2_1_compiler_errors

* format issues
2026-05-31 17:34:30 +02:00
gullradriel 8f5b25b10d PRALINE Tx and clock fix (Pr 3192 takeover (#3198) )
* fix clock init and add mayhem tx capacity
* aligned external clock and RX config
* fix(clock): cap TX interpolation to x16 to prevent doubled waveform speed

  The TX gateware only implements interpolation factors x1..x16 (tx_intrp
  0..4), but the rate search could select n==5 (x32) for very low TX
  sample rates. That left the AFE/CLK0/CLK1 running at the x32 rate while
  the FPGA only interpolated by x16, clocking the TX datapath at twice the
  gateware output rate. Cap afe_rate and _resampling_n to the x16 limit in
  TX so the clocks stay aligned with the interpolation setting.

* fix(praline): source PLL1 from 40MHz GP_CLKIN instead of 12MHz XTAL

  For the intermediate 90-110MHz PLL1 stepping stage, source PLL1 from
  GP_CLKIN (Si5351 CLK2/MCU_CLK at 40MHz) giving N=2, M=10, Fclk=100MHz,
  rather than the on-chip 12MHz XTAL bootstrap path. This matches the
  non-PRALINE path. Remove the dead commented-out XTAL enable/PLL code.

* docs(fpga_bridge): correct register 0x03 description for PRALINE gateware

  Register 0x03 is RX_DIGITAL_GAIN in RX and TX_NCO_CTRL in TX, not
  RX_PSTEP.

* Fix fpga_tx_set_nco_enable to use FPGA_REG3_TX_NCO_CTRL (0x03)
2026-05-31 12:44:40 +02:00
Synray 4e56d0f420 Add RSSI toggle button to looking glass (#3204) 2026-05-31 11:24:24 +02:00
gullradriel 2bb3b415d1 update submodule (#3199) 2026-05-30 20:31:01 +02:00
Pezsma 6e0394fb6b Filename scroll (#3193) 2026-05-30 08:24:20 +02:00
Totoo da21ea7866 Fix flashing (#3188)
Fixed some bugs
2026-05-30 01:49:49 +02:00
未来方舟 2c434946cd Display if IQ inverted in DFU overlay menu while the algo decides to (#3196)
* init

* expose more info

* copilot suggest
2026-05-29 12:06:17 +02:00
gullradriel f3aee44e3b update submodule (#3187)
* update submodule
* fix iwyu CI errors
* fix sd over usb with hackrf submodule si5351c refactored functions
2026-05-21 17:48:53 +02:00
未来方舟 165c190d92 Use html comments to make som text only visible by PR creator (#3186)
* Use html comments to make som text only visible by PR creator

* reformat a bit
2026-05-21 15:51:56 +02:00
Totoo 8f9246c1ea Enhance transmitter model and UI for better handling of TX and RF ampstates (#3185) 2026-05-21 15:50:47 +02:00
Pezsma f1d52ae78f expanding channels (#3183) 2026-05-21 15:50:10 +02:00
qwer123 865bdee9b3 add external clock detect method (#3180) 2026-05-20 22:40:39 +02:00
Totoo 314e910cbc make battleship smaller (#3181) 2026-05-20 18:39:21 +02:00
Pezsma ec6c7eff86 Hard reset app (#3182) 2026-05-20 18:38:13 +02:00
Pezsma 95b2e9fbc3 Charge sleep bug (#3177) 2026-05-15 09:24:30 +00:00
未来方舟 52ff442fcc Tune pull_request_template a bit (#3176) 2026-05-14 00:48:27 +08:00
gullradriel 95d246a020 Update hackrf submodule (#3174)
* updated submodule

* usb_serial: populate usb_device before usb_run. Upstream now leaves the global usb_device zero-initialized and expects hackrf_usb.c to memcpy a per-board const into it. We don't link hackrf_usb.c, so do the memcpy here. Also flip control_in to enable_zlp=true to match the new endpoint API.

* baseband: derive IS_* board defines from BOARD. Upstream moved platform IS_* macros from platform_detect.h to cmake. Set them per BOARD here since we don't include platform-detect.cmake. Drop the duplicate clkin.c entry.

* sd_over_usb: drop duplicate driver objects, fix setup callback. Upstream now defines i2c0/i2c1, spi_bus_ssp0/1, sgpio_config and rf_path in the common driver files, so stop redefining them here and patch the runtime fields in pin_setup() instead. Update si5351c_reset_pll() call to the two-arg form. Replace the no-op transceiver_usb_setup_complete stub with one that delegates to usb_setup_complete.

* sd_over_usb: populate usb_device before usb_run. Same fix as the application side: memcpy a const template into usb_device before usb_device_init/usb_run, otherwise the descriptor pointer is NULL. Also pass enable_zlp=true on control_in to match the new endpoint API.

* fix unicode in comment

* remove lz4 bufs reservation in M0
2026-05-13 14:53:54 +02:00
Frederic BORRY c34a6940ca EPIRB RX app rework + EPIRB TX app fixes (#3172)
* First step to EPIRB rx app fix
* Added spike filtering on carrier detection for more robustness.
* Created dedicated packet builder
* First step to fixing UI.
* First step to epirb-rx ui rework.
* Made static methods static members
* Code optim
* Code cleanup + first step to new UI
* Fixed display
* Added BeaconList widget + BeaconDB
* Epirb TX app memory optim
* Fixed OOM when opening Map Display or Locator editor on EPIRB TX app
* First step to tabs + code cleanup to free some space...
* Added spectrum analyzer + fixed display
* Fixed display refresh issue after leaving spectrum.
* More code optim + first step to detail view.
* Fixed float coord display.
* Fixed protocol names
* Fixed location display
* Externalized country base to sd card + more code optim
* Fixed country display
* Fixed path
* Added protocol description and resource manager
* Added main/aux loc device
* Removed specan to save space
* First step to adding QR code
* Used a custom TextArea component instead of Console
* Fixed QR code display for map URL
* Added beacon selection management + fixes
* First step to map/data qr.
* QR tab : added data + detail/map toggle
* Removed sqhelch to save space
* Removed audio to save space
* Removed freq label to save space
* Removed packet timestamp to save space
* Moved frequency values to resource file
* Fixed detection parameters to suite real beacons (slower phase shift time).
* Fixed countdown field size
* Fixed frequency list.
* Fixed detail view on select.
* First step to fix settings.
* Fixed squelch for a 0-99 range.
* Added bch code correction (single bit error).
* More code optim, disable country cache, removed inline from location to reduce code size.
* Removed inline, code formatting.
* Added bch code correction display.
* Fixed focus bug, fixed packet size check. Added BCH1/BCH2 correction test cases in BEACONS.TXT file for EPIRB TX application.
* Fixed merge
* Added countdown setting.
* Added selection display to beacon list
* Restored squelch control. Added countdown reset.
* Code optim on formatSummary
* More code optim
* Made ResourceManager methods static
* Externalized beacon strings to res file
*Removed frame parameter from methods
* Added test beacons for emergency. Removed old res file.
* Fixed resource manager
* Fixed resources. Added emergency display.
* BCH + res fixes
* Fixed beacons
* Fixed app color. Fixed HexId and Serial calculation.
* Fixed frequency list. Comments.
* Code cleanup and comments
* Added slideshow mode to EPIRB TX application. Code comments / cleanup.
* Fixed frame end after wrong code cleanup
* More code optim and comments. Removed touch from beacon list.
* Added beacon selection with encoder on detail view.
* Added beacon paging in header. Fixed timeout display
* Fixed build for PRALINE
* Fixed formatting
* Potential fix for pull request finding
* stop on carriage return
* empty string if unknown
* zeroing data buffer before use
* fix typo
* add include
* fix case if pair == 0
* fix scpectrum_on to spectrum_on
* fix typos
* replaced 3 inlined offset blocks with apply_offset calls
* set_beacon consolidation
2026-05-12 14:00:47 +02:00
Synray 33330e4c34 Use gradient for level view, fix height scaling (#3173)
* Use gradient for level view, fix height scaling
* Add RSSI indicators
2026-05-12 12:48:17 +02:00
Pezsma 66f1835232 Wait tx complete (#3165)
* Implement radio transmission drain mechanism and update shared memory structure
* don't wait for timeout if baseband already in shut down state
2026-05-12 12:45:02 +02:00
Synray 4dfbf666d0 Add more waterfalls (#3170) 2026-05-10 09:37:08 +02:00
Synray 245a36459f UI and bug fixes (#3164)
* Fix BLE RX RSSI and Channel scale

* Properly reset gradient index 0

* Use move constructor in MenuView::add_item

* Avoid erasing during iteration

* Fix gradient rounding. Extend to end of table

* Add comment to add_item
2026-05-10 00:08:05 +02:00
janhaverkamp 590f19062b Add support for larger battery capacities (#3168) 2026-05-09 20:54:25 +02:00
Totoo f38ef4ebc8 Added CU8 playback support (#3166) 2026-05-09 12:23:43 +02:00
gullradriel 900d2f2482 fix missing header (#3167) 2026-05-09 12:23:10 +02:00
Totoo 5aff5ac277 Fix text color multiline handling (#3161) 2026-05-08 09:57:06 +02:00
Totoo cf4d245964 Fixes #3159 (#3160) 2026-05-07 23:54:30 +08:00
Pezsma c7f7173958 sleep warning fix (#3162) 2026-05-07 15:26:34 +02:00
274 changed files with 15877 additions and 4679 deletions
+46 -22
View File
@@ -1,58 +1,80 @@
<!--
👋 Thank you for your contribution!
⚠️ Before you start, please review our Contributing Guidelines:
https://github.com/portapack-mayhem/mayhem-firmware/wiki/Contributing-Guidelines
-->
## Brief description of what you did
<!--
Replace this line with a description of your changes. What problem does this PR solve? What does it add or fix?
Don't forget to describe how maintainers can test your modifications!
-->
<!-- Replace this line with a description of your changes. What problem does this PR solve? What does it add or fix?
Don't forget to describe how maintainers can test your modifications! -->
---
## Proof that your changes work
<!--
1. PRs without proof will not be merged. Please provide evidence for every section below that applies to your change.
2. Trusted contributors: If you have a track record of merged PRs that were properly tested and documented, maintainers may waive one or more of the proof sections below at their discretion. You are still encouraged to include whatever evidence you have, but sections may be marked `N/A — trusted contributor` with a short note.
3. First-time contributors must provide full proof. See the Contributing Guidelines(https://github.com/portapack-mayhem/mayhem-firmware/wiki/Contributing-Guidelines) and Trusted Contributors (https://github.com/portapack-mayhem/mayhem-firmware/wiki/Trusted-Contributors) articles for details.
-->
> ⚠️ **PRs without proof will not be merged.** Please provide evidence for every section below that applies to your change.
>
> 🛈 **Trusted contributors:** If you have a track record of merged PRs that were properly tested and documented, maintainers may waive one or more of the proof sections below at their discretion. You are still encouraged to include whatever evidence you have, but sections may be marked `N/A — trusted contributor` with a short note. First-time contributors must provide full proof. See the [Contributing Guidelines](https://github.com/portapack-mayhem/mayhem-firmware/wiki/Contributing-Guidelines) and [Trusted Contributors](https://github.com/portapack-mayhem/mayhem-firmware/wiki/Trusted-Contributors) articles for details.
### 🖥️ Proof it compiles
<!--
Attach a log snippet showing a successful build from the final firmware size/space summary in the build output. Example: `Space remaining in flash ROM: 19552 bytes ( 1.9 % )`.
Paste log below.
-->
<!-- Paste log below -->
### 📱 Proof of testing on a real device
<!--
Attach photos, screenshots, or a short video of your changes running on actual PortaPack hardware. Emulator or simulator output is not a substitute for real-device testing.
Paste photo / video / screenshot below
-->
Attach photos, screenshots, or a short video of your changes running on actual PortaPack hardware. **Emulator or simulator output is not a substitute for real-device testing.**
<!-- Paste photo / video / screenshot below -->
### 📡 Proof against a real emitter/receiver (if applicable)
<!--
1. If your PR involves RX/TX, protocol decoding, signal generation, or any RF-related functionality, you **must** demonstrate it working against a real emitter or receiver. Include photos, videos, logs, or waterfall/spectrum screenshots.
2. If this section does not apply to your PR, write `N/A` below and briefly explain why.
3. Paste RF evidence, or write N/A with a short justification
-->
If your PR involves RX/TX, protocol decoding, signal generation, or any RF-related functionality, you **must** demonstrate it working against a real emitter or receiver. Include photos, videos, logs, or waterfall/spectrum screenshots.
If this section does not apply to your PR, write `N/A` below and briefly explain why.
<!-- Paste RF evidence, or write N/A with a short justification -->
---
## 📚 Wiki documentation commitment
**By submitting this PR, you acknowledge that once it is merged you are responsible for creating or updating the corresponding page on the [project wiki](https://github.com/portapack-mayhem/mayhem-firmware/wiki).**
<!--
By submitting this PR, you acknowledge that once it is merged you are responsible for creating or updating the corresponding page on the project wiki(https://github.com/portapack-mayhem/mayhem-firmware/wiki).
Your wiki article should include:
- A **screenshot of the main app screen** (strongly recommended)
- A clear **description** of what the app does
- An explanation of the **controls** and UI elements
- Any known **limitations**, quirks, or caveats
- A screenshot of the main app screen (strongly recommended)
- A clear description of what the app does
- An explanation of the controls and UI elements
- Any known limitations, quirks, or caveats
- Anything else a user should know to use the app effectively (dependencies, required hardware, file formats, etc.)
- If you are modifying an existing app, make sure the wiki page reflects your changes.
-->
- [ ] I will (or already) create(d) wiki document for my newly added feature
If you are modifying an existing app, make sure the wiki page reflects your changes.
---
@@ -64,4 +86,6 @@ If you are modifying an existing app, make sure the wiki page reflects your chan
- [ ] Attached proof of testing on real PortaPack hardware *(or marked N/A as a trusted contributor)*
- [ ] Attached proof of testing against a real emitter/receiver (if RF-related), or marked N/A with justification
- [ ] I understand that by getting this PR merged, I am implicitly agreeing to create or update the corresponding wiki page (including a main-screen screenshot, description, controls, and limitations)
- [ ] I own all rights to this code (i.e., all code contained in this PR), including compliant usage rights for third-party libraries, and I agree that this code is licensed under the license of this project (GPL-3.0).
- [ ] If any third-party libraries are used, I confirm that their licenses comply with the requirements for contributing to this repository.
- [ ] Reviewed the [Contributing Guidelines](https://github.com/portapack-mayhem/mayhem-firmware/wiki/Contributing-Guidelines)
+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/*
+65
View File
@@ -0,0 +1,65 @@
# PortaPack Mayhem Agent Guidelines
Welcome to the PortaPack Mayhem repository. This document sets mandatory rules and best practices for human developers and AI coding assistants. It exists to prevent unverified, untested code from overwhelming the maintainers, while still enabling AI to accelerate development.
## 🚫 STRICTLY PROHIBITED
1. **No Fully Automated Pull Requests**
Autonomous agents (bots, crawlers, automated workflows) must **never** open a Pull Request without direct human supervision, review, and explicit approval.
2. **No “Blind” AI-Generated Submissions**
If you cannot explain, debug, or maintain the C++/DSP code an AI produced for you, do **not** submit it. PRs that appear to be untested, AI-only “compile-and-pray” attempts will be closed immediately.
3. **No Unverified Hardware Interactions**
AI cannot test on real hardware. Do **not** submit code that has not been compiled and physically tested on a PortaPack device (or a highly accurate emulator). This includes UI rendering, RF transmission quality, and baseband stability.
## ✅ ENCOURAGED AI USE CASES
We strongly encourage using AI assistants (Copilot, ChatGPT, Claude, local LLMs) as pair-programmers. Excellent use cases include:
- **Bug Triage & Root-Causing:** Analyze crash logs, stack traces, and complex memory issues to identify the root cause of an open issue.
- **Code Comprehension:** Map Mayhems architecture, understand M0M4 IPC, or break down complex DSP algorithms.
- **Development Assistance:** Generate boilerplate UI code, write unit tests, optimize math functions, or draft documentation provided a human reviews and refines the output.
- **Targeted Refactoring:** Suggest cleaner, modern C++ for small legacy code segments. **Do not attempt to rewrite the entire codebase.**
## 📋 MANDATORY RULES FOR AIASSISTED CONTRIBUTIONS
When submitting AIassisted work, you must:
1. **Take Full Ownership**
You are strictly responsible for every line of code you submit, regardless of origin. Be prepared to answer detailed technical questions during review.
2. **Hardware Verification Required**
In your PR, explicitly state: *“I have compiled this code and tested it on physical PortaPack hardware.”* PRs without this statement will be rejected.
3. **Transparency (Recommended)**
If AI significantly helped architect a feature or fix a complex bug, mention it briefly in the PR description (e.g., *“Used an LLM to help optimize the DSP filter loop”*).
4. **Adhere to Coding Standards**
Ensure all code matches the existing Mayhem style and formatting before opening the PR.
## ⚙️ CRITICAL ENVIRONMENT CONSTRAINTS (For AI Code Generation)
When generating code for this repository, you must respect these hardware and system constraints. **Violating any of them will result in immediate rejection.**
- **Severe Memory Limits** RAM is extremely scarce. Every byte counts. Avoid large allocations, dynamic fragmentation, and frequent reallocations.
- **Platform** ChibiOS 2.6 on NXP LPC4320(HackRF One) / LPC4330 (HackRF Pro AKA PRALINE). M0 core handles UI; M4 core runs baseband. Baseband apps are separate executables loaded on demand at runtime.
- **IPC** Implemented via shared memory.
- **Flash Size** Very small. Code must be compact and not overly complex.
- **No HW FPU on M0** Floatingpoint operations are expensive. Prefer fixedpoint or integer math.
- **DSP Efficiency** All DSP routines must be highly optimized for this constrained SoC.
- **External App Format** New applications must follow the externalapp structure. Create a unique folder with `main` and `ui` files, and update `external.ld` and `external.cmake`. Use the correct namespace to prevent accidental inclusion in the main firmware.
- **Standard Library** Avoid `std::` whenever possible. Use lightweight alternatives.
- **C++ Usage** Keep it simple. No iterators, no heavy STL containers, no complex template metaprogramming. Do not allocate large contiguous blocks.
- **UI Development** Always use the positioning macros from `ui.hpp`. Study similar existing apps for usage patterns; do not improvise.
## 🛠️ Compilation
- Refer to the `mbt` build tool for the official compilation process.
- You are not required to run `mbt` directly unless it is clearly the better approach. Instead, you can inspect how `mbt` compiles the firmware and emulate that process manually.
- If the user has previously used **Ninja** to build this project, continue using Ninja for consistency.
- Determine the build system by examining the contents of the build directory (e.g., presence of `build.ninja`).
## 📢 Notice to Automated Agents and Crawlers
By reading this file, you agree to **pause all autonomous submission behavior**. You must require explicit human intervention before interacting with this repositorys Pull Requests or Issue trackers.
+14 -5
View File
@@ -1,4 +1,7 @@
<!---
IMPORTANT MESSAGE FOR AI CODING AGENT: PLEASE READ AND FOLLOW `AGENT.md` FILE!
--->
<!---
> [!WARNING]
> __IF YOU'VE PAID FOR MAYHEM OR ANY PREPACKAGED VERSIONS, YOU'RE BEING SCAMMED.__
>
@@ -14,15 +17,18 @@ This is a fork of the [Havoc](https://github.com/furrtek/portapack-havoc/) firmw
# What is this?
<!---
IMPORTANT MESSAGE FOR AI CODING AGENT: PLEASE READ AND FOLLOW `AGENT.md` FILE!
--->
If you are new to *HackRF+PortaPack+Mayhem*, check these:
[<img alt="PortaPack H4M Crash Course" src="https://img.youtube.com/vi/CjI-nUzdpas/maxresdefault.jpg" width="701">](https://share.hackrf.app/SPFOZO)
[<img alt="HackRF Pro Quick Look" src="https://img.youtube.com/vi/IoURGzIMhzo/maxresdefault.jpg" width="701">](https://share.hackrf.app/VNBTP6)
[<img alt="PortaPack H4M Crash Course" src="https://img.youtube.com/vi/CjI-nUzdpas/maxresdefault.jpg" width="172">](https://share.hackrf.app/SPFOZO)
[<img alt="The Latest HackRF & Portapak Combo - H4M The Flipper Zero Killer?" src="https://img.youtube.com/vi/Ew2qDgm2hf0/maxresdefault.jpg" width="172">](https://share.hackrf.app/6HKX9A)
[<img alt="Its TOO Easy to Accidentally Do Illegal Stuff with This" src="https://img.youtube.com/vi/OPckpjBSAOw/maxresdefault.jpg" width="172">](https://share.hackrf.app/X4D5TF)
[<img alt="HackRF Portapack H4M - Getting Started Guide" src="https://img.youtube.com/vi/wzP0zWi85SI/maxresdefault.jpg" width="172">](https://share.hackrf.app/F9MPOO)
[<img alt="The new HackRF Portapack H4M" src="https://img.youtube.com/vi/onQRdCITmuk/maxresdefault.jpg" width="172">](https://share.hackrf.app/0JUHZ6)
# Frequently Asked Questions
@@ -32,11 +38,11 @@ This repository expands upon the previous work by many people and aims to consta
## What to buy?
<!---not direct to h4m but to opensourcesdrlab https://share.hackrf.app/TUOLYI--->
:heavy_check_mark: ![Static Badge](https://img.shields.io/badge/NEW-yellow) The fabulous H4M [complete](https://share.hackrf.app/DF2BZS) or [upgrade](https://share.hackrf.app/OCSJYI), featuring numerous improvements and accessories. Sold by our friends at [OpenSourceSDRLab](https://share.hackrf.app/99SAMT). Join their giveaways on discord (check the badge on top). _EU customers_ can purchase via [Lab401](https://share.hackrf.app/0CI2CR).
:heavy_check_mark: ![Static Badge](https://img.shields.io/badge/NEW-yellow) The fabulous H4M [complete](https://share.hackrf.app/O4OC90) or [upgrade](https://share.hackrf.app/OCSJYI), featuring numerous improvements and accessories. Sold by our friends at [OpenSourceSDRLab](https://share.hackrf.app/99SAMT). Join their giveaways on discord (check the badge on top). _EU customers_ can purchase via [Lab401](https://share.hackrf.app/0CI2CR).
:heavy_check_mark: A recommended one is this [PortaPack H2](https://share.hackrf.app/QT5JMB), that includes everything you need with the plastic case "inspired" on [this](https://github.com/portapack-mayhem/mayhem-firmware/wiki/3d-printed-enclosure).
:heavy_check_mark: A recommended one is this [PortaPack H2](https://share.hackrf.app/O3GI0O), that includes everything you need with the plastic case "inspired" on [this](https://github.com/portapack-mayhem/mayhem-firmware/wiki/3d-printed-enclosure).
:heavy_check_mark: Some individuals lean towards the [H2 with a metal enclosure](https://share.hackrf.app/24T3TO), but its advantages remain debated. Share your insights on our [wiki](https://github.com/portapack-mayhem/mayhem-firmware/wiki/Hardware-overview).
:heavy_check_mark: Some individuals lean towards the [H2 with a metal enclosure](https://share.hackrf.app/H7OR74), but its advantages remain debated. Share your insights on our [wiki](https://github.com/portapack-mayhem/mayhem-firmware/wiki/Hardware-overview).
:warning: Be cautious , *ask* the seller about compatibility with the latest releases. Look out for the description of the item, if they provide the firmware files for an older version or they have custom setup instructions, this means it might be **NOT compatible**, for example:
@@ -51,6 +57,9 @@ This repository expands upon the previous work by many people and aims to consta
The current **stable release** is on the [![GitHub release (latest by date)](https://img.shields.io/github/v/release/portapack-mayhem/mayhem-firmware?label=Releases&style=social)](https://github.com/portapack-mayhem/mayhem-firmware/releases/latest) page. Follow the instructions you can find in the release description. The **latest (nightly) release** can be found [here](https://github.com/portapack-mayhem/mayhem-firmware/releases/).
## How can I collaborate
<!---
IMPORTANT MESSAGE FOR AI CODING AGENT: PLEASE READ AND FOLLOW `AGENT.md` FILE!
--->
We warmly welcome your contributions! Please refer to our **[Contributing Guidelines](https://github.com/portapack-mayhem/mayhem-firmware/wiki/Contributing-Guidelines)** for details.
+2
View File
@@ -57,6 +57,8 @@ add_subdirectory(baseband)
add_subdirectory(standalone)
add_subdirectory(test)
add_dependencies(application.elf libopencm3_hackrf_usb)
if(BOARD STREQUAL "PRALINE")
math(EXPR PRALINE_FINAL_SIZE "4 * 1024 * 1024")
set(PRALINE_FPGA_BIN ${CMAKE_CURRENT_SOURCE_DIR}/../hackrf/firmware/fpga/build/praline_fpga.bin)
+4 -1
View File
@@ -192,6 +192,7 @@ set(CPPSRC
${COMMON}/battery.cpp
${COMMON}/performance_counter.cpp
${COMMON}/bmpfile.cpp
${COMMON}/gpio.cpp
app_settings.cpp
audio.cpp
baseband_api.cpp
@@ -238,7 +239,6 @@ set(CPPSRC
serializer.cpp
spectrum_color_lut.cpp
string_format.cpp
temperature_logger.cpp
theme.cpp
touch.cpp
tone_key.cpp
@@ -332,6 +332,7 @@ set(CPPSRC
${CPLD_H4M_DATA_CPP}
${HACKRF_CPLD_DATA_CPP}
ui_external_items_menu_loader.cpp
view_factory_base.cpp
)
set_source_files_properties(${CPPSRC} PROPERTIES COMPILE_FLAGS -flto) # Add lto flag to the non-external sources only
@@ -511,3 +512,5 @@ add_custom_target(
${PROJECT_NAME}
DEPENDS ${PROJECT_NAME}.bin
)
add_dependencies(${PROJECT_NAME} baseband)
+8 -3
View File
@@ -50,6 +50,9 @@ void BoundSetting::parse(std::string_view value) {
case SettingType::I64:
parse_int(value, as<int64_t>());
break;
case SettingType::U64:
parse_int(value, as<uint64_t>());
break;
case SettingType::I32:
parse_int(value, as<int32_t>());
break;
@@ -87,6 +90,9 @@ void BoundSetting::write(File& file) const {
case SettingType::I64:
file.write(to_string_dec_int(as<int64_t>(), buffer, length), length);
break;
case SettingType::U64:
file.write(to_string_dec_uint(as<uint64_t>(), buffer, length), length);
break;
case SettingType::I32:
file.write(to_string_dec_int(as<int32_t>(), buffer, length), length);
break;
@@ -97,9 +103,8 @@ void BoundSetting::write(File& file) const {
file.write(to_string_dec_uint(as<uint8_t>(), buffer, length), length);
break;
case SettingType::String: {
const auto& str = as<std::string>();
file.write(str.data(), str.length());
break;
file.write_line(as<std::string>());
return;
}
case SettingType::Bool:
file.write(as<bool>() ? "1" : "0", 1);
+6 -1
View File
@@ -33,6 +33,7 @@
#include <variant>
#include "file.hpp"
#include "hackrf_hal.hpp"
#include "max283x.hpp"
#include "string_format.hpp"
@@ -47,6 +48,7 @@ class BoundSetting {
/* The type of bound setting. */
enum class SettingType : uint8_t {
I64,
U64,
I32,
U32,
U8,
@@ -59,6 +61,9 @@ class BoundSetting {
BoundSetting(std::string_view name, int64_t* target)
: name_{name}, target_{target}, type_{SettingType::I64} {}
BoundSetting(std::string_view name, uint64_t* target)
: name_{name}, target_{target}, type_{SettingType::U64} {}
BoundSetting(std::string_view name, int32_t* target)
: name_{name}, target_{target}, type_{SettingType::I32} {}
@@ -134,7 +139,7 @@ struct AppSettings {
uint8_t vga = 32;
uint8_t rx_amp = 0;
uint8_t tx_amp = 0;
uint8_t tx_gain = 35;
uint8_t tx_gain = hackrf::one::default_tx_gain_db;
uint32_t channel_bandwidth = 1;
uint32_t rx_frequency;
uint32_t tx_frequency;
+2 -2
View File
@@ -322,10 +322,10 @@ class BLERxView : public View {
{UI_POS_X(21), UI_POS_Y(0)}};
RSSI rssi{
{UI_POS_X(24), 0, UI_POS_WIDTH_REMAINING(6), 4}};
{UI_POS_X(24), 0, UI_POS_WIDTH_REMAINING(24), 4}};
Channel channel{
{UI_POS_X(24), 5, UI_POS_WIDTH_REMAINING(6), 4}};
{UI_POS_X(24), 5, UI_POS_WIDTH_REMAINING(24), 4}};
Labels label_sort{
{{UI_POS_X(0), UI_POS_Y(1)}, "Sort:", Theme::getInstance()->fg_light->foreground}};
+62 -53
View File
@@ -116,7 +116,7 @@ static std::uint64_t get_freq_by_channel_number(uint8_t channel_number) {
freq_hz = 2'428'000'000ull + (channel_number - 11) * 2'000'000ull;
break;
default:
freq_hz = UINT64_MAX;
freq_hz = 2'402'000'000ull; // Default to channel 37 frequency if invalid channel number
}
return freq_hz;
@@ -254,7 +254,7 @@ void BLETxView::send_packet() {
}
void BLETxView::start() {
if (file_path.empty()) {
if (file_path.empty() || num_packets == 0) {
file_error();
check_loop.set_value(false);
return;
@@ -266,6 +266,8 @@ void BLETxView::start() {
button_play.set_bitmap(&bitmap_stop);
is_running = true;
advCount = 0;
send_packet();
}
@@ -296,55 +298,61 @@ void BLETxView::on_timer() {
void BLETxView::on_tx_progress(const bool done, uint32_t progress) {
// TODO: make use of progress variable
(void)progress;
if (!is_active() || num_packets == 0) {
return;
}
if (done) {
if (is_active()) {
transmitter_model.disable();
if (auto_channel) {
switch (advCount) {
case 0:
channel_number = 37;
break;
case 1:
channel_number = 38;
break;
case 2:
channel_number = 39;
break;
}
field_frequency.set_value(get_freq_by_channel_number(channel_number));
advCount++;
if (advCount == 3) {
channel_number = 37;
packet_counter--;
field_frequency.set_value(get_freq_by_channel_number(channel_number));
if (packet_counter > 0) packet_counter--;
packetDone = true;
advCount = 0;
} else {
channel_number = 37 + advCount;
field_frequency.set_value(get_freq_by_channel_number(channel_number));
send_packet();
}
} else if (all_channels) {
advCount++;
if (advCount == 40) {
channel_number = 0;
field_frequency.set_value(get_freq_by_channel_number(channel_number));
if (packet_counter > 0) packet_counter--;
packetDone = true;
advCount = 0;
} else {
channel_number = advCount;
field_frequency.set_value(get_freq_by_channel_number(channel_number));
send_packet();
advCount++;
}
} else {
packet_counter--;
if (packet_counter > 0) packet_counter--;
packetDone = true;
}
}
}
// Reached end of current packet repeats.
if (packet_counter == 0) {
// Done sending all packets.
if (current_packet == (num_packets - 1)) {
current_packet = 0;
// Reached end of current packet repeats.
if (packet_counter == 0) {
// Done sending all packets.
current_packet++;
if (current_packet >= num_packets) {
current_packet = 0;
// If looping, restart from beginning.
if (check_loop.value()) {
update_current_packet(packets[current_packet], current_packet);
} else {
stop();
}
} else {
current_packet++;
// If looping, restart from beginning.
if (check_loop.value()) {
update_current_packet(packets[current_packet], current_packet);
} else {
stop();
}
} else {
update_current_packet(packets[current_packet], current_packet);
}
}
}
@@ -385,16 +393,20 @@ BLETxView::BLETxView(NavigationView& nav)
};
options_channel.on_change = [this](size_t, int32_t i) {
// If we selected Auto don't do anything and Auto will handle changing.
if (i == 40) {
auto_channel = true;
return;
auto_channel = (i == BLT_CHAN_AUTO);
all_channels = (i == BLT_CHAN_ALL);
advCount = 0;
if (auto_channel) {
channel_number = 37;
} else if (all_channels) {
channel_number = 0;
} else {
auto_channel = false;
channel_number = i;
}
field_frequency.set_value(get_freq_by_channel_number(i));
channel_number = i;
field_frequency.set_value(get_freq_by_channel_number(channel_number));
};
options_speed.on_change = [this](size_t, int32_t i) {
@@ -403,7 +415,7 @@ BLETxView::BLETxView(NavigationView& nav)
};
options_speed.set_selected_index(0);
options_channel.set_selected_index(3);
options_channel.set_by_value(BLT_CHAN_AUTO);
check_rand_mac.set_value(false);
check_rand_mac.on_select = [this](Checkbox&, bool v) {
@@ -413,8 +425,7 @@ BLETxView::BLETxView(NavigationView& nav)
button_open.on_select = [this](Button&) {
auto open_view = nav_.push<FileLoadView>(".TXT");
open_view->on_changed = [this](std::filesystem::path new_file_path) {
on_file_changed(new_file_path);
this->on_file_changed(new_file_path);
nav_.set_on_pop([this]() { button_play.focus(); });
};
};
@@ -427,7 +438,7 @@ BLETxView::BLETxView(NavigationView& nav)
64,
ENTER_KEYBOARD_MODE_ALPHA,
[this](std::string& buffer) {
on_save_file(buffer);
this->on_save_file(buffer);
});
};
@@ -506,10 +517,10 @@ void BLETxView::on_file_changed(const fs::path& new_file_path) {
}
do {
readUntil(data_file, packets[num_packets].macAddress, mac_address_size_str, ' ');
readUntil(data_file, packets[num_packets].advertisementData, max_packet_size_str, ' ');
readUntil(data_file, packets[num_packets].packetType, max_packet_type_str, ' ');
readUntil(data_file, packets[num_packets].packetCount, max_packet_repeat_str, '\n');
readUntil(data_file, packets[num_packets].macAddress, sizeof(packets[num_packets].macAddress), ' ');
readUntil(data_file, packets[num_packets].advertisementData, sizeof(packets[num_packets].advertisementData), ' ');
readUntil(data_file, packets[num_packets].packetType, sizeof(packets[num_packets].packetType), ' ');
readUntil(data_file, packets[num_packets].packetCount, sizeof(packets[num_packets].packetCount), '\n');
uint64_t macAddressSize = strlen(packets[num_packets].macAddress);
uint64_t advertisementDataSize = strlen(packets[num_packets].advertisementData);
@@ -542,10 +553,8 @@ void BLETxView::on_file_changed(const fs::path& new_file_path) {
num_packets++;
} while (num_packets < max_num_packets);
update_current_packet(packets[0], 0);
data_file.close();
update_current_packet(packets[0], 0);
}
}
@@ -570,6 +579,9 @@ void BLETxView::update_current_packet(BLETxPacket packet, uint32_t currentIndex)
std::vector<std::string> strings = splitIntoStrings(packet.advertisementData);
packet_counter = packet.packet_count;
current_packet = currentIndex;
text_packet_index.set(to_string_dec_uint(current_packet));
text_packets_sent.set(to_string_dec_uint(packet.packet_count));
@@ -578,9 +590,6 @@ void BLETxView::update_current_packet(BLETxPacket packet, uint32_t currentIndex)
progressbar.set_max(packet.packet_count);
packet_counter = packet.packet_count;
current_packet = currentIndex;
dataFile.create(dataTempFilePath);
for (const std::string& str : strings) {
+49 -7
View File
@@ -45,6 +45,9 @@
#include <string>
#include <memory>
#define BLT_CHAN_AUTO 40
#define BLT_CHAN_ALL 41
class BLELoggerTx {
public:
Optional<File::Error> append(const std::filesystem::path& filename) {
@@ -94,7 +97,7 @@ struct BLETxPacket {
char advertisementData[63];
char packetCount[11];
char packetType[17];
uint32_t packet_count;
uint32_t packet_count; // repeat the same packet count times
PKT_TYPE pduType;
};
@@ -145,6 +148,7 @@ class BLETxView : public View {
std::filesystem::path packet_save_path{bletx_dir / u"BLETX_????.TXT"};
uint8_t channel_number = 37;
bool auto_channel = false;
bool all_channels = false;
char randomMac[13] = "010203040506";
@@ -156,10 +160,10 @@ class BLETxView : public View {
int16_t auto_channel_period{6};
bool repeatLoop = false;
uint32_t packet_counter{0};
uint32_t num_packets{0};
uint32_t current_packet{0};
uint8_t packetTxCount{0};
uint32_t packet_counter{0}; // the counter (decr) of the actual packet as it can be sent multiple times
uint32_t num_packets{0}; // how many packets in the file
uint32_t current_packet{0}; // the actual packet's num, i'm sending if i have multiple packets
uint8_t packetTxCount{0}; // not used
bool packetDone = false;
bool random_mac = false;
bool file_override = false;
@@ -241,10 +245,48 @@ class BLETxView : public View {
OptionsField options_channel{
{11 * 8, 6 * 8},
5,
{{"Ch.37 ", 37},
{{"Ch.0", 0},
{"Ch.1", 1},
{"Ch.2", 2},
{"Ch.3", 3},
{"Ch.4", 4},
{"Ch.5", 5},
{"Ch.6", 6},
{"Ch.7", 7},
{"Ch.8", 8},
{"Ch.9", 9},
{"Ch.10", 10},
{"Ch.11", 11},
{"Ch.12", 12},
{"Ch.13", 13},
{"Ch.14", 14},
{"Ch.15", 15},
{"Ch.16", 16},
{"Ch.17", 17},
{"Ch.18", 18},
{"Ch.19", 19},
{"Ch.20", 20},
{"Ch.21", 21},
{"Ch.22", 22},
{"Ch.23", 23},
{"Ch.24", 24},
{"Ch.25", 25},
{"Ch.26", 26},
{"Ch.27", 27},
{"Ch.28", 28},
{"Ch.29", 29},
{"Ch.30", 30},
{"Ch.31", 31},
{"Ch.32", 32},
{"Ch.33", 33},
{"Ch.34", 34},
{"Ch.35", 35},
{"Ch.36", 36},
{"Ch.37", 37},
{"Ch.38", 38},
{"Ch.39", 39},
{"Auto", 40}}};
{"Auto", BLT_CHAN_AUTO},
{"All", BLT_CHAN_ALL}}};
OptionsField options_adv_type{
{17 * 8, 6 * 8},
+2 -3
View File
@@ -75,7 +75,7 @@ void RecentEntriesTable<AircraftRecentEntries>::draw(
uint8_t firstcolwidth = columns.at(0).second;
ipc.resize(firstcolwidth, ' '); // Make sure this is always match the first column's width that is dynamic.
entry_string += ipc + to_string_dec_uint((unsigned int)(entry.pos.altitude / 100), 4);
entry_string += ipc + to_string_dec_int((int32_t)(entry.pos.altitude / 100), 4);
if (entry.velo.type == SPD_IAS && entry.pos.alt_valid) { // IAS can be converted to TAS
// It is generally accepted that for every thousand feet of altitude,
@@ -668,9 +668,8 @@ void ADSBRxView::on_frame(const ADSBFrameMessage* message) {
(raw_data[3] & 15);
// The final altitude is due to the resulting number multiplied by 25, minus 1000.
// altitude = 25N - 1000 (ft); minimum is -1000 ft when N=0 (Q=1, M=0 per ICAO Annex 10).
altitude = 25 * n - 1000;
if (altitude < 0)
altitude = 0;
} // else N is an 11 bit Gillham coded altitude
}
+1
View File
@@ -238,6 +238,7 @@ APRSTXView::APRSTXView(NavigationView& nav) {
process_coordinates(lat, lon);
});
};
// process_coordinates(last_lat, last_lon); //don't load last, so won't confuse users
}
+8 -5
View File
@@ -52,7 +52,6 @@ void BattinfoView::update_result() {
text_voltage.set("UNKNOWN");
text_current.set("-");
text_charge.set("-");
// text_cycles.set("-");
text_ttef.set("-");
text_method.set("-");
// text_warn.set("");
@@ -60,7 +59,9 @@ void BattinfoView::update_result() {
}
bool uichg = false;
uint8_t valid_mask = 0;
battery::BatteryManagement::getBatteryInfo(valid_mask, percent, voltage, current);
bool battMayChanged = false;
battery::BatteryManagement::getBatteryInfo(valid_mask, percent, voltage, current, battMayChanged);
text_state.set((battMayChanged || !persistent_memory::battery_cap_valid()) ? "Needs config." : "ok");
// update text fields
if (percent <= 100 && (valid_mask & battery::BatteryManagement::BATT_VALID_VOLTAGE) == battery::BatteryManagement::BATT_VALID_VOLTAGE)
text_percent.set(to_string_dec_uint(percent) + " %");
@@ -76,6 +77,7 @@ void BattinfoView::update_result() {
labels_opt.hidden(false);
text_current.hidden(false);
text_charge.hidden(false);
text_state.hidden(false);
text_current.set(to_string_dec_int(current) + " mA");
if (current >= 25) // when >25mA it is charging in any scenario
text_charge.set("Charging");
@@ -98,7 +100,7 @@ void BattinfoView::update_result() {
labels_opt.hidden(true);
text_current.hidden(true);
text_charge.hidden(true);
// text_cycles.hidden(true);
text_state.hidden(true);
text_ttef.hidden(true);
// text_warn.set("");
}
@@ -143,9 +145,10 @@ void BattinfoView::update_result() {
} else {
text_method.set("Voltage");
}
if (battMayChanged) text_capacity.set_style(Theme::getInstance()->fg_red);
if (uichg) set_dirty();
// to update status bar too, send message in behalf of batt manager
BatteryStateMessage msg{valid_mask, percent, current >= 25, voltage};
BatteryStateMessage msg{valid_mask, percent, current >= 25, voltage, battMayChanged};
EventDispatcher::send_message(msg);
}
@@ -161,7 +164,7 @@ BattinfoView::BattinfoView(NavigationView& nav)
&button_settings,
&button_exit,
&text_capacity,
// &text_cycles,
//&text_state, //disabled for now
// &text_warn,
&text_ttef});
+7 -6
View File
@@ -64,7 +64,7 @@ class BattinfoView : public View {
{{UI_POS_X(2), UI_POS_Y(5)}, "Current:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(2), UI_POS_Y(6)}, "Charge:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(2), UI_POS_Y(7)}, "TTF/E:", Theme::getInstance()->fg_light->foreground},
// {{UI_POS_X(2), UI_POS_Y(8)}, "Cycles:", Theme::getInstance()->fg_light->foreground},
//{{UI_POS_X(2), UI_POS_Y(8)}, "State:", Theme::getInstance()->fg_light->foreground}, //for now, this is disabled. see max ic implementation
{{UI_POS_X(2), UI_POS_Y(10)}, "Change settings:", Theme::getInstance()->fg_light->foreground},
};
@@ -90,13 +90,14 @@ class BattinfoView : public View {
{UI_POS_X(13), UI_POS_Y(7), UI_POS_WIDTH(11), UI_POS_HEIGHT(1)},
"-"};
/* Text text_cycles{
{UI_POS_X(13), UI_POS_Y(8), UI_POS_WIDTH(11), UI_POS_HEIGHT(1)},
"-"};
Text text_state{
{UI_POS_X(13), UI_POS_Y(8), UI_POS_WIDTH_REMAINING(13), UI_POS_HEIGHT(1)},
"ok"};
/*
Text text_warn{
{UI_POS_X(1), UI_POS_Y(9), screen_width, UI_POS_HEIGHT(2)},
""}; */
""};
*/
Button button_settings{
{UI_POS_X(2), UI_POS_Y(11) + 5, UI_POS_WIDTH(10), UI_POS_HEIGHT(2)},
+2 -2
View File
@@ -3176,9 +3176,9 @@ void RFFC5072StatusView::refresh_status() {
// Blink LED
/*for (int i = 0; i < 3; i++) {
hackrf::one::led_rx.on();
hackrf::one::led_rx.setActive();
chThdSleepMilliseconds(100);
hackrf::one::led_rx.off();
hackrf::one::led_rx.setInactive();
chThdSleepMilliseconds(100);
}*/
} else {
+6 -2
View File
@@ -103,7 +103,8 @@ DfuMenu2::DfuMenu2(NavigationView& nav)
&text_info_line_8,
&text_info_line_9,
&text_info_line_10,
&text_info_line_11});
&text_info_line_11,
&text_info_line_12});
}
void DfuMenu2::paint(Painter& painter) {
@@ -118,9 +119,12 @@ void DfuMenu2::paint(Painter& painter) {
text_info_line_9.set(to_string_dec_uint(portapack::transmitter_model.target_frequency(), 10));
text_info_line_10.set(to_string_dec_uint(portapack::transmitter_model.baseband_bandwidth(), 10));
text_info_line_11.set(to_string_dec_uint(portapack::transmitter_model.sampling_rate(), 10));
text_info_line_12.set(std::string("M") + (radio::get_mixer_invert() ? "Y" : "N") +
" B" + (radio::get_baseband_invert() ? "Y" : "N") +
" IQ" + ((radio::get_mixer_invert() ^ radio::get_baseband_invert()) ? "Y" : "N"));
constexpr auto margin = 5;
constexpr auto lines = 11 + 2;
constexpr auto lines = 12 + 2;
painter.fill_rectangle(
{{5 * CHARACTER_WIDTH - margin, 3 * LINE_HEIGHT - margin},
@@ -96,6 +96,7 @@ class DfuMenu2 : public View {
{{5 * CHARACTER_WIDTH, 13 * LINE_HEIGHT}, "TX Freq:", Theme::getInstance()->fg_darkcyan->foreground},
{{5 * CHARACTER_WIDTH, 14 * LINE_HEIGHT}, "TX BW:", Theme::getInstance()->fg_darkcyan->foreground},
{{5 * CHARACTER_WIDTH, 15 * LINE_HEIGHT}, "TX SampR:", Theme::getInstance()->fg_darkcyan->foreground},
{{5 * CHARACTER_WIDTH, 16 * LINE_HEIGHT}, "IQ Inv:", Theme::getInstance()->fg_darkcyan->foreground},
};
Text text_info_line_1{{14 * CHARACTER_WIDTH, 5 * LINE_HEIGHT, 10 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
@@ -109,6 +110,7 @@ class DfuMenu2 : public View {
Text text_info_line_9{{14 * CHARACTER_WIDTH, 13 * LINE_HEIGHT, 10 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
Text text_info_line_10{{14 * CHARACTER_WIDTH, 14 * LINE_HEIGHT, 10 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
Text text_info_line_11{{14 * CHARACTER_WIDTH, 15 * LINE_HEIGHT, 10 * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
Text text_info_line_12{{(14 + 1) * CHARACTER_WIDTH, 16 * LINE_HEIGHT, (10 - 1) * CHARACTER_WIDTH, 1 * LINE_HEIGHT}, ""};
};
} /* namespace ui */
+11 -4
View File
@@ -43,6 +43,7 @@ namespace ui {
static const fs::path txt_ext{u".TXT"};
static const fs::path ppl_ext{u".PPL"};
static const fs::path c8_ext{u".C8"};
static const fs::path cu8_ext{u".CU8"};
static const fs::path c16_ext{u".C16"};
static const fs::path cxx_ext{u".C*"};
static const fs::path png_ext{u".PNG"};
@@ -88,8 +89,11 @@ fs::path get_partner_file(fs::path path) {
path.replace_extension(txt_ext);
else if (path_iequal(ext, txt_ext)) {
path.replace_extension(c8_ext);
if (!fs::file_exists(path))
path.replace_extension(cu8_ext);
if (!fs::file_exists(path))
path.replace_extension(c16_ext);
} else
return {};
@@ -386,6 +390,7 @@ std::string get_stem(std::string t) {
return t.substr(0, index);
}
}
std::string get_filename(std::string _s) {
const auto index = _s.find_last_of("/");
if (index == _s.npos) {
@@ -394,6 +399,7 @@ std::string get_filename(std::string _s) {
return _s.substr(index + 1);
}
}
void FileManBaseView::refresh_list() {
if (on_refresh_widgets)
on_refresh_widgets(false);
@@ -404,8 +410,6 @@ void FileManBaseView::refresh_list() {
menu_view.clear();
for (const auto& entry : entry_list) {
auto entry_name = std::string{entry.path.length() <= max_filename_length ? entry.path : entry.path.substr(0, max_filename_length)};
if (entry.is_directory) {
std::string size_str{};
if (entry.path == str_next || entry.path == str_back) {
@@ -414,8 +418,10 @@ void FileManBaseView::refresh_list() {
size_str = (entry.path == parent_dir_path.string()) ? "" : to_string_dec_uint(file_count(current_path / entry.path));
}
// CRITICAL: Use "\t" (Tab) to separate the path and the size!
// Do not use spaces or padding here. The menu will handle the alignment.
menu_view.add_item(
{entry_name.substr(0, max_filename_length) + std::string((max_filename_length + 1) - entry_name.length(), ' ') + size_str,
{entry.path + "\t" + size_str,
Theme::getInstance()->fg_yellow->foreground,
&bitmap_icon_dir,
[this](KeyEvent key) {
@@ -427,8 +433,9 @@ void FileManBaseView::refresh_list() {
const auto& assoc = get_assoc(get_extension(entry.path));
auto size_str = to_string_file_size(entry.size);
// CRITICAL: Use "\t" (Tab) to separate the path and the size!
menu_view.add_item(
{entry_name.substr(0, max_filename_length) + std::string((max_filename_length + 1) - entry_name.length(), ' ') + size_str,
{entry.path + "\t" + size_str,
assoc.color,
assoc.icon,
[this](KeyEvent key) {
+1
View File
@@ -80,6 +80,7 @@ class FileManBaseView : public View {
{u".PNG", &bitmap_icon_file_image, ui::Color::green()},
{u".BMP", &bitmap_icon_file_image, ui::Color::green()},
{u".C8", &bitmap_icon_file_iq, ui::Color::dark_cyan()},
{u".CU8", &bitmap_icon_file_iq, ui::Color::dark_cyan()},
{u".C16", &bitmap_icon_file_iq, ui::Color::dark_cyan()},
{u".WAV", &bitmap_icon_file_wav, ui::Color::dark_magenta()},
{u".PPL", &bitmap_icon_file_iq, ui::Color::white()}, // Playlist/Replay
@@ -32,7 +32,7 @@ static const char* hackrf_magic = "HACKRFFW";
#define FIRST_CHECKSUM_NIGHTLY 240125
Thread* FlashUtilityView::thread{nullptr};
static constexpr size_t max_filename_length = 26;
static constexpr size_t max_filename_length = 60; // max length of filename
bool valid_firmware_file(std::filesystem::path::string_type path) {
File firmware_file;
@@ -134,12 +134,6 @@ bool FlashUtilityView::endsWith(const std::u16string& str, const std::u16string&
}
}
void FlashUtilityView::wait_till_loaded() {
while (!isLoaded) {
chThdSleepMilliseconds(50);
}
}
std::filesystem::path FlashUtilityView::extract_tar(std::filesystem::path::string_type path, ui::Painter& painter) {
//
painter.fill_rectangle(
@@ -157,6 +151,7 @@ std::filesystem::path FlashUtilityView::extract_tar(std::filesystem::path::strin
bool FlashUtilityView::flash_firmware(std::filesystem::path::string_type path) {
ui::Painter painter;
menu_view.hidden(true);
if (endsWith(path, u".tar")) {
// extract, then update
path = extract_tar(u'/' + path, painter).native();
@@ -166,13 +161,14 @@ bool FlashUtilityView::flash_firmware(std::filesystem::path::string_type path) {
painter.fill_rectangle({0, 50, portapack::display.width(), 90}, Theme::getInstance()->bg_darkest->background);
painter.draw_string({0, 60}, *Theme::getInstance()->fg_red, "BAD FIRMWARE FILE OR W/R ERR");
chThdSleepMilliseconds(5000);
menu_view.hidden(false);
return false;
}
painter.fill_rectangle(
{0, 0, portapack::display.width(), portapack::display.height()},
Theme::getInstance()->bg_darkest->background);
painter.draw_string({12, 24}, this->nav_.style(), "This will take 15 seconds.");
painter.draw_string({12, 24}, this->nav_.style(), "This will take 15-45 sec.");
painter.draw_string({12, 64}, this->nav_.style(), "Please wait while LED RX");
painter.draw_string({12, 84}, this->nav_.style(), "is on and TX is flashing.");
painter.draw_string({12, 124}, this->nav_.style(), "Device will then restart.");
@@ -49,7 +49,6 @@ class FlashUtilityView : public View {
std::string title() const override { return "Flash Utility"; };
bool flash_firmware(std::filesystem::path::string_type path);
void wait_till_loaded();
private:
NavigationView& nav_;
@@ -68,14 +67,6 @@ class FlashUtilityView : public View {
void firmware_selected(std::filesystem::path::string_type path);
bool endsWith(const std::u16string& str, const std::u16string& suffix);
bool isLoaded = false;
uint8_t refreshcnt = 0;
MessageHandlerRegistration message_handler_frame_sync{
Message::ID::DisplayFrameSync,
[this](const Message* const) {
refreshcnt++;
if (refreshcnt > 5) isLoaded = true;
}};
};
} /* namespace ui */
+1
View File
@@ -172,6 +172,7 @@ void IQTrimView::update_range_controls(iq::TrimRange trim_range) {
void IQTrimView::profile_capture() {
power_buckets_.clear();
power_buckets_.resize(screen_width);
iq::PowerBuckets buckets{
.p = power_buckets_.data(),
.size = power_buckets_.size()};
@@ -24,6 +24,7 @@
#include "ui_looking_glass_app.hpp"
#include "convert.hpp"
#include "file_reader.hpp"
#include "file.hpp"
#include "string_format.hpp"
#include "audio.hpp"
#include "file_path.hpp"
@@ -31,6 +32,7 @@
using namespace portapack;
namespace ui {
void GlassView::focus() {
range_presets.focus();
}
@@ -44,7 +46,7 @@ GlassView::~GlassView() {
// Function to map the value from one range to another
int32_t GlassView::map(int32_t value, int32_t fromLow, int32_t fromHigh, int32_t toLow, int32_t toHigh) {
return toLow + (value - fromLow) * (toHigh - toLow) / (fromHigh - fromLow);
return toLow + ((value - fromLow) * (toHigh - toLow) + (fromHigh - fromLow) / 2) / (fromHigh - fromLow);
}
void GlassView::update_display_beep() {
@@ -128,8 +130,9 @@ void GlassView::reset_live_view() {
max_freq_power = -1000;
// Clear screen in peak mode.
const int spectrum_height = screen_height - spectrum_y;
if (live_frequency_view == 2)
display.fill_rectangle({{0, 108 + 16}, {screen_width, screen_height - (108 + 16)}}, {0, 0, 0});
display.fill_rectangle({{0, spectrum_y}, {screen_width, spectrum_height}}, {0, 0, 0});
}
void GlassView::add_spectrum_pixel(uint8_t power) {
@@ -144,11 +147,9 @@ void GlassView::add_spectrum_pixel(uint8_t power) {
constexpr float rssi_voltage_min = 0.4;
constexpr float rssi_voltage_max = 2.2;
constexpr float adc_voltage_max = 3.3;
constexpr int raw_min = rssi_sample_range * rssi_voltage_min / adc_voltage_max;
constexpr int raw_max = rssi_sample_range * rssi_voltage_max / adc_voltage_max;
constexpr int raw_delta = raw_max - raw_min;
const range_t<int> y_max_range{0, screen_height - (108 + 16)};
constexpr int raw_min = 0.5f + rssi_sample_range * rssi_voltage_min / adc_voltage_max;
constexpr int raw_max = 0.5f + rssi_sample_range * rssi_voltage_max / adc_voltage_max;
const int spectrum_height = screen_height - spectrum_y;
// drawing and keeping track of max freq
for (uint16_t xpos = 0; xpos < screen_width; xpos++) {
// save max powerwull freq
@@ -156,17 +157,21 @@ void GlassView::add_spectrum_pixel(uint8_t power) {
max_freq_power = spectrum_data[xpos];
max_freq_hold = get_freq_from_bin_pos(xpos);
}
int16_t point = y_max_range.clip(((spectrum_data[xpos] - raw_min) * (screen_height - (108 + 16))) / raw_delta);
uint8_t color_gradient = (point * 255) / 212;
int16_t point = map(spectrum_data[xpos], 0, 255, 0, spectrum_height);
// clear if not in peak view
if (live_frequency_view != 2) {
display.fill_rectangle({{xpos, 108 + 16}, {1, screen_height - point}}, {0, 0, 0});
display.fill_rectangle({{xpos, spectrum_y}, {1, screen_height - point}}, {0, 0, 0});
}
display.fill_rectangle({{xpos, screen_height - point}, {1, point}}, {color_gradient, 0, uint8_t(255 - color_gradient)});
display.fill_rectangle({{xpos, screen_height - point}, {1, point}}, gradient.lut[spectrum_data[xpos]]);
}
// indicate RSSI min and max power
if (show_rssi_guides) {
display.fill_rectangle({{0, screen_height - map(raw_min, 0, 255, 0, spectrum_height)}, {screen_width, 1}}, -gradient.lut[raw_min]);
display.fill_rectangle({{0, screen_height - map(raw_max, 0, 255, 0, spectrum_height)}, {screen_width, 1}}, -gradient.lut[raw_max]);
}
if (last_max_freq != max_freq_hold) {
last_max_freq = max_freq_hold;
freq_stats.set("MAX HOLD: " + to_string_short_freq(max_freq_hold));
freq_stats.set("MAX: " + to_string_short_freq(max_freq_hold));
}
plot_marker(marker_pixel_index);
} else {
@@ -323,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);
@@ -343,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)
@@ -392,7 +400,8 @@ GlassView::GlassView(
&button_marker_plus,
&button_jump,
&button_rst,
&freq_stats});
&button_rssi,
&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());
@@ -446,6 +455,7 @@ GlassView::GlassView(
freq_stats.hidden(true);
button_jump.hidden(true);
button_rst.hidden(true);
button_rssi.hidden(true);
display.scroll_set_area(109, screen_height - 1); // Restart scroll on the correct coordinates.
break;
@@ -456,6 +466,7 @@ GlassView::GlassView(
freq_stats.hidden(false);
button_jump.hidden(false);
button_rst.hidden(false);
button_rssi.hidden(false);
break;
case 2: // PEAK
@@ -466,6 +477,7 @@ GlassView::GlassView(
freq_stats.hidden(false);
button_jump.hidden(false);
button_rst.hidden(false);
button_rssi.hidden(false);
break;
}
@@ -535,6 +547,31 @@ GlassView::GlassView(
reset_live_view();
};
button_rssi.on_select = [this](Button& button) {
show_rssi_guides = !show_rssi_guides;
if (show_rssi_guides) {
button.set_style(Theme::getInstance()->fg_green);
} else {
button.set_style(Theme::getInstance()->bg_darkest);
}
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:
@@ -585,34 +622,39 @@ void GlassView::on_freqchg(int64_t freq) {
}
void GlassView::load_presets() {
File presets_file;
auto error = presets_file.open(looking_glass_dir / u"PRESETS.TXT");
presets_db.clear();
// Add the "Manual" entry.
presets_db.push_back({0, 0, "Manual"});
if (!error) {
auto reader = FileLineReader(presets_file);
for (const auto& line : reader) {
if (line.length() == 0 || line[0] == '#')
continue;
scan_root_files(looking_glass_dir, u"*.TXT", [this](const std::filesystem::path& path) {
if (path.empty() || path.native()[0] == u'.')
return;
auto cols = split_string(line, ',');
if (cols.size() != 3)
continue;
File presets_file;
auto error = presets_file.open(looking_glass_dir / path);
if (!error) {
auto reader = FileLineReader(presets_file);
for (const auto& line : reader) {
if (line.length() == 0 || line[0] == '#')
continue;
preset_entry entry{};
parse_int(cols[0], entry.min);
parse_int(cols[1], entry.max);
entry.label = trimr(cols[2]);
auto cols = split_string(line, ',');
if (cols.size() != 3)
continue;
if (entry.min == 0 || entry.max == 0 || entry.min >= entry.max)
continue; // Invalid line.
preset_entry entry{};
parse_int(cols[0], entry.min);
parse_int(cols[1], entry.max);
entry.label = trimr(cols[2]);
presets_db.emplace_back(std::move(entry));
if (entry.max == 0 || entry.min >= entry.max)
continue; // Invalid line.
presets_db.emplace_back(std::move(entry));
}
}
}
});
populate_presets();
}
@@ -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
@@ -104,6 +105,7 @@ class GlassView : public View {
};
void on_freqchg(int64_t freq);
// Function to map the value from one range to another
int32_t map(int32_t value, int32_t fromLow, int32_t fromHigh, int32_t toLow, int32_t toHigh);
std::vector<preset_entry> presets_db{};
void manage_beep_audio();
@@ -128,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};
@@ -163,40 +167,44 @@ class GlassView : public View {
uint8_t bin_length = screen_width;
uint8_t offset = 0;
uint8_t ignore_dc = 0;
bool show_rssi_guides = false;
// start of spectrum area
static constexpr int spectrum_y = (108 + 16);
Labels labels{
{{0, UI_POS_Y(0)}, "MIN: MAX: LNA VGA ", Theme::getInstance()->fg_light->foreground},
{{0, 1 * 16}, "RANGE: FILTER: AMP:", Theme::getInstance()->fg_light->foreground},
{{0, 2 * 16}, "P:", Theme::getInstance()->fg_light->foreground},
{{0, 3 * 16}, "MARKER( / ): MHz", Theme::getInstance()->fg_light->foreground},
{{0, 4 * 16}, "RES: VOL:", Theme::getInstance()->fg_light->foreground}};
{{0, UI_POS_Y(1)}, "RANGE: FILTER: AMP:", Theme::getInstance()->fg_light->foreground},
{{0, UI_POS_Y(2)}, "P:", Theme::getInstance()->fg_light->foreground},
{{0, UI_POS_Y(3)}, "MARKER( / ): MHz", Theme::getInstance()->fg_light->foreground},
{{0, UI_POS_Y(4)}, "RES: VOL:", Theme::getInstance()->fg_light->foreground}};
NumberField field_frequency_min{
{4 * 8, UI_POS_Y(0)},
{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{
{13 * 8, UI_POS_Y(0)},
{UI_POS_X(13), UI_POS_Y(0)},
4,
{1, 7200},
{1, LOOKING_GLASS_MAX_FREQ_MHZ},
1, // number of steps by encoder delta
' '};
LNAGainField field_lna{
{21 * 8, UI_POS_Y(0)}};
{UI_POS_X(21), UI_POS_Y(0)}};
VGAGainField field_vga{
{27 * 8, UI_POS_Y(0)}};
{UI_POS_X(27), UI_POS_Y(0)}};
TextField field_range{
{6 * 8, 1 * 16, 6 * 8, 16},
{UI_POS_X(6), UI_POS_Y(1), UI_POS_WIDTH(6), UI_POS_HEIGHT(1)},
""};
OptionsField filter_config{
{20 * 8, 1 * 16},
{UI_POS_X(20), UI_POS_Y(1)},
4,
{
{"OFF ", 0},
@@ -205,41 +213,47 @@ class GlassView : public View {
}};
RFAmpField field_rf_amp{
{28 * 8, 1 * 16}};
{UI_POS_X(28), UI_POS_Y(1)}};
OptionsField range_presets{
{2 * 8, 2 * 16},
{UI_POS_X(2), UI_POS_Y(2)},
20,
{}};
ButtonWithEncoder button_beep_squelch{
{screen_width - 8 * 8, 2 * 16 + 4, 8 * 8, 1 * 8},
{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{
{12 * 8, 3 * 16, 9 * 8, 16},
{UI_POS_X(12), UI_POS_Y(3), UI_POS_WIDTH(9), UI_POS_HEIGHT(1)},
""};
Button button_marker_minus{
{7 * 8, 3 * 16 + 4, 1 * 8, 1 * 8},
{UI_POS_X(7), UI_POS_Y(3.25), UI_POS_WIDTH(1), UI_POS_HEIGHT(0.5)},
"-"};
Button button_marker_plus{
{9 * 8, 3 * 16 + 4, 1 * 8, 1 * 8},
{UI_POS_X(9), UI_POS_Y(3.25), UI_POS_WIDTH(1), UI_POS_HEIGHT(0.5)},
"+"};
NumberField field_trigger{
{4 * 8, 4 * 16},
{UI_POS_X(4), UI_POS_Y(4)},
3,
{2, 128},
2,
' '};
AudioVolumeField field_volume{
{13 * 8, 4 * 16}};
{UI_POS_X(13), UI_POS_Y(4)}};
/*OptionsField steps_config{
{13 * 8, 4 * 16},
{UI_POS_X(13), UI_POS_Y(4)},
3,
{
{"1", 1},
@@ -251,7 +265,7 @@ class GlassView : public View {
}};*/
OptionsField scan_type{
{17 * 8, 4 * 16},
{UI_POS_X(17), UI_POS_Y(4)},
2,
{
{"F-", LOOKING_GLASS_FASTSCAN},
@@ -259,7 +273,7 @@ class GlassView : public View {
}};
OptionsField view_config{
{19 * 8, 4 * 16},
{UI_POS_X(19), UI_POS_Y(4)},
7,
{
{"SPCTR-V", 0},
@@ -268,7 +282,7 @@ class GlassView : public View {
}};
OptionsField level_integration{
{27 * 8, 4 * 16},
{UI_POS_X(27), UI_POS_Y(4)},
2,
{
{"x0", 0},
@@ -284,15 +298,19 @@ class GlassView : public View {
}};
Button button_jump{
{screen_width - 4 * 8, 5 * 16, 4 * 8, 16},
{UI_POS_X_RIGHT(4), UI_POS_Y(5), UI_POS_WIDTH(4), UI_POS_HEIGHT(1)},
"JMP"};
Button button_rst{
{screen_width - 9 * 8, 5 * 16, 4 * 8, 16},
{UI_POS_X_RIGHT(9), UI_POS_Y(5), UI_POS_WIDTH(4), UI_POS_HEIGHT(1)},
"RST"};
Button button_rssi{
{UI_POS_X_RIGHT(15), UI_POS_Y(5), UI_POS_WIDTH(5), UI_POS_HEIGHT(1)},
"RSSI"};
Text freq_stats{
{UI_POS_X(0), 5 * 16, screen_width - 10 * 8, 8},
{UI_POS_X(0), UI_POS_Y(5), UI_POS_WIDTH(14), UI_POS_HEIGHT(1)},
""};
MessageHandlerRegistration message_handler_spectrum_config{
+11 -3
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.
*
@@ -1163,6 +1163,7 @@ SetBatteryView::SetBatteryView(NavigationView& nav) {
&field_battcap,
&button_help_cap,
&checkbox_overridebatt,
&checkbox_battery_replaceable,
&checkbox_battery_charge_hint});
if (i2cdev::I2CDevManager::get_dev_by_model(I2C_DEVMDL::I2CDEVMDL_MAX17055)) add_children({&button_reset, &labels2});
@@ -1170,14 +1171,20 @@ SetBatteryView::SetBatteryView(NavigationView& nav) {
button_save.on_select = [&nav, this](Button&) {
pmem::set_ui_override_batt_calc(checkbox_overridebatt.value());
pmem::set_ui_battery_charge_hint(checkbox_battery_charge_hint.value());
pmem::set_battery_replaceable(checkbox_battery_replaceable.value());
battery::BatteryManagement::set_calc_override(checkbox_overridebatt.value());
if (((uint32_t)field_battcap.value() != pmem::battery_cap_mah()) || (!pmem::battery_cap_valid())) {
i2cdev::I2cDev_MAX17055* dev = (i2cdev::I2cDev_MAX17055*)i2cdev::I2CDevManager::get_dev_by_model(I2C_DEVMDL::I2CDEVMDL_MAX17055);
if (dev) dev->resetChangedFlag();
if ((((uint32_t)field_battcap.value() != pmem::battery_cap_mah()) || (!pmem::battery_cap_valid())) || (dev && dev->getIsBattChanged())) {
pmem::set_battery_cap_mah(field_battcap.value());
i2cdev::I2cDev_MAX17055* dev = (i2cdev::I2cDev_MAX17055*)i2cdev::I2CDevManager::get_dev_by_model(I2C_DEVMDL::I2CDEVMDL_MAX17055);
if (dev && !dev->reInit()) {
nav.display_modal("Error", "Battery gauge re-init failed");
return;
}
if (dev) {
dev->resetChangedFlag();
dev->update(); // resend the new data
}
}
send_system_refresh();
nav.pop();
@@ -1206,6 +1213,7 @@ SetBatteryView::SetBatteryView(NavigationView& nav) {
checkbox_overridebatt.set_value(pmem::ui_override_batt_calc());
checkbox_battery_charge_hint.set_value(pmem::ui_battery_charge_hint());
checkbox_battery_replaceable.set_value(pmem::battery_replaceable());
field_battcap.set_value(pmem::battery_cap_mah());
+10 -5
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.
*
@@ -1053,7 +1053,7 @@ class SetBatteryView : public View {
{{UI_POS_X_CENTER(17), UI_POS_Y(8)}, "Battery capacity", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(7), UI_POS_Y(9)}, "mAh", Theme::getInstance()->fg_light->foreground}};
Labels labels2{{{UI_POS_X(1), UI_POS_Y(11)}, "Reset IC's learned params.", Theme::getInstance()->fg_light->foreground}};
Labels labels2{{{UI_POS_X(1), UI_POS_Y_BOTTOM(7)}, "Reset IC's learned params.", Theme::getInstance()->fg_light->foreground}};
NumberField field_battcap{
{UI_POS_X(1), UI_POS_Y(9)},
@@ -1078,17 +1078,22 @@ class SetBatteryView : public View {
23,
"Charge hint"};
Checkbox checkbox_battery_replaceable{
{UI_POS_X(2), UI_POS_Y(11)},
23,
"Replaceable battery"};
Button button_cancel{
{UI_POS_X_CENTER(12) + UI_POS_WIDTH(8), UI_POS_Y_BOTTOM(4), UI_POS_WIDTH(12), UI_POS_HEIGHT(2)},
{UI_POS_X_CENTER(12) + UI_POS_WIDTH(8), UI_POS_Y_BOTTOM(3), UI_POS_WIDTH(12), UI_POS_HEIGHT(2)},
"Cancel",
};
Button button_save{
{UI_POS_X_CENTER(12) - UI_POS_WIDTH(8), UI_POS_Y_BOTTOM(4), UI_POS_WIDTH(12), UI_POS_HEIGHT(2)},
{UI_POS_X_CENTER(12) - UI_POS_WIDTH(8), UI_POS_Y_BOTTOM(3), UI_POS_WIDTH(12), UI_POS_HEIGHT(2)},
"Save"};
Button button_reset{
{UI_POS_X_CENTER(12) - UI_POS_WIDTH(8), UI_POS_Y(13), UI_POS_WIDTH(12), UI_POS_HEIGHT(2)},
{UI_POS_X_CENTER(12) - UI_POS_WIDTH(8), UI_POS_Y_BOTTOM(6), UI_POS_WIDTH(12), UI_POS_HEIGHT(2)},
"Reset",
};
};
@@ -186,26 +186,26 @@ void set_dirty() {
}
standalone_application_api_t api = {
/* .malloc = */ &alloc,
/* .calloc = */ &calloc,
/* .realloc = */ &realloc,
/* .free = */ &chHeapFree,
/* .create_thread = */ &create_thread,
/* .fill_rectangle = */ &fill_rectangle,
/* .swizzled_switches = */ &swizzled_switches,
/* .get_switches_state = */ &get_switches_state_ulong,
/* .fixed_5x8_glyph_data = */ ui::font::fixed_5x8.get_data(),
/* .fixed_8x16_glyph_data = */ ui::font::fixed_8x16.get_data(),
/* .fill_rectangle_unrolled8 = */ &fill_rectangle_unrolled8,
/* .draw_bitmap = */ &draw_bitmap,
/* .scroll_area_y = */ &scroll_area_y,
/* .scroll_set_area = */ &scroll_set_area,
/* .scroll_disable = */ &scroll_disable,
/* .scroll_set_position = */ &scroll_set_position,
/* .scroll = */ &scroll,
/* .i2c_read = */ &i2c_read,
/* .panic = */ &chDbgPanic,
/* .set_dirty = */ &set_dirty,
.malloc = &alloc,
.calloc = &calloc,
.realloc = &realloc,
.free = &chHeapFree,
.create_thread = &create_thread,
.fill_rectangle = &fill_rectangle,
.swizzled_switches = &swizzled_switches,
.get_switches_state = &get_switches_state_ulong,
.fixed_5x8_glyph_data = ui::font::fixed_5x8.get_data(),
.fixed_8x16_glyph_data = ui::font::fixed_8x16.get_data(),
.fill_rectangle_unrolled8 = &fill_rectangle_unrolled8,
.draw_bitmap = &draw_bitmap,
.scroll_area_y = &scroll_area_y,
.scroll_set_area = &scroll_set_area,
.scroll_disable = &scroll_disable,
.scroll_set_position = &scroll_set_position,
.scroll = &scroll,
.i2c_read = &i2c_read,
.panic = &chDbgPanic,
.set_dirty = &set_dirty,
// Version 3
.f_open = &ext_f_open,
.f_close = &ext_f_close,
+26
View File
@@ -361,6 +361,16 @@ void set_noaaapt_config() {
send_message(&message);
}
void set_vor_config(bool enabled) {
const VorRxConfigureMessage message{enabled};
send_message(&message);
}
void set_vor_tx_config(uint16_t radial_deg, bool ident_enabled, const std::string& ident, bool enabled) {
const VorTxConfigureMessage message{radial_deg, ident_enabled, ident.c_str(), enabled};
send_message(&message);
}
void set_flex_config() {
const FlexConfigureMessage message{};
send_message(&message);
@@ -432,6 +442,10 @@ void set_epirb_tx_config(EPIRBTXDataMessage& message) {
send_message(&message);
}
void set_epirb_rx_config(EPIRBRXConfig& message) {
send_message(&message);
}
void set_p25tx_data(const uint8_t* dibits, uint16_t frame_length) {
const size_t max_len = sizeof(shared_memory.bb_data.data);
if (frame_length > max_len) frame_length = max_len;
@@ -441,8 +455,20 @@ 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) {
if (baseband_image_running) {
chDbgPanic("BBRunning");
+5
View File
@@ -113,18 +113,23 @@ 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
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();
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 shutdown();
+258 -157
View File
@@ -25,12 +25,19 @@
#include "portapack_io.hpp"
#include "portapack.hpp"
#include "lpc43xx.inc"
#include "hackrf_hal.hpp"
using namespace hackrf::one;
#include "lpc43xx_cpp.hpp"
using namespace lpc43xx;
#ifdef PRALINE
#include "hackrf_gpio.hpp"
#include "gpio.hpp"
#endif
#ifdef PRALINE
extern "C" {
#include "fpga_bridge.h"
@@ -325,6 +332,12 @@ static constexpr ClockControl::MultiSynthSource get_si5351a_reference_clock_gene
? ClockControl::MultiSynthSource::PLLA
: ClockControl::MultiSynthSource::PLLB;
}
static constexpr ClockControl::MultiSynthSource get_si5351c_reference_clock_generator_pll(const ClockManager::ReferenceSource reference_source) {
return (reference_source == ClockManager::ReferenceSource::Xtal)
? ClockControl::MultiSynthSource::PLLA
: ClockControl::MultiSynthSource::PLLB;
}
#else
static constexpr ClockControl::MultiSynthSource get_si5351c_reference_clock_generator_pll(const ClockManager::ReferenceSource reference_source) {
return (reference_source == ClockManager::ReferenceSource::Xtal)
@@ -406,7 +419,11 @@ std::string ClockManager::get_freq() {
to_string_dec_uint((reference.frequency % 1000000) / 100, 4, '0') + " MHz";
}
static void portapack_tcxo_enable() {
void ClockManager::portapack_tcxo_enable() {
#ifdef PRALINE
gpio_control::clkin_ctrl.setActive();
set_p1_control(P1_Function::P22_ClkIn);
#endif
portapack::io.reference_oscillator(true);
/* Delay >10ms at 96MHz clock speed for reference oscillator to start. */
@@ -415,7 +432,10 @@ static void portapack_tcxo_enable() {
while (delay--);
}
static void portapack_tcxo_disable() {
void ClockManager::portapack_tcxo_disable() {
#ifdef PRALINE
gpio_control::clkin_ctrl.setInactive();
#endif
portapack::io.reference_oscillator(false);
}
@@ -423,47 +443,50 @@ static void portapack_tcxo_disable() {
using namespace hackrf::one;
void ClockManager::init_clock_generator() {
#ifdef PRALINE
// PRALINE: Configure clock input mux GPIO
// GPIO0_15 (clkin_ctrl) selects GP_CLKIN source:
// 0 = P1 connector (external)
// 1 = P22 (internal Si5351 CLK2)
constexpr GPIO gpio_clkin_ctrl = gpio[GPIO0_15];
gpio_clkin_ctrl.output();
gpio_clkin_ctrl.write(1); // CLKIN_SIGNAL_P22 = 1 = internal Si5351 CLK2
// Also enable MCU clock gate (GPIO0_8)
gpio_r9_mcu_clk_en.output();
gpio_r9_mcu_clk_en.write(1);
#else
// HackRF One r9: GPIO0_8 (mcu_clk_en) gates Si5351 CLK2/CLK7 to GP_CLKIN
if (hackrf_r9) {
gpio_r9_mcu_clk_en.output();
gpio_r9_mcu_clk_en.write(1);
}
#endif
clock_generator.reset();
clock_generator.set_crystal_internal_load_capacitance(CrystalInternalLoadCapacitance::XTAL_CL_8pF);
clock_generator.enable_fanout();
#ifdef PRALINE
/* PRALINE has Si5351A (NOT Si5351C like HackRF One OG).
* Must use Si5351A configuration: PLLA only, no CLKIN support.
*
* IMPORTANT: Follow HackRF reference sequence:
* 1. Set PLL input sources
* 2. Configure PLL and multisynths
* 3. Set clock control registers (AFTER multisynths!)
* 4. Reset PLLs
* 5. Enable outputs
constexpr size_t clock_generator_output_pro_mcu_clkin = 2;
auto si5351_clock_control_common = si5351a_clock_control_common;
/*
* Match the HackRF One init flow:
* 1. PLLA stays on the local 25MHz crystal.
* 2. PLLB is configured from the Si5351 CLKIN pin.
* 3. CLK2 temporarily drives GP_CLKIN directly from CLKIN so we can
* detect whether P22 is carrying a valid 10MHz reference.
* 4. Once the source is chosen, all runtime clocks are switched to the
* selected PLL and the working multisynths are programmed.
*/
clock_generator.set_pll_input_sources(si5351a_pll_input_sources);
clock_generator.set_pll_input_sources(si5351c_pll_input_sources);
/* Skip MCU CLKIN setup and reference detection for PRALINE - not applicable */
reference = Reference{ReferenceSource::Xtal, 0}; // PLLA
clock_generator.set_clock_control(
clock_generator_output_pro_mcu_clkin,
si5351_clock_control_common[clock_generator_output_pro_mcu_clkin]
.clk_src(ClockControl::ClockSource::CLKIN)
.clk_pdn(ClockControl::ClockPowerDown::Power_On));
clock_generator.enable_output(clock_generator_output_pro_mcu_clkin);
/* Clock control will be set AFTER multisynth configuration - see below */
chThdSleepMilliseconds(20);
reference = choose_reference();
clock_generator.disable_output(clock_generator_output_pro_mcu_clkin);
const auto ref_pll =
get_si5351c_reference_clock_generator_pll(reference.source);
const ClockControls si5351_clock_control = ClockControls{{
si5351_clock_control_common[0].ms_src(ref_pll),
si5351_clock_control_common[1].ms_src(ref_pll),
si5351_clock_control_common[2].ms_src(ref_pll),
si5351_clock_control_common[3].ms_src(ref_pll),
si5351_clock_control_common[4].ms_src(ref_pll),
si5351_clock_control_common[5].ms_src(ref_pll),
si5351_clock_control_common[6].ms_src(ref_pll),
si5351_clock_control_common[7].ms_src(ref_pll),
}};
clock_generator.set_clock_control_single_byte(si5351_clock_control);
#else
clock_generator.set_pll_input_sources(hackrf_r9
? si5351a_pll_input_sources
@@ -508,86 +531,50 @@ void ClockManager::init_clock_generator() {
#endif
#ifdef PRALINE
clock_generator.write_pll_single_byte(0, si5351_pll_a_afe_800m);
/* * Praline HackRF Pro Clock Assignments (800 MHz VCO Configuration)
* VCO Frequency: 800,000,000 Hz (Master Reference)
* * CLK0: AFE_CLK (MAX5864 Codec & FPGA ADC Interface)
* - Note: Defines hardware sample rate. Essential for WFM purity.
* * CLK1: SCT_CLK (iCE40 FPGA System/Timing Clock)
* - Note: Timing for SGPIO data bus; scales to 2x SR in wideband modes.
* * CLK2: MCU_CLKIN (LPC43xx MCU External Clock Input)
* - Note: Synchronizes MCU processing to the RF clock tree.
* * CLK3: SG_CLK (Switching Regulator/Internal Logic Sync) SMA Port 1
* - Note: Used for internal FPGA logic/gateware synchronization.
* * CLK4: P_CLK (MAX2831 Peripheral/Expansion Clock)
* - Note: Routed to expansion headers for external hardware sync.
* * CLK5: AUX_CLK (RFFC5371 Auxiliary reference for secondary logic)
* - Note: Provides additional timing flexibility for the iCE40 FPGA.
* * CLK6: SG_CLK (Switching Regulator/Internal Logic Sync) SMA Port 2
* - Note: Used for internal FPGA logic/gateware synchronization.
* * CLK7: Unused / Power-Down
* - State: Disabled (si5351a_ms6_7_off_reg)
* - Note: Kept OFF to reduce EMI/RFI near the RF front-end.
* * CLKOUT: Optional external clock output on the header.
*/
/* Write PLL A (800 MHz based on 25 MHz xtal for RF) and
* PLL B (800 MHZ based on 25 MHz xtalfor Digital)
* Use single-byte writes to debug I2C issues
*/
{
// Write PLLA (Registers 26-33)
/* Write PLL A configuration (Base 26) */
const auto& pll_a = si5351_pll_a_800_reg;
for (size_t i = 1; i < pll_a.size(); i++) {
clock_generator.write_register(pll_a[0] + i - 1, pll_a[i]);
}
// Write PLLB (Registers 34-41)
/* Write PLL B configuration (Base 34) */
const auto& pll_b = si5351_pll_b_800_reg;
const auto& pll_b = si5351c_pll_b_clkin_reg;
for (size_t i = 1; i < pll_b.size(); i++) {
clock_generator.write_register(pll_b[0] + i - 1, pll_b[i]);
}
}
// CLK0: DAFE_CLK initial 4 MHz
clock_generator.write_ms_single_byte(
0,
si5351_ms_afe_4m);
/* Write multisynth configurations using single-byte writes */
// These cover all active channels on the Praline board
clock_generator.write_ms_single_byte(0, si5351_ms_afe_4m); // CLK0: PLL A AFE Codec (4 MHz)
clock_generator.write_ms_single_byte(1, si5351_ms_10m); // CLK1: PLL B SGPIO/FPGA Timing (10 MHz)
clock_generator.write_ms_single_byte(2, si5351_ms_afe_40m); // CLK2: PLL A Audio and MCU Input (40 MHz)
clock_generator.write_ms_single_byte(3, si5351_ms_0_4m); // CLK3: PLL B SMA Port 1 Logic Sync (4 MHz or 10 MHz)
clock_generator.write_ms_single_byte(4, si5351_ms_afe_40m); // CLK4: PLL A MAX2831 Second IF (40 MHz)
clock_generator.write_ms_single_byte(5, si5351_ms_afe_40m); // CLK5: PLL A RFFC5071First IF (40 MHz)
clock_generator.write_ms_single_byte(6, si5351_ms_0_4m); // CLK6: PLL B SMA Port 2 Logic Sync (4 MHz or 10 MHz)
clock_generator.write_ms_single_byte(7, si5351_ms_0_4m); // CLK7: PLL B Unused (4 MHz)
// CLK1: DSCT_CLK initial 10 MHz
clock_generator.write_ms_single_byte(
1,
si5351_ms_10m);
/* NOW set clock control registers (AFTER multisynths per HackRF reference) */
const auto ref_pll_a = ClockControl::MultiSynthSource::PLLA;
const auto ref_pll_b = ClockControl::MultiSynthSource::PLLB;
const ClockControls si5351_clock_control = ClockControls{{
si5351a_clock_control_common[0].ms_src(ref_pll_a),
si5351a_clock_control_common[1].ms_src(ref_pll_b),
si5351a_clock_control_common[2].ms_src(ref_pll_a),
si5351a_clock_control_common[3].ms_src(ref_pll_b),
si5351a_clock_control_common[4].ms_src(ref_pll_a),
si5351a_clock_control_common[5].ms_src(ref_pll_a),
si5351a_clock_control_common[6].ms_src(ref_pll_b),
si5351a_clock_control_common[7].ms_src(ref_pll_b),
}};
// clock_generator.set_clock_control(si5351_clock_control);
// Use single-byte writes instead of multi-byte
clock_generator.set_clock_control_single_byte(si5351_clock_control);
// CLK2: MCU_CLK 40 MHz
clock_generator.write_ms_single_byte(
2,
si5351_ms_afe_40m);
// Don't write CLKS 3, 6, and 7 multisynth
// Ensure CLK3 clock control has Power_Off
// Verify output is disabled
clock_generator.disable_output(3);
clock_generator.disable_clock(3);
clock_generator.disable_output(6);
clock_generator.disable_clock(6);
clock_generator.disable_output(7);
clock_generator.disable_clock(7);
// CLK3: P2 clock mux, disabled below
clock_generator.write_ms_single_byte(
3,
si5351_ms_10m);
// CLK4: DXCVR_CLK 40 MHz
clock_generator.write_ms_single_byte(
4,
si5351_ms_afe_40m);
// CLK5: DMIX_CLK 40 MHz
clock_generator.write_ms_single_byte(
5,
si5351_ms_afe_40m);
{
const auto& ms6_7 = si5351a_ms6_7_off_reg;
for (size_t i = 1; i < ms6_7.size(); i++) {
clock_generator.write_register(ms6_7[0] + i - 1, ms6_7[i]);
}
}
#else
if (hackrf_r9) {
const PLLReg pll_reg = (reference.source == ReferenceSource::Xtal)
@@ -615,15 +602,14 @@ void ClockManager::init_clock_generator() {
// Wait for PLL(s) to lock.
#ifdef PRALINE
// PRALINE: Wait for 0x60 (0x20 | 0x40), PLLA and PLLB to lock (0x20 = LOL_A bit, 0x40 = LOL_B bit)
uint8_t device_status_mask = 0x60;
uint8_t device_status_mask =
(ref_pll == ClockControl::MultiSynthSource::PLLB) ? 0x40 : 0x20;
uint32_t pll_timeout = 100000;
while ((clock_generator.device_status() & device_status_mask) != 0 && pll_timeout > 0) {
pll_timeout--;
}
// Store PLL lock status for debugging
static volatile uint32_t pll_lock_timeout = pll_timeout;
(void)pll_lock_timeout;
clock_generator.enable_output(clock_generator_output_pro_mcu_clkin);
#else
// Wait for PLL(s) to lock - with timeout to prevent hang
@@ -694,7 +680,7 @@ ClockManager::Reference ClockManager::choose_reference() {
}
#else
if (hackrf_r9) {
gpio_r9_clkin_en.write(1);
gpio_control::r9_clkin_en.setActive();
volatile uint32_t delay = 240000 + 24000;
while (delay--);
}
@@ -709,7 +695,7 @@ ClockManager::Reference ClockManager::choose_reference() {
}
if (hackrf_r9) {
gpio_r9_clkin_en.write(0);
gpio_control::r9_clkin_en.setInactive();
}
#endif
@@ -723,13 +709,13 @@ void ClockManager::shutdown() {
void ClockManager::enable_codec_clocks() {
#ifdef PRALINE
/* PRALINE: CLK0 (AFE_CLK) for codec/FPGA, CLK1 (SCT_CLK) for FPGA timing.
* Reference hackrf_core.c shows PRALINE needs both CLK0 and CLK1. */
/* PRALINE: only gate the clocks owned by the RF datapath here.
* CLK2 is MCU_CLKIN and must stay independent of runtime RX/TX clock
* management or the SGPIO/GPIO subsystem can glitch mid-stream. */
clock_generator.enable_clock(clock_generator_output_og_codec); /* CLK0 MAX5864*/
clock_generator.enable_clock(clock_generator_output_og_cpld); /* CLK1 iCE40 FPGA*/
clock_generator.enable_clock(clock_generator_output_og_sgpio); /* CLK2 LPC43xx*/
clock_generator.enable_output_mask(
(1U << clock_generator_output_og_codec) | (1U << clock_generator_output_og_cpld) | (1U << clock_generator_output_og_sgpio));
(1U << clock_generator_output_og_codec) | (1U << clock_generator_output_og_cpld));
#else
if (hackrf_r9) {
clock_generator.enable_clock(clock_generator_output_r9_sgpio);
@@ -757,12 +743,11 @@ void ClockManager::disable_codec_clocks() {
* CLKx_DISABLE_STATE.
*/
#ifdef PRALINE
/* PRALINE: CLK0 (AFE_CLK), CLK1 (SCT_CLK), and CLK2 MCU used for codec/FPGA */
/* PRALINE: leave CLK2/MCU_CLKIN alone; only disable datapath-owned clocks. */
clock_generator.disable_output_mask(
(1U << clock_generator_output_og_codec) | (1U << clock_generator_output_og_cpld) | (1U << clock_generator_output_og_sgpio));
(1U << clock_generator_output_og_codec) | (1U << clock_generator_output_og_cpld));
clock_generator.disable_clock(clock_generator_output_og_codec);
clock_generator.disable_clock(clock_generator_output_og_cpld);
clock_generator.disable_clock(clock_generator_output_og_sgpio);
#else
if (hackrf_r9) {
clock_generator.disable_output_mask(1U << clock_generator_output_r9_sgpio);
@@ -821,7 +806,7 @@ void ClockManager::set_sampling_frequency(const uint32_t frequency) {
/*
* PRALINE sample rate strategy:
* 1. Maximize AFE rate to push Nyquist above MAX2831's 11.6 MHz LPF minimum
* 2. Use FPGA decimation to achieve desired output rate
* 2. Use FPGA decimation/interpolation to achieve desired output rate
* 3. Ensure AFE rate is achievable by Si5351 (clean division from 800 MHz VCO)
*/
@@ -842,50 +827,96 @@ void ClockManager::set_sampling_frequency(const uint32_t frequency) {
n++;
}
_resampling_n = n;
// === Stop FPGA processing and flush filters ===
fpga_debug_register_write(1, 0x00); // Disable FPGA filters (resets CIC accumulators)
// Verify we're in RX mode before writing RX registers
if (fpga_get_mode() != FPGA_MODE_RX) {
// Either set mode or return error
fpga_set_mode(FPGA_MODE_RX);
/* The TX gateware only implements interpolation factors x1..x16
* (tx_intrp 0..4). The rate search above can select n == 5 (x32) for very
* low TX sample rates. If we kept afe_rate/CLK0/CLK1 at the x32 rate while
* the FPGA only interpolated by x16, the TX datapath would be clocked at
* twice the gateware's output rate, doubling the waveform speed. Cap the
* AFE rate and _resampling_n to the x16 limit in TX so the clocks and the
* interpolation setting stay aligned. */
if (radio::debug::get_cached_direction() == rf::Direction::Transmit) {
constexpr uint8_t MAX_TX_N = 4; // x16, matches max tx_intrp
if (n > MAX_TX_N) {
n = MAX_TX_N;
afe_rate = frequency << n;
}
}
// 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.
* Relationship: ds = (Stages * n) - Offset
* For a 3-stage filter, every increment of n grows the signal by 3 bits.
* We subtract a baseline offset to keep the signal within 8-bit bounds.
* Add a baseline shift to ensure the signal isn't too quiet
*/
uint8_t ds = (3 * n);
ds += 2;
fpga_debug_register_write(FPGA_REG_RX_DIGITAL_GAIN, ds);
_resampling_n = n;
radio::invalidate_spi_config();
// Configure Si5351 clocks
// CLK0: AFE_CLK (with r_div=1 for ÷2)
// CLK1: SCT_CLK (with r_div=0 for ÷1, runs at 2× AFE for FPGA timing)
// Configure Si5351 clocks using the correct AFE VCO
// CLK0 always drives the AFE/MAX5864 clock.
clock_generator.set_ms_frequency(0, afe_rate * 2, si5351_vco_afe_f, 1);
clock_generator.set_ms_frequency(1, afe_rate * 2, si5351_vco_afe_f, 0);
// === Reset PLL A for phase alignment ===
clock_generator.write_register(si5351::Register::PLLReset, 0x20);
/* Do not reset PLLA here. On PRALINE, CLK2/MCU_CLKIN is sourced from PLLA,
* so a runtime PLLA reset can glitch the LPC43xx peripheral clock tree and
* break SGPIO-driven RX apps such as ADSB/APRS. Updating the multisynths is
* sufficient for sample-rate changes. */
// Brief delay for PLL lock and clock stability ===
// ~1ms at 96MHz = ~96000 cycles, use 10ms for safety
volatile uint32_t delay = 240000; // ~2.5ms
while (delay--);
if (radio::debug::get_cached_direction() == rf::Direction::Transmit) {
/*
* TX path:
* Baseband generators such as AFSK still synthesize samples at their
* legacy logical sample rates (for APRS this is 1.536MHz). On PRALINE,
* the FPGA must interpolate those samples up to the active AFE rate.
*
* The TX datapath is clocked from CLK1/fpgaclk. Therefore CLK1 must
* equal the desired complex sample rate at the DAC side. Driving CLK1
* at 2x the intended TX sample rate causes the entire APRS waveform to
* run twice as fast, which matches the "energy present, undecodable"
* symptom seen on HackRF One receivers.
*
* Gateware mapping from standard.py:
* tx_intrp = 0 -> x1
* tx_intrp = 1 -> x2
* tx_intrp = 2 -> x4
* tx_intrp = 3 -> x8
* tx_intrp = 4 -> x16
*/
if (fpga_get_mode() != FPGA_MODE_TX) {
fpga_set_mode(FPGA_MODE_TX);
}
// Re-enable FPGA processing with clean state ===
fpga_debug_register_write(1, 0x01);
// TX gateware consumes complex samples at the rate presented on CLK1.
clock_generator.set_ms_frequency(1, afe_rate, si5351_vco_afe_f, 0);
uint8_t tx_interp = 0;
uint32_t tx_rate = frequency;
while ((tx_rate < afe_rate) && (tx_interp < 4)) {
tx_rate <<= 1;
tx_interp++;
}
fpga_tx_set_interpolation(tx_interp);
fpga_tx_set_nco_enable(false);
fpga_tx_set_phase_step(0);
}
// for RX mode
else {
// RX path keeps CLK1 at 2x AFE for receive timing / SGPIO alignment.
clock_generator.set_ms_frequency(1, afe_rate * 2, si5351_vco_afe_f, 0);
// === Stop FPGA processing and flush filters ===
fpga_debug_register_write(1, 0x00); // Disable FPGA filters (resets CIC accumulators)
if (fpga_get_mode() != FPGA_MODE_RX) {
fpga_set_mode(FPGA_MODE_RX);
}
// 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.
*/
uint8_t ds = (3 * n);
ds += 2;
fpga_debug_register_write(FPGA_REG_RX_DIGITAL_GAIN, ds);
// Re-enable FPGA processing with clean state ===
fpga_debug_register_write(1, 0x01);
}
#else
/* Codec clock is at sampling frequency, CPLD and SGPIO clocks are at
@@ -1151,8 +1182,7 @@ void ClockManager::enable_clock_output(bool enable) {
}
#else
if (hackrf_r9) {
gpio_r9_clkout_en.output();
gpio_r9_clkout_en.write(enable);
gpio_control::r9_clkout_en.setState(enable);
// NOTE: RETURNING HERE IF HACKRF_R9 TO PREVENT CLK2 FROM BEING DISABLED OR FREQ MODIFIED SINCE CLK2 ON R9 IS
// USED FOR BOTH CLKOUT AND FOR THE MCU_CLOCK (== GP_CLKIN) WHICH OTHER LP43XX CLOCKS CURRENTLY RELY ON.
@@ -1180,3 +1210,74 @@ void ClockManager::enable_clock_output(bool enable) {
}
#endif
}
#ifdef PRALINE
void ClockManager::set_p1_control(P1_Function func) {
// Truth table based on P1_Control.csv (L=setInactive, H=setActive)
switch (func) {
case P1_Function::TriggerIn:
gpio_control::p1_ctrl2.setInactive();
gpio_control::p1_ctrl1.setInactive();
gpio_control::p1_ctrl0.setInactive();
break;
case P1_Function::AuxClk1:
gpio_control::p1_ctrl2.setInactive();
gpio_control::p1_ctrl1.setInactive();
gpio_control::p1_ctrl0.setActive();
break;
case P1_Function::ClkIn:
gpio_control::p1_ctrl2.setInactive();
gpio_control::p1_ctrl1.setActive();
gpio_control::p1_ctrl0.setInactive();
break;
case P1_Function::TriggerOut:
gpio_control::p1_ctrl2.setInactive();
gpio_control::p1_ctrl1.setActive();
gpio_control::p1_ctrl0.setActive();
break;
case P1_Function::P22_ClkIn:
gpio_control::p1_ctrl2.setActive();
gpio_control::p1_ctrl1.setInactive();
gpio_control::p1_ctrl0.setInactive();
break;
case P1_Function::P2_5:
gpio_control::p1_ctrl2.setActive();
gpio_control::p1_ctrl1.setInactive();
gpio_control::p1_ctrl0.setActive();
break;
case P1_Function::NotConnected:
gpio_control::p1_ctrl2.setActive();
gpio_control::p1_ctrl1.setActive();
gpio_control::p1_ctrl0.setInactive();
break;
case P1_Function::AuxClk2:
gpio_control::p1_ctrl2.setActive();
gpio_control::p1_ctrl1.setActive();
gpio_control::p1_ctrl0.setActive();
break;
}
}
void ClockManager::set_p2_control(P2_Function func) {
// Ensure all P2 control pins are configured as outputs
// Truth table based on P2_Control.csv (L=setInactive, H=setActive)
switch (func) {
case P2_Function::Clk3:
// CTRL0 is 'X' (don't care) according to CSV, we default it to Low (setInactive)
gpio_control::p2_ctrl1.setInactive();
gpio_control::p2_ctrl0.setInactive();
break;
case P2_Function::TriggerIn:
gpio_control::p2_ctrl1.setActive();
gpio_control::p2_ctrl0.setInactive();
break;
case P2_Function::TriggerOut:
gpio_control::p2_ctrl1.setActive();
gpio_control::p2_ctrl0.setActive();
break;
}
}
#endif
+26
View File
@@ -77,6 +77,29 @@ class ClockManager {
void set_reference_ppb(const int32_t ppb);
#ifdef PRALINE
enum class P1_Function {
TriggerIn, // CTRL2: L, CTRL1: L, CTRL0: L
AuxClk1, // CTRL2: L, CTRL1: L, CTRL0: H
ClkIn, // CTRL2: L, CTRL1: H, CTRL0: L
TriggerOut, // CTRL2: L, CTRL1: H, CTRL0: H
P22_ClkIn, // CTRL2: H, CTRL1: L, CTRL0: L
P2_5, // CTRL2: H, CTRL1: L, CTRL0: H
NotConnected, // CTRL2: H, CTRL1: H, CTRL0: L (NC)
AuxClk2 // CTRL2: H, CTRL1: H, CTRL0: H
};
// Multiplexer 2 (P2) Functions - Based on P2_Control.csv
enum class P2_Function {
Clk3, // CTRL1: L, CTRL0: X (Don't care)
TriggerIn, // CTRL1: H, CTRL0: L
TriggerOut // CTRL1: H, CTRL0: H
};
// Function declarations
void set_p1_control(P1_Function func);
void set_p2_control(P2_Function func);
uint8_t get_resampling_n() const { return _resampling_n; }
// Si5351 diagnostic methods
@@ -93,6 +116,9 @@ class ClockManager {
void enable_clock_output(bool enable);
void portapack_tcxo_enable();
void portapack_tcxo_disable();
private:
I2C& i2c0;
si5351::Si5351& clock_generator;
+19 -15
View File
@@ -24,6 +24,11 @@
#include "hackrf_gpio.hpp"
#include "portapack_hal.hpp"
#include "board.h"
#include "gpio.hpp"
using namespace gpio_control;
void config_mode_blink_until_dfu();
void config_mode_set() {
@@ -56,7 +61,6 @@ uint32_t blink_patterns[] = {
void config_mode_run() {
configure_pins_portapack();
portapack::gpio_dfu.input();
portapack::persistent_memory::cache::init();
if (hackrf_r9) {
@@ -75,14 +79,14 @@ void config_mode_run() {
config_mode_blink_until_dfu();
}
auto last_dfu_btn = portapack::gpio_dfu.read();
auto last_dfu_btn = dfu_button.read();
int32_t counter = 0;
int8_t blink_pattern_value = portapack::persistent_memory::config_cpld() +
(portapack::persistent_memory::config_disable_external_tcxo() ? 5 : 0);
while (true) {
auto dfu_btn = portapack::gpio_dfu.read();
auto dfu_btn = dfu_button.read();
auto dfu_clicked = last_dfu_btn == true && dfu_btn == false;
last_dfu_btn = dfu_btn;
@@ -103,16 +107,16 @@ void config_mode_run() {
auto tx_value = ((tx_blink_pattern >> ((counter >> 0) & 31)) & 0x1) == 0x1;
if (tx_value) {
hackrf::one::led_tx.on();
led_tx.setActive();
} else {
hackrf::one::led_tx.off();
led_tx.setInactive();
}
auto rx_value = ((rx_blink_pattern >> ((counter >> 0) & 31)) & 0x1) == 0x1;
if (rx_value) {
hackrf::one::led_rx.on();
led_rx.setActive();
} else {
hackrf::one::led_rx.off();
led_rx.setInactive();
}
chThdSleepMilliseconds(100);
@@ -122,17 +126,17 @@ void config_mode_run() {
void config_mode_blink_until_dfu() {
while (true) {
hackrf::one::led_tx.on();
hackrf::one::led_rx.on();
hackrf::one::led_usb.on();
led_tx.setActive();
led_rx.setActive();
led_usb.setActive();
chThdSleepMilliseconds(10);
hackrf::one::led_tx.off();
hackrf::one::led_rx.off();
hackrf::one::led_usb.off();
led_tx.setInactive();
led_rx.setInactive();
led_usb.setInactive();
chThdSleepMilliseconds(115);
auto dfu_btn = portapack::gpio_dfu.read();
auto dfu_btn = dfu_button.read();
if (dfu_btn)
break;
}
@@ -140,7 +144,7 @@ void config_mode_blink_until_dfu() {
while (true) {
chThdSleepMilliseconds(10);
auto dfu_btn = portapack::gpio_dfu.read();
auto dfu_btn = dfu_button.read();
if (!dfu_btn)
break;
}
+5 -2
View File
@@ -33,6 +33,9 @@
#include "irq_controls.hpp"
#include "file_path.hpp"
#include "gpio.hpp"
using namespace gpio_control;
using namespace ui;
#define DEBUG_LOG_FILE "debug_log.txt"
@@ -134,9 +137,9 @@ void draw_line(int32_t y_offset, const char* label, regarm_t value) {
}
void runtime_error(uint8_t source) {
LED led = (source == CORTEX_M0) ? hackrf::one::led_rx : hackrf::one::led_tx;
const auto& led = (source == CORTEX_M0) ? led_rx : led_tx;
led.off();
led.setInactive();
// wait for DFU button release if pressed, so we don't immediately jump into stack dump
while (swizzled_switches() & (1 << (int)Switch::Dfu));
+17 -27
View File
@@ -37,8 +37,8 @@
#include "ch.h"
#include "hackrf_gpio.hpp"
using namespace hackrf::one;
#include "gpio.hpp"
using namespace gpio_control;
#include "irq_rtc.hpp"
@@ -185,10 +185,11 @@ void EventDispatcher::charge_deep_sleep(const bool sleep) {
SCB->ICSR |= SCB_ICSR_PENDSTCLR_Msk;
power_control::vaa_power_off();
power_control::core_power_off();
#ifdef PRALINE
// Power management and GPIO configuration for Praline hardware
gpio_vaa_disable.set();
gpio_1v2_enable.clear();
LPC_GPIO->DIR[0] &= ~0xFFFF4000;
LPC_GPIO->DIR[1] &= ~0xFFFF1000;
LPC_GPIO->DIR[2] &= ~((1 << 14) | (1 << 13) | (1 << 12) | (1 << 11) | (1 << 10) | (1 << 6) | (1 << 0));
@@ -196,8 +197,6 @@ void EventDispatcher::charge_deep_sleep(const bool sleep) {
LPC_GPIO->DIR[5] &= ~(1 << 16);
#else
// Power management and GPIO configuration for legacy hardware
gpio_og_vaa_disable.set();
gpio_r9_vaa_disable.clear();
LPC_GPIO->DIR[0] &= ~0xFFFF4000;
LPC_GPIO->DIR[1] &= ~0xFFFF1000;
LPC_GPIO->DIR[2] &= ~((1 << 14) | (1 << 13) | (1 << 12) | (1 << 11) | (1 << 10) | (1 << 6) | (1 << 0));
@@ -228,8 +227,8 @@ void EventDispatcher::charge_deep_sleep(const bool sleep) {
LPC_ADC0->CR &= ~(1 << 21);
LPC_ADC1->CR &= ~(1 << 21);
led_rx.off();
led_usb.off();
led_rx.setInactive();
led_usb.setInactive();
rtc_wakeup_init();
NVIC_EnableIRQ(I2C0_OR_I2C1_IRQn);
@@ -238,28 +237,29 @@ void EventDispatcher::charge_deep_sleep(const bool sleep) {
// --- Battery Status Check (I2C) ---
detect = battery::BatteryManagement::isDetected();
if (detect) {
battery::BatteryManagement::getBatteryInfo(valid_mask, percent, voltage, current);
bool dummy;
battery::BatteryManagement::getBatteryInfo(valid_mask, percent, voltage, current, dummy);
bool is_full = (valid_mask == 31 && percent == 100 && current <= 10) ||
(valid_mask == 1 && percent == 100);
if (is_full) {
// Case 1: Battery full (All LEDs off)
led_rx.off();
led_tx.off();
led_rx.setInactive();
led_tx.setInactive();
} else if ((voltage < 4150 && current < 10) || valid_mask == 0) {
// Case 2: Not full but low current draw (<10mA) -> Charging error
led_tx.on(); // LED indicates error/idle
led_rx.off();
led_tx.setActive(); // LED indicates error/idle
led_rx.setInactive();
} else {
// Case 3: Actively charging
led_rx.on(); // LED indicates charging
led_tx.off();
led_rx.setActive(); // LED indicates charging
led_tx.setInactive();
}
} else {
// Case 4: Battery IC not detected -> Error or H2 or older, so don't show that as an error.
led_tx.on();
led_rx.on();
led_tx.setActive();
led_rx.setActive();
}
// Shut down I2C and power down the APB bus for sleep
@@ -268,17 +268,11 @@ void EventDispatcher::charge_deep_sleep(const bool sleep) {
// Save interrupt states before mass disable
uint32_t saved_iser0 = NVIC->ISER[0];
uint32_t saved_iser1 = NVIC->ISER[1];
uint32_t saved_iser2 = NVIC->ISER[2];
// Disable and clear all pending interrupts
NVIC->ICER[0] = 0xFFFFFFFF;
NVIC->ICER[1] = 0xFFFFFFFF;
NVIC->ICER[2] = 0xFFFFFFFF;
NVIC->ICPR[0] = 0xFFFFFFFF;
NVIC->ICPR[1] = 0xFFFFFFFF;
NVIC->ICPR[2] = 0xFFFFFFFF;
// Re-enable only necessary wakeup sources
NVIC_EnableIRQ(RTC_IRQn);
@@ -326,8 +320,6 @@ void EventDispatcher::charge_deep_sleep(const bool sleep) {
// Restore original interrupt enable states
NVIC->ISER[0] = saved_iser0;
NVIC->ISER[1] = saved_iser1;
NVIC->ISER[2] = saved_iser2;
} // End of while(1) deep sleep loop
} else {
@@ -408,8 +400,6 @@ void EventDispatcher::handle_local_queue() {
void EventDispatcher::handle_rtc_tick() {
sd_card::poll_inserted();
portapack::temperature_logger.second_tick();
const auto backlight_timer = portapack::persistent_memory::config_backlight_timer();
if (backlight_timer.timeout_enabled()) {
if (portapack::bl_tick_counter == backlight_timer.timeout_seconds())
@@ -1,5 +1,5 @@
/*
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
* copyleft 2024 zxkmm
*
* This file is part of PortaPack.
*
@@ -1,5 +1,5 @@
/*
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
* copyleft 2024 zxkmm
*
* This file is part of PortaPack.
*
+89 -105
View File
@@ -6,11 +6,9 @@
* ------------------------------------------------------------
*/
#pragma GCC push_options
#pragma GCC optimize("Os")
#include "ui_battleship.hpp"
#include "portapack_shared_memory.hpp"
#include "utility.hpp"
#include "modems.hpp"
#include "bch_code.hpp"
using namespace portapack;
using namespace modems;
@@ -28,7 +26,6 @@ BattleshipView::BattleshipView(NavigationView& nav)
baseband::run_image(portapack::spi_flash::image_tag_pocsag2);
add_children({&text_title, &text_subtitle,
//&rect_radio_settings, &rect_audio_settings, &rect_team_selection,
&label_radio, &button_frequency,
&label_rf_amp, &checkbox_rf_amp,
&label_lna, &field_lna,
@@ -151,8 +148,6 @@ BattleshipView::BattleshipView(NavigationView& nav)
field_tx_gain.set_parent_rect({185, 118, 32, 16});
checkbox_sound.set_parent_rect({17, 187, 80, 24});
field_volume.set_parent_rect({165, 187, 32, 16});
// button_red_team.set_parent_rect({25, 242, 85, 45});
// button_blue_team.set_parent_rect({130, 242, 85, 45});
// Make menu elements focusable
button_frequency.set_focusable(true);
@@ -196,34 +191,22 @@ void BattleshipView::init_game() {
}
void BattleshipView::show_hide_menu(bool menu_vis) {
text_title.hidden(!menu_vis);
text_subtitle.hidden(!menu_vis);
// rect_radio_settings.hidden(!menu_vis); rect_audio_settings.hidden(!menu_vis); rect_team_selection.hidden(!menu_vis);
label_radio.hidden(!menu_vis);
button_frequency.hidden(!menu_vis);
label_rf_amp.hidden(!menu_vis);
checkbox_rf_amp.hidden(!menu_vis);
label_lna.hidden(!menu_vis);
field_lna.hidden(!menu_vis);
label_vga.hidden(!menu_vis);
field_vga.hidden(!menu_vis);
label_tx_gain.hidden(!menu_vis);
field_tx_gain.hidden(!menu_vis);
label_audio.hidden(!menu_vis);
checkbox_sound.hidden(!menu_vis);
label_volume.hidden(!menu_vis);
field_volume.hidden(!menu_vis);
label_team.hidden(!menu_vis);
button_red_team.hidden(!menu_vis);
button_blue_team.hidden(!menu_vis);
rssi.hidden(menu_vis);
button_rotate.hidden(menu_vis);
button_place.hidden(menu_vis);
button_menu.hidden(menu_vis);
Widget* const menu_widgets[] = {
&text_title, &text_subtitle, &label_radio, &button_frequency,
&label_rf_amp, &checkbox_rf_amp, &label_lna, &field_lna,
&label_vga, &field_vga, &label_tx_gain, &field_tx_gain,
&label_audio, &checkbox_sound, &label_volume, &field_volume,
&label_team, &button_red_team, &button_blue_team};
for (auto* w : menu_widgets) {
w->hidden(!menu_vis);
}
Widget* const game_widgets[] = {
&rssi, &button_rotate, &button_place, &button_menu};
for (auto* w : game_widgets) {
w->hidden(menu_vis);
}
// button_rotate.set_focusable(false); //no need, since can't focus on hidden
// button_place.set_focusable(false);
// button_menu.set_focusable(false);
set_dirty();
}
@@ -340,40 +323,23 @@ void BattleshipView::paint(Painter& painter) {
if (game_state == GameState::MENU) {
draw_menu_screen(painter);
// Custom paint team buttons
if (!button_red_team.hidden()) {
Rect r = button_red_team.screen_rect();
painter.fill_rectangle(r, Color::dark_red());
painter.draw_rectangle(r, Color::red());
auto draw_team_btn = [&painter](Button& btn, Color bg, Color border, const char* name) {
if (btn.hidden()) return;
Rect r = btn.screen_rect();
painter.fill_rectangle(r, bg);
painter.draw_rectangle(r, border);
if (button_red_team.has_focus()) {
if (btn.has_focus()) {
painter.draw_rectangle({r.location().x() - 1, r.location().y() - 1, r.size().width() + 2, r.size().height() + 2}, Color::yellow());
}
auto style_white = Style{
.font = ui::font::fixed_8x16,
.background = Color::dark_red(),
.foreground = Color::white()};
painter.draw_string(r.center() - Point(24, 16), style_white, "RED");
painter.draw_string(r.center() - Point(24, 0), style_white, "TEAM");
}
auto style = Style{.font = ui::font::fixed_8x16, .background = bg, .foreground = Color::white()};
painter.draw_string(r.center() - Point(24, 16), style, name);
painter.draw_string(r.center() - Point(24, 0), style, "TEAM");
};
if (!button_blue_team.hidden()) {
Rect r = button_blue_team.screen_rect();
painter.fill_rectangle(r, Color::dark_blue());
painter.draw_rectangle(r, Color::blue());
if (button_blue_team.has_focus()) {
painter.draw_rectangle({r.location().x() - 1, r.location().y() - 1, r.size().width() + 2, r.size().height() + 2}, Color::yellow());
}
auto style_white = Style{
.font = ui::font::fixed_8x16,
.background = Color::dark_blue(),
.foreground = Color::white()};
painter.draw_string(r.center() - Point(24, 16), style_white, "BLUE");
painter.draw_string(r.center() - Point(24, 0), style_white, "TEAM");
}
draw_team_btn(button_red_team, Color::dark_red(), Color::red(), "RED");
draw_team_btn(button_blue_team, Color::dark_blue(), Color::blue(), "BLUE");
return;
}
@@ -417,27 +383,25 @@ void BattleshipView::paint(Painter& painter) {
void BattleshipView::draw_menu_screen(Painter& painter) {
painter.draw_hline({12, 20 + 16}, screen_width - 24, Color::dark_cyan());
painter.fill_rectangle({13, 59, screen_width - 26, 116}, Color::dark_grey());
painter.draw_hline({12, 58}, screen_width - 24, Color::cyan());
painter.draw_hline({12, 157}, screen_width - 24, Color::cyan());
const struct {
Rect rect;
Point line1_pos;
Point line2_pos;
} boxes[] = {
{{13, 59, screen_width - 26, 116}, {12, 58}, {12, 157}},
{{13, 165, screen_width - 24, 57}, {12, 164}, {12, 223}},
{{13, 232, screen_width - 26, 67}, {12, 232}, {12, 300}}};
painter.fill_rectangle({13, 165, screen_width - 24, 57}, Color::dark_grey());
painter.draw_hline({12, 164}, screen_width - 24, Color::cyan());
painter.draw_hline({12, 223}, screen_width - 24, Color::cyan());
painter.fill_rectangle({13, 232, screen_width - 26, 67}, Color::dark_grey());
painter.draw_hline({12, 232}, screen_width - 24, Color::cyan());
painter.draw_hline({12, 300}, screen_width - 24, Color::cyan());
for (const auto& box : boxes) {
painter.fill_rectangle(box.rect, Color::dark_grey());
painter.draw_hline(box.line1_pos, screen_width - 24, Color::cyan());
painter.draw_hline(box.line2_pos, screen_width - 24, Color::cyan());
}
// Radio status indicator
Point indicator_pos{UI_POS_MAXWIDTH - 20, 59};
if (is_transmitting) {
painter.fill_rectangle({indicator_pos, {6, 6}}, Color::red());
painter.draw_rectangle({indicator_pos.x() - 1, indicator_pos.y() - 1, 8, 8}, Color::light_grey());
} else {
painter.fill_rectangle({indicator_pos, {6, 6}}, Color::green());
painter.draw_rectangle({indicator_pos.x() - 1, indicator_pos.y() - 1, 8, 8}, Color::light_grey());
}
painter.fill_rectangle({indicator_pos, {6, 6}}, is_transmitting ? Color::red() : Color::green());
painter.draw_rectangle({indicator_pos.x() - 1, indicator_pos.y() - 1, 8, 8}, Color::light_grey());
}
void BattleshipView::draw_grid(Painter& painter, uint16_t grid_x, uint16_t grid_y, const std::array<std::array<CellState, GRID_SIZE>, GRID_SIZE>& grid, bool show_ships, bool is_offense_grid) {
@@ -464,11 +428,14 @@ void BattleshipView::draw_cell(Painter& painter, uint16_t cell_x, uint16_t cell_
if (game_state == GameState::PLACING_SHIPS && !is_offense_grid && current_ship_index < 5) {
uint8_t ship_size = my_ships[current_ship_index].size();
uint8_t dx = placing_horizontal ? 1 : 0;
uint8_t dy = placing_horizontal ? 0 : 1;
uint16_t grid_x = (cell_x - 1) / CELL_SIZE;
uint16_t grid_y = (cell_y - GRID_OFFSET_Y - 6) / CELL_SIZE;
for (uint8_t i = 0; i < ship_size; i++) {
uint16_t preview_x = placing_horizontal ? cursor_x + i : cursor_x;
uint16_t preview_y = placing_horizontal ? cursor_y : cursor_y + i;
uint16_t grid_x = (cell_x - 1) / CELL_SIZE;
uint16_t grid_y = (cell_y - GRID_OFFSET_Y - 6) / CELL_SIZE;
uint16_t preview_x = cursor_x + (i * dx);
uint16_t preview_y = cursor_y + (i * dy);
if (grid_x == preview_x && grid_y == preview_y && preview_x < GRID_SIZE && preview_y < GRID_SIZE) {
return;
}
@@ -540,9 +507,12 @@ void BattleshipView::draw_ship_preview(Painter& painter) {
uint8_t size = ship.size();
bool can_place = can_place_ship(cursor_x, cursor_y, size, placing_horizontal);
uint8_t dx = placing_horizontal ? 1 : 0;
uint8_t dy = placing_horizontal ? 0 : 1;
for (uint8_t i = 0; i < size; i++) {
uint8_t x = placing_horizontal ? cursor_x + i : cursor_x;
uint8_t y = placing_horizontal ? cursor_y : cursor_y + i;
uint8_t x = cursor_x + (i * dx);
uint8_t y = cursor_y + (i * dy);
if (x < GRID_SIZE && y < GRID_SIZE) {
uint16_t cell_x = GRID_OFFSET_X + x * CELL_SIZE + 1;
@@ -561,18 +531,21 @@ bool BattleshipView::can_place_ship(uint8_t x, uint8_t y, uint8_t size, bool hor
return false;
}
uint8_t dx = horizontal ? 1 : 0;
uint8_t dy = horizontal ? 0 : 1;
for (uint8_t i = 0; i < size; i++) {
uint8_t check_x = horizontal ? x + i : x;
uint8_t check_y = horizontal ? y : y + i;
uint8_t check_x = x + (i * dx);
uint8_t check_y = y + (i * dy);
if (my_grid[check_y][check_x] != CellState::EMPTY) {
return false;
}
for (int dy = -1; dy <= 1; dy++) {
for (int dx = -1; dx <= 1; dx++) {
int adj_x = check_x + dx;
int adj_y = check_y + dy;
for (int ddy = -1; ddy <= 1; ddy++) {
for (int ddx = -1; ddx <= 1; ddx++) {
int adj_x = check_x + ddx;
int adj_y = check_y + ddy;
if (adj_x >= 0 && adj_x < GRID_SIZE &&
adj_y >= 0 && adj_y < GRID_SIZE) {
@@ -604,10 +577,11 @@ void BattleshipView::place_ship() {
ship.horizontal = placing_horizontal;
ship.placed = true;
uint8_t dx = placing_horizontal ? 1 : 0;
uint8_t dy = placing_horizontal ? 0 : 1;
for (uint8_t i = 0; i < size; i++) {
uint8_t x = placing_horizontal ? cursor_x + i : cursor_x;
uint8_t y = placing_horizontal ? cursor_y : cursor_y + i;
my_grid[y][x] = CellState::SHIP;
my_grid[cursor_y + (i * dy)][cursor_x + (i * dx)] = CellState::SHIP;
}
current_ship_index++;
@@ -644,9 +618,14 @@ void BattleshipView::place_ship() {
void BattleshipView::send_message(const GameMessage& msg) {
static const char* msg_strings[] = {"READY", "SHOT:", "HIT:", "MISS:", "SUNK:", "WIN"};
std::string message = msg_strings[static_cast<int>(msg.type)];
std::string message;
message.reserve(16);
message.append(msg_strings[static_cast<int>(msg.type)]);
if (msg.type != MessageType::READY && msg.type != MessageType::WIN) {
message += to_string_dec_uint(msg.x) + "," + to_string_dec_uint(msg.y);
message.append(to_string_dec_uint(msg.x));
message.append(",");
message.append(to_string_dec_uint(msg.y));
}
configure_radio_tx();
@@ -836,11 +815,12 @@ void BattleshipView::process_shot(uint8_t x, uint8_t y) {
if (!ship.placed) continue;
bool hit_this_ship = false;
for (uint8_t i = 0; i < ship.size(); i++) {
uint8_t check_x = ship.horizontal ? ship.x + i : ship.x;
uint8_t check_y = ship.horizontal ? ship.y : ship.y + i;
if (check_x == x && check_y == y) {
uint8_t dx = ship.horizontal ? 1 : 0;
uint8_t dy = ship.horizontal ? 0 : 1;
for (uint8_t i = 0; i < ship.size(); i++) {
if ((ship.x + i * dx) == x && (ship.y + i * dy) == y) {
hit_this_ship = true;
ship.hits++;
break;
@@ -852,9 +832,7 @@ void BattleshipView::process_shot(uint8_t x, uint8_t y) {
ships_remaining--;
for (uint8_t i = 0; i < ship.size(); i++) {
uint8_t sunk_x = ship.horizontal ? ship.x + i : ship.x;
uint8_t sunk_y = ship.horizontal ? ship.y : ship.y + i;
my_grid[sunk_y][sunk_x] = CellState::SUNK;
my_grid[ship.y + (i * dy)][ship.x + (i * dx)] = CellState::SUNK;
}
break;
}
@@ -887,7 +865,12 @@ void BattleshipView::process_shot(uint8_t x, uint8_t y) {
}
void BattleshipView::update_score() {
current_score = "W:" + to_string_dec_uint(wins) + " L:" + to_string_dec_uint(losses);
current_score.clear();
current_score.reserve(16);
current_score.append("W:");
current_score.append(to_string_dec_uint(wins));
current_score.append(" L:");
current_score.append(to_string_dec_uint(losses));
}
bool BattleshipView::on_touch(const TouchEvent event) {
@@ -980,3 +963,4 @@ bool BattleshipView::on_key(const KeyEvent key) {
}
} // namespace ui::external_app::battleship
#pragma GCC pop_options
@@ -9,6 +9,11 @@
#ifndef __UI_BATTLESHIP_H__
#define __UI_BATTLESHIP_H__
#include "portapack_shared_memory.hpp"
#include "utility.hpp"
#include "modems.hpp"
#include "bch_code.hpp"
#include "ui.hpp"
#include "ui_navigation.hpp"
#include "ui_receiver.hpp"
+66 -4
View File
@@ -294,11 +294,11 @@ void BLESpamView::createWindowsPacket() {
std::copy(res.begin(), res.end(), advertisementData);
}
void BLESpamView::createNameSpamPacket() {
const char* names[] = {"PortaHack", "PwnBt", "iSpam", "GenericFoodVagon", "SignalSnoop", "ByteBandit", "RadioRogue", "RadioRebel", "ByteBlast"};
const char* name = names[rand() % 9]; //"PortaHack";
// Builds a "Complete Local Name" HID advertisement for the given name.
// Shared by NameSpam (built-in list) and Custom (SD card list) modes.
void BLESpamView::buildNamePacket(const char* name) {
uint8_t name_len = strlen(name);
if (name_len > 19) name_len = 19; // clamp so the hex string fits advertisementData[63]
uint8_t size = 12 + name_len;
uint8_t i = 0;
@@ -327,6 +327,65 @@ void BLESpamView::createNameSpamPacket() {
std::copy(res.begin(), res.end(), advertisementData);
}
void BLESpamView::createNameSpamPacket() {
const char* names[] = {"PortaHack", "PwnBt", "iSpam", "GenericFoodVagon", "SignalSnoop", "ByteBandit", "RadioRogue", "RadioRebel", "ByteBlast"};
buildNamePacket(names[rand() % 9]);
}
// Opens /BLESPAM/NAME.txt once and keeps it open for the session.
void BLESpamView::open_custom_file() {
custom_loaded_ = true; // attempt only once
auto open_error = custom_file_.open(u"/BLESPAM/NAME.txt");
custom_file_valid_ = !open_error; // false -> caller falls back to NameSpam
}
// Reads the next line into custom_name_ (clamped to 19 chars), advancing the
// file position. At end of file it seeks back to the start to cycle. Returns
// false if the file holds no usable name. Only one line is ever held in RAM.
bool BLESpamView::read_next_custom_name() {
if (!custom_file_valid_) return false;
custom_name_.clear();
bool wrapped = false;
char c;
while (true) {
auto read_result = custom_file_.read(&c, 1);
if (read_result.is_error()) return false;
if (read_result.value() == 0) {
// end of file
if (!custom_name_.empty()) break;
// use the final line
if (wrapped) return false;
// no usable name in file
if (custom_file_.seek(0).is_error()) return false;
// loop back to the first line
wrapped = true;
continue;
}
if (c == '\n' || c == '\r') {
if (!custom_name_.empty()) break; // got a complete line
continue; // skip blank lines / CRLF pairs
}
if (custom_name_.size() < 19) // clamp; keep scanning rest of long line
custom_name_.push_back(c);
}
return true;
}
void BLESpamView::createCustomPacket() {
if (!custom_loaded_) open_custom_file();
if (!read_next_custom_name()) { // no file / unreadable / empty -> fall back
createNameSpamPacket();
return;
}
buildNamePacket(custom_name_.c_str());
}
char BLESpamView::randomNameChar() {
int randVal = rand() % 62; // 26 uppercase + 26 lowercase + 10 digits
if (randVal < 26) {
@@ -622,6 +681,9 @@ void BLESpamView::createPacket(ATK_TYPE attackType) {
case ATK_NAMERANDOM:
createNameRandomPacket();
break;
case ATK_CUSTOM:
createCustomPacket();
break;
case ATK_ALL_SAFE:
createAnyPacket(true);
break;
+14 -2
View File
@@ -39,6 +39,7 @@
#include "radio_state.hpp"
#include "log_file.hpp"
#include "utility.hpp"
#include "file.hpp"
using namespace ui;
@@ -52,6 +53,7 @@ enum ATK_TYPE {
ATK_SAMSUNG,
ATK_NAMESPAM,
ATK_NAMERANDOM,
ATK_CUSTOM,
ATK_ALL_SAFE,
ATK_ALL
};
@@ -131,8 +133,9 @@ class BLESpamView : public View {
{"Samsung", 4},
{"NameSpam", 5},
{"NameRandom", 6},
{"All-Safe", 7},
{"All", 8}}};
{"NameCustom", 7},
{"All-Safe", 8},
{"All", 9}}};
bool is_running{false};
@@ -144,6 +147,11 @@ class BLESpamView : public View {
bool randomMac{true};
bool randomDev{true};
File custom_file_{}; // kept open while in Custom mode
std::string custom_name_{}; // holds only the current name (clamped)
bool custom_loaded_{false}; // true once an open attempt has been made
bool custom_file_valid_{false}; // true if /BLESPAM/NAME.txt opened successfully
uint8_t channel_number = 37;
char mac[13] = "010203040407";
char advertisementData[63] = {"03032CFE06162CFED5A59E020AB4\0"};
@@ -156,7 +164,11 @@ class BLESpamView : public View {
void createIosPacket(bool crash);
void createSamsungPacket();
void createWindowsPacket();
void buildNamePacket(const char* name); // shared name advertisement builder (NameSpam / Custom)
void createNameSpamPacket();
void createCustomPacket();
void open_custom_file(); // open /BLESPAM/NAME.txt once (one name per line)
bool read_next_custom_name(); // read next line into custom_name_, looping at EOF
void createNameRandomPacket();
void createAnyPacket(bool safe);
void createPacket(ATK_TYPE attackType);
+59
View File
@@ -0,0 +1,59 @@
/*
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
*
* 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 "beacon.hpp"
#include <cstdio>
#include "string_format.hpp"
// Some but not all methods of beacon class have been externalized in .cpp file
// This has been done to try and optimize application size by preventing the compiler to inline methods
namespace ui::external_app::epirb_rx {
size_t Beacon::formatSummary(char* buffer, bool with_time) {
size_t result = 0;
if (with_time) {
result += formatTime(buffer);
buffer[result++] = '-';
}
result += sprintf((buffer + result), "%4s-%5s-", shortId().c_str(), getType());
if (location.isUnknown()) {
result += sprintf((buffer + result), " ");
} else {
result += location.toString((buffer + result), Location::LocationFormat::MAIDENHEAD_LOCATOR);
}
result += sprintf((buffer + result), "[%s%s%s]", isFrameValid() ? (bch1Corrected || bch2Corrected) ? STR_COLOR_YELLOW : STR_COLOR_GREEN : STR_COLOR_RED, getSatus().c_str(), STR_COLOR_WHITE);
return result;
}
bool Beacon::isFrameValid() {
return isBch1Valid() && ((!hasBch2) || isBch2Valid()) && (!isEmpty);
}
std::string Beacon::shortId() {
std::string result = std::string(hexId);
return (result.size() >= 4) ? result.substr(0, 4) : result;
}
size_t Beacon::formatTime(char* buffer) {
return sprintf(buffer, "%02d:%02d:%02d", date.hour(), date.minute(), date.second());
}
} // namespace ui::external_app::epirb_rx
+856
View File
@@ -0,0 +1,856 @@
/*
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
*
* 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 __BEACON_RX_H__
#define __BEACON_RX_H__
#include "country.hpp"
#include "location.hpp"
#include "rtc_time.hpp"
#include "resources.hpp"
namespace ui::external_app::epirb_rx {
// Size for beacon frame buffer
#define BEACON_DATA_SIZE 18 // Max 144 bits => 18 bytes
// Constants for BCH control code calculation
#define BCH_21_POLYNOMIAL 0b1001101101100111100011UL
#define BCH_21_POLY_LENGTH 22
#define BCH_12_POLYNOMIAL 0b1010100111001UL
#define BCH_12_POLY_LENGTH 13
// See content of sdcard/EPIRB/RES/BEACON.RES
#define ADDITIONAL_DATA_RESOURCE_START 25
#define LONG_ADDITIONAL_DATA_RESOURCE_START 33
#define EMERGENCY_RESOURCE_START 39
#define EMERGENCY_OTHER_RESOURCE_START 49
#define AUX_DEVICE_RESOURCE_START 53
/**
* This is the main class for beaon handling
*/
class Beacon {
public:
// Real beacon or test beacon ?
enum class FrameMode { NORMAL,
SELF_TEST,
UNKNOWN };
// Type of main location device
enum class MainLocatingDevice { UNDEFINED,
INTERNAL_NAV,
EXTERNAL_NAV };
// Type of aux location device
enum class AuxLocatingDevice { UNDEFINED,
NONE,
NONE_OR_OTHER,
OTHER,
MHZ121_5,
SART };
// Protcol type
enum class ProtocolType {
UNKNOWN,
USER,
STANDARD_LOCATION,
NATIONAL_LOCATION,
RLS_LOCATION,
ELT_DT_LOCATION,
SPARE
};
// Protocol
enum class Protocol {
USER_EPIRB_MARITIME,
USER_EPIRB_RADIO,
USER_ELT,
USER_SERIAL,
USER_TEST,
USER_ORB,
USER_NAT,
USER_2G,
STD_EPIRB,
STD_ELT_24,
STD_ELT_SERIAL,
STD_ELT_AIRCRAFT,
STD_EPIRB_SERIAL,
STD_PLB_SERIAL,
STD_SHIP,
STD_TEST,
NAT_ELT,
NAT_EPIRB,
NAT_PLB,
NAT_TEST,
RLS,
ELT_DT,
SPARE,
UNKNOWN
};
#define UNKNOWN_LABEL "Unk."
// Returns true for User protocol beacons
bool protocolIsUser() { return (protocolType == ProtocolType::USER); };
// Returns true for National Location protocol beacons
bool protocolIsNational() { return (protocolType == ProtocolType::NATIONAL_LOCATION); };
// Returns true for Standard Location protocol beacons
bool protocolIsStandard() { return (protocolType == ProtocolType::STANDARD_LOCATION); };
// Returns true for RLS location protocol beacons
bool protocolIsRls() { return (protocolType == ProtocolType::RLS_LOCATION); };
// Returns true for RLS or ELT Location protocol beacons
bool protocolIsRlsOrElt() { return (protocolType == ProtocolType::RLS_LOCATION || protocolType == ProtocolType::ELT_DT_LOCATION); };
// Returns true for unknown protocol beacons
bool protocolIsUnknown() { return (protocolType == ProtocolType::UNKNOWN); };
// Returns the protocol name
const char* getProtocolName() {
switch (protocolType) {
case ProtocolType::STANDARD_LOCATION:
return "Standard";
case ProtocolType::NATIONAL_LOCATION:
return "National";
case ProtocolType::RLS_LOCATION:
return "RLS";
case ProtocolType::ELT_DT_LOCATION:
return "ELT(DT)";
case ProtocolType::USER:
return longFrame ? "User Location" : "User";
case ProtocolType::SPARE:
return "Spare";
default:
return UNKNOWN_LABEL;
}
}
// Get protocol type for this beacon
ProtocolType getProtocolType() {
switch (protocol) {
case Protocol::USER_EPIRB_MARITIME:
case Protocol::USER_EPIRB_RADIO:
case Protocol::USER_ELT:
case Protocol::USER_SERIAL:
case Protocol::USER_TEST:
case Protocol::USER_ORB:
case Protocol::USER_NAT:
case Protocol::USER_2G:
return ProtocolType::USER;
case Protocol::STD_EPIRB:
case Protocol::STD_ELT_24:
case Protocol::STD_ELT_SERIAL:
case Protocol::STD_ELT_AIRCRAFT:
case Protocol::STD_PLB_SERIAL:
case Protocol::STD_SHIP:
case Protocol::STD_TEST:
return ProtocolType::STANDARD_LOCATION;
case Protocol::NAT_ELT:
case Protocol::NAT_EPIRB:
case Protocol::NAT_PLB:
case Protocol::NAT_TEST:
return ProtocolType::NATIONAL_LOCATION;
case Protocol::RLS:
return ProtocolType::RLS_LOCATION;
case Protocol::ELT_DT:
return ProtocolType::ELT_DT_LOCATION;
case Protocol::SPARE:
return ProtocolType::SPARE;
case Protocol::UNKNOWN:
default:
return ProtocolType::UNKNOWN;
}
}
// True for long frames, fals for short grames
bool longFrame{true};
// True if protocol flag is set for this frame
bool protocolFlag{false};
// Frame mode (Test or Real)
FrameMode frameMode{FrameMode::UNKNOWN};
// Main location device
MainLocatingDevice mainLocatingDevice{MainLocatingDevice::UNDEFINED};
// Axiliary location device
AuxLocatingDevice auxLocatingDevice{AuxLocatingDevice::UNDEFINED};
// Protocol code for this beacon
long protocolCode{0};
// Protocol of this beacon
Protocol protocol{Protocol::UNKNOWN};
// Protocol type of this beacon
ProtocolType protocolType{ProtocolType::UNKNOWN};
// Country of this beacon
Country country{};
// Frame data
uint8_t frame[BEACON_DATA_SIZE];
// Location of this beacon
Location location{};
// HexId of this beacon
uint64_t identifier{0};
// Date of reception of this beacon
rtc::RTC date{};
// Trus if this beacon has additional data
bool hasAdditionalData{false};
// The additional data tring
char additionalData[48]{0};
// True if this beacon has a serial number
bool hasSerialNumber{false};
// The serial number string
char serialNumber[32]{0};
// Beacon's Hex ID string
char hexId[32]{0};
// BCH1 code value
uint32_t bch1{};
// BCH1 calculated value
uint32_t computedBch1{};
// True if data has been corrected to match BCH1 code
bool bch1Corrected{false};
// True if this beacon has a BCH2 correction code
bool hasBch2{false};
// BCH2 code value
uint32_t bch2{};
// BCH2 calculated value
uint32_t computedBch2{};
// True if data has been corrected to match BCH2 code
bool bch2Corrected{false};
// True if this beacon is empty (not initialized)
bool isEmpty{true};
// True if thsi beacon contains emergency data
bool hasEmergency{false};
// True if emenrgency data is automatic for this beacon
bool isAutoamticEmergency{false};
// True if this beacon is a Maritime beacon
bool isMaritime{false};
// Emergency type string
char emergencyType[32]{0};
/**
* Returns the requested bits in this beacon's frame
*/
uint64_t getBits(int startBit, int endBit) {
uint64_t result = 0;
startBit--;
int numBits = endBit - startBit;
const uint8_t* pData = &(frame[startBit / 8]);
uint8_t b = *pData;
int bitOffset = 7 - (startBit % 8);
for (int i = 0; i < numBits; ++i) {
result <<= 1;
result |= ((b >> bitOffset) & 0x01);
if (--bitOffset < 0) {
b = *(++pData);
bitOffset = 7;
}
}
return result;
}
/**
* Compute a BCH control code for the given bits and poly
*/
uint64_t computeBCH(int startBit, int endBit, unsigned long poly, int polyLength) {
int dataLength = endBit - startBit + 1;
int totalLength = dataLength + polyLength - 1;
uint64_t result = getBits(startBit, startBit + polyLength - 1);
for (int i = polyLength; i <= totalLength; i++) {
bool firstBit = result >> (polyLength - 1);
if (firstBit) result = result ^ poly;
if (i < totalLength) {
result = result << 1;
if (i < dataLength) result |= getBits(startBit + i, startBit + i);
}
}
return result;
}
/**
* Compute BCH1 code
*/
uint64_t computeBCH1() { return computeBCH(25, 85, BCH_21_POLYNOMIAL, BCH_21_POLY_LENGTH); }
/**
* Compute BCH2 code
*/
uint64_t computeBCH2() { return computeBCH(107, 132, BCH_12_POLYNOMIAL, BCH_12_POLY_LENGTH); }
/**
* Convert this frame's data to an hex string
*/
static size_t toHexString(char* buffer, uint8_t* frame, bool withSpace, int start, int end) {
size_t result = 0;
for (uint8_t i = start; i < end; i++) {
if (withSpace && i > start) buffer[result++] = ' ';
result += sprintf((buffer + result), "%02X", frame[i]);
}
buffer[result] = 0;
return result;
}
Beacon() {}
Beacon(const Beacon& other) = delete;
Beacon& operator=(const Beacon& other) = delete;
/**
* Set the data for this Beacon and parse it
*/
void setFrame(const uint8_t* frameBuffer) {
frameMode = FrameMode::UNKNOWN;
mainLocatingDevice = MainLocatingDevice::UNDEFINED;
auxLocatingDevice = AuxLocatingDevice::UNDEFINED;
protocolCode = 0;
protocol = Protocol::UNKNOWN;
protocolType = ProtocolType::UNKNOWN;
country.code = 0;
country.alphaCode[0] = 0;
country.shortName[0] = 0;
location.clear();
identifier = 0;
hasAdditionalData = false;
additionalData[0] = 0;
hasSerialNumber = false;
serialNumber[0] = 0;
hexId[0] = 0;
bch1 = 0;
computedBch1 = 0;
hasBch2 = false;
bch1Corrected = false;
bch2 = 0;
computedBch2 = 0;
bch2Corrected = false;
isEmpty = true;
hasEmergency = false;
isAutoamticEmergency = false;
isMaritime = false;
emergencyType[0] = 0;
std::memcpy(frame, (const void*)frameBuffer, BEACON_DATA_SIZE);
parseFrame();
}
/**
* Flip the specified bit in this beacon's data
*/
void flipBit(int bit) {
int byteIdx = (bit - 1) / 8;
int bitIdx = 7 - ((bit - 1) % 8);
frame[byteIdx] ^= (1 << bitIdx);
}
/**
* Single bit error correction to match BCH1 or BCH2 code
*/
void simpleCorrection(bool isBCH1) {
for (int i = (isBCH1 ? 25 : 107); i <= (isBCH1 ? 85 : 132); i++) {
flipBit(i);
if ((isBCH1 ? (computeBCH1() == bch1) : (computeBCH2() == bch2))) {
if (isBCH1) {
computedBch1 = bch1;
bch1Corrected = true;
} else {
computedBch2 = bch2;
bch2Corrected = true;
}
break;
}
flipBit(i); // Restore bit value
}
}
/**
* Return the string representation of this beacon's type
*/
const char* getType() {
if ((protocol == Protocol::USER_EPIRB_MARITIME) || (protocol == Protocol::USER_EPIRB_RADIO) || (protocol == Protocol::STD_EPIRB) || (protocol == Protocol::STD_EPIRB_SERIAL) || (protocol == Protocol::NAT_EPIRB)) return "EPIRB";
if ((protocol == Protocol::USER_ELT) || (protocol == Protocol::STD_ELT_24) || (protocol == Protocol::STD_ELT_SERIAL) || (protocol == Protocol::STD_ELT_AIRCRAFT) || (protocol == Protocol::NAT_ELT) || (protocol == Protocol::ELT_DT)) return "ELT";
if ((protocol == Protocol::STD_PLB_SERIAL) || (protocol == Protocol::NAT_PLB)) return "PLB";
if ((protocol == Protocol::USER_TEST) || (protocol == Protocol::STD_TEST) || (protocol == Protocol::NAT_TEST)) return "TEST";
if (protocol == Protocol::USER_SERIAL) return "SRIAL";
if (protocol == Protocol::USER_ORB) return "ORB";
if (protocol == Protocol::USER_NAT) return "NAT";
if (protocol == Protocol::USER_2G) return "2G";
if (protocol == Protocol::STD_SHIP) return "SHIP";
if (protocol == Protocol::RLS) return "RLS";
if (protocol == Protocol::SPARE) return "SPARE";
return UNKNOWN_LABEL;
}
/**
* Returns true if this beacon has a main location device
*/
bool hasMainLocatingDevice() { return (mainLocatingDevice != MainLocatingDevice::UNDEFINED); }
/**
* Returns this beacon's main location device string representation
*/
const char* getMainLocatingDeviceName() {
if (mainLocatingDevice == MainLocatingDevice::EXTERNAL_NAV) return "Exernal";
if (mainLocatingDevice == MainLocatingDevice::INTERNAL_NAV) return "Internal";
return UNKNOWN_LABEL;
}
/**
* Returns true if this beacon has a main location device
*/
bool hasAuxLocatingDevice() { return (auxLocatingDevice != AuxLocatingDevice::UNDEFINED); }
/**
* Returns true if this beacon has a main location device
*/
const char* getAuxLocatingDeviceName() {
return ResourceManager::get_beacon_resource(AUX_DEVICE_RESOURCE_START + ((int)auxLocatingDevice));
}
/**
* Set the serial number for this beacon
*/
void setSerialNumber(uint32_t serial) {
sprintf(serialNumber, "%ld (0x%08lX)", serial, serial);
}
/**
* Returns true if BCH1 code is valid
*/
bool isBch1Valid() { return (bch1 == computedBch1); }
/**
* Returns true if BCH2 code is valid
*/
bool isBch2Valid() { return (bch2 == computedBch2); }
/**
* Returns true if frame is valid
*/
bool isFrameValid();
/**
* Returns true if frame is orbito
*/
bool isOrbito() { return (protocol == Protocol::USER_ORB); }
/**
* Write the hex preresentation of this frame in the provided buffer
*/
size_t hexString(char* buffer, bool withHeader) {
return toHexString(buffer, frame, false, (withHeader ? 0 : 3), (longFrame ? 18 : 14));
}
/**
* Returns the short ID for this beacon
*/
std::string shortId();
/**
* Returns the string representation of the status of this frame
*/
std::string getSatus() {
if (isFrameValid())
return "OK";
else
return "KO";
}
/**
* Format this beacon's time
*/
size_t formatTime(char* buffer);
/**
* Format this beacon's summary
*/
size_t formatSummary(char* buffer, bool with_time);
private:
/* Baudot code matrix */
static constexpr char BAUDOT_CODE[64] = {' ', '5', ' ', '9', ' ', ' ', ' ', ' ', ' ', ' ', '4', ' ', '8', '0', ' ', ' ',
'3', ' ', ' ', ' ', ' ', '6', ' ', '/', '-', '2', ' ', ' ', '7', '1', ' ', ' ',
' ', 'T', ' ', 'O', ' ', 'H', 'N', 'M', ' ', 'L', 'R', 'G', 'I', 'P', 'C', 'V',
'E', 'Z', 'D', 'B', 'S', 'Y', 'F', 'X', 'A', 'W', 'J', ' ', 'U', 'Q', 'K', '\0'};
/**
* Parse this beacon's protocol
*/
void parseProtocol() {
protocolFlag = getBits(26, 26);
if (protocolFlag)
protocolCode = getBits(37, 39);
else
protocolCode = getBits(37, 40);
if (!longFrame || protocolFlag == 1) {
switch (protocolCode) {
case 0b000:
protocol = Protocol::USER_ORB;
break;
case 0b001:
protocol = Protocol::USER_ELT;
break;
case 0b010:
protocol = Protocol::USER_EPIRB_MARITIME;
isMaritime = true;
break;
case 0b011:
protocol = Protocol::USER_SERIAL;
break;
case 0b100:
protocol = Protocol::USER_NAT;
break;
case 0b101:
protocol = Protocol::USER_2G;
break;
case 0b110:
protocol = Protocol::USER_EPIRB_RADIO;
isMaritime = true;
break;
case 0b111:
protocol = Protocol::USER_TEST;
break;
default:
protocol = Protocol::UNKNOWN;
}
} else {
switch (protocolCode) {
case 0b0010:
protocol = Protocol::STD_EPIRB;
break;
case 0b0011:
protocol = Protocol::STD_ELT_24;
break;
case 0b0100:
protocol = Protocol::STD_ELT_SERIAL;
break;
case 0b0101:
protocol = Protocol::STD_ELT_AIRCRAFT;
break;
case 0b0110:
protocol = Protocol::STD_EPIRB_SERIAL;
break;
case 0b0111:
protocol = Protocol::STD_PLB_SERIAL;
break;
case 0b1000:
protocol = Protocol::NAT_ELT;
break;
case 0b1001:
protocol = Protocol::ELT_DT;
break;
case 0b1010:
protocol = Protocol::NAT_EPIRB;
break;
case 0b1011:
protocol = Protocol::NAT_PLB;
break;
case 0b1100:
protocol = Protocol::STD_SHIP;
break;
case 0b1101:
protocol = Protocol::RLS;
break;
case 0b1110:
protocol = Protocol::STD_TEST;
break;
case 0b1111:
protocol = Protocol::NAT_TEST;
break;
default:
protocol = Protocol::UNKNOWN;
}
}
}
/**
* Parse this beacon's additional data
*/
void parseAdditionalData() {
hasAdditionalData = false;
hasSerialNumber = false;
if (protocolFlag) {
if (protocolCode == 0b011) {
uint8_t serialUserProtocol = getBits(40, 42);
hasAdditionalData = true;
switch (serialUserProtocol) {
case 0b100:
case 0b010:
isMaritime = true;
// fallthrough
case 0b000:
case 0b001:
case 0b011:
case 0b110:
strcpy(additionalData, ResourceManager::get_beacon_resource(ADDITIONAL_DATA_RESOURCE_START + serialUserProtocol));
break;
default:
hasAdditionalData = false;
}
hasSerialNumber = true;
setSerialNumber((serialUserProtocol == 0b011) ? getBits(44, 67) : (serialUserProtocol == 0b001) ? getBits(62, 73)
: getBits(44, 63));
}
if (!longFrame) {
hasEmergency = getBits(107, 107);
if (hasEmergency) {
isAutoamticEmergency = getBits(108, 108);
if (isMaritime) {
uint8_t emmergency = getBits(109, 112);
switch (emmergency) {
case 0b0001:
case 0b0010:
case 0b0011:
case 0b0100:
case 0b0101:
case 0b0110:
case 0b0111:
case 0b1000:
strcpy(emergencyType, ResourceManager::get_beacon_resource(EMERGENCY_RESOURCE_START + emmergency));
break;
default:
strcpy(emergencyType, ResourceManager::get_beacon_resource(EMERGENCY_RESOURCE_START));
}
} else {
bool fire = getBits(109, 109);
bool med = getBits(110, 110);
bool disabled = getBits(111, 111);
char* emmergency_pointer = emergencyType;
if (fire) emmergency_pointer += sprintf(emmergency_pointer, "%s", ResourceManager::get_beacon_resource(EMERGENCY_OTHER_RESOURCE_START));
if (med) {
if (fire) emmergency_pointer += sprintf(emmergency_pointer, "%c", '+');
emmergency_pointer += sprintf(emmergency_pointer, "%s", ResourceManager::get_beacon_resource(EMERGENCY_OTHER_RESOURCE_START + 1));
}
if (disabled) {
if (fire || med) emmergency_pointer += sprintf(emmergency_pointer, "%c", '+');
sprintf(emmergency_pointer, "%s", ResourceManager::get_beacon_resource(EMERGENCY_OTHER_RESOURCE_START + 2));
}
}
}
}
} else if (longFrame) {
switch (protocolCode) {
case 0b0010:
hasSerialNumber = true;
setSerialNumber(getBits(61, 64));
break;
case 0b1100: {
hasAdditionalData = true;
uint32_t mmsi = getBits(41, 60);
sprintf(additionalData, ResourceManager::get_beacon_resource(LONG_ADDITIONAL_DATA_RESOURCE_START), mmsi, mmsi);
} break;
case 0b0011: {
hasAdditionalData = true;
hasSerialNumber = true;
setSerialNumber(getBits(41, 64));
strcpy(additionalData, ResourceManager::get_beacon_resource(LONG_ADDITIONAL_DATA_RESOURCE_START + 1));
} break;
case 0b0100:
case 0b0110:
case 0b0111: {
hasAdditionalData = true;
hasSerialNumber = true;
uint32_t csTaNumber = getBits(41, 50);
sprintf(additionalData, ResourceManager::get_beacon_resource(LONG_ADDITIONAL_DATA_RESOURCE_START + 2), csTaNumber);
setSerialNumber(getBits(51, 64));
} break;
case 0b0101: {
hasAdditionalData = true;
hasSerialNumber = true;
uint32_t data = getBits(41, 45);
char char1 = BAUDOT_CODE[data + 32];
data = getBits(46, 50);
char char2 = BAUDOT_CODE[data + 32];
data = getBits(51, 55);
char char3 = BAUDOT_CODE[data + 32];
sprintf(additionalData, ResourceManager::get_beacon_resource(LONG_ADDITIONAL_DATA_RESOURCE_START + 3), char1, char2, char3);
setSerialNumber(getBits(56, 64));
} break;
case 0b1000:
case 0b1010:
case 0b1011:
case 0b1111: {
hasSerialNumber = true;
hasAdditionalData = true;
setSerialNumber(getBits(41, 58));
uint32_t natNum = getBits(127, 132);
sprintf(additionalData, ResourceManager::get_beacon_resource(LONG_ADDITIONAL_DATA_RESOURCE_START + 4), natNum);
} break;
}
}
}
/**
* Parse this beacon's locating device
*/
void parseLocatingDevices() {
bool mainLoc;
if (protocolIsStandard() || protocolIsNational()) {
mainLoc = getBits(111, 111);
mainLocatingDevice = mainLoc ? MainLocatingDevice::INTERNAL_NAV : MainLocatingDevice::EXTERNAL_NAV;
} else if (protocolIsUser() || protocolIsRls()) {
mainLoc = getBits(107, 107);
mainLocatingDevice = mainLoc ? MainLocatingDevice::INTERNAL_NAV : MainLocatingDevice::EXTERNAL_NAV;
}
if (protocolIsStandard() || protocolIsNational()) {
auxLocatingDevice = getBits(112, 112) ? AuxLocatingDevice::MHZ121_5 : AuxLocatingDevice::NONE_OR_OTHER;
} else if (protocolIsRls()) {
auxLocatingDevice = getBits(108, 108) ? AuxLocatingDevice::MHZ121_5 : AuxLocatingDevice::NONE_OR_OTHER;
} else if (protocolIsUser() && protocolCode != 0b100) {
uint8_t aux = getBits(84, 85);
if (aux == 0b00)
auxLocatingDevice = AuxLocatingDevice::NONE;
else if (aux == 0b01)
auxLocatingDevice = AuxLocatingDevice::MHZ121_5;
else if (aux == 0b10)
auxLocatingDevice = AuxLocatingDevice::SART;
else
auxLocatingDevice = AuxLocatingDevice::OTHER;
}
}
/**
* Parse this beacon's frame data
*/
void parseFrame() {
long latofmin, latofsec, lonofmin, lonofsec;
bool latoffset, lonoffset;
longFrame = getBits(25, 25);
parseProtocol();
protocolType = getProtocolType();
CountryManager::get_country(getBits(27, 36), country);
if (frame[2] == 0xD0)
frameMode = FrameMode::SELF_TEST;
else if (frame[2] == 0x2F)
frameMode = FrameMode::NORMAL;
else
frameMode = FrameMode::UNKNOWN;
if (longFrame) {
if (protocolIsUser() && !isOrbito()) {
location.latitude.orientation = (frame[13] & 0x10) >> 4;
location.latitude.degrees = ((frame[13] & 0x0F) << 3 | (frame[14] & 0xE0) >> 5);
location.latitude.minutes = ((frame[14] & 0x1E) >> 1) * 4;
location.longitude.orientation = (frame[14] & 0x01);
location.longitude.degrees = (frame[15]);
location.longitude.minutes = ((frame[16] & 0xF0) >> 4) * 4;
} else if (protocolIsNational()) {
latoffset = (frame[14] & 0x80) >> 7;
location.latitude.orientation = (frame[7] & 0x20) >> 5;
location.latitude.degrees = ((frame[7] & 0x1F) << 2 | (frame[8] & 0xC0) >> 6);
location.latitude.minutes = ((frame[8] & 0x3E) >> 1) * 2;
latofmin = (frame[14] & 0x60) >> 5;
latofsec = ((frame[14] & 0x1E) >> 1) * 4;
location.latitude.apply_offset(latoffset, latofmin, latofsec);
lonoffset = (frame[14] & 0x01);
location.longitude.orientation = (frame[8] & 0x01);
location.longitude.degrees = (frame[9]);
location.longitude.minutes = ((frame[10] & 0xF8) >> 3) * 2;
lonofmin = (frame[15] & 0xC0) >> 6;
lonofsec = ((frame[15] & 0x3C) >> 2) * 4;
location.longitude.apply_offset(lonoffset, lonofmin, lonofsec);
} else if (protocolIsStandard()) {
latoffset = (frame[14] & 0x80) >> 7;
location.latitude.orientation = (frame[8] & 0x80) >> 7;
location.latitude.degrees = (frame[8] & 0x7F);
location.latitude.minutes = ((frame[9] & 0xC0) >> 6) * 15;
latofmin = (frame[14] & 0x7C) >> 2;
latofsec = ((frame[14] & 0x03) << 2 | (frame[15] & 0xC0) >> 6) * 4;
location.latitude.apply_offset(latoffset, latofmin, latofsec);
lonoffset = (frame[15] & 0x20) >> 5;
location.longitude.orientation = (frame[9] & 0x20) >> 5;
location.longitude.degrees = ((frame[9] & 0x1F) << 3 | (frame[10] & 0xE0) >> 5);
location.longitude.minutes = ((frame[10] & 0x18) >> 3) * 15;
lonofmin = (frame[15] & 0x1F);
lonofsec = ((frame[16] & 0xF0) >> 4) * 4;
location.longitude.apply_offset(lonoffset, lonofmin, lonofsec);
} else if (protocolIsRlsOrElt()) {
latoffset = (frame[14] & 0x20) >> 5;
location.latitude.orientation = (frame[8] & 0x20) >> 5;
location.latitude.degrees = ((frame[8] & 0x1F) << 2) | ((frame[9] & 0xC0) >> 6);
location.latitude.minutes = ((frame[9] & 0x20) >> 5) * 30;
latofmin = (frame[14] & 0x1E) >> 1;
latofsec = ((frame[14] & 0x01) << 3 | (frame[15] & 0xE0) >> 5) * 4;
location.latitude.apply_offset(latoffset, latofmin, latofsec);
lonoffset = (frame[15] & 0x10) >> 4;
location.longitude.orientation = (frame[9] & 0x10) >> 4;
location.longitude.degrees = ((frame[9] & 0x0F) << 4 | (frame[10] & 0xF0) >> 4);
location.longitude.minutes = ((frame[10] & 0x08) >> 3) * 30;
lonofmin = (frame[15] & 0x0F);
lonofsec = ((frame[16] & 0xF0) >> 4) * 4;
location.longitude.apply_offset(lonoffset, lonofmin, lonofsec);
}
}
parseAdditionalData();
parseLocatingDevices();
identifier = getBits(26, 85);
uint32_t hexIdHigh = (uint32_t)(identifier >> 32);
uint32_t hexIdLow = (uint32_t)identifier;
if (protocolIsStandard()) {
// default value of bits 65 to 74 = 0 111111111 / default value of bits 75 to 85 = 0 1111111111
hexIdLow &= 0b11111111'11100000'00000000'00000000;
hexIdLow |= 0b00000000'00001111'11111011'11111111;
} else if (protocolIsNational()) {
// default value of bits 59 to 71 = 0 1111111 00000 / default value of bits 72 to 85 = 0 11111111 00000
hexIdLow &= 0b11111000'00000000'00000000'00000000;
hexIdLow |= 0b00000011'11111000'00011111'11100000;
} else if (protocolIsRlsOrElt()) {
// default value of bits 67 to 75 = 0 11111111 / default value of bits 76 to 85 = 0 111111111
hexIdLow &= 0b11111111'11111000'00000000'00000000;
hexIdLow |= 0b00000000'00000011'11111101'11111111;
}
std::sprintf(hexId, "%07lX%08lX", hexIdHigh, hexIdLow);
if (isOrbito() && !longFrame) {
isEmpty = true;
for (size_t i = 3; i < BEACON_DATA_SIZE; i++) {
if (frame[i] != 0) {
isEmpty = false;
break;
}
}
} else
isEmpty = false;
bch1 = getBits(86, 106);
computedBch1 = computeBCH1();
// Try and correct single bit error on BCH1 (bits 25-85)
if (computedBch1 != bch1) {
simpleCorrection(true);
}
hasBch2 = longFrame && !isOrbito();
if (hasBch2) {
bch2 = getBits(133, 144);
computedBch2 = computeBCH2();
// Try and correct single bit error on BCH2 (bits 107 - 132)
if (computedBch2 != bch2) {
simpleCorrection(false);
}
}
static bool isCorrecting = false;
// If BCH1 or BCH2 has been corrected, we need to parse frame again to update beacon properties
if (!isCorrecting && (bch1Corrected || bch2Corrected)) {
// Prevent recursion
isCorrecting = true;
parseFrame();
isCorrecting = false;
}
}
};
} // namespace ui::external_app::epirb_rx
#endif // __BEACON_RX_H__
+95
View File
@@ -0,0 +1,95 @@
/*
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
*
* 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 "beacon_db.hpp"
namespace ui::external_app::epirb_rx {
/**
* Add a beacon to the database.
* @return the added beacon reference.
*/
Beacon& BeaconDB::add_beacon() {
Beacon& result = recent_beacons[recent_beacon_pos];
recent_beacon_pos = (recent_beacon_pos + 1) % BEACON_HISTORY_SIZE;
if (recent_beacon_pos == 0) recent_beacon_full = true;
return result;
}
/**
* Get the size of the beacon database
* @return the size of the beacon database
*/
size_t BeaconDB::size() {
return recent_beacon_full ? BEACON_HISTORY_SIZE : recent_beacon_pos;
}
/**
* Returns true if the database is empty
* @return true if the database is empty
*/
bool BeaconDB::empty() {
return (!recent_beacon_full && (recent_beacon_pos == 0));
}
/**
* Returns the beacon at the provided index
*/
Beacon& BeaconDB::get_beacon(size_t index) {
// Start from last beacon et go backward
int16_t pos = (int16_t)recent_beacon_pos - 1 - index;
while (pos < 0) pos += BEACON_HISTORY_SIZE;
return recent_beacons[pos % BEACON_HISTORY_SIZE];
}
/**
* Set currently selected beacon
* @param index the selected index
*/
void BeaconDB::set_current_beacon(size_t index) {
current_beacon_index = index;
}
/**
* Get currently selected index
* @return the currently selected index
*/
size_t BeaconDB::get_current_beacon_index() {
return current_beacon_index;
}
/**
* Get the currently selected beacon
* @return the currently selected beacon
*/
Beacon& BeaconDB::get_current_beacon() {
return get_beacon(current_beacon_index);
}
/**
* Clear beacon database
*/
void BeaconDB::clear() {
recent_beacon_pos = 0;
recent_beacon_full = false;
}
} // namespace ui::external_app::epirb_rx
+56
View File
@@ -0,0 +1,56 @@
/*
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
*
* 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 __BEACON_DB_H__
#define __BEACON_DB_H__
#include "beacon.hpp"
namespace ui::external_app::epirb_rx {
// Size of beacon database calculated to match the beacon list component size
#define BEACON_HISTORY_SIZE 13
class BeaconDB {
public:
Beacon& add_beacon();
Beacon& get_beacon(size_t index);
Beacon& get_current_beacon();
size_t get_current_beacon_index();
void set_current_beacon(size_t index);
size_t size();
bool empty();
void clear();
private:
// Actual beacon database array
Beacon recent_beacons[BEACON_HISTORY_SIZE];
// Position of the insertion pointer
int8_t recent_beacon_pos{0};
// Beacon selection handling
size_t current_beacon_index{0};
// True when database is full
bool recent_beacon_full{false};
};
} // namespace ui::external_app::epirb_rx
#endif /* __BEACON_DB_H__ */
+122
View File
@@ -0,0 +1,122 @@
/*
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
*
* 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 __COUNTRY_RX_H__
#define __COUNTRY_RX_H__
#include "string_format.hpp"
#include "file_reader.hpp"
#include "file_path.hpp"
namespace ui::external_app::epirb_rx {
#define DISABLE_COUNTRY_CACHE
// Country struct
struct Country {
int16_t code;
char alphaCode[4]; // 3 chars + null
char shortName[11]; // 10 chars + null
};
class CountryManager {
public:
static void get_country(int code, Country& out) {
#ifndef DISABLE_COUNTRY_CACHE
// Check cache
for (int i = 0; i < cache_count; i++) {
if (cache[i].code == code) {
out = cache[i];
if (i > 0) { // Promotion
Country tmp = cache[i];
for (int j = i; j > 0; j--) cache[j] = cache[j - 1];
cache[0] = tmp;
}
return;
}
}
#endif
// Cache miss => load from file
if (load_from_file(code, out)) {
#ifndef DISABLE_COUNTRY_CACHE
// Insert in cache
int limit = (cache_count < 16) ? cache_count : 15;
for (int j = limit; j > 0; j--) cache[j] = cache[j - 1];
cache[0] = out;
if (cache_count < 16) cache_count++;
#endif
} else {
out.code = code;
out.alphaCode[0] = '\0';
out.shortName[0] = '\0';
}
}
private:
#ifndef DISABLE_COUNTRY_CACHE
static Country cache[16];
static int cache_count;
#endif
static bool load_from_file(int target_code, Country& out) {
FIL file;
if (f_open(&file, (epirb_dir / u"RES/COUNTRY.RES").tchar(), FA_READ) != FR_OK) return false;
TCHAR line[48];
bool found = false;
while (f_gets(line, sizeof(line) / sizeof(TCHAR), &file)) {
// Light parsing method
TCHAR* p = line;
// Parse CODE
int c = 0;
while (*p >= '0' && *p <= '9') c = c * 10 + (*p++ - '0');
if (c != target_code || *p != ':') continue;
p++; // Skip ':'
out.code = (int16_t)c;
// Parse ALPHA CODE (3 chars)
for (int i = 0; i < 3 && *p != ':'; i++) out.alphaCode[i] = (char)*p++;
out.alphaCode[3] = '\0';
if (*p != ':') continue;
p++; // Skip ':'
// Parse SHORT NAME
int i = 0;
while (i < static_cast<int>(sizeof(out.shortName) - 1) && *p != '\r' && *p != '\n' && *p != '\0') {
out.shortName[i++] = (char)*p++;
}
out.shortName[i] = '\0';
found = true;
break;
}
f_close(&file);
return found;
}
};
} // namespace ui::external_app::epirb_rx
#endif // __COUNTRY_RX_H__
+149
View File
@@ -0,0 +1,149 @@
/*
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
*
* 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 "location.hpp"
#include <cstdio>
#include "string_format.hpp"
namespace ui::external_app::epirb_rx {
void Location::Angle::clear() {
degrees = 255;
minutes = 0;
seconds = 0;
orientation = false;
floatValue = 255;
}
float Location::Angle::getFloatValue() {
if (floatValue >= 255) {
floatValue = (float)degrees;
floatValue += ((float)minutes / 60.0f);
floatValue += ((float)seconds / 3600.0f);
if (orientation) {
floatValue = -floatValue;
}
}
return floatValue;
}
void Location::Angle::apply_offset(bool positive, long ofmin, long ofsec) {
if (positive) {
minutes += ofmin;
seconds += ofsec;
} else {
minutes -= ofmin;
if (minutes < 0) {
minutes += 60;
degrees -= 1;
}
seconds -= ofsec;
if (seconds < 0) {
seconds += 60;
minutes -= 1;
}
}
}
void Location::clear() {
latitude.clear();
longitude.clear();
}
bool Location::isUnknown() {
return (latitude.degrees >= 255 || longitude.degrees >= 255);
}
char Location::gps_letterize(int x) {
return (char)x + 65;
}
size_t Location::gps_compute_locator(char* buffer, float lat, float lon, int precision) {
size_t result = 0;
lon += 180.0f;
lat += 90.0f;
int A = lon / 20;
int B = lat / 10;
lon -= A * 20;
lat -= B * 10;
int C = lon / 2;
int D = lat / 1;
lon -= C * 2;
lat -= D * 1;
int E = lon / (5.0f / 60.0f);
int F = lat / (2.5f / 60.0f);
lon -= E * (5.0f / 60.0f);
lat -= F * (2.5f / 60.0f);
int G = lon / (5.0f / 600.0f);
int H = lat / (2.5f / 600.0f);
buffer[result++] = char('A' + A);
buffer[result++] = char('A' + B);
if (precision >= 4) {
buffer[result++] = char('0' + C);
buffer[result++] = char('0' + D);
}
if (precision >= 6) {
buffer[result++] = char('a' + E);
buffer[result++] = char('a' + F);
}
if (precision >= 8) {
buffer[result++] = char('0' + G);
buffer[result++] = char('0' + H);
}
buffer[result] = 0;
return result;
}
size_t Location::toString(char* buffer, LocationFormat format, int precision) {
if (isUnknown()) {
return sprintf(buffer, "%s", "GPS not synchronized");
}
switch (format) {
case LocationFormat::SEXAGESIMAL: {
return sprintf(buffer, "%ld\xB0%02ld'%02ld\"%c, %ld\xB0%02ld'%02ld\"%c", // 0xB0 is degree ° symbol in our 8x16 font
latitude.degrees, latitude.minutes, latitude.seconds, latitude.orientation ? 'S' : 'N',
longitude.degrees, longitude.minutes, longitude.seconds, longitude.orientation ? 'W' : 'E');
}
case LocationFormat::MAIDENHEAD_LOCATOR:
return gps_compute_locator(buffer, latitude.getFloatValue(), longitude.getFloatValue(), precision);
default:
case LocationFormat::DECIMAL:
return sprintf(buffer, "%s, %s", to_string_decimal(latitude.getFloatValue(), 5).c_str(), to_string_decimal(longitude.getFloatValue(), 5).c_str());
}
}
void Location::formatFloatLocation(char* buffer, const char* format) {
sprintf(buffer, format, to_string_decimal(latitude.getFloatValue(), 6).c_str(), to_string_decimal(longitude.getFloatValue(), 6).c_str());
}
} // namespace ui::external_app::epirb_rx
+76
View File
@@ -0,0 +1,76 @@
/*
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
*
* 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 __LOCATION_RX_H__
#define __LOCATION_RX_H__
#include <cstddef>
namespace ui::external_app::epirb_rx {
class Location {
public:
class Angle {
public:
long degrees = 0;
long minutes = 0;
long seconds = 0;
bool orientation = false; // false = N/E, true = S/W
Angle() {}
Angle(long degrees)
: degrees(degrees) {}
void clear();
float getFloatValue();
// Apply an offset (minutes / seconds) with sign-based borrow into minutes/degrees.
// Out-of-line to keep parseFrame from inlining the same arithmetic 6 times.
void apply_offset(bool positive, long ofmin, long ofsec);
private:
float floatValue = 255.0f;
};
enum class LocationFormat { DECIMAL,
SEXAGESIMAL,
MAIDENHEAD_LOCATOR };
Angle latitude = Angle(127);
Angle longitude = Angle(255);
void clear();
bool isUnknown();
static char gps_letterize(int x);
// For code size optimization reasons, string manipulation is performed with char buffers
// with no char buffer size check. Caller methods take care of providing large enough buffers
static size_t gps_compute_locator(char* buffer, float lat, float lon, int precision = 6);
size_t toString(char* buffer, LocationFormat format, int precision = 6);
void formatFloatLocation(char* buffer, const char* format);
};
} // namespace ui::external_app::epirb_rx
#endif // __LOCATION_RX_H__
+1 -1
View File
@@ -73,7 +73,7 @@ __attribute__((section(".external_app.app_epirb_rx.application_information"), us
0x00,
0x00,
},
/*.icon_color = */ ui::Color::orange().v,
/*.icon_color = */ ui::Color::green().v,
/*.menu_location = */ app_location_t::RX,
/*.desired_menu_position = */ -1,
+100
View File
@@ -0,0 +1,100 @@
/*
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
*
* 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 __RESOUCES_H__
#define __RESOUCES_H__
#include <cstring>
#include <vector>
#include "file_reader.hpp"
#include "file_path.hpp"
namespace ui::external_app::epirb_rx {
#define UNKNOWN_LABEL "Unk."
extern std::vector<std::string> beacon_res;
extern std::vector<std::string> freq;
/**
* This class is used to load string resources from SD card.
* This is done mostly to reduce application size and keep it under the 32kb limit.
* It's also used to load frequency values, allowing users to customize frequency list by editing FREQ.TXT file on SD card.
*/
class ResourceManager {
public:
// Singleton getter
static ResourceManager* getInstance();
// Singleton instance
static ResourceManager* current;
// Used from standalone app, to prevent memleak
static void destroy();
// Get a string from BEACON.RES file
static const char* get_beacon_resource(uint8_t line) {
ResourceManager* rm = getInstance();
if (rm->beacon_res.empty()) {
load_file((epirb_dir / u"RES/BEACON.RES"), rm->beacon_res);
}
if (line < rm->beacon_res.size()) {
return rm->beacon_res[line].c_str();
}
return UNKNOWN_LABEL;
}
// Get frequency values
static const std::vector<std::string>& get_frequencies() {
ResourceManager* rm = getInstance();
if (rm->freq.empty()) {
load_file((epirb_dir / u"FREQ.TXT"), rm->freq);
}
return rm->freq;
}
private:
// BEACON.RES content
std::vector<std::string> beacon_res{};
// FREQ.TXT content
std::vector<std::string> freq{};
static void load_file(const std::filesystem::path& path, std::vector<std::string>& vect) {
FIL file;
if (f_open(&file, path.tchar(), FA_READ) == FR_OK) {
TCHAR line_buffer[32];
// Read the file line by line
while (f_gets(line_buffer, sizeof(line_buffer) / sizeof(TCHAR), &file)) {
std::string s;
for (int i = 0; line_buffer[i] != '\0' && line_buffer[i] != '\n' && line_buffer[i] != '\r'; i++) {
// Convert each TCHAR in char
s += (char)line_buffer[i];
}
vect.push_back(std::move(s));
}
f_close(&file);
}
if (vect.empty()) vect.push_back(UNKNOWN_LABEL);
}
};
} // namespace ui::external_app::epirb_rx
#endif // __RESOUCES_H__
+140
View File
@@ -0,0 +1,140 @@
/*
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
*
* 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_beaconlist.hpp"
#include <algorithm>
namespace ui::external_app::epirb_rx {
BeaconUIList::BeaconUIList(Rect parent_rect)
: View{parent_rect} {
this->set_focusable(true);
}
void BeaconUIList::paint(Painter& painter) {
auto rect = screen_rect();
// Clear view
painter.fill_rectangle(rect, Theme::getInstance()->bg_darkest->background);
if (!db_ || (db_->empty())) {
return;
}
auto base_style = Theme::getInstance()->bg_darkest;
for (auto offset = 0u; offset < BEACON_HISTORY_SIZE; ++offset) {
auto index = start_index_ + offset;
if (index < db_->size()) {
auto line_position = rect.location() + Point{0, 1 + (int)offset * char_height};
auto is_selected = (offset == selected_index_);
auto style = base_style;
// Get beacon entry and format it's summary
auto& entry = db_->get_beacon(index);
char buffer[64];
entry.formatSummary(buffer, true);
if (index == db_->get_current_beacon_index())
// If this is the currently displayed beacon change color
style = Theme::getInstance()->bg_medium;
// Draw entry line using stack buffer directly to avoid heap allocation
painter.draw_string(
line_position, (is_selected ? style->invert() : *style), buffer);
}
}
// Draw a bounding rectangle when focused.
painter.draw_rectangle(rect, (has_focus() ? Theme::getInstance()->bg_darkest->foreground : Theme::getInstance()->bg_darkest->background));
}
void BeaconUIList::on_show() {
set_dirty();
}
bool BeaconUIList::on_key(const KeyEvent key) {
if (!db_ || db_->empty())
return false;
if (key == KeyEvent::Select && on_select) {
// Select this beacon
on_select(get_index());
set_dirty();
return true;
}
auto delta = 0;
// Change position in the list
if (key == KeyEvent::Up && get_index() > 0)
delta = -1;
else if (key == KeyEvent::Down && get_index() < db_->size() - 1)
delta = 1;
else
return false;
adjust_selected_index(delta);
set_dirty();
return true;
}
bool BeaconUIList::on_encoder(EncoderEvent delta) {
if (!db_ || db_->empty())
return false;
// Change position in the list according to encoder
adjust_selected_index(delta);
set_dirty();
return true;
}
size_t BeaconUIList::get_index() const {
return start_index_ + selected_index_;
}
void BeaconUIList::set_db(BeaconDB& db) {
db_ = &db;
start_index_ = 0;
selected_index_ = 0;
set_dirty();
}
void BeaconUIList::adjust_selected_index(int delta) {
int32_t new_index = selected_index_ + delta;
// The selection went off the top of the screen, move up.
if (new_index < 0) {
start_index_ = std::max<int32_t>(start_index_ + new_index, 0);
selected_index_ = 0;
}
// Selection is off the bottom of the screen, move down.
else if (new_index >= (int32_t)BEACON_HISTORY_SIZE) {
start_index_ = std::min<int32_t>(start_index_ + delta, db_->size() - BEACON_HISTORY_SIZE);
selected_index_ = BEACON_HISTORY_SIZE - 1;
}
// Otherwise, scroll within the screen, but not past the end.
else {
selected_index_ = std::min<int32_t>(new_index, db_->size() - 1);
}
}
} // namespace ui::external_app::epirb_rx
@@ -0,0 +1,61 @@
/*
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
*
* 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_BEACONLIST_H__
#define __UI_BEACONLIST_H__
#include "ui.hpp"
#include "ui_painter.hpp"
#include "ui_widget.hpp"
#include "beacon_db.hpp"
namespace ui::external_app::epirb_rx {
/**
* Beacon list component
*/
class BeaconUIList : public View {
public:
std::function<void(size_t)> on_select{};
BeaconUIList(Rect parent_rect);
BeaconUIList(const BeaconUIList& other) = delete;
BeaconUIList& operator=(const BeaconUIList& other) = delete;
void paint(Painter& painter) override;
void on_show() override;
bool on_key(const KeyEvent key) override;
bool on_encoder(EncoderEvent delta) override;
size_t get_index() const;
void set_db(BeaconDB& db);
private:
void adjust_selected_index(int index);
BeaconDB* db_{nullptr};
size_t start_index_{0};
size_t selected_index_{0};
};
} // namespace ui::external_app::epirb_rx
#endif /*__UI_BEACONLIST_H__*/
File diff suppressed because it is too large Load Diff
+216 -189
View File
@@ -1,5 +1,6 @@
/*
* Copyright (C) 2024 EPIRB Decoder Implementation
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
*
* This file is part of PortaPack.
*
@@ -29,267 +30,293 @@
#include "ui_receiver.hpp"
#include "ui_geomap.hpp"
// Specan is disable to keep application size below the 32k limit
// #define SPECAN
// Comment to disable timout reset on select and save approx 200 bytes of flash
#ifndef PRALINE
// Application does not fit on Praline with RESET_TIMER enabled
#define RESET_TIMER
#endif
// Comment to disable squelch control
#define SQUELCH
// Comment to disable beacon selection by encoder on detail tab
#define DETAIL_TAB_BEACON_SEL
// #define LOGGER
#ifdef SPECAN
#include "ui_spectrum.hpp"
#endif
#include "ui_tabview.hpp"
#include "ui_qrcode.hpp"
#include "event_m0.hpp"
#include "signal.hpp"
#include "message.hpp"
#include "log_file.hpp"
#include "baseband_packet.hpp"
/* #include <cstdint>
#include <cstddef>
#include <string>
#include <array> */
#include "audio.hpp"
#include "beacon.hpp"
#include "beacon_db.hpp"
#include "ui_beaconlist.hpp"
#include "resources.hpp"
namespace ui::external_app::epirb_rx {
// EPIRB 406 MHz beacon types
enum class BeaconType : uint8_t {
OrbitingLocationBeacon = 0,
PersonalLocatorBeacon = 1,
EmergencyLocatorTransmitter = 2,
SerialELT = 3,
NationalELT = 4,
Other = 15
};
// EPIRB distress and emergency types
enum class EmergencyType : uint8_t {
Fire = 0,
Flooding = 1,
Collision = 2,
Grounding = 3,
Sinking = 4,
Disabled = 5,
Abandoning = 6,
Piracy = 7,
Man_Overboard = 8,
Other = 15
};
struct EPIRBLocation {
float latitude; // degrees, -90 to +90
float longitude; // degrees, -180 to +180
bool valid;
EPIRBLocation()
: latitude(0.0f), longitude(0.0f), valid(false) {}
EPIRBLocation(float lat, float lon)
: latitude(lat), longitude(lon), valid(true) {}
};
/**
* Status of a packet
*/
enum class PacketStatus : uint8_t {
Valid = 0,
Corrected = 1,
Error = 2
};
struct EPIRBBeacon {
uint32_t beacon_id;
BeaconType beacon_type;
EmergencyType emergency_type;
EPIRBLocation location;
uint32_t country_code;
std::string vessel_name;
rtc::RTC timestamp;
uint32_t sequence_number;
PacketStatus packet_status;
uint8_t error_count;
EPIRBBeacon()
: beacon_id(0), beacon_type(BeaconType::Other), emergency_type(EmergencyType::Other), location(), country_code(0), vessel_name(), timestamp(), sequence_number(0), packet_status(PacketStatus::Error), error_count(0) {}
};
class EPIRBDecoder {
public:
static EPIRBBeacon decode_packet(const baseband::Packet& packet);
private:
static EPIRBLocation decode_location(const std::array<uint8_t, 16>& data);
static BeaconType decode_beacon_type(uint8_t type_bits);
static EmergencyType decode_emergency_type(uint8_t emergency_bits);
static uint32_t decode_country_code(const std::array<uint8_t, 16>& data);
static std::string decode_vessel_name(const std::array<uint8_t, 16>& data);
// BCH error correction methods
static PacketStatus perform_bch_check(std::array<uint8_t, 16>& data, uint8_t& error_count);
static uint32_t calculate_bch_syndrome(const std::array<uint8_t, 16>& data);
static bool correct_single_error(std::array<uint8_t, 16>& data, uint32_t syndrome);
static uint8_t count_bit_errors(const std::array<uint8_t, 16>& original, const std::array<uint8_t, 16>& corrected);
};
// Position of tabs in tab view
#define EPIRB_TAB_POS_Y (UI_POS_Y(4) + 3 * 8)
// Height of tabs in tab view
#define EPIRB_TAB_HEIGHT (screen_height - EPIRB_TAB_POS_Y - UI_POS_HEIGHT(1))
#ifdef LOGGER
class EPIRBLogger {
public:
Optional<File::Error> append(const std::filesystem::path& filename) {
return log_file.append(filename);
}
void on_packet(const EPIRBBeacon& beacon);
void on_packet(Beacon& beacon);
private:
LogFile log_file{};
};
#endif
// Forward declarations of formatting functions
std::string format_beacon_type(BeaconType type);
std::string format_emergency_type(EmergencyType type);
std::string format_packet_status(PacketStatus status);
ui::Color get_packet_status_color(PacketStatus status);
class EPIRBBeaconDetailView : public ui::View {
/**
* Dedicated TextArea component used to optimize application code size
*/
class TextArea : public Widget {
public:
std::function<void(void)> on_close{};
TextArea(Rect parent_rect);
EPIRBBeaconDetailView(ui::NavigationView& nav);
EPIRBBeaconDetailView(const EPIRBBeaconDetailView&) = delete;
EPIRBBeaconDetailView& operator=(const EPIRBBeaconDetailView&) = delete;
#ifdef RESET_TIMER
std::function<void(TextArea&)> on_select{};
bool on_key(const KeyEvent key) override;
#endif
void set_beacon(const EPIRBBeacon& beacon);
const EPIRBBeacon& beacon() const { return beacon_; }
void focus() override;
void paint(ui::Painter&) override;
ui::GeoMapView* get_geomap_view() { return geomap_view; }
void set_content(std::string_view value);
void paint(Painter& painter) override;
private:
EPIRBBeacon beacon_{};
ui::Button button_done{
{125, 224, 96, 24},
"Done"};
ui::Button button_see_map{
{19, 224, 96, 24},
"See on map"};
ui::GeoMapView* geomap_view{nullptr};
ui::Rect draw_field(
ui::Painter& painter,
const ui::Rect& draw_rect,
const ui::Style& style,
const std::string& label,
const std::string& value);
std::string content{};
};
class EPIRBAppView : public ui::View {
// Forward declaration
class EPIRBAppView;
/**
* View for beacon detail tab
*/
class EPIRBDetailView : public View {
public:
EPIRBDetailView(Rect parent_rect, EPIRBAppView& parent);
void set_beacon(Beacon& beacon);
#ifdef DETAIL_TAB_BEACON_SEL
bool on_encoder(EncoderEvent delta) override;
#endif
private:
TextArea text_beacon{{UI_POS_X(0), UI_POS_Y(0), UI_POS_MAXWIDTH, EPIRB_TAB_HEIGHT}};
EPIRBAppView& parent_app;
};
#define EPIRB_RX_DEFAULT_LATITUDE 43.604f
#define EPIRB_RX_DEFAULT_LONGITUDE 1.458f
/**
* View for beacon map tab
*/
class EPIRBMapView : public View {
public:
EPIRBMapView(Rect parent_rect);
void paint(Painter& painter) override;
void on_show() override;
void set_main_marker(const std::string& label, float lat, float lon);
void clear_markers();
void add_marker(GeoMarker& marker);
void hide_map(bool hide);
void repaint();
private:
GeoMap geomap{{0, 0, UI_POS_MAXWIDTH, EPIRB_TAB_HEIGHT}};
float lat_{EPIRB_RX_DEFAULT_LATITUDE};
float lon_{EPIRB_RX_DEFAULT_LONGITUDE};
bool map_hidden{true};
};
#define QR_WIDTH 126
#define QR_HEIGHT 127
/**
* Vieaw for Beacon QR tab
*/
class EPRIBQRView : public View {
public:
EPRIBQRView(Rect parent_rect);
EPRIBQRView(const EPRIBQRView&) = delete;
EPRIBQRView& operator=(const EPRIBQRView&) = delete;
void set_beacon(Beacon* beacon);
void update_qr();
void update_display();
private:
bool show_map{true};
Beacon* current_beacon{nullptr};
char qr_url[128];
OptionsField options_qr{
{UI_POS_X(5), UI_POS_Y(1)},
6,
{{"Map", 0},
{"Detail", 1}}};
QRCodeImage qr_code{
{UI_POS_MAXWIDTH - QR_WIDTH - UI_POS_X(1), UI_POS_Y(1), QR_WIDTH, QR_HEIGHT}};
TextArea text_data{{UI_POS_X(0), UI_POS_Y(1), UI_POS_MAXWIDTH, EPIRB_TAB_HEIGHT - UI_POS_Y(1)}};
};
#ifdef SPECAN
class EPIRBRxView : public spectrum::WaterfallView {
public:
EPIRBRxView(EPIRBAppView& parent, Rect parent_rect);
void on_show() override;
void on_hide() override;
private:
EPIRBAppView& app_view;
};
#endif
class EPIRBAppView final : public ui::View {
public:
EPIRBAppView(ui::NavigationView& nav);
~EPIRBAppView();
void set_parent_rect(const ui::Rect new_parent_rect) override;
void paint(ui::Painter&) override;
void focus() override;
void refresh();
// Message to configure rx baseband
EPIRBRXConfig epirb_rx_config_message{};
void send_config();
// Beacons database
BeaconDB beacon_db{};
// Update display when beacon selection changed0
void on_beacon_change();
std::string title() const override { return "EPIRB RX"; }
private:
uint8_t squelch{50};
// The delay between each frame
uint32_t countdown{50};
app_settings::SettingsManager settings_{
"rx_epirb", app_settings::Mode::RX};
"rx_epirb",
app_settings::Mode::RX,
{
{"epirb_squelch"sv, &squelch},
{"countdown"sv, &countdown},
}};
ui::NavigationView& nav_;
std::vector<EPIRBBeacon> recent_beacons{};
#ifdef LOGGER
std::unique_ptr<EPIRBLogger> logger{};
#endif
EPIRBBeaconDetailView beacon_detail_view{nav_};
static constexpr auto header_height = 4 * 16;
ui::Text label_frequency{
{UI_POS_X(0), UI_POS_Y(0), 4 * 8, 1 * 16},
"Freq"};
ui::OptionsField options_frequency{
{5 * 8, UI_POS_Y(0)},
OptionsField options_frequency{
{UI_POS_X(0), UI_POS_Y(0)},
7,
{
{"406.028", 406028000},
{"406.025", 406025000},
{"406.037", 406037000},
{"433.025", 433025000},
{"144.875", 144875000},
}};
{}};
ui::RFAmpField field_rf_amp{
{13 * 8, UI_POS_Y(0)}};
{UI_POS_X(8), UI_POS_Y(0)}};
ui::LNAGainField field_lna{
{15 * 8, UI_POS_Y(0)}};
{UI_POS_X(10), UI_POS_Y(0)}};
ui::VGAGainField field_vga{
{18 * 8, UI_POS_Y(0)}};
{UI_POS_X(13), UI_POS_Y(0)}};
ui::RSSI rssi{
{UI_POS_X(21), 0, UI_POS_WIDTH_REMAINING(24), 4}};
{UI_POS_X(16), UI_POS_Y(0), UI_POS_WIDTH_REMAINING(22), 4}};
ui::Channel channel{
{UI_POS_X(21), 5, UI_POS_WIDTH_REMAINING(24), 4}};
{UI_POS_X(16), UI_POS_Y(0) + 5, UI_POS_WIDTH_REMAINING(22), 4}};
// ui::Audio audio{
// {UI_POS_X(16), UI_POS_Y(0) + 10, UI_POS_WIDTH_REMAINING(22), 4}};
ui::AudioVolumeField field_volume{
{screen_width - 2 * 8, UI_POS_Y(0)}};
{UI_POS_WIDTH_REMAINING(2), UI_POS_Y(0)}};
#ifdef SQUELCH
NumberField field_squelch{
{UI_POS_WIDTH_REMAINING(5), UI_POS_Y(0)},
2,
{0, 99},
1,
' '};
#endif
// Status display
ui::Text label_status{
{UI_POS_X(0), 1 * 16, 15 * 8, 1 * 16},
"Listening..."};
ui::Text label_beacons_count{
{16 * 8, 1 * 16, 14 * 8, 1 * 16},
"Beacons: 0"};
ui::Text label_packet_stats{
{UI_POS_X(0), 3 * 16, 29 * 8, 1 * 16},
""};
// Latest beacon info display
ui::Text label_latest{
{UI_POS_X(0), 2 * 16, 8 * 8, 1 * 16},
"Latest:"};
ui::Text text_latest_info{
{8 * 8, 2 * 16, 22 * 8, 1 * 16},
""};
// Beacon list
ui::Console console{
{0, 4 * 16, 240, 152}};
ui::Button button_map{
{0, 224, 60, 24},
"Map"};
ui::Button button_clear{
{64, 224, 60, 24},
"Clear"};
ui::Button button_log{
{128, 224, 60, 24},
"Log"};
TextArea text_status{{UI_POS_X(0), UI_POS_Y(1), UI_POS_MAXWIDTH, UI_POS_HEIGHT(3)}};
TextArea text_timeout{
{UI_POS_X(13), UI_POS_Y(1), UI_POS_WIDTH(3), UI_POS_HEIGHT(1)}};
SignalToken signal_token_tick_second{};
uint32_t beacons_received = 0;
uint32_t packets_valid = 0;
uint32_t packets_corrected = 0;
uint32_t packets_error = 0;
// Timeout string
int16_t timeout{0};
// Tab View
Rect view_rect = {0, EPIRB_TAB_POS_Y, UI_POS_MAXWIDTH, EPIRB_TAB_HEIGHT};
BeaconUIList view_list{view_rect};
EPIRBDetailView view_detail{view_rect, (*this)};
EPIRBMapView view_map{view_rect};
#ifdef SPECAN
EPIRBRxView view_rx{*this, view_rect};
#endif
EPRIBQRView view_qr{view_rect};
TabView tab_view{
{"List", Theme::getInstance()->fg_cyan->foreground, &view_list},
{"Detail", Theme::getInstance()->fg_green->foreground, &view_detail},
{"Map", Theme::getInstance()->fg_yellow->foreground, &view_map},
#ifdef SPECAN
{"RX", Theme::getInstance()->fg_orange->foreground, &view_rx},
#endif
{"QR", Theme::getInstance()->fg_orange->foreground, &view_qr}};
uint16_t beacons_received = 0;
uint16_t packets_valid = 0;
uint16_t packets_corrected = 0;
uint16_t packets_error = 0;
MessageHandlerRegistration message_handler_packet{
Message::ID::EPIRBPacket,
[this](Message* const p) {
const auto message = static_cast<const EPIRBPacketMessage*>(p);
this->on_packet(message->packet);
}};
[this](Message* const p) { on_packet(p); }};
void on_packet(const baseband::Packet& packet);
void on_beacon_decoded(const EPIRBBeacon& beacon);
void on_show_map();
void on_clear_beacons();
void on_toggle_log();
static void decode_packet(const baseband::Packet& packet, Beacon& beacon);
void on_packet(Message* const p);
void update_map();
void on_tick_second();
void update_display();
std::string format_beacon_summary(const EPIRBBeacon& beacon);
std::string format_location(const EPIRBLocation& location);
};
} // namespace ui::external_app::epirb_rx
+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__*/
+61 -57
View File
@@ -24,7 +24,6 @@
#include "portapack.hpp"
#include "ui_epirb_tx.hpp"
#include <cmath>
#include <cctype>
#include <cstdio>
namespace ui::external_app::epirb_tx {
@@ -37,17 +36,17 @@ namespace ui::external_app::epirb_tx {
* @param min output minutes value
* @param sec output seconds value
*/
static void decimal_to_dms(double value, bool& negative, uint16_t& deg, uint8_t& min, uint8_t& sec) {
static void decimal_to_dms(float value, bool& negative, int16_t& deg, int8_t& min, int8_t& sec) {
negative = value < 0;
value = std::fabs(value);
deg = (uint16_t)value;
deg = (int16_t)value;
double m = (value - deg) * 60.0;
min = (uint8_t)m;
float m = (value - deg) * 60.0f;
min = (int8_t)m;
double s = (m - min) * 60.0;
sec = (uint8_t)(s + 0.5);
float s = (m - min) * 60.0f;
sec = (int8_t)(s + 0.5f);
if (sec == 60) {
sec = 0;
@@ -68,8 +67,8 @@ static void decimal_to_dms(double value, bool& negative, uint16_t& deg, uint8_t&
* @param sec seconds value
* @return value the decimal value
*/
static double dms_to_decimal(bool negative, uint16_t deg, uint8_t min, uint8_t sec) {
double v = deg + min / 60.0 + sec / 3600.0;
static float dms_to_decimal(bool negative, int16_t deg, int8_t min, int8_t sec) {
float v = deg + min / 60.0f + sec / 3600.0f;
return negative ? -v : v;
}
@@ -80,55 +79,60 @@ static double dms_to_decimal(bool negative, uint16_t deg, uint8_t min, uint8_t s
* @param lon output longitude
*/
static void maidenhead_to_decimal(const std::string& loc, float& lat, float& lon) {
static const double lon_step[] =
static const float lon_step[] =
{
20.0,
2.0,
1.0 / 12.0,
1.0 / 120.0,
1.0 / 2880.0};
20.0f,
2.0f,
1.0f / 12.0f,
1.0f / 120.0f,
1.0f / 2880.0f};
static const double lat_step[] =
static const float lat_step[] =
{
10.0,
1.0,
1.0 / 24.0,
1.0 / 240.0,
1.0 / 5760.0};
10.0f,
1.0f,
1.0f / 24.0f,
1.0f / 240.0f,
1.0f / 5760.0f};
lon = -180.0;
lat = -90.0;
lon = -180.0f;
lat = -90.0f;
int pairs = loc.size() / 2;
if (pairs > 5)
pairs = 5;
for (int i = 0; i < pairs; i++) {
char c1 = std::toupper(loc[i * 2]);
char c2 = std::toupper(loc[i * 2 + 1]);
if (pairs > 0) {
for (int i = 0; i < pairs; i++) {
char c1 = loc[i * 2];
char c2 = loc[i * 2 + 1];
if (c1 >= 'a' && c1 <= 'z') c1 -= ('a' - 'A');
if (c2 >= 'a' && c2 <= 'z') c2 -= ('a' - 'A');
double lon_size = lon_step[i];
double lat_size = lat_step[i];
float lon_size = lon_step[i];
float lat_size = lat_step[i];
int v1, v2;
int v1, v2;
if (i % 2 == 0) // letters
{
v1 = c1 - 'A';
v2 = c2 - 'A';
} else // digits
{
v1 = c1 - '0';
v2 = c2 - '0';
if (i % 2 == 0) // letters
{
v1 = c1 - 'A';
v2 = c2 - 'A';
} else // digits
{
v1 = c1 - '0';
v2 = c2 - '0';
}
lon += v1 * lon_size;
lat += v2 * lat_size;
}
lon += v1 * lon_size;
lat += v2 * lat_size;
// Cell center
lon += lon_step[pairs - 1] / 2.0f;
lat += lat_step[pairs - 1] / 2.0f;
}
// Cell center
lon += lon_step[pairs - 1] / 2.0;
lat += lat_step[pairs - 1] / 2.0;
}
/**
@@ -138,9 +142,9 @@ static void maidenhead_to_decimal(const std::string& loc, float& lat, float& lon
* @param precision (optional, defaults to 8) the number of charater for the maidenhead locator
* @return the locator
*/
static std::string decimal_to_maidenhead(double lat, double lon, int precision = 8) {
lon += 180.0;
lat += 90.0;
static std::string decimal_to_maidenhead(float lat, float lon, int precision = 8) {
lon += 180.0f;
lat += 90.0f;
int A = lon / 20;
int B = lat / 10;
@@ -154,14 +158,14 @@ static std::string decimal_to_maidenhead(double lat, double lon, int precision =
lon -= C * 2;
lat -= D * 1;
int E = lon / (5.0 / 60.0);
int F = lat / (2.5 / 60.0);
int E = lon / (5.0f / 60.0f);
int F = lat / (2.5f / 60.0f);
lon -= E * (5.0 / 60.0);
lat -= F * (2.5 / 60.0);
lon -= E * (5.0f / 60.0f);
lat -= F * (2.5f / 60.0f);
int G = lon / (5.0 / 600.0);
int H = lat / (2.5 / 600.0);
int G = lon / (5.0f / 600.0f);
int H = lat / (2.5f / 600.0f);
std::string locator;
@@ -247,9 +251,9 @@ void init_from_dms(Location& loc) {
* Convert the provided Loaction to it's latitude string (format <xxx°yy'zz"N>)
*/
std::string to_latitude_string(const Location& location) {
char buffer[16];
char buffer[20];
// Format : <xxx°yy'zz"N>
snprintf(buffer, sizeof(buffer), "%3d\260%02d'%02d\"%c", location.lat_deg, location.lat_min, location.lat_sec, location.south ? 'S' : 'N');
sprintf(buffer, "%3d\260%02d'%02d\"%c", location.lat_deg, location.lat_min, location.lat_sec, location.south ? 'S' : 'N');
return std::string(buffer);
}
@@ -257,12 +261,12 @@ std::string to_latitude_string(const Location& location) {
* Convert the provided Loaction to it's longitude string (format <xxx°yy'zz"W>)
*/
std::string to_longitude_string(const Location& location) {
char buffer[16];
char buffer[20];
// Format : <xxx°yy'zz"W>
snprintf(buffer, sizeof(buffer), "%3d\260%02d'%02d\"%c", location.long_deg, location.long_min, location.long_sec, location.west ? 'W' : 'E');
sprintf(buffer, "%3d\260%02d'%02d\"%c", location.long_deg, location.long_min, location.long_sec, location.west ? 'W' : 'E');
return std::string(buffer);
}
} // namespace ui::external_app::epirb_tx
#endif /*__LOCATION_H__*/
#endif /*__LOCATION_H__*/
File diff suppressed because it is too large Load Diff
+438 -404
View File
@@ -1,404 +1,438 @@
/*
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
*
* 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 __EPIRB_TX_H__
#define __EPIRB_TX_H__
#include "app_settings.hpp"
#include "radio_state.hpp"
#include "ui.hpp"
#include "ui_widget.hpp"
#include "ui_navigation.hpp"
#include "ui_transmitter.hpp"
#include "portapack.hpp"
#include "message.hpp"
#include "tonesets.hpp"
#define BEACON_HEXA_SIZE 36
#define BEACON_HEXA_HALF_SIZE 18
#define BEACON_SIZE 18
#define AM_TEST_FREQUENCY 121375000
#define AM_REAL_FREQUENCY 121500000
#define BPSK_FREQUENCY_HAM 433025000
#define BPSK_FREQUENCY_B 406025000
#define BPSK_FREQUENCY_C 406028000
#define BPSK_FREQUENCY_F 406037000
#define BPSK_FREQUENCY_G 406040000
#define BPSK_FREQUENCY_J 406049000
#define BPSK_FREQUENCY_K 406052000
#define BPSK_FREQUENCY_N 406061000
#define BPSK_FREQUENCY_O 406064000
namespace ui::external_app::epirb_tx {
enum class BeaconMode {
FILE = 0,
MODE_MANUAL = 1
};
enum class BeaconType {
EPIRB = 0,
ELT = 1,
PLB = 2
};
enum class BeaconProtocol {
USER = 0,
STANDARD = 1,
NATIONAL = 2
};
enum class AmChannel {
TEST = 0,
REAL = 1,
MANUAL = 2
};
enum class BpskChannel {
HAM = 0,
B = 1,
C = 2,
F = 3,
G = 4,
J = 5,
K = 6,
N = 7,
O = 8,
MANUAL = 10
};
struct Location {
std::string locator;
bool south;
uint16_t lat_deg;
uint8_t lat_min;
uint8_t lat_sec;
float latitude;
bool west;
uint16_t long_deg;
uint8_t long_min;
uint8_t long_sec;
float longitude;
};
struct BeaconParams {
BeaconType type;
BeaconProtocol protocol;
uint32_t country;
bool is_test;
bool is_internal;
bool has_121_5;
Location location;
};
class EPIRBTXAppView : public View {
public:
EPIRBTXAppView(NavigationView& nav);
~EPIRBTXAppView();
void focus() override;
std::string title() const override { return "EPIRB TX"; };
private:
void start_tx();
void stop_tx();
void update_config();
void on_tx_progress(const uint32_t progress, const bool done);
void on_timer();
void load_beacons();
void set_tx_button_state(bool active);
std::string frame_to_hex_string(bool start);
void generate_frame(BeaconParams params);
void update_frame(bool updateConfig = true);
void update_bpsk_frequency();
void update_am_transmission();
void update_mode();
void update_location(bool updateLocatorField = true);
struct Beacon {
std::string title{};
std::string description{};
std::string frame{};
};
std::vector<Beacon> beacons{};
Beacon default_beacon{"Self test", "Serial User Location Protocol", "FFFED0D6E6202820000C29FF51041775302D"};
BeaconParams beacon_params{BeaconType::ELT, BeaconProtocol::STANDARD, 227, true, true, true, {"JN03RO", false, 0, 0, 0, 0, false, 0, 0, 0, 0}};
// Currently selected beacon index (from BEACONS.TXT file / options_frame combo)
uint32_t selected_beacon{0};
// Frequency of the transmitter before starting the app (used to restore frequency when leaving)
rf::Frequency original_frequency{0};
// Frequency of the AM emergency signal
rf::Frequency am_frequency{AM_TEST_FREQUENCY};
// Frequency of the 406 MHz BPSK signal
rf::Frequency bpsk_frequency{BPSK_FREQUENCY_HAM};
// Selected am channel
uint8_t am_channel{(uint8_t)AmChannel::TEST};
// Selected bpsk channel
uint8_t bpsk_channel{(uint8_t)BpskChannel::HAM};
// Manual AM frequency value
rf::Frequency manual_am_frequency{AM_TEST_FREQUENCY};
// Manual BPSK frequency value
rf::Frequency manual_bpsk_frequency{BPSK_FREQUENCY_HAM};
// True when using a beacon from the BEACONS.TXT file
bool mode_file{false};
// True when looping on sending beacons is enabled
bool loop_enabled{true};
// True if AM emergency signal transmission is enabled
bool am_enabled{true};
// True if we want to send a new frame each time the user changes the current beacon
bool send_on_change{true};
// The current locator string
std::string locator{"JN03RO"};
// The delay between each frame when on loop mode
uint32_t delay{50};
uint8_t beacon_type{(uint8_t)BeaconType::EPIRB};
// Currently selected beacon protocol
uint8_t beacon_protocol{(uint8_t)BeaconProtocol::USER};
// Currently selected beacon country
uint32_t beacon_country{227};
// Current beacon's internal state (true for internal location system)
bool beacon_internal{true};
TxRadioState radio_state_{
0 /* frequency */,
1750000 /* bandwidth */,
TONES_SAMPLERATE /* sampling rate */
};
app_settings::SettingsManager settings_{
"tx_epirb",
app_settings::Mode::TX,
{
{"sbeacon"sv, &selected_beacon},
{"amfreq"sv, &am_frequency},
{"bpskfreq"sv, &bpsk_frequency},
{"amchan"sv, &am_channel},
{"bpskchan"sv, &bpsk_channel},
{"loop"sv, &loop_enabled},
{"delay"sv, &delay},
{"file"sv, &mode_file},
{"am"sv, &am_enabled},
{"soc"sv, &send_on_change},
{"type"sv, &beacon_type},
{"proto"sv, &beacon_protocol},
{"country"sv, &beacon_country},
{"internal"sv, &beacon_internal},
{"locator"sv, &locator},
}};
// Time of the last sent frame
uint32_t last_frame_time{0};
// True when transmission is enabled
bool transmitting{false};
// True when transmitting a BPSK frame
bool transmitting_bpsk{false};
// True when currently looping on sending beacons
bool loop{false};
// Current EPIRBTXDataMessage for baseband
EPIRBTXDataMessage epirb_tx_message{};
const size_t max_text_width = UI_POS_WIDTH_REMAINING(6) / UI_POS_DEFAULT_WIDTH;
const size_t max_text_width_ext = UI_POS_WIDTH_REMAINING(0) / UI_POS_DEFAULT_WIDTH;
Labels labels{
{{UI_POS_X(0), UI_POS_Y(0)}, "Source:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(6)}, "Frame:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(10)}, "Next frame in s.", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(12)}, "AM frequency: MHz", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(14)}, "AM chan.:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(15)}, "BPSK chan.:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(17), UI_POS_Y(9)}, "s.", Theme::getInstance()->fg_light->foreground}};
// For file mode
Text text_beacon{
{UI_POS_X(0), UI_POS_Y(1), UI_POS_WIDTH(7), UI_POS_DEFAULT_HEIGHT},
"Beacon:"};
// Beacon selection from BEACONS.TXT
OptionsField options_frame{
{UI_POS_X(7), UI_POS_Y(1)},
30,
{}};
Text text_description_label{
{UI_POS_X(0), UI_POS_Y(2), UI_POS_WIDTH(12), UI_POS_DEFAULT_HEIGHT},
"Description:"};
Text text_description{
{UI_POS_X(0), UI_POS_Y(3), UI_POS_WIDTH_REMAINING(0), UI_POS_DEFAULT_HEIGHT},
""};
Text text_description_end{
{UI_POS_X(0), UI_POS_Y(4), UI_POS_WIDTH_REMAINING(0), UI_POS_DEFAULT_HEIGHT},
""};
// For manual mode
Text text_beacon_type{
{UI_POS_X(0), UI_POS_Y(1), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT},
"Type:"};
Text text_beacon_country{
{UI_POS_X(0), UI_POS_Y(2), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT},
"Country:"};
Checkbox checkbox_beacon_internal{
{UI_POS_X_RIGHT(12), UI_POS_Y(2)},
12,
"Internal",
true};
Text text_beacon_locator{
{UI_POS_X(0), UI_POS_Y(3), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT},
"Locator:"};
Text text_beacon_latitude{
{UI_POS_X(0), UI_POS_Y(4), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT},
"Lat.:"};
Text text_beacon_longitude{
{UI_POS_X(0), UI_POS_Y(5), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT},
"Long.:"};
Text text_beacon_latitude_value{
{UI_POS_X(7), UI_POS_Y(4), UI_POS_WIDTH(12), UI_POS_DEFAULT_HEIGHT},
""};
Text text_beacon_longitude_value{
{UI_POS_X(7), UI_POS_Y(5), UI_POS_WIDTH(12), UI_POS_DEFAULT_HEIGHT},
""};
OptionsField options_beacon_type{
{UI_POS_X(9), UI_POS_Y(1)},
7,
{{"EPIRB", (uint8_t)BeaconType::EPIRB},
{"ELT", (uint8_t)BeaconType::ELT},
{"PLB", (uint8_t)BeaconType::PLB}}};
OptionsField options_beacon_protocol{
{UI_POS_X(9 + 7), UI_POS_Y(1)},
30,
{{"User", (uint8_t)BeaconProtocol::USER},
{"Standard", (uint8_t)BeaconProtocol::STANDARD},
{"National", (uint8_t)BeaconProtocol::NATIONAL}}};
OptionsField options_beacon_country{
{UI_POS_X(9), UI_POS_Y(2)},
7,
{{"France", 227},
{"USA", 366},
{"Germany", 211},
{"Russia", 273},
{"Spain", 224},
{"Japan", 431},
{"UK", 232}}};
TextField text_field_beacon_locator{
{UI_POS_X(9), UI_POS_Y(3), UI_POS_WIDTH(10), UI_POS_DEFAULT_HEIGHT},
"JN03RO"};
Button button_mangps{
{UI_POS_X_RIGHT(9), UI_POS_Y(3), UI_POS_WIDTH(9), UI_POS_HEIGHT(2)},
"Set pos."};
// Mode selection (file/manual)
OptionsField options_mode{
{UI_POS_X(7), UI_POS_Y(0)},
30,
{{"File (BEACONS.TXT)", (uint8_t)BeaconMode::FILE},
{"Manual (Editor)", (uint8_t)BeaconMode::MODE_MANUAL}}};
// Frame content
Text text_frame{
{UI_POS_X(6), UI_POS_Y(6), UI_POS_WIDTH_REMAINING(6), UI_POS_DEFAULT_HEIGHT},
""};
Text text_frame_end{
{UI_POS_X(6), UI_POS_Y(7), 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},
""};
// Transmission settings
Checkbox checkbox_loop{
{UI_POS_X(0), UI_POS_Y(9)},
10,
"Resend every",
true};
NumberField field_delay{
{UI_POS_X(15), UI_POS_Y(9)},
2,
{1, 99},
1,
' '};
Checkbox checkbox_am{
{UI_POS_X(0), UI_POS_Y(11)},
10,
"AM signal",
true};
FrequencyField field_am_frequency{
{UI_POS_X(13), UI_POS_Y(12)}};
Checkbox checkbox_send_on_change{
{UI_POS_X(0), UI_POS_Y(13)},
14,
"Send on change",
true};
Button button_tx{
{UI_POS_X_RIGHT(9), UI_POS_Y(9), UI_POS_WIDTH(9), UI_POS_HEIGHT(2)},
"START"};
const Style& style_tx_start = *Theme::getInstance()->fg_green;
const Style& style_tx_stop = *Theme::getInstance()->fg_red;
OptionsField options_am_channel{
{UI_POS_X(11), UI_POS_Y(14)},
20,
{{"121.375 MHz (Test)", 0},
{"121.500 MHz /!\\Real", 1},
{"Manual", 2}}};
OptionsField options_bpsk_channel{
{UI_POS_X(11), UI_POS_Y(15)},
20,
{{"433.025 MHz (Ham)", (uint8_t)BpskChannel::HAM},
{"406.025 MHz (B)", (uint8_t)BpskChannel::B},
{"406.028 MHz (C)", (uint8_t)BpskChannel::C},
{"406.037 MHz (F)", (uint8_t)BpskChannel::F},
{"406.040 MHz (G)", (uint8_t)BpskChannel::G},
{"406.049 MHz (J)", (uint8_t)BpskChannel::J},
{"406.052 MHz (K)", (uint8_t)BpskChannel::K},
{"406.061 MHz (N)", (uint8_t)BpskChannel::N},
{"406.064 MHz (O)", (uint8_t)BpskChannel::O},
{"Manual", (uint8_t)BpskChannel::MANUAL}}};
// Transmitter view
TransmitterView tx_view{
(int16_t)UI_POS_Y_BOTTOM(4),
10000,
12};
MessageHandlerRegistration message_handler_tx_progress{
Message::ID::TXProgress,
[this](const Message* const p) {
const auto message = *reinterpret_cast<const TXProgressMessage*>(p);
this->on_tx_progress(message.progress, message.done);
}};
MessageHandlerRegistration message_handler_frame_sync{
// Use frame sync for our applcation timer callback
Message::ID::DisplayFrameSync,
[this](const Message* const) {
this->on_timer();
}};
};
} // namespace ui::external_app::epirb_tx
#endif /*__EPIRB_TX_H__*/
/*
* Copyright (C) 2026 Frederic BORRY - ADRASEC 31
*
* 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 __EPIRB_TX_H__
#define __EPIRB_TX_H__
#include "app_settings.hpp"
#include "radio_state.hpp"
#include "ui.hpp"
#include "ui_widget.hpp"
#include "ui_navigation.hpp"
#include "ui_transmitter.hpp"
#include "portapack.hpp"
#include "message.hpp"
#include "tonesets.hpp"
#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
#define BPSK_FREQUENCY_HAM 433025000
#define BPSK_FREQUENCY_B 406025000
#define BPSK_FREQUENCY_C 406028000
#define BPSK_FREQUENCY_F 406037000
#define BPSK_FREQUENCY_G 406040000
#define BPSK_FREQUENCY_J 406049000
#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 {
enum class BeaconMode {
FILE = 0,
MODE_MANUAL = 1
};
enum class BeaconType {
EPIRB = 0,
ELT = 1,
PLB = 2
};
enum class BeaconProtocol {
USER = 0,
STANDARD = 1,
NATIONAL = 2
};
enum class AmChannel {
TEST = 0,
REAL = 1,
MANUAL = 2
};
enum class BpskChannel {
HAM = 0,
B = 1,
C = 2,
F = 3,
G = 4,
J = 5,
K = 6,
N = 7,
O = 8,
MANUAL = 10,
SGB = 100
};
struct Location {
std::string locator;
bool south;
int16_t lat_deg;
int8_t lat_min;
int8_t lat_sec;
float latitude;
bool west;
int16_t long_deg;
int8_t long_min;
int8_t long_sec;
float longitude;
};
struct BeaconParams {
BeaconType type;
BeaconProtocol protocol;
uint32_t country;
bool is_test;
bool is_internal;
bool has_121_5;
Location location;
};
class EPIRBTXAppView : public View {
public:
EPIRBTXAppView(NavigationView& nav);
~EPIRBTXAppView();
void focus() override;
std::string title() const override { return "EPIRB TX"; };
private:
void start_tx();
void stop_tx();
void update_config();
void on_tx_progress(const uint32_t progress, const bool done);
void on_timer();
void load_beacons();
void set_tx_button_state(bool active);
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();
void update_am_transmission();
void update_mode();
void update_location(bool updateLocatorField = true);
struct Beacon {
std::string title{};
std::string description{};
std::string frame{};
};
NavigationView& nav_;
std::vector<Beacon> beacons{};
Beacon default_beacon{"Self test", "Serial User Location Protocol", "FFFED0D6E6202820000C29FF51041775302D"};
BeaconParams beacon_params{BeaconType::ELT, BeaconProtocol::STANDARD, 227, true, true, true, {"JN03RO", false, 0, 0, 0, 0, false, 0, 0, 0, 0}};
// Currently selected beacon index (from BEACONS.TXT file / options_frame combo)
uint32_t selected_beacon{0};
// Frequency of the transmitter before starting the app (used to restore frequency when leaving)
rf::Frequency original_frequency{0};
// Frequency of the AM emergency signal
rf::Frequency am_frequency{AM_TEST_FREQUENCY};
// Frequency of the 406 MHz BPSK signal
rf::Frequency bpsk_frequency{BPSK_FREQUENCY_HAM};
// Selected am channel
uint8_t am_channel{(uint8_t)AmChannel::TEST};
// Selected bpsk channel
uint8_t bpsk_channel{(uint8_t)BpskChannel::HAM};
// Manual AM frequency value
rf::Frequency manual_am_frequency{AM_TEST_FREQUENCY};
// Manual BPSK frequency value
rf::Frequency manual_bpsk_frequency{BPSK_FREQUENCY_HAM};
// True when using a beacon from the BEACONS.TXT file
bool mode_file{false};
// True when slideshow is enabled for file mode (change beacon after every transmission)
bool slideshow_enabled{false};
// True when looping on sending beacons is enabled
bool loop_enabled{true};
// True if AM emergency signal transmission is enabled
bool am_enabled{true};
// True if we want to send a new frame each time the user changes the current beacon
bool send_on_change{true};
// The current locator string
std::string locator{"JN03RO"};
// The delay between each frame when on loop mode
uint32_t delay{50};
uint8_t beacon_type{(uint8_t)BeaconType::EPIRB};
// Currently selected beacon protocol
uint8_t beacon_protocol{(uint8_t)BeaconProtocol::USER};
// Currently selected beacon country
uint32_t beacon_country{227};
// Current beacon's internal state (true for internal location system)
bool beacon_internal{true};
TxRadioState radio_state_{
0 /* frequency */,
1750000 /* bandwidth */,
TONES_SAMPLERATE /* sampling rate */
};
app_settings::SettingsManager settings_{
"tx_epirb",
app_settings::Mode::TX,
{
{"sbeacon"sv, &selected_beacon},
{"amfreq"sv, &am_frequency},
{"bpskfreq"sv, &bpsk_frequency},
{"amchan"sv, &am_channel},
{"bpskchan"sv, &bpsk_channel},
{"loop"sv, &loop_enabled},
{"delay"sv, &delay},
{"file"sv, &mode_file},
{"slideshow"sv, &slideshow_enabled},
{"am"sv, &am_enabled},
{"soc"sv, &send_on_change},
{"type"sv, &beacon_type},
{"proto"sv, &beacon_protocol},
{"country"sv, &beacon_country},
{"internal"sv, &beacon_internal},
{"locator"sv, &locator},
{"sgb"sv, &format_sgb},
}};
// Time of the last sent frame
uint32_t last_frame_time{0};
// True when transmission is enabled
bool transmitting{false};
// True when transmitting a BPSK frame
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{};
const size_t max_text_width = UI_POS_WIDTH_REMAINING(6) / UI_POS_DEFAULT_WIDTH;
const size_t max_text_width_ext = UI_POS_WIDTH_REMAINING(0) / UI_POS_DEFAULT_WIDTH;
Labels labels{
{{UI_POS_X(0), UI_POS_Y(0)}, "Source:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(6)}, "Frame:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(10)}, "Next frame in s.", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(12)}, "AM frequency: MHz", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(14)}, "AM chan.:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(15)}, "BPSK chan.:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(17), UI_POS_Y(9)}, "s.", Theme::getInstance()->fg_light->foreground}};
// For file mode
struct FileModeWidgets {
Text text_beacon{
{UI_POS_X(0), UI_POS_Y(1), UI_POS_WIDTH(7), UI_POS_DEFAULT_HEIGHT},
"Beacon:"};
// Beacon selection from BEACONS.TXT
OptionsField options_frame{
{UI_POS_X(7), UI_POS_Y(1)},
30,
{}};
Text text_description_label{
{UI_POS_X(0), UI_POS_Y(2), UI_POS_WIDTH(12), UI_POS_DEFAULT_HEIGHT},
"Description:"};
Text text_description{
{UI_POS_X(0), UI_POS_Y(3), UI_POS_WIDTH_REMAINING(0), UI_POS_DEFAULT_HEIGHT},
""};
Text text_description_end{
{UI_POS_X(0), UI_POS_Y(4), UI_POS_WIDTH_REMAINING(0), UI_POS_DEFAULT_HEIGHT},
""};
Checkbox checkbox_slideshow{
{UI_POS_X(0), UI_POS_Y(5)},
9,
"Slideshow",
true};
};
std::unique_ptr<FileModeWidgets> file_mode_ui{};
// For manual mode
Text text_beacon_type{
{UI_POS_X(0), UI_POS_Y(1), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT},
"Type:"};
Text text_beacon_country{
{UI_POS_X(0), UI_POS_Y(2), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT},
"Country:"};
Checkbox checkbox_beacon_internal{
{UI_POS_X_RIGHT(12), UI_POS_Y(2)},
12,
"Internal",
true};
Text text_beacon_locator{
{UI_POS_X(0), UI_POS_Y(3), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT},
"Locator:"};
Text text_beacon_latitude{
{UI_POS_X(0), UI_POS_Y(4), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT},
"Lat.:"};
Text text_beacon_longitude{
{UI_POS_X(0), UI_POS_Y(5), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT},
"Long.:"};
Text text_beacon_latitude_value{
{UI_POS_X(7), UI_POS_Y(4), UI_POS_WIDTH(12), UI_POS_DEFAULT_HEIGHT},
""};
Text text_beacon_longitude_value{
{UI_POS_X(7), UI_POS_Y(5), UI_POS_WIDTH(12), UI_POS_DEFAULT_HEIGHT},
""};
OptionsField options_beacon_type{
{UI_POS_X(9), UI_POS_Y(1)},
7,
{{"EPIRB", (uint8_t)BeaconType::EPIRB},
{"ELT", (uint8_t)BeaconType::ELT},
{"PLB", (uint8_t)BeaconType::PLB}}};
OptionsField options_beacon_protocol{
{UI_POS_X(9 + 7), UI_POS_Y(1)},
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,
{{"France", 227},
{"USA", 366},
{"Germany", 211},
{"Russia", 273},
{"Spain", 224},
{"Japan", 431},
{"UK", 232}}};
TextField text_field_beacon_locator{
{UI_POS_X(9), UI_POS_Y(3), UI_POS_WIDTH(10), UI_POS_DEFAULT_HEIGHT},
"JN03RO"};
Button button_mangps{
{UI_POS_X_RIGHT(9), UI_POS_Y(3), UI_POS_WIDTH(9), UI_POS_HEIGHT(2)},
"Set pos."};
// Mode selection (file/manual)
OptionsField options_mode{
{UI_POS_X(7), UI_POS_Y(0)},
30,
{{"File (BEACONS.TXT)", (uint8_t)BeaconMode::FILE},
{"Manual (Editor)", (uint8_t)BeaconMode::MODE_MANUAL}}};
// Frame content
Text text_frame{
{UI_POS_X(6), UI_POS_Y(6), UI_POS_WIDTH_REMAINING(6), UI_POS_DEFAULT_HEIGHT},
""};
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},
""};
// Transmission settings
Checkbox checkbox_loop{
{UI_POS_X(0), UI_POS_Y(9)},
10,
"Resend every",
true};
NumberField field_delay{
{UI_POS_X(15), UI_POS_Y(9)},
2,
{1, 99},
1,
' '};
Checkbox checkbox_am{
{UI_POS_X(0), UI_POS_Y(11)},
10,
"AM signal",
true};
FrequencyField field_am_frequency{
{UI_POS_X(13), UI_POS_Y(12)}};
Checkbox checkbox_send_on_change{
{UI_POS_X(0), UI_POS_Y(13)},
14,
"Send on change",
true};
Button button_tx{
{UI_POS_X_RIGHT(9), UI_POS_Y(9), UI_POS_WIDTH(9), UI_POS_HEIGHT(2)},
"START"};
const Style& style_tx_start = *Theme::getInstance()->fg_green;
const Style& style_tx_stop = *Theme::getInstance()->fg_red;
OptionsField options_am_channel{
{UI_POS_X(11), UI_POS_Y(14)},
20,
{{"121.375 MHz (Test)", 0},
{"121.500 MHz /!\\Real", 1},
{"Manual", 2}}};
OptionsField options_bpsk_channel{
{UI_POS_X(11), UI_POS_Y(15)},
20,
{{"433.025 MHz (Ham)", (uint8_t)BpskChannel::HAM},
{"406.025 MHz (B)", (uint8_t)BpskChannel::B},
{"406.028 MHz (C)", (uint8_t)BpskChannel::C},
{"406.037 MHz (F)", (uint8_t)BpskChannel::F},
{"406.040 MHz (G)", (uint8_t)BpskChannel::G},
{"406.049 MHz (J)", (uint8_t)BpskChannel::J},
{"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
TransmitterView tx_view{
(int16_t)UI_POS_Y_BOTTOM(4),
10000,
12};
MessageHandlerRegistration message_handler_tx_progress{
Message::ID::TXProgress,
[this](const Message* const p) {
const auto message = *reinterpret_cast<const TXProgressMessage*>(p);
this->on_tx_progress(message.progress, message.done);
}};
MessageHandlerRegistration message_handler_frame_sync{
// Use frame sync for our applcation timer callback
Message::ID::DisplayFrameSync,
[this](const Message* const) {
this->on_timer();
}};
};
} // namespace ui::external_app::epirb_tx
#endif /*__EPIRB_TX_H__*/
+45 -2
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
@@ -263,6 +272,10 @@ set(EXTCPPSRC
#epirb_rx 168 byte flash
external/epirb_rx/main.cpp
external/epirb_rx/ui_epirb_rx.cpp
external/epirb_rx/ui_beaconlist.cpp
external/epirb_rx/beacon_db.cpp
external/epirb_rx/beacon.cpp
external/epirb_rx/location.cpp
#epirb_tx
external/epirb_tx/main.cpp
@@ -323,7 +336,7 @@ set(EXTCPPSRC
external/rtty_tx/ui_rtty_tx.cpp
external/rtty_tx/baudot.cpp
#pocsag_tx
#pocsag_tx
external/pocsag_tx/main.cpp
external/pocsag_tx/ui_pocsag_tx.cpp
@@ -353,7 +366,31 @@ set(EXTCPPSRC
#two_tone_rx
external/two_tone_rx/main.cpp
external/two_tone_rx/ui_two_tone_rx.cpp
external/two_tone_rx/ui_two_tone_rx.cpp
#hard_reset
external/hard_reset/main.cpp
external/hard_reset/ui_hard_reset.cpp
#secplustx
external/secplustx/main.cpp
external/secplustx/ui_secplustx.cpp
external/secplustx/secplustx.cpp
#signal_hunter
external/signal_hunter/main.cpp
external/signal_hunter/ui_signal_hunter.cpp
#tetra rx
external/tetra_rx/main.cpp
external/tetra_rx/ui_tetra_rx.cpp
external/tetra_rx/tetra_crc.cpp
external/tetra_rx/tetra_descrambler.cpp
external/tetra_rx/tetra_interleave.cpp
external/tetra_rx/tetra_rcpc.cpp
external/tetra_rx/tetra_viterbi.cpp
)
set(EXTAPPLIST
@@ -388,6 +425,8 @@ set(EXTAPPLIST
acars_rx
wefax_rx
noaaapt_rx
vor_rx
vor_tx
shoppingcart_lock
ookbrute
ook_editor
@@ -442,6 +481,10 @@ set(EXTAPPLIST
p25_tx
two_tone_pager
two_tone_rx
hard_reset
secplustx
signal_hunter
tetra_rx
)
# sdusb has type conflicts with PRALINE (HackRF Pro) - add only for non-PRALINE builds
+45
View File
@@ -109,6 +109,12 @@ MEMORY
ram_external_app_two_tone_pager (rwx) : org = 0xAE040000, len = 32k
ram_external_app_two_tone_rx (rwx) : org = 0xAE050000, len = 32k
ram_external_app_flex_tx (rwx) : org = 0xAE060000, len = 32k
ram_external_app_hard_reset (rwx) : org = 0xAE070000, len = 32k
ram_external_app_secplustx (rwx) : org = 0xAE080000, len = 32k
ram_external_app_vor_rx (rwx) : org = 0xAE090000, len = 32k
ram_external_app_vor_tx (rwx) : org = 0xAE0A0000, len = 32k
ram_external_app_signal_hunter (rwx) : org = 0xAE0B0000, len = 32k
ram_external_app_tetra_rx (rwx) : org = 0xAE0C0000, len = 32k
}
SECTIONS
@@ -389,6 +395,18 @@ SECTIONS
*(*ui*external_app*noaaapt_rx*);
} > ram_external_app_noaaapt_rx
.external_app_vor_rx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_vor_rx.application_information));
*(*ui*external_app*vor_rx*);
} > ram_external_app_vor_rx
.external_app_vor_tx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_vor_tx.application_information));
*(*ui*external_app*vor_tx*);
} > ram_external_app_vor_tx
.external_app_breakout : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_breakout.application_information));
@@ -628,4 +646,31 @@ SECTIONS
KEEP(*(.external_app.app_flex_tx.application_information));
*(*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*);
} > ram_external_app_hard_reset
.external_app_secplustx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_secplustx.application_information));
*(*ui*external_app*secplustx*);
} > ram_external_app_secplustx
.external_app_signal_hunter : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_signal_hunter.application_information));
*(*ui*external_app*signal_hunter*);
} > ram_external_app_signal_hunter
.external_app_tetra_rx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_tetra_rx.application_information));
*(*ui*external_app*tetra_rx*);
} > ram_external_app_tetra_rx
}
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2023 Mark Thompson
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
* copyleft 2024 zxkmm
*
* This file is part of PortaPack.
*
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2023 Mark Thompson
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
* copyleft 2024 zxkmm
*
* This file is part of PortaPack.
*
+1 -1
View File
@@ -1,5 +1,5 @@
/*
* copyleft 2025 zxkmm AKA zix aka sommermorgentraum
* copyleft 2025 zxkmm
*
* This file is part of PortaPack.
*
+1 -1
View File
@@ -1,5 +1,5 @@
/*
* copyleft 2025 zxkmm AKA zix aka sommermorgentraum
* copyleft 2025 zxkmm
*
* This file is part of PortaPack.
*
+87
View File
@@ -0,0 +1,87 @@
/*
* Copyright (C) 2026 Pezsma
*
* 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_hard_reset.hpp"
#include "ui_navigation.hpp"
#include "external_app.hpp"
namespace ui::external_app::hard_reset {
void initialize_app(NavigationView& nav) {
nav.push<HardResetView>();
}
} // namespace ui::external_app::hard_reset
extern "C" {
__attribute__((section(".external_app.app_hard_reset.application_information"), used))
application_information_t _application_information_hard_reset = {
/*.memory_location = */ (uint8_t*)0x00000000,
/*.externalAppEntry = */ ui::external_app::hard_reset::initialize_app,
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
/*.app_name = */ "Hard Reset",
/*.bitmap_data = */ {
0x00,
0x00,
0x00,
0x00,
0xC0,
0x01,
0xFF,
0x7F,
0xFF,
0x7F,
0x56,
0x35,
0x56,
0x35,
0x56,
0x35,
0x56,
0x35,
0x56,
0x35,
0x56,
0x35,
0x56,
0x35,
0x56,
0x35,
0xFE,
0x3F,
0xFE,
0x3F,
0x00,
0x00,
},
/*.icon_color = */ ui::Color::red().v,
/*.menu_location = */ app_location_t::SETTINGS,
/*.desired_menu_position = */ -1,
/*.m4_app_tag = portapack::spi_flash::image_tag_none */ {0, 0, 0, 0},
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
};
} // extern "C"
@@ -0,0 +1,239 @@
/*
* Copyright (C) 2026 Pezsma
*
* 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_hard_reset.hpp"
#include "portapack.hpp"
#include "file.hpp"
#include "file_path.hpp"
#include "ui_touch_calibration.hpp"
#include "ui_external_items_menu_loader.hpp"
#include "app_settings.hpp"
using namespace portapack;
namespace pmem = portapack::persistent_memory;
namespace ui::external_app::hard_reset {
HardResetView::HardResetView(ui::NavigationView& nav)
: nav_(nav) {
add_children({&chk_settings_file,
&chk_bad_apps,
&chk_pmem,
&txt_settings_file_count,
&txt_bad_apps_count,
&btn_yes,
&btn_no,
&text,
&txt_wait});
txt_wait.set_style(Theme::getInstance()->warning_dark);
btn_yes.on_select = [this](Button&) {
{ // to free painter after draw
Painter painter;
txt_wait.hidden(false);
text.hidden(true);
painter.fill_rectangle(text.screen_rect(), this->style().background);
txt_wait.paint(painter);
}
if (chk_settings_file.value()) {
clear_settings_folder();
}
if (chk_bad_apps.value()) {
delete_bad_apps(apps_dir);
}
if (chk_pmem.value()) {
pmem::cache::defaults();
StatusRefreshMessage message{};
EventDispatcher::send_message(message);
nav_.replace<TouchCalibrationView>();
} else {
nav_.pop();
}
};
btn_no.on_select = [this](Button&) {
nav_.pop();
};
txt_settings_file_count.set_style(Theme::getInstance()->fg_magenta);
txt_bad_apps_count.set_style(Theme::getInstance()->fg_magenta);
signal_token_tick_second = rtc_time::signal_tick_second += [this]() {
this->calculate();
rtc_time::signal_tick_second -= signal_token_tick_second;
};
}
HardResetView::~HardResetView() {
rtc_time::signal_tick_second -= signal_token_tick_second;
}
void HardResetView::calculate() {
settings_file_count = count_ini_files_in_directory(settings_dir);
txt_settings_file_count.set(to_string_dec_uint(settings_file_count));
bad_apps_count = count_bad_apps(apps_dir);
txt_bad_apps_count.set(to_string_dec_uint(bad_apps_count));
if (settings_file_count > 0) {
chk_settings_file.set_value(true);
}
if (bad_apps_count > 0) {
chk_bad_apps.set_value(true);
}
chk_pmem.set_value(true);
txt_wait.hidden(true);
text.set("Warning! Checked items will beerased (bad apps, P.Mem, settings). Uncheck to keep. Touch calibration is required only if P.Mem is reset.");
}
void HardResetView::focus() {
btn_no.focus();
}
uint16_t HardResetView::count_ini_files_in_directory(const std::filesystem::path& dir_path) {
int count = 0;
for (const auto& entry : std::filesystem::directory_iterator(dir_path, u"*.ini")) {
if (std::filesystem::is_regular_file(entry.status())) {
count++;
}
}
return count;
}
void HardResetView::delete_ini_files_in_directory(const std::filesystem::path& dir_path) {
bool found_and_deleted;
do {
found_and_deleted = false;
for (const auto& entry : std::filesystem::directory_iterator(dir_path, u"*.ini")) {
if (std::filesystem::is_regular_file(entry.status())) {
auto full_path = dir_path / entry.path().filename();
delete_file(full_path);
found_and_deleted = true;
break; // iterator can become invalid if the dir is modified, so break and restart the loop after deletion
}
}
} while (found_and_deleted);
}
void HardResetView::clear_settings_folder() {
delete_ini_files_in_directory(settings_dir);
}
bool HardResetView::is_bad_app(const std::filesystem::path& file_path, bool is_ppma) {
File app;
if (app.open(file_path)) {
return true;
}
bool is_bad = false;
if (is_ppma) {
application_information_t app_info = {};
auto readResult = app.read(&app_info, sizeof(application_information_t));
if (!readResult || readResult.value() != sizeof(application_information_t)) {
is_bad = true;
} else if (app_info.header_version != CURRENT_HEADER_VERSION) {
is_bad = true;
} else if (VERSION_MD5 != app_info.app_version) {
is_bad = true;
} else {
// --- CHECKSUM VALIDATION ---
// Similar to run_external_app: read through the file and calculate the checksum
app.seek(0);
uint32_t checksum = 0;
uint8_t buffer[512]; // 512-byte buffer to conserve RAM
while (true) {
auto res = app.read(buffer, sizeof(buffer));
if (!res) break;
checksum += simple_checksum((uint32_t)buffer, res.value());
if (res.value() < sizeof(buffer)) break; // End of file
}
if (checksum != EXT_APP_EXPECTED_CHECKSUM) {
is_bad = true; // The file body is corrupted / truncated!
}
}
} else {
// PPMP validation
standalone_application_information_t app_info = {};
auto readResult = app.read(&app_info, sizeof(standalone_application_information_t));
if (!readResult || readResult.value() != sizeof(standalone_application_information_t)) {
is_bad = true;
} else if (app_info.header_version > CURRENT_STANDALONE_APPLICATION_API_VERSION) {
is_bad = true;
}
}
app.close();
return is_bad;
}
uint16_t HardResetView::count_bad_apps(const std::filesystem::path& apps_dir) {
int bad_count = 0;
// Check .ppma files
for (const auto& entry : std::filesystem::directory_iterator(apps_dir, u"*.ppma")) {
auto file_path = apps_dir / entry.path();
if (is_bad_app(file_path, true)) {
bad_count++;
}
}
// Check .ppmp files
for (const auto& entry : std::filesystem::directory_iterator(apps_dir, u"*.ppmp")) {
auto file_path = apps_dir / entry.path();
if (is_bad_app(file_path, false)) {
bad_count++;
}
}
return bad_count;
}
void HardResetView::delete_bad_apps(const std::filesystem::path& apps_dir) {
// Delete bad .ppma files
bool deleted_something;
do {
deleted_something = false;
for (const auto& entry : std::filesystem::directory_iterator(apps_dir, u"*.ppma")) {
auto file_path = apps_dir / entry.path();
if (is_bad_app(file_path, true)) {
delete_file(file_path);
deleted_something = true;
break;
}
}
} while (deleted_something);
// Delete bad .ppmp files
do {
deleted_something = false;
for (const auto& entry : std::filesystem::directory_iterator(apps_dir, u"*.ppmp")) {
auto file_path = apps_dir / entry.path();
if (is_bad_app(file_path, false)) {
delete_file(file_path);
deleted_something = true;
break;
}
}
} while (deleted_something);
}
} // namespace ui::external_app::hard_reset
@@ -0,0 +1,69 @@
/*
* Copyright (C) 2026 Pezsma
*
* 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_HARD_RESET_H__
#define __UI_HARD_RESET_H__
#include "ui_navigation.hpp"
#include "signal.hpp"
using namespace ui;
namespace ui::external_app::hard_reset {
class HardResetView : public ui::View {
public:
HardResetView(ui::NavigationView& nav);
~HardResetView();
std::string title() const override { return "Hard Reset"; }
void focus() override;
private:
ui::NavigationView& nav_;
SignalToken signal_token_tick_second{};
ui::Button btn_yes{{UI_POS_X(1), UI_POS_Y_BOTTOM(4), UI_POS_WIDTH(11), UI_POS_HEIGHT(2)}, "Erase sel."};
ui::Button btn_no{{UI_POS_X_RIGHT(11), UI_POS_Y_BOTTOM(4), UI_POS_WIDTH(10), UI_POS_HEIGHT(2)}, "No"};
ui::Text text{{UI_POS_X(0), UI_POS_Y(6), UI_POS_MAXWIDTH, UI_POS_HEIGHT(5)}, ""};
Checkbox chk_settings_file{{UI_POS_X(1), UI_POS_Y(1)}, 14, "Settings file:", true};
Checkbox chk_bad_apps{{UI_POS_X(1), UI_POS_Y(2.5)}, 9, "Bad apps:", true};
Checkbox chk_pmem{{UI_POS_X(1), UI_POS_Y(4)}, 11, "P.mem reset", true};
Text txt_settings_file_count{{UI_POS_X(19), UI_POS_Y(1), UI_POS_WIDTH(5), UI_POS_HEIGHT(1)}, "?"};
Text txt_bad_apps_count{{UI_POS_X(14), UI_POS_Y(2.5), UI_POS_WIDTH(5), UI_POS_HEIGHT(1)}, "?"};
Text txt_wait{{UI_POS_X_CENTER(14), UI_POS_Y(8), UI_POS_WIDTH(14), UI_POS_HEIGHT(1)}, "Please wait..."};
uint16_t count_ini_files_in_directory(const std::filesystem::path& dir_path);
void delete_ini_files_in_directory(const std::filesystem::path& dir_path);
void clear_settings_folder();
void calculate();
bool is_bad_app(const std::filesystem::path& file_path, bool is_ppma);
uint16_t count_bad_apps(const std::filesystem::path& apps_dir);
void delete_bad_apps(const std::filesystem::path& apps_dir);
uint16_t settings_file_count = 0;
uint16_t bad_apps_count = 0;
};
} // namespace ui::external_app::hard_reset
#endif // __UI_HARD_RESET_H__
+1 -1
View File
@@ -2,7 +2,7 @@
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2016 Furrtek
* Copyright (C) 2020 euquiq
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
* copyleft 2024 zxkmm
*
* This file is part of PortaPack.
*
+1 -1
View File
@@ -2,7 +2,7 @@
* Copyright (C) 2015 Jared Boone, ShareBrained Technology, Inc.
* Copyright (C) 2016 Furrtek
* Copyright (C) 2020 euquiq
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
* copyleft 2024 zxkmm
*
* This file is part of PortaPack.
*
@@ -8,14 +8,12 @@ using namespace portapack;
namespace ui::external_app::mcu_temperature {
void McuTemperatureWidget::paint(Painter& painter) {
const auto logger = portapack::temperature_logger;
const auto rect = screen_rect();
const Color color_background{0, 0, 64};
const Color color_foreground = Theme::getInstance()->fg_green->foreground;
const Color color_reticle{128, 128, 128};
const auto graph_width = static_cast<int>(logger.capacity()) * bar_width;
const auto graph_width = static_cast<int>(temperature_logger.capacity()) * bar_width;
const Rect graph_rect{
rect.left() + (rect.width() - graph_width) / 2, rect.top() + 8,
graph_width, rect.height()};
@@ -25,7 +23,7 @@ void McuTemperatureWidget::paint(Painter& painter) {
painter.draw_rectangle(frame_rect, color_reticle);
painter.fill_rectangle(graph_rect, color_background);
const auto history = logger.history();
const auto history = temperature_logger.history();
for (size_t i = 0; i < history.size(); i++) {
const Coord x = graph_rect.right() - (history.size() - i) * bar_width;
const auto sample = history[i];
@@ -32,7 +32,7 @@
#include "portapack.hpp"
#include "memory_map.hpp"
#include "irq_controls.hpp"
#include "temperature_logger.hpp"
#include <functional>
#include <utility>
@@ -43,14 +43,25 @@ class McuTemperatureWidget : public Widget {
explicit McuTemperatureWidget(
Rect parent_rect)
: Widget{parent_rect} {
signal_token_tick_second = rtc_time::signal_tick_second += [this]() {
this->on_tick_second();
};
}
~McuTemperatureWidget() {
rtc_time::signal_tick_second -= signal_token_tick_second;
}
void on_tick_second() {
temperature_logger.second_tick();
set_dirty();
}
void paint(Painter& painter) override;
private:
TemperatureLogger temperature_logger{};
using sample_t = uint32_t;
using temperature_t = int32_t;
SignalToken signal_token_tick_second{};
temperature_t temperature(const sample_t sensor_value) const;
Coord screen_y(const temperature_t temperature, const Rect& screen_rect) const;
@@ -25,6 +25,8 @@
#include <algorithm>
namespace ui::external_app::mcu_temperature {
void TemperatureLogger::second_tick() {
sample_phase++;
if (sample_phase >= sample_interval) {
@@ -67,3 +69,5 @@ void TemperatureLogger::push_sample(const TemperatureLogger::sample_t sample) {
samples_count++;
sample_phase = 0;
}
} // namespace ui::external_app::mcu_temperature
@@ -27,6 +27,8 @@
#include <array>
#include <vector>
namespace ui::external_app::mcu_temperature {
class TemperatureLogger {
public:
using sample_t = int8_t;
@@ -49,4 +51,6 @@ class TemperatureLogger {
void push_sample(const sample_t sample);
};
} // namespace ui::external_app::mcu_temperature
#endif /*__TEMPERATURE_LOGGER_H__*/
+1 -1
View File
@@ -1,5 +1,5 @@
/*
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
* copyleft 2024 zxkmm
*
* This file is part of PortaPack.
*
+1 -1
View File
@@ -1,5 +1,5 @@
/*
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
* copyleft 2024 zxkmm
*
* This file is part of PortaPack.
*
+1 -1
View File
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2024 Samir Sánchez Garnica @sasaga92
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
* copyleft 2024 zxkmm
*
* This file is part of PortaPack.
+1 -1
View File
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2024 Samir Sánchez Garnica @sasaga92
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
* copyleft 2024 zxkmm
*
* This file is part of PortaPack.
*
+1 -1
View File
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2024 HTotoo
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
* copyleft 2024 zxkmm
*
* This file is part of PortaPack.
*
+1 -1
View File
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2024 HTotoo
* copyleft 2024 zxkmm AKA zix aka sommermorgentraum
* copyleft 2024 zxkmm
*
* This file is part of PortaPack.
*
@@ -1,5 +1,5 @@
/*
* copyleft 2025 zxkmm AKA zix aka sommermorgentraum
* copyleft 2025 zxkmm
*
* This file is part of PortaPack.
*
@@ -1,5 +1,5 @@
/*
* copyleft 2025 zxkmm AKA zix aka sommermorgentraum
* copyleft 2025 zxkmm
*
* This file is part of PortaPack.
*
+81
View File
@@ -0,0 +1,81 @@
/*
* Copyright (C) 2026 Synray
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#include "ui.hpp"
#include "ui_navigation.hpp"
#include "ui_secplustx.hpp"
#include "external_app.hpp"
namespace ui::external_app::ui_secplustx {
void initialize_app(ui::NavigationView& nav) {
nav.push<SecplusTXView>();
}
} // namespace ui::external_app::ui_secplustx
extern "C" {
__attribute__((section(".external_app.app_secplustx.application_information"), used)) application_information_t _application_information_secplustx = {
/*.memory_location = */ (uint8_t*)0x00000000,
/*.externalAppEntry = */ ui::external_app::ui_secplustx::initialize_app,
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
/*.app_name = */ "Security+",
/*.bitmap_data = */ {
0b00100000,
0b00000000,
0b00100000,
0b00100000,
0b00100000,
0b01110000,
0b00100000,
0b00100000,
0b11100000,
0b00000111,
0b11110000,
0b00001111,
0b00110000,
0b00001100,
0b00110000,
0b00001100,
0b11110000,
0b00001111,
0b11110000,
0b00001111,
0b01110000,
0b00001101,
0b10110000,
0b00001110,
0b01110000,
0b00001101,
0b10110000,
0b00001110,
0b11110000,
0b00001111,
0b11100000,
0b00000111,
},
/*.icon_color = */ ui::Color::yellow().v,
/*.menu_location = */ app_location_t::TX,
/*.desired_menu_position = */ -1,
/*.m4_app_tag = portapack::spi_flash::image_tag_ook */ {'P', 'O', 'O', 'K'},
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
};
}
+150
View File
@@ -0,0 +1,150 @@
/*
* Copyright 2022 Clayton Smith (argilo@gmail.com)
*
* This file is part of secplus.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
*/
#include "secplustx.hpp"
namespace ui::external_app::ui_secplustx {
static void v2_calc_parity(const uint64_t fixed, uint32_t* data) {
uint32_t parity = (fixed >> 32) & 0xf;
int8_t offset;
*data &= 0xffff0fff;
for (offset = 0; offset < 32; offset += 4) {
parity ^= ((*data >> offset) & 0xf);
}
*data |= (parity << 12);
}
static void encode_v2_rolling(const uint32_t rolling,
uint32_t* rolling_halves) {
uint32_t rolling_reversed = 0;
int8_t i, half;
for (i = 0; i < 28; i++) {
rolling_reversed |= ((rolling >> i) & 1) << (28 - i - 1);
}
rolling_halves[0] = 0;
rolling_halves[1] = 0;
for (half = 0; half < 2; half++) {
for (i = 0; i < 8; i += 2) {
rolling_halves[half] |= rolling_reversed % 3 << i;
rolling_reversed /= 3;
}
}
for (half = 0; half < 2; half++) {
for (i = 10; i < 18; i += 2) {
rolling_halves[half] |= rolling_reversed % 3 << i;
rolling_reversed /= 3;
}
}
rolling_halves[0] |= (rolling_reversed % 3) << 8;
rolling_reversed /= 3;
rolling_halves[1] |= (rolling_reversed % 3) << 8;
}
static const int8_t ORDER[16] = {9, 33, 6, -1, 24, 18, 36, -1,
24, 36, 6, -1, -1, -1, -1, -1};
static const int8_t INVERT[16] = {6, 2, 1, -1, 7, 5, 3, -1,
4, 0, 5, -1, -1, -1, -1, -1};
static void v2_scramble(const uint32_t* parts, const uint8_t frame_type, uint8_t* packet_half) {
const int8_t order = ORDER[packet_half[0] >> 4];
const int8_t invert = INVERT[packet_half[0] & 0xf];
int8_t i;
uint8_t out_offset = 10;
int8_t end;
uint32_t parts_permuted[3];
end = (frame_type == 0 ? 5 : 8);
for (i = 1; i < end; i++) {
packet_half[i] = 0;
}
parts_permuted[0] =
(invert & 4) ? ~parts[(order >> 4) & 3] : parts[(order >> 4) & 3];
parts_permuted[1] =
(invert & 2) ? ~parts[(order >> 2) & 3] : parts[(order >> 2) & 3];
parts_permuted[2] = (invert & 1) ? ~parts[order & 3] : parts[order & 3];
end = (frame_type == 0 ? 8 : 0);
for (i = 18 - 1; i >= end; i--) {
packet_half[out_offset >> 3] |= ((parts_permuted[0] >> i) & 1)
<< (7 - (out_offset % 8));
out_offset++;
packet_half[out_offset >> 3] |= ((parts_permuted[1] >> i) & 1)
<< (7 - (out_offset % 8));
out_offset++;
packet_half[out_offset >> 3] |= ((parts_permuted[2] >> i) & 1)
<< (7 - (out_offset % 8));
out_offset++;
}
}
static void encode_v2_half_parts(const uint32_t rolling, const uint32_t fixed, const uint16_t data, const uint8_t frame_type, uint8_t* packet_half) {
uint32_t parts[3];
parts[0] = ((fixed >> 10) << 8) | (data >> 8);
parts[1] = ((fixed & 0x3ff) << 8) | (data & 0xff);
parts[2] = rolling;
packet_half[0] = (uint8_t)rolling;
v2_scramble(parts, frame_type, packet_half);
}
static int8_t v2_check_limits(const uint32_t rolling, const uint64_t fixed) {
if ((rolling >> 28) != 0) {
return -1;
}
if ((fixed >> 40) != 0) {
return -1;
}
return 0;
}
static void encode_v2_half(const uint32_t rolling, const uint32_t fixed, const uint16_t data, const uint8_t frame_type, uint8_t* packet_half) {
encode_v2_half_parts(rolling, fixed, data, frame_type, packet_half);
/* shift indicator two bits to the right */
packet_half[1] |= (packet_half[0] & 0x3) << 6;
packet_half[0] >>= 2;
/* set frame type */
packet_half[0] |= (frame_type << 6);
}
int8_t encode_v2(const uint32_t rolling, const uint64_t fixed, uint32_t data, const uint8_t frame_type, uint8_t* packet1, uint8_t* packet2) {
int8_t err = 0;
uint32_t rolling_halves[2];
err = v2_check_limits(rolling, fixed);
if (err < 0) {
return err;
}
encode_v2_rolling(rolling, rolling_halves);
v2_calc_parity(fixed, &data);
encode_v2_half(rolling_halves[0], fixed >> 20, data >> 16, frame_type,
packet1);
encode_v2_half(rolling_halves[1], fixed & 0xfffff, data & 0xffff, frame_type,
packet2);
return 0;
}
}; // namespace ui::external_app::ui_secplustx
+19
View File
@@ -0,0 +1,19 @@
/*
* Copyright 2022 Clayton Smith (argilo@gmail.com)
*
* This file is part of secplus.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
*/
#ifndef __SECPLUSTX__
#define __SECPLUSTX__
#include <cstdint>
namespace ui::external_app::ui_secplustx {
int8_t encode_v2(const uint32_t rolling, const uint64_t fixed, uint32_t data, const uint8_t frame_type, uint8_t* packet1, uint8_t* packet2);
};
#endif
+227
View File
@@ -0,0 +1,227 @@
#include "ui_secplustx.hpp"
#include <string_view>
#include <string>
#include <utility>
#include <vector>
#include "secplustx.hpp"
#include "ui_fileman.hpp"
#include "file_reader.hpp"
#include "baseband_api.hpp"
#include "string_format.hpp"
#include "optional.hpp"
using namespace portapack;
using namespace ui;
namespace fs = std::filesystem;
namespace ui::external_app::ui_secplustx {
static constexpr uint8_t secplus_repeat_count = 3;
static constexpr float secplus_sample_rate = OOK_SAMPLERATE / 4000.0f;
Optional<SecplusData> SecplusTXView::read_secplus_file(const fs::path& file_path) {
File file{};
if (auto error = file.open(file_path)) return {};
std::string raw = *FileLineReader{file}.begin();
std::vector chunks = split_string(raw, ';');
if (chunks.empty()) return {};
SecplusData data{};
data.version = static_cast<SecplusVersion>(std::strtoul(chunks[0].data(), NULL, 10));
switch (data.version) {
case SecplusVersion::V1:
return {}; // unimplemented
case SecplusVersion::V2: {
if (chunks.size() != 6) return {};
data.name = std::string{chunks[1]};
if (data.name.empty()) data.name = "Remote";
data.has_data = std::strtoul(chunks[2].data(), NULL, 10);
data.fixed_code = std::strtoull(chunks[3].data(), NULL, 16);
data.rolling_code = std::strtoul(chunks[4].data(), NULL, 16);
data.data = std::strtoul(chunks[5].data(), NULL, 16);
return data;
}
default:
return {};
}
}
bool SecplusTXView::write_secplus_file(const fs::path& file_path, const SecplusData& data) {
ensure_directory(secplus_dir);
File file{};
if (auto error = file.create(file_path)) {
return false;
}
switch (data.version) {
case SecplusVersion::V1:
return false; // unimplemented
case SecplusVersion::V2: {
std::string formatted = to_string_dec_uint(static_cast<uint8_t>(data.version));
formatted += ';';
formatted += data.name;
formatted += ';';
formatted += to_string_dec_uint(data.has_data);
formatted += ';';
formatted += to_string_hex(data.fixed_code);
formatted += ';';
formatted += to_string_hex(data.rolling_code);
formatted += ';';
formatted += to_string_hex(data.data);
file.write_line(formatted);
file.close();
return true;
}
default:
return false;
}
}
SecplusTXView::SecplusTXView(NavigationView& nav)
: nav_{nav} {
if (!fs::file_exists(file_path)) write_secplus_file(file_path, data);
baseband::run_image(portapack::spi_flash::image_tag_ook);
add_children({&button_open,
&button_save,
&field_name,
&labels,
&field_fixed,
&field_rolling,
&has_data,
&field_data,
&learn_mode,
&autosave,
&progressbar,
&tx_view});
button_open.on_select = [this](const Button&) {
ensure_directory(secplus_dir);
auto open_view = nav_.push<FileLoadView>(".SECPLUS");
open_view->push_dir(secplus_dir);
open_view->on_changed = [this](fs::path path) { file_path = path.string(); };
nav_.set_on_pop([this]() { reload_data(); });
};
button_save.on_select = [this](const Button&) { write_secplus_file(file_path, data); };
field_name.on_select = [this, &nav](TextField&) {
buffer = data.name;
text_prompt(nav_, buffer, field_name.parent_rect().width() / UI_POS_DEFAULT_WIDTH, ENTER_KEYBOARD_MODE_ALPHA, [this](std::string& new_name) {
data.name = new_name;
field_name.set_text(new_name);
save_data();
});
};
field_fixed.on_change = [this](SymField& field) { data.fixed_code = field.to_integer(); };
field_rolling.on_change = [this](SymField& field) { data.rolling_code = field.to_integer(); };
field_data.on_change = [this](SymField& field) { data.data = field.to_integer(); };
has_data.on_select = [this](Checkbox& checkbox, bool value) {
data.has_data = value;
// fade if inactive, otherwise use default style
const Style* new_style = value ? &style() : Theme::getInstance()->fg_medium;
checkbox.set_style(new_style);
field_data.set_style(new_style);
field_data.set_focusable(value);
};
tx_view.on_edit_frequency = [this]() {
auto new_view = nav_.push<FrequencyKeypadView>(transmitter_model.target_frequency());
new_view->on_changed = [](rf::Frequency f) {
transmitter_model.set_target_frequency(f);
};
};
tx_view.on_start = [this]() { start_tx(); };
tx_view.on_stop = [this]() {
baseband::kill_ook();
stop_tx();
};
autosave.set_value(true);
reload_data();
}
void SecplusTXView::save_data() {
if (autosave.value()) write_secplus_file(file_path, data);
}
void SecplusTXView::reload_data() {
if (auto result = read_secplus_file(file_path)) data = std::move(*result);
// always update data, use default values if read failed
field_name.set_text(data.name);
has_data.set_value(data.has_data);
field_rolling.set_value(data.rolling_code);
field_fixed.set_value(data.fixed_code);
field_data.set_value(data.data);
}
void SecplusTXView::start_tx() {
uint8_t packet1[8]{};
uint8_t packet2[8]{};
size_t bitstream_length = 0;
constexpr uint8_t packet1_indicator = 0b00;
constexpr uint8_t packet2_indicator = 0b01;
auto encode_packet = [&bitstream_length](uint8_t indicator, auto& packet, bool has_data) {
constexpr uint32_t preamble = 0b0000000000000000'1111;
constexpr uint32_t blank_size = 24;
auto manchester_encode = [&bitstream_length](auto& x, uint32_t size) {
for (uint32_t i = 0; i < size; ++i) {
bool bit = (x >> (size - 1 - i)) & 1;
encoders::bitstream_append(bitstream_length, 2, bit ? 0b01 : 0b10);
}
};
manchester_encode(preamble, 20);
manchester_encode(indicator, 2);
for (uint8_t byte = 0; byte < (has_data ? 8 : 5); ++byte) manchester_encode(packet[byte], 8);
encoders::bitstream_append(bitstream_length, blank_size, 0);
};
if (encode_v2(data.rolling_code, data.fixed_code, data.data, data.has_data, packet1, packet2) < 0) {
nav_.display_modal("Error", "Invalid rolling/fixed code");
return;
}
encode_packet(packet1_indicator, packet1, has_data.value());
encode_packet(packet2_indicator, packet2, has_data.value());
if (!learn_mode.value()) {
field_rolling.set_value(++data.rolling_code);
field_rolling.set_dirty();
}
progressbar.set_max(secplus_repeat_count);
progressbar.set_value(1);
tx_view.set_transmitting(true);
transmitter_model.enable();
baseband::set_ook_data(
bitstream_length,
secplus_sample_rate,
secplus_repeat_count,
0);
}
void SecplusTXView::stop_tx() {
transmitter_model.disable();
progressbar.set_value(0);
tx_view.set_transmitting(false);
}
void SecplusTXView::on_tx_progress(uint32_t progress, bool done) {
progressbar.set_value(progress + 1);
if (done) {
stop_tx();
save_data();
}
}
SecplusTXView::~SecplusTXView() {
transmitter_model.disable();
baseband::shutdown();
save_data();
}
} // namespace ui::external_app::ui_secplustx
@@ -0,0 +1,96 @@
#ifndef __UI_SECPLUSTX__
#define __UI_SECPLUSTX__
#include <cstdint>
#include <string>
#include "ui.hpp"
#include "file.hpp"
#include "ui_transmitter.hpp"
#include "transmitter_model.hpp"
#include "file_path.hpp"
#include "app_settings.hpp"
#include "radio_state.hpp"
#include "encoders.hpp"
#include "string_format.hpp"
namespace ui::external_app::ui_secplustx {
enum class SecplusVersion : uint8_t {
V1,
V2,
};
struct SecplusData {
SecplusVersion version;
std::string name;
bool has_data;
uint64_t fixed_code;
uint32_t rolling_code;
uint32_t data;
};
class SecplusTXView : public View {
public:
void focus() override { button_open.focus(); }
SecplusTXView(NavigationView& nav);
~SecplusTXView();
std::string title() const override {
return "Security+";
}
private:
std::string file_path = (secplus_dir / "DEFAULT.SECPLUS").string();
SecplusData data{SecplusVersion::V2, "Remote", false, 0, 0, 0};
std::string buffer{};
NavigationView& nav_;
TxRadioState radio_state_{
315000000,
1750000,
OOK_SAMPLERATE};
app_settings::SettingsManager settings_{"tx_secplus", app_settings::Mode::TX, {{"file_path"sv, &file_path}}};
Button button_open{{UI_POS_X(1), UI_POS_Y(1), screen_width / 2 - UI_POS_X(1), UI_POS_HEIGHT(2)}, "Open"};
Button button_save{{screen_width / 2, UI_POS_Y(1), screen_width / 2 - UI_POS_X(1), UI_POS_HEIGHT(2)}, "Save"};
// remote data
TextField field_name{{UI_POS_X(1), UI_POS_Y(3), UI_POS_WIDTH_REMAINING(2), UI_POS_HEIGHT(1)}, "Remote"};
Labels labels{
{{UI_POS_X(1), UI_POS_Y(4)}, "Fixed:", Theme::getInstance()->fg_medium->foreground},
{{UI_POS_X(1), UI_POS_Y(5)}, "Rolling:", Theme::getInstance()->fg_medium->foreground}};
SymField field_fixed{{UI_POS_X(10), UI_POS_Y(4)}, 10, SymField::Type::Hex, true};
SymField field_rolling{{UI_POS_X(10), UI_POS_Y(5)}, 7, SymField::Type::Hex, true};
Checkbox has_data{{UI_POS_X(1), UI_POS_Y(6)}, 5, "Data:", true};
SymField field_data{{UI_POS_X(10), UI_POS_Y(6)}, 8, SymField::Type::Hex, true};
// options
Checkbox learn_mode{{UI_POS_X(1), UI_POS_Y(7)}, 5, "Learn", true};
Checkbox autosave{{UI_POS_X(1), UI_POS_Y(8)}, 8, "Autosave", true};
ProgressBar progressbar{{UI_POS_X(2), UI_POS_Y_BOTTOM(5.75), UI_POS_WIDTH_REMAINING(4), UI_POS_HEIGHT(1)}};
TransmitterView tx_view{UI_POS_Y_BOTTOM(4), 1000000, 0};
Optional<SecplusData> read_secplus_file(const std::filesystem::path& file_path);
bool write_secplus_file(const std::filesystem::path& file_path, const SecplusData& data);
void save_data();
void reload_data();
void start_tx();
void stop_tx();
void on_tx_progress(uint32_t progress, bool done);
MessageHandlerRegistration message_handler_tx_progress{
Message::ID::TXProgress,
[this](const Message* const p) {
const auto message = *reinterpret_cast<const TXProgressMessage*>(p);
this->on_tx_progress(message.progress, message.done);
}};
};
} // namespace ui::external_app::ui_secplustx
#endif
+48
View File
@@ -0,0 +1,48 @@
/*
* Copyright (C) 2026 Matej Sochan
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#include "ui.hpp"
#include "ui_signal_hunter.hpp"
#include "ui_navigation.hpp"
#include "external_app.hpp"
namespace ui::external_app::signal_hunter {
void initialize_app(ui::NavigationView& nav) {
nav.push<SignalHunterAppView>();
}
} // namespace ui::external_app::signal_hunter
extern "C" {
__attribute__((section(".external_app.app_signal_hunter.application_information"), used)) application_information_t _application_information_signal_hunter = {
/*.memory_location = */ (uint8_t*)0x00000000,
/*.externalAppEntry = */ ui::external_app::signal_hunter::initialize_app,
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
/*.app_name = */ "SigHunter",
/*.bitmap_data = */ {0x80, 0x01, 0x80, 0x01, 0xE0, 0x07, 0xF0, 0x0F, 0x98, 0x19, 0x8C, 0x31, 0x84, 0x21, 0xFF, 0xFF, 0xFF, 0xFF, 0x84, 0x21, 0x8C, 0x31, 0x98, 0x19, 0xF0, 0x0F, 0xE0, 0x07, 0x80, 0x01, 0x80, 0x01},
/*.icon_color = */ ui::Color::green().v,
/*.menu_location = */ app_location_t::RX,
/*.desired_menu_position = */ -1,
/*.m4_app_tag = */ {'H', 'U', 'N', 'T'},
/*.m4_app_offset = */ 0x00000000,
};
}
@@ -0,0 +1,403 @@
/*
* Copyright (C) 2026 Matej Sochan
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#include "ui_signal_hunter.hpp"
#include "receiver_model.hpp"
#include "baseband_api.hpp"
#include "ui_fileman.hpp"
#include "freqman_db.hpp"
#include "string_format.hpp"
#include "io_file.hpp"
#include "rtc_time.hpp"
using namespace portapack;
namespace ui::external_app::signal_hunter {
// --- TAB 1: Main View ---
HunterMainView::HunterMainView(Rect parent_rect, SignalHunterAppView& parent)
: View(parent_rect), parent_app(parent) {
add_children({&text_current_freq, &text_status, &button_start_stop, &text_hits});
update_frequency(433920000);
button_start_stop.on_select = [this](Button&) {
parent_app.is_hunting = !parent_app.is_hunting;
if (parent_app.is_hunting) {
button_start_stop.set_text("STOP");
// Set initial frequency based on set mode (single freq / freq hopping)
if (parent_app.freq_hop_mode && !parent_app.frequency_list.empty()) {
parent_app.current_freq_index = 0;
auto freq = parent_app.frequency_list[0];
receiver_model.set_target_frequency(freq);
update_frequency(freq);
} else {
receiver_model.set_target_frequency(parent_app.single_frequency);
update_frequency(parent_app.single_frequency);
}
update_status("HUNTING...", Theme::getInstance()->fg_green);
parent_app.send_hunter_config(true);
} else {
button_start_stop.set_text("START");
update_status("IDLE", Theme::getInstance()->fg_light);
set_recording_state(false);
parent_app.send_hunter_config(false);
}
};
}
void HunterMainView::on_show() {
if (!parent_app.freq_hop_mode) {
current_freq_ = receiver_model.target_frequency();
}
update_frequency(current_freq_); // force repaint
}
void HunterMainView::focus() {
if (!parent_app.freq_hop_mode) {
current_freq_ = receiver_model.target_frequency();
}
update_frequency(current_freq_);
button_start_stop.focus();
}
void HunterMainView::set_recording_state(bool recording) {
if (recording)
text_current_freq.set_style(Theme::getInstance()->fg_red);
else
text_current_freq.set_style(Theme::getInstance()->fg_light);
text_current_freq.set_dirty(); // force ui update
update_frequency(current_freq_); // repaint with the new color
}
void HunterMainView::update_frequency(rf::Frequency freq) {
current_freq_ = freq;
text_current_freq.set(to_string_dec_uint(freq) + " Hz");
}
void HunterMainView::update_status(const std::string& status, const Style* style) {
text_status.set(status);
text_status.set_style(style);
}
void HunterMainView::update_hits(uint32_t hits) {
text_hits.set("Hits: " + to_string_dec_uint(hits));
}
// --- TAB 2: Freqs View ---
HunterFreqsView::HunterFreqsView(Rect parent_rect, SignalHunterAppView& parent)
: View(parent_rect), parent_app(parent) {
add_children({&button_load_file,
&button_clear,
&labels,
&field_dwell,
&text_loaded_info});
field_dwell.set_value(parent_app.hop_dwell_ms);
field_dwell.on_change = [this](int32_t v) { parent_app.hop_dwell_ms = v; };
button_load_file.on_select = [this](Button&) {
auto* view = parent_app.get_nav().push<FileLoadView>(".TXT");
if (view) {
view->on_changed = [this](std::filesystem::path path) {
parent_app.frequency_list.clear();
parent_app.current_freq_index = 0;
FreqmanDB db{};
db.open(path);
for (const auto& entry : db) {
if (entry.frequency_a > 0)
parent_app.frequency_list.push_back(entry.frequency_a);
}
parent_app.freqman_file = path.stem().string();
// Switch to HOP mode if a file was loaded
if (!parent_app.frequency_list.empty())
parent_app.freq_hop_mode = true;
update_list_count();
// Update the mode display in the Config tab
if (parent_app.get_config_view()) {
parent_app.get_config_view()->update_mode_display();
}
};
}
};
button_clear.on_select = [this](Button&) {
parent_app.frequency_list.clear();
parent_app.current_freq_index = 0;
parent_app.freq_hop_mode = false;
update_list_count();
// Update the mode display in the Config tab
if (parent_app.get_config_view()) {
parent_app.get_config_view()->update_mode_display();
}
};
}
void HunterFreqsView::update_list_count() {
text_loaded_info.set("Loaded: " +
to_string_dec_uint(parent_app.frequency_list.size()) + " freqs");
}
void HunterFreqsView::focus() {
button_load_file.focus();
}
// --- TAB 3: Config View ---
HunterConfigView::HunterConfigView(Rect parent_rect, SignalHunterAppView& parent)
: View(parent_rect), parent_app(parent), field_single_freq{{UI_POS_X(2), UI_POS_Y(4)}, parent.get_nav()} {
add_children({&button_mode,
&field_single_freq,
&labels,
&field_threshold,
&field_hang_time,
&text_info_config});
button_mode.on_select = [this](Button&) {
parent_app.freq_hop_mode = !parent_app.freq_hop_mode;
update_mode_display();
};
field_single_freq.set_value(parent_app.single_frequency);
field_threshold.set_value(parent_app.energy_threshold);
field_threshold.on_change = [this](int32_t v) { parent_app.energy_threshold = v; };
field_hang_time.set_value(parent_app.hangtime_ms);
field_hang_time.on_change = [this](int32_t v) { parent_app.hangtime_ms = v; };
update_mode_display();
}
void HunterConfigView::on_show() {
auto current = receiver_model.target_frequency();
parent_app.single_frequency = current;
field_single_freq.set_value(current);
}
void HunterConfigView::update_mode_display() {
if (parent_app.freq_hop_mode)
button_mode.set_text("MODE: HOP (" +
to_string_dec_uint(parent_app.frequency_list.size()) + " freqs)");
else
button_mode.set_text("MODE: SINGLE");
}
void HunterConfigView::focus() {
field_threshold.focus();
}
// --- Main App View ---
SignalHunterAppView::SignalHunterAppView(ui::NavigationView& nav)
: frequency_list{},
nav_(nav) {
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
receiver_model.set_target_frequency(433920000);
receiver_model.set_baseband_bandwidth(1750000);
receiver_model.set_sampling_rate(2000000);
receiver_model.enable();
view_main = std::make_unique<HunterMainView>(content_rect, *this);
view_freqs = std::make_unique<HunterFreqsView>(content_rect, *this);
view_config = std::make_unique<HunterConfigView>(content_rect, *this);
tab_view = std::make_unique<TabView>(std::initializer_list<TabView::TabDef>{
{"Target", Color::cyan(), view_main.get()},
{"Freqs", Color::green(), view_freqs.get()},
{"Config", Color::yellow(), view_config.get()}});
tab_view->set_parent_rect(view_rect);
add_children({&field_lna,
&field_vga,
&field_rf_amp,
&rssi,
tab_view.get(),
view_main.get(),
view_freqs.get(),
view_config.get()});
view_freqs->hidden(true);
view_config->hidden(true);
using namespace app_settings;
settings_ = std::make_unique<SettingsManager>(
"ext_signal_hunter", Mode::RX,
SettingBindings{
{"threshold"sv, &energy_threshold},
{"hangtime_ms"sv, &hangtime_ms},
{"hop_dwell_ms"sv, &hop_dwell_ms},
{"single_freq"sv, &single_frequency},
{"file"sv, &freqman_file}});
}
SignalHunterAppView::~SignalHunterAppView() {
if (!freq_hop_mode) {
single_frequency = receiver_model.target_frequency();
}
// Safely shutdown recording
stop_recording();
receiver_model.disable();
baseband::shutdown();
}
void SignalHunterAppView::focus() {
if (tab_view)
tab_view->focus();
}
// TIMER LOGIC: Based on display refresh rate
void SignalHunterAppView::on_frame_sync() {
// Timer running only if, HUNTING, in HOPPING mode, not recording + we have the freqs list
if (is_hunting && freq_hop_mode && !capture_thread && !frequency_list.empty()) {
hop_timer_ms += 17; // 1 frame in a 60 Hz display ~ 16.6 ms
// if Dwell Time has passed, change frequency
if (hop_timer_ms >= hop_dwell_ms) {
hop_timer_ms = 0;
current_freq_index = (current_freq_index + 1) % frequency_list.size();
auto next_freq = frequency_list[current_freq_index];
receiver_model.set_target_frequency(next_freq);
if (view_main) {
view_main->update_frequency(next_freq);
}
}
} else {
hop_timer_ms = 0; // if not hunting / recording -> timer = 0
}
}
// Recording control logic via M0 <-> M4 IPC
void SignalHunterAppView::send_hunter_config(bool start) {
// Send config to baseband processor via IPC mechanism
baseband::set_hunter_config(energy_threshold, hangtime_ms, start);
if (!start) {
stop_recording();
}
}
void SignalHunterAppView::on_hunter_trigger(const HunterTriggerMessage* /*message*/) {
// M4 has detected signal above threshold - start I/Q capture
if (!capture_thread && is_hunting) {
trigger_hits++;
view_main->update_hits(trigger_hits);
view_main->update_status("RECORDING!", Theme::getInstance()->fg_red);
view_main->set_recording_state(true);
start_recording();
}
}
void SignalHunterAppView::on_hunter_stop(const HunterStopMessage*) {
if (capture_thread) {
stop_recording();
view_main->set_recording_state(false);
if (is_hunting) {
if (freq_hop_mode && !frequency_list.empty()) {
// Next frequency
current_freq_index = (current_freq_index + 1) % frequency_list.size();
auto next_freq = frequency_list[current_freq_index];
receiver_model.set_target_frequency(next_freq);
view_main->update_frequency(next_freq);
// Reset the timer
hop_timer_ms = 0;
}
view_main->update_status("HUNTING...", Theme::getInstance()->fg_green);
}
}
}
void SignalHunterAppView::start_recording() {
rtc::RTC datetime{};
rtc_time::now(datetime);
// Generate timestamped filename for capture session
const std::string capture_filename = "HNT_" +
to_string_dec_uint(datetime.year(), 4, '0') +
to_string_dec_uint(datetime.month(), 2, '0') +
to_string_dec_uint(datetime.day(), 2, '0') + "T" +
to_string_dec_uint(datetime.hour(), 2, '0') +
to_string_dec_uint(datetime.minute(), 2, '0') +
to_string_dec_uint(datetime.second(), 2, '0');
// Use FileWriter for direct raw I/Q dump to avoid M0 CPU bottlenecks.
// FileConvertWriter is heavier
auto writer = std::make_unique<FileWriter>();
auto create_error = writer->create(std::filesystem::path(u"/CAPTURES") / (capture_filename + ".C16"));
if (create_error.is_valid()) {
if (view_main) view_main->update_status("SD ERROR", Theme::getInstance()->fg_red);
send_hunter_config(false);
is_hunting = false;
if (view_main) view_main->set_start_button_text("START");
return;
}
current_capture_filename = capture_filename;
capture_thread = std::make_unique<CaptureThread>(
std::move(writer),
4096,
4,
[]() {},
[](File::Error) {});
if (view_main) view_main->update_status("RECORDING!", Theme::getInstance()->fg_red);
}
void SignalHunterAppView::stop_recording() {
// Stop I/Q capture and release SD card for metadata write
capture_thread.reset();
// Generate metadata file after capture completes
if (!current_capture_filename.empty()) {
File txt_file;
auto txt_error = txt_file.create(std::filesystem::path(u"/CAPTURES") / (current_capture_filename + ".TXT"));
// is_valid() returns true only if error is present (counter-intuitive)
if (!txt_error.is_valid()) {
std::string metadata =
"center_frequency=" + to_string_dec_uint(receiver_model.target_frequency()) + "\n" +
// Divide main sample rate by decimation factor (8) to match C16 sample rate
"sample_rate=" + to_string_dec_uint(receiver_model.sampling_rate() / 8) + "\n";
txt_file.write(metadata.c_str(), metadata.length());
}
// Clear filename for next capture cycle
current_capture_filename.clear();
}
}
} // namespace ui::external_app::signal_hunter
@@ -0,0 +1,192 @@
/*
* Copyright (C) 2026 Matej Sochan
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#ifndef __UI_SIGNAL_HUNTER_H__
#define __UI_SIGNAL_HUNTER_H__
#include "app_settings.hpp"
#include "rf_path.hpp"
#include "ui_widget.hpp"
#include "ui_navigation.hpp"
#include "ui_receiver.hpp"
#include "ui_tabview.hpp"
#include "ui_freq_field.hpp"
#include "capture_thread.hpp"
#include "message.hpp"
#include <vector>
#include <memory>
namespace ui::external_app::signal_hunter {
class SignalHunterAppView;
// --- TAB 1: Main View ---
class HunterMainView : public View {
public:
HunterMainView(Rect parent_rect, SignalHunterAppView& parent);
void on_show() override;
void focus() override;
void update_status(const std::string& status, const Style* style);
void update_hits(uint32_t hits);
void update_frequency(rf::Frequency freq);
void set_recording_state(bool recording);
void set_start_button_text(const std::string& text) { button_start_stop.set_text(text); }
private:
SignalHunterAppView& parent_app;
rf::Frequency current_freq_{433920000};
Text text_current_freq{{UI_POS_X_CENTER(14), UI_POS_Y(3), 112, 16}, ""};
Text text_status{{UI_POS_X_CENTER(12), UI_POS_Y(6), 96, 16}, "IDLE"};
Button button_start_stop{{UI_POS_X_CENTER(10), UI_POS_Y(8), 80, 32}, "START"};
Text text_hits{{UI_POS_X_CENTER(10), UI_POS_Y(11), 80, 16}, "Hits: 0"};
};
// --- TAB 2: Freqs View ---
class HunterFreqsView : public View {
public:
HunterFreqsView(Rect parent_rect, SignalHunterAppView& parent);
void focus() override;
void update_list_count();
private:
SignalHunterAppView& parent_app;
Button button_load_file{{UI_POS_X(2), UI_POS_Y(1), 80, 28}, "LOAD FILE"};
Button button_clear{{UI_POS_X_RIGHT(12), UI_POS_Y(1), 80, 28}, "CLEAR"};
Labels labels{
{{UI_POS_X(2), UI_POS_Y(4)}, "Dwell Time (ms):", Color::light_grey()}};
NumberField field_dwell{{UI_POS_X_RIGHT(8), UI_POS_Y(4)}, 4, {10, 9999}, 10, ' '};
Text text_loaded_info{{UI_POS_X(2), UI_POS_Y(6), 208, 16}, "Loaded: 0 freqs"};
};
// --- TAB 3: Config View ---
class HunterConfigView : public View {
public:
HunterConfigView(Rect parent_rect, SignalHunterAppView& parent);
void update_mode_display();
void focus() override;
void on_show() override;
private:
SignalHunterAppView& parent_app;
Button button_mode{
{UI_POS_X(2), UI_POS_Y(2), 208, 28},
"MODE: SINGLE"};
RxFrequencyField field_single_freq; // UI_POS_Y(4)
Labels labels{
{{UI_POS_X(2), UI_POS_Y(6)}, "Energy Threshold:", Color::light_grey()},
{{UI_POS_X(2), UI_POS_Y(8)}, "Hang-Time (ms):", Color::light_grey()}};
NumberField field_threshold{{UI_POS_X_RIGHT(8), UI_POS_Y(6)}, 5, {100, 99999}, 100, ' '};
NumberField field_hang_time{{UI_POS_X_RIGHT(8), UI_POS_Y(8)}, 4, {10, 5000}, 10, ' '};
Text text_info_config{
{UI_POS_X(2), UI_POS_Y(10), 208, 16},
"Restart HUNT after change"};
};
// --- MAIN APP VIEW ---
class SignalHunterAppView final : public ui::View {
public:
SignalHunterAppView(ui::NavigationView& nav);
~SignalHunterAppView();
void focus() override;
std::string title() const override { return "SignalHunter"; }
ui::NavigationView& get_nav() { return nav_; }
// Application state shared across all views
std::vector<rf::Frequency> frequency_list;
uint32_t current_freq_index{0};
uint32_t energy_threshold{5000};
uint32_t hangtime_ms{500};
uint32_t hop_dwell_ms{200};
std::string freqman_file{"TARGETS"};
bool is_hunting{false};
uint32_t trigger_hits{0};
rf::Frequency single_frequency{433920000};
bool freq_hop_mode{false};
void send_hunter_config(bool start);
HunterMainView* get_main_view() { return view_main.get(); }
HunterConfigView* get_config_view() { return view_config.get(); }
private:
ui::NavigationView& nav_;
std::unique_ptr<app_settings::SettingsManager> settings_{};
std::unique_ptr<HunterMainView> view_main{};
std::unique_ptr<HunterFreqsView> view_freqs{};
std::unique_ptr<HunterConfigView> view_config{};
std::unique_ptr<TabView> tab_view{};
std::string current_capture_filename{};
std::unique_ptr<CaptureThread> capture_thread{};
ui::LNAGainField field_lna{{UI_POS_X(0), UI_POS_Y(0)}};
ui::VGAGainField field_vga{{UI_POS_X(7), UI_POS_Y(0)}};
ui::RFAmpField field_rf_amp{{UI_POS_X(14), UI_POS_Y(0)}};
ui::RSSI rssi{{UI_POS_X(0), UI_POS_Y(1), UI_POS_MAXWIDTH, 4}};
static constexpr Dim tab_bar_h = 20;
Rect view_rect{
0, UI_POS_Y(2) + 4, UI_POS_MAXWIDTH,
screen_height - (UI_POS_Y(2) + 4) - UI_POS_HEIGHT(1)};
Rect content_rect{
0, UI_POS_Y(2) + 4 + tab_bar_h, UI_POS_MAXWIDTH,
screen_height - (UI_POS_Y(2) + 4 + tab_bar_h) - UI_POS_HEIGHT(1)};
void on_hunter_trigger(const HunterTriggerMessage* message);
void on_hunter_stop(const HunterStopMessage* message);
void start_recording();
void stop_recording();
// timer variables
uint32_t hop_timer_ms{0};
void on_frame_sync();
MessageHandlerRegistration message_handler_frame_sync{
Message::ID::DisplayFrameSync,
[this](const Message* const) {
this->on_frame_sync();
}};
MessageHandlerRegistration message_handler_trigger{
Message::ID::HunterTrigger,
[this](const Message* const p) {
this->on_hunter_trigger(static_cast<const HunterTriggerMessage*>(p));
}};
MessageHandlerRegistration message_handler_stop{
Message::ID::HunterStop,
[this](const Message* const p) {
this->on_hunter_stop(static_cast<const HunterStopMessage*>(p));
}};
};
} // namespace ui::external_app::signal_hunter
#endif /*__UI_SIGNAL_HUNTER_H__*/
+1 -1
View File
@@ -1,6 +1,6 @@
/*
* Copyright 2025 Mark Thompson
* copyleft 2025 zxkmm AKA zix aka sommermorgentraum
* copyleft 2025 zxkmm
*
* This file is part of PortaPack.
*
+1 -1
View File
@@ -1,6 +1,6 @@
/*
* Copyright 2025 Mark Thompson
* copyleft 2025 zxkmm AKA zix aka sommermorgentraum
* copyleft 2025 zxkmm
*
* This file is part of PortaPack.
*
+81
View File
@@ -0,0 +1,81 @@
/*
* Copyright (C) 2026 HTotoo
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#include "ui_tetra_rx.hpp"
#include "ui_navigation.hpp"
#include "external_app.hpp"
namespace ui::external_app::tetra_rx {
void initialize_app(ui::NavigationView& nav) {
nav.push<TetraRxView>();
}
} // namespace ui::external_app::tetra_rx
extern "C" {
__attribute__((section(".external_app.app_tetra_rx.application_information"), used)) application_information_t _application_information_tetra_rx = {
/*.memory_location = */ (uint8_t*)0x00000000,
/*.externalAppEntry = */ ui::external_app::tetra_rx::initialize_app,
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
/*.app_name = */ "Tetra",
/*.bitmap_data = */ {
0xE0,
0x0F,
0x18,
0x38,
0xE4,
0x67,
0x7E,
0xCE,
0xC7,
0xCC,
0x00,
0x00,
0xFF,
0x4F,
0xBA,
0xB2,
0x9A,
0xEE,
0xBA,
0xB2,
0x00,
0x00,
0x3B,
0xE3,
0x73,
0x7E,
0xC6,
0x27,
0x1C,
0x18,
0xF0,
0x07,
},
/*.icon_color = */ ui::Color::orange().v,
/*.menu_location = */ app_location_t::RX,
/*.desired_menu_position = */ -1,
/*.m4_app_tag = portapack::spi_flash::image_tag_tetrarx*/ {'P', 'T', 'E', 'T'},
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
};
}
+45
View File
@@ -0,0 +1,45 @@
#pragma once
#include <stdint.h>
#include <stddef.h>
namespace ui::external_app::tetra_rx {
class BitVector {
public:
BitVector(uint8_t* p, size_t bits)
: data_(p),
bits_(bits) {
}
inline bool get(size_t bit) const {
return (data_[bit >> 3] >>
(7 - (bit & 7))) &
1;
}
inline void set(size_t bit, bool value) {
if (value)
data_[bit >> 3] |=
1 << (7 - (bit & 7));
else
data_[bit >> 3] &=
~(1 << (7 - (bit & 7)));
}
inline void xor_bit(size_t bit, bool value) {
if (value)
data_[bit >> 3] ^=
1 << (7 - (bit & 7));
}
inline size_t size() const {
return bits_;
}
private:
uint8_t* data_;
size_t bits_;
};
} // namespace ui::external_app::tetra_rx
+33
View File
@@ -0,0 +1,33 @@
#include "tetra_crc.hpp"
namespace ui::external_app::tetra_rx {
uint16_t CRC::crc16_itut_bits(
const uint8_t* bits,
uint32_t n_bits,
uint16_t init) {
uint16_t crc = init;
for (uint32_t i = 0; i < n_bits; i++) {
uint8_t bit =
(bits[i >> 3] >>
(7 - (i & 7))) &
1;
crc ^= uint16_t(bit) << 15;
if (crc & 0x8000) {
crc =
(crc << 1) ^
0x1021;
} else {
crc <<= 1;
}
crc &= 0xffff;
}
return crc;
}
} // namespace ui::external_app::tetra_rx
+17
View File
@@ -0,0 +1,17 @@
#pragma once
#include <stdint.h>
namespace ui::external_app::tetra_rx {
class CRC {
public:
static constexpr uint16_t CRC_OK = 0x1d0f;
static uint16_t crc16_itut_bits(
const uint8_t* bits,
uint32_t n_bits,
uint16_t init = 0xffff);
};
} // namespace ui::external_app::tetra_rx
@@ -0,0 +1,60 @@
#include "tetra_descrambler.hpp"
namespace ui::external_app::tetra_rx {
static inline uint8_t lfsr_step(uint32_t& s) {
uint8_t b = 0;
b ^= (s >> (32 - 32)) & 1;
b ^= (s >> (32 - 26)) & 1;
b ^= (s >> (32 - 23)) & 1;
b ^= (s >> (32 - 22)) & 1;
b ^= (s >> (32 - 16)) & 1;
b ^= (s >> (32 - 12)) & 1;
b ^= (s >> (32 - 11)) & 1;
b ^= (s >> (32 - 10)) & 1;
b ^= (s >> (32 - 8)) & 1;
b ^= (s >> (32 - 7)) & 1;
b ^= (s >> (32 - 5)) & 1;
b ^= (s >> (32 - 4)) & 1;
b ^= (s >> (32 - 2)) & 1;
b ^= (s >> (32 - 1)) & 1;
s = (s >> 1) | (uint32_t(b) << 31);
return b;
}
void Descrambler::generate(
uint32_t init,
uint8_t* seq,
size_t bits) {
uint32_t s = init;
for (size_t i = 0; i < bits; i++)
seq[i] = lfsr_step(s);
}
void Descrambler::descramble(
BitVector& bits,
uint32_t init) {
uint32_t s = init;
for (size_t i = 0; i < bits.size(); i++)
bits.xor_bit(i, lfsr_step(s));
}
uint32_t Descrambler::dmo_scramb_init(
uint32_t mni,
uint32_t src) {
uint32_t init =
(src & 0xFFFFFF) |
((mni & 0x3F) << 24);
init <<= 2;
init |= 3;
return init;
}
} // namespace ui::external_app::tetra_rx
@@ -0,0 +1,26 @@
#pragma once
#include <stdint.h>
#include "tetra_bits.hpp"
namespace ui::external_app::tetra_rx {
class Descrambler {
public:
static constexpr uint32_t SCRAMB_INIT_BSCH = 0x00000003;
static void descramble(
BitVector& bits,
uint32_t init = SCRAMB_INIT_BSCH);
static void generate(
uint32_t init,
uint8_t* sequence,
size_t bits);
static uint32_t dmo_scramb_init(
uint32_t mni,
uint32_t src);
};
} // namespace ui::external_app::tetra_rx
@@ -0,0 +1,21 @@
#include "tetra_interleave.hpp"
namespace ui::external_app::tetra_rx {
void Interleave::block_deinterleave(
BitVector& in,
BitVector& out,
uint32_t K,
uint32_t a) {
for (uint32_t i = 1; i <= K; i++) {
uint32_t k =
1 +
((a * i) % K);
out.set(
i - 1,
in.get(k - 1));
}
}
} // namespace ui::external_app::tetra_rx
@@ -0,0 +1,16 @@
#pragma once
#include "tetra_bits.hpp"
namespace ui::external_app::tetra_rx {
class Interleave {
public:
static void block_deinterleave(
BitVector& in,
BitVector& out,
uint32_t K,
uint32_t a);
};
} // namespace ui::external_app::tetra_rx
+24
View File
@@ -0,0 +1,24 @@
#include "tetra_rcpc.hpp"
#include <string.h>
namespace ui::external_app::tetra_rx {
static constexpr uint8_t P_RATE_2_3[3] = {1, 2, 5};
static constexpr uint32_t T_RATE_2_3 = 3;
static constexpr uint32_t PERIOD = 8;
void RCPC::depuncture_2_3(BitVector& in, uint8_t* out, uint32_t in_bits) {
const uint32_t mother_bits = ((in_bits + T_RATE_2_3 - 1) / T_RATE_2_3) * PERIOD + PERIOD;
memset(out, ERASED, mother_bits);
for (uint32_t j = 1; j <= in_bits; j++) {
uint32_t i = j;
uint32_t k = PERIOD * ((i - 1) / T_RATE_2_3);
k += P_RATE_2_3[(i - 1) % T_RATE_2_3];
if (k >= 1 && k <= mother_bits) {
out[k - 1] = in.get(j - 1);
}
}
}
} // namespace ui::external_app::tetra_rx

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