mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2026-08-19 14:14:04 +00:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d1a2318fe4 | |||
| 5c719645d6 | |||
| b0eb776bdd | |||
| ce87161f9b | |||
| 8883f2decc | |||
| e06d23ea3e | |||
| 0da90c7c5e | |||
| e7572e5d41 | |||
| 50f1e51d7e | |||
| 55a15958ec | |||
| 395e20d17b | |||
| 9bf1f2175e | |||
| ec57f99a2c | |||
| 5c2d1d3dfb | |||
| 52dd2ea2bb | |||
| b273165a0c | |||
| 93775129cb | |||
| fe9448f149 |
@@ -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
|
||||
@@ -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 ..
|
||||
|
||||
@@ -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 @@
|
||||
v2.3.1
|
||||
v2.3.2
|
||||
|
||||
@@ -1 +1 @@
|
||||
v2.3.2
|
||||
v2.4.0
|
||||
|
||||
@@ -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 [](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
@@ -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"]
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
@@ -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 */
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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++) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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__ */
|
||||
@@ -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")
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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'};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user