Compare commits

...

14 Commits

Author SHA1 Message Date
Pezsma 877ede5f86 Added morse receiver app. (#2923)
* Adder morse receiver app. Works with cw and fm mode. Adaptive speed learning. Logging.
2026-01-16 09:57:45 +01:00
gullradriel 19eb1c40ab fix NumberField wrapping when steps is not '1' and minimum can be negative (#2920) 2026-01-11 22:49:18 +01:00
gullradriel 9c03a18893 Looking glass marker no encoder (#2919)
* enable repeat mode for select button too
* added marker + and marker - buttons, removed non effective iqcalc tuning
2026-01-10 20:19:10 +01:00
gullradriel cd504eb4a7 Udpdate hackrf submodule (#2906)
* add missing parameter to usb endpoint init
* make firmware compile with latest hackrf
* updated submodule
* format code
2026-01-10 16:19:53 +01:00
Totoo dc976c7f38 make sd over usb external (#2918) 2026-01-10 15:10:23 +01:00
Totoo 266d90a600 Updated subcar processors (#2913) 2026-01-10 14:47:33 +01:00
Totoo 2884bb9ab7 make siggen ext (#2917) 2026-01-10 21:25:41 +08:00
Totoo 133b2d2065 fix for #2903 (#2914) 2026-01-10 20:25:30 +08:00
Totoo 2f2671f76c Some tweaks to support some usecases without encoder, also maximize the NewButton's text size (#2915) 2026-01-10 20:21:57 +08:00
Totoo f5c2c78733 fix looking glass marker on portarf (#2912) 2026-01-09 20:32:31 +00:00
Harin Lee e4ab7227fc Fix typos of 'transceiver' (#2910) 2026-01-08 10:34:05 +01:00
Harin Lee 93799bde6d New approach to support RX-only mode more cleanly (#2908)
* Add enabled checks in ReceiverModel and TransmitterModel

* Move TX limit logic to TransmitterModel from radio API

* Add TX disable functionality and UI
2026-01-07 12:29:56 +08:00
RML 3d979a613a remove github pages CNAME (#2905) 2026-01-04 09:25:45 +01:00
Harin Lee ee97b51eb3 Add some features to support RX-only mode (#2904)
* Remove duplicate LED control code
* Add TX limit settings and UI
* Fix App Manager to handle non-application menu items
2026-01-03 22:10:34 +01:00
56 changed files with 2195 additions and 707 deletions
-1
View File
@@ -1 +0,0 @@
portapack.ried.cl
-2
View File
@@ -296,10 +296,8 @@ set(CPPSRC
apps/ui_rds.cpp
apps/ui_recon_settings.cpp
apps/ui_recon.cpp
apps/ui_sd_over_usb.cpp
apps/ui_search.cpp
apps/ui_settings.cpp
apps/ui_siggen.cpp
apps/ui_sonde.cpp
apps/ui_ss_viewer.cpp
apps/ui_standalone_view.cpp
+1 -1
View File
@@ -98,7 +98,7 @@ class BattinfoView : public View {
"Volt"};
Button button_exit{
{UI_POS_X_CENTER(12), 17 * 16, UI_POS_WIDTH(12), UI_POS_HEIGHT(3)},
{UI_POS_X_CENTER(12), UI_POS_Y_BOTTOM(4), UI_POS_WIDTH(12), UI_POS_HEIGHT(2)},
"Back"};
static msg_t static_fn(void* arg);
Thread* thread{nullptr};
@@ -69,7 +69,7 @@ void GlassView::manage_beep_audio() {
}
}
void GlassView::get_max_power(const ChannelSpectrum& spectrum, uint8_t bin, uint8_t& max_power) {
void GlassView::get_max_power(const ChannelSpectrum& spectrum, uint16_t bin, uint8_t& max_power) {
if (mode == LOOKING_GLASS_SINGLEPASS) {
// <20MHz spectrum mode
if (bin < 120) {
@@ -91,7 +91,7 @@ void GlassView::get_max_power(const ChannelSpectrum& spectrum, uint8_t bin, uint
}
}
rf::Frequency GlassView::get_freq_from_bin_pos(uint8_t pos) {
rf::Frequency GlassView::get_freq_from_bin_pos(uint16_t pos) {
rf::Frequency freq_at_pos = 0;
if (mode == LOOKING_GLASS_SINGLEPASS) {
// starting from the middle, minus 8 ignored bin on each side. Since pos is [-120,120] after the (pos - 120), it's divided by screen_width(240)/2 => 120
@@ -101,7 +101,14 @@ rf::Frequency GlassView::get_freq_from_bin_pos(uint8_t pos) {
return freq_at_pos;
}
void GlassView::on_marker_change() {
void GlassView::on_marker_change(int8_t delta) {
if ((marker_pixel_index + delta) < 0)
marker_pixel_index = marker_pixel_index + delta + screen_width;
else if ((marker_pixel_index + delta) > screen_width)
marker_pixel_index = marker_pixel_index + delta - screen_width;
else
marker_pixel_index = marker_pixel_index + delta;
marker = get_freq_from_bin_pos(marker_pixel_index);
field_marker.set_text(to_string_short_freq(marker));
plot_marker(marker_pixel_index); // Refresh marker on screen
@@ -200,7 +207,7 @@ void GlassView::on_channel_spectrum(const ChannelSpectrum& spectrum) {
baseband::spectrum_streaming_stop();
// Convert bins of this spectrum slice into a representative max_power and when enough, into pixels
// we actually need screen_width (240) of those bins
for (uint8_t bin = 0; bin < bin_length; bin++) {
for (uint16_t bin = 0; bin < bin_length; bin++) {
get_max_power(spectrum, bin, max_power);
if (max_power > range_max_power)
range_max_power = max_power;
@@ -208,7 +215,7 @@ void GlassView::on_channel_spectrum(const ChannelSpectrum& spectrum) {
if (bin == 119) {
uint8_t next_max_power = 0;
get_max_power(spectrum, bin + 1, next_max_power);
for (uint8_t it = 0; it < ignore_dc; it++) {
for (uint16_t it = 0; it < ignore_dc; it++) {
uint8_t med_max_power = (max_power + next_max_power) / 2; // due to the way process_bins works we have to keep resetting the color
if (process_bins(&med_max_power) == true)
return; // new line signaled, return
@@ -285,7 +292,7 @@ void GlassView::on_range_changed() {
max_power = 0;
bins_hz_size = 0;
on_marker_change();
on_marker_change(0);
update_range_field();
// set the sample rate and bandwidth
@@ -298,8 +305,8 @@ void GlassView::on_range_changed() {
receiver_model.set_target_frequency(f_center); // tune rx for this slice
}
void GlassView::plot_marker(uint8_t pos) {
uint8_t shift_y = 0;
void GlassView::plot_marker(uint16_t pos) {
uint16_t shift_y = 0;
if (live_frequency_view > 0) // plot one line down when in live view
{
shift_y = 16;
@@ -381,9 +388,10 @@ GlassView::GlassView(
&button_beep_squelch,
&field_marker,
&field_trigger,
&button_marker_minus,
&button_marker_plus,
&button_jump,
&button_rst,
&field_rx_iq_phase_cal,
&freq_stats});
load_presets(); // Load available presets from TXT files (or default).
@@ -491,13 +499,7 @@ GlassView::GlassView(
range_presets.set_selected_index(preset_index);
field_marker.on_encoder_change = [this](TextField&, EncoderEvent delta) {
if ((marker_pixel_index + delta) < 0)
marker_pixel_index = marker_pixel_index + delta + screen_width;
else if ((marker_pixel_index + delta) > screen_width)
marker_pixel_index = marker_pixel_index + delta - screen_width;
else
marker_pixel_index = marker_pixel_index + delta;
on_marker_change();
on_marker_change(delta);
};
field_marker.on_select = [this](TextField&) {
@@ -516,6 +518,14 @@ GlassView::GlassView(
update_range_field();
};
button_marker_minus.on_select = [this](Button&) {
on_marker_change(-1);
};
button_marker_plus.on_select = [this](Button&) {
on_marker_change(1);
};
button_jump.on_select = [this](Button&) {
// Launch Audio with peak frequency.
launch_audio(max_freq_hold);
@@ -525,13 +535,6 @@ GlassView::GlassView(
reset_live_view();
};
field_rx_iq_phase_cal.set_range(0, hackrf_r9 ? 63 : 31); // max2839 has 6 bits [0..63], max2837 has 5 bits [0..31]
field_rx_iq_phase_cal.set_value(get_spec_iq_phase_calibration_value()); // using accessor function of AnalogAudioView to read iq_phase_calibration_value from rx_audio.ini
field_rx_iq_phase_cal.on_change = [this](int32_t v) {
set_spec_iq_phase_calibration_value(v); // using accessor function of AnalogAudioView to write inside SPEC submenu, register value to max283x and save it to rx_audio.ini
};
set_spec_iq_phase_calibration_value(get_spec_iq_phase_calibration_value()); // initialize iq_phase_calibration in radio
display.scroll_set_area(109, screen_height - 1); // Restart scroll on the correct coordinates
// trigger:
@@ -581,15 +584,6 @@ void GlassView::on_freqchg(int64_t freq) {
on_range_changed();
}
uint8_t GlassView::get_spec_iq_phase_calibration_value() { // define accessor functions inside AnalogAudioView to read & write real iq_phase_calibration_value
return iq_phase_calibration_value;
}
void GlassView::set_spec_iq_phase_calibration_value(uint8_t cal_value) { // define accessor functions
iq_phase_calibration_value = cal_value;
radio::set_rx_max283x_iq_phase_calibration(iq_phase_calibration_value);
}
void GlassView::load_presets() {
File presets_file;
auto error = presets_file.open(looking_glass_dir / u"PRESETS.TXT");
@@ -66,9 +66,6 @@ class GlassView : public View {
void on_hide() override;
void focus() override;
uint8_t get_spec_iq_phase_calibration_value();
void set_spec_iq_phase_calibration_value(uint8_t cal_value);
private:
NavigationView& nav_;
Gradient gradient{};
@@ -80,11 +77,10 @@ class GlassView : public View {
uint8_t filter_index = 0; // OFF
uint8_t trigger = 32;
uint8_t mode = LOOKING_GLASS_FASTSCAN;
uint8_t live_frequency_view = 0; // Spectrum
uint8_t live_frequency_integrate = 3; // Default (3 * old value + new_value) / 4
uint8_t iq_phase_calibration_value{15}; // initial default RX IQ phase calibration value , used for both max2837 & max2839
int32_t beep_squelch = 20; // range from -100 to +20, >=20 disabled
bool beep_enabled = false; // activate on bip button click
uint8_t live_frequency_view = 0; // Spectrum
uint8_t live_frequency_integrate = 3; // Default (3 * old value + new_value) / 4
int32_t beep_squelch = 20; // range from -100 to +20, >=20 disabled
bool beep_enabled = false; // activate on bip button click
app_settings::SettingsManager settings_{
"rx_glass"sv,
app_settings::Mode::RX,
@@ -97,7 +93,6 @@ class GlassView : public View {
{"scan_mode"sv, &mode},
{"freq_view"sv, &live_frequency_view},
{"freq_integrate"sv, &live_frequency_integrate},
{"iq_phase_calibration"sv, &iq_phase_calibration_value}, // we are saving and restoring that CAL from Settings.
{"beep_squelch"sv, &beep_squelch},
{"beep_enabled"sv, &beep_enabled},
}};
@@ -116,9 +111,9 @@ class GlassView : public View {
void update_min(int32_t v);
void update_max(int32_t v);
void update_range_field();
void get_max_power(const ChannelSpectrum& spectrum, uint8_t bin, uint8_t& max_power);
rf::Frequency get_freq_from_bin_pos(uint8_t pos);
void on_marker_change();
void get_max_power(const ChannelSpectrum& spectrum, uint16_t bin, uint8_t& max_power);
rf::Frequency get_freq_from_bin_pos(uint16_t pos);
void on_marker_change(int8_t delta);
void retune();
bool process_bins(uint8_t* powerlevel);
void on_channel_spectrum(const ChannelSpectrum& spectrum);
@@ -128,7 +123,7 @@ class GlassView : public View {
void on_range_changed();
void reset_live_view();
void add_spectrum_pixel(uint8_t power);
void plot_marker(uint8_t pos);
void plot_marker(uint16_t pos);
void load_presets();
void populate_presets();
void launch_audio(rf::Frequency center_freq);
@@ -138,7 +133,7 @@ class GlassView : public View {
rf::Frequency f_center_ini{0};
rf::Frequency marker{0};
uint8_t marker_pixel_index{0};
uint16_t marker_pixel_index{0};
rf::Frequency marker_pixel_step{0};
// Size of one spectrum bin in Hz.
@@ -173,8 +168,7 @@ class GlassView : public View {
{{0, UI_POS_Y(0)}, "MIN: MAX: LNA VGA ", Theme::getInstance()->fg_light->foreground},
{{0, 1 * 16}, "RANGE: FILTER: AMP:", Theme::getInstance()->fg_light->foreground},
{{0, 2 * 16}, "P:", Theme::getInstance()->fg_light->foreground},
{{0, 3 * 16}, "MARKER: MHz RXIQCAL", Theme::getInstance()->fg_light->foreground},
//{{0, 4 * 16}, "RES: STEPS:", Theme::getInstance()->fg_light->foreground}};
{{0, 3 * 16}, "MARKER( / ): MHz", Theme::getInstance()->fg_light->foreground},
{{0, 4 * 16}, "RES: VOL:", Theme::getInstance()->fg_light->foreground}};
NumberField field_frequency_min{
@@ -223,16 +217,16 @@ class GlassView : public View {
""};
TextField field_marker{
{7 * 8, 3 * 16, 9 * 8, 16},
{12 * 8, 3 * 16, 9 * 8, 16},
""};
NumberField field_rx_iq_phase_cal{
{28 * 8, 3 * 16},
2,
{0, 63}, // 5 or 6 bits IQ CAL phase adjustment (range updated later)
1,
' ',
};
Button button_marker_minus{
{7 * 8, 3 * 16 + 4, 1 * 8, 1 * 8},
"-"};
Button button_marker_plus{
{9 * 8, 3 * 16 + 4, 1 * 8, 1 * 8},
"+"};
NumberField field_trigger{
{4 * 8, 4 * 16},
+34
View File
@@ -309,6 +309,39 @@ SetFrequencyCorrectionModel SetRadioView::form_collect() {
};
}
/* SetTXLimitView ************************************/
SetTXLimitView::SetTXLimitView(NavigationView& nav) {
add_children({
&labels,
&tx_gain_max_db,
&tx_disable_switch,
&tx_amp_disable_switch,
&button_save,
&button_cancel,
});
tx_disable_switch.set_value(pmem::config_tx_disabled());
tx_amp_disable_switch.set_value(pmem::config_tx_amp_disabled());
tx_gain_max_db.set_value(pmem::config_tx_gain_max_db());
button_save.on_select = [&nav, this](Button&) {
pmem::set_config_tx_disabled(tx_disable_switch.value());
pmem::set_config_tx_amp_disabled(tx_amp_disable_switch.value());
pmem::set_config_tx_gain_max_db(tx_gain_max_db.value());
send_system_refresh();
nav.pop();
};
button_cancel.on_select = [&nav, this](Button&) {
nav.pop();
};
}
void SetTXLimitView::focus() {
button_save.focus();
}
/* SetUIView *********************************************/
SetUIView::SetUIView(NavigationView& nav) {
@@ -1107,6 +1140,7 @@ void SettingsMenuView::on_populate() {
{"Freq. Correct", ui::Color::dark_cyan(), &bitmap_icon_options_radio, [this]() { nav_.push<SetFrequencyCorrectionView>(); }},
{"P.Memory Mgmt", ui::Color::dark_cyan(), &bitmap_icon_memory, [this]() { nav_.push<SetPersistentMemoryView>(); }},
{"Radio", ui::Color::dark_cyan(), &bitmap_icon_options_radio, [this]() { nav_.push<SetRadioView>(); }},
{"TX Limit", ui::Color::dark_cyan(), &bitmap_icon_options_radio, [this]() { nav_.push<SetTXLimitView>(); }},
{"SD Card", ui::Color::dark_cyan(), &bitmap_icon_sdcard, [this]() { nav_.push<SetSDCardView>(); }},
{"User Interface", ui::Color::dark_cyan(), &bitmap_icon_options_ui, [this]() { nav_.push<SetUIView>(); }},
{"Display", ui::Color::dark_cyan(), &bitmap_icon_brightness, [this]() { nav_.push<SetDisplayView>(); }},
+45
View File
@@ -264,6 +264,51 @@ class SetRadioView : public View {
SetFrequencyCorrectionModel form_collect();
};
class SetTXLimitView : public View {
public:
SetTXLimitView(NavigationView& nav);
void focus() override;
std::string title() const override { return "TX Limit"; };
private:
Labels labels{
{{1 * 8, 1 * 16}, "Limits RF TX Gain", Theme::getInstance()->fg_light->foreground},
{{1 * 8, 2 * 16}, "(This may affect", Theme::getInstance()->fg_light->foreground},
{{1 * 8, 3 * 16}, "all applications.)", Theme::getInstance()->fg_light->foreground},
{{2 * 8, 12 * 16}, "TX Max Gain:", Theme::getInstance()->fg_light->foreground},
};
Checkbox tx_disable_switch{
{1 * 8, 6 * 16},
23,
"Disable TX"};
Checkbox tx_amp_disable_switch{
{1 * 8, 8 * 16},
23,
"Disable TX Amp"};
NumberField tx_gain_max_db{
{20 * 8, 12 * 16},
6,
{0, 47},
1,
' ',
};
Button button_save{
{UI_POS_X_CENTER(12) - UI_POS_WIDTH(8), UI_POS_Y_BOTTOM(4), UI_POS_WIDTH(12), UI_POS_HEIGHT(2)},
"Save"};
Button button_cancel{
{UI_POS_X_CENTER(16) + UI_POS_WIDTH(8), UI_POS_Y_BOTTOM(4), UI_POS_WIDTH(12), UI_POS_HEIGHT(2)},
"Cancel",
};
};
using portapack::persistent_memory::backlight_timeout_t;
class SetUIView : public View {
+5
View File
@@ -368,6 +368,11 @@ void set_subghzd_config(uint8_t modulation = 0, uint32_t sampling_rate = 0) {
send_message(&message);
}
void set_moreserx_config() {
const MorseRXConfigureMessage message{};
send_message(&message);
}
static bool baseband_image_running = false;
void run_image(const spi_flash::image_tag_t image_tag) {
+1
View File
@@ -102,6 +102,7 @@ void set_siggen_tone(const uint32_t tone);
void set_siggen_config(const uint32_t bw, const uint32_t shape, const uint32_t duration);
void set_spectrum_painter_config(const uint16_t width, const uint16_t height, bool update, int32_t bw);
void set_subghzd_config(uint8_t modulation, uint32_t sampling_rate);
void set_moreserx_config();
void set_wefax_config(uint8_t lpm, uint8_t ioc);
void set_noaaapt_config();
void set_flex_config();
+3 -3
View File
@@ -4297,7 +4297,7 @@ static constexpr Bitmap bitmap_icon_tpms{
{16, 16},
bitmap_icon_tpms_data};
static constexpr uint8_t bitmap_icon_tranceivers_data[] = {
static constexpr uint8_t bitmap_icon_transceivers_data[] = {
0x80,
0x01,
0xC0,
@@ -4331,9 +4331,9 @@ static constexpr uint8_t bitmap_icon_tranceivers_data[] = {
0xFF,
0xFF,
};
static constexpr Bitmap bitmap_icon_tranceivers{
static constexpr Bitmap bitmap_icon_transceivers{
{16, 16},
bitmap_icon_tranceivers_data};
bitmap_icon_transceivers_data};
static constexpr uint8_t bitmap_icon_transmit_data[] = {
0x80,
+38 -25
View File
@@ -62,19 +62,24 @@ AppManagerView::AppManagerView(NavigationView& nav)
auto app_name = get_app_info(menu_view.highlighted_index(), true);
auto app_id = get_app_info(menu_view.highlighted_index(), false);
info += "Hidden:";
if (!app_name.empty()) {
info += "Hidden:";
if (is_blacklisted(app_name)) {
info += "Yes ";
} else {
info += "No ";
if (is_blacklisted(app_name)) {
info += "Yes ";
} else {
info += "No ";
}
}
info += "Autostart:";
if (is_autostart_app(app_id)) {
info += "Yes";
} else {
info += "No";
if (!app_id.empty()) {
info += "Autostart:";
if (is_autostart_app(app_id)) {
info += "Yes";
} else {
info += "No";
}
}
if (info.empty()) {
@@ -82,6 +87,8 @@ AppManagerView::AppManagerView(NavigationView& nav)
}
text_app_info.set(info);
button_hide_unhide.set_focusable(!app_name.empty());
button_set_cancel_autostart.set_focusable(!app_id.empty());
};
button_hide_unhide.on_select = [this](Button&) {
@@ -120,27 +127,23 @@ void AppManagerView::refresh_list() {
};
for (auto& app : NavigationView::appList) {
if (app.id == nullptr) continue;
app_list_index++;
menu_view.add_item({app.displayName + std::string(is_autostart_app(app.id) ? padding(app.displayName) : ""),
menu_view.add_item({app.displayName + std::string((app.id != nullptr && is_autostart_app(app.id)) ? padding(app.displayName) : ""),
app.iconColor,
app.icon,
[this, app_id = std::string(app.id)](KeyEvent) {
[this, app_id = std::string(app.id != nullptr ? app.id : "")](KeyEvent) {
button_hide_unhide.focus();
}});
}
ExternalItemsMenuLoader::load_all_external_items_callback([this, &index, &padding](ui::AppInfoConsole& app) {
if (app.appCallName == nullptr) return;
app_list_index++;
menu_view.add_item({app.appFriendlyName + std::string(is_autostart_app(app.appCallName) ? padding(app.appFriendlyName) : ""),
menu_view.add_item({app.appFriendlyName + std::string((app.appCallName != nullptr && is_autostart_app(app.appCallName)) ? padding(app.appFriendlyName) : ""),
ui::Color::light_grey(),
&bitmap_icon_sdcard,
[this, app_id = std::string(app.appCallName)](KeyEvent) {
[this, app_id = std::string(app.appCallName != nullptr ? app.appCallName : "")](KeyEvent) {
button_hide_unhide.focus();
}});
});
@@ -156,19 +159,29 @@ void AppManagerView::hide_app() {
std::vector<std::string> blacklist;
get_blacklist(blacklist);
blacklist.push_back(get_app_info(menu_view.highlighted_index(), true));
auto app_name = get_app_info(menu_view.highlighted_index(), true);
if (app_name.empty()) return;
blacklist.push_back(app_name);
write_blacklist(blacklist);
}
void AppManagerView::unhide_app() {
std::vector<std::string> blacklist;
get_blacklist(blacklist);
blacklist.erase(std::remove(blacklist.begin(), blacklist.end(), get_app_info(menu_view.highlighted_index(), true)), blacklist.end());
auto app_name = get_app_info(menu_view.highlighted_index(), true);
if (app_name.empty()) return;
blacklist.erase(std::remove(blacklist.begin(), blacklist.end(), app_name), blacklist.end());
write_blacklist(blacklist);
}
void AppManagerView::hide_unhide_app() {
if (is_blacklisted(get_app_info(menu_view.highlighted_index(), true)))
auto app_name = get_app_info(menu_view.highlighted_index(), true);
if (app_name.empty()) return;
if (is_blacklisted(app_name))
unhide_app();
else
hide_app();
@@ -218,6 +231,7 @@ void AppManagerView::set_auto_start() {
if (app_index >= app_list_index) return;
auto id_aka_app_call_name = get_app_info(app_index, false);
if (id_aka_app_call_name.empty()) return;
autostart_app = id_aka_app_call_name;
@@ -234,6 +248,7 @@ void AppManagerView::set_unset_autostart_app() {
if (app_index >= app_list_index) return;
auto id_aka_friendly_name = get_app_info(app_index, false);
if (id_aka_friendly_name.empty()) return;
if (is_autostart_app(id_aka_friendly_name))
unset_auto_start();
@@ -254,17 +269,15 @@ std::string AppManagerView::get_app_info(uint16_t index, bool is_display_name) {
std::string result;
for (auto& app : NavigationView::appList) {
if (app.id == nullptr) continue;
if (current_index == index) {
return is_display_name ? std::string(app.displayName) : std::string(app.id);
return is_display_name ? std::string(app.displayName != nullptr ? app.displayName : "") : std::string(app.id != nullptr ? app.id : "");
}
current_index++;
}
ExternalItemsMenuLoader::load_all_external_items_callback([&current_index, &index, &result, &is_display_name](ui::AppInfoConsole& app) {
if (app.appCallName == nullptr) return;
if (current_index == index) {
result = is_display_name ? app.appFriendlyName : app.appCallName;
result = is_display_name ? (app.appFriendlyName != nullptr ? app.appFriendlyName : "") : (app.appCallName != nullptr ? app.appCallName : "");
}
current_index++;
});
+17
View File
@@ -276,6 +276,20 @@ set(EXTCPPSRC
#subcarrx
external/subcarrx/main.cpp
external/subcarrx/ui_subcar.cpp
#siggen
external/siggen/main.cpp
external/siggen/ui_siggen.cpp
#sdusb
external/sdusb/main.cpp
external/sdusb/ui_sd_over_usb.cpp
#morse_radio
external/morse_radio/main.cpp
external/morse_radio/ui_morse_radio.cpp
)
set(EXTAPPLIST
@@ -345,4 +359,7 @@ set(EXTAPPLIST
adult_toys_controller
flex_rx
subcarrx
siggen
sdusb
morse_radio
)
+21 -2
View File
@@ -89,7 +89,9 @@ MEMORY
ram_external_app_flex_rx (rwx) : org = 0xADF00000, len = 32k
ram_external_app_sstvrx (rwx) : org = 0xADF10000, len = 32k
ram_external_app_subcarrx (rwx) : org = 0xADF20000, len = 32k
ram_external_app_siggen (rwx) : org = 0xADF30000, len = 32k
ram_external_app_sdusb (rwx) : org = 0xADF40000, len = 32k
ram_external_app_morse_radio (rwx) : org = 0xADF50000, len = 32k
}
SECTIONS
@@ -492,6 +494,23 @@ SECTIONS
*(*ui*external_app*subcarrx*);
} > ram_external_app_subcarrx
.external_app_siggen : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_siggen.application_information));
*(*ui*external_app*siggen*);
} > ram_external_app_siggen
.external_app_sdusb : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_sdusb.application_information));
*(*ui*external_app*sdusb*);
} > ram_external_app_sdusb
.external_app_morse_radio : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_morse_radio.application_information));
*(*ui*external_app*morse_radio*);
} > ram_external_app_morse_radio
}
+27 -12
View File
@@ -24,8 +24,7 @@
#include <cstdint>
#include <string>
#define MORSEDEC_ERROR "<ERR>"
#include "string_format.hpp"
namespace ui::external_app::morse_practice {
@@ -89,21 +88,28 @@ class MorseDecoder {
};
MorseDecoder() {}
DecodeResult handleInput(int32_t duration_ms) {
DecodeResult
handleInput(int32_t duration_ms) {
DecodeResult result = {"", 0.0};
if (duration_ms < 5 && duration_ms > -5) return result;
if (duration_ms > 0) {
pulse_history_.push_back(duration_ms);
double dah_prob = getDahProbability(duration_ms);
current_sequence_ += (dah_prob > 0.5) ? '-' : '.';
last_confidence_ = (dah_prob > 0.5) ? dah_prob : (1.0 - dah_prob);
last_sequence_ = current_sequence_;
} else {
uint32_t gap_duration = -duration_ms;
pulse_gaps_.push_back(gap_duration);
if (gap_duration >= getInterCharThreshold() && !current_sequence_.empty()) {
result.text = lookupMorse(current_sequence_);
result.confidence = (result.text != MORSEDEC_ERROR) ? last_confidence_ : 0.0;
result.confidence = (result.text[0] != '{') ? last_confidence_ : 0.0;
if (gap_duration >= getInterWordThreshold()) {
result.text += " ";
}
@@ -116,9 +122,9 @@ class MorseDecoder {
return result;
}
inline double getInterElementThreshold() { return time_unit_ms_ * 2.0; }
inline double getInterCharThreshold() { return time_unit_ms_ * 4.0; }
inline double getInterWordThreshold() { return time_unit_ms_ * 7.0; }
inline double getInterElementThreshold() { return time_unit_ms_ * 0.8; }
inline double getInterCharThreshold() { return time_unit_ms_ * 2.5; }
inline double getInterWordThreshold() { return time_unit_ms_ * 6.0; }
inline double getCurrentTimeUnit() { return time_unit_ms_; }
inline std::string getLastSequence() { return last_sequence_; }
@@ -126,7 +132,7 @@ class MorseDecoder {
private:
std::string current_sequence_ = "";
std::string last_sequence_ = "";
double time_unit_ms_ = 160.0;
double time_unit_ms_ = 119.0;
double last_confidence_ = 0.0;
MorseRingBuffer pulse_history_{};
MorseRingBuffer pulse_gaps_{};
@@ -136,7 +142,7 @@ class MorseDecoder {
if (seq == morse_table_[i].code)
return morse_table_[i].letter;
}
return MORSEDEC_ERROR; // not found
return "{" + seq + "}"; // not found
}
double getDahProbability(uint32_t duration_ms) {
@@ -298,8 +304,8 @@ class MorseDecoder {
time_unit_ms_ = (time_unit_ms_ * (1.0 - learning_factor)) + (new_time_unit * learning_factor);
}
size_t morse_table_size_ = 41;
MorseEntry morse_table_[41] = {
size_t morse_table_size_ = 50;
MorseEntry morse_table_[50] = {
{".-", "A"},
{"-...", "B"},
{"-.-.", "C"},
@@ -340,7 +346,16 @@ class MorseDecoder {
{"..--..", "?"},
{"-.-.--", "!"},
{"--..--", ","},
{"-...-", "="}};
{"-...-", "="},
{"-..-.", "/"},
{".--.-.", "@"},
{"---...", ":"},
{"-....-", "-"},
{".----.", "'"},
{".-..-.", "\""},
{"-.--.", "("},
{"-.--.-", ")"},
{".-.-.", "+"}};
};
} // namespace ui::external_app::morse_practice
@@ -98,7 +98,7 @@ void MorsePracticeView::writeCharToConsole(const std::string& ch, double confide
if (ch == " ") {
color_id = 0;
} else if (ch == MORSEDEC_ERROR) {
} else if (ch[0] == '{') { // no match
color_id = 0;
} else {
if (confidence < 0.8)
@@ -47,7 +47,7 @@ class MorsePracticeView : public ui::View {
MorsePracticeView(ui::NavigationView& nav);
~MorsePracticeView();
std::string title() { return "Morse P"; }
std::string title() const override { return "Morse Practice"; }
void focus() override;
void on_show() override;
+86
View File
@@ -0,0 +1,86 @@
/*
* Copyright (C) 2026 Pezsma
*
* 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_morse_radio.hpp"
#include "ui_navigation.hpp"
#include "external_app.hpp"
namespace ui::external_app::morse_radio {
void initialize_app(NavigationView& nav) {
nav.push<MorseRadioView>();
}
} // namespace ui::external_app::morse_radio
extern "C" {
__attribute__((section(".external_app.app_morse_radio.application_information"), used))
application_information_t _application_information_morse_radio = {
/*.memory_location = */ (uint8_t*)0x00000000,
/*.externalAppEntry = */ ui::external_app::morse_radio::initialize_app,
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
/*.app_name = */ "Morse",
/*.bitmap_data = */ {
0x00,
0x00,
0xFE,
0x7F,
0xFF,
0xFF,
0xBB,
0xD0,
0xFF,
0xFF,
0xFF,
0xFF,
0x0B,
0xE1,
0xFF,
0xFF,
0xFF,
0xFF,
0xEB,
0xD0,
0xFF,
0xFF,
0xFE,
0x7F,
0x70,
0x00,
0x30,
0x00,
0x10,
0x00,
0x00,
0x00,
},
/*.icon_color = */ ui::Color::yellow().v,
/*.menu_location = */ app_location_t::RX,
/*.desired_menu_position = */ -1,
/*.m4_app_tag = portapack::spi_flash::image_tag_morse_radio */ {'P', 'M', 'R', 'S'},
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
};
}
@@ -0,0 +1,378 @@
#ifndef __MORSEDECODER_HPP__
#define __MORSEDECODER_HPP__
/*
* Copyright (C) 2025 Pezsma
*
* 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 <cstdint>
#include <string>
#include "string_format.hpp"
namespace ui::external_app::morse_radio {
class MorseRingBuffer {
public:
MorseRingBuffer()
: head_(0), tail_(0), count_(0) {}
void push_back(const uint32_t& value) {
data_[head_] = value;
head_ = (head_ + 1) % 40;
if (count_ < 40) {
count_++;
} else {
// overwrite oldest element
tail_ = (tail_ + 1) % 40;
}
}
void pop_front() {
if (count_ > 0) {
tail_ = (tail_ + 1) % 40;
count_--;
}
}
size_t size() const { return count_; }
bool empty() const { return count_ == 0; }
const uint32_t& front() const { return data_[tail_]; }
// Access by index (0 = oldest)
uint32_t operator[](size_t idx) const {
return data_[(tail_ + idx) % 40];
}
// Convert to vector-like access for sorting etc.
void copy_to_array(uint32_t* out) const {
for (size_t i = 0; i < count_; ++i)
out[i] = (*this)[i];
}
void clear() {
head_ = 0;
tail_ = 0;
count_ = 0;
}
private:
uint32_t data_[40];
size_t head_;
size_t tail_;
size_t count_;
};
class MorseDecoder {
public:
struct DecodeResult {
std::string text = "";
double confidence = 0.0;
bool isValid() const {
return !text.empty();
}
};
struct MorseEntry {
std::string code;
std::string letter;
};
MorseDecoder() {}
void resetLearning() {
time_unit_ms_ = 119.0;
current_sequence_ = "";
last_sequence_ = "";
last_confidence_ = 0.0;
pulse_history_.clear();
pulse_gaps_.clear();
}
DecodeResult
handleInput(int32_t duration_ms) {
DecodeResult result = {"", 0.0};
if (duration_ms < 5 && duration_ms > -5) return result;
if (duration_ms > 0) {
pulse_history_.push_back(duration_ms);
double dah_prob = getDahProbability(duration_ms);
current_sequence_ += (dah_prob > 0.5) ? '-' : '.';
last_confidence_ = (dah_prob > 0.5) ? dah_prob : (1.0 - dah_prob);
last_sequence_ = current_sequence_;
} else {
uint32_t gap_duration = -duration_ms;
pulse_gaps_.push_back(gap_duration);
if (gap_duration >= getInterCharThreshold() && !current_sequence_.empty()) {
result.text = lookupMorse(current_sequence_);
result.confidence = (result.text[0] != '{') ? last_confidence_ : 0.0;
if (gap_duration >= getInterWordThreshold()) {
result.text += " ";
}
current_sequence_ = "";
}
}
updateLearning();
return result;
}
inline double getInterElementThreshold() { return time_unit_ms_ * 0.8; }
inline double getInterCharThreshold() { return time_unit_ms_ * 2.5; }
inline double getInterWordThreshold() { return time_unit_ms_ * 6.0; }
inline double getCurrentTimeUnit() { return time_unit_ms_; }
inline std::string getLastSequence() { return last_sequence_; }
private:
std::string current_sequence_ = "";
std::string last_sequence_ = "";
double time_unit_ms_ = 119.0;
double last_confidence_ = 0.0;
MorseRingBuffer pulse_history_{};
MorseRingBuffer pulse_gaps_{};
std::string lookupMorse(const std::string& seq) {
for (size_t i = 0; i < morse_table_size_; i++) {
if (seq == morse_table_[i].code)
return morse_table_[i].letter;
}
return "{" + seq + "}"; // not found
}
double getDahProbability(uint32_t duration_ms) {
double start_interp = 1.5 * time_unit_ms_;
double end_interp = 2.5 * time_unit_ms_;
if (duration_ms <= start_interp) return 0.0;
if (duration_ms >= end_interp) return 1.0;
return ((double)(duration_ms)-start_interp) / (end_interp - start_interp);
}
size_t findDecisionBoundary(uint32_t* sorted_data, size_t sorted_data_size) {
if (sorted_data_size < 4) return 0;
size_t best_split_index = 0;
uint32_t max_diff = 0;
for (size_t i = 1; i < sorted_data_size; ++i) {
uint32_t diff = sorted_data[i] - sorted_data[i - 1];
if (diff > sorted_data[i - 1] * 0.5 && diff > max_diff) {
max_diff = diff;
best_split_index = i;
}
}
return best_split_index;
}
bool calculatePulseUnit(double& unit, double& confidence) {
if (pulse_history_.size() < 10) return false;
uint32_t sorted_pulses[pulse_history_.size()];
pulse_history_.copy_to_array(sorted_pulses);
sort_uint32(sorted_pulses, pulse_history_.size());
size_t split_index = findDecisionBoundary(sorted_pulses, pulse_history_.size());
if (split_index == 0 || split_index < 3 || (pulse_history_.size() - split_index) < 2) {
return false;
}
double dit_sum = sum_uint32_range(sorted_pulses, 0, split_index);
double dah_sum = sum_uint32_range(sorted_pulses, split_index, pulse_history_.size());
double avg_dit = dit_sum / split_index;
double avg_dah = dah_sum / (pulse_history_.size() - split_index);
if (avg_dah <= avg_dit) return false;
double ratio = avg_dah / avg_dit;
if (ratio > 1.5 && ratio < 5.0) {
unit = avg_dit;
double tmpabs = ratio - 3.0;
if (tmpabs < 0) tmpabs *= -1;
tmpabs /= 3.0;
tmpabs = 1.0 - tmpabs;
if (tmpabs < 0) tmpabs = 0;
confidence = tmpabs; // 0..1
return true;
}
return false;
}
bool calculateGapUnit(double& unit, double& confidence) {
if (pulse_gaps_.size() < 10) return false;
double threshold = getInterElementThreshold();
double valid_gaps[pulse_gaps_.size()];
size_t valid_count = 0;
for (size_t i = 0; i < pulse_gaps_.size(); i++) {
double gap = pulse_gaps_[i];
if (gap <= threshold) {
valid_gaps[valid_count++] = gap;
}
}
if (valid_count < 2) {
return false;
}
double sum = sum_double_range(valid_gaps, 0, valid_count);
size_t count_to_average = valid_count;
if (count_to_average > 0) {
unit = sum / count_to_average;
confidence = 0.8;
return true;
}
return false;
}
double sum_uint32_range(const uint32_t* data, size_t start, size_t end) {
double sum = 0.0;
for (size_t i = start; i < end; i++) {
sum += data[i];
}
return sum;
}
double sum_double_range(const double* data, size_t start, size_t end) {
double sum = 0.0;
for (size_t i = start; i < end; i++) {
sum += data[i];
}
return sum;
}
void sort_uint32(uint32_t* data, size_t size) {
if (size < 2)
return;
for (size_t i = 1; i < size; i++) {
uint32_t key = data[i];
size_t j = i;
while (j > 0 && data[j - 1] > key) {
data[j] = data[j - 1];
j--;
}
data[j] = key;
}
}
double clamp_double(double value, double min_val, double max_val) {
if (value < min_val)
return min_val;
else if (value > max_val)
return max_val;
else
return value;
}
void updateLearning() {
double pulse_unit = -1.0, pulse_confidence = 0.0;
double gap_unit = -1.0, gap_confidence = 0.0;
bool pulse_success = calculatePulseUnit(pulse_unit, pulse_confidence);
bool gap_success = calculateGapUnit(gap_unit, gap_confidence);
double new_time_unit = -1.0;
if (pulse_success && pulse_confidence > 0.5) {
new_time_unit = pulse_unit;
} else if (pulse_success && gap_success) {
gap_confidence = 0.2;
double total_confidence = pulse_confidence + gap_confidence;
new_time_unit = (pulse_unit * pulse_confidence + gap_unit * gap_confidence) / total_confidence;
} else if (gap_success) {
new_time_unit = gap_unit;
} else {
return;
}
double max_change = time_unit_ms_ * 0.25;
new_time_unit = clamp_double(new_time_unit, time_unit_ms_ - max_change, time_unit_ms_ + max_change);
double DEFAULT_TIME_UNIT = 160.0;
double BASE_LEARNING_RATE = 0.05;
double MAX_LEARNING_RATE = 0.25;
double tudeltaabs = new_time_unit - DEFAULT_TIME_UNIT;
if (tudeltaabs < 0) tudeltaabs *= -1;
double deviation_from_default = tudeltaabs / DEFAULT_TIME_UNIT;
double tpp = deviation_from_default * 2.0;
if (tpp > 1) tpp = 1;
double learning_factor = BASE_LEARNING_RATE + (MAX_LEARNING_RATE - BASE_LEARNING_RATE) * tpp;
time_unit_ms_ = (time_unit_ms_ * (1.0 - learning_factor)) + (new_time_unit * learning_factor);
}
size_t morse_table_size_ = 50;
MorseEntry morse_table_[50] = {
{".-", "A"},
{"-...", "B"},
{"-.-.", "C"},
{"-..", "D"},
{".", "E"},
{"..-.", "F"},
{"--.", "G"},
{"....", "H"},
{"..", "I"},
{".---", "J"},
{"-.-", "K"},
{".-..", "L"},
{"--", "M"},
{"-.", "N"},
{"---", "O"},
{".--.", "P"},
{"--.-", "Q"},
{".-.", "R"},
{"...", "S"},
{"-", "T"},
{"..-", "U"},
{"...-", "V"},
{".--", "W"},
{"-..-", "X"},
{"-.--", "Y"},
{"--..", "Z"},
{".----", "1"},
{"..---", "2"},
{"...--", "3"},
{"....-", "4"},
{".....", "5"},
{"-....", "6"},
{"--...", "7"},
{"---..", "8"},
{"----.", "9"},
{"-----", "0"},
{".-.-.-", "."},
{"..--..", "?"},
{"-.-.--", "!"},
{"--..--", ","},
{"-...-", "="},
{"-..-.", "/"},
{".--.-.", "@"},
{"---...", ":"},
{"-....-", "-"},
{".----.", "'"},
{".-..-.", "\""},
{"-.--.", "("},
{"-.--.-", ")"},
{".-.-.", "+"}};
};
} // namespace ui::external_app::morse_radio
#endif // __MORSEDECODER_HPP__
@@ -0,0 +1,266 @@
#include "ui_morse_radio.hpp"
#include "portapack_persistent_memory.hpp"
using namespace portapack;
namespace ui::external_app::morse_radio {
MorseRadioView::MorseRadioView(ui::NavigationView& nav)
: nav_(nav) {
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
add_children({&rssi,
&field_rf_amp,
&field_lna,
&field_vga,
&field_volume,
&field_frequency,
&txt_last,
&txt_speed,
&btn_clear,
&console_text,
&labels,
&chk_log,
&field_squelch});
field_frequency.set_step(100);
btn_clear.on_select = [this](Button&) {
console_text.clear(true);
txt_last.set("");
time_stamp = false;
if (logger && save_log)
logger->init_daily_log(logs_dir);
};
audio::set_rate(audio::Rate::Hz_24000);
audio::output::start();
receiver_model.set_sampling_rate(3072000);
receiver_model.set_baseband_bandwidth(1750000);
receiver_model.set_hidden_offset(0);
baseband::set_moreserx_config();
receiver_model.set_am_configuration(4);
receiver_model.set_modulation(ReceiverModel::Mode::NarrowbandFMAudio);
receiver_model.enable();
auto vol = field_volume.value(); // audio volume fix
field_volume.set_value(0);
field_volume.set_value(vol);
field_squelch.set_value(receiver_model.squelch_level());
field_squelch.on_change = [this](int32_t v) {
receiver_model.set_squelch_level(v);
};
logger = std::make_unique<MorseLogger>();
chk_log.on_select = [this](Checkbox&, bool save) {
save_log = save;
if (logger && save_log)
logger->init_daily_log(logs_dir);
};
}
void MorseLogger::init_daily_log(const std::filesystem::path& log_dir) {
auto now = rtc_time::now();
std::string pattern = "morse_";
uint16_t y = now.year();
pattern += (char)('0' + (y / 1000) % 10);
pattern += (char)('0' + (y / 100) % 10);
pattern += (char)('0' + (y / 10) % 10);
pattern += (char)('0' + (y % 10));
uint8_t m = now.month();
pattern += (char)('0' + (m / 10) % 10);
pattern += (char)('0' + (m % 10));
uint8_t d = now.day();
pattern += (char)('0' + (d / 10) % 10);
pattern += (char)('0' + (d % 10));
pattern += "_???.TXT";
auto full_pattern_path = log_dir / pattern;
auto final_path = next_filename_matching_pattern(full_pattern_path);
if (!final_path.empty()) {
this->append(final_path);
}
}
void MorseLogger::radio_set_log() {
int64_t freq = receiver_model.target_frequency();
std::string header = "Freq:" + to_string_dec_int(freq / 1000000) + "." + to_string_dec_int((freq % 1000000) / 1000, 3);
header += "MHz SQ:" + to_string_dec_uint(receiver_model.squelch_level());
header += " LNA:" + to_string_dec_uint(receiver_model.lna());
header += " VGA:" + to_string_dec_uint(receiver_model.vga());
header += " AMP:" + std::string(receiver_model.rf_amp() ? "ON" : "OFF");
header += "\r\nMessage:\r\n";
log_file.write_raw(header);
}
bool MorseLogger::on_packet(const std::string& content, bool time) {
if (!time) {
log_file.write_raw_no_newline("\r\n\r\n");
auto timestamp = to_string_datetime(rtc_time::now(), YMDHMS);
log_file.write_raw("[" + timestamp + "] ");
radio_set_log();
char_count = 0;
time = true;
}
if (content.empty()) return time;
for (char c : content) {
std::string s(1, c);
if ((char_count >= 35 && c == ' ') || (char_count >= 47)) {
log_file.write_raw_no_newline("\r\n");
if (c != ' ') {
log_file.write_raw_no_newline(s);
char_count = 1;
} else {
char_count = 0;
}
} else {
log_file.write_raw_no_newline(s);
char_count++;
}
}
return time;
}
MorseRadioView::~MorseRadioView() {
receiver_model.disable();
baseband::shutdown();
audio::output::stop();
}
void MorseRadioView::focus() {
field_frequency.focus();
}
void MorseRadioView::check_for_timeout() {
uint64_t now = chTimeNow();
if (last_activity_time == 0) {
last_activity_time = now;
return;
}
uint64_t quiet_duration = now - last_activity_time;
if (quiet_duration >= 60000) {
morse_decoder_.resetLearning();
time_stamp = false;
last_activity_time = now;
}
}
int32_t MorseRadioView::ProcessSignal(int32_t sig_time_us) {
int8_t sign = (sig_time_us > 0) ? 1 : ((sig_time_us < 0) ? -1 : 0);
int32_t result = 0;
if (last_sign_ == 0) {
last_sign_ = sign;
accumulator_us_ = sig_time_us;
long_pause_sent_ = false;
return 0;
}
if (sign == last_sign_) {
accumulator_us_ += sig_time_us;
if (sign < 0) {
int32_t threshold_us = morse_decoder_.getInterWordThreshold() * 1000;
if (!long_pause_sent_ && accumulator_us_ <= -threshold_us) {
result = accumulator_us_ / 1000;
long_pause_sent_ = true;
}
}
} else {
// state change
if (!long_pause_sent_) {
result = accumulator_us_ / 1000;
} else {
result = 0;
}
accumulator_us_ = sig_time_us;
last_sign_ = sign;
long_pause_sent_ = false;
}
return result;
}
void MorseRadioView::on_data(const MorseRXDataMessage* message) {
uint16_t start = 0;
uint16_t stop = 0;
bool has_valid = false;
int32_t r;
for (uint16_t i = 0; i <= message->maxptr; ++i) {
if (message->state_durations[i] >= 30000 || message->state_durations[i] <= -30000) {
if (!has_valid) {
start = i;
}
} else {
has_valid = true;
stop = i;
}
}
if (!has_valid) return; // no valid data arrived
for (uint16_t i = start; i <= stop; i++) {
r = ProcessSignal(message->state_durations[i]);
auto result = morse_decoder_.handleInput((r != 0) ? r : 0);
if (result.isValid()) {
last_activity_time = chTimeNow(); // start reset timer on valid input
writeCharToConsole(result.text, result.confidence);
if (logger && save_log) {
time_stamp = logger->on_packet(result.text, time_stamp);
}
float dah_time = morse_decoder_.getCurrentTimeUnit() * 3.0f;
if (dah_time > 0) {
uint16_t wpm = (uint16_t)(3600.0f / (dah_time + 18.0f) + 0.5f);
txt_speed.set(to_string_dec_uint(wpm));
}
}
}
}
void MorseRadioView::writeCharToConsole(const std::string& ch, double confidence) {
if (ch.empty()) {
return;
}
txt_last.set(morse_decoder_.getLastSequence().c_str());
last_color_id = color_id;
std::string color = "";
if (ch == " ") {
color_id = 0;
} else if (ch[0] == '{') { // no match
color_id = 0;
} else {
if (confidence < 0.8)
color_id = 1;
else if (confidence < 0.9)
color_id = 2;
else
color_id = 3;
}
color = arr_color[color_id];
last_color_id = color_id;
console_text.write(color + ch);
}
} // namespace ui::external_app::morse_radio
@@ -0,0 +1,150 @@
/*
* Copyright (C) 2026 Pezsma
*
* 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 __MORSE_RADIO_H__
#define __MORSE_RADIO_H__
#include "ui.hpp"
#include "ui_widget.hpp"
#include "ui_navigation.hpp"
#include "string_format.hpp"
#include "external_app.hpp"
#include "ui_freq_field.hpp"
#include "radio_state.hpp"
#include "portapack.hpp"
#include "message.hpp"
#include "audio.hpp"
#include "baseband_api.hpp"
#include "receiver_model.hpp"
#include "tone_key.hpp"
#include "log_file.hpp"
#include "file_path.hpp"
#include "file.hpp"
#include "rtc_time.hpp"
#include "morsedecoder.hpp"
#include <cstdint>
namespace ui::external_app::morse_radio {
class MorseRadioView;
class MorseLogger {
public:
Optional<File::Error> append(const std::filesystem::path& filename) {
return log_file.append(filename);
}
void init_daily_log(const std::filesystem::path& log_dir);
bool on_packet(const std::string& content, bool time);
void radio_set_log();
private:
LogFile log_file{};
uint8_t char_count{0}; // log file line break
};
class MorseRadioView : public ui::View {
public:
MorseRadioView(ui::NavigationView& nav);
~MorseRadioView();
std::string title() const override {
return "Morse";
}
void focus() override;
private:
void writeCharToConsole(const std::string& ch, double confidence);
void on_data(const MorseRXDataMessage* message);
void on_message(const Message* const p);
void check_for_timeout();
int32_t ProcessSignal(int32_t sig_time_us);
ui::NavigationView& nav_;
MorseDecoder morse_decoder_{};
RxRadioState radio_state_{};
std::unique_ptr<MorseLogger> logger{};
app_settings::SettingsManager settings_{
"trx_morese_radio", app_settings::Mode::RX_TX};
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)}};
NumberField field_squelch{
{UI_POS_X(10), UI_POS_Y(1)},
2,
{0, 99},
1,
' ',
};
ui::Text txt_speed{{UI_POS_X(23), UI_POS_Y(1), UI_POS_WIDTH(3), UI_POS_HEIGHT(1)}, "??"};
ui::Text txt_last{{UI_POS_X(12), UI_POS_Y(4), UI_POS_WIDTH_REMAINING(12), UI_POS_HEIGHT(1)}, ""};
ui::Console console_text{{UI_POS_X(0), UI_POS_Y(5), UI_POS_MAXWIDTH, UI_POS_HEIGHT_REMAINING(7)}};
ui::Labels labels{
{{UI_POS_X(0), UI_POS_Y(1)}, "Squelch:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(15), UI_POS_Y(1)}, "Speed:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(26), UI_POS_Y(1)}, "wpm", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(4)}, "Last seq.:", Theme::getInstance()->fg_light->foreground},
};
Checkbox chk_log{{UI_POS_X(0), UI_POS_Y(2)}, 12, "Log", false};
ui::Button btn_clear{{UI_POS_X(0), UI_POS_Y_BOTTOM(2), UI_POS_WIDTH(6), UI_POS_HEIGHT(1)}, "CLR"};
uint8_t last_color_id{255};
uint8_t color_id{255};
std::string arr_color[4] = {STR_COLOR_WHITE, STR_COLOR_RED, STR_COLOR_YELLOW, STR_COLOR_GREEN};
int32_t accumulator_us_{0};
int8_t last_sign_{0};
uint8_t space_timer{0};
bool long_pause_sent_{false};
bool save_log{false};
bool reset_triggered_{false};
bool time_stamp{false};
uint64_t last_activity_time{0};
MessageHandlerRegistration message_handler_packet{
Message::ID::MorseRXData,
[this](Message* const p) {
const auto message = static_cast<const MorseRXDataMessage*>(p);
this->on_data(message);
}};
MessageHandlerRegistration message_handler_framesync{
Message::ID::DisplayFrameSync,
[this](const Message* const p) {
(void)p;
this->check_for_timeout();
}};
};
} // namespace ui::external_app::morse_radio
#endif // __MORSE_RADIO_H__
+83
View File
@@ -0,0 +1,83 @@
/*
* Copyright (C) 2024 Bernd Herzog
*
* 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_sd_over_usb.hpp"
#include "ui_navigation.hpp"
#include "external_app.hpp"
namespace ui::external_app::sdusb {
void initialize_app(ui::NavigationView& nav) {
nav.push<SdOverUsbView>();
}
} // namespace ui::external_app::sdusb
extern "C" {
__attribute__((section(".external_app.app_sdusb.application_information"), used)) application_information_t _application_information_sdusb = {
/*.memory_location = */ (uint8_t*)0x00000000,
/*.externalAppEntry = */ ui::external_app::sdusb::initialize_app,
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
/*.app_name = */ "SD Over USB",
/*.bitmap_data = */ {
0xF0,
0x0F,
0x10,
0x08,
0x50,
0x0A,
0x10,
0x08,
0x10,
0x08,
0x10,
0x08,
0xF8,
0x1F,
0xF8,
0x1F,
0xF8,
0x1F,
0xF8,
0x1F,
0xF8,
0x1F,
0xF8,
0x1F,
0xF8,
0x1F,
0xF0,
0x0F,
0x80,
0x01,
0x80,
0x01,
},
/*.icon_color = */ ui::Color::yellow().v,
/*.menu_location = */ app_location_t::UTILITIES,
/*.desired_menu_position = */ -1,
/*.m4_app_tag = portapack::spi_flash::image_tag_usb_sd */ {'P', 'U', 'S', 'B'},
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
};
}
@@ -23,7 +23,7 @@
#include "ui_sd_over_usb.hpp"
#include "portapack_shared_memory.hpp"
namespace ui {
namespace ui::external_app::sdusb {
SdOverUsbView::SdOverUsbView(NavigationView& nav)
: nav_(nav) {
@@ -46,7 +46,7 @@ SdOverUsbView::SdOverUsbView(NavigationView& nav)
sdcStop(&SDCD1);
portapack::shutdown(true);
m4_init(portapack::spi_flash::image_tag_usb_sd, portapack::memory::map::m4_code, false);
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
m0_halt();
/* will not return*/
};
@@ -56,4 +56,4 @@ void SdOverUsbView::focus() {
button_run.focus();
}
} /* namespace ui */
} // namespace ui::external_app::sdusb
@@ -32,7 +32,7 @@
#include <cstdint>
namespace ui {
namespace ui::external_app::sdusb {
class SdOverUsbView : public View {
public:
@@ -58,6 +58,6 @@ class SdOverUsbView : public View {
"Run"};
};
} /* namespace ui */
} // namespace ui::external_app::sdusb
#endif /*__UI_SD_OVER_USB_H__*/
+82
View File
@@ -0,0 +1,82 @@
/*
* Copyright (C) 2023 Bernd Herzog
*
* 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_siggen.hpp"
#include "ui_navigation.hpp"
#include "external_app.hpp"
namespace ui::external_app::siggen {
void initialize_app(ui::NavigationView& nav) {
nav.push<SigGenView>();
}
} // namespace ui::external_app::siggen
extern "C" {
__attribute__((section(".external_app.app_siggen.application_information"), used)) application_information_t _application_information_siggen = {
/*.memory_location = */ (uint8_t*)0x00000000,
/*.externalAppEntry = */ ui::external_app::siggen::initialize_app,
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
/*.app_name = */ "Signal gen",
/*.bitmap_data = */ {
0x18,
0x00,
0x24,
0x00,
0x42,
0x00,
0x42,
0x00,
0x42,
0x00,
0x42,
0x00,
0x81,
0x00,
0xAB,
0x6A,
0x80,
0x40,
0x00,
0x21,
0x00,
0x21,
0x00,
0x21,
0x00,
0x21,
0x00,
0x12,
0x00,
0x0C,
0x00,
0x00,
},
/*.icon_color = */ ui::Color::green().v,
/*.menu_location = */ app_location_t::TX,
/*.desired_menu_position = */ -1,
/*.m4_app_tag = portapack::spi_flash::image_tag_siggen */ {'P', 'S', 'I', 'G'},
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
};
}
@@ -31,7 +31,7 @@
using namespace portapack;
namespace ui {
namespace ui::external_app::siggen {
void SigGenView::focus() {
options_shape.focus();
@@ -75,7 +75,7 @@ void SigGenView::on_tx_progress(const uint32_t progress, const bool done) {
SigGenView::SigGenView(
NavigationView& nav) {
baseband::run_image(portapack::spi_flash::image_tag_siggen);
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
add_children({&labels,
&options_mod,
@@ -172,4 +172,4 @@ SigGenView::SigGenView(
};
}
} /* namespace ui */
} // namespace ui::external_app::siggen
@@ -33,7 +33,7 @@
#include "portapack.hpp"
#include "message.hpp"
namespace ui {
namespace ui::external_app::siggen {
class SigGenView : public View {
public:
@@ -139,6 +139,6 @@ class SigGenView : public View {
}};
};
} /* namespace ui */
} // namespace ui::external_app::siggen
#endif /*__SIGGEN_H__*/
+73 -9
View File
@@ -266,10 +266,11 @@ void SubCarRecentEntryDetailView::parseProtocol() {
if (entry_.sensorType == FPC_Invalid) return;
if (entry_.sensorType == FPC_SUZUKI) {
uint32_t serial_button = (((entry_.data >> 32) & 0xFFF) << 20) | (entry_.data >> 12);
serial = serial_button >> 4;
uint8_t buttonid = serial_button & 0xF;
cnt = (entry_.data >> 44) & 0xFFFF;
uint32_t data_high = (uint32_t)(entry_.data >> 32);
uint32_t data_low = (uint32_t)entry_.data;
serial = ((data_high & 0xFFF) << 16) | (data_low >> 16);
uint8_t buttonid = (data_low >> 12) & 0xF;
cnt = (data_high << 4) >> 16;
btn = to_string_dec_uint(buttonid);
return;
}
@@ -320,15 +321,78 @@ void SubCarRecentEntryDetailView::parseProtocol() {
}
if (entry_.sensorType == FPC_KIAV5) {
serial = (uint32_t)(((entry_.data >> 32) & 0x0FFFFFFF) >> 1);
uint8_t button = (entry_.data >> 61) & 0x07;
uint64_t yek = 0;
for (int i = 0; i < 8; i++) {
uint8_t byte = (entry_.data2 >> (i * 8)) & 0xFF;
uint8_t reversed = 0;
for (int b = 0; b < 8; b++) {
if (byte & (1 << b))
reversed |= (1 << (7 - b));
}
yek |= ((uint64_t)reversed << ((7 - i) * 8));
}
serial = (uint32_t)((yek >> 32) & 0x0FFFFFFF);
uint8_t button = (uint8_t)((yek >> 60) & 0x0F);
uint32_t encrypted = (uint32_t)(yek & 0xFFFFFFFF);
btn = to_string_dec_uint(button);
cnt = (uint16_t)(entry_.data & 0xFFFF);
// decode
uint8_t keystore_bytes[] = {0x53, 0x54, 0x46, 0x52, 0x4b, 0x45, 0x30, 0x30};
uint8_t s0 = (encrypted & 0xFF);
uint8_t s1 = (encrypted >> 8) & 0xFF;
uint8_t s2 = (encrypted >> 16) & 0xFF;
uint8_t s3 = (encrypted >> 24) & 0xFF;
int round_index = 1;
for (size_t i = 0; i < 18; i++) {
uint8_t r = keystore_bytes[round_index] & 0xFF;
int steps = 8;
while (steps > 0) {
uint8_t base;
if ((s3 & 0x40) == 0) {
base = (s3 & 0x02) == 0 ? 0x74 : 0x2E;
} else {
base = (s3 & 0x02) == 0 ? 0x3A : 0x5C;
}
if (s2 & 0x08) {
base = (((base >> 4) & 0x0F) | ((base & 0x0F) << 4)) & 0xFF;
}
if (s1 & 0x01) {
base = ((base & 0x3F) << 2) & 0xFF;
}
if (s0 & 0x01) {
base = (base << 1) & 0xFF;
}
uint8_t temp = (s3 ^ s1) & 0xFF;
s3 = ((s3 & 0x7F) << 1) & 0xFF;
if (s2 & 0x80) {
s3 |= 0x01;
}
s2 = ((s2 & 0x7F) << 1) & 0xFF;
if (s1 & 0x80) {
s2 |= 0x01;
}
s1 = ((s1 & 0x7F) << 1) & 0xFF;
if (s0 & 0x80) {
s1 |= 0x01;
}
s0 = ((s0 & 0x7F) << 1) & 0xFF;
uint8_t chk = (base ^ (r ^ temp)) & 0xFF;
if (chk & 0x80) {
s0 |= 0x01;
}
r = ((r & 0x7F) << 1) & 0xFF;
steps--;
}
round_index = (round_index - 1) & 0x7;
}
cnt = (s0 + (s1 << 8)) & 0xFFFF;
}
if (entry_.sensorType == FPC_KIAV3V4) {
// not decrypted!
serial = SD_NO_SERIAL; //(uint32_t)entry_.data;
serial = (uint32_t)entry_.data;
// uint8_t button = entry_.data2 & 0xFF;
btn = "?"; // to_string_dec_uint(button);
}
@@ -344,7 +408,7 @@ void SubCarRecentEntryDetailView::parseProtocol() {
if (entry_.sensorType == FPC_KIAV1) {
serial = (uint32_t)((entry_.data >> 24) & 0xFFFFFFFF);
uint8_t button = (uint8_t)((entry_.data >> 16) & 0xFF);
cnt = (uint8_t)((entry_.data >> 8) & 0xFF);
cnt = (uint8_t)((entry_.data >> 4) & 0xF) << 8 | ((entry_.data >> 8) & 0xFF);
btn = to_string_dec_uint(button);
}
+2 -2
View File
@@ -227,8 +227,8 @@ void controls_init() {
gptStart(&GPTD1, &timer0_config);
gptStartContinuous(&GPTD1, timer0_match_count);
// Enable repeat for directional switches only
for (auto i = Switch::Right; i <= Switch::Up; incr(i))
// Enable repeat for directional and Select switches only
for (auto i = Switch::Right; i <= Switch::Sel; incr(i))
switch_debounce[toUType(i)].enable_repeat();
}
+8
View File
@@ -38,3 +38,11 @@ Optional<File::Error> LogFile::write_raw(const std::string& message) {
}
return error;
}
Optional<File::Error> LogFile::write_raw_no_newline(const std::string& message) {
auto result_s = file.write(message.c_str(), message.size());
if (!result_s.is_error()) {
file.sync();
}
return {result_s.error()};
}
+1
View File
@@ -40,6 +40,7 @@ class LogFile {
Optional<File::Error> write_entry(const std::string& entry);
Optional<File::Error> write_entry(const rtc::RTC& datetime, const std::string& entry);
Optional<File::Error> write_raw(const std::string& message);
Optional<File::Error> write_raw_no_newline(const std::string& message);
private:
File file{};
-7
View File
@@ -206,13 +206,6 @@ bool set_tuning_frequency(const rf::Frequency frequency) {
void set_rf_amp(const bool rf_amp) {
rf_path.set_rf_amp(rf_amp);
if (direction == rf::Direction::Transmit) {
if (rf_amp)
led_tx.on();
else
led_tx.off();
}
}
void set_lna_gain(const int_fast8_t db) {
+18 -10
View File
@@ -252,8 +252,6 @@ void ReceiverModel::enable() {
// TODO: maybe not the perfect place for this, but it's reasonable.
update_headphone_volume();
led_rx.on();
}
void ReceiverModel::disable() {
@@ -262,7 +260,6 @@ void ReceiverModel::disable() {
// TODO: Responsibility for enabling/disabling the radio is muddy.
// Some happens in ReceiverModel, some inside radio namespace.
radio::disable();
led_rx.off();
}
void ReceiverModel::initialize() {
@@ -306,7 +303,8 @@ int32_t ReceiverModel::tuning_offset() {
void ReceiverModel::update_tuning_frequency() {
// TODO: use positive offset if freq < offset.
radio::set_tuning_frequency(target_frequency() + hidden_offset + tuning_offset());
if (enabled_)
radio::set_tuning_frequency(target_frequency() + hidden_offset + tuning_offset());
}
void ReceiverModel::set_hidden_offset(rf::Frequency offset) {
@@ -315,7 +313,8 @@ void ReceiverModel::set_hidden_offset(rf::Frequency offset) {
}
void ReceiverModel::update_baseband_bandwidth() {
radio::set_baseband_filter_bandwidth_rx(baseband_bandwidth());
if (enabled_)
radio::set_baseband_filter_bandwidth_rx(baseband_bandwidth());
}
void ReceiverModel::update_sampling_rate() {
@@ -324,23 +323,31 @@ void ReceiverModel::update_sampling_rate() {
// protocols that need quick RX/TX turn-around.
// Disabling baseband while changing sampling rates seems like a good idea...
radio::set_baseband_rate(sampling_rate());
if (enabled_)
radio::set_baseband_rate(sampling_rate());
update_tuning_frequency();
}
void ReceiverModel::update_lna() {
radio::set_lna_gain(lna());
if (enabled_)
radio::set_lna_gain(lna());
}
void ReceiverModel::update_vga() {
radio::set_vga_gain(vga());
if (enabled_)
radio::set_vga_gain(vga());
}
void ReceiverModel::update_rf_amp() {
radio::set_rf_amp(rf_amp());
if (enabled_)
radio::set_rf_amp(rf_amp());
}
void ReceiverModel::update_modulation() {
if (!enabled_)
return;
switch (modulation()) {
default:
case Mode::AMAudio:
@@ -395,5 +402,6 @@ void ReceiverModel::update_antenna_bias() {
}
void ReceiverModel::update_headphone_volume() {
audio::headphone::set_volume(headphone_volume());
if (enabled_)
audio::headphone::set_volume(headphone_volume());
}
+23 -9
View File
@@ -31,6 +31,8 @@
#include "portapack_persistent_memory.hpp"
#include "radio.hpp"
#include <algorithm>
using namespace hackrf::one;
using namespace portapack;
@@ -70,7 +72,7 @@ void TransmitterModel::set_channel_bandwidth(uint32_t v) {
}
uint8_t TransmitterModel::tx_gain() const {
return settings_.tx_gain_db;
return std::min(settings_.tx_gain_db, portapack::persistent_memory::config_tx_gain_max_db());
}
void TransmitterModel::set_tx_gain(uint8_t v_db) {
@@ -79,7 +81,7 @@ void TransmitterModel::set_tx_gain(uint8_t v_db) {
}
bool TransmitterModel::rf_amp() const {
return settings_.rf_amp;
return settings_.rf_amp && !portapack::persistent_memory::config_tx_amp_disabled();
}
void TransmitterModel::set_rf_amp(bool enabled) {
@@ -92,6 +94,15 @@ void TransmitterModel::set_antenna_bias() {
}
void TransmitterModel::enable() {
if (portapack::persistent_memory::config_tx_disabled()) {
radio::disable();
TXDisabledMessage message;
EventDispatcher::send_message(message);
return;
}
enabled_ = true;
radio::set_direction(rf::Direction::Transmit);
update_tuning_frequency();
@@ -101,7 +112,6 @@ void TransmitterModel::enable() {
update_sampling_rate();
update_tx_gain();
led_tx.on();
signal_token_tick_second = rtc_time::signal_tick_second += [this]() {
this->on_tick_second();
};
@@ -118,7 +128,6 @@ void TransmitterModel::disable() {
radio::disable();
rtc_time::signal_tick_second -= signal_token_tick_second;
led_tx.off();
}
void TransmitterModel::initialize() {
@@ -135,11 +144,13 @@ void TransmitterModel::configure_from_app_settings(
}
void TransmitterModel::update_tuning_frequency() {
radio::set_tuning_frequency(target_frequency());
if (enabled_)
radio::set_tuning_frequency(target_frequency());
}
void TransmitterModel::update_baseband_bandwidth() {
radio::set_baseband_filter_bandwidth_tx(baseband_bandwidth());
if (enabled_)
radio::set_baseband_filter_bandwidth_tx(baseband_bandwidth());
}
void TransmitterModel::update_sampling_rate() {
@@ -148,17 +159,20 @@ void TransmitterModel::update_sampling_rate() {
// protocols that need quick RX/TX turn-around.
// Disabling baseband while changing sampling rates seems like a good idea...
if (enabled_)
radio::set_baseband_rate(sampling_rate());
radio::set_baseband_rate(sampling_rate());
update_tuning_frequency();
}
void TransmitterModel::update_tx_gain() {
radio::set_tx_gain(tx_gain());
if (enabled_)
radio::set_tx_gain(tx_gain());
}
void TransmitterModel::update_rf_amp() {
radio::set_rf_amp(rf_amp());
if (enabled_)
radio::set_rf_amp(rf_amp());
}
void TransmitterModel::update_antenna_bias() {
+14 -8
View File
@@ -49,10 +49,9 @@
#include "ui_rds.hpp"
#include "ui_recon.hpp"
// #include "ui_scanner.hpp"
#include "ui_sd_over_usb.hpp"
// #include "ui_sd_over_usb.hpp"
#include "ui_search.hpp"
#include "ui_settings.hpp"
#include "ui_siggen.hpp"
#include "ui_sonde.hpp"
#include "ui_ss_viewer.hpp"
// #include "ui_test.hpp"
@@ -114,7 +113,7 @@ const NavigationView::AppList NavigationView::appList = {
/* HOME ******************************************************************/
{nullptr, "Receive", HOME, Color::cyan(), &bitmap_icon_receivers, new ViewFactory<ReceiversMenuView>()},
{nullptr, "Transmit", HOME, Color::cyan(), &bitmap_icon_transmit, new ViewFactory<TransmittersMenuView>()},
{nullptr, "Tranceiver", HOME, Color::cyan(), &bitmap_icon_tranceivers, new ViewFactory<TranceiversMenuView>()},
{nullptr, "Transceiver", HOME, Color::cyan(), &bitmap_icon_transceivers, new ViewFactory<TransceiversMenuView>()},
{"recon", "Recon", HOME, Color::green(), &bitmap_icon_scanner, new ViewFactory<ReconView>()},
{"capture", "Capture", HOME, Color::red(), &bitmap_icon_capture, new ViewFactory<CaptureAppView>()},
{"replay", "Replay", HOME, Color::green(), &bitmap_icon_replay, new ViewFactory<PlaylistView>()},
@@ -140,7 +139,6 @@ const NavigationView::AppList NavigationView::appList = {
{"pocsagtx", "POCSAG TX", TX, ui::Color::green(), &bitmap_icon_pocsag, new ViewFactory<POCSAGTXView>()},
{"rdstx", "RDS", TX, ui::Color::green(), &bitmap_icon_rds, new ViewFactory<RDSView>()},
{"touchtune", "TouchTune", TX, ui::Color::green(), &bitmap_icon_touchtunes, new ViewFactory<TouchTunesView>()},
{"signalgen", "SignalGen", TX, Color::green(), &bitmap_icon_cwgen, new ViewFactory<SigGenView>()},
/* TRX ********************************************************************/
{"microphone", "Mic", TRX, Color::green(), &bitmap_icon_microphone, new ViewFactory<MicTXView>()},
/* UTILITIES *************************************************************/
@@ -148,7 +146,7 @@ const NavigationView::AppList NavigationView::appList = {
{"freqman", "Freq. Manager", UTILITIES, Color::green(), &bitmap_icon_freqman, new ViewFactory<FrequencyManagerView>()},
{"iqtrim", "IQ Trim", UTILITIES, Color::orange(), &bitmap_icon_trim, new ViewFactory<IQTrimView>()},
{"notepad", "Notepad", UTILITIES, Color::dark_cyan(), &bitmap_icon_notepad, new ViewFactory<TextEditorView>()},
{nullptr, "SD Over USB", UTILITIES, Color::yellow(), &bitmap_icon_hackrf, new ViewFactory<SdOverUsbView>()},
//{nullptr, "SD Over USB", UTILITIES, Color::yellow(), &bitmap_icon_hackrf, new ViewFactory<SdOverUsbView>()},
{nullptr, "Debug", UTILITIES, Color::light_grey(), &bitmap_icon_debug, new ViewFactory<DebugMenuView>()},
//{"testapp", "Test App", UTILITIES, Color::dark_grey(), nullptr, new ViewFactory<TestView>()},
// Dangerous apps.
@@ -320,6 +318,14 @@ SystemStatusView::SystemStatusView(
refresh();
}
void SystemStatusView::on_tx_disabled() {
if (!nav_.is_valid())
return;
nav_.pop();
nav_.display_modal("Error", "RF transmit disabled.\nApplication closed.");
}
// when battery icon / text is clicked
void SystemStatusView::on_battery_details() {
if (!nav_.is_valid()) return;
@@ -837,12 +843,12 @@ void TransmittersMenuView::on_populate() {
add_external_items(nav_, app_location_t::TX, *this, return_icon ? 1 : 0);
}
/* TranceiversMenuView **************************************************/
/* TransceiversMenuView **************************************************/
TranceiversMenuView::TranceiversMenuView(NavigationView& nav)
TransceiversMenuView::TransceiversMenuView(NavigationView& nav)
: nav_(nav) {}
void TranceiversMenuView::on_populate() {
void TransceiversMenuView::on_populate() {
bool return_icon = pmem::show_gui_return_icon();
if (return_icon) {
add_items({{"..", Theme::getInstance()->fg_light->foreground, &bitmap_icon_previous, [this]() { nav_.pop(); }}});
+11 -3
View File
@@ -303,6 +303,7 @@ class SystemStatusView : public View {
void on_title();
void refresh();
void on_clk();
void on_tx_disabled();
void rtc_battery_workaround();
void on_battery_data(const BatteryStateMessage* msg);
void on_battery_details();
@@ -314,6 +315,13 @@ class SystemStatusView : public View {
this->refresh();
}};
MessageHandlerRegistration message_handler_tx_disabled{
Message::ID::TXDisabled,
[this](const Message* const p) {
(void)p;
this->on_tx_disabled();
}};
MessageHandlerRegistration message_handler_battery{
Message::ID::BatteryStateData,
[this](const Message* const p) {
@@ -381,10 +389,10 @@ class TransmittersMenuView : public BtnGridView {
void on_populate() override;
};
class TranceiversMenuView : public BtnGridView {
class TransceiversMenuView : public BtnGridView {
public:
TranceiversMenuView(NavigationView& nav);
std::string title() const override { return "Tranceiver"; };
TransceiversMenuView(NavigationView& nav);
std::string title() const override { return "Transceiver"; };
private:
NavigationView& nav_;
+5 -5
View File
@@ -71,9 +71,9 @@ void nvic_enable_irq(uint8_t irqn) {
void usb_configuration_changed(usb_device_t* const device) {
(void)device;
usb_endpoint_init(&usb_endpoint_int_in);
usb_endpoint_init(&usb_endpoint_bulk_in);
usb_endpoint_init(&usb_endpoint_bulk_out);
usb_endpoint_init(&usb_endpoint_int_in, false);
usb_endpoint_init(&usb_endpoint_bulk_in, false);
usb_endpoint_init(&usb_endpoint_bulk_out, false);
}
void setup_usb_serial_controller(void) {
@@ -88,8 +88,8 @@ void setup_usb_serial_controller(void) {
usb_queue_init(&usb_endpoint_bulk_out_queue);
usb_queue_init(&usb_endpoint_bulk_in_queue);
usb_endpoint_init(&usb_endpoint_control_out);
usb_endpoint_init(&usb_endpoint_control_in);
usb_endpoint_init(&usb_endpoint_control_out, false);
usb_endpoint_init(&usb_endpoint_control_in, false);
usb_run(&usb_device);
}
+62 -55
View File
@@ -447,14 +447,6 @@ set(MODE_CPPSRC
)
DeclareTargets(PREP replay)
### Signal generator
set(MODE_CPPSRC
proc_siggen.cpp
)
DeclareTargets(PSIG siggen)
### Tones
@@ -505,53 +497,6 @@ set(MODE_CPPSRC
)
DeclareTargets(PFUT flash_utility)
### SD over USB
set(MODE_INCDIR
${HACKRF_PATH}/firmware
${HACKRF_PATH}/firmware/common
${HACKRF_PATH}/firmware/libopencm3/include
)
set(MODE_CPPSRC
sd_over_usb/proc_sd_over_usb.cpp
sd_over_usb/scsi.c
sd_over_usb/diskio.c
sd_over_usb/sd_over_usb.c
sd_over_usb/usb_descriptor.c
sd_over_usb/hackrf_core.c
${HACKRF_PATH}/firmware/common/usb.c
${HACKRF_PATH}/firmware/common/usb_queue.c
${HACKRF_PATH}/firmware/hackrf_usb/usb_device.c
${HACKRF_PATH}/firmware/hackrf_usb/usb_endpoint.c
${HACKRF_PATH}/firmware/common/usb_request.c
${HACKRF_PATH}/firmware/common/usb_standard_request.c
${HACKRF_PATH}/firmware/common/platform_detect.c
${HACKRF_PATH}/firmware/common/gpio_lpc.c
${HACKRF_PATH}/firmware/common/firmware_info.c
${HACKRF_PATH}/firmware/common/si5351c.c
${HACKRF_PATH}/firmware/common/i2c_bus.c
${HACKRF_PATH}/firmware/common/mixer.c
${HACKRF_PATH}/firmware/common/clkin.c
${HACKRF_PATH}/firmware/common/spi_bus.c
${HACKRF_PATH}/firmware/common/sgpio.c
${HACKRF_PATH}/firmware/common/rf_path.c
${HACKRF_PATH}/firmware/common/i2c_lpc.c
${HACKRF_PATH}/firmware/common/spi_ssp.c
${HACKRF_PATH}/firmware/common/rffc5071_spi.c
${HACKRF_PATH}/firmware/common/rffc5071.c
${HACKRF_PATH}/firmware/common/clkin.c
${HACKRF_PATH}/firmware/common/gpdma.c
${HACKRF_PATH}/firmware/libopencm3/lib/cm3/nvic.c
${HACKRF_PATH}/firmware/libopencm3/lib/cm3/sync.c
${HACKRF_PATH}/firmware/libopencm3/lib/lpc43xx/scu.c
${HACKRF_PATH}/firmware/libopencm3/lib/lpc43xx/timer.c
${HACKRF_PATH}/firmware/libopencm3/lib/lpc43xx/i2c.c
)
DeclareTargets(PUSB sd_over_usb)
### Place external app and disabled images below so they don't get added to the firmware
set(add_to_firmware FALSE)
set(MODE_FLAGS "-O3")
@@ -681,6 +626,13 @@ set(MODE_CPPSRC
DeclareTargets(POSK ookstream)
### Signal generator
set(MODE_CPPSRC
proc_siggen.cpp
)
DeclareTargets(PSIG siggen)
### WeFax Rx
@@ -711,6 +663,61 @@ set(MODE_CPPSRC
)
DeclareTargets(PSCD subcar)
set(MODE_CPPSRC
proc_morse.cpp
)
DeclareTargets(PMRS morse)
### SD over USB
set(MODE_INCDIR
${HACKRF_PATH}/firmware
${HACKRF_PATH}/firmware/common
${HACKRF_PATH}/firmware/libopencm3/include
)
set(MODE_CPPSRC
sd_over_usb/proc_sd_over_usb.cpp
sd_over_usb/scsi.c
sd_over_usb/diskio.c
sd_over_usb/sd_over_usb.c
sd_over_usb/usb_descriptor.c
sd_over_usb/hackrf_core.c
${HACKRF_PATH}/firmware/common/adc.c
${HACKRF_PATH}/firmware/common/selftest.c
${HACKRF_PATH}/firmware/common/usb.c
${HACKRF_PATH}/firmware/common/usb_queue.c
${HACKRF_PATH}/firmware/hackrf_usb/usb_device.c
${HACKRF_PATH}/firmware/hackrf_usb/usb_endpoint.c
${HACKRF_PATH}/firmware/common/usb_request.c
${HACKRF_PATH}/firmware/common/usb_standard_request.c
${HACKRF_PATH}/firmware/common/platform_detect.c
${HACKRF_PATH}/firmware/common/gpio_lpc.c
${HACKRF_PATH}/firmware/common/firmware_info.c
${HACKRF_PATH}/firmware/common/si5351c.c
${HACKRF_PATH}/firmware/common/i2c_bus.c
${HACKRF_PATH}/firmware/common/mixer.c
${HACKRF_PATH}/firmware/common/clkin.c
${HACKRF_PATH}/firmware/common/spi_bus.c
${HACKRF_PATH}/firmware/common/sgpio.c
${HACKRF_PATH}/firmware/common/rf_path.c
${HACKRF_PATH}/firmware/common/i2c_lpc.c
${HACKRF_PATH}/firmware/common/spi_ssp.c
${HACKRF_PATH}/firmware/common/rffc5071_spi.c
${HACKRF_PATH}/firmware/common/rffc5071.c
${HACKRF_PATH}/firmware/common/clkin.c
${HACKRF_PATH}/firmware/common/gpdma.c
${HACKRF_PATH}/firmware/libopencm3/lib/cm3/nvic.c
${HACKRF_PATH}/firmware/libopencm3/lib/cm3/sync.c
${HACKRF_PATH}/firmware/libopencm3/lib/lpc43xx/scu.c
${HACKRF_PATH}/firmware/libopencm3/lib/lpc43xx/timer.c
${HACKRF_PATH}/firmware/libopencm3/lib/lpc43xx/i2c.c
)
DeclareTargets(PUSB sd_over_usb)
### HackRF "factory" firmware
+2 -6
View File
@@ -161,8 +161,6 @@ class FProtoSubCarFiatV0 : public FProtoSubCarBase {
}
if (bit_count > 0x46) {
final_count = bit_count;
endbyte = (uint8_t)data_low;
/*
generic.data = ((uint64_t)hop << 32) | fix;
@@ -192,13 +190,11 @@ class FProtoSubCarFiatV0 : public FProtoSubCarBase {
ManchesterState manchester_state = ManchesterStateMid1;
uint8_t decoder_state = 0;
uint8_t bit_count = 0;
uint8_t endbyte = 0;
uint16_t preamble_count = 0;
uint32_t data_low = 0;
uint32_t data_high = 0;
uint8_t bit_count = 0;
uint32_t hop = 0;
uint32_t fix = 0;
uint8_t endbyte = 0;
uint8_t final_count = 0;
uint32_t te_last = 0;
};
+16 -17
View File
@@ -59,20 +59,26 @@ class FProtoSubCarKiaV0 : public FProtoSubCarBase {
break;
case KIADecoderStepSaveDuration:
if (level) {
if (duration >=
(te_long + te_delta * 2UL)) {
// Signal ended too early!
// FURI_LOG_W(TAG, "Signal ended at %u bits (expected 61). Duration: %lu", decode_count_bit, duration);
if (duration >= (te_long + te_delta * 2UL)) {
// End of transmission detected
parser_step = KIADecoderStepReset;
if (decode_count_bit == min_count_bit_for_found) {
// instance->generic.data = decode_data;
// data = decode_data;
data_count_bit = decode_count_bit;
// just used for log yet, so skip
// if (kia_verify_crc()) {
// FURI_LOG_I(TAG, "Valid signal received with correct CRC");
// } else {
// FURI_LOG_W(TAG, "Signal received but CRC mismatch!");
// }
if (callback)
callback(this);
} else {
// FURI_LOG_E(TAG, "Incomplete signal: only %u bits", decode_count_bit);
// FURI_LOG_E(TAG, "Incomplete signal: only %u bits", instance->decoder.decode_count_bit);
}
decode_data = 0;
decode_count_bit = 0;
break;
@@ -80,26 +86,19 @@ class FProtoSubCarKiaV0 : public FProtoSubCarBase {
te_last = duration;
parser_step = KIADecoderStepCheckDuration;
}
} else {
parser_step = KIADecoderStepReset;
}
break;
case KIADecoderStepCheckDuration:
if (!level) {
if ((DURATION_DIFF(te_last, te_short) < te_delta) &&
(DURATION_DIFF(duration, te_short) < te_delta)) {
if ((DURATION_DIFF(te_last, te_short) < te_delta) && (DURATION_DIFF(duration, te_short) < te_delta)) {
subghz_protocol_blocks_add_bit(0);
if (decode_count_bit % 10 == 0) {
// FURI_LOG_D(TAG, "Decoded %u bits so far", decode_count_bit);
}
parser_step = KIADecoderStepSaveDuration;
} else if (
(DURATION_DIFF(te_last, te_long) < te_delta) &&
(DURATION_DIFF(duration, te_long) < te_delta)) {
(DURATION_DIFF(te_last, te_long) < te_delta) && (DURATION_DIFF(duration, te_long) < te_delta)) {
subghz_protocol_blocks_add_bit(1);
if (decode_count_bit % 10 == 0) {
// FURI_LOG_D(TAG, "Decoded %u bits so far", decode_count_bit);
}
parser_step = KIADecoderStepSaveDuration;
} else {
// FURI_LOG_W(TAG, "Timing mismatch at bit %u. Last: %lu, Current: %lu", decode_count_bit, te_last, duration);
+41 -145
View File
@@ -5,8 +5,7 @@
typedef enum {
KiaV1DecoderStepReset = 0,
KiaV1DecoderStepCheckPreamble,
KiaV1DecoderStepFoundShortLow,
KiaV1DecoderStepCollectRawBits,
KiaV1DecoderStepDecodeData,
} KiaV1DecoderStep;
class FProtoSubCarKiaV1 : public FProtoSubCarBase {
@@ -16,177 +15,74 @@ class FProtoSubCarKiaV1 : public FProtoSubCarBase {
te_short = 800;
te_long = 1600;
te_delta = 200;
min_count_bit_for_found = 56;
}
void kia_v1_add_raw_bit(bool bit) {
if (raw_bit_count < 192) {
uint16_t byte_idx = raw_bit_count / 8;
uint8_t bit_idx = 7 - (raw_bit_count % 8);
if (bit) {
raw_bits[byte_idx] |= (1 << bit_idx);
} else {
raw_bits[byte_idx] &= ~(1 << bit_idx);
}
raw_bit_count++;
}
}
inline bool kia_v1_get_raw_bit(uint16_t idx) {
uint16_t byte_idx = idx / 8;
uint8_t bit_idx = 7 - (idx % 8);
return (raw_bits[byte_idx] >> bit_idx) & 1;
}
bool kia_v1_manchester_decode() {
if (raw_bit_count < 113) {
// FURI_LOG_D(TAG, "Not enough raw bits: %u", raw_bit_count);
return false;
}
// Try different offsets to find best alignment (RTL-433 uses -1 bit offset)
uint16_t best_bits = 0;
uint64_t best_data = 0;
// uint16_t best_offset = 0;
for (uint16_t offset = 0; offset < 8; offset++) {
uint64_t data = 0;
uint16_t decoded_bits = 0;
for (uint16_t i = offset; i + 1 < raw_bit_count && decoded_bits < 56; i += 2) {
bool bit1 = kia_v1_get_raw_bit(i);
bool bit2 = kia_v1_get_raw_bit(i + 1);
uint8_t two_bits = (bit1 << 1) | bit2;
// V1 uses: 10=1, 01=0
if (two_bits == 0x02) { // 10 = decoded 1
data = (data << 1) | 1;
decoded_bits++;
} else if (two_bits == 0x01) { // 01 = decoded 0
data = (data << 1);
decoded_bits++;
} else {
break;
}
}
if (decoded_bits > best_bits) {
best_bits = decoded_bits;
best_data = data;
// best_offset = offset;
}
}
// FURI_LOG_I(TAG, "Best: offset=%u bits=%u data=%014llX", best_offset, best_bits, best_data);
decode_data = best_data;
decode_count_bit = best_bits;
return best_bits >= min_count_bit_for_found;
min_count_bit_for_found = 57;
}
void feed(bool level, uint32_t duration) {
ManchesterEvent event = ManchesterEventReset;
switch (parser_step) {
case KiaV1DecoderStepReset:
// Preamble 0xCCCCCCCD produces alternating LONG pulses
if ((level) && (DURATION_DIFF(duration, te_long) <
te_delta)) {
if ((level) && (DURATION_DIFF(duration, te_long) < te_delta)) {
parser_step = KiaV1DecoderStepCheckPreamble;
te_last = duration;
header_count = 1;
header_count = 0;
decode_data = 0;
decode_count_bit = 0;
FProtoGeneral::manchester_advance(manchester_saved_state, ManchesterEventReset, &manchester_saved_state, NULL);
}
break;
case KiaV1DecoderStepCheckPreamble:
if (level) {
if (DURATION_DIFF(duration, te_long) <
te_delta) {
te_last = duration;
if (!level) {
if ((DURATION_DIFF(duration, te_long) < te_delta) && (DURATION_DIFF(te_last, te_long) < te_delta)) {
header_count++;
} else if (
DURATION_DIFF(duration, te_short) <
te_delta) {
te_last = duration;
} else {
parser_step = KiaV1DecoderStepReset;
}
} else {
// LOW pulse
if (DURATION_DIFF(duration, te_long) <
te_delta) {
header_count++;
} else if (
DURATION_DIFF(duration, te_short) <
te_delta) {
// Short LOW - this is the start of sync (0xCD ends: ...long H, short L, short H)
if (header_count > 12) {
parser_step = KiaV1DecoderStepFoundShortLow;
}
} else {
parser_step = KiaV1DecoderStepReset;
}
if (header_count > 70) {
if ((!level) && (DURATION_DIFF(duration, te_short) < te_delta) && (DURATION_DIFF(te_last, te_long) < te_delta)) {
decode_count_bit = 1;
subghz_protocol_blocks_add_bit(1);
header_count = 0;
parser_step = KiaV1DecoderStepDecodeData;
}
}
break;
case KiaV1DecoderStepFoundShortLow:
// Expecting SHORT HIGH to complete sync
if (level && (DURATION_DIFF(duration, te_short) <
te_delta)) {
// FURI_LOG_I(TAG, "Sync! hdr=%u", header_count);
parser_step = KiaV1DecoderStepCollectRawBits;
raw_bit_count = 0;
memset(raw_bits, 0, sizeof(raw_bits));
// Add the sync short HIGH as first raw bit
kia_v1_add_raw_bit(true);
} else {
parser_step = KiaV1DecoderStepReset;
case KiaV1DecoderStepDecodeData:
if ((DURATION_DIFF(duration, te_short) < te_delta)) {
event = level ? ManchesterEventShortLow : ManchesterEventShortHigh;
} else if ((DURATION_DIFF(duration, te_long) <
te_delta)) {
event = level ? ManchesterEventLongLow : ManchesterEventLongHigh;
}
break;
case KiaV1DecoderStepCollectRawBits:
if (duration > 2400) {
// FURI_LOG_I(TAG, "End! raw_bits=%u", raw_bit_count);
if (kia_v1_manchester_decode()) {
// instance->generic.data = decode_data;
data_count_bit = raw_bit_count / 8;
// Extract fields from 56-bit data per RTL-433:
// Serial: bits 55-24 (32 bits)
// Btn: bits 23-16 (8 bits)
// Count: bits 15-8 (8 bits)
// CRC: bits 7-0 (8 bits)
// instance->generic.serial = (uint32_t)((instance->generic.data >> 24) & 0xFFFFFFFF);
// instance->generic.btn = (uint8_t)((instance->generic.data >> 16) & 0xFF);
// instance->generic.cnt = (uint8_t)((instance->generic.data >> 8) & 0xFF);
if (callback)
callback(this);
if (event != ManchesterEventReset) {
bool data;
bool data_ok = FProtoGeneral::manchester_advance(manchester_saved_state, event, &manchester_saved_state, &data);
if (data_ok) {
decode_data = (decode_data << 1) | data;
decode_count_bit++;
}
}
if (decode_count_bit == min_count_bit_for_found) {
// instance->generic.data = decode_data;
data_count_bit = decode_count_bit;
if (callback)
callback(this);
decode_data = 0;
decode_count_bit = 0;
parser_step = KiaV1DecoderStepReset;
break;
}
int num_bits = 0;
if (DURATION_DIFF(duration, te_short) <
te_delta) {
num_bits = 1;
} else if (
DURATION_DIFF(duration, te_long) <
te_delta) {
num_bits = 2;
} else {
parser_step = KiaV1DecoderStepReset;
break;
}
for (int i = 0; i < num_bits; i++) {
kia_v1_add_raw_bit(level);
}
break;
}
}
uint8_t raw_bits[24]{0};
uint16_t raw_bit_count = 0;
uint16_t header_count = 0;
uint8_t header_count = 0;
ManchesterState manchester_saved_state = ManchesterStateStart1;
};
+55 -114
View File
@@ -15,65 +15,7 @@ class FProtoSubCarKiaV2 : public FProtoSubCarBase {
te_short = 500;
te_long = 1000;
te_delta = 160;
min_count_bit_for_found = 51;
}
void kia_v2_add_raw_bit(bool bit) {
if (raw_bit_count < 160) {
uint16_t byte_idx = raw_bit_count / 8;
uint8_t bit_idx = 7 - (raw_bit_count % 8);
if (bit) {
raw_bits[byte_idx] |= (1 << bit_idx);
} else {
raw_bits[byte_idx] &= ~(1 << bit_idx);
}
raw_bit_count++;
}
}
inline bool kia_v2_get_raw_bit(uint16_t idx) {
uint16_t byte_idx = idx / 8;
uint8_t bit_idx = 7 - (idx % 8);
return (raw_bits[byte_idx] >> bit_idx) & 1;
}
bool kia_v2_manchester_decode() {
if (raw_bit_count < 100) {
return false;
}
uint16_t best_bits = 0;
uint64_t best_data = 0;
for (uint16_t offset = 0; offset < 8; offset++) {
uint64_t data = 0;
uint16_t decoded_bits = 0;
for (uint16_t i = offset; i + 1 < raw_bit_count && decoded_bits < 53; i += 2) {
bool bit1 = kia_v2_get_raw_bit(i);
bool bit2 = kia_v2_get_raw_bit(i + 1);
uint8_t two_bits = (bit1 << 1) | bit2;
if (two_bits == 0x02) {
data = (data << 1) | 1;
decoded_bits++;
} else if (two_bits == 0x01) {
data = (data << 1);
decoded_bits++;
} else {
break;
}
}
if (decoded_bits > best_bits) {
best_bits = decoded_bits;
best_data = data;
}
}
decode_data = best_data;
decode_count_bit = best_bits;
return best_bits >= min_count_bit_for_found;
min_count_bit_for_found = 53;
}
void feed(bool level, uint32_t duration) {
@@ -82,86 +24,85 @@ class FProtoSubCarKiaV2 : public FProtoSubCarBase {
if ((level) && (DURATION_DIFF(duration, te_long) < te_delta)) {
parser_step = KiaV2DecoderStepCheckPreamble;
te_last = duration;
header_count = 1;
header_count = 0;
FProtoGeneral::manchester_advance(manchester_state, ManchesterEventReset, &manchester_state, NULL);
}
break;
case KiaV2DecoderStepCheckPreamble:
if (level) {
if (DURATION_DIFF(duration, te_long) <
te_delta) {
if (level) // HIGH pulse
{
if (DURATION_DIFF(duration, te_long) < te_delta) {
te_last = duration;
header_count++;
} else if (
DURATION_DIFF(duration, te_short) <
te_delta) {
te_last = duration;
} else {
parser_step = KiaV2DecoderStepReset;
}
} else {
if (DURATION_DIFF(duration, te_long) <
te_delta) {
header_count++;
} else if (
DURATION_DIFF(duration, te_short) <
te_delta) {
if (header_count > 10 &&
DURATION_DIFF(te_last, te_short) <
te_delta) {
DURATION_DIFF(duration, te_short) < te_delta) {
if (header_count >= 100) {
header_count = 0;
decode_data = 0;
decode_count_bit = 1;
parser_step = KiaV2DecoderStepCollectRawBits;
raw_bit_count = 0;
memset(raw_bits, 0, sizeof(raw_bits));
subghz_protocol_blocks_add_bit(1);
} else {
te_last = duration;
}
} else {
parser_step = KiaV2DecoderStepReset;
}
} else {
if (DURATION_DIFF(duration, te_long) < te_delta) {
header_count++;
te_last = duration;
} else if (
DURATION_DIFF(duration, te_short) < te_delta) {
te_last = duration;
} else {
parser_step = KiaV2DecoderStepReset;
}
}
break;
case KiaV2DecoderStepCollectRawBits:
if (duration > 1500) {
if (kia_v2_manchester_decode()) {
/*data = decode_data;
data_count_bit = decode_count_bit;
case KiaV2DecoderStepCollectRawBits: {
ManchesterEvent event;
serial = (uint32_t)((data >> 20) & 0xFFFFFFFF);
btn = (uint8_t)((data >> 16) & 0x0F);
uint16_t raw_count = (uint16_t)((data >> 4) & 0xFFF);
cnt = ((raw_count >> 4) | (raw_count << 8)) & 0xFFF;
*/
data_count_bit = decode_count_bit;
if (callback)
callback(this);
}
parser_step = KiaV2DecoderStepReset;
break;
}
int num_bits = 0;
if (DURATION_DIFF(duration, te_short) <
te_delta) {
num_bits = 1;
if (DURATION_DIFF(duration, te_short) < te_delta) {
event = level ? ManchesterEventShortLow : ManchesterEventShortHigh;
} else if (
DURATION_DIFF(duration, te_long) <
te_delta) {
num_bits = 2;
DURATION_DIFF(duration, te_long) < te_delta) {
event = level ? ManchesterEventLongLow : ManchesterEventLongHigh;
} else {
parser_step = KiaV2DecoderStepReset;
break;
}
for (int i = 0; i < num_bits; i++) {
kia_v2_add_raw_bit(level);
}
bool data_bit;
if (FProtoGeneral::manchester_advance(manchester_state, event, &manchester_state, &data_bit)) {
decode_data = (decode_data << 1) | data_bit;
decode_count_bit++;
if (decode_count_bit == 53) {
// instance->generic.data = decode_data;
data_count_bit = decode_count_bit;
// instance->generic.serial = (uint32_t)((instance->generic.data >> 20) & 0xFFFFFFFF);
// instance->generic.btn = (uint8_t)((instance->generic.data >> 16) & 0x0F);
// uint16_t raw_count = (uint16_t)((instance->generic.data >> 4) & 0xFFF);
// instance->generic.cnt = ((raw_count >> 4) | (raw_count << 8)) & 0xFFF;
if (callback)
callback(this);
decode_data = 0;
decode_count_bit = 0;
header_count = 0;
parser_step = KiaV2DecoderStepReset;
}
}
break;
}
}
}
uint8_t raw_bits[20]{0};
uint16_t raw_bit_count = 0;
uint16_t header_count = 0;
ManchesterState manchester_state = ManchesterStateMid1;
};
+8 -16
View File
@@ -15,7 +15,7 @@ class FProtoSubCarKiaV3V4 : public FProtoSubCarBase {
te_short = 400;
te_long = 800;
te_delta = 150;
min_count_bit_for_found = 64;
min_count_bit_for_found = 68;
}
uint8_t reverse8(uint8_t byte) {
byte = (byte & 0xF0) >> 4 | (byte & 0x0F) << 4;
@@ -36,7 +36,7 @@ class FProtoSubCarKiaV3V4 : public FProtoSubCarBase {
}
}
bool kia_v3_v4_process_buffer() {
if (raw_bit_count < 64) {
if (raw_bit_count < 68) {
return false;
}
@@ -56,7 +56,7 @@ class FProtoSubCarKiaV3V4 : public FProtoSubCarBase {
uint8_t btn = (reverse8(b[7]) & 0xF0) >> 4;
decode_data = serial;
decode_count_bit = 64;
decode_count_bit = 68;
decode_data2 = btn;
data_count_bit = decode_count_bit;
if (callback)
@@ -93,8 +93,7 @@ class FProtoSubCarKiaV3V4 : public FProtoSubCarBase {
void feed(bool level, uint32_t duration) {
switch (parser_step) {
case KiaV3V4DecoderStepReset:
if (level && DURATION_DIFF(duration, te_short) <
te_delta) {
if (level && DURATION_DIFF(duration, te_short) < te_delta) {
parser_step = KiaV3V4DecoderStepCheckPreamble;
te_last = duration;
header_count = 1;
@@ -103,8 +102,7 @@ class FProtoSubCarKiaV3V4 : public FProtoSubCarBase {
case KiaV3V4DecoderStepCheckPreamble:
if (level) {
if (DURATION_DIFF(duration, te_short) <
te_delta) {
if (DURATION_DIFF(duration, te_short) < te_delta) {
te_last = duration;
} else if (duration > 1000 && duration < 1500) {
// V4 style: Sync is LONG HIGH
@@ -130,11 +128,7 @@ class FProtoSubCarKiaV3V4 : public FProtoSubCarBase {
} else {
parser_step = KiaV3V4DecoderStepReset;
}
} else if (
DURATION_DIFF(duration, te_short) <
te_delta &&
DURATION_DIFF(te_last, te_short) <
te_delta) {
} else if (DURATION_DIFF(duration, te_short) < te_delta && DURATION_DIFF(te_last, te_short) < te_delta) {
header_count++;
} else if (duration > 1500) {
parser_step = KiaV3V4DecoderStepReset;
@@ -149,12 +143,10 @@ class FProtoSubCarKiaV3V4 : public FProtoSubCarBase {
kia_v3_v4_process_buffer();
parser_step = KiaV3V4DecoderStepReset;
} else if (
DURATION_DIFF(duration, te_short) <
te_delta) {
DURATION_DIFF(duration, te_short) < te_delta) {
kia_v3_v4_add_raw_bit(false);
} else if (
DURATION_DIFF(duration, te_long) <
te_delta) {
DURATION_DIFF(duration, te_long) < te_delta) {
kia_v3_v4_add_raw_bit(true);
} else {
parser_step = KiaV3V4DecoderStepReset;
+43 -103
View File
@@ -1,10 +1,11 @@
#pragma once
#include "subcarbase.hpp"
#include <cstring>
typedef enum {
KiaV5DecoderStepReset = 0,
KiaV5DecoderStepCheckPreamble,
KiaV5DecoderStepCollectRawBits,
KiaV5DecoderStepData,
} KiaV5DecoderStep;
class FProtoSubCarKiaV5 : public FProtoSubCarBase {
@@ -17,74 +18,37 @@ class FProtoSubCarKiaV5 : public FProtoSubCarBase {
min_count_bit_for_found = 64;
}
inline bool kia_v5_get_raw_bit(uint16_t idx) {
uint16_t byte_idx = idx / 8;
uint8_t bit_idx = 7 - (idx % 8);
return (raw_bits[byte_idx] >> bit_idx) & 1;
}
void kia_v5_add_raw_bit(bool bit) {
if (raw_bit_count < 256) {
uint16_t byte_idx = raw_bit_count / 8;
uint8_t bit_idx = 7 - (raw_bit_count % 8);
if (bit) {
raw_bits[byte_idx] |= (1 << bit_idx);
} else {
raw_bits[byte_idx] &= ~(1 << bit_idx);
}
raw_bit_count++;
}
}
bool kia_v5_manchester_decode() {
if (raw_bit_count < 130) {
return false;
}
decode_data = 0;
decode_count_bit = 0;
// Start at offset 2 for proper Manchester alignment
const uint16_t start_bit = 2;
for (uint16_t i = start_bit;
i + 1 < raw_bit_count && decode_count_bit < 64;
i += 2) {
bool bit1 = kia_v5_get_raw_bit(i);
bool bit2 = kia_v5_get_raw_bit(i + 1);
uint8_t two_bits = (bit1 << 1) | bit2;
if (two_bits == 0x01) { // 01 = decoded 1
decode_data = (decode_data << 1) | 1;
decode_count_bit++;
} else if (two_bits == 0x02) { // 10 = decoded 0
decode_data = (decode_data << 1);
decode_count_bit++;
} else {
break;
}
}
return decode_count_bit >= min_count_bit_for_found;
void kia_v5_add_bit(bool bit) {
decode_data = (decode_data << 1) | (bit ? 1 : 0);
decode_count_bit++;
}
void feed(bool level, uint32_t duration) {
switch (parser_step) {
case KiaV5DecoderStepReset:
if ((level) && (DURATION_DIFF(duration, te_short) <
te_delta)) {
if ((level) && (DURATION_DIFF(duration, te_short) < te_delta)) {
parser_step = KiaV5DecoderStepCheckPreamble;
te_last = duration;
header_count = 1;
decode_count_bit = 0;
decode_data = 0;
FProtoGeneral::manchester_advance(manchester_state, ManchesterEventReset, &manchester_state, NULL);
}
break;
case KiaV5DecoderStepCheckPreamble:
if (level) {
if ((DURATION_DIFF(duration, te_short) <
te_delta) ||
(DURATION_DIFF(duration, te_long) <
te_delta)) {
if (DURATION_DIFF(duration, te_long) < te_delta) {
if (header_count > 40) {
parser_step = KiaV5DecoderStepData;
decode_count_bit = 0;
decode_data = 0;
decode_data2 = 0;
header_count = 0;
} else {
te_last = duration;
}
} else if (DURATION_DIFF(duration, te_short) < te_delta) {
te_last = duration;
} else {
parser_step = KiaV5DecoderStepReset;
@@ -100,13 +64,7 @@ class FProtoSubCarKiaV5 : public FProtoSubCarBase {
te_delta) &&
(DURATION_DIFF(te_last, te_short) <
te_delta)) {
if (header_count > 40) {
parser_step = KiaV5DecoderStepCollectRawBits;
raw_bit_count = 0;
memset(raw_bits, 0, sizeof(raw_bits));
} else {
header_count++;
}
header_count++;
} else if (
DURATION_DIFF(te_last, te_long) <
te_delta) {
@@ -114,33 +72,21 @@ class FProtoSubCarKiaV5 : public FProtoSubCarBase {
} else {
parser_step = KiaV5DecoderStepReset;
}
te_last = duration;
}
break;
case KiaV5DecoderStepCollectRawBits:
if (duration > 1200) {
if (kia_v5_manchester_decode()) {
// generic.data = decode_data;
// generic.data_count_bit = decode_count_bit;
data_count_bit = decode_count_bit;
// Compute yek (bit-reverse each byte)
uint64_t yek = 0;
for (int i = 0; i < 8; i++) {
uint8_t byte = (decode_data >> (i * 8)) & 0xFF;
uint8_t reversed = 0;
for (int b = 0; b < 8; b++) {
if (byte & (1 << b))
reversed |= (1 << (7 - b));
}
yek |= ((uint64_t)reversed << ((7 - i) * 8));
}
decode_data = yek;
// Shift serial right by 1 to correct alignment
// generic.serial = (uint32_t)(((yek >> 32) & 0x0FFFFFFF) >> 1);
// generic.btn = (uint8_t)((yek >> 61) & 0x07); // Shift btn too
// generic.cnt = (uint16_t)(yek & 0xFFFF);
case KiaV5DecoderStepData: {
ManchesterEvent event;
if (DURATION_DIFF(duration, te_short) < te_delta) {
event = level ? ManchesterEventShortHigh : ManchesterEventShortLow;
} else if (DURATION_DIFF(duration, te_long) < te_delta) {
event = level ? ManchesterEventLongHigh : ManchesterEventLongLow;
} else {
if (decode_count_bit >= min_count_bit_for_found) {
// instance->generic.data = instance->decode_data2;
data_count_bit = (decode_count_bit > 67) ? 67 : decode_count_bit;
if (callback)
callback(this);
}
@@ -149,28 +95,22 @@ class FProtoSubCarKiaV5 : public FProtoSubCarBase {
break;
}
int num_bits = 0;
if (DURATION_DIFF(duration, te_short) <
te_delta) {
num_bits = 1;
} else if (
DURATION_DIFF(duration, te_long) <
te_delta) {
num_bits = 2;
} else {
parser_step = KiaV5DecoderStepReset;
break;
}
for (int i = 0; i < num_bits; i++) {
kia_v5_add_raw_bit(level);
bool data_bit;
if (decode_count_bit <= 66 && FProtoGeneral::manchester_advance(manchester_state, event, &manchester_state, &data_bit)) {
kia_v5_add_bit(data_bit);
if (decode_count_bit == 64) {
decode_data2 = decode_data;
decode_data = 0;
}
}
te_last = duration;
break;
}
}
}
uint8_t raw_bits[32]{};
uint16_t raw_bit_count = 0;
uint16_t header_count = 0;
ManchesterState manchester_state = ManchesterStateMid1;
};
+1
View File
@@ -20,6 +20,7 @@ class FProtoSubCarSubaru : public FProtoSubCarBase {
te_delta = 260;
min_count_bit_for_found = 64;
}
void subghz_protocol_decoder_subaru_reset() {
parser_step = SubaruDecoderStepReset;
te_last = 0;
+65 -50
View File
@@ -6,8 +6,8 @@
typedef enum {
SuzukiDecoderStepReset = 0,
SuzukiDecoderStepFoundStartPulse,
SuzukiDecoderStepSaveDuration,
SuzukiDecoderStepCountPreamble = 1,
SuzukiDecoderStepDecodeData = 2,
} SuzukiDecoderStep;
class FProtoSubCarSuzuki : public FProtoSubCarBase {
@@ -20,54 +20,45 @@ class FProtoSubCarSuzuki : public FProtoSubCarBase {
min_count_bit_for_found = 64;
}
void suzuki_add_bit(uint32_t bit) {
uint32_t carry = data_low >> 31;
data_low = (data_low << 1) | bit;
data_high = (data_high << 1) | carry;
data_count_bit++;
decode_data = (decode_data << 1) | bit;
decode_count_bit++;
}
void subghz_protocol_decoder_suzuki_reset() {
parser_step = SuzukiDecoderStepReset;
header_count = 0;
data_count_bit = 0;
data_low = 0;
data_high = 0;
decode_data = 0;
}
void feed(bool level, uint32_t duration) {
switch (parser_step) {
case SuzukiDecoderStepReset:
// Wait for short HIGH pulse (~250µs) to start preamble
if (!level)
// Wait for HIGH pulse (~250µs) to start preamble
if (!level) {
return;
}
if (DURATION_DIFF(duration, te_short) > te_delta) {
return;
}
data_low = 0;
data_high = 0;
parser_step = SuzukiDecoderStepFoundStartPulse;
decode_data = 0;
decode_count_bit = 0;
parser_step = SuzukiDecoderStepCountPreamble;
header_count = 0;
data_count_bit = 0;
break;
case SuzukiDecoderStepFoundStartPulse:
case SuzukiDecoderStepCountPreamble:
if (level) {
// HIGH pulse
if (header_count < 257) {
// Still in preamble - just count
return;
}
// After preamble, look for long HIGH to start data
if (DURATION_DIFF(duration, te_long) < te_delta) {
parser_step = SuzukiDecoderStepSaveDuration;
suzuki_add_bit(1);
if (header_count >= 300) {
if (DURATION_DIFF(duration, te_long) <= te_delta) {
parser_step = SuzukiDecoderStepDecodeData;
suzuki_add_bit(1);
}
}
// Ignore short HIGHs after preamble until we see a long one
} else {
// LOW pulse - count as header if short
if (DURATION_DIFF(duration, te_short) < te_delta) {
if (DURATION_DIFF(duration, te_short) <= te_delta) {
te_last = duration;
header_count++;
} else {
@@ -76,45 +67,69 @@ class FProtoSubCarSuzuki : public FProtoSubCarBase {
}
break;
case SuzukiDecoderStepSaveDuration:
case SuzukiDecoderStepDecodeData:
if (level) {
// HIGH pulse - determines bit value
// Long HIGH (~500µs) = 1, Short HIGH (~250µs) = 0
if (DURATION_DIFF(duration, te_long) < te_delta) {
suzuki_add_bit(1);
} else if (DURATION_DIFF(duration, te_short) < te_delta) {
suzuki_add_bit(0);
if (duration < te_long) {
uint32_t diff_long = 500 - duration;
if (diff_long > 99) {
uint32_t diff_short;
if (duration < 250) {
diff_short = 250 - duration;
} else {
diff_short = duration - 250;
}
if (diff_short <= 99) {
suzuki_add_bit(0);
}
} else {
suzuki_add_bit(1);
}
} else {
parser_step = SuzukiDecoderStepReset;
uint32_t diff_long = duration - 500;
if (diff_long <= 99) {
suzuki_add_bit(1);
}
}
// Stay in this state for next bit
} else {
// LOW pulse - check for gap (end of transmission)
if (DURATION_DIFF(duration, SUZUKI_GAP_TIME) < SUZUKI_GAP_DELTA) {
// Gap found - end of transmission
if (data_count_bit == 64) {
uint32_t diff_gap;
if (duration < SUZUKI_GAP_TIME) {
diff_gap = SUZUKI_GAP_TIME - duration;
} else {
diff_gap = duration - SUZUKI_GAP_TIME;
}
if (diff_gap <= SUZUKI_GAP_DELTA) {
if (decode_count_bit == 64) {
// instance->generic.data = decode_data;
data_count_bit = 64;
decode_data = ((uint64_t)data_high << 32) | (uint64_t)data_low;
// Check manufacturer nibble (should be 0xF)
uint8_t manufacturer = (data_high >> 28) & 0xF;
if (manufacturer == 0xF) {
// Extract fields
decode_data2 = 0; // Not used
if (callback) {
callback(this);
}
/*uint64_t data = instance->generic.data;
uint32_t data_high = (uint32_t)(data >> 32);
uint32_t data_low = (uint32_t)data;
instance->generic.serial = ((data_high & 0xFFF) << 16) | (data_low >> 16);
instance->generic.btn = (data_low >> 12) & 0xF;
instance->generic.cnt = (data_high << 4) >> 16;
*/
if (callback) {
callback(this);
}
}
decode_data = 0;
decode_count_bit = 0;
parser_step = SuzukiDecoderStepReset;
}
// Short LOW pulses are ignored - stay in this state
}
break;
}
}
uint16_t header_count = 0;
uint32_t data_high = 0;
uint32_t data_low = 0;
uint8_t data_count_bit = 0;
};
+213
View File
@@ -0,0 +1,213 @@
/*
* Copyright (C) 2026 Pezsma
*
* 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_morse.hpp"
#include "audio_dma.hpp"
#include "portapack_shared_memory.hpp"
#include "event_m4.hpp"
#include <cmath>
void MorseProcessor::configure() {
configured = false;
baseband_fs = 3072000;
baseband_thread.set_sampling_rate(baseband_fs);
// 1. DSP filters
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.configure(24000, 5000);
audio_output.configure(iir_config_passthrough, iir_config_passthrough, 0.0f);
// 2. Resetting variables
dc_offset = 0;
lpf_sample = 0;
prev_sample = 0;
// 3. Algorithm reset
zc_counter = 0;
last_zc_counter = 0;
current_freq = 700.0f;
update_goertzel_coeff(700.0f);
goertzel_count = 0;
s_prev_i = 0;
s_prev2_i = 0;
duration_samples = 0;
was_signaling = false;
noise_floor = 5000; // learning speed
startup_delay = 20;
squelch_is_open = false;
configured = true;
}
void MorseProcessor::on_message(const Message* const p) {
if (p->id == Message::ID::MorseRXConfig) {
configure();
} else if (p->id == Message::ID::NBFMConfigure) {
auto nbfm_msg = *reinterpret_cast<const NBFMConfigureMessage*>(p);
user_squelch_level = nbfm_msg.squelch_level;
audio_output.configure(iir_config_passthrough, iir_config_passthrough, (float)user_squelch_level / 100.0f);
}
}
void MorseProcessor::update_goertzel_coeff(float freq) {
if (freq < 400.0f) freq = 400.0f;
if (freq > 1500.0f) freq = 1500.0f; // limit to algo capacity min/max
float omega = 2.0f * M_PI * freq / 24000.0f;
coeff_int = (int32_t)(2.0f * cosf(omega) * 16384.0f);
}
void MorseProcessor::execute(const buffer_c8_t& buffer) {
if (!configured) return;
buffer_c16_t dst_buffer_c16(dst_buffer.data(), dst_buffer.size());
const auto decim_0_out = decim_0.execute(buffer, dst_buffer_c16);
const auto decim_1_out = decim_1.execute(decim_0_out, dst_buffer_c16);
const auto channel = channel_filter.execute(decim_1_out, dst_buffer_c16);
feed_channel_stats(channel);
buffer_s16_t audio_buffer_s16(audio_buffer.data(), audio_buffer.size());
auto audio_buf = demod.execute(channel, audio_buffer_s16);
for (size_t i = 0; i < audio_buf.count; i++) {
int32_t raw_sample = audio_buf.p[i];
// 1. DC & LPF
dc_offset += (raw_sample - dc_offset) / 32;
int32_t sample = raw_sample - dc_offset;
lpf_sample = (lpf_sample * 3 + sample) / 4;
// 2. Squelch
int32_t abs_sample = (sample < 0) ? -sample : sample;
int32_t audio_threshold = (user_squelch_level * user_squelch_level) * 3;
int32_t current_audio_threshold = squelch_is_open ? (audio_threshold / 2) : audio_threshold;
if (abs_sample > current_audio_threshold || user_squelch_level == 0) {
squelch_is_open = true;
squelch_hold = 2400;
} else {
if (squelch_hold > 0)
squelch_hold--;
else
squelch_is_open = false;
}
// 3. Frequency measurement (ZC)
if (squelch_is_open && !was_signaling) {
if ((lpf_sample >= 0 && prev_sample < 0) || (lpf_sample < 0 && prev_sample >= 0)) {
if (zc_counter >= 8 && zc_counter <= 32) {
int32_t diff = zc_counter - last_zc_counter;
if (diff >= -1 && diff <= 1) {
float n_freq = 24000.0f / (zc_counter * 2.0f);
// Hybrid tracking
if (zc_counter < 15) {
// This stabilizes the 1000-1400 Hz range
current_freq = (current_freq * 0.6f) + (n_freq * 0.4f);
} else {
current_freq = n_freq;
}
update_goertzel_coeff(current_freq);
}
last_zc_counter = zc_counter;
} else {
last_zc_counter = 0;
}
zc_counter = 0;
} else {
if (zc_counter < 100) zc_counter++;
}
}
prev_sample = (int16_t)lpf_sample;
// 4. Goertzel
int64_t s = (int64_t)sample + (((int64_t)coeff_int * s_prev_i) >> 14) - s_prev2_i;
s_prev2_i = s_prev_i;
s_prev_i = (int32_t)s;
goertzel_count++;
// 5. Detection
if (goertzel_count >= 60) {
if (startup_delay > 0) {
startup_delay--;
int64_t pwr = (int64_t)s_prev_i * s_prev_i + (int64_t)s_prev2_i * s_prev2_i -
(((int64_t)s_prev_i * s_prev2_i * coeff_int) >> 14);
noise_floor = (noise_floor * 15 + pwr) / 16;
} else {
int64_t power = (int64_t)s_prev_i * s_prev_i + (int64_t)s_prev2_i * s_prev2_i -
(((int64_t)s_prev_i * s_prev2_i * coeff_int) >> 14);
if (!was_signaling) {
noise_floor = (noise_floor * 127 + power) / 128;
}
int64_t sensitivity = 4 + (user_squelch_level / 10);
int64_t base_pwr_threshold = noise_floor * sensitivity;
int64_t current_pwr_threshold = was_signaling ? (base_pwr_threshold / 2) : base_pwr_threshold;
bool is_tone = squelch_is_open && (power > current_pwr_threshold) && (power > 150000);
if (is_tone != was_signaling) {
int32_t duration_us = (int32_t)((int64_t)duration_samples * 125 / 3);
if (duration_us > 10000) {
message.state_durations[0] = was_signaling ? duration_us : -duration_us;
message.measured_frequency = (uint32_t)current_freq;
message.state_cnt = 1;
shared_memory.application_queue.push(message);
}
was_signaling = is_tone;
duration_samples = 0;
}
if (!was_signaling && duration_samples > 28800) {
int32_t duration_us = (int32_t)((int64_t)duration_samples * 125 / 3);
message.state_durations[0] = -duration_us;
message.state_cnt = 1;
shared_memory.application_queue.push(message);
duration_samples = 0;
}
}
duration_samples += 60;
s_prev_i = 0;
s_prev2_i = 0;
goertzel_count = 0;
}
audio_buf.p[i] = squelch_is_open ? (int16_t)sample : 0;
}
audio_output.write(audio_buf);
}
int main() {
audio::dma::init_audio_out();
EventDispatcher event_dispatcher{std::make_unique<MorseProcessor>()};
event_dispatcher.run();
return 0;
}
+90
View File
@@ -0,0 +1,90 @@
/*
* Copyright (C) 2026 Pezsma
*
* 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_MORSE_H__
#define __PROC_MORSE_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"
class MorseProcessor : public BasebandProcessor {
public:
MorseProcessor() {}
void execute(const buffer_c8_t& buffer) override;
void on_message(const Message* const p) override;
void update_goertzel_coeff(float freq);
private:
bool configured{false};
size_t baseband_fs = 3072000;
BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive};
RSSIThread rssi_thread{};
std::array<complex16_t, 512> dst_buffer{};
std::array<int16_t, 32> audio_buffer{};
dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0{};
dsp::decimate::FIRC16xR16x32Decim8 decim_1{};
dsp::decimate::FIRAndDecimateComplex channel_filter{};
dsp::demodulate::FM demod{};
AudioOutput audio_output{};
// Goertzel and signal detection
int32_t coeff_int{0};
int32_t s_prev_i{0};
int32_t s_prev2_i{0};
uint32_t goertzel_count{0};
uint32_t duration_samples{0};
bool was_signaling{false};
// Variables required for measurement
int16_t prev_sample{0};
uint32_t zc_counter{0};
float current_freq{0.0f};
int32_t lpf_sample{0};
int32_t dc_offset{0};
// Squelch and stability
bool squelch_is_open{false};
int32_t squelch_hold{0};
int32_t user_squelch_level{0};
int64_t noise_floor{0};
int32_t startup_delay{0}; // Power-on delay
int32_t last_zc_counter{0}; // Previous ZC measurement for comparison
MorseRXDataMessage message{};
void configure();
};
#endif // __PROC_MORSE_H__
+4 -4
View File
@@ -241,7 +241,7 @@ w25q80bv_driver_t spi_flash = {
sgpio_config_t sgpio_config = {
.gpio_q_invert = &gpio_q_invert,
.gpio_hw_sync_enable = &gpio_hw_sync_enable,
.gpio_trigger_enable = &gpio_hw_sync_enable,
.slice_mode_multislice = true,
};
@@ -950,7 +950,7 @@ void pin_setup(void) {
#ifdef HACKRF_ONE
if (detected_platform() == BOARD_ID_HACKRF1_R9) {
rf_path.gpio_rx = &gpio_h1r9_rx;
sgpio_config.gpio_hw_sync_enable = &gpio_h1r9_hw_sync_enable;
sgpio_config.gpio_trigger_enable = &gpio_h1r9_hw_sync_enable;
}
#endif
rf_path_pin_setup(&rf_path);
@@ -1041,8 +1041,8 @@ void set_leds(const uint8_t state) {
}
}
void hw_sync_enable(const hw_sync_mode_t hw_sync_mode) {
gpio_write(sgpio_config.gpio_hw_sync_enable, hw_sync_mode == 1);
void trigger_enable(bool hw_sync_mode) {
gpio_write(sgpio_config.gpio_trigger_enable, hw_sync_mode == 1);
}
void halt_and_flash(const uint32_t duration) {
+5 -5
View File
@@ -65,8 +65,8 @@ const usb_request_handlers_t usb_request_handlers = {
void usb_configuration_changed(usb_device_t* const device) {
(void)device;
usb_endpoint_init(&usb_endpoint_bulk_in);
usb_endpoint_init(&usb_endpoint_bulk_out);
usb_endpoint_init(&usb_endpoint_bulk_in, false);
usb_endpoint_init(&usb_endpoint_bulk_out, false);
}
void start_usb(void) {
@@ -84,8 +84,8 @@ void start_usb(void) {
usb_queue_init(&usb_endpoint_bulk_out_queue);
usb_queue_init(&usb_endpoint_bulk_in_queue);
usb_endpoint_init(&usb_endpoint_control_out);
usb_endpoint_init(&usb_endpoint_control_in);
usb_endpoint_init(&usb_endpoint_control_out, false);
usb_endpoint_init(&usb_endpoint_control_in, false);
nvic_set_priority(NVIC_USB0_IRQ, 255);
@@ -126,4 +126,4 @@ void usb_transfer(void) {
scsi_command(msd_cbw_data);
}
}
}
}
+26
View File
@@ -146,6 +146,9 @@ class Message {
SSTVRXPhaseSlant = 88,
SSTVRXCalibration = 89,
SubCarData = 90,
MorseRXData = 91,
MorseRXConfig = 92,
TXDisabled = 93,
MAX
};
@@ -1699,4 +1702,27 @@ class SubCarDataMessage : public Message {
uint64_t data2 = 0;
};
class MorseRXDataMessage : public Message {
public:
constexpr MorseRXDataMessage()
: Message{ID::MorseRXData} {}
uint32_t measured_frequency = 0;
int32_t state_durations[2] = {0}; // positive: high, negative: low
uint16_t state_cnt = 0;
const uint16_t maxptr = 1;
};
class MorseRXConfigureMessage : public Message {
public:
constexpr MorseRXConfigureMessage()
: Message{ID::MorseRXConfig} {}
};
class TXDisabledMessage : public Message {
public:
constexpr TXDisabledMessage()
: Message{ID::TXDisabled} {
}
};
#endif /*__MESSAGE_H__*/
@@ -152,12 +152,12 @@ struct misc_config_t {
bool config_sdcard_high_speed_io : 1;
bool config_disable_config_mode : 1;
bool beep_on_packets : 1;
bool UNUSED_6 : 1;
bool UNUSED_7 : 1;
bool tx_disabled : 1;
bool tx_amp_disabled : 1;
uint8_t tx_gain_max_db;
uint8_t PLACEHOLDER_1;
uint8_t PLACEHOLDER_2;
uint8_t PLACEHOLDER_3;
};
static_assert(sizeof(misc_config_t) == sizeof(uint32_t));
@@ -441,6 +441,10 @@ void defaults() {
set_recon_repeat_delay(1);
set_config_sdcard_high_speed_io(false, true);
set_config_tx_disabled(false);
set_config_tx_amp_disabled(false);
set_config_tx_gain_max_db(47);
}
void init() {
@@ -649,6 +653,18 @@ bool config_sdcard_high_speed_io() {
return data->misc_config.config_sdcard_high_speed_io;
}
bool config_tx_disabled() {
return data->misc_config.tx_disabled;
}
bool config_tx_amp_disabled() {
return data->misc_config.tx_amp_disabled;
}
uint8_t config_tx_gain_max_db() {
return data->misc_config.tx_gain_max_db;
}
bool stealth_mode() {
return data->ui_config.stealth_mode;
}
@@ -743,6 +759,18 @@ void set_config_sdcard_high_speed_io(bool v, bool save) {
data->misc_config.config_sdcard_high_speed_io = v;
}
void set_config_tx_disabled(bool v) {
data->misc_config.tx_disabled = v;
}
void set_config_tx_amp_disabled(bool v) {
data->misc_config.tx_amp_disabled = v;
}
void set_config_tx_gain_max_db(uint8_t v) {
data->misc_config.tx_gain_max_db = v;
}
void set_stealth_mode(bool v) {
data->ui_config.stealth_mode = v;
}
@@ -203,6 +203,9 @@ bool config_disable_external_tcxo();
bool config_sdcard_high_speed_io();
bool config_disable_config_mode();
bool beep_on_packets();
bool config_tx_disabled();
bool config_tx_amp_disabled();
uint8_t config_tx_gain_max_db();
bool config_splash();
bool config_converter();
@@ -226,6 +229,9 @@ void set_config_disable_external_tcxo(bool v);
void set_config_sdcard_high_speed_io(bool v, bool save);
void set_config_disable_config_mode(bool v);
void set_beep_on_packets(bool v);
void set_config_tx_disabled(bool v);
void set_config_tx_amp_disabled(bool v);
void set_config_tx_gain_max_db(uint8_t v);
void set_config_splash(bool v);
bool config_converter();
+1
View File
@@ -124,6 +124,7 @@ constexpr image_tag_t image_tag_protoview{'P', 'P', 'V', 'W'};
constexpr image_tag_t image_tag_wefaxrx{'P', 'W', 'F', 'X'};
constexpr image_tag_t image_tag_noaaapt_rx{'P', 'N', 'O', 'A'};
constexpr image_tag_t image_tag_sstv_rx{'P', 'S', 'R', 'X'};
constexpr image_tag_t image_tag_morse{'P', 'M', 'R', 'S'};
constexpr image_tag_t image_tag_noop{'P', 'N', 'O', 'P'};
+70 -19
View File
@@ -1358,14 +1358,18 @@ void NewButton::paint(Painter& painter) {
}
if (!text_.empty()) {
const auto label_r = style.font.size_of(text_);
auto label_r = style.font.size_of(text_);
std::string text_to_draw = text_;
if (label_r.width() > r.width() - 2) {
// Truncate text to fit
size_t max_chars = (r.width() - 2) / style.font.char_width();
text_to_draw = text_.substr(0, max_chars);
label_r = style.font.size_of(text_to_draw);
}
if (bitmap_) {
y += spacing;
}
painter.draw_string(
{r.left() + (r.width() - label_r.width()) / 2, y},
style,
text_);
painter.draw_string({r.left() + (r.width() - label_r.width()) / 2, y}, style, text_to_draw);
}
} else { // no valign
if (bitmap_) {
@@ -1379,11 +1383,16 @@ void NewButton::paint(Painter& painter) {
}
if (!text_.empty()) {
const auto label_r = style.font.size_of(text_);
painter.draw_string(
{r.left() + (r.width() - label_r.width()) / 2, y + (r.height() - label_r.height()) / 2},
style,
text_);
auto label_r = style.font.size_of(text_);
std::string text_to_draw = text_;
if (label_r.width() > r.width() - 2) {
// Truncate text to fit
size_t max_chars = (r.width() - 2) / style.font.char_width();
text_to_draw = text_.substr(0, max_chars);
label_r = style.font.size_of(text_to_draw);
}
painter.draw_string({r.left() + (r.width() - label_r.width()) / 2, y + (r.height() - label_r.height()) / 2}, style,
text_to_draw);
}
}
}
@@ -1732,6 +1741,14 @@ bool ImageOptionsField::on_keyboard(const KeyboardEvent key) {
return false;
}
bool ImageOptionsField::on_key(const KeyEvent event) {
if (event == KeyEvent::Select) {
on_encoder(1);
return true;
}
return false;
}
bool ImageOptionsField::on_touch(const TouchEvent event) {
if (event.type == TouchEvent::Type::Start) {
focus();
@@ -1881,6 +1898,14 @@ bool OptionsField::on_keyboard(const KeyboardEvent key) {
return false;
}
bool OptionsField::on_key(const KeyEvent event) {
if (event == KeyEvent::Select) {
on_encoder(1);
return true;
}
return false;
}
bool OptionsField::on_touch(const TouchEvent event) {
if (event.type == TouchEvent::Type::Start) {
focus();
@@ -2269,20 +2294,44 @@ void NumberField::getWidgetName(std::string& result) {
}
void NumberField::set_value(int32_t new_value, bool trigger_change) {
const int32_t lo = range.first;
const int32_t hi = range.second;
// Helper: floor-div for negatives
auto floordiv = [](int64_t a, int64_t b) -> int64_t { // b > 0
int64_t q = a / b;
int64_t r = a % b;
return (r < 0) ? (q - 1) : q;
};
// Helper: positive modulo
auto posmod = [](int64_t a, int64_t m) -> int64_t { // m > 0
int64_t r = a % m;
return (r < 0) ? (r + m) : r;
};
if (can_loop) {
if (new_value >= range.first)
new_value = new_value % (range.second + 1);
else
new_value = range.second + new_value + 1;
// number of discrete positions
const int64_t count = (int64_t)(hi - lo) / step + 1;
// map value -> step index (floor so negatives behave)
int64_t idx = floordiv((int64_t)new_value - lo, step);
// wrap index
idx = posmod(idx, count);
// rebuild value
new_value = (int32_t)(lo + idx * step);
} else {
new_value = clip(new_value, lo, hi);
// optionally snap to step here too
int64_t idx = floordiv((int64_t)new_value - lo, step);
new_value = (int32_t)(lo + idx * step);
}
new_value = clip(new_value, range.first, range.second);
// set final value if needed
if (new_value != value()) {
value_ = new_value;
if (on_change && trigger_change) {
on_change(value_);
}
if (on_change && trigger_change) on_change(value_);
set_dirty();
}
}
@@ -2313,6 +2362,8 @@ bool NumberField::on_key(const KeyEvent key) {
if (on_select) {
on_select(*this);
return true;
} else {
return on_encoder(1);
}
}
+7 -2
View File
@@ -666,6 +666,8 @@ class ImageOptionsField : public Widget {
bool on_encoder(const EncoderEvent delta) override;
bool on_touch(const TouchEvent event) override;
bool on_keyboard(const KeyboardEvent event) override;
bool on_key(const KeyEvent event) override;
void getAccessibilityText(std::string& result) override;
void getWidgetName(std::string& result) override;
@@ -674,6 +676,7 @@ class ImageOptionsField : public Widget {
size_t selected_index_{0};
Color foreground_;
Color background_;
bool is_activated = false;
};
class OptionsField : public Widget {
@@ -706,6 +709,7 @@ class OptionsField : public Widget {
bool on_encoder(const EncoderEvent delta) override;
bool on_touch(const TouchEvent event) override;
bool on_keyboard(const KeyboardEvent event) override;
bool on_key(const KeyEvent event) override;
void getAccessibilityText(std::string& result) override;
void getWidgetName(std::string& result) override;
@@ -715,6 +719,7 @@ class OptionsField : public Widget {
options_t options_;
size_t selected_index_{0};
bool centered_{false}; // e.g.: length as screen_width/8, x position as 0, it will be centered in x axis
bool is_activated = false;
};
// A TextEdit is bound to a string reference and allows the string
@@ -851,11 +856,11 @@ class NumberField : public Widget {
NumberField(Point parent_pos, int length, range_t range, int32_t step, char fill_char, bool can_loop);
NumberField(Point parent_pos, int length, range_t range, int32_t step, char fill_char)
: NumberField{parent_pos, length, range, step, fill_char, false} {
: NumberField{parent_pos, length, range, step, fill_char, true} {
}
NumberField()
: NumberField{{0, 0}, 1, {0, 1}, 1, ' ', false} {
: NumberField{{0, 0}, 1, {0, 1}, 1, ' ', true} {
}
NumberField(const NumberField&) = delete;
+1 -1
Submodule hackrf updated: c0b15549cb...714e0dccc0