Compare commits

...

18 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
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
63 changed files with 2589 additions and 176 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
+4 -1
View File
@@ -51,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"]
+1 -1
View File
@@ -446,7 +446,7 @@ class BLERxView : public View {
};
// Variable to store the current limit
size_t max_recent_entries = 32;
size_t max_recent_entries = 25;
}; /* BLERxView */
} /* namespace ui */
+10
View File
@@ -21,6 +21,7 @@
*/
#include "baseband_api.hpp"
#include <cstring>
#include "audio.hpp"
#include "tonesets.hpp"
@@ -426,6 +427,15 @@ 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) {
+1
View File
@@ -117,6 +117,7 @@ void set_bitstream_config(uint32_t deviation, uint8_t mode);
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();
+51 -5
View File
@@ -319,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},
@@ -335,6 +344,7 @@ 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
@@ -451,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
@@ -640,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() {
@@ -668,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;
@@ -686,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};
@@ -881,10 +907,6 @@ 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)
@@ -893,6 +915,10 @@ void ClockManager::set_reference_ppb(const int32_t ppb) {
}
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
@@ -1105,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);
@@ -1133,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,
+17 -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
@@ -305,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
@@ -328,6 +332,14 @@ set(EXTCPPSRC
#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
@@ -356,6 +368,7 @@ set(EXTAPPLIST
#morse_tx
sstvtx
same_tx
mdc_tx
sstvrx
random_password
acars_rx
@@ -388,6 +401,7 @@ set(EXTAPPLIST
gfxeq
waterfall_designer
detector_rx
fpv_detect
spaceinv
blackjack
battleship
@@ -410,6 +424,7 @@ set(EXTAPPLIST
pocsag_tx
time_sink
kiss_tnc
p25_tx
)
# sdusb has type conflicts with PRALINE (HackRF Pro) - add only for non-PRALINE builds
+122 -104
View File
@@ -23,87 +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_kiss_tnc (rwx) : org = 0xADFF0000, len = 32k
ram_external_app_epirb_tx (rwx) : org = 0xAE000000, 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
@@ -144,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*);
@@ -192,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*);
@@ -264,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*);
@@ -288,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*);
@@ -372,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*);
@@ -480,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));
@@ -504,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)
{
@@ -524,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));
@@ -547,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));
@@ -583,10 +584,27 @@ SECTIONS
*(*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
+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,
+8 -4
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};
@@ -415,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);
+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};
+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;
+7
View File
@@ -543,6 +543,13 @@ set(MODE_CPPSRC
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++) {
+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
}
@@ -342,7 +342,7 @@
// ============================================================================
uint8_t fpga_register_read(uint8_t reg) {
if (reg == 0 || reg > 5) return;
if (reg == 0 || reg > 5) return 0xFF;
ssp1_set_mode_ice40();
uint8_t val = fpga_spi_read(reg);
@@ -112,10 +112,11 @@ if(BOARD STREQUAL "PRALINE")
# 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 "32k") # Local SRAM heap for additional 16-bit IQ
set(M0_LOCAL_HEAP_ORIGIN "0x10018000") # After M4's 96KB
set(M4_RAM_SIZE "96k") # Local SRAM Bank 1 (same as HackRF One)
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")
+9
View File
@@ -157,6 +157,7 @@ class Message {
NotificationData = 99,
TimeSinkConfig = 100,
EPIRBTXData = 101,
P25TxConfigure = 102,
MAX
};
@@ -1124,6 +1125,14 @@ class EPIRBTXDataMessage : public Message {
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;
+1
View File
@@ -89,6 +89,7 @@ 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'};
+1 -1
View File
@@ -24,4 +24,4 @@
# external app address ranges below must match those in linker file "external.ld"
maximum_application_size = 32*1024
external_apps_address_start = 0xADB00000
external_apps_address_end = 0xADFC0000
external_apps_address_end = 0xAE020000
-5
View File
@@ -1,5 +0,0 @@
## Brief description what you did
## Checklist
- [ ] Kept changes minimal and limited to necessary files
- [ ] Verified functionality remains intact and code compiles