Compare commits

...

6 Commits

Author SHA1 Message Date
gullradriel ef2ae9874b fix build (#3129)
Co-authored-by: gullradriel <gullradriel@users.noreply.github.com>
2026-04-08 08:28:44 +02:00
Pezsma 5deeaed5f0 Praline rssi (#3127) 2026-04-07 19:42:21 +02:00
Ryan Harden d5f94398ce Add external 2-Tone TX/RX Apps (#3128) 2026-04-07 19:32:41 +02:00
Totoo fba15c48e5 Battery capacity options added (#3125) 2026-04-04 09:19:17 +13:00
Totoo 888634ce35 praline without the much fixed ram (#3123) 2026-04-04 09:17:21 +13:00
Frederic BORRY 03131c8a0c EPIRB TX application update (#3120) 2026-04-03 11:35:46 +02:00
38 changed files with 3466 additions and 995 deletions
+8 -13
View File
@@ -27,6 +27,8 @@
#include "portapack.hpp"
#include "battery.hpp"
#include <cstring>
#include "ui_settings.hpp"
#include "portapack_persistent_memory.hpp"
using namespace portapack;
@@ -138,10 +140,8 @@ void BattinfoView::update_result() {
}
if ((valid_mask & battery::BatteryManagement::BATT_VALID_PERCENT) == battery::BatteryManagement::BATT_VALID_PERCENT) {
text_method.set("IC");
button_mode.set_text("Volt");
} else {
text_method.set("Voltage");
button_mode.set_text("IC");
}
if (uichg) set_dirty();
// to update status bar too, send message in behalf of batt manager
@@ -158,8 +158,9 @@ BattinfoView::BattinfoView(NavigationView& nav)
&text_current,
&text_charge,
&text_method,
&button_mode,
&button_settings,
&button_exit,
&text_capacity,
// &text_cycles,
// &text_warn,
&text_ttef});
@@ -167,17 +168,11 @@ BattinfoView::BattinfoView(NavigationView& nav)
button_exit.on_select = [this, &nav](Button&) {
nav.pop();
};
button_mode.on_select = [this, &nav](Button&) {
if (button_mode.text() == "IC") {
battery::BatteryManagement::set_calc_override(false);
persistent_memory::set_ui_override_batt_calc(false);
button_mode.set_text("Volt");
} else {
battery::BatteryManagement::set_calc_override(true);
persistent_memory::set_ui_override_batt_calc(true);
button_mode.set_text("IC");
}
button_settings.on_select = [this, &nav](Button&) {
nav.replace<SetBatteryView>();
};
text_capacity.set(to_string_dec_uint(persistent_memory::battery_cap_mah()) + " mAh");
if (!persistent_memory::battery_cap_valid()) text_capacity.set_style(Theme::getInstance()->fg_red);
update_result();
if (thread == nullptr) thread = chThdCreateFromHeap(NULL, 1024, NORMALPRIO + 10, BattinfoView::static_fn, this);
}
+24 -19
View File
@@ -54,48 +54,53 @@ class BattinfoView : public View {
int32_t current = 0;
Labels labels{
{{2 * 8, 1 * 16}, "Percent:", Theme::getInstance()->fg_light->foreground},
{{2 * 8, 2 * 16}, "Voltage:", Theme::getInstance()->fg_light->foreground},
{{2 * 8, 3 * 16}, "Method:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(2), UI_POS_Y(1)}, "Percent:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(2), UI_POS_Y(2)}, "Voltage:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(2), UI_POS_Y(3)}, "Method:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(2), UI_POS_Y(4)}, "Capacity:", Theme::getInstance()->fg_light->foreground},
};
Labels labels_opt{
{{2 * 8, 4 * 16}, "Current:", Theme::getInstance()->fg_light->foreground},
{{2 * 8, 5 * 16}, "Charge:", Theme::getInstance()->fg_light->foreground},
{{2 * 8, 6 * 16}, "TTF/E:", Theme::getInstance()->fg_light->foreground},
// {{2 * 8, 7 * 16}, "Cycles:", Theme::getInstance()->fg_light->foreground},
{{2 * 8, 10 * 16}, "Change method:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(2), UI_POS_Y(5)}, "Current:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(2), UI_POS_Y(6)}, "Charge:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(2), UI_POS_Y(7)}, "TTF/E:", Theme::getInstance()->fg_light->foreground},
// {{UI_POS_X(2), UI_POS_Y(8)}, "Cycles:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(2), UI_POS_Y(10)}, "Change settings:", Theme::getInstance()->fg_light->foreground},
};
Text text_percent{
{13 * 8, 1 * 16, 10 * 16, 16},
{UI_POS_X(13), UI_POS_Y(1), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)},
"-"};
Text text_voltage{
{13 * 8, 2 * 16, 10 * 16, 16},
{UI_POS_X(13), UI_POS_Y(2), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)},
"-"};
Text text_method{
{13 * 8, 3 * 16, 10 * 16, 16},
{UI_POS_X(13), UI_POS_Y(3), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)},
"-"};
Text text_capacity{
{UI_POS_X(13), UI_POS_Y(4), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)},
"-"};
Text text_current{
{13 * 8, 4 * 16, 10 * 16, 16},
{UI_POS_X(13), UI_POS_Y(5), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)},
"-"};
Text text_charge{
{13 * 8, 5 * 16, 10 * 16, 16},
{UI_POS_X(13), UI_POS_Y(6), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)},
"-"};
Text text_ttef{
{13 * 8, 6 * 16, 10 * 16, 16},
{UI_POS_X(13), UI_POS_Y(7), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)},
"-"};
/* Text text_cycles{
{13 * 8, 7 * 16, 10 * 16, 16},
{UI_POS_X(13), UI_POS_Y(8), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)},
"-"};
Text text_warn{
{1 * 8, 8 * 16, screen_width, 2 * 16},
{UI_POS_X(1), UI_POS_Y(9), screen_width, UI_POS_HEIGHT(2)},
""}; */
Button button_mode{
{2 * 8, 11 * 16 + 5, 5 * 16, 32},
"Volt"};
Button button_settings{
{UI_POS_X(2), UI_POS_Y(11) + 5, UI_POS_WIDTH(10), UI_POS_HEIGHT(2)},
"Settings"};
Button button_exit{
{UI_POS_X_CENTER(12), UI_POS_Y_BOTTOM(4), UI_POS_WIDTH(12), UI_POS_HEIGHT(2)},
+26 -1
View File
@@ -1160,6 +1160,8 @@ SetBatteryView::SetBatteryView(NavigationView& nav) {
add_children({&labels,
&button_save,
&button_cancel,
&field_battcap,
&button_help_cap,
&checkbox_overridebatt,
&checkbox_battery_charge_hint});
@@ -1169,21 +1171,44 @@ SetBatteryView::SetBatteryView(NavigationView& nav) {
pmem::set_ui_override_batt_calc(checkbox_overridebatt.value());
pmem::set_ui_battery_charge_hint(checkbox_battery_charge_hint.value());
battery::BatteryManagement::set_calc_override(checkbox_overridebatt.value());
if (((uint32_t)field_battcap.value() != pmem::battery_cap_mah()) || (!pmem::battery_cap_valid())) {
pmem::set_battery_cap_mah(field_battcap.value());
i2cdev::I2cDev_MAX17055* dev = (i2cdev::I2cDev_MAX17055*)i2cdev::I2CDevManager::get_dev_by_model(I2C_DEVMDL::I2CDEVMDL_MAX17055);
if (dev && !dev->reInit()) {
nav.display_modal("Error", "Battery gauge re-init failed");
return;
}
}
send_system_refresh();
nav.pop();
};
button_reset.on_select = [&nav, this](Button&) {
auto dev = (i2cdev::I2cDev_MAX17055*)i2cdev::I2CDevManager::get_dev_by_model(I2C_DEVMDL::I2CDEVMDL_MAX17055);
if (dev->reset_learned())
if (dev && dev->reset_learned())
nav.display_modal("Reset", "Battery parameters reset");
else
nav.display_modal("Error", "Error parameter reset");
};
button_help_cap.on_select = [&nav, this](Button&) {
nav.display_modal("Battery Capacity",
"Only change default, if you\n"
" changed the battery!\n"
"Defaults:\n"
"H4 + Hackrf One: 2500\n"
"H4 + Hackrf Pro: 2000\n"
"H4Pro + Hackrf Pro: custom\n"
"PortaRf: 3000\n"
);
};
checkbox_overridebatt.set_value(pmem::ui_override_batt_calc());
checkbox_battery_charge_hint.set_value(pmem::ui_battery_charge_hint());
field_battcap.set_value(pmem::battery_cap_mah());
button_cancel.on_select = [&nav, this](Button&) {
nav.pop();
};
+22 -7
View File
@@ -1045,21 +1045,36 @@ class SetBatteryView : public View {
private:
int32_t selected = 0;
Labels labels{
{{1 * 8, 1 * 16}, "Override batt calculation", Theme::getInstance()->fg_light->foreground},
{{1 * 8, 2 * 16}, "method to voltage based", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X_CENTER(26), UI_POS_Y(0)}, "Override batt calculation", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X_CENTER(24), UI_POS_Y(1)}, "method to voltage based", Theme::getInstance()->fg_light->foreground},
/**/
{{1 * 8, 6 * 16}, "Display a hint to remind you", Theme::getInstance()->fg_light->foreground},
{{1 * 8, 7 * 16}, "when you charge", Theme::getInstance()->fg_light->foreground}};
{{UI_POS_X_CENTER(29), UI_POS_Y(4)}, "Display a hint to remind you", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X_CENTER(16), UI_POS_Y(5)}, "when you charge", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X_CENTER(17), UI_POS_Y(8)}, "Battery capacity", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(7), UI_POS_Y(9)}, "mAh", Theme::getInstance()->fg_light->foreground}};
Labels labels2{{{1 * 8, 11 * 16}, "Reset IC's learned params.", Theme::getInstance()->fg_light->foreground}};
Labels labels2{{{UI_POS_X(1), UI_POS_Y(11)}, "Reset IC's learned params.", Theme::getInstance()->fg_light->foreground}};
NumberField field_battcap{
{UI_POS_X(1), UI_POS_Y(9)},
5,
{BATT_18650_MIN_MAH, BATT_18650_MAX_MAH},
100,
' ',
};
Button button_help_cap{
{UI_POS_X(12), UI_POS_Y(9), UI_POS_WIDTH(5), UI_POS_HEIGHT(1)},
"Help",
};
Checkbox checkbox_overridebatt{
{2 * 8, 4 * 16},
{UI_POS_X(2), UI_POS_Y(2)},
23,
"Override"};
Checkbox checkbox_battery_charge_hint{
{2 * 8, 9 * 16},
{UI_POS_X(2), UI_POS_Y(6)},
23,
"Charge hint"};
+5
View File
@@ -393,6 +393,11 @@ void set_moreserx_config(uint8_t mode) {
send_message(&message);
}
void set_tonedetect_config(uint8_t squelch, uint32_t ctcss_freq_x10) {
const ToneDetectConfigureMessage message{squelch, ctcss_freq_x10};
send_message(&message);
}
void set_morsetx_config(uint8_t mode, uint32_t tone, float fm_delta) {
const MorseTXConfigureMessage message{mode, tone, fm_delta};
send_message(&message);
+1
View File
@@ -108,6 +108,7 @@ void set_siggen_config(const uint32_t bw, const uint32_t shape, const uint32_t d
void set_spectrum_painter_config(const uint16_t width, const uint16_t height, bool update, int32_t bw);
void set_subghzd_config(uint8_t modulation, uint32_t sampling_rate);
void set_moreserx_config(uint8_t mode);
void set_tonedetect_config(uint8_t squelch, uint32_t ctcss_freq_x10 = 0);
void set_morsetx_config(uint8_t mode, uint32_t tone, float fm_delta);
void set_morsetx_key(bool key_down);
void set_wefax_config(uint8_t lpm, uint8_t ioc);
+175 -22
View File
@@ -98,6 +98,39 @@ void EPIRBTXAppView::on_timer() {
}
}
void EPIRBTXAppView::update_bpsk_frequency() {
bool was_transmitting = false;
if (transmitting && (am_enabled || transmitting_bpsk)) {
// We need to stop transmission before changing frequency
transmitter_model.disable();
was_transmitting = true;
}
transmitter_model.set_target_frequency(bpsk_frequency);
// Update displayed frequency
tx_view.on_show();
if (was_transmitting) {
// Start over
start_tx();
}
}
void EPIRBTXAppView::update_am_transmission() {
if (am_enabled && transmitting && !transmitting_bpsk) {
// Start am transmission
// Restore am frequency
epirb_tx_message.mode_bpsk = false;
transmitter_model.set_target_frequency(am_frequency);
// Send config to baseband
baseband::set_epirb_tx_config(epirb_tx_message);
// Start transmitting
transmitter_model.enable();
} else if (transmitting && !transmitting_bpsk) {
// Stop am transmission
transmitter_model.disable();
tx_view.set_transmitting(false);
}
}
void EPIRBTXAppView::update_frame(bool updateConfig) {
if (mode_file) {
// In file mode, currently selected beacon has changed => load the new one
@@ -136,17 +169,17 @@ void EPIRBTXAppView::update_frame(bool updateConfig) {
}
void EPIRBTXAppView::update_config() {
if (epirb_tx_message.mode_bpsk) {
// Already in BPSK mode => backup bpsk frequency
bpsk_frequency = transmitter_model.target_frequency();
} else {
if (!epirb_tx_message.mode_bpsk) {
// Previously in AM mode => restore bpsk frequency
transmitter_model.set_target_frequency(bpsk_frequency);
// Update displayed frequency
tx_view.on_show();
}
// Set mode to bpsk
epirb_tx_message.mode_bpsk = true;
transmitting_bpsk = true;
// Set pre/post count
epirb_tx_message.pre_count = (150 * TONES_SAMPLERATE) / 1000; // 150 ms
epirb_tx_message.pre_count = (160 * TONES_SAMPLERATE) / 1000; // 160 ms carrier (COSPAS spec.)
epirb_tx_message.post_count = (100 * TONES_SAMPLERATE) / 1000; // 100 ms
// Send config to baseband
baseband::set_epirb_tx_config(epirb_tx_message);
@@ -178,15 +211,12 @@ void EPIRBTXAppView::stop_tx() {
void EPIRBTXAppView::on_tx_progress(const uint32_t progress, const bool done) {
(void)progress;
if (done) {
transmitting_bpsk = false;
if (am_enabled) {
// BPSK frame sent, switch back to 121.5 AM signal
epirb_tx_message.mode_bpsk = false;
// Backup bpsk frequency for next run
bpsk_frequency = transmitter_model.target_frequency();
// Restore am frequency
transmitter_model.set_target_frequency(am_frequency);
// Send config to baseband
baseband::set_epirb_tx_config(epirb_tx_message);
// Start am transmission
update_am_transmission();
} else {
// End of BPSK frame
transmitter_model.disable();
@@ -262,6 +292,8 @@ EPIRBTXAppView::EPIRBTXAppView(
&checkbox_am,
&field_am_frequency,
&checkbox_send_on_change,
&options_am_channel,
&options_bpsk_channel,
&tx_view});
text_beacon.set_style(Theme::getInstance()->fg_light);
@@ -279,6 +311,11 @@ EPIRBTXAppView::EPIRBTXAppView(
options_mode.set_by_value(!mode_file);
transmitter_model.set_target_frequency(bpsk_frequency);
field_am_frequency.set_value(am_frequency);
options_am_channel.set_by_value(am_channel);
options_am_channel.set_style((am_channel == (uint8_t)AmChannel::REAL) ? Theme::getInstance()->fg_red : Theme::getInstance()->bg_darkest);
manual_am_frequency = am_frequency;
options_bpsk_channel.set_by_value(bpsk_channel);
manual_bpsk_frequency = bpsk_frequency;
field_delay.set_value(delay);
options_beacon_type.set_by_value(beacon_type);
options_beacon_protocol.set_by_value(beacon_protocol);
@@ -292,23 +329,23 @@ EPIRBTXAppView::EPIRBTXAppView(
update_mode();
update_location();
options_mode.on_change = [this](size_t index, OptionsField::value_t) {
mode_file = (index == 0);
options_mode.on_change = [this](size_t, OptionsField::value_t value) {
mode_file = (((BeaconMode)value) == BeaconMode::FILE);
update_mode();
update_frame();
set_dirty();
};
options_beacon_type.on_change = [this](size_t index, OptionsField::value_t) {
beacon_params.type = (BeaconType)index;
beacon_type = index;
options_beacon_type.on_change = [this](size_t, OptionsField::value_t value) {
beacon_params.type = (BeaconType)value;
beacon_type = value;
update_frame();
set_dirty();
};
options_beacon_protocol.on_change = [this](size_t index, OptionsField::value_t) {
beacon_params.protocol = (BeaconProtocol)index;
beacon_protocol = index;
options_beacon_protocol.on_change = [this](size_t, OptionsField::value_t value) {
beacon_params.protocol = (BeaconProtocol)value;
beacon_protocol = value;
update_frame();
set_dirty();
};
@@ -320,6 +357,71 @@ EPIRBTXAppView::EPIRBTXAppView(
set_dirty();
};
options_am_channel.on_change = [this](size_t, OptionsField::value_t v) {
bool is_real = false;
switch ((AmChannel)v) {
case AmChannel::REAL:
is_real = true;
am_frequency = AM_REAL_FREQUENCY;
break;
case AmChannel::MANUAL:
am_frequency = manual_am_frequency;
break;
default:
v = (uint8_t)AmChannel::TEST;
// fallthrough
case AmChannel::TEST:
am_frequency = AM_TEST_FREQUENCY;
break;
}
// Actual frequency change will be done by field_am_frequency.on_change()
field_am_frequency.set_value(am_frequency);
am_channel = v;
options_am_channel.set_style(is_real ? Theme::getInstance()->fg_red : Theme::getInstance()->bg_darkest);
set_dirty();
};
options_bpsk_channel.on_change = [this](size_t, OptionsField::value_t v) {
switch ((BpskChannel)v) {
case BpskChannel::MANUAL:
bpsk_frequency = manual_bpsk_frequency;
break;
case BpskChannel::B:
bpsk_frequency = BPSK_FREQUENCY_B;
break;
case BpskChannel::C:
bpsk_frequency = BPSK_FREQUENCY_C;
break;
case BpskChannel::F:
bpsk_frequency = BPSK_FREQUENCY_F;
break;
case BpskChannel::G:
bpsk_frequency = BPSK_FREQUENCY_G;
break;
case BpskChannel::J:
bpsk_frequency = BPSK_FREQUENCY_J;
break;
case BpskChannel::K:
bpsk_frequency = BPSK_FREQUENCY_K;
break;
case BpskChannel::N:
bpsk_frequency = BPSK_FREQUENCY_N;
break;
case BpskChannel::O:
bpsk_frequency = BPSK_FREQUENCY_O;
break;
default:
v = (uint8_t)BpskChannel::HAM;
// fallthrough
case BpskChannel::HAM:
bpsk_frequency = BPSK_FREQUENCY_HAM;
break;
}
bpsk_channel = v;
update_bpsk_frequency();
set_dirty();
};
checkbox_beacon_internal.on_select = [this](Checkbox&, bool v) {
beacon_internal = v;
beacon_params.is_internal = v;
@@ -358,7 +460,7 @@ EPIRBTXAppView::EPIRBTXAppView(
field_am_frequency.on_change = [this](rf::Frequency freq) {
am_frequency = freq;
if (transmitting && !epirb_tx_message.mode_bpsk && am_enabled)
if (transmitting && !transmitting_bpsk && am_enabled)
// Update transmitter frequency
transmitter_model.set_target_frequency(am_frequency);
};
@@ -397,6 +499,8 @@ EPIRBTXAppView::EPIRBTXAppView(
checkbox_am.on_select = [this](Checkbox&, bool v) {
beacon_params.has_121_5 = v;
am_enabled = v;
update_am_transmission();
// We update the additional location device data in the frame based on this
if (!mode_file) update_frame(false);
};
@@ -404,7 +508,21 @@ EPIRBTXAppView::EPIRBTXAppView(
field_am_frequency.on_edit = [this, &nav]() {
auto new_view = nav.push<FrequencyKeypadView>(field_am_frequency.value());
new_view->on_changed = [this](rf::Frequency f) {
field_am_frequency.set_value(f);
switch (f) {
case AM_REAL_FREQUENCY:
am_channel = (uint8_t)AmChannel::REAL;
break;
case AM_TEST_FREQUENCY:
am_channel = (uint8_t)AmChannel::TEST;
break;
default:
manual_am_frequency = f;
am_channel = (uint8_t)AmChannel::MANUAL;
break;
}
// Actual frequency change will be done by options_am_channel.on_change()
options_am_channel.set_by_value(am_channel);
set_dirty();
};
};
@@ -415,8 +533,43 @@ EPIRBTXAppView::EPIRBTXAppView(
tx_view.on_edit_frequency = [this, &nav]() {
auto new_view = nav.push<FrequencyKeypadView>(transmitter_model.target_frequency());
new_view->on_changed = [this](rf::Frequency f) {
transmitter_model.set_target_frequency(f);
bpsk_frequency = f;
switch (f) {
case BPSK_FREQUENCY_HAM:
bpsk_channel = (uint8_t)BpskChannel::HAM;
break;
case BPSK_FREQUENCY_B:
bpsk_channel = (uint8_t)BpskChannel::B;
break;
case BPSK_FREQUENCY_C:
bpsk_channel = (uint8_t)BpskChannel::C;
break;
case BPSK_FREQUENCY_F:
bpsk_channel = (uint8_t)BpskChannel::F;
break;
case BPSK_FREQUENCY_G:
bpsk_channel = (uint8_t)BpskChannel::G;
break;
case BPSK_FREQUENCY_J:
bpsk_channel = (uint8_t)BpskChannel::J;
break;
case BPSK_FREQUENCY_K:
bpsk_channel = (uint8_t)BpskChannel::K;
break;
case BPSK_FREQUENCY_N:
bpsk_channel = (uint8_t)BpskChannel::N;
break;
case BPSK_FREQUENCY_O:
bpsk_channel = (uint8_t)BpskChannel::O;
break;
default:
bpsk_channel = (uint8_t)BpskChannel::MANUAL;
manual_bpsk_frequency = bpsk_frequency;
break;
}
// Actual frequency change will be done by options_bpsk_channel.on_change()
options_bpsk_channel.set_by_value(bpsk_channel);
set_dirty();
};
};
+87 -15
View File
@@ -37,8 +37,26 @@
#define BEACON_HEXA_HALF_SIZE 18
#define BEACON_SIZE 18
#define AM_TEST_FREQUENCY 121375000
#define AM_REAL_FREQUENCY 121500000
#define BPSK_FREQUENCY_HAM 433025000
#define BPSK_FREQUENCY_B 406025000
#define BPSK_FREQUENCY_C 406028000
#define BPSK_FREQUENCY_F 406037000
#define BPSK_FREQUENCY_G 406040000
#define BPSK_FREQUENCY_J 406049000
#define BPSK_FREQUENCY_K 406052000
#define BPSK_FREQUENCY_N 406061000
#define BPSK_FREQUENCY_O 406064000
namespace ui::external_app::epirb_tx {
enum class BeaconMode {
FILE = 0,
MODE_MANUAL = 1
};
enum class BeaconType {
EPIRB = 0,
ELT = 1,
@@ -51,6 +69,25 @@ enum class BeaconProtocol {
NATIONAL = 2
};
enum class AmChannel {
TEST = 0,
REAL = 1,
MANUAL = 2
};
enum class BpskChannel {
HAM = 0,
B = 1,
C = 2,
F = 3,
G = 4,
J = 5,
K = 6,
N = 7,
O = 8,
MANUAL = 10
};
struct Location {
std::string locator;
bool south;
@@ -95,6 +132,8 @@ class EPIRBTXAppView : public View {
std::string frame_to_hex_string(bool start);
void generate_frame(BeaconParams params);
void update_frame(bool updateConfig = true);
void update_bpsk_frequency();
void update_am_transmission();
void update_mode();
void update_location(bool updateLocatorField = true);
@@ -114,25 +153,33 @@ class EPIRBTXAppView : public View {
// Frequency of the transmitter before starting the app (used to restore frequency when leaving)
rf::Frequency original_frequency{0};
// Frequency of the AM emergency signal
rf::Frequency am_frequency{121500000};
rf::Frequency am_frequency{AM_TEST_FREQUENCY};
// Frequency of the 406 MHz BPSK signal
rf::Frequency bpsk_frequency{406025000};
rf::Frequency bpsk_frequency{BPSK_FREQUENCY_HAM};
// Selected am channel
uint8_t am_channel{(uint8_t)AmChannel::TEST};
// Selected bpsk channel
uint8_t bpsk_channel{(uint8_t)BpskChannel::HAM};
// Manual AM frequency value
rf::Frequency manual_am_frequency{AM_TEST_FREQUENCY};
// Manual BPSK frequency value
rf::Frequency manual_bpsk_frequency{BPSK_FREQUENCY_HAM};
// True when using a beacon from the BEACONS.TXT file
bool mode_file{true};
bool mode_file{false};
// True when looping on sending beacons is enabled
bool loop_enabled{true};
// True if AM emergency signal transmission is enabled
bool am_enabled{true};
// True if we want to send a new frame each time the user changes the current beacon
bool send_on_change{false};
bool send_on_change{true};
// The current locator string
std::string locator{"JN03RO"};
// The delay between each frame when on loop mode
uint32_t delay{50};
uint8_t beacon_type{0};
uint8_t beacon_type{(uint8_t)BeaconType::EPIRB};
// Currently selected beacon protocol
uint8_t beacon_protocol{0};
uint8_t beacon_protocol{(uint8_t)BeaconProtocol::USER};
// Currently selected beacon country
uint32_t beacon_country{227};
// Current beacon's internal state (true for internal location system)
@@ -150,6 +197,8 @@ class EPIRBTXAppView : public View {
{"sbeacon"sv, &selected_beacon},
{"amfreq"sv, &am_frequency},
{"bpskfreq"sv, &bpsk_frequency},
{"amchan"sv, &am_channel},
{"bpskchan"sv, &bpsk_channel},
{"loop"sv, &loop_enabled},
{"delay"sv, &delay},
{"file"sv, &mode_file},
@@ -166,6 +215,8 @@ class EPIRBTXAppView : public View {
uint32_t last_frame_time{0};
// True when transmission is enabled
bool transmitting{false};
// True when transmitting a BPSK frame
bool transmitting_bpsk{false};
// True when currently looping on sending beacons
bool loop{false};
@@ -179,7 +230,9 @@ class EPIRBTXAppView : public View {
{{UI_POS_X(0), UI_POS_Y(0)}, "Source:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(6)}, "Frame:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(10)}, "Next frame in s.", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(12)}, "AM frequency MHz", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(12)}, "AM frequency: MHz", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(14)}, "AM chan.:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(15)}, "BPSK chan.:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(17), UI_POS_Y(9)}, "s.", Theme::getInstance()->fg_light->foreground}};
// For file mode
@@ -231,15 +284,15 @@ class EPIRBTXAppView : public View {
OptionsField options_beacon_type{
{UI_POS_X(9), UI_POS_Y(1)},
7,
{{"EPIRB", 0},
{"ELT", 1},
{"PLB", 2}}};
{{"EPIRB", (uint8_t)BeaconType::EPIRB},
{"ELT", (uint8_t)BeaconType::ELT},
{"PLB", (uint8_t)BeaconType::PLB}}};
OptionsField options_beacon_protocol{
{UI_POS_X(9 + 7), UI_POS_Y(1)},
30,
{{"User", 0},
{"Standard", 1},
{"National", 2}}};
{{"User", (uint8_t)BeaconProtocol::USER},
{"Standard", (uint8_t)BeaconProtocol::STANDARD},
{"National", (uint8_t)BeaconProtocol::NATIONAL}}};
OptionsField options_beacon_country{
{UI_POS_X(9), UI_POS_Y(2)},
7,
@@ -261,8 +314,8 @@ class EPIRBTXAppView : public View {
OptionsField options_mode{
{UI_POS_X(7), UI_POS_Y(0)},
30,
{{"File (BEACONS.TXT)", 0},
{"Manual (Editor)", 1}}};
{{"File (BEACONS.TXT)", (uint8_t)BeaconMode::FILE},
{"Manual (Editor)", (uint8_t)BeaconMode::MODE_MANUAL}}};
// Frame content
Text text_frame{
@@ -305,6 +358,25 @@ class EPIRBTXAppView : public View {
"START"};
const Style& style_tx_start = *Theme::getInstance()->fg_green;
const Style& style_tx_stop = *Theme::getInstance()->fg_red;
OptionsField options_am_channel{
{UI_POS_X(11), UI_POS_Y(14)},
20,
{{"121.375 MHz (Test)", 0},
{"121.500 MHz /!\\Real", 1},
{"Manual", 2}}};
OptionsField options_bpsk_channel{
{UI_POS_X(11), UI_POS_Y(15)},
20,
{{"433.025 MHz (Ham)", (uint8_t)BpskChannel::HAM},
{"406.025 MHz (B)", (uint8_t)BpskChannel::B},
{"406.028 MHz (C)", (uint8_t)BpskChannel::C},
{"406.037 MHz (F)", (uint8_t)BpskChannel::F},
{"406.040 MHz (G)", (uint8_t)BpskChannel::G},
{"406.049 MHz (J)", (uint8_t)BpskChannel::J},
{"406.052 MHz (K)", (uint8_t)BpskChannel::K},
{"406.061 MHz (N)", (uint8_t)BpskChannel::N},
{"406.064 MHz (O)", (uint8_t)BpskChannel::O},
{"Manual", (uint8_t)BpskChannel::MANUAL}}};
// Transmitter view
TransmitterView tx_view{
+10
View File
@@ -340,6 +340,14 @@ set(EXTCPPSRC
#p25_tx
external/p25_tx/main.cpp
external/p25_tx/ui_p25_tx.cpp
#two_tone_pager
external/two_tone_pager/main.cpp
external/two_tone_pager/ui_two_tone_pager.cpp
#two_tone_rx
external/two_tone_rx/main.cpp
external/two_tone_rx/ui_two_tone_rx.cpp
)
set(EXTAPPLIST
@@ -425,6 +433,8 @@ set(EXTAPPLIST
time_sink
kiss_tnc
p25_tx
two_tone_pager
two_tone_rx
)
# sdusb has type conflicts with PRALINE (HackRF Pro) - add only for non-PRALINE builds
+14
View File
@@ -106,6 +106,8 @@ MEMORY
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
ram_external_app_two_tone_pager (rwx) : org = 0xAE040000, len = 32k
ram_external_app_two_tone_rx (rwx) : org = 0xAE050000, len = 32k
}
SECTIONS
@@ -607,4 +609,16 @@ SECTIONS
KEEP(*(.external_app.app_p25_tx.application_information));
*(*ui*external_app*p25_tx*);
} > ram_external_app_p25_tx
.external_app_two_tone_pager : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_two_tone_pager.application_information));
*(*ui*external_app*two_tone_pager*);
} > ram_external_app_two_tone_pager
.external_app_two_tone_rx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_two_tone_rx.application_information));
*(*ui*external_app*two_tone_rx*);
} > ram_external_app_two_tone_rx
}
+86
View File
@@ -0,0 +1,86 @@
/*
* Copyright (C) 2024 PortaPack Mayhem
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#include "ui.hpp"
#include "ui_two_tone_pager.hpp"
#include "ui_navigation.hpp"
#include "external_app.hpp"
namespace ui::external_app::two_tone_pager {
void initialize_app(ui::NavigationView& nav) {
nav.push<TwoTonePagerView>();
}
} // namespace ui::external_app::two_tone_pager
extern "C" {
__attribute__((section(".external_app.app_two_tone_pager.application_information"), used)) application_information_t _application_information_two_tone_pager = {
/*.memory_location = */ (uint8_t*)0x00000000,
/*.externalAppEntry = */ ui::external_app::two_tone_pager::initialize_app,
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
/*.app_name = */ "2-Tone TX",
/*.bitmap_data = */ {
// 16×16 pager icon — two-tone radio / pager device
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0xFC,
0x3F,
0xFE,
0x7F,
0x02,
0x40,
0xBA,
0x45,
0x02,
0x40,
0xFE,
0x7F,
0xFE,
0x7F,
0x92,
0x7C,
0x92,
0x7C,
0xFC,
0x3F,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
},
/*.icon_color = */ ui::Color::orange().v,
/*.menu_location = */ app_location_t::TX,
/*.desired_menu_position = */ -1,
// Uses the proc_tones baseband processor (same as Morse TX)
/*.m4_app_tag = portapack::spi_flash::image_tag_tones */ {'P', 'T', 'O', 'N'},
/*.m4_app_offset = */ 0x00000000, // filled at compile time
};
} // extern "C"
@@ -0,0 +1,616 @@
/*
* Copyright (C) 2024 PortaPack Mayhem
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#include "ui_two_tone_pager.hpp"
#include "portapack.hpp"
#include "baseband_api.hpp"
#include "portapack_shared_memory.hpp"
#include "string_format.hpp"
#include "theme.hpp"
#include <algorithm>
#include <cstring>
using namespace portapack;
namespace ui::external_app::two_tone_pager {
// ---------------------------------------------------------------------------
// Tone tables — store only freq×10 values; names are generated at runtime
// to keep static data small and avoid const-char-pointer patching issues.
// ---------------------------------------------------------------------------
static const uint32_t MOTO_FREQS[45] = {
2885,
3047,
3217,
3396,
3586,
3786,
3998,
4221,
4457,
4705,
4968,
5246,
5539,
5848,
6174,
6519,
6883,
7268,
7674,
8102,
8555,
9032,
9537,
10073,
10642,
11225,
11247,
11534,
11852,
11885,
12178,
12514,
12555,
12858,
13258,
13576,
13950,
13996,
14768,
15579,
16430,
17325,
18262,
19245,
20275,
};
// Index 0 = None (0), indices 1-50 = standard CTCSS tones
static const uint32_t CTCSS_FREQS[51] = {
0,
670,
719,
744,
770,
797,
825,
854,
885,
915,
948,
974,
1000,
1035,
1072,
1109,
1148,
1188,
1230,
1273,
1318,
1365,
1413,
1462,
1500,
1514,
1567,
1598,
1622,
1655,
1679,
1713,
1738,
1773,
1799,
1835,
1862,
1899,
1928,
1966,
1995,
2035,
2065,
2107,
2181,
2257,
2291,
2336,
2418,
2503,
2541,
};
// Generate a display name from a freq×10 value ("288.5Hz", "None" for 0)
static std::string freq_name(uint32_t freq_x10) {
if (freq_x10 == 0) return "None";
return to_string_dec_uint(freq_x10 / 10) + "." +
to_string_dec_uint(freq_x10 % 10) + "Hz";
}
// Common timing profiles used in the field
struct TimingPreset {
uint32_t dur_a; // ms
uint32_t dur_b; // ms
uint32_t gap; // ms
};
static const TimingPreset TIMING_PRESETS[4] = {
{1000, 3000, 0}, // Moto Std
{700, 1000, 0}, // Short Alert
{2000, 1000, 0}, // Fire Std
{3000, 3000, 0}, // Long Alert
};
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
// Parse one unsigned decimal integer from *p, advancing p past the digits and
// an optional trailing comma.
static uint32_t parse_uint_field(const char*& p) {
uint32_t v = 0;
while (*p >= '0' && *p <= '9')
v = v * 10 + (*p++ - '0');
if (*p == ',')
++p;
return v;
}
// Phase-delta for the 32-bit sine-table accumulator used by proc_tones.
// delta = freq_hz * 2^32 / sample_rate
// Using freq_x10 to avoid floating-point: delta = (freq_x10 * 2^32) / (sample_rate * 10)
uint32_t TwoTonePagerView::tone_delta(uint32_t freq_x10) const {
if (freq_x10 == 0) return 0;
return static_cast<uint32_t>(
(static_cast<uint64_t>(freq_x10) << 32) /
(static_cast<uint64_t>(SAMPLE_RATE) * 10ULL));
}
uint32_t TwoTonePagerView::ms_to_samples(uint32_t ms) const {
return static_cast<uint32_t>(
(static_cast<uint64_t>(ms) * SAMPLE_RATE) / 1000ULL);
}
// ---------------------------------------------------------------------------
// Preset encode / decode
// ---------------------------------------------------------------------------
std::string TwoTonePagerView::encode_preset() const {
return to_string_dec_uint(ctcss_idx) + "," +
to_string_dec_uint(tone_a_idx) + "," +
to_string_dec_uint(tone_b_idx) + "," +
to_string_dec_uint(dur_a) + "," +
to_string_dec_uint(dur_b) + "," +
to_string_dec_uint(gap_ms) + "," +
to_string_dec_uint(custom_freq_a_hz) + "," +
to_string_dec_uint(custom_freq_b_hz);
}
void TwoTonePagerView::decode_preset(const std::string& s) {
const char* p = s.c_str();
uint32_t ci = parse_uint_field(p);
uint32_t ai = parse_uint_field(p);
uint32_t bi = parse_uint_field(p);
uint32_t da = parse_uint_field(p);
uint32_t db = parse_uint_field(p);
uint32_t gp = parse_uint_field(p);
uint32_t cfa = (*p != '\0') ? parse_uint_field(p) : 405;
uint32_t cfb = (*p != '\0') ? parse_uint_field(p) : 814;
ctcss_idx = std::min(ci, static_cast<uint32_t>(CTCSS_COUNT - 1));
tone_a_idx = std::min(ai, CUSTOM_TONE_IDX);
tone_b_idx = std::min(bi, CUSTOM_TONE_IDX);
dur_a = std::max(uint32_t{100}, std::min(da, uint32_t{9900}));
dur_b = std::max(uint32_t{100}, std::min(db, uint32_t{9900}));
gap_ms = std::min(gp, uint32_t{9900});
custom_freq_a_hz = std::max(uint32_t{100}, std::min(cfa, uint32_t{9999}));
custom_freq_b_hz = std::max(uint32_t{100}, std::min(cfb, uint32_t{9999}));
}
std::string& TwoTonePagerView::slot_ref(uint32_t slot) {
switch (slot) {
case 2:
return preset_2;
case 3:
return preset_3;
case 4:
return preset_4;
case 5:
return preset_5;
default:
return preset_1;
}
}
std::string& TwoTonePagerView::slot_name_ref(uint32_t slot) {
switch (slot) {
case 2:
return preset_name_2;
case 3:
return preset_name_3;
case 4:
return preset_name_4;
case 5:
return preset_name_5;
default:
return preset_name_1;
}
}
void TwoTonePagerView::update_slot_name_display() {
const auto& name = slot_name_ref(preset_slot);
std::string display = name.empty() ? "(none)" : name;
if (display.size() > 8) display = display.substr(0, 8);
text_slot_name.set(display);
}
void TwoTonePagerView::save_preset(uint32_t slot) {
slot_ref(slot) = encode_preset();
text_status.set("Saved to slot " + to_string_dec_uint(slot));
}
void TwoTonePagerView::load_preset(uint32_t slot) {
decode_preset(slot_ref(slot));
// Update all UI fields without firing their on_change callbacks
options_ctcss.set_selected_index(ctcss_idx, false);
options_tone_a.set_selected_index(tone_a_idx, false);
options_tone_b.set_selected_index(tone_b_idx, false);
field_dur_a.set_value(static_cast<int32_t>(dur_a), false);
field_dur_b.set_value(static_cast<int32_t>(dur_b), false);
field_gap.set_value(static_cast<int32_t>(gap_ms), false);
symfield_custom_a.set_value(custom_freq_a_hz);
symfield_custom_b.set_value(custom_freq_b_hz);
options_timing.set_selected_index(detect_timing_preset(), false);
update_tx_time();
update_slot_name_display();
text_status.set("Loaded slot " + to_string_dec_uint(slot));
}
// ---------------------------------------------------------------------------
// Timing presets
// ---------------------------------------------------------------------------
size_t TwoTonePagerView::detect_timing_preset() const {
for (size_t i = 0; i < 4; i++) {
if (TIMING_PRESETS[i].dur_a == dur_a &&
TIMING_PRESETS[i].dur_b == dur_b &&
TIMING_PRESETS[i].gap == gap_ms)
return i;
}
return 4; // "Custom"
}
void TwoTonePagerView::apply_timing_preset(size_t idx) {
if (idx >= 4) return; // "Custom" — do not override current values
dur_a = TIMING_PRESETS[idx].dur_a;
dur_b = TIMING_PRESETS[idx].dur_b;
gap_ms = TIMING_PRESETS[idx].gap;
field_dur_a.set_value(static_cast<int32_t>(dur_a), false);
field_dur_b.set_value(static_cast<int32_t>(dur_b), false);
field_gap.set_value(static_cast<int32_t>(gap_ms), false);
update_tx_time();
}
// ---------------------------------------------------------------------------
// UI helpers
// ---------------------------------------------------------------------------
void TwoTonePagerView::update_tx_time() {
uint32_t total_ms = dur_a + gap_ms + dur_b;
std::string name_a = (tone_a_idx == CUSTOM_TONE_IDX)
? to_string_dec_uint(custom_freq_a_hz) + "Hz"
: freq_name(MOTO_FREQS[tone_a_idx]);
std::string name_b = (tone_b_idx == CUSTOM_TONE_IDX)
? to_string_dec_uint(custom_freq_b_hz) + "Hz"
: freq_name(MOTO_FREQS[tone_b_idx]);
text_time.set("TX time: " +
to_string_dec_uint(total_ms / 1000) + "." +
to_string_dec_uint((total_ms / 100) % 10) + "s" +
" A:" + name_a + " B:" + name_b);
}
// ---------------------------------------------------------------------------
// Transmission
// ---------------------------------------------------------------------------
bool TwoTonePagerView::start_tx() {
auto& td = shared_memory.bb_data.tones_data;
const uint32_t freq_a_x10 = (tone_a_idx == CUSTOM_TONE_IDX)
? custom_freq_a_hz * 10
: MOTO_FREQS[tone_a_idx];
const uint32_t freq_b_x10 = (tone_b_idx == CUSTOM_TONE_IDX)
? custom_freq_b_hz * 10
: MOTO_FREQS[tone_b_idx];
const uint32_t delta_a = tone_delta(freq_a_x10);
const uint32_t delta_b = tone_delta(freq_b_x10);
const uint32_t delta_c = tone_delta(CTCSS_FREQS[ctcss_idx]);
const uint32_t samp_a = ms_to_samples(dur_a);
const uint32_t samp_b = ms_to_samples(dur_b);
const uint32_t samp_g = ms_to_samples(gap_ms);
const bool with_ctcss = (ctcss_idx > 0);
// Clear tone defs we'll use
memset(&td, 0, sizeof(td));
uint8_t tone_count;
if (!with_ctcss) {
// Single-tone sequential mode
// tone_defs[0] = Tone A, tone_defs[1] = Tone B
td.tone_defs[0].delta = delta_a;
td.tone_defs[0].duration = samp_a;
td.tone_defs[1].delta = delta_b;
td.tone_defs[1].duration = samp_b;
if (gap_ms > 0) {
td.silence = samp_g;
td.message[0] = 0; // Tone A
td.message[1] = 255; // Silence (any index ≥ 32 triggers silence)
td.message[2] = 1; // Tone B
tone_count = 3;
} else {
td.message[0] = 0; // Tone A
td.message[1] = 1; // Tone B
tone_count = 2;
}
} else {
// Dual-tone mode: CTCSS mixed simultaneously with pager tones.
//
// proc_tones dual-tone indexing for digit n:
// main delta = tone_deltas[n * 2] = tone_defs[n*2].delta
// sub delta = tone_deltas[n*2 + 1] = tone_defs[n*2+1].delta
// duration = tone_durations[n] = tone_defs[n].duration
//
// digit 0 → Tone A (main) + CTCSS (sub), duration from tone_defs[0]
// digit 1 → Tone B (main) + CTCSS (sub), duration from tone_defs[1]
td.tone_defs[0].delta = delta_a; // digit 0 main
td.tone_defs[0].duration = samp_a;
td.tone_defs[1].delta = delta_c; // digit 0 sub (CTCSS); also digit 1 duration source
td.tone_defs[1].duration = samp_b;
td.tone_defs[2].delta = delta_b; // digit 1 main (Tone B)
td.tone_defs[2].duration = 0; // duration for digit 1 comes from tone_defs[1]
td.tone_defs[3].delta = delta_c; // digit 1 sub (CTCSS)
td.tone_defs[3].duration = 0;
if (gap_ms > 0) {
td.silence = samp_g;
td.message[0] = 0;
td.message[1] = 255;
td.message[2] = 1;
tone_count = 3;
} else {
td.message[0] = 0;
td.message[1] = 1;
tone_count = 2;
}
}
progressbar.set_max(tone_count);
progressbar.set_value(0);
transmitter_model.set_baseband_bandwidth(1'750'000);
transmitter_model.enable();
baseband::set_tones_config(
transmitter_model.channel_bandwidth(),
0, // no pre-silence
tone_count,
with_ctcss, // dual_tone flag
false); // no audio monitor output
return true;
}
void TwoTonePagerView::stop_tx() {
transmitter_model.disable();
baseband::kill_tone();
tx_view.set_transmitting(false);
text_status.set("Stopped.");
}
void TwoTonePagerView::on_tx_progress(uint32_t progress, bool done) {
if (done) {
transmitter_model.disable();
progressbar.set_value(0);
tx_view.set_transmitting(false);
text_status.set("Done.");
} else {
progressbar.set_value(progress);
}
}
// ---------------------------------------------------------------------------
// View lifecycle
// ---------------------------------------------------------------------------
void TwoTonePagerView::focus() {
options_tone_a.focus();
}
TwoTonePagerView::~TwoTonePagerView() {
transmitter_model.disable();
baseband::shutdown();
}
TwoTonePagerView::TwoTonePagerView(NavigationView& nav)
: nav_(nav) {
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
// Clamp restored settings to valid ranges before touching UI
ctcss_idx = std::min(ctcss_idx, static_cast<uint32_t>(CTCSS_COUNT - 1));
tone_a_idx = std::min(tone_a_idx, CUSTOM_TONE_IDX);
tone_b_idx = std::min(tone_b_idx, CUSTOM_TONE_IDX);
dur_a = std::max(uint32_t{100}, std::min(dur_a, uint32_t{9900}));
dur_b = std::max(uint32_t{100}, std::min(dur_b, uint32_t{9900}));
gap_ms = std::min(gap_ms, uint32_t{9900});
preset_slot = std::max(uint32_t{1}, std::min(preset_slot, uint32_t{5}));
custom_freq_a_hz = std::max(uint32_t{100}, std::min(custom_freq_a_hz, uint32_t{9999}));
custom_freq_b_hz = std::max(uint32_t{100}, std::min(custom_freq_b_hz, uint32_t{9999}));
// Build CTCSS options list
{
std::vector<std::pair<std::string, int32_t>> opts;
opts.reserve(CTCSS_COUNT);
for (size_t i = 0; i < CTCSS_COUNT; i++)
opts.push_back({freq_name(CTCSS_FREQS[i]), static_cast<int32_t>(i)});
options_ctcss.set_options(std::move(opts));
}
// Build Motorola tone options (same pool for both A and B); "Custom" appended
{
std::vector<std::pair<std::string, int32_t>> opts;
opts.reserve(MOTO_TONE_COUNT + 1);
for (size_t i = 0; i < MOTO_TONE_COUNT; i++)
opts.push_back({freq_name(MOTO_FREQS[i]), static_cast<int32_t>(i)});
opts.push_back({"Custom", static_cast<int32_t>(CUSTOM_TONE_IDX)});
options_tone_a.set_options(opts);
options_tone_b.set_options(std::move(opts));
}
add_children({&labels,
&options_ctcss,
&options_tone_a,
&options_tone_b,
&symfield_custom_a,
&symfield_custom_b,
&field_dur_a,
&field_dur_b,
&field_gap,
&options_timing,
&field_slot,
&button_save,
&button_load,
&text_slot_name,
&text_time,
&text_status,
&progressbar,
&tx_view});
// Restore saved indices into the options / number fields (no callbacks)
options_ctcss.set_selected_index(ctcss_idx, false);
options_tone_a.set_selected_index(tone_a_idx, false);
options_tone_b.set_selected_index(tone_b_idx, false);
symfield_custom_a.set_value(custom_freq_a_hz);
symfield_custom_b.set_value(custom_freq_b_hz);
field_dur_a.set_value(static_cast<int32_t>(dur_a), false);
field_dur_b.set_value(static_cast<int32_t>(dur_b), false);
field_gap.set_value(static_cast<int32_t>(gap_ms), false);
options_timing.set_selected_index(detect_timing_preset(), false);
field_slot.set_value(static_cast<int32_t>(preset_slot), false);
update_tx_time();
update_slot_name_display();
// --- Callbacks ---
options_ctcss.on_change = [this](size_t i, int32_t) {
ctcss_idx = static_cast<uint32_t>(i);
};
options_tone_a.on_change = [this](size_t i, int32_t) {
tone_a_idx = static_cast<uint32_t>(i);
update_tx_time();
};
options_tone_b.on_change = [this](size_t i, int32_t) {
tone_b_idx = static_cast<uint32_t>(i);
update_tx_time();
};
symfield_custom_a.on_change = [this](SymField&) {
uint32_t v = static_cast<uint32_t>(symfield_custom_a.to_integer());
custom_freq_a_hz = std::max(uint32_t{100}, std::min(v, uint32_t{9999}));
update_tx_time();
};
symfield_custom_b.on_change = [this](SymField&) {
uint32_t v = static_cast<uint32_t>(symfield_custom_b.to_integer());
custom_freq_b_hz = std::max(uint32_t{100}, std::min(v, uint32_t{9999}));
update_tx_time();
};
field_dur_a.on_change = [this](int32_t v) {
dur_a = static_cast<uint32_t>(v);
options_timing.set_selected_index(detect_timing_preset(), false);
update_tx_time();
};
field_dur_b.on_change = [this](int32_t v) {
dur_b = static_cast<uint32_t>(v);
options_timing.set_selected_index(detect_timing_preset(), false);
update_tx_time();
};
field_gap.on_change = [this](int32_t v) {
gap_ms = static_cast<uint32_t>(v);
options_timing.set_selected_index(detect_timing_preset(), false);
update_tx_time();
};
options_timing.on_change = [this](size_t i, int32_t) {
apply_timing_preset(i);
};
field_slot.on_change = [this](int32_t v) {
preset_slot = static_cast<uint32_t>(v);
update_slot_name_display();
};
button_save.on_select = [this](Button&) {
text_prompt(nav_, slot_name_ref(preset_slot), 12, ENTER_KEYBOARD_MODE_ALPHA,
[this](std::string&) {
save_preset(preset_slot);
update_slot_name_display();
});
};
button_load.on_select = [this](Button&) {
load_preset(preset_slot);
};
tx_view.on_edit_frequency = [this, &nav]() {
auto new_view = nav.push<FrequencyKeypadView>(transmitter_model.target_frequency());
new_view->on_changed = [this](rf::Frequency f) {
transmitter_model.set_target_frequency(f);
};
};
tx_view.on_start = [this]() {
if (start_tx()) {
tx_view.set_transmitting(true);
text_status.set("Transmitting...");
}
};
tx_view.on_stop = [this]() {
stop_tx();
};
}
} // namespace ui::external_app::two_tone_pager
@@ -0,0 +1,250 @@
/*
* Copyright (C) 2024 PortaPack Mayhem
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#ifndef __UI_TWO_TONE_PAGER_H__
#define __UI_TWO_TONE_PAGER_H__
#include "ui.hpp"
#include "ui_widget.hpp"
#include "ui_navigation.hpp"
#include "ui_transmitter.hpp"
#include "ui_textentry.hpp"
#include "app_settings.hpp"
#include "radio_state.hpp"
#include "portapack.hpp"
#include "message.hpp"
#include "string_format.hpp"
namespace ui::external_app::two_tone_pager {
class TwoTonePagerView : public View {
public:
TwoTonePagerView(NavigationView& nav);
~TwoTonePagerView();
TwoTonePagerView(const TwoTonePagerView&) = delete;
TwoTonePagerView(TwoTonePagerView&&) = delete;
TwoTonePagerView& operator=(const TwoTonePagerView&) = delete;
TwoTonePagerView& operator=(TwoTonePagerView&&) = delete;
void focus() override;
std::string title() const override { return "2-Tone TX"; };
private:
NavigationView& nav_;
static constexpr uint32_t SAMPLE_RATE = 1536000;
static constexpr size_t MOTO_TONE_COUNT = 45;
static constexpr size_t CTCSS_COUNT = 51;
static constexpr uint32_t CUSTOM_TONE_IDX = MOTO_TONE_COUNT; // sentinel: one past the table, use custom Hz field
TxRadioState radio_state_{
154280000ULL, // 154.280 MHz — common VHF paging frequency
1750000,
SAMPLE_RATE};
// Persisted settings
uint32_t ctcss_idx{0};
uint32_t tone_a_idx{8}; // 405.3 Hz
uint32_t tone_b_idx{24}; // 813.9 Hz
uint32_t dur_a{1000};
uint32_t dur_b{3000};
uint32_t gap_ms{0};
uint32_t preset_slot{1};
uint32_t custom_freq_a_hz{405}; // used when tone_a_idx == CUSTOM_TONE_IDX
uint32_t custom_freq_b_hz{814};
// Five preset slots — encoded tone/timing data and a user-chosen display name
std::string preset_1{"0,8,24,1000,3000,0"};
std::string preset_2{"0,8,24,1000,3000,0"};
std::string preset_3{"0,8,24,1000,3000,0"};
std::string preset_4{"0,8,24,1000,3000,0"};
std::string preset_5{"0,8,24,1000,3000,0"};
std::string preset_name_1{""};
std::string preset_name_2{""};
std::string preset_name_3{""};
std::string preset_name_4{""};
std::string preset_name_5{""};
app_settings::SettingsManager settings_{
"tx_twotone",
app_settings::Mode::TX,
{
{"ctcss"sv, &ctcss_idx},
{"tone_a"sv, &tone_a_idx},
{"tone_b"sv, &tone_b_idx},
{"dur_a"sv, &dur_a},
{"dur_b"sv, &dur_b},
{"gap"sv, &gap_ms},
{"slot"sv, &preset_slot},
{"custom_a"sv, &custom_freq_a_hz},
{"custom_b"sv, &custom_freq_b_hz},
{"preset1"sv, &preset_1},
{"preset2"sv, &preset_2},
{"preset3"sv, &preset_3},
{"preset4"sv, &preset_4},
{"preset5"sv, &preset_5},
{"pname1"sv, &preset_name_1},
{"pname2"sv, &preset_name_2},
{"pname3"sv, &preset_name_3},
{"pname4"sv, &preset_name_4},
{"pname5"sv, &preset_name_5},
}};
bool start_tx();
void stop_tx();
void on_tx_progress(uint32_t progress, bool done);
void apply_timing_preset(size_t idx);
void save_preset(uint32_t slot);
void load_preset(uint32_t slot);
std::string encode_preset() const;
void decode_preset(const std::string& s);
std::string& slot_ref(uint32_t slot);
std::string& slot_name_ref(uint32_t slot);
uint32_t tone_delta(uint32_t freq_x10) const;
uint32_t ms_to_samples(uint32_t ms) const;
size_t detect_timing_preset() const;
void update_tx_time();
void update_slot_name_display();
// --- Widgets ---
Labels labels{
{{0 * 8, 1 * 16}, "CTCSS:", Theme::getInstance()->fg_light->foreground},
{{0 * 8, 2 * 16}, "A:", Theme::getInstance()->fg_light->foreground},
{{13 * 8, 2 * 16}, "B:", Theme::getInstance()->fg_light->foreground},
{{0 * 8, 3 * 16}, "ADur:", Theme::getInstance()->fg_light->foreground},
{{9 * 8, 3 * 16}, "ms", Theme::getInstance()->fg_light->foreground},
{{12 * 8, 3 * 16}, "BDur:", Theme::getInstance()->fg_light->foreground},
{{21 * 8, 3 * 16}, "ms", Theme::getInstance()->fg_light->foreground},
{{0 * 8, 4 * 16}, "Gap: ", Theme::getInstance()->fg_light->foreground},
{{9 * 8, 4 * 16}, "ms", Theme::getInstance()->fg_light->foreground},
{{0 * 8, 5 * 16}, "Timing:", Theme::getInstance()->fg_light->foreground},
{{0 * 8, 6 * 16}, "Slot: ", Theme::getInstance()->fg_light->foreground},
{{0 * 8, 9 * 16}, "AHz:", Theme::getInstance()->fg_light->foreground},
{{8 * 8, 9 * 16}, "Hz", Theme::getInstance()->fg_light->foreground},
{{13 * 8, 9 * 16}, "BHz:", Theme::getInstance()->fg_light->foreground},
{{21 * 8, 9 * 16}, "Hz", Theme::getInstance()->fg_light->foreground},
};
OptionsField options_ctcss{
{7 * 8, 1 * 16},
9,
{}};
OptionsField options_tone_a{
{3 * 8, 2 * 16},
9,
{}};
OptionsField options_tone_b{
{16 * 8, 2 * 16},
9,
{}};
NumberField field_dur_a{
{5 * 8, 3 * 16},
4,
{100, 9900},
100,
' '};
NumberField field_dur_b{
{17 * 8, 3 * 16},
4,
{100, 9900},
100,
' '};
// Digit-by-digit entry for custom tone frequency (integer Hz, 4 slots, 01009999)
SymField symfield_custom_a{
{4 * 8, 9 * 16},
4,
SymField::Type::Dec};
SymField symfield_custom_b{
{17 * 8, 9 * 16},
4,
SymField::Type::Dec};
NumberField field_gap{
{5 * 8, 4 * 16},
4,
{0, 9900},
50,
' '};
OptionsField options_timing{
{8 * 8, 5 * 16},
12,
{{"Moto Std", 0},
{"Short Alert", 1},
{"Fire Std", 2},
{"Long Alert", 3},
{"Custom", 4}}};
NumberField field_slot{
{7 * 8, 6 * 16},
1,
{1, 5},
1,
' '};
Button button_save{
{9 * 8, 6 * 16, 6 * 8, 20},
"Save"};
Button button_load{
{16 * 8, 6 * 16, 6 * 8, 20},
"Load"};
// Shows the name of the currently selected preset slot (up to 8 visible chars)
Text text_slot_name{
{22 * 8, 6 * 16, 8 * 8, 16},
""};
Text text_time{
{0, 7 * 16, 30 * 8, 16},
""};
Text text_status{
{0, 8 * 16, 30 * 8, 16},
""};
ProgressBar progressbar{
{2 * 8, 14 * 16, UI_POS_WIDTH_REMAINING(4), 16}};
TransmitterView tx_view{
(int16_t)UI_POS_Y_BOTTOM(4),
10000,
9};
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::two_tone_pager
#endif /* __UI_TWO_TONE_PAGER_H__ */
+86
View File
@@ -0,0 +1,86 @@
/*
* Copyright (C) 2024 PortaPack Mayhem
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#include "ui.hpp"
#include "ui_two_tone_rx.hpp"
#include "ui_navigation.hpp"
#include "external_app.hpp"
namespace ui::external_app::two_tone_rx {
void initialize_app(ui::NavigationView& nav) {
nav.push<TwoToneRxView>();
}
} // namespace ui::external_app::two_tone_rx
extern "C" {
__attribute__((section(".external_app.app_two_tone_rx.application_information"), used)) application_information_t _application_information_two_tone_rx = {
/*.memory_location = */ (uint8_t*)0x00000000,
/*.externalAppEntry = */ ui::external_app::two_tone_rx::initialize_app,
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
/*.app_name = */ "2-Tone RX",
/*.bitmap_data = */ {
// 16×16 icon — pager device with receive arrow
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0xFC,
0x3F,
0xFE,
0x7F,
0x02,
0x40,
0xBA,
0x5D,
0x02,
0x40,
0xFE,
0x7F,
0xFE,
0x7F,
0x12,
0x48,
0x12,
0x48,
0xFC,
0x3F,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
},
/*.icon_color = */ ui::Color::yellow().v,
/*.menu_location = */ app_location_t::RX,
/*.desired_menu_position = */ -1,
// Uses the proc_tonedetect baseband processor
/*.m4_app_tag = portapack::spi_flash::image_tag_tonedetect */ {'P', 'T', 'N', 'E'},
/*.m4_app_offset = */ 0x00000000, // filled at compile time
};
} // extern "C"
@@ -0,0 +1,599 @@
/*
* Copyright (C) 2024 PortaPack Mayhem
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#include "ui_two_tone_rx.hpp"
#include "audio.hpp"
#include "baseband_api.hpp"
#include "portapack.hpp"
#include "string_format.hpp"
using namespace portapack;
namespace ui::external_app::two_tone_rx {
static constexpr uint32_t DETECT_WINDOW_MS = 40;
static constexpr Coord screen_width_px = 240;
static constexpr Coord screen_height_px = 320;
static constexpr Coord tone_log_top = 4 * 16;
static constexpr Coord waterfall_top = 13 * 16;
static constexpr Coord waterfall_height = 6 * 16;
static constexpr Rect tone_log_rect{0, tone_log_top, 30 * 8, waterfall_top - tone_log_top};
// ---------------------------------------------------------------------------
// Tone tables
// ---------------------------------------------------------------------------
static const uint32_t MOTO_FREQS[45] = {
2885,
3047,
3217,
3396,
3586,
3786,
3998,
4221,
4457,
4705,
4968,
5246,
5539,
5848,
6174,
6519,
6883,
7268,
7674,
8102,
8555,
9032,
9537,
10073,
10642,
11225,
11247,
11534,
11852,
11885,
12178,
12514,
12555,
12858,
13258,
13576,
13950,
13996,
14768,
15579,
16430,
17325,
18262,
19245,
20275,
};
// Index 0 = None (0), indices 150 = standard CTCSS tones (freq × 10)
static const uint32_t CTCSS_FREQS[51] = {
0,
670,
719,
744,
770,
797,
825,
854,
885,
915,
948,
974,
1000,
1035,
1072,
1109,
1148,
1188,
1230,
1273,
1318,
1365,
1413,
1462,
1500,
1514,
1567,
1598,
1622,
1655,
1679,
1713,
1738,
1773,
1799,
1835,
1862,
1899,
1928,
1966,
1995,
2035,
2065,
2107,
2181,
2257,
2291,
2336,
2418,
2503,
2541,
};
static std::string ctcss_name(uint32_t freq_x10) {
if (freq_x10 == 0) return "None";
return to_string_dec_uint(freq_x10 / 10) + "." +
to_string_dec_uint(freq_x10 % 10) + "Hz";
}
// ---------------------------------------------------------------------------
// MOTO table helpers
// ---------------------------------------------------------------------------
static constexpr uint32_t MOTO_NONE = 255;
static constexpr uint32_t MOTO_TRANSITION_SNAP_HZ = 30; // stricter live transition tolerance
static constexpr uint32_t MOTO_FINAL_MATCH_HZ = 60; // looser phase-end/logging tolerance
static constexpr uint32_t MOTO_TRANSITION_DELTA_HZ = 25; // raw shift needed to confirm close-in A→B handoff
static uint32_t abs_diff_u32(uint32_t a, uint32_t b) {
return (a > b) ? (a - b) : (b - a);
}
// Return the nearest MOTO table index within match_hz, or MOTO_NONE.
static uint32_t moto_index(uint32_t freq_hz, uint32_t match_hz = MOTO_TRANSITION_SNAP_HZ) {
if (freq_hz == 0) return MOTO_NONE;
uint32_t best_idx = MOTO_NONE;
uint32_t best_diff = match_hz + 1;
for (size_t i = 0; i < 45; i++) {
const uint32_t hz = MOTO_FREQS[i] / 10;
const uint32_t diff = (freq_hz > hz) ? (freq_hz - hz) : (hz - freq_hz);
if (diff < best_diff) {
best_diff = diff;
best_idx = i;
}
}
return best_idx;
}
// Format a detected tone for display, snapping to nearest MOTO table entry.
static std::string format_tone(uint32_t freq_hz, uint32_t duration_ms) {
const uint32_t idx = moto_index(freq_hz, MOTO_FINAL_MATCH_HZ);
std::string freq_str;
if (idx != MOTO_NONE) {
const uint32_t matched_x10 = MOTO_FREQS[idx];
freq_str = to_string_dec_uint(matched_x10 / 10) + "." +
to_string_dec_uint(matched_x10 % 10) + "Hz";
} else {
freq_str = to_string_dec_uint(freq_hz) + "Hz";
}
return freq_str + " " + to_string_dec_uint(duration_ms) + "ms";
}
// ---------------------------------------------------------------------------
// Waterfall rect
// ---------------------------------------------------------------------------
static constexpr ui::Rect waterfall_rect{0, waterfall_top, screen_width_px, waterfall_height};
// ---------------------------------------------------------------------------
// TwoToneRxView
// ---------------------------------------------------------------------------
void TwoToneRxView::focus() {
button_startstop.focus();
}
void TwoToneRxView::on_hide() {
if (running_) stop_rx();
}
TwoToneRxView::TwoToneRxView(NavigationView& nav)
: nav_{nav} {
prior_antenna_bias_ = get_antenna_bias();
debug_file_.append(u"DEBUG/TWOTONERX.TXT");
debug_file_.write_entry("==== two_tone_rx session start ====");
add_children({
&field_frequency,
&field_rf_amp,
&field_lna,
&field_vga,
&rssi,
&field_volume,
&labels,
&options_ctcss,
&field_squelch,
&button_startstop,
&button_clear,
&text_status,
&check_bias_t,
&tone_log_view,
});
// Populate CTCSS options
using opt_t = std::pair<std::string, int32_t>;
options_ctcss.set_options([&]() {
std::vector<opt_t> opts;
opts.reserve(51);
for (size_t i = 0; i < 51; i++)
opts.push_back({ctcss_name(CTCSS_FREQS[i]), (int32_t)i});
return opts;
}());
options_ctcss.set_by_value((int32_t)ctcss_idx);
options_ctcss.on_change = [this](size_t, int32_t v) {
ctcss_idx = (uint32_t)v;
if (running_) baseband::set_tonedetect_config((uint8_t)squelch_val, CTCSS_FREQS[ctcss_idx]);
};
field_squelch.set_value((int32_t)squelch_val);
field_squelch.on_change = [this](int32_t v) {
squelch_val = (uint32_t)v;
if (running_) baseband::set_tonedetect_config((uint8_t)squelch_val, CTCSS_FREQS[ctcss_idx]);
};
button_startstop.on_select = [this](Button&) {
if (running_)
stop_rx();
else
start_rx();
};
button_clear.on_select = [this](Button&) {
tone_log_entries_.clear();
tone_log_view.set_dirty();
text_status.set("");
reset_detect_state();
};
check_bias_t.set_value(bias_t_enabled);
check_bias_t.on_select = [this](Checkbox&, bool v) {
bias_t_enabled = v;
apply_bias_t(running_);
};
field_frequency.set_step(12500);
tone_log_view.set_parent_rect(tone_log_rect);
}
TwoToneRxView::~TwoToneRxView() {
if (running_) stop_rx();
}
void TwoToneRxView::reset_detect_state() {
detect_state_ = DetectState::IDLE;
phase_window_count_ = 0;
phase_freq_accum_ = 0;
phase_valid_windows_ = 0;
phase_last_freq_ = 0;
phase_last_is_first_ = true;
t1_avg_freq_ = 0;
t1_window_count_ = 0;
t1_transition_candidate_windows_ = 0;
t2_zero_window_count_ = 0;
debug_trace_active_ = false;
debug_trace_id_ = 0;
}
void TwoToneRxView::start_rx() {
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
audio::set_rate(audio::Rate::Hz_24000);
audio::output::start();
receiver_model.set_hidden_offset(0);
receiver_model.set_sampling_rate(3072000);
receiver_model.set_baseband_bandwidth(1750000);
receiver_model.enable();
apply_bias_t(true);
baseband::set_tonedetect_config((uint8_t)squelch_val, CTCSS_FREQS[ctcss_idx]);
add_child(&waterfall);
waterfall.set_parent_rect(waterfall_rect);
running_ = true;
button_startstop.set_text("Stop");
text_status.set("");
}
void TwoToneRxView::stop_rx() {
remove_child(&waterfall);
apply_bias_t(false);
receiver_model.disable();
baseband::shutdown();
audio::output::stop();
running_ = false;
button_startstop.set_text("Start");
text_status.set("");
reset_detect_state();
}
void TwoToneRxView::apply_bias_t(bool active) {
if (active && bias_t_enabled) {
set_antenna_bias(true);
receiver_model.set_antenna_bias();
applied_bias_t_ = true;
return;
}
if (!applied_bias_t_) return;
set_antenna_bias(prior_antenna_bias_);
receiver_model.set_antenna_bias();
applied_bias_t_ = false;
}
void TwoToneRxView::debug_log(const std::string& line) {
const std::string trace_prefix = debug_trace_active_
? ("#" + to_string_dec_uint(debug_trace_id_) + " ")
: "";
const std::string entry = "[D] " + trace_prefix + line;
debug_file_.write_entry(entry);
}
void TwoToneRxView::debug_trace_begin(const std::string& line) {
if (!debug_trace_active_) {
debug_trace_id_ = ++debug_trace_counter_;
debug_trace_active_ = true;
}
debug_log(line);
}
void TwoToneRxView::finalize_detected_pair(uint32_t t2_avg, uint32_t t2_dur, const char* reason) {
const uint32_t t1_dur = t1_window_count_ * DETECT_WINDOW_MS;
const std::string suffix = std::string(" via ") + reason;
if (t1_avg_freq_ > 0 && t2_avg > 0 &&
t1_dur >= 500 && t2_dur >= 500 &&
moto_index(t1_avg_freq_, MOTO_FINAL_MATCH_HZ) != MOTO_NONE &&
moto_index(t2_avg, MOTO_FINAL_MATCH_HZ) != MOTO_NONE) {
debug_trace_begin("END ok T1=" + format_tone(t1_avg_freq_, t1_dur) +
" T2=" + format_tone(t2_avg, t2_dur) + suffix);
log_tone_pair(t1_avg_freq_, t1_dur, t2_avg, t2_dur);
} else {
debug_trace_begin("END drop T1=" + format_tone(t1_avg_freq_, t1_dur) +
" T2=" + format_tone(t2_avg, t2_dur) + suffix);
}
}
// ---------------------------------------------------------------------------
// on_tone_data — window-based two-tone detection state machine
//
// Every 40 ms the baseband sends tone_end=false with a raw frequency estimate
// for that window (freq_hz=0 if no stable QCII candidate).
// When the CTCSS gate closes it sends tone_end=true (freq_hz=0).
//
// First and last windows of each phase are discarded:
// - "First" is tracked with phase_last_is_first_.
// - "Last" is phase_last_freq_, never added to the accumulator while
// pending; discarded when the phase ends.
//
// T1→T2 transition: consecutive windows that snap to different MOTO table
// entries. The transition window becomes T2's first window.
//
// Final matching is done from the phase-average raw estimates collected here,
// not from the per-window nearest-table snap used for transition detection.
// ---------------------------------------------------------------------------
void TwoToneRxView::on_tone_data(const ToneDetectDataMessage* msg) {
if (!running_) return;
if (msg->tone_end) {
// Gate closed — finalize whatever phase we're in
if (detect_state_ == DetectState::T2_COLLECTING) {
// phase_last_freq_ is the last T2 window → discard it
uint32_t t2_avg = (phase_valid_windows_ > 0)
? (phase_freq_accum_ / phase_valid_windows_)
: phase_last_freq_;
uint32_t t2_dur = phase_window_count_ * DETECT_WINDOW_MS;
finalize_detected_pair(t2_avg, t2_dur, "tone_end");
} else if (debug_trace_active_) {
debug_log("END before T2");
}
reset_detect_state();
text_status.set("");
return;
}
// tone_end=false: this is a 40 ms measurement window
const uint32_t freq = msg->freq_hz;
if (freq == 0) {
if (detect_state_ == DetectState::T2_COLLECTING) {
const uint32_t t2_avg = (phase_valid_windows_ > 0)
? (phase_freq_accum_ / phase_valid_windows_)
: phase_last_freq_;
const uint32_t t2_dur = phase_window_count_ * DETECT_WINDOW_MS;
if (t2_avg > 0 && t2_dur >= 2000) {
finalize_detected_pair(t2_avg, t2_dur, "t2_break");
reset_detect_state();
text_status.set("");
return;
}
}
// A single weak/noisy dropout is tolerated during T2 so standard 3 s
// B tones are not lost to one marginal 40 ms estimate window.
if (detect_state_ == DetectState::T2_COLLECTING && t2_zero_window_count_ == 0) {
t2_zero_window_count_++;
if (debug_trace_active_) {
debug_log("T2 zero dropout");
}
text_status.set("T1:" + format_tone(t1_avg_freq_, t1_window_count_ * DETECT_WINDOW_MS) +
" T2 ?");
return;
}
// Otherwise, treat the missing estimate as a break in the sequence.
if (debug_trace_active_) {
debug_log("RESET zero");
}
reset_detect_state();
text_status.set("");
return;
}
t2_zero_window_count_ = 0;
switch (detect_state_) {
case DetectState::IDLE:
detect_state_ = DetectState::T1_COLLECTING;
phase_window_count_ = 1;
phase_freq_accum_ = 0;
phase_valid_windows_ = 0;
phase_last_freq_ = freq;
phase_last_is_first_ = true;
text_status.set("T1 ...");
break;
case DetectState::T1_COLLECTING: {
// Check for T1→T2 MOTO index transition.
// Skip if phase_last_freq_ is the first (noisy) window — it's
// marked for discard and must not trigger a false transition.
if (freq > 0 && phase_last_freq_ > 0 && !phase_last_is_first_) {
const uint32_t t1_ref_freq = (phase_valid_windows_ > 0)
? (phase_freq_accum_ / phase_valid_windows_)
: phase_last_freq_;
const uint32_t idx_ref = moto_index(t1_ref_freq, MOTO_FINAL_MATCH_HZ);
const uint32_t idx_last = moto_index(phase_last_freq_);
const uint32_t idx_cur = moto_index(freq);
const uint32_t idx_cur_final = moto_index(freq, MOTO_FINAL_MATCH_HZ);
bool transition_detected = false;
if (idx_last != MOTO_NONE && idx_cur != MOTO_NONE && idx_last != idx_cur) {
if (!debug_trace_active_) {
debug_trace_begin("T1 start " + format_tone(t1_ref_freq, phase_window_count_ * DETECT_WINDOW_MS));
}
debug_log("T1->T2 snap " + format_tone(phase_last_freq_, DETECT_WINDOW_MS) +
" -> " + format_tone(freq, DETECT_WINDOW_MS));
transition_detected = true;
} else if (idx_ref != MOTO_NONE &&
idx_cur_final != MOTO_NONE &&
idx_cur_final != idx_ref &&
abs_diff_u32(freq, t1_ref_freq) >= MOTO_TRANSITION_DELTA_HZ) {
t1_transition_candidate_windows_++;
if (debug_trace_active_) {
debug_log("T1 cand " + to_string_dec_uint(t1_transition_candidate_windows_) +
" ref=" + to_string_dec_uint(t1_ref_freq) +
" cur=" + to_string_dec_uint(freq));
}
transition_detected = (t1_transition_candidate_windows_ >= 2);
} else {
if (debug_trace_active_ && t1_transition_candidate_windows_ > 0) {
debug_log("T1 cand reset");
}
t1_transition_candidate_windows_ = 0;
}
if (transition_detected) {
// Transition detected.
// phase_last_freq_ is the last T1 window → discard.
t1_avg_freq_ = (phase_valid_windows_ > 0)
? (phase_freq_accum_ / phase_valid_windows_)
: phase_last_freq_;
t1_window_count_ = phase_window_count_;
// Start T2 with current window as first (also discarded)
detect_state_ = DetectState::T2_COLLECTING;
phase_window_count_ = 1;
phase_freq_accum_ = 0;
phase_valid_windows_ = 0;
phase_last_freq_ = freq;
phase_last_is_first_ = true;
t1_transition_candidate_windows_ = 0;
t2_zero_window_count_ = 0;
if (!debug_trace_active_) {
debug_trace_begin("T1 start " + format_tone(t1_avg_freq_, t1_window_count_ * DETECT_WINDOW_MS));
}
debug_log("T2 start " + format_tone(freq, DETECT_WINDOW_MS));
text_status.set("T1:" + format_tone(t1_avg_freq_, t1_window_count_ * DETECT_WINDOW_MS) + " T2...");
break;
}
}
// No transition: promote pending window into accumulator (unless first)
if (!phase_last_is_first_ && phase_last_freq_ > 0) {
phase_freq_accum_ += phase_last_freq_;
phase_valid_windows_++;
}
phase_last_freq_ = freq;
phase_last_is_first_ = false;
phase_window_count_++;
if (!debug_trace_active_ &&
phase_window_count_ >= 3 &&
moto_index(phase_last_freq_, MOTO_FINAL_MATCH_HZ) != MOTO_NONE) {
const uint32_t t1_ref_freq = (phase_valid_windows_ > 0)
? (phase_freq_accum_ / phase_valid_windows_)
: phase_last_freq_;
debug_trace_begin("T1 start " + format_tone(t1_ref_freq, phase_window_count_ * DETECT_WINDOW_MS));
}
text_status.set("T1 " + to_string_dec_uint(phase_window_count_ * DETECT_WINDOW_MS) + "ms...");
break;
}
case DetectState::T2_COLLECTING:
// Promote pending window into accumulator (unless first)
if (!phase_last_is_first_ && phase_last_freq_ > 0) {
phase_freq_accum_ += phase_last_freq_;
phase_valid_windows_++;
}
phase_last_freq_ = freq;
phase_last_is_first_ = false;
phase_window_count_++;
text_status.set("T1:" + format_tone(t1_avg_freq_, t1_window_count_ * DETECT_WINDOW_MS) +
" T2 " + to_string_dec_uint(phase_window_count_ * DETECT_WINDOW_MS) + "ms");
break;
}
}
void TwoToneRxView::log_tone_pair(uint32_t f1, uint32_t d1_ms, uint32_t f2, uint32_t d2_ms) {
// Keep the oldest entry at the top so newer pairs appear below it.
tone_log_entries_.push_back({++next_log_serial_, format_tone(f1, d1_ms) + " " + format_tone(f2, d2_ms)});
tone_log_view.set_dirty();
}
} // namespace ui::external_app::two_tone_rx
namespace ui {
template <>
void RecentEntriesTable<ui::external_app::two_tone_rx::TwoToneLogEntries>::draw(
const Entry& entry,
const Rect& target_rect,
Painter& painter,
const Style& style,
ui::RecentEntriesColumns&) {
std::string line = entry.line;
line.resize(target_rect.width() / 8, ' ');
painter.draw_string(target_rect.location(), style, line);
}
} // namespace ui
@@ -0,0 +1,214 @@
/*
* Copyright (C) 2024 PortaPack Mayhem
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#ifndef __UI_TWO_TONE_RX_H__
#define __UI_TWO_TONE_RX_H__
#include "ui.hpp"
#include "ui_navigation.hpp"
#include "ui_receiver.hpp"
#include "ui_freq_field.hpp"
#include "ui_spectrum.hpp"
#include "app_settings.hpp"
#include "radio_state.hpp"
#include "message.hpp"
#include "string_format.hpp"
#include "log_file.hpp"
#include "recent_entries.hpp"
namespace ui::external_app::two_tone_rx {
struct TwoToneLogEntry {
using Key = uint32_t;
static constexpr Key invalid_key = 0;
Key serial{};
std::string line{};
Key key() const { return serial; }
};
using TwoToneLogEntries = RecentEntries<TwoToneLogEntry>;
class TwoToneRxView : public View {
public:
TwoToneRxView(NavigationView& nav);
~TwoToneRxView();
TwoToneRxView(const TwoToneRxView&) = delete;
TwoToneRxView(TwoToneRxView&&) = delete;
TwoToneRxView& operator=(const TwoToneRxView&) = delete;
TwoToneRxView& operator=(TwoToneRxView&&) = delete;
void focus() override;
void on_hide() override;
std::string title() const override { return "2-Tone RX"; };
private:
NavigationView& nav_;
RxRadioState radio_state_{};
bool running_{false};
TwoToneLogEntries tone_log_entries_{};
uint32_t next_log_serial_{0};
uint32_t squelch_val{50};
uint32_t ctcss_idx{0};
bool bias_t_enabled{false};
bool prior_antenna_bias_{false};
bool applied_bias_t_{false};
LogFile debug_file_{};
app_settings::SettingsManager settings_{
"rx_twotone",
app_settings::Mode::RX,
{
{"tone_sq"sv, &squelch_val},
{"ctcss"sv, &ctcss_idx},
{"bias_t"sv, &bias_t_enabled},
}};
void start_rx();
void stop_rx();
void apply_bias_t(bool active);
void debug_log(const std::string& line);
void debug_trace_begin(const std::string& line);
void on_tone_data(const ToneDetectDataMessage* msg);
void log_tone_pair(uint32_t f1, uint32_t d1_ms, uint32_t f2, uint32_t d2_ms);
void finalize_detected_pair(uint32_t t2_avg, uint32_t t2_dur, const char* reason);
void reset_detect_state();
// ── Two-tone detection state machine ─────────────────────────────────────
//
// Each 40 ms measurement window from the baseband arrives as tone_end=false.
// tone_end=true signals that the CTCSS gate closed (end of transmission).
//
// Collection rule: discard first and last window of each phase.
// - "First" is always discarded (may contain carrier ramp-up).
// - "Last" is never added to the accumulator while pending; it is
// discarded when the next window confirms it is not the last, OR when
// the phase ends (tone_end=true or T1→T2 transition).
//
// Transition detection: two consecutive windows that snap to different MOTO
// table entries trigger T1→T2. The transition window becomes T2's first
// window (also discarded as T2's first).
enum class DetectState : uint8_t { IDLE,
T1_COLLECTING,
T2_COLLECTING };
DetectState detect_state_{DetectState::IDLE};
// Per-phase window collection
uint32_t phase_window_count_{0}; // total windows seen in this phase (including first)
uint32_t phase_freq_accum_{0}; // sum of non-first, non-last window frequencies
uint32_t phase_valid_windows_{0}; // count of windows in phase_freq_accum_
uint32_t phase_last_freq_{0}; // pending window (not yet added; discarded if last)
bool phase_last_is_first_{true}; // true when phase_last_freq_ is window 1 (discard)
// T1 result (stored at T1→T2 transition for use when pair is logged)
uint32_t t1_avg_freq_{0};
uint32_t t1_window_count_{0};
uint8_t t1_transition_candidate_windows_{0};
uint8_t t2_zero_window_count_{0};
bool debug_trace_active_{false};
uint32_t debug_trace_id_{0};
uint32_t debug_trace_counter_{0};
// ── Row 0: frequency + RF controls ───────────────────────────────────────
RxFrequencyField field_frequency{
{UI_POS_X(0), UI_POS_Y(0)},
nav_};
RFAmpField field_rf_amp{
{UI_POS_X(13), UI_POS_Y(0)}};
LNAGainField field_lna{
{UI_POS_X(15), UI_POS_Y(0)}};
VGAGainField field_vga{
{UI_POS_X(18), UI_POS_Y(0)}};
RSSI rssi{
{UI_POS_X(21), UI_POS_Y(0), UI_POS_WIDTH_REMAINING(24), 4}};
AudioVolumeField field_volume{
{UI_POS_X_RIGHT(2), UI_POS_Y(0)}};
// ── Row 1: CTCSS / Squelch / Start+Stop / Clear ──────────────────────────
Labels labels{
{{0 * 8, 1 * 16}, "CTCSS:", Theme::getInstance()->fg_light->foreground},
{{15 * 8, 1 * 16}, "Sq:", Theme::getInstance()->fg_light->foreground},
};
OptionsField options_ctcss{
{7 * 8, 1 * 16},
8,
{}};
NumberField field_squelch{
{19 * 8, 1 * 16},
2,
{0, 99},
1,
' '};
Button button_startstop{
{21 * 8, 1 * 16, 5 * 8, 18},
"Start"};
Button button_clear{
{26 * 8, 1 * 16, 4 * 8, 18},
"Clr"};
// ── Row 2: live status (active tone in progress) + Bias-T toggle ─────────
Text text_status{
{0, 2 * 16 + 4, 20 * 8, 16},
""};
Checkbox check_bias_t{
{20 * 8, 2 * 16 + 2},
9,
"Bias-T",
true};
ui::RecentEntriesColumns tone_log_columns{{
{"Tones", 0},
}};
ui::RecentEntriesTable<TwoToneLogEntries> tone_log_view{tone_log_entries_, tone_log_columns};
// ── Waterfall (rest of screen) ────────────────────────────────────────────
spectrum::WaterfallView waterfall{};
// ── Message handler ───────────────────────────────────────────────────────
MessageHandlerRegistration message_handler_tone{
Message::ID::ToneDetectData,
[this](const Message* const p) {
const auto* msg = reinterpret_cast<const ToneDetectDataMessage*>(p);
this->on_tone_data(msg);
}};
};
} // namespace ui::external_app::two_tone_rx
#endif /* __UI_TWO_TONE_RX_H__ */
+37
View File
@@ -182,20 +182,24 @@ void MAX2831::set_mode(const Mode mode) {
case Mode::Shutdown:
gpio_max2831_rx_enable.write(0); /* RXTX=0 */
gpio_max283x_enable.write(0); /* ENABLE=0 */
set_rssi_mux(0);
break;
case Mode::Standby:
gpio_max2831_rx_enable.write(1); /* RXTX=1 */
gpio_max283x_enable.write(0); /* ENABLE=0 */
set_rssi_mux(0);
break;
case Mode::Transmit:
case Mode::Tx_Calibration:
gpio_max2831_rx_enable.write(1); /* RXTX=1 for TX */
gpio_max283x_enable.write(1); /* ENABLE=1 */
set_rssi_mux(2); // transmit power
break;
case Mode::Receive:
case Mode::Rx_Calibration:
gpio_max2831_rx_enable.write(0); /* RXTX=0 for RX */
gpio_max283x_enable.write(1); /* ENABLE=1 */
set_rssi_mux(1); // RSSI
break;
}
@@ -475,5 +479,38 @@ void MAX2831::write(const address_t reg_num, const reg_t value) {
}
}
void MAX2831::set_rssi_mux(const uint8_t mode) {
/* RSSI MUX allows switching the RSSI output between different internal signals.
* 0 = disable mux
* 1 = RSSI
* 2 = TX_POWER
* 3 = TEMP
*/
uint16_t mux_val = 0;
// Select the appropriate constant based on the input mode.
if (mode == 0) {
mux_val = 0;
} else {
// Select the appropriate constant based on the input mode.
switch (mode) {
case 3:
mux_val = REG8_RSSI_MUX_TEMP;
break;
case 2:
mux_val = REG8_RSSI_MUX_TX_POWER;
break;
case 1:
default:
mux_val = REG8_RSSI_MUX_RSSI;
break;
}
mux_val |= REG8_RSSI_EN;
}
set_reg_field(8, REG8_RSSI_MUX_MASK | REG8_RSSI_EN, mux_val);
flush_reg(8);
}
} // namespace max2831
#endif
+3
View File
@@ -151,6 +151,7 @@ constexpr uint16_t REG8_RSSI_MUX_MASK = 0x0300; /* D9:D8 */
constexpr uint16_t REG8_RSSI_MUX_RSSI = (0 << REG8_RSSI_MUX_SHIFT);
constexpr uint16_t REG8_RSSI_MUX_TEMP = (1 << REG8_RSSI_MUX_SHIFT);
constexpr uint16_t REG8_RSSI_MUX_TX_POWER = (2 << REG8_RSSI_MUX_SHIFT);
constexpr uint16_t REG8_RSSI_EN = (1 << 10);
constexpr uint16_t REG8_RXVGA_GAIN_SPI_EN_SHIFT = 12;
constexpr uint16_t REG8_RXVGA_GAIN_SPI_EN = (1 << REG8_RXVGA_GAIN_SPI_EN_SHIFT);
@@ -213,6 +214,8 @@ class MAX2831 : public MAX283x {
reg_t read(const address_t reg_num) override;
void write(const address_t reg_num, const reg_t value) override;
void set_rssi_mux(const uint8_t mode);
private:
spi::arbiter::Target& _target;
Mode _mode{Mode::Standby};
+1 -1
View File
@@ -636,7 +636,7 @@ init_status_t init() {
// This function returns LD_SUCCESS (0) if the FPGA confirms the bitstream
// Call fpga_bridge_init and continue boot regardless of result
// (Watchdog was resetting device when we halted with while(1))
int load_result = fpga_bridge_init();
int load_result = fpga_bridge_init(&shared_memory.bb_data.data[0]);
(void)load_result; // Ignore result for now, just let boot continue
/* RELEASE FPGA RESET */
+27 -22
View File
@@ -46,17 +46,17 @@ 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_min = 0.4;
#ifdef PRALINE
constexpr float rssi_voltage_max = 2.4;
#else
constexpr float rssi_voltage_max = 2.2;
// constexpr int raw_min = rssi_sample_range * rssi_voltage_min / adc_voltage_max;
constexpr int raw_min = 0;
#endif
constexpr float adc_voltage_max = 3.3;
constexpr int raw_max = rssi_sample_range * rssi_voltage_max / adc_voltage_max;
constexpr int raw_min = (rssi_sample_range * rssi_voltage_min) / adc_voltage_max;
constexpr int raw_max = (int)(((rssi_sample_range * rssi_voltage_max) / adc_voltage_max) + 0.5f);
constexpr int raw_delta = raw_max - raw_min;
if (!vertical_rssi_enabled) {
@@ -117,7 +117,6 @@ 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};
@@ -226,7 +225,6 @@ 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_) {
@@ -434,25 +432,38 @@ void RSSIGraph::paint(Painter& painter) {
void RSSIGraph::add_values(int16_t rssi_min, int16_t rssi_avg, int16_t rssi_max, int16_t db) {
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.4V to 2.4V
*/
constexpr int rssi_sample_range = 256;
// constexpr float rssi_voltage_min = 0.4;
constexpr float rssi_voltage_min = 0.4;
#ifdef PRALINE
constexpr float rssi_voltage_max = 2.4;
#else
constexpr float rssi_voltage_max = 2.2;
// constexpr int raw_min = rssi_sample_range * rssi_voltage_min / adc_voltage_max;
constexpr int raw_min = 0;
#endif
constexpr float adc_voltage_max = 3.3;
constexpr int raw_max = rssi_sample_range * rssi_voltage_max / adc_voltage_max;
constexpr int raw_min = (rssi_sample_range * rssi_voltage_min) / adc_voltage_max;
constexpr int raw_max = (int)(((rssi_sample_range * rssi_voltage_max) / adc_voltage_max) + 0.5f);
constexpr int raw_delta = raw_max - raw_min;
// vertical bottom to top level meters
// y_avg
const range_t<int> y_avg_range{0, r.height() - 1};
const int16_t y_avg = y_avg_range.clip((rssi_avg - raw_min) * r.height() / raw_delta);
// y_min
const range_t<int> y_min_range{0, y_avg};
const int16_t y_min = y_min_range.clip((rssi_min - raw_min) * r.height() / raw_delta);
const range_t<int> y_max_range{y_avg + 1, r.height() - 1};
// y_max
const range_t<int> y_max_range{y_avg, r.height() - 1};
const int16_t y_max = y_max_range.clip((rssi_max - raw_min) * r.height() / raw_delta);
// range
const range_t<int> db_range{-80, 10};
int16_t db_ = db_range.clip(db);
db_ = db_ - 10;
db_ -= 10;
db_ = db_ * r.height() / 90;
db_ = r.height() + db_;
@@ -529,12 +540,6 @@ 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 */
+1 -8
View File
@@ -64,18 +64,11 @@ 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};
+8
View File
@@ -683,6 +683,14 @@ set(MODE_CPPSRC
)
DeclareTargets(PSCD subcar)
### Tone Detect RX
set(MODE_CPPSRC
proc_tonedetect.cpp
)
DeclareTargets(PTNE tonedetect)
### Morse RX Decoder
set(MODE_CPPSRC
-25
View File
@@ -102,31 +102,6 @@ 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++) {
+6 -6
View File
@@ -46,15 +46,15 @@ void EPIRBTXProcessor::execute(const buffer_c8_t& buffer) {
if (mode_bpsk) {
// BPSK Manchester beacon signal
if (bpsk_pre_count < config_pre_count) {
// Pre-count state: send a negative phase carrier during pre-count
// Pre-count state: send carrier only during pre-count
bpsk_pre_count++;
re = i_neg;
im = q_neg;
re = i_carrier;
im = q_carrier;
} else if (bpsk_post_count > 0) {
// Post-count: send a negative phase carrier during post-count
// Post-count: send carrier only during post-count
bpsk_post_count++;
re = i_neg;
im = q_neg;
re = i_carrier;
im = q_carrier;
if (bpsk_post_count >= config_post_count) {
// End transmission here
byte_index = 0;
+5 -2
View File
@@ -60,14 +60,17 @@ class EPIRBTXProcessor : public BasebandProcessor {
// Size of the frame to send in BPSK mode
uint8_t frame_data_len = 0;
// BPSK parameters: Target phase +/-63° as per COSPAS/SARSAT specifications
static constexpr float phase_rad = 63.0f * M_PI / 180.0f;
// BPSK parameters: Target phase +/-1.1 RAD as per COSPAS/SARSAT specifications
static constexpr float phase_rad = 1.1f;
// I/Q values for BPSK (positive phase and negative phase)
int8_t i_pos = (int8_t)(cos(phase_rad) * 127);
int8_t q_pos = (int8_t)(sin(phase_rad) * 127);
int8_t i_neg = i_pos;
int8_t q_neg = -q_pos;
// I/Q values for carrier only
static constexpr int8_t i_carrier = 127;
static constexpr int8_t q_carrier = 0;
// COSPAS/SARSAT signal is manchester (2 states per bit) encoded 400 bit/sec
static const uint32_t samples_per_halfbit = TONES_SAMPLERATE / 400 / 2;
+332
View File
@@ -0,0 +1,332 @@
/*
* Copyright (C) 2024 PortaPack Mayhem
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#include "proc_tonedetect.hpp"
#include "audio_dma.hpp"
#include "portapack_shared_memory.hpp"
#include "event_m4.hpp"
#include <cmath>
static constexpr float SAMPLE_RATE = 24000.0f;
static constexpr uint32_t WINDOW_SAMPLES = 960; // 40 ms at 24 kHz (30 execute() calls)
static constexpr uint32_t WINDOW_MS = (1000 * WINDOW_SAMPLES) / (uint32_t)SAMPLE_RATE;
static constexpr uint32_t AUDIO_BLOCK_SAMPLES = 32; // demod_fm emits 32 audio samples per execute()
static constexpr uint32_t SQUELCH_HOLD_BLOCKS =
(100 * (uint32_t)SAMPLE_RATE) / (1000 * AUDIO_BLOCK_SAMPLES); // 100 ms hold at execute() block rate
static constexpr float PI_F = 3.14159265f;
// Motorola/EIA QCII paging frequencies (×10 to avoid float in table)
static constexpr uint32_t MOTO_FREQS_X10[45] = {
2885,
3047,
3217,
3396,
3586,
3786,
3998,
4221,
4457,
4705,
4968,
5246,
5539,
5848,
6174,
6519,
6883,
7268,
7674,
8102,
8555,
9032,
9537,
10073,
10642,
11225,
11247,
11534,
11852,
11885,
12178,
12514,
12555,
12858,
13258,
13576,
13950,
13996,
14768,
15579,
16430,
17325,
18262,
19245,
20275,
};
// Minimum coherent Goertzel energy for MOTO tone detection.
// For amplitude A over WINDOW_SAMPLES: energy ≈ (A × N/2)² = (A × 240)².
// Threshold 1000 → requires A > ~13% of FM demod full scale (~650 Hz deviation).
// Set high enough that FM broadband noise (σ ≈ 0.3, expected per-bin energy ≈ 43)
// never triggers a false detection even when the carrier gate is briefly open.
static constexpr float MOTO_ENERGY_THRESHOLD = 1000.0f;
// Fixed carrier-detect threshold for the detection gate (independent of user squelch).
// FMSquelch returns true when HF noise is BELOW this value (= FM carrier present).
// 0.20 opens reliably on any clean FM carrier without false-opening on noise.
static constexpr float CARRIER_DETECT_THRESHOLD = 0.20f;
// Goertzel energy threshold for CTCSS detection.
// Pure CTCSS at 5% amplitude over 960 samples → energy ≈ (0.05 × 480)² = 576.
static constexpr float CTCSS_ENERGY_THRESHOLD = 30.0f;
void ToneDetectProcessor::configure(uint8_t squelch, uint32_t ctcss_f_x10) {
configured = false;
user_squelch_level = squelch;
ctcss_freq_x10 = ctcss_f_x10;
decim_0.configure(taps_11k0_decim_0.taps);
decim_1.configure(taps_11k0_decim_1.taps);
channel_filter.configure(taps_11k0_channel.taps, 2);
demod_fm.configure(24000, 5000);
audio_output.configure(false);
fm_squelch.set_threshold((float)user_squelch_level / 100.0f);
channel_spectrum.set_decimation_factor(1);
// Precompute Goertzel coefficient for CTCSS (if active).
// k = nearest DFT bin for the CTCSS frequency over WINDOW_SAMPLES.
if (ctcss_freq_x10 > 0) {
const float freq = (float)ctcss_freq_x10 / 10.0f;
const float k = roundf((float)WINDOW_SAMPLES * freq / SAMPLE_RATE);
const float omega = 2.0f * PI_F * k / (float)WINDOW_SAMPLES;
goertzel_coeff = 2.0f * cosf(omega);
} else {
goertzel_coeff = 0.0f;
}
// Precompute Goertzel coefficients for all 45 MOTO frequencies.
// Each filter is tuned to the EXACT MOTO frequency (not the nearest DFT bin center)
// so that every entry has a unique coefficient and maximum energy only at its
// specific frequency. This eliminates bin-sharing ambiguity and gives the best
// discrimination between close entries such as 1357.6 Hz and 1395.0 Hz.
for (size_t i = 0; i < 45; i++) {
const float freq = (float)MOTO_FREQS_X10[i] / 10.0f;
const float omega = 2.0f * PI_F * freq / SAMPLE_RATE;
moto_coeff[i] = 2.0f * cosf(omega);
moto_s1[i] = 0.0f;
moto_s2[i] = 0.0f;
}
// Fixed carrier-detect squelch — threshold never changes with user settings.
carrier_sq.set_threshold(CARRIER_DETECT_THRESHOLD);
// Reset all per-window state
goertzel_s1 = 0.0f;
goertzel_s2 = 0.0f;
window_sample_count = 0;
was_ctcss_detected = false;
tone_duration_windows = 0;
squelch_is_open = false;
squelch_hold = 0;
carrier_is_open = false;
carrier_hold = 0;
configured = true;
}
void ToneDetectProcessor::execute(const buffer_c8_t& buffer) {
if (!configured) return;
const auto decim_0_out = decim_0.execute(buffer, dst_buffer);
const auto decim_1_out = decim_1.execute(decim_0_out, dst_buffer);
// Feed IQ data into spectrum collector for the RF waterfall.
channel_spectrum.feed(decim_1_out, -5500, 5500, 3400);
const auto channel_out = channel_filter.execute(decim_1_out, dst_buffer);
auto audio_buf = demod_fm.execute(channel_out, audio_buffer);
// --- Audio muting squelch (user-adjustable level) ---
// FMSquelch returns true when HF noise is LOW (carrier present).
const bool fm_open = fm_squelch.execute(audio_buf);
if (fm_open) {
squelch_hold = SQUELCH_HOLD_BLOCKS;
squelch_is_open = true;
} else if (squelch_hold > 0) {
squelch_hold--;
} else {
squelch_is_open = false;
}
// --- Fixed carrier-detect gate (independent of user squelch level) ---
// carrier_sq always uses CARRIER_DETECT_THRESHOLD (0.20) regardless of squelch_val.
// This ensures the detection gate closes when the carrier disappears even when the
// user sets squelch=0 (always-open audio), preventing the state machine from
// accumulating noise windows or getting stuck between transmissions.
const bool carrier_raw = carrier_sq.execute(audio_buf);
if (carrier_raw) {
carrier_hold = SQUELCH_HOLD_BLOCKS;
carrier_is_open = true;
} else if (carrier_hold > 0) {
carrier_hold--;
} else {
carrier_is_open = false;
}
for (size_t i = 0; i < audio_buf.count; i++) {
const float s = audio_buf.p[i];
// Mute audio output when FM squelch is closed (does not affect Goertzel).
if (!squelch_is_open) audio_buf.p[i] = 0.0f;
// CTCSS Goertzel step (CTCSS mode only).
// Uses original unmuted sample so CTCSS energy is unaffected by audio muting.
if (ctcss_freq_x10 > 0) {
const float s0 = s + goertzel_coeff * goertzel_s1 - goertzel_s2;
goertzel_s2 = goertzel_s1;
goertzel_s1 = s0;
}
// MOTO frequency bank — Goertzel step for all 45 MOTO bins.
// Runs unconditionally so the full window always contributes to energy.
// Benefit over zero-crossing: coherent detection, no warm-up period,
// accurate on the very first window after the gate opens.
for (size_t j = 0; j < 45; j++) {
const float s0 = s + moto_coeff[j] * moto_s1[j] - moto_s2[j];
moto_s2[j] = moto_s1[j];
moto_s1[j] = s0;
}
// Window boundary: every WINDOW_SAMPLES samples = one 40 ms estimate window
if (++window_sample_count >= WINDOW_SAMPLES) {
window_sample_count = 0;
// --- Detection gate ---
// Carrier presence (carrier_is_open) is always required — without it,
// the FM demod outputs broadband noise that floods every Goertzel bin and
// triggers false detections regardless of the CTCSS threshold.
// CTCSS mode adds a second requirement: coherent CTCSS energy must also
// be present. This is the standard two-condition squelch used in real radios.
bool gate_open;
if (ctcss_freq_x10 > 0) {
const float power = goertzel_s1 * goertzel_s1 + goertzel_s2 * goertzel_s2 - goertzel_coeff * goertzel_s1 * goertzel_s2;
gate_open = carrier_is_open && (power > CTCSS_ENERGY_THRESHOLD);
goertzel_s1 = 0.0f;
goertzel_s2 = 0.0f;
} else {
gate_open = carrier_is_open;
}
// --- MOTO frequency identification via Goertzel energy ---
// Find the MOTO table entry with the highest coherent energy this window.
// Reset all states regardless of gate so each window starts fresh.
float energies[45]{};
uint32_t best_idx = 45; // 45 = sentinel (no match)
float best_energy = MOTO_ENERGY_THRESHOLD;
for (size_t j = 0; j < 45; j++) {
const float pwr = moto_s1[j] * moto_s1[j] + moto_s2[j] * moto_s2[j] - moto_coeff[j] * moto_s1[j] * moto_s2[j];
energies[j] = pwr;
moto_s1[j] = 0.0f;
moto_s2[j] = 0.0f;
if (pwr > best_energy) {
best_energy = pwr;
best_idx = j;
}
}
// Report a raw estimate from the local energy centroid around the best
// entry, and let the UI do the final table snap from the phase average.
uint32_t win_freq_hz = 0;
if (best_idx < 45) {
const size_t start = (best_idx > 0) ? (best_idx - 1) : best_idx;
const size_t end = (best_idx + 1 < 45) ? (best_idx + 1) : best_idx;
float weight_sum = 0.0f;
float weighted_freq_x10 = 0.0f;
for (size_t j = start; j <= end; j++) {
const float weight = energies[j] - MOTO_ENERGY_THRESHOLD;
if (weight > 0.0f) {
weight_sum += weight;
weighted_freq_x10 += weight * (float)MOTO_FREQS_X10[j];
}
}
if (weight_sum > 0.0f) {
win_freq_hz = (uint32_t)((weighted_freq_x10 / weight_sum) / 10.0f + 0.5f);
} else {
win_freq_hz = MOTO_FREQS_X10[best_idx] / 10;
}
}
if (gate_open) {
if (!was_ctcss_detected) {
tone_duration_windows = 0;
}
tone_duration_windows++;
was_ctcss_detected = true;
data_message.freq_hz = win_freq_hz;
data_message.duration_ms = tone_duration_windows * WINDOW_MS;
data_message.tone_end = false;
shared_memory.application_queue.push(data_message);
} else {
if (was_ctcss_detected) {
// Gate just closed — signal tone end to application
data_message.freq_hz = 0;
data_message.duration_ms = tone_duration_windows * WINDOW_MS;
data_message.tone_end = true;
shared_memory.application_queue.push(data_message);
tone_duration_windows = 0;
}
was_ctcss_detected = false;
}
}
}
audio_output.write(audio_buf);
}
void ToneDetectProcessor::on_message(const Message* const p) {
switch (p->id) {
case Message::ID::ToneDetectConfig: {
const auto& msg = *reinterpret_cast<const ToneDetectConfigureMessage*>(p);
configure(msg.squelch_level, msg.ctcss_freq_x10);
break;
}
case Message::ID::NBFMConfigure: {
const auto& msg = *reinterpret_cast<const NBFMConfigureMessage*>(p);
user_squelch_level = msg.squelch_level;
fm_squelch.set_threshold((float)user_squelch_level / 100.0f);
break;
}
case Message::ID::UpdateSpectrum:
case Message::ID::SpectrumStreamingConfig:
channel_spectrum.on_message(p);
break;
default:
break;
}
}
int main() {
audio::dma::init_audio_out();
EventDispatcher event_dispatcher{std::make_unique<ToneDetectProcessor>()};
event_dispatcher.run();
return 0;
}
+94
View File
@@ -0,0 +1,94 @@
/*
* Copyright (C) 2024 PortaPack Mayhem
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#ifndef __PROC_TONEDETECT_H__
#define __PROC_TONEDETECT_H__
#include "baseband_processor.hpp"
#include "baseband_thread.hpp"
#include "message.hpp"
#include "dsp_decimate.hpp"
#include "dsp_demodulate.hpp"
#include "audio_output.hpp"
#include "dsp_fir_taps.hpp"
#include "rssi_thread.hpp"
#include "spectrum_collector.hpp"
#include "dsp_squelch.hpp"
class ToneDetectProcessor : public BasebandProcessor {
public:
ToneDetectProcessor() {}
void execute(const buffer_c8_t& buffer) override;
void on_message(const Message* const p) override;
private:
void configure(uint8_t squelch, uint32_t ctcss_freq_x10);
BasebandThread baseband_thread{3072000, this, baseband::Direction::Receive};
RSSIThread rssi_thread{};
std::array<complex16_t, 512> dst{};
const buffer_c16_t dst_buffer{dst.data(), dst.size()};
std::array<float, 32> audio{};
const buffer_f32_t audio_buffer{audio.data(), audio.size()};
dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0{};
dsp::decimate::FIRC16xR16x32Decim8 decim_1{};
dsp::decimate::FIRAndDecimateComplex channel_filter{};
dsp::demodulate::FM demod_fm{};
AudioOutput audio_output{};
bool configured{false};
uint8_t user_squelch_level{0};
FMSquelch fm_squelch{}; // user-adjustable: drives audio muting only
FMSquelch carrier_sq{}; // fixed threshold: drives detection gate
// Audio muting squelch state (user-adjustable level, has hold for smooth audio)
bool squelch_is_open{false};
uint32_t squelch_hold{0};
// Carrier-detect gate state (fixed 0.20 threshold, separate from audio squelch)
bool carrier_is_open{false};
uint32_t carrier_hold{0};
// CTCSS gate via Goertzel algorithm (per 40 ms window)
uint32_t ctcss_freq_x10{0}; // 0 = None → fall back to FMSquelch gate
float goertzel_coeff{0.0f}; // 2 * cos(2π * k / WINDOW_SAMPLES)
float goertzel_s1{0.0f};
float goertzel_s2{0.0f};
// 40 ms measurement window
uint32_t window_sample_count{0};
bool was_ctcss_detected{false};
uint32_t tone_duration_windows{0};
// MOTO frequency bank — one Goertzel filter per MOTO table entry.
// States reset each window; coefficients set once in configure().
float moto_coeff[45]{};
float moto_s1[45]{};
float moto_s2[45]{};
ToneDetectDataMessage data_message{};
SpectrumCollector channel_spectrum{};
};
#endif /* __PROC_TONEDETECT_H__ */
-184
View File
@@ -28,110 +28,6 @@
#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;
@@ -158,85 +54,6 @@ 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);
@@ -260,5 +77,4 @@ void RSSIThread::run() {
rf::rssi::stop();
rf::rssi::dma::free();
#endif
}
File diff suppressed because it is too large Load Diff
@@ -17,16 +17,16 @@ extern "C" {
#ifdef PRALINE
/* FPGA Register Map Address 0x03 (Dual Purpose) */
#define FPGA_REG_RX_DIGITAL_GAIN 0x03 /* Digital Shift / scaling (RX Mode) */
#define FPGA_REG_TX_CONTROL 0x03 /* NCO_EN and TX flags (TX Mode) */
#define FPGA_REG_RX_DIGITAL_GAIN 0x03 /* Digital Shift / scaling (RX Mode) */
#define FPGA_REG_TX_CONTROL 0x03 /* NCO_EN and TX flags (TX Mode) */
/* FPGA Register Map Address 0x04 (Shared) */
#define FPGA_REG_RX_DC_BLOCK_WIDTH 0x04 /* Notch filter cutoff (RX Mode) */
#define FPGA_REG_TX_INTERP 0x04 /* Interpolation ratio (TX Mode) */
#define FPGA_REG_RX_DC_BLOCK_WIDTH 0x04 /* Notch filter cutoff (RX Mode) */
#define FPGA_REG_TX_INTERP 0x04 /* Interpolation ratio (TX Mode) */
/* FPGA Register Map Address 0x05 (Shared) */
#define FPGA_REG_RX_DC_ADAPT_RATE 0x05 /* Settle time/Integration (RX Mode) */
#define FPGA_REG_TX_PHASE_STEP 0x05 /* NCO frequency step (TX Mode) */
#define FPGA_REG_RX_DC_ADAPT_RATE 0x05 /* Settle time/Integration (RX Mode) */
#define FPGA_REG_TX_PHASE_STEP 0x05 /* NCO frequency step (TX Mode) */
/*
* FPGA Operating Mode
@@ -41,60 +41,60 @@ typedef enum {
* FPGA Register Addresses
* Note: Registers 3-5 are dual-purpose (meaning depends on RX/TX mode)
*/
#define FPGA_REG_CTRL 0x01 /* Control register */
#define FPGA_REG_DECIM 0x02 /* Decimation (RX) / unused (TX) */
#define FPGA_REG_SHARED_3 0x03 /* Dual-purpose register */
#define FPGA_REG_SHARED_4 0x04 /* Dual-purpose register */
#define FPGA_REG_SHARED_5 0x05 /* Dual-purpose register */
#define FPGA_REG_CTRL 0x01 /* Control register */
#define FPGA_REG_DECIM 0x02 /* Decimation (RX) / unused (TX) */
#define FPGA_REG_SHARED_3 0x03 /* Dual-purpose register */
#define FPGA_REG_SHARED_4 0x04 /* Dual-purpose register */
#define FPGA_REG_SHARED_5 0x05 /* Dual-purpose register */
/*
* Register 1 (CTRL) Bit Definitions
*/
#define FPGA_CTRL_DC_BLOCK_EN (1 << 0) /* DC block enable */
#define FPGA_CTRL_QUARTER_SHIFT_EN (1 << 1) /* Quarter-rate shift enable */
#define FPGA_CTRL_QUARTER_SHIFT_UP (1 << 2) /* Shift direction: 1=up, 0=down */
#define FPGA_CTRL_TX_MODE (1 << 5) /* TX mode indicator (if applicable) */
#define FPGA_CTRL_PRBS_EN (1 << 6) /* PRBS test mode */
#define FPGA_CTRL_TRIGGER_EN (1 << 7) /* External trigger enable */
#define FPGA_CTRL_DC_BLOCK_EN (1 << 0) /* DC block enable */
#define FPGA_CTRL_QUARTER_SHIFT_EN (1 << 1) /* Quarter-rate shift enable */
#define FPGA_CTRL_QUARTER_SHIFT_UP (1 << 2) /* Shift direction: 1=up, 0=down */
#define FPGA_CTRL_TX_MODE (1 << 5) /* TX mode indicator (if applicable) */
#define FPGA_CTRL_PRBS_EN (1 << 6) /* PRBS test mode */
#define FPGA_CTRL_TRIGGER_EN (1 << 7) /* External trigger enable */
/*
* Register 3 Dual-Purpose Definitions
*/
/* RX Mode: Digital gain/shift */
#define FPGA_REG3_RX_DIGITAL_GAIN 0x03
#define FPGA_RX_GAIN_SHIFT_MASK 0x0F /* Bits [3:0] - shift amount */
#define FPGA_REG3_RX_DIGITAL_GAIN 0x03
#define FPGA_RX_GAIN_SHIFT_MASK 0x0F /* Bits [3:0] - shift amount */
/* TX Mode: NCO control */
#define FPGA_REG3_TX_NCO_CTRL 0x03
#define FPGA_TX_NCO_EN (1 << 0) /* NCO enable */
#define FPGA_TX_NCO_INVERT (1 << 1) /* Invert spectrum */
#define FPGA_REG3_TX_NCO_CTRL 0x03
#define FPGA_TX_NCO_EN (1 << 0) /* NCO enable */
#define FPGA_TX_NCO_INVERT (1 << 1) /* Invert spectrum */
/*
* Register 4 Dual-Purpose Definitions
*/
/* RX Mode: DC block notch width */
#define FPGA_REG4_RX_DC_WIDTH 0x04
#define FPGA_RX_DC_WIDTH_MASK 0x07 /* Bits [2:0] */
#define FPGA_REG4_RX_DC_WIDTH 0x04
#define FPGA_RX_DC_WIDTH_MASK 0x07 /* Bits [2:0] */
/* TX Mode: Interpolation ratio */
#define FPGA_REG4_TX_INTERP 0x04
#define FPGA_TX_INTERP_MASK 0x07 /* Bits [2:0] */
#define FPGA_REG4_TX_INTERP 0x04
#define FPGA_TX_INTERP_MASK 0x07 /* Bits [2:0] */
/*
* Register 5 Dual-Purpose Definitions
*/
/* RX Mode: DC block adaptation rate */
#define FPGA_REG5_RX_DC_RATE 0x05
#define FPGA_RX_DC_RATE_MASK 0xFF /* Bits [7:0] */
#define FPGA_REG5_RX_DC_RATE 0x05
#define FPGA_RX_DC_RATE_MASK 0xFF /* Bits [7:0] */
/* TX Mode: NCO phase step (frequency) */
#define FPGA_REG5_TX_PHASE_STEP 0x05
#define FPGA_TX_PHASE_STEP_MASK 0xFF /* Bits [7:0] */
#define FPGA_REG5_TX_PHASE_STEP 0x05
#define FPGA_TX_PHASE_STEP_MASK 0xFF /* Bits [7:0] */
/* Export default values so other methods can use them */
#define FPGA_RX_DEFAULT_DIGITAL_GAIN 0x00
#define FPGA_RX_DEFAULT_DC_WIDTH 0x04
#define FPGA_RX_DEFAULT_ADAPT_RATE 0x08
#define FPGA_RX_DEFAULT_DC_WIDTH 0x04
#define FPGA_RX_DEFAULT_ADAPT_RATE 0x08
/*
* Core Functions
@@ -102,7 +102,7 @@ typedef enum {
/* Initialize the FPGA - loads bitstream from SPIFI flash
* Returns: 0 on success, non-zero on failure */
int fpga_bridge_init(void);
int fpga_bridge_init(uint8_t* mem_base);
/* Set operating mode - MUST be called before using mode-specific functions
* This ensures registers 3-5 are interpreted correctly */
+14 -6
View File
@@ -214,6 +214,14 @@ bool I2cDev_MAX17055::init(uint8_t addr_) {
return false;
}
bool I2cDev_MAX17055::reInit() {
if (!full_reset_and_init()) {
return false;
}
partialInit();
return true;
}
bool I2cDev_MAX17055::full_reset_and_init() {
if (!soft_reset()) {
return false;
@@ -235,12 +243,12 @@ bool I2cDev_MAX17055::soft_reset() {
}
bool I2cDev_MAX17055::initialize_custom_parameters() {
if (!write_register(0xD0, 0x03E8)) return false; // Unknown register, possibly related to battery profile
if (!write_register(0xDB, 0x0000)) return false; // ModelCfg
if (!write_register(0x05, 0x0000)) return false; // RepCap
uint32_t designcap = portapack::device_type == portapack::DEV_PORTARF ? __MAX17055_Design_Capacity_PRF__ * 2 : __MAX17055_Design_Capacity__ * 2; // the original design has a 2x multiplier here, so i keep it
if (!write_register(0x18, designcap)) return false; // DesignCap
if (!write_register(0x45, designcap / 32)) return false; // dQAcc = DesignCap / 32
if (!write_register(0xD0, 0x03E8)) return false; // Unknown register, possibly related to battery profile
if (!write_register(0xDB, 0x0000)) return false; // ModelCfg
if (!write_register(0x05, 0x0000)) return false; // RepCap
uint32_t designcap = portapack::persistent_memory::battery_cap_mah() * 2; // the original design has a 2x multiplier here, so i keep it
if (!write_register(0x18, designcap)) return false; // DesignCap
if (!write_register(0x45, designcap / 32)) return false; // dQAcc = DesignCap / 32
if (!write_register(0x1E, 0x03C0)) return false; // IChgTerm
if (!write_register(0x3A, 0x9661)) return false; // VEmpty
+1 -1
View File
@@ -290,7 +290,7 @@ class I2cDev_MAX17055 : public I2cDev {
uint16_t averageMVoltage(void);
int32_t instantCurrent(void);
uint16_t stateOfCharge(void);
bool reInit(); // call when battery parameters changed from ui. don't call if not needed, or the battery is not changed!!!
private:
const RegisterEntry* findEntry(const char* name) const;
+23
View File
@@ -158,6 +158,8 @@ class Message {
TimeSinkConfig = 100,
EPIRBTXData = 101,
P25TxConfigure = 102,
ToneDetectData = 103,
ToneDetectConfig = 104,
MAX
};
@@ -1861,4 +1863,25 @@ class NotificationDataMessage : public Message {
uint16_t timeout = 10000;
};
// Sent M0→M4: a tone detection event (tone ended, or periodic update while active)
class ToneDetectDataMessage : public Message {
public:
constexpr ToneDetectDataMessage()
: Message{ID::ToneDetectData} {}
uint32_t freq_hz{0}; // Dominant audio frequency in Hz (0 = silence)
uint32_t duration_ms{0}; // How long the tone lasted in milliseconds
bool tone_end{false}; // True = tone just ended; False = tone still active (periodic update)
};
// Sent M4→M0: configure the tone detector
class ToneDetectConfigureMessage : public Message {
public:
constexpr ToneDetectConfigureMessage(uint8_t squelch = 0, uint32_t ctcss_freq_x10 = 0)
: Message{ID::ToneDetectConfig},
squelch_level{squelch},
ctcss_freq_x10{ctcss_freq_x10} {}
uint8_t squelch_level{0};
uint32_t ctcss_freq_x10{0}; // CTCSS frequency × 10 (e.g. 1000 = 100.0 Hz); 0 = None
};
#endif /*__MESSAGE_H__*/
@@ -156,8 +156,7 @@ struct misc_config_t {
bool tx_amp_disabled : 1;
uint8_t tx_gain_max_db;
uint8_t PLACEHOLDER_1;
uint8_t PLACEHOLDER_2;
uint16_t batt_cap_mah;
};
static_assert(sizeof(misc_config_t) == sizeof(uint32_t));
@@ -445,6 +444,8 @@ void defaults() {
set_config_tx_disabled(false);
set_config_tx_amp_disabled(false);
set_config_tx_gain_max_db(47);
set_battery_cap_mah(0);
}
void init() {
@@ -1243,6 +1244,32 @@ int load_persistent_settings_from_file() {
return true;
}
bool battery_cap_valid() {
return (data->misc_config.batt_cap_mah >= BATT_18650_MIN_MAH && data->misc_config.batt_cap_mah <= BATT_18650_MAX_MAH);
}
void set_battery_cap_mah(uint16_t mah) {
if ((mah < BATT_18650_MIN_MAH || mah > BATT_18650_MAX_MAH) && mah != 0) {
// Invalid value, ignore it.
return;
}
if (data->misc_config.batt_cap_mah != mah) {
data->misc_config.batt_cap_mah = mah;
}
}
uint32_t battery_cap_mah() {
if (battery_cap_valid()) {
return data->misc_config.batt_cap_mah;
}
// we don't know, need to assume.
if (portapack::device_type == portapack::DEV_PORTARF) return 3000;
#ifdef PRALINE
return 2000; // with h4 + pro and h4pro + pro it is ~safe
#endif
return 2500; // h4 + one
}
// Pmem size helper
size_t data_size() {
@@ -45,6 +45,10 @@
#define PMEM_SIZE_BYTES 256 // total amount of pmem space in bytes, including checksum
#define PMEM_SIZE_WORDS (PMEM_SIZE_BYTES / 4)
// to check battery mins and maxes in pmem, and validity
#define BATT_18650_MIN_MAH 1000
#define BATT_18650_MAX_MAH 5000
using namespace modems;
using namespace serializer;
using namespace ui;
@@ -387,6 +391,11 @@ uint32_t pmem_data_word(uint32_t index);
uint32_t pmem_stored_checksum(void);
uint32_t pmem_calculated_checksum(void);
// battery capacity settings
void set_battery_cap_mah(uint16_t mah); // 0 is not known; use assumed default based on device type/build config
uint32_t battery_cap_mah();
bool battery_cap_valid();
size_t data_size();
} /* namespace persistent_memory */
@@ -55,19 +55,6 @@ struct ToneData {
/* NOTE: These structures must be located in the same location in both M4 and M0 binaries */
struct SharedMemory {
#ifdef PRALINE
/*
* Software RSSI: 8 packed I/Q samples from baseband_thread.
*
* baseband_thread copies 8 samples spread across buffer (no computation).
* Each sample is packed: Q in high 16 bits, I in low 16 bits.
* rssi_thread uses __SMUAD on each to compute I²+Q², finds peak.
*
* 8 samples avoids zero-crossing artifacts from single-sample approach.
*/
volatile uint32_t software_rssi_iq[8]{0}; // 8 packed I/Q samples
#endif
static constexpr size_t application_queue_k = 11;
static constexpr size_t app_local_queue_k = 11;
+1
View File
@@ -131,6 +131,7 @@ constexpr image_tag_t image_tag_morse{'P', 'M', 'R', 'S'};
constexpr image_tag_t image_tag_morsetx{'P', 'M', 'R', 'T'};
constexpr image_tag_t image_tag_rttyrx{'P', 'R', 'T', 'R'};
constexpr image_tag_t image_tag_rttytx{'P', 'R', 'T', 'T'};
constexpr image_tag_t image_tag_tonedetect{'P', 'T', 'N', 'E'};
constexpr image_tag_t image_tag_noop{'P', 'N', 'O', 'P'};
+1 -1
View File
@@ -24,4 +24,4 @@
# external app address ranges below must match those in linker file "external.ld"
maximum_application_size = 32*1024
external_apps_address_start = 0xADB00000
external_apps_address_end = 0xAE020000
external_apps_address_end = 0xAE060000