mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2026-09-14 18:43:01 +00:00
Add external 2-Tone TX/RX Apps (#3128)
This commit is contained in:
@@ -393,6 +393,11 @@ void set_moreserx_config(uint8_t mode) {
|
|||||||
send_message(&message);
|
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) {
|
void set_morsetx_config(uint8_t mode, uint32_t tone, float fm_delta) {
|
||||||
const MorseTXConfigureMessage message{mode, tone, fm_delta};
|
const MorseTXConfigureMessage message{mode, tone, fm_delta};
|
||||||
send_message(&message);
|
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_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_subghzd_config(uint8_t modulation, uint32_t sampling_rate);
|
||||||
void set_moreserx_config(uint8_t mode);
|
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_config(uint8_t mode, uint32_t tone, float fm_delta);
|
||||||
void set_morsetx_key(bool key_down);
|
void set_morsetx_key(bool key_down);
|
||||||
void set_wefax_config(uint8_t lpm, uint8_t ioc);
|
void set_wefax_config(uint8_t lpm, uint8_t ioc);
|
||||||
|
|||||||
+10
@@ -340,6 +340,14 @@ set(EXTCPPSRC
|
|||||||
#p25_tx
|
#p25_tx
|
||||||
external/p25_tx/main.cpp
|
external/p25_tx/main.cpp
|
||||||
external/p25_tx/ui_p25_tx.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
|
set(EXTAPPLIST
|
||||||
@@ -425,6 +433,8 @@ set(EXTAPPLIST
|
|||||||
time_sink
|
time_sink
|
||||||
kiss_tnc
|
kiss_tnc
|
||||||
p25_tx
|
p25_tx
|
||||||
|
two_tone_pager
|
||||||
|
two_tone_rx
|
||||||
)
|
)
|
||||||
|
|
||||||
# sdusb has type conflicts with PRALINE (HackRF Pro) - add only for non-PRALINE builds
|
# 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_epirb_tx (rwx) : org = 0xAE010000, len = 32k
|
||||||
ram_external_app_fpv_detect (rwx) : org = 0xAE020000, 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_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
|
SECTIONS
|
||||||
@@ -607,4 +609,16 @@ SECTIONS
|
|||||||
KEEP(*(.external_app.app_p25_tx.application_information));
|
KEEP(*(.external_app.app_p25_tx.application_information));
|
||||||
*(*ui*external_app*p25_tx*);
|
*(*ui*external_app*p25_tx*);
|
||||||
} > ram_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__ */
|
||||||
@@ -683,6 +683,14 @@ set(MODE_CPPSRC
|
|||||||
)
|
)
|
||||||
DeclareTargets(PSCD subcar)
|
DeclareTargets(PSCD subcar)
|
||||||
|
|
||||||
|
### Tone Detect RX
|
||||||
|
|
||||||
|
set(MODE_CPPSRC
|
||||||
|
proc_tonedetect.cpp
|
||||||
|
)
|
||||||
|
DeclareTargets(PTNE tonedetect)
|
||||||
|
|
||||||
|
|
||||||
### Morse RX Decoder
|
### Morse RX Decoder
|
||||||
|
|
||||||
set(MODE_CPPSRC
|
set(MODE_CPPSRC
|
||||||
|
|||||||
@@ -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__ */
|
||||||
@@ -158,6 +158,8 @@ class Message {
|
|||||||
TimeSinkConfig = 100,
|
TimeSinkConfig = 100,
|
||||||
EPIRBTXData = 101,
|
EPIRBTXData = 101,
|
||||||
P25TxConfigure = 102,
|
P25TxConfigure = 102,
|
||||||
|
ToneDetectData = 103,
|
||||||
|
ToneDetectConfig = 104,
|
||||||
MAX
|
MAX
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1861,4 +1863,25 @@ class NotificationDataMessage : public Message {
|
|||||||
uint16_t timeout = 10000;
|
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__*/
|
#endif /*__MESSAGE_H__*/
|
||||||
|
|||||||
@@ -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_morsetx{'P', 'M', 'R', 'T'};
|
||||||
constexpr image_tag_t image_tag_rttyrx{'P', 'R', 'T', 'R'};
|
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_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'};
|
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"
|
# external app address ranges below must match those in linker file "external.ld"
|
||||||
maximum_application_size = 32*1024
|
maximum_application_size = 32*1024
|
||||||
external_apps_address_start = 0xADB00000
|
external_apps_address_start = 0xADB00000
|
||||||
external_apps_address_end = 0xAE020000
|
external_apps_address_end = 0xAE060000
|
||||||
|
|||||||
Reference in New Issue
Block a user