Compare commits

..

26 Commits

Author SHA1 Message Date
jLynx d1a2318fe4 Merge pull request #3106 from portapack-mayhem/next
v2.4.0
2026-03-24 22:54:24 +13:00
jLynx 5c719645d6 Update version.txt 2026-03-24 22:31:30 +13:00
jLynx b0eb776bdd Update past_version.txt 2026-03-24 22:31:18 +13:00
jLynx ce87161f9b Revert "Switched build to use ninja by default (#3072)" (#3102)
This reverts commit 22932e636d.
2026-03-20 12:16:14 +13:00
Totoo 8883f2decc fix sd test app mem usage (#3101) 2026-03-17 15:42:10 +01:00
Totoo e06d23ea3e app color 2026-03-17 15:41:31 +01:00
Totoo 0da90c7c5e modified app colors based on wiki (#3099) 2026-03-17 15:00:25 +01:00
Totoo e7572e5d41 transparent bg on map marker text (#3098) 2026-03-17 21:46:05 +08:00
Totoo 50f1e51d7e pocsag fix (#3097) 2026-03-17 21:35:37 +08:00
Sarah Rose 55a15958ec Add P25 Phase 1 TSCC TX external app (p25_tx) (#3090)
* Add P25 TX external app (UI shell)

- Register p25_tx in external.cmake and external.ld at 0xAE010000
- UI with NAC, SYSID, WACN, RFSSID, SITEID, TG, VCH fields
- 2.4 Msps TX, 155 MHz default, no baseband (m4_app_tag={0,0,0,0})

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

* Add P25 TX baseband processor (M4)

- P25TxConfigure = 102 in message.hpp + P25TxConfigureMessage
- image_tag_p25_tx in spi_image.hpp
- set_p25tx_data() in baseband_api (copies dibits to bb_data, sends msg)
- proc_p25_tx registered in baseband CMakeLists (tag P25T)
- m4_app_tag updated to {'P','2','5','T'}
- TX wired: transmits P25 FSW test pattern (512 dibits) via M4 C4FM proc
- on_tx_progress handler stops TX when frame complete

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

* p25_tx: implement correct P25 TSBK encoding from ccemu

- BCH(63,16) via generator matrix (not polynomial division)
- CRC-CCITT bit-by-bit init=0 over 80 bits
- 1/2-rate trellis encoder (4 states)
- DataInterleave per TIA-102
- Status symbol insertion (SS=0x02 every 35 dibits)
- TSBK rotation: IDEN_UP -> NET_STS -> RFSS_STS -> NET_STS
- tsbk_idx_ persists across buffer refills, resets on stop_tx

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

* p25_tx: apply clang-format

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

* p25_tx: fix HackRF lockup - zero output buffer when idle

When configured=false, execute() returned without writing to the
output buffer. The DMA would re-use stale IQ samples from the
previous cycle, sending garbage to the HackRF and causing it to
lock up mid-transmission.

Fix: explicitly zero the output buffer when the proc is idle.

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

* p25_tx: address PR review feedback and fix TX restart lockup

- Replace NumberField with SymField::Type::Hex for NAC, SYSID, WACN,
  RFSSID, SITEID, TG, VCH (hex values should display as hex)
- Add voice grant TSBK (OpcodeGrpVChGrant 0x00) to rotation cycle;
  rotation is now 5-step: IDEN_UP->NET_STS->RFSS_STS->GRP_V_GRANT->NET_STS
- Wire TG and VCH fields into fill_tx_buffer for voice grant encoding
- Reset txprogress_message.done=false on new P25TxConfigure in baseband
- Add #include <cstring> to baseband_api.cpp (needed for memcpy)
- Fix TX restart lockup: remove baseband::shutdown() from stop_tx() so
  the M4 stays loaded between sessions; only shut down in destructor.
  Previously, the stale ShutdownMessage in the queue would be picked up
  by the restarted M4 on second TX, causing it to exit immediately and
  leaving the HackRF stuck in TX mode.

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

* ifndef guard instead of pragma
2026-03-17 08:41:01 +01:00
stafur 395e20d17b Next praline clean up (add SW: RSSI for hackrf pro praline) (#3091)
* Cleaned up #ifndef PRALINE and updated logic to being with #ifdef PRALINE entries where possible to make logic flow for PRALINE code execution pipeline clearer. Cleaned up compiletime warnings for PRALINE related codebase updates.

* Addressed comments provided by copilot during PR review. Combed through frequency definitions for consistency between PLL A and PLL B register definitions for CLKs 0-7. Ensured CLK3/LK6 <- SMA PORTs and CLK7 <- not utiliized are disabled during core development phase to support root cause analysis of any spectral artifacts. Updated MCU frequency to 40MHz to ensure audio harmonics are outside FM radio band range (< 80 MHz, >120MHz) and added comments clarifying choice of 40 over 10 MHz for potential future root cause analysis in other bands where audio may be expected as needed. Added CLK6 and CLK7 to Clocks Status View Debug display. Moved CLK defintions and PLL instantiations for components that are most RF sensitive to PLL A. Left others in PLL B. That is move FPGA CLK1 to PLL B, while moving CLK2, CLK4, and CLK5 to PLL A.

* Cleaned up PLL A and B XTAL reference checks relative to 800 MHz.

* Encapsulated HackRF Pro Praline debug and status vies into a single Pro Debug submenu as part of clean up.

* Fixed BLE RX Out of Memory error. Updated LPC43xx ld scripts to accouint for additional HackRF Pro praline memory.

* Addressed copilot comments for ble_rx_app by adding recent_entries_view.set_dirty. Updated ble_rx_app for easier use with heap limit set to one less than recent entries max limit.

* Addressed copilot comments by updating comment clarity in source files. Updated ui_debug to allow for return reference if set for PRO debug menu item.

* Improved readability of intialization parameters for the FPGA registers, and addressed 20Mhz nulls by initializing DC Notch width with standard setting, and DC Adaptiation rate with a balanced setting.

* Ran format-code.sh

* Added option to allow for user to set number if entries in recent list. Default is set to a relatively stable 32.

* Removed #ifdef PRALINE pragmas from ble_rx_app such that HackRF One can also use the updated UI widget to allow for user to set number of entries in recent list.

* Ran format-code.sh

* Improved RSSI for praline. Values are now being counted correctly by statistics methods after selecting ADC = 0 0 for RSSI. This was different than for the hackrf one which uses ADC = 1.

* Implemented SW RSSI calculation since HW RSSI not currently available in hackrf pro (praline) via fpga pass through. Calibrated RSSI power sensistivity to mid level signal intensitities.

* In preparation for WIP allocated full 128k to M4 limiting heap for M0 to 0k, and leabing it only with 64k from bank 0. That is, bank1 and bank2 are fully allocated to M4. Cleaned up #ifdef PRALINE conditionals such taht max2837/39 are in #else conditionals, as well as continuing to clean up hackrf_r9 booleans by placing them within #else conditionals.

* fix typo

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

* Addressed co-pilot comments. Clarified use of 25 recent samples in ble app for stability, udpated comments to reflect use of avg power over pwak pwer detection, improved safety of sample packing while relying on intrinstics for execute the implementation in a single machine instruction (PKHBT) for computational and memory efficiency.
2026-03-17 08:13:09 +01:00
zxkmm 9bf1f2175e fix bad color (#3096) 2026-03-17 08:10:07 +01:00
zxkmm ec57f99a2c Contribution template (#3088) 2026-03-16 19:41:43 +01:00
Sarah Rose 5c2d1d3dfb Add MDC-1200 TX external app (#3083)
Implements MDC-1200 single and double-packet TX as a PPMA external app.

Features:
- Supports PTT ID, Emergency, Radio Check, Stun/Unstun, Call Alert,
  Selective Call (1 & 2), Status Request/Response, Remote Monitor, Message
- Auto-fills Arg byte on Op selection
- AFSK FSK modulation: 1800 Hz mark / 1200 Hz space, 1200 baud
- NRZI encoding with Golay FEC per MDC-1200 spec
- Tap-to-edit frequency via keypad
- Default TX gain of 30 dB
- Uses AFSK baseband (same as POCSAG/SAME TX)
- All buffers kept off the stack to avoid hard faults in ext app environment

* mdc_tx: address PR review feedback

- Move MDC helpers into ui::external_app::mdc_tx namespace (drop anonymous namespace)
- Use AFSK_TX_SAMPLERATE macro instead of hardcoded 1536000
- Add #include "modems.hpp" explicitly for AFSK_TX_SAMPLERATE
- Replace n*8/n*16 pixel positions with UI_POS_X/UI_POS_Y macros
- Fix field comments: 'hex bytes' -> 'decimal bytes (0-255)'
- Fix TransmitterView frequency_step: 1750000 -> 12500 (12.5kHz NFM step)
- Fix MDC preamble: replace leading 0x00 bytes with 0xAA
  after NRZI encoding stayed 0x00, terminating TX on the first word.
  0xAA bytes NRZI-encode to 0xFF (continuous 1800Hz mark tone), which
  is the standard MDC-1200 preamble and does not trigger the sentinel.
2026-03-16 19:28:07 +01:00
berkeozkir 52dd2ea2bb Add FPV DETECT external app with Auto-scan FPV Presence Receiver (#3084)
* fpv_rx external app implemented

* FPV DETECT: PR feedback and Copilot suggestions

- Rename app from 'FPV RX' to 'FPV DETECT' (menu and title)
- Add fpv_rx to EXTAPPLIST in external.cmake so app is included in build
- Set external_apps_address_end = 0xAE010000 in external_app_info.py
- Fix clang-format in ui_fpv_rx.cpp (update_visual_state block)
- Re-sample adjacent channels during candidate verification so
  neighbor_margin lock decision uses current measurements instead of
  stale channel_memory_ from scan (reduces false/missed locks)

Made-with: Cursor

* Rename fpv_rx to fpv_detect (files, dir, app id)

- Rename directory external/fpv_rx -> external/fpv_detect
- Rename ui_fpv_rx.{cpp,hpp} -> ui_fpv_detect.{cpp,hpp}
- Update external.cmake and external.ld for fpv_detect
- Namespace fpv_rx -> fpv_detect, class FpvRxView -> FpvDetectView
- Section/symbol .app_fpv_rx -> .app_fpv_detect

Made-with: Cursor
2026-03-16 10:28:07 +08:00
zxkmm b273165a0c fix warning and ub (#3089) 2026-03-14 00:51:15 +08:00
Frederic BORRY 5aecd4097e EPIRB TX Application (#3081)
* First step to epirb-tx app

* Next step to epirb_tx app

* Fixed merge

* EPIRB TX app progress

- Fixed UI
- Added automatic frame resend after delay
- Added load of beacons from EPIRB/BEACONS.TXT file

* First step to adding 121.5 MHz signal

* Refactored 121.5 signal generation code

* Added frequency change when switching from BPSK to AM

* Added checkbox for AM signal.

Added START/STOP button
Added AM frequency
First step to editor mode.

* Next step to frame editor

* Fixed location bits.

* Refactored code + fixed location

* Added location fields

* UI fixes + settings backup

Fixed lat/long display
Added send on change checkbox
Added settings backup
Fixed locator edition
Fixed BEACONS.TXT to move to Self test beacons.

* Update ui_epirb_tx.cpp

Fixed timeout reset

* Added national location protocol.

Added national location protocol.
Fixed send on change.

* UI fixes

Added country selection
Separated beacon protocol and beacon type selectors

* Standard protocol

Added standard protocol
Added internal checkbox

* Fixed fomratting

* Code cleanup

* Comments / doc

* More comments / doc

* More comments / doc

Fixed pre count duration.
Added frequency restore after leaving

* Prepare merge

* Prepare merge

* Finished merge

* More comments and doc

* Update ui_epirb_tx.cpp

Fixed out of memory error when using AlphanumView

* Update proc_epirb_tx.hpp

Fixed comment

* Fixed copilot PR comments.

* Update proc_epirb_tx.hpp

Fixed formatting

* Update external.ld

Merge with kiss_tnc app
2026-03-12 22:05:42 +13:00
qwer123 2c2959df30 change pin mux (#3085)
Co-authored-by: fuwenbo <16166717+fuwenbo95@user.noreply.gitee.com>
2026-03-12 20:24:42 +13:00
stafur 9a3c2cc6ac Next praline clean up (fixed BLE RX crashes) (#3080)
* Cleaned up #ifndef PRALINE and updated logic to being with #ifdef PRALINE entries where possible to make logic flow for PRALINE code execution pipeline clearer. Cleaned up compiletime warnings for PRALINE related codebase updates.

* Addressed comments provided by copilot during PR review. Combed through frequency definitions for consistency between PLL A and PLL B register definitions for CLKs 0-7. Ensured CLK3/LK6 <- SMA PORTs and CLK7 <- not utiliized are disabled during core development phase to support root cause analysis of any spectral artifacts. Updated MCU frequency to 40MHz to ensure audio harmonics are outside FM radio band range (< 80 MHz, >120MHz) and added comments clarifying choice of 40 over 10 MHz for potential future root cause analysis in other bands where audio may be expected as needed. Added CLK6 and CLK7 to Clocks Status View Debug display. Moved CLK defintions and PLL instantiations for components that are most RF sensitive to PLL A. Left others in PLL B. That is move FPGA CLK1 to PLL B, while moving CLK2, CLK4, and CLK5 to PLL A.

* Cleaned up PLL A and B XTAL reference checks relative to 800 MHz.

* Encapsulated HackRF Pro Praline debug and status vies into a single Pro Debug submenu as part of clean up.

* Fixed BLE RX Out of Memory error. Updated LPC43xx ld scripts to accouint for additional HackRF Pro praline memory.

* Addressed copilot comments for ble_rx_app by adding recent_entries_view.set_dirty. Updated ble_rx_app for easier use with heap limit set to one less than recent entries max limit.

* Addressed copilot comments by updating comment clarity in source files. Updated ui_debug to allow for return reference if set for PRO debug menu item.

* Improved readability of intialization parameters for the FPGA registers, and addressed 20Mhz nulls by initializing DC Notch width with standard setting, and DC Adaptiation rate with a balanced setting.

* Ran format-code.sh

* Added option to allow for user to set number if entries in recent list. Default is set to a relatively stable 32.

* Removed #ifdef PRALINE pragmas from ble_rx_app such that HackRF One can also use the updated UI widget to allow for user to set number of entries in recent list.

* Ran format-code.sh
2026-03-10 21:40:43 -04:00
zxkmm c91a6db165 remove redundant code from #3078 (#3082) 2026-03-10 16:13:10 +01:00
Sarah Rose bf5ea0a004 Add KISS TNC external app for APRS RX/TX over USB serial (#3078)
* Add KISS TNC external app for APRS RX/TX over USB serial

Adds a KISS TNC (Terminal Node Controller) app that bridges APRS
packets between the HackRF radio and a connected PC over USB CDC
serial using the KISS protocol.

Features:
- APRS receive at 144.390 MHz (configurable), decoding packets via
  the existing APRS RX baseband (PAPR.bin)
- Received AX.25 frames forwarded to host as KISS-framed data over
  USB serial
- KISS frames received from host transmitted as AFSK audio using the
  existing AFSK TX baseband (PAFT.bin)
- Clean RX->TX->RX baseband switching with proper timing delays
- USB connection status indicator with periodic refresh
- Compatible with standard KISS TNC clients (Xastir, APRSISCE/32,
  Dire Wolf, etc.)

Firmware changes required to support the external app:
- baseband_api.cpp: mark set_aprs() __attribute__((used)) so it is
  retained by the linker for external app use
- protocols/ax25: add make_frame_from_raw() to build NRZI bitstream
  from raw AX.25 bytes, marked __attribute__((used))
- usb_serial_host_to_device: add kiss_raw_handler hook to route
  incoming USB bytes directly to a registered callback, bypassing
  the shell; marked __attribute__((used))
- baseband/proc_aprsrx: add constructor that auto-configures at
  1200 baud so external apps do not need to call set_aprs() at init

* Address PR review: generic RAII USB handler, fix TX cutoff and buffer overflow

- Replace KISS-specific set_kiss_raw_handler() in shared USB code with a
  generic UsbSerialInputHandler RAII class; any app can now register a
  handler via the ctor and it auto-clears in the dtor — no app-specific
  'if' checks in shared code, no __attribute__((used)) needed
- Remove __attribute__((used)) from set_aprs() — it is referenced directly
  by ui_aprs_rx.cpp so it will never be dead-stripped
- Fix send_kiss_frame() buffer overflow: per-byte bounds check prevents
  2-byte escape sequences from writing past the end of the output buffer
- Replace fillOBuffer(TIME_INFINITE) with chOQWriteTimeout(TIME_IMMEDIATE)
  to drop bytes instead of blocking the event loop
- Remove chThdSleepMilliseconds() calls from start_tx() and finish_tx()
- Fix TX cutoff bug: call start_tx() directly from process_kiss_frame()
  instead of deferring via tx_pending_ flag (deferral caused kiss_idx_
  to be 0 by the time start_tx() fired, producing an empty frame)

* Move USB write into UsbSerialInputHandler class

Add a write() method to UsbSerialInputHandler so all USB I/O
(both input callback and output) is encapsulated in the class.
KISS TNC now uses usb_input_handler_->write() instead of calling
chOQWriteTimeout(&SUSBD1.oqueue, ...) directly, keeping raw USB
details out of app code.

* Address PR review: remove auto-configure from APRSRxProcessor, add timeout param to write()

- Remove default_config from APRSRxProcessor() constructor; baseband should
  start unconfigured and wait for APRSRxConfigureMessage from the app side
  (already sent via baseband::set_aprs() in KISS TNC and APRS RX app)
- Remove stray #include "stdio.h" from proc_aprsrx.cpp
- Add systime_t timeout parameter with default TIME_IMMEDIATE to
  UsbSerialInputHandler::write() so callers can choose blocking behaviour
2026-03-10 10:40:27 +01:00
Erwin Ried bf166bb2fc Update main video 2026-03-08 21:32:39 +01:00
stafur 52239d320a Next praline clean up (#3077)
* Cleaned up #ifndef PRALINE and updated logic to being with #ifdef PRALINE entries where possible to make logic flow for PRALINE code execution pipeline clearer. Cleaned up compiletime warnings for PRALINE related codebase updates.

* Addressed comments provided by copilot during PR review. Combed through frequency definitions for consistency between PLL A and PLL B register definitions for CLKs 0-7. Ensured CLK3/LK6 <- SMA PORTs and CLK7 <- not utiliized are disabled during core development phase to support root cause analysis of any spectral artifacts. Updated MCU frequency to 40MHz to ensure audio harmonics are outside FM radio band range (< 80 MHz, >120MHz) and added comments clarifying choice of 40 over 10 MHz for potential future root cause analysis in other bands where audio may be expected as needed. Added CLK6 and CLK7 to Clocks Status View Debug display. Moved CLK defintions and PLL instantiations for components that are most RF sensitive to PLL A. Left others in PLL B. That is move FPGA CLK1 to PLL B, while moving CLK2, CLK4, and CLK5 to PLL A.

* Cleaned up PLL A and B XTAL reference checks relative to 800 MHz.
2026-03-08 13:39:53 +13:00
E.T. 561143cfb0 Improve SD Card Status Display and Error Handling (#3079)
Summary:
Fixed SD card mount error detection by enabling immediate mounting, made the status icon clickable to access settings, and added real-time status display (including filesystem type) in the SD card settings page.

Changes:
- Changed f_mount() to immediate mounting for error detection
- Made SD card status icon clickable, linking to an enhanced sd card settings page that now includes live display of card status and filesystem type
- Removed "SDCard Error" menu item
2026-03-06 20:05:44 +01:00
gullradriel 93775129cb Merge pull request #2896 from portapack-mayhem/next
Christmas stable merge
2025-12-21 12:00:18 +01:00
jLynx fe9448f149 Merge pull request #2859 from portapack-mayhem/next
V2.3.1
2025-11-08 10:35:16 +13:00
103 changed files with 6036 additions and 416 deletions
+15
View File
@@ -0,0 +1,15 @@
<!--
👋 Thank you for your contribution!
⚠️ To help avoid extensive change requests or potential rejection, please review our simple Contributing Guidelines before you start coding: https://github.com/portapack-mayhem/mayhem-firmware/wiki/Contributing-Guidelines.
Thank you again for contributing!
-->
## Brief description what you did
<!-- Describe your changes in detail here. What problem does this PR solve? What features does it add or fix? -->
## Checklist
- [ ] Kept changes minimal and limited to necessary files
- [ ] Verified functionality remains intact and code compiles
+1 -1
View File
@@ -60,7 +60,7 @@ jobs:
- name: Make build folder
run: mkdir ${{ github.workspace }}/build
- name: Run the Docker image
run: docker run -e VERSION_STRING=${{ steps.version_date.outputs.date }} -i -v ${{ github.workspace }}:/havoc portapack-dev ninja ${{ matrix.cmake_args }}
run: docker run -e VERSION_STRING=${{ steps.version_date.outputs.date }} -i -v ${{ github.workspace }}:/havoc portapack-dev ${{ matrix.cmake_args }}
- name: Create Small SD Card ZIP - No World Map
run: |
mkdir -p sdcard/FIRMWARE && cp build/firmware/portapack-mayhem-firmware.bin sdcard/FIRMWARE/portapack-mayhem_${{ steps.version_date.outputs.date }}.bin && cp build/firmware/portapack-mayhem_OCI.ppfw.tar sdcard/FIRMWARE/mayhem_nightly_${{ steps.version_date.outputs.date }}${{ matrix.artifact_suffix }}_OCI.ppfw.tar && mkdir -p sdcard/APPS && cp build/firmware/application/*.ppma sdcard/APPS && cp build/firmware/standalone/*/*.ppmp sdcard/APPS && cd sdcard && zip -r ../sdcard-no-map.zip . && cd ..
+1 -1
View File
@@ -43,7 +43,7 @@ jobs:
- name: Make build folder
run: mkdir ${{ github.workspace }}/build
- name: Run the Docker image
run: docker run -e VERSION_STRING=${{ steps.version.outputs.version }} -i -v ${{ github.workspace }}:/havoc portapack-dev ninja ${{ matrix.cmake_args }}
run: docker run -e VERSION_STRING=${{ steps.version.outputs.version }} -i -v ${{ github.workspace }}:/havoc portapack-dev ${{ matrix.cmake_args }}
- name: Create Small SD Card ZIP - No World Map
run: |
mkdir -p sdcard/FIRMWARE && cp build/firmware/portapack-mayhem-firmware.bin sdcard/FIRMWARE/portapack-mayhem_${{ steps.version.outputs.version }}.bin && cp build/firmware/portapack-mayhem_OCI.ppfw.tar sdcard/FIRMWARE/mayhem_${{ steps.version.outputs.version }}${{ matrix.artifact_suffix }}_OCI.ppfw.tar && mkdir -p sdcard/APPS && cp build/firmware/application/*.ppma sdcard/APPS && cp build/firmware/standalone/*/*.ppmp sdcard/APPS && cd sdcard && zip -r ../sdcard-no-map.zip . && cd ..
+1 -1
View File
@@ -1 +1 @@
v2.3.1
v2.3.2
+1 -1
View File
@@ -1 +1 @@
v2.3.2
v2.4.0
+10 -3
View File
@@ -17,9 +17,13 @@ This is a fork of the [Havoc](https://github.com/furrtek/portapack-havoc/) firmw
If you are new to *HackRF+PortaPack+Mayhem*, check these:
[<img alt="The Latest HackRF & Portapak Combo - H4M The Flipper Zero Killer?" src="https://img.youtube.com/vi/Ew2qDgm2hf0/maxresdefault.jpg" width="701">](https://share.hackrf.app/6HKX9A)
[<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="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)
[<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) [<img alt="HackRF 101 : Everything You Need to Know to Get Started!" src="https://img.youtube.com/vi/xGR_PMD9LeU/maxresdefault.jpg" width="172">](https://share.hackrf.app/C0J6ZR)
# Frequently Asked Questions
@@ -47,7 +51,10 @@ 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
You can write [documentation](https://github.com/portapack-mayhem/mayhem-firmware/wiki), fix bugs and [answer issues](https://github.com/portapack-mayhem/mayhem-firmware/issues) or add new functionality. Please check the following [guide](https://github.com/portapack-mayhem/mayhem-firmware/wiki/How-to-collaborate) with details.
We warmly welcome your contributions! Please refer to our **[Contributing Guidelines](https://github.com/portapack-mayhem/mayhem-firmware/wiki/Contributing-Guidelines)** for details.
You can write [documentation](https://github.com/portapack-mayhem/mayhem-firmware/wiki), fix bugs and [answer issues](https://github.com/portapack-mayhem/mayhem-firmware/issues) or add new functionality. Please also check the following [guide](https://github.com/portapack-mayhem/mayhem-firmware/wiki/How-to-collaborate) with details.
Consider that the hardware and firmware has been created and maintain by a [lot](https://github.com/mossmann/hackrf/graphs/contributors) of [people](https://github.com/portapack-mayhem/mayhem-firmware/graphs/contributors), so always try collaborating your time and effort first. For coding related questions, if something does not fit as an issue, please join our Discord by clicking the chat badge on [top](#portapack-mayhem).
+2 -2
View File
@@ -29,5 +29,5 @@ RUN mkdir /opt/build \
ADD firmware/tools/docker-entrypoint.sh /usr/local/bin/entrypoint.sh
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
# replace ninja with make if you prefer to use that by default
CMD ["ninja"]
# replace make with ninja temporarily while building your image if you prefer to use that by default
CMD ["make"]
@@ -203,15 +203,15 @@ SPECOptionsView::SPECOptionsView(
#ifdef PRALINE
PralineOptionsView::PralineOptionsView(Rect parent_rect, const Style* style) {
set_style(style);
set_parent_rect(parent_rect);
add_children({&label_sr, &options_sr, &label_dc, &options_dc,
&label_qi, &options_qi, &label_qs, &options_qs});
// 1. READ the current state from hardware
// READ the current state from hardware
fpga_reg_1 = radio::debug::fpga::register_read(1);
uint32_t fpga_reg_2 = radio::debug::fpga::register_read(2);
// 2. INITIALIZE UI WIDGETS based on read bits
// INITIALIZE UI WIDGETS based on read bits
options_dc.set_by_value((fpga_reg_1 & 0x01) ? 1 : 0); // Bit 0
options_qi.set_by_value((fpga_reg_1 & 0x02) ? 1 : 0); // Bit 1
options_qs.set_by_value((fpga_reg_1 & 0x04) ? 1 : 0); // Bit 2
+40 -3
View File
@@ -645,6 +645,20 @@ BLERxView::BLERxView(NavigationView& nav)
options_sort.set_selected_index(sort_index, true);
options_filter.set_selected_index(filter_index, true);
// ------------------------------------------------------------------------------
// Handle Max Recent Entries
// ------------------------------------------------------------------------------
// Max Recent Entries UI widget
add_child(&label_max_entries);
add_child(&field_max_entries);
field_max_entries.set_value(max_recent_entries);
// Define what happens when the user changes the number
field_max_entries.on_change = [this](int32_t v) {
max_recent_entries = (size_t)v;
};
// Auto-configure modem for LCR RX (will be removed later)
baseband::set_btlerx(channel_number);
@@ -819,6 +833,10 @@ void BLERxView::on_data(BlePacketData* packet) {
recent.push_front(*it);
recent.erase(it);
} else {
// Enforce limit
while (recent.size() >= max_recent_entries) {
recent.pop_back();
}
recent.emplace_front(key);
truncate_entries(recent);
}
@@ -887,6 +905,9 @@ void BLERxView::on_filter_change(std::string value) {
}
void BLERxView::on_file_changed(const std::filesystem::path& new_file_path) {
// Clear searchList
searchList.clear();
file_path = new_file_path;
found_count = 0;
total_count = 0;
@@ -919,8 +940,10 @@ void BLERxView::on_file_changed(const std::filesystem::path& new_file_path) {
break;
}
searchList.push_back(currentLine);
total_count++;
if (searchList.size() < max_recent_entries) {
searchList.push_back(currentLine);
total_count++;
}
bytePos += bytesRead;
@@ -940,6 +963,21 @@ void BLERxView::on_timer() {
channel_number = (channel_number < 39) ? channel_number + 1 : 37;
}
}
// Debug: Check heap status every ~1 second
static int heap_check_counter = 0;
if (++heap_check_counter >= 60) {
heap_check_counter = 0;
size_t heap_free = chCoreStatus();
if (heap_free < 4096) { // Less than 4KB free
// Emergency: clear old entries
while (recent.size() > (max_recent_entries - 1)) {
recent.pop_back();
}
recent_entries_view.set_dirty();
}
}
if (ble_rx_error != BLE_RX_NO_ERROR) {
if (ble_rx_error == BLE_RX_LIST_FILENAME_EMPTY_ERROR) {
nav_.display_modal("Error", "List filename is empty !");
@@ -1059,7 +1097,6 @@ bool BLERxView::updateEntry(const BlePacketData* packet, BleRecentEntry& entry,
entry.dbValue = packet->max_dB - (receiver_model.lna() + receiver_model.vga() + (receiver_model.rf_amp() ? 14 : 0));
entry.timestamp = to_string_timestamp(rtc_time::now());
entry.dataString = data_string;
entry.packetData.type = packet->type;
entry.packetData.size = packet->size;
entry.packetData.dataLen = packet->dataLen;
+21
View File
@@ -42,6 +42,12 @@
#include "recent_entries.hpp"
// Add for heap debugging
// For ChibiOS core functions
#include "ch.h"
#include "chcore.h"
#include "chheap.h"
class BLELogger {
public:
Optional<File::Error> append(const std::filesystem::path& filename) {
@@ -90,6 +96,7 @@ struct BleRecentEntry {
uint64_t uniqueKey;
int dbValue;
BlePacketData packetData;
std::string timestamp;
std::string dataString;
std::string nameString;
@@ -426,6 +433,20 @@ class BLERxView : public View {
[this](const Message* const) {
this->on_timer();
}};
// Widget to control the list limit
Labels label_max_entries{
{{UI_POS_X(22), 10 * 8 - 2}, "List:", Theme::getInstance()->fg_light->foreground}};
NumberField field_max_entries{
{UI_POS_X(28), 10 * 8 - 2}, // Correct: Position (x, y)
2, // Number of digits
{5, 64}, // Range (min, max) <- Coincides with max_entries
1, // Step size
' ' // Filler character
};
// Variable to store the current limit
size_t max_recent_entries = 25;
}; /* BLERxView */
} /* namespace ui */
+42 -20
View File
@@ -2593,7 +2593,9 @@ PralineClockDebugView::PralineClockDebugView(NavigationView& nav)
{&t2_id, &t2_ma, &t2_mode, &t2_src, &t2_ph, &t2_st},
{&t3_id, &t3_ma, &t3_mode, &t3_src, &t3_ph, &t3_st},
{&t4_id, &t4_ma, &t4_mode, &t4_src, &t4_ph, &t4_st},
{&t5_id, &t5_ma, &t5_mode, &t5_src, &t5_ph, &t5_st}} {
{&t5_id, &t5_ma, &t5_mode, &t5_src, &t5_ph, &t5_st},
{&t6_id, &t6_ma, &t6_mode, &t6_src, &t6_ph, &t6_st},
{&t7_id, &t7_ma, &t7_mode, &t7_src, &t7_ph, &t7_st}} {
add_children({&text_title, &text_lbl_pll, &text_pll_status,
&text_lbl_afe, &text_afe_rate, &text_lbl_n, &text_n_val,
&text_header,
@@ -2603,6 +2605,8 @@ PralineClockDebugView::PralineClockDebugView(NavigationView& nav)
&t3_id, &t3_ma, &t3_mode, &t3_src, &t3_ph, &t3_st,
&t4_id, &t4_ma, &t4_mode, &t4_src, &t4_ph, &t4_st,
&t5_id, &t5_ma, &t5_mode, &t5_src, &t5_ph, &t5_st,
&t6_id, &t6_ma, &t6_mode, &t6_src, &t6_ph, &t6_st,
&t7_id, &t7_ma, &t7_mode, &t7_src, &t7_ph, &t7_st,
&button_refresh, &button_done});
button_refresh.on_select = [this](Button&) { this->refresh(); };
@@ -2633,7 +2637,7 @@ void PralineClockDebugView::refresh() {
uint8_t output_en = portapack::clock_manager.si5351_read_register(3);
const char* ma_lookup[] = {"2m", "4m", "6m", "8m"};
for (size_t i = 0; i < 6; i++) {
for (size_t i = 0; i < 8; i++) {
uint8_t ctrl = portapack::clock_manager.si5351_read_register(16 + i);
// mA (Bits 1:0)
@@ -3474,30 +3478,48 @@ DebugMenuView::DebugMenuView(NavigationView& nav)
set_max_rows(2); // allow wider buttons
}
#ifdef PRALINE
/* PralineDebugMenuView *************************************************/
PralineDebugMenuView::PralineDebugMenuView(NavigationView& nav)
: nav_(nav) {
set_max_rows(2); // Allows wider buttons for descriptive titles
}
void PralineDebugMenuView::on_populate() {
if (portapack::persistent_memory::show_gui_return_icon()) {
add_items({{"..", ui::Theme::getInstance()->fg_light->foreground, &bitmap_icon_previous, [this]() { nav_.pop(); }}});
}
add_items({
{"WFM Audio", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<WFMAudioDebugView>(); }},
{"Clocks", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<ui::PralineClockDebugView>(); }},
{"MSynth", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<Si5351MultiSynthDebugView>(); }},
{"Radio Diag", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<RadioDiagnosticsView>(); }},
{"Radio Debug", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<PralineRadioDebugView>(); }},
{"Signal Path", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<SignalPathStatusView>(); }},
{"System Diag", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<SystemDiagnosticsView>(); }},
{"PLL A", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<Si5351PLLADebugView>(); }},
{"PLL B", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<Si5351PLLBDebugView>(); }},
{"GPIO", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<GPIODebugView>(); }},
{"RFFC Status", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<RFFC5072StatusView>(); }},
{"RFFC Tuning", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<RFFCTuningDebugView>(); }},
{"MAX2831", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<MAX2831DebugView>(); }},
{"Si5351", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<Si5351DebugView>(); }},
{"SGPIO Clk", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<SGPIO8ClockDetectorView>(); }},
{"Baseband", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<BasebandStatusView>(); }},
{"SGPIO Live", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<SGPIOLiveMonitorView>(); }},
{"RX Test", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<RadioRxTestView>(); }},
});
}
#endif
void DebugMenuView::on_populate() {
if (portapack::persistent_memory::show_gui_return_icon()) {
add_items({{"..", ui::Theme::getInstance()->fg_light->foreground, &bitmap_icon_previous, [this]() { nav_.pop(); }}});
}
add_items({
#ifdef PRALINE
{"WFM Audio", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<WFMAudioDebugView>(); }},
{"MSynth Debug", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<Si5351MultiSynthDebugView>(); }},
{"Radio Diag", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<RadioDiagnosticsView>(); }},
{"Radio Debug", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<PralineRadioDebugView>(); }},
{"Signal Path", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<SignalPathStatusView>(); }},
{"System Diag", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<SystemDiagnosticsView>(); }},
{"Clocks", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<ui::PralineClockDebugView>(); }},
{"PLL A Debug", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<Si5351PLLADebugView>(); }},
{"PLL B Debug", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<Si5351PLLBDebugView>(); }},
{"GPIO Debug", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<GPIODebugView>(); }},
{"RFFC Status", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<RFFC5072StatusView>(); }},
{"RFFC Tuning", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<RFFCTuningDebugView>(); }},
{"MAX2831 Debug", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<MAX2831DebugView>(); }},
{"Si5351 Clocks", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<Si5351DebugView>(); }},
{"SGPIO8 Clock", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<SGPIO8ClockDetectorView>(); }},
{"Baseband Status", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<BasebandStatusView>(); }},
{"SGPIO Live", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<SGPIOLiveMonitorView>(); }},
{"RX Test", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push<RadioRxTestView>(); }},
{"Pro Debug", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_setup, [this]() { nav_.push<PralineDebugMenuView>(); }},
#endif
{"Buttons Test", ui::Theme::getInstance()->fg_darkcyan->foreground, &bitmap_icon_controls, [this]() { nav_.push<DebugControlsView>(); }},
{"M0 Stack Dump", ui::Theme::getInstance()->fg_darkcyan->foreground, &bitmap_icon_memory, [this]() { stack_dump(); }},
+27 -1
View File
@@ -439,6 +439,16 @@ class RadioDiagnosticsView : public View {
};
#ifdef PRALINE
class PralineDebugMenuView : public BtnGridView {
public:
PralineDebugMenuView(NavigationView& nav);
std::string title() const override { return "Pro Debug"; };
private:
NavigationView& nav_;
void on_populate() override;
};
/* Praline-Specific Radio Debug View
* Monitors Mixer Lock, SPI Bit Depth, and toggles Si5351 CLK5
*/
@@ -1339,7 +1349,7 @@ class PralineClockDebugView : public View {
Text t2_ph{{160, 120, 48, 16}, "-"};
Text t2_st{{208, 120, 32, 16}, "-"};
// C3: SG_CLK (Switching Regulator/Logic Sync)
// C3: SG_CLK PORT 1 (Switching Regulator/Logic Sync)
Text t3_id{{0, 136, 24, 16}, "C3:"};
Text t3_ma{{32, 136, 24, 16}, "-"};
Text t3_mode{{64, 136, 48, 16}, "-"};
@@ -1363,6 +1373,22 @@ class PralineClockDebugView : public View {
Text t5_ph{{160, 168, 48, 16}, "-"};
Text t5_st{{208, 168, 32, 16}, "-"};
// C6: SG_CLK PORT 2 (Switching Regulator/Logic Sync)
Text t6_id{{0, 184, 24, 16}, "C6:"};
Text t6_ma{{32, 184, 24, 16}, "-"};
Text t6_mode{{64, 184, 48, 16}, "-"};
Text t6_src{{112, 184, 32, 16}, "-"};
Text t6_ph{{160, 184, 48, 16}, "-"};
Text t6_st{{208, 184, 32, 16}, "-"};
// C7: UNUSED_CLK (Internal Expansion)
Text t7_id{{0, 200, 24, 16}, "C7:"};
Text t7_ma{{32, 200, 24, 16}, "-"};
Text t7_mode{{64, 200, 48, 16}, "-"};
Text t7_src{{112, 200, 32, 16}, "-"};
Text t7_ph{{160, 200, 48, 16}, "-"};
Text t7_st{{208, 200, 32, 16}, "-"};
Button button_refresh{{8, 260, 100, 24}, "Refresh"};
Button button_done{{132, 260, 100, 24}, "Done"};
};
+83 -2
View File
@@ -35,6 +35,7 @@
#include "ui_external_items_menu_loader.hpp"
#include "ui_ss_viewer.hpp"
#include "ui_fileman.hpp"
#include "ui_sd_card_debug.hpp"
#include "portapack_persistent_memory.hpp"
#include "lpc43xx_cpp.hpp"
@@ -446,7 +447,11 @@ void SetUIView::focus() {
/* SetSDCardView *********************************************/
SetSDCardView::SetSDCardView(NavigationView& nav) {
add_children({&labels,
add_children({&status_labels,
&text_card_status,
&text_filesystem_type,
&button_more_info,
&labels,
&checkbox_sdcard_speed,
&button_test_sdcard_high_speed,
&text_sdcard_test_status,
@@ -455,6 +460,10 @@ SetSDCardView::SetSDCardView(NavigationView& nav) {
checkbox_sdcard_speed.set_value(pmem::config_sdcard_high_speed_io());
button_more_info.on_select = [&nav, this](Button&) {
nav.push<SDCardDebugView>();
};
button_test_sdcard_high_speed.on_select = [&nav, this](Button&) {
pmem::set_config_sdcard_high_speed_io(true, false);
text_sdcard_test_status.set("!! HIGH SPEED MODE ON !!");
@@ -472,7 +481,79 @@ SetSDCardView::SetSDCardView(NavigationView& nav) {
}
void SetSDCardView::focus() {
button_save.focus();
button_cancel.focus();
}
void SetSDCardView::on_show() {
sd_card_status_signal_token = sd_card::status_signal += [this](const sd_card::Status) {
update_sd_card_status();
};
update_sd_card_status();
}
void SetSDCardView::on_hide() {
sd_card::status_signal -= sd_card_status_signal_token;
}
void SetSDCardView::update_sd_card_status() {
using sd_card::Status;
const auto status = sd_card::status();
// Update card status text
switch (status) {
case Status::NotPresent:
text_card_status.set("Not Inserted");
text_filesystem_type.set("---");
break;
case Status::Present:
text_card_status.set("Inserted");
text_filesystem_type.set("---");
break;
case Status::Mounted:
text_card_status.set("Mounted");
// Determine filesystem type
{
const auto fs_type = sd_card::fs.fs_type;
std::string fs_name;
switch (fs_type) {
case FS_FAT12:
fs_name = "FAT12";
break;
case FS_FAT16:
fs_name = "FAT16";
break;
case FS_FAT32:
fs_name = "FAT32";
break;
case FS_EXFAT:
fs_name = "exFAT";
break;
default:
fs_name = "Unknown";
break;
}
text_filesystem_type.set(fs_name);
}
break;
case Status::ConnectError:
text_card_status.set("Connect Error");
text_filesystem_type.set("---");
break;
case Status::MountError:
text_card_status.set("Mount Error");
text_filesystem_type.set("---");
break;
case Status::IOError:
text_card_status.set("I/O Error");
text_filesystem_type.set("---");
break;
default:
text_card_status.set("Unknown");
text_filesystem_type.set("---");
break;
}
}
/* SetConverterSettingsView ******************************/
+32 -7
View File
@@ -34,6 +34,7 @@
#include "ui_navigation.hpp"
#include "bitmap.hpp"
#include "ff.h"
#include "sd_card.hpp"
#include "portapack_persistent_memory.hpp"
#include "irq_controls.hpp"
@@ -424,36 +425,60 @@ class SetSDCardView : public View {
public:
SetSDCardView(NavigationView& nav);
void on_show() override;
void on_hide() override;
void focus() override;
std::string title() const override { return "SD Card"; };
private:
SignalToken sd_card_status_signal_token{};
// Status section (top half)
Labels status_labels{
{{10, 24}, "Card Status:", Theme::getInstance()->fg_light->foreground},
{{10, 48}, "Filesystem:", Theme::getInstance()->fg_light->foreground}};
Text text_card_status{
{120, 24, 110, 16},
""};
Text text_filesystem_type{
{120, 48, 110, 16},
""};
Button button_more_info{
{UI_POS_X_CENTER(20), 90, UI_POS_WIDTH(20), UI_POS_HEIGHT(2)},
"More Info"};
// Settings section (bottom half)
Labels labels{
// 01234567890123456789012345678
{{UI_POS_X_CENTER(26), 120 - 48}, " HIGH SPEED SDCARD IO ", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X_CENTER(26), 120 - 32}, " May or may not work !! ", Theme::getInstance()->fg_light->foreground}};
{{UI_POS_X_CENTER(26), 140}, " HIGH SPEED SDCARD IO ", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X_CENTER(26), 156}, " May or may not work !! ", Theme::getInstance()->fg_light->foreground}};
Checkbox checkbox_sdcard_speed{
{UI_POS_X_CENTER(26), 120},
{UI_POS_X_CENTER(26), 180},
20,
"enable high speed IO"};
Button button_test_sdcard_high_speed{
{UI_POS_X_CENTER(27), 152, UI_POS_WIDTH(27), UI_POS_HEIGHT(2)},
{UI_POS_X_CENTER(27), 210, UI_POS_WIDTH(27), UI_POS_HEIGHT(2)},
"TEST BUTTON (NO PMEM SAVE)"};
Text text_sdcard_test_status{
{UI_POS_X_CENTER(28), 198, UI_POS_WIDTH(28), UI_POS_HEIGHT(1)},
{UI_POS_X_CENTER(28), 256, UI_POS_WIDTH(28), UI_POS_HEIGHT(1)},
""};
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(4), 12 * 8, 32},
"Save"};
Button button_cancel{
{UI_POS_X_CENTER(16) + 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(4), 12 * 8, 32},
"Cancel"};
void update_sd_card_status();
};
class SetConverterSettingsView : public View {
+14
View File
@@ -21,6 +21,7 @@
*/
#include "baseband_api.hpp"
#include <cstring>
#include "audio.hpp"
#include "tonesets.hpp"
@@ -422,6 +423,19 @@ void set_rtty_config(RTTYDataMessage& message) {
send_message(&message);
}
void set_epirb_tx_config(EPIRBTXDataMessage& 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;
memcpy(shared_memory.bb_data.data, dibits, frame_length);
P25TxConfigureMessage msg{};
msg.frame_length = frame_length;
send_message(&msg);
}
static bool baseband_image_running = false;
void run_image(const spi_flash::image_tag_t image_tag) {
+2
View File
@@ -116,6 +116,8 @@ 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_p25tx_data(const uint8_t* dibits, uint16_t frame_length);
void request_roger_beep();
void request_rssi_beep();
+255 -66
View File
@@ -35,14 +35,14 @@ using namespace lpc43xx;
extern "C" {
#include "fpga_bridge.h"
}
// Need access to ssp1_arbiter from radio namespace
// Need access to ssp1_arbiter from radio namespace for FPGA related radio method dependencies.
#include "radio.hpp"
#endif
constexpr uint32_t si5351_vco_f = 800000000;
#ifdef PRALINE
constexpr uint32_t si5351_vco_afe_f = 800000000; // Optimal for 3.072 MHz sample frequencies commonly used by apps
constexpr uint32_t si5351_vco_afe_f = 800000000; // If necessary may be changed to 768 MHz for optimal for 3.072 MHz sample frequencies commonly used by apps
#endif
constexpr si5351::Inputs si5351_inputs{
@@ -67,23 +67,43 @@ constexpr si5351::PLL si5351_pll_xtal_25m{
.c = 1,
};
// PLL A registers (Base 26)
constexpr auto si5351_pll_a_xtal_reg = si5351_pll_xtal_25m.reg(0);
#ifdef PRALINE
// Define pll_a from 25MHz clock for stable PLL A, and AFE locked reference
// PLL A: 800 MHz VCO (32x Multiplier for jitter-free 3.072 MHz sampling)
constexpr si5351::PLL si5351_pll_xtal_800m{
.f_in = si5351_inputs.f_xtal,
.a = 32,
constexpr si5351::PLL si5351_pll_a_afe_800m{
.f_in = si5351_inputs.f_xtal, // 25,000,000 Hz
.a = 32, // Multiplier: 25 * 32 = 800
.b = 0,
.c = 1,
};
constexpr auto si5351_pll_a_800_reg = si5351_pll_xtal_800m.reg(0); // Base 26
// PLL A: registers (Base 34) 800 MHz VCO (For jitter-free AFE sampling frequencies)
constexpr auto si5351_pll_a_800_reg = si5351_pll_a_afe_800m.reg(0); // Base 26
// PLL B: registers (Base 34) 800 MHz VCO (32x Multiplier for stable Digital/SGPIO bus)
constexpr auto si5351_pll_b_800_reg = si5351_pll_xtal_25m.reg(1); // Base 34
constexpr auto si5351_pll_b_xtal_reg = si5351_pll_xtal_25m.reg(1);
static_assert(si5351_pll_a_afe_800m.f_vco() == si5351_vco_f, "PLL A XTAL frequency wrong");
static_assert(si5351_pll_a_afe_800m.p1() == 3584, "PLL A XTAL P1 wrong");
static_assert(si5351_pll_a_afe_800m.p2() == 0, "PLL A XTAL P2 wrong");
static_assert(si5351_pll_a_afe_800m.p3() == 1, "PLL A XTAL P3 wrong");
// Define pll_b 25MHz clock for stable PLL B
// PLL B: 800 MHz VCO (32x Multiplier for (For stable Digital/SGPIO bus)
constexpr si5351::PLL si5351_pll_b_800m{
.f_in = si5351_inputs.f_xtal, // 25,000,000 Hz
.a = 32, // Multiplier: 25 * 32 = 800
.b = 0,
.c = 1,
};
// PLL B: registers (Base 34) 800 MHz VCO (For stable Digital/SGPIO bus)
constexpr auto si5351_pll_b_800_reg = si5351_pll_b_800m.reg(1); // Base 34
static_assert(si5351_pll_b_800m.f_vco() == si5351_vco_f, "PLL B XTAL frequency wrong");
static_assert(si5351_pll_b_800m.p1() == 3584, "PLL B XTAL P1 wrong");
static_assert(si5351_pll_b_800m.p2() == 0, "PLL B XTAL P2 wrong");
static_assert(si5351_pll_b_800m.p3() == 1, "PLL B XTAL P3 wrong");
#else
// PLL A registers (Base 26)
constexpr auto si5351_pll_a_xtal_reg = si5351_pll_xtal_25m.reg(0);
static_assert(si5351_pll_xtal_25m.f_vco() == si5351_vco_f, "PLL XTAL frequency wrong");
static_assert(si5351_pll_xtal_25m.p1() == 3584, "PLL XTAL P1 wrong");
static_assert(si5351_pll_xtal_25m.p2() == 0, "PLL XTAL P2 wrong");
@@ -99,13 +119,6 @@ constexpr si5351::PLL si5351_pll_clkin_10m{
constexpr auto si5351c_pll_b_clkin_reg = si5351_pll_clkin_10m.reg(1);
constexpr auto si5351a_pll_a_clkin_reg = si5351_pll_clkin_10m.reg(0);
#ifndef PRALINE
static_assert(si5351_pll_xtal_25m.f_vco() == si5351_vco_f, "PLL XTAL frequency wrong");
static_assert(si5351_pll_xtal_25m.p1() == 3584, "PLL XTAL P1 wrong");
static_assert(si5351_pll_xtal_25m.p2() == 0, "PLL XTAL P2 wrong");
static_assert(si5351_pll_xtal_25m.p3() == 1, "PLL XTAL P3 wrong");
#endif
static_assert(si5351_pll_clkin_10m.f_vco() == si5351_vco_f, "PLL CLKIN frequency wrong");
static_assert(si5351_pll_clkin_10m.p1() == 9728, "PLL CLKIN P1 wrong");
static_assert(si5351_pll_clkin_10m.p2() == 0, "PLL CLKIN P2 wrong");
@@ -153,6 +166,26 @@ constexpr si5351::MultisynthFractional si5351_ms_afe_10m{
.b = 0,
.c = 1,
.r_div = 0};
// (20 MHz from 800 MHz VCO: 800 / 40 = 20)
constexpr si5351::MultisynthFractional si5351_ms_afe_20m{
.f_src = si5351_vco_afe_f,
.a = 40,
.b = 0,
.c = 1,
.r_div = 0,
};
// (25 MHz from 800 MHz VCO: 800 / 32 = 25)
// Define xtal MultiSynth 25MHz clock for stable PLL A, and AFE locked XTAL reference
constexpr si5351::MultisynthFractional si5351_ms_afe_25m{
.f_src = si5351_vco_afe_f, // 800,000,000 Hz
.a = 32, // 800 / 32 = 25 MHz
.b = 0,
.c = 1,
.r_div = 0 // No final bit-shifting
};
// (40 MHz from 800 MHz VCO: 800 / 20 = 40)
constexpr si5351::MultisynthFractional si5351_ms_afe_40m{
.f_src = si5351_vco_afe_f, // 800 MHz
@@ -170,6 +203,17 @@ constexpr si5351::MultisynthFractional si5351_ms_20m{
.r_div = 0,
};
// constexpr auto si5351_ms_20m_reg = si5351_ms_20m.reg(0);
// (25 MHz from 800 MHz VCO: 800 / 32 = 25)
// Define xtal MultiSynth 25MHz clock for stable PLL B, and XTAL reference
constexpr si5351::MultisynthFractional si5351_ms_25m{
.f_src = si5351_vco_f, // 800,000,000 Hz
.a = 32, // 800 / 32 = 25 MHz
.b = 0,
.c = 1,
.r_div = 0 // No final bit-shifting
};
#endif
constexpr auto si5351c_ms_0_8m_reg = si5351_ms_0_8m.reg(clock_generator_output_og_codec);
@@ -275,12 +319,21 @@ static_assert(si5351a_ms_int_mcu_clkin.f_out() == mcu_clkin_r9_f, "MS int MCU CL
using namespace si5351;
#ifdef PRALINE
static constexpr ClockControl::MultiSynthSource get_si5351a_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)
? ClockControl::MultiSynthSource::PLLA
: ClockControl::MultiSynthSource::PLLB;
}
#endif
#ifndef PRALINE
constexpr ClockControls si5351c_clock_control_common{{
{ClockControl::ClockCurrentDrive::_8mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, get_si5351c_reference_clock_generator_pll(ClockManager::ReferenceSource::Xtal), ClockControl::MultiSynthMode::Fractional, ClockControl::ClockPowerDown::Power_Off},
{ClockControl::ClockCurrentDrive::_2mA, ClockControl::ClockSource::MS_Group, ClockControl::ClockInvert::Invert, get_si5351c_reference_clock_generator_pll(ClockManager::ReferenceSource::Xtal), ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off},
@@ -291,22 +344,25 @@ constexpr ClockControls si5351c_clock_control_common{{
{ClockControl::ClockCurrentDrive::_2mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, get_si5351c_reference_clock_generator_pll(ClockManager::ReferenceSource::Xtal), ClockControl::MultiSynthMode::Fractional, ClockControl::ClockPowerDown::Power_Off},
{ClockControl::ClockCurrentDrive::_2mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, get_si5351c_reference_clock_generator_pll(ClockManager::ReferenceSource::Xtal), ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off},
}};
#endif
constexpr ClockControls si5351a_clock_control_common{{
#ifdef PRALINE
// CLK0: MAX5864 (ADC) - 4mA, Normal PLLA Integer (Standard for Praline sync)
// CLK0: MAX5864 (ADC) - 4mA, Normal PLLA Integer
{ClockControl::ClockCurrentDrive::_4mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_On},
// CLK1: SCT_CLK (iCE40 FPGA) - 6mA, PLLA Normal Integer
{ClockControl::ClockCurrentDrive::_6mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_On},
// CLK2: LPC43xx MCU - 4mA, Normal PLLB (Must be Integer for MCU stability)
// CLK1: SCT_CLK (iCE40 FPGA) - 4mA, Normal PLLB Integer
{ClockControl::ClockCurrentDrive::_4mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLB, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_On},
// CLK3: CLKOUT SMA Port P1 - 8mA, Normal PLLB Integer
{ClockControl::ClockCurrentDrive::_8mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLB, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_On},
// CLK4: MAX2831 reference (40 MHz) - Inverted PLLA Integer (Required for mixer lock)
// CLK2: LPC43xx MCU - 2mA, Normal PLLA (Must be Integer for MCU stability)
{ClockControl::ClockCurrentDrive::_2mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_On},
// CLK3: CLKOUT SMA Port P1 - 2mA, Normal PLLB Integer Power_Off
{ClockControl::ClockCurrentDrive::_2mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLB, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off},
// CLK4: MAX2831 reference (40 MHz) - 4mA, Invert PLLA Integer (Required for mixer lock)
{ClockControl::ClockCurrentDrive::_4mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Invert, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_On},
// CLK5: RFFC5072 reference (40 MHz) - Inverted PLLA Integer (Required for mixer lock)
{ClockControl::ClockCurrentDrive::_6mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Invert, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_On},
// CLK6: Not used (disabled) 2mA, Normal PLLB, Power_Off
// CLK5: RFFC5072 reference (40 MHz) - 4mA, Invert PLLA Integer (Required for mixer lock)
{ClockControl::ClockCurrentDrive::_4mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Invert, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_On},
// CLK6: Not used (disabled) 2mA, Normal PLLB Integer, Power_Off
{ClockControl::ClockCurrentDrive::_2mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLB, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off},
// CLK7: Not used (disabled) 2mA, Normal PLLB Integer, Power_Off
{ClockControl::ClockCurrentDrive::_2mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLB, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off},
#else
{ClockControl::ClockCurrentDrive::_6mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off},
@@ -319,9 +375,9 @@ constexpr ClockControls si5351a_clock_control_common{{
{ClockControl::ClockCurrentDrive::_2mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off},
// CLK6: Not used
{ClockControl::ClockCurrentDrive::_2mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off},
#endif
// CLK7: Not used
{ClockControl::ClockCurrentDrive::_2mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off},
#endif
}};
@@ -405,7 +461,7 @@ void ClockManager::init_clock_generator() {
clock_generator.set_pll_input_sources(si5351a_pll_input_sources);
/* Skip MCU CLKIN setup and reference detection for PRALINE - not applicable */
reference = Reference{ReferenceSource::Xtal, 0};
reference = Reference{ReferenceSource::Xtal, 0}; // PLLA
/* Clock control will be set AFTER multisynth configuration - see below */
#else
@@ -461,20 +517,24 @@ void ClockManager::init_clock_generator() {
* - 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)
* * CLK3: SG_CLK (Switching Regulator/Internal Logic Sync) SMA Port 1
* - Note: Used for internal FPGA logic/gateware synchronization.
* * CLK4: P_CLK (Peripheral/Expansion Clock)
* * CLK4: P_CLK (MAX2831 Peripheral/Expansion Clock)
* - Note: Routed to expansion headers for external hardware sync.
* * CLK5: AUX_CLK (Auxiliary reference for secondary logic)
* * CLK5: AUX_CLK (RFFC5371 Auxiliary reference for secondary logic)
* - Note: Provides additional timing flexibility for the iCE40 FPGA.
* * CLK6/7: Unused / Power-Down
* * 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.
*/
/* Step 1: Write PLL A (800 MHz for RF) and PLL B (800 MHz for Digital) */
/* Use single-byte writes to debug I2C issues */
/* 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) */
@@ -484,28 +544,31 @@ void ClockManager::init_clock_generator() {
}
// Write PLLB (Registers 34-41)
/* Write PLL B configuration (Base 34) */
const auto& pll_b = si5351_pll_b_800_reg;
for (size_t i = 1; i < pll_b.size(); i++) {
clock_generator.write_register(pll_b[0] + i - 1, pll_b[i]);
}
}
/* Step 2: Write multisynth configurations using single-byte writes */
/* 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_40m); // CLK0: PLL A Codec (40 MHz)
clock_generator.write_ms_single_byte(1, si5351_ms_afe_40m); // CLK1: PLL A FPGA Timing (40 MHz)
clock_generator.write_ms_single_byte(2, si5351_ms_40m); // CLK2: PLL B MCU Input (40 MHz)
clock_generator.write_ms_single_byte(3, si5351_ms_10m); // CLK3: PLL B Logic Sync (10 MHz)
clock_generator.write_ms_single_byte(4, si5351_ms_afe_40m); // CLK4: PLL A Second IF (40 MHz)
clock_generator.write_ms_single_byte(5, si5351_ms_afe_40m); // CLK5: PLL A First IF (40 MHz)
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)
/* Step 3: NOW set clock control registers (AFTER multisynths per HackRF reference) */
/* 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_a),
si5351a_clock_control_common[2].ms_src(ref_pll_b),
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),
@@ -515,6 +578,16 @@ void ClockManager::init_clock_generator() {
// 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);
// 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);
#else
if (hackrf_r9) {
const PLLReg pll_reg = (reference.source == ReferenceSource::Xtal)
@@ -577,12 +650,16 @@ uint32_t ClockManager::measure_gp_clkin_frequency() {
}
bool ClockManager::loss_of_signal() {
#ifdef PRALINE
return clock_generator.clkin_loss_of_signal();
#else
if (hackrf_r9) {
const auto frequency = measure_gp_clkin_frequency();
return (frequency < 9850000) || (frequency > 10150000);
} else {
return clock_generator.clkin_loss_of_signal();
}
#endif
}
ClockManager::ReferenceSource ClockManager::detect_reference_source() {
@@ -605,6 +682,17 @@ ClockManager::ReferenceSource ClockManager::detect_reference_source() {
}
ClockManager::Reference ClockManager::choose_reference() {
#ifdef PRALINE
const auto detected_reference = detect_reference_source();
if ((detected_reference == ReferenceSource::External) ||
(detected_reference == ReferenceSource::PortaPack)) {
const auto frequency = measure_gp_clkin_frequency();
if ((frequency >= 9850000) && (frequency <= 10150000)) {
return {detected_reference, 10000000};
}
}
#else
if (hackrf_r9) {
gpio_r9_clkin_en.write(1);
volatile uint32_t delay = 240000 + 24000;
@@ -623,6 +711,7 @@ ClockManager::Reference ClockManager::choose_reference() {
if (hackrf_r9) {
gpio_r9_clkin_en.write(0);
}
#endif
portapack_tcxo_disable();
return {ReferenceSource::Xtal, 25000000};
@@ -755,8 +844,29 @@ void ClockManager::set_sampling_frequency(const uint32_t frequency) {
_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);
}
// Set FPGA RX decimation register
fpga_debug_register_write(2, n);
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);
radio::invalidate_spi_config();
// Configure Si5351 clocks
@@ -766,6 +876,17 @@ void ClockManager::set_sampling_frequency(const uint32_t frequency) {
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);
// 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--);
// 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
* twice the frequency, and derived from the MS0 synth. So it's only
@@ -786,18 +907,18 @@ void ClockManager::set_reference_ppb(const int32_t ppb) {
* It is assumed an external clock coming in to CLKIN/PLLB is sufficiently accurate as to not need adjustment.
* TODO: Revisit the above policy. It may be good to allow adjustment of the external reference too.
*/
if (hackrf_r9 && reference.source != ReferenceSource::Xtal) {
return;
}
#ifdef PRALINE
// On Praline, only apply if we aren't locked to a superior external 10MHz source
// (Assuming you have a way to detect the 10MHz presence on Praline)
if (reference.source == ReferenceSource::External) {
return;
}
constexpr uint32_t pll_multiplier = si5351_pll_xtal_800m.a;
constexpr uint32_t pll_multiplier = si5351_pll_a_afe_800m.a;
#else
if (hackrf_r9 && reference.source != ReferenceSource::Xtal) {
return;
}
constexpr uint32_t pll_multiplier = si5351_pll_xtal_25m.a;
#endif
@@ -815,7 +936,6 @@ void ClockManager::set_reference_ppb(const int32_t ppb) {
#ifdef PRALINE
clock_generator.write_pll_single_byte(0, pll);
clock_generator.reset_plls();
#else
const auto pll_a_reg = pll.reg(0);
clock_generator.write(pll_a_reg);
@@ -834,15 +954,15 @@ void ClockManager::start_frequency_monitor_measurement(const cgu::CLK_SEL clk_se
}
void ClockManager::wait_For_frequency_monitor_measurement_done() {
// FREQ_MON mechanism fails to finish if there's no clock present on selected input?!
#ifndef PRALINE
while (LPC_CGU->FREQ_MON.MEAS == 1);
#else
// FREQ_MON mechanism fails to finish if there's no clock present on selected input?!
#ifdef PRALINE
// PRALINE FIX: Add timeout to prevent infinite hang
uint32_t timeout = 100000;
while (LPC_CGU->FREQ_MON.MEAS == 1 && timeout > 0) {
timeout--;
}
#else
while (LPC_CGU->FREQ_MON.MEAS == 1);
#endif
}
@@ -855,7 +975,8 @@ uint32_t ClockManager::get_frequency_monitor_measurement_in_hertz() {
void ClockManager::start_audio_pll() {
#ifdef PRALINE
// Comprehensive init matches non-PRALINE for stability and source selection
// Control Block
cgu::pll0audio::ctrl({
.pd = 1, // Start powered down
.bypass = 0, // Use the PLL
@@ -864,19 +985,66 @@ void ClockManager::start_audio_pll() {
.clken = 0, // Disable output initially
.frm = 0, // Normal mode
.autoblock = 1, // Glitchless switching
.pllfract_req = 0, // Integer mode
.sel_ext = 1, // MUST BE 1 to use clk_sel GP_CLKIN
.mod_pd = 1, // Power down modulator (Reduces noise/hiss)
.pllfract_req = 1, // Integer, Disabled
.sel_ext = 1, // Use GP_CLKIN (CLK2)
.mod_pd = 0, // Modulator OFF
.clk_sel = cgu::CLK_SEL::GP_CLKIN,
});
cgu::pll0audio::mdiv({.mdec = 30542UL});
cgu::pll0audio::np_div({.pdec = 31, .ndec = 45});
cgu::pll0audio::frac({.pllfract_ctrl = 0});
/*
* Audio PLL Configuration for 48 kHz audio with 256Fs MCLK
* Target output: Fout = 12.288 MHz
*
* Formulas:
* Fout = Fin × MSEL / (NSEL × PSEL)
* FCO = 2 × Fin × MSEL / NSEL (must be 275-550 MHz)
*/
/*
*
* 10 MHz INPUT (HackRF r9 compatible but interfere with fm band)
*
* MSEL=3072, NSEL=125, PSEL=20
* Fout = 10 × 3072 / (125 × 20) = 30720 / 2500 = 12.288 MHz
* FCO = 2 × 10 × 3072 / 125 = 61440 / 125 = 491.52 MHz
*
* Encoded values: MDEC=8308, NDEC=45, PDEC=31
* CLK2 harmonics: 90, 100, 110 MHz (interfere with FM band!)
*
*/
/*
// Math: (10 MHz * 3072) / (125 * 20) * 2 = 12.288MHz
cgu::pll0audio::mdiv({.mdec = 8308UL}); // MSEL = 3072
cgu::pll0audio::np_div({
.pdec = 31, // PSEL = 20 for 10 MHz
.ndec = 45 // NSEL = 125 for 10 MHz
});
*/
/*
*
* 40 MHz INPUT (Recommended - avoids FM band harmonics)
*
* MSEL=768, NSEL=125, PSEL=20
* Fout = 40 × 768 / (125 × 20) = 30720 / 2500 = 12.288 MHz
* FCO = 2 × 40 × 768 / 125 = 61440 / 125 = 491.52 MHz
*
* Encoded values: MDEC=30542, NDEC=45, PDEC=31
* CLK2 harmonics: 80, 120, 160 MHz (none in FM 88-108 MHz band)
*
*/
// 40 MHz input → 12.288 MHz output (same as HackRF OG)
// Math: (40MHz * 768) / (125 * 20) * 2 = 12.288MHz
cgu::pll0audio::mdiv({.mdec = 30542UL}); // MSEL = 768
cgu::pll0audio::np_div({.pdec = 31,
.ndec = 45});
cgu::pll0audio::frac({.pllfract_ctrl = 0});
cgu::pll0audio::power_up();
// Safety timeout prevents boot hang if Si5351 clock is missing
// Lock and Routing (Keep as is)
{
uint32_t timeout = 100000;
while (!cgu::pll0audio::is_locked() && timeout > 0) {
@@ -889,6 +1057,7 @@ void ClockManager::start_audio_pll() {
LPC_CGU->BASE_AUDIO_CLK.AUTOBLOCK = 1;
LPC_CGU->BASE_AUDIO_CLK.CLK_SEL = toUType(cgu::CLK_SEL::IDIVD);
#else
cgu::pll0audio::ctrl({
.pd = 1,
@@ -962,6 +1131,25 @@ void ClockManager::stop_audio_pll() {
}
void ClockManager::enable_clock_output(bool enable) {
#ifdef PRALINE
auto clkout_select = clock_generator_output_og_clkout;
if (enable) {
clock_generator.enable_output(clkout_select);
if (portapack::persistent_memory::clkout_freq() < 1000) {
clock_generator.set_ms_frequency(clkout_select, portapack::persistent_memory::clkout_freq() * 128000, si5351_vco_f, 7);
} else {
clock_generator.set_ms_frequency(clkout_select, portapack::persistent_memory::clkout_freq() * 1000, si5351_vco_f, 0);
}
auto si5351_clock_control_common = si5351a_clock_control_common;
const auto ref_pll = get_si5351a_reference_clock_generator_pll(reference.source);
clock_generator.set_clock_control(clkout_select, si5351_clock_control_common[clkout_select].ms_src(ref_pll).clk_pdn(ClockControl::ClockPowerDown::Power_On));
} else {
clock_generator.disable_output(clkout_select);
clock_generator.set_clock_control(clkout_select, ClockControl::power_off());
}
#else
if (hackrf_r9) {
gpio_r9_clkout_en.output();
gpio_r9_clkout_en.write(enable);
@@ -990,4 +1178,5 @@ void ClockManager::enable_clock_output(bool enable) {
clock_generator.disable_output(clkout_select);
clock_generator.set_clock_control(clkout_select, ClockControl::power_off());
}
#endif
}
+1 -1
View File
@@ -45,7 +45,7 @@ __attribute__((section(".external_app.app_battleship.application_information"),
0xFF, 0xFF, // ################
0xFC, 0x3F, // ..############..
},
/*.icon_color = */ ui::Color::blue().v,
/*.icon_color = */ ui::Color::green().v,
/*.menu_location = */ app_location_t::GAMES,
/*.desired_menu_position = */ -1,
+1 -1
View File
@@ -43,7 +43,7 @@ __attribute__((section(".external_app.app_blackjack.application_information"), u
0x80, 0x01, // .......##.......
0x00, 0x00, // ................
},
ui::Color::red().v, // Red color for diamonds
ui::Color::green().v, // Red color for diamonds
app_location_t::GAMES,
-1,
+1 -1
View File
@@ -24,7 +24,7 @@ __attribute__((section(".external_app.app_cvs_spam.application_information"), us
/*.app_name = */ "CVS Spam",
/*.bitmap_data = */ {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81, 0x81},
/*.icon_color = */ ui::Color::red().v,
/*.icon_color = */ ui::Color::yellow().v,
/*.menu_location = */ app_location_t::TX,
/*.desired_menu_position = */ -1,
+1 -1
View File
@@ -73,7 +73,7 @@ __attribute__((section(".external_app.app_detector_rx.application_information"),
0x0C,
0xF0,
0x0F},
/*.icon_color = */ ui::Color::orange().v,
/*.icon_color = */ ui::Color::yellow().v,
/*.menu_location = */ app_location_t::RX,
/*.desired_menu_position = */ -1,
+1 -1
View File
@@ -58,7 +58,7 @@ __attribute__((section(".external_app.app_doom.application_information"), used))
0x00,
0x00,
},
ui::Color::red().v,
ui::Color::green().v,
app_location_t::GAMES,
-1,
{0, 0, 0, 0},
+1 -1
View File
@@ -73,7 +73,7 @@ __attribute__((section(".external_app.app_epirb_rx.application_information"), us
0x00,
0x00,
},
/*.icon_color = */ ui::Color::red().v,
/*.icon_color = */ ui::Color::orange().v,
/*.menu_location = */ app_location_t::RX,
/*.desired_menu_position = */ -1,
+356
View File
@@ -0,0 +1,356 @@
/*
* 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_H__
#define __BEACON_H__
#include "portapack.hpp"
#include "ui_epirb_tx.hpp"
#include "location.hpp"
namespace ui::external_app::epirb_tx {
/**
* Get bits frop the provided frame
* @param data the frame data
* @param startBit the start bit number (one based to match documentation)
* @param endBit the end bit number (one based to match documentation)
* @return the selected bits (max 64)
*/
static uint64_t get_bits(uint8_t* data, int startBit, int endBit) {
uint64_t result = 0;
// 0 bases bit count
startBit--;
int numBits = endBit - startBit;
// get a pointer to the starting byte...
const uint8_t* pData = &(data[startBit / 8]);
uint8_t b = *pData;
// calculate the starting bit within that byte...
int bitOffset = 7 - (startBit % 8);
// iterate for the desired number of bits...
for (int i = 0; i < numBits; ++i) {
// make room for the next bit...
result <<= 1;
// copy the bit...
result |= ((b >> bitOffset) & 0x01);
// reached the end of the current byte?
if (--bitOffset < 0) {
b = *(++pData); // go to the next byte...
bitOffset = 7; // restart at the first bit in that byte...
}
}
// all done...
return result;
}
/**
* Compute a BCH code
* @param frame the frame data
* @param startBit the bit to start BCH calculation
* @param endBit the bit to stop calculation
* @param poly the BCH polynome
* @param polyLength the BCH polynome length
* @return the BCH code
*/
static uint64_t compute_bch(uint8_t* frame, int startBit, int endBit, unsigned long poly, int polyLength) { // Length of data to be checked (not including the BCH code)
int dataLength = endBit - startBit + 1;
// Total lengh (including the BCH code that will be padded to zeros (BCH code length is polyLengh-1))
int totalLength = dataLength + polyLength - 1;
// Start with the first polyLength bits
uint64_t result = get_bits(frame, startBit, startBit + polyLength - 1);
for (int i = polyLength; i <= totalLength; i++) { // Iterate on each bit after the first polyLength batch
bool firstBit = result >> (polyLength - 1);
if (firstBit) { // We have a leading 1 => xor the result with the poly
result = result ^ poly;
}
if (i < totalLength) { // Move to next bit
result = result << 1;
if (i < dataLength) { // Append next bit
result |= get_bits(frame, startBit + i, startBit + i);
} // else : 0 padding after data length
}
}
return result;
}
// 21 bits BCH polynomial
#define BCH_21_POLYNOMIAL 0b1001101101100111100011UL
#define BCH_21_POLY_LENGTH 22
// 12 bits BCH polynomial
#define BCH_12_POLYNOMIAL 0b1010100111001UL
#define BCH_12_POLY_LENGTH 13
/**
* Combyte BCH1 code for the provided frame
*/
static uint64_t compute_bch1(uint8_t* frame) {
return compute_bch(frame, 25, 85, BCH_21_POLYNOMIAL, BCH_21_POLY_LENGTH);
}
/**
* Combyte BCH2 code for the provided frame
*/
static uint64_t compute_bch2(uint8_t* frame) {
return compute_bch(frame, 107, 132, BCH_12_POLYNOMIAL, BCH_12_POLY_LENGTH);
}
/**
* Set a bit in the provided frame
* @param buf the frame
* @param bit the position of the bit to set (0 based)
* @param v the bit value
*/
static void set_bit(uint8_t* buf, int bit, bool v) {
int byte = bit >> 3;
int off = 7 - (bit & 7);
if (v)
buf[byte] |= (1 << off);
else
buf[byte] &= ~(1 << off);
}
/**
* Push bits to the provided frame
* @param buf the frame
* @param pos the current frame possition (in/out, will be incremented during the opreration)
* @param v the bits to push (max 64)
* @param n the number of bits to push
*/
static void push_bits(uint8_t* buf, int& pos, uint64_t v, int n) {
for (int i = n - 1; i >= 0; i--)
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
* @param params the beacon parameters
* @return the size of the generated frame
*/
size_t generate_beacon(uint8_t* frame, const BeaconParams& params) {
// Clear the content of the frame
memset(frame, 0, 18);
int pos = 0;
uint32_t deg, min, sec;
// bit sync
for (int i = 0; i < 15; i++)
push_bits(frame, pos, 1, 1);
// frame sync
push_bits(frame, pos, params.is_test ? 0b011010000 : 0b000101111, 9);
// PDF-1 (Protexted Data field 1)
int pdf1_start = pos;
push_bits(frame, pos, 1, 1); // format flag (long)
bool is_user = (params.protocol == BeaconProtocol::USER);
bool is_standard = (params.protocol == BeaconProtocol::STANDARD);
push_bits(frame, pos, is_user, 1); // protocol flag
push_bits(frame, pos, params.country, 10); // country code
switch (params.type) {
case BeaconType::EPIRB:
if (is_user)
push_bits(frame, pos, 0b010, 3);
else if (is_standard)
push_bits(frame, pos, 0b0010, 4);
else
push_bits(frame, pos, 0b1010, 4);
break;
case BeaconType::PLB:
if (is_user)
push_bits(frame, pos, 0b011, 3);
else if (is_standard)
push_bits(frame, pos, 0b0111, 4);
else
push_bits(frame, pos, 0b1011, 4);
break;
default:
case BeaconType::ELT:
if (is_user)
push_bits(frame, pos, 0b001, 3);
else if (is_standard)
push_bits(frame, pos, 0b0011, 4);
else
push_bits(frame, pos, 0b1000, 4);
break;
}
// Fill the rest of PDF 1 with zeros
while (pos < pdf1_start + 61)
push_bits(frame, pos, 0, 1);
if (is_user) {
// User Location Protocol: no position in PDF1
if (params.type == BeaconType::PLB) {
// Set bits for PLB
set_bit(frame, 40 - 1, 1);
set_bit(frame, 41 - 1, 1);
set_bit(frame, 42 - 1, 0);
}
set_bit(frame, 85 - 1, params.has_121_5);
} else if (is_standard) {
// Standard Location Protocol
// North / south
set_bit(frame, 65 - 1, params.location.south);
// E / W
set_bit(frame, 75 - 1, params.location.west);
pos = 66 - 1;
// Latitude 1/4 degrees (9 bits)
deg = (params.location.lat_deg << 2) + (params.location.lat_min / 15);
push_bits(frame, pos, deg, 9);
pos = 76 - 1;
// Longitude 1/4 degrees (10 bits)
deg = (params.location.long_deg << 2) + (params.location.long_min / 15);
push_bits(frame, pos, deg, 10);
pos = pdf1_start + 61;
} else {
// National Location Protocol
// North / south
set_bit(frame, 59 - 1, params.location.south);
// E / W
set_bit(frame, 72 - 1, params.location.west);
pos = 60 - 1;
// Latitude degrees (7 bits)
push_bits(frame, pos, params.location.lat_deg, 7);
// Latitude min (5 bits, 2 min increment)
min = params.location.lat_min / 2;
push_bits(frame, pos, min, 5);
pos = 73 - 1;
// Longitude degrees (8 bits)
push_bits(frame, pos, params.location.long_deg, 8);
// Longiitude min (5 bits, 2 min increment)
min = params.location.long_min / 2;
push_bits(frame, pos, min, 5);
pos = pdf1_start + 61;
}
// Set BCH1
uint64_t bch1 = compute_bch1(frame);
push_bits(frame, pos, bch1, 21);
// PDF-2 (Protexted Data Field 2)
int pdf2_start = pos;
if (is_user) {
// User Location Protocol
push_bits(frame, pos, params.is_internal, 1);
// Latitude N/S
push_bits(frame, pos, params.location.south, 1);
// Latitude degrees (7 bits)
push_bits(frame, pos, params.location.lat_deg, 7);
// Latitude minutes (4 bits, 4 minutes precision)
min = params.location.lat_min / 4;
push_bits(frame, pos, min, 4);
// Longitude E/W
push_bits(frame, pos, params.location.west, 1);
// Longitude degrees (8 bits)
push_bits(frame, pos, params.location.long_deg, 8);
// Longitude minutes (4 bits, 4 minutes precision)
min = params.location.long_min / 4;
push_bits(frame, pos, min, 4);
} else if (is_standard) {
// Standard Location Protocol
push_bits(frame, pos, 0b1101, 4);
push_bits(frame, pos, params.is_internal, 1);
push_bits(frame, pos, params.has_121_5, 1);
// Latiitude
// +
push_bits(frame, pos, 1, 1);
// Min
min = params.location.lat_min % 15;
push_bits(frame, pos, min, 5);
// Sec (4 bits, 4 sec precision)
sec = params.location.lat_sec / 4;
push_bits(frame, pos, sec, 4);
// Longitude
// +
push_bits(frame, pos, 1, 1);
// Min
min = params.location.long_min % 15;
push_bits(frame, pos, min, 5);
// Sec (4 bits, 4 sec precision)
sec = params.location.long_sec / 4;
push_bits(frame, pos, sec, 4);
} else {
// National Location Protocol
push_bits(frame, pos, 0b1101, 4);
push_bits(frame, pos, params.is_internal, 1);
push_bits(frame, pos, params.has_121_5, 1);
// Lat
// +
push_bits(frame, pos, 1, 1);
// Min
push_bits(frame, pos, (params.location.lat_min % 2), 2);
// Sec (4 bits, 4 sec precision)
sec = params.location.lat_sec / 4;
push_bits(frame, pos, sec, 4);
// LLon
// +
push_bits(frame, pos, 1, 1);
// Min
push_bits(frame, pos, (params.location.long_min % 2), 2);
// Sec (4 bits, 4 sec precision)
sec = params.location.long_sec / 4;
push_bits(frame, pos, sec, 4);
}
while (pos < pdf2_start + 26)
push_bits(frame, pos, 0, 1);
// Compute BCH 2
uint64_t bch2 = compute_bch2(frame);
push_bits(frame, pos, bch2, 12);
return 18;
}
} // namespace ui::external_app::epirb_tx
#endif /*__BEACON_H__*/
+268
View File
@@ -0,0 +1,268 @@
/*
* 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_H__
#define __LOCATION_H__
#include "portapack.hpp"
#include "ui_epirb_tx.hpp"
#include <cmath>
#include <cctype>
#include <cstdio>
namespace ui::external_app::epirb_tx {
/**
* Convert decimal coordinates to sexagesimal coordinates
* @param value the decimal value
* @param negative output N(false)/S(true) or E(false)/W(true) value
* @param deg output degrees value
* @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) {
negative = value < 0;
value = std::fabs(value);
deg = (uint16_t)value;
double m = (value - deg) * 60.0;
min = (uint8_t)m;
double s = (m - min) * 60.0;
sec = (uint8_t)(s + 0.5);
if (sec == 60) {
sec = 0;
min++;
}
if (min == 60) {
min = 0;
deg++;
}
}
/**
* Convert sexagesimal coordinates to decimal
* @param negative N(false)/S(true) or E(false)/W(true) value
* @param deg degrees value
* @param min minutes value
* @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;
return negative ? -v : v;
}
/**
* Convert maidenhead locator to decima coordinates
* @param loc the locator
* @param lat output latitude
* @param lon output longitude
*/
static void maidenhead_to_decimal(const std::string& loc, float& lat, float& lon) {
static const double lon_step[] =
{
20.0,
2.0,
1.0 / 12.0,
1.0 / 120.0,
1.0 / 2880.0};
static const double lat_step[] =
{
10.0,
1.0,
1.0 / 24.0,
1.0 / 240.0,
1.0 / 5760.0};
lon = -180.0;
lat = -90.0;
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]);
double lon_size = lon_step[i];
double lat_size = lat_step[i];
int v1, v2;
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;
}
// Cell center
lon += lon_step[pairs - 1] / 2.0;
lat += lat_step[pairs - 1] / 2.0;
}
/**
* Convert decimal coordinates to maidenhead locator
* @param lat the latitude
* @param lon the longitude
* @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;
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.0 / 60.0);
int F = lat / (2.5 / 60.0);
lon -= E * (5.0 / 60.0);
lat -= F * (2.5 / 60.0);
int G = lon / (5.0 / 600.0);
int H = lat / (2.5 / 600.0);
std::string locator;
locator += char('A' + A);
locator += char('A' + B);
if (precision >= 4) {
locator += char('0' + C);
locator += char('0' + D);
}
if (precision >= 6) {
locator += char('a' + E);
locator += char('a' + F);
}
if (precision >= 8) {
locator += char('0' + G);
locator += char('0' + H);
}
return locator;
}
/**
* Init the provided Location values from its locator
*/
void init_from_locator(Location& loc) {
maidenhead_to_decimal(loc.locator, loc.latitude, loc.longitude);
decimal_to_dms(loc.latitude,
loc.south,
loc.lat_deg,
loc.lat_min,
loc.lat_sec);
decimal_to_dms(loc.longitude,
loc.west,
loc.long_deg,
loc.long_min,
loc.long_sec);
}
/**
* Init the provided Location values from its decimal coordinates
*/
void init_from_decimal(Location& loc) {
decimal_to_dms(loc.latitude,
loc.south,
loc.lat_deg,
loc.lat_min,
loc.lat_sec);
decimal_to_dms(loc.longitude,
loc.west,
loc.long_deg,
loc.long_min,
loc.long_sec);
loc.locator = decimal_to_maidenhead(loc.latitude, loc.longitude);
}
/**
* Init the provided Location values from its sexagesimal coordinates
*/
void init_from_dms(Location& loc) {
loc.latitude = dms_to_decimal(
loc.south,
loc.lat_deg,
loc.lat_min,
loc.lat_sec);
loc.longitude = dms_to_decimal(
loc.west,
loc.long_deg,
loc.long_min,
loc.long_sec);
loc.locator = decimal_to_maidenhead(loc.latitude, loc.longitude);
}
/**
* 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];
// 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');
return std::string(buffer);
}
/**
* 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];
// 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');
return std::string(buffer);
}
} // namespace ui::external_app::epirb_tx
#endif /*__LOCATION_H__*/
+82
View File
@@ -0,0 +1,82 @@
/*
* 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.hpp"
#include "ui_epirb_tx.hpp"
#include "ui_navigation.hpp"
#include "external_app.hpp"
namespace ui::external_app::epirb_tx {
void initialize_app(ui::NavigationView& nav) {
nav.push<EPIRBTXAppView>();
}
} // namespace ui::external_app::epirb_tx
extern "C" {
__attribute__((section(".external_app.app_epirb_tx.application_information"), used)) application_information_t _application_information_epirb_tx = {
/*.memory_location = */ (uint8_t*)0x00000000,
/*.externalAppEntry = */ ui::external_app::epirb_tx::initialize_app,
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
/*.app_name = */ "EPIRB TX",
/*.bitmap_data = */ {
0x00,
0x00,
0x00,
0x00,
0x7C,
0x3E,
0xFE,
0x7F,
0xFF,
0xFF,
0xF7,
0xEF,
0xE3,
0xC7,
0xE3,
0xC7,
0xE3,
0xC7,
0xE3,
0xC7,
0xF7,
0xEF,
0xFF,
0xFF,
0xFE,
0x7F,
0x7C,
0x3E,
0x00,
0x00,
0x00,
0x00,
},
/*.icon_color = */ ui::Color::green().v,
/*.menu_location = */ app_location_t::TX,
/*.desired_menu_position = */ -1,
/*.m4_app_tag = portapack::spi_flash::image_tag_epirb_tx */ {'P', 'E', 'P', 'T'},
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
};
}
+479
View File
@@ -0,0 +1,479 @@
/*
* 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_epirb_tx.hpp"
#include "tonesets.hpp"
#include "portapack.hpp"
#include "baseband_api.hpp"
#include "string_format.hpp"
#include "file_reader.hpp"
#include "file_path.hpp"
#include "ui_geomap.hpp"
#include "ui_alphanum.hpp"
#include <cstring>
#include <stdio.h>
#include "beacon.hpp"
using namespace portapack;
namespace ui::external_app::epirb_tx {
void EPIRBTXAppView::focus() {
button_tx.focus();
}
EPIRBTXAppView::~EPIRBTXAppView() {
// Restore original frequency before leaving
transmitter_model.set_target_frequency(original_frequency);
transmitter_model.disable();
baseband::shutdown();
}
/**
* Conversion from hex char to half byte
*/
static uint8_t hexval(char c) {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
return 0;
}
/**
* Convert from hex chars to uint8_t
*/
static uint8_t hexToByte(char high, char low) {
return (hexval(high) << 4) | hexval(low);
}
std::string EPIRBTXAppView::frame_to_hex_string(bool start) {
return beacon_to_hex_string(epirb_tx_message.data, start);
}
void EPIRBTXAppView::generate_frame(BeaconParams params) {
epirb_tx_message.data_len = generate_beacon(epirb_tx_message.data, params);
}
void EPIRBTXAppView::on_timer() {
// Timer is called on display refresh
if (loop) {
if (loop_enabled) {
// chTimeNow() returns milliseconds on our version of ChibiOS / Hardware
auto now = chTimeNow();
auto elapsed = ((now - last_frame_time) / 1000);
std::string timeout = std::to_string((uint32_t)(delay > elapsed ? delay - elapsed : 0));
if (timeout != text_timeout.get()) {
// Update timeout text every seconds
text_timeout.set(timeout);
}
if (now > (last_frame_time + (delay * 1000))) {
// Send a new frame after the delay
start_tx();
}
} else {
// Stop send loop
loop = false;
}
}
}
void EPIRBTXAppView::update_frame(bool updateConfig) {
if (mode_file) {
// In file mode, currently selected beacon has changed => load the new one
Beacon& beacon = beacons[selected_beacon];
// Set desciption
text_description.set(beacon.description.substr(0, max_text_width_ext));
text_description_end.set(beacon.description.size() > max_text_width_ext ? "-" + beacon.description.substr(max_text_width_ext, max_text_width_ext + max_text_width_ext - 1) : "");
// Udapte frame content on display
text_frame.set(beacon.frame.substr(0, 18));
text_frame_end.set(beacon.frame.size() > 18 ? beacon.frame.substr(18, 36) : "");
// Prepare tx configuration
epirb_tx_message.data_len = std::min<size_t>((beacon.frame.size() / 2), 18);
for (uint8_t i = 0; i < epirb_tx_message.data_len; i++) {
epirb_tx_message.data[i] = hexToByte(
beacon.frame[2 * i],
beacon.frame[2 * i + 1]);
}
} else {
// In manual mode, generate frame content for current beacon params
generate_frame(beacon_params);
// Update frame content on display
text_frame.set(frame_to_hex_string(true));
text_frame_end.set(frame_to_hex_string(false));
}
if (updateConfig && send_on_change && loop) {
// Need to update config / send new beacon
if (am_enabled) {
// Already transmitting => update config
last_frame_time = chTimeNow();
update_config();
} else {
// Not yet transmitting => start tx
start_tx();
}
}
}
void EPIRBTXAppView::update_config() {
if (epirb_tx_message.mode_bpsk) {
// Already in BPSK mode => backup bpsk frequency
bpsk_frequency = transmitter_model.target_frequency();
} else {
// Previously in AM mode => restore bpsk frequency
transmitter_model.set_target_frequency(bpsk_frequency);
}
// Set mode to bpsk
epirb_tx_message.mode_bpsk = true;
// Set pre/post count
epirb_tx_message.pre_count = (150 * TONES_SAMPLERATE) / 1000; // 150 ms
epirb_tx_message.post_count = (100 * TONES_SAMPLERATE) / 1000; // 100 ms
// Send config to baseband
baseband::set_epirb_tx_config(epirb_tx_message);
}
void EPIRBTXAppView::set_tx_button_state(bool active) {
button_tx.set_text(active ? "START" : "STOP");
button_tx.set_style(active ? &style_tx_start : &style_tx_stop);
}
void EPIRBTXAppView::start_tx() {
last_frame_time = chTimeNow();
update_config();
loop = loop_enabled;
transmitter_model.enable();
tx_view.set_transmitting(true);
set_tx_button_state(false);
transmitting = true;
}
void EPIRBTXAppView::stop_tx() {
loop = false;
transmitter_model.disable();
tx_view.set_transmitting(false);
set_tx_button_state(true);
transmitting = false;
}
void EPIRBTXAppView::on_tx_progress(const uint32_t progress, const bool done) {
(void)progress;
if (done) {
if (am_enabled) {
// BPSK frame sent, switch back to 121.5 AM signal
epirb_tx_message.mode_bpsk = false;
// Backup bpsk frequency for next run
bpsk_frequency = transmitter_model.target_frequency();
// Restore am frequency
transmitter_model.set_target_frequency(am_frequency);
// Send config to baseband
baseband::set_epirb_tx_config(epirb_tx_message);
} else {
// End of BPSK frame
transmitter_model.disable();
tx_view.set_transmitting(false);
if (!loop) {
// Not looping => update transmission state
set_tx_button_state(true);
transmitting = false;
}
}
}
}
void EPIRBTXAppView::update_location(bool updateLocatorField) {
locator = beacon_params.location.locator;
if (updateLocatorField) text_field_beacon_locator.set_text(beacon_params.location.locator);
text_beacon_latitude_value.set(to_latitude_string(beacon_params.location));
text_beacon_longitude_value.set(to_longitude_string(beacon_params.location));
}
void EPIRBTXAppView::update_mode() {
// Hide / show widgets for file mode or manual mode
text_beacon.hidden(!mode_file);
text_description_label.hidden(!mode_file);
options_frame.hidden(!mode_file);
text_description.hidden(!mode_file);
text_description_end.hidden(!mode_file);
text_beacon_type.hidden(mode_file);
text_beacon_country.hidden(mode_file);
checkbox_beacon_internal.hidden(mode_file);
text_beacon_locator.hidden(mode_file);
text_beacon_latitude.hidden(mode_file);
text_beacon_latitude_value.hidden(mode_file);
text_beacon_longitude.hidden(mode_file);
text_beacon_longitude_value.hidden(mode_file);
button_mangps.hidden(mode_file);
options_beacon_type.hidden(mode_file);
options_beacon_protocol.hidden(mode_file);
options_beacon_country.hidden(mode_file);
text_field_beacon_locator.hidden(mode_file);
}
EPIRBTXAppView::EPIRBTXAppView(
NavigationView& nav) {
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
add_children({&labels,
&options_mode,
&text_beacon,
&text_description_label,
&text_beacon_type,
&options_beacon_type,
&options_beacon_protocol,
&text_beacon_country,
&options_beacon_country,
&checkbox_beacon_internal,
&text_beacon_locator,
&text_beacon_latitude,
&text_beacon_latitude_value,
&text_beacon_longitude,
&text_beacon_longitude_value,
&button_mangps,
&text_field_beacon_locator,
&options_frame,
&text_description,
&text_description_end,
&text_frame,
&text_frame_end,
&text_timeout,
&checkbox_loop,
&field_delay,
&button_tx,
&checkbox_am,
&field_am_frequency,
&checkbox_send_on_change,
&tx_view});
text_beacon.set_style(Theme::getInstance()->fg_light);
text_description_label.set_style(Theme::getInstance()->fg_light);
text_beacon_type.set_style(Theme::getInstance()->fg_light);
text_beacon_country.set_style(Theme::getInstance()->fg_light);
text_beacon_locator.set_style(Theme::getInstance()->fg_light);
text_beacon_latitude.set_style(Theme::getInstance()->fg_light);
text_beacon_longitude.set_style(Theme::getInstance()->fg_light);
// Restore settings
checkbox_am.set_value(am_enabled);
checkbox_loop.set_value(loop_enabled);
checkbox_send_on_change.set_value(send_on_change);
options_mode.set_by_value(!mode_file);
transmitter_model.set_target_frequency(bpsk_frequency);
field_am_frequency.set_value(am_frequency);
field_delay.set_value(delay);
options_beacon_type.set_by_value(beacon_type);
options_beacon_protocol.set_by_value(beacon_protocol);
options_beacon_country.set_by_value(beacon_country);
checkbox_beacon_internal.set_value(beacon_internal);
beacon_params.type = (BeaconType)beacon_type;
beacon_params.has_121_5 = am_enabled;
beacon_params.location.locator = locator;
beacon_params.is_internal = beacon_internal;
init_from_locator(beacon_params.location);
update_mode();
update_location();
options_mode.on_change = [this](size_t index, OptionsField::value_t) {
mode_file = (index == 0);
update_mode();
update_frame();
set_dirty();
};
options_beacon_type.on_change = [this](size_t index, OptionsField::value_t) {
beacon_params.type = (BeaconType)index;
beacon_type = index;
update_frame();
set_dirty();
};
options_beacon_protocol.on_change = [this](size_t index, OptionsField::value_t) {
beacon_params.protocol = (BeaconProtocol)index;
beacon_protocol = index;
update_frame();
set_dirty();
};
options_beacon_country.on_change = [this](size_t, OptionsField::value_t v) {
beacon_params.country = v;
beacon_country = v;
update_frame();
set_dirty();
};
checkbox_beacon_internal.on_select = [this](Checkbox&, bool v) {
beacon_internal = v;
beacon_params.is_internal = v;
update_frame();
set_dirty();
};
text_field_beacon_locator.on_select = [this, &nav](TextField&) mutable {
auto te_view = nav.push<AlphanumView>(locator, 10, ENTER_KEYBOARD_MODE_ALPHA);
te_view->on_changed = [this](std::string& value) {
beacon_params.location.locator = value;
init_from_locator(beacon_params.location);
update_location();
update_frame();
set_dirty();
};
};
button_mangps.on_select = [this, &nav](Button&) {
nav.push<GeoMapView>(
0,
GeoPos::alt_unit::METERS,
GeoPos::spd_unit::HIDDEN,
beacon_params.location.latitude,
beacon_params.location.longitude,
[this](int32_t, float lat, float lon, int32_t) {
beacon_params.location.latitude = lat;
beacon_params.location.longitude = lon;
init_from_decimal(beacon_params.location);
// Update locator field
update_location();
update_frame();
set_dirty();
});
};
field_am_frequency.on_change = [this](rf::Frequency freq) {
am_frequency = freq;
if (transmitting && !epirb_tx_message.mode_bpsk && am_enabled)
// Update transmitter frequency
transmitter_model.set_target_frequency(am_frequency);
};
// Load available beacons from BEACONS.TXT files (or default).
load_beacons();
// Setup options_frame content with loaded beacons
using option_t = std::pair<std::string, int32_t>;
using options_t = std::vector<option_t>;
options_t entries;
for (const auto& beacon : beacons)
entries.emplace_back(beacon.title, entries.size());
options_frame.set_options(std::move(entries));
if (selected_beacon >= beacons.size())
// BEACONS.TXT file has changed since last launch, default index to 0
selected_beacon = 0;
options_frame.set_selected_index(selected_beacon);
options_frame.on_change = [this](size_t index, OptionsField::value_t) {
selected_beacon = index;
update_frame();
set_dirty();
};
// Init frame content / baseband param with currently setup beacon
update_frame(false);
checkbox_loop.on_select = [this](Checkbox&, bool v) {
loop_enabled = v;
};
field_delay.on_change = [this](int32_t v) {
delay = v;
};
checkbox_am.on_select = [this](Checkbox&, bool v) {
beacon_params.has_121_5 = v;
am_enabled = v;
if (!mode_file) update_frame(false);
};
// AM frequency field edit
field_am_frequency.on_edit = [this, &nav]() {
auto new_view = nav.push<FrequencyKeypadView>(field_am_frequency.value());
new_view->on_changed = [this](rf::Frequency f) {
field_am_frequency.set_value(f);
};
};
checkbox_send_on_change.on_select = [this](Checkbox&, bool v) {
send_on_change = v;
};
tx_view.on_edit_frequency = [this, &nav]() {
auto new_view = nav.push<FrequencyKeypadView>(transmitter_model.target_frequency());
new_view->on_changed = [this](rf::Frequency f) {
transmitter_model.set_target_frequency(f);
bpsk_frequency = f;
};
};
tx_view.on_start = [this]() {
start_tx();
};
tx_view.on_stop = [this]() {
stop_tx();
};
button_tx.on_select = [this](Button&) {
if (!transmitting)
start_tx();
else
stop_tx();
};
}
/**
* Load beacons from /EPIRB/BEACONS.TXT file on sd card
*/
void EPIRBTXAppView::load_beacons() {
File beacons_file;
auto error = beacons_file.open(epirb_dir / u"BEACONS.TXT");
beacons.clear();
if (!error) {
// BEACONS.TXT file exists
auto reader = FileLineReader(beacons_file);
for (const auto& line : reader) {
// Skip comment lines
if (line.length() == 0 || line[0] == '#')
continue;
// Split line with ';' separator
auto cols = split_string(line, ';');
if (cols.size() != 3)
continue;
// Read current beacon
Beacon beacon{};
beacon.title = trim(cols[0]);
beacon.description = trim(cols[1]);
// Make sure frame is not longer tha 18 bytes / 36 hex character
beacon.frame = trim(cols[2]).substr(0, 36);
size_t size = beacon.frame.size();
if (size <= 0)
continue; // Invalid line.
// Beacon is valid, add it to the list
beacons.emplace_back(std::move(beacon));
}
}
if (beacons.empty()) {
// No beacons file or empty flile: just add default beacon
beacons.push_back(default_beacon);
}
}
} // namespace ui::external_app::epirb_tx
+332
View File
@@ -0,0 +1,332 @@
/*
* 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
namespace ui::external_app::epirb_tx {
enum class BeaconType {
EPIRB = 0,
ELT = 1,
PLB = 2
};
enum class BeaconProtocol {
USER = 0,
STANDARD = 1,
NATIONAL = 2
};
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_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{121500000};
// Frequency of the 406 MHz BPSK signal
rf::Frequency bpsk_frequency{406025000};
// True when using a beacon from the BEACONS.TXT file
bool mode_file{true};
// 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{false};
// 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{0};
// Currently selected beacon protocol
uint8_t beacon_protocol{0};
// 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},
{"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 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(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", 0},
{"ELT", 1},
{"PLB", 2}}};
OptionsField options_beacon_protocol{
{UI_POS_X(9 + 7), UI_POS_Y(1)},
30,
{{"User", 0},
{"Standard", 1},
{"National", 2}}};
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)", 0},
{"Manual (Editor)", 1}}};
// 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;
// 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__*/
+27 -2
View File
@@ -107,6 +107,10 @@ set(EXTCPPSRC
external/same_tx/main.cpp
external/same_tx/ui_same_tx.cpp
#mdc_tx
external/mdc_tx/main.cpp
external/mdc_tx/ui_mdc_tx.cpp
#sstvrx
external/sstvrx/main.cpp
external/sstvrx/ui_sstvrx.cpp
@@ -260,6 +264,10 @@ set(EXTCPPSRC
external/epirb_rx/main.cpp
external/epirb_rx/ui_epirb_rx.cpp
#epirb_tx
external/epirb_tx/main.cpp
external/epirb_tx/ui_epirb_tx.cpp
#soundboard 272byte - 1236 bytes
external/soundboard/main.cpp
external/soundboard/soundboard_app.cpp
@@ -301,8 +309,8 @@ set(EXTCPPSRC
external/morseradiotx/main.cpp
external/morseradiotx/ui_morse_radiotx.cpp
external/keeloqtx/main.cpp
external/keeloqtx/ui_keeloqtx.cpp
external/keeloqtx/main.cpp
external/keeloqtx/ui_keeloqtx.cpp
#rtty_rx
external/rtty_rx/main.cpp
external/rtty_rx/ui_rtty_rx.cpp
@@ -320,6 +328,18 @@ set(EXTCPPSRC
#time_sink
external/time_sink/main.cpp
external/time_sink/ui_time_sink.cpp
#kiss_tnc
external/kiss_tnc/main.cpp
external/kiss_tnc/ui_kiss_tnc.cpp
#fpv_detect
external/fpv_detect/main.cpp
external/fpv_detect/ui_fpv_detect.cpp
#p25_tx
external/p25_tx/main.cpp
external/p25_tx/ui_p25_tx.cpp
)
set(EXTAPPLIST
@@ -348,6 +368,7 @@ set(EXTAPPLIST
#morse_tx
sstvtx
same_tx
mdc_tx
sstvrx
random_password
acars_rx
@@ -380,11 +401,13 @@ set(EXTAPPLIST
gfxeq
waterfall_designer
detector_rx
fpv_detect
spaceinv
blackjack
battleship
ert
epirb_rx
epirb_tx
soundboard
game2048
bht_tx
@@ -400,6 +423,8 @@ set(EXTAPPLIST
rtty_tx
pocsag_tx
time_sink
kiss_tnc
p25_tx
)
# sdusb has type conflicts with PRALINE (HackRF Pro) - add only for non-PRALINE builds
+134 -102
View File
@@ -23,85 +23,89 @@ MEMORY
* Also need to consider processor memory map - reading 0xADxxxxxx generates a fault which may be better than unexpected behavior.
* External app address ranges below must match those in python file "external_app_info.py".
*/
ram_external_app_afsk_rx (rwx) : org = 0xADB10000, len = 32k
ram_external_app_calculator (rwx) : org = 0xADB20000, len = 32k
ram_external_app_font_viewer(rwx) : org = 0xADB30000, len = 32k
ram_external_app_blespam (rwx) : org = 0xADB40000, len = 32k
ram_external_app_analogtv (rwx) : org = 0xADB50000, len = 32k
ram_external_app_nrf_rx (rwx) : org = 0xADB60000, len = 32k
ram_external_app_coasterp (rwx) : org = 0xADB70000, len = 32k
ram_external_app_lge (rwx) : org = 0xADB80000, len = 32k
ram_external_app_lcr (rwx) : org = 0xADB90000, len = 32k
ram_external_app_jammer (rwx) : org = 0xADBA0000, len = 32k
ram_external_app_gpssim (rwx) : org = 0xADBB0000, len = 32k
ram_external_app_spainter (rwx) : org = 0xADBC0000, len = 32k
ram_external_app_keyfob (rwx) : org = 0xADBD0000, len = 32k
ram_external_app_tetris (rwx) : org = 0xADBE0000, len = 32k
ram_external_app_extsensors (rwx) : org = 0xADBF0000, len = 32k
ram_external_app_foxhunt_rx (rwx) : org = 0xADC00000, len = 32k
ram_external_app_audio_test (rwx) : org = 0xADC10000, len = 32k
ram_external_app_wardrivemap(rwx) : org = 0xADC20000, len = 32k
ram_external_app_tpmsrx (rwx) : org = 0xADC30000, len = 32k
ram_external_app_protoview (rwx) : org = 0xADC40000, len = 32k
ram_external_app_adsbtx (rwx) : org = 0xADC50000, len = 32k
ram_external_app_morse_tx (rwx) : org = 0xADC60000, len = 32k
ram_external_app_sstvtx (rwx) : org = 0xADC70000, len = 32k
ram_external_app_random_password(rwx): org = 0xADC80000, len = 32k
ram_external_app_acars_rx (rwx) : org = 0xADC90000, len = 32k
ram_external_app_shoppingcart_lock(rwx): org = 0xADCA0000, len = 32k
ram_external_app_cvs_spam (rwx) : org = 0xADCB0000, len = 32k
ram_external_app_ookbrute (rwx) : org = 0xADCC0000, len = 32k
ram_external_app_flippertx (rwx) : org = 0xADCD0000, len = 32k
ram_external_app_ook_editor (rwx) : org = 0xADCE0000, len = 32k
ram_external_app_remote (rwx) : org = 0xADCF0000, len = 32k
ram_external_app_mcu_temperature(rwx): org = 0xADD00000, len = 32k
ram_external_app_fmradio (rwx) : org = 0xADD10000, len = 32k
ram_external_app_tuner (rwx) : org = 0xADD20000, len = 32k
ram_external_app_metronome (rwx) : org = 0xADD30000, len = 32k
ram_external_app_app_manager(rwx) : org = 0xADD40000, len = 32k
ram_external_app_hopper (rwx) : org = 0xADD50000, len = 32k
ram_external_app_antenna_length(rwx): org = 0xADD60000, len = 32k
ram_external_app_view_wav (rwx) : org = 0xADD70000, len = 32k
ram_external_app_sd_wipe (rwx) : org = 0xADD80000, len = 32k
ram_external_app_playlist_editor(rwx): org = 0xADD90000, len = 32k
ram_external_app_snake (rwx) : org = 0xADDA0000, len = 32k
ram_external_app_stopwatch (rwx) : org = 0xADDB0000, len = 32k
ram_external_app_wefax_rx (rwx) : org = 0xADDC0000, len = 32k
ram_external_app_breakout (rwx) : org = 0xADDD0000, len = 32k
ram_external_app_doom (rwx) : org = 0xADDE0000, len = 32k
ram_external_app_debug_pmem (rwx) : org = 0xADDF0000, len = 32k
ram_external_app_scanner (rwx) : org = 0xADE00000, len = 32k
ram_external_app_level (rwx) : org = 0xADE10000, len = 32k
ram_external_app_gfxeq (rwx) : org = 0xADE20000, len = 32k
ram_external_app_noaaapt_rx (rwx) : org = 0xADE30000, len = 32k
ram_external_app_detector_rx (rwx) : org = 0xADE40000, len = 32k
ram_external_app_dinogame (rwx) : org = 0xADE50000, len = 32k
ram_external_app_spaceinv (rwx) : org = 0xADE60000, len = 32k
ram_external_app_blackjack (rwx) : org = 0xADE70000, len = 32k
ram_external_app_battleship (rwx) : org = 0xADE80000, len = 32k
ram_external_app_ert (rwx) : org = 0xADE90000, len = 32k
ram_external_app_epirb_rx (rwx) : org = 0xADEA0000, len = 32k
ram_external_app_soundboard (rwx) : org = 0xADEB0000, len = 32k
ram_external_app_game2048 (rwx) : org = 0xADEC0000, len = 32k
ram_external_app_bht_tx (rwx) : org = 0xADED0000, len = 32k
ram_external_app_morse_practice (rwx) : org = 0xADEE0000, len = 32k
ram_external_app_adult_toys_controller (rwx) : org = 0xADEF0000, len = 32k
ram_external_app_flex_rx (rwx) : org = 0xADF00000, len = 32k
ram_external_app_sstvrx (rwx) : org = 0xADF10000, len = 32k
ram_external_app_subcarrx (rwx) : org = 0xADF20000, len = 32k
ram_external_app_siggen (rwx) : org = 0xADF30000, len = 32k
ram_external_app_sdusb (rwx) : org = 0xADF40000, len = 32k
ram_external_app_morse_radio (rwx) : org = 0xADF50000, len = 32k
ram_external_app_waterfall_designer (rwx) : org = 0xADF60000, len = 32k
ram_external_app_morseradiotx (rwx) : org = 0xADF70000, len = 32k
ram_external_app_keeloqtx (rwx) : org = 0xADF80000, len = 32k
ram_external_app_rtty_rx (rwx) : org = 0xADF90000, len = 32k
ram_external_app_rtty_tx (rwx) : org = 0xADFA0000, len = 32k
ram_external_app_tpmstx (rwx) : org = 0xADFB0000, len = 32k
ram_external_app_pocsag_tx (rwx) : org = 0xADFC0000, len = 32k
ram_external_app_time_sink (rwx) : org = 0xADFD0000, len = 32k
ram_external_app_same_tx (rwx) : org = 0xADFE0000, len = 32k
ram_external_app_afsk_rx (rwx) : org = 0xADB10000, len = 32k
ram_external_app_calculator (rwx) : org = 0xADB20000, len = 32k
ram_external_app_font_viewer (rwx) : org = 0xADB30000, len = 32k
ram_external_app_blespam (rwx) : org = 0xADB40000, len = 32k
ram_external_app_analogtv (rwx) : org = 0xADB50000, len = 32k
ram_external_app_nrf_rx (rwx) : org = 0xADB60000, len = 32k
ram_external_app_coasterp (rwx) : org = 0xADB70000, len = 32k
ram_external_app_lge (rwx) : org = 0xADB80000, len = 32k
ram_external_app_lcr (rwx) : org = 0xADB90000, len = 32k
ram_external_app_jammer (rwx) : org = 0xADBA0000, len = 32k
ram_external_app_gpssim (rwx) : org = 0xADBB0000, len = 32k
ram_external_app_spainter (rwx) : org = 0xADBC0000, len = 32k
ram_external_app_keyfob (rwx) : org = 0xADBD0000, len = 32k
ram_external_app_tetris (rwx) : org = 0xADBE0000, len = 32k
ram_external_app_extsensors (rwx) : org = 0xADBF0000, len = 32k
ram_external_app_foxhunt_rx (rwx) : org = 0xADC00000, len = 32k
ram_external_app_audio_test (rwx) : org = 0xADC10000, len = 32k
ram_external_app_wardrivemap (rwx) : org = 0xADC20000, len = 32k
ram_external_app_tpmsrx (rwx) : org = 0xADC30000, len = 32k
ram_external_app_protoview (rwx) : org = 0xADC40000, len = 32k
ram_external_app_adsbtx (rwx) : org = 0xADC50000, len = 32k
ram_external_app_morse_tx (rwx) : org = 0xADC60000, len = 32k
ram_external_app_sstvtx (rwx) : org = 0xADC70000, len = 32k
ram_external_app_random_password (rwx) : org = 0xADC80000, len = 32k
ram_external_app_acars_rx (rwx) : org = 0xADC90000, len = 32k
ram_external_app_shoppingcart_lock (rwx) : org = 0xADCA0000, len = 32k
ram_external_app_cvs_spam (rwx) : org = 0xADCB0000, len = 32k
ram_external_app_ookbrute (rwx) : org = 0xADCC0000, len = 32k
ram_external_app_flippertx (rwx) : org = 0xADCD0000, len = 32k
ram_external_app_ook_editor (rwx) : org = 0xADCE0000, len = 32k
ram_external_app_remote (rwx) : org = 0xADCF0000, len = 32k
ram_external_app_mcu_temperature (rwx) : org = 0xADD00000, len = 32k
ram_external_app_fmradio (rwx) : org = 0xADD10000, len = 32k
ram_external_app_tuner (rwx) : org = 0xADD20000, len = 32k
ram_external_app_metronome (rwx) : org = 0xADD30000, len = 32k
ram_external_app_app_manager (rwx) : org = 0xADD40000, len = 32k
ram_external_app_hopper (rwx) : org = 0xADD50000, len = 32k
ram_external_app_antenna_length (rwx) : org = 0xADD60000, len = 32k
ram_external_app_view_wav (rwx) : org = 0xADD70000, len = 32k
ram_external_app_sd_wipe (rwx) : org = 0xADD80000, len = 32k
ram_external_app_playlist_editor (rwx) : org = 0xADD90000, len = 32k
ram_external_app_snake (rwx) : org = 0xADDA0000, len = 32k
ram_external_app_stopwatch (rwx) : org = 0xADDB0000, len = 32k
ram_external_app_wefax_rx (rwx) : org = 0xADDC0000, len = 32k
ram_external_app_breakout (rwx) : org = 0xADDD0000, len = 32k
ram_external_app_doom (rwx) : org = 0xADDE0000, len = 32k
ram_external_app_debug_pmem (rwx) : org = 0xADDF0000, len = 32k
ram_external_app_scanner (rwx) : org = 0xADE00000, len = 32k
ram_external_app_level (rwx) : org = 0xADE10000, len = 32k
ram_external_app_gfxeq (rwx) : org = 0xADE20000, len = 32k
ram_external_app_noaaapt_rx (rwx) : org = 0xADE30000, len = 32k
ram_external_app_detector_rx (rwx) : org = 0xADE40000, len = 32k
ram_external_app_dinogame (rwx) : org = 0xADE50000, len = 32k
ram_external_app_spaceinv (rwx) : org = 0xADE60000, len = 32k
ram_external_app_blackjack (rwx) : org = 0xADE70000, len = 32k
ram_external_app_battleship (rwx) : org = 0xADE80000, len = 32k
ram_external_app_ert (rwx) : org = 0xADE90000, len = 32k
ram_external_app_epirb_rx (rwx) : org = 0xADEA0000, len = 32k
ram_external_app_soundboard (rwx) : org = 0xADEB0000, len = 32k
ram_external_app_game2048 (rwx) : org = 0xADEC0000, len = 32k
ram_external_app_bht_tx (rwx) : org = 0xADED0000, len = 32k
ram_external_app_morse_practice (rwx) : org = 0xADEE0000, len = 32k
ram_external_app_adult_toys_controller (rwx) : org = 0xADEF0000, len = 32k
ram_external_app_flex_rx (rwx) : org = 0xADF00000, len = 32k
ram_external_app_sstvrx (rwx) : org = 0xADF10000, len = 32k
ram_external_app_subcarrx (rwx) : org = 0xADF20000, len = 32k
ram_external_app_siggen (rwx) : org = 0xADF30000, len = 32k
ram_external_app_sdusb (rwx) : org = 0xADF40000, len = 32k
ram_external_app_morse_radio (rwx) : org = 0xADF50000, len = 32k
ram_external_app_waterfall_designer (rwx) : org = 0xADF60000, len = 32k
ram_external_app_morseradiotx (rwx) : org = 0xADF70000, len = 32k
ram_external_app_keeloqtx (rwx) : org = 0xADF80000, len = 32k
ram_external_app_rtty_rx (rwx) : org = 0xADF90000, len = 32k
ram_external_app_rtty_tx (rwx) : org = 0xADFA0000, len = 32k
ram_external_app_tpmstx (rwx) : org = 0xADFB0000, len = 32k
ram_external_app_pocsag_tx (rwx) : org = 0xADFC0000, len = 32k
ram_external_app_time_sink (rwx) : org = 0xADFD0000, len = 32k
ram_external_app_same_tx (rwx) : org = 0xADFE0000, len = 32k
ram_external_app_kiss_tnc (rwx) : org = 0xADFF0000, len = 32k
ram_external_app_mdc_tx (rwx) : org = 0xAE000000, len = 32k
ram_external_app_epirb_tx (rwx) : org = 0xAE010000, len = 32k
ram_external_app_fpv_detect (rwx) : org = 0xAE020000, len = 32k
ram_external_app_p25_tx (rwx) : org = 0xAE030000, len = 32k
}
SECTIONS
@@ -142,7 +146,7 @@ SECTIONS
*(*ui*external_app*nrf_rx*);
} > ram_external_app_nrf_rx
.external_app_coasterp : ALIGN(4) SUBALIGN(4)
.external_app_coasterp : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_coasterp.application_information));
*(*ui*external_app*coasterp*);
@@ -190,61 +194,61 @@ SECTIONS
*(*ui*external_app*tetris*);
} > ram_external_app_tetris
.external_app_extsensors : ALIGN(4) SUBALIGN(4)
.external_app_extsensors : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_extsensors.application_information));
*(*ui*external_app*extsensors*);
} > ram_external_app_extsensors
.external_app_foxhunt_rx : ALIGN(4) SUBALIGN(4)
.external_app_foxhunt_rx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_foxhunt_rx.application_information));
*(*ui*external_app*foxhunt_rx*);
} > ram_external_app_foxhunt_rx
.external_app_audio_test : ALIGN(4) SUBALIGN(4)
.external_app_audio_test : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_audio_test.application_information));
*(*ui*external_app*audio_test*);
} > ram_external_app_audio_test
.external_app_wardrivemap : ALIGN(4) SUBALIGN(4)
.external_app_wardrivemap : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_wardrivemap.application_information));
*(*ui*external_app*wardrivemap*);
} > ram_external_app_wardrivemap
.external_app_tpmsrx : ALIGN(4) SUBALIGN(4)
.external_app_tpmsrx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_tpmsrx.application_information));
*(*ui*external_app*tpmsrx*);
} > ram_external_app_tpmsrx
.external_app_tpmstx : ALIGN(4) SUBALIGN(4)
.external_app_tpmstx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_tpmstx.application_information));
*(*ui*external_app*tpmstx*);
} > ram_external_app_tpmstx
.external_app_protoview : ALIGN(4) SUBALIGN(4)
.external_app_protoview : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_protoview.application_information));
*(*ui*external_app*protoview*);
} > ram_external_app_protoview
.external_app_adsbtx : ALIGN(4) SUBALIGN(4)
.external_app_adsbtx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_adsbtx.application_information));
*(*ui*external_app*adsbtx*);
} > ram_external_app_adsbtx
.external_app_morse_tx : ALIGN(4) SUBALIGN(4)
.external_app_morse_tx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_morse_tx.application_information));
*(*ui*external_app*morse_tx*);
} > ram_external_app_morse_tx
.external_app_sstvtx : ALIGN(4) SUBALIGN(4)
.external_app_sstvtx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_sstvtx.application_information));
*(*ui*external_app*sstvtx*);
@@ -262,19 +266,19 @@ SECTIONS
*(*ui*external_app*acars_rx*);
} > ram_external_app_acars_rx
.external_app_shoppingcart_lock : ALIGN(4) SUBALIGN(4)
.external_app_shoppingcart_lock : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_shoppingcart_lock.application_information));
*(*ui*external_app*shoppingcart_lock*);
} > ram_external_app_shoppingcart_lock
.external_app_cvs_spam : ALIGN(4) SUBALIGN(4)
.external_app_cvs_spam : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_cvs_spam.application_information));
*(*ui*external_app*cvs_spam*);
} > ram_external_app_cvs_spam
.external_app_ookbrute : ALIGN(4) SUBALIGN(4)
.external_app_ookbrute : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_ookbrute.application_information));
*(*ui*external_app*ookbrute*);
@@ -286,7 +290,7 @@ SECTIONS
*(*ui*external_app*ook_editor*);
} > ram_external_app_ook_editor
.external_app_flippertx : ALIGN(4) SUBALIGN(4)
.external_app_flippertx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_flippertx.application_information));
*(*ui*external_app*flippertx*);
@@ -370,13 +374,13 @@ SECTIONS
*(*ui*external_app*stopwatch*);
} > ram_external_app_stopwatch
.external_app_wefax_rx : ALIGN(4) SUBALIGN(4)
.external_app_wefax_rx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_wefax_rx.application_information));
*(*ui*external_app*wefax_rx*);
} > ram_external_app_wefax_rx
.external_app_noaaapt_rx : ALIGN(4) SUBALIGN(4)
.external_app_noaaapt_rx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_noaaapt_rx.application_information));
*(*ui*external_app*noaaapt_rx*);
@@ -478,14 +482,12 @@ SECTIONS
*(*ui*external_app*game2048*);
} > ram_external_app_game2048
.external_app_bht_tx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_bht_tx.application_information));
*(*ui*external_app*bht_tx*);
} > ram_external_app_bht_tx
.external_app_morse_practice : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_morse_practice.application_information));
@@ -502,7 +504,7 @@ SECTIONS
{
KEEP(*(.external_app.app_flex_rx.application_information));
*(*ui*external_app*flex_rx*);
} > ram_external_app_flex_rx
} > ram_external_app_flex_rx
.external_app_sstvrx : ALIGN(4) SUBALIGN(4)
{
@@ -522,12 +524,12 @@ SECTIONS
*(*ui*external_app*siggen*);
} > ram_external_app_siggen
.external_app_sdusb : ALIGN(4) SUBALIGN(4)
.external_app_sdusb : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_sdusb.application_information));
*(*ui*external_app*sdusb*);
} > ram_external_app_sdusb
.external_app_morse_radio : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_morse_radio.application_information));
@@ -545,6 +547,7 @@ SECTIONS
KEEP(*(.external_app.app_keeloqtx.application_information));
*(*ui*external_app*keeloqtx*);
} > ram_external_app_keeloqtx
.external_app_rtty_rx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_rtty_rx.application_information));
@@ -574,5 +577,34 @@ SECTIONS
KEEP(*(.external_app.app_same_tx.application_information));
*(*ui*external_app*same_tx*);
} > ram_external_app_same_tx
}
.external_app_kiss_tnc : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_kiss_tnc.application_information));
*(*ui*external_app*kiss_tnc*);
} > ram_external_app_kiss_tnc
.external_app_mdc_tx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_mdc_tx.application_information));
*(*ui*external_app*mdc_tx*);
} > ram_external_app_mdc_tx
.external_app_epirb_tx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_epirb_tx.application_information));
*(*ui*external_app*epirb_tx*);
} > ram_external_app_epirb_tx
.external_app_fpv_detect : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_fpv_detect.application_information));
*(*ui*external_app*fpv_detect*);
} > ram_external_app_fpv_detect
.external_app_p25_tx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_p25_tx.application_information));
*(*ui*external_app*p25_tx*);
} > ram_external_app_p25_tx
}
+1 -1
View File
@@ -73,7 +73,7 @@ __attribute__((section(".external_app.app_extsensors.application_information"),
0xF0,
0x0F,
},
/*.icon_color = */ ui::Color::orange().v,
/*.icon_color = */ ui::Color::yellow().v,
/*.menu_location = */ app_location_t::DEBUG,
/*.desired_menu_position = */ -1,
+1 -1
View File
@@ -73,7 +73,7 @@ __attribute__((section(".external_app.app_flex_rx.application_information"), use
0x00,
0x00,
},
/*.icon_color = */ ui::Color::orange().v,
/*.icon_color = */ ui::Color::yellow().v,
/*.menu_location = */ app_location_t::RX,
/*.desired_menu_position = */ -1,
+1 -1
View File
@@ -73,7 +73,7 @@ __attribute__((section(".external_app.app_flippertx.application_information"), u
0xE0,
0x07,
},
/*.icon_color = */ ui::Color::orange().v,
/*.icon_color = */ ui::Color::yellow().v,
/*.menu_location = */ app_location_t::TX,
/*.desired_menu_position = */ -1,
+65
View File
@@ -0,0 +1,65 @@
// Author: berkeozkir (Berke Özkır)
#include "ui.hpp"
#include "ui_fpv_detect.hpp"
#include "ui_navigation.hpp"
#include "external_app.hpp"
namespace ui::external_app::fpv_detect {
void initialize_app(ui::NavigationView& nav) {
nav.push<FpvDetectView>();
}
} // namespace ui::external_app::fpv_detect
extern "C" {
__attribute__((section(".external_app.app_fpv_detect.application_information"), used)) application_information_t _application_information_fpv_detect = {
/*.memory_location = */ (uint8_t*)0x00000000,
/*.externalAppEntry = */ ui::external_app::fpv_detect::initialize_app,
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
/*.app_name = */ "FPV DETECT",
/*.bitmap_data = */
{
0x00,
0x00,
0xC0,
0x03,
0x20,
0x04,
0x10,
0x08,
0x48,
0x12,
0xA4,
0x25,
0x54,
0x2A,
0xA8,
0x15,
0x54,
0x2A,
0xA4,
0x25,
0x48,
0x12,
0x10,
0x08,
0x20,
0x04,
0xC0,
0x03,
0x00,
0x00,
0x00,
0x00,
},
/*.icon_color = */ ui::Color::orange().v,
/*.menu_location = */ app_location_t::RX,
/*.desired_menu_position = */ -1,
/*.m4_app_tag = portapack::spi_flash::image_tag_wfm_audio */ {'P', 'W', 'F', 'M'},
/*.m4_app_offset = */ 0x00000000,
};
}
@@ -0,0 +1,666 @@
// Author: berkeozkir (Berke Özkır)
/*
* FPV RX how frequency search and lock work
*
* 1. Power metric: We use channelized power (baseband IQ magnitude² in the
* capture bandwidth), not raw RF RSSI. Stats come from ChannelStatsCollector
* over filtered IQ, so we see power in the tuned channel, not wideband.
*
* 2. Scanning: We step through FPV bands/channels (A/B/E/F/R, 8 ch each). When
* power on the current channel exceeds the detect threshold, we enter
* "Candidate" and run verification we do not lock on a single peak.
*
* 3. Verification (making sure the drone is on that freq):
* - Multiple samples: verify_hits / verify_misses over several updates.
* - Confidence score: combines average above threshold, peak margin,
* hit count, and crucially neighbor_margin = center power max(adjacent ch).
* - Neighbor check: we only lock if the center channel is at least
* MIN_NEIGHBOR_MARGIN_FOR_LOCK_DB above both adjacent channels. That
* avoids locking on wide/noise that looks strong on many channels.
* During Candidate we re-sample left/right adjacent channels so the
* margin is based on current measurements, not stale scan data.
* - Lock requires: enough hits, confidence LOCK_CONFIDENCE_MIN, and
* center stronger than neighbors (strong lock path has same requirement).
*
* 4. Lock hold: Once locked, we only unlock when power stays below
* unlock_threshold_db() long enough to drain lock_hold_ (down from
* LOCK_HOLD_MAX), so brief fades don't drop lock.
*/
#include "ui_fpv_detect.hpp"
#include <cstdio>
#include "baseband_api.hpp"
#include "oversample.hpp"
using namespace portapack;
namespace ui::external_app::fpv_detect {
static constexpr uint8_t SCAN_DWELL_FRAMES = 1;
static constexpr uint8_t VERIFY_SAMPLE_TARGET = 5;
static constexpr uint8_t VERIFY_MIN_HITS = 4;
static constexpr uint8_t STRONG_LOCK_PEAK_MARGIN_DB = 8;
static constexpr uint8_t LOCK_HOLD_MAX = 12;
static constexpr uint8_t LOCK_CONFIDENCE_MIN = 72;
/** Center channel must be at least this many dB above adjacent channels to lock.
* Ensures we lock on a real FPV carrier (narrow peak) rather than wide/noise. */
static constexpr int32_t MIN_NEIGHBOR_MARGIN_FOR_LOCK_DB = 3;
static constexpr uint8_t BEEP_GUARD_FRAMES = 8;
int32_t FpvDetectView::clamp_value(int32_t value, int32_t low, int32_t high) {
if (value < low) return low;
if (value > high) return high;
return value;
}
int32_t FpvDetectView::map_value(int32_t value, int32_t from_low, int32_t from_high, int32_t to_low, int32_t to_high) {
const auto clamped = clamp_value(value, from_low, from_high);
return to_low + (clamped - from_low) * (to_high - to_low) / (from_high - from_low);
}
void FpvDetectView::focus() {
field_lna.focus();
}
FpvDetectView::~FpvDetectView() {
shared_memory.request_m4_performance_counter = 0;
receiver_model.disable();
audio::output::stop();
baseband::shutdown();
}
uint8_t FpvDetectView::channel_index(uint8_t band, uint8_t ch) const {
return static_cast<uint8_t>(band * FPV_CHANNELS_PER_BAND + ch);
}
uint8_t FpvDetectView::current_channel_index() const {
return channel_index(scan_band, scan_ch);
}
int16_t FpvDetectView::detect_threshold_db() const {
return static_cast<int16_t>(detect_threshold_db_);
}
int16_t FpvDetectView::unlock_threshold_db() const {
return static_cast<int16_t>(detect_threshold_db() - 6);
}
int16_t FpvDetectView::candidate_average_db() const {
if (!verify_samples_) {
return -120;
}
return static_cast<int16_t>(verify_sum_db_ / verify_samples_);
}
int16_t FpvDetectView::neighbor_best_db() const {
int16_t best = -120;
if (candidate_ch_ > 0) {
const auto& left = channel_memory_[channel_index(candidate_band_, static_cast<uint8_t>(candidate_ch_ - 1))];
if (left.last_db > best) {
best = left.last_db;
}
}
if ((candidate_ch_ + 1) < FPV_CHANNELS_PER_BAND) {
const auto& right = channel_memory_[channel_index(candidate_band_, static_cast<uint8_t>(candidate_ch_ + 1))];
if (right.last_db > best) {
best = right.last_db;
}
}
return best;
}
void FpvDetectView::request_event_beep(uint32_t hz, uint32_t duration_ms) {
if ((frame_counter_ - last_beep_frame_) < BEEP_GUARD_FRAMES) {
return;
}
last_beep_frame_ = frame_counter_;
baseband::request_audio_beep(hz, 24000, duration_ms);
}
void FpvDetectView::retune_to(uint8_t band, uint8_t ch) {
scan_band = band;
scan_ch = ch;
freq_ = fpv_frequencies[band][ch];
receiver_model.set_target_frequency(freq_);
update_freq_display();
}
void FpvDetectView::step_scan() {
if (band_mode < FPV_NUM_BANDS) {
scan_band = band_mode;
scan_ch = static_cast<uint8_t>((scan_ch + 1) % FPV_CHANNELS_PER_BAND);
} else {
scan_ch = static_cast<uint8_t>(scan_ch + 1);
if (scan_ch >= FPV_CHANNELS_PER_BAND) {
scan_ch = 0;
scan_band = static_cast<uint8_t>(scan_band + 1);
if (scan_band >= FPV_NUM_BANDS) {
scan_band = 0;
}
}
}
retune_to(scan_band, scan_ch);
scan_dwell_frames_ = SCAN_DWELL_FRAMES;
}
void FpvDetectView::reset_detector(bool retune_current) {
detect_state_ = DetectState::Scanning;
candidate_band_ = scan_band;
candidate_ch_ = scan_ch;
verify_samples_ = 0;
verify_hits_ = 0;
verify_misses_ = 0;
verify_sum_db_ = 0;
verify_peak_db_ = -120;
candidate_confidence_ = 0;
lock_hold_ = 0;
neighbor_phase_ = 0;
neighbors_fresh_ = false;
scan_dwell_frames_ = 0;
update_state_badge();
update_confidence_text();
update_status_text();
update_detail_text();
update_visual_state();
if (retune_current) {
retune_to(scan_band, scan_ch);
scan_dwell_frames_ = SCAN_DWELL_FRAMES;
}
}
bool FpvDetectView::is_possible_analog_carrier(const ChannelStatistics& statistics) const {
return statistics.max_db >= detect_threshold_db();
}
uint8_t FpvDetectView::compute_candidate_confidence() const {
if (!verify_samples_) {
return 0;
}
const int32_t avg_db = candidate_average_db();
const int32_t center_margin = avg_db - detect_threshold_db();
const int32_t peak_margin = verify_peak_db_ - detect_threshold_db();
const int32_t neighbor_margin = avg_db - neighbor_best_db();
int32_t score = 18;
score += center_margin * 7;
score += peak_margin * 4;
score += static_cast<int32_t>(verify_hits_) * 5;
score -= static_cast<int32_t>(verify_misses_) * 10;
if (neighbor_margin >= 6) {
score += 12;
} else if (neighbor_margin >= 3) {
score += 6;
} else if (neighbor_margin <= 0) {
score -= 8;
}
const auto& mem = channel_memory_[channel_index(candidate_band_, candidate_ch_)];
if (mem.hits >= 2) {
score += 4;
}
return static_cast<uint8_t>(clamp_value(score, 0, 99));
}
void FpvDetectView::enter_candidate(const ChannelStatistics& statistics) {
detect_state_ = DetectState::Candidate;
candidate_band_ = scan_band;
candidate_ch_ = scan_ch;
verify_samples_ = 1;
verify_hits_ = 1;
verify_misses_ = 0;
verify_sum_db_ = statistics.max_db;
verify_peak_db_ = statistics.max_db;
candidate_confidence_ = compute_candidate_confidence();
neighbor_phase_ = 0;
neighbors_fresh_ = false;
retune_to(candidate_band_, candidate_ch_);
update_state_badge();
update_confidence_text();
update_status_text();
update_detail_text();
update_visual_state();
request_event_beep(1150, 60);
}
void FpvDetectView::evaluate_candidate_sample(const ChannelStatistics& statistics) {
if (detect_state_ != DetectState::Candidate) {
return;
}
++verify_samples_;
verify_sum_db_ += statistics.max_db;
if (statistics.max_db > verify_peak_db_) {
verify_peak_db_ = statistics.max_db;
}
if (statistics.max_db >= detect_threshold_db()) {
++verify_hits_;
} else {
++verify_misses_;
}
candidate_confidence_ = compute_candidate_confidence();
update_confidence_text();
update_detail_text();
/* Refresh adjacent channel power so neighbor_best_db() uses current
* measurements instead of stale scan data. Do this before any lock decision. */
if (verify_samples_ >= 2 && !neighbors_fresh_) {
if (candidate_ch_ > 0) {
neighbor_phase_ = 1;
retune_to(candidate_band_, static_cast<uint8_t>(candidate_ch_ - 1));
} else {
neighbor_phase_ = 2;
retune_to(candidate_band_, static_cast<uint8_t>(candidate_ch_ + 1));
}
return;
}
/* Lock decisions use neighbor margin; only run when we have fresh neighbor data. */
if (!neighbors_fresh_) {
return;
}
const bool strong_lock =
verify_samples_ >= 3 &&
verify_hits_ >= 3 &&
verify_peak_db_ >= (detect_threshold_db() + STRONG_LOCK_PEAK_MARGIN_DB) &&
candidate_confidence_ >= 80 &&
(candidate_average_db() - neighbor_best_db()) >= MIN_NEIGHBOR_MARGIN_FOR_LOCK_DB;
if (strong_lock) {
enter_lock();
return;
}
if (verify_samples_ < VERIFY_SAMPLE_TARGET) {
return;
}
const bool center_stronger_than_neighbors =
(candidate_average_db() - neighbor_best_db()) >= MIN_NEIGHBOR_MARGIN_FOR_LOCK_DB;
if (verify_hits_ >= VERIFY_MIN_HITS && candidate_confidence_ >= LOCK_CONFIDENCE_MIN &&
center_stronger_than_neighbors) {
enter_lock();
return;
}
detect_state_ = DetectState::Scanning;
candidate_confidence_ = static_cast<uint8_t>(candidate_confidence_ / 2);
verify_samples_ = 0;
verify_hits_ = 0;
verify_misses_ = 0;
verify_sum_db_ = 0;
verify_peak_db_ = -120;
scan_dwell_frames_ = 0;
update_state_badge();
update_confidence_text();
update_status_text();
update_detail_text();
update_visual_state();
}
void FpvDetectView::enter_lock() {
detect_state_ = DetectState::Locked;
lock_hold_ = LOCK_HOLD_MAX;
candidate_confidence_ = static_cast<uint8_t>(clamp_value(candidate_confidence_, LOCK_CONFIDENCE_MIN, 99));
retune_to(candidate_band_, candidate_ch_);
update_state_badge();
update_confidence_text();
update_status_text();
update_detail_text();
update_visual_state();
request_event_beep(1850, 180);
}
void FpvDetectView::update_lock(const ChannelStatistics& statistics) {
if (detect_state_ != DetectState::Locked) {
return;
}
if (statistics.max_db >= unlock_threshold_db()) {
if (lock_hold_ < LOCK_HOLD_MAX) {
++lock_hold_;
}
if (candidate_confidence_ < 99) {
++candidate_confidence_;
}
} else {
if (lock_hold_ > 0) {
--lock_hold_;
}
if (candidate_confidence_ > 0) {
--candidate_confidence_;
}
}
update_confidence_text();
update_detail_text();
if (lock_hold_ > 0) {
return;
}
detect_state_ = DetectState::Scanning;
candidate_confidence_ = 0;
verify_samples_ = 0;
verify_hits_ = 0;
verify_misses_ = 0;
verify_sum_db_ = 0;
verify_peak_db_ = -120;
scan_dwell_frames_ = 0;
update_state_badge();
update_confidence_text();
update_status_text();
update_detail_text();
update_visual_state();
request_event_beep(650, 90);
}
void FpvDetectView::update_freq_display() {
char buf[28];
std::snprintf(buf, sizeof(buf), "%c%d %ld MHz",
band_labels[scan_band],
static_cast<int>(scan_ch + 1),
static_cast<long>(freq_ / 1000000LL));
text_freq.set(buf);
}
void FpvDetectView::update_state_badge() {
switch (detect_state_) {
case DetectState::Scanning:
text_state.set("SCANNING");
break;
case DetectState::Candidate:
text_state.set("VERIFY FPV");
break;
case DetectState::Locked:
text_state.set("DRONE FOUND");
break;
}
}
void FpvDetectView::update_confidence_text() {
char buf[16];
std::snprintf(buf, sizeof(buf), "Conf %u%%", static_cast<unsigned>(candidate_confidence_));
text_confidence.set(buf);
}
void FpvDetectView::update_status_text() {
char buf[40];
switch (detect_state_) {
case DetectState::Scanning:
if (band_mode < FPV_NUM_BANDS) {
std::snprintf(buf, sizeof(buf), "Scanning band %c for analog FPV", band_labels[band_mode]);
} else {
std::snprintf(buf, sizeof(buf), "Scanning all FPV bands");
}
break;
case DetectState::Candidate:
std::snprintf(buf, sizeof(buf), "VERIFYING %c%d %ld MHz",
band_labels[candidate_band_],
static_cast<int>(candidate_ch_ + 1),
static_cast<long>(fpv_frequencies[candidate_band_][candidate_ch_] / 1000000LL));
break;
case DetectState::Locked:
std::snprintf(buf, sizeof(buf), "!!! DRONE FOUND !!! %c%d %ld",
band_labels[candidate_band_],
static_cast<int>(candidate_ch_ + 1),
static_cast<long>(fpv_frequencies[candidate_band_][candidate_ch_] / 1000000LL));
break;
}
text_status.set(buf);
}
void FpvDetectView::update_detail_text() {
char buf[40];
switch (detect_state_) {
case DetectState::Scanning:
std::snprintf(buf, sizeof(buf), "Need >= %d dB, stable dwell", static_cast<int>(detect_threshold_db()));
break;
case DetectState::Candidate:
std::snprintf(buf, sizeof(buf), "avg %d peak %d hits %u/%u",
static_cast<int>(candidate_average_db()),
static_cast<int>(verify_peak_db_),
static_cast<unsigned>(verify_hits_),
static_cast<unsigned>(verify_samples_));
break;
case DetectState::Locked:
std::snprintf(buf, sizeof(buf), "LOCKED %c%d conf %u%% hold %u",
band_labels[candidate_band_],
static_cast<int>(candidate_ch_ + 1),
static_cast<unsigned>(candidate_confidence_),
static_cast<unsigned>(lock_hold_));
break;
}
text_detail.set(buf);
}
void FpvDetectView::update_visual_state() {
auto* theme = Theme::getInstance();
/*
* Conservative styling for cross-branch compatibility.
* If your branch has explicit blue/red banner theme styles,
* swap the styles in this function only.
*/
auto normal_style = theme->bg_darkest;
auto attention_style = theme->fg_red;
switch (detect_state_) {
case DetectState::Scanning:
text_state.set_style(normal_style);
text_status.set_style(normal_style);
break;
case DetectState::Candidate:
text_state.set_style(normal_style);
text_status.set_style(normal_style);
break;
case DetectState::Locked:
text_state.set_style(attention_style);
text_status.set_style(attention_style);
break;
}
freq_stats_rssi.set_style(normal_style);
freq_stats_db.set_style(normal_style);
text_detail.set_style(normal_style);
text_confidence.set_style(normal_style);
}
void FpvDetectView::update_stats_text(const ChannelStatistics& statistics) {
rssi_graph.add_values(rssi.get_min(), rssi.get_avg(), rssi.get_max(), statistics.max_db);
auto& mem = channel_memory_[current_channel_index()];
mem.last_db = statistics.max_db;
if (statistics.max_db > mem.peak_db) {
mem.peak_db = statistics.max_db;
}
if (statistics.max_db >= detect_threshold_db()) {
if (mem.hits < 255) {
++mem.hits;
}
}
mem.confidence = candidate_confidence_;
if (last_max_db_ != statistics.max_db) {
last_max_db_ = statistics.max_db;
char power_buf[20];
std::snprintf(power_buf, sizeof(power_buf), "PWR %d dB", static_cast<int>(statistics.max_db));
freq_stats_db.set(power_buf);
rssi.set_db(statistics.max_db);
}
const uint8_t graph_min = rssi_graph.get_graph_min();
const uint8_t graph_avg = rssi_graph.get_graph_avg();
const uint8_t graph_max = rssi_graph.get_graph_max();
if (last_min_rssi_ != graph_min || last_avg_rssi_ != graph_avg || last_max_rssi_ != graph_max) {
last_min_rssi_ = graph_min;
last_avg_rssi_ = graph_avg;
last_max_rssi_ = graph_max;
char rssi_buf[22];
std::snprintf(rssi_buf, sizeof(rssi_buf), "RSSI %u/%u/%u",
static_cast<unsigned>(graph_min),
static_cast<unsigned>(graph_avg),
static_cast<unsigned>(graph_max));
freq_stats_rssi.set(rssi_buf);
}
}
void FpvDetectView::on_timer() {
++frame_counter_;
if (detect_state_ != DetectState::Scanning) {
return;
}
if (scan_dwell_frames_ > 0) {
--scan_dwell_frames_;
return;
}
step_scan();
}
FpvDetectView::FpvDetectView(NavigationView& nav)
: nav_{nav} {
add_children({
&labels,
&field_lna,
&field_vga,
&field_rf_amp,
&field_volume,
&field_band,
&text_freq,
&text_state,
&text_confidence,
&text_detect_label,
&field_detect_threshold,
&freq_stats_rssi,
&freq_stats_db,
&text_status,
&text_detail,
&rssi,
&rssi_graph,
});
rssi.set_vertical_rssi(true);
rssi.set_peak(true, 3000);
rssi_graph.set_nb_columns(256);
field_detect_threshold.set_value(detect_threshold_db_);
field_detect_threshold.on_change = [this](int32_t v) {
detect_threshold_db_ = v;
update_detail_text();
};
field_band.on_change = [this](size_t, int32_t value) {
band_mode = static_cast<uint8_t>(value);
scan_band = (band_mode < FPV_NUM_BANDS) ? band_mode : 0;
scan_ch = 0;
reset_detector(true);
};
change_mode();
field_band.set_selected_index(FPV_AUTO_SCAN_MODE);
reset_detector(true);
}
void FpvDetectView::on_statistics_update(const ChannelStatistics& statistics) {
const bool is_sampling_neighbor =
(detect_state_ == DetectState::Candidate && (neighbor_phase_ == 1 || neighbor_phase_ == 2));
if (!is_sampling_neighbor) {
update_stats_text(statistics);
}
switch (detect_state_) {
case DetectState::Scanning:
if (is_possible_analog_carrier(statistics)) {
enter_candidate(statistics);
}
break;
case DetectState::Candidate:
if (neighbor_phase_ == 1) {
const auto idx = channel_index(candidate_band_, static_cast<uint8_t>(candidate_ch_ - 1));
channel_memory_[idx].last_db = statistics.max_db;
if (candidate_ch_ + 1 < FPV_CHANNELS_PER_BAND) {
neighbor_phase_ = 2;
retune_to(candidate_band_, static_cast<uint8_t>(candidate_ch_ + 1));
} else {
neighbor_phase_ = 0;
neighbors_fresh_ = true;
retune_to(candidate_band_, candidate_ch_);
}
break;
}
if (neighbor_phase_ == 2) {
const auto idx = channel_index(candidate_band_, static_cast<uint8_t>(candidate_ch_ + 1));
channel_memory_[idx].last_db = statistics.max_db;
neighbor_phase_ = 0;
neighbors_fresh_ = true;
retune_to(candidate_band_, candidate_ch_);
break;
}
evaluate_candidate_sample(statistics);
break;
case DetectState::Locked:
update_lock(statistics);
break;
}
}
size_t FpvDetectView::change_mode() {
audio::output::stop();
receiver_model.disable();
baseband::shutdown();
audio_sampling_rate = audio::Rate::Hz_24000;
baseband::run_image(portapack::spi_flash::image_tag_capture);
receiver_model.set_modulation(ReceiverModel::Mode::Capture);
baseband::set_sample_rate(FPV_RX_BW, get_oversample_rate(FPV_RX_BW));
auto actual_sampling_rate = get_actual_sample_rate(FPV_RX_BW);
receiver_model.set_sampling_rate(actual_sampling_rate);
receiver_model.set_baseband_bandwidth(filter_bandwidth_for_sampling_rate(actual_sampling_rate));
audio::set_rate(audio_sampling_rate);
audio::output::start();
receiver_model.set_headphone_volume(receiver_model.headphone_volume());
receiver_model.enable();
return 0;
}
} // namespace ui::external_app::fpv_detect
@@ -0,0 +1,200 @@
// Author: berkeozkir (Berke Özkır)
#ifndef _UI_FPV_DETECT
#define _UI_FPV_DETECT
#include <array>
#include <cstdint>
#include "analog_audio_app.hpp"
#include "app_settings.hpp"
#include "audio.hpp"
#include "baseband_api.hpp"
#include "radio_state.hpp"
#include "receiver_model.hpp"
#include "string_format.hpp"
#include "ui.hpp"
#include "ui_receiver.hpp"
#include "ui_spectrum.hpp"
namespace ui::external_app::fpv_detect {
static constexpr uint8_t FPV_NUM_BANDS = 5;
static constexpr uint8_t FPV_CHANNELS_PER_BAND = 8;
static constexpr uint8_t FPV_TOTAL_CHANNELS = FPV_NUM_BANDS * FPV_CHANNELS_PER_BAND;
static constexpr uint8_t FPV_AUTO_SCAN_MODE = FPV_NUM_BANDS;
static constexpr int64_t fpv_frequencies[FPV_NUM_BANDS][FPV_CHANNELS_PER_BAND] = {
/* Band A */ {5865000000LL, 5845000000LL, 5825000000LL, 5805000000LL, 5785000000LL, 5765000000LL, 5745000000LL, 5725000000LL},
/* Band B */ {5733000000LL, 5752000000LL, 5771000000LL, 5790000000LL, 5809000000LL, 5828000000LL, 5847000000LL, 5866000000LL},
/* Band E */ {5705000000LL, 5685000000LL, 5665000000LL, 5645000000LL, 5885000000LL, 5905000000LL, 5925000000LL, 5945000000LL},
/* Band F */ {5740000000LL, 5760000000LL, 5780000000LL, 5800000000LL, 5820000000LL, 5840000000LL, 5860000000LL, 5880000000LL},
/* Band R */ {5658000000LL, 5695000000LL, 5732000000LL, 5769000000LL, 5806000000LL, 5843000000LL, 5880000000LL, 5917000000LL},
};
static constexpr char band_labels[] = {'A', 'B', 'E', 'F', 'R'};
#define FPV_RX_BW 750000
class FpvDetectView : public View {
public:
explicit FpvDetectView(NavigationView& nav);
~FpvDetectView();
void focus() override;
std::string title() const override { return "FPV DETECT"; };
private:
enum class DetectState : uint8_t {
Scanning,
Candidate,
Locked,
};
struct ChannelMemory {
int16_t last_db = -120;
int16_t peak_db = -120;
uint8_t hits = 0;
uint8_t confidence = 0;
};
NavigationView& nav_;
RxRadioState radio_state_{};
static int32_t clamp_value(int32_t value, int32_t low, int32_t high);
static int32_t map_value(int32_t value, int32_t from_low, int32_t from_high, int32_t to_low, int32_t to_high);
size_t change_mode();
void on_statistics_update(const ChannelStatistics& statistics);
void on_timer();
void retune_to(uint8_t band, uint8_t ch);
void step_scan();
void reset_detector(bool retune_current);
bool is_possible_analog_carrier(const ChannelStatistics& statistics) const;
void enter_candidate(const ChannelStatistics& statistics);
void evaluate_candidate_sample(const ChannelStatistics& statistics);
void enter_lock();
void update_lock(const ChannelStatistics& statistics);
uint8_t current_channel_index() const;
uint8_t channel_index(uint8_t band, uint8_t ch) const;
uint8_t compute_candidate_confidence() const;
int16_t detect_threshold_db() const;
int16_t unlock_threshold_db() const;
int16_t candidate_average_db() const;
int16_t neighbor_best_db() const;
void request_event_beep(uint32_t hz, uint32_t duration_ms);
void update_freq_display();
void update_state_badge();
void update_confidence_text();
void update_status_text();
void update_detail_text();
void update_stats_text(const ChannelStatistics& statistics);
void update_visual_state();
DetectState detect_state_ = DetectState::Scanning;
uint8_t scan_band = 0;
uint8_t scan_ch = 0;
uint8_t band_mode = FPV_AUTO_SCAN_MODE;
uint8_t candidate_band_ = 0;
uint8_t candidate_ch_ = 0;
uint8_t verify_samples_ = 0;
uint8_t verify_hits_ = 0;
uint8_t verify_misses_ = 0;
int32_t verify_sum_db_ = 0;
int16_t verify_peak_db_ = -120;
uint8_t candidate_confidence_ = 0;
uint8_t lock_hold_ = 0;
/** During Candidate: 0=center, 1=left neighbor, 2=right neighbor. Used to
* re-sample adjacent channels so neighbor_margin uses current measurements. */
uint8_t neighbor_phase_ = 0;
bool neighbors_fresh_ = false;
uint8_t scan_dwell_frames_ = 0;
uint32_t frame_counter_ = 0;
uint32_t last_beep_frame_ = 0;
int16_t last_max_db_ = -127;
uint8_t last_min_rssi_ = 0;
uint8_t last_avg_rssi_ = 0;
uint8_t last_max_rssi_ = 0;
std::array<ChannelMemory, FPV_TOTAL_CHANNELS> channel_memory_{};
rf::Frequency freq_ = {fpv_frequencies[0][0]};
int32_t detect_threshold_db_ = -38;
audio::Rate audio_sampling_rate = audio::Rate::Hz_48000;
app_settings::SettingsManager settings_{
"rx_fpv",
app_settings::Mode::RX,
{
{"detect_threshold"sv, &detect_threshold_db_},
}};
Labels labels{
{{UI_POS_X(0), UI_POS_Y(0)}, "LNA: VGA: AMP: ", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X_RIGHT(6), UI_POS_Y(0)}, "VOL: ", Theme::getInstance()->fg_light->foreground},
};
LNAGainField field_lna{{UI_POS_X(4), UI_POS_Y(0)}};
VGAGainField field_vga{{UI_POS_X(11), UI_POS_Y(0)}};
RFAmpField field_rf_amp{{UI_POS_X(18), UI_POS_Y(0)}};
AudioVolumeField field_volume{{UI_POS_X_RIGHT(2), UI_POS_Y(0)}};
OptionsField field_band{
{UI_POS_X(0), UI_POS_Y(1)},
9,
{
{"Band A ", 0},
{"Band B ", 1},
{"Band E ", 2},
{"Band F ", 3},
{"Band R ", 4},
{"AutoScan", FPV_AUTO_SCAN_MODE},
}};
Text text_freq{{UI_POS_X_RIGHT(20), UI_POS_Y(1), UI_POS_WIDTH(20), UI_POS_DEFAULT_HEIGHT}, ""};
Text text_state{{UI_POS_X(0), UI_POS_Y(2), UI_POS_WIDTH(12), UI_POS_DEFAULT_HEIGHT}, "SCANNING"};
Text text_confidence{{UI_POS_X(12), UI_POS_Y(2), UI_POS_WIDTH(8), UI_POS_DEFAULT_HEIGHT}, "Conf 0%"};
Text text_detect_label{{UI_POS_X_RIGHT(10), UI_POS_Y(2), UI_POS_WIDTH(5), UI_POS_DEFAULT_HEIGHT}, "Thr>"};
NumberField field_detect_threshold{
{UI_POS_X_RIGHT(5), UI_POS_Y(2)},
4,
{-100, 20},
1,
' ',
};
Text freq_stats_rssi{{UI_POS_X(0), UI_POS_Y(3), UI_POS_WIDTH(15), UI_POS_DEFAULT_HEIGHT}, "RSSI 0/0/0"};
Text freq_stats_db{{UI_POS_X_RIGHT(14), UI_POS_Y(3), UI_POS_WIDTH(14), UI_POS_DEFAULT_HEIGHT}, "PWR -120 dB"};
Text text_status{{UI_POS_X(0), UI_POS_Y(4), UI_POS_WIDTH(30), UI_POS_DEFAULT_HEIGHT}, "SCANNING FOR ANALOG FPV"};
Text text_detail{{UI_POS_X(0), UI_POS_Y(5), UI_POS_WIDTH(30), UI_POS_DEFAULT_HEIGHT}, "Waiting for FPV-like carrier"};
RSSIGraph rssi_graph{{UI_POS_X(0), UI_POS_Y(6), UI_POS_WIDTH_REMAINING(5), UI_POS_HEIGHT_REMAINING(7)}};
RSSI rssi{{UI_POS_X_RIGHT(5), UI_POS_Y(6), UI_POS_WIDTH(5), UI_POS_HEIGHT_REMAINING(7)}};
MessageHandlerRegistration message_handler_stats{
Message::ID::ChannelStatistics,
[this](const Message* const p) {
this->on_statistics_update(static_cast<const ChannelStatisticsMessage*>(p)->statistics);
}};
MessageHandlerRegistration message_handler_frame_sync{
Message::ID::DisplayFrameSync,
[this](const Message* const) {
this->on_timer();
}};
};
} // namespace ui::external_app::fpv_detect
#endif
+84
View File
@@ -0,0 +1,84 @@
/*
* Copyright (C) 2024 Sarah Rose
*
* 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_kiss_tnc.hpp"
#include "ui_navigation.hpp"
#include "external_app.hpp"
namespace ui::external_app::kiss_tnc {
void initialize_app(ui::NavigationView& nav) {
nav.push<KissTncView>();
}
} // namespace ui::external_app::kiss_tnc
extern "C" {
__attribute__((section(".external_app.app_kiss_tnc.application_information"), used)) application_information_t _application_information_kiss_tnc = {
/*.memory_location = */ (uint8_t*)0x00000000,
/*.externalAppEntry = */ ui::external_app::kiss_tnc::initialize_app,
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
/*.app_name = */ "KISS TNC",
/*.bitmap_data = */ {
// 16x16 icon - radio tower / TNC
0x00,
0x00,
0x08,
0x00,
0x14,
0x00,
0x22,
0x00,
0x08,
0x00,
0x08,
0x00,
0x08,
0x00,
0x3E,
0x00,
0x22,
0x00,
0x22,
0x00,
0x22,
0x00,
0x22,
0x00,
0x3E,
0x00,
0x1C,
0x00,
0x08,
0x00,
0x00,
0x00,
},
/*.icon_color = */ ui::Color::green().v,
/*.menu_location = */ app_location_t::TRX,
/*.desired_menu_position = */ -1,
/*.m4_app_tag = portapack::spi_flash::image_tag_aprs_rx */ {'P', 'A', 'P', 'R'},
/*.m4_app_offset = */ 0x00000000,
};
}
+222
View File
@@ -0,0 +1,222 @@
/*
* Copyright (C) 2024 Sarah Rose
*
* 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_kiss_tnc.hpp"
#include "baseband_api.hpp"
#include "portapack.hpp"
#include "receiver_model.hpp"
#include "transmitter_model.hpp"
#include "string_format.hpp"
#include "memory_map.hpp"
#include "ax25.hpp"
#include "spi_image.hpp"
extern "C" {
#include "usb_serial_device_to_host.h"
}
using namespace portapack;
namespace ui::external_app::kiss_tnc {
namespace {
KissTncView* g_kiss_tnc_instance = nullptr;
}
void KissTncView::kiss_input_trampoline(const uint8_t* data, size_t len) {
if (g_kiss_tnc_instance) g_kiss_tnc_instance->on_kiss_bytes(data, len);
}
KissTncView::KissTncView(NavigationView& nav)
: nav_{nav} {
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
add_children({&field_frequency,
&field_rf_amp,
&field_lna,
&field_vga,
&rssi,
&channel,
&text_status,
&text_usb_status,
&text_rx_count,
&text_tx_count,
&labels,
&console});
receiver_model.enable();
baseband::set_aprs(1200);
g_kiss_tnc_instance = this;
usb_input_handler_.emplace(kiss_input_trampoline);
update_stats();
}
KissTncView::~KissTncView() {
g_kiss_tnc_instance = nullptr;
usb_input_handler_.reset();
if (tx_active_) {
transmitter_model.disable();
} else {
receiver_model.disable();
}
baseband::shutdown();
}
void KissTncView::focus() {
field_frequency.focus();
}
void KissTncView::update_stats() {
text_rx_count.set(to_string_dec_uint(rx_count_));
text_tx_count.set(to_string_dec_uint(tx_count_));
text_usb_status.set(portapack::usb_serial.serial_connected() ? "Connected" : "Disconnected");
}
void KissTncView::send_kiss_frame(const uint8_t* data, size_t len) {
// Stack-allocate output buffer: max KISS overhead is 2x data + 2 FENDs + 1 cmd byte
uint8_t buf[512];
size_t i = 0;
buf[i++] = 0xC0; // FEND
buf[i++] = 0x00; // data command
for (size_t j = 0; j < len; j++) {
if (data[j] == 0xC0) {
if (i + 3 > sizeof(buf)) break; // need 2 for escape + 1 for final FEND
buf[i++] = 0xDB;
buf[i++] = 0xDC;
} else if (data[j] == 0xDB) {
if (i + 3 > sizeof(buf)) break; // need 2 for escape + 1 for final FEND
buf[i++] = 0xDB;
buf[i++] = 0xDD;
} else {
if (i + 2 > sizeof(buf)) break; // need 1 for data + 1 for final FEND
buf[i++] = data[j];
}
}
buf[i++] = 0xC0; // FEND
if (usb_input_handler_)
usb_input_handler_->write(buf, i);
}
void KissTncView::on_packet(const APRSPacketMessage* message) {
aprs::APRSPacket pkt = message->packet;
uint8_t payload_size = pkt.size();
if (payload_size > 2) {
uint8_t raw[256];
size_t raw_len = payload_size - 2;
for (size_t i = 0; i < raw_len; i++)
raw[i] = static_cast<uint8_t>(pkt[i]);
send_kiss_frame(raw, raw_len);
}
console.writeln(pkt.get_source_formatted() + ">" + pkt.get_destination_formatted());
rx_count_++;
update_stats();
}
void KissTncView::on_kiss_bytes(const uint8_t* data, size_t len) {
for (size_t i = 0; i < len; i++) {
uint8_t b = data[i];
switch (kiss_state_) {
case KissState::IDLE:
if (b == 0xC0) kiss_state_ = KissState::CMD;
break;
case KissState::CMD:
if (b == 0xC0) break;
if (b == 0x00) {
kiss_idx_ = 0;
kiss_state_ = KissState::DATA;
} else {
kiss_state_ = KissState::IDLE;
}
break;
case KissState::DATA:
if (b == 0xC0) {
if (kiss_idx_ > 0) process_kiss_frame();
kiss_idx_ = 0;
kiss_state_ = KissState::CMD;
} else if (b == 0xDB) {
kiss_state_ = KissState::ESC;
} else if (kiss_idx_ < sizeof(kiss_buf_)) {
kiss_buf_[kiss_idx_++] = b;
}
break;
case KissState::ESC:
if (b == 0xDC) {
if (kiss_idx_ < sizeof(kiss_buf_)) kiss_buf_[kiss_idx_++] = 0xC0;
} else if (b == 0xDD) {
if (kiss_idx_ < sizeof(kiss_buf_)) kiss_buf_[kiss_idx_++] = 0xDB;
}
kiss_state_ = KissState::DATA;
break;
}
}
}
void KissTncView::process_kiss_frame() {
if (tx_active_ || kiss_idx_ == 0) return;
// AFSK shared buffer is 512 bytes (256 uint16_t words). Each raw byte
// produces ~10 encoded bits after bit-stuffing + flags, so cap at 200 bytes
// to ensure the bitstream plus required 0-word terminator always fits.
if (kiss_idx_ > 200) return;
start_tx();
}
void KissTncView::start_tx() {
tx_active_ = true;
ax25::AX25Frame frame;
frame.make_frame_from_raw(kiss_buf_, kiss_idx_);
rx_frequency_ = receiver_model.target_frequency();
receiver_model.disable();
baseband::shutdown();
baseband::run_image(portapack::spi_flash::image_tag_afsk);
transmitter_model.set_target_frequency(rx_frequency_);
transmitter_model.set_sampling_rate(AFSK_SAMPLE_RATE);
transmitter_model.set_baseband_bandwidth(AFSK_BASEBAND_BW);
transmitter_model.enable();
baseband::set_afsk_data(AFSK_SAMPLE_RATE / 1200, 1200, 2200, 1, 10000, 8);
text_status.set("Transmitting");
tx_count_++;
update_stats();
}
void KissTncView::finish_tx() {
transmitter_model.disable();
baseband::shutdown();
// Must reload from SPI flash — run_image(afsk) overwrote m4_code.base()
baseband::run_image(portapack::spi_flash::image_tag_aprs_rx);
receiver_model.set_target_frequency(rx_frequency_);
receiver_model.set_sampling_rate(APRS_RX_SAMPLE_RATE);
receiver_model.set_baseband_bandwidth(APRS_RX_BASEBAND_BW);
receiver_model.enable();
baseband::set_aprs(1200);
tx_active_ = false;
text_status.set("Listening");
update_stats();
}
} // namespace ui::external_app::kiss_tnc
+138
View File
@@ -0,0 +1,138 @@
/*
* Copyright (C) 2024 Sarah Rose
*
* 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.
*/
#pragma once
#include "ui.hpp"
#include "ui_widget.hpp"
#include "ui_navigation.hpp"
#include "ui_freq_field.hpp"
#include "ui_rssi.hpp"
#include "radio_state.hpp"
#include "app_settings.hpp"
#include "message.hpp"
#include "baseband_api.hpp"
#include "portapack.hpp"
#include "receiver_model.hpp"
#include "aprs_packet.hpp"
#include "usb_serial_host_to_device.hpp"
#include <optional>
namespace ui::external_app::kiss_tnc {
class KissTncView : public View {
public:
KissTncView(NavigationView& nav);
~KissTncView();
void focus() override;
std::string title() const override { return "KISS TNC"; }
private:
static constexpr rf::Frequency APRS_FREQ_DEFAULT = 144390000;
static constexpr uint32_t APRS_RX_SAMPLE_RATE = 3072000U;
static constexpr uint32_t APRS_RX_BASEBAND_BW = 1750000U;
static constexpr uint32_t AFSK_SAMPLE_RATE = 1536000U;
static constexpr uint32_t AFSK_BASEBAND_BW = 1750000U;
NavigationView& nav_;
// CRITICAL: RxRadioState must be declared before SettingsManager
RxRadioState radio_state_{
APRS_FREQ_DEFAULT,
APRS_RX_BASEBAND_BW,
APRS_RX_SAMPLE_RATE};
app_settings::SettingsManager settings_{"kiss_tnc", app_settings::Mode::RX};
RxFrequencyField field_frequency{
{0, 0},
nav_};
RFAmpField field_rf_amp{{13 * 8, 0}};
LNAGainField field_lna{{15 * 8, 0}};
VGAGainField field_vga{{18 * 8, 0}};
RSSI rssi{{21 * 8, 0, 6 * 8, 4}};
Channel channel{{21 * 8, 5, 6 * 8, 4}};
Text text_status{{0, 16, 240, 16}, "Listening"};
Text text_usb_status{{4 * 8, 2 * 16, 14 * 8, 16}, "Disconnected"};
Text text_rx_count{{3 * 8, 3 * 16, 5 * 8, 16}, "0"};
Text text_tx_count{{11 * 8, 3 * 16, 5 * 8, 16}, "0"};
Labels labels{{
{{0, 2 * 16}, "USB:", Color::light_grey()},
{{0, 3 * 16}, "RX:", Color::light_grey()},
{{8 * 8, 3 * 16}, "TX:", Color::light_grey()},
}};
Console console{{0, 4 * 16, 240, 180}};
enum class KissState : uint8_t { IDLE,
CMD,
DATA,
ESC };
KissState kiss_state_{KissState::IDLE};
uint8_t kiss_buf_[350]{};
size_t kiss_idx_{0};
bool tx_active_{false};
rf::Frequency rx_frequency_{APRS_FREQ_DEFAULT};
uint32_t rx_count_{0};
uint32_t tx_count_{0};
void on_packet(const APRSPacketMessage* message);
void on_kiss_bytes(const uint8_t* data, size_t len);
void process_kiss_frame();
void send_kiss_frame(const uint8_t* data, size_t len);
void start_tx();
void finish_tx();
void update_stats();
uint32_t frame_counter_{0};
static void kiss_input_trampoline(const uint8_t* data, size_t len);
std::optional<UsbSerialInputHandler> usb_input_handler_{};
MessageHandlerRegistration message_handler_packet{
Message::ID::APRSPacket,
[this](Message* const p) {
on_packet(reinterpret_cast<const APRSPacketMessage*>(p));
}};
MessageHandlerRegistration message_handler_tx_progress{
Message::ID::TXProgress,
[this](Message* const p) {
auto* msg = reinterpret_cast<const TXProgressMessage*>(p);
if (msg->done) finish_tx();
}};
MessageHandlerRegistration message_handler_frame_sync{
Message::ID::DisplayFrameSync,
[this](const Message* const) {
if (++frame_counter_ == 60) {
frame_counter_ = 0;
text_usb_status.set(portapack::usb_serial.serial_connected() ? "Connected" : "Disconnected");
}
}};
};
} // namespace ui::external_app::kiss_tnc
+83
View File
@@ -0,0 +1,83 @@
/*
* Copyright (C) 2025 Sarah Rose
*
* 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_mdc_tx.hpp"
#include "ui_navigation.hpp"
#include "external_app.hpp"
namespace ui::external_app::mdc_tx {
void initialize_app(ui::NavigationView& nav) {
nav.push<MdcTxView>();
}
} // namespace ui::external_app::mdc_tx
extern "C" {
__attribute__((section(".external_app.app_mdc_tx.application_information"), used))
application_information_t _application_information_mdc_tx = {
/*.memory_location = */ (uint8_t*)0x00000000,
/*.externalAppEntry = */ ui::external_app::mdc_tx::initialize_app,
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
/*.app_name = */ "MDC-1200 TX",
/*.bitmap_data = */ {
0x00,
0x00,
0xFE,
0x7F,
0xFF,
0xFF,
0xBB,
0xD0,
0xFF,
0xFF,
0xFF,
0xFF,
0x0B,
0xE1,
0xFF,
0xFF,
0xFF,
0xFF,
0xEB,
0xD0,
0xFF,
0xFF,
0xFE,
0x7F,
0x70,
0x00,
0x30,
0x00,
0x10,
0x00,
0x00,
0x00,
},
/*.icon_color = */ ui::Color::orange().v,
/*.menu_location = */ app_location_t::TX,
/*.desired_menu_position = */ -1,
/*.m4_app_tag = */ {'P', 'A', 'F', 'T'},
/*.m4_app_offset = */ 0x00000000,
};
}
+289
View File
@@ -0,0 +1,289 @@
/*
* Copyright (C) 2025 Sarah Rose
*
* 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_mdc_tx.hpp"
#include "baseband_api.hpp"
#include "portapack.hpp"
#include "portapack_shared_memory.hpp"
#include "transmitter_model.hpp"
#include "ui/ui_receiver.hpp"
using namespace portapack;
namespace ui::external_app::mdc_tx {
static constexpr uint32_t MDC_SAMPLES_PER_BIT = AFSK_TX_SAMPLERATE / 1200U;
static constexpr uint32_t MDC_MARK_FREQ = 1800U;
static constexpr uint32_t MDC_SPACE_FREQ = 1200U;
static constexpr uint32_t MDC_FM_DEVIATION = 2500U;
static constexpr uint8_t MDC_PREAMBLE[7] = {0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA};
static constexpr uint8_t MDC_SYNC[5] = {0x07, 0x09, 0x2A, 0x44, 0x6F};
static constexpr int MDC_NUM_PREAMBLES = 3;
static constexpr uint8_t MDC_DP_OP = 0x35;
static constexpr uint8_t MDC_DP_ARG = 0x89;
static constexpr uint8_t MDC_DP_CALL_ARG = 0x0D;
static uint8_t mdc_flip8(uint8_t in) {
uint8_t out = 0;
for (int i = 0; i < 8; i++) {
out = static_cast<uint8_t>((out << 1) | (in & 1));
in >>= 1;
}
return out;
}
static uint16_t mdc_flip16(uint16_t in) {
uint16_t out = 0;
for (int i = 0; i < 16; i++) {
out = static_cast<uint16_t>((out << 1) | (in & 1));
in >>= 1;
}
return out;
}
static uint16_t mdc_crc(const uint8_t* data, int length) {
uint16_t crc = 0;
for (int i = 0; i < length; i++) {
uint16_t c = mdc_flip8(data[i]);
for (int j = 0x80; j != 0; j >>= 1) {
uint16_t bit = crc & 0x8000;
crc = static_cast<uint16_t>(crc << 1);
if (c & static_cast<uint16_t>(j)) bit ^= 0x8000;
if (bit) crc ^= 0x1021;
}
}
crc = mdc_flip16(crc);
crc ^= 0xFFFF;
return crc;
}
// CRC + convolutional encoding + bit interleaving for a 14-byte codeword.
// Uses lbits_ (class member) so no large allocation hits the stack.
void MdcTxView::pack_codeword(uint8_t cw[14]) {
uint16_t crc = mdc_crc(cw, 4);
cw[4] = static_cast<uint8_t>(crc & 0xFF);
cw[5] = static_cast<uint8_t>((crc >> 8) & 0xFF);
cw[6] = 0x00;
// Convolutional encoding — csr[7] is only 7 bytes, fine on stack
uint8_t csr[7] = {};
for (int i = 0; i < 7; i++) {
cw[i + 7] = 0;
for (int j = 0; j <= 7; j++) {
for (int k = 6; k > 0; k--) csr[k] = csr[k - 1];
csr[0] = (cw[i] >> j) & 1;
int b = csr[0] + csr[2] + csr[5] + csr[6];
cw[i + 7] |= static_cast<uint8_t>((b & 1) << j);
}
}
// Bit interleaving — uses lbits_ class member (112 bytes off the stack)
for (int i = 0; i < 112; i++) lbits_[i] = 0;
int l = 0, m = 0;
for (int i = 0; i < 14; i++) {
for (int j = 0; j <= 7; j++) {
lbits_[l] = (cw[i] >> j) & 1;
l += 16;
if (l > 111) {
m++;
l = m;
}
}
}
l = 0;
for (int i = 0; i < 14; i++) {
cw[i] = 0;
for (int j = 7; j >= 0; j--) {
if (lbits_[l]) cw[i] |= static_cast<uint8_t>(1 << j);
l++;
}
}
}
// Writes preamble + sync + one packed codeword into tx_stream_, returns length.
int MdcTxView::build_single(uint8_t op, uint8_t arg, uint8_t id_hi, uint8_t id_lo) {
int idx = 0;
for (int i = 0; i < MDC_NUM_PREAMBLES; i++)
for (int j = 0; j < 7; j++) tx_stream_[idx++] = MDC_PREAMBLE[j];
for (int j = 0; j < 5; j++) tx_stream_[idx++] = MDC_SYNC[j];
uint8_t cw[14] = {};
cw[0] = op;
cw[1] = arg;
cw[2] = id_hi;
cw[3] = id_lo;
pack_codeword(cw);
for (int j = 0; j < 14; j++) tx_stream_[idx++] = cw[j];
return idx;
}
// Writes preamble + sync + two packed codewords into tx_stream_, returns length.
int MdcTxView::build_double(uint8_t op1, uint8_t arg1, uint8_t id1_hi, uint8_t id1_lo, uint8_t op2, uint8_t arg2, uint8_t id2_hi, uint8_t id2_lo) {
int idx = 0;
for (int i = 0; i < MDC_NUM_PREAMBLES; i++)
for (int j = 0; j < 7; j++) tx_stream_[idx++] = MDC_PREAMBLE[j];
for (int j = 0; j < 5; j++) tx_stream_[idx++] = MDC_SYNC[j];
uint8_t cw1[14] = {};
cw1[0] = op1;
cw1[1] = arg1;
cw1[2] = id1_hi;
cw1[3] = id1_lo;
pack_codeword(cw1);
for (int j = 0; j < 14; j++) tx_stream_[idx++] = cw1[j];
uint8_t cw2[14] = {};
cw2[0] = op2;
cw2[1] = arg2;
cw2[2] = id2_hi;
cw2[3] = id2_lo;
pack_codeword(cw2);
for (int j = 0; j < 14; j++) tx_stream_[idx++] = cw2[j];
return idx;
}
MdcTxView::MdcTxView(NavigationView& nav)
: nav_{nav} {
baseband::run_image(portapack::spi_flash::image_tag_afsk);
add_children({
&labels_,
&field_op,
&field_id_hi,
&field_id_lo,
&field_arg,
&field_src_hi,
&field_src_lo,
&text_status,
&tx_view,
});
tx_view.on_start = [this]() {
if (!tx_active_) {
start_tx();
tx_view.set_transmitting(true);
}
};
tx_view.on_stop = [this]() { stop_tx(); };
transmitter_model.set_tx_gain(30);
tx_view.on_edit_frequency = [this, &nav]() {
auto new_view = nav.push<FrequencyKeypadView>(transmitter_model.target_frequency());
new_view->on_changed = [this](rf::Frequency f) {
transmitter_model.set_target_frequency(f);
};
};
field_op.on_change = [this](size_t, int32_t op) {
switch (op) {
case 0xFE:
field_arg.set_value(0x0C);
break; // Unstun
case 0x2B:
field_arg.set_value(0x00);
break; // Stun
case 0x63:
field_arg.set_value(0x85);
break; // Radio Check
case 0x11:
field_arg.set_value(0x8A);
break; // Remote Monitor
default:
field_arg.set_value(0x00);
break;
}
};
}
MdcTxView::~MdcTxView() {
transmitter_model.disable();
baseband::shutdown();
}
void MdcTxView::focus() {
field_op.focus();
}
void MdcTxView::start_tx() {
int32_t raw_op = field_op.selected_index_value();
uint8_t op = (raw_op == 0xFE) ? 0x2B : static_cast<uint8_t>(raw_op);
uint8_t arg = static_cast<uint8_t>(field_arg.value());
uint8_t id_hi = static_cast<uint8_t>(field_id_hi.value());
uint8_t id_lo = static_cast<uint8_t>(field_id_lo.value());
uint8_t src_hi = static_cast<uint8_t>(field_src_hi.value());
uint8_t src_lo = static_cast<uint8_t>(field_src_lo.value());
int stream_len;
bool is_double = (op == 0x80 || op == 0x81 || op == 0x82);
if (is_double) {
stream_len = build_double(MDC_DP_OP, MDC_DP_ARG, id_hi, id_lo,
op, MDC_DP_CALL_ARG, src_hi, src_lo);
} else {
stream_len = build_single(op, arg, id_hi, id_lo);
}
// NRZI encode tx_stream_ into shared memory words
auto* words = reinterpret_cast<uint16_t*>(shared_memory.bb_data.data);
uint8_t prev_bit = 0;
for (int i = 0; i < stream_len; i++) {
uint8_t raw = tx_stream_[i];
uint8_t nrzi = 0;
for (int bit = 7; bit >= 0; bit--) {
uint8_t rb = (raw >> bit) & 1;
uint8_t nb = rb ^ prev_bit;
prev_bit = rb;
nrzi = static_cast<uint8_t>((nrzi << 1) | nb);
}
words[i] = nrzi;
}
words[stream_len] = 0;
tx_active_ = true;
text_status.set("TX...");
transmitter_model.enable();
baseband::set_afsk_data(
MDC_SAMPLES_PER_BIT,
MDC_MARK_FREQ,
MDC_SPACE_FREQ,
1,
MDC_FM_DEVIATION,
8);
}
void MdcTxView::stop_tx() {
baseband::kill_afsk();
transmitter_model.disable();
tx_active_ = false;
text_status.set("Ready");
tx_view.set_transmitting(false);
}
void MdcTxView::on_tx_progress(uint32_t, bool done) {
if (done) {
stop_tx();
text_status.set("Done!");
}
}
} // namespace ui::external_app::mdc_tx
+124
View File
@@ -0,0 +1,124 @@
/*
* Copyright (C) 2025 Sarah Rose
*
* 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_MDC_TX_H__
#define __UI_MDC_TX_H__
#include "ui.hpp"
#include "ui_widget.hpp"
#include "ui_navigation.hpp"
#include "ui_transmitter.hpp"
#include "app_settings.hpp"
#include "radio_state.hpp"
#include "message.hpp"
#include "modems.hpp"
namespace ui::external_app::mdc_tx {
class MdcTxView : public View {
public:
MdcTxView(NavigationView& nav);
~MdcTxView();
MdcTxView(const MdcTxView&) = delete;
MdcTxView& operator=(const MdcTxView&) = delete;
void focus() override;
std::string title() const override { return "MDC-1200 TX"; }
private:
NavigationView& nav_;
// TxRadioState MUST be declared before SettingsManager
TxRadioState radio_state_{
462562500,
1750000,
AFSK_TX_SAMPLERATE};
app_settings::SettingsManager settings_{
"tx_mdc_tx",
app_settings::Mode::TX};
Labels labels_{{
{{UI_POS_X(0), UI_POS_Y(0)}, "Op:", ui::Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(1)}, "ID:", ui::Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(13), UI_POS_Y(1)}, "Arg:", ui::Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(2)}, "Src:", ui::Theme::getInstance()->fg_light->foreground},
}};
// Only one OptionsField allowed per external app
// Stun=Op 0x2B Arg 0x00, Unstun=Op 0x2B Arg 0x0C (auto-set via on_change)
OptionsField field_op{
{UI_POS_X(4), UI_POS_Y(0)},
14,
{{"PTT ID", 0x01},
{"Emergency", 0x00},
{"Radio Check", 0x63},
{"Stun", 0x2B},
{"Unstun", 0xFE}, // sentinel: op=0x2B, arg=0x0C
{"Call Alert", 0x81},
{"Sel Call 1", 0x80},
{"Sel Call 2", 0x82},
{"Status Req", 0x22},
{"Status Resp", 0x06},
{"Rem Monitor", 0x11},
{"Message", 0x07}}};
// Unit ID: two decimal bytes (0-255) combined into a 16-bit ID
NumberField field_id_hi{{UI_POS_X(4), UI_POS_Y(1)}, 3, {0, 255}, 1, '0'};
NumberField field_id_lo{{UI_POS_X(7), UI_POS_Y(1)}, 3, {0, 255}, 1, '0'};
// Argument byte (decimal 0-255, auto-set by op selection)
NumberField field_arg{{UI_POS_X(17), UI_POS_Y(1)}, 3, {0, 255}, 1, '0'};
// Source ID: two decimal bytes (0-255) combined into a 16-bit ID
NumberField field_src_hi{{UI_POS_X(4), UI_POS_Y(2)}, 3, {0, 255}, 1, '0'};
NumberField field_src_lo{{UI_POS_X(7), UI_POS_Y(2)}, 3, {0, 255}, 1, '0'};
Text text_status{{0, UI_POS_Y(3), UI_POS_MAXWIDTH, UI_POS_DEFAULT_HEIGHT}, "Ready"};
TransmitterView tx_view{
(int16_t)UI_POS_Y_BOTTOM(4),
12500,
0};
bool tx_active_{false};
// Class-owned scratch buffers — keep large allocations off the stack
uint8_t tx_stream_[128]{};
uint8_t lbits_[112]{};
void pack_codeword(uint8_t cw[14]);
int build_single(uint8_t op, uint8_t arg, uint8_t id_hi, uint8_t id_lo);
int build_double(uint8_t op1, uint8_t arg1, uint8_t id1_hi, uint8_t id1_lo, uint8_t op2, uint8_t arg2, uint8_t id2_hi, uint8_t id2_lo);
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 msg = *reinterpret_cast<const TXProgressMessage*>(p);
this->on_tx_progress(msg.progress, msg.done);
}};
};
} // namespace ui::external_app::mdc_tx
#endif
+1 -1
View File
@@ -73,7 +73,7 @@ __attribute__((section(".external_app.app_noaaapt_rx.application_information"),
0x78,
0x00,
},
/*.icon_color = */ ui::Color::orange().v,
/*.icon_color = */ ui::Color::yellow().v,
/*.menu_location = */ app_location_t::RX,
/*.desired_menu_position = */ -1,
+1 -1
View File
@@ -73,7 +73,7 @@ __attribute__((section(".external_app.app_ook_editor.application_information"),
0xE0,
0x07,
},
/*.icon_color = */ ui::Color::orange().v,
/*.icon_color = */ ui::Color::yellow().v,
/*.menu_location = */ app_location_t::TX,
/*.desired_menu_position = */ -1,
+1 -1
View File
@@ -73,7 +73,7 @@ __attribute__((section(".external_app.app_ookbrute.application_information"), us
0xE0,
0x07,
},
/*.icon_color = */ ui::Color::orange().v,
/*.icon_color = */ ui::Color::yellow().v,
/*.menu_location = */ app_location_t::TX,
/*.desired_menu_position = */ -1,
+83
View File
@@ -0,0 +1,83 @@
/*
* Copyright (C) 2025 Sarah Rose
*
* 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_p25_tx.hpp"
#include "ui_navigation.hpp"
#include "external_app.hpp"
namespace ui::external_app::p25_tx {
void initialize_app(ui::NavigationView& nav) {
nav.push<P25TxView>();
}
} // namespace ui::external_app::p25_tx
extern "C" {
__attribute__((section(".external_app.app_p25_tx.application_information"), used))
application_information_t _application_information_p25_tx = {
/*.memory_location = */ (uint8_t*)0x00000000,
/*.externalAppEntry = */ ui::external_app::p25_tx::initialize_app,
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
/*.app_name = */ "P25 TX",
/*.bitmap_data = */ {
0x00,
0x00,
0xFE,
0x7F,
0x02,
0x40,
0xFA,
0x5F,
0x0A,
0x50,
0x0A,
0x50,
0xFA,
0x5F,
0x02,
0x40,
0x02,
0x40,
0xE2,
0x47,
0x22,
0x44,
0xE2,
0x47,
0x02,
0x40,
0x02,
0x40,
0xFE,
0x7F,
0x00,
0x00,
},
/*.icon_color = */ ui::Color::orange().v,
/*.menu_location = */ app_location_t::TX,
/*.desired_menu_position = */ -1,
/*.m4_app_tag = */ {'P', '2', '5', 'T'},
/*.m4_app_offset = */ 0x00000000,
};
}
+282
View File
@@ -0,0 +1,282 @@
/*
* Copyright (C) 2025 Sarah Rose
*
* 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_p25_tx.hpp"
#include "portapack.hpp"
#include "transmitter_model.hpp"
#include "baseband_api.hpp"
#include "ui_freq_field.hpp"
#include <cstring>
using namespace portapack;
namespace ui::external_app::p25_tx {
static constexpr uint8_t DUID_TSBK = 0x07;
static constexpr uint8_t SS_TSCC = 0x02;
// BCH(63,16) generator matrix from TIA-102.BAAA-A (ccemu/p25/bch.go)
static const uint64_t bch_matrix[16] = {
0x8000cd930bdd3b2aULL,
0x4000ab5a8e33a6beULL,
0x2000983e4cc4e874ULL,
0x10004c1f2662743aULL,
0x0800eb9c98ec0136ULL,
0x0400b85d47ab3bb0ULL,
0x02005c2ea3d59dd8ULL,
0x01002e1751eaceecULL,
0x0080170ba8f56776ULL,
0x0040c616dfa78890ULL,
0x0020630b6fd3c448ULL,
0x00103185b7e9e224ULL,
0x000818c2dbf4f112ULL,
0x0004c1f2662743a2ULL,
0x0002ad6a38ce9afbULL,
0x00019b2617ba7657ULL,
};
static uint64_t bch_encode(uint16_t data) {
uint64_t cw = 0;
for (int i = 0; i < 16; i++)
if (data & (0x8000u >> i))
cw ^= bch_matrix[i];
return cw;
}
// CRC-CCITT bit-by-bit, init=0, final XOR=0xFFFF (ccemu/p25/crc.go)
static uint16_t crc_ccitt_80(uint16_t high, uint64_t low) {
uint32_t crc = 0;
for (int i = 15; i >= 0; i--) {
crc <<= 1;
if (((crc >> 16) ^ ((high >> i) & 1)) & 1) crc ^= 0x1021;
}
for (int i = 63; i >= 0; i--) {
crc <<= 1;
if (((crc >> 16) ^ ((uint32_t)((low >> i) & 1))) & 1) crc ^= 0x1021;
}
return (uint16_t)((crc & 0xFFFF) ^ 0xFFFF);
}
// 1/2-rate trellis: [state][inputDibit]={outDibit0,outDibit1} (ccemu/p25/trellis.go)
static const uint8_t trellis_table[4][4][2] = {
{{0, 2}, {3, 0}, {0, 1}, {3, 3}},
{{3, 2}, {0, 0}, {3, 1}, {0, 3}},
{{2, 1}, {1, 3}, {2, 2}, {1, 0}},
{{1, 1}, {2, 3}, {1, 2}, {2, 0}},
};
static void trellis_encode(const uint8_t* in48, uint8_t* out98) {
uint8_t state = 0;
for (int i = 0; i < 49; i++) {
uint8_t d = (i < 48) ? in48[i] : 0;
out98[i * 2 + 0] = trellis_table[state][d][0];
out98[i * 2 + 1] = trellis_table[state][d][1];
state = d;
}
}
static void data_interleave(const uint8_t* in98, uint8_t* out98) {
int idx = 0;
for (int j = 0; j < 97; j += 8) {
out98[idx++] = in98[j];
out98[idx++] = in98[j + 1];
}
for (int i = 2; i < 7; i += 2)
for (int j = 0; j < 89; j += 8) {
out98[idx++] = in98[i + j];
out98[idx++] = in98[i + j + 1];
}
}
static int insert_status(const uint8_t* data, int dlen, uint8_t* out, uint8_t ss) {
int num_ss = (dlen + 34) / 35, remaining = num_ss, oi = 0, i = 1;
for (int d = 0; d < dlen; d++) {
out[oi++] = data[d];
if ((i % 35 == 0) && remaining > 0) {
out[oi++] = ss;
remaining--;
}
i++;
}
while (remaining > 0) {
out[oi++] = 0;
if (i % 35 == 0) {
out[oi++] = ss;
remaining--;
}
i++;
}
return oi;
}
static void build_tsbk(uint8_t* out12, uint8_t opcode, uint64_t args) {
uint16_t high = (1u << 15) | ((uint16_t)opcode << 8);
uint16_t crc = crc_ccitt_80(high, args);
out12[0] = (uint8_t)(high >> 8);
out12[1] = (uint8_t)high;
for (int i = 0; i < 8; i++) out12[2 + i] = (uint8_t)(args >> (56 - 8 * i));
out12[10] = (uint8_t)(crc >> 8);
out12[11] = (uint8_t)crc;
}
static void tsbk_iden_up(uint8_t* out12, uint64_t freq_hz) {
uint64_t args = ((uint64_t)100 << 51) | ((uint64_t)100 << 32) | ((freq_hz / 5) & 0xFFFFFFFF);
build_tsbk(out12, 0x3D, args);
}
static void tsbk_net_status(uint8_t* out12, uint32_t wacn, uint16_t sysid) {
build_tsbk(out12, 0x3B, ((uint64_t)(wacn & 0xFFFFF) << 36) | ((uint64_t)(sysid & 0xFFF) << 24));
}
static void tsbk_grp_v_grant(uint8_t* out12, uint8_t chan_id, uint16_t chan_num, uint16_t tg) {
uint64_t ch = ((uint64_t)(chan_id & 0xF) << 12) | (chan_num & 0xFFF);
// opts: bit1=group
uint64_t args = ((uint64_t)0x02 << 56) | (ch << 40) | ((uint64_t)tg << 24);
build_tsbk(out12, 0x00, args);
}
static void tsbk_rfss_status(uint8_t* out12, uint16_t sysid, uint8_t rfssid, uint8_t siteid) {
build_tsbk(out12, 0x3A, ((uint64_t)(sysid & 0xFFF) << 40) | ((uint64_t)rfssid << 32) | ((uint64_t)siteid << 24));
}
static int build_frame(uint8_t* out, uint16_t nac, const uint8_t* tsbk12) {
uint8_t pre[160];
int idx = 0;
static const uint64_t FSW = 0x5575F5FF77FFULL;
for (int i = 0; i < 24; i++) pre[idx++] = (uint8_t)((FSW >> (46 - i * 2)) & 3);
uint64_t nid = bch_encode((uint16_t)((nac << 4) | DUID_TSBK));
for (int i = 0; i < 32; i++) pre[idx++] = (uint8_t)((nid >> (62 - i * 2)) & 3);
uint8_t in_d[48];
for (int i = 0; i < 12; i++) {
in_d[i * 4 + 0] = (tsbk12[i] >> 6) & 3;
in_d[i * 4 + 1] = (tsbk12[i] >> 4) & 3;
in_d[i * 4 + 2] = (tsbk12[i] >> 2) & 3;
in_d[i * 4 + 3] = (tsbk12[i] >> 0) & 3;
}
uint8_t enc[98], ilv[98];
trellis_encode(in_d, enc);
data_interleave(enc, ilv);
memcpy(&pre[idx], ilv, 98);
idx += 98;
return insert_status(pre, idx, out, SS_TSCC);
}
static constexpr int TX_BUF_SIZE = 512;
static int fill_tx_buffer(uint8_t* buf, uint16_t nac, uint32_t wacn, uint16_t sysid, uint8_t rfssid, uint8_t siteid, uint64_t freq_hz, uint16_t tg, uint16_t vch, int& tsbk_idx) {
memset(buf, 0, TX_BUF_SIZE);
int total = 0;
uint8_t tsbk[12];
while (true) {
switch (tsbk_idx % 5) {
case 0:
tsbk_iden_up(tsbk, freq_hz);
break;
case 1:
tsbk_net_status(tsbk, wacn, sysid);
break;
case 2:
tsbk_rfss_status(tsbk, sysid, rfssid, siteid);
break;
case 3:
tsbk_grp_v_grant(tsbk, 0, vch, tg);
break;
default:
tsbk_net_status(tsbk, wacn, sysid);
break;
}
tsbk_idx++;
uint8_t probe[200];
int flen = build_frame(probe, nac, tsbk);
if (total + flen > TX_BUF_SIZE) break;
memcpy(&buf[total], probe, flen);
total += flen;
}
return total;
}
void P25TxView::start_tx() {
uint8_t dibits[TX_BUF_SIZE];
int len = fill_tx_buffer(dibits,
(uint16_t)field_nac.to_integer(), (uint32_t)field_wacn.to_integer(),
(uint16_t)field_sysid.to_integer(), (uint8_t)field_rfssid.to_integer(),
(uint8_t)field_siteid.to_integer(), (uint64_t)transmitter_model.target_frequency(),
(uint16_t)field_tg.to_integer(), (uint16_t)field_vch.to_integer(),
tsbk_idx_);
transmitter_model.enable();
tx_view.set_transmitting(true);
text_status.set("TX TSCC");
transmitting = true;
baseband::set_p25tx_data(dibits, (uint16_t)len);
}
void P25TxView::stop_tx() {
transmitting = false;
transmitter_model.disable();
tx_view.set_transmitting(false);
text_status.set("Ready");
tsbk_idx_ = 0;
}
void P25TxView::on_tx_progress(const uint32_t, const bool done) {
if (done && transmitting) {
uint8_t dibits[TX_BUF_SIZE];
int len = fill_tx_buffer(dibits,
(uint16_t)field_nac.to_integer(), (uint32_t)field_wacn.to_integer(),
(uint16_t)field_sysid.to_integer(), (uint8_t)field_rfssid.to_integer(),
(uint8_t)field_siteid.to_integer(), (uint64_t)transmitter_model.target_frequency(),
(uint16_t)field_tg.to_integer(), (uint16_t)field_vch.to_integer(),
tsbk_idx_);
baseband::set_p25tx_data(dibits, (uint16_t)len);
}
}
P25TxView::P25TxView(NavigationView& nav)
: nav_{nav} {
add_children({&labels_, &field_nac, &field_sysid, &field_wacn,
&field_rfssid, &field_siteid, &field_tg, &field_vch,
&text_status, &tx_view});
field_nac.set_value(0x293);
field_wacn.set_value(0xBEEF0);
field_sysid.set_value(0x001);
field_rfssid.set_value(1);
field_siteid.set_value(1);
field_tg.set_value(1);
field_vch.set_value(1);
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
tx_view.on_start = [this]() { start_tx(); };
tx_view.on_stop = [this]() { stop_tx(); };
tx_view.on_edit_frequency = [this, &nav]() {
auto new_view = nav.push<FrequencyKeypadView>(transmitter_model.target_frequency());
new_view->on_changed = [this](rf::Frequency f) { transmitter_model.set_target_frequency(f); };
};
}
P25TxView::~P25TxView() {
if (transmitting)
stop_tx();
transmitter_model.disable();
baseband::shutdown();
}
void P25TxView::focus() {
field_nac.focus();
}
} // namespace ui::external_app::p25_tx
+96
View File
@@ -0,0 +1,96 @@
/*
* Copyright (C) 2025 Sarah Rose
*
* 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_P25_TX
#define __UI_P25_TX
#include "ui.hpp"
#include "ui_widget.hpp"
#include "ui_transmitter.hpp"
#include "ui_freq_field.hpp"
#include "app_settings.hpp"
#include "radio_state.hpp"
#include "message.hpp"
namespace ui::external_app::p25_tx {
class P25TxView : public View {
public:
explicit P25TxView(NavigationView& nav);
~P25TxView();
void focus() override;
std::string title() const override { return "P25 TX"; }
private:
void start_tx();
void stop_tx();
void on_tx_progress(const uint32_t progress, const bool done);
NavigationView& nav_;
TxRadioState radio_state_{
155'000'000,
12500,
2'400'000};
app_settings::SettingsManager settings_{
"tx_p25_tx",
app_settings::Mode::TX};
Labels labels_{{
{{UI_POS_X(0), UI_POS_Y(0)}, "NAC:", ui::Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(9), UI_POS_Y(0)}, "SYS:", ui::Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(1)}, "WACN:", ui::Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(2)}, "RFSS:", ui::Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(9), UI_POS_Y(2)}, "SITE:", ui::Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(3)}, "TG:", ui::Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(9), UI_POS_Y(3)}, "VCH:", ui::Theme::getInstance()->fg_light->foreground},
}};
SymField field_nac{{UI_POS_X(4), UI_POS_Y(0)}, 3, SymField::Type::Hex};
SymField field_sysid{{UI_POS_X(13), UI_POS_Y(0)}, 3, SymField::Type::Hex};
SymField field_wacn{{UI_POS_X(5), UI_POS_Y(1)}, 5, SymField::Type::Hex};
SymField field_rfssid{{UI_POS_X(5), UI_POS_Y(2)}, 2, SymField::Type::Hex};
SymField field_siteid{{UI_POS_X(14), UI_POS_Y(2)}, 2, SymField::Type::Hex};
SymField field_tg{{UI_POS_X(3), UI_POS_Y(3)}, 4, SymField::Type::Hex};
SymField field_vch{{UI_POS_X(13), UI_POS_Y(3)}, 3, SymField::Type::Hex};
Text text_status{{0, UI_POS_Y(4), UI_POS_MAXWIDTH, UI_POS_DEFAULT_HEIGHT}, "Ready"};
TransmitterView tx_view{
(int16_t)UI_POS_Y_BOTTOM(5),
12500,
0};
bool transmitting{false};
int tsbk_idx_{0};
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::p25_tx
#endif /* __UI_P25_TX */
+1 -1
View File
@@ -44,7 +44,7 @@ POCSAGTXView::~POCSAGTXView() {
baseband::shutdown();
}
void POCSAGTXView::on_remote(const PocsagTosendMessage data) {
void POCSAGTXView::on_remete(const PocsagTosendMessage data) {
// check if still sending or not
size_t tmp = 0;
if (data.baud == 1200) tmp = 1;
+8 -8
View File
@@ -76,7 +76,7 @@ class POCSAGTXView : public View {
void on_set_text(NavigationView& nav);
void on_tx_progress(const uint32_t progress, const bool done);
void on_remote(const PocsagTosendMessage data);
void on_remete(const PocsagTosendMessage data);
bool start_tx();
Labels labels{
@@ -140,19 +140,19 @@ class POCSAGTXView : public View {
10000,
9};
MessageHandlerRegistration message_handler_tx_remete{
Message::ID::PocsagTosend,
[this](const Message* const p) {
const auto message = *reinterpret_cast<const PocsagTosendMessage*>(p);
this->on_remete(message);
}};
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_tx_remote{
Message::ID::PocsagTosend,
[this](const Message* const p) {
const auto message = *reinterpret_cast<const PocsagTosendMessage*>(p);
this->on_remote(message);
}};
};
} /* namespace ui::external_app::pocsag_tx */
+1 -1
View File
@@ -73,7 +73,7 @@ __attribute__((section(".external_app.app_rtty_rx.application_information"), use
0x00,
0x00,
},
/*.icon_color = */ ui::Color::orange().v,
/*.icon_color = */ ui::Color::yellow().v,
/*.menu_location = */ app_location_t::RX,
/*.desired_menu_position = */ -1,
+1 -1
View File
@@ -73,7 +73,7 @@ __attribute__((section(".external_app.app_rtty_tx.application_information"), use
0x00,
0x00,
},
/*.icon_color = */ ui::Color::orange().v,
/*.icon_color = */ ui::Color::yellow().v,
/*.menu_location = */ app_location_t::TX,
/*.desired_menu_position = */ -1,
+1 -1
View File
@@ -69,7 +69,7 @@ __attribute__((section(".external_app.app_same_tx.application_information"), use
0x00,
0x00,
},
/*.icon_color = */ ui::Color::orange().v,
/*.icon_color = */ ui::Color::yellow().v,
/*.menu_location = */ app_location_t::TX,
/*.desired_menu_position = */ -1,
+1 -1
View File
@@ -58,7 +58,7 @@ __attribute__((section(".external_app.app_shoppingcart_lock.application_informat
0x7E,
0x00,
},
/*.icon_color = */ ui::Color::red().v,
/*.icon_color = */ ui::Color::yellow().v,
/*.menu_location = */ app_location_t::UTILITIES,
/*.desired_menu_position = */ -1,
+1 -1
View File
@@ -73,7 +73,7 @@ __attribute__((section(".external_app.app_soundboard.application_information"),
0xF0,
0xFF,
},
/*.icon_color = */ ui::Color::orange().v,
/*.icon_color = */ ui::Color::yellow().v,
/*.menu_location = */ app_location_t::TX,
/*.desired_menu_position = */ -1,
+1 -1
View File
@@ -73,7 +73,7 @@ __attribute__((section(".external_app.app_spainter.application_information"), us
0xC0,
0x01,
},
/*.icon_color = */ ui::Color::orange().v,
/*.icon_color = */ ui::Color::yellow().v,
/*.menu_location = */ app_location_t::TX,
/*.desired_menu_position = */ -1,
+1 -1
View File
@@ -72,7 +72,7 @@ __attribute__((section(".external_app.app_time_sink.application_information"), u
0x00,
0x00,
},
/*.icon_color = */ ui::Color::cyan().v,
/*.icon_color = */ ui::Color::green().v,
/*.menu_location = */ app_location_t::RX,
/*.desired_menu_position = */ -1,
+1 -1
View File
@@ -73,7 +73,7 @@ __attribute__((section(".external_app.app_wefax_rx.application_information"), us
0x00,
0x00,
},
/*.icon_color = */ ui::Color::orange().v,
/*.icon_color = */ ui::Color::yellow().v,
/*.menu_location = */ app_location_t::RX,
/*.desired_menu_position = */ -1,
+1
View File
@@ -57,3 +57,4 @@ const std::filesystem::path macaddress_dir = u"MACADDRESS";
const std::filesystem::path splash_dot_bmp = u"/splash.bmp";
const std::filesystem::path keeloq_keys_dir = u"KEELOQKEYS";
const std::filesystem::path keeloq_remotes_dir = u"KEELOQREMOTES";
const std::filesystem::path epirb_dir = u"EPIRB";
+1
View File
@@ -58,6 +58,7 @@ extern const std::filesystem::path waterfalls_dir;
extern const std::filesystem::path macaddress_dir;
extern const std::filesystem::path keeloq_keys_dir;
extern const std::filesystem::path keeloq_remotes_dir;
extern const std::filesystem::path epirb_dir;
extern const std::filesystem::path splash_dot_bmp;
+11 -7
View File
@@ -361,15 +361,18 @@ class Si5351 {
}
void wait_for_device_ready() {
#ifndef PRALINE
while (device_status() & 0x80);
#else
#ifdef PRALINE
// Add timeout to prevent infinite loop if I2C communication fails
// (e.g., on PRALINE hardware with different configuration)
uint32_t timeout = 100000;
while ((device_status() & 0x80) && (timeout > 0)) {
timeout--;
if (timeout == 0) {
chDbgAssert(false, "si5351: wait_for_device_ready timeout", nullptr);
}
}
#else
while (device_status() & 0x80);
#endif
}
@@ -396,13 +399,14 @@ class Si5351 {
}
void reset_plls() {
#ifndef PRALINE
// Datasheet recommends value 0xac, though the low nibble bits are not defined in AN619.
write_register(Register::PLLReset, 0xac);
#else
#ifdef PRALINE
// Reset both PLLA and PLLB. Use 0xA0 to match HackRF reference firmware.
// The low nibble bits are reserved/undefined in AN619.
write_register(Register::PLLReset, 0xa0);
#else
// Datasheet recommends value 0xac, though the low nibble bits are not defined in AN619.
write_register(Register::PLLReset, 0xac);
#endif
}
regvalue_t read_register(const uint8_t reg);
+14 -11
View File
@@ -392,18 +392,8 @@ static void set_cpu_clock_speed() {
#endif
cgu::pll1::enable();
#ifndef PRALINE
while (!cgu::pll1::is_locked());
set_clock_config(clock_config_pll1_step);
/* Delay >50us at 90-110MHz clock speed */
volatile uint32_t delay = 1400;
while (delay--);
set_clock_config(clock_config_pll1);
/* Remove /2P divider from PLL1 output to achieve full speed */
cgu::pll1::direct();
#else
#ifdef PRALINE
// Wait for PLL1 to lock with timeout
{
uint32_t timeout = 100000;
@@ -423,6 +413,18 @@ static void set_cpu_clock_speed() {
/* Remove /2P divider from PLL1 output to achieve full speed */
cgu::pll1::direct();
}
#else
while (!cgu::pll1::is_locked());
set_clock_config(clock_config_pll1_step);
/* Delay >50us at 90-110MHz clock speed */
volatile uint32_t delay = 1400;
while (delay--);
set_clock_config(clock_config_pll1);
/* Remove /2P divider from PLL1 output to achieve full speed */
cgu::pll1::direct();
#endif
}
@@ -660,6 +662,7 @@ init_status_t init() {
draw_splash_screen_icon(2, ui::bitmap_icon_sd);
init_status_t return_code = init_status_t::INIT_SUCCESS;
#ifndef PRALINE
// HackRF One uses CPLD - load it via JTAG
if (!hackrf::cpld::load_sram()) {
+25
View File
@@ -131,4 +131,29 @@ void AX25Frame::make_ui_frame(char* const address, const uint8_t control, const
flush();
}
void AX25Frame::make_frame_from_raw(const uint8_t* data, size_t len) {
bb_data_ptr = (uint16_t*)shared_memory.bb_data.data;
memset(bb_data_ptr, 0, sizeof(shared_memory.bb_data.data));
bit_counter = 0;
current_bit = 0;
current_byte = 0;
ones_counter = 0;
crc_ccitt.reset();
add_flag();
add_flag();
add_flag();
add_flag();
for (size_t i = 0; i < len; i++)
add_data(data[i]);
add_checksum();
add_flag();
add_flag();
flush();
}
} /* namespace ax25 */
+1
View File
@@ -44,6 +44,7 @@ enum protocol_id_t {
class AX25Frame {
public:
void make_ui_frame(char* const address, const uint8_t control, const uint8_t protocol, const std::string& info, const std::string& path = "");
void make_frame_from_raw(const uint8_t* data, size_t len);
private:
void NRZI_add_bit(const uint32_t bit);
+56 -17
View File
@@ -24,8 +24,6 @@
#include "rf_path.hpp"
#include "rffc507x.hpp"
#include "max2837.hpp"
#include "max2839.hpp"
#ifdef PRALINE
#include "max2831.hpp"
@@ -33,6 +31,8 @@ extern "C" {
#include "fpga_bridge.h"
}
#else
#include "max2837.hpp"
#include "max2839.hpp"
#include "baseband_cpld.hpp"
#endif
@@ -122,11 +122,11 @@ static spi::arbiter::Target ssp1_target_max5864{
static rf::path::Path rf_path;
rffc507x::RFFC507x first_if;
max283x::MAX283x* second_if;
max2837::MAX2837 second_if_max2837{ssp1_target_max283x};
max2839::MAX2839 second_if_max2839{ssp1_target_max283x};
#ifdef PRALINE
max2831::MAX2831 second_if_max2831{ssp1_target_max283x};
#else
max2837::MAX2837 second_if_max2837{ssp1_target_max283x};
max2839::MAX2839 second_if_max2839{ssp1_target_max283x};
static baseband::CPLD baseband_cpld;
#endif
static max5864::MAX5864 baseband_codec{ssp1_target_max5864};
@@ -180,12 +180,21 @@ void init() {
/* Initialize FPGA registers - DC_BLOCK must be enabled for RX */
// debug::fpga::init();
fpga_set_mode(FPGA_MODE_RX);
// These FPGA registers control DC_BLOCK, Q-Inv, QUARTER SHIFT, and Decimation.
fpga_debug_register_write(1, 0x00); // DC_BLOCK=1, QUARTER_SHIFT=0, Q_INVERT=0
fpga_debug_register_write(2, 0x00); // RX_DECIM=No Decim
fpga_debug_register_write(3, 0x00); // TX_CTRL=0
fpga_debug_register_write(4, 0x00); // TX_INTRP=0
fpga_debug_register_write(5, 0x00); // TX_PSTEP=0
fpga_debug_register_write(FPGA_REG_CTRL, FPGA_CTRL_DC_BLOCK_EN); // DC_BLOCK=1, QUARTER_SHIFT=0, Q_INVERT=0
fpga_debug_register_write(FPGA_REG_DECIM, 0x00); // RX_DECIM=No Decim
// RX Mode: Register 3 is RX Digital Gain. Start with 0dB (no shift).
fpga_debug_register_write(FPGA_REG_RX_DIGITAL_GAIN, FPGA_RX_DEFAULT_DIGITAL_GAIN);
/* RX Mode: Initialize DC Block parameters to standard Praline values.
* 0x04 Width and 0x08 Adapt Rate are typical for 40MHz stability.
*/
fpga_debug_register_write(FPGA_REG_RX_DC_BLOCK_WIDTH, FPGA_RX_DEFAULT_DC_WIDTH);
fpga_debug_register_write(FPGA_REG_RX_DC_ADAPT_RATE, FPGA_RX_DEFAULT_ADAPT_RATE);
ssp1_arbiter.invalidate();
chThdSleepMilliseconds(10); // Let FPGA registers settle
@@ -206,6 +215,24 @@ void set_direction(const rf::Direction new_direction) {
#ifdef PRALINE
cached_direction = new_direction; // Track state for debug and potentially other purposes.
if (new_direction == rf::Direction::Transmit) {
fpga_set_mode(FPGA_MODE_TX);
// TX Mode: Clear RX gain and ensure NCO is off initially
fpga_debug_register_write(FPGA_REG_TX_CONTROL, 0x00);
// Placeholder: Set TX-specific interpolation and phase
fpga_debug_register_write(FPGA_REG_TX_INTERP, 0x00);
fpga_debug_register_write(FPGA_REG_TX_PHASE_STEP, 0x00);
} else {
fpga_set_mode(FPGA_MODE_RX);
// RX Mode: Ensure NCO is disabled and reset digital gain
fpga_debug_register_write(FPGA_REG_RX_DIGITAL_GAIN, FPGA_RX_DEFAULT_DIGITAL_GAIN);
/* RX Mode: Initialize DC Block parameters to standard Praline values.
* 0x04 Width and 0x08 Adapt Rate are typical for 40MHz stability.
*/
fpga_debug_register_write(FPGA_REG_RX_DC_BLOCK_WIDTH, FPGA_RX_DEFAULT_DC_WIDTH);
fpga_debug_register_write(FPGA_REG_RX_DC_ADAPT_RATE, FPGA_RX_DEFAULT_ADAPT_RATE);
}
#endif
direction = new_direction;
@@ -388,7 +415,11 @@ void set_rx_max283x_iq_phase_calibration(const size_t v) {
void disable() {
set_antenna_bias(false);
baseband_codec.set_mode(max5864::Mode::Shutdown);
#ifdef PRALINE
second_if->set_mode(max283x::Mode::Standby);
#else
second_if->set_mode(max2837::Mode::Standby);
#endif
first_if.disable();
set_rf_amp(false);
@@ -519,14 +550,22 @@ void register_write(const size_t register_number, uint32_t value) {
}
void init() {
// Initialize FPGA registers after bitstream load
// DC_BLOCK (bit 0) must be enabled for RX to work
fpga_debug_register_write(1, 0x00); // CTRL: DC_BLOCK=1
fpga_debug_register_write(2, 0x00); // RX_DECIM: no decimation
fpga_debug_register_write(3, 0x00); // TX_CTRL: NCO disabled
fpga_debug_register_write(4, 0x00); // TX_INTRP: no interpolation
fpga_debug_register_write(5, 0x00); // TX_PSTEP: zero phase step
ssp1_arbiter.invalidate(); // Force arbiter to reconfigure on next transfer
fpga_set_mode(FPGA_MODE_RX);
// These FPGA registers control DC_BLOCK, Q-Inv, QUARTER SHIFT, and Decimation.
fpga_debug_register_write(FPGA_REG_CTRL, FPGA_CTRL_DC_BLOCK_EN); // DC_BLOCK=1, QUARTER_SHIFT=0, Q_INVERT=0
fpga_debug_register_write(FPGA_REG_DECIM, 0x00); // RX_DECIM=No Decim
// RX Mode: Register 3 is RX Digital Gain. Start with 0dB (no shift).
fpga_debug_register_write(FPGA_REG_RX_DIGITAL_GAIN, FPGA_RX_DEFAULT_DIGITAL_GAIN);
/* RX Mode: Initialize DC Block parameters to standard Praline values.
* 0x04 Width and 0x08 Adapt Rate are typical for 40MHz stability.
*/
fpga_debug_register_write(FPGA_REG_RX_DC_BLOCK_WIDTH, FPGA_RX_DEFAULT_DC_WIDTH);
fpga_debug_register_write(FPGA_REG_RX_DC_ADAPT_RATE, FPGA_RX_DEFAULT_ADAPT_RATE);
ssp1_arbiter.invalidate(); // Force arbiter to reconfigure on next transfer
}
} /* namespace fpga */
+1 -5
View File
@@ -83,9 +83,7 @@ constexpr GPIOs gpios{
gpio_rx_amp,
gpio_not_rx_amp_pwr,
};
#endif
#ifndef PRALINE
/* HackRF One Config struct - not used on PRALINE */
struct Config {
using base_type = uint16_t;
@@ -186,9 +184,7 @@ struct Config {
}
}
};
#endif /* !PRALINE */
#ifndef PRALINE
/* HackRF One config table - not used on PRALINE */
using ConfigAmp = std::array<Config, 2>;
using ConfigDirection = std::array<ConfigAmp, 2>;
@@ -229,7 +225,7 @@ constexpr Config get_config(
const bool amplify) {
return config_table[toUType(band)][toUType(direction)][amplify ? 1 : 0];
}
#endif /* !PRALINE */
#endif /* PRALINE */
} /* namespace */
+1 -1
View File
@@ -36,7 +36,7 @@ bool card_present = false;
Status status_{Status::NotPresent};
FRESULT mount() {
return f_mount(&fs, reinterpret_cast<const TCHAR*>(_T("")), 0);
return f_mount(&fs, reinterpret_cast<const TCHAR*>(_T("")), 1);
}
} /* namespace */
+2 -2
View File
@@ -294,8 +294,8 @@ bool BtnGridView::set_highlighted(int32_t new_value, bool force_update) {
}
// Normalize offset to show maximum items when count decreased
if (offset + displayed_max > item_count && item_count > 0) {
if (item_count >= displayed_max) {
if (item_count > 0 && offset + displayed_max > static_cast<size_t>(item_count)) {
if (static_cast<size_t>(item_count) >= displayed_max) {
offset = item_count - displayed_max;
} else {
offset = 0;
+1 -1
View File
@@ -265,7 +265,7 @@ void GeoMap::map_read_line_bin(ui::Color* buffer, uint16_t pixels) {
void GeoMap::draw_markers(Painter& painter) {
for (int i = 0; i < markerListLen; ++i) {
draw_marker_item(painter, markerList[i], markerList[i].color, markerList[i].color, Color::black());
draw_marker_item(painter, markerList[i], markerList[i].color, markerList[i].color, Color::magenta());
}
}
+14 -2
View File
@@ -46,12 +46,16 @@ RSSI::RSSI(
void RSSI::paint(Painter& painter) {
const auto r = screen_rect();
/* RSSI scaling based on transceiver output voltage range.
* MAX2837 (HackRF One): 0.4V to 2.2V
* MAX2831 (HackRF Pro): 0.5V to 2.0V (similar enough to use same scaling)
*/
constexpr int rssi_sample_range = 256;
// 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_min = 0;
constexpr float adc_voltage_max = 3.3;
constexpr int raw_max = rssi_sample_range * rssi_voltage_max / adc_voltage_max;
constexpr int raw_delta = raw_max - raw_min;
@@ -113,6 +117,7 @@ void RSSI::paint(Painter& painter) {
const Rect r_db{r.left() + x_db, r.top(), 1, r.height()};
if (db_) painter.fill_rectangle(r_db, Color::green());
} else {
// vertical bottom to top level meters
const range_t<int> y_avg_range{0, r.height() - 1};
@@ -221,6 +226,7 @@ void RSSI::on_statistics_update(const RSSIStatistics& statistics) {
min_ = statistics.min;
avg_ = statistics.accumulator / statistics.count;
max_ = statistics.max;
if (peak_enabled) {
peak_duration_ = peak_duration_ + 100;
if (max_ > peak_) {
@@ -431,9 +437,9 @@ void RSSIGraph::add_values(int16_t rssi_min, int16_t rssi_avg, int16_t rssi_max,
constexpr int rssi_sample_range = 256;
// 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_min = 0;
constexpr float adc_voltage_max = 3.3;
constexpr int raw_max = rssi_sample_range * rssi_voltage_max / adc_voltage_max;
constexpr int raw_delta = raw_max - raw_min;
@@ -523,6 +529,12 @@ bool RSSI::on_touch(const TouchEvent event) {
}
void RSSI::set_db(int16_t db) {
#ifdef PRALINE
/* Add a +30dB global boost to align 40MHz/1.2V VCM data
with the UI's existing display scale. */
db_ = db + 30;
#else
db_ = db;
#endif
}
} /* namespace ui */
+8
View File
@@ -64,10 +64,18 @@ class RSSI : public Widget {
void set_db(int16_t db);
private:
#ifdef PRALINE
// Changed from int8_t to uint8_t:
uint8_t min_ = 0;
uint8_t avg_ = 0;
uint8_t max_ = 0;
uint8_t peak_ = 0;
#else
int8_t min_ = 0;
int8_t avg_ = 0;
int8_t max_ = 0;
int8_t peak_ = 0;
#endif
size_t peak_duration_ = 0;
int16_t db_ = 0;
bool instant_exec_{false};
+14 -12
View File
@@ -301,6 +301,10 @@ SystemStatusView::SystemStatusView(
this->on_clk();
};
sd_card_status_view.on_select = [this](ImageButton&) {
this->on_sd_card();
};
// Initialize toggle buttons
toggle_speaker.set_value(pmem::config_speaker_disable());
toggle_mute.set_value(pmem::config_audio_mute());
@@ -507,6 +511,16 @@ void SystemStatusView::on_clk() {
refresh();
}
void SystemStatusView::on_sd_card() {
if (!nav_.is_valid()) return;
if (sd_info_up) return;
sd_info_up = true;
nav_.push<SetSDCardView>();
nav_.set_on_pop([this]() {
sd_info_up = false;
});
}
void SystemStatusView::on_title() {
if (nav_.is_top())
nav_.push<AboutView>();
@@ -806,12 +820,6 @@ void add_external_items(NavigationView& nav, app_location_t location, BtnGridVie
}
// clang-format on
bool verify_sdcard_format() {
FATFS* fs = &sd_card::fs;
return (fs->fs_type == FS_FAT32 || fs->fs_type == FS_EXFAT) || !(sd_card::status() == sd_card::Status::Mounted);
/* ^ to satisfy those users that not use an sd*/
}
/* ReceiversMenuView *****************************************************/
ReceiversMenuView::ReceiversMenuView(NavigationView& nav)
@@ -913,12 +921,6 @@ void SystemMenuView::on_populate() {
add_apps(nav_, *this, HOME);
add_external_items(nav_, app_location_t::HOME, *this, 0);
add_item({"HackRF", Theme::getInstance()->fg_cyan->foreground, &bitmap_icon_hackrf, [this]() { hackrf_mode(nav_); }});
if (!verify_sdcard_format()) { // Moved to the end... after sd status change event, fstype wasn't populated fast enough..
insert_item({"SDCard Error", Theme::getInstance()->error_dark->foreground, nullptr, [this]() {
nav_.display_modal("Error", "SD Card is not exFAT/FAT32");
}},
0, true);
}
}
/* SystemView ************************************************************/
+2 -1
View File
@@ -62,7 +62,6 @@ namespace ui {
void add_apps(NavigationView& nav, BtnGridView& grid, app_location_t loc);
void add_external_items(NavigationView& nav, app_location_t location, BtnGridView& grid, uint8_t error_tile_pos, bool show_error_tile = true);
bool verify_sdcard_format();
enum modal_t {
INFO = 0,
@@ -202,6 +201,7 @@ class SystemStatusView : public View {
static constexpr auto default_title = "";
bool batt_was_inited = false; // if the battery was off on tart, but later turned on.
bool batt_info_up = false; // to prevent show multiple batt info dialog
bool sd_info_up = false; // to prevent show multiple sd info dialog
NavigationView& nav_;
@@ -303,6 +303,7 @@ class SystemStatusView : public View {
void on_title();
void refresh();
void on_clk();
void on_sd_card();
void on_tx_disabled();
void rtc_battery_workaround();
void on_battery_data(const BatteryStateMessage* msg);
+13 -9
View File
@@ -91,7 +91,7 @@ class SDCardTestThread {
}
private:
static constexpr File::Size write_size = 16384;
static constexpr File::Size write_size = 4096; // will call it 4 times
static constexpr File::Size bytes_to_write = 16 * 1024 * 1024;
static constexpr File::Size bytes_to_read = bytes_to_write;
@@ -160,12 +160,14 @@ class SDCardTestThread {
sizeof(*buffer.get()) / sizeof(lfsr_word_t));
const halrtcnt_t write_start = halGetCounterValue();
const auto result_write = file.write(buffer->data(), buffer->size());
if (result_write.is_error()) {
break;
for (uint8_t i = 0; i < 4; i++) {
const auto result_write = file.write(buffer->data(), buffer->size());
if (result_write.is_error()) {
break;
}
_stats.write_bytes += buffer->size();
}
const halrtcnt_t write_end = halGetCounterValue();
_stats.write_bytes += buffer->size();
_stats.write_count++;
const halrtcnt_t write_duration = write_end - write_start;
@@ -202,12 +204,14 @@ class SDCardTestThread {
const halrtcnt_t test_start = halGetCounterValue();
while (!chThdShouldTerminate() && (_stats.read_bytes < bytes_to_read)) {
const halrtcnt_t read_start = halGetCounterValue();
const auto result_read = file.read(buffer->data(), buffer->size());
if (result_read.is_error()) {
break;
for (uint8_t i = 0; i < 4; i++) {
const auto result_read = file.read(buffer->data(), buffer->size());
if (result_read.is_error()) {
break;
}
_stats.read_bytes += buffer->size();
}
const halrtcnt_t read_end = halGetCounterValue();
_stats.read_bytes += buffer->size();
_stats.read_count++;
const halrtcnt_t read_duration = read_end - read_start;
@@ -83,7 +83,7 @@ const Color color_sd_card(const sd_card::Status status) {
SDCardStatusView::SDCardStatusView(
const Rect parent_rect)
: Image{parent_rect, &bitmap_sd_card_unknown, detail::color_sd_card_unknown, Theme::getInstance()->bg_dark->background} {
: ImageButton{parent_rect, &bitmap_sd_card_unknown, detail::color_sd_card_unknown, Theme::getInstance()->bg_dark->background} {
}
void SDCardStatusView::on_show() {
@@ -101,7 +101,7 @@ void SDCardStatusView::paint(Painter& painter) {
set_bitmap(&detail::bitmap_sd_card(status));
set_foreground(detail::color_sd_card(status));
Image::paint(painter);
ImageButton::paint(painter);
}
void SDCardStatusView::on_status(const sd_card::Status) {
@@ -26,10 +26,11 @@
#include "theme.hpp"
#include "ui_widget.hpp"
#include "sd_card.hpp"
#include <functional>
namespace ui {
class SDCardStatusView : public Image {
class SDCardStatusView : public ImageButton {
public:
SDCardStatusView(const Rect parent_rect);
@@ -33,6 +33,7 @@ extern "C" {
#include <vector>
static Thread* thread_usb_event = NULL;
usb_serial_input_handler_t usb_serial_active_input_handler = nullptr;
struct usb_bulk_buffer_t {
uint8_t* data;
@@ -110,20 +111,27 @@ void complete_host_to_device_transfer() {
return;
chSysLock();
for (unsigned int i = 0; i < transfer_data->length; i++) {
msg_t ret;
do {
ret = chIQPutI(&SUSBD1.iqueue, transfer_data->data[i]);
if (usb_serial_active_input_handler) {
// An input handler is active: route raw bytes directly to it
chSysUnlock();
usb_serial_active_input_handler(transfer_data->data, transfer_data->length);
} else {
// Normal operation: feed bytes into the shell iqueue
for (unsigned int i = 0; i < transfer_data->length; i++) {
msg_t ret;
do {
ret = chIQPutI(&SUSBD1.iqueue, transfer_data->data[i]);
if (ret == Q_FULL) {
chSysUnlock();
chThdSleepMilliseconds(1); // wait for shell thread when buffer is full
chSysLock();
}
if (ret == Q_FULL) {
chSysUnlock();
chThdSleepMilliseconds(1); // wait for shell thread when buffer is full
chSysLock();
}
} while (ret == Q_FULL);
} while (ret == Q_FULL);
}
chSysUnlock();
}
chSysUnlock();
usb_bulk_buffer_spare.push(transfer_data);
}
@@ -24,6 +24,7 @@
#include "ch.h"
#include "hal.h"
#include "usb_serial_device_to_host.h"
#define USB_BULK_BUFFER_SIZE 64
@@ -33,4 +34,46 @@ void serial_bulk_transfer_complete(void* user_data, unsigned int bytes_transferr
void schedule_host_to_device_transfer();
void complete_host_to_device_transfer();
typedef void (*usb_serial_input_handler_t)(const uint8_t* data, size_t len);
/**
* Global storing the currently active USB serial input handler.
* When non-null, all incoming USB bytes are routed to this handler
* instead of the normal shell iqueue.
*
* Managed via UsbSerialInputHandler RAII below do not write directly.
*/
extern usb_serial_input_handler_t usb_serial_active_input_handler;
/**
* RAII wrapper that registers a USB serial input handler on construction
* and automatically deregisters it on destruction.
*
* Only one handler may be active at a time.
*
* The handler is called from the USB transfer completion context (main event
* loop thread). It must return quickly; blocking will stall USB/UI servicing.
* The data pointer is only valid for the duration of the call.
*
* Use write() to send data to the host. By default bytes are dropped if the
* TX queue is full (TIME_IMMEDIATE); pass a timeout in ticks to block instead.
*/
class UsbSerialInputHandler {
public:
UsbSerialInputHandler() = delete;
explicit UsbSerialInputHandler(usb_serial_input_handler_t handler) {
usb_serial_active_input_handler = handler;
}
~UsbSerialInputHandler() {
usb_serial_active_input_handler = nullptr;
}
UsbSerialInputHandler(const UsbSerialInputHandler&) = delete;
UsbSerialInputHandler& operator=(const UsbSerialInputHandler&) = delete;
// Send bytes to the USB host. timeout defaults to TIME_IMMEDIATE (drop if full).
void write(const uint8_t* data, size_t len, systime_t timeout = TIME_IMMEDIATE) {
chOQWriteTimeout(&SUSBD1.oqueue, data, len, timeout);
}
};
#endif
+15
View File
@@ -535,6 +535,21 @@ set(MODE_CPPSRC
)
DeclareTargets(PEPI epirb_rx)
### EPIRB TX
set(MODE_CPPSRC
proc_epirb_tx.cpp
)
DeclareTargets(PEPT epirb_tx)
### P25 TX
set(MODE_CPPSRC
proc_p25_tx.cpp
)
DeclareTargets(P25T p25_tx)
### NRF RX
+25
View File
@@ -102,6 +102,31 @@ void BasebandThread::run() {
buffer_c8_t buffer{
buffer_tmp.p, buffer_tmp.count, sampling_rate_};
#ifdef PRALINE
/*
* Software RSSI: Copy 8 I/Q samples spread across buffer.
*
* Just pack and copy - no computation here.
* rssi_thread does __SMUAD power and rssi calculation.
* 8 samples avoids zero-crossing artifacts.
*/
if (direction_ == baseband::Direction::Receive && buffer_tmp.count >= 32) {
const size_t step = buffer_tmp.count / 8;
for (size_t i = 0; i < 8; i++) {
const size_t idx = i * step + (step / 2);
const auto sample = buffer_tmp.p[idx];
// Pack into 32 bits: Q in high 16 bits, I in low 16 bits, the safe approach:
// 1. Cast to uint16_t to capture the raw 16-bit pattern (e.g., -1 becomes 0xFFFF)
// 2. OR them together. The uint16_t will be promoted to uint32_t cleanly.
// This is accomplished with the specific hardware instruction designed
// for this __PKHBT (Pack Halfword Bottom Top).
shared_memory.software_rssi_iq[i] = __PKHBT(sample.real(), sample.imag(), 16);
}
}
#endif
if (shared_memory.request_m4_performance_counter == 0x02) {
uint8_t max = shared_memory.m4_performance_counter;
for (size_t i = 0; i < buffer_tmp.count; i++) {
+4 -4
View File
@@ -76,13 +76,13 @@ class NarrowbandAMAudio : public BasebandProcessor {
SpectrumCollector channel_spectrum{};
/* NB: Threads should be the last members in the class definition. */
#ifndef PRALINE
BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive};
RSSIThread rssi_thread{};
#else
#ifdef PRALINE
BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive,
/*auto_start*/ false}; // Phase 2: Manual start
RSSIThread rssi_thread{/*auto_start*/ false}; // Phase 2: Manual start
#else
BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive};
RSSIThread rssi_thread{};
#endif
void configure(const AMConfigureMessage& message);
+2 -1
View File
@@ -27,7 +27,8 @@
#include "event_m4.hpp"
#include "stdio.h"
APRSRxProcessor::APRSRxProcessor() {
}
void APRSRxProcessor::execute(const buffer_c8_t& buffer) {
// This is called at 3072000 / 2048 = 1500Hz
+1
View File
@@ -74,6 +74,7 @@ static uint16_t crc_ccitt_tab[256] = {
class APRSRxProcessor : public BasebandProcessor {
public:
APRSRxProcessor();
void execute(const buffer_c8_t& buffer) override;
void on_message(const Message* const message) override;
+191
View File
@@ -0,0 +1,191 @@
/*
* 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 "proc_epirb_tx.hpp"
#include "portapack_shared_memory.hpp"
#include "sine_table_int8.hpp"
#include "event_m4.hpp"
#include <cstdint>
#include <cstring>
/**
* Processing method for this processor
*/
void EPIRBTXProcessor::execute(const buffer_c8_t& buffer) {
if (!configured) return;
// Iterate on each sample of the buffer
for (size_t i = 0; i < buffer.count; i++) {
if (end_of_transmission) {
// Stop transmission
configured = false;
end_of_transmission = false;
txprogress_message.done = true;
shared_memory.application_queue.push(txprogress_message);
}
if (mode_bpsk) {
// BPSK Manchester beacon signal
if (bpsk_pre_count < config_pre_count) {
// Pre-count state: send a negative phase carrier during pre-count
bpsk_pre_count++;
re = i_neg;
im = q_neg;
} else if (bpsk_post_count > 0) {
// Post-count: send a negative phase carrier during post-count
bpsk_post_count++;
re = i_neg;
im = q_neg;
if (bpsk_post_count >= config_post_count) {
// End transmission here
byte_index = 0;
bpsk_post_count = 0;
bpsk_pre_count = 0;
end_of_transmission = true;
}
} else {
if (sample_counter == 0 && manchester_half == false) {
if (bit_index == 0) {
// Read current byte
current_byte = frame_data[byte_index];
// Move to next byte
byte_index++;
}
// Get current bit
current_bit = (current_byte >> (7 - bit_index)) & 0x01;
}
// Manchester encoding
if (current_bit == 1) {
// 1 = falling signal
if (manchester_half == false) {
re = i_pos;
im = q_pos;
} else {
re = i_neg;
im = q_neg;
}
} else {
// 0 = rising signal
if (manchester_half == false) {
re = i_neg;
im = q_neg;
} else {
re = i_pos;
im = q_pos;
}
}
// Move to next sample
sample_counter++;
if (sample_counter >= samples_per_halfbit) {
// Move to next half-bit
sample_counter = 0;
manchester_half = !manchester_half;
// Next bit after two half bits
if (manchester_half == false) {
// Move to next bit
bit_index++;
if (bit_index >= 8) {
// End of byte
bit_index = 0;
if (byte_index >= frame_data_len) {
// End of frame => move to post-count
bpsk_post_count = 1;
}
}
}
}
}
} else {
// AM 127.5 MHz sine sweep
// ---- 3 Hz Sweep ----
sweep_phase += sweep_inc;
uint8_t sweep_index = (sweep_phase & 0xFF000000) >> 24;
int8_t sweep = sine_table_i8[sweep_index]; // -128..127
// Audio frequency based on sweep
int32_t audio_freq = center_freq + sweep * freq_dev;
// ---- Audio signal (sine wave) ----
uint32_t audio_inc = audio_freq * freq_scale;
audio_phase += audio_inc;
uint8_t audio_index = (audio_phase & 0xFF000000) >> 24;
int8_t audio = sine_table_i8[audio_index];
// ---- AM ----
// Double Side Band modulation with modulation index of ~80% (100/128) + offset (74)
int16_t amplitude = 74 + ((100 * audio) >> 7); // 1/128 via shift
if (amplitude > 127) amplitude = 127;
if (amplitude < -128) amplitude = -128;
re = (int8_t)amplitude;
im = 0;
}
buffer.p[i] = {re, im};
}
};
void EPIRBTXProcessor::on_message(const Message* const msg) {
// Configure the processor
switch (msg->id) {
case Message::ID::EPIRBTXData: {
const auto message = *reinterpret_cast<const EPIRBTXDataMessage*>(msg);
// Check transmission mode
mode_bpsk = message.mode_bpsk;
if (mode_bpsk) {
// BPSK mode for 406 frame
config_pre_count = message.pre_count;
config_post_count = message.post_count;
frame_data_len = message.data_len;
// Get the frame data from the message
memcpy(frame_data, message.data, std::min(frame_data_len, EPIRBTXDataMessage::max_len));
// Init BPSK sequencer
sample_counter = 0;
bpsk_pre_count = 0;
bpsk_post_count = 0;
bit_index = 0;
byte_index = 0;
current_byte = 0;
current_bit = 0;
} else {
// AM mode for 121.5 signal => init AM sequencer
sweep_phase = 0;
audio_phase = 0;
}
// Tell the processor to start
configured = true;
} break;
default:
break;
}
}
int main() {
EventDispatcher event_dispatcher{std::make_unique<EPIRBTXProcessor>()};
event_dispatcher.run();
return 0;
}
+111
View File
@@ -0,0 +1,111 @@
/*
* 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 __PROC_EPIRB_TX_H__
#define __PROC_EPIRB_TX_H__
#include "baseband_processor.hpp"
#include "baseband_thread.hpp"
#include "portapack_shared_memory.hpp"
#include "tonesets.hpp"
#include <cmath>
/**
* Processor used by epirb_tx app to simulate a COSPAS/SARSAT emergency beacon
* The processor will alternatively:
* - Send a 406 MHz Manchester encoded BPSK signal containing the beacon information
* - Send a 127.5 MHz AM distress audio signal
*/
class EPIRBTXProcessor : public BasebandProcessor {
public:
void execute(const buffer_c8_t& buffer) override;
void on_message(const Message* const msg) override;
private:
// True when the processor has received a configuration message from the app
bool configured{false};
// True when in BPSK transmission mode, false for AM transmission mode
bool mode_bpsk{false};
// True when the transmission has to be stopped (e.g. at the end of a frame)
bool end_of_transmission{};
// I/Q values for current sample (ranging from -128 to 127)
int8_t re{0}, im{0};
// Configured pre-count value: used to generate a carrier for config_pre_count samples before starting a frame
uint32_t config_pre_count = 0;
// Configured post-count value: used to continue the carrier for config_post_count samples after the end of a frame
uint32_t config_post_count = 0;
// Data of the frame to send in BPSK mode
uint8_t frame_data[18]{0};
// Size of the frame to send in BPSK mode
uint8_t frame_data_len = 0;
// BPSK parameters: Target phase +/-63° as per COSPAS/SARSAT specifications
static constexpr float phase_rad = 63.0f * M_PI / 180.0f;
// I/Q values for BPSK (positive phase and negative phase)
int8_t i_pos = (int8_t)(cos(phase_rad) * 127);
int8_t q_pos = (int8_t)(sin(phase_rad) * 127);
int8_t i_neg = i_pos;
int8_t q_neg = -q_pos;
// COSPAS/SARSAT signal is manchester (2 states per bit) encoded 400 bit/sec
static const uint32_t samples_per_halfbit = TONES_SAMPLERATE / 400 / 2;
// Sequencer state for BPSK
uint32_t sample_counter = 0;
uint32_t bpsk_pre_count = 0;
uint32_t bpsk_post_count = 0;
// Bit position in current byte
uint32_t bit_index = 0;
// Byte position in current frame
uint32_t byte_index = 0;
// Value of the current byte
uint8_t current_byte = 0;
// Value of the current bit
uint8_t current_bit = 0;
// Position in the current manchester bit
bool manchester_half = false; // false = first half
// 127.5 AM signal parameters
static const uint32_t sweep_rate = 3; // 3 Hz
static const uint32_t f_min = 300; // Sweep min frequency (Hz)
static const uint32_t f_max = 1600; // Sweep max frequency (Hz)
static const uint32_t freq_span = f_max - f_min;
// Frequency
static const uint32_t freq_scale = (1ULL << 32) / TONES_SAMPLERATE;
static const int32_t center_freq = f_min + (freq_span / 2);
static const int32_t freq_dev = freq_span / 256;
// Increments
static const uint32_t sweep_inc = sweep_rate * freq_scale;
// Phase accumulators for sweep and audio
uint32_t sweep_phase = 0;
uint32_t audio_phase = 0;
TXProgressMessage txprogress_message{};
/* NB: Threads should be the last members in the class definition. */
BasebandThread baseband_thread{TONES_SAMPLERATE, this, baseband::Direction::Transmit};
};
#endif
+93
View File
@@ -0,0 +1,93 @@
/*
* Copyright (C) 2024 PortaPack-MAYHEM Contributors
*
* 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 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "proc_p25_tx.hpp"
#include "portapack_shared_memory.hpp"
#include "sine_table_int8.hpp"
#include "event_m4.hpp"
#include <cstdint>
// Gray-coded dibit → FM deviation level:
// dibit 0 → +1 unit (+600 Hz)
// dibit 1 → +3 units (+1800 Hz)
// dibit 2 → -1 unit (-600 Hz)
// dibit 3 → -3 units (-1800 Hz)
static constexpr int8_t dibit_to_level[4] = {1, 3, -1, -3};
void P25TxProcessor::execute(const buffer_c8_t& buffer) {
if (!configured) {
// Zero output - never emit garbage while idle
for (size_t i = 0; i < buffer.count; i++)
buffer.p[i] = {0, 0};
return;
}
for (size_t i = 0; i < buffer.count; i++) {
if (sample_count == 0) {
if (dibit_index >= frame_length) {
// Frame complete - notify A7 and stop
txprogress_message.done = true;
shared_memory.application_queue.push(txprogress_message);
configured = false;
for (size_t j = i; j < buffer.count; j++)
buffer.p[j] = {0, 0};
return;
}
}
// Current dibit → frequency level
uint8_t dibit = shared_memory.bb_data.data[dibit_index] & 0x03;
int8_t level = dibit_to_level[dibit];
// Accumulate FM phase at ±level × base_phase_step per sample
phase += (uint32_t)((int32_t)level * (int32_t)base_phase_step);
int8_t re = sine_table_i8[(phase + 0x40000000) >> 24]; // cos
int8_t im = sine_table_i8[phase >> 24]; // sin
buffer.p[i] = {re, im};
sample_count++;
if (sample_count >= P25_SAMPLES_PER_DIBIT) {
sample_count = 0;
dibit_index++;
}
}
}
void P25TxProcessor::on_message(const Message* const msg) {
if (msg->id == Message::ID::P25TxConfigure) {
const auto& m = *reinterpret_cast<const P25TxConfigureMessage*>(msg);
if (m.frame_length == 0) {
configured = false;
return;
}
frame_length = m.frame_length;
dibit_index = 0;
sample_count = 0;
txprogress_message.done = false;
configured = true;
}
}
int main() {
EventDispatcher event_dispatcher{std::make_unique<P25TxProcessor>()};
event_dispatcher.run();
return 0;
}
+55
View File
@@ -0,0 +1,55 @@
/*
* Copyright (C) 2024 PortaPack-MAYHEM Contributors
*
* 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 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef __PROC_P25_TX_H__
#define __PROC_P25_TX_H__
#include "baseband_processor.hpp"
#include "baseband_thread.hpp"
#include "message.hpp"
// P25 C4FM: 4800 baud, 2.4 Msps = 500 samples per dibit
#define P25_SAMPLERATE 2400000
#define P25_SAMPLES_PER_DIBIT 500
class P25TxProcessor : public BasebandProcessor {
public:
void execute(const buffer_c8_t& buffer) override;
void on_message(const Message* const msg) override;
private:
bool configured{false};
uint16_t frame_length{0}; // total dibits in frame
uint16_t dibit_index{0}; // current dibit being transmitted
uint32_t sample_count{0}; // samples emitted for current dibit
// FM phase accumulator (32-bit wrapping)
uint32_t phase{0};
// Phase step per sample per deviation unit (600 Hz at 2.4 Msps)
// = round(600 * 2^32 / 2400000) = 1073742
static constexpr uint32_t base_phase_step{1073742};
TXProgressMessage txprogress_message{};
/* NB: Threads should be the last members in the class definition. */
BasebandThread baseband_thread{P25_SAMPLERATE, this, baseband::Direction::Transmit};
};
#endif /* __PROC_P25_TX_H__ */
+184
View File
@@ -28,6 +28,110 @@
#include "message.hpp"
#include "portapack_shared_memory.hpp"
#ifdef PRALINE
/*
* =============================================================================
* PRALINE Software RSSI
* =============================================================================
*
* Architecture:
* - baseband_thread: Copies 8 packed I/Q samples (no computation)
* - rssi_thread: __SMUAD power calc, avg power, LUT, trackers
*
* All DSP happens here to keep baseband_thread minimal.
*/
/*
* Power-to-RSSI Lookup Table (32 entries)
*
* Maps I²+Q² power to RSSI (0-255) using logarithmic scaling.
* For 8-bit I/Q: max |I|=|Q|=127, so max I²+Q² = 32258
*
* For example, the formula: rssi = 32 * log2((index * 8) + 1), clamped to 255
* where using >> 8 scaling provide 4x more sensitive than >> 10:
*
* The trade-off: more sensitivity means the meter saturates (hits max) at lower signal levels.
* We'll want the bar to be mid-range at typical signal levels, not pegged at max.
* - Index 0: power 0-255 (I/Q magnitude ~11)
* - Index 31: power 7936+ (I/Q magnitude ~63+)
*/
static constexpr uint8_t power_to_rssi_lut[32] = {
0, 101, 130, 148, 161, 171, 179, 186,
192, 197, 202, 206, 210, 213, 217, 220,
222, 225, 227, 230, 232, 234, 236, 238,
240, 241, 243, 244, 246, 247, 249, 255};
/*
* Convert power to RSSI using 32-entry LUT.
* If more sensitivity thank >> 10 is needed:
* use power >= 8192 & >> 8, yields 4x more sensitivity than >> 10.
* use power >= 4096 & >> 7, yields 8x more sensitivity than >> 10
* use power >= 2048 & >> 6, yields 16x more sensitivity than >> 10
* use power >= 1024 & >> 5, yields 32x more sensitivity than >> 10
*/
static inline uint8_t power_to_rssi(uint32_t power) {
// uint8_t index = (power >= 32768) ? 31 : static_cast<uint8_t>(power >> 10);
uint8_t index = (power >= 2048) ? 31 : static_cast<uint8_t>(power >> 6);
return power_to_rssi_lut[index];
}
/*
* IIR Smoothing Filter (Exponential Moving Average)
* Formula: smooth = (current + 7*smooth) / 8 (α = 1/8)
*/
class IIRFilter {
public:
uint8_t update(uint8_t current) {
uint16_t current_q8 = static_cast<uint16_t>(current) << 8;
smooth_q8_ = (current_q8 + 7 * smooth_q8_) >> 3;
return static_cast<uint8_t>(smooth_q8_ >> 8);
}
private:
uint16_t smooth_q8_ = 0;
};
/*
* Running min tracker with decay.
* Instantly captures new minimums, slowly decays upward.
*/
class MinTracker {
public:
uint8_t update(uint8_t current) {
if (current < min_) {
min_ = current;
} else {
// Slow decay upward (α = 1/16)
min_ = min_ + ((current - min_) >> 4);
}
return min_;
}
private:
uint8_t min_ = 255;
};
/*
* Running max tracker with decay.
* Instantly captures new maximums, slowly decays downward.
*/
class MaxTracker {
public:
uint8_t update(uint8_t current) {
if (current > max_) {
max_ = current;
} else {
// Slow decay downward (α = 1/16)
max_ = max_ - ((max_ - current) >> 4);
}
return max_;
}
private:
uint8_t max_ = 0;
};
#endif // PRALINE
WORKING_AREA(rssi_thread_wa, 128);
Thread* RSSIThread::thread = nullptr;
@@ -54,6 +158,85 @@ void RSSIThread::start() {
}
void RSSIThread::run() {
#ifdef PRALINE
/*
* PRALINE (HackRF Pro): Software RSSI from I/Q samples
*
* Hardware ADC-based RSSI doesn't work on HackRF Pro.
*
* baseband_thread copies 8 packed I/Q samples.
* We use __SMUAD to compute power, then apply LUT and
* maintain running stats.
*/
IIRFilter avg_filter;
MinTracker min_tracker;
MaxTracker max_tracker;
RSSIStatistics stats{};
uint32_t accumulator = 0;
uint32_t sample_count = 0;
constexpr uint32_t samples_per_report = 50; // ~10Hz reporting at 2ms poll
while (!chThdShouldTerminate()) {
chThdSleepMilliseconds(2); // Poll at ~500Hz
/*
* SIMD-accelerated I/Q power calculation for Cortex-M4.
*
* Uses __SMUAD (Signed Multiply Accumulate Dual) instruction to compute
* sum of products of packed halfwords: (a0*a0) + (a1*a1)
*
* Processes complex samples per iteration, computing I²+Q² with SIMD.
* SIMD-accelerated I/Q power calculation for Cortex-M4.
* ~4x faster than scalar loop.
* Sum power from all 8 samples (more stable than peak).
*
* __SMUAD(val, val) computes:
* (low16 * low16) + (high16 * high16) = I² + Q²
*/
uint32_t total_power = 0;
for (size_t i = 0; i < 8; i++) {
const uint32_t packed = shared_memory.software_rssi_iq[i];
total_power += __SMUAD(packed, packed);
}
// Average the 8 samples for min, and avg power
const uint32_t avg_power = total_power >> 3;
// Convert to RSSI scale (0-255) via LUT
const uint8_t avg_rssi = power_to_rssi(avg_power);
// Update running trackers
const uint8_t smooth_min = min_tracker.update(avg_rssi);
const uint8_t smooth_avg = avg_filter.update(avg_rssi);
const uint8_t smooth_max = max_tracker.update(avg_rssi);
// Accumulate for periodic report
accumulator += smooth_avg;
sample_count++;
// Report periodically
if (sample_count >= samples_per_report) {
stats.min = smooth_min;
stats.max = smooth_max;
stats.accumulator = accumulator;
stats.count = sample_count;
const RSSIStatisticsMessage message{stats};
shared_memory.application_queue.push(message);
// Reset accumulator for next period
accumulator = 0;
sample_count = 0;
}
}
#else
/* HackRF One: Use hardware ADC-based RSSI */
rf::rssi::init();
rf::rssi::dma::allocate(4, 400);
@@ -77,4 +260,5 @@ void RSSIThread::run() {
rf::rssi::stop();
rf::rssi::dma::free();
#endif
}
@@ -542,6 +542,50 @@ static const std::array<scu_setup_t, 9> pins_setup_r9 { {
#endif
#ifdef PRALINE
static const std::array<scu_setup_t, 30> pins_setup_portapack { {
{ 2, 0, scu_config_normal_drive_t { .mode=4, .epd=0, .epun=1, .ehs=0, .ezi=1, .zif=0 } }, /* U0_TXD: PortaPack P2_0/IO_STBX */
{ 2, 1, scu_config_normal_drive_t { .mode=4, .epd=0, .epun=1, .ehs=0, .ezi=1, .zif=0 } }, /* U0_RXD: PortaPack P2_1/ADDR */
{ 2, 3, scu_config_normal_drive_t { .mode=4, .epd=0, .epun=1, .ehs=0, .ezi=1, .zif=0 } }, /* I2C1_SDA: PortaPack P2_3/LCD_TE */
{ 2, 4, scu_config_normal_drive_t { .mode=4, .epd=0, .epun=1, .ehs=0, .ezi=1, .zif=0 } }, /* I2C1_SCL: PortaPack P2_4/LCD_RDX */
{ 2, 8, scu_config_normal_drive_t { .mode=1, .epd=0, .epun=0, .ehs=0, .ezi=1, .zif=1 } }, /* P2_8: 10K PD, BOOT2, DFU switch, PortaPack P2_8/<unused> */
{ 2, 9, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=1, .zif=0 } }, /* P2_9: 10K PD, BOOT3, PortaPack P2_9/LCD_WRX */
{ 2, 13, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=1, .zif=0 } }, /* P2_13: PortaPack P2_13/DIR */
{ 7, 0, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=1, .zif=0 } }, /* GPIO3_8: PortaPack GPIO3_8(IO) */
{ 7, 1, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=1, .zif=0 } }, /* GPIO3_9: PortaPack GPIO3_9(IO) */
{ 7, 2, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=1, .zif=0 } }, /* GPIO3_10: PortaPack GPIO3_10(IO) */
{ 7, 3, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=1, .zif=0 } }, /* GPIO3_11: PortaPack GPIO3_11(IO) */
{ 7, 4, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=1, .zif=0 } }, /* GPIO3_12: PortaPack GPIO3_12(IO) */
{ 7, 5, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=1, .zif=0 } }, /* GPIO3_13: PortaPack GPIO3_13(IO) */
{ 7, 6, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=1, .zif=0 } }, /* GPIO3_14: PortaPack GPIO3_14(IO) */
{ 7, 7, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=1, .ehs=0, .ezi=1, .zif=0 } }, /* GPIO3_15: PortaPack GPIO3_15(IO) */
/* PortaPack: Audio */
{ 3, 0, scu_config_normal_drive_t { .mode=2, .epd=0, .epun=0, .ehs=0, .ezi=1, .zif=0 } }, /* I2S0_TX_SCK: PortaPack I2S0_TX_SCK(I) */
{ 3, 1, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=0, .ehs=0, .ezi=1, .zif=0 } }, /* I2S0_RX_WS: PortaPack I2S0_TX_WS(I). Input enabled to fold back into RX. */
{ 3, 2, scu_config_normal_drive_t { .mode=0, .epd=0, .epun=0, .ehs=0, .ezi=0, .zif=0 } }, /* I2S0_RX_SDA: PortaPack I2S0_TX_SDA(I) */
{ 24, 2, scu_config_normal_drive_t { .mode=6, .epd=1, .epun=1, .ehs=0, .ezi=0, .zif=0 } }, /* I2S0_TX_CLK: PortaPack I2S0_TX_MCLK */
/* PortaPack: SD card socket */
{ 24, 0, scu_config_normal_drive_t { .mode=4, .epd=1, .epun=1, .ehs=0, .ezi=1, .zif=1 } }, /* SD_CLK: PortaPack SD.CLK, enable input buffer for timing feedback? */
{ 1, 6, scu_config_normal_drive_t { .mode=7, .epd=0, .epun=0, .ehs=0, .ezi=1, .zif=1 } }, /* SD_CMD: PortaPack SD.CMD(IO) */
{ 1, 9, scu_config_normal_drive_t { .mode=7, .epd=0, .epun=0, .ehs=0, .ezi=1, .zif=1 } }, /* SD_DAT0: PortaPack SD.DAT0(IO) */
{ 1, 10, scu_config_normal_drive_t { .mode=7, .epd=0, .epun=0, .ehs=0, .ezi=1, .zif=1 } }, /* SD_DAT1: PortaPack SD.DAT1(IO) */
{ 1, 11, scu_config_normal_drive_t { .mode=7, .epd=0, .epun=0, .ehs=0, .ezi=1, .zif=1 } }, /* SD_DAT2: PortaPack SD.DAT2(IO) */
{ 1, 12, scu_config_normal_drive_t { .mode=7, .epd=0, .epun=0, .ehs=0, .ezi=1, .zif=1 } }, /* SD_DAT3: PortaPack SD.DAT3(IO) */
{ 1, 13, scu_config_normal_drive_t { .mode=7, .epd=0, .epun=0, .ehs=0, .ezi=1, .zif=0 } }, /* SD_CD: PortaPack SD.CD(O) */
// for pro touch
// those pins should choose reserved function mode
// and the behavior same as hackrf one(YN YP XN XP)
{ 11, 6, scu_config_normal_drive_t { .mode=7, .epd=0, .epun=0, .ehs=0, .ezi=0, .zif=0 } }, /* PB_6: ADC0_0 yp*/
{ 4, 5, scu_config_normal_drive_t { .mode=3, .epd=0, .epun=0, .ehs=0, .ezi=0, .zif=0 } }, /* P4_5: ADC0_2 yn*/
{ 4, 4, scu_config_normal_drive_t { .mode=3, .epd=0, .epun=0, .ehs=0, .ezi=0, .zif=0 } }, /* P4_4: ADC0_5 xp */
{ 15, 4, scu_config_normal_drive_t { .mode=3, .epd=0, .epun=0, .ehs=0, .ezi=0, .zif=0 } }, /* PF_4: ADC0_6 xn*/
} };
#else
static const std::array<scu_setup_t, 26> pins_setup_portapack { {
{ 2, 0, scu_config_normal_drive_t { .mode=4, .epd=0, .epun=1, .ehs=0, .ezi=1, .zif=0 } }, /* U0_TXD: PortaPack P2_0/IO_STBX */
{ 2, 1, scu_config_normal_drive_t { .mode=4, .epd=0, .epun=1, .ehs=0, .ezi=1, .zif=0 } }, /* U0_RXD: PortaPack P2_1/ADDR */
@@ -574,6 +618,7 @@ static const std::array<scu_setup_t, 26> pins_setup_portapack { {
{ 1, 12, scu_config_normal_drive_t { .mode=7, .epd=0, .epun=0, .ehs=0, .ezi=1, .zif=1 } }, /* SD_DAT3: PortaPack SD.DAT3(IO) */
{ 1, 13, scu_config_normal_drive_t { .mode=7, .epd=0, .epun=0, .ehs=0, .ezi=1, .zif=0 } }, /* SD_CD: PortaPack SD.CD(O) */
} };
#endif
static const std::array<scu_setup_t, 6> pins_setup_spifi { {
{ 3, 3, scu_config_normal_drive_t { .mode=3, .epd=0, .epun=1, .ehs=1, .ezi=1, .zif=1 } }, /* SPIFI_SCK: W25Q80BV.CLK(I), enable input buffer for timing feedback */
@@ -867,33 +912,8 @@ extern "C" void __late_init(void) {
* @todo Add your board-specific code, if any.
*/
extern "C" void boardInit(void) {
#ifndef PRALINE
/* Detect HackRF variant */
/* 1. Perform Standard Initialization first */
/* This configures VAA power, LED pins, and detects board revision */
/* Let detect_hackrf_r9() run - don't force for PRALINE */
hackrf_r9 = detect_hackrf_r9();
/* Configure variant-dependent pins. */
if (hackrf_r9) {
setup_gpios(gpio_setup_r9);
setup_pins(pins_setup_r9);
} else {
setup_gpios(gpio_setup_og);
setup_pins(pins_setup_og);
}
/* 2. Turn on VAA (Critical for Radio/Transceiver) */
vaa_power_on();
/* 3. Handle VAA Enable Pin Latching */
if (hackrf_r9) {
LPC_GPIO->W2[9] = 1;
} else {
LPC_GPIO->W3[6] = 1;
}
/* 4. HackRF Pro Specific: Initialize and Load FPGA */
#else
#ifdef PRALINE
/* HackRF Pro Specific: Initialize and Load FPGA */
hackrf_r9 = false;
/* Enable 3.3V aux power - P6_7 = GPIO5[15], active LOW (clear to enable) */
LPC_SCU->SFSP[6][7] = 0xF4; /* SCU_GPIO_FAST | FUNCTION4 */
@@ -1048,6 +1068,32 @@ extern "C" void boardInit(void) {
LPC_GPIO->SET[2] = (1 << 1) | (1 << 2) | (1 << 8);
{ volatile uint32_t delay = 200000; while(delay--); }
#else
/* Detect HackRF variant */
/* 1. Perform Standard Initialization first */
/* This configures VAA power, LED pins, and detects board revision */
/* Let detect_hackrf_r9() run - don't force for PRALINE */
hackrf_r9 = detect_hackrf_r9();
/* Configure variant-dependent pins. */
if (hackrf_r9) {
setup_gpios(gpio_setup_r9);
setup_pins(pins_setup_r9);
} else {
setup_gpios(gpio_setup_og);
setup_pins(pins_setup_og);
}
/* 2. Turn on VAA (Critical for Radio/Transceiver) */
vaa_power_on();
/* 3. Handle VAA Enable Pin Latching */
if (hackrf_r9) {
LPC_GPIO->W2[9] = 1;
} else {
LPC_GPIO->W3[6] = 1;
}
#endif
}
@@ -3,6 +3,7 @@
// Check if PRALINE was passed from CMake
#ifdef PRALINE
#include "fpga_bridge.h"
// Necessary headers
#include "lz4_blk.h"
@@ -120,6 +121,27 @@
#define FPGA_CDONE_PIN 14
#define FPGA_SPI_CS_PORT 2
#define FPGA_SPI_CS_PIN 10
// ============================================================================
// Canonical Default Values - SINGLE SOURCE OF TRUTH
// ============================================================================
/* Define the canonical RX defaults in ONE place */
#define FPGA_RX_DEFAULT_DC_WIDTH 0x04 /* Typical for 40MHz stability */
#define FPGA_RX_DEFAULT_ADAPT_RATE 0x08 /* Typical for 40MHz stability */
#define FPGA_RX_DEFAULT_DIGITAL_GAIN 0x00 /* No shift initially */
/* Define TX defaults */
#define FPGA_TX_DEFAULT_NCO_CTRL 0x00 /* NCO disabled */
#define FPGA_TX_DEFAULT_INTERP 0x00 /* No interpolation */
#define FPGA_TX_DEFAULT_PHASE_STEP 0x00 /* Zero phase step */
// ============================================================================
// Static Variables - Declare BEFORE use
// ============================================================================
static fpga_mode_t current_mode = FPGA_MODE_OFF;
// Cached register values for debug reads (since reads may require mode switch)
static uint8_t fpga_reg_cache[6] = {0, 0x01, 0x00, 0x00, 0x00, 0x00};
// Context structure for SPIFI-based reading
struct spifi_fpga_read_ctx {
@@ -129,6 +151,10 @@
uint8_t buffer[4096 + 2]; // Compressed block + next size
};
// ============================================================================
// Low-Level Helper Functions
// ============================================================================
// Simple delay loop
static void delay_cycles(volatile uint32_t count) {
while (count--) {
@@ -224,14 +250,19 @@
// FPGA Register Map:
// Reg 1 (CTRL): DC_BLOCK(b0), QUARTER_SHIFT_EN(b1), QUARTER_SHIFT_UP(b2), PRBS(b6), TRIGGER_EN(b7)
// Reg 2 (RX_DECIM): Decimation ratio [2:0]
// Reg 3 (TX_CTRL): NCO_EN(b0)
// Reg 4 (TX_INTRP): Interpolation ratio [2:0]
// Reg 5 (TX_PSTEP): NCO phase step [7:0]
// Reg 3 (RX/TX): RX Digital Shift OR TX NCO Control
// Reg 4 (RX_DC_BLOCK_WIDTH/TX_INTERP) [2:0]
// Reg 5 (RX_DC_ADAPT_RATE/TX_PSTEP) [7:0]
//
// SPI Protocol:
// Read: Send [reg & 0x7F, 0x00, 0x00] -> value in byte 3
// Write: Send [(reg | 0x80), value, 0x00]
// ============================================================================
// SPI Mode Switching
// ============================================================================
// Configure SSP1 for iCE40 FPGA register access (Mode 3, 8-bit)
static void ssp1_set_mode_ice40(void) {
SSP1_CR1_LOCAL = 0; // Disable SSP1
@@ -252,6 +283,11 @@
SSP1_CR1_LOCAL = SSP_CR1_SSE; // Enable SSP1
}
// ============================================================================
// Low-Level SPI Register Access (internal, no mode switch)
// ============================================================================
// Read an FPGA register via SPI
static uint8_t fpga_spi_read(uint8_t reg) {
uint8_t value;
@@ -271,31 +307,10 @@
ssp1_transfer_byte(0x00); // Dummy byte
fpga_cs_high();
}
// Initialize FPGA registers after bitstream load
// This is equivalent to fpga_init() in the reference HackRF firmware
static void fpga_register_init(void) {
// Already in iCE40 mode after programming, so we can directly access registers
// Register 1 (CTRL): Enable DC block (bit 0), disable everything else
// DC_BLOCK is CRITICAL for RX to work!
fpga_spi_write(1, 0x01); // DC_BLOCK = 1
// Register 2 (RX_DECIM): No decimation
fpga_spi_write(2, 0x00);
// Register 3 (TX_CTRL): Disable NCO
fpga_spi_write(3, 0x00);
// Register 4 (TX_INTRP): No interpolation
fpga_spi_write(4, 0x00);
// Register 5 (TX_PSTEP): Zero phase step
fpga_spi_write(5, 0x00);
}
// Cached register values for debug reads (since reads may require mode switch)
static uint8_t fpga_reg_cache[6] = {0, 0x01, 0x00, 0x00, 0x00, 0x00};
// ============================================================================
// Public Debug Functions (switch SPI mode, access register, switch back)
// ============================================================================
// Public function to read FPGA register (callable from C++ application code)
// Switches SPI mode, reads register, switches back
@@ -322,6 +337,139 @@
fpga_reg_cache[reg] = value;
}
// ============================================================================
// Public Register Access (wraps debug functions)
// ============================================================================
uint8_t fpga_register_read(uint8_t reg) {
if (reg == 0 || reg > 5) return 0xFF;
ssp1_set_mode_ice40();
uint8_t val = fpga_spi_read(reg);
ssp1_set_mode_max2831();
fpga_reg_cache[reg] = val;
return val;
}
void fpga_register_write(uint8_t reg, uint8_t value) {
if (reg == 0 || reg > 5) return;
ssp1_set_mode_ice40();
fpga_spi_write(reg, value);
ssp1_set_mode_max2831();
fpga_reg_cache[reg] = value;
}
// ============================================================================
// Mode Management
// ============================================================================
fpga_mode_t fpga_get_mode(void) {
return current_mode;
}
/* fpga_set_mode with consistent values */
void fpga_set_mode(fpga_mode_t mode) {
current_mode = mode;
}
// ============================================================================
// RX Mode Functions (with mode assertion)
// ============================================================================
/* RX decimation */
void fpga_rx_set_decimation(uint8_t ratio) {
if (current_mode != FPGA_MODE_RX) return;
fpga_register_write(FPGA_REG_DECIM, ratio & 0x07);
}
/* RX DC block enable (bit 0 of register 1) */
void fpga_rx_enable_dc_block(bool enable) {
if (current_mode != FPGA_MODE_RX) return;
uint8_t ctrl = fpga_register_read(FPGA_REG_CTRL);
if (enable)
ctrl |= FPGA_CTRL_DC_BLOCK_EN;
else
ctrl &= ~FPGA_CTRL_DC_BLOCK_EN;
fpga_register_write(FPGA_REG_CTRL, ctrl);
}
/* RX Functions with mode assertion */
void fpga_rx_set_digital_gain(uint8_t shift) {
if (current_mode != FPGA_MODE_RX) {
/* Log error or assert - wrong mode! */
return;
}
fpga_register_write(FPGA_REG_SHARED_3, shift & FPGA_RX_GAIN_SHIFT_MASK);
}
void fpga_rx_set_dc_block_width(uint8_t width) {
if (current_mode != FPGA_MODE_RX) return;
fpga_register_write(FPGA_REG_SHARED_4, width & FPGA_RX_DC_WIDTH_MASK);
}
void fpga_rx_set_dc_adapt_rate(uint8_t rate) {
if (current_mode != FPGA_MODE_RX) return;
fpga_register_write(FPGA_REG_SHARED_5, rate);
}
// ============================================================================
// TX Mode Functions (with mode assertion)
// ============================================================================
/* TX Functions with mode assertion */
void fpga_tx_set_nco_enable(bool enable) {
if (current_mode != FPGA_MODE_TX) return;
uint8_t val = fpga_register_read(FPGA_REG_SHARED_3);
if (enable)
val |= FPGA_TX_NCO_EN;
else
val &= ~FPGA_TX_NCO_EN;
fpga_register_write(FPGA_REG_SHARED_3, val);
}
void fpga_tx_set_interpolation(uint8_t ratio) {
if (current_mode != FPGA_MODE_TX) return;
fpga_register_write(FPGA_REG_SHARED_4, ratio & FPGA_TX_INTERP_MASK);
}
void fpga_tx_set_phase_step(uint8_t step) {
if (current_mode != FPGA_MODE_TX) return;
fpga_register_write(FPGA_REG_SHARED_5, step);
}
// ============================================================================
// FPGA Register Initialization (called after bitstream load)
// ============================================================================
// Initialize FPGA registers after bitstream load
// This is equivalent to fpga_init() in the reference HackRF firmware
/* fpga_register_init with consistent values */
static void fpga_register_init(void) {
/* Boot into RX mode */
current_mode = FPGA_MODE_RX;
fpga_spi_write(FPGA_REG_CTRL, FPGA_CTRL_DC_BLOCK_EN);
fpga_spi_write(FPGA_REG_DECIM, 0x00);
fpga_spi_write(FPGA_REG_SHARED_3, FPGA_RX_DEFAULT_DIGITAL_GAIN);
fpga_spi_write(FPGA_REG_SHARED_4, FPGA_RX_DEFAULT_DC_WIDTH);
fpga_spi_write(FPGA_REG_SHARED_5, FPGA_RX_DEFAULT_ADAPT_RATE);
/* Update cache */
fpga_reg_cache[1] = FPGA_CTRL_DC_BLOCK_EN;
fpga_reg_cache[2] = 0x00;
fpga_reg_cache[3] = FPGA_RX_DEFAULT_DIGITAL_GAIN;
fpga_reg_cache[4] = FPGA_RX_DEFAULT_DC_WIDTH;
fpga_reg_cache[5] = FPGA_RX_DEFAULT_ADAPT_RATE;
}
// ============================================================================
// LZ4 Decompression for FPGA Bitstream
// ============================================================================
// SPIFI-based read callback for LZ4 decompression
// Reads from SPIFI memory-mapped address instead of using SPI flash driver
static size_t spifi_fpga_read_block_cb(void* _ctx, uint8_t* out_buffer) {
@@ -415,6 +563,10 @@
return success;
}
// ============================================================================
// Main Initialization Entry Point
// ============================================================================
int fpga_bridge_init(void) {
// Enable SSP1 clock for FPGA programming
// Use PLL1 (204MHz) to match original HackRF - IRC (12MHz) is 17x too slow
@@ -12,15 +12,131 @@ extern "C" {
#endif
#include <stdint.h>
#include <stdbool.h>
#ifdef PRALINE
/* FPGA Register Map Address 0x03 (Dual Purpose) */
#define FPGA_REG_RX_DIGITAL_GAIN 0x03 /* Digital Shift / scaling (RX Mode) */
#define FPGA_REG_TX_CONTROL 0x03 /* NCO_EN and TX flags (TX Mode) */
/* FPGA Register Map Address 0x04 (Shared) */
#define FPGA_REG_RX_DC_BLOCK_WIDTH 0x04 /* Notch filter cutoff (RX Mode) */
#define FPGA_REG_TX_INTERP 0x04 /* Interpolation ratio (TX Mode) */
/* FPGA Register Map Address 0x05 (Shared) */
#define FPGA_REG_RX_DC_ADAPT_RATE 0x05 /* Settle time/Integration (RX Mode) */
#define FPGA_REG_TX_PHASE_STEP 0x05 /* NCO frequency step (TX Mode) */
/*
* Initialize the FPGA - loads bitstream from SPIFI flash
* Returns: 0 on success, non-zero on failure
* FPGA Operating Mode
*/
typedef enum {
FPGA_MODE_OFF = 0,
FPGA_MODE_RX,
FPGA_MODE_TX
} fpga_mode_t;
/*
* FPGA Register Addresses
* Note: Registers 3-5 are dual-purpose (meaning depends on RX/TX mode)
*/
#define FPGA_REG_CTRL 0x01 /* Control register */
#define FPGA_REG_DECIM 0x02 /* Decimation (RX) / unused (TX) */
#define FPGA_REG_SHARED_3 0x03 /* Dual-purpose register */
#define FPGA_REG_SHARED_4 0x04 /* Dual-purpose register */
#define FPGA_REG_SHARED_5 0x05 /* Dual-purpose register */
/*
* Register 1 (CTRL) Bit Definitions
*/
#define FPGA_CTRL_DC_BLOCK_EN (1 << 0) /* DC block enable */
#define FPGA_CTRL_QUARTER_SHIFT_EN (1 << 1) /* Quarter-rate shift enable */
#define FPGA_CTRL_QUARTER_SHIFT_UP (1 << 2) /* Shift direction: 1=up, 0=down */
#define FPGA_CTRL_TX_MODE (1 << 5) /* TX mode indicator (if applicable) */
#define FPGA_CTRL_PRBS_EN (1 << 6) /* PRBS test mode */
#define FPGA_CTRL_TRIGGER_EN (1 << 7) /* External trigger enable */
/*
* Register 3 Dual-Purpose Definitions
*/
/* RX Mode: Digital gain/shift */
#define FPGA_REG3_RX_DIGITAL_GAIN 0x03
#define FPGA_RX_GAIN_SHIFT_MASK 0x0F /* Bits [3:0] - shift amount */
/* TX Mode: NCO control */
#define FPGA_REG3_TX_NCO_CTRL 0x03
#define FPGA_TX_NCO_EN (1 << 0) /* NCO enable */
#define FPGA_TX_NCO_INVERT (1 << 1) /* Invert spectrum */
/*
* Register 4 Dual-Purpose Definitions
*/
/* RX Mode: DC block notch width */
#define FPGA_REG4_RX_DC_WIDTH 0x04
#define FPGA_RX_DC_WIDTH_MASK 0x07 /* Bits [2:0] */
/* TX Mode: Interpolation ratio */
#define FPGA_REG4_TX_INTERP 0x04
#define FPGA_TX_INTERP_MASK 0x07 /* Bits [2:0] */
/*
* Register 5 Dual-Purpose Definitions
*/
/* RX Mode: DC block adaptation rate */
#define FPGA_REG5_RX_DC_RATE 0x05
#define FPGA_RX_DC_RATE_MASK 0xFF /* Bits [7:0] */
/* TX Mode: NCO phase step (frequency) */
#define FPGA_REG5_TX_PHASE_STEP 0x05
#define FPGA_TX_PHASE_STEP_MASK 0xFF /* Bits [7:0] */
/* Export default values so other methods can use them */
#define FPGA_RX_DEFAULT_DIGITAL_GAIN 0x00
#define FPGA_RX_DEFAULT_DC_WIDTH 0x04
#define FPGA_RX_DEFAULT_ADAPT_RATE 0x08
/*
* Core Functions
*/
/* Initialize the FPGA - loads bitstream from SPIFI flash
* Returns: 0 on success, non-zero on failure */
int fpga_bridge_init(void);
/* Set operating mode - MUST be called before using mode-specific functions
* This ensures registers 3-5 are interpreted correctly */
void fpga_set_mode(fpga_mode_t mode);
/* Get current operating mode */
fpga_mode_t fpga_get_mode(void);
/*
* Low-Level Register Access (use with caution)
*/
uint8_t fpga_register_read(uint8_t reg);
void fpga_register_write(uint8_t reg, uint8_t value);
/*
* RX Mode Functions (only valid when mode == FPGA_MODE_RX)
*/
void fpga_rx_set_decimation(uint8_t ratio);
void fpga_rx_set_digital_gain(uint8_t shift);
void fpga_rx_set_dc_block_width(uint8_t width);
void fpga_rx_set_dc_adapt_rate(uint8_t rate);
void fpga_rx_enable_dc_block(bool enable);
/*
* TX Mode Functions (only valid when mode == FPGA_MODE_TX)
*/
void fpga_tx_set_interpolation(uint8_t ratio);
void fpga_tx_set_nco_enable(bool enable);
void fpga_tx_set_phase_step(uint8_t step);
/*
* Debug Functions
*/
/*
* Read an FPGA register via SPI
* reg: Register number (1-5)
@@ -29,9 +145,9 @@ int fpga_bridge_init(void);
* FPGA Register Map:
* Reg 1 (CTRL): DC_BLOCK(b0), QUARTER_SHIFT_EN(b1), QUARTER_SHIFT_UP(b2), PRBS(b6), TRIGGER_EN(b7)
* Reg 2 (RX_DECIM): Decimation ratio [2:0]
* Reg 3 (TX_CTRL): NCO_EN(b0)
* Reg 4 (TX_INTRP): Interpolation ratio [2:0]
* Reg 5 (TX_PSTEP): NCO phase step [7:0]
* Reg 3 (RX/TX): RX Digital Shift OR TX NCO Control
* Reg 4 (RX_DC_BLOCK_WIDTH/TX_INTERP) [2:0]
* Reg 5 (RX_DC_ADAPT_RATE/TX_PSTEP) [7:0]
*/
uint8_t fpga_debug_register_read(uint8_t reg);
@@ -24,7 +24,15 @@ __process_stack_size__ = 0x1000; /* main() stack */
MEMORY
{
flash (rx) : org = 0x00000000, len = LD_FLASH_SIZE /* SPIFI flash @ 0x140????? */
ram (rwx) : org = 0x20000000, len = 64k /* AHB SRAM @ 0x20000000 */
/* HackRF One (Standard): 64KB AHB SRAM @ 0x20000000
* HackRF Pro (Praline): Hardware supports 128KB high-throughput buffers.
* The actual size is controlled by LD_RAM_SIZE (set in rules.cmake).
* AHB SRAM @ 0x20000000 (stacks, data, bss) */
ram (rwx) : org = 0x20000000, len = LD_RAM_SIZE
/* Local SRAM slice for extended heap (0 length on HackRF One) */
ram_local (rwx) : org = LD_M0_LOCAL_HEAP_ORIGIN, len = LD_M0_LOCAL_HEAP_SIZE
}
__ram_start__ = ORIGIN(ram);
@@ -151,5 +159,10 @@ SECTIONS
PROVIDE(end = .);
_end = .;
__heap_base__ = _end;
__heap_end__ = __ram_end__;
/*
* Heap configuration:
* - If ram_local has size > 0: use Local SRAM for heap (larger, Praline)
* - Otherwise: use remaining AHB SRAM after bss (HackRF One)
*/
__heap_base__ = (LD_M0_LOCAL_HEAP_SIZE > 0) ? ORIGIN(ram_local) : _end;
__heap_end__ = (LD_M0_LOCAL_HEAP_SIZE > 0) ? (ORIGIN(ram_local) + LENGTH(ram_local)) : __ram_end__;
@@ -23,12 +23,23 @@ __process_stack_size__ = 0x1000; /* main() stack */
MEMORY
{
flash (rx) : org = 0x00000000, len = 32752 /* Local SRAM @ 0x10080000 */
ram (rwx) : org = 0x10000000, len = 96k /* Local SRAM @ 0x10000000 */
ram_usb (rwx) : org = 0x20008000, len = 32K
/* flash (rx) is actually Local SRAM Bank 2 (shadowed to 0x0 code/rodata) */
flash (rx) : org = 0x00000000, len = LD_M4_FLASH_LEN /* Local SRAM @ 0x10080000 */
/* ram (rwx) is
* Local SRAM Bank 1 @ 0x10000000 (data/bss/stacks)
* Note: On Praline, M0 local heap uses 0x10018000-0x1001FFFF */
ram (rwx) : org = 0x10000000, len = LD_M4_RAM_LEN /* Local SRAM @ 0x10000000 */
/* ram_usb is AHB SRAM Bank 1 @ 0x20008000 (USB buffer)
* HackRF One: 16KB?? originally set to 32KB, so left alone
* Praline: same configuration as HackRF One, while M0 uses all AHB SRAM) */
ram_usb (rwx) : org = 0x20008000, len = LD_USB_RAM_LEN
}
/* Only meaningful if LD_USB_RAM_LEN > 0 */
usb_bulk_buffer = ORIGIN(ram_usb);
__ram_start__ = ORIGIN(ram);
__ram_size__ = LENGTH(ram);
__ram_end__ = __ram_start__ + __ram_size__;
@@ -23,8 +23,15 @@ __process_stack_size__ = 0x1000; /* main() stack */
MEMORY
{
flash : org = 0x00000000, len = 96k /* Local SRAM @ 0x10000000 */
ram : org = 0x10080000, len = 32k /* Local SRAM @ 0x10080000 */
/* Local SRAM Bank 1 shadowed to 0x0 (code)
* HackRF One: 96KB
* Praline: 96KB (M0 local heap uses remaining 32KB) */
flash : org = 0x00000000, len = LD_M4_RAM_LEN /* Local SRAM @ 0x10000000 */
/* Local SRAM Bank 2 @ 0x10080000 (data/bss/stacks)
* HackRF One: ~32KB
* Praline: 72KB */
ram : org = 0x10080000, len = LD_M4_FLASH_LEN /* Local SRAM @ 0x10080000 */
}
__ram_start__ = ORIGIN(ram);
@@ -104,4 +104,47 @@ endif()
set(CMAKE_C_FLAGS "${CFLAGS} ${TOPT}")
set(CMAKE_CXX_FLAGS "${CPPFLAGS} ${TOPT}")
set(CMAKE_AS_FLAGS "${ASFLAGS} ${TOPT}")
set(CMAKE_EXE_LINKER_FLAGS "${LDFLAGS}")
if(BOARD STREQUAL "PRALINE")
# Praline (LPC4330) - Expanded Memory
# AHB SRAM: 32KB + 32KB = 64KB
# Left M0_RAM_SIZE as 64k and USB_RAM_SIZE as 32k for
# backwards consistenct with hackrf one setup.
# Recommend revisit by devs.
# Local SRAM: 128KB (Bank 1) + 72KB (Bank 2)
# Bank 1 can also be allocated as 96KB for M4, and 32KB for M0
set(M0_RAM_SIZE "64k") # AHB SRAM Bank 0 for M0 (stacks, data, bss)
set(M0_LOCAL_HEAP_SIZE "0") # No Local SRAM, could be 32k heap for additional 16-bit IQ
set(M0_LOCAL_HEAP_ORIGIN "0x10020000") # 0x10018000 if allocating 32k heap for M0 after M4's 96KB
set(M4_RAM_SIZE "128k") # Local SRAM Bank 1 Fully allocated to M4 (HackRF One is 96k)
set(M4_FLASH_SIZE "72k") # Local SRAM Bank 2
set(USB_RAM_SIZE "32k") # AHB SRAM shared with M0
set(FLASH_SIZE "4M")
else()
# HackRF One (LPC4320) - Standard Memory
# AHB SRAM: 32KB + 16KB = 48KB (split between M0 and USB)
# Left M0_RAM_SIZE as 64k and USB_RAM_SIZE as 32k to keep
# original hackrf one settings intact.
# Recommend revisit by devs.
# Local SRAM: 96KB (Bank 1) + 40KB (Bank 2)
set(M0_RAM_SIZE "64k") # Standard AHB SRAM for M0
set(M0_LOCAL_HEAP_SIZE "0k") # No local heap available
set(M0_LOCAL_HEAP_ORIGIN "0") # Not used
set(M4_RAM_SIZE "96k") # Standard Local SRAM Bank 1
set(M4_FLASH_SIZE "32752") # Standard Local SRAM Bank 2 (~32KB)
set(USB_RAM_SIZE "32k") # Standard 32k/32k split for AHB Bank 1
set(FLASH_SIZE "1M") # Standard SPI Flash limit
endif()
# Apply all symbols to the linker flags in one command
# Build linker flags
set(CMAKE_EXE_LINKER_FLAGS "${LDFLAGS} \
-Wl,--defsym=LD_RAM_SIZE=${M0_RAM_SIZE} \
-Wl,--defsym=LD_M0_LOCAL_HEAP_SIZE=${M0_LOCAL_HEAP_SIZE} \
-Wl,--defsym=LD_M0_LOCAL_HEAP_ORIGIN=${M0_LOCAL_HEAP_ORIGIN} \
-Wl,--defsym=LD_M4_RAM_LEN=${M4_RAM_SIZE} \
-Wl,--defsym=LD_M4_FLASH_LEN=${M4_FLASH_SIZE} \
-Wl,--defsym=LD_USB_RAM_LEN=${USB_RAM_SIZE} \
-Wl,--defsym=LD_FLASH_SIZE=${FLASH_SIZE}"
)
+8 -10
View File
@@ -199,22 +199,22 @@ constexpr uint32_t out_mux_cfg(const P_OUT_CFG out, const P_OE_CFG oe) {
constexpr uint32_t data_sgpio_mux_cfg(
const CONCAT_ENABLE concat_enable,
const CONCAT_ORDER concat_order) {
#ifndef PRALINE
return (1U << 0) | (0U << 1) | (0U << 3) | (3U << 5) | (1U << 7) | (0U << 9) | (toUType(concat_enable) << 11) | (toUType(concat_order) << 12);
#else
#ifdef PRALINE
return (1U << 0) | (0U << 1) | (3U << 3) | (3U << 5) | (1U << 7) | (0U << 9) | (toUType(concat_enable) << 11) | (toUType(concat_order) << 12);
// Bits 3-4: CLK_SOURCE_SLICE_MODE = 3 (slice D as clock source for data slices)
#else
return (1U << 0) | (0U << 1) | (0U << 3) | (3U << 5) | (1U << 7) | (0U << 9) | (toUType(concat_enable) << 11) | (toUType(concat_order) << 12);
#endif
}
constexpr uint32_t data_slice_mux_cfg(
const PARALLEL_MODE parallel_mode,
const CLK_CAPTURE_MODE clk_capture_mode) {
#ifndef PRALINE
return (0U << 0) | (toUType(clk_capture_mode) << 1) | (1U << 2) | (0U << 3) | (0U << 4) | (toUType(parallel_mode) << 6) | (0U << 8);
#else
#ifdef PRALINE
return (0U << 0) | (toUType(clk_capture_mode) << 1) | (1U << 2) | (0U << 3) | (1U << 4) | (toUType(parallel_mode) << 6) | (0U << 8);
// Bit 4 CLKGEN_MODE: 0=internal counter, 1=external clock (REQUIRED for PRALINE!)
#else
return (0U << 0) | (toUType(clk_capture_mode) << 1) | (1U << 2) | (0U << 3) | (0U << 4) | (toUType(parallel_mode) << 6) | (0U << 8);
#endif
}
@@ -330,10 +330,6 @@ void SGPIO::configure(const Direction direction) {
const auto clk_capture_mode = data_clk_capture_mode(direction);
const auto single_slice = !slice_mode_multislice;
#ifndef PRALINE
uint32_t slice_enable_mask = 0;
#endif
#ifdef PRALINE
// Configure slice D as clock generator (REQUIRED for PRALINE!)
// Reference: HackRF sgpio.c line 193
@@ -347,6 +343,8 @@ void SGPIO::configure(const Direction direction) {
LPC_SGPIO->REG_SS[slice_d] = 0x11111111;
uint32_t slice_enable_mask = (1U << slice_d); // Start with slice D enabled
#else
uint32_t slice_enable_mask = 0;
#endif
for (size_t i = 0; i < slice_count; i++) {
+23
View File
@@ -156,6 +156,8 @@ class Message {
RTTYData = 98,
NotificationData = 99,
TimeSinkConfig = 100,
EPIRBTXData = 101,
P25TxConfigure = 102,
MAX
};
@@ -1110,6 +1112,27 @@ class SigGenToneMessage : public Message {
const uint32_t tone_delta;
};
class EPIRBTXDataMessage : public Message {
public:
static constexpr uint8_t max_len = 18;
constexpr EPIRBTXDataMessage()
: Message{ID::EPIRBTXData} {
}
bool mode_bpsk = true;
uint8_t data[max_len]{0};
uint8_t data_len = 0;
uint32_t pre_count = 0;
uint32_t post_count = 0;
};
class P25TxConfigureMessage : public Message {
public:
constexpr P25TxConfigureMessage()
: Message{ID::P25TxConfigure} {
}
uint16_t frame_length{0};
};
class AFSKTxConfigureMessage : public Message {
public:
constexpr AFSKTxConfigureMessage(
+6
View File
@@ -35,6 +35,12 @@ constexpr size_t adc0_touch_xn_input = 6;
/* ADC1 */
/*
* RSSI input channel is the same on both platforms:
* - HackRF One (MAX2837): RSSI routed to ADC1 Channel 1 (PC_0)
* - HackRF Pro/PRALINE (MAX2831): RSSI also routed to ADC1 Channel 1 (PC_0)
* Confirmed by GSG hackrf firmware: adc_read(1) used for RSSI
*/
constexpr size_t adc1_rssi_input = 1;
} /* namespace portapack */
@@ -55,6 +55,19 @@ struct ToneData {
/* NOTE: These structures must be located in the same location in both M4 and M0 binaries */
struct SharedMemory {
#ifdef PRALINE
/*
* Software RSSI: 8 packed I/Q samples from baseband_thread.
*
* baseband_thread copies 8 samples spread across buffer (no computation).
* Each sample is packed: Q in high 16 bits, I in low 16 bits.
* rssi_thread uses __SMUAD on each to compute I²+Q², finds peak.
*
* 8 samples avoids zero-crossing artifacts from single-sample approach.
*/
volatile uint32_t software_rssi_iq[8]{0}; // 8 packed I/Q samples
#endif
static constexpr size_t application_queue_k = 11;
static constexpr size_t app_local_queue_k = 11;
+2
View File
@@ -88,6 +88,8 @@ constexpr image_tag_t image_tag_am_tv{'P', 'A', 'M', 'T'};
constexpr image_tag_t image_tag_capture{'P', 'C', 'A', 'P'};
constexpr image_tag_t image_tag_ert{'P', 'E', 'R', 'T'};
constexpr image_tag_t image_tag_epirb_rx{'P', 'E', 'P', 'I'};
constexpr image_tag_t image_tag_epirb_tx{'P', 'E', 'P', 'T'};
constexpr image_tag_t image_tag_p25_tx{'P', '2', '5', 'T'};
constexpr image_tag_t image_tag_nfm_audio{'P', 'N', 'F', 'M'};
constexpr image_tag_t image_tag_pocsag{'P', 'P', 'O', 'C'};
constexpr image_tag_t image_tag_pocsag2{'P', 'P', 'O', '2'};

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