Compare commits

..

18 Commits

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

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

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

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

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

Modified files:
- firmware/application/ui/ui_geomap.hpp
- firmware/standalone/common/ui/ui_geomap.hpp
2026-01-31 20:53:58 +01:00
Herbenderbler 97f2ca42f1 Fix aviation 8.33kHz step size to reduce frequency drift (#2950)
Update aviation radio step size from 8330 Hz to 8333 Hz in freqman_steps
and freqman_steps_short arrays. This better represents the actual 8⅓ kHz
(25000/3 Hz) aviation channel spacing and significantly reduces frequency
drift during dial tuning.
With this fix, stepping 24 times from 123.000 MHz now correctly reaches
123.200 MHz instead of 123.199 MHz.
2026-01-31 19:35:14 +01:00
Pezsma e2b642ae25 New Morse TX app (#2948)
* morse tx
* global button repeat function improvement
2026-01-30 17:18:13 +01:00
gullradriel 106d9b1207 updated submodule (#2947) 2026-01-30 10:14:08 +01:00
gullradriel c8ae419e0b updated submodule (#2946) 2026-01-30 00:21:36 +01:00
Totoo 2319dcd0d6 Aprs tx gps added, and save config added, path added (#2937) 2026-01-25 10:01:33 +01:00
lifegame1lu111 44d9ce00cd Holtek HT6P20B support for SubGhzD (#2941) 2026-01-25 09:41:06 +01:00
Pezsma 5cc5f8faa6 Morserx added ssb modes (#2934)
* USB receive good
* LSB receive good
* all mode receive good, log is dead
* frequency measurement good, decode dead
* total killed
* szutyok, gerjed
* frequenty meas only.
* ui ok
* Add frequency measurement handling and update Morse processing logic
* fix ssb speed
2026-01-22 18:49:21 +01:00
gullradriel 3a0ca480b0 conform to clang spec (#2925)
* conform to clang spec
* proper find call
* Update format-code.sh
2026-01-22 15:26:31 +01:00
Totoo 7a2b432bfa Dyn flash size (#2933) 2026-01-22 15:24:42 +01:00
78 changed files with 3476 additions and 503 deletions
+6 -7
View File
@@ -1,14 +1,13 @@
---
BasedOnStyle: Chromium
IndentWidth: '4'
SortIncludes: 'false'
AllowAllArgumentsOnNextLine: 'true'
IndentWidth: 4
SortIncludes: false
AllowAllArgumentsOnNextLine: true
ColumnLimit: 0
Cpp11BracedListStyle: 'true'
AllowShortLoopsOnASingleLine: 'true'
AllowShortIfStatementsOnASingleLine: 'true'
Cpp11BracedListStyle: true
AllowShortLoopsOnASingleLine: true
AllowShortIfStatementsOnASingleLine: true
SpacesInLineCommentPrefix:
Minimum: 1
Maximum: 1
...
+1
View File
@@ -83,3 +83,4 @@ venv/
# generated bitmap arr file
# TODO: generate bitmap during build, since we use python during build anyway, lemme know if this is a bad idea @zxkmm
/firmware/tools/bitmap.hpp
firmware/flashsize.h
+21
View File
@@ -27,6 +27,27 @@ set(CMAKE_COLOR_MAKEFILE ON)
add_compile_options(-fdiagnostics-color=always)
#Flash size related options
#This is the size of the flash memory we SUPPORT. Don't worry, the internl flash app has harder limits based on device type.
if(NOT DEFINED FLASH_MB_SIZE)
set(FLASH_MB_SIZE 2) # Default to 2MB if not provided. For PortaRf this should be 2MB passed to cmake with -DFLASH_MB_SIZE=2
endif()
message("Configuring for FLASH_MB_SIZE=${FLASH_MB_SIZE}MB")
#This is the flash memory size we build. This can be less, so the FW size will be less, so can be flashed with older utils.
if(NOT DEFINED FLASH_MB_LIMIT_SIZE)
set(FLASH_MB_LIMIT_SIZE 1) #TODO: should be ${FLASH_MB_SIZE} remove once we got a stable build for all devices we support with bigger than 1 mb flash
endif()
message("Configuring for FLASH_MB_LIMIT_SIZE=${FLASH_MB_LIMIT_SIZE}MB")
if(FLASH_MB_SIZE LESS FLASH_MB_LIMIT_SIZE)
message(FATAL_ERROR "Error: The supported flash size is lower than the generated flash file. Build halted.")
endif()
math(EXPR FLASH_BYTES_SIZE "${FLASH_MB_SIZE} * 1024 * 1024")
math(EXPR FLASH_BYTES_LIMIT_SIZE "${FLASH_MB_LIMIT_SIZE} * 1024 * 1024")
#generate h files
configure_file(flashsize.h.in ${CMAKE_CURRENT_SOURCE_DIR}/firmware/flashsize.h)
option(USE_CCACHE "Enable ccache, please keep an eye on this because sometimes it's not quite stable and generated unusable binary" OFF)
project(portapack-h1)
+1 -1
View File
@@ -56,7 +56,7 @@ add_subdirectory(test)
# NOTE: Dependencies break if the .bin files aren't included in DEPENDS. WTF, CMake?
add_custom_command(
OUTPUT ${FIRMWARE_FILENAME}
COMMAND ${MAKE_SPI_IMAGE} ${application_BINARY_DIR}/application.bin ${baseband_BINARY_DIR}/baseband.img ${FIRMWARE_FILENAME}
COMMAND ${MAKE_SPI_IMAGE} ${application_BINARY_DIR}/application.bin ${baseband_BINARY_DIR}/baseband.img ${FIRMWARE_FILENAME} ${FLASH_BYTES_LIMIT_SIZE}
DEPENDS baseband application ${MAKE_SPI_IMAGE}
${baseband_BINARY_DIR}/baseband.img ${application_BINARY_DIR}/application.bin
VERBATIM
+3
View File
@@ -467,6 +467,9 @@ add_custom_command(
)
add_executable(${PROJECT_NAME}.elf ${CSRC} ${CPPSRC} ${ASMSRC})
target_link_options(${PROJECT_NAME}.elf PRIVATE
"LINKER:--defsym,LD_FLASH_SIZE=${FLASH_BYTES_LIMIT_SIZE}"
)
set_target_properties(${PROJECT_NAME}.elf PROPERTIES LINK_DEPENDS ${LDSCRIPT})
add_definitions(${DEFS})
include_directories(. ${INCDIR})
+8
View File
@@ -68,6 +68,9 @@ void BoundSetting::parse(std::string_view value) {
as<bool>() = (parsed != 0);
break;
}
case SettingType::Float:
as<float>() = atof(value.data());
break;
};
}
@@ -101,6 +104,11 @@ void BoundSetting::write(File& file) const {
case SettingType::Bool:
file.write(as<bool>() ? "1" : "0", 1);
break;
case SettingType::Float: {
auto float_str = to_string_decimal(as<float>(), 6);
file.write(float_str.c_str(), float_str.length());
}
}
file.write("\r\n", 2);
+4
View File
@@ -52,6 +52,7 @@ class BoundSetting {
U8,
String,
Bool,
Float
};
public:
@@ -73,6 +74,9 @@ class BoundSetting {
BoundSetting(std::string_view name, bool* target)
: name_{name}, target_{target}, type_{SettingType::Bool} {}
BoundSetting(std::string_view name, float* target)
: name_{name}, target_{target}, type_{SettingType::Float} {}
std::string_view name() const { return name_; }
void parse(std::string_view value);
void write(File& file) const;
+124 -1
View File
@@ -29,6 +29,7 @@
#include "baseband_api.hpp"
#include "portapack_shared_memory.hpp"
#include "portapack_persistent_memory.hpp"
#include "ui_geomap.hpp"
#include <cstring>
#include <stdio.h>
@@ -49,10 +50,24 @@ APRSTXView::~APRSTXView() {
void APRSTXView::start_tx() {
// TODO: Clean up this API to take string_views to avoid allocations.
std::string new_payload = payload;
std::string gps = text_gps_coord.get();
std::string token = "?GPS?";
size_t pos = 0;
while ((pos = new_payload.find(token, pos)) != std::string::npos) {
new_payload.replace(pos, token.size(), gps);
pos += gps.size();
}
text_payload.set(new_payload);
std::string path = text_path.get();
make_aprs_frame(
sym_source.to_string().c_str(), num_ssid_source.value(),
sym_dest.to_string().c_str(), num_ssid_dest.value(),
payload);
new_payload, path);
// uint8_t * bb_data_ptr = shared_memory.bb_data.data;
// text_payload.set(to_string_hex_array(bb_data_ptr + 56, 15));
@@ -77,6 +92,61 @@ void APRSTXView::on_tx_progress(const uint32_t progress, const bool done) {
}
}
void APRSTXView::process_coordinates(float lat_, float lon_) {
std::string s;
char ns = lat_ >= 0 ? 'N' : 'S';
float lat = (lat_ >= 0) ? lat_ : -lat_;
uint32_t lat_d = (uint32_t)lat;
uint32_t lat_m = (uint32_t)((lat - lat_d) * 6000.0f + 0.5f);
if (lat_m >= 6000) {
lat_m = 0;
lat_d++;
}
s += to_string_dec_uint(lat_d, 2, '0');
s += to_string_dec_uint(lat_m / 100, 2, '0');
s += ".";
s += to_string_dec_uint(lat_m % 100, 2, '0');
s += ns;
s += "/";
char ew = lon_ >= 0 ? 'E' : 'W';
float lon = (lon_ >= 0) ? lon_ : -lon_;
uint32_t lon_d = (uint32_t)lon;
uint32_t lon_m = (uint32_t)((lon - lon_d) * 6000.0f + 0.5f);
if (lon_m >= 6000) {
lon_m = 0;
lon_d++;
}
s += to_string_dec_uint(lon_d, 3, '0');
s += to_string_dec_uint(lon_m / 100, 2, '0');
s += ".";
s += to_string_dec_uint(lon_m % 100, 2, '0');
s += ew;
text_gps_coord.set(s);
}
void APRSTXView::on_gps(const GPSPosDataMessage* message) {
if (manual_gps_mode) {
return; // no more update once set manually
}
if (message->lat < -90.0 || message->lat > 90.0 ||
message->lon < -180.0 || message->lon > 180.0) {
return;
}
if (message->lat == 0.0f && message->lon == 0.0f) {
return;
}
last_lat = message->lat;
last_lon = message->lon;
process_coordinates(message->lat, message->lon);
}
APRSTXView::APRSTXView(NavigationView& nav) {
baseband::run_image(portapack::spi_flash::image_tag_afsk);
@@ -87,8 +157,33 @@ APRSTXView::APRSTXView(NavigationView& nav) {
&num_ssid_dest,
&text_payload,
&button_set,
&text_gps_coord,
&button_mangps,
&gps_is_manual,
&text_path,
&button_setpath,
&tx_view});
sym_source.set_value(symsrc);
num_ssid_source.set_value(ssidsrc);
sym_dest.set_value(symdst);
num_ssid_dest.set_value(ssiddst);
text_payload.set(payload);
text_path.set(path_cache);
sym_source.on_change = [this](SymField&) {
symsrc = sym_source.to_string();
};
num_ssid_source.on_change = [this](int32_t v) {
ssidsrc = v;
};
sym_dest.on_change = [this](SymField&) {
symdst = sym_dest.to_string();
};
num_ssid_dest.on_change = [this](int32_t v) {
ssiddst = v;
};
button_set.on_select = [this, &nav](Button&) {
text_prompt(
nav,
@@ -99,6 +194,17 @@ APRSTXView::APRSTXView(NavigationView& nav) {
text_payload.set(s);
});
};
button_setpath.on_select = [this, &nav](Button&) {
text_prompt(
nav,
path_cache,
60,
ENTER_KEYBOARD_MODE_ALPHA,
[this](std::string& s) {
text_path.set(s);
path_cache = s;
});
};
tx_view.on_edit_frequency = [this, &nav]() {
auto new_view = nav.push<FrequencyKeypadView>(transmitter_model.target_frequency());
@@ -116,6 +222,23 @@ APRSTXView::APRSTXView(NavigationView& nav) {
tx_view.set_transmitting(false);
transmitter_model.disable();
};
button_mangps.on_select = [this, &nav](Button&) {
nav.push<GeoMapView>(
0,
GeoPos::alt_unit::METERS,
GeoPos::spd_unit::HIDDEN,
last_lat,
last_lon,
[this](int32_t, float lat, float lon, int32_t) {
last_lat = lat;
last_lon = lon;
manual_gps_mode = true;
gps_is_manual.set("MANUAL");
process_coordinates(lat, lon);
});
};
// process_coordinates(last_lat, last_lon); //don't load last, so won't confuse users
}
} /* namespace ui */
+62 -12
View File
@@ -50,59 +50,109 @@ class APRSTXView : public View {
1750000 /* bandwidth */,
AFSK_TX_SAMPLERATE /* sampling rate */
};
app_settings::SettingsManager settings_{
"tx_aprs", app_settings::Mode::TX};
std::string symsrc = "";
std::string symdst = "";
std::string payload{""};
std::string path_cache = "";
int32_t ssidsrc = 0;
int32_t ssiddst = 0;
bool manual_gps_mode = false;
float last_lat = 0.0f;
float last_lon = 0.0f;
app_settings::SettingsManager settings_{
"tx_aprs",
app_settings::Mode::TX,
{{"symsrc"sv, &symsrc},
{"symdst"sv, &symdst},
{"ssidsrc"sv, &ssidsrc},
{"ssiddst"sv, &ssiddst},
{"payload"sv, &payload},
{"last_lat"sv, &last_lat},
{"last_lon"sv, &last_lon},
{"path"sv, &path_cache}}};
void start_tx();
void generate_frame();
void generate_frame_pos();
void on_tx_progress(const uint32_t progress, const bool done);
void on_gps(const GPSPosDataMessage* message);
void process_coordinates(float lat, float lon);
Labels labels{
{{UI_POS_X(0), 1 * 16}, "Source: SSID:", Theme::getInstance()->fg_light->foreground}, // 6 alphanum + SSID
{{UI_POS_X(0), 2 * 16}, " Dest.: SSID:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), 4 * 16}, "Info field:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(0)}, "Source: SSID:", Theme::getInstance()->fg_light->foreground}, // 6 alphanum + SSID
{{UI_POS_X(0), UI_POS_Y(1)}, " Dest.: SSID:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(2)}, "Path: (ex.: WIDE1-1,WIDE2-1)", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(6)}, "Info field:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(10)}, "GPS:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X_CENTER(22), UI_POS_Y(14)}, "Use ?GPS? in the info", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X_CENTER(17), UI_POS_Y(15)}, "as a placeholder.", Theme::getInstance()->fg_light->foreground},
};
Text text_path{
{UI_POS_X(0), UI_POS_Y(3), UI_POS_MAXWIDTH, UI_POS_HEIGHT(1)},
"WIDE1-1",
};
Text gps_is_manual{
{UI_POS_X(6), UI_POS_Y(10), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)},
"",
};
SymField sym_source{
{7 * 8, 1 * 16},
{UI_POS_X(7), UI_POS_Y(0)},
6,
SymField::Type::Alpha};
NumberField num_ssid_source{
{19 * 8, 1 * 16},
{UI_POS_X(19), UI_POS_Y(0)},
2,
{0, 15},
1,
' '};
SymField sym_dest{
{7 * 8, 2 * 16},
{UI_POS_X(7), UI_POS_Y(1)},
6,
SymField::Type::Alpha};
NumberField num_ssid_dest{
{19 * 8, 2 * 16},
{UI_POS_X(19), UI_POS_Y(1)},
2,
{0, 15},
1,
' '};
Text text_payload{
{UI_POS_X(0), 5 * 16, screen_width, 16},
"-"};
{UI_POS_X(0), UI_POS_Y(7), UI_POS_MAXWIDTH, UI_POS_HEIGHT(1)},
""};
Button button_set{
{UI_POS_X(0), 6 * 16, 80, 32},
{UI_POS_X(0), UI_POS_Y(8), UI_POS_WIDTH(10), UI_POS_HEIGHT(2)},
"Set"};
Text text_gps_coord{
{UI_POS_X(0), UI_POS_Y(11), UI_POS_WIDTH(20), UI_POS_HEIGHT(1)},
"-"};
Button button_mangps{
{UI_POS_X(0), UI_POS_Y(12), UI_POS_WIDTH(14), UI_POS_HEIGHT(2)},
"Manual GPS"};
Button button_setpath{
{UI_POS_X(0), UI_POS_Y(4), UI_POS_WIDTH(14), UI_POS_HEIGHT(2)},
"Set Path"};
TransmitterView tx_view{
(int16_t)UI_POS_Y_BOTTOM(4),
5000,
0 // disable setting bandwith, since APRS used fixed 10k bandwidth
};
MessageHandlerRegistration message_handler_gps{
Message::ID::GPSPosData,
[this](Message* const p) {
const auto message = static_cast<const GPSPosDataMessage*>(p);
this->on_gps(message);
}};
MessageHandlerRegistration message_handler_tx_progress{
Message::ID::TXProgress,
[this](const Message* const p) {
@@ -45,10 +45,15 @@ bool valid_firmware_file(std::filesystem::path::string_type path) {
// test read of the whole file just to validate checksum (baseband flash code will re-read when flashing)
auto result = firmware_file.open(path.c_str());
if (!result.is_valid()) {
uint64_t file_size = firmware_file.size();
if (portapack::device_type == portapack::DeviceType::DEV_PORTAPACK && file_size > 1 * 1024 * 1024) {
// Portapack firmware files must not be larger than 1MB
return false;
}
// May need to add a check to portarf and portarf pro too.
checksum = 0;
for (uint32_t offset = 0; offset < FLASH_ROM_SIZE; offset += sizeof(read_buffer)) {
for (uint64_t offset = 0; offset < FLASH_ROM_SIZE && offset < file_size; offset += sizeof(read_buffer)) {
auto readResult = firmware_file.read(&read_buffer, sizeof(read_buffer));
if ((!readResult) || (readResult.value() != sizeof(read_buffer))) {
// File was smaller than 1MB:
// If version is such that the file SHOULD have been 1MB, call it a checksum error (otherwise say it's OK).
@@ -32,7 +32,9 @@
#include "untar.hpp"
#include <cstdint>
#define FLASH_ROM_SIZE 1048576
#include "../../flashsize.h"
#define FLASH_ROM_SIZE (FLASH_SIZE_MB * 1024 * 1024)
#define FLASH_STARTING_ADDRESS 0x00000000
#define FLASH_EXPECTED_CHECKSUM 0x00000000
#define FLASH_CHECKSUM_ERROR 0xFFFFFFFF
+9
View File
@@ -350,6 +350,12 @@ MicTXView::MicTXView(
&tx_button,
&tx_icon});
// disable key repeat on select
initial_switch_config_ = get_switches_repeat_config();
SwitchesState config = initial_switch_config_;
config[toUType(Switch::Sel)] = false;
set_switches_repeat_config(config);
set_rxbw_options();
set_rxbw_defaults(settings_.loaded());
@@ -640,6 +646,9 @@ MicTXView::MicTXView(
}
MicTXView::~MicTXView() {
// restore select key repeat mode
set_switches_repeat_config(initial_switch_config_);
audio::input::stop();
if (rx_enabled) { // Also turn off both (audio rx if enabled, and disable mic_loop to HP)
rxaudio(false);
+3
View File
@@ -90,6 +90,9 @@ class MicTXView : public View {
sampling_rate /* sampling rate */
};
// structure used to control key repeat
SwitchesState initial_switch_config_{};
enum Mic_Modulation : uint32_t {
MIC_MOD_NFM = 0,
MIC_MOD_WFM = 1,
+9 -1
View File
@@ -242,6 +242,8 @@ const char* SubGhzDView::getSensorTypeName(FPROTO_SUBGHZD_SENSOR type) {
return "GangQi";
case FPS_MARANTEC24:
return "Marantec24";
case FPS_HOLTEKHT6P20B:
return "Holtek HT6P20B";
case FPS_Invalid:
default:
return "Unknown";
@@ -756,5 +758,11 @@ void SubGhzDRecentEntryDetailView::parseProtocol() {
btn = entry_.data & 0xf;
return;
}
if (entry_.sensorType == FPS_HOLTEKHT6P20B) {
serial = entry_.data >> 8;
btn = (entry_.data >> 4) & 0xF;
return;
}
}
} // namespace ui
} // namespace ui
+17 -2
View File
@@ -368,8 +368,23 @@ void set_subghzd_config(uint8_t modulation = 0, uint32_t sampling_rate = 0) {
send_message(&message);
}
void set_moreserx_config() {
const MorseRXConfigureMessage message{};
void set_moreserx_config(uint8_t mode) {
const MorseRXConfigureMessage message{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);
}
+4 -1
View File
@@ -102,10 +102,13 @@ void set_siggen_tone(const uint32_t tone);
void set_siggen_config(const uint32_t bw, const uint32_t shape, const uint32_t duration);
void set_spectrum_painter_config(const uint16_t width, const uint16_t height, bool update, int32_t bw);
void set_subghzd_config(uint8_t modulation, uint32_t sampling_rate);
void set_moreserx_config();
void set_moreserx_config(uint8_t mode);
void set_morsetx_config(uint8_t mode, uint32_t tone, float fm_delta);
void set_morsetx_key(bool key_down);
void set_wefax_config(uint8_t lpm, uint8_t ioc);
void set_noaaapt_config();
void set_flex_config();
void set_bitstream_config(uint32_t deviation, uint8_t mode); // mode 0 for am, 1 for 2fsk
void request_roger_beep();
void request_rssi_beep();
+9 -4
View File
@@ -91,9 +91,9 @@ set(EXTCPPSRC
external/adsbtx/main.cpp
external/adsbtx/ui_adsb_tx.cpp
#morse_tx 768 bytes
external/morse_tx/main.cpp
external/morse_tx/ui_morse.cpp
#morse_tx 768 bytes -- disabled because of the new morse tx app with more functions
#external/morse_tx/main.cpp
#external/morse_tx/ui_morse.cpp
#sstvtx 456 bytes
external/sstvtx/main.cpp
@@ -293,6 +293,10 @@ set(EXTCPPSRC
#morse_radio
external/morse_radio/main.cpp
external/morse_radio/ui_morse_radio.cpp
#morseradiotx
external/morseradiotx/main.cpp
external/morseradiotx/ui_morse_radiotx.cpp
)
set(EXTAPPLIST
@@ -317,7 +321,7 @@ set(EXTAPPLIST
tpmsrx
protoview
adsbtx
morse_tx
#morse_tx
sstvtx
sstvrx
random_password
@@ -366,4 +370,5 @@ set(EXTAPPLIST
siggen
sdusb
morse_radio
morseradiotx
)
+8
View File
@@ -93,6 +93,7 @@ MEMORY
ram_external_app_sdusb (rwx) : org = 0xADF40000, len = 32k
ram_external_app_morse_radio (rwx) : org = 0xADF50000, len = 32k
ram_external_app_waterfall_designer (rwx) : org = 0xADF60000, len = 32k
ram_external_app_morseradiotx (rwx) : org = 0xADF70000, len = 32k
}
SECTIONS
@@ -518,5 +519,12 @@ SECTIONS
KEEP(*(.external_app.app_morse_radio.application_information));
*(*ui*external_app*morse_radio*);
} > ram_external_app_morse_radio
.external_app_morseradiotx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_morseradiotx.application_information));
*(*ui*external_app*morseradiotx*);
} > ram_external_app_morseradiotx
}
+6 -1
View File
@@ -80,7 +80,7 @@ bool FlipperTxView::on_file_changed(std::filesystem::path new_file_path) {
return false;
}
preset = submeta.value().preset;
if (preset != FLIPPER_PRESET_OOK) {
if (preset != FLIPPER_PRESET_OOK && preset != FLIPPER_PRESET_2FSK) {
field_filename.set_text("File: err, not supp. preset");
return false;
}
@@ -108,8 +108,13 @@ void FlipperTxView::stop() {
bool FlipperTxView::start() {
if (filename.empty()) return false;
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
transmitter_model.set_sampling_rate(OOK_SAMPLERATE);
transmitter_model.enable();
button_startstop.set_text(LanguageHelper::currentMessages[LANG_STOP]);
uint8_t mode = 0;
if (preset == FLIPPER_PRESET_2FSK)
mode = 1;
baseband::set_bitstream_config(FM_DEVIATION, mode);
// start thread
replay_thread = std::make_unique<FlipperPlayThread>(
filename,
@@ -28,6 +28,8 @@ using namespace ui;
namespace ui::external_app::flippertx {
#define OOK_SAMPLERATE 2280000U
#define FM_DEVIATION 60000U
class FlipperPlayThread;
class FlipperTxView : public View {
public:
@@ -12,6 +12,13 @@ MorsePracticeView::MorsePracticeView(ui::NavigationView& nav)
&btn_clear,
&console_text,
&field_volume});
initial_switch_config_ = get_switches_repeat_config();
SwitchesState config = initial_switch_config_;
config[toUType(Switch::Sel)] = false;
set_switches_repeat_config(config);
audio::set_rate(audio::Rate::Hz_24000);
btn_tt.on_select = [this](Button&) {
@@ -43,6 +50,7 @@ MorsePracticeView::MorsePracticeView(ui::NavigationView& nav)
}
MorsePracticeView::~MorsePracticeView() {
set_switches_repeat_config(initial_switch_config_);
receiver_model.disable();
baseband::shutdown();
audio::output::stop();
@@ -68,7 +68,7 @@ class MorsePracticeView : public ui::View {
ui::Text txt_last{{UI_POS_X(0), UI_POS_Y(6), UI_POS_MAXWIDTH, UI_POS_HEIGHT(1)}, ""};
ui::Button btn_clear{{UI_POS_X(0), UI_POS_Y_BOTTOM(2), UI_POS_WIDTH(6), UI_POS_HEIGHT(1)}, "CLR"};
ui::Console console_text{{UI_POS_X(0), UI_POS_Y(7), UI_POS_MAXWIDTH, UI_POS_HEIGHT_REMAINING(10)}};
AudioVolumeField field_volume{{UI_POS_X_RIGHT(2), UI_POS_X(0)}};
AudioVolumeField field_volume{{UI_POS_X_RIGHT(2), UI_POS_Y(0)}};
uint8_t last_color_id = 255;
uint8_t color_id = 255;
@@ -77,6 +77,7 @@ class MorsePracticeView : public ui::View {
bool button_touch = false;
bool button_was_selected = false;
bool decode_timeout_calc = false;
SwitchesState initial_switch_config_{};
MessageHandlerRegistration message_handler_framesync{
Message::ID::DisplayFrameSync,
+73 -57
View File
@@ -16,6 +16,9 @@ MorseRadioView::MorseRadioView(ui::NavigationView& nav)
&field_frequency,
&txt_last,
&txt_speed,
&txt_freq,
&txt_clip,
&options_mode,
&btn_clear,
&console_text,
&labels,
@@ -31,28 +34,46 @@ MorseRadioView::MorseRadioView(ui::NavigationView& nav)
logger->init_daily_log(logs_dir);
};
audio::set_rate(audio::Rate::Hz_24000);
audio::output::start();
receiver_model.set_sampling_rate(3072000);
receiver_model.set_baseband_bandwidth(1750000);
receiver_model.set_hidden_offset(0);
baseband::set_moreserx_config();
receiver_model.set_am_configuration(4);
receiver_model.set_modulation(ReceiverModel::Mode::NarrowbandFMAudio);
receiver_model.enable();
auto vol = field_volume.value(); // audio volume fix
field_volume.set_value(0);
field_volume.set_value(vol);
field_squelch.set_value(receiver_model.squelch_level());
field_squelch.on_change = [this](int32_t v) {
receiver_model.set_squelch_level(v);
};
logger = std::make_unique<MorseLogger>();
options_mode.on_change = [this](size_t, int32_t mode) {
audio::output::stop();
receiver_model.disable();
morse_decoder_.resetLearning();
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);
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);
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);
}
baseband::set_moreserx_config(mode);
saved_mode = mode;
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) {
save_log = save;
if (logger && save_log)
@@ -86,35 +107,29 @@ void MorseLogger::init_daily_log(const std::filesystem::path& log_dir) {
}
}
void MorseLogger::radio_set_log() {
void MorseLogger::radio_set_log(const std::string& morse_mode) {
int64_t freq = receiver_model.target_frequency();
std::string header = "Freq:" + to_string_dec_int(freq / 1000000) + "." + to_string_dec_int((freq % 1000000) / 1000, 3);
header += "MHz SQ:" + to_string_dec_uint(receiver_model.squelch_level());
header += " LNA:" + to_string_dec_uint(receiver_model.lna());
header += " VGA:" + to_string_dec_uint(receiver_model.vga());
header += " AMP:" + std::string(receiver_model.rf_amp() ? "ON" : "OFF");
header += "\r\nMessage:\r\n";
std::string header = "Freq:" + to_string_rounded_freq(freq, 4);
header += "MHz, ";
header += "RX MODE:" + morse_mode;
header += "\r\nMessage:";
log_file.write_raw(header);
}
bool MorseLogger::on_packet(const std::string& content, bool time) {
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();
radio_set_log(morse_mode);
char_count = 0;
time = true;
}
if (content.empty()) return time;
std::string s;
for (char c : content) {
std::string s(1, c);
s = c;
if ((char_count >= 35 && c == ' ') || (char_count >= 47)) {
log_file.write_raw_no_newline("\r\n");
@@ -141,18 +156,17 @@ 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() {
uint64_t now = chTimeNow();
if (last_activity_time == 0) {
last_activity_time = now;
return;
}
uint64_t quiet_duration = now - last_activity_time;
if (quiet_duration >= 60000) {
morse_decoder_.resetLearning();
time_stamp = false;
@@ -170,13 +184,11 @@ int32_t MorseRadioView::ProcessSignal(int32_t sig_time_us) {
long_pause_sent_ = false;
return 0;
}
if (sign == last_sign_) {
accumulator_us_ += sig_time_us;
if (sign < 0) {
int32_t threshold_us = morse_decoder_.getInterWordThreshold() * 1000;
if (!long_pause_sent_ && accumulator_us_ <= -threshold_us) {
result = accumulator_us_ / 1000;
long_pause_sent_ = true;
@@ -189,53 +201,57 @@ int32_t MorseRadioView::ProcessSignal(int32_t sig_time_us) {
} else {
result = 0;
}
accumulator_us_ = sig_time_us;
last_sign_ = sign;
long_pause_sent_ = false;
}
return result;
}
void MorseRadioView::on_data(const MorseRXDataMessage* message) {
uint16_t start = 0;
uint16_t stop = 0;
bool has_valid = false;
int32_t r;
for (uint16_t i = 0; i <= message->maxptr; ++i) {
if (message->state_durations[i] >= 30000 || message->state_durations[i] <= -30000) {
if (!has_valid) {
start = i;
}
} else {
has_valid = true;
stop = i;
}
}
if (!has_valid) return; // no valid data arrived
txt_clip.hidden(!message->clipped);
for (uint16_t i = start; i <= stop; i++) {
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);
if (result.isValid()) {
last_activity_time = chTimeNow(); // start reset timer on valid input
writeCharToConsole(result.text, result.confidence);
if (logger && save_log) {
time_stamp = logger->on_packet(result.text, time_stamp);
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) {
uint16_t wpm = (uint16_t)(3600.0f / (dah_time + 18.0f) + 0.5f);
txt_speed.set(to_string_dec_uint(wpm));
}
}
}
}
void MorseRadioView::on_freq(const MorseRXfreqMessage* message) {
std::string ret = " ";
uint32_t freq = message->measured_frequency;
if (freq < 301) {
ret = "<";
freq = 300;
}
if (freq > 2299) {
freq = 2300;
ret = ">";
}
if (freq < 400 || freq > 1400)
txt_freq.set_style(Theme::getInstance()->fg_red);
else if (freq <= 580 || freq >= 1220)
txt_freq.set_style(Theme::getInstance()->fg_yellow);
else
txt_freq.set_style(Theme::getInstance()->fg_green);
ret += to_string_dec_uint(freq);
txt_freq.set(ret);
}
void MorseRadioView::writeCharToConsole(const std::string& ch, double confidence) {
if (ch.empty()) {
return;
@@ -40,6 +40,7 @@
#include "file.hpp"
#include "rtc_time.hpp"
#include "morsedecoder.hpp"
#include <cstdint>
namespace ui::external_app::morse_radio {
@@ -53,8 +54,8 @@ class MorseLogger {
}
void init_daily_log(const std::filesystem::path& log_dir);
bool on_packet(const std::string& content, bool time);
void radio_set_log();
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{};
@@ -73,6 +74,7 @@ class MorseRadioView : public ui::View {
private:
void writeCharToConsole(const std::string& ch, double confidence);
void on_data(const MorseRXDataMessage* message);
void on_freq(const MorseRXfreqMessage* message);
void on_message(const Message* const p);
void check_for_timeout();
int32_t ProcessSignal(int32_t sig_time_us);
@@ -80,8 +82,22 @@ class MorseRadioView : public ui::View {
MorseDecoder morse_decoder_{};
RxRadioState radio_state_{};
std::unique_ptr<MorseLogger> logger{};
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_{
"trx_morese_radio", app_settings::Mode::RX_TX};
"rx_morse_radio",
app_settings::Mode::RX,
{
{"cwmode"sv, &saved_mode},
}};
RxFrequencyField field_frequency{
{UI_POS_X(0), UI_POS_Y(0)},
@@ -107,27 +123,42 @@ 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},
{{UI_POS_X(15), UI_POS_Y(1)}, "Speed:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(26), UI_POS_Y(1)}, "wpm", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(4)}, "Last seq.:", Theme::getInstance()->fg_light->foreground},
};
{{UI_POS_X(15), UI_POS_Y(2)}, "Tone:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(27), UI_POS_Y(2)}, "Hz", Theme::getInstance()->fg_light->foreground}};
ui::OptionsField options_mode{
{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"};
uint8_t last_color_id{255};
uint8_t color_id{255};
std::string arr_color[4] = {STR_COLOR_WHITE, STR_COLOR_RED, STR_COLOR_YELLOW, STR_COLOR_GREEN};
int32_t accumulator_us_{0};
int8_t last_sign_{0};
uint8_t space_timer{0};
bool long_pause_sent_{false};
bool save_log{false};
bool reset_triggered_{false};
bool time_stamp{false};
uint8_t last_color_id{255};
uint8_t color_id{255};
int8_t last_sign_{0};
uint8_t space_timer{0};
int32_t accumulator_us_{0};
uint64_t last_activity_time{0};
MessageHandlerRegistration message_handler_packet{
@@ -137,6 +168,13 @@ class MorseRadioView : public ui::View {
this->on_data(message);
}};
MessageHandlerRegistration message_handler_freq{
Message::ID::MorseRXfreq,
[this](Message* const p) {
const auto message = static_cast<const MorseRXfreqMessage*>(p);
this->on_freq(message);
}};
MessageHandlerRegistration message_handler_framesync{
Message::ID::DisplayFrameSync,
[this](const Message* const p) {
+87
View File
@@ -0,0 +1,87 @@
/*
* Copyright (C) 2026 Pezsma
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#include "ui.hpp"
#include "ui_morse_radiotx.hpp"
#include "ui_navigation.hpp"
#include "external_app.hpp"
namespace ui::external_app::morseradiotx {
void initialize_app(NavigationView& nav) {
nav.push<MorseRadiotxView>();
}
} // namespace ui::external_app::morseradiotx
extern "C" {
__attribute__((section(".external_app.app_morseradiotx.application_information"), used))
application_information_t _application_information_morseradiotx = {
/*.memory_location = */ (uint8_t*)0x00000000,
/*.externalAppEntry = */ ui::external_app::morseradiotx::initialize_app,
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
/*.app_name = */ "Morse TX",
/*.bitmap_data = */ {
0x00,
0x00,
0xFE,
0x7F,
0xFF,
0xFF,
0xBB,
0xD0,
0xFF,
0xFF,
0xFF,
0xFF,
0x0B,
0xE1,
0xFF,
0xFF,
0xFF,
0xFF,
0xEB,
0xD0,
0xFF,
0xFF,
0xFE,
0x7F,
0x70,
0x00,
0x30,
0x00,
0x10,
0x00,
0x00,
0x00,
},
/*.icon_color = */ ui::Color::yellow().v,
/*.menu_location = */ app_location_t::TX,
/*.desired_menu_position = */ -1,
/*.m4_app_tag = portapack::spi_flash::image_tag_none */ {'P', 'M', 'R', 'T'},
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
};
} // extern "C"
@@ -0,0 +1,389 @@
#ifndef __MORSEDECODER_HPP__
#define __MORSEDECODER_HPP__
/*
* Copyright (C) 2025 Pezsma
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#include <cstdint>
#include <string>
#include "string_format.hpp"
namespace ui::external_app::morseradiotx {
class MorseRingBuffer {
public:
MorseRingBuffer()
: head_(0), tail_(0), count_(0) {}
void push_back(const uint32_t& value) {
data_[head_] = value;
head_ = (head_ + 1) % 40;
if (count_ < 40) {
count_++;
} else {
// overwrite oldest element
tail_ = (tail_ + 1) % 40;
}
}
void pop_front() {
if (count_ > 0) {
tail_ = (tail_ + 1) % 40;
count_--;
}
}
size_t size() const { return count_; }
bool empty() const { return count_ == 0; }
const uint32_t& front() const { return data_[tail_]; }
// Access by index (0 = oldest)
uint32_t operator[](size_t idx) const {
return data_[(tail_ + idx) % 40];
}
// Convert to vector-like access for sorting etc.
void copy_to_array(uint32_t* out) const {
for (size_t i = 0; i < count_; ++i)
out[i] = (*this)[i];
}
void clear() {
head_ = 0;
tail_ = 0;
count_ = 0;
}
private:
uint32_t data_[40];
size_t head_;
size_t tail_;
size_t count_;
};
class MorseDecoder {
public:
struct DecodeResult {
std::string text = "";
double confidence = 0.0;
bool isValid() const {
return !text.empty();
}
};
struct MorseEntry {
std::string code;
std::string letter;
};
MorseDecoder() {}
void resetLearning() {
time_unit_ms_ = 119.0;
current_sequence_ = "";
last_sequence_ = "";
last_confidence_ = 0.0;
pulse_history_.clear();
pulse_gaps_.clear();
}
const char* get_morse_pattern(char c) {
char upper_c = (c >= 'a' && c <= 'z') ? c - 32 : c;
for (size_t i = 0; i < morse_table_size_; ++i) {
if (morse_table_[i].letter[0] == upper_c) {
return morse_table_[i].code.c_str();
}
}
return nullptr;
}
DecodeResult
handleInput(int32_t duration_ms) {
DecodeResult result = {"", 0.0};
if (duration_ms < 5 && duration_ms > -5) return result;
if (duration_ms > 0) {
pulse_history_.push_back(duration_ms);
double dah_prob = getDahProbability(duration_ms);
current_sequence_ += (dah_prob > 0.5) ? '-' : '.';
last_confidence_ = (dah_prob > 0.5) ? dah_prob : (1.0 - dah_prob);
last_sequence_ = current_sequence_;
} else {
uint32_t gap_duration = -duration_ms;
pulse_gaps_.push_back(gap_duration);
if (gap_duration >= getInterCharThreshold() && !current_sequence_.empty()) {
result.text = lookupMorse(current_sequence_);
result.confidence = (result.text[0] != '{') ? last_confidence_ : 0.0;
if (gap_duration >= getInterWordThreshold()) {
result.text += " ";
}
current_sequence_ = "";
}
}
updateLearning();
return result;
}
inline double getInterElementThreshold() { return time_unit_ms_ * 0.8; }
inline double getInterCharThreshold() { return time_unit_ms_ * 2.5; }
inline double getInterWordThreshold() { return time_unit_ms_ * 6.0; }
inline double getCurrentTimeUnit() { return time_unit_ms_; }
inline std::string getLastSequence() { return last_sequence_; }
private:
std::string current_sequence_ = "";
std::string last_sequence_ = "";
double time_unit_ms_ = 119.0;
double last_confidence_ = 0.0;
MorseRingBuffer pulse_history_{};
MorseRingBuffer pulse_gaps_{};
std::string lookupMorse(const std::string& seq) {
for (size_t i = 0; i < morse_table_size_; i++) {
if (seq == morse_table_[i].code)
return morse_table_[i].letter;
}
return "{" + seq + "}"; // not found
}
double getDahProbability(uint32_t duration_ms) {
double start_interp = 1.5 * time_unit_ms_;
double end_interp = 2.5 * time_unit_ms_;
if (duration_ms <= start_interp) return 0.0;
if (duration_ms >= end_interp) return 1.0;
return ((double)(duration_ms)-start_interp) / (end_interp - start_interp);
}
size_t findDecisionBoundary(uint32_t* sorted_data, size_t sorted_data_size) {
if (sorted_data_size < 4) return 0;
size_t best_split_index = 0;
uint32_t max_diff = 0;
for (size_t i = 1; i < sorted_data_size; ++i) {
uint32_t diff = sorted_data[i] - sorted_data[i - 1];
if (diff > sorted_data[i - 1] * 0.5 && diff > max_diff) {
max_diff = diff;
best_split_index = i;
}
}
return best_split_index;
}
bool calculatePulseUnit(double& unit, double& confidence) {
if (pulse_history_.size() < 10) return false;
uint32_t sorted_pulses[pulse_history_.size()];
pulse_history_.copy_to_array(sorted_pulses);
sort_uint32(sorted_pulses, pulse_history_.size());
size_t split_index = findDecisionBoundary(sorted_pulses, pulse_history_.size());
if (split_index == 0 || split_index < 3 || (pulse_history_.size() - split_index) < 2) {
return false;
}
double dit_sum = sum_uint32_range(sorted_pulses, 0, split_index);
double dah_sum = sum_uint32_range(sorted_pulses, split_index, pulse_history_.size());
double avg_dit = dit_sum / split_index;
double avg_dah = dah_sum / (pulse_history_.size() - split_index);
if (avg_dah <= avg_dit) return false;
double ratio = avg_dah / avg_dit;
if (ratio > 1.5 && ratio < 5.0) {
unit = avg_dit;
double tmpabs = ratio - 3.0;
if (tmpabs < 0) tmpabs *= -1;
tmpabs /= 3.0;
tmpabs = 1.0 - tmpabs;
if (tmpabs < 0) tmpabs = 0;
confidence = tmpabs; // 0..1
return true;
}
return false;
}
bool calculateGapUnit(double& unit, double& confidence) {
if (pulse_gaps_.size() < 10) return false;
double threshold = getInterElementThreshold();
double valid_gaps[pulse_gaps_.size()];
size_t valid_count = 0;
for (size_t i = 0; i < pulse_gaps_.size(); i++) {
double gap = pulse_gaps_[i];
if (gap <= threshold) {
valid_gaps[valid_count++] = gap;
}
}
if (valid_count < 2) {
return false;
}
double sum = sum_double_range(valid_gaps, 0, valid_count);
size_t count_to_average = valid_count;
if (count_to_average > 0) {
unit = sum / count_to_average;
confidence = 0.8;
return true;
}
return false;
}
double sum_uint32_range(const uint32_t* data, size_t start, size_t end) {
double sum = 0.0;
for (size_t i = start; i < end; i++) {
sum += data[i];
}
return sum;
}
double sum_double_range(const double* data, size_t start, size_t end) {
double sum = 0.0;
for (size_t i = start; i < end; i++) {
sum += data[i];
}
return sum;
}
void sort_uint32(uint32_t* data, size_t size) {
if (size < 2)
return;
for (size_t i = 1; i < size; i++) {
uint32_t key = data[i];
size_t j = i;
while (j > 0 && data[j - 1] > key) {
data[j] = data[j - 1];
j--;
}
data[j] = key;
}
}
double clamp_double(double value, double min_val, double max_val) {
if (value < min_val)
return min_val;
else if (value > max_val)
return max_val;
else
return value;
}
void updateLearning() {
double pulse_unit = -1.0, pulse_confidence = 0.0;
double gap_unit = -1.0, gap_confidence = 0.0;
bool pulse_success = calculatePulseUnit(pulse_unit, pulse_confidence);
bool gap_success = calculateGapUnit(gap_unit, gap_confidence);
double new_time_unit = -1.0;
if (pulse_success && pulse_confidence > 0.5) {
new_time_unit = pulse_unit;
} else if (pulse_success && gap_success) {
gap_confidence = 0.2;
double total_confidence = pulse_confidence + gap_confidence;
new_time_unit = (pulse_unit * pulse_confidence + gap_unit * gap_confidence) / total_confidence;
} else if (gap_success) {
new_time_unit = gap_unit;
} else {
return;
}
double max_change = time_unit_ms_ * 0.25;
new_time_unit = clamp_double(new_time_unit, time_unit_ms_ - max_change, time_unit_ms_ + max_change);
double DEFAULT_TIME_UNIT = 160.0;
double BASE_LEARNING_RATE = 0.05;
double MAX_LEARNING_RATE = 0.25;
double tudeltaabs = new_time_unit - DEFAULT_TIME_UNIT;
if (tudeltaabs < 0) tudeltaabs *= -1;
double deviation_from_default = tudeltaabs / DEFAULT_TIME_UNIT;
double tpp = deviation_from_default * 2.0;
if (tpp > 1) tpp = 1;
double learning_factor = BASE_LEARNING_RATE + (MAX_LEARNING_RATE - BASE_LEARNING_RATE) * tpp;
time_unit_ms_ = (time_unit_ms_ * (1.0 - learning_factor)) + (new_time_unit * learning_factor);
}
size_t morse_table_size_ = 50;
MorseEntry morse_table_[50] = {
{".-", "A"},
{"-...", "B"},
{"-.-.", "C"},
{"-..", "D"},
{".", "E"},
{"..-.", "F"},
{"--.", "G"},
{"....", "H"},
{"..", "I"},
{".---", "J"},
{"-.-", "K"},
{".-..", "L"},
{"--", "M"},
{"-.", "N"},
{"---", "O"},
{".--.", "P"},
{"--.-", "Q"},
{".-.", "R"},
{"...", "S"},
{"-", "T"},
{"..-", "U"},
{"...-", "V"},
{".--", "W"},
{"-..-", "X"},
{"-.--", "Y"},
{"--..", "Z"},
{".----", "1"},
{"..---", "2"},
{"...--", "3"},
{"....-", "4"},
{".....", "5"},
{"-....", "6"},
{"--...", "7"},
{"---..", "8"},
{"----.", "9"},
{"-----", "0"},
{".-.-.-", "."},
{"..--..", "?"},
{"-.-.--", "!"},
{"--..--", ","},
{"-...-", "="},
{"-..-.", "/"},
{".--.-.", "@"},
{"---...", ":"},
{"-....-", "-"},
{".----.", "'"},
{".-..-.", "\""},
{"-.--.", "("},
{"-.--.-", ")"},
{".-.-.", "+"}};
};
} // namespace ui::external_app::morseradiotx
#endif // __MORSEDECODER_HPP__
@@ -0,0 +1,437 @@
#include "ui_morse_radiotx.hpp"
using namespace portapack;
namespace ui::external_app::morseradiotx {
static msg_t tx_thread_fn(void* arg) {
auto view = reinterpret_cast<MorseRadiotxView*>(arg);
chRegSetThreadName("morse_tx_thread");
view->transmit_morse_message();
return 0;
}
MorseRadiotxView::MorseRadiotxView(ui::NavigationView& nav)
: current_timings(calculate_morse_timings(20)),
nav_(nav) {
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
add_children({&tx_view,
&field_volume,
&labels,
&options_mode,
&tone_,
&wpm_,
&txt_msg,
&btn_message,
&btn_calls,
&chk_trans,
&bandwidth,
&chk_callsgn,
&txt_last,
&console_text,
&btn_clear,
&btn_ptt});
initial_switch_config_ = get_switches_repeat_config();
SwitchesState config = initial_switch_config_;
config[toUType(Switch::Sel)] = false;
set_switches_repeat_config(config);
audio::set_rate(audio::Rate::Hz_24000);
btn_clear.on_select = [this](Button&) {
console_text.clear(true);
txt_last.set("");
};
options_mode.on_change = [this](size_t, int32_t value) {
current_mode = (uint8_t)value;
baseband::set_morsetx_config(current_mode, tone, band);
ui_toggle();
};
tone_.on_change = [this](int32_t v) {
tone = static_cast<uint32_t>(v);
baseband::set_morsetx_config(current_mode, tone, band);
};
wpm_.on_change = [this](int16_t v) {
wpm = v;
current_timings = calculate_morse_timings(wpm);
};
bandwidth.on_change = [this](float v) {
band = v;
baseband::set_morsetx_config(current_mode, tone, band);
};
options_mode.set_selected_index(current_mode, true);
tone_.set_value(tone, true);
wpm_.set_value(wpm, true);
bandwidth.set_value(band, true);
btn_ptt.on_select = [this](Button&) {
if (button_touch) {
button_touch = false;
return;
}
button_was_selected = true;
onPress();
};
btn_ptt.on_touch_press = [this](Button&) {
button_touch = true;
button_was_selected = false;
onPress();
};
btn_ptt.on_touch_release = [this](Button&) {
if (chk_trans.value() && transmit) {
button_touch = true;
button_was_selected = false;
onRelease();
}
};
btn_message.on_select = [this, &nav](Button&) {
if (!transmit)
text_prompt(nav, msg_buffer, 27, ENTER_KEYBOARD_MODE_ALPHA, [this](std::string& buffer) {
msg_buffer = buffer;
txt_msg.set("[" + msg_buffer + "] ");
});
};
btn_calls.on_select = [this, &nav](Button&) {
if (!transmit) {
text_prompt(nav, call_sign, 10, ENTER_KEYBOARD_MODE_ALPHA, [this](std::string& buffer) {
call_sign = buffer;
if (call_sign.empty())
btn_calls.set_text("call sign?");
else
btn_calls.set_text(call_sign);
});
}
};
audio::output::start();
auto vol = field_volume.value();
field_volume.set_value(0);
field_volume.set_value(vol);
tx_view.on_start = [this]() {
if (!chk_trans.value() && msg_buffer.empty()) {
tx_view.set_transmitting(false);
return;
}
tx_view.focus();
if (!tx_thread && !thread_running) {
thread_running = true;
tx_thread = chThdCreateFromHeap(NULL, 1024, NORMALPRIO + 5, tx_thread_fn, this);
tx_view.set_transmitting(true);
}
else
tx_view.set_transmitting(false);
};
tx_view.on_stop = [this]() {
baseband::set_morsetx_key(false);
transmitter_model.disable();
if (tx_thread) {
chThdTerminate(tx_thread);
chThdWait(tx_thread);
}
transmit = false;
transmit_time = 0;
tx_thread = nullptr;
thread_running = false;
tx_view.set_transmitting(false);
ui_toggle();
};
tx_view.on_edit_frequency = [this, &nav]() {
auto new_view = nav.push<FrequencyKeypadView>(transmitter_model.target_frequency());
new_view->on_changed = [this](rf::Frequency f) {
transmitter_model.set_target_frequency(f);
};
};
}
MorseRadiotxView::~MorseRadiotxView() {
set_switches_repeat_config(initial_switch_config_);
if (tx_thread) {
chThdTerminate(tx_thread);
chThdWait(tx_thread);
if (!thread_running) tx_thread = nullptr;
}
transmitter_model.disable();
baseband::shutdown();
audio::output::stop();
}
void MorseRadiotxView::on_show() {
ptt_button_visibility(true);
start_time = 0;
end_time = 0;
transmit_time = 0;
}
void MorseRadiotxView::focus() {
options_mode.focus();
}
MorseRadiotxView::MorseTimings MorseRadiotxView::calculate_morse_timings(uint32_t wpm) {
MorseRadiotxView::MorseTimings t;
if (wpm < 10) wpm = 10;
if (wpm > 45) wpm = 45;
t.dot_ms = 1200 / wpm;
t.dash_ms = 3 * t.dot_ms;
t.symbol_gap = t.dot_ms;
t.char_gap = 3 * t.dot_ms;
t.word_gap = 7 * t.dot_ms;
return t;
}
void MorseRadiotxView::transmit_morse_message() {
std::string full_message = "";
// cal sign
if (chk_callsgn.value() && !call_sign.empty()) {
full_message = call_sign;
if (!chk_trans.value() && !msg_buffer.empty()) {
full_message += " " + msg_buffer;
}
} else {
if (!chk_trans.value()) full_message = msg_buffer;
}
if (full_message.empty() && !chk_trans.value()) {
ptt_button_visibility(false);
return;
}
// enable transmit
if (!transmit) {
transmit = true;
transmitter_model.enable();
ui_toggle();
}
for (size_t i = 0; i < full_message.length(); i++) {
if (chThdShouldTerminate()) break;
char c = full_message[i];
std::string s_char(1, c);
// space
if (c == ' ') {
console_text.write(" ");
chThdSleepMilliseconds(current_timings.word_gap);
continue;
}
const char* pattern = morse_decoder_.get_morse_pattern(c);
if (pattern != nullptr) {
txt_last.set(pattern);
// write blue char to console
console_text.write(STR_COLOR_BLUE + s_char);
// Morze (dih/dah) send
for (int j = 0; pattern[j] != '\0'; j++) {
baseband::set_morsetx_key(true);
if (pattern[j] == '.') {
chThdSleepMilliseconds(current_timings.dot_ms);
} else if (pattern[j] == '-') {
chThdSleepMilliseconds(current_timings.dash_ms);
}
if (chThdShouldTerminate()) break;
// pause between signs
baseband::set_morsetx_key(false);
chThdSleepMilliseconds(current_timings.symbol_gap);
}
if (chThdShouldTerminate()) break;
if (current_timings.char_gap > current_timings.symbol_gap) {
chThdSleepMilliseconds(current_timings.char_gap - current_timings.symbol_gap);
}
}
}
if (chk_trans.value()) ptt_button_visibility(false);
baseband::set_morsetx_key(false);
transmit_time = chTimeNow();
decode_timeout_calc = false;
thread_running = false;
}
void MorseRadiotxView::onPress() {
start_time = chTimeNow();
if (!transmit) {
transmit = true;
transmitter_model.enable();
}
baseband::set_morsetx_key(true);
if (end_time != 0) {
int64_t gap_delta = (chTimeNow() - end_time);
auto result = morse_decoder_.handleInput(-gap_delta);
if (result.isValid()) {
writeCharToConsole(result.text, result.confidence);
}
}
end_time = 0;
transmit_time = 0;
decode_timeout_calc = false;
}
void MorseRadiotxView::onRelease() {
end_time = chTimeNow();
transmit_time = end_time;
baseband::set_morsetx_key(false);
if (start_time != 0) {
int32_t press_delta = (end_time - start_time);
auto result = morse_decoder_.handleInput(press_delta);
if (result.isValid()) {
writeCharToConsole(result.text, result.confidence);
}
}
start_time = 0;
decode_timeout_calc = true;
baseband::request_beep_stop();
}
void MorseRadiotxView::writeCharToConsole(const std::string& ch, double confidence) {
if (ch.empty()) {
return;
}
txt_last.set(morse_decoder_.getLastSequence().c_str());
last_color_id = color_id;
std::string color = "";
if (ch == " ") {
color_id = 0;
} else if (ch[0] == '{') { // no match
color_id = 0;
} else {
if (confidence == 1.5)
color_id = 4;
else if (confidence < 0.8)
color_id = 1;
else if (confidence < 0.9)
color_id = 2;
else
color_id = 3;
}
color = arr_color[color_id];
last_color_id = color_id;
console_text.write(color + ch);
}
void MorseRadiotxView::ptt_button_visibility(bool hidden) {
bool hiddenorig = btn_ptt.hidden();
if (hiddenorig != hidden) {
btn_ptt.hidden(hidden);
if (!hidden) btn_ptt.focus();
if (hidden) {
// hack fix until widget hidig problem is not solved.
auto r = btn_ptt.screen_rect();
Painter p;
p.fill_rectangle_unrolled8(r, Theme::getInstance()->fg_light->background);
}
}
}
void MorseRadiotxView::ui_toggle() {
// 0=AM, 1=FM, 2=DSB, 3=USB, 4=LSB
if (transmit) {
options_mode.set_style(Theme::getInstance()->fg_dark);
options_mode.set_focusable(false);
tone_.set_style(Theme::getInstance()->fg_dark);
tone_.set_focusable(false);
wpm_.set_style(Theme::getInstance()->fg_dark);
wpm_.set_focusable(false);
btn_message.set_style(Theme::getInstance()->fg_dark);
btn_calls.set_style(Theme::getInstance()->fg_dark);
chk_trans.set_style(Theme::getInstance()->fg_dark);
chk_trans.set_focusable(false);
bandwidth.set_style(Theme::getInstance()->fg_dark);
bandwidth.set_focusable(false);
chk_callsgn.set_style(Theme::getInstance()->fg_dark);
chk_callsgn.set_focusable(false);
} else {
options_mode.set_style(Theme::getInstance()->bg_darker);
options_mode.set_focusable(true);
wpm_.set_style(Theme::getInstance()->bg_darker);
wpm_.set_focusable(true);
btn_message.set_style(Theme::getInstance()->bg_darker);
btn_calls.set_style(Theme::getInstance()->bg_darker);
chk_trans.set_style(Theme::getInstance()->bg_darker);
chk_trans.set_focusable(true);
chk_callsgn.set_style(Theme::getInstance()->bg_darker);
chk_callsgn.set_focusable(true);
ptt_button_visibility(true);
tone_.set_style(Theme::getInstance()->bg_darker);
tone_.set_focusable(true);
if (current_mode == 1) { // FM mode
bandwidth.set_style(Theme::getInstance()->bg_darker);
bandwidth.set_focusable(true);
} else {
bandwidth.set_style(Theme::getInstance()->fg_dark);
bandwidth.set_focusable(false);
}
} // transmit false
}
inline bool MorseRadiotxView::tx_button_held() {
const auto switches_state = get_switches_state();
return switches_state[(size_t)ui::KeyEvent::Select];
}
void MorseRadiotxView::on_framesync() {
if (button_was_selected && !button_touch && !tx_button_held()) {
button_was_selected = false;
onRelease();
}
if (end_time != 0 && decode_timeout_calc) {
int64_t gap_delta = (chTimeNow() - end_time);
if (gap_delta >= morse_decoder_.getInterCharThreshold()) {
auto result = morse_decoder_.handleInput(-(int32_t)gap_delta);
if (result.isValid()) {
writeCharToConsole(result.text, result.confidence);
}
}
if (gap_delta >= morse_decoder_.getInterWordThreshold()) {
writeCharToConsole(" ", 1.0);
end_time = 0;
decode_timeout_calc = false;
}
}
if (transmit_time != 0 && transmit) { // Tx disable if time is up
int64_t gap_delta = (chTimeNow() - transmit_time);
if (gap_delta >= ((morse_decoder_.getInterWordThreshold() * ((chk_trans.value()) ? 10 : 1)))) {
if (tx_thread) {
chThdTerminate(tx_thread);
chThdWait(tx_thread);
}
transmit = false;
tx_view.set_transmitting(false);
transmitter_model.disable();
thread_running = false;
tx_thread = nullptr;
transmit_time = 0;
if (!chk_trans.value()) writeCharToConsole(" ", 1.0);
ui_toggle();
}
}
}
} // namespace ui::external_app::morseradiotx
@@ -0,0 +1,171 @@
/*
* Copyright (C) 2026 Pezsma
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#ifndef __MORSE_RADIOTX_H__
#define __MORSE_RADIOTX_H__
#include "ui.hpp"
#include "ui_widget.hpp"
#include "ui_navigation.hpp"
#include "ui_language.hpp"
#include "ui_painter.hpp"
#include "ui_freq_field.hpp"
#include "ui_transmitter.hpp"
#include "ui_textentry.hpp"
#include "string_format.hpp"
#include "morsedecoder.hpp"
#include "irq_controls.hpp"
#include "radio_state.hpp"
#include "portapack.hpp"
#include "message.hpp"
#include "volume.hpp"
#include "audio.hpp"
#include "baseband_api.hpp"
#include "external_app.hpp"
#include "string_format.hpp"
#include <ch.h>
#include <hal.h>
namespace ui::external_app::morseradiotx {
class MorseRadiotxView : public ui::View {
public:
MorseRadiotxView(ui::NavigationView& nav);
~MorseRadiotxView();
MorseRadiotxView(const MorseRadiotxView&) = delete;
MorseRadiotxView(MorseRadiotxView&&) = delete;
MorseRadiotxView& operator=(const MorseRadiotxView&) = delete;
MorseRadiotxView& operator=(MorseRadiotxView&&) = delete;
std::string title() const override { return "Morse Tx"; }
void focus() override;
void on_show() override;
void transmit_morse_message();
private:
struct MorseTimings {
uint32_t dot_ms;
uint32_t dash_ms;
uint32_t symbol_gap;
uint32_t char_gap;
uint32_t word_gap;
};
MorseTimings current_timings{};
std::string msg_indicator{""};
MorseTimings calculate_morse_timings(uint32_t wpm);
void onPress();
void onRelease();
void on_framesync();
void writeCharToConsole(const std::string& ch, double confidence);
void ui_toggle();
bool tx_button_held();
void ptt_button_visibility(bool hidden);
ui::NavigationView& nav_;
MorseDecoder morse_decoder_{};
TxRadioState radio_state_{};
Thread* tx_thread{nullptr};
std::string msg_buffer{"PORTAPACK"};
uint8_t current_mode{0}; // 0=AM, 1=FM, 2=DSB, 3=USB, 4=LSB
uint8_t wpm{20};
uint32_t tone{700};
float band{5.8};
std::string call_sign{""};
app_settings::SettingsManager settings_{
"tx_morseradio",
app_settings::Mode::TX,
{
{"cmode"sv, &current_mode},
{"audtone"sv, &tone},
{"wpm"sv, &wpm},
{"message"sv, &msg_buffer},
{"bandwith"sv, &band},
{"call_sign"sv, &call_sign},
}};
TransmitterView tx_view{
(int16_t)UI_POS_Y_BOTTOM(4),
10000,
0, false};
AudioVolumeField field_volume{{UI_POS_X_RIGHT(2), UI_POS_Y_BOTTOM(5)}};
ui::OptionsField options_mode{
{UI_POS_X(5), UI_POS_Y(0)},
3,
{{"AM", 0}, {"FM", 1}, {"DSB", 2}, {"USB", 3}, {"LSB", 4}}};
NumberField tone_{{UI_POS_X(14), UI_POS_Y(0)}, 4, {400, 1400}, 10, ' ', true};
NumberField wpm_{{UI_POS_X(25), UI_POS_Y(0)}, 2, {10, 45}, 1, ' ', true};
FloatField bandwidth{{UI_POS_X(20), UI_POS_Y(3)}, 4, {1.0, 16.0}, 0.1, ' ', true, 1};
ui::Text txt_msg{{UI_POS_X(0), UI_POS_Y(1), UI_POS_MAXWIDTH, UI_POS_HEIGHT(1)}, "[" + msg_buffer + "] "};
ui::Button btn_message{{UI_POS_X(0), UI_POS_Y(2), UI_POS_WIDTH(11), UI_POS_HEIGHT(1)}, "Message"};
ui::Button btn_calls{{UI_POS_X(0), UI_POS_Y(4), UI_POS_WIDTH(11), UI_POS_HEIGHT(1)}, (call_sign.empty()) ? "call sign?" : call_sign};
Checkbox chk_trans{{UI_POS_X(14), UI_POS_Y(2)}, 13, "Manual trans.", true};
Checkbox chk_callsgn{{UI_POS_X(14), UI_POS_Y(4)}, 13, "Call sign", true};
ui::Text txt_last{{UI_POS_X(10), UI_POS_Y(5), UI_POS_WIDTH_REMAINING(10), UI_POS_HEIGHT(1)}, ""};
ui::Console console_text{{UI_POS_X(0), UI_POS_Y(7), UI_POS_MAXWIDTH, UI_POS_HEIGHT_REMAINING(14)}};
ui::Button btn_clear{{UI_POS_X(0), UI_POS_Y_BOTTOM(5), UI_POS_WIDTH(5), UI_POS_HEIGHT(1)}, "CLR"};
ui::Button btn_ptt{{UI_POS_X_CENTER(12), UI_POS_Y_BOTTOM(7), UI_POS_WIDTH(12), UI_POS_HEIGHT(3)}, "PTT"};
ui::Labels labels{
{{UI_POS_X(0), UI_POS_Y(0)}, "Mode:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(9), UI_POS_Y(0)}, "Tone:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(18), UI_POS_Y(0)}, "Hz", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(21), UI_POS_Y(0)}, "WPM:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(14), UI_POS_Y(3)}, "BandW:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(24), UI_POS_Y(3)}, "kHz", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(5)}, "Last seq:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(6)}, "Sent Message:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X_RIGHT(7), UI_POS_Y_BOTTOM(5)}, "Vol.:", Theme::getInstance()->fg_light->foreground},
};
uint8_t last_color_id{255};
uint8_t color_id{255};
std::string arr_color[4] = {STR_COLOR_WHITE, STR_COLOR_RED, STR_COLOR_YELLOW, STR_COLOR_GREEN};
bool button_touch{false};
bool button_was_selected{false};
bool decode_timeout_calc{false};
bool transmit{false};
bool thread_running = false;
uint8_t send_indicator{5};
int64_t start_time{0};
int64_t end_time{0};
int64_t transmit_time{0};
SwitchesState initial_switch_config_{};
MessageHandlerRegistration message_handler_framesync{
Message::ID::DisplayFrameSync,
[this](const Message* const p) {
(void)p;
this->on_framesync();
}};
};
} // namespace ui::external_app::morseradiotx
#endif // __MORSE_RADIOTX_H__
+18 -2
View File
@@ -91,6 +91,7 @@ SubCarView::SubCarView(NavigationView& nav)
&button_clear_list,
&check_log,
&labels,
&options_mode,
&recent_entries_view});
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
@@ -114,11 +115,17 @@ SubCarView::SubCarView(NavigationView& nav)
recent_entries_view.on_select = [this](const SubCarRecentEntry& entry) {
nav_.push<SubCarRecentEntryDetailView>(entry);
};
baseband::set_subghzd_config(0, receiver_model.sampling_rate()); // 0=am
receiver_model.enable();
options_mode.on_change = [this](size_t, int32_t v) {
modulation = v;
chThdSleepMilliseconds(100); // wait for the baseband thread to process the previous config, to avoid glitchy output when switching modes
baseband::set_subghzd_config(modulation, receiver_model.sampling_rate());
};
signal_token_tick_second = rtc_time::signal_tick_second += [this]() {
on_tick_second();
};
options_mode.set_selected_index(modulation, true);
receiver_model.enable();
}
void SubCarView::on_tick_second() {
@@ -176,6 +183,8 @@ const char* SubCarView::getSensorTypeName(FPROTO_SUBCAR_SENSOR type) {
return "Fiat V0";
case FPC_BMWV0:
return "BMW V0";
/* case FPC_KIAV6:
return "Kia V6";*/
case FPC_Invalid:
default:
@@ -491,6 +500,13 @@ void SubCarRecentEntryDetailView::parseProtocol() {
btn = to_string_dec_uint(button);
}
/*if (entry_.sensorType == FPC_KIAV6) {
// not decrypted!
serial = 0;
btn = "?";
cnt = 0;
}*/
return;
}
+19 -13
View File
@@ -116,25 +116,27 @@ class SubCarView : public View {
4'000'000 /* sampling rate */,
ReceiverModel::Mode::AMAudio};
bool logging = false;
uint8_t modulation = 0;
app_settings::SettingsManager settings_{
"rx_subcar",
app_settings::Mode::RX,
{
{"log"sv, &logging},
{"modulationmode"sv, &modulation},
}};
SubCarRecentEntries recent{};
RFAmpField field_rf_amp{
{13 * 8, UI_POS_Y(0)}};
{UI_POS_X(13), UI_POS_Y(0)}};
LNAGainField field_lna{
{15 * 8, UI_POS_Y(0)}};
{UI_POS_X(15), UI_POS_Y(0)}};
VGAGainField field_vga{
{18 * 8, UI_POS_Y(0)}};
{UI_POS_X(18), UI_POS_Y(0)}};
RSSI rssi{
{21 * 8, 0, UI_POS_WIDTH_REMAINING(24), 4}};
{UI_POS_X(21), 0, UI_POS_WIDTH_REMAINING(24), 4}};
Channel channel{
{21 * 8, 5, UI_POS_WIDTH_REMAINING(24), 4},
{UI_POS_X(21), 5, UI_POS_WIDTH_REMAINING(24), 4},
};
RxFrequencyField field_frequency{
{UI_POS_X(0), UI_POS_Y(0)},
@@ -143,18 +145,22 @@ class SubCarView : public View {
SignalToken signal_token_tick_second{};
Button button_clear_list{
{0, 16, 7 * 8, 32},
{UI_POS_X(0), UI_POS_Y(1), UI_POS_WIDTH(7), UI_POS_HEIGHT(2)},
"Clear"};
Checkbox check_log{
{10 * 8, 18},
{UI_POS_X(8), UI_POS_Y(1)},
3,
"Log",
true};
Labels labels{
{{UI_POS_X_RIGHT(14), UI_POS_Y(1)}, "no fm yet :(", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(15), UI_POS_Y(1)}, "Mode:", Theme::getInstance()->fg_light->foreground},
};
ui::OptionsField options_mode{
{UI_POS_X(22), UI_POS_Y(1)},
3,
{{"AM", 0}, {"FM", 1}}};
static constexpr auto header_height = 3 * 16;
@@ -198,16 +204,16 @@ class SubCarRecentEntryDetailView : public View {
std::string btn = "";
uint32_t cnt = SD_NO_CNT;
Text text_type{{UI_POS_X(0), 1 * 16, 15 * 8, 16}, "?"};
Text text_id{{6 * 8, 2 * 16, 10 * 8, 16}, "?"};
Text text_type{{UI_POS_X(0), UI_POS_Y(1), UI_POS_WIDTH(15), UI_POS_HEIGHT(1)}, "?"};
Text text_id{{UI_POS_X(6), UI_POS_Y(2), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)}, "?"};
Console console{
{0, 4 * 16, screen_width, screen_height - (4 * 16) - 36}};
{UI_POS_X(0), UI_POS_Y(4), UI_POS_MAXWIDTH, screen_height - (4 * 16) - 36}};
Labels labels{
{{UI_POS_X(0), UI_POS_Y(0)}, "Type:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), 2 * 16}, "Serial: ", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), 3 * 16}, "Data:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(2)}, "Serial: ", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(3)}, "Data:", Theme::getInstance()->fg_light->foreground},
};
Button button_done{
+2 -2
View File
@@ -122,7 +122,7 @@ options_t freqman_steps = {
{"1kHz ", 1000},
{"5kHz (SA AM)", 5000},
{"6.25kHz(NFM)", 6250},
{"8.33kHz(AIR)", 8330},
{"8.33kHz(AIR)", 8333},
{"9kHz (EU AM)", 9000},
{"10kHz(US AM)", 10000},
{"12.5kHz(NFM)", 12500},
@@ -144,7 +144,7 @@ options_t freqman_steps_short = {
{"1kHz", 1000},
{"5kHz", 5000},
{"6.25kHz", 6250},
{"8.33kHz", 8330},
{"8.33kHz", 8333},
{"9kHz", 9000},
{"10kHz", 10000},
{"12.5kHz", 12500},
+8
View File
@@ -37,6 +37,14 @@ void Debounce::enable_repeat() {
repeat_enabled_ = true;
}
void Debounce::set_enable_repeat(bool enabled) {
repeat_enabled_ = enabled;
}
bool Debounce::get_repeat_enabled() {
return repeat_enabled_;
}
bool Debounce::get_long_press_enabled() const {
return long_press_enabled_;
}
+2
View File
@@ -43,6 +43,8 @@ class Debounce {
bool feed(const uint8_t bit);
uint8_t state();
void enable_repeat();
void set_enable_repeat(bool enabled);
bool get_repeat_enabled();
bool get_long_press_enabled() const;
void set_long_press_enabled(bool v);
bool long_press_occurred();
+16
View File
@@ -250,6 +250,22 @@ SwitchesState get_switches_state() {
return result;
}
/* Gets the repeat enabled state for all the switches. */
SwitchesState get_switches_repeat_config() {
SwitchesState result;
for (size_t i = 0; i < result.size(); i++)
result[i] = switch_debounce[i].get_repeat_enabled();
return result;
}
/* Configures which switches support repeat.*/
void set_switches_repeat_config(SwitchesState switch_config) {
for (size_t i = 0; i < switch_config.size(); i++)
switch_debounce[i].set_enable_repeat(switch_config[i]);
}
/* Gets the long press enabled state for all the switches. */
SwitchesState get_switches_long_press_config() {
SwitchesState result;
+2
View File
@@ -48,6 +48,8 @@ uint8_t swizzled_switches();
SwitchesState get_switches_state();
EncoderPosition get_encoder_position();
touch::Frame get_touch_frame();
SwitchesState get_switches_repeat_config();
void set_switches_repeat_config(SwitchesState switch_config);
SwitchesState get_switches_long_press_config();
void set_switches_long_press_config(SwitchesState switch_config);
+55 -8
View File
@@ -31,20 +31,67 @@ using namespace ax25;
namespace aprs {
void make_aprs_frame(const char* src_address, const uint32_t src_ssid, const char* dest_address, const uint32_t dest_ssid, const std::string& payload) {
void make_aprs_frame(const char* src_address, const uint32_t src_ssid, const char* dest_address, const uint32_t dest_ssid, const std::string& payload, const std::string& path) {
AX25Frame frame;
char address[14] = {0};
memset(address, ' ', 14);
memcpy(&address[0], dest_address, 6);
memcpy(&address[7], src_address, 6);
size_t dest_len = strlen(dest_address);
if (dest_len > 6) dest_len = 6;
memcpy(&address[0], dest_address, dest_len);
size_t src_len = strlen(src_address);
if (src_len > 6) src_len = 6;
memcpy(&address[7], src_address, src_len);
// euquiq: According to ax.25 doc section 2.2.13.x.x and 2.4.1.2
// SSID need bits 5.6 set, so later when shifted it will end up being 011xxxx0 (xxxx = SSID number)
// Notice that if need to signal usage of AX.25 V2.0, (dest_ssid | 112); (MSb will need to be set at the end)
address[6] = (dest_ssid | 48);
address[13] = (src_ssid | 48);
// SSID need bits 5.6 set, so later when shifted it will end up being 011xxxx0 (xxxx = SSID number)
// Notice that if need to signal usage of AX.25 V2.0, (dest_ssid | 112); (MSb will need to be set at the end)
address[6] = (dest_ssid | 0x30);
address[13] = (src_ssid | 0x30);
frame.make_ui_frame(address, 0x03, protocol_id_t::NO_LAYER3, payload);
// fix path ssid bits. the result will be the same as above
std::string fixed_path = "";
const char* p = path.c_str();
while (*p) {
while (*p == ' ') p++;
if (!*p) break;
char call[6] = {' ', ' ', ' ', ' ', ' ', ' '};
int idx = 0;
while (*p && *p != '-' && *p != ',') {
if (*p != ' ') {
if (idx < 6) {
char c = *p;
if (c >= 'a' && c <= 'z') c -= 32;
call[idx++] = c;
}
}
p++;
}
int ssid = 0;
if (*p == '-') {
p++;
while (*p == ' ') p++;
while (*p >= '0' && *p <= '9') {
ssid = ssid * 10 + (*p - '0');
p++;
}
}
if (ssid >= 0 && ssid <= 15) {
for (int i = 0; i < 6; i++) fixed_path += call[i];
fixed_path += (char)(ssid | 0x30);
}
while (*p && *p != ',') p++;
if (*p == ',') p++;
}
frame.make_ui_frame(address, 0x03, protocol_id_t::NO_LAYER3, payload, fixed_path);
}
} /* namespace aprs */
+2 -1
View File
@@ -33,7 +33,8 @@ void make_aprs_frame(
const uint32_t src_ssid,
const char* dest_address,
const uint32_t dest_ssid,
const std::string& payload);
const std::string& payload,
const std::string& path = "");
} /* namespace aprs */
+21 -13
View File
@@ -26,13 +26,16 @@
namespace ax25 {
void AX25Frame::make_extended_field(char* const data, size_t length) {
void AX25Frame::make_extended_field(char* const data, size_t length, bool is_last) {
size_t i = 0;
for (i = 0; i < length - 1; i++)
add_data(data[i] << 1);
add_data((data[i] << 1) | 1);
if (is_last)
add_data((data[i] << 1) | 1);
else
add_data((data[i] << 1));
}
void AX25Frame::NRZI_add_bit(const uint32_t bit) {
@@ -92,7 +95,7 @@ void AX25Frame::add_checksum() {
add_byte(checksum >> 8, false, false);
}
void AX25Frame::make_ui_frame(char* const address, const uint8_t control, const uint8_t protocol, const std::string& info) {
void AX25Frame::make_ui_frame(char* const address, const uint8_t control, const uint8_t protocol, const std::string& info, const std::string& path) {
size_t i;
bb_data_ptr = (uint16_t*)shared_memory.bb_data.data;
@@ -103,22 +106,27 @@ void AX25Frame::make_ui_frame(char* const address, const uint8_t control, const
ones_counter = 0;
crc_ccitt.reset();
add_flag();
add_flag();
add_flag();
add_flag();
add_flag(); // 0x73
add_flag(); // 0x73
add_flag(); // 0x73
add_flag(); // 0x73
make_extended_field(address, 14);
add_data(control);
add_data(protocol);
// path must not contain the - character! just the callsign and SSID next to each other
bool has_path = (path.size() > 0 && path.size() <= 63 && path.size() % 7 == 0);
make_extended_field(address, 14, !has_path);
if (has_path) {
make_extended_field(const_cast<char*>(path.data()), path.size(), true);
}
add_data(control); // 0x03
add_data(protocol); // 0xf0
for (i = 0; i < info.size(); i++)
add_data(info[i]);
add_checksum();
add_checksum(); // fcs
add_flag();
add_flag();
add_flag(); // 0x73
add_flag(); // 0x73
flush();
}
+2 -2
View File
@@ -43,11 +43,11 @@ enum protocol_id_t {
class AX25Frame {
public:
void make_ui_frame(char* const address, const uint8_t control, const uint8_t protocol, const std::string& info);
void make_ui_frame(char* const address, const uint8_t control, const uint8_t protocol, const std::string& info, const std::string& path = "");
private:
void NRZI_add_bit(const uint32_t bit);
void make_extended_field(char* const data, size_t length);
void make_extended_field(char* const data, size_t length, bool is_last = false);
void add_byte(uint8_t byte, bool is_flag, bool is_data);
void add_data(uint8_t byte);
void add_checksum();
+7 -1
View File
@@ -657,7 +657,13 @@ void GeoMap::move(const float lon, const float lat) {
} else {
if (is_changed) {
set_osm_max_zoom();
redraw_map = true;
double global_center_px = lon_to_pixel_x_tile(lon_, map_osm_real_zoom);
double global_center_py = lat_to_pixel_y_tile(lat_, map_osm_real_zoom);
// Redraw if viewport moved by at least 1 pixel (includes zoom level changes)
if (abs(global_center_px - (r.width() / 2.0) - viewport_top_left_px) >= 1.0 ||
abs(global_center_py - (r.height() / 2.0) - viewport_top_left_py) >= 1.0) {
redraw_map = true;
}
}
}
}
+8 -2
View File
@@ -26,6 +26,7 @@
#include "string_format.hpp"
#include "ui_receiver.hpp"
#include "ui_freqman.hpp"
#include "audio.hpp"
using namespace portapack;
@@ -591,8 +592,13 @@ AudioVolumeField::AudioVolumeField(
/* fill char */ ' '} {
set_value(receiver_model.normalized_headphone_volume());
on_change = [](int32_t v) {
receiver_model.set_normalized_headphone_volume(v);
on_change = [](int32_t vol) {
// don't call receiver model, because this widget shuld be able to handle volume settinsg from any app, regardless of the receiver model's enables state. like the tx apps should be able to set volume too.
// this is identical to the receiver model's method
uint8_t v = clip<uint8_t>(vol, 0, 99);
auto new_volume = volume_t::decibel(v - 99) + audio::headphone::volume_range().max;
persistent_memory::set_headphone_volume(new_volume);
audio::headphone::set_volume(new_volume);
};
}
+1 -1
View File
@@ -607,7 +607,7 @@ bool InformationView::firmware_checksum_error() {
// only checking firmware checksum once per boot
if (!fw_checksum_checked) {
fw_checksum_error = (simple_checksum(FLASH_STARTING_ADDRESS, FLASH_ROM_SIZE) != FLASH_EXPECTED_CHECKSUM);
fw_checksum_error = (simple_checksum(FLASH_STARTING_ADDRESS, FLASH_SIZE_LIMIT_MB * 1024 * 1024) != FLASH_EXPECTED_CHECKSUM);
}
return fw_checksum_error;
}
+21
View File
@@ -1383,6 +1383,25 @@ static void cmd_getres(BaseSequentialStream* chp, int argc, char* argv[]) {
chprintf(chp, res.c_str());
}
static void cmd_getflash(BaseSequentialStream* chp, int argc, char* argv[]) {
(void)argc;
(void)argv;
uint8_t allowrun = FLASH_SIZE_MB;
if (portapack::device_type == portapack::DeviceType::DEV_PORTAPACK) {
allowrun = 1;
}
std::string res = "ALLOWEDFW:" + to_string_dec_uint(FLASH_SIZE_MB) + "\r\nALLOWEDRUNTIME:" + to_string_dec_uint(allowrun) + "\r\nCURRENT:" + to_string_dec_uint(FLASH_SIZE_LIMIT_MB) + "\r\nok\r\n";
chprintf(chp, res.c_str());
}
static void cmd_getdevtype(BaseSequentialStream* chp, int argc, char* argv[]) {
(void)argc;
(void)argv;
std::string res = portapack::device_type == portapack::DeviceType::DEV_PORTARF ? "PORTARF" : "PORTAPACK";
res += "\r\nok\r\n";
chprintf(chp, res.c_str());
}
static const ShellCommand commands[] = {
{"reboot", cmd_reboot},
{"dfu", cmd_dfu},
@@ -1419,6 +1438,8 @@ static const ShellCommand commands[] = {
{"asyncmsg", cmd_asyncmsg},
{"setfreq", cmd_setfreq},
{"getres", cmd_getres},
{"getflash", cmd_getflash},
{"getdevtype", cmd_getdevtype},
{NULL, NULL}};
static const ShellConfig shell_cfg1 = {
+13 -4
View File
@@ -618,12 +618,12 @@ set(MODE_CPPSRC
)
DeclareTargets(PADT adsbtx)
### OOKStream
### Binary Timed stream TX
set(MODE_CPPSRC
proc_ook_stream_tx.cpp
proc_bint_stream_tx.cpp
)
DeclareTargets(POSK ookstream)
DeclareTargets(POSK bintstream)
### Signal generator
@@ -663,9 +663,10 @@ set(MODE_CPPSRC
)
DeclareTargets(PSCD subcar)
### Morse RX Decoder
set(MODE_CPPSRC
proc_morse.cpp
)
DeclareTargets(PMRS morse)
@@ -676,6 +677,14 @@ set(MODE_INCDIR
${HACKRF_PATH}/firmware/common
${HACKRF_PATH}/firmware/libopencm3/include
)
### Morse TX
set(MODE_CPPSRC
proc_morsetx.cpp
)
DeclareTargets(PMRT morsetx)
set(MODE_CPPSRC
sd_over_usb/proc_sd_over_usb.cpp
+204
View File
@@ -0,0 +1,204 @@
#pragma once
#include "subcarbase.hpp"
#include <cstring>
typedef enum {
KiaV6DecoderStepReset = 0,
KiaV6DecoderStepWaitFirstHigh,
KiaV6DecoderStepCountPreamble,
KiaV6DecoderStepWaitLongHigh,
KiaV6DecoderStepData,
} KiaV6DecoderStep;
#define KIA_V6_XOR_MASK_LOW 0x84AF25FB
#define KIA_V6_XOR_MASK_HIGH 0x638766AB
class FProtoSubCarKiaV6 : public FProtoSubCarBase {
public:
FProtoSubCarKiaV6() {
sensorType = FPC_KIAV6;
te_short = 200;
te_long = 400;
te_delta = 100;
min_count_bit_for_found = 144;
}
void feed(bool level, uint32_t duration) {
uint32_t uVar4, uVar5;
ManchesterEvent event;
bool data_bit;
uint8_t bit_count_inc;
uint32_t step_value;
switch (parser_step) {
case KiaV6DecoderStepReset: // case 0
if (level == 0) {
return;
}
if (DURATION_DIFF(duration, te_short) <
te_delta) {
parser_step = KiaV6DecoderStepWaitFirstHigh;
te_last = duration;
header_count = 0;
FProtoGeneral::manchester_advance(
manchester_state,
ManchesterEventReset,
&manchester_state,
NULL);
}
return;
case KiaV6DecoderStepWaitFirstHigh: { // case 1
if (level != 0) {
return;
}
uint32_t diff_short = DURATION_DIFF(duration, te_short);
uint32_t diff_long = DURATION_DIFF(duration, te_long);
uint32_t diff = (diff_long < diff_short) ? diff_long : diff_short;
if (diff_long < te_delta && diff_long < diff_short) {
if (header_count >= 0x259) { // 601 decimal
header_count = 0;
te_last = duration;
parser_step = KiaV6DecoderStepWaitLongHigh;
return;
}
}
if (diff >= te_delta) {
step_value = KiaV6DecoderStepReset;
goto LAB_reset;
}
if (DURATION_DIFF(te_last, te_short) <
te_delta) {
te_last = duration;
header_count++;
return;
} else {
step_value = KiaV6DecoderStepReset;
goto LAB_reset;
}
}
case KiaV6DecoderStepWaitLongHigh: { // case 2
if (level == 0) {
step_value = KiaV6DecoderStepReset;
goto LAB_reset;
}
uint32_t diff_long_check = DURATION_DIFF(duration, te_long);
uint32_t diff_short_check = DURATION_DIFF(duration, te_short);
if (diff_long_check >= te_delta) {
if (diff_short_check >= te_delta) {
step_value = KiaV6DecoderStepReset;
goto LAB_reset;
}
}
if (DURATION_DIFF(te_last, te_long) >=
te_delta) {
step_value = KiaV6DecoderStepReset;
goto LAB_reset;
}
decode_data = 0;
decode_count_bit = 0;
subghz_protocol_blocks_add_bit(1);
subghz_protocol_blocks_add_bit(1);
subghz_protocol_blocks_add_bit(0);
subghz_protocol_blocks_add_bit(1);
data_part1_low = (uint32_t)(decode_data & 0xFFFFFFFF);
data_part1_high = (uint32_t)((decode_data >> 32) & 0xFFFFFFFF);
bit_count = decode_count_bit;
parser_step = KiaV6DecoderStepData;
return;
}
case KiaV6DecoderStepData: // case 3
if (DURATION_DIFF(duration, te_short) <
te_delta) {
event = (ManchesterEvent)((level & 0x7F) << 1);
goto manchester_process;
} else if (
DURATION_DIFF(duration, te_long) <
te_delta) {
event = (ManchesterEvent)(level ? 6 : 4);
goto manchester_process;
}
step_value = KiaV6DecoderStepReset;
goto LAB_reset;
manchester_process:
if (FProtoGeneral::manchester_advance(
manchester_state, event, &manchester_state, &data_bit)) {
uVar4 = data_part1_low;
uVar5 = (uVar4 << 1) | (data_bit ? 1 : 0);
uint32_t carry = (uVar4 >> 31) & 1;
uVar4 = (data_part1_high << 1) | carry;
data_part1_low = uVar5;
data_part1_high = uVar4;
decode_data = ((uint64_t)uVar4 << 32) | uVar5;
bit_count_inc = bit_count + 1;
bit_count = bit_count_inc;
if (bit_count_inc == 0x40) {
// stored_part1_low = ~uVar5;
// stored_part1_high = ~uVar4;
data_part1_low = 0;
data_part1_high = 0;
} else if (bit_count_inc == 0x80) {
// stored_part2_low = ~uVar5;
// stored_part2_high = ~uVar4;
data_part1_low = 0;
data_part1_high = 0;
}
}
te_last = duration;
if (bit_count != min_count_bit_for_found) {
return;
}
data_count_bit = min_count_bit_for_found;
// data_part3 = ~((uint16_t)data_part1_low);
// kia_v6_decrypt(); --won't
decode_data = data_part1_low | ((uint64_t)data_part1_high << 32);
if (callback) {
callback(this);
}
data_part1_low = 0;
data_part1_high = 0;
bit_count = 0;
step_value = KiaV6DecoderStepReset;
goto LAB_reset;
default:
return;
}
LAB_reset:
parser_step = step_value;
return;
}
uint8_t bit_count = 0;
uint16_t header_count = 0;
ManchesterState manchester_state = ManchesterStateMid1;
uint32_t data_part1_low = 0;
uint32_t data_part1_high = 0;
// uint32_t stored_part1_low = 0;
// uint32_t stored_part1_high = 0;
// uint32_t stored_part2_low = 0;
// uint32_t stored_part2_high = 0;
// uint16_t data_part3 = 0;
};
@@ -0,0 +1,84 @@
#ifndef __FPROTO_HOLTEKHT6P20B_H__
#define __FPROTO_HOLTEKHT6P20B_H__
#include "subghzdbase.hpp"
typedef enum : uint8_t {
Holtek_HT6P20BDecoderStepReset = 0,
Holtek_HT6P20BDecoderStepFoundStartBit,
Holtek_HT6P20BDecoderStepSaveDuration,
Holtek_HT6P20BDecoderStepCheckDuration,
} Holtek_HT6P20BDecoderStep;
class FProtoSubGhzDHoltekHt6p20b : public FProtoSubGhzDBase {
public:
FProtoSubGhzDHoltekHt6p20b() {
sensorType = FPS_HOLTEKHT6P20B;
te_short = 450;
te_long = 900;
te_delta = 270;
min_count_bit_for_found = 28;
}
void feed(bool level, uint32_t duration) {
switch (parser_step) {
case Holtek_HT6P20BDecoderStepReset:
if ((!level) && (DURATION_DIFF(duration, te_short * 23) < te_delta * 23)) {
// Found Preambula
parser_step = Holtek_HT6P20BDecoderStepFoundStartBit;
}
break;
case Holtek_HT6P20BDecoderStepFoundStartBit:
if ((level) && (DURATION_DIFF(duration, te_short) < te_delta)) {
// Found StartBit
parser_step = Holtek_HT6P20BDecoderStepSaveDuration;
decode_data = 0;
decode_count_bit = 0;
} else {
parser_step = Holtek_HT6P20BDecoderStepReset;
}
break;
case Holtek_HT6P20BDecoderStepSaveDuration:
// save duration
if (!level) {
if (duration >= ((uint32_t)te_short * 10 + te_delta)) {
if (decode_count_bit == min_count_bit_for_found) {
data_count_bit = decode_count_bit;
if (callback) callback(this);
}
decode_data = 0;
decode_count_bit = 0;
parser_step = Holtek_HT6P20BDecoderStepFoundStartBit;
break;
} else {
te_last = duration;
parser_step = Holtek_HT6P20BDecoderStepCheckDuration;
}
} else {
parser_step = Holtek_HT6P20BDecoderStepReset;
}
break;
case Holtek_HT6P20BDecoderStepCheckDuration:
if (level) {
if ((DURATION_DIFF(te_last, te_long) < te_delta * 2) &&
(DURATION_DIFF(duration, te_short) < te_delta)) {
subghz_protocol_blocks_add_bit(1);
parser_step = Holtek_HT6P20BDecoderStepSaveDuration;
} else if (
(DURATION_DIFF(te_last, te_short) < te_delta) &&
(DURATION_DIFF(duration, te_long) < te_delta * 2)) {
subghz_protocol_blocks_add_bit(0);
parser_step = Holtek_HT6P20BDecoderStepSaveDuration;
} else {
parser_step = Holtek_HT6P20BDecoderStepReset;
}
} else {
parser_step = Holtek_HT6P20BDecoderStepReset;
}
break;
}
}
};
#endif
+1 -1
View File
@@ -76,7 +76,7 @@ class FProtoSubGhzDSecPlusV1 : public FProtoSubGhzDBase {
packet_accepted |= SECPLUS_V1_PACKET_2_ACCEPTED;
if (packet_accepted == (SECPLUS_V1_PACKET_1_ACCEPTED | SECPLUS_V1_PACKET_2_ACCEPTED)) {
// subghz_protocol_secplus_v1_decode(); // disabled doe to lack of flash
// subghz_protocol_secplus_v1_decode(); // disabled due to lack of flash
// controller
// uint32_t fixed = (data >> 32) & 0xFFFFFFFF;
// cnt = data & 0xFFFFFFFF;
@@ -21,6 +21,7 @@ So include here the .hpp, and add a new element to the protos vector in the cons
#include "c-ford_v0.hpp"
#include "c-fiat_v0.hpp"
#include "c-bmw_v0.hpp"
// #include "c-kia_v6.hpp"
#ifndef __FPROTO_PROTOLISTCAR_H__
#define __FPROTO_PROTOLISTCAR_H__
@@ -42,6 +43,7 @@ class SubCarProtos : public FProtoListGeneral {
protos[FPC_FORDV0] = new FProtoSubCarFordV0();
protos[FPC_FIATV0] = new FProtoSubCarFiatV0();
protos[FPC_BMWV0] = new FProtoSubCarBMWV0();
// protos[FPC_KIAV6] = new FProtoSubCarKiaV6(); //-- disabled, due to whole encrypted.
for (uint8_t i = 0; i < FPC_COUNT; ++i) {
if (protos[i] != NULL) protos[i]->setCallback(callbackTarget);
+2 -3
View File
@@ -8,9 +8,6 @@ These values must be present on the protocol's constructor, like FProtoWeatherAc
Also it must have a switch-case element in the getSubGhzDSensorTypeName() function, to display it's name.
*/
#define FPM_AM 0
#define FPM_FM 1
enum FPROTO_SUBCAR_SENSOR : uint8_t {
FPC_Invalid = 0,
FPC_SUZUKI = 1,
@@ -24,6 +21,8 @@ enum FPROTO_SUBCAR_SENSOR : uint8_t {
FPC_FORDV0 = 9,
FPC_FIATV0 = 10,
FPC_BMWV0 = 11,
// FPC_KIAV6 = 12, //disabled, due to whole encrypted.
// FPC_PSA = 13, // IS whole encrypted
FPC_COUNT
};
@@ -54,6 +54,7 @@ So include here the .hpp, and add a new element to the protos vector in the cons
#include "s-gangqi.hpp"
#include "s-marantec24.hpp"
// GENIE FROM PR
#include "s-holtek_ht6p20b.hpp"
#ifndef __FPROTO_PROTOLISTSGZ_H__
#define __FPROTO_PROTOLISTSGZ_H__
@@ -107,6 +108,7 @@ class SubGhzDProtos : public FProtoListGeneral {
protos[FPS_LEGRAND] = new FProtoSubGhzDLegrand();
protos[FPS_GANGQI] = new FProtoSubGhzDGangqi();
protos[FPS_MARANTEC24] = new FProtoSubGhzDMarantec24();
protos[FPS_HOLTEKHT6P20B] = new FProtoSubGhzDHoltekHt6p20b();
for (uint8_t i = 0; i < FPS_COUNT; ++i) {
if (protos[i] != NULL) protos[i]->setCallback(callbackTarget);
@@ -58,6 +58,7 @@ enum FPROTO_SUBGHZD_SENSOR : uint8_t {
FPS_SOMIFY_TELIS,
FPS_GANGQI,
FPS_MARANTEC24,
FPS_HOLTEKHT6P20B,
FPS_COUNT
};
@@ -19,7 +19,7 @@
* Boston, MA 02110-1301, USA.
*/
#include "proc_ook_stream_tx.hpp"
#include "proc_bint_stream_tx.hpp"
#include "sine_table_int8.hpp"
#include "portapack_shared_memory.hpp"
@@ -27,27 +27,35 @@
#include "utility.hpp"
OOKProcessorStreamed::OOKProcessorStreamed() {
BinaryTimedProcessorStreamed::BinaryTimedProcessorStreamed() {
configured = false;
baseband_thread.start();
}
inline void OOKProcessorStreamed::write_sample(const buffer_c8_t& buffer, bool bit_value, size_t i) {
int8_t re, im;
if (bit_value) {
phase = (phase + 200); // What ?
inline void BinaryTimedProcessorStreamed::write_sample(const buffer_c8_t& buffer, bool bit_value, size_t i) {
int8_t re = 0, im = 0;
if (mode == 0) {
if (bit_value) {
phase = (phase + 200);
sphase = phase + (64 << 18);
re = (sine_table_i8[(sphase & 0x03FC0000) >> 18]);
im = (sine_table_i8[(phase & 0x03FC0000) >> 18]);
}
} else if (mode == 1) {
// calculate the re, im based on the deviation uint32_t variable to get the re, im, to send out 2fsk signal. based on the bit_value
if (bit_value) {
phase += deviation_delta;
} else {
phase -= deviation_delta;
}
sphase = phase + (64 << 18);
re = (sine_table_i8[(sphase & 0x03FC0000) >> 18]);
im = (sine_table_i8[(phase & 0x03FC0000) >> 18]);
} else {
re = 0;
im = 0;
}
buffer.p[i] = {re, im};
}
void OOKProcessorStreamed::execute(const buffer_c8_t& buffer) {
void BinaryTimedProcessorStreamed::execute(const buffer_c8_t& buffer) {
if (!configured || !stream) return;
for (size_t i = 0; i < buffer.count; i++) {
@@ -87,7 +95,7 @@ void OOKProcessorStreamed::execute(const buffer_c8_t& buffer) {
}
}
void OOKProcessorStreamed::on_message(const Message* const message) {
void BinaryTimedProcessorStreamed::on_message(const Message* const message) {
switch (message->id) {
case Message::ID::ReplayConfig:
configured = false;
@@ -100,12 +108,23 @@ void OOKProcessorStreamed::on_message(const Message* const message) {
shared_memory.application_queue.push(txprogress_message);
break;
case Message::ID::StreamTXConfiguration:
streamtx_config(*reinterpret_cast<const StreamTXConfigurationMessage*>(message));
break;
default:
break;
}
}
void OOKProcessorStreamed::replay_config(const ReplayConfigMessage& message) {
void BinaryTimedProcessorStreamed::streamtx_config(const StreamTXConfigurationMessage& message) {
mode = message.mode;
deviation = message.deviation;
uint64_t big_calc = (uint64_t)message.deviation << 26;
deviation_delta = (uint32_t)(big_calc / OOK_SAMPLERATE);
}
void BinaryTimedProcessorStreamed::replay_config(const ReplayConfigMessage& message) {
if (message.config) {
txprogress_message.progress = -2;
shared_memory.application_queue.push(txprogress_message);
@@ -121,7 +140,7 @@ void OOKProcessorStreamed::replay_config(const ReplayConfigMessage& message) {
}
int main() {
EventDispatcher event_dispatcher{std::make_unique<OOKProcessorStreamed>()};
EventDispatcher event_dispatcher{std::make_unique<BinaryTimedProcessorStreamed>()};
event_dispatcher.run();
return 0;
}
@@ -34,10 +34,11 @@
#include <memory>
#define OOK_SAMPLERATE 2280000U
#define FM_DEVIATION 60000U
class OOKProcessorStreamed : public BasebandProcessor {
class BinaryTimedProcessorStreamed : public BasebandProcessor {
public:
OOKProcessorStreamed();
BinaryTimedProcessorStreamed();
void execute(const buffer_c8_t& buffer) override;
void on_message(const Message* const message) override;
@@ -53,6 +54,11 @@ class OOKProcessorStreamed : public BasebandProcessor {
std::unique_ptr<StreamOutput> stream{};
bool configured{false};
void replay_config(const ReplayConfigMessage& message);
void streamtx_config(const StreamTXConfigurationMessage& message);
uint8_t mode = 0; // am = 0, 2fsk = 1
uint32_t deviation = FM_DEVIATION; // used in 2fsk
uint32_t deviation_delta = (FM_DEVIATION * 4294967296ULL) / OOK_SAMPLERATE;
int32_t endsignals[3] = {0, 42069, 613379}; // 0 is skipped, count from 1, don't ask...
uint8_t readerrs = 0; // to count in the array
+3 -1
View File
@@ -28,8 +28,10 @@
#include "debug.hpp"
#include "portapack_shared_memory.hpp"
#include "../flashsize.h"
#define PAGE_LEN 256U
#define NUM_PAGES 4096U
#define NUM_PAGES (4096U * FLASH_SIZE_MB)
void initialize_flash();
void erase_flash();
+262 -134
View File
@@ -25,186 +25,314 @@
#include "event_m4.hpp"
#include <cmath>
void MorseProcessor::configure() {
void MorseProcessor::configure(uint8_t mode) {
configured = false;
baseband_fs = 3072000;
baseband_thread.set_sampling_rate(baseband_fs);
// 1. DSP filters
decim_0.configure(taps_11k0_decim_0.taps);
decim_1.configure(taps_11k0_decim_1.taps);
channel_filter.configure(taps_11k0_channel.taps, 2);
demod.configure(24000, 5000);
audio_output.configure(iir_config_passthrough, iir_config_passthrough, 0.0f);
modulation = static_cast<ModulationMode>((uint8_t)mode);
// 2. Resetting variables
dc_offset = 0;
lpf_sample = 0;
prev_sample = 0;
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 {
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);
}
}
// 3. Algorithm reset
zc_counter = 0;
last_zc_counter = 0;
current_freq = 700.0f;
update_goertzel_coeff(700.0f);
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);
goertzel_count = 0;
meas_samples_in_period = 0;
meas_last_period_len = 0;
meas_consistency_count = 0;
meas_signal_state_high = false;
meas_freq_accumulator = 0.0f;
meas_freq_count = 0;
ui_update_timer = 0;
// Decoding reset
s_prev_i = 0;
s_prev2_i = 0;
goertzel_count = 0;
duration_samples = 0;
was_signaling = false;
noise_floor = 5000; // learning speed
// Thresholds
noise_floor = 5000;
startup_delay = 20;
squelch_is_open = true;
squelch_hold = 0;
dc_average = 0.0f;
current_freq = 700.0f;
update_goertzel_coeff(current_freq);
squelch_is_open = false;
configured = true;
}
void MorseProcessor::on_message(const Message* const p) {
if (p->id == Message::ID::MorseRXConfig) {
configure();
} else if (p->id == Message::ID::NBFMConfigure) {
auto nbfm_msg = *reinterpret_cast<const NBFMConfigureMessage*>(p);
user_squelch_level = nbfm_msg.squelch_level;
audio_output.configure(iir_config_passthrough, iir_config_passthrough, (float)user_squelch_level / 100.0f);
inline buffer_f32_t MorseProcessor::demodulate(const buffer_c16_t& channel) {
// AM,SSB,DSB always keeps squelch "technically" open for the demodulator
if (modulation == ModulationMode::AM || modulation == ModulationMode::DSB) {
squelch_is_open = true;
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);
}
void MorseProcessor::update_goertzel_coeff(float freq) {
if (freq < 400.0f) freq = 400.0f;
if (freq > 1500.0f) freq = 1500.0f; // limit to algo capacity min/max
float omega = 2.0f * M_PI * freq / 24000.0f;
coeff_int = (int32_t)(2.0f * cosf(omega) * 16384.0f);
// limit to algo capacity min/max
if (freq < 300.0f) freq = 300.0f;
if (freq > 2300.0f) freq = 2300.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);
// 16384 is the scaling factor for the Goertzel integer math
coeff_int = (int32_t)(2.0f * cos_approx * 16384.0f);
}
void MorseProcessor::measure_frequency(int32_t sample) {
// Noise gate threshold
const int32_t gate_threshold = (modulation == ModulationMode::FM) ? 4000 : 2000;
if (sample > gate_threshold || sample < -gate_threshold) {
if (sample > 0 && !meas_signal_state_high) {
// Rising Edge (End of Period)
meas_signal_state_high = true;
if (meas_samples_in_period > 0) {
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 <= 2) {
meas_consistency_count++;
period_is_stable = true;
} else {
meas_consistency_count = 0;
}
}
meas_last_period_len = meas_samples_in_period;
// Wait for AT LEAST 3 STABLE CYCLES
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;
meas_freq_count++;
}
}
}
meas_samples_in_period = 0;
} else if (sample < 0) {
meas_signal_state_high = false;
}
} else {
// Silence detection
if (meas_samples_in_period > 200) {
meas_last_period_len = 0;
meas_consistency_count = 0;
}
}
meas_samples_in_period++;
ui_update_timer++;
uint32_t update_limit = (modulation == ModulationMode::FM) ? 4800 : 2400; // ~200ms
if (ui_update_timer > update_limit) {
if (meas_freq_count > 0) {
float avg_freq = meas_freq_accumulator / (float)meas_freq_count;
current_freq = avg_freq;
// Round to 5Hz for display
uint32_t stable_disp = (uint32_t)avg_freq;
stable_disp = (stable_disp / 5) * 5;
freq_message.measured_frequency = stable_disp;
shared_memory.application_queue.push(freq_message);
}
meas_freq_accumulator = 0.0f;
meas_freq_count = 0;
ui_update_timer = 0;
}
}
void MorseProcessor::process_decoding(int32_t sample) {
update_goertzel_coeff(current_freq);
// 1. Goertzel Algorithm
int64_t s = (int64_t)sample + (((int64_t)coeff_int * s_prev_i) >> 14) - s_prev2_i;
s_prev2_i = s_prev_i;
s_prev_i = (int32_t)s;
goertzel_count++;
// 2. Evaluation every 60 samples
if (goertzel_count >= 60) {
if (startup_delay > 0) {
startup_delay--;
// Fast learning phase
int64_t pwr = (int64_t)s_prev_i * s_prev_i + (int64_t)s_prev2_i * s_prev2_i -
(((int64_t)s_prev_i * s_prev2_i * coeff_int) >> 14);
noise_floor = (noise_floor * 15 + pwr) / 16;
} else {
// Power calculation
int64_t power = (int64_t)s_prev_i * s_prev_i + (int64_t)s_prev2_i * s_prev2_i -
(((int64_t)s_prev_i * s_prev2_i * coeff_int) >> 14);
// Adaptive Noise Floor
if (!was_signaling) {
noise_floor = (noise_floor * 127 + power) / 128;
}
// Threshold Calculation
int64_t sensitivity = 4 + (user_squelch_level / 10);
int64_t base_pwr_threshold = noise_floor * sensitivity;
int64_t current_pwr_threshold = was_signaling ? (base_pwr_threshold / 2) : base_pwr_threshold;
// Tone Detection
bool is_tone = squelch_is_open && (power > current_pwr_threshold) && (power > 150000);
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);
// Send message if significant
if (duration_us > 10000 || was_signaling) {
message.state_durations[0] = was_signaling ? duration_us : -duration_us;
message.state_cnt = 1; // 1 indicates valid state duration data
shared_memory.application_queue.push(message);
}
was_signaling = is_tone;
duration_samples = 0;
}
// Timeout Logic
if (!was_signaling && duration_samples > 28800) {
int32_t duration_us = (int32_t)((int64_t)duration_samples * time_base / 3);
message.state_durations[0] = -duration_us;
message.state_cnt = 1;
shared_memory.application_queue.push(message);
duration_samples = 0;
}
}
duration_samples += 60;
s_prev_i = 0;
s_prev2_i = 0;
goertzel_count = 0;
}
}
void MorseProcessor::execute(const buffer_c8_t& buffer) {
if (!configured) return;
buffer_c16_t dst_buffer_c16(dst_buffer.data(), dst_buffer.size());
const auto decim_0_out = decim_0.execute(buffer, dst_buffer_c16);
const auto decim_1_out = decim_1.execute(decim_0_out, dst_buffer_c16);
const auto channel = channel_filter.execute(decim_1_out, dst_buffer_c16);
const auto decim_0_out = decim_0.execute(buffer, dst_buffer);
const auto decim_1_out = decim_1.execute(decim_0_out, dst_buffer);
const auto channel_out = channel_filter.execute(decim_1_out, dst_buffer);
feed_channel_stats(channel);
buffer_s16_t audio_buffer_s16(audio_buffer.data(), audio_buffer.size());
auto audio_buf = demod.execute(channel, audio_buffer_s16);
auto audio_buf = demodulate(channel_out);
for (size_t i = 0; i < audio_buf.count; i++) {
int32_t raw_sample = audio_buf.p[i];
float raw_audio = audio_buf.p[i];
// 1. DC & LPF
dc_offset += (raw_sample - dc_offset) / 32;
int32_t sample = raw_sample - dc_offset;
lpf_sample = (lpf_sample * 3 + sample) / 4;
// Squelch Logic
int32_t raw_int_abs = (int32_t)(raw_audio * 32768.0f);
if (raw_int_abs < 0) raw_int_abs = -raw_int_abs;
// 2. Squelch
int32_t abs_sample = (sample < 0) ? -sample : sample;
int32_t audio_threshold = (user_squelch_level * user_squelch_level) * 3;
int32_t current_audio_threshold = squelch_is_open ? (audio_threshold / 2) : audio_threshold;
if (abs_sample > current_audio_threshold || user_squelch_level == 0) {
if (raw_int_abs > audio_threshold || user_squelch_level == 0) {
squelch_is_open = true;
squelch_hold = 2400;
squelch_hold = (modulation == ModulationMode::FM) ? 2400 : 1200;
} else {
if (squelch_hold > 0)
squelch_hold--;
else
else if (modulation == ModulationMode::FM)
squelch_is_open = false;
}
// 3. Frequency measurement (ZC)
if (squelch_is_open && !was_signaling) {
if ((lpf_sample >= 0 && prev_sample < 0) || (lpf_sample < 0 && prev_sample >= 0)) {
if (zc_counter >= 8 && zc_counter <= 32) {
int32_t diff = zc_counter - last_zc_counter;
if (diff >= -1 && diff <= 1) {
float n_freq = 24000.0f / (zc_counter * 2.0f);
float decode_audio = raw_audio;
if (modulation != ModulationMode::FM) {
float gain = 16.0f;
if (modulation == ModulationMode::USB || modulation == ModulationMode::LSB)
gain = 5.0f;
decode_audio *= gain;
// Hybrid tracking
if (zc_counter < 15) {
// This stabilizes the 1000-1400 Hz range
current_freq = (current_freq * 0.6f) + (n_freq * 0.4f);
} else {
current_freq = n_freq;
}
update_goertzel_coeff(current_freq);
}
last_zc_counter = zc_counter;
} else {
last_zc_counter = 0;
}
zc_counter = 0;
} else {
if (zc_counter < 100) zc_counter++;
// Hard Limiting / Clipping
message.clipped = false;
if (decode_audio > 1.0f) {
decode_audio = 1.0f;
message.clipped = true;
} else if (decode_audio < -1.0f) {
decode_audio = -1.0f;
}
}
prev_sample = (int16_t)lpf_sample;
// 4. Goertzel
int64_t s = (int64_t)sample + (((int64_t)coeff_int * s_prev_i) >> 14) - s_prev2_i;
s_prev2_i = s_prev_i;
s_prev_i = (int32_t)s;
goertzel_count++;
// 5. Detection
if (goertzel_count >= 60) {
if (startup_delay > 0) {
startup_delay--;
int64_t pwr = (int64_t)s_prev_i * s_prev_i + (int64_t)s_prev2_i * s_prev2_i -
(((int64_t)s_prev_i * s_prev2_i * coeff_int) >> 14);
noise_floor = (noise_floor * 15 + pwr) / 16;
} else {
int64_t power = (int64_t)s_prev_i * s_prev_i + (int64_t)s_prev2_i * s_prev2_i -
(((int64_t)s_prev_i * s_prev2_i * coeff_int) >> 14);
if (!was_signaling) {
noise_floor = (noise_floor * 127 + power) / 128;
}
int64_t sensitivity = 4 + (user_squelch_level / 10);
int64_t base_pwr_threshold = noise_floor * sensitivity;
int64_t current_pwr_threshold = was_signaling ? (base_pwr_threshold / 2) : base_pwr_threshold;
bool is_tone = squelch_is_open && (power > current_pwr_threshold) && (power > 150000);
if (is_tone != was_signaling) {
int32_t duration_us = (int32_t)((int64_t)duration_samples * 125 / 3);
if (duration_us > 10000) {
message.state_durations[0] = was_signaling ? duration_us : -duration_us;
message.measured_frequency = (uint32_t)current_freq;
message.state_cnt = 1;
shared_memory.application_queue.push(message);
}
was_signaling = is_tone;
duration_samples = 0;
}
if (!was_signaling && duration_samples > 28800) {
int32_t duration_us = (int32_t)((int64_t)duration_samples * 125 / 3);
message.state_durations[0] = -duration_us;
message.state_cnt = 1;
shared_memory.application_queue.push(message);
duration_samples = 0;
}
}
duration_samples += 60;
s_prev_i = 0;
s_prev2_i = 0;
goertzel_count = 0;
audio_buf.p[i] = decode_audio;
}
audio_buf.p[i] = squelch_is_open ? (int16_t)sample : 0;
// 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 == ModulationMode::FM && !squelch_is_open) {
audio_buf.p[i] = 0.0f; // mute nfm on squelch
}
}
audio_output.write(audio_buf);
}
void MorseProcessor::on_message(const Message* const p) {
switch (p->id) {
case Message::ID::MorseRXConfig: {
auto morse_rx_msg = *reinterpret_cast<const MorseRXConfigureMessage*>(p);
configure(morse_rx_msg.mode);
break;
}
case Message::ID::NBFMConfigure: {
auto nbfm_msg = *reinterpret_cast<const NBFMConfigureMessage*>(p);
user_squelch_level = nbfm_msg.squelch_level;
audio_output.configure(iir_config_passthrough, iir_config_passthrough, (float)user_squelch_level / 100.0f);
break;
}
default:
break;
}
}
int main() {
audio::dma::init_audio_out();
EventDispatcher event_dispatcher{std::make_unique<MorseProcessor>()};
+49 -29
View File
@@ -37,54 +37,74 @@ class MorseProcessor : public BasebandProcessor {
void execute(const buffer_c8_t& buffer) override;
void on_message(const Message* const p) override;
void update_goertzel_coeff(float freq);
private:
bool configured{false};
void configure(uint8_t mode);
buffer_f32_t demodulate(const buffer_c16_t& channel);
void update_goertzel_coeff(float freq);
void measure_frequency(int32_t sample);
void process_decoding(int32_t sample);
size_t baseband_fs = 3072000;
BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive};
BasebandThread baseband_thread{3072000, this, baseband::Direction::Receive};
RSSIThread rssi_thread{};
std::array<complex16_t, 512> dst_buffer{};
std::array<int16_t, 32> audio_buffer{};
std::array<complex16_t, 512> dst{};
const buffer_c16_t dst_buffer{
dst.data(),
dst.size()};
std::array<float, 32> audio{};
const buffer_f32_t audio_buffer{
audio.data(),
audio.size()};
dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0{};
dsp::decimate::FIRC16xR16x32Decim8 decim_1{};
dsp::decimate::FIRAndDecimateComplex channel_filter{};
dsp::demodulate::FM demod{};
dsp::demodulate::AM demod_AM{};
dsp::demodulate::FM demod_cw_fm{};
dsp::demodulate::SSB demod_ssb{};
AudioOutput audio_output{};
// Goertzel and signal detection
bool configured{false};
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};
// frequency measurement variables
uint32_t meas_samples_in_period{0};
bool meas_signal_state_high{false};
uint32_t meas_last_period_len{0};
uint32_t meas_consistency_count{0};
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) ---
int32_t coeff_int{0};
int32_t s_prev_i{0};
int32_t s_prev2_i{0};
uint32_t goertzel_count{0};
uint32_t duration_samples{0};
bool was_signaling{false};
int64_t noise_floor{5000};
int32_t startup_delay{20};
// Variables required for measurement
int16_t prev_sample{0};
uint32_t zc_counter{0};
float current_freq{0.0f};
int32_t lpf_sample{0};
int32_t dc_offset{0};
// Squelch and stability
bool squelch_is_open{false};
int32_t squelch_hold{0};
int32_t user_squelch_level{0};
int64_t noise_floor{0};
int32_t startup_delay{0}; // Power-on delay
int32_t last_zc_counter{0}; // Previous ZC measurement for comparison
float dc_average = 0.0f; // DC level tracking
int32_t dc_average_int = 0;
MorseRXDataMessage message{};
void configure();
MorseRXfreqMessage freq_message{};
};
#endif // __PROC_MORSE_H__
#endif
+97
View File
@@ -0,0 +1,97 @@
#include "proc_morsetx.hpp"
#include "portapack_shared_memory.hpp"
#include "sine_table_int8.hpp"
#include "event_m4.hpp"
#include <cstdint>
void MorseTXProcessor::execute(const buffer_c8_t& buffer) {
if (!configured) return;
for (size_t i = 0; i < buffer.count; i++) {
int8_t sample_sin;
int8_t sample_cos;
sample_sin = (sine_table_i8[(tone_phase & 0xFF000000) >> 24]);
tone_phase += tone_delta;
im = 0;
re = 0;
// modulation logic
if (modulation == 0) { // AM modulation
if (key_down) {
re = 64 + (sample_sin >> 2);
}
} else if (modulation == 1) { // FM modulation
if (key_down) {
delta = sample_sin * fm_delta;
} else {
delta = 0;
}
phase += delta;
sphase = phase + (64 << 24);
re = (sine_table_i8[(sphase & 0xFF000000) >> 24]);
im = (sine_table_i8[(phase & 0xFF000000) >> 24]);
} else if (modulation == 2) { // DSB
if (key_down) {
re = sample_sin;
}
} else if (modulation == 3) { // USB
if (key_down) {
sample_cos = (sine_table_i8[((tone_phase + 0x40000000) & 0xFF000000) >> 24]);
re = sample_cos;
im = sample_sin;
}
} else if (modulation == 4) { // LSB
if (key_down) {
sample_cos = (sine_table_i8[((tone_phase + 0x40000000) & 0xFF000000) >> 24]);
re = sample_cos;
im = (sine_table_i8[((tone_phase + 0x80000000) & 0xFF000000) >> 24]);
}
}
buffer.p[i] = {re, im};
}
}
void MorseTXProcessor::on_message(const Message* const p) {
switch (p->id) {
case Message::ID::MorseTXConfigure: {
auto message = *reinterpret_cast<const MorseTXConfigureMessage*>(p);
tone_delta = message.tone * 1398; // scale
tone = message.tone; // audio tone
modulation = message.modulation;
if (message.fm_delta == 0 && modulation == 1) {
fm_delta = 90000;
} else {
uint64_t scale = 0xFFFFFFFFULL; // 32-bit max
fm_delta = (uint32_t)((uint64_t)message.fm_delta * (scale / 1536000));
}
break;
}
case Message::ID::MorseTXkey: {
auto key = *reinterpret_cast<const MorseTXkeyMessage*>(p);
key_down = key.key_down;
configured = true;
if (key_down)
audio::dma::beep_start(tone, 24000, 0);
else
audio::dma::beep_stop();
break;
}
default:
break;
}
}
int main() {
audio::dma::init_audio_out();
EventDispatcher event_dispatcher{std::make_unique<MorseTXProcessor>()};
event_dispatcher.run();
return 0;
}
+33
View File
@@ -0,0 +1,33 @@
#ifndef __PROC_MORSETX_H__
#define __PROC_MORSETX_H__
#include "baseband_processor.hpp"
#include "baseband_thread.hpp"
#include "portapack_shared_memory.hpp"
#include "audio_output.hpp"
#include "audio_dma.hpp"
#define AUDIO_OUTPUT_BUFFER_SIZE 32
#define AUDIO_SAMPLING_RATE 24000
class MorseTXProcessor : public BasebandProcessor {
public:
void execute(const buffer_c8_t& buffer) override;
void on_message(const Message* const msg) override;
private:
bool configured{false};
bool key_down{false}; // currently the virtual key is pressed or not.
int8_t sample{0}, re{0}, im{0};
uint8_t modulation{0}; // 0=AM, 1=FM, 2=DSB, 3=USB, 4=LSB
uint32_t tone_delta{0}; // shifting value by tone
uint32_t fm_delta{};
uint32_t tone_phase{0};
uint32_t tone{0}; // audio tone frequeny
int32_t phase{0}, sphase{0}, delta{0}; // sample generation.
BasebandThread baseband_thread{1536000, this, baseband::Direction::Transmit};
};
#endif
+99 -126
View File
@@ -25,14 +25,6 @@
#include "portapack_shared_memory.hpp"
#include "event_m4.hpp"
static inline int get_quadrant(int16_t i, int16_t q) {
if (i >= 0) {
return (q >= 0) ? 0 : 3;
} else {
return (q >= 0) ? 1 : 2;
}
}
void SubCarProcessor::execute(const buffer_c8_t& buffer) {
if (!configured) return;
@@ -46,10 +38,6 @@ void SubCarProcessor::execute(const buffer_c8_t& buffer) {
const auto decim_1_out = decim_1.execute(decim_0_out, dst_buffer); // Input:512 complex/2 (decim factor) = 256_output complex ( 512 I/Q samples)
feed_channel_stats(decim_1_out);
// for fm
const int32_t DC_ALPHA = 5; // Auto-centering speed
int32_t buffer_rotation_sum = 0;
for (size_t i = 0; i < decim_1_out.count; i++) {
// am
threshold = (low_estimate + high_estimate) / 2;
@@ -57,126 +45,103 @@ void SubCarProcessor::execute(const buffer_c8_t& buffer) {
int16_t re = decim_1_out.p[i].real();
int16_t im = decim_1_out.p[i].imag();
uint32_t mag = ((uint32_t)re * (uint32_t)re) + ((uint32_t)im * (uint32_t)im);
mag = (mag >> 10);
int32_t const ook_low_delta = mag - low_estimate;
bool meashl = currentHiLow;
if (sig_state == STATE_IDLE) {
if (mag > (threshold + hysteresis)) { // just become high
meashl = true;
sig_state = STATE_PULSE;
numg = 0;
} else {
meashl = false; // still low
low_estimate += ook_low_delta / OOK_EST_LOW_RATIO;
low_estimate += ((ook_low_delta > 0) ? 1 : -1); // Hack to compensate for lack of fixed-point scaling
// Calculate default OOK high level estimate
high_estimate = 1.35 * low_estimate; // Default is a ratio of low level
high_estimate = std::max(high_estimate, min_high_level);
high_estimate = std::min(high_estimate, (uint32_t)OOK_MAX_HIGH_LEVEL);
}
} else if (sig_state == STATE_PULSE) {
++numg;
if (numg > 100) numg = 100;
if (mag < (threshold - hysteresis)) {
// check if really a bad value
if (numg < 3) {
// susp
sig_state = STATE_GAP;
} else {
if (modulation == 0) {
int32_t const ook_low_delta = mag - low_estimate;
bool meashl = currentHiLow;
if (sig_state == STATE_IDLE) {
if (mag > (threshold + hysteresis)) { // just become high
meashl = true;
sig_state = STATE_PULSE;
numg = 0;
sig_state = STATE_GAP_START;
} else {
meashl = false; // still low
low_estimate += ook_low_delta / OOK_EST_LOW_RATIO;
low_estimate += ((ook_low_delta > 0) ? 1 : -1); // Hack to compensate for lack of fixed-point scaling
// Calculate default OOK high level estimate
high_estimate = 1.35 * low_estimate; // Default is a ratio of low level
high_estimate = std::max(high_estimate, min_high_level);
high_estimate = std::min(high_estimate, (uint32_t)OOK_MAX_HIGH_LEVEL);
}
} else if (sig_state == STATE_PULSE) {
++numg;
if (numg > 100) numg = 100;
if (mag < (threshold - hysteresis)) {
// check if really a bad value
if (numg < 3) {
// susp
sig_state = STATE_GAP;
} else {
numg = 0;
sig_state = STATE_GAP_START;
}
meashl = false; // low
} else {
high_estimate += mag / OOK_EST_HIGH_RATIO - high_estimate / OOK_EST_HIGH_RATIO;
high_estimate = std::max(high_estimate, min_high_level);
high_estimate = std::min(high_estimate, (uint32_t)OOK_MAX_HIGH_LEVEL);
meashl = true; // still high
}
} else if (sig_state == STATE_GAP_START) {
++numg;
if (mag > (threshold + hysteresis)) { // New pulse?
sig_state = STATE_PULSE;
meashl = true;
} else if (numg >= 3) {
sig_state = STATE_GAP;
meashl = false; // gap
}
} else if (sig_state == STATE_GAP) {
++numg;
if (mag > (threshold + hysteresis)) { // New pulse?
numg = 0;
sig_state = STATE_PULSE;
meashl = true;
} else {
meashl = false;
}
}
if (meashl == currentHiLow && currentDuration < 30'000'000) // allow pass 'end' signal
{
currentDuration += nsPerDecSamp;
} else { // called on change, so send the last duration and dir.
if (currentDuration >= 30'000'000) sig_state = STATE_IDLE;
if (protoList) protoList->feed(currentHiLow, currentDuration / 1000);
currentDuration = nsPerDecSamp;
currentHiLow = meashl;
}
}
if (modulation == 1) {
int32_t discrim = ((int32_t)im * fm_state.last_re) - ((int32_t)re * fm_state.last_im);
fm_state.last_re = re;
fm_state.last_im = im;
fm_state.smoothed_discrim += (discrim - fm_state.smoothed_discrim) >> 4;
// --- FM Part (Simple 2-FSK) ---
if (mag > (threshold / 2)) {
const int32_t fm_hysteresis = 2000;
bool new_level = fm_state.current_logic_level;
if (fm_state.smoothed_discrim > fm_hysteresis) {
new_level = true;
} else if (fm_state.smoothed_discrim < -fm_hysteresis) {
new_level = false;
}
if (new_level == fm_state.current_logic_level) {
fm_state.buffer_count++;
} else {
int32_t duration_us = (fm_state.buffer_count * nsPerDecSamp) / 1000;
if (duration_us > 15) {
if (protoList) protoList->feed(fm_state.current_logic_level, duration_us);
}
fm_state.current_logic_level = new_level;
fm_state.buffer_count = 1;
}
meashl = false; // low
} else {
high_estimate += mag / OOK_EST_HIGH_RATIO - high_estimate / OOK_EST_HIGH_RATIO;
high_estimate = std::max(high_estimate, min_high_level);
high_estimate = std::min(high_estimate, (uint32_t)OOK_MAX_HIGH_LEVEL);
meashl = true; // still high
}
} else if (sig_state == STATE_GAP_START) {
++numg;
if (mag > (threshold + hysteresis)) { // New pulse?
sig_state = STATE_PULSE;
meashl = true;
} else if (numg >= 3) {
sig_state = STATE_GAP;
meashl = false; // gap
}
} else if (sig_state == STATE_GAP) {
++numg;
if (mag > (threshold + hysteresis)) { // New pulse?
numg = 0;
sig_state = STATE_PULSE;
meashl = true;
} else {
meashl = false;
fm_state.buffer_count = 0;
}
}
if (meashl == currentHiLow && currentDuration < 30'000'000) // allow pass 'end' signal
{
currentDuration += nsPerDecSamp;
} else { // called on change, so send the last duration and dir.
if (currentDuration >= 30'000'000) sig_state = STATE_IDLE;
if (protoList) protoList->feed(currentHiLow, currentDuration / 1000);
currentDuration = nsPerDecSamp;
currentHiLow = meashl;
}
// fm part: -- NOT WORKING!!!! TODO FIX. AI code ;)
int current_quad = get_quadrant(re, im);
// Calculate Step (Current - Previous)
int diff = current_quad - fm_state.prev_quad;
// Handle Wrap-Around (crossing from Q3 to Q0 or Q0 to Q3)
// 3 -> 0 should be +1 (CCW)
// 0 -> 3 should be -1 (CW)
if (diff == -3)
diff = 1;
else if (diff == 3)
diff = -1;
// Update History
fm_state.prev_quad = current_quad;
// Accumulate Rotation
buffer_rotation_sum += diff;
}
// fm finish:
// 3. AUTO-CENTERING (DC BLOCKER)
// Even with quadrant counting, "drift" (hand effect) makes the wheel spin
// faster or slower. We need to subtract the average speed.
// Update our "Average Speed" estimate
// Note: buffer_rotation_sum is roughly proportional to frequency.
fm_state.dc_offset = (fm_state.dc_offset * ((1 << DC_ALPHA) - 1) + buffer_rotation_sum) >> DC_ALPHA;
// Remove the drift
int32_t centered_rotation = buffer_rotation_sum - fm_state.dc_offset;
// 4. LOW PASS FILTER
const int32_t LPF_ALPHA = 4;
fm_state.smoothed_error = (fm_state.smoothed_error * (LPF_ALPHA - 1) + centered_rotation) / LPF_ALPHA;
// 5. DECISION LOGIC
// Threshold is small now because we are counting quadrant steps.
// Max steps per buffer (256 samples) is 256.
// Typical FSK deviation might give you +/- 10 to 50 steps per buffer.
const int32_t THRESHOLD = 3;
bool new_level = fm_state.current_logic_level;
if (fm_state.smoothed_error > THRESHOLD) {
new_level = true;
} else if (fm_state.smoothed_error < -THRESHOLD) {
new_level = false;
}
// 6. TIMING OUTPUT
if (new_level == fm_state.current_logic_level) {
fm_state.buffer_count++;
} else {
// Output pulse duration
int32_t duration_us = fm_state.buffer_count * 512;
if (duration_us > 250) {
if (protoListFm) protoListFm->feed(fm_state.current_logic_level, duration_us);
}
fm_state.current_logic_level = new_level;
fm_state.buffer_count = 1;
}
}
@@ -189,6 +154,14 @@ void SubCarProcessor::configure(const SubGhzFPRxConfigureMessage& message) {
// constexpr size_t decim_0_output_fs = baseband_fs / decim_0.decimation_factor; //unused
// constexpr size_t decim_1_output_fs = decim_0_output_fs / decim_1.decimation_factor; //unused
if (modulation != message.modulation) {
// reload protos to reset them all
if (protoList) {
delete protoList;
}
protoList = new SubCarProtos();
}
modulation = message.modulation;
baseband_fs = message.sampling_rate;
baseband_thread.set_sampling_rate(baseband_fs);
nsPerDecSamp = 1'000'000'000 / baseband_fs * 8; // Scaled it due to less array buffer sampes due to /8 decimation. 250 nseg (4Mhz) * 8
+8 -8
View File
@@ -56,8 +56,8 @@ class SubCarProcessor : public BasebandProcessor {
uint32_t low_estimate = 100;
uint32_t high_estimate = 12000;
uint32_t min_high_level = 10;
uint8_t numg = 0; // count of matched signals to filter spikes
size_t baseband_fs = 0; // will be set later by configure message
uint8_t numg = 0; // count of matched signals to filter spikes
size_t baseband_fs = 4'000'000; // will be set later by configure message
uint32_t nsPerDecSamp = 0;
/* Array Buffer aux. used in decim0 and decim1 IQ c16 signed data ; (decim0 defines the max length of the array) */
@@ -74,20 +74,20 @@ class SubCarProcessor : public BasebandProcessor {
uint32_t threshold = 0x0630;
bool currentHiLow = false;
bool configured{false};
uint8_t mode = 0; // 0 = am, 1 = fm
// fm part:
struct DemodFMState {
int prev_quad = 0; // Stores 0, 1, 2, or 3
int32_t dc_offset = 0;
int32_t smoothed_error = 0;
bool current_logic_level = false;
uint32_t buffer_count = 0;
int16_t last_re = 0; // Store previous Real sample
int16_t last_im = 0;
int32_t smoothed_discrim = 0;
};
DemodFMState fm_state{};
FProtoListGeneral* protoList = new SubCarProtos(); // holds all the protocols we can parse
FProtoListGeneral* protoListFm = new SubCarProtos(); // holds all the protocols we can parse, but for fm (dupe, bc most of it is dual)
uint8_t modulation = 0; // 0 am, 1 fm
FProtoListGeneral* protoList = new SubCarProtos(); // holds all the protocols we can parse
void configure(const SubGhzFPRxConfigureMessage& message);
/* NB: Threads should be the last members in the class definition. */
@@ -23,7 +23,7 @@ __process_stack_size__ = 0x1000; /* main() stack */
MEMORY
{
flash (rx) : org = 0x00000000, len = 1024k /* SPIFI flash @ 0x140????? */
flash (rx) : org = 0x00000000, len = LD_FLASH_SIZE /* SPIFI flash @ 0x140????? */
ram (rwx) : org = 0x20000000, len = 64k /* AHB SRAM @ 0x20000000 */
}
+7 -5
View File
@@ -103,8 +103,10 @@ class APRSPacket {
std::string get_digipeaters_formatted() {
uint8_t position = DIGIPEATER_START;
bool has_more = parse_address(SOURCE_START, REPEATER);
std::string repeaters = "";
if (!has_more) {
return "";
}
std::string repeaters = ",";
while (has_more) {
has_more = parse_address(position, REPEATER);
repeaters += std::string(address_buffer);
@@ -112,7 +114,7 @@ class APRSPacket {
position += ADDRESS_SIZE;
if (has_more) {
repeaters += ">";
repeaters += ",";
}
}
@@ -146,8 +148,8 @@ class APRSPacket {
}
std::string get_stream_text() {
std::string stream = get_source_formatted() + ">" + get_destination_formatted() + ";" + get_digipeaters_formatted() + ";" + get_information_text_formatted();
// get_digipeaters_formatted will add the "," to the beginning if there are digipeaters
std::string stream = get_source_formatted() + ">" + get_destination_formatted() + get_digipeaters_formatted() + ":" + get_information_text_formatted();
return stream;
}
+358
View File
@@ -1681,4 +1681,362 @@ static constexpr fir_taps_real<24> taps_BTLE_Dual_PHY = {
3}},
};
static constexpr fir_taps_real<63> taps_2k0_am_lpf_channel = {
.low_frequency_normalized = -2000.0f / 48000.0f,
.high_frequency_normalized = 2000.0f / 48000.0f,
.transition_normalized = 1500.0f / 48000.0f,
.taps = {{
26,
29,
31,
33,
32,
28,
18,
0,
-26,
-61,
-104,
-152,
-202,
-246,
-279,
-292,
-277,
-227,
-136,
0,
182,
409,
673,
968,
1280,
1595,
1899,
2174,
2407,
2583,
2693,
2731,
2693,
2583,
2407,
2174,
1899,
1595,
1280,
968,
673,
409,
182,
0,
-136,
-227,
-277,
-292,
-279,
-246,
-202,
-152,
-104,
-61,
-26,
0,
18,
28,
32,
33,
31,
29,
26,
}},
};
static constexpr fir_taps_real<64> taps_1K5_FM_channel = {
.low_frequency_normalized = 300.0f / 48000.0f,
.high_frequency_normalized = 1800.0f / 48000.0f,
.transition_normalized = 1000.0f / 48000.0f,
.taps = {{
24,
22,
20,
15,
9,
-2,
-18,
-39,
-66,
-98,
-132,
-167,
-198,
-220,
-228,
-217,
-181,
-117,
-19,
112,
277,
474,
697,
941,
1197,
1454,
1702,
1930,
2128,
2285,
2394,
2451,
2451,
2394,
2285,
2128,
1930,
1702,
1454,
1197,
941,
697,
474,
277,
112,
-19,
-117,
-181,
-217,
-228,
-220,
-198,
-167,
-132,
-98,
-66,
-39,
-18,
-2,
9,
15,
20,
22,
24,
}},
};
static constexpr fir_taps_real<63> taps_1k5_dsb_lpf = {
.low_frequency_normalized = -1500.0f / 48000.0f,
.high_frequency_normalized = 1500.0f / 48000.0f,
.transition_normalized = 1000.0f / 48000.0f,
.taps = {{
0,
0,
-1,
-5,
-10,
-19,
-31,
-46,
-64,
-82,
-99,
-111,
-113,
-100,
-64,
0,
99,
239,
424,
655,
932,
1251,
1605,
1983,
2372,
2757,
3120,
3447,
3719,
3925,
4053,
4096,
4053,
3925,
3719,
3447,
3120,
2757,
2372,
1983,
1605,
1251,
932,
655,
424,
239,
99,
0,
-64,
-100,
-113,
-111,
-99,
-82,
-64,
-46,
-31,
-19,
-10,
-5,
-1,
0,
0,
}},
};
// GAIN: 4.0x
static constexpr fir_taps_complex<63> taps_1k5_USB_channel = {
.low_frequency_normalized = 300.0f / 48000.0f,
.high_frequency_normalized = 1800.0f / 48000.0f,
.transition_normalized = 300.0f / 48000.0f,
.taps = {{
{0, 0},
{0, 0},
{2, 0},
{4, 2},
{10, 6},
{17, 14},
{27, 29},
{37, 53},
{45, 89},
{49, 140},
{41, 207},
{17, 290},
{-31, 389},
{-109, 499},
{-227, 614},
{-387, 725},
{-595, 819},
{-850, 884},
{-1147, 904},
{-1477, 866},
{-1827, 757},
{-2179, 569},
{-2512, 297},
{-2804, -55},
{-3031, -480},
{-3173, -962},
{-3213, -1481},
{-3142, -2011},
{-2955, -2524},
{-2658, -2991},
{-2262, -3386},
{-1788, -3685},
{-1258, -3873},
{-703, -3939},
{-153, -3884},
{366, -3713},
{826, -3440},
{1208, -3087},
{1499, -2677},
{1693, -2236},
{1789, -1789},
{1796, -1359},
{1726, -966},
{1594, -624},
{1420, -341},
{1220, -120},
{1012, 40},
{809, 144},
{622, 202},
{460, 223},
{324, 217},
{217, 193},
{137, 160},
{80, 125},
{42, 91},
{19, 62},
{6, 39},
{0, 22},
{-1, 11},
{-1, 5},
{-1, 1},
{0, 0},
{0, 0},
}},
};
// GAIN: 4.0x
static constexpr fir_taps_complex<63> taps_1k5_LSB_channel = {
.low_frequency_normalized = 300.0f / 48000.0f,
.high_frequency_normalized = 1800.0f / 48000.0f,
.transition_normalized = 300.0f / 48000.0f,
.taps = {{
{0, 0},
{0, 0},
{2, 0},
{4, -2},
{10, -6},
{17, -14},
{27, -29},
{37, -53},
{45, -89},
{49, -140},
{41, -207},
{17, -290},
{-31, -389},
{-109, -499},
{-227, -614},
{-387, -725},
{-595, -819},
{-850, -884},
{-1147, -904},
{-1477, -866},
{-1827, -757},
{-2179, -569},
{-2512, -297},
{-2804, 55},
{-3031, 480},
{-3173, 962},
{-3213, 1481},
{-3142, 2011},
{-2955, 2524},
{-2658, 2991},
{-2262, 3386},
{-1788, 3685},
{-1258, 3873},
{-703, 3939},
{-153, 3884},
{366, 3713},
{826, 3440},
{1208, 3087},
{1499, 2677},
{1693, 2236},
{1789, 1789},
{1796, 1359},
{1726, 966},
{1594, 624},
{1420, 341},
{1220, 120},
{1012, -40},
{809, -144},
{622, -202},
{460, -223},
{324, -217},
{217, -193},
{137, -160},
{80, -125},
{42, -91},
{19, -62},
{6, -39},
{0, -22},
{-1, -11},
{-1, -5},
{-1, -1},
{0, 0},
{0, 0},
}},
};
#endif /*__DSP_FIR_TAPS_H__*/
+2 -1
View File
@@ -29,6 +29,7 @@
using namespace lpc43xx;
#include "utility.hpp"
#include "../flashsize.h"
namespace portapack {
namespace memory {
@@ -70,7 +71,7 @@ constexpr region_t ahb_ram_2{0x2000c000, 16_KiB};
constexpr region_t backup_ram{LPC_BACKUP_REG_BASE, 256};
constexpr region_t spifi_uncached{LPC_SPIFI_DATA_BASE, 1_MiB};
constexpr region_t spifi_uncached{LPC_SPIFI_DATA_BASE, FLASH_SIZE_MB * 1024 * 1024};
constexpr region_t spifi_cached{LPC_SPIFI_DATA_CACHED_BASE, spifi_uncached.size()};
/////////////////////////////////
+52 -7
View File
@@ -147,8 +147,12 @@ class Message {
SSTVRXCalibration = 89,
SubCarData = 90,
MorseRXData = 91,
MorseRXConfig = 92,
TXDisabled = 93,
MorseRXfreq = 92,
MorseRXConfig = 93,
TXDisabled = 94,
MorseTXConfigure = 95,
MorseTXkey = 96,
StreamTXConfiguration = 97,
MAX
};
@@ -1706,16 +1710,25 @@ class MorseRXDataMessage : public Message {
public:
constexpr MorseRXDataMessage()
: 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;
};
class MorseRXfreqMessage : public Message {
public:
constexpr MorseRXfreqMessage()
: Message{ID::MorseRXfreq} {}
uint32_t measured_frequency = 0;
int32_t state_durations[2] = {0}; // positive: high, negative: low
uint16_t state_cnt = 0;
const uint16_t maxptr = 1;
};
class MorseRXConfigureMessage : public Message {
public:
constexpr MorseRXConfigureMessage()
: Message{ID::MorseRXConfig} {}
constexpr MorseRXConfigureMessage(uint8_t mode)
: Message{ID::MorseRXConfig},
mode{mode} {}
uint8_t mode = 0;
};
class TXDisabledMessage : public Message {
@@ -1725,4 +1738,36 @@ class TXDisabledMessage : public Message {
}
};
class MorseTXConfigureMessage : public Message {
public:
constexpr MorseTXConfigureMessage(uint8_t modulation, uint32_t tone, float fm_delta)
: Message{ID::MorseTXConfigure},
modulation{modulation},
tone{tone},
fm_delta{fm_delta} {}
uint8_t modulation = 0;
uint32_t tone = 0;
float fm_delta = 0;
};
class MorseTXkeyMessage : public Message {
public:
constexpr MorseTXkeyMessage(bool key_down)
: Message{ID::MorseTXkey},
key_down{key_down} {}
bool key_down = false;
};
class StreamTXConfigurationMessage : public Message {
public:
constexpr StreamTXConfigurationMessage(uint32_t deviation, uint8_t mode)
: Message{ID::StreamTXConfiguration},
deviation{deviation},
mode{mode} {}
uint32_t deviation = 60000; // used in 2fsk
uint8_t mode = 0; // am = 0, 2fsk = 1
};
#endif /*__MESSAGE_H__*/
+1
View File
@@ -125,6 +125,7 @@ constexpr image_tag_t image_tag_wefaxrx{'P', 'W', 'F', 'X'};
constexpr image_tag_t image_tag_noaaapt_rx{'P', 'N', 'O', 'A'};
constexpr image_tag_t image_tag_sstv_rx{'P', 'S', 'R', 'X'};
constexpr image_tag_t image_tag_morse{'P', 'M', 'R', 'S'};
constexpr image_tag_t image_tag_morsetx{'P', 'M', 'R', 'T'};
constexpr image_tag_t image_tag_noop{'P', 'N', 'O', 'P'};
+135
View File
@@ -384,6 +384,10 @@ void Text::set(std::string_view value) {
set_dirty();
}
std::string Text::get() {
return text;
}
void Text::getAccessibilityText(std::string& result) {
result = text;
}
@@ -2406,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(
+48
View File
@@ -217,6 +217,7 @@ class Text : public Widget {
Text(Rect parent_rect);
void set(std::string_view value);
std::string get();
void paint(Painter& painter) override;
void getAccessibilityText(std::string& result) override;
@@ -890,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:
+9
View File
@@ -0,0 +1,9 @@
#pragma once
// DO NOT EDIT: IT IS AUTO GENERATED BY CMAKE!!!!
// clang-format off
//Allowed fw size in MB
#define FLASH_SIZE_MB 2
//Current compiled fw size in MB
#define FLASH_SIZE_LIMIT_MB 1
// clang-format on
+7 -1
View File
@@ -650,7 +650,13 @@ void GeoMap::move(const float lon, const float lat) {
} else {
if (is_changed) {
set_osm_max_zoom();
redraw_map = true;
double global_center_px = lon_to_pixel_x_tile(lon_, map_osm_zoom);
double global_center_py = lat_to_pixel_y_tile(lat_, map_osm_zoom);
// Redraw if viewport moved by at least 1 pixel (includes zoom level changes)
if (abs(global_center_px - (r.width() / 2.0) - viewport_top_left_px) >= 1.0 ||
abs(global_center_py - (r.height() / 2.0) - viewport_top_left_py) >= 1.0) {
redraw_map = true;
}
}
}
}
+131
View File
@@ -2338,6 +2338,137 @@ bool NumberField::on_touch(const TouchEvent event) {
return true;
}
/* FloatField ***********************************************************/
FloatField::FloatField(
Point parent_pos,
int length,
range_t range,
float step,
char fill_char,
bool can_loop,
uint8_t precision_)
: Widget{{parent_pos, {8 * length, 16}}},
range{range},
step{step},
length_{length},
fill_char{fill_char},
can_loop{can_loop},
precision{precision_} {
set_focusable(true);
}
float FloatField::value() const {
return value_;
}
void FloatField::getAccessibilityText(std::string& result) {
result = to_string_decimal(value_, precision);
}
void FloatField::getWidgetName(std::string& result) {
result = "FloatField";
}
void FloatField::set_value(float new_value, bool trigger_change) {
const float lo = range.first;
const float hi = range.second;
if (can_loop) {
if (new_value > hi)
new_value = lo;
else if (new_value < lo)
new_value = hi;
}
new_value = clip(new_value, lo, hi);
// set final value if needed
if (new_value != value_) {
value_ = new_value;
if (on_change && trigger_change) on_change(value_);
set_dirty();
}
}
void FloatField::set_range(const float min, const float max) {
range.first = min;
range.second = max;
set_value(value_, false);
}
void FloatField::set_step(const float new_step) {
step = new_step;
}
void FloatField::paint(Painter& painter) {
auto text = to_string_decimal(value_, precision);
const auto paint_style = has_focus() ? style().invert() : style();
// clip to widget size
const auto r = screen_rect();
auto label_r = style().font.size_of(text);
size_t max_chars = (r.width()) / style().font.char_width();
if (label_r.width() > r.width()) {
text = text.substr(0, max_chars);
}
size_t padneeded = max_chars - text.length();
if (padneeded > 0 && fill_char) {
std::string filler(fill_char, padneeded);
text = filler + text;
}
painter.fill_rectangle(r, style().background);
painter.draw_string(
screen_pos(),
paint_style,
text);
}
bool FloatField::on_key(const KeyEvent key) {
if (key == KeyEvent::Select) {
if (on_select) {
on_select(*this);
return true;
} else {
return on_encoder(1);
}
}
return false;
}
bool FloatField::on_encoder(const EncoderEvent delta) {
float old_value = value();
set_value(old_value + ((float)delta * step));
if (on_wrap) {
if ((delta > 0) && (value_ < old_value))
on_wrap(1);
else if ((delta < 0) && (value_ > old_value))
on_wrap(-1);
}
return true;
}
bool FloatField::on_keyboard(const KeyboardEvent key) {
if (key == 10) {
if (on_select) {
on_select(*this);
return true;
}
}
if (key == '+' || key == ' ') {
return on_encoder(1);
}
if (key == '-' || key == 8) {
return on_encoder(-1);
}
return false;
}
bool FloatField::on_touch(const TouchEvent event) {
if (event.type == TouchEvent::Type::Start) {
focus();
}
return true;
}
/* SymField **************************************************************/
SymField::SymField(
@@ -885,6 +885,53 @@ class NumberField : public Widget {
bool can_loop{};
};
class FloatField : public Widget {
public:
std::function<void(FloatField&)> on_select{};
std::function<void(float)> on_change{};
std::function<void(int32_t)> on_wrap{};
using range_t = std::pair<float, float>;
FloatField(Point parent_pos, int length, range_t range, float step, char fill_char, bool can_loop, uint8_t precision_ = 1);
FloatField(Point parent_pos, int length, range_t range, float step, char fill_char)
: FloatField{parent_pos, length, range, step, fill_char, true, 1} {
}
FloatField()
: FloatField{{0, 0}, 1, {0, 1}, 1, ' ', true, 1} {
}
FloatField(const FloatField&) = delete;
FloatField(FloatField&&) = delete;
float value() const;
void set_value(float new_value, bool trigger_change = true);
void set_range(const float min, const float max);
void set_step(const float new_step);
void set_precision(uint8_t precision);
void paint(Painter& painter) override;
bool on_key(const KeyEvent key) override;
bool on_encoder(const EncoderEvent delta) override;
bool on_touch(const TouchEvent event) override;
bool on_keyboard(const KeyboardEvent event) override;
void getAccessibilityText(std::string& result) override;
void getWidgetName(std::string& result) override;
private:
range_t range;
float step;
const int length_;
const char fill_char;
float value_{0};
bool can_loop{};
uint8_t precision = 1;
};
/* A widget that allows for character-by-character editing of its value. */
class SymField : public Widget {
public:
+5 -3
View File
@@ -126,9 +126,11 @@ struct is_flags_type {
template <typename TEnum>
constexpr bool is_flags_type_v = is_flags_type<TEnum>::value;
#define ENABLE_FLAGS_OPERATORS(type) \
template <> \
struct is_flags_type<type> { static constexpr bool value = true; };
#define ENABLE_FLAGS_OPERATORS(type) \
template <> \
struct is_flags_type<type> { \
static constexpr bool value = true; \
};
template <typename TEnum>
constexpr std::enable_if_t<is_flags_type_v<TEnum>, TEnum> operator|(TEnum a, TEnum b) {
+43 -12
View File
@@ -21,35 +21,66 @@
# Boston, MA 02110-1301, USA.
#
set -e # exit immediatelly on any failure
set -e # exit immediately on any failure
# 1. PARSE ARGUMENTS
# We separate arguments into two lists:
# - CMAKE_OPTS: Anything starting with -D (e.g., -DFLASH_MB_SIZE=4)
# - BUILD_ARGS: Everything else (e.g., make, ninja, -j20)
CMAKE_OPTS=()
BUILD_ARGS=()
for arg in "$@"; do
if [[ "$arg" == -D* ]]; then
CMAKE_OPTS+=("$arg")
else
BUILD_ARGS+=("$arg")
fi
done
# Reset the script arguments ($1, $2...) to contain only the BUILD_ARGS. This allows the original logic below to work exactly as before.
set -- "${BUILD_ARGS[@]}"
build_make() {
cd ..
mkdir -p build
cd build
cmake ..
cmake "${CMAKE_OPTS[@]}" ..
make "$@"
exit 0 # Report success :)
exit 0
}
build_ninja() {
cd ..
mkdir -p build
cd build
cmake -G Ninja ..
cmake -G Ninja "${CMAKE_OPTS[@]}" ..
ninja "$@"
exit 0 # Report success :)
exit 0
}
if [ "$1" = 'make' ]; then # build the default (or specified) target with make
shift # remove the first item from $@ as we consumed it, we can then pass the rest on to make
if [ "$1" = 'make' ]; then
# User explicitly typed 'make'
shift
build_make "$@"
elif [[ $1 == -j* ]]; then # allow passing -j without typing make_default
# dont shift here as we wanna pass the -j
elif [[ $1 == -j* ]]; then
# User passed -j20 directly (Implied make)
build_make "$@"
elif [ "$1" = 'ninja' ]; then # build the default (or specified) target with ninja
shift # remove the first item from $@ as we consumed it, we can then pass the rest on to make
elif [ "$1" = 'ninja' ]; then
# User explicitly typed 'ninja'
shift
build_ninja "$@"
elif [ ${#CMAKE_OPTS[@]} -gt 0 ] && [ ${#BUILD_ARGS[@]} -eq 0 ]; then
# User passed ONLY CMake flags (e.g. "-DFLASH_MB_SIZE=4")
# We assume they want to run a default 'make' build.
build_make
fi
exec "$@"
# Fallback for other commands (e.g. /bin/bash)
exec "$@"
+4 -4
View File
@@ -36,8 +36,9 @@ from pathlib import Path
usage_message = """
PortaPack SPI flash image generator
Usage: <command> <application_path> <baseband_path> <output_path>
Usage: <command> <application_path> <baseband_path> <output_path> <spi_size>
Where paths refer to the .bin files for each component project.
spi_size is the total size of the target flash (e.g. 1048576).
"""
@@ -166,13 +167,14 @@ def get_gcc_version_from_elf_files_in_giving_path_or_filename_s_path(path):
#^^^^^^^^gcc version check from elf file^^^^^^^^
if len(sys.argv) != 4:
if len(sys.argv) != 5:
print(usage_message)
sys.exit(-1)
application_image = read_image(sys.argv[1])
baseband_image = read_image(sys.argv[2])
output_path = sys.argv[3]
spi_size = int(sys.argv[4], 0)
print("\ncheck gcc versions from all elf target\n")
application_gcc_versions = get_gcc_version_from_elf_files_in_giving_path_or_filename_s_path(sys.argv[1])
@@ -207,8 +209,6 @@ except Exception as e:
#^^^^^^^^external app linker script address check worker^^^^^^^^
spi_size = 1048576
images = (
{
'name': 'application',
+9
View File
@@ -0,0 +1,9 @@
#pragma once
// DO NOT EDIT: IT IS AUTO GENERATED BY CMAKE!!!!
// clang-format off
//Allowed fw size in MB
#define FLASH_SIZE_MB @FLASH_MB_SIZE@
//Current compiled fw size in MB
#define FLASH_SIZE_LIMIT_MB @FLASH_MB_LIMIT_SIZE@
// clang-format on
+10 -1
View File
@@ -1,2 +1,11 @@
#!/bin/sh
find firmware/common firmware/baseband firmware/application firmware/test/application firmware/test/baseband -iname '*.h' -o -iname '*.hpp' -o -iname '*.cpp' -o -iname '*.c' | xargs clang-format-18 -style=file -i
find firmware/common \
firmware/baseband \
firmware/application \
firmware/test/application \
firmware/test/baseband \
\( -iname '*.h' -o -iname '*.hpp' -o -iname '*.c' -o -iname '*.cpp' \) \
-print0 | \
xargs -0 clang-format-18 -style=file -i
+1 -1
Submodule hackrf updated: 714e0dccc0...fa505353a5