Compare commits

...

14 Commits

Author SHA1 Message Date
gullradriel 9e7758718c update submodule (#2971) 2026-02-08 20:44:14 +01:00
E.T. 89b878d3bb Better hackrf board detection for the serial info command (#2967) 2026-02-07 15:45:26 +01:00
gullradriel 56d965f3ae update submodule to latest (#2968) 2026-02-06 19:55:53 +01:00
Totoo fe9f0c228f SubCar fm mode (#2965) 2026-02-06 17:20:14 +01:00
Totoo 6562e596e8 OSM change detection 2026-02-06 15:07:07 +01:00
Totoo c27192bdbb Flipper fsk tx support (#2964) 2026-02-06 09:48:20 +01:00
gullradriel 927e3b11e7 Morse rx cleanings (#2962)
* fix ui overlap, align with log box
* bring back enum inside the class as a simple enum
* correctly restore saved setting whatever the modulation was at exit
* set squelch / field_squelch / receiver model in one go, add audio/receiver model stop and start
2026-02-04 14:01:29 +01:00
Pezsma a44a00acf0 Morse rx further development (#2959)
* filters OK, radio configurations set

* all modes are working, noise still needs work, especially in FM. A baseband enum is also needed

* audio and decoding fixed, clipping and the baseband enum are still missing
2026-02-03 22:25:52 +01:00
gullradriel 6739b43914 disable select key repeat in Mic TX (#2956) 2026-02-01 19:44:56 +01:00
gullradriel b306a0d490 Revert "Update Map view labels to match marine navigation standards (#2951)" (#2955)
This reverts commit c6130c6e8d.
2026-01-31 21:15:12 +01:00
Herbenderbler c6130c6e8d Update Map view labels to match marine navigation standards (#2951)
Replace Alt: with CoG: and Spd: with SoG: in the Map view to better align with standard marine navigation terminology and the AIS Rx screen.

- Alt (Altitude) → CoG (Course over Ground)
- Spd (Speed) → SoG (Speed over Ground)

Rationale: AIS is primarily used for marine navigation where altitude is typically zero (sea level), while Course over Ground is essential navigation information. This change provides consistency with the AIS Rx screen which already displays CoG and SoG.

Modified files:
- firmware/application/ui/ui_geomap.hpp
- firmware/standalone/common/ui/ui_geomap.hpp
2026-01-31 20:53:58 +01:00
Herbenderbler 97f2ca42f1 Fix aviation 8.33kHz step size to reduce frequency drift (#2950)
Update aviation radio step size from 8330 Hz to 8333 Hz in freqman_steps
and freqman_steps_short arrays. This better represents the actual 8⅓ kHz
(25000/3 Hz) aviation channel spacing and significantly reduces frequency
drift during dial tuning.
With this fix, stepping 24 times from 123.000 MHz now correctly reaches
123.200 MHz instead of 123.199 MHz.
2026-01-31 19:35:14 +01:00
Pezsma e2b642ae25 New Morse TX app (#2948)
* morse tx
* global button repeat function improvement
2026-01-30 17:18:13 +01:00
gullradriel 106d9b1207 updated submodule (#2947) 2026-01-30 10:14:08 +01:00
51 changed files with 2730 additions and 261 deletions
+5
View File
@@ -118,11 +118,16 @@ set(CSRC
usb_serial_endpoints.c
usb_serial_device_to_host.c
i2c_device_to_host.c
hackrf_core_mini.c
${HACKRF_PATH}/firmware/common/adc.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/common/usb_request.c
${HACKRF_PATH}/firmware/common/usb_standard_request.c
${HACKRF_PATH}/firmware/libopencm3/lib/lpc43xx/scu.c
${HACKRF_PATH}/firmware/common/gpio_lpc.c
${HACKRF_PATH}/firmware/common/platform_detect.c
${CHIBIOS}/os/various/chprintf.c
)
+9
View File
@@ -350,6 +350,12 @@ MicTXView::MicTXView(
&tx_button,
&tx_icon});
// disable key repeat on select
initial_switch_config_ = get_switches_repeat_config();
SwitchesState config = initial_switch_config_;
config[toUType(Switch::Sel)] = false;
set_switches_repeat_config(config);
set_rxbw_options();
set_rxbw_defaults(settings_.loaded());
@@ -640,6 +646,9 @@ MicTXView::MicTXView(
}
MicTXView::~MicTXView() {
// restore select key repeat mode
set_switches_repeat_config(initial_switch_config_);
audio::input::stop();
if (rx_enabled) { // Also turn off both (audio rx if enabled, and disable mic_loop to HP)
rxaudio(false);
+3
View File
@@ -90,6 +90,9 @@ class MicTXView : public View {
sampling_rate /* sampling rate */
};
// structure used to control key repeat
SwitchesState initial_switch_config_{};
enum Mic_Modulation : uint32_t {
MIC_MOD_NFM = 0,
MIC_MOD_WFM = 1,
+15
View File
@@ -373,6 +373,21 @@ void set_moreserx_config(uint8_t mode) {
send_message(&message);
}
void set_morsetx_config(uint8_t mode, uint32_t tone, float fm_delta) {
const MorseTXConfigureMessage message{mode, tone, fm_delta};
send_message(&message);
}
void set_morsetx_key(bool key_down) {
const MorseTXkeyMessage message{key_down};
send_message(&message);
}
void set_bitstream_config(uint32_t deviation, uint8_t mode) {
const StreamTXConfigurationMessage message{deviation, mode};
send_message(&message);
}
static bool baseband_image_running = false;
void run_image(const spi_flash::image_tag_t image_tag) {
+3
View File
@@ -103,9 +103,12 @@ void set_siggen_config(const uint32_t bw, const uint32_t shape, const uint32_t d
void set_spectrum_painter_config(const uint16_t width, const uint16_t height, bool update, int32_t bw);
void set_subghzd_config(uint8_t modulation, uint32_t sampling_rate);
void set_moreserx_config(uint8_t mode);
void set_morsetx_config(uint8_t mode, uint32_t tone, float fm_delta);
void set_morsetx_key(bool key_down);
void set_wefax_config(uint8_t lpm, uint8_t ioc);
void set_noaaapt_config();
void set_flex_config();
void set_bitstream_config(uint32_t deviation, uint8_t mode); // mode 0 for am, 1 for 2fsk
void request_roger_beep();
void request_rssi_beep();
+9 -4
View File
@@ -91,9 +91,9 @@ set(EXTCPPSRC
external/adsbtx/main.cpp
external/adsbtx/ui_adsb_tx.cpp
#morse_tx 768 bytes
external/morse_tx/main.cpp
external/morse_tx/ui_morse.cpp
#morse_tx 768 bytes -- disabled because of the new morse tx app with more functions
#external/morse_tx/main.cpp
#external/morse_tx/ui_morse.cpp
#sstvtx 456 bytes
external/sstvtx/main.cpp
@@ -293,6 +293,10 @@ set(EXTCPPSRC
#morse_radio
external/morse_radio/main.cpp
external/morse_radio/ui_morse_radio.cpp
#morseradiotx
external/morseradiotx/main.cpp
external/morseradiotx/ui_morse_radiotx.cpp
)
set(EXTAPPLIST
@@ -317,7 +321,7 @@ set(EXTAPPLIST
tpmsrx
protoview
adsbtx
morse_tx
#morse_tx
sstvtx
sstvrx
random_password
@@ -366,4 +370,5 @@ set(EXTAPPLIST
siggen
sdusb
morse_radio
morseradiotx
)
+8
View File
@@ -93,6 +93,7 @@ MEMORY
ram_external_app_sdusb (rwx) : org = 0xADF40000, len = 32k
ram_external_app_morse_radio (rwx) : org = 0xADF50000, len = 32k
ram_external_app_waterfall_designer (rwx) : org = 0xADF60000, len = 32k
ram_external_app_morseradiotx (rwx) : org = 0xADF70000, len = 32k
}
SECTIONS
@@ -518,5 +519,12 @@ SECTIONS
KEEP(*(.external_app.app_morse_radio.application_information));
*(*ui*external_app*morse_radio*);
} > ram_external_app_morse_radio
.external_app_morseradiotx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_morseradiotx.application_information));
*(*ui*external_app*morseradiotx*);
} > ram_external_app_morseradiotx
}
+6 -1
View File
@@ -80,7 +80,7 @@ bool FlipperTxView::on_file_changed(std::filesystem::path new_file_path) {
return false;
}
preset = submeta.value().preset;
if (preset != FLIPPER_PRESET_OOK) {
if (preset != FLIPPER_PRESET_OOK && preset != FLIPPER_PRESET_2FSK) {
field_filename.set_text("File: err, not supp. preset");
return false;
}
@@ -108,8 +108,13 @@ void FlipperTxView::stop() {
bool FlipperTxView::start() {
if (filename.empty()) return false;
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
transmitter_model.set_sampling_rate(OOK_SAMPLERATE);
transmitter_model.enable();
button_startstop.set_text(LanguageHelper::currentMessages[LANG_STOP]);
uint8_t mode = 0;
if (preset == FLIPPER_PRESET_2FSK)
mode = 1;
baseband::set_bitstream_config(FM_DEVIATION, mode);
// start thread
replay_thread = std::make_unique<FlipperPlayThread>(
filename,
@@ -28,6 +28,8 @@ using namespace ui;
namespace ui::external_app::flippertx {
#define OOK_SAMPLERATE 2280000U
#define FM_DEVIATION 60000U
class FlipperPlayThread;
class FlipperTxView : public View {
public:
@@ -12,6 +12,13 @@ MorsePracticeView::MorsePracticeView(ui::NavigationView& nav)
&btn_clear,
&console_text,
&field_volume});
initial_switch_config_ = get_switches_repeat_config();
SwitchesState config = initial_switch_config_;
config[toUType(Switch::Sel)] = false;
set_switches_repeat_config(config);
audio::set_rate(audio::Rate::Hz_24000);
btn_tt.on_select = [this](Button&) {
@@ -43,6 +50,7 @@ MorsePracticeView::MorsePracticeView(ui::NavigationView& nav)
}
MorsePracticeView::~MorsePracticeView() {
set_switches_repeat_config(initial_switch_config_);
receiver_model.disable();
baseband::shutdown();
audio::output::stop();
@@ -68,7 +68,7 @@ class MorsePracticeView : public ui::View {
ui::Text txt_last{{UI_POS_X(0), UI_POS_Y(6), UI_POS_MAXWIDTH, UI_POS_HEIGHT(1)}, ""};
ui::Button btn_clear{{UI_POS_X(0), UI_POS_Y_BOTTOM(2), UI_POS_WIDTH(6), UI_POS_HEIGHT(1)}, "CLR"};
ui::Console console_text{{UI_POS_X(0), UI_POS_Y(7), UI_POS_MAXWIDTH, UI_POS_HEIGHT_REMAINING(10)}};
AudioVolumeField field_volume{{UI_POS_X_RIGHT(2), UI_POS_X(0)}};
AudioVolumeField field_volume{{UI_POS_X_RIGHT(2), UI_POS_Y(0)}};
uint8_t last_color_id = 255;
uint8_t color_id = 255;
@@ -77,6 +77,7 @@ class MorsePracticeView : public ui::View {
bool button_touch = false;
bool button_was_selected = false;
bool decode_timeout_calc = false;
SwitchesState initial_switch_config_{};
MessageHandlerRegistration message_handler_framesync{
Message::ID::DisplayFrameSync,
+35 -27
View File
@@ -17,6 +17,7 @@ MorseRadioView::MorseRadioView(ui::NavigationView& nav)
&txt_last,
&txt_speed,
&txt_freq,
&txt_clip,
&options_mode,
&btn_clear,
&console_text,
@@ -33,41 +34,44 @@ MorseRadioView::MorseRadioView(ui::NavigationView& nav)
logger->init_daily_log(logs_dir);
};
audio::output::start();
receiver_model.set_sampling_rate(3072000);
receiver_model.set_baseband_bandwidth(1750000);
receiver_model.enable();
field_squelch.on_change = [this](int32_t v) {
receiver_model.set_squelch_level(v);
};
options_mode.on_change = [this](size_t, int32_t value) {
current_mode = (uint8_t)value;
options_mode.on_change = [this](size_t, int32_t mode) {
audio::output::stop();
receiver_model.disable();
morse_decoder_.resetLearning();
if (current_mode == 0) {
if (mode == MORSE_NFM) {
receiver_model.set_am_configuration(4);
receiver_model.set_modulation(ReceiverModel::Mode::NarrowbandFMAudio);
field_squelch.set_style(Theme::getInstance()->option_active);
field_squelch.set_focusable(true);
receiver_model.set_squelch_level(field_squelch.value());
field_squelch.set_value(receiver_model.squelch_level());
audio::set_rate(audio::Rate::Hz_24000);
} else {
audio::set_rate(audio::Rate::Hz_12000);
receiver_model.set_modulation(ReceiverModel::Mode::AMAudio);
receiver_model.set_am_configuration(current_mode + 7);
receiver_model.set_squelch_level(0);
if (mode == MORSE_AM_CW || mode == MORSE_AM_DSB)
receiver_model.set_am_configuration(7);
else if (mode == MORSE_AM_USB)
receiver_model.set_am_configuration(9);
else // LSB
receiver_model.set_am_configuration(10);
field_squelch.set_style(Theme::getInstance()->fg_dark);
field_squelch.set_focusable(false);
audio::set_rate(audio::Rate::Hz_12000);
}
baseband::set_moreserx_config(current_mode);
};
field_squelch.set_value(receiver_model.squelch_level(), false); // will be sent later, no need to send 2x
field_squelch.on_change = [this](int32_t v) {
if (current_mode == 0)
receiver_model.set_squelch_level(v);
};
options_mode.set_selected_index(current_mode, true);
baseband::set_moreserx_config(mode);
saved_mode = mode;
auto vol = field_volume.value(); // audio volume fix
field_volume.set_value(0);
field_volume.set_value(vol);
audio::output::start();
receiver_model.set_headphone_volume(receiver_model.headphone_volume()); // WM8731 hack.
receiver_model.set_sampling_rate(3072000);
receiver_model.set_baseband_bandwidth(1750000);
receiver_model.enable();
};
options_mode.set_selected_index(saved_mode);
logger = std::make_unique<MorseLogger>();
chk_log.on_select = [this](Checkbox&, bool save) {
@@ -103,21 +107,21 @@ void MorseLogger::init_daily_log(const std::filesystem::path& log_dir) {
}
}
void MorseLogger::radio_set_log(uint8_t current_mode) {
void MorseLogger::radio_set_log(const std::string& morse_mode) {
int64_t freq = receiver_model.target_frequency();
std::string header = "Freq:" + to_string_rounded_freq(freq, 4);
header += "MHz, ";
header += "RX MODE:" + std::string(current_mode == 0 ? "CW/FM" : (current_mode == 1 ? "USB" : "LSB"));
header += "RX MODE:" + morse_mode;
header += "\r\nMessage:";
log_file.write_raw(header);
}
bool MorseLogger::on_packet(const std::string& content, bool time, uint8_t current_mode) {
bool MorseLogger::on_packet(const std::string& content, bool time, const std::string& morse_mode) {
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(current_mode);
radio_set_log(morse_mode);
char_count = 0;
time = true;
}
@@ -152,6 +156,8 @@ MorseRadioView::~MorseRadioView() {
void MorseRadioView::focus() {
field_frequency.focus();
txt_clip.set_style(Theme::getInstance()->fg_red);
txt_clip.hidden(true);
}
void MorseRadioView::check_for_timeout() {
@@ -205,6 +211,8 @@ int32_t MorseRadioView::ProcessSignal(int32_t sig_time_us) {
void MorseRadioView::on_data(const MorseRXDataMessage* message) {
int32_t r;
txt_clip.hidden(!message->clipped);
for (uint8_t i = 0; i <= message->state_cnt; ++i) {
r = ProcessSignal(message->state_durations[i]);
auto result = morse_decoder_.handleInput((r != 0) ? r : 0);
@@ -212,7 +220,7 @@ void MorseRadioView::on_data(const MorseRXDataMessage* message) {
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, current_mode);
time_stamp = logger->on_packet(result.text, time_stamp, options_mode.selected_index_name());
}
float dah_time = morse_decoder_.getCurrentTimeUnit() * 3.0f;
if (dah_time > 0) {
@@ -54,8 +54,8 @@ class MorseLogger {
}
void init_daily_log(const std::filesystem::path& log_dir);
bool on_packet(const std::string& content, bool time, uint8_t current_mode);
void radio_set_log(uint8_t current_mode);
bool on_packet(const std::string& content, bool time, const std::string& morse_mode);
void radio_set_log(const std::string& morse_mode);
private:
LogFile log_file{};
@@ -83,12 +83,21 @@ class MorseRadioView : public ui::View {
RxRadioState radio_state_{};
std::unique_ptr<MorseLogger> logger{};
uint8_t current_mode{0}; // 0=CW/FM, 1=USB, 2=LSB
enum morse_modes : uint8_t {
MORSE_AM_CW = 0,
MORSE_NFM,
MORSE_AM_DSB,
MORSE_AM_USB,
MORSE_AM_LSB,
};
uint8_t saved_mode = MORSE_AM_CW;
app_settings::SettingsManager settings_{
"rx_morese_radio",
"rx_morse_radio",
app_settings::Mode::RX,
{{"cwmode"sv, &current_mode}}};
{
{"cwmode"sv, &saved_mode},
}};
RxFrequencyField field_frequency{
{UI_POS_X(0), UI_POS_Y(0)},
@@ -115,6 +124,7 @@ class MorseRadioView : public ui::View {
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::Text txt_freq{{UI_POS_X(21), UI_POS_Y(2), UI_POS_WIDTH(5), UI_POS_HEIGHT(1)}, "??"};
ui::Text txt_clip{{UI_POS_X(15), UI_POS_Y(3), UI_POS_WIDTH(5), UI_POS_HEIGHT(1)}, "clipping"};
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},
@@ -125,9 +135,15 @@ class MorseRadioView : public ui::View {
{{UI_POS_X(27), UI_POS_Y(2)}, "Hz", Theme::getInstance()->fg_light->foreground}};
ui::OptionsField options_mode{
{UI_POS_X(9), UI_POS_Y(2)},
5,
{{"CW/FM", 0}, {"USB", 1}, {"LSB", 2}}};
{UI_POS_X(8), UI_POS_Y(2) + 4}, // +4 to align with 'Log' checkbox text
6,
{
{"AM/CW", MORSE_AM_CW},
{"NFM", MORSE_NFM},
{"AM/DSB", MORSE_AM_DSB},
{"AM/USB", MORSE_AM_USB},
{"AM/LSB", MORSE_AM_LSB},
}};
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"};
+87
View File
@@ -0,0 +1,87 @@
/*
* 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_radiotx.hpp"
#include "ui_navigation.hpp"
#include "external_app.hpp"
namespace ui::external_app::morseradiotx {
void initialize_app(NavigationView& nav) {
nav.push<MorseRadiotxView>();
}
} // namespace ui::external_app::morseradiotx
extern "C" {
__attribute__((section(".external_app.app_morseradiotx.application_information"), used))
application_information_t _application_information_morseradiotx = {
/*.memory_location = */ (uint8_t*)0x00000000,
/*.externalAppEntry = */ ui::external_app::morseradiotx::initialize_app,
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
/*.app_name = */ "Morse TX",
/*.bitmap_data = */ {
0x00,
0x00,
0xFE,
0x7F,
0xFF,
0xFF,
0xBB,
0xD0,
0xFF,
0xFF,
0xFF,
0xFF,
0x0B,
0xE1,
0xFF,
0xFF,
0xFF,
0xFF,
0xEB,
0xD0,
0xFF,
0xFF,
0xFE,
0x7F,
0x70,
0x00,
0x30,
0x00,
0x10,
0x00,
0x00,
0x00,
},
/*.icon_color = */ ui::Color::yellow().v,
/*.menu_location = */ app_location_t::TX,
/*.desired_menu_position = */ -1,
/*.m4_app_tag = portapack::spi_flash::image_tag_none */ {'P', 'M', 'R', 'T'},
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
};
} // extern "C"
@@ -0,0 +1,389 @@
#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::morseradiotx {
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();
}
const char* get_morse_pattern(char c) {
char upper_c = (c >= 'a' && c <= 'z') ? c - 32 : c;
for (size_t i = 0; i < morse_table_size_; ++i) {
if (morse_table_[i].letter[0] == upper_c) {
return morse_table_[i].code.c_str();
}
}
return nullptr;
}
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::morseradiotx
#endif // __MORSEDECODER_HPP__
@@ -0,0 +1,437 @@
#include "ui_morse_radiotx.hpp"
using namespace portapack;
namespace ui::external_app::morseradiotx {
static msg_t tx_thread_fn(void* arg) {
auto view = reinterpret_cast<MorseRadiotxView*>(arg);
chRegSetThreadName("morse_tx_thread");
view->transmit_morse_message();
return 0;
}
MorseRadiotxView::MorseRadiotxView(ui::NavigationView& nav)
: current_timings(calculate_morse_timings(20)),
nav_(nav) {
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
add_children({&tx_view,
&field_volume,
&labels,
&options_mode,
&tone_,
&wpm_,
&txt_msg,
&btn_message,
&btn_calls,
&chk_trans,
&bandwidth,
&chk_callsgn,
&txt_last,
&console_text,
&btn_clear,
&btn_ptt});
initial_switch_config_ = get_switches_repeat_config();
SwitchesState config = initial_switch_config_;
config[toUType(Switch::Sel)] = false;
set_switches_repeat_config(config);
audio::set_rate(audio::Rate::Hz_24000);
btn_clear.on_select = [this](Button&) {
console_text.clear(true);
txt_last.set("");
};
options_mode.on_change = [this](size_t, int32_t value) {
current_mode = (uint8_t)value;
baseband::set_morsetx_config(current_mode, tone, band);
ui_toggle();
};
tone_.on_change = [this](int32_t v) {
tone = static_cast<uint32_t>(v);
baseband::set_morsetx_config(current_mode, tone, band);
};
wpm_.on_change = [this](int16_t v) {
wpm = v;
current_timings = calculate_morse_timings(wpm);
};
bandwidth.on_change = [this](float v) {
band = v;
baseband::set_morsetx_config(current_mode, tone, band);
};
options_mode.set_selected_index(current_mode, true);
tone_.set_value(tone, true);
wpm_.set_value(wpm, true);
bandwidth.set_value(band, true);
btn_ptt.on_select = [this](Button&) {
if (button_touch) {
button_touch = false;
return;
}
button_was_selected = true;
onPress();
};
btn_ptt.on_touch_press = [this](Button&) {
button_touch = true;
button_was_selected = false;
onPress();
};
btn_ptt.on_touch_release = [this](Button&) {
if (chk_trans.value() && transmit) {
button_touch = true;
button_was_selected = false;
onRelease();
}
};
btn_message.on_select = [this, &nav](Button&) {
if (!transmit)
text_prompt(nav, msg_buffer, 27, ENTER_KEYBOARD_MODE_ALPHA, [this](std::string& buffer) {
msg_buffer = buffer;
txt_msg.set("[" + msg_buffer + "] ");
});
};
btn_calls.on_select = [this, &nav](Button&) {
if (!transmit) {
text_prompt(nav, call_sign, 10, ENTER_KEYBOARD_MODE_ALPHA, [this](std::string& buffer) {
call_sign = buffer;
if (call_sign.empty())
btn_calls.set_text("call sign?");
else
btn_calls.set_text(call_sign);
});
}
};
audio::output::start();
auto vol = field_volume.value();
field_volume.set_value(0);
field_volume.set_value(vol);
tx_view.on_start = [this]() {
if (!chk_trans.value() && msg_buffer.empty()) {
tx_view.set_transmitting(false);
return;
}
tx_view.focus();
if (!tx_thread && !thread_running) {
thread_running = true;
tx_thread = chThdCreateFromHeap(NULL, 1024, NORMALPRIO + 5, tx_thread_fn, this);
tx_view.set_transmitting(true);
}
else
tx_view.set_transmitting(false);
};
tx_view.on_stop = [this]() {
baseband::set_morsetx_key(false);
transmitter_model.disable();
if (tx_thread) {
chThdTerminate(tx_thread);
chThdWait(tx_thread);
}
transmit = false;
transmit_time = 0;
tx_thread = nullptr;
thread_running = false;
tx_view.set_transmitting(false);
ui_toggle();
};
tx_view.on_edit_frequency = [this, &nav]() {
auto new_view = nav.push<FrequencyKeypadView>(transmitter_model.target_frequency());
new_view->on_changed = [this](rf::Frequency f) {
transmitter_model.set_target_frequency(f);
};
};
}
MorseRadiotxView::~MorseRadiotxView() {
set_switches_repeat_config(initial_switch_config_);
if (tx_thread) {
chThdTerminate(tx_thread);
chThdWait(tx_thread);
if (!thread_running) tx_thread = nullptr;
}
transmitter_model.disable();
baseband::shutdown();
audio::output::stop();
}
void MorseRadiotxView::on_show() {
ptt_button_visibility(true);
start_time = 0;
end_time = 0;
transmit_time = 0;
}
void MorseRadiotxView::focus() {
options_mode.focus();
}
MorseRadiotxView::MorseTimings MorseRadiotxView::calculate_morse_timings(uint32_t wpm) {
MorseRadiotxView::MorseTimings t;
if (wpm < 10) wpm = 10;
if (wpm > 45) wpm = 45;
t.dot_ms = 1200 / wpm;
t.dash_ms = 3 * t.dot_ms;
t.symbol_gap = t.dot_ms;
t.char_gap = 3 * t.dot_ms;
t.word_gap = 7 * t.dot_ms;
return t;
}
void MorseRadiotxView::transmit_morse_message() {
std::string full_message = "";
// cal sign
if (chk_callsgn.value() && !call_sign.empty()) {
full_message = call_sign;
if (!chk_trans.value() && !msg_buffer.empty()) {
full_message += " " + msg_buffer;
}
} else {
if (!chk_trans.value()) full_message = msg_buffer;
}
if (full_message.empty() && !chk_trans.value()) {
ptt_button_visibility(false);
return;
}
// enable transmit
if (!transmit) {
transmit = true;
transmitter_model.enable();
ui_toggle();
}
for (size_t i = 0; i < full_message.length(); i++) {
if (chThdShouldTerminate()) break;
char c = full_message[i];
std::string s_char(1, c);
// space
if (c == ' ') {
console_text.write(" ");
chThdSleepMilliseconds(current_timings.word_gap);
continue;
}
const char* pattern = morse_decoder_.get_morse_pattern(c);
if (pattern != nullptr) {
txt_last.set(pattern);
// write blue char to console
console_text.write(STR_COLOR_BLUE + s_char);
// Morze (dih/dah) send
for (int j = 0; pattern[j] != '\0'; j++) {
baseband::set_morsetx_key(true);
if (pattern[j] == '.') {
chThdSleepMilliseconds(current_timings.dot_ms);
} else if (pattern[j] == '-') {
chThdSleepMilliseconds(current_timings.dash_ms);
}
if (chThdShouldTerminate()) break;
// pause between signs
baseband::set_morsetx_key(false);
chThdSleepMilliseconds(current_timings.symbol_gap);
}
if (chThdShouldTerminate()) break;
if (current_timings.char_gap > current_timings.symbol_gap) {
chThdSleepMilliseconds(current_timings.char_gap - current_timings.symbol_gap);
}
}
}
if (chk_trans.value()) ptt_button_visibility(false);
baseband::set_morsetx_key(false);
transmit_time = chTimeNow();
decode_timeout_calc = false;
thread_running = false;
}
void MorseRadiotxView::onPress() {
start_time = chTimeNow();
if (!transmit) {
transmit = true;
transmitter_model.enable();
}
baseband::set_morsetx_key(true);
if (end_time != 0) {
int64_t gap_delta = (chTimeNow() - end_time);
auto result = morse_decoder_.handleInput(-gap_delta);
if (result.isValid()) {
writeCharToConsole(result.text, result.confidence);
}
}
end_time = 0;
transmit_time = 0;
decode_timeout_calc = false;
}
void MorseRadiotxView::onRelease() {
end_time = chTimeNow();
transmit_time = end_time;
baseband::set_morsetx_key(false);
if (start_time != 0) {
int32_t press_delta = (end_time - start_time);
auto result = morse_decoder_.handleInput(press_delta);
if (result.isValid()) {
writeCharToConsole(result.text, result.confidence);
}
}
start_time = 0;
decode_timeout_calc = true;
baseband::request_beep_stop();
}
void MorseRadiotxView::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 == 1.5)
color_id = 4;
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);
}
void MorseRadiotxView::ptt_button_visibility(bool hidden) {
bool hiddenorig = btn_ptt.hidden();
if (hiddenorig != hidden) {
btn_ptt.hidden(hidden);
if (!hidden) btn_ptt.focus();
if (hidden) {
// hack fix until widget hidig problem is not solved.
auto r = btn_ptt.screen_rect();
Painter p;
p.fill_rectangle_unrolled8(r, Theme::getInstance()->fg_light->background);
}
}
}
void MorseRadiotxView::ui_toggle() {
// 0=AM, 1=FM, 2=DSB, 3=USB, 4=LSB
if (transmit) {
options_mode.set_style(Theme::getInstance()->fg_dark);
options_mode.set_focusable(false);
tone_.set_style(Theme::getInstance()->fg_dark);
tone_.set_focusable(false);
wpm_.set_style(Theme::getInstance()->fg_dark);
wpm_.set_focusable(false);
btn_message.set_style(Theme::getInstance()->fg_dark);
btn_calls.set_style(Theme::getInstance()->fg_dark);
chk_trans.set_style(Theme::getInstance()->fg_dark);
chk_trans.set_focusable(false);
bandwidth.set_style(Theme::getInstance()->fg_dark);
bandwidth.set_focusable(false);
chk_callsgn.set_style(Theme::getInstance()->fg_dark);
chk_callsgn.set_focusable(false);
} else {
options_mode.set_style(Theme::getInstance()->bg_darker);
options_mode.set_focusable(true);
wpm_.set_style(Theme::getInstance()->bg_darker);
wpm_.set_focusable(true);
btn_message.set_style(Theme::getInstance()->bg_darker);
btn_calls.set_style(Theme::getInstance()->bg_darker);
chk_trans.set_style(Theme::getInstance()->bg_darker);
chk_trans.set_focusable(true);
chk_callsgn.set_style(Theme::getInstance()->bg_darker);
chk_callsgn.set_focusable(true);
ptt_button_visibility(true);
tone_.set_style(Theme::getInstance()->bg_darker);
tone_.set_focusable(true);
if (current_mode == 1) { // FM mode
bandwidth.set_style(Theme::getInstance()->bg_darker);
bandwidth.set_focusable(true);
} else {
bandwidth.set_style(Theme::getInstance()->fg_dark);
bandwidth.set_focusable(false);
}
} // transmit false
}
inline bool MorseRadiotxView::tx_button_held() {
const auto switches_state = get_switches_state();
return switches_state[(size_t)ui::KeyEvent::Select];
}
void MorseRadiotxView::on_framesync() {
if (button_was_selected && !button_touch && !tx_button_held()) {
button_was_selected = false;
onRelease();
}
if (end_time != 0 && decode_timeout_calc) {
int64_t gap_delta = (chTimeNow() - end_time);
if (gap_delta >= morse_decoder_.getInterCharThreshold()) {
auto result = morse_decoder_.handleInput(-(int32_t)gap_delta);
if (result.isValid()) {
writeCharToConsole(result.text, result.confidence);
}
}
if (gap_delta >= morse_decoder_.getInterWordThreshold()) {
writeCharToConsole(" ", 1.0);
end_time = 0;
decode_timeout_calc = false;
}
}
if (transmit_time != 0 && transmit) { // Tx disable if time is up
int64_t gap_delta = (chTimeNow() - transmit_time);
if (gap_delta >= ((morse_decoder_.getInterWordThreshold() * ((chk_trans.value()) ? 10 : 1)))) {
if (tx_thread) {
chThdTerminate(tx_thread);
chThdWait(tx_thread);
}
transmit = false;
tx_view.set_transmitting(false);
transmitter_model.disable();
thread_running = false;
tx_thread = nullptr;
transmit_time = 0;
if (!chk_trans.value()) writeCharToConsole(" ", 1.0);
ui_toggle();
}
}
}
} // namespace ui::external_app::morseradiotx
@@ -0,0 +1,171 @@
/*
* 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_RADIOTX_H__
#define __MORSE_RADIOTX_H__
#include "ui.hpp"
#include "ui_widget.hpp"
#include "ui_navigation.hpp"
#include "ui_language.hpp"
#include "ui_painter.hpp"
#include "ui_freq_field.hpp"
#include "ui_transmitter.hpp"
#include "ui_textentry.hpp"
#include "string_format.hpp"
#include "morsedecoder.hpp"
#include "irq_controls.hpp"
#include "radio_state.hpp"
#include "portapack.hpp"
#include "message.hpp"
#include "volume.hpp"
#include "audio.hpp"
#include "baseband_api.hpp"
#include "external_app.hpp"
#include "string_format.hpp"
#include <ch.h>
#include <hal.h>
namespace ui::external_app::morseradiotx {
class MorseRadiotxView : public ui::View {
public:
MorseRadiotxView(ui::NavigationView& nav);
~MorseRadiotxView();
MorseRadiotxView(const MorseRadiotxView&) = delete;
MorseRadiotxView(MorseRadiotxView&&) = delete;
MorseRadiotxView& operator=(const MorseRadiotxView&) = delete;
MorseRadiotxView& operator=(MorseRadiotxView&&) = delete;
std::string title() const override { return "Morse Tx"; }
void focus() override;
void on_show() override;
void transmit_morse_message();
private:
struct MorseTimings {
uint32_t dot_ms;
uint32_t dash_ms;
uint32_t symbol_gap;
uint32_t char_gap;
uint32_t word_gap;
};
MorseTimings current_timings{};
std::string msg_indicator{""};
MorseTimings calculate_morse_timings(uint32_t wpm);
void onPress();
void onRelease();
void on_framesync();
void writeCharToConsole(const std::string& ch, double confidence);
void ui_toggle();
bool tx_button_held();
void ptt_button_visibility(bool hidden);
ui::NavigationView& nav_;
MorseDecoder morse_decoder_{};
TxRadioState radio_state_{};
Thread* tx_thread{nullptr};
std::string msg_buffer{"PORTAPACK"};
uint8_t current_mode{0}; // 0=AM, 1=FM, 2=DSB, 3=USB, 4=LSB
uint8_t wpm{20};
uint32_t tone{700};
float band{5.8};
std::string call_sign{""};
app_settings::SettingsManager settings_{
"tx_morseradio",
app_settings::Mode::TX,
{
{"cmode"sv, &current_mode},
{"audtone"sv, &tone},
{"wpm"sv, &wpm},
{"message"sv, &msg_buffer},
{"bandwith"sv, &band},
{"call_sign"sv, &call_sign},
}};
TransmitterView tx_view{
(int16_t)UI_POS_Y_BOTTOM(4),
10000,
0, false};
AudioVolumeField field_volume{{UI_POS_X_RIGHT(2), UI_POS_Y_BOTTOM(5)}};
ui::OptionsField options_mode{
{UI_POS_X(5), UI_POS_Y(0)},
3,
{{"AM", 0}, {"FM", 1}, {"DSB", 2}, {"USB", 3}, {"LSB", 4}}};
NumberField tone_{{UI_POS_X(14), UI_POS_Y(0)}, 4, {400, 1400}, 10, ' ', true};
NumberField wpm_{{UI_POS_X(25), UI_POS_Y(0)}, 2, {10, 45}, 1, ' ', true};
FloatField bandwidth{{UI_POS_X(20), UI_POS_Y(3)}, 4, {1.0, 16.0}, 0.1, ' ', true, 1};
ui::Text txt_msg{{UI_POS_X(0), UI_POS_Y(1), UI_POS_MAXWIDTH, UI_POS_HEIGHT(1)}, "[" + msg_buffer + "] "};
ui::Button btn_message{{UI_POS_X(0), UI_POS_Y(2), UI_POS_WIDTH(11), UI_POS_HEIGHT(1)}, "Message"};
ui::Button btn_calls{{UI_POS_X(0), UI_POS_Y(4), UI_POS_WIDTH(11), UI_POS_HEIGHT(1)}, (call_sign.empty()) ? "call sign?" : call_sign};
Checkbox chk_trans{{UI_POS_X(14), UI_POS_Y(2)}, 13, "Manual trans.", true};
Checkbox chk_callsgn{{UI_POS_X(14), UI_POS_Y(4)}, 13, "Call sign", true};
ui::Text txt_last{{UI_POS_X(10), UI_POS_Y(5), UI_POS_WIDTH_REMAINING(10), UI_POS_HEIGHT(1)}, ""};
ui::Console console_text{{UI_POS_X(0), UI_POS_Y(7), UI_POS_MAXWIDTH, UI_POS_HEIGHT_REMAINING(14)}};
ui::Button btn_clear{{UI_POS_X(0), UI_POS_Y_BOTTOM(5), UI_POS_WIDTH(5), UI_POS_HEIGHT(1)}, "CLR"};
ui::Button btn_ptt{{UI_POS_X_CENTER(12), UI_POS_Y_BOTTOM(7), UI_POS_WIDTH(12), UI_POS_HEIGHT(3)}, "PTT"};
ui::Labels labels{
{{UI_POS_X(0), UI_POS_Y(0)}, "Mode:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(9), UI_POS_Y(0)}, "Tone:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(18), UI_POS_Y(0)}, "Hz", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(21), UI_POS_Y(0)}, "WPM:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(14), UI_POS_Y(3)}, "BandW:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(24), UI_POS_Y(3)}, "kHz", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(5)}, "Last seq:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(6)}, "Sent Message:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X_RIGHT(7), UI_POS_Y_BOTTOM(5)}, "Vol.:", Theme::getInstance()->fg_light->foreground},
};
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};
bool button_touch{false};
bool button_was_selected{false};
bool decode_timeout_calc{false};
bool transmit{false};
bool thread_running = false;
uint8_t send_indicator{5};
int64_t start_time{0};
int64_t end_time{0};
int64_t transmit_time{0};
SwitchesState initial_switch_config_{};
MessageHandlerRegistration message_handler_framesync{
Message::ID::DisplayFrameSync,
[this](const Message* const p) {
(void)p;
this->on_framesync();
}};
};
} // namespace ui::external_app::morseradiotx
#endif // __MORSE_RADIOTX_H__
+18 -2
View File
@@ -91,6 +91,7 @@ SubCarView::SubCarView(NavigationView& nav)
&button_clear_list,
&check_log,
&labels,
&options_mode,
&recent_entries_view});
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
@@ -114,11 +115,17 @@ SubCarView::SubCarView(NavigationView& nav)
recent_entries_view.on_select = [this](const SubCarRecentEntry& entry) {
nav_.push<SubCarRecentEntryDetailView>(entry);
};
baseband::set_subghzd_config(0, receiver_model.sampling_rate()); // 0=am
receiver_model.enable();
options_mode.on_change = [this](size_t, int32_t v) {
modulation = v;
chThdSleepMilliseconds(100); // wait for the baseband thread to process the previous config, to avoid glitchy output when switching modes
baseband::set_subghzd_config(modulation, receiver_model.sampling_rate());
};
signal_token_tick_second = rtc_time::signal_tick_second += [this]() {
on_tick_second();
};
options_mode.set_selected_index(modulation, true);
receiver_model.enable();
}
void SubCarView::on_tick_second() {
@@ -176,6 +183,8 @@ const char* SubCarView::getSensorTypeName(FPROTO_SUBCAR_SENSOR type) {
return "Fiat V0";
case FPC_BMWV0:
return "BMW V0";
/* case FPC_KIAV6:
return "Kia V6";*/
case FPC_Invalid:
default:
@@ -491,6 +500,13 @@ void SubCarRecentEntryDetailView::parseProtocol() {
btn = to_string_dec_uint(button);
}
/*if (entry_.sensorType == FPC_KIAV6) {
// not decrypted!
serial = 0;
btn = "?";
cnt = 0;
}*/
return;
}
+19 -13
View File
@@ -116,25 +116,27 @@ class SubCarView : public View {
4'000'000 /* sampling rate */,
ReceiverModel::Mode::AMAudio};
bool logging = false;
uint8_t modulation = 0;
app_settings::SettingsManager settings_{
"rx_subcar",
app_settings::Mode::RX,
{
{"log"sv, &logging},
{"modulationmode"sv, &modulation},
}};
SubCarRecentEntries recent{};
RFAmpField field_rf_amp{
{13 * 8, UI_POS_Y(0)}};
{UI_POS_X(13), UI_POS_Y(0)}};
LNAGainField field_lna{
{15 * 8, UI_POS_Y(0)}};
{UI_POS_X(15), UI_POS_Y(0)}};
VGAGainField field_vga{
{18 * 8, UI_POS_Y(0)}};
{UI_POS_X(18), UI_POS_Y(0)}};
RSSI rssi{
{21 * 8, 0, UI_POS_WIDTH_REMAINING(24), 4}};
{UI_POS_X(21), 0, UI_POS_WIDTH_REMAINING(24), 4}};
Channel channel{
{21 * 8, 5, UI_POS_WIDTH_REMAINING(24), 4},
{UI_POS_X(21), 5, UI_POS_WIDTH_REMAINING(24), 4},
};
RxFrequencyField field_frequency{
{UI_POS_X(0), UI_POS_Y(0)},
@@ -143,18 +145,22 @@ class SubCarView : public View {
SignalToken signal_token_tick_second{};
Button button_clear_list{
{0, 16, 7 * 8, 32},
{UI_POS_X(0), UI_POS_Y(1), UI_POS_WIDTH(7), UI_POS_HEIGHT(2)},
"Clear"};
Checkbox check_log{
{10 * 8, 18},
{UI_POS_X(8), UI_POS_Y(1)},
3,
"Log",
true};
Labels labels{
{{UI_POS_X_RIGHT(14), UI_POS_Y(1)}, "no fm yet :(", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(15), UI_POS_Y(1)}, "Mode:", Theme::getInstance()->fg_light->foreground},
};
ui::OptionsField options_mode{
{UI_POS_X(22), UI_POS_Y(1)},
3,
{{"AM", 0}, {"FM", 1}}};
static constexpr auto header_height = 3 * 16;
@@ -198,16 +204,16 @@ class SubCarRecentEntryDetailView : public View {
std::string btn = "";
uint32_t cnt = SD_NO_CNT;
Text text_type{{UI_POS_X(0), 1 * 16, 15 * 8, 16}, "?"};
Text text_id{{6 * 8, 2 * 16, 10 * 8, 16}, "?"};
Text text_type{{UI_POS_X(0), UI_POS_Y(1), UI_POS_WIDTH(15), UI_POS_HEIGHT(1)}, "?"};
Text text_id{{UI_POS_X(6), UI_POS_Y(2), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)}, "?"};
Console console{
{0, 4 * 16, screen_width, screen_height - (4 * 16) - 36}};
{UI_POS_X(0), UI_POS_Y(4), UI_POS_MAXWIDTH, screen_height - (4 * 16) - 36}};
Labels labels{
{{UI_POS_X(0), UI_POS_Y(0)}, "Type:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), 2 * 16}, "Serial: ", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), 3 * 16}, "Data:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(2)}, "Serial: ", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(3)}, "Data:", Theme::getInstance()->fg_light->foreground},
};
Button button_done{
+2 -2
View File
@@ -122,7 +122,7 @@ options_t freqman_steps = {
{"1kHz ", 1000},
{"5kHz (SA AM)", 5000},
{"6.25kHz(NFM)", 6250},
{"8.33kHz(AIR)", 8330},
{"8.33kHz(AIR)", 8333},
{"9kHz (EU AM)", 9000},
{"10kHz(US AM)", 10000},
{"12.5kHz(NFM)", 12500},
@@ -144,7 +144,7 @@ options_t freqman_steps_short = {
{"1kHz", 1000},
{"5kHz", 5000},
{"6.25kHz", 6250},
{"8.33kHz", 8330},
{"8.33kHz", 8333},
{"9kHz", 9000},
{"10kHz", 10000},
{"12.5kHz", 12500},
+56
View File
@@ -0,0 +1,56 @@
#include "gpio_lpc.h"
typedef enum {
LED1 = 0,
LED2 = 1,
LED3 = 2,
LED4 = 3,
} led_t;
/* GPIO Output PinMux */
static struct gpio_t gpio_led[] = {
GPIO(2, 1),
GPIO(2, 2),
GPIO(2, 8),
#ifdef RAD1O
GPIO(5, 26),
#endif
};
void delay(uint32_t duration) {
while (duration--) {
/* cannot be optimized out */
__asm__ volatile("nop");
}
}
void delay_us_at_mhz(uint32_t us, uint32_t mhz) {
/* overflow-safe multiply */
uint64_t cycles64 = (uint64_t)us * (uint64_t)mhz;
if (cycles64 > UINT32_MAX) {
cycles64 = UINT32_MAX;
}
delay((uint32_t)cycles64);
}
void led_on(const led_t led) {
gpio_set(&gpio_led[led]);
}
void led_off(const led_t led) {
gpio_clear(&gpio_led[led]);
}
void halt_and_flash(const uint32_t duration) {
/* blink LED1, LED2, and LED3 */
while (1) {
led_on(LED1);
led_on(LED2);
led_on(LED3);
delay(duration);
led_off(LED1);
led_off(LED2);
led_off(LED3);
delay(duration);
}
}
+8
View File
@@ -37,6 +37,14 @@ void Debounce::enable_repeat() {
repeat_enabled_ = true;
}
void Debounce::set_enable_repeat(bool enabled) {
repeat_enabled_ = enabled;
}
bool Debounce::get_repeat_enabled() {
return repeat_enabled_;
}
bool Debounce::get_long_press_enabled() const {
return long_press_enabled_;
}
+2
View File
@@ -43,6 +43,8 @@ class Debounce {
bool feed(const uint8_t bit);
uint8_t state();
void enable_repeat();
void set_enable_repeat(bool enabled);
bool get_repeat_enabled();
bool get_long_press_enabled() const;
void set_long_press_enabled(bool v);
bool long_press_occurred();
+16
View File
@@ -250,6 +250,22 @@ SwitchesState get_switches_state() {
return result;
}
/* Gets the repeat enabled state for all the switches. */
SwitchesState get_switches_repeat_config() {
SwitchesState result;
for (size_t i = 0; i < result.size(); i++)
result[i] = switch_debounce[i].get_repeat_enabled();
return result;
}
/* Configures which switches support repeat.*/
void set_switches_repeat_config(SwitchesState switch_config) {
for (size_t i = 0; i < switch_config.size(); i++)
switch_debounce[i].set_enable_repeat(switch_config[i]);
}
/* Gets the long press enabled state for all the switches. */
SwitchesState get_switches_long_press_config() {
SwitchesState result;
+2
View File
@@ -48,6 +48,8 @@ uint8_t swizzled_switches();
SwitchesState get_switches_state();
EncoderPosition get_encoder_position();
touch::Frame get_touch_frame();
SwitchesState get_switches_repeat_config();
void set_switches_repeat_config(SwitchesState switch_config);
SwitchesState get_switches_long_press_config();
void set_switches_long_press_config(SwitchesState switch_config);
+43 -1
View File
@@ -32,6 +32,10 @@
#include "chprintf.h"
#include "portapack.hpp"
extern "C" {
#include "platform_detect.h"
}
/**
* @brief Shell termination event source.
*/
@@ -65,8 +69,42 @@ static void list_commands(BaseSequentialStream* chp, const ShellCommand* scp) {
}
}
static const char* get_board_revision_string(board_rev_t rev) {
switch (rev) {
case BOARD_REV_HACKRF1_OLD:
return "HackRF R1-R5";
case BOARD_REV_HACKRF1_R6:
return "HackRF R6";
case BOARD_REV_HACKRF1_R7:
return "HackRF R7";
case BOARD_REV_HACKRF1_R8:
return "HackRF R8";
case BOARD_REV_HACKRF1_R9:
return "HackRF R9";
case BOARD_REV_HACKRF1_R10:
return "HackRF R10";
case BOARD_REV_GSG_HACKRF1_R6:
return "GSG HackRF R6";
case BOARD_REV_GSG_HACKRF1_R7:
return "GSG HackRF R7";
case BOARD_REV_GSG_HACKRF1_R8:
return "GSG HackRF R8";
case BOARD_REV_GSG_HACKRF1_R9:
return "GSG HackRF R9";
case BOARD_REV_GSG_HACKRF1_R10:
return "GSG HackRF R10";
case BOARD_REV_UNRECOGNIZED:
return "Unrecognized";
case BOARD_REV_UNDETECTED:
return "Undetected";
default:
return "Unknown";
}
}
static void cmd_info(BaseSequentialStream* chp, int argc, char* argv[]) {
(void)argv;
if (argc > 0) {
usage(chp, const_cast<char*>("info"));
return;
@@ -92,7 +130,11 @@ static void cmd_info(BaseSequentialStream* chp, int argc, char* argv[]) {
#ifdef VERSION_STRING
chprintf(chp, "Mayhem Version: %s\r\n", VERSION_STRING);
#endif
chprintf(chp, "HackRF Board Rev: %s\r\n", hackrf_r9 ? "R9" : "R1-R8");
detect_hardware_platform();
board_rev_t revision = detected_revision();
const char* revision_string = get_board_revision_string(revision);
chprintf(chp, "HackRF Board Rev: %s\r\n", revision_string);
chprintf(chp, "Reference Source: %s\r\n", portapack::clock_manager.get_source().c_str());
chprintf(chp, "Reference Freq: %s\r\n", portapack::clock_manager.get_freq().c_str());
#ifdef __DATE__
+7 -1
View File
@@ -657,7 +657,13 @@ void GeoMap::move(const float lon, const float lat) {
} else {
if (is_changed) {
set_osm_max_zoom();
redraw_map = true;
double global_center_px = lon_to_pixel_x_tile(lon_, map_osm_real_zoom);
double global_center_py = lat_to_pixel_y_tile(lat_, map_osm_real_zoom);
// Redraw if viewport moved by at least 1 pixel (includes zoom level changes)
if (abs(global_center_px - (r.width() / 2.0) - viewport_top_left_px) >= 1.0 ||
abs(global_center_py - (r.height() / 2.0) - viewport_top_left_py) >= 1.0) {
redraw_map = true;
}
}
}
}
+8 -2
View File
@@ -26,6 +26,7 @@
#include "string_format.hpp"
#include "ui_receiver.hpp"
#include "ui_freqman.hpp"
#include "audio.hpp"
using namespace portapack;
@@ -591,8 +592,13 @@ AudioVolumeField::AudioVolumeField(
/* fill char */ ' '} {
set_value(receiver_model.normalized_headphone_volume());
on_change = [](int32_t v) {
receiver_model.set_normalized_headphone_volume(v);
on_change = [](int32_t vol) {
// don't call receiver model, because this widget shuld be able to handle volume settinsg from any app, regardless of the receiver model's enables state. like the tx apps should be able to set volume too.
// this is identical to the receiver model's method
uint8_t v = clip<uint8_t>(vol, 0, 99);
auto new_volume = volume_t::decibel(v - 99) + audio::headphone::volume_range().max;
persistent_memory::set_headphone_volume(new_volume);
audio::headphone::set_volume(new_volume);
};
}
+13 -4
View File
@@ -618,12 +618,12 @@ set(MODE_CPPSRC
)
DeclareTargets(PADT adsbtx)
### OOKStream
### Binary Timed stream TX
set(MODE_CPPSRC
proc_ook_stream_tx.cpp
proc_bint_stream_tx.cpp
)
DeclareTargets(POSK ookstream)
DeclareTargets(POSK bintstream)
### Signal generator
@@ -663,9 +663,10 @@ set(MODE_CPPSRC
)
DeclareTargets(PSCD subcar)
### Morse RX Decoder
set(MODE_CPPSRC
proc_morse.cpp
)
DeclareTargets(PMRS morse)
@@ -676,6 +677,14 @@ set(MODE_INCDIR
${HACKRF_PATH}/firmware/common
${HACKRF_PATH}/firmware/libopencm3/include
)
### Morse TX
set(MODE_CPPSRC
proc_morsetx.cpp
)
DeclareTargets(PMRT morsetx)
set(MODE_CPPSRC
sd_over_usb/proc_sd_over_usb.cpp
+204
View File
@@ -0,0 +1,204 @@
#pragma once
#include "subcarbase.hpp"
#include <cstring>
typedef enum {
KiaV6DecoderStepReset = 0,
KiaV6DecoderStepWaitFirstHigh,
KiaV6DecoderStepCountPreamble,
KiaV6DecoderStepWaitLongHigh,
KiaV6DecoderStepData,
} KiaV6DecoderStep;
#define KIA_V6_XOR_MASK_LOW 0x84AF25FB
#define KIA_V6_XOR_MASK_HIGH 0x638766AB
class FProtoSubCarKiaV6 : public FProtoSubCarBase {
public:
FProtoSubCarKiaV6() {
sensorType = FPC_KIAV6;
te_short = 200;
te_long = 400;
te_delta = 100;
min_count_bit_for_found = 144;
}
void feed(bool level, uint32_t duration) {
uint32_t uVar4, uVar5;
ManchesterEvent event;
bool data_bit;
uint8_t bit_count_inc;
uint32_t step_value;
switch (parser_step) {
case KiaV6DecoderStepReset: // case 0
if (level == 0) {
return;
}
if (DURATION_DIFF(duration, te_short) <
te_delta) {
parser_step = KiaV6DecoderStepWaitFirstHigh;
te_last = duration;
header_count = 0;
FProtoGeneral::manchester_advance(
manchester_state,
ManchesterEventReset,
&manchester_state,
NULL);
}
return;
case KiaV6DecoderStepWaitFirstHigh: { // case 1
if (level != 0) {
return;
}
uint32_t diff_short = DURATION_DIFF(duration, te_short);
uint32_t diff_long = DURATION_DIFF(duration, te_long);
uint32_t diff = (diff_long < diff_short) ? diff_long : diff_short;
if (diff_long < te_delta && diff_long < diff_short) {
if (header_count >= 0x259) { // 601 decimal
header_count = 0;
te_last = duration;
parser_step = KiaV6DecoderStepWaitLongHigh;
return;
}
}
if (diff >= te_delta) {
step_value = KiaV6DecoderStepReset;
goto LAB_reset;
}
if (DURATION_DIFF(te_last, te_short) <
te_delta) {
te_last = duration;
header_count++;
return;
} else {
step_value = KiaV6DecoderStepReset;
goto LAB_reset;
}
}
case KiaV6DecoderStepWaitLongHigh: { // case 2
if (level == 0) {
step_value = KiaV6DecoderStepReset;
goto LAB_reset;
}
uint32_t diff_long_check = DURATION_DIFF(duration, te_long);
uint32_t diff_short_check = DURATION_DIFF(duration, te_short);
if (diff_long_check >= te_delta) {
if (diff_short_check >= te_delta) {
step_value = KiaV6DecoderStepReset;
goto LAB_reset;
}
}
if (DURATION_DIFF(te_last, te_long) >=
te_delta) {
step_value = KiaV6DecoderStepReset;
goto LAB_reset;
}
decode_data = 0;
decode_count_bit = 0;
subghz_protocol_blocks_add_bit(1);
subghz_protocol_blocks_add_bit(1);
subghz_protocol_blocks_add_bit(0);
subghz_protocol_blocks_add_bit(1);
data_part1_low = (uint32_t)(decode_data & 0xFFFFFFFF);
data_part1_high = (uint32_t)((decode_data >> 32) & 0xFFFFFFFF);
bit_count = decode_count_bit;
parser_step = KiaV6DecoderStepData;
return;
}
case KiaV6DecoderStepData: // case 3
if (DURATION_DIFF(duration, te_short) <
te_delta) {
event = (ManchesterEvent)((level & 0x7F) << 1);
goto manchester_process;
} else if (
DURATION_DIFF(duration, te_long) <
te_delta) {
event = (ManchesterEvent)(level ? 6 : 4);
goto manchester_process;
}
step_value = KiaV6DecoderStepReset;
goto LAB_reset;
manchester_process:
if (FProtoGeneral::manchester_advance(
manchester_state, event, &manchester_state, &data_bit)) {
uVar4 = data_part1_low;
uVar5 = (uVar4 << 1) | (data_bit ? 1 : 0);
uint32_t carry = (uVar4 >> 31) & 1;
uVar4 = (data_part1_high << 1) | carry;
data_part1_low = uVar5;
data_part1_high = uVar4;
decode_data = ((uint64_t)uVar4 << 32) | uVar5;
bit_count_inc = bit_count + 1;
bit_count = bit_count_inc;
if (bit_count_inc == 0x40) {
// stored_part1_low = ~uVar5;
// stored_part1_high = ~uVar4;
data_part1_low = 0;
data_part1_high = 0;
} else if (bit_count_inc == 0x80) {
// stored_part2_low = ~uVar5;
// stored_part2_high = ~uVar4;
data_part1_low = 0;
data_part1_high = 0;
}
}
te_last = duration;
if (bit_count != min_count_bit_for_found) {
return;
}
data_count_bit = min_count_bit_for_found;
// data_part3 = ~((uint16_t)data_part1_low);
// kia_v6_decrypt(); --won't
decode_data = data_part1_low | ((uint64_t)data_part1_high << 32);
if (callback) {
callback(this);
}
data_part1_low = 0;
data_part1_high = 0;
bit_count = 0;
step_value = KiaV6DecoderStepReset;
goto LAB_reset;
default:
return;
}
LAB_reset:
parser_step = step_value;
return;
}
uint8_t bit_count = 0;
uint16_t header_count = 0;
ManchesterState manchester_state = ManchesterStateMid1;
uint32_t data_part1_low = 0;
uint32_t data_part1_high = 0;
// uint32_t stored_part1_low = 0;
// uint32_t stored_part1_high = 0;
// uint32_t stored_part2_low = 0;
// uint32_t stored_part2_high = 0;
// uint16_t data_part3 = 0;
};
+1 -1
View File
@@ -76,7 +76,7 @@ class FProtoSubGhzDSecPlusV1 : public FProtoSubGhzDBase {
packet_accepted |= SECPLUS_V1_PACKET_2_ACCEPTED;
if (packet_accepted == (SECPLUS_V1_PACKET_1_ACCEPTED | SECPLUS_V1_PACKET_2_ACCEPTED)) {
// subghz_protocol_secplus_v1_decode(); // disabled doe to lack of flash
// subghz_protocol_secplus_v1_decode(); // disabled due to lack of flash
// controller
// uint32_t fixed = (data >> 32) & 0xFFFFFFFF;
// cnt = data & 0xFFFFFFFF;
@@ -21,6 +21,7 @@ So include here the .hpp, and add a new element to the protos vector in the cons
#include "c-ford_v0.hpp"
#include "c-fiat_v0.hpp"
#include "c-bmw_v0.hpp"
// #include "c-kia_v6.hpp"
#ifndef __FPROTO_PROTOLISTCAR_H__
#define __FPROTO_PROTOLISTCAR_H__
@@ -42,6 +43,7 @@ class SubCarProtos : public FProtoListGeneral {
protos[FPC_FORDV0] = new FProtoSubCarFordV0();
protos[FPC_FIATV0] = new FProtoSubCarFiatV0();
protos[FPC_BMWV0] = new FProtoSubCarBMWV0();
// protos[FPC_KIAV6] = new FProtoSubCarKiaV6(); //-- disabled, due to whole encrypted.
for (uint8_t i = 0; i < FPC_COUNT; ++i) {
if (protos[i] != NULL) protos[i]->setCallback(callbackTarget);
+2 -3
View File
@@ -8,9 +8,6 @@ These values must be present on the protocol's constructor, like FProtoWeatherAc
Also it must have a switch-case element in the getSubGhzDSensorTypeName() function, to display it's name.
*/
#define FPM_AM 0
#define FPM_FM 1
enum FPROTO_SUBCAR_SENSOR : uint8_t {
FPC_Invalid = 0,
FPC_SUZUKI = 1,
@@ -24,6 +21,8 @@ enum FPROTO_SUBCAR_SENSOR : uint8_t {
FPC_FORDV0 = 9,
FPC_FIATV0 = 10,
FPC_BMWV0 = 11,
// FPC_KIAV6 = 12, //disabled, due to whole encrypted.
// FPC_PSA = 13, // IS whole encrypted
FPC_COUNT
};
@@ -19,7 +19,7 @@
* Boston, MA 02110-1301, USA.
*/
#include "proc_ook_stream_tx.hpp"
#include "proc_bint_stream_tx.hpp"
#include "sine_table_int8.hpp"
#include "portapack_shared_memory.hpp"
@@ -27,27 +27,35 @@
#include "utility.hpp"
OOKProcessorStreamed::OOKProcessorStreamed() {
BinaryTimedProcessorStreamed::BinaryTimedProcessorStreamed() {
configured = false;
baseband_thread.start();
}
inline void OOKProcessorStreamed::write_sample(const buffer_c8_t& buffer, bool bit_value, size_t i) {
int8_t re, im;
if (bit_value) {
phase = (phase + 200); // What ?
inline void BinaryTimedProcessorStreamed::write_sample(const buffer_c8_t& buffer, bool bit_value, size_t i) {
int8_t re = 0, im = 0;
if (mode == 0) {
if (bit_value) {
phase = (phase + 200);
sphase = phase + (64 << 18);
re = (sine_table_i8[(sphase & 0x03FC0000) >> 18]);
im = (sine_table_i8[(phase & 0x03FC0000) >> 18]);
}
} else if (mode == 1) {
// calculate the re, im based on the deviation uint32_t variable to get the re, im, to send out 2fsk signal. based on the bit_value
if (bit_value) {
phase += deviation_delta;
} else {
phase -= deviation_delta;
}
sphase = phase + (64 << 18);
re = (sine_table_i8[(sphase & 0x03FC0000) >> 18]);
im = (sine_table_i8[(phase & 0x03FC0000) >> 18]);
} else {
re = 0;
im = 0;
}
buffer.p[i] = {re, im};
}
void OOKProcessorStreamed::execute(const buffer_c8_t& buffer) {
void BinaryTimedProcessorStreamed::execute(const buffer_c8_t& buffer) {
if (!configured || !stream) return;
for (size_t i = 0; i < buffer.count; i++) {
@@ -87,7 +95,7 @@ void OOKProcessorStreamed::execute(const buffer_c8_t& buffer) {
}
}
void OOKProcessorStreamed::on_message(const Message* const message) {
void BinaryTimedProcessorStreamed::on_message(const Message* const message) {
switch (message->id) {
case Message::ID::ReplayConfig:
configured = false;
@@ -100,12 +108,23 @@ void OOKProcessorStreamed::on_message(const Message* const message) {
shared_memory.application_queue.push(txprogress_message);
break;
case Message::ID::StreamTXConfiguration:
streamtx_config(*reinterpret_cast<const StreamTXConfigurationMessage*>(message));
break;
default:
break;
}
}
void OOKProcessorStreamed::replay_config(const ReplayConfigMessage& message) {
void BinaryTimedProcessorStreamed::streamtx_config(const StreamTXConfigurationMessage& message) {
mode = message.mode;
deviation = message.deviation;
uint64_t big_calc = (uint64_t)message.deviation << 26;
deviation_delta = (uint32_t)(big_calc / OOK_SAMPLERATE);
}
void BinaryTimedProcessorStreamed::replay_config(const ReplayConfigMessage& message) {
if (message.config) {
txprogress_message.progress = -2;
shared_memory.application_queue.push(txprogress_message);
@@ -121,7 +140,7 @@ void OOKProcessorStreamed::replay_config(const ReplayConfigMessage& message) {
}
int main() {
EventDispatcher event_dispatcher{std::make_unique<OOKProcessorStreamed>()};
EventDispatcher event_dispatcher{std::make_unique<BinaryTimedProcessorStreamed>()};
event_dispatcher.run();
return 0;
}
@@ -34,10 +34,11 @@
#include <memory>
#define OOK_SAMPLERATE 2280000U
#define FM_DEVIATION 60000U
class OOKProcessorStreamed : public BasebandProcessor {
class BinaryTimedProcessorStreamed : public BasebandProcessor {
public:
OOKProcessorStreamed();
BinaryTimedProcessorStreamed();
void execute(const buffer_c8_t& buffer) override;
void on_message(const Message* const message) override;
@@ -53,6 +54,11 @@ class OOKProcessorStreamed : public BasebandProcessor {
std::unique_ptr<StreamOutput> stream{};
bool configured{false};
void replay_config(const ReplayConfigMessage& message);
void streamtx_config(const StreamTXConfigurationMessage& message);
uint8_t mode = 0; // am = 0, 2fsk = 1
uint32_t deviation = FM_DEVIATION; // used in 2fsk
uint32_t deviation_delta = (FM_DEVIATION * 4294967296ULL) / OOK_SAMPLERATE;
int32_t endsignals[3] = {0, 42069, 613379}; // 0 is skipped, count from 1, don't ask...
uint8_t readerrs = 0; // to count in the array
+59 -34
View File
@@ -28,26 +28,32 @@
void MorseProcessor::configure(uint8_t mode) {
configured = false;
if (mode == 0) { // CW/FM
modulation = static_cast<ModulationMode>((uint8_t)mode);
if (modulation == ModulationMode::FM) { // FM
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_cw_fm.configure(24000, 5000);
} else { // USB, LSB
decim_0.configure(taps_6k0_decim_0.taps);
decim_1.configure(taps_6k0_decim_1.taps);
if (mode == 1) // USB
channel_filter.configure(taps_2k8_usb_channel.taps, 4);
else // LSB
channel_filter.configure(taps_2k8_lsb_channel.taps, 4);
} else {
decim_0.configure(taps_4k25_decim_0.taps);
decim_1.configure(taps_4k25_decim_1.taps);
if (modulation == ModulationMode::AM) { // AM
channel_filter.configure(taps_2k0_am_lpf_channel.taps, 4);
} else { // SSB, DSB
if (modulation == ModulationMode::DSB) // DSB
channel_filter.configure(taps_1k5_dsb_lpf.taps, 4);
else if (modulation == ModulationMode::USB) // USB
channel_filter.configure(taps_1k5_USB_channel.taps, 4);
else // LSB
channel_filter.configure(taps_1k5_LSB_channel.taps, 4);
}
}
modulation = mode;
if (mode > 0)
audio_output.configure(audio_12k_hpf_300hz_config);
else
if (modulation == ModulationMode::FM)
audio_output.configure(iir_config_passthrough, iir_config_passthrough, (float)user_squelch_level / 100.0f);
else
audio_output.configure(audio_12k_hpf_300hz_config);
meas_samples_in_period = 0;
meas_last_period_len = 0;
@@ -70,6 +76,8 @@ void MorseProcessor::configure(uint8_t mode) {
squelch_is_open = true;
squelch_hold = 0;
dc_average = 0.0f;
current_freq = 700.0f;
update_goertzel_coeff(current_freq);
@@ -77,10 +85,15 @@ void MorseProcessor::configure(uint8_t mode) {
}
inline buffer_f32_t MorseProcessor::demodulate(const buffer_c16_t& channel) {
if (modulation > 0) {
// SSB always keeps squelch "technically" open for the demodulator
// AM,SSB,DSB always keeps squelch "technically" open for the demodulator
if (modulation == ModulationMode::AM || modulation == ModulationMode::DSB) {
squelch_is_open = true;
return demod_ssb.execute(channel, audio_buffer);
return demod_AM.execute(channel, audio_buffer);
} else {
if (modulation == ModulationMode::USB || modulation == ModulationMode::LSB) {
squelch_is_open = true;
return demod_ssb.execute(channel, audio_buffer);
}
}
return demod_cw_fm.execute(channel, audio_buffer);
}
@@ -90,7 +103,7 @@ void MorseProcessor::update_goertzel_coeff(float freq) {
if (freq < 300.0f) freq = 300.0f;
if (freq > 2300.0f) freq = 2300.0f;
float sample_rate = (modulation == 0) ? 24000.0f : 12000.0f;
float sample_rate = (modulation == ModulationMode::FM) ? 24000.0f : 12000.0f;
float omega = 2.0f * M_PI * freq / sample_rate;
float omega_sq = omega * omega;
float cos_approx = 1.0f - (omega_sq * 0.5f);
@@ -100,7 +113,7 @@ void MorseProcessor::update_goertzel_coeff(float freq) {
void MorseProcessor::measure_frequency(int32_t sample) {
// Noise gate threshold
const int32_t gate_threshold = (modulation == 0) ? 4000 : 2000;
const int32_t gate_threshold = (modulation == ModulationMode::FM) ? 4000 : 2000;
if (sample > gate_threshold || sample < -gate_threshold) {
if (sample > 0 && !meas_signal_state_high) {
@@ -111,7 +124,7 @@ void MorseProcessor::measure_frequency(int32_t sample) {
bool period_is_stable = false;
if (meas_last_period_len > 0) {
int32_t diff = std::abs((int)meas_samples_in_period - (int)meas_last_period_len);
if (diff <= 1) {
if (diff <= 2) {
meas_consistency_count++;
period_is_stable = true;
} else {
@@ -121,10 +134,12 @@ void MorseProcessor::measure_frequency(int32_t sample) {
meas_last_period_len = meas_samples_in_period;
// Wait for AT LEAST 3 STABLE CYCLES
if (period_is_stable && meas_consistency_count > 3) {
float base_rate = (modulation == 0) ? 24000.0f : 12000.0f;
if (period_is_stable && meas_consistency_count > 5) {
float base_rate = (modulation == ModulationMode::FM) ? 24000.0f : 12000.0f;
float inst_freq = base_rate / (float)meas_samples_in_period;
if (modulation == ModulationMode::DSB) {
inst_freq /= 2.0f;
}
// Check for overflows
if (inst_freq > 250 && inst_freq < 3000) {
meas_freq_accumulator += inst_freq;
@@ -148,7 +163,7 @@ void MorseProcessor::measure_frequency(int32_t sample) {
meas_samples_in_period++;
ui_update_timer++;
uint32_t update_limit = (modulation == 0) ? 4800 : 2400; // ~200ms
uint32_t update_limit = (modulation == ModulationMode::FM) ? 4800 : 2400; // ~200ms
if (ui_update_timer > update_limit) {
if (meas_freq_count > 0) {
@@ -202,7 +217,7 @@ void MorseProcessor::process_decoding(int32_t sample) {
// Tone Detection
bool is_tone = squelch_is_open && (power > current_pwr_threshold) && (power > 150000);
int32_t time_base = modulation == 0 ? 125 : 250;
int32_t time_base = 250;
// State Change Logic
if (is_tone != was_signaling) {
int32_t duration_us = (int32_t)((int64_t)duration_samples * time_base / 3);
@@ -254,33 +269,43 @@ void MorseProcessor::execute(const buffer_c8_t& buffer) {
if (raw_int_abs > audio_threshold || user_squelch_level == 0) {
squelch_is_open = true;
squelch_hold = (modulation == 0) ? 2400 : 1200;
squelch_hold = (modulation == ModulationMode::FM) ? 2400 : 1200;
} else {
if (squelch_hold > 0)
squelch_hold--;
else if (modulation == 0)
else if (modulation == ModulationMode::FM)
squelch_is_open = false;
}
measure_frequency((int32_t)(raw_audio * 32768.0f));
float decode_audio = raw_audio;
if (modulation > 0) {
const float gain = 6.0f;
if (modulation != ModulationMode::FM) {
float gain = 16.0f;
if (modulation == ModulationMode::USB || modulation == ModulationMode::LSB)
gain = 5.0f;
decode_audio *= gain;
// Hard Limiting / Clipping
if (decode_audio > 1.0f)
message.clipped = false;
if (decode_audio > 1.0f) {
decode_audio = 1.0f;
else if (decode_audio < -1.0f)
message.clipped = true;
} else if (decode_audio < -1.0f) {
decode_audio = -1.0f;
}
audio_buf.p[i] = decode_audio;
}
// DC BLOCKING
if (modulation != ModulationMode::FM) {
dc_average = (dc_average * 0.95f) + (decode_audio * 0.05f);
measure_frequency((int32_t)((decode_audio - dc_average) * 32768.0f));
} else {
measure_frequency((int32_t)(raw_audio * 32768.0f));
}
process_decoding((int32_t)(decode_audio * 32768.0f));
if (modulation == 0 && !squelch_is_open) {
if (modulation == ModulationMode::FM && !squelch_is_open) {
audio_buf.p[i] = 0.0f; // mute nfm on squelch
}
}
+13 -2
View File
@@ -60,13 +60,22 @@ class MorseProcessor : public BasebandProcessor {
dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0{};
dsp::decimate::FIRC16xR16x32Decim8 decim_1{};
dsp::decimate::FIRAndDecimateComplex channel_filter{};
dsp::demodulate::AM demod_AM{};
dsp::demodulate::FM demod_cw_fm{};
dsp::demodulate::SSB demod_ssb{};
AudioOutput audio_output{};
bool configured{false};
uint8_t modulation{0}; // 0=CW/FM, 1=USB, 2=LSB
enum class ModulationMode : uint8_t {
AM = 0,
FM = 1,
DSB = 2,
USB = 3,
LSB = 4
};
ModulationMode modulation = ModulationMode::AM;
int32_t user_squelch_level{0};
bool squelch_is_open{true};
int32_t squelch_hold{0};
@@ -79,7 +88,6 @@ class MorseProcessor : public BasebandProcessor {
float meas_freq_accumulator{0.0f};
uint32_t meas_freq_count{0};
uint32_t ui_update_timer{0};
float current_freq{700.0f};
// --- Decoding variables (Goertzel) ---
@@ -92,6 +100,9 @@ class MorseProcessor : public BasebandProcessor {
int64_t noise_floor{5000};
int32_t startup_delay{20};
float dc_average = 0.0f; // DC level tracking
int32_t dc_average_int = 0;
MorseRXDataMessage message{};
MorseRXfreqMessage freq_message{};
};
+97
View File
@@ -0,0 +1,97 @@
#include "proc_morsetx.hpp"
#include "portapack_shared_memory.hpp"
#include "sine_table_int8.hpp"
#include "event_m4.hpp"
#include <cstdint>
void MorseTXProcessor::execute(const buffer_c8_t& buffer) {
if (!configured) return;
for (size_t i = 0; i < buffer.count; i++) {
int8_t sample_sin;
int8_t sample_cos;
sample_sin = (sine_table_i8[(tone_phase & 0xFF000000) >> 24]);
tone_phase += tone_delta;
im = 0;
re = 0;
// modulation logic
if (modulation == 0) { // AM modulation
if (key_down) {
re = 64 + (sample_sin >> 2);
}
} else if (modulation == 1) { // FM modulation
if (key_down) {
delta = sample_sin * fm_delta;
} else {
delta = 0;
}
phase += delta;
sphase = phase + (64 << 24);
re = (sine_table_i8[(sphase & 0xFF000000) >> 24]);
im = (sine_table_i8[(phase & 0xFF000000) >> 24]);
} else if (modulation == 2) { // DSB
if (key_down) {
re = sample_sin;
}
} else if (modulation == 3) { // USB
if (key_down) {
sample_cos = (sine_table_i8[((tone_phase + 0x40000000) & 0xFF000000) >> 24]);
re = sample_cos;
im = sample_sin;
}
} else if (modulation == 4) { // LSB
if (key_down) {
sample_cos = (sine_table_i8[((tone_phase + 0x40000000) & 0xFF000000) >> 24]);
re = sample_cos;
im = (sine_table_i8[((tone_phase + 0x80000000) & 0xFF000000) >> 24]);
}
}
buffer.p[i] = {re, im};
}
}
void MorseTXProcessor::on_message(const Message* const p) {
switch (p->id) {
case Message::ID::MorseTXConfigure: {
auto message = *reinterpret_cast<const MorseTXConfigureMessage*>(p);
tone_delta = message.tone * 1398; // scale
tone = message.tone; // audio tone
modulation = message.modulation;
if (message.fm_delta == 0 && modulation == 1) {
fm_delta = 90000;
} else {
uint64_t scale = 0xFFFFFFFFULL; // 32-bit max
fm_delta = (uint32_t)((uint64_t)message.fm_delta * (scale / 1536000));
}
break;
}
case Message::ID::MorseTXkey: {
auto key = *reinterpret_cast<const MorseTXkeyMessage*>(p);
key_down = key.key_down;
configured = true;
if (key_down)
audio::dma::beep_start(tone, 24000, 0);
else
audio::dma::beep_stop();
break;
}
default:
break;
}
}
int main() {
audio::dma::init_audio_out();
EventDispatcher event_dispatcher{std::make_unique<MorseTXProcessor>()};
event_dispatcher.run();
return 0;
}
+33
View File
@@ -0,0 +1,33 @@
#ifndef __PROC_MORSETX_H__
#define __PROC_MORSETX_H__
#include "baseband_processor.hpp"
#include "baseband_thread.hpp"
#include "portapack_shared_memory.hpp"
#include "audio_output.hpp"
#include "audio_dma.hpp"
#define AUDIO_OUTPUT_BUFFER_SIZE 32
#define AUDIO_SAMPLING_RATE 24000
class MorseTXProcessor : public BasebandProcessor {
public:
void execute(const buffer_c8_t& buffer) override;
void on_message(const Message* const msg) override;
private:
bool configured{false};
bool key_down{false}; // currently the virtual key is pressed or not.
int8_t sample{0}, re{0}, im{0};
uint8_t modulation{0}; // 0=AM, 1=FM, 2=DSB, 3=USB, 4=LSB
uint32_t tone_delta{0}; // shifting value by tone
uint32_t fm_delta{};
uint32_t tone_phase{0};
uint32_t tone{0}; // audio tone frequeny
int32_t phase{0}, sphase{0}, delta{0}; // sample generation.
BasebandThread baseband_thread{1536000, this, baseband::Direction::Transmit};
};
#endif
+99 -126
View File
@@ -25,14 +25,6 @@
#include "portapack_shared_memory.hpp"
#include "event_m4.hpp"
static inline int get_quadrant(int16_t i, int16_t q) {
if (i >= 0) {
return (q >= 0) ? 0 : 3;
} else {
return (q >= 0) ? 1 : 2;
}
}
void SubCarProcessor::execute(const buffer_c8_t& buffer) {
if (!configured) return;
@@ -46,10 +38,6 @@ void SubCarProcessor::execute(const buffer_c8_t& buffer) {
const auto decim_1_out = decim_1.execute(decim_0_out, dst_buffer); // Input:512 complex/2 (decim factor) = 256_output complex ( 512 I/Q samples)
feed_channel_stats(decim_1_out);
// for fm
const int32_t DC_ALPHA = 5; // Auto-centering speed
int32_t buffer_rotation_sum = 0;
for (size_t i = 0; i < decim_1_out.count; i++) {
// am
threshold = (low_estimate + high_estimate) / 2;
@@ -57,126 +45,103 @@ void SubCarProcessor::execute(const buffer_c8_t& buffer) {
int16_t re = decim_1_out.p[i].real();
int16_t im = decim_1_out.p[i].imag();
uint32_t mag = ((uint32_t)re * (uint32_t)re) + ((uint32_t)im * (uint32_t)im);
mag = (mag >> 10);
int32_t const ook_low_delta = mag - low_estimate;
bool meashl = currentHiLow;
if (sig_state == STATE_IDLE) {
if (mag > (threshold + hysteresis)) { // just become high
meashl = true;
sig_state = STATE_PULSE;
numg = 0;
} else {
meashl = false; // still low
low_estimate += ook_low_delta / OOK_EST_LOW_RATIO;
low_estimate += ((ook_low_delta > 0) ? 1 : -1); // Hack to compensate for lack of fixed-point scaling
// Calculate default OOK high level estimate
high_estimate = 1.35 * low_estimate; // Default is a ratio of low level
high_estimate = std::max(high_estimate, min_high_level);
high_estimate = std::min(high_estimate, (uint32_t)OOK_MAX_HIGH_LEVEL);
}
} else if (sig_state == STATE_PULSE) {
++numg;
if (numg > 100) numg = 100;
if (mag < (threshold - hysteresis)) {
// check if really a bad value
if (numg < 3) {
// susp
sig_state = STATE_GAP;
} else {
if (modulation == 0) {
int32_t const ook_low_delta = mag - low_estimate;
bool meashl = currentHiLow;
if (sig_state == STATE_IDLE) {
if (mag > (threshold + hysteresis)) { // just become high
meashl = true;
sig_state = STATE_PULSE;
numg = 0;
sig_state = STATE_GAP_START;
} else {
meashl = false; // still low
low_estimate += ook_low_delta / OOK_EST_LOW_RATIO;
low_estimate += ((ook_low_delta > 0) ? 1 : -1); // Hack to compensate for lack of fixed-point scaling
// Calculate default OOK high level estimate
high_estimate = 1.35 * low_estimate; // Default is a ratio of low level
high_estimate = std::max(high_estimate, min_high_level);
high_estimate = std::min(high_estimate, (uint32_t)OOK_MAX_HIGH_LEVEL);
}
} else if (sig_state == STATE_PULSE) {
++numg;
if (numg > 100) numg = 100;
if (mag < (threshold - hysteresis)) {
// check if really a bad value
if (numg < 3) {
// susp
sig_state = STATE_GAP;
} else {
numg = 0;
sig_state = STATE_GAP_START;
}
meashl = false; // low
} else {
high_estimate += mag / OOK_EST_HIGH_RATIO - high_estimate / OOK_EST_HIGH_RATIO;
high_estimate = std::max(high_estimate, min_high_level);
high_estimate = std::min(high_estimate, (uint32_t)OOK_MAX_HIGH_LEVEL);
meashl = true; // still high
}
} else if (sig_state == STATE_GAP_START) {
++numg;
if (mag > (threshold + hysteresis)) { // New pulse?
sig_state = STATE_PULSE;
meashl = true;
} else if (numg >= 3) {
sig_state = STATE_GAP;
meashl = false; // gap
}
} else if (sig_state == STATE_GAP) {
++numg;
if (mag > (threshold + hysteresis)) { // New pulse?
numg = 0;
sig_state = STATE_PULSE;
meashl = true;
} else {
meashl = false;
}
}
if (meashl == currentHiLow && currentDuration < 30'000'000) // allow pass 'end' signal
{
currentDuration += nsPerDecSamp;
} else { // called on change, so send the last duration and dir.
if (currentDuration >= 30'000'000) sig_state = STATE_IDLE;
if (protoList) protoList->feed(currentHiLow, currentDuration / 1000);
currentDuration = nsPerDecSamp;
currentHiLow = meashl;
}
}
if (modulation == 1) {
int32_t discrim = ((int32_t)im * fm_state.last_re) - ((int32_t)re * fm_state.last_im);
fm_state.last_re = re;
fm_state.last_im = im;
fm_state.smoothed_discrim += (discrim - fm_state.smoothed_discrim) >> 4;
// --- FM Part (Simple 2-FSK) ---
if (mag > (threshold / 2)) {
const int32_t fm_hysteresis = 2000;
bool new_level = fm_state.current_logic_level;
if (fm_state.smoothed_discrim > fm_hysteresis) {
new_level = true;
} else if (fm_state.smoothed_discrim < -fm_hysteresis) {
new_level = false;
}
if (new_level == fm_state.current_logic_level) {
fm_state.buffer_count++;
} else {
int32_t duration_us = (fm_state.buffer_count * nsPerDecSamp) / 1000;
if (duration_us > 15) {
if (protoList) protoList->feed(fm_state.current_logic_level, duration_us);
}
fm_state.current_logic_level = new_level;
fm_state.buffer_count = 1;
}
meashl = false; // low
} else {
high_estimate += mag / OOK_EST_HIGH_RATIO - high_estimate / OOK_EST_HIGH_RATIO;
high_estimate = std::max(high_estimate, min_high_level);
high_estimate = std::min(high_estimate, (uint32_t)OOK_MAX_HIGH_LEVEL);
meashl = true; // still high
}
} else if (sig_state == STATE_GAP_START) {
++numg;
if (mag > (threshold + hysteresis)) { // New pulse?
sig_state = STATE_PULSE;
meashl = true;
} else if (numg >= 3) {
sig_state = STATE_GAP;
meashl = false; // gap
}
} else if (sig_state == STATE_GAP) {
++numg;
if (mag > (threshold + hysteresis)) { // New pulse?
numg = 0;
sig_state = STATE_PULSE;
meashl = true;
} else {
meashl = false;
fm_state.buffer_count = 0;
}
}
if (meashl == currentHiLow && currentDuration < 30'000'000) // allow pass 'end' signal
{
currentDuration += nsPerDecSamp;
} else { // called on change, so send the last duration and dir.
if (currentDuration >= 30'000'000) sig_state = STATE_IDLE;
if (protoList) protoList->feed(currentHiLow, currentDuration / 1000);
currentDuration = nsPerDecSamp;
currentHiLow = meashl;
}
// fm part: -- NOT WORKING!!!! TODO FIX. AI code ;)
int current_quad = get_quadrant(re, im);
// Calculate Step (Current - Previous)
int diff = current_quad - fm_state.prev_quad;
// Handle Wrap-Around (crossing from Q3 to Q0 or Q0 to Q3)
// 3 -> 0 should be +1 (CCW)
// 0 -> 3 should be -1 (CW)
if (diff == -3)
diff = 1;
else if (diff == 3)
diff = -1;
// Update History
fm_state.prev_quad = current_quad;
// Accumulate Rotation
buffer_rotation_sum += diff;
}
// fm finish:
// 3. AUTO-CENTERING (DC BLOCKER)
// Even with quadrant counting, "drift" (hand effect) makes the wheel spin
// faster or slower. We need to subtract the average speed.
// Update our "Average Speed" estimate
// Note: buffer_rotation_sum is roughly proportional to frequency.
fm_state.dc_offset = (fm_state.dc_offset * ((1 << DC_ALPHA) - 1) + buffer_rotation_sum) >> DC_ALPHA;
// Remove the drift
int32_t centered_rotation = buffer_rotation_sum - fm_state.dc_offset;
// 4. LOW PASS FILTER
const int32_t LPF_ALPHA = 4;
fm_state.smoothed_error = (fm_state.smoothed_error * (LPF_ALPHA - 1) + centered_rotation) / LPF_ALPHA;
// 5. DECISION LOGIC
// Threshold is small now because we are counting quadrant steps.
// Max steps per buffer (256 samples) is 256.
// Typical FSK deviation might give you +/- 10 to 50 steps per buffer.
const int32_t THRESHOLD = 3;
bool new_level = fm_state.current_logic_level;
if (fm_state.smoothed_error > THRESHOLD) {
new_level = true;
} else if (fm_state.smoothed_error < -THRESHOLD) {
new_level = false;
}
// 6. TIMING OUTPUT
if (new_level == fm_state.current_logic_level) {
fm_state.buffer_count++;
} else {
// Output pulse duration
int32_t duration_us = fm_state.buffer_count * 512;
if (duration_us > 250) {
if (protoListFm) protoListFm->feed(fm_state.current_logic_level, duration_us);
}
fm_state.current_logic_level = new_level;
fm_state.buffer_count = 1;
}
}
@@ -189,6 +154,14 @@ void SubCarProcessor::configure(const SubGhzFPRxConfigureMessage& message) {
// constexpr size_t decim_0_output_fs = baseband_fs / decim_0.decimation_factor; //unused
// constexpr size_t decim_1_output_fs = decim_0_output_fs / decim_1.decimation_factor; //unused
if (modulation != message.modulation) {
// reload protos to reset them all
if (protoList) {
delete protoList;
}
protoList = new SubCarProtos();
}
modulation = message.modulation;
baseband_fs = message.sampling_rate;
baseband_thread.set_sampling_rate(baseband_fs);
nsPerDecSamp = 1'000'000'000 / baseband_fs * 8; // Scaled it due to less array buffer sampes due to /8 decimation. 250 nseg (4Mhz) * 8
+8 -8
View File
@@ -56,8 +56,8 @@ class SubCarProcessor : public BasebandProcessor {
uint32_t low_estimate = 100;
uint32_t high_estimate = 12000;
uint32_t min_high_level = 10;
uint8_t numg = 0; // count of matched signals to filter spikes
size_t baseband_fs = 0; // will be set later by configure message
uint8_t numg = 0; // count of matched signals to filter spikes
size_t baseband_fs = 4'000'000; // will be set later by configure message
uint32_t nsPerDecSamp = 0;
/* Array Buffer aux. used in decim0 and decim1 IQ c16 signed data ; (decim0 defines the max length of the array) */
@@ -74,20 +74,20 @@ class SubCarProcessor : public BasebandProcessor {
uint32_t threshold = 0x0630;
bool currentHiLow = false;
bool configured{false};
uint8_t mode = 0; // 0 = am, 1 = fm
// fm part:
struct DemodFMState {
int prev_quad = 0; // Stores 0, 1, 2, or 3
int32_t dc_offset = 0;
int32_t smoothed_error = 0;
bool current_logic_level = false;
uint32_t buffer_count = 0;
int16_t last_re = 0; // Store previous Real sample
int16_t last_im = 0;
int32_t smoothed_discrim = 0;
};
DemodFMState fm_state{};
FProtoListGeneral* protoList = new SubCarProtos(); // holds all the protocols we can parse
FProtoListGeneral* protoListFm = new SubCarProtos(); // holds all the protocols we can parse, but for fm (dupe, bc most of it is dual)
uint8_t modulation = 0; // 0 am, 1 fm
FProtoListGeneral* protoList = new SubCarProtos(); // holds all the protocols we can parse
void configure(const SubGhzFPRxConfigureMessage& message);
/* NB: Threads should be the last members in the class definition. */
+358
View File
@@ -1681,4 +1681,362 @@ static constexpr fir_taps_real<24> taps_BTLE_Dual_PHY = {
3}},
};
static constexpr fir_taps_real<63> taps_2k0_am_lpf_channel = {
.low_frequency_normalized = -2000.0f / 48000.0f,
.high_frequency_normalized = 2000.0f / 48000.0f,
.transition_normalized = 1500.0f / 48000.0f,
.taps = {{
26,
29,
31,
33,
32,
28,
18,
0,
-26,
-61,
-104,
-152,
-202,
-246,
-279,
-292,
-277,
-227,
-136,
0,
182,
409,
673,
968,
1280,
1595,
1899,
2174,
2407,
2583,
2693,
2731,
2693,
2583,
2407,
2174,
1899,
1595,
1280,
968,
673,
409,
182,
0,
-136,
-227,
-277,
-292,
-279,
-246,
-202,
-152,
-104,
-61,
-26,
0,
18,
28,
32,
33,
31,
29,
26,
}},
};
static constexpr fir_taps_real<64> taps_1K5_FM_channel = {
.low_frequency_normalized = 300.0f / 48000.0f,
.high_frequency_normalized = 1800.0f / 48000.0f,
.transition_normalized = 1000.0f / 48000.0f,
.taps = {{
24,
22,
20,
15,
9,
-2,
-18,
-39,
-66,
-98,
-132,
-167,
-198,
-220,
-228,
-217,
-181,
-117,
-19,
112,
277,
474,
697,
941,
1197,
1454,
1702,
1930,
2128,
2285,
2394,
2451,
2451,
2394,
2285,
2128,
1930,
1702,
1454,
1197,
941,
697,
474,
277,
112,
-19,
-117,
-181,
-217,
-228,
-220,
-198,
-167,
-132,
-98,
-66,
-39,
-18,
-2,
9,
15,
20,
22,
24,
}},
};
static constexpr fir_taps_real<63> taps_1k5_dsb_lpf = {
.low_frequency_normalized = -1500.0f / 48000.0f,
.high_frequency_normalized = 1500.0f / 48000.0f,
.transition_normalized = 1000.0f / 48000.0f,
.taps = {{
0,
0,
-1,
-5,
-10,
-19,
-31,
-46,
-64,
-82,
-99,
-111,
-113,
-100,
-64,
0,
99,
239,
424,
655,
932,
1251,
1605,
1983,
2372,
2757,
3120,
3447,
3719,
3925,
4053,
4096,
4053,
3925,
3719,
3447,
3120,
2757,
2372,
1983,
1605,
1251,
932,
655,
424,
239,
99,
0,
-64,
-100,
-113,
-111,
-99,
-82,
-64,
-46,
-31,
-19,
-10,
-5,
-1,
0,
0,
}},
};
// GAIN: 4.0x
static constexpr fir_taps_complex<63> taps_1k5_USB_channel = {
.low_frequency_normalized = 300.0f / 48000.0f,
.high_frequency_normalized = 1800.0f / 48000.0f,
.transition_normalized = 300.0f / 48000.0f,
.taps = {{
{0, 0},
{0, 0},
{2, 0},
{4, 2},
{10, 6},
{17, 14},
{27, 29},
{37, 53},
{45, 89},
{49, 140},
{41, 207},
{17, 290},
{-31, 389},
{-109, 499},
{-227, 614},
{-387, 725},
{-595, 819},
{-850, 884},
{-1147, 904},
{-1477, 866},
{-1827, 757},
{-2179, 569},
{-2512, 297},
{-2804, -55},
{-3031, -480},
{-3173, -962},
{-3213, -1481},
{-3142, -2011},
{-2955, -2524},
{-2658, -2991},
{-2262, -3386},
{-1788, -3685},
{-1258, -3873},
{-703, -3939},
{-153, -3884},
{366, -3713},
{826, -3440},
{1208, -3087},
{1499, -2677},
{1693, -2236},
{1789, -1789},
{1796, -1359},
{1726, -966},
{1594, -624},
{1420, -341},
{1220, -120},
{1012, 40},
{809, 144},
{622, 202},
{460, 223},
{324, 217},
{217, 193},
{137, 160},
{80, 125},
{42, 91},
{19, 62},
{6, 39},
{0, 22},
{-1, 11},
{-1, 5},
{-1, 1},
{0, 0},
{0, 0},
}},
};
// GAIN: 4.0x
static constexpr fir_taps_complex<63> taps_1k5_LSB_channel = {
.low_frequency_normalized = 300.0f / 48000.0f,
.high_frequency_normalized = 1800.0f / 48000.0f,
.transition_normalized = 300.0f / 48000.0f,
.taps = {{
{0, 0},
{0, 0},
{2, 0},
{4, -2},
{10, -6},
{17, -14},
{27, -29},
{37, -53},
{45, -89},
{49, -140},
{41, -207},
{17, -290},
{-31, -389},
{-109, -499},
{-227, -614},
{-387, -725},
{-595, -819},
{-850, -884},
{-1147, -904},
{-1477, -866},
{-1827, -757},
{-2179, -569},
{-2512, -297},
{-2804, 55},
{-3031, 480},
{-3173, 962},
{-3213, 1481},
{-3142, 2011},
{-2955, 2524},
{-2658, 2991},
{-2262, 3386},
{-1788, 3685},
{-1258, 3873},
{-703, 3939},
{-153, 3884},
{366, 3713},
{826, 3440},
{1208, 3087},
{1499, 2677},
{1693, 2236},
{1789, 1789},
{1796, 1359},
{1726, 966},
{1594, 624},
{1420, 341},
{1220, 120},
{1012, -40},
{809, -144},
{622, -202},
{460, -223},
{324, -217},
{217, -193},
{137, -160},
{80, -125},
{42, -91},
{19, -62},
{6, -39},
{0, -22},
{-1, -11},
{-1, -5},
{-1, -1},
{0, 0},
{0, 0},
}},
};
#endif /*__DSP_FIR_TAPS_H__*/
+36
View File
@@ -150,6 +150,9 @@ class Message {
MorseRXfreq = 92,
MorseRXConfig = 93,
TXDisabled = 94,
MorseTXConfigure = 95,
MorseTXkey = 96,
StreamTXConfiguration = 97,
MAX
};
@@ -1709,6 +1712,7 @@ class MorseRXDataMessage : public Message {
: Message{ID::MorseRXData} {}
int32_t state_durations[5] = {0}; // positive: high, negative: low
uint8_t state_cnt = 0;
bool clipped = false;
const uint8_t maxptr = 4;
};
@@ -1734,4 +1738,36 @@ class TXDisabledMessage : public Message {
}
};
class MorseTXConfigureMessage : public Message {
public:
constexpr MorseTXConfigureMessage(uint8_t modulation, uint32_t tone, float fm_delta)
: Message{ID::MorseTXConfigure},
modulation{modulation},
tone{tone},
fm_delta{fm_delta} {}
uint8_t modulation = 0;
uint32_t tone = 0;
float fm_delta = 0;
};
class MorseTXkeyMessage : public Message {
public:
constexpr MorseTXkeyMessage(bool key_down)
: Message{ID::MorseTXkey},
key_down{key_down} {}
bool key_down = false;
};
class StreamTXConfigurationMessage : public Message {
public:
constexpr StreamTXConfigurationMessage(uint32_t deviation, uint8_t mode)
: Message{ID::StreamTXConfiguration},
deviation{deviation},
mode{mode} {}
uint32_t deviation = 60000; // used in 2fsk
uint8_t mode = 0; // am = 0, 2fsk = 1
};
#endif /*__MESSAGE_H__*/
+1
View File
@@ -125,6 +125,7 @@ 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_morsetx{'P', 'M', 'R', 'T'};
constexpr image_tag_t image_tag_noop{'P', 'N', 'O', 'P'};
+131
View File
@@ -2410,6 +2410,137 @@ bool NumberField::on_touch(const TouchEvent event) {
return true;
}
/* FloatField ***********************************************************/
FloatField::FloatField(
Point parent_pos,
int length,
range_t range,
float step,
char fill_char,
bool can_loop,
uint8_t precision_)
: Widget{{parent_pos, {8 * length, 16}}},
range{range},
step{step},
length_{length},
fill_char{fill_char},
can_loop{can_loop},
precision{precision_} {
set_focusable(true);
}
float FloatField::value() const {
return value_;
}
void FloatField::getAccessibilityText(std::string& result) {
result = to_string_decimal(value_, precision);
}
void FloatField::getWidgetName(std::string& result) {
result = "FloatField";
}
void FloatField::set_value(float new_value, bool trigger_change) {
const float lo = range.first;
const float hi = range.second;
if (can_loop) {
if (new_value > hi)
new_value = lo;
else if (new_value < lo)
new_value = hi;
}
new_value = clip(new_value, lo, hi);
// set final value if needed
if (new_value != value_) {
value_ = new_value;
if (on_change && trigger_change) on_change(value_);
set_dirty();
}
}
void FloatField::set_range(const float min, const float max) {
range.first = min;
range.second = max;
set_value(value_, false);
}
void FloatField::set_step(const float new_step) {
step = new_step;
}
void FloatField::paint(Painter& painter) {
auto text = to_string_decimal(value_, precision);
const auto paint_style = has_focus() ? style().invert() : style();
// clip to widget size
const auto r = screen_rect();
auto label_r = style().font.size_of(text);
size_t max_chars = (r.width()) / style().font.char_width();
if (label_r.width() > r.width()) {
text = text.substr(0, max_chars);
}
size_t padneeded = max_chars - text.length();
if (padneeded > 0 && fill_char) {
std::string filler(padneeded, fill_char);
text = filler + text;
}
painter.fill_rectangle(r, style().background);
painter.draw_string(
screen_pos(),
paint_style,
text);
}
bool FloatField::on_key(const KeyEvent key) {
if (key == KeyEvent::Select) {
if (on_select) {
on_select(*this);
return true;
} else {
return on_encoder(1);
}
}
return false;
}
bool FloatField::on_encoder(const EncoderEvent delta) {
float old_value = value_;
set_value(value() + (delta * step));
if (on_wrap) {
if ((delta > 0) && (value_ < old_value))
on_wrap(1);
else if ((delta < 0) && (value_ > old_value))
on_wrap(-1);
}
return true;
}
bool FloatField::on_keyboard(const KeyboardEvent key) {
if (key == 10) {
if (on_select) {
on_select(*this);
return true;
}
}
if (key == '+' || key == ' ') {
return on_encoder(1);
}
if (key == '-' || key == 8) {
return on_encoder(-1);
}
return false;
}
bool FloatField::on_touch(const TouchEvent event) {
if (event.type == TouchEvent::Type::Start) {
focus();
}
return true;
}
/* SymField **************************************************************/
SymField::SymField(
+47
View File
@@ -891,6 +891,53 @@ class NumberField : public Widget {
bool can_loop{};
};
class FloatField : public Widget {
public:
std::function<void(FloatField&)> on_select{};
std::function<void(float)> on_change{};
std::function<void(int32_t)> on_wrap{};
using range_t = std::pair<float, float>;
FloatField(Point parent_pos, int length, range_t range, float step, char fill_char, bool can_loop, uint8_t precision_ = 1);
FloatField(Point parent_pos, int length, range_t range, float step, char fill_char)
: FloatField{parent_pos, length, range, step, fill_char, true, 1} {
}
FloatField()
: FloatField{{0, 0}, 1, {0, 1}, 1, ' ', true, 1} {
}
FloatField(const FloatField&) = delete;
FloatField(FloatField&&) = delete;
float value() const;
void set_value(float new_value, bool trigger_change = true);
void set_range(const float min, const float max);
void set_step(const float new_step);
void set_precision(uint8_t precision);
void paint(Painter& painter) override;
bool on_key(const KeyEvent key) override;
bool on_encoder(const EncoderEvent delta) override;
bool on_touch(const TouchEvent event) override;
bool on_keyboard(const KeyboardEvent event) override;
void getAccessibilityText(std::string& result) override;
void getWidgetName(std::string& result) override;
private:
range_t range;
float step;
const int length_;
const char fill_char;
float value_{0};
bool can_loop{};
uint8_t precision = 1;
};
/* A widget that allows for character-by-character editing of its value. */
class SymField : public Widget {
public:
+7 -1
View File
@@ -650,7 +650,13 @@ void GeoMap::move(const float lon, const float lat) {
} else {
if (is_changed) {
set_osm_max_zoom();
redraw_map = true;
double global_center_px = lon_to_pixel_x_tile(lon_, map_osm_zoom);
double global_center_py = lat_to_pixel_y_tile(lat_, map_osm_zoom);
// Redraw if viewport moved by at least 1 pixel (includes zoom level changes)
if (abs(global_center_px - (r.width() / 2.0) - viewport_top_left_px) >= 1.0 ||
abs(global_center_py - (r.height() / 2.0) - viewport_top_left_py) >= 1.0) {
redraw_map = true;
}
}
}
}
+131
View File
@@ -2338,6 +2338,137 @@ bool NumberField::on_touch(const TouchEvent event) {
return true;
}
/* FloatField ***********************************************************/
FloatField::FloatField(
Point parent_pos,
int length,
range_t range,
float step,
char fill_char,
bool can_loop,
uint8_t precision_)
: Widget{{parent_pos, {8 * length, 16}}},
range{range},
step{step},
length_{length},
fill_char{fill_char},
can_loop{can_loop},
precision{precision_} {
set_focusable(true);
}
float FloatField::value() const {
return value_;
}
void FloatField::getAccessibilityText(std::string& result) {
result = to_string_decimal(value_, precision);
}
void FloatField::getWidgetName(std::string& result) {
result = "FloatField";
}
void FloatField::set_value(float new_value, bool trigger_change) {
const float lo = range.first;
const float hi = range.second;
if (can_loop) {
if (new_value > hi)
new_value = lo;
else if (new_value < lo)
new_value = hi;
}
new_value = clip(new_value, lo, hi);
// set final value if needed
if (new_value != value_) {
value_ = new_value;
if (on_change && trigger_change) on_change(value_);
set_dirty();
}
}
void FloatField::set_range(const float min, const float max) {
range.first = min;
range.second = max;
set_value(value_, false);
}
void FloatField::set_step(const float new_step) {
step = new_step;
}
void FloatField::paint(Painter& painter) {
auto text = to_string_decimal(value_, precision);
const auto paint_style = has_focus() ? style().invert() : style();
// clip to widget size
const auto r = screen_rect();
auto label_r = style().font.size_of(text);
size_t max_chars = (r.width()) / style().font.char_width();
if (label_r.width() > r.width()) {
text = text.substr(0, max_chars);
}
size_t padneeded = max_chars - text.length();
if (padneeded > 0 && fill_char) {
std::string filler(fill_char, padneeded);
text = filler + text;
}
painter.fill_rectangle(r, style().background);
painter.draw_string(
screen_pos(),
paint_style,
text);
}
bool FloatField::on_key(const KeyEvent key) {
if (key == KeyEvent::Select) {
if (on_select) {
on_select(*this);
return true;
} else {
return on_encoder(1);
}
}
return false;
}
bool FloatField::on_encoder(const EncoderEvent delta) {
float old_value = value();
set_value(old_value + ((float)delta * step));
if (on_wrap) {
if ((delta > 0) && (value_ < old_value))
on_wrap(1);
else if ((delta < 0) && (value_ > old_value))
on_wrap(-1);
}
return true;
}
bool FloatField::on_keyboard(const KeyboardEvent key) {
if (key == 10) {
if (on_select) {
on_select(*this);
return true;
}
}
if (key == '+' || key == ' ') {
return on_encoder(1);
}
if (key == '-' || key == 8) {
return on_encoder(-1);
}
return false;
}
bool FloatField::on_touch(const TouchEvent event) {
if (event.type == TouchEvent::Type::Start) {
focus();
}
return true;
}
/* SymField **************************************************************/
SymField::SymField(
@@ -885,6 +885,53 @@ class NumberField : public Widget {
bool can_loop{};
};
class FloatField : public Widget {
public:
std::function<void(FloatField&)> on_select{};
std::function<void(float)> on_change{};
std::function<void(int32_t)> on_wrap{};
using range_t = std::pair<float, float>;
FloatField(Point parent_pos, int length, range_t range, float step, char fill_char, bool can_loop, uint8_t precision_ = 1);
FloatField(Point parent_pos, int length, range_t range, float step, char fill_char)
: FloatField{parent_pos, length, range, step, fill_char, true, 1} {
}
FloatField()
: FloatField{{0, 0}, 1, {0, 1}, 1, ' ', true, 1} {
}
FloatField(const FloatField&) = delete;
FloatField(FloatField&&) = delete;
float value() const;
void set_value(float new_value, bool trigger_change = true);
void set_range(const float min, const float max);
void set_step(const float new_step);
void set_precision(uint8_t precision);
void paint(Painter& painter) override;
bool on_key(const KeyEvent key) override;
bool on_encoder(const EncoderEvent delta) override;
bool on_touch(const TouchEvent event) override;
bool on_keyboard(const KeyboardEvent event) override;
void getAccessibilityText(std::string& result) override;
void getWidgetName(std::string& result) override;
private:
range_t range;
float step;
const int length_;
const char fill_char;
float value_{0};
bool can_loop{};
uint8_t precision = 1;
};
/* A widget that allows for character-by-character editing of its value. */
class SymField : public Widget {
public:
+5 -3
View File
@@ -126,9 +126,11 @@ struct is_flags_type {
template <typename TEnum>
constexpr bool is_flags_type_v = is_flags_type<TEnum>::value;
#define ENABLE_FLAGS_OPERATORS(type) \
template <> \
struct is_flags_type<type> { static constexpr bool value = true; };
#define ENABLE_FLAGS_OPERATORS(type) \
template <> \
struct is_flags_type<type> { \
static constexpr bool value = true; \
};
template <typename TEnum>
constexpr std::enable_if_t<is_flags_type_v<TEnum>, TEnum> operator|(TEnum a, TEnum b) {
+1 -1
Submodule hackrf updated: d4fbc32928...40f441900a