mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2026-09-15 19:13:00 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ef2ae9874b | |||
| 5deeaed5f0 | |||
| d5f94398ce | |||
| fba15c48e5 | |||
| 888634ce35 | |||
| 03131c8a0c |
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)},
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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"};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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, 0100–9999)
|
||||
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__ */
|
||||
@@ -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 1–50 = 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__ */
|
||||
@@ -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
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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++) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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__ */
|
||||
@@ -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 */
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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'};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user