mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2026-09-15 02:52:58 +00:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9e7758718c | |||
| 89b878d3bb | |||
| 56d965f3ae | |||
| fe9f0c228f | |||
| 6562e596e8 | |||
| c27192bdbb | |||
| 927e3b11e7 | |||
| a44a00acf0 | |||
| 6739b43914 | |||
| b306a0d490 | |||
| c6130c6e8d | |||
| 97f2ca42f1 | |||
| e2b642ae25 | |||
| 106d9b1207 |
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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, ¤t_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"};
|
||||
|
||||
@@ -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, ¤t_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
@@ -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
@@ -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{
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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_;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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__
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
+33
-14
@@ -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;
|
||||
}
|
||||
+8
-2
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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{};
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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__*/
|
||||
|
||||
@@ -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__*/
|
||||
|
||||
@@ -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'};
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user