Compare commits

...

15 Commits

Author SHA1 Message Date
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
zxkmm 86d7b345d0 waterfall color designer app (#2625) 2026-01-19 21:46:25 +01:00
69 changed files with 3906 additions and 329 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
+12 -2
View File
@@ -368,8 +368,18 @@ 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);
}
+3 -1
View File
@@ -102,7 +102,9 @@ 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();
+14 -5
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
@@ -222,7 +222,11 @@ set(EXTCPPSRC
#gfxEQ 80 byte
external/gfxeq/main.cpp
external/gfxeq/ui_gfxeq.cpp
external/gfxeq/ui_gfxeq.cpp
#waterfall designer
external/waterfall_designer/main.cpp
external/waterfall_designer/ui_waterfall_designer.cpp
#detector_rx 168 byte
external/detector_rx/main.cpp
@@ -290,6 +294,9 @@ set(EXTCPPSRC
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
@@ -314,7 +321,7 @@ set(EXTAPPLIST
tpmsrx
protoview
adsbtx
morse_tx
#morse_tx
sstvtx
sstvrx
random_password
@@ -346,6 +353,7 @@ set(EXTAPPLIST
scanner
level
gfxeq
waterfall_designer
detector_rx
spaceinv
blackjack
@@ -362,4 +370,5 @@ set(EXTAPPLIST
siggen
sdusb
morse_radio
morseradiotx
)
+14
View File
@@ -92,6 +92,8 @@ MEMORY
ram_external_app_siggen (rwx) : org = 0xADF30000, len = 32k
ram_external_app_sdusb (rwx) : org = 0xADF40000, len = 32k
ram_external_app_morse_radio (rwx) : org = 0xADF50000, len = 32k
ram_external_app_waterfall_designer (rwx) : org = 0xADF60000, len = 32k
ram_external_app_morseradiotx (rwx) : org = 0xADF70000, len = 32k
}
SECTIONS
@@ -402,6 +404,12 @@ SECTIONS
*(*ui*external_app*gfxeq*);
} > ram_external_app_gfxeq
.external_app_waterfall_designer : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_waterfall_designer.application_information));
*(*ui*external_app*waterfall_designer*);
} > ram_external_app_waterfall_designer
.external_app_detector_rx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_detector_rx.application_information));
@@ -512,5 +520,11 @@ SECTIONS
*(*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
}
@@ -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__
@@ -0,0 +1,83 @@
/*
* Copyright (C) 2025 Bernd Herzog
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#include "ui.hpp"
#include "ui_waterfall_designer.hpp"
#include "ui_navigation.hpp"
#include "external_app.hpp"
namespace ui::external_app::waterfall_designer {
void initialize_app(ui::NavigationView& nav) {
nav.push<WaterfallDesignerView>();
}
} // namespace ui::external_app::waterfall_designer
extern "C" {
__attribute__((section(".external_app.app_waterfall_designer.application_information"), used)) application_information_t _application_information_waterfall_designer = {
/*.memory_location = */ (uint8_t*)0x00000000,
/*.externalAppEntry = */ ui::external_app::waterfall_designer::initialize_app,
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
/*.app_name = */ "Wt Design",
/*.bitmap_data = */ {
0x00,
0x00,
0x00,
0x00,
0xFE,
0x7F,
0x03,
0xC0,
0x53,
0xD5,
0xAB,
0xCA,
0x53,
0xD5,
0xAB,
0xCA,
0x53,
0xD5,
0xAB,
0xCA,
0x53,
0xD5,
0x03,
0xC0,
0xFF,
0xFF,
0xFB,
0xD7,
0xFE,
0x7F,
0x00,
0x00,
},
/*.icon_color = */ ui::Color::cyan().v,
/*.menu_location = */ app_location_t::UTILITIES,
/*.desired_menu_position = */ -1,
/*.m4_app_tag = portapack::spi_flash::image_tag_none */ {'P', 'C', 'A', 'P'},
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
};
}
@@ -0,0 +1,503 @@
/*
* Copyleft Mr. Robot
* Copyright HTotoo
*
* 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_waterfall_designer.hpp"
#include "baseband_api.hpp"
#include "portapack.hpp"
#include "ui_freqman.hpp"
#include "file_path.hpp"
#include "ui_fileman.hpp"
#include "file_reader.hpp"
#include "ui_textentry.hpp"
using namespace portapack;
namespace ui::external_app::waterfall_designer {
WaterfallDesignerView::WaterfallDesignerView(NavigationView& nav)
: nav_{nav} {
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
add_children({&labels,
&field_frequency,
&field_frequency_step,
&field_rf_amp,
&field_lna,
&field_vga,
&option_bandwidth,
&record_view,
&menu_view,
&button_new,
&button_open,
&button_save,
&button_add_level,
&button_remove_level,
&button_edit_color,
&button_apply_setting,
&waterfall});
ensure_directory(waterfalls_dir);
ui::Rect waterfall_rect{0, header_height, screen_rect().width(), screen_rect().height() - header_height};
waterfall.set_parent_rect(waterfall_rect);
menu_view.set_parent_rect({0, 1 * 16, screen_width, 7 * 16});
field_frequency_step.set_by_value(receiver_model.frequency_step());
field_frequency_step.on_change = [this](size_t, OptionsField::value_t v) {
receiver_model.set_frequency_step(v);
this->field_frequency.set_step(v);
};
freqman_set_bandwidth_option(SPEC_MODULATION, option_bandwidth);
option_bandwidth.on_change = [this](size_t, uint32_t new_capture_rate) {
/* Nyquist would imply a sample rate of 2x bandwidth, but because the ADC
* provides 2 values (I,Q), the sample_rate is equal to bandwidth here. */
/* capture_rate (bandwidth) is used for FFT calculation and display LCD, and also in recording writing SD Card rate. */
/* ex. sampling_rate values, 4Mhz, when recording 500 kHz (BW) and fs 8 Mhz, when selected 1 Mhz BW ... */
/* ex. recording 500kHz BW to .C16 file, base_rate clock 500kHz x2(I,Q) x 2 bytes (int signed) =2MB/sec rate SD Card. */
waterfall.stop();
// record_view determines the correct oversampling to apply and returns the actual sample rate.
// NB: record_view is what actually updates proc_capture baseband settings.
auto actual_sample_rate = record_view.set_sampling_rate(new_capture_rate);
// Update the radio model with the actual sampling rate.
receiver_model.set_sampling_rate(actual_sample_rate);
// Get suitable anti-aliasing BPF bandwidth for MAX2837 given the actual sample rate.
auto anti_alias_filter_bandwidth = filter_bandwidth_for_sampling_rate(actual_sample_rate);
receiver_model.set_baseband_bandwidth(anti_alias_filter_bandwidth);
capture_rate = new_capture_rate;
waterfall.start();
};
button_new.on_select = [this]() {
on_create_new_profile();
};
button_open.on_select = [this]() {
on_open_profile();
};
button_save.on_select = [this]() {
on_save_profile();
};
button_add_level.on_select = [this]() {
on_add_level();
};
button_remove_level.on_select = [this]() {
on_remove_level();
};
button_edit_color.on_select = [this]() {
on_edit_color();
};
button_apply_setting.on_select = [this]() {
if_apply_setting = true;
on_apply_current_to_wtf();
// nav_.pop();
};
menu_view.on_highlight = [this]() {
highlighted_index_ = menu_view.highlighted_index();
};
receiver_model.enable();
option_bandwidth.set_by_value(capture_rate);
record_view.on_error = [&nav](std::string message) {
nav.display_modal("Error", message);
};
button_save.hidden(true);
button_add_level.hidden(true);
button_remove_level.hidden(true);
button_edit_color.hidden(true);
button_apply_setting.hidden(true);
refresh_menu_view();
}
WaterfallDesignerView::WaterfallDesignerView(
NavigationView& nav,
ReceiverModel::settings_t override)
: WaterfallDesignerView(nav) {
// Settings to override when launched from another app (versus from AppSettings .ini file)
field_frequency.set_value(override.frequency_app_override);
}
WaterfallDesignerView::~WaterfallDesignerView() {
if (!if_apply_setting) restore_current_profile();
receiver_model.disable();
baseband::shutdown();
}
void WaterfallDesignerView::set_parent_rect(const Rect new_parent_rect) {
View::set_parent_rect(new_parent_rect);
ui::Rect waterfall_rect{0, header_height, new_parent_rect.width(), new_parent_rect.height() - header_height};
waterfall.set_parent_rect(waterfall_rect);
}
void WaterfallDesignerView::focus() {
button_open.focus();
}
void WaterfallDesignerView::on_freqchg(int64_t freq) {
field_frequency.set_value(freq);
}
void WaterfallDesignerView::on_open_profile() {
auto open_view = nav_.push<FileLoadView>(".txt");
open_view->push_dir(waterfalls_dir);
open_view->on_changed = [this](std::filesystem::path new_file_path) {
on_profile_changed(new_file_path);
};
}
void WaterfallDesignerView::on_profile_changed(std::filesystem::path new_profile_path) {
current_profile_path = new_profile_path;
profile_levels.clear();
File playlist_file;
auto error = playlist_file.open(new_profile_path.string());
if (error) return;
menu_view.clear();
auto reader = FileLineReader(playlist_file);
for (const auto& line : reader) {
profile_levels.push_back(line);
}
for (auto& line : profile_levels) {
// remove empty lines
if (line == "\n" || line == "\r\n" || line == "\r") {
profile_levels.erase(std::remove(profile_levels.begin(), profile_levels.end(), line), profile_levels.end());
}
// remove line end \n etc
if (line.length() > 0 && (line[line.length() - 1] == '\n' || line[line.length() - 1] == '\r')) {
line = line.substr(0, line.length() - 1);
}
}
button_save.hidden(false);
button_add_level.hidden(false);
button_remove_level.hidden(false);
button_edit_color.hidden(false);
button_apply_setting.hidden(false);
refresh_menu_view();
on_apply_current_to_wtf();
}
void WaterfallDesignerView::refresh_menu_view() {
menu_view.clear();
for (const auto& line : profile_levels) {
if (line.length() == 0 || line[0] == '#') {
menu_view.add_item({line,
ui::Color::grey(),
&bitmap_icon_notepad,
[this](KeyEvent) {
button_add_level.focus();
}});
} else {
// index,R,G,B
size_t pos = 0;
size_t next_pos = 0;
// pass index
next_pos = line.find(',', pos);
if (next_pos == std::string::npos) continue;
pos = next_pos + 1;
// r
next_pos = line.find(',', pos);
if (next_pos == std::string::npos) continue;
uint8_t r = static_cast<uint8_t>(std::stoi(line.substr(pos, next_pos - pos)));
pos = next_pos + 1;
// g
next_pos = line.find(',', pos);
if (next_pos == std::string::npos) continue;
uint8_t g = static_cast<uint8_t>(std::stoi(line.substr(pos, next_pos - pos)));
pos = next_pos + 1;
// b
uint8_t b = static_cast<uint8_t>(std::stoi(line.substr(pos)));
ui::Color color = ui::Color(r, g, b);
menu_view.add_item({line,
color,
&bitmap_icon_cwgen,
[this](KeyEvent) {
button_remove_level.focus();
}});
}
}
set_dirty();
}
void WaterfallDesignerView::on_apply_current_to_wtf() {
std::filesystem::path system_read_path = "waterfall.txt";
copy_file(current_profile_path, system_read_path);
waterfall.load_gradient();
set_dirty();
}
void WaterfallDesignerView::on_save_profile() {
if (current_profile_path.empty()) {
nav_.display_modal("Err", "No profile file loaded");
return;
} else if (profile_levels.empty()) {
nav_.display_modal("Err", "List is empty");
return;
}
File profile_file;
auto error = profile_file.open(current_profile_path.string(), false, false);
if (error) {
nav_.display_modal("Err", "open err");
return;
}
// clear file
profile_file.seek(0);
profile_file.truncate();
// write new data
for (const auto& entry : profile_levels) {
profile_file.write_line(entry);
}
nav_.display_modal("Save", "Saved profile\n" + current_profile_path.string());
}
void WaterfallDesignerView::on_add_level() {
if (highlighted_index_ >= profile_levels.size()) return;
if (profile_levels[highlighted_index_].empty()) return;
if (profile_levels[highlighted_index_][0] == '#') return;
if (profile_levels[highlighted_index_].find(',') == std::string::npos) return;
size_t insert_pos = highlighted_index_;
std::string new_entry = "0,128,128,128";
profile_levels.insert(profile_levels.begin() + insert_pos, new_entry);
refresh_menu_view();
on_edit_color();
}
void WaterfallDesignerView::on_remove_level() {
if (highlighted_index_ >= profile_levels.size()) return;
if (profile_levels[highlighted_index_].empty()) return;
if (profile_levels[highlighted_index_][0] == '#') return;
if (profile_levels[highlighted_index_].find(',') == std::string::npos) return;
profile_levels.erase(profile_levels.begin() + highlighted_index_);
refresh_menu_view();
}
void WaterfallDesignerView::on_edit_color() {
if (highlighted_index_ >= profile_levels.size()) return;
if (profile_levels[highlighted_index_].empty()) return;
if (profile_levels[highlighted_index_][0] == '#') return;
if (profile_levels[highlighted_index_].find(',') == std::string::npos) return;
auto color_picker_view = nav_.push<WaterfallDesignerColorPickerView>(profile_levels[highlighted_index_]);
color_picker_view->on_save = [this](std::string new_color) {
profile_levels[highlighted_index_] = new_color;
refresh_menu_view();
on_apply_current_to_wtf();
};
}
void WaterfallDesignerView::backup_current_profile() {
std::filesystem::path curren_wtf_path = "waterfall.txt";
std::filesystem::path backup_path = waterfalls_dir / "wtf_des_bk.bk";
copy_file(curren_wtf_path, backup_path);
}
void WaterfallDesignerView::restore_current_profile() {
std::filesystem::path backup_path = waterfalls_dir / "wtf_des_bk.bk";
std::filesystem::path put_back_path = "waterfall.txt";
copy_file(backup_path, put_back_path);
delete_file(backup_path);
}
void WaterfallDesignerView::on_apply_setting() {
}
void WaterfallDesignerView::on_create_new_profile() {
text_prompt(
nav_,
file_name_buffer,
32 - 4, // fat32
ENTER_KEYBOARD_MODE_ALPHA,
[this](std::string& buffer) {
if (buffer.empty()) return;
if (buffer.length() < 4 || buffer.substr(buffer.length() - 4) != ".txt") {
buffer += ".txt";
}
File new_file;
auto error = new_file.create(waterfalls_dir / buffer);
if (error) {
nav_.display_modal("Err", "create file err");
return;
}
profile_levels.clear();
current_profile_path = waterfalls_dir / buffer;
on_profile_changed(current_profile_path);
});
}
WaterfallDesignerColorPickerView::WaterfallDesignerColorPickerView(NavigationView& nav, std::string color_str)
: nav_{nav},
color_str_{color_str} {
add_children({&labels,
&field_red,
&field_green,
&field_blue,
&field_step,
&field_index,
&progressbar,
&button_save});
progressbar.set_max(UINT8_MAX);
size_t pos = 0;
size_t next_pos = 0;
// index
next_pos = color_str.find(',', pos);
if (next_pos != std::string::npos) {
index_ = static_cast<uint8_t>(std::stoi(color_str.substr(pos, next_pos - pos)));
pos = next_pos + 1;
}
// r
next_pos = color_str.find(',', pos);
if (next_pos != std::string::npos) {
red_ = static_cast<uint8_t>(std::stoi(color_str.substr(pos, next_pos - pos)));
pos = next_pos + 1;
}
// g
next_pos = color_str.find(',', pos);
if (next_pos != std::string::npos) {
green_ = static_cast<uint8_t>(std::stoi(color_str.substr(pos, next_pos - pos)));
pos = next_pos + 1;
}
// b
blue_ = static_cast<uint8_t>(std::stoi(color_str.substr(pos)));
field_red.set_value(red_);
field_green.set_value(green_);
field_blue.set_value(blue_);
field_index.set_value(index_);
// cb
field_red.on_change = [this](int32_t) {
update_color_index();
};
field_green.on_change = [this](int32_t) {
update_color_index();
};
field_blue.on_change = [this](int32_t) {
update_color_index();
};
button_save.on_select = [this](Button&) {
if (on_save) on_save(build_color_str());
nav_.pop();
};
field_index.on_change = [this](int32_t) {
update_color_index();
};
field_step.on_change = [this](int32_t) {
field_red.set_step(field_step.value());
field_green.set_step(field_step.value());
field_blue.set_step(field_step.value());
field_index.set_step(field_step.value());
};
update_color_index();
}
void WaterfallDesignerColorPickerView::focus() {
button_save.focus();
}
void WaterfallDesignerColorPickerView::update_color_index() {
index_ = static_cast<uint8_t>(field_index.value());
red_ = static_cast<uint8_t>(field_red.value());
green_ = static_cast<uint8_t>(field_green.value());
blue_ = static_cast<uint8_t>(field_blue.value());
const Rect preview_rect{screen_width - 48, 1 * 16, 40, 40};
Painter painter_instance_2;
painter_instance_2.fill_rectangle(
{preview_rect.left(), preview_rect.top(), preview_rect.width(), preview_rect.height()},
ui::Color(red_, green_, blue_));
}
void WaterfallDesignerColorPickerView::paint(Painter& painter) {
// this is not duplicated code.
// because need to display color when enter,
// but it is too early to call update_color() in the constructor.
const Rect preview_rect{screen_width - 48, 1 * 16, 40, 40};
painter.fill_rectangle(
{preview_rect.left(), preview_rect.top(), preview_rect.width(), preview_rect.height()},
ui::Color(red_, green_, blue_));
}
std::string WaterfallDesignerColorPickerView::build_color_str() {
size_t index_pos = color_str_.find(',');
if (index_pos != std::string::npos) {
return color_str_.substr(0, index_pos + 1) +
to_string_dec_uint(red_) + "," +
to_string_dec_uint(green_) + "," +
to_string_dec_uint(blue_);
}
return to_string_dec_uint(index_) + "," + to_string_dec_uint(red_) + "," + to_string_dec_uint(green_) + "," + to_string_dec_uint(blue_);
}
} /* namespace ui::external_app::waterfall_designer */
@@ -0,0 +1,265 @@
/*
* Copyleft Mr. Robot
* Copyright HTotoo
*
* 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 __WATERFALL_DESIGNER_APP_HPP__
#define __WATERFALL_DESIGNER_APP_HPP__
#include "ui_widget.hpp"
#include "ui_navigation.hpp"
#include "ui_receiver.hpp"
#include "ui_freq_field.hpp"
#include "ui_record_view.hpp"
#include "ui_spectrum.hpp"
#include "app_settings.hpp"
#include "radio_state.hpp"
#include "file_path.hpp"
namespace ui::external_app::waterfall_designer {
enum class ColorComponent {
RED,
GREEN,
BLUE
};
class WaterfallDesignerColorPickerView : public View {
public:
std::function<void(std::string)> on_save{};
WaterfallDesignerColorPickerView(NavigationView& nav, std::string color_str);
std::string title() const override { return "Color Picker"; };
void focus() override;
void paint(Painter& painter) override;
private:
NavigationView& nav_;
std::string color_str_;
uint8_t index_{0};
uint8_t red_{0};
uint8_t green_{0};
uint8_t blue_{0};
void update_color_index();
std::string build_color_str();
Labels labels{
{{0 * 8, 0 * 16}, "Index", Theme::getInstance()->fg_light->foreground},
{{0 * 8, 2 * 16}, "Red", Theme::getInstance()->fg_light->foreground},
{{0 * 8, 4 * 16}, "Green", Theme::getInstance()->fg_light->foreground},
{{0 * 8, 6 * 16}, "Blue", Theme::getInstance()->fg_light->foreground},
{{0 * 8, 8 * 16}, "Step", Theme::getInstance()->fg_light->foreground}};
NumberField field_index{
{0 * 8, 1 * 16},
3,
{0, 255},
1,
' '};
NumberField field_red{
{0 * 8, 3 * 16},
3,
{0, 255},
1,
' '};
NumberField field_green{
{0 * 8, 5 * 16},
3,
{0, 255},
1,
' '};
NumberField field_blue{
{0 * 8, 7 * 16},
3,
{0, 255},
1,
' '};
NumberField field_step{
{0 * 8, 9 * 16},
3,
{0, 255},
1,
' '};
ProgressBar progressbar{
{0 * 8,
screen_height - 4 * 16 - 2 * 16 - 1 * 16,
screen_width,
2 * 16}};
Button button_save{
{0, screen_height - 4 * 16, screen_width, 4 * 16},
"Save"};
};
class WaterfallDesignerView : public View {
public:
WaterfallDesignerView(NavigationView& nav);
WaterfallDesignerView(NavigationView& nav, ReceiverModel::settings_t override);
~WaterfallDesignerView();
void focus() override;
void set_parent_rect(const Rect new_parent_rect) override;
std::string title() const override { return "Wtf Design"; };
private:
uint32_t capture_rate{500000};
uint32_t file_format{0};
uint32_t highlighted_index_{0};
bool trim{false};
std::filesystem::path current_profile_path = "";
NavigationView& nav_;
RxRadioState radio_state_{ReceiverModel::Mode::Capture};
app_settings::SettingsManager settings_{
"rx_wtf_designer",
app_settings::Mode::RX,
{
{"capture_rate"sv, &capture_rate},
{"file_format"sv, &file_format},
{"trim"sv, &trim},
}};
Labels labels{
{{0 * 8, 1 * 16}, "Rate:", Theme::getInstance()->fg_light->foreground},
{{11 * 8, 1 * 16}, "Format:", Theme::getInstance()->fg_light->foreground},
};
RxFrequencyField field_frequency{
{0 * 8, 0 * 16},
nav_};
FrequencyStepView field_frequency_step{
{10 * 8, 0 * 16}};
RFAmpField field_rf_amp{
{16 * 8, 0 * 16}};
LNAGainField field_lna{
{18 * 8, 0 * 16}};
VGAGainField field_vga{
{21 * 8, 0 * 16}};
OptionsField option_bandwidth{
{24 * 8, 0 * 16},
5,
{}};
MenuView menu_view{};
NewButton button_new{
{0 * 8, 8 * 16, 4 * 8, 32},
{},
&bitmap_icon_file,
Theme::getInstance()->fg_blue->foreground};
NewButton button_open{
{4 * 8, 8 * 16, 4 * 8, 32},
{},
&bitmap_icon_load,
Theme::getInstance()->fg_blue->foreground};
NewButton button_save{
{8 * 8, 8 * 16, 4 * 8, 32},
{},
&bitmap_icon_save,
Theme::getInstance()->fg_blue->foreground};
NewButton button_add_level{
{12 * 8, 8 * 16, 4 * 8, 32},
{},
&bitmap_icon_add,
Theme::getInstance()->fg_blue->foreground};
NewButton button_remove_level{
{16 * 8, 8 * 16, 4 * 8, 32},
{},
&bitmap_icon_delete,
Theme::getInstance()->fg_blue->foreground};
NewButton button_edit_color{
{20 * 8, 8 * 16, 4 * 8, 32},
{},
&bitmap_icon_notepad,
Theme::getInstance()->fg_blue->foreground};
NewButton button_apply_setting{
{24 * 8, 8 * 16, 4 * 8, 32},
{},
&bitmap_icon_replay,
Theme::getInstance()->fg_blue->foreground};
void backup_current_profile();
void restore_current_profile();
void on_create_new_profile();
void on_open_profile();
void on_profile_changed(std::filesystem::path new_profile_path);
void on_save_profile();
void on_add_level();
void on_remove_level();
void on_edit_color();
void refresh_menu_view();
void on_apply_current_to_wtf(); // will restore if didn't apple, when distruct
void on_apply_setting(); // apply set
bool if_apply_setting{false};
/*NB:
this works as:
each time you change color, it apply as file realtime
however if you don't push the apply (play) btn, it would resotore in distructor,
if you push apply, it would apply and exit*/
std::vector<std::string> profile_levels{};
static constexpr ui::Dim header_height = 10 * 16;
RecordView record_view{// we still need it cuz it make waterfall correct
{screen_width, screen_height, 30 * 8, 1 * 16},
u"BBD_????.*",
captures_dir,
RecordView::FileType::RawS16,
16384,
3};
spectrum::WaterfallView waterfall{};
std::string file_name_buffer{}; // needed by text_prompy
MessageHandlerRegistration message_handler_freqchg{
Message::ID::FreqChangeCommand,
[this](Message* const p) {
const auto message = static_cast<const FreqChangeCommandMessage*>(p);
this->on_freqchg(message->freq);
}};
void on_freqchg(int64_t freq);
};
} /* namespace ui::external_app::waterfall_designer */
#endif /*__WATERFALL_DESIGNER_APP_HPP__*/
+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();
+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);
};
}
+4
View File
@@ -332,6 +332,10 @@ WaterfallView::WaterfallView(const bool cursor) {
}
};
load_gradient();
}
void WaterfallView::load_gradient() {
if (!waterfall_widget.gradient.load_file(default_gradient_file)) {
waterfall_widget.gradient.set_default();
}
+1
View File
@@ -146,6 +146,7 @@ class WaterfallView : public View {
void set_parent_rect(const Rect new_parent_rect) override;
void show_audio_spectrum_view(const bool show);
void load_gradient();
private:
void update_widgets_rect();
+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 = {
+10 -1
View File
@@ -663,9 +663,10 @@ set(MODE_CPPSRC
)
DeclareTargets(PSCD subcar)
### Morse RX Decoder
set(MODE_CPPSRC
proc_morse.cpp
)
DeclareTargets(PMRS morse)
@@ -676,6 +677,14 @@ set(MODE_INCDIR
${HACKRF_PATH}/firmware/common
${HACKRF_PATH}/firmware/libopencm3/include
)
### Morse TX
set(MODE_CPPSRC
proc_morsetx.cpp
)
DeclareTargets(PMRT morsetx)
set(MODE_CPPSRC
sd_over_usb/proc_sd_over_usb.cpp
@@ -0,0 +1,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
@@ -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
};
+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
@@ -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()};
/////////////////////////////////
+40 -7
View File
@@ -147,8 +147,11 @@ class Message {
SSTVRXCalibration = 89,
SubCarData = 90,
MorseRXData = 91,
MorseRXConfig = 92,
TXDisabled = 93,
MorseRXfreq = 92,
MorseRXConfig = 93,
TXDisabled = 94,
MorseTXConfigure = 95,
MorseTXkey = 96,
MAX
};
@@ -1706,16 +1709,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 +1737,25 @@ 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;
};
#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
+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...22e44face8