mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2026-08-21 06:59:02 +00:00
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a44a00acf0 | |||
| 6739b43914 | |||
| b306a0d490 | |||
| c6130c6e8d | |||
| 97f2ca42f1 | |||
| e2b642ae25 | |||
| 106d9b1207 | |||
| c8ae419e0b | |||
| 2319dcd0d6 | |||
| 44d9ce00cd | |||
| 5cc5f8faa6 | |||
| 3a0ca480b0 | |||
| 7a2b432bfa | |||
| 86d7b345d0 | |||
| 877ede5f86 | |||
| 19eb1c40ab | |||
| 9c03a18893 | |||
| cd504eb4a7 | |||
| dc976c7f38 | |||
| 266d90a600 | |||
| 2884bb9ab7 | |||
| 133b2d2065 | |||
| 2f2671f76c | |||
| f5c2c78733 | |||
| e4ab7227fc | |||
| 93799bde6d | |||
| 3d979a613a | |||
| ee97b51eb3 |
+6
-7
@@ -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
|
||||
|
||||
...
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 +0,0 @@
|
||||
portapack.ried.cl
|
||||
@@ -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
|
||||
|
||||
@@ -296,10 +296,8 @@ set(CPPSRC
|
||||
apps/ui_rds.cpp
|
||||
apps/ui_recon_settings.cpp
|
||||
apps/ui_recon.cpp
|
||||
apps/ui_sd_over_usb.cpp
|
||||
apps/ui_search.cpp
|
||||
apps/ui_settings.cpp
|
||||
apps/ui_siggen.cpp
|
||||
apps/ui_sonde.cpp
|
||||
apps/ui_ss_viewer.cpp
|
||||
apps/ui_standalone_view.cpp
|
||||
@@ -469,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})
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -98,7 +98,7 @@ class BattinfoView : public View {
|
||||
"Volt"};
|
||||
|
||||
Button button_exit{
|
||||
{UI_POS_X_CENTER(12), 17 * 16, UI_POS_WIDTH(12), UI_POS_HEIGHT(3)},
|
||||
{UI_POS_X_CENTER(12), UI_POS_Y_BOTTOM(4), UI_POS_WIDTH(12), UI_POS_HEIGHT(2)},
|
||||
"Back"};
|
||||
static msg_t static_fn(void* arg);
|
||||
Thread* thread{nullptr};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -69,7 +69,7 @@ void GlassView::manage_beep_audio() {
|
||||
}
|
||||
}
|
||||
|
||||
void GlassView::get_max_power(const ChannelSpectrum& spectrum, uint8_t bin, uint8_t& max_power) {
|
||||
void GlassView::get_max_power(const ChannelSpectrum& spectrum, uint16_t bin, uint8_t& max_power) {
|
||||
if (mode == LOOKING_GLASS_SINGLEPASS) {
|
||||
// <20MHz spectrum mode
|
||||
if (bin < 120) {
|
||||
@@ -91,7 +91,7 @@ void GlassView::get_max_power(const ChannelSpectrum& spectrum, uint8_t bin, uint
|
||||
}
|
||||
}
|
||||
|
||||
rf::Frequency GlassView::get_freq_from_bin_pos(uint8_t pos) {
|
||||
rf::Frequency GlassView::get_freq_from_bin_pos(uint16_t pos) {
|
||||
rf::Frequency freq_at_pos = 0;
|
||||
if (mode == LOOKING_GLASS_SINGLEPASS) {
|
||||
// starting from the middle, minus 8 ignored bin on each side. Since pos is [-120,120] after the (pos - 120), it's divided by screen_width(240)/2 => 120
|
||||
@@ -101,7 +101,14 @@ rf::Frequency GlassView::get_freq_from_bin_pos(uint8_t pos) {
|
||||
return freq_at_pos;
|
||||
}
|
||||
|
||||
void GlassView::on_marker_change() {
|
||||
void GlassView::on_marker_change(int8_t delta) {
|
||||
if ((marker_pixel_index + delta) < 0)
|
||||
marker_pixel_index = marker_pixel_index + delta + screen_width;
|
||||
else if ((marker_pixel_index + delta) > screen_width)
|
||||
marker_pixel_index = marker_pixel_index + delta - screen_width;
|
||||
else
|
||||
marker_pixel_index = marker_pixel_index + delta;
|
||||
|
||||
marker = get_freq_from_bin_pos(marker_pixel_index);
|
||||
field_marker.set_text(to_string_short_freq(marker));
|
||||
plot_marker(marker_pixel_index); // Refresh marker on screen
|
||||
@@ -200,7 +207,7 @@ void GlassView::on_channel_spectrum(const ChannelSpectrum& spectrum) {
|
||||
baseband::spectrum_streaming_stop();
|
||||
// Convert bins of this spectrum slice into a representative max_power and when enough, into pixels
|
||||
// we actually need screen_width (240) of those bins
|
||||
for (uint8_t bin = 0; bin < bin_length; bin++) {
|
||||
for (uint16_t bin = 0; bin < bin_length; bin++) {
|
||||
get_max_power(spectrum, bin, max_power);
|
||||
if (max_power > range_max_power)
|
||||
range_max_power = max_power;
|
||||
@@ -208,7 +215,7 @@ void GlassView::on_channel_spectrum(const ChannelSpectrum& spectrum) {
|
||||
if (bin == 119) {
|
||||
uint8_t next_max_power = 0;
|
||||
get_max_power(spectrum, bin + 1, next_max_power);
|
||||
for (uint8_t it = 0; it < ignore_dc; it++) {
|
||||
for (uint16_t it = 0; it < ignore_dc; it++) {
|
||||
uint8_t med_max_power = (max_power + next_max_power) / 2; // due to the way process_bins works we have to keep resetting the color
|
||||
if (process_bins(&med_max_power) == true)
|
||||
return; // new line signaled, return
|
||||
@@ -285,7 +292,7 @@ void GlassView::on_range_changed() {
|
||||
max_power = 0;
|
||||
bins_hz_size = 0;
|
||||
|
||||
on_marker_change();
|
||||
on_marker_change(0);
|
||||
update_range_field();
|
||||
|
||||
// set the sample rate and bandwidth
|
||||
@@ -298,8 +305,8 @@ void GlassView::on_range_changed() {
|
||||
receiver_model.set_target_frequency(f_center); // tune rx for this slice
|
||||
}
|
||||
|
||||
void GlassView::plot_marker(uint8_t pos) {
|
||||
uint8_t shift_y = 0;
|
||||
void GlassView::plot_marker(uint16_t pos) {
|
||||
uint16_t shift_y = 0;
|
||||
if (live_frequency_view > 0) // plot one line down when in live view
|
||||
{
|
||||
shift_y = 16;
|
||||
@@ -381,9 +388,10 @@ GlassView::GlassView(
|
||||
&button_beep_squelch,
|
||||
&field_marker,
|
||||
&field_trigger,
|
||||
&button_marker_minus,
|
||||
&button_marker_plus,
|
||||
&button_jump,
|
||||
&button_rst,
|
||||
&field_rx_iq_phase_cal,
|
||||
&freq_stats});
|
||||
|
||||
load_presets(); // Load available presets from TXT files (or default).
|
||||
@@ -491,13 +499,7 @@ GlassView::GlassView(
|
||||
range_presets.set_selected_index(preset_index);
|
||||
|
||||
field_marker.on_encoder_change = [this](TextField&, EncoderEvent delta) {
|
||||
if ((marker_pixel_index + delta) < 0)
|
||||
marker_pixel_index = marker_pixel_index + delta + screen_width;
|
||||
else if ((marker_pixel_index + delta) > screen_width)
|
||||
marker_pixel_index = marker_pixel_index + delta - screen_width;
|
||||
else
|
||||
marker_pixel_index = marker_pixel_index + delta;
|
||||
on_marker_change();
|
||||
on_marker_change(delta);
|
||||
};
|
||||
|
||||
field_marker.on_select = [this](TextField&) {
|
||||
@@ -516,6 +518,14 @@ GlassView::GlassView(
|
||||
update_range_field();
|
||||
};
|
||||
|
||||
button_marker_minus.on_select = [this](Button&) {
|
||||
on_marker_change(-1);
|
||||
};
|
||||
|
||||
button_marker_plus.on_select = [this](Button&) {
|
||||
on_marker_change(1);
|
||||
};
|
||||
|
||||
button_jump.on_select = [this](Button&) {
|
||||
// Launch Audio with peak frequency.
|
||||
launch_audio(max_freq_hold);
|
||||
@@ -525,13 +535,6 @@ GlassView::GlassView(
|
||||
reset_live_view();
|
||||
};
|
||||
|
||||
field_rx_iq_phase_cal.set_range(0, hackrf_r9 ? 63 : 31); // max2839 has 6 bits [0..63], max2837 has 5 bits [0..31]
|
||||
field_rx_iq_phase_cal.set_value(get_spec_iq_phase_calibration_value()); // using accessor function of AnalogAudioView to read iq_phase_calibration_value from rx_audio.ini
|
||||
field_rx_iq_phase_cal.on_change = [this](int32_t v) {
|
||||
set_spec_iq_phase_calibration_value(v); // using accessor function of AnalogAudioView to write inside SPEC submenu, register value to max283x and save it to rx_audio.ini
|
||||
};
|
||||
set_spec_iq_phase_calibration_value(get_spec_iq_phase_calibration_value()); // initialize iq_phase_calibration in radio
|
||||
|
||||
display.scroll_set_area(109, screen_height - 1); // Restart scroll on the correct coordinates
|
||||
|
||||
// trigger:
|
||||
@@ -581,15 +584,6 @@ void GlassView::on_freqchg(int64_t freq) {
|
||||
on_range_changed();
|
||||
}
|
||||
|
||||
uint8_t GlassView::get_spec_iq_phase_calibration_value() { // define accessor functions inside AnalogAudioView to read & write real iq_phase_calibration_value
|
||||
return iq_phase_calibration_value;
|
||||
}
|
||||
|
||||
void GlassView::set_spec_iq_phase_calibration_value(uint8_t cal_value) { // define accessor functions
|
||||
iq_phase_calibration_value = cal_value;
|
||||
radio::set_rx_max283x_iq_phase_calibration(iq_phase_calibration_value);
|
||||
}
|
||||
|
||||
void GlassView::load_presets() {
|
||||
File presets_file;
|
||||
auto error = presets_file.open(looking_glass_dir / u"PRESETS.TXT");
|
||||
|
||||
@@ -66,9 +66,6 @@ class GlassView : public View {
|
||||
void on_hide() override;
|
||||
void focus() override;
|
||||
|
||||
uint8_t get_spec_iq_phase_calibration_value();
|
||||
void set_spec_iq_phase_calibration_value(uint8_t cal_value);
|
||||
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
Gradient gradient{};
|
||||
@@ -80,11 +77,10 @@ class GlassView : public View {
|
||||
uint8_t filter_index = 0; // OFF
|
||||
uint8_t trigger = 32;
|
||||
uint8_t mode = LOOKING_GLASS_FASTSCAN;
|
||||
uint8_t live_frequency_view = 0; // Spectrum
|
||||
uint8_t live_frequency_integrate = 3; // Default (3 * old value + new_value) / 4
|
||||
uint8_t iq_phase_calibration_value{15}; // initial default RX IQ phase calibration value , used for both max2837 & max2839
|
||||
int32_t beep_squelch = 20; // range from -100 to +20, >=20 disabled
|
||||
bool beep_enabled = false; // activate on bip button click
|
||||
uint8_t live_frequency_view = 0; // Spectrum
|
||||
uint8_t live_frequency_integrate = 3; // Default (3 * old value + new_value) / 4
|
||||
int32_t beep_squelch = 20; // range from -100 to +20, >=20 disabled
|
||||
bool beep_enabled = false; // activate on bip button click
|
||||
app_settings::SettingsManager settings_{
|
||||
"rx_glass"sv,
|
||||
app_settings::Mode::RX,
|
||||
@@ -97,7 +93,6 @@ class GlassView : public View {
|
||||
{"scan_mode"sv, &mode},
|
||||
{"freq_view"sv, &live_frequency_view},
|
||||
{"freq_integrate"sv, &live_frequency_integrate},
|
||||
{"iq_phase_calibration"sv, &iq_phase_calibration_value}, // we are saving and restoring that CAL from Settings.
|
||||
{"beep_squelch"sv, &beep_squelch},
|
||||
{"beep_enabled"sv, &beep_enabled},
|
||||
}};
|
||||
@@ -116,9 +111,9 @@ class GlassView : public View {
|
||||
void update_min(int32_t v);
|
||||
void update_max(int32_t v);
|
||||
void update_range_field();
|
||||
void get_max_power(const ChannelSpectrum& spectrum, uint8_t bin, uint8_t& max_power);
|
||||
rf::Frequency get_freq_from_bin_pos(uint8_t pos);
|
||||
void on_marker_change();
|
||||
void get_max_power(const ChannelSpectrum& spectrum, uint16_t bin, uint8_t& max_power);
|
||||
rf::Frequency get_freq_from_bin_pos(uint16_t pos);
|
||||
void on_marker_change(int8_t delta);
|
||||
void retune();
|
||||
bool process_bins(uint8_t* powerlevel);
|
||||
void on_channel_spectrum(const ChannelSpectrum& spectrum);
|
||||
@@ -128,7 +123,7 @@ class GlassView : public View {
|
||||
void on_range_changed();
|
||||
void reset_live_view();
|
||||
void add_spectrum_pixel(uint8_t power);
|
||||
void plot_marker(uint8_t pos);
|
||||
void plot_marker(uint16_t pos);
|
||||
void load_presets();
|
||||
void populate_presets();
|
||||
void launch_audio(rf::Frequency center_freq);
|
||||
@@ -138,7 +133,7 @@ class GlassView : public View {
|
||||
rf::Frequency f_center_ini{0};
|
||||
|
||||
rf::Frequency marker{0};
|
||||
uint8_t marker_pixel_index{0};
|
||||
uint16_t marker_pixel_index{0};
|
||||
rf::Frequency marker_pixel_step{0};
|
||||
|
||||
// Size of one spectrum bin in Hz.
|
||||
@@ -173,8 +168,7 @@ class GlassView : public View {
|
||||
{{0, UI_POS_Y(0)}, "MIN: MAX: LNA VGA ", Theme::getInstance()->fg_light->foreground},
|
||||
{{0, 1 * 16}, "RANGE: FILTER: AMP:", Theme::getInstance()->fg_light->foreground},
|
||||
{{0, 2 * 16}, "P:", Theme::getInstance()->fg_light->foreground},
|
||||
{{0, 3 * 16}, "MARKER: MHz RXIQCAL", Theme::getInstance()->fg_light->foreground},
|
||||
//{{0, 4 * 16}, "RES: STEPS:", Theme::getInstance()->fg_light->foreground}};
|
||||
{{0, 3 * 16}, "MARKER( / ): MHz", Theme::getInstance()->fg_light->foreground},
|
||||
{{0, 4 * 16}, "RES: VOL:", Theme::getInstance()->fg_light->foreground}};
|
||||
|
||||
NumberField field_frequency_min{
|
||||
@@ -223,16 +217,16 @@ class GlassView : public View {
|
||||
""};
|
||||
|
||||
TextField field_marker{
|
||||
{7 * 8, 3 * 16, 9 * 8, 16},
|
||||
{12 * 8, 3 * 16, 9 * 8, 16},
|
||||
""};
|
||||
|
||||
NumberField field_rx_iq_phase_cal{
|
||||
{28 * 8, 3 * 16},
|
||||
2,
|
||||
{0, 63}, // 5 or 6 bits IQ CAL phase adjustment (range updated later)
|
||||
1,
|
||||
' ',
|
||||
};
|
||||
Button button_marker_minus{
|
||||
{7 * 8, 3 * 16 + 4, 1 * 8, 1 * 8},
|
||||
"-"};
|
||||
|
||||
Button button_marker_plus{
|
||||
{9 * 8, 3 * 16 + 4, 1 * 8, 1 * 8},
|
||||
"+"};
|
||||
|
||||
NumberField field_trigger{
|
||||
{4 * 8, 4 * 16},
|
||||
|
||||
@@ -350,6 +350,12 @@ MicTXView::MicTXView(
|
||||
&tx_button,
|
||||
&tx_icon});
|
||||
|
||||
// disable key repeat on select
|
||||
initial_switch_config_ = get_switches_repeat_config();
|
||||
SwitchesState config = initial_switch_config_;
|
||||
config[toUType(Switch::Sel)] = false;
|
||||
set_switches_repeat_config(config);
|
||||
|
||||
set_rxbw_options();
|
||||
set_rxbw_defaults(settings_.loaded());
|
||||
|
||||
@@ -640,6 +646,9 @@ MicTXView::MicTXView(
|
||||
}
|
||||
|
||||
MicTXView::~MicTXView() {
|
||||
// restore select key repeat mode
|
||||
set_switches_repeat_config(initial_switch_config_);
|
||||
|
||||
audio::input::stop();
|
||||
if (rx_enabled) { // Also turn off both (audio rx if enabled, and disable mic_loop to HP)
|
||||
rxaudio(false);
|
||||
|
||||
@@ -90,6 +90,9 @@ class MicTXView : public View {
|
||||
sampling_rate /* sampling rate */
|
||||
};
|
||||
|
||||
// structure used to control key repeat
|
||||
SwitchesState initial_switch_config_{};
|
||||
|
||||
enum Mic_Modulation : uint32_t {
|
||||
MIC_MOD_NFM = 0,
|
||||
MIC_MOD_WFM = 1,
|
||||
|
||||
@@ -309,6 +309,39 @@ SetFrequencyCorrectionModel SetRadioView::form_collect() {
|
||||
};
|
||||
}
|
||||
|
||||
/* SetTXLimitView ************************************/
|
||||
|
||||
SetTXLimitView::SetTXLimitView(NavigationView& nav) {
|
||||
add_children({
|
||||
&labels,
|
||||
&tx_gain_max_db,
|
||||
&tx_disable_switch,
|
||||
&tx_amp_disable_switch,
|
||||
&button_save,
|
||||
&button_cancel,
|
||||
});
|
||||
|
||||
tx_disable_switch.set_value(pmem::config_tx_disabled());
|
||||
tx_amp_disable_switch.set_value(pmem::config_tx_amp_disabled());
|
||||
tx_gain_max_db.set_value(pmem::config_tx_gain_max_db());
|
||||
|
||||
button_save.on_select = [&nav, this](Button&) {
|
||||
pmem::set_config_tx_disabled(tx_disable_switch.value());
|
||||
pmem::set_config_tx_amp_disabled(tx_amp_disable_switch.value());
|
||||
pmem::set_config_tx_gain_max_db(tx_gain_max_db.value());
|
||||
send_system_refresh();
|
||||
nav.pop();
|
||||
};
|
||||
|
||||
button_cancel.on_select = [&nav, this](Button&) {
|
||||
nav.pop();
|
||||
};
|
||||
}
|
||||
|
||||
void SetTXLimitView::focus() {
|
||||
button_save.focus();
|
||||
}
|
||||
|
||||
/* SetUIView *********************************************/
|
||||
|
||||
SetUIView::SetUIView(NavigationView& nav) {
|
||||
@@ -1107,6 +1140,7 @@ void SettingsMenuView::on_populate() {
|
||||
{"Freq. Correct", ui::Color::dark_cyan(), &bitmap_icon_options_radio, [this]() { nav_.push<SetFrequencyCorrectionView>(); }},
|
||||
{"P.Memory Mgmt", ui::Color::dark_cyan(), &bitmap_icon_memory, [this]() { nav_.push<SetPersistentMemoryView>(); }},
|
||||
{"Radio", ui::Color::dark_cyan(), &bitmap_icon_options_radio, [this]() { nav_.push<SetRadioView>(); }},
|
||||
{"TX Limit", ui::Color::dark_cyan(), &bitmap_icon_options_radio, [this]() { nav_.push<SetTXLimitView>(); }},
|
||||
{"SD Card", ui::Color::dark_cyan(), &bitmap_icon_sdcard, [this]() { nav_.push<SetSDCardView>(); }},
|
||||
{"User Interface", ui::Color::dark_cyan(), &bitmap_icon_options_ui, [this]() { nav_.push<SetUIView>(); }},
|
||||
{"Display", ui::Color::dark_cyan(), &bitmap_icon_brightness, [this]() { nav_.push<SetDisplayView>(); }},
|
||||
|
||||
@@ -264,6 +264,51 @@ class SetRadioView : public View {
|
||||
SetFrequencyCorrectionModel form_collect();
|
||||
};
|
||||
|
||||
class SetTXLimitView : public View {
|
||||
public:
|
||||
SetTXLimitView(NavigationView& nav);
|
||||
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "TX Limit"; };
|
||||
|
||||
private:
|
||||
Labels labels{
|
||||
{{1 * 8, 1 * 16}, "Limits RF TX Gain", Theme::getInstance()->fg_light->foreground},
|
||||
{{1 * 8, 2 * 16}, "(This may affect", Theme::getInstance()->fg_light->foreground},
|
||||
{{1 * 8, 3 * 16}, "all applications.)", Theme::getInstance()->fg_light->foreground},
|
||||
{{2 * 8, 12 * 16}, "TX Max Gain:", Theme::getInstance()->fg_light->foreground},
|
||||
|
||||
};
|
||||
|
||||
Checkbox tx_disable_switch{
|
||||
{1 * 8, 6 * 16},
|
||||
23,
|
||||
"Disable TX"};
|
||||
|
||||
Checkbox tx_amp_disable_switch{
|
||||
{1 * 8, 8 * 16},
|
||||
23,
|
||||
"Disable TX Amp"};
|
||||
|
||||
NumberField tx_gain_max_db{
|
||||
{20 * 8, 12 * 16},
|
||||
6,
|
||||
{0, 47},
|
||||
1,
|
||||
' ',
|
||||
};
|
||||
|
||||
Button button_save{
|
||||
{UI_POS_X_CENTER(12) - UI_POS_WIDTH(8), UI_POS_Y_BOTTOM(4), UI_POS_WIDTH(12), UI_POS_HEIGHT(2)},
|
||||
"Save"};
|
||||
|
||||
Button button_cancel{
|
||||
{UI_POS_X_CENTER(16) + UI_POS_WIDTH(8), UI_POS_Y_BOTTOM(4), UI_POS_WIDTH(12), UI_POS_HEIGHT(2)},
|
||||
"Cancel",
|
||||
};
|
||||
};
|
||||
|
||||
using portapack::persistent_memory::backlight_timeout_t;
|
||||
|
||||
class SetUIView : public View {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -368,6 +368,21 @@ void set_subghzd_config(uint8_t modulation = 0, uint32_t sampling_rate = 0) {
|
||||
send_message(&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);
|
||||
}
|
||||
|
||||
static bool baseband_image_running = false;
|
||||
|
||||
void run_image(const spi_flash::image_tag_t image_tag) {
|
||||
|
||||
@@ -102,6 +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(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();
|
||||
|
||||
@@ -4297,7 +4297,7 @@ static constexpr Bitmap bitmap_icon_tpms{
|
||||
{16, 16},
|
||||
bitmap_icon_tpms_data};
|
||||
|
||||
static constexpr uint8_t bitmap_icon_tranceivers_data[] = {
|
||||
static constexpr uint8_t bitmap_icon_transceivers_data[] = {
|
||||
0x80,
|
||||
0x01,
|
||||
0xC0,
|
||||
@@ -4331,9 +4331,9 @@ static constexpr uint8_t bitmap_icon_tranceivers_data[] = {
|
||||
0xFF,
|
||||
0xFF,
|
||||
};
|
||||
static constexpr Bitmap bitmap_icon_tranceivers{
|
||||
static constexpr Bitmap bitmap_icon_transceivers{
|
||||
{16, 16},
|
||||
bitmap_icon_tranceivers_data};
|
||||
bitmap_icon_transceivers_data};
|
||||
|
||||
static constexpr uint8_t bitmap_icon_transmit_data[] = {
|
||||
0x80,
|
||||
|
||||
+38
-25
@@ -62,19 +62,24 @@ AppManagerView::AppManagerView(NavigationView& nav)
|
||||
auto app_name = get_app_info(menu_view.highlighted_index(), true);
|
||||
auto app_id = get_app_info(menu_view.highlighted_index(), false);
|
||||
|
||||
info += "Hidden:";
|
||||
if (!app_name.empty()) {
|
||||
info += "Hidden:";
|
||||
|
||||
if (is_blacklisted(app_name)) {
|
||||
info += "Yes ";
|
||||
} else {
|
||||
info += "No ";
|
||||
if (is_blacklisted(app_name)) {
|
||||
info += "Yes ";
|
||||
} else {
|
||||
info += "No ";
|
||||
}
|
||||
}
|
||||
|
||||
info += "Autostart:";
|
||||
if (is_autostart_app(app_id)) {
|
||||
info += "Yes";
|
||||
} else {
|
||||
info += "No";
|
||||
if (!app_id.empty()) {
|
||||
info += "Autostart:";
|
||||
|
||||
if (is_autostart_app(app_id)) {
|
||||
info += "Yes";
|
||||
} else {
|
||||
info += "No";
|
||||
}
|
||||
}
|
||||
|
||||
if (info.empty()) {
|
||||
@@ -82,6 +87,8 @@ AppManagerView::AppManagerView(NavigationView& nav)
|
||||
}
|
||||
|
||||
text_app_info.set(info);
|
||||
button_hide_unhide.set_focusable(!app_name.empty());
|
||||
button_set_cancel_autostart.set_focusable(!app_id.empty());
|
||||
};
|
||||
|
||||
button_hide_unhide.on_select = [this](Button&) {
|
||||
@@ -120,27 +127,23 @@ void AppManagerView::refresh_list() {
|
||||
};
|
||||
|
||||
for (auto& app : NavigationView::appList) {
|
||||
if (app.id == nullptr) continue;
|
||||
|
||||
app_list_index++;
|
||||
|
||||
menu_view.add_item({app.displayName + std::string(is_autostart_app(app.id) ? padding(app.displayName) : ""),
|
||||
menu_view.add_item({app.displayName + std::string((app.id != nullptr && is_autostart_app(app.id)) ? padding(app.displayName) : ""),
|
||||
app.iconColor,
|
||||
app.icon,
|
||||
[this, app_id = std::string(app.id)](KeyEvent) {
|
||||
[this, app_id = std::string(app.id != nullptr ? app.id : "")](KeyEvent) {
|
||||
button_hide_unhide.focus();
|
||||
}});
|
||||
}
|
||||
|
||||
ExternalItemsMenuLoader::load_all_external_items_callback([this, &index, &padding](ui::AppInfoConsole& app) {
|
||||
if (app.appCallName == nullptr) return;
|
||||
|
||||
app_list_index++;
|
||||
|
||||
menu_view.add_item({app.appFriendlyName + std::string(is_autostart_app(app.appCallName) ? padding(app.appFriendlyName) : ""),
|
||||
menu_view.add_item({app.appFriendlyName + std::string((app.appCallName != nullptr && is_autostart_app(app.appCallName)) ? padding(app.appFriendlyName) : ""),
|
||||
ui::Color::light_grey(),
|
||||
&bitmap_icon_sdcard,
|
||||
[this, app_id = std::string(app.appCallName)](KeyEvent) {
|
||||
[this, app_id = std::string(app.appCallName != nullptr ? app.appCallName : "")](KeyEvent) {
|
||||
button_hide_unhide.focus();
|
||||
}});
|
||||
});
|
||||
@@ -156,19 +159,29 @@ void AppManagerView::hide_app() {
|
||||
std::vector<std::string> blacklist;
|
||||
get_blacklist(blacklist);
|
||||
|
||||
blacklist.push_back(get_app_info(menu_view.highlighted_index(), true));
|
||||
auto app_name = get_app_info(menu_view.highlighted_index(), true);
|
||||
if (app_name.empty()) return;
|
||||
|
||||
blacklist.push_back(app_name);
|
||||
write_blacklist(blacklist);
|
||||
}
|
||||
|
||||
void AppManagerView::unhide_app() {
|
||||
std::vector<std::string> blacklist;
|
||||
get_blacklist(blacklist);
|
||||
blacklist.erase(std::remove(blacklist.begin(), blacklist.end(), get_app_info(menu_view.highlighted_index(), true)), blacklist.end());
|
||||
|
||||
auto app_name = get_app_info(menu_view.highlighted_index(), true);
|
||||
if (app_name.empty()) return;
|
||||
|
||||
blacklist.erase(std::remove(blacklist.begin(), blacklist.end(), app_name), blacklist.end());
|
||||
write_blacklist(blacklist);
|
||||
}
|
||||
|
||||
void AppManagerView::hide_unhide_app() {
|
||||
if (is_blacklisted(get_app_info(menu_view.highlighted_index(), true)))
|
||||
auto app_name = get_app_info(menu_view.highlighted_index(), true);
|
||||
if (app_name.empty()) return;
|
||||
|
||||
if (is_blacklisted(app_name))
|
||||
unhide_app();
|
||||
else
|
||||
hide_app();
|
||||
@@ -218,6 +231,7 @@ void AppManagerView::set_auto_start() {
|
||||
if (app_index >= app_list_index) return;
|
||||
|
||||
auto id_aka_app_call_name = get_app_info(app_index, false);
|
||||
if (id_aka_app_call_name.empty()) return;
|
||||
|
||||
autostart_app = id_aka_app_call_name;
|
||||
|
||||
@@ -234,6 +248,7 @@ void AppManagerView::set_unset_autostart_app() {
|
||||
if (app_index >= app_list_index) return;
|
||||
|
||||
auto id_aka_friendly_name = get_app_info(app_index, false);
|
||||
if (id_aka_friendly_name.empty()) return;
|
||||
|
||||
if (is_autostart_app(id_aka_friendly_name))
|
||||
unset_auto_start();
|
||||
@@ -254,17 +269,15 @@ std::string AppManagerView::get_app_info(uint16_t index, bool is_display_name) {
|
||||
std::string result;
|
||||
|
||||
for (auto& app : NavigationView::appList) {
|
||||
if (app.id == nullptr) continue;
|
||||
if (current_index == index) {
|
||||
return is_display_name ? std::string(app.displayName) : std::string(app.id);
|
||||
return is_display_name ? std::string(app.displayName != nullptr ? app.displayName : "") : std::string(app.id != nullptr ? app.id : "");
|
||||
}
|
||||
current_index++;
|
||||
}
|
||||
|
||||
ExternalItemsMenuLoader::load_all_external_items_callback([¤t_index, &index, &result, &is_display_name](ui::AppInfoConsole& app) {
|
||||
if (app.appCallName == nullptr) return;
|
||||
if (current_index == index) {
|
||||
result = is_display_name ? app.appFriendlyName : app.appCallName;
|
||||
result = is_display_name ? (app.appFriendlyName != nullptr ? app.appFriendlyName : "") : (app.appCallName != nullptr ? app.appCallName : "");
|
||||
}
|
||||
current_index++;
|
||||
});
|
||||
|
||||
+31
-5
@@ -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
|
||||
@@ -276,6 +280,23 @@ set(EXTCPPSRC
|
||||
#subcarrx
|
||||
external/subcarrx/main.cpp
|
||||
external/subcarrx/ui_subcar.cpp
|
||||
|
||||
#siggen
|
||||
external/siggen/main.cpp
|
||||
external/siggen/ui_siggen.cpp
|
||||
|
||||
#sdusb
|
||||
external/sdusb/main.cpp
|
||||
external/sdusb/ui_sd_over_usb.cpp
|
||||
|
||||
|
||||
#morse_radio
|
||||
external/morse_radio/main.cpp
|
||||
external/morse_radio/ui_morse_radio.cpp
|
||||
|
||||
#morseradiotx
|
||||
external/morseradiotx/main.cpp
|
||||
external/morseradiotx/ui_morse_radiotx.cpp
|
||||
)
|
||||
|
||||
set(EXTAPPLIST
|
||||
@@ -300,7 +321,7 @@ set(EXTAPPLIST
|
||||
tpmsrx
|
||||
protoview
|
||||
adsbtx
|
||||
morse_tx
|
||||
#morse_tx
|
||||
sstvtx
|
||||
sstvrx
|
||||
random_password
|
||||
@@ -332,6 +353,7 @@ set(EXTAPPLIST
|
||||
scanner
|
||||
level
|
||||
gfxeq
|
||||
waterfall_designer
|
||||
detector_rx
|
||||
spaceinv
|
||||
blackjack
|
||||
@@ -345,4 +367,8 @@ set(EXTAPPLIST
|
||||
adult_toys_controller
|
||||
flex_rx
|
||||
subcarrx
|
||||
siggen
|
||||
sdusb
|
||||
morse_radio
|
||||
morseradiotx
|
||||
)
|
||||
|
||||
+35
-2
@@ -89,7 +89,11 @@ MEMORY
|
||||
ram_external_app_flex_rx (rwx) : org = 0xADF00000, len = 32k
|
||||
ram_external_app_sstvrx (rwx) : org = 0xADF10000, len = 32k
|
||||
ram_external_app_subcarrx (rwx) : org = 0xADF20000, len = 32k
|
||||
|
||||
ram_external_app_siggen (rwx) : org = 0xADF30000, len = 32k
|
||||
ram_external_app_sdusb (rwx) : org = 0xADF40000, len = 32k
|
||||
ram_external_app_morse_radio (rwx) : org = 0xADF50000, len = 32k
|
||||
ram_external_app_waterfall_designer (rwx) : org = 0xADF60000, len = 32k
|
||||
ram_external_app_morseradiotx (rwx) : org = 0xADF70000, len = 32k
|
||||
}
|
||||
|
||||
SECTIONS
|
||||
@@ -400,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));
|
||||
@@ -492,6 +502,29 @@ SECTIONS
|
||||
*(*ui*external_app*subcarrx*);
|
||||
} > ram_external_app_subcarrx
|
||||
|
||||
|
||||
.external_app_siggen : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_siggen.application_information));
|
||||
*(*ui*external_app*siggen*);
|
||||
} > ram_external_app_siggen
|
||||
|
||||
.external_app_sdusb : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_sdusb.application_information));
|
||||
*(*ui*external_app*sdusb*);
|
||||
} > ram_external_app_sdusb
|
||||
|
||||
.external_app_morse_radio : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_morse_radio.application_information));
|
||||
*(*ui*external_app*morse_radio*);
|
||||
} > ram_external_app_morse_radio
|
||||
|
||||
.external_app_morseradiotx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_morseradiotx.application_information));
|
||||
*(*ui*external_app*morseradiotx*);
|
||||
} > ram_external_app_morseradiotx
|
||||
|
||||
}
|
||||
|
||||
|
||||
+27
-12
@@ -24,8 +24,7 @@
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
#define MORSEDEC_ERROR "<ERR>"
|
||||
#include "string_format.hpp"
|
||||
|
||||
namespace ui::external_app::morse_practice {
|
||||
|
||||
@@ -89,21 +88,28 @@ class MorseDecoder {
|
||||
};
|
||||
|
||||
MorseDecoder() {}
|
||||
DecodeResult handleInput(int32_t duration_ms) {
|
||||
|
||||
DecodeResult
|
||||
handleInput(int32_t duration_ms) {
|
||||
DecodeResult result = {"", 0.0};
|
||||
|
||||
if (duration_ms < 5 && duration_ms > -5) return result;
|
||||
|
||||
if (duration_ms > 0) {
|
||||
pulse_history_.push_back(duration_ms);
|
||||
double dah_prob = getDahProbability(duration_ms);
|
||||
current_sequence_ += (dah_prob > 0.5) ? '-' : '.';
|
||||
last_confidence_ = (dah_prob > 0.5) ? dah_prob : (1.0 - dah_prob);
|
||||
last_sequence_ = current_sequence_;
|
||||
|
||||
} else {
|
||||
uint32_t gap_duration = -duration_ms;
|
||||
pulse_gaps_.push_back(gap_duration);
|
||||
|
||||
if (gap_duration >= getInterCharThreshold() && !current_sequence_.empty()) {
|
||||
result.text = lookupMorse(current_sequence_);
|
||||
result.confidence = (result.text != MORSEDEC_ERROR) ? last_confidence_ : 0.0;
|
||||
|
||||
result.confidence = (result.text[0] != '{') ? last_confidence_ : 0.0;
|
||||
if (gap_duration >= getInterWordThreshold()) {
|
||||
result.text += " ";
|
||||
}
|
||||
@@ -116,9 +122,9 @@ class MorseDecoder {
|
||||
return result;
|
||||
}
|
||||
|
||||
inline double getInterElementThreshold() { return time_unit_ms_ * 2.0; }
|
||||
inline double getInterCharThreshold() { return time_unit_ms_ * 4.0; }
|
||||
inline double getInterWordThreshold() { return time_unit_ms_ * 7.0; }
|
||||
inline double getInterElementThreshold() { return time_unit_ms_ * 0.8; }
|
||||
inline double getInterCharThreshold() { return time_unit_ms_ * 2.5; }
|
||||
inline double getInterWordThreshold() { return time_unit_ms_ * 6.0; }
|
||||
|
||||
inline double getCurrentTimeUnit() { return time_unit_ms_; }
|
||||
inline std::string getLastSequence() { return last_sequence_; }
|
||||
@@ -126,7 +132,7 @@ class MorseDecoder {
|
||||
private:
|
||||
std::string current_sequence_ = "";
|
||||
std::string last_sequence_ = "";
|
||||
double time_unit_ms_ = 160.0;
|
||||
double time_unit_ms_ = 119.0;
|
||||
double last_confidence_ = 0.0;
|
||||
MorseRingBuffer pulse_history_{};
|
||||
MorseRingBuffer pulse_gaps_{};
|
||||
@@ -136,7 +142,7 @@ class MorseDecoder {
|
||||
if (seq == morse_table_[i].code)
|
||||
return morse_table_[i].letter;
|
||||
}
|
||||
return MORSEDEC_ERROR; // not found
|
||||
return "{" + seq + "}"; // not found
|
||||
}
|
||||
|
||||
double getDahProbability(uint32_t duration_ms) {
|
||||
@@ -298,8 +304,8 @@ class MorseDecoder {
|
||||
time_unit_ms_ = (time_unit_ms_ * (1.0 - learning_factor)) + (new_time_unit * learning_factor);
|
||||
}
|
||||
|
||||
size_t morse_table_size_ = 41;
|
||||
MorseEntry morse_table_[41] = {
|
||||
size_t morse_table_size_ = 50;
|
||||
MorseEntry morse_table_[50] = {
|
||||
{".-", "A"},
|
||||
{"-...", "B"},
|
||||
{"-.-.", "C"},
|
||||
@@ -340,7 +346,16 @@ class MorseDecoder {
|
||||
{"..--..", "?"},
|
||||
{"-.-.--", "!"},
|
||||
{"--..--", ","},
|
||||
{"-...-", "="}};
|
||||
{"-...-", "="},
|
||||
{"-..-.", "/"},
|
||||
{".--.-.", "@"},
|
||||
{"---...", ":"},
|
||||
{"-....-", "-"},
|
||||
{".----.", "'"},
|
||||
{".-..-.", "\""},
|
||||
{"-.--.", "("},
|
||||
{"-.--.-", ")"},
|
||||
{".-.-.", "+"}};
|
||||
};
|
||||
|
||||
} // namespace ui::external_app::morse_practice
|
||||
|
||||
@@ -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();
|
||||
@@ -98,7 +106,7 @@ void MorsePracticeView::writeCharToConsole(const std::string& ch, double confide
|
||||
|
||||
if (ch == " ") {
|
||||
color_id = 0;
|
||||
} else if (ch == MORSEDEC_ERROR) {
|
||||
} else if (ch[0] == '{') { // no match
|
||||
color_id = 0;
|
||||
} else {
|
||||
if (confidence < 0.8)
|
||||
|
||||
@@ -47,7 +47,7 @@ class MorsePracticeView : public ui::View {
|
||||
MorsePracticeView(ui::NavigationView& nav);
|
||||
~MorsePracticeView();
|
||||
|
||||
std::string title() { return "Morse P"; }
|
||||
std::string title() const override { return "Morse Practice"; }
|
||||
void focus() override;
|
||||
void on_show() override;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Pezsma
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "ui_morse_radio.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "external_app.hpp"
|
||||
|
||||
namespace ui::external_app::morse_radio {
|
||||
|
||||
void initialize_app(NavigationView& nav) {
|
||||
nav.push<MorseRadioView>();
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::morse_radio
|
||||
|
||||
extern "C" {
|
||||
|
||||
__attribute__((section(".external_app.app_morse_radio.application_information"), used))
|
||||
application_information_t _application_information_morse_radio = {
|
||||
/*.memory_location = */ (uint8_t*)0x00000000,
|
||||
/*.externalAppEntry = */ ui::external_app::morse_radio::initialize_app,
|
||||
/*.header_version = */ CURRENT_HEADER_VERSION,
|
||||
/*.app_version = */ VERSION_MD5,
|
||||
|
||||
/*.app_name = */ "Morse",
|
||||
/*.bitmap_data = */ {
|
||||
0x00,
|
||||
0x00,
|
||||
0xFE,
|
||||
0x7F,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xBB,
|
||||
0xD0,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0x0B,
|
||||
0xE1,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xEB,
|
||||
0xD0,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFE,
|
||||
0x7F,
|
||||
0x70,
|
||||
0x00,
|
||||
0x30,
|
||||
0x00,
|
||||
0x10,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
},
|
||||
/*.icon_color = */ ui::Color::yellow().v,
|
||||
/*.menu_location = */ app_location_t::RX,
|
||||
/*.desired_menu_position = */ -1,
|
||||
|
||||
/*.m4_app_tag = portapack::spi_flash::image_tag_morse_radio */ {'P', 'M', 'R', 'S'},
|
||||
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,378 @@
|
||||
#ifndef __MORSEDECODER_HPP__
|
||||
#define __MORSEDECODER_HPP__
|
||||
|
||||
/*
|
||||
* Copyright (C) 2025 Pezsma
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include "string_format.hpp"
|
||||
|
||||
namespace ui::external_app::morse_radio {
|
||||
|
||||
class MorseRingBuffer {
|
||||
public:
|
||||
MorseRingBuffer()
|
||||
: head_(0), tail_(0), count_(0) {}
|
||||
|
||||
void push_back(const uint32_t& value) {
|
||||
data_[head_] = value;
|
||||
head_ = (head_ + 1) % 40;
|
||||
if (count_ < 40) {
|
||||
count_++;
|
||||
} else {
|
||||
// overwrite oldest element
|
||||
tail_ = (tail_ + 1) % 40;
|
||||
}
|
||||
}
|
||||
|
||||
void pop_front() {
|
||||
if (count_ > 0) {
|
||||
tail_ = (tail_ + 1) % 40;
|
||||
count_--;
|
||||
}
|
||||
}
|
||||
|
||||
size_t size() const { return count_; }
|
||||
bool empty() const { return count_ == 0; }
|
||||
const uint32_t& front() const { return data_[tail_]; }
|
||||
// Access by index (0 = oldest)
|
||||
uint32_t operator[](size_t idx) const {
|
||||
return data_[(tail_ + idx) % 40];
|
||||
}
|
||||
// Convert to vector-like access for sorting etc.
|
||||
void copy_to_array(uint32_t* out) const {
|
||||
for (size_t i = 0; i < count_; ++i)
|
||||
out[i] = (*this)[i];
|
||||
}
|
||||
|
||||
void clear() {
|
||||
head_ = 0;
|
||||
tail_ = 0;
|
||||
count_ = 0;
|
||||
}
|
||||
|
||||
private:
|
||||
uint32_t data_[40];
|
||||
size_t head_;
|
||||
size_t tail_;
|
||||
size_t count_;
|
||||
};
|
||||
|
||||
class MorseDecoder {
|
||||
public:
|
||||
struct DecodeResult {
|
||||
std::string text = "";
|
||||
double confidence = 0.0;
|
||||
|
||||
bool isValid() const {
|
||||
return !text.empty();
|
||||
}
|
||||
};
|
||||
|
||||
struct MorseEntry {
|
||||
std::string code;
|
||||
std::string letter;
|
||||
};
|
||||
|
||||
MorseDecoder() {}
|
||||
|
||||
void resetLearning() {
|
||||
time_unit_ms_ = 119.0;
|
||||
current_sequence_ = "";
|
||||
last_sequence_ = "";
|
||||
last_confidence_ = 0.0;
|
||||
pulse_history_.clear();
|
||||
pulse_gaps_.clear();
|
||||
}
|
||||
|
||||
DecodeResult
|
||||
handleInput(int32_t duration_ms) {
|
||||
DecodeResult result = {"", 0.0};
|
||||
|
||||
if (duration_ms < 5 && duration_ms > -5) return result;
|
||||
|
||||
if (duration_ms > 0) {
|
||||
pulse_history_.push_back(duration_ms);
|
||||
double dah_prob = getDahProbability(duration_ms);
|
||||
current_sequence_ += (dah_prob > 0.5) ? '-' : '.';
|
||||
last_confidence_ = (dah_prob > 0.5) ? dah_prob : (1.0 - dah_prob);
|
||||
last_sequence_ = current_sequence_;
|
||||
|
||||
} else {
|
||||
uint32_t gap_duration = -duration_ms;
|
||||
pulse_gaps_.push_back(gap_duration);
|
||||
|
||||
if (gap_duration >= getInterCharThreshold() && !current_sequence_.empty()) {
|
||||
result.text = lookupMorse(current_sequence_);
|
||||
|
||||
result.confidence = (result.text[0] != '{') ? last_confidence_ : 0.0;
|
||||
if (gap_duration >= getInterWordThreshold()) {
|
||||
result.text += " ";
|
||||
}
|
||||
current_sequence_ = "";
|
||||
}
|
||||
}
|
||||
|
||||
updateLearning();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
inline double getInterElementThreshold() { return time_unit_ms_ * 0.8; }
|
||||
inline double getInterCharThreshold() { return time_unit_ms_ * 2.5; }
|
||||
inline double getInterWordThreshold() { return time_unit_ms_ * 6.0; }
|
||||
|
||||
inline double getCurrentTimeUnit() { return time_unit_ms_; }
|
||||
inline std::string getLastSequence() { return last_sequence_; }
|
||||
|
||||
private:
|
||||
std::string current_sequence_ = "";
|
||||
std::string last_sequence_ = "";
|
||||
double time_unit_ms_ = 119.0;
|
||||
double last_confidence_ = 0.0;
|
||||
MorseRingBuffer pulse_history_{};
|
||||
MorseRingBuffer pulse_gaps_{};
|
||||
|
||||
std::string lookupMorse(const std::string& seq) {
|
||||
for (size_t i = 0; i < morse_table_size_; i++) {
|
||||
if (seq == morse_table_[i].code)
|
||||
return morse_table_[i].letter;
|
||||
}
|
||||
return "{" + seq + "}"; // not found
|
||||
}
|
||||
|
||||
double getDahProbability(uint32_t duration_ms) {
|
||||
double start_interp = 1.5 * time_unit_ms_;
|
||||
double end_interp = 2.5 * time_unit_ms_;
|
||||
|
||||
if (duration_ms <= start_interp) return 0.0;
|
||||
if (duration_ms >= end_interp) return 1.0;
|
||||
return ((double)(duration_ms)-start_interp) / (end_interp - start_interp);
|
||||
}
|
||||
|
||||
size_t findDecisionBoundary(uint32_t* sorted_data, size_t sorted_data_size) {
|
||||
if (sorted_data_size < 4) return 0;
|
||||
|
||||
size_t best_split_index = 0;
|
||||
uint32_t max_diff = 0;
|
||||
|
||||
for (size_t i = 1; i < sorted_data_size; ++i) {
|
||||
uint32_t diff = sorted_data[i] - sorted_data[i - 1];
|
||||
if (diff > sorted_data[i - 1] * 0.5 && diff > max_diff) {
|
||||
max_diff = diff;
|
||||
best_split_index = i;
|
||||
}
|
||||
}
|
||||
return best_split_index;
|
||||
}
|
||||
|
||||
bool calculatePulseUnit(double& unit, double& confidence) {
|
||||
if (pulse_history_.size() < 10) return false;
|
||||
uint32_t sorted_pulses[pulse_history_.size()];
|
||||
pulse_history_.copy_to_array(sorted_pulses);
|
||||
sort_uint32(sorted_pulses, pulse_history_.size());
|
||||
|
||||
size_t split_index = findDecisionBoundary(sorted_pulses, pulse_history_.size());
|
||||
if (split_index == 0 || split_index < 3 || (pulse_history_.size() - split_index) < 2) {
|
||||
return false;
|
||||
}
|
||||
double dit_sum = sum_uint32_range(sorted_pulses, 0, split_index);
|
||||
double dah_sum = sum_uint32_range(sorted_pulses, split_index, pulse_history_.size());
|
||||
|
||||
double avg_dit = dit_sum / split_index;
|
||||
double avg_dah = dah_sum / (pulse_history_.size() - split_index);
|
||||
if (avg_dah <= avg_dit) return false;
|
||||
|
||||
double ratio = avg_dah / avg_dit;
|
||||
if (ratio > 1.5 && ratio < 5.0) {
|
||||
unit = avg_dit;
|
||||
double tmpabs = ratio - 3.0;
|
||||
if (tmpabs < 0) tmpabs *= -1;
|
||||
tmpabs /= 3.0;
|
||||
tmpabs = 1.0 - tmpabs;
|
||||
if (tmpabs < 0) tmpabs = 0;
|
||||
confidence = tmpabs; // 0..1
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool calculateGapUnit(double& unit, double& confidence) {
|
||||
if (pulse_gaps_.size() < 10) return false;
|
||||
double threshold = getInterElementThreshold();
|
||||
double valid_gaps[pulse_gaps_.size()];
|
||||
size_t valid_count = 0;
|
||||
for (size_t i = 0; i < pulse_gaps_.size(); i++) {
|
||||
double gap = pulse_gaps_[i];
|
||||
if (gap <= threshold) {
|
||||
valid_gaps[valid_count++] = gap;
|
||||
}
|
||||
}
|
||||
if (valid_count < 2) {
|
||||
return false;
|
||||
}
|
||||
double sum = sum_double_range(valid_gaps, 0, valid_count);
|
||||
size_t count_to_average = valid_count;
|
||||
|
||||
if (count_to_average > 0) {
|
||||
unit = sum / count_to_average;
|
||||
confidence = 0.8;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
double sum_uint32_range(const uint32_t* data, size_t start, size_t end) {
|
||||
double sum = 0.0;
|
||||
for (size_t i = start; i < end; i++) {
|
||||
sum += data[i];
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
double sum_double_range(const double* data, size_t start, size_t end) {
|
||||
double sum = 0.0;
|
||||
for (size_t i = start; i < end; i++) {
|
||||
sum += data[i];
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
void sort_uint32(uint32_t* data, size_t size) {
|
||||
if (size < 2)
|
||||
return;
|
||||
|
||||
for (size_t i = 1; i < size; i++) {
|
||||
uint32_t key = data[i];
|
||||
size_t j = i;
|
||||
|
||||
while (j > 0 && data[j - 1] > key) {
|
||||
data[j] = data[j - 1];
|
||||
j--;
|
||||
}
|
||||
|
||||
data[j] = key;
|
||||
}
|
||||
}
|
||||
|
||||
double clamp_double(double value, double min_val, double max_val) {
|
||||
if (value < min_val)
|
||||
return min_val;
|
||||
else if (value > max_val)
|
||||
return max_val;
|
||||
else
|
||||
return value;
|
||||
}
|
||||
|
||||
void updateLearning() {
|
||||
double pulse_unit = -1.0, pulse_confidence = 0.0;
|
||||
double gap_unit = -1.0, gap_confidence = 0.0;
|
||||
|
||||
bool pulse_success = calculatePulseUnit(pulse_unit, pulse_confidence);
|
||||
bool gap_success = calculateGapUnit(gap_unit, gap_confidence);
|
||||
|
||||
double new_time_unit = -1.0;
|
||||
if (pulse_success && pulse_confidence > 0.5) {
|
||||
new_time_unit = pulse_unit;
|
||||
} else if (pulse_success && gap_success) {
|
||||
gap_confidence = 0.2;
|
||||
double total_confidence = pulse_confidence + gap_confidence;
|
||||
new_time_unit = (pulse_unit * pulse_confidence + gap_unit * gap_confidence) / total_confidence;
|
||||
} else if (gap_success) {
|
||||
new_time_unit = gap_unit;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
double max_change = time_unit_ms_ * 0.25;
|
||||
new_time_unit = clamp_double(new_time_unit, time_unit_ms_ - max_change, time_unit_ms_ + max_change);
|
||||
double DEFAULT_TIME_UNIT = 160.0;
|
||||
double BASE_LEARNING_RATE = 0.05;
|
||||
double MAX_LEARNING_RATE = 0.25;
|
||||
double tudeltaabs = new_time_unit - DEFAULT_TIME_UNIT;
|
||||
if (tudeltaabs < 0) tudeltaabs *= -1;
|
||||
double deviation_from_default = tudeltaabs / DEFAULT_TIME_UNIT;
|
||||
double tpp = deviation_from_default * 2.0;
|
||||
if (tpp > 1) tpp = 1;
|
||||
double learning_factor = BASE_LEARNING_RATE + (MAX_LEARNING_RATE - BASE_LEARNING_RATE) * tpp;
|
||||
|
||||
time_unit_ms_ = (time_unit_ms_ * (1.0 - learning_factor)) + (new_time_unit * learning_factor);
|
||||
}
|
||||
|
||||
size_t morse_table_size_ = 50;
|
||||
MorseEntry morse_table_[50] = {
|
||||
{".-", "A"},
|
||||
{"-...", "B"},
|
||||
{"-.-.", "C"},
|
||||
{"-..", "D"},
|
||||
{".", "E"},
|
||||
{"..-.", "F"},
|
||||
{"--.", "G"},
|
||||
{"....", "H"},
|
||||
{"..", "I"},
|
||||
{".---", "J"},
|
||||
{"-.-", "K"},
|
||||
{".-..", "L"},
|
||||
{"--", "M"},
|
||||
{"-.", "N"},
|
||||
{"---", "O"},
|
||||
{".--.", "P"},
|
||||
{"--.-", "Q"},
|
||||
{".-.", "R"},
|
||||
{"...", "S"},
|
||||
{"-", "T"},
|
||||
{"..-", "U"},
|
||||
{"...-", "V"},
|
||||
{".--", "W"},
|
||||
{"-..-", "X"},
|
||||
{"-.--", "Y"},
|
||||
{"--..", "Z"},
|
||||
{".----", "1"},
|
||||
{"..---", "2"},
|
||||
{"...--", "3"},
|
||||
{"....-", "4"},
|
||||
{".....", "5"},
|
||||
{"-....", "6"},
|
||||
{"--...", "7"},
|
||||
{"---..", "8"},
|
||||
{"----.", "9"},
|
||||
{"-----", "0"},
|
||||
{".-.-.-", "."},
|
||||
{"..--..", "?"},
|
||||
{"-.-.--", "!"},
|
||||
{"--..--", ","},
|
||||
{"-...-", "="},
|
||||
{"-..-.", "/"},
|
||||
{".--.-.", "@"},
|
||||
{"---...", ":"},
|
||||
{"-....-", "-"},
|
||||
{".----.", "'"},
|
||||
{".-..-.", "\""},
|
||||
{"-.--.", "("},
|
||||
{"-.--.-", ")"},
|
||||
{".-.-.", "+"}};
|
||||
};
|
||||
|
||||
} // namespace ui::external_app::morse_radio
|
||||
|
||||
#endif // __MORSEDECODER_HPP__
|
||||
@@ -0,0 +1,305 @@
|
||||
#include "ui_morse_radio.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
|
||||
namespace ui::external_app::morse_radio {
|
||||
|
||||
MorseRadioView::MorseRadioView(ui::NavigationView& nav)
|
||||
: nav_(nav) {
|
||||
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
|
||||
add_children({&rssi,
|
||||
&field_rf_amp,
|
||||
&field_lna,
|
||||
&field_vga,
|
||||
&field_volume,
|
||||
&field_frequency,
|
||||
&txt_last,
|
||||
&txt_speed,
|
||||
&txt_freq,
|
||||
&txt_clip,
|
||||
&options_mode,
|
||||
&btn_clear,
|
||||
&console_text,
|
||||
&labels,
|
||||
&chk_log,
|
||||
&field_squelch});
|
||||
field_frequency.set_step(100);
|
||||
|
||||
btn_clear.on_select = [this](Button&) {
|
||||
console_text.clear(true);
|
||||
txt_last.set("");
|
||||
time_stamp = false;
|
||||
if (logger && save_log)
|
||||
logger->init_daily_log(logs_dir);
|
||||
};
|
||||
|
||||
audio::output::start();
|
||||
receiver_model.set_sampling_rate(3072000);
|
||||
receiver_model.set_baseband_bandwidth(1750000);
|
||||
receiver_model.enable();
|
||||
|
||||
options_mode.on_change = [this](size_t, int32_t value) {
|
||||
current_mode = static_cast<ModulationMode>((uint8_t)value);
|
||||
morse_decoder_.resetLearning();
|
||||
if (current_mode == ModulationMode::FM) {
|
||||
receiver_model.set_am_configuration(4);
|
||||
receiver_model.set_modulation(ReceiverModel::Mode::NarrowbandFMAudio);
|
||||
field_squelch.set_style(Theme::getInstance()->option_active);
|
||||
field_squelch.set_focusable(true);
|
||||
receiver_model.set_squelch_level(field_squelch.value());
|
||||
audio::set_rate(audio::Rate::Hz_24000);
|
||||
} else {
|
||||
audio::set_rate(audio::Rate::Hz_12000);
|
||||
receiver_model.set_modulation(ReceiverModel::Mode::AMAudio);
|
||||
if (current_mode == ModulationMode::AM || current_mode == ModulationMode::DSB)
|
||||
receiver_model.set_am_configuration(7);
|
||||
else if (current_mode == ModulationMode::USB)
|
||||
receiver_model.set_am_configuration(9);
|
||||
else // LSB
|
||||
receiver_model.set_am_configuration(10);
|
||||
receiver_model.set_squelch_level(0);
|
||||
field_squelch.set_style(Theme::getInstance()->fg_dark);
|
||||
field_squelch.set_focusable(false);
|
||||
}
|
||||
baseband::set_moreserx_config(static_cast<uint8_t>(current_mode));
|
||||
};
|
||||
field_squelch.set_value(receiver_model.squelch_level(), false); // will be sent later, no need to send 2x
|
||||
field_squelch.on_change = [this](int32_t v) {
|
||||
if (current_mode == ModulationMode::FM)
|
||||
receiver_model.set_squelch_level(v);
|
||||
};
|
||||
options_mode.set_selected_index(static_cast<int32_t>(current_mode), true);
|
||||
|
||||
auto vol = field_volume.value(); // audio volume fix
|
||||
field_volume.set_value(0);
|
||||
field_volume.set_value(vol);
|
||||
|
||||
logger = std::make_unique<MorseLogger>();
|
||||
chk_log.on_select = [this](Checkbox&, bool save) {
|
||||
save_log = save;
|
||||
if (logger && save_log)
|
||||
logger->init_daily_log(logs_dir);
|
||||
};
|
||||
}
|
||||
|
||||
void MorseLogger::init_daily_log(const std::filesystem::path& log_dir) {
|
||||
auto now = rtc_time::now();
|
||||
std::string pattern = "morse_";
|
||||
uint16_t y = now.year();
|
||||
pattern += (char)('0' + (y / 1000) % 10);
|
||||
pattern += (char)('0' + (y / 100) % 10);
|
||||
pattern += (char)('0' + (y / 10) % 10);
|
||||
pattern += (char)('0' + (y % 10));
|
||||
|
||||
uint8_t m = now.month();
|
||||
pattern += (char)('0' + (m / 10) % 10);
|
||||
pattern += (char)('0' + (m % 10));
|
||||
|
||||
uint8_t d = now.day();
|
||||
pattern += (char)('0' + (d / 10) % 10);
|
||||
pattern += (char)('0' + (d % 10));
|
||||
|
||||
pattern += "_???.TXT";
|
||||
|
||||
auto full_pattern_path = log_dir / pattern;
|
||||
auto final_path = next_filename_matching_pattern(full_pattern_path);
|
||||
if (!final_path.empty()) {
|
||||
this->append(final_path);
|
||||
}
|
||||
}
|
||||
|
||||
void MorseLogger::radio_set_log(uint8_t current_mode) {
|
||||
int64_t freq = receiver_model.target_frequency();
|
||||
std::string mode_str;
|
||||
switch (static_cast<MorseRadioView::ModulationMode>(current_mode)) {
|
||||
case MorseRadioView::ModulationMode::AM:
|
||||
mode_str = "AM";
|
||||
break;
|
||||
case MorseRadioView::ModulationMode::FM:
|
||||
mode_str = "FM";
|
||||
break;
|
||||
case MorseRadioView::ModulationMode::DSB:
|
||||
mode_str = "DSB";
|
||||
break;
|
||||
case MorseRadioView::ModulationMode::USB:
|
||||
mode_str = "USB";
|
||||
break;
|
||||
case MorseRadioView::ModulationMode::LSB:
|
||||
mode_str = "LSB";
|
||||
break;
|
||||
default:
|
||||
mode_str = "???";
|
||||
break;
|
||||
}
|
||||
std::string header = "Freq:" + to_string_rounded_freq(freq, 4);
|
||||
header += "MHz, ";
|
||||
header += "RX MODE:" + mode_str;
|
||||
header += "\r\nMessage:";
|
||||
log_file.write_raw(header);
|
||||
}
|
||||
|
||||
bool MorseLogger::on_packet(const std::string& content, bool time, uint8_t current_mode) {
|
||||
if (!time) {
|
||||
log_file.write_raw_no_newline("\r\n\r\n");
|
||||
auto timestamp = to_string_datetime(rtc_time::now(), YMDHMS);
|
||||
log_file.write_raw("[" + timestamp + "] ");
|
||||
radio_set_log(current_mode);
|
||||
char_count = 0;
|
||||
time = true;
|
||||
}
|
||||
|
||||
if (content.empty()) return time;
|
||||
std::string s;
|
||||
for (char c : content) {
|
||||
s = c;
|
||||
if ((char_count >= 35 && c == ' ') || (char_count >= 47)) {
|
||||
log_file.write_raw_no_newline("\r\n");
|
||||
|
||||
if (c != ' ') {
|
||||
log_file.write_raw_no_newline(s);
|
||||
char_count = 1;
|
||||
} else {
|
||||
char_count = 0;
|
||||
}
|
||||
} else {
|
||||
log_file.write_raw_no_newline(s);
|
||||
char_count++;
|
||||
}
|
||||
}
|
||||
|
||||
return time;
|
||||
}
|
||||
|
||||
MorseRadioView::~MorseRadioView() {
|
||||
receiver_model.disable();
|
||||
baseband::shutdown();
|
||||
audio::output::stop();
|
||||
}
|
||||
|
||||
void MorseRadioView::focus() {
|
||||
field_frequency.focus();
|
||||
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;
|
||||
last_activity_time = now;
|
||||
}
|
||||
}
|
||||
|
||||
int32_t MorseRadioView::ProcessSignal(int32_t sig_time_us) {
|
||||
int8_t sign = (sig_time_us > 0) ? 1 : ((sig_time_us < 0) ? -1 : 0);
|
||||
int32_t result = 0;
|
||||
|
||||
if (last_sign_ == 0) {
|
||||
last_sign_ = sign;
|
||||
accumulator_us_ = sig_time_us;
|
||||
long_pause_sent_ = false;
|
||||
return 0;
|
||||
}
|
||||
if (sign == last_sign_) {
|
||||
accumulator_us_ += sig_time_us;
|
||||
|
||||
if (sign < 0) {
|
||||
int32_t threshold_us = morse_decoder_.getInterWordThreshold() * 1000;
|
||||
if (!long_pause_sent_ && accumulator_us_ <= -threshold_us) {
|
||||
result = accumulator_us_ / 1000;
|
||||
long_pause_sent_ = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// state change
|
||||
if (!long_pause_sent_) {
|
||||
result = accumulator_us_ / 1000;
|
||||
} else {
|
||||
result = 0;
|
||||
}
|
||||
accumulator_us_ = sig_time_us;
|
||||
last_sign_ = sign;
|
||||
long_pause_sent_ = false;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void MorseRadioView::on_data(const MorseRXDataMessage* message) {
|
||||
int32_t r;
|
||||
|
||||
txt_clip.hidden(!message->clipped);
|
||||
|
||||
for (uint8_t i = 0; i <= message->state_cnt; ++i) {
|
||||
r = ProcessSignal(message->state_durations[i]);
|
||||
auto result = morse_decoder_.handleInput((r != 0) ? r : 0);
|
||||
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, static_cast<uint8_t>(current_mode));
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
txt_last.set(morse_decoder_.getLastSequence().c_str());
|
||||
|
||||
last_color_id = color_id;
|
||||
std::string color = "";
|
||||
|
||||
if (ch == " ") {
|
||||
color_id = 0;
|
||||
} else if (ch[0] == '{') { // no match
|
||||
color_id = 0;
|
||||
} else {
|
||||
if (confidence < 0.8)
|
||||
color_id = 1;
|
||||
else if (confidence < 0.9)
|
||||
color_id = 2;
|
||||
else
|
||||
color_id = 3;
|
||||
}
|
||||
color = arr_color[color_id];
|
||||
last_color_id = color_id;
|
||||
console_text.write(color + ch);
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::morse_radio
|
||||
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Pezsma
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef __MORSE_RADIO_H__
|
||||
#define __MORSE_RADIO_H__
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "ui_widget.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "external_app.hpp"
|
||||
#include "ui_freq_field.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "portapack.hpp"
|
||||
#include "message.hpp"
|
||||
#include "audio.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
#include "receiver_model.hpp"
|
||||
#include "tone_key.hpp"
|
||||
#include "log_file.hpp"
|
||||
#include "file_path.hpp"
|
||||
#include "file.hpp"
|
||||
#include "rtc_time.hpp"
|
||||
#include "morsedecoder.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace ui::external_app::morse_radio {
|
||||
|
||||
class MorseRadioView;
|
||||
|
||||
class MorseLogger {
|
||||
public:
|
||||
Optional<File::Error> append(const std::filesystem::path& filename) {
|
||||
return log_file.append(filename);
|
||||
}
|
||||
|
||||
void init_daily_log(const std::filesystem::path& log_dir);
|
||||
bool on_packet(const std::string& content, bool time, uint8_t current_mode);
|
||||
void radio_set_log(uint8_t current_mode);
|
||||
|
||||
private:
|
||||
LogFile log_file{};
|
||||
uint8_t char_count{0}; // log file line break
|
||||
};
|
||||
|
||||
class MorseRadioView : public ui::View {
|
||||
public:
|
||||
enum class ModulationMode : uint8_t {
|
||||
AM = 0,
|
||||
FM = 1,
|
||||
DSB = 2,
|
||||
USB = 3,
|
||||
LSB = 4
|
||||
};
|
||||
|
||||
MorseRadioView(ui::NavigationView& nav);
|
||||
~MorseRadioView();
|
||||
std::string title() const override {
|
||||
return "Morse";
|
||||
}
|
||||
void focus() override;
|
||||
|
||||
private:
|
||||
void writeCharToConsole(const std::string& ch, double confidence);
|
||||
void on_data(const MorseRXDataMessage* message);
|
||||
void on_freq(const MorseRXfreqMessage* message);
|
||||
void on_message(const Message* const p);
|
||||
void check_for_timeout();
|
||||
int32_t ProcessSignal(int32_t sig_time_us);
|
||||
ui::NavigationView& nav_;
|
||||
MorseDecoder morse_decoder_{};
|
||||
RxRadioState radio_state_{};
|
||||
std::unique_ptr<MorseLogger> logger{};
|
||||
|
||||
ModulationMode current_mode = ModulationMode::AM;
|
||||
|
||||
app_settings::SettingsManager settings_{
|
||||
"rx_morese_radio",
|
||||
app_settings::Mode::RX,
|
||||
{{"cwmode"sv, reinterpret_cast<uint8_t*>(¤t_mode)}}};
|
||||
|
||||
RxFrequencyField field_frequency{
|
||||
{UI_POS_X(0), UI_POS_Y(0)},
|
||||
nav_};
|
||||
RFAmpField field_rf_amp{
|
||||
{UI_POS_X(13), UI_POS_Y(0)}};
|
||||
LNAGainField field_lna{
|
||||
{UI_POS_X(15), UI_POS_Y(0)}};
|
||||
VGAGainField field_vga{
|
||||
{UI_POS_X(18), UI_POS_Y(0)}};
|
||||
RSSI rssi{
|
||||
{UI_POS_X(21), UI_POS_Y(0), UI_POS_WIDTH_REMAINING(24), 4}};
|
||||
|
||||
AudioVolumeField field_volume{
|
||||
{UI_POS_X_RIGHT(2), UI_POS_Y(0)}};
|
||||
|
||||
NumberField field_squelch{
|
||||
{UI_POS_X(10), UI_POS_Y(1)},
|
||||
2,
|
||||
{0, 99},
|
||||
1,
|
||||
' ',
|
||||
};
|
||||
ui::Text txt_speed{{UI_POS_X(23), UI_POS_Y(1), UI_POS_WIDTH(3), UI_POS_HEIGHT(1)}, "??"};
|
||||
ui::Text txt_last{{UI_POS_X(12), UI_POS_Y(4), UI_POS_WIDTH_REMAINING(12), UI_POS_HEIGHT(1)}, ""};
|
||||
ui::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(9), UI_POS_Y(2)},
|
||||
5,
|
||||
{{"AM", 0}, {"FM", 1}, {"DSB", 2}, {"USB", 3}, {"LSB", 4}}};
|
||||
|
||||
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"};
|
||||
|
||||
std::string arr_color[4] = {STR_COLOR_WHITE, STR_COLOR_RED, STR_COLOR_YELLOW, STR_COLOR_GREEN};
|
||||
|
||||
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{
|
||||
Message::ID::MorseRXData,
|
||||
[this](Message* const p) {
|
||||
const auto message = static_cast<const MorseRXDataMessage*>(p);
|
||||
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) {
|
||||
(void)p;
|
||||
this->check_for_timeout();
|
||||
}};
|
||||
};
|
||||
|
||||
} // namespace ui::external_app::morse_radio
|
||||
|
||||
#endif // __MORSE_RADIO_H__
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Pezsma
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "ui_morse_radiotx.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "external_app.hpp"
|
||||
|
||||
namespace ui::external_app::morseradiotx {
|
||||
|
||||
void initialize_app(NavigationView& nav) {
|
||||
nav.push<MorseRadiotxView>();
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::morseradiotx
|
||||
|
||||
extern "C" {
|
||||
|
||||
__attribute__((section(".external_app.app_morseradiotx.application_information"), used))
|
||||
application_information_t _application_information_morseradiotx = {
|
||||
/*.memory_location = */ (uint8_t*)0x00000000,
|
||||
/*.externalAppEntry = */ ui::external_app::morseradiotx::initialize_app,
|
||||
/*.header_version = */ CURRENT_HEADER_VERSION,
|
||||
/*.app_version = */ VERSION_MD5,
|
||||
|
||||
/*.app_name = */ "Morse TX",
|
||||
/*.bitmap_data = */ {
|
||||
0x00,
|
||||
0x00,
|
||||
0xFE,
|
||||
0x7F,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xBB,
|
||||
0xD0,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0x0B,
|
||||
0xE1,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xEB,
|
||||
0xD0,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFE,
|
||||
0x7F,
|
||||
0x70,
|
||||
0x00,
|
||||
0x30,
|
||||
0x00,
|
||||
0x10,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
},
|
||||
/*.icon_color = */ ui::Color::yellow().v,
|
||||
/*.menu_location = */ app_location_t::TX,
|
||||
/*.desired_menu_position = */ -1,
|
||||
|
||||
/*.m4_app_tag = portapack::spi_flash::image_tag_none */ {'P', 'M', 'R', 'T'},
|
||||
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
|
||||
};
|
||||
|
||||
} // extern "C"
|
||||
@@ -0,0 +1,389 @@
|
||||
#ifndef __MORSEDECODER_HPP__
|
||||
#define __MORSEDECODER_HPP__
|
||||
|
||||
/*
|
||||
* Copyright (C) 2025 Pezsma
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include "string_format.hpp"
|
||||
|
||||
namespace ui::external_app::morseradiotx {
|
||||
|
||||
class MorseRingBuffer {
|
||||
public:
|
||||
MorseRingBuffer()
|
||||
: head_(0), tail_(0), count_(0) {}
|
||||
|
||||
void push_back(const uint32_t& value) {
|
||||
data_[head_] = value;
|
||||
head_ = (head_ + 1) % 40;
|
||||
if (count_ < 40) {
|
||||
count_++;
|
||||
} else {
|
||||
// overwrite oldest element
|
||||
tail_ = (tail_ + 1) % 40;
|
||||
}
|
||||
}
|
||||
|
||||
void pop_front() {
|
||||
if (count_ > 0) {
|
||||
tail_ = (tail_ + 1) % 40;
|
||||
count_--;
|
||||
}
|
||||
}
|
||||
|
||||
size_t size() const { return count_; }
|
||||
bool empty() const { return count_ == 0; }
|
||||
const uint32_t& front() const { return data_[tail_]; }
|
||||
// Access by index (0 = oldest)
|
||||
uint32_t operator[](size_t idx) const {
|
||||
return data_[(tail_ + idx) % 40];
|
||||
}
|
||||
// Convert to vector-like access for sorting etc.
|
||||
void copy_to_array(uint32_t* out) const {
|
||||
for (size_t i = 0; i < count_; ++i)
|
||||
out[i] = (*this)[i];
|
||||
}
|
||||
|
||||
void clear() {
|
||||
head_ = 0;
|
||||
tail_ = 0;
|
||||
count_ = 0;
|
||||
}
|
||||
|
||||
private:
|
||||
uint32_t data_[40];
|
||||
size_t head_;
|
||||
size_t tail_;
|
||||
size_t count_;
|
||||
};
|
||||
|
||||
class MorseDecoder {
|
||||
public:
|
||||
struct DecodeResult {
|
||||
std::string text = "";
|
||||
double confidence = 0.0;
|
||||
|
||||
bool isValid() const {
|
||||
return !text.empty();
|
||||
}
|
||||
};
|
||||
|
||||
struct MorseEntry {
|
||||
std::string code;
|
||||
std::string letter;
|
||||
};
|
||||
|
||||
MorseDecoder() {}
|
||||
|
||||
void resetLearning() {
|
||||
time_unit_ms_ = 119.0;
|
||||
current_sequence_ = "";
|
||||
last_sequence_ = "";
|
||||
last_confidence_ = 0.0;
|
||||
pulse_history_.clear();
|
||||
pulse_gaps_.clear();
|
||||
}
|
||||
|
||||
const char* get_morse_pattern(char c) {
|
||||
char upper_c = (c >= 'a' && c <= 'z') ? c - 32 : c;
|
||||
|
||||
for (size_t i = 0; i < morse_table_size_; ++i) {
|
||||
if (morse_table_[i].letter[0] == upper_c) {
|
||||
return morse_table_[i].code.c_str();
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
DecodeResult
|
||||
handleInput(int32_t duration_ms) {
|
||||
DecodeResult result = {"", 0.0};
|
||||
|
||||
if (duration_ms < 5 && duration_ms > -5) return result;
|
||||
|
||||
if (duration_ms > 0) {
|
||||
pulse_history_.push_back(duration_ms);
|
||||
double dah_prob = getDahProbability(duration_ms);
|
||||
current_sequence_ += (dah_prob > 0.5) ? '-' : '.';
|
||||
last_confidence_ = (dah_prob > 0.5) ? dah_prob : (1.0 - dah_prob);
|
||||
last_sequence_ = current_sequence_;
|
||||
|
||||
} else {
|
||||
uint32_t gap_duration = -duration_ms;
|
||||
pulse_gaps_.push_back(gap_duration);
|
||||
|
||||
if (gap_duration >= getInterCharThreshold() && !current_sequence_.empty()) {
|
||||
result.text = lookupMorse(current_sequence_);
|
||||
|
||||
result.confidence = (result.text[0] != '{') ? last_confidence_ : 0.0;
|
||||
if (gap_duration >= getInterWordThreshold()) {
|
||||
result.text += " ";
|
||||
}
|
||||
current_sequence_ = "";
|
||||
}
|
||||
}
|
||||
|
||||
updateLearning();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
inline double getInterElementThreshold() { return time_unit_ms_ * 0.8; }
|
||||
inline double getInterCharThreshold() { return time_unit_ms_ * 2.5; }
|
||||
inline double getInterWordThreshold() { return time_unit_ms_ * 6.0; }
|
||||
|
||||
inline double getCurrentTimeUnit() { return time_unit_ms_; }
|
||||
inline std::string getLastSequence() { return last_sequence_; }
|
||||
|
||||
private:
|
||||
std::string current_sequence_ = "";
|
||||
std::string last_sequence_ = "";
|
||||
double time_unit_ms_ = 119.0;
|
||||
double last_confidence_ = 0.0;
|
||||
MorseRingBuffer pulse_history_{};
|
||||
MorseRingBuffer pulse_gaps_{};
|
||||
|
||||
std::string lookupMorse(const std::string& seq) {
|
||||
for (size_t i = 0; i < morse_table_size_; i++) {
|
||||
if (seq == morse_table_[i].code)
|
||||
return morse_table_[i].letter;
|
||||
}
|
||||
return "{" + seq + "}"; // not found
|
||||
}
|
||||
|
||||
double getDahProbability(uint32_t duration_ms) {
|
||||
double start_interp = 1.5 * time_unit_ms_;
|
||||
double end_interp = 2.5 * time_unit_ms_;
|
||||
|
||||
if (duration_ms <= start_interp) return 0.0;
|
||||
if (duration_ms >= end_interp) return 1.0;
|
||||
return ((double)(duration_ms)-start_interp) / (end_interp - start_interp);
|
||||
}
|
||||
|
||||
size_t findDecisionBoundary(uint32_t* sorted_data, size_t sorted_data_size) {
|
||||
if (sorted_data_size < 4) return 0;
|
||||
|
||||
size_t best_split_index = 0;
|
||||
uint32_t max_diff = 0;
|
||||
|
||||
for (size_t i = 1; i < sorted_data_size; ++i) {
|
||||
uint32_t diff = sorted_data[i] - sorted_data[i - 1];
|
||||
if (diff > sorted_data[i - 1] * 0.5 && diff > max_diff) {
|
||||
max_diff = diff;
|
||||
best_split_index = i;
|
||||
}
|
||||
}
|
||||
return best_split_index;
|
||||
}
|
||||
|
||||
bool calculatePulseUnit(double& unit, double& confidence) {
|
||||
if (pulse_history_.size() < 10) return false;
|
||||
uint32_t sorted_pulses[pulse_history_.size()];
|
||||
pulse_history_.copy_to_array(sorted_pulses);
|
||||
sort_uint32(sorted_pulses, pulse_history_.size());
|
||||
|
||||
size_t split_index = findDecisionBoundary(sorted_pulses, pulse_history_.size());
|
||||
if (split_index == 0 || split_index < 3 || (pulse_history_.size() - split_index) < 2) {
|
||||
return false;
|
||||
}
|
||||
double dit_sum = sum_uint32_range(sorted_pulses, 0, split_index);
|
||||
double dah_sum = sum_uint32_range(sorted_pulses, split_index, pulse_history_.size());
|
||||
|
||||
double avg_dit = dit_sum / split_index;
|
||||
double avg_dah = dah_sum / (pulse_history_.size() - split_index);
|
||||
if (avg_dah <= avg_dit) return false;
|
||||
|
||||
double ratio = avg_dah / avg_dit;
|
||||
if (ratio > 1.5 && ratio < 5.0) {
|
||||
unit = avg_dit;
|
||||
double tmpabs = ratio - 3.0;
|
||||
if (tmpabs < 0) tmpabs *= -1;
|
||||
tmpabs /= 3.0;
|
||||
tmpabs = 1.0 - tmpabs;
|
||||
if (tmpabs < 0) tmpabs = 0;
|
||||
confidence = tmpabs; // 0..1
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool calculateGapUnit(double& unit, double& confidence) {
|
||||
if (pulse_gaps_.size() < 10) return false;
|
||||
double threshold = getInterElementThreshold();
|
||||
double valid_gaps[pulse_gaps_.size()];
|
||||
size_t valid_count = 0;
|
||||
for (size_t i = 0; i < pulse_gaps_.size(); i++) {
|
||||
double gap = pulse_gaps_[i];
|
||||
if (gap <= threshold) {
|
||||
valid_gaps[valid_count++] = gap;
|
||||
}
|
||||
}
|
||||
if (valid_count < 2) {
|
||||
return false;
|
||||
}
|
||||
double sum = sum_double_range(valid_gaps, 0, valid_count);
|
||||
size_t count_to_average = valid_count;
|
||||
|
||||
if (count_to_average > 0) {
|
||||
unit = sum / count_to_average;
|
||||
confidence = 0.8;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
double sum_uint32_range(const uint32_t* data, size_t start, size_t end) {
|
||||
double sum = 0.0;
|
||||
for (size_t i = start; i < end; i++) {
|
||||
sum += data[i];
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
double sum_double_range(const double* data, size_t start, size_t end) {
|
||||
double sum = 0.0;
|
||||
for (size_t i = start; i < end; i++) {
|
||||
sum += data[i];
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
void sort_uint32(uint32_t* data, size_t size) {
|
||||
if (size < 2)
|
||||
return;
|
||||
|
||||
for (size_t i = 1; i < size; i++) {
|
||||
uint32_t key = data[i];
|
||||
size_t j = i;
|
||||
|
||||
while (j > 0 && data[j - 1] > key) {
|
||||
data[j] = data[j - 1];
|
||||
j--;
|
||||
}
|
||||
|
||||
data[j] = key;
|
||||
}
|
||||
}
|
||||
|
||||
double clamp_double(double value, double min_val, double max_val) {
|
||||
if (value < min_val)
|
||||
return min_val;
|
||||
else if (value > max_val)
|
||||
return max_val;
|
||||
else
|
||||
return value;
|
||||
}
|
||||
|
||||
void updateLearning() {
|
||||
double pulse_unit = -1.0, pulse_confidence = 0.0;
|
||||
double gap_unit = -1.0, gap_confidence = 0.0;
|
||||
|
||||
bool pulse_success = calculatePulseUnit(pulse_unit, pulse_confidence);
|
||||
bool gap_success = calculateGapUnit(gap_unit, gap_confidence);
|
||||
|
||||
double new_time_unit = -1.0;
|
||||
if (pulse_success && pulse_confidence > 0.5) {
|
||||
new_time_unit = pulse_unit;
|
||||
} else if (pulse_success && gap_success) {
|
||||
gap_confidence = 0.2;
|
||||
double total_confidence = pulse_confidence + gap_confidence;
|
||||
new_time_unit = (pulse_unit * pulse_confidence + gap_unit * gap_confidence) / total_confidence;
|
||||
} else if (gap_success) {
|
||||
new_time_unit = gap_unit;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
double max_change = time_unit_ms_ * 0.25;
|
||||
new_time_unit = clamp_double(new_time_unit, time_unit_ms_ - max_change, time_unit_ms_ + max_change);
|
||||
double DEFAULT_TIME_UNIT = 160.0;
|
||||
double BASE_LEARNING_RATE = 0.05;
|
||||
double MAX_LEARNING_RATE = 0.25;
|
||||
double tudeltaabs = new_time_unit - DEFAULT_TIME_UNIT;
|
||||
if (tudeltaabs < 0) tudeltaabs *= -1;
|
||||
double deviation_from_default = tudeltaabs / DEFAULT_TIME_UNIT;
|
||||
double tpp = deviation_from_default * 2.0;
|
||||
if (tpp > 1) tpp = 1;
|
||||
double learning_factor = BASE_LEARNING_RATE + (MAX_LEARNING_RATE - BASE_LEARNING_RATE) * tpp;
|
||||
|
||||
time_unit_ms_ = (time_unit_ms_ * (1.0 - learning_factor)) + (new_time_unit * learning_factor);
|
||||
}
|
||||
|
||||
size_t morse_table_size_ = 50;
|
||||
MorseEntry morse_table_[50] = {
|
||||
{".-", "A"},
|
||||
{"-...", "B"},
|
||||
{"-.-.", "C"},
|
||||
{"-..", "D"},
|
||||
{".", "E"},
|
||||
{"..-.", "F"},
|
||||
{"--.", "G"},
|
||||
{"....", "H"},
|
||||
{"..", "I"},
|
||||
{".---", "J"},
|
||||
{"-.-", "K"},
|
||||
{".-..", "L"},
|
||||
{"--", "M"},
|
||||
{"-.", "N"},
|
||||
{"---", "O"},
|
||||
{".--.", "P"},
|
||||
{"--.-", "Q"},
|
||||
{".-.", "R"},
|
||||
{"...", "S"},
|
||||
{"-", "T"},
|
||||
{"..-", "U"},
|
||||
{"...-", "V"},
|
||||
{".--", "W"},
|
||||
{"-..-", "X"},
|
||||
{"-.--", "Y"},
|
||||
{"--..", "Z"},
|
||||
{".----", "1"},
|
||||
{"..---", "2"},
|
||||
{"...--", "3"},
|
||||
{"....-", "4"},
|
||||
{".....", "5"},
|
||||
{"-....", "6"},
|
||||
{"--...", "7"},
|
||||
{"---..", "8"},
|
||||
{"----.", "9"},
|
||||
{"-----", "0"},
|
||||
{".-.-.-", "."},
|
||||
{"..--..", "?"},
|
||||
{"-.-.--", "!"},
|
||||
{"--..--", ","},
|
||||
{"-...-", "="},
|
||||
{"-..-.", "/"},
|
||||
{".--.-.", "@"},
|
||||
{"---...", ":"},
|
||||
{"-....-", "-"},
|
||||
{".----.", "'"},
|
||||
{".-..-.", "\""},
|
||||
{"-.--.", "("},
|
||||
{"-.--.-", ")"},
|
||||
{".-.-.", "+"}};
|
||||
};
|
||||
|
||||
} // namespace ui::external_app::morseradiotx
|
||||
|
||||
#endif // __MORSEDECODER_HPP__
|
||||
@@ -0,0 +1,437 @@
|
||||
#include "ui_morse_radiotx.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
|
||||
namespace ui::external_app::morseradiotx {
|
||||
|
||||
static msg_t tx_thread_fn(void* arg) {
|
||||
auto view = reinterpret_cast<MorseRadiotxView*>(arg);
|
||||
chRegSetThreadName("morse_tx_thread");
|
||||
view->transmit_morse_message();
|
||||
return 0;
|
||||
}
|
||||
|
||||
MorseRadiotxView::MorseRadiotxView(ui::NavigationView& nav)
|
||||
: current_timings(calculate_morse_timings(20)),
|
||||
nav_(nav) {
|
||||
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
|
||||
add_children({&tx_view,
|
||||
&field_volume,
|
||||
&labels,
|
||||
&options_mode,
|
||||
&tone_,
|
||||
&wpm_,
|
||||
&txt_msg,
|
||||
&btn_message,
|
||||
&btn_calls,
|
||||
&chk_trans,
|
||||
&bandwidth,
|
||||
&chk_callsgn,
|
||||
&txt_last,
|
||||
&console_text,
|
||||
&btn_clear,
|
||||
&btn_ptt});
|
||||
|
||||
initial_switch_config_ = get_switches_repeat_config();
|
||||
|
||||
SwitchesState config = initial_switch_config_;
|
||||
config[toUType(Switch::Sel)] = false;
|
||||
set_switches_repeat_config(config);
|
||||
|
||||
audio::set_rate(audio::Rate::Hz_24000);
|
||||
|
||||
btn_clear.on_select = [this](Button&) {
|
||||
console_text.clear(true);
|
||||
txt_last.set("");
|
||||
};
|
||||
|
||||
options_mode.on_change = [this](size_t, int32_t value) {
|
||||
current_mode = (uint8_t)value;
|
||||
baseband::set_morsetx_config(current_mode, tone, band);
|
||||
ui_toggle();
|
||||
};
|
||||
|
||||
tone_.on_change = [this](int32_t v) {
|
||||
tone = static_cast<uint32_t>(v);
|
||||
baseband::set_morsetx_config(current_mode, tone, band);
|
||||
};
|
||||
|
||||
wpm_.on_change = [this](int16_t v) {
|
||||
wpm = v;
|
||||
current_timings = calculate_morse_timings(wpm);
|
||||
};
|
||||
|
||||
bandwidth.on_change = [this](float v) {
|
||||
band = v;
|
||||
baseband::set_morsetx_config(current_mode, tone, band);
|
||||
};
|
||||
|
||||
options_mode.set_selected_index(current_mode, true);
|
||||
tone_.set_value(tone, true);
|
||||
wpm_.set_value(wpm, true);
|
||||
bandwidth.set_value(band, true);
|
||||
|
||||
btn_ptt.on_select = [this](Button&) {
|
||||
if (button_touch) {
|
||||
button_touch = false;
|
||||
return;
|
||||
}
|
||||
button_was_selected = true;
|
||||
onPress();
|
||||
};
|
||||
|
||||
btn_ptt.on_touch_press = [this](Button&) {
|
||||
button_touch = true;
|
||||
button_was_selected = false;
|
||||
onPress();
|
||||
};
|
||||
btn_ptt.on_touch_release = [this](Button&) {
|
||||
if (chk_trans.value() && transmit) {
|
||||
button_touch = true;
|
||||
button_was_selected = false;
|
||||
onRelease();
|
||||
}
|
||||
};
|
||||
|
||||
btn_message.on_select = [this, &nav](Button&) {
|
||||
if (!transmit)
|
||||
text_prompt(nav, msg_buffer, 27, ENTER_KEYBOARD_MODE_ALPHA, [this](std::string& buffer) {
|
||||
msg_buffer = buffer;
|
||||
txt_msg.set("[" + msg_buffer + "] ");
|
||||
});
|
||||
};
|
||||
|
||||
btn_calls.on_select = [this, &nav](Button&) {
|
||||
if (!transmit) {
|
||||
text_prompt(nav, call_sign, 10, ENTER_KEYBOARD_MODE_ALPHA, [this](std::string& buffer) {
|
||||
call_sign = buffer;
|
||||
if (call_sign.empty())
|
||||
btn_calls.set_text("call sign?");
|
||||
else
|
||||
btn_calls.set_text(call_sign);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
audio::output::start();
|
||||
auto vol = field_volume.value();
|
||||
field_volume.set_value(0);
|
||||
field_volume.set_value(vol);
|
||||
|
||||
tx_view.on_start = [this]() {
|
||||
if (!chk_trans.value() && msg_buffer.empty()) {
|
||||
tx_view.set_transmitting(false);
|
||||
return;
|
||||
}
|
||||
tx_view.focus();
|
||||
if (!tx_thread && !thread_running) {
|
||||
thread_running = true;
|
||||
tx_thread = chThdCreateFromHeap(NULL, 1024, NORMALPRIO + 5, tx_thread_fn, this);
|
||||
tx_view.set_transmitting(true);
|
||||
}
|
||||
|
||||
else
|
||||
tx_view.set_transmitting(false);
|
||||
};
|
||||
|
||||
tx_view.on_stop = [this]() {
|
||||
baseband::set_morsetx_key(false);
|
||||
transmitter_model.disable();
|
||||
|
||||
if (tx_thread) {
|
||||
chThdTerminate(tx_thread);
|
||||
chThdWait(tx_thread);
|
||||
}
|
||||
transmit = false;
|
||||
transmit_time = 0;
|
||||
tx_thread = nullptr;
|
||||
thread_running = false;
|
||||
tx_view.set_transmitting(false);
|
||||
ui_toggle();
|
||||
};
|
||||
|
||||
tx_view.on_edit_frequency = [this, &nav]() {
|
||||
auto new_view = nav.push<FrequencyKeypadView>(transmitter_model.target_frequency());
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
transmitter_model.set_target_frequency(f);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
MorseRadiotxView::~MorseRadiotxView() {
|
||||
set_switches_repeat_config(initial_switch_config_);
|
||||
if (tx_thread) {
|
||||
chThdTerminate(tx_thread);
|
||||
chThdWait(tx_thread);
|
||||
if (!thread_running) tx_thread = nullptr;
|
||||
}
|
||||
|
||||
transmitter_model.disable();
|
||||
baseband::shutdown();
|
||||
audio::output::stop();
|
||||
}
|
||||
|
||||
void MorseRadiotxView::on_show() {
|
||||
ptt_button_visibility(true);
|
||||
start_time = 0;
|
||||
end_time = 0;
|
||||
transmit_time = 0;
|
||||
}
|
||||
|
||||
void MorseRadiotxView::focus() {
|
||||
options_mode.focus();
|
||||
}
|
||||
|
||||
MorseRadiotxView::MorseTimings MorseRadiotxView::calculate_morse_timings(uint32_t wpm) {
|
||||
MorseRadiotxView::MorseTimings t;
|
||||
if (wpm < 10) wpm = 10;
|
||||
if (wpm > 45) wpm = 45;
|
||||
t.dot_ms = 1200 / wpm;
|
||||
|
||||
t.dash_ms = 3 * t.dot_ms;
|
||||
t.symbol_gap = t.dot_ms;
|
||||
t.char_gap = 3 * t.dot_ms;
|
||||
t.word_gap = 7 * t.dot_ms;
|
||||
return t;
|
||||
}
|
||||
|
||||
void MorseRadiotxView::transmit_morse_message() {
|
||||
std::string full_message = "";
|
||||
// cal sign
|
||||
if (chk_callsgn.value() && !call_sign.empty()) {
|
||||
full_message = call_sign;
|
||||
if (!chk_trans.value() && !msg_buffer.empty()) {
|
||||
full_message += " " + msg_buffer;
|
||||
}
|
||||
} else {
|
||||
if (!chk_trans.value()) full_message = msg_buffer;
|
||||
}
|
||||
|
||||
if (full_message.empty() && !chk_trans.value()) {
|
||||
ptt_button_visibility(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// enable transmit
|
||||
if (!transmit) {
|
||||
transmit = true;
|
||||
transmitter_model.enable();
|
||||
ui_toggle();
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < full_message.length(); i++) {
|
||||
if (chThdShouldTerminate()) break;
|
||||
|
||||
char c = full_message[i];
|
||||
|
||||
std::string s_char(1, c);
|
||||
// space
|
||||
if (c == ' ') {
|
||||
console_text.write(" ");
|
||||
chThdSleepMilliseconds(current_timings.word_gap);
|
||||
continue;
|
||||
}
|
||||
|
||||
const char* pattern = morse_decoder_.get_morse_pattern(c);
|
||||
|
||||
if (pattern != nullptr) {
|
||||
txt_last.set(pattern);
|
||||
|
||||
// write blue char to console
|
||||
console_text.write(STR_COLOR_BLUE + s_char);
|
||||
|
||||
// Morze (dih/dah) send
|
||||
for (int j = 0; pattern[j] != '\0'; j++) {
|
||||
baseband::set_morsetx_key(true);
|
||||
if (pattern[j] == '.') {
|
||||
chThdSleepMilliseconds(current_timings.dot_ms);
|
||||
} else if (pattern[j] == '-') {
|
||||
chThdSleepMilliseconds(current_timings.dash_ms);
|
||||
}
|
||||
if (chThdShouldTerminate()) break;
|
||||
// pause between signs
|
||||
baseband::set_morsetx_key(false);
|
||||
chThdSleepMilliseconds(current_timings.symbol_gap);
|
||||
}
|
||||
if (chThdShouldTerminate()) break;
|
||||
if (current_timings.char_gap > current_timings.symbol_gap) {
|
||||
chThdSleepMilliseconds(current_timings.char_gap - current_timings.symbol_gap);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (chk_trans.value()) ptt_button_visibility(false);
|
||||
baseband::set_morsetx_key(false);
|
||||
transmit_time = chTimeNow();
|
||||
decode_timeout_calc = false;
|
||||
thread_running = false;
|
||||
}
|
||||
|
||||
void MorseRadiotxView::onPress() {
|
||||
start_time = chTimeNow();
|
||||
if (!transmit) {
|
||||
transmit = true;
|
||||
transmitter_model.enable();
|
||||
}
|
||||
baseband::set_morsetx_key(true);
|
||||
if (end_time != 0) {
|
||||
int64_t gap_delta = (chTimeNow() - end_time);
|
||||
auto result = morse_decoder_.handleInput(-gap_delta);
|
||||
if (result.isValid()) {
|
||||
writeCharToConsole(result.text, result.confidence);
|
||||
}
|
||||
}
|
||||
end_time = 0;
|
||||
transmit_time = 0;
|
||||
decode_timeout_calc = false;
|
||||
}
|
||||
|
||||
void MorseRadiotxView::onRelease() {
|
||||
end_time = chTimeNow();
|
||||
transmit_time = end_time;
|
||||
baseband::set_morsetx_key(false);
|
||||
if (start_time != 0) {
|
||||
int32_t press_delta = (end_time - start_time);
|
||||
auto result = morse_decoder_.handleInput(press_delta);
|
||||
if (result.isValid()) {
|
||||
writeCharToConsole(result.text, result.confidence);
|
||||
}
|
||||
}
|
||||
start_time = 0;
|
||||
decode_timeout_calc = true;
|
||||
baseband::request_beep_stop();
|
||||
}
|
||||
|
||||
void MorseRadiotxView::writeCharToConsole(const std::string& ch, double confidence) {
|
||||
if (ch.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
txt_last.set(morse_decoder_.getLastSequence().c_str());
|
||||
|
||||
last_color_id = color_id;
|
||||
std::string color = "";
|
||||
|
||||
if (ch == " ") {
|
||||
color_id = 0;
|
||||
} else if (ch[0] == '{') { // no match
|
||||
color_id = 0;
|
||||
} else {
|
||||
if (confidence == 1.5)
|
||||
color_id = 4;
|
||||
else if (confidence < 0.8)
|
||||
color_id = 1;
|
||||
else if (confidence < 0.9)
|
||||
color_id = 2;
|
||||
else
|
||||
color_id = 3;
|
||||
}
|
||||
color = arr_color[color_id];
|
||||
last_color_id = color_id;
|
||||
console_text.write(color + ch);
|
||||
}
|
||||
|
||||
void MorseRadiotxView::ptt_button_visibility(bool hidden) {
|
||||
bool hiddenorig = btn_ptt.hidden();
|
||||
if (hiddenorig != hidden) {
|
||||
btn_ptt.hidden(hidden);
|
||||
if (!hidden) btn_ptt.focus();
|
||||
if (hidden) {
|
||||
// hack fix until widget hidig problem is not solved.
|
||||
auto r = btn_ptt.screen_rect();
|
||||
Painter p;
|
||||
p.fill_rectangle_unrolled8(r, Theme::getInstance()->fg_light->background);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MorseRadiotxView::ui_toggle() {
|
||||
// 0=AM, 1=FM, 2=DSB, 3=USB, 4=LSB
|
||||
|
||||
if (transmit) {
|
||||
options_mode.set_style(Theme::getInstance()->fg_dark);
|
||||
options_mode.set_focusable(false);
|
||||
tone_.set_style(Theme::getInstance()->fg_dark);
|
||||
tone_.set_focusable(false);
|
||||
wpm_.set_style(Theme::getInstance()->fg_dark);
|
||||
wpm_.set_focusable(false);
|
||||
btn_message.set_style(Theme::getInstance()->fg_dark);
|
||||
btn_calls.set_style(Theme::getInstance()->fg_dark);
|
||||
chk_trans.set_style(Theme::getInstance()->fg_dark);
|
||||
chk_trans.set_focusable(false);
|
||||
bandwidth.set_style(Theme::getInstance()->fg_dark);
|
||||
bandwidth.set_focusable(false);
|
||||
chk_callsgn.set_style(Theme::getInstance()->fg_dark);
|
||||
chk_callsgn.set_focusable(false);
|
||||
|
||||
} else {
|
||||
options_mode.set_style(Theme::getInstance()->bg_darker);
|
||||
options_mode.set_focusable(true);
|
||||
wpm_.set_style(Theme::getInstance()->bg_darker);
|
||||
wpm_.set_focusable(true);
|
||||
btn_message.set_style(Theme::getInstance()->bg_darker);
|
||||
btn_calls.set_style(Theme::getInstance()->bg_darker);
|
||||
chk_trans.set_style(Theme::getInstance()->bg_darker);
|
||||
chk_trans.set_focusable(true);
|
||||
chk_callsgn.set_style(Theme::getInstance()->bg_darker);
|
||||
chk_callsgn.set_focusable(true);
|
||||
ptt_button_visibility(true);
|
||||
tone_.set_style(Theme::getInstance()->bg_darker);
|
||||
tone_.set_focusable(true);
|
||||
|
||||
if (current_mode == 1) { // FM mode
|
||||
bandwidth.set_style(Theme::getInstance()->bg_darker);
|
||||
bandwidth.set_focusable(true);
|
||||
} else {
|
||||
bandwidth.set_style(Theme::getInstance()->fg_dark);
|
||||
bandwidth.set_focusable(false);
|
||||
}
|
||||
} // transmit false
|
||||
}
|
||||
|
||||
inline bool MorseRadiotxView::tx_button_held() {
|
||||
const auto switches_state = get_switches_state();
|
||||
return switches_state[(size_t)ui::KeyEvent::Select];
|
||||
}
|
||||
|
||||
void MorseRadiotxView::on_framesync() {
|
||||
if (button_was_selected && !button_touch && !tx_button_held()) {
|
||||
button_was_selected = false;
|
||||
onRelease();
|
||||
}
|
||||
|
||||
if (end_time != 0 && decode_timeout_calc) {
|
||||
int64_t gap_delta = (chTimeNow() - end_time);
|
||||
|
||||
if (gap_delta >= morse_decoder_.getInterCharThreshold()) {
|
||||
auto result = morse_decoder_.handleInput(-(int32_t)gap_delta);
|
||||
if (result.isValid()) {
|
||||
writeCharToConsole(result.text, result.confidence);
|
||||
}
|
||||
}
|
||||
if (gap_delta >= morse_decoder_.getInterWordThreshold()) {
|
||||
writeCharToConsole(" ", 1.0);
|
||||
end_time = 0;
|
||||
decode_timeout_calc = false;
|
||||
}
|
||||
}
|
||||
if (transmit_time != 0 && transmit) { // Tx disable if time is up
|
||||
int64_t gap_delta = (chTimeNow() - transmit_time);
|
||||
if (gap_delta >= ((morse_decoder_.getInterWordThreshold() * ((chk_trans.value()) ? 10 : 1)))) {
|
||||
if (tx_thread) {
|
||||
chThdTerminate(tx_thread);
|
||||
chThdWait(tx_thread);
|
||||
}
|
||||
transmit = false;
|
||||
tx_view.set_transmitting(false);
|
||||
transmitter_model.disable();
|
||||
thread_running = false;
|
||||
tx_thread = nullptr;
|
||||
transmit_time = 0;
|
||||
if (!chk_trans.value()) writeCharToConsole(" ", 1.0);
|
||||
ui_toggle();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::morseradiotx
|
||||
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Pezsma
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef __MORSE_RADIOTX_H__
|
||||
#define __MORSE_RADIOTX_H__
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "ui_widget.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_language.hpp"
|
||||
#include "ui_painter.hpp"
|
||||
#include "ui_freq_field.hpp"
|
||||
#include "ui_transmitter.hpp"
|
||||
#include "ui_textentry.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "morsedecoder.hpp"
|
||||
#include "irq_controls.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "portapack.hpp"
|
||||
#include "message.hpp"
|
||||
#include "volume.hpp"
|
||||
#include "audio.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
#include "external_app.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include <ch.h>
|
||||
#include <hal.h>
|
||||
|
||||
namespace ui::external_app::morseradiotx {
|
||||
|
||||
class MorseRadiotxView : public ui::View {
|
||||
public:
|
||||
MorseRadiotxView(ui::NavigationView& nav);
|
||||
~MorseRadiotxView();
|
||||
|
||||
MorseRadiotxView(const MorseRadiotxView&) = delete;
|
||||
MorseRadiotxView(MorseRadiotxView&&) = delete;
|
||||
MorseRadiotxView& operator=(const MorseRadiotxView&) = delete;
|
||||
MorseRadiotxView& operator=(MorseRadiotxView&&) = delete;
|
||||
|
||||
std::string title() const override { return "Morse Tx"; }
|
||||
void focus() override;
|
||||
void on_show() override;
|
||||
void transmit_morse_message();
|
||||
|
||||
private:
|
||||
struct MorseTimings {
|
||||
uint32_t dot_ms;
|
||||
uint32_t dash_ms;
|
||||
uint32_t symbol_gap;
|
||||
uint32_t char_gap;
|
||||
uint32_t word_gap;
|
||||
};
|
||||
|
||||
MorseTimings current_timings{};
|
||||
|
||||
std::string msg_indicator{""};
|
||||
MorseTimings calculate_morse_timings(uint32_t wpm);
|
||||
void onPress();
|
||||
void onRelease();
|
||||
void on_framesync();
|
||||
void writeCharToConsole(const std::string& ch, double confidence);
|
||||
void ui_toggle();
|
||||
bool tx_button_held();
|
||||
void ptt_button_visibility(bool hidden);
|
||||
|
||||
ui::NavigationView& nav_;
|
||||
MorseDecoder morse_decoder_{};
|
||||
TxRadioState radio_state_{};
|
||||
Thread* tx_thread{nullptr};
|
||||
|
||||
std::string msg_buffer{"PORTAPACK"};
|
||||
uint8_t current_mode{0}; // 0=AM, 1=FM, 2=DSB, 3=USB, 4=LSB
|
||||
uint8_t wpm{20};
|
||||
uint32_t tone{700};
|
||||
float band{5.8};
|
||||
std::string call_sign{""};
|
||||
|
||||
app_settings::SettingsManager settings_{
|
||||
"tx_morseradio",
|
||||
app_settings::Mode::TX,
|
||||
{
|
||||
{"cmode"sv, ¤t_mode},
|
||||
{"audtone"sv, &tone},
|
||||
{"wpm"sv, &wpm},
|
||||
{"message"sv, &msg_buffer},
|
||||
{"bandwith"sv, &band},
|
||||
{"call_sign"sv, &call_sign},
|
||||
}};
|
||||
|
||||
TransmitterView tx_view{
|
||||
(int16_t)UI_POS_Y_BOTTOM(4),
|
||||
10000,
|
||||
0, false};
|
||||
|
||||
AudioVolumeField field_volume{{UI_POS_X_RIGHT(2), UI_POS_Y_BOTTOM(5)}};
|
||||
ui::OptionsField options_mode{
|
||||
{UI_POS_X(5), UI_POS_Y(0)},
|
||||
3,
|
||||
{{"AM", 0}, {"FM", 1}, {"DSB", 2}, {"USB", 3}, {"LSB", 4}}};
|
||||
NumberField tone_{{UI_POS_X(14), UI_POS_Y(0)}, 4, {400, 1400}, 10, ' ', true};
|
||||
NumberField wpm_{{UI_POS_X(25), UI_POS_Y(0)}, 2, {10, 45}, 1, ' ', true};
|
||||
FloatField bandwidth{{UI_POS_X(20), UI_POS_Y(3)}, 4, {1.0, 16.0}, 0.1, ' ', true, 1};
|
||||
|
||||
ui::Text txt_msg{{UI_POS_X(0), UI_POS_Y(1), UI_POS_MAXWIDTH, UI_POS_HEIGHT(1)}, "[" + msg_buffer + "] "};
|
||||
ui::Button btn_message{{UI_POS_X(0), UI_POS_Y(2), UI_POS_WIDTH(11), UI_POS_HEIGHT(1)}, "Message"};
|
||||
ui::Button btn_calls{{UI_POS_X(0), UI_POS_Y(4), UI_POS_WIDTH(11), UI_POS_HEIGHT(1)}, (call_sign.empty()) ? "call sign?" : call_sign};
|
||||
Checkbox chk_trans{{UI_POS_X(14), UI_POS_Y(2)}, 13, "Manual trans.", true};
|
||||
Checkbox chk_callsgn{{UI_POS_X(14), UI_POS_Y(4)}, 13, "Call sign", true};
|
||||
ui::Text txt_last{{UI_POS_X(10), UI_POS_Y(5), UI_POS_WIDTH_REMAINING(10), UI_POS_HEIGHT(1)}, ""};
|
||||
ui::Console console_text{{UI_POS_X(0), UI_POS_Y(7), UI_POS_MAXWIDTH, UI_POS_HEIGHT_REMAINING(14)}};
|
||||
ui::Button btn_clear{{UI_POS_X(0), UI_POS_Y_BOTTOM(5), UI_POS_WIDTH(5), UI_POS_HEIGHT(1)}, "CLR"};
|
||||
ui::Button btn_ptt{{UI_POS_X_CENTER(12), UI_POS_Y_BOTTOM(7), UI_POS_WIDTH(12), UI_POS_HEIGHT(3)}, "PTT"};
|
||||
|
||||
ui::Labels labels{
|
||||
{{UI_POS_X(0), UI_POS_Y(0)}, "Mode:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(9), UI_POS_Y(0)}, "Tone:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(18), UI_POS_Y(0)}, "Hz", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(21), UI_POS_Y(0)}, "WPM:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(14), UI_POS_Y(3)}, "BandW:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(24), UI_POS_Y(3)}, "kHz", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(5)}, "Last seq:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(6)}, "Sent Message:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X_RIGHT(7), UI_POS_Y_BOTTOM(5)}, "Vol.:", Theme::getInstance()->fg_light->foreground},
|
||||
};
|
||||
|
||||
uint8_t last_color_id{255};
|
||||
uint8_t color_id{255};
|
||||
std::string arr_color[4] = {STR_COLOR_WHITE, STR_COLOR_RED, STR_COLOR_YELLOW, STR_COLOR_GREEN};
|
||||
|
||||
bool button_touch{false};
|
||||
bool button_was_selected{false};
|
||||
bool decode_timeout_calc{false};
|
||||
bool transmit{false};
|
||||
bool thread_running = false;
|
||||
|
||||
uint8_t send_indicator{5};
|
||||
int64_t start_time{0};
|
||||
int64_t end_time{0};
|
||||
int64_t transmit_time{0};
|
||||
SwitchesState initial_switch_config_{};
|
||||
|
||||
MessageHandlerRegistration message_handler_framesync{
|
||||
Message::ID::DisplayFrameSync,
|
||||
[this](const Message* const p) {
|
||||
(void)p;
|
||||
this->on_framesync();
|
||||
}};
|
||||
};
|
||||
|
||||
} // namespace ui::external_app::morseradiotx
|
||||
|
||||
#endif // __MORSE_RADIOTX_H__
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Bernd Herzog
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "ui_sd_over_usb.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "external_app.hpp"
|
||||
|
||||
namespace ui::external_app::sdusb {
|
||||
void initialize_app(ui::NavigationView& nav) {
|
||||
nav.push<SdOverUsbView>();
|
||||
}
|
||||
} // namespace ui::external_app::sdusb
|
||||
|
||||
extern "C" {
|
||||
|
||||
__attribute__((section(".external_app.app_sdusb.application_information"), used)) application_information_t _application_information_sdusb = {
|
||||
/*.memory_location = */ (uint8_t*)0x00000000,
|
||||
/*.externalAppEntry = */ ui::external_app::sdusb::initialize_app,
|
||||
/*.header_version = */ CURRENT_HEADER_VERSION,
|
||||
/*.app_version = */ VERSION_MD5,
|
||||
|
||||
/*.app_name = */ "SD Over USB",
|
||||
/*.bitmap_data = */ {
|
||||
0xF0,
|
||||
0x0F,
|
||||
0x10,
|
||||
0x08,
|
||||
0x50,
|
||||
0x0A,
|
||||
0x10,
|
||||
0x08,
|
||||
0x10,
|
||||
0x08,
|
||||
0x10,
|
||||
0x08,
|
||||
0xF8,
|
||||
0x1F,
|
||||
0xF8,
|
||||
0x1F,
|
||||
0xF8,
|
||||
0x1F,
|
||||
0xF8,
|
||||
0x1F,
|
||||
0xF8,
|
||||
0x1F,
|
||||
0xF8,
|
||||
0x1F,
|
||||
0xF8,
|
||||
0x1F,
|
||||
0xF0,
|
||||
0x0F,
|
||||
0x80,
|
||||
0x01,
|
||||
0x80,
|
||||
0x01,
|
||||
},
|
||||
/*.icon_color = */ ui::Color::yellow().v,
|
||||
/*.menu_location = */ app_location_t::UTILITIES,
|
||||
/*.desired_menu_position = */ -1,
|
||||
|
||||
/*.m4_app_tag = portapack::spi_flash::image_tag_usb_sd */ {'P', 'U', 'S', 'B'},
|
||||
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
|
||||
};
|
||||
}
|
||||
+3
-3
@@ -23,7 +23,7 @@
|
||||
#include "ui_sd_over_usb.hpp"
|
||||
#include "portapack_shared_memory.hpp"
|
||||
|
||||
namespace ui {
|
||||
namespace ui::external_app::sdusb {
|
||||
|
||||
SdOverUsbView::SdOverUsbView(NavigationView& nav)
|
||||
: nav_(nav) {
|
||||
@@ -46,7 +46,7 @@ SdOverUsbView::SdOverUsbView(NavigationView& nav)
|
||||
sdcStop(&SDCD1);
|
||||
|
||||
portapack::shutdown(true);
|
||||
m4_init(portapack::spi_flash::image_tag_usb_sd, portapack::memory::map::m4_code, false);
|
||||
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
|
||||
m0_halt();
|
||||
/* will not return*/
|
||||
};
|
||||
@@ -56,4 +56,4 @@ void SdOverUsbView::focus() {
|
||||
button_run.focus();
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
} // namespace ui::external_app::sdusb
|
||||
+2
-2
@@ -32,7 +32,7 @@
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace ui {
|
||||
namespace ui::external_app::sdusb {
|
||||
|
||||
class SdOverUsbView : public View {
|
||||
public:
|
||||
@@ -58,6 +58,6 @@ class SdOverUsbView : public View {
|
||||
"Run"};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
} // namespace ui::external_app::sdusb
|
||||
|
||||
#endif /*__UI_SD_OVER_USB_H__*/
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Bernd Herzog
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "ui_siggen.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "external_app.hpp"
|
||||
|
||||
namespace ui::external_app::siggen {
|
||||
void initialize_app(ui::NavigationView& nav) {
|
||||
nav.push<SigGenView>();
|
||||
}
|
||||
} // namespace ui::external_app::siggen
|
||||
extern "C" {
|
||||
|
||||
__attribute__((section(".external_app.app_siggen.application_information"), used)) application_information_t _application_information_siggen = {
|
||||
/*.memory_location = */ (uint8_t*)0x00000000,
|
||||
/*.externalAppEntry = */ ui::external_app::siggen::initialize_app,
|
||||
/*.header_version = */ CURRENT_HEADER_VERSION,
|
||||
/*.app_version = */ VERSION_MD5,
|
||||
|
||||
/*.app_name = */ "Signal gen",
|
||||
/*.bitmap_data = */ {
|
||||
0x18,
|
||||
0x00,
|
||||
0x24,
|
||||
0x00,
|
||||
0x42,
|
||||
0x00,
|
||||
0x42,
|
||||
0x00,
|
||||
0x42,
|
||||
0x00,
|
||||
0x42,
|
||||
0x00,
|
||||
0x81,
|
||||
0x00,
|
||||
0xAB,
|
||||
0x6A,
|
||||
0x80,
|
||||
0x40,
|
||||
0x00,
|
||||
0x21,
|
||||
0x00,
|
||||
0x21,
|
||||
0x00,
|
||||
0x21,
|
||||
0x00,
|
||||
0x21,
|
||||
0x00,
|
||||
0x12,
|
||||
0x00,
|
||||
0x0C,
|
||||
0x00,
|
||||
0x00,
|
||||
},
|
||||
/*.icon_color = */ ui::Color::green().v,
|
||||
/*.menu_location = */ app_location_t::TX,
|
||||
/*.desired_menu_position = */ -1,
|
||||
|
||||
/*.m4_app_tag = portapack::spi_flash::image_tag_siggen */ {'P', 'S', 'I', 'G'},
|
||||
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
|
||||
};
|
||||
}
|
||||
Vendored
+3
-3
@@ -31,7 +31,7 @@
|
||||
|
||||
using namespace portapack;
|
||||
|
||||
namespace ui {
|
||||
namespace ui::external_app::siggen {
|
||||
|
||||
void SigGenView::focus() {
|
||||
options_shape.focus();
|
||||
@@ -75,7 +75,7 @@ void SigGenView::on_tx_progress(const uint32_t progress, const bool done) {
|
||||
|
||||
SigGenView::SigGenView(
|
||||
NavigationView& nav) {
|
||||
baseband::run_image(portapack::spi_flash::image_tag_siggen);
|
||||
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
|
||||
|
||||
add_children({&labels,
|
||||
&options_mod,
|
||||
@@ -172,4 +172,4 @@ SigGenView::SigGenView(
|
||||
};
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
} // namespace ui::external_app::siggen
|
||||
Vendored
+2
-2
@@ -33,7 +33,7 @@
|
||||
#include "portapack.hpp"
|
||||
#include "message.hpp"
|
||||
|
||||
namespace ui {
|
||||
namespace ui::external_app::siggen {
|
||||
|
||||
class SigGenView : public View {
|
||||
public:
|
||||
@@ -139,6 +139,6 @@ class SigGenView : public View {
|
||||
}};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
} // namespace ui::external_app::siggen
|
||||
|
||||
#endif /*__SIGGEN_H__*/
|
||||
+73
-9
@@ -266,10 +266,11 @@ void SubCarRecentEntryDetailView::parseProtocol() {
|
||||
if (entry_.sensorType == FPC_Invalid) return;
|
||||
|
||||
if (entry_.sensorType == FPC_SUZUKI) {
|
||||
uint32_t serial_button = (((entry_.data >> 32) & 0xFFF) << 20) | (entry_.data >> 12);
|
||||
serial = serial_button >> 4;
|
||||
uint8_t buttonid = serial_button & 0xF;
|
||||
cnt = (entry_.data >> 44) & 0xFFFF;
|
||||
uint32_t data_high = (uint32_t)(entry_.data >> 32);
|
||||
uint32_t data_low = (uint32_t)entry_.data;
|
||||
serial = ((data_high & 0xFFF) << 16) | (data_low >> 16);
|
||||
uint8_t buttonid = (data_low >> 12) & 0xF;
|
||||
cnt = (data_high << 4) >> 16;
|
||||
btn = to_string_dec_uint(buttonid);
|
||||
return;
|
||||
}
|
||||
@@ -320,15 +321,78 @@ void SubCarRecentEntryDetailView::parseProtocol() {
|
||||
}
|
||||
|
||||
if (entry_.sensorType == FPC_KIAV5) {
|
||||
serial = (uint32_t)(((entry_.data >> 32) & 0x0FFFFFFF) >> 1);
|
||||
uint8_t button = (entry_.data >> 61) & 0x07;
|
||||
uint64_t yek = 0;
|
||||
for (int i = 0; i < 8; i++) {
|
||||
uint8_t byte = (entry_.data2 >> (i * 8)) & 0xFF;
|
||||
uint8_t reversed = 0;
|
||||
for (int b = 0; b < 8; b++) {
|
||||
if (byte & (1 << b))
|
||||
reversed |= (1 << (7 - b));
|
||||
}
|
||||
yek |= ((uint64_t)reversed << ((7 - i) * 8));
|
||||
}
|
||||
serial = (uint32_t)((yek >> 32) & 0x0FFFFFFF);
|
||||
uint8_t button = (uint8_t)((yek >> 60) & 0x0F);
|
||||
uint32_t encrypted = (uint32_t)(yek & 0xFFFFFFFF);
|
||||
btn = to_string_dec_uint(button);
|
||||
cnt = (uint16_t)(entry_.data & 0xFFFF);
|
||||
// decode
|
||||
uint8_t keystore_bytes[] = {0x53, 0x54, 0x46, 0x52, 0x4b, 0x45, 0x30, 0x30};
|
||||
uint8_t s0 = (encrypted & 0xFF);
|
||||
uint8_t s1 = (encrypted >> 8) & 0xFF;
|
||||
uint8_t s2 = (encrypted >> 16) & 0xFF;
|
||||
uint8_t s3 = (encrypted >> 24) & 0xFF;
|
||||
int round_index = 1;
|
||||
for (size_t i = 0; i < 18; i++) {
|
||||
uint8_t r = keystore_bytes[round_index] & 0xFF;
|
||||
int steps = 8;
|
||||
while (steps > 0) {
|
||||
uint8_t base;
|
||||
if ((s3 & 0x40) == 0) {
|
||||
base = (s3 & 0x02) == 0 ? 0x74 : 0x2E;
|
||||
} else {
|
||||
base = (s3 & 0x02) == 0 ? 0x3A : 0x5C;
|
||||
}
|
||||
|
||||
if (s2 & 0x08) {
|
||||
base = (((base >> 4) & 0x0F) | ((base & 0x0F) << 4)) & 0xFF;
|
||||
}
|
||||
if (s1 & 0x01) {
|
||||
base = ((base & 0x3F) << 2) & 0xFF;
|
||||
}
|
||||
if (s0 & 0x01) {
|
||||
base = (base << 1) & 0xFF;
|
||||
}
|
||||
|
||||
uint8_t temp = (s3 ^ s1) & 0xFF;
|
||||
s3 = ((s3 & 0x7F) << 1) & 0xFF;
|
||||
if (s2 & 0x80) {
|
||||
s3 |= 0x01;
|
||||
}
|
||||
s2 = ((s2 & 0x7F) << 1) & 0xFF;
|
||||
if (s1 & 0x80) {
|
||||
s2 |= 0x01;
|
||||
}
|
||||
s1 = ((s1 & 0x7F) << 1) & 0xFF;
|
||||
if (s0 & 0x80) {
|
||||
s1 |= 0x01;
|
||||
}
|
||||
s0 = ((s0 & 0x7F) << 1) & 0xFF;
|
||||
|
||||
uint8_t chk = (base ^ (r ^ temp)) & 0xFF;
|
||||
if (chk & 0x80) {
|
||||
s0 |= 0x01;
|
||||
}
|
||||
r = ((r & 0x7F) << 1) & 0xFF;
|
||||
steps--;
|
||||
}
|
||||
round_index = (round_index - 1) & 0x7;
|
||||
}
|
||||
cnt = (s0 + (s1 << 8)) & 0xFFFF;
|
||||
}
|
||||
|
||||
if (entry_.sensorType == FPC_KIAV3V4) {
|
||||
// not decrypted!
|
||||
serial = SD_NO_SERIAL; //(uint32_t)entry_.data;
|
||||
serial = (uint32_t)entry_.data;
|
||||
// uint8_t button = entry_.data2 & 0xFF;
|
||||
btn = "?"; // to_string_dec_uint(button);
|
||||
}
|
||||
@@ -344,7 +408,7 @@ void SubCarRecentEntryDetailView::parseProtocol() {
|
||||
if (entry_.sensorType == FPC_KIAV1) {
|
||||
serial = (uint32_t)((entry_.data >> 24) & 0xFFFFFFFF);
|
||||
uint8_t button = (uint8_t)((entry_.data >> 16) & 0xFF);
|
||||
cnt = (uint8_t)((entry_.data >> 8) & 0xFF);
|
||||
cnt = (uint8_t)((entry_.data >> 4) & 0xF) << 8 | ((entry_.data >> 8) & 0xFF);
|
||||
btn = to_string_dec_uint(button);
|
||||
}
|
||||
|
||||
|
||||
@@ -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__*/
|
||||
@@ -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},
|
||||
|
||||
@@ -37,6 +37,14 @@ void Debounce::enable_repeat() {
|
||||
repeat_enabled_ = true;
|
||||
}
|
||||
|
||||
void Debounce::set_enable_repeat(bool enabled) {
|
||||
repeat_enabled_ = enabled;
|
||||
}
|
||||
|
||||
bool Debounce::get_repeat_enabled() {
|
||||
return repeat_enabled_;
|
||||
}
|
||||
|
||||
bool Debounce::get_long_press_enabled() const {
|
||||
return long_press_enabled_;
|
||||
}
|
||||
|
||||
@@ -43,6 +43,8 @@ class Debounce {
|
||||
bool feed(const uint8_t bit);
|
||||
uint8_t state();
|
||||
void enable_repeat();
|
||||
void set_enable_repeat(bool enabled);
|
||||
bool get_repeat_enabled();
|
||||
bool get_long_press_enabled() const;
|
||||
void set_long_press_enabled(bool v);
|
||||
bool long_press_occurred();
|
||||
|
||||
@@ -227,8 +227,8 @@ void controls_init() {
|
||||
gptStart(&GPTD1, &timer0_config);
|
||||
gptStartContinuous(&GPTD1, timer0_match_count);
|
||||
|
||||
// Enable repeat for directional switches only
|
||||
for (auto i = Switch::Right; i <= Switch::Up; incr(i))
|
||||
// Enable repeat for directional and Select switches only
|
||||
for (auto i = Switch::Right; i <= Switch::Sel; incr(i))
|
||||
switch_debounce[toUType(i)].enable_repeat();
|
||||
}
|
||||
|
||||
@@ -250,6 +250,22 @@ SwitchesState get_switches_state() {
|
||||
return result;
|
||||
}
|
||||
|
||||
/* Gets the repeat enabled state for all the switches. */
|
||||
SwitchesState get_switches_repeat_config() {
|
||||
SwitchesState result;
|
||||
|
||||
for (size_t i = 0; i < result.size(); i++)
|
||||
result[i] = switch_debounce[i].get_repeat_enabled();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/* Configures which switches support repeat.*/
|
||||
void set_switches_repeat_config(SwitchesState switch_config) {
|
||||
for (size_t i = 0; i < switch_config.size(); i++)
|
||||
switch_debounce[i].set_enable_repeat(switch_config[i]);
|
||||
}
|
||||
|
||||
/* Gets the long press enabled state for all the switches. */
|
||||
SwitchesState get_switches_long_press_config() {
|
||||
SwitchesState result;
|
||||
|
||||
@@ -48,6 +48,8 @@ uint8_t swizzled_switches();
|
||||
SwitchesState get_switches_state();
|
||||
EncoderPosition get_encoder_position();
|
||||
touch::Frame get_touch_frame();
|
||||
SwitchesState get_switches_repeat_config();
|
||||
void set_switches_repeat_config(SwitchesState switch_config);
|
||||
|
||||
SwitchesState get_switches_long_press_config();
|
||||
void set_switches_long_press_config(SwitchesState switch_config);
|
||||
|
||||
@@ -38,3 +38,11 @@ Optional<File::Error> LogFile::write_raw(const std::string& message) {
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
Optional<File::Error> LogFile::write_raw_no_newline(const std::string& message) {
|
||||
auto result_s = file.write(message.c_str(), message.size());
|
||||
if (!result_s.is_error()) {
|
||||
file.sync();
|
||||
}
|
||||
return {result_s.error()};
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ class LogFile {
|
||||
Optional<File::Error> write_entry(const std::string& entry);
|
||||
Optional<File::Error> write_entry(const rtc::RTC& datetime, const std::string& entry);
|
||||
Optional<File::Error> write_raw(const std::string& message);
|
||||
Optional<File::Error> write_raw_no_newline(const std::string& message);
|
||||
|
||||
private:
|
||||
File file{};
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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 */
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -206,13 +206,6 @@ bool set_tuning_frequency(const rf::Frequency frequency) {
|
||||
|
||||
void set_rf_amp(const bool rf_amp) {
|
||||
rf_path.set_rf_amp(rf_amp);
|
||||
|
||||
if (direction == rf::Direction::Transmit) {
|
||||
if (rf_amp)
|
||||
led_tx.on();
|
||||
else
|
||||
led_tx.off();
|
||||
}
|
||||
}
|
||||
|
||||
void set_lna_gain(const int_fast8_t db) {
|
||||
|
||||
@@ -252,8 +252,6 @@ void ReceiverModel::enable() {
|
||||
|
||||
// TODO: maybe not the perfect place for this, but it's reasonable.
|
||||
update_headphone_volume();
|
||||
|
||||
led_rx.on();
|
||||
}
|
||||
|
||||
void ReceiverModel::disable() {
|
||||
@@ -262,7 +260,6 @@ void ReceiverModel::disable() {
|
||||
// TODO: Responsibility for enabling/disabling the radio is muddy.
|
||||
// Some happens in ReceiverModel, some inside radio namespace.
|
||||
radio::disable();
|
||||
led_rx.off();
|
||||
}
|
||||
|
||||
void ReceiverModel::initialize() {
|
||||
@@ -306,7 +303,8 @@ int32_t ReceiverModel::tuning_offset() {
|
||||
|
||||
void ReceiverModel::update_tuning_frequency() {
|
||||
// TODO: use positive offset if freq < offset.
|
||||
radio::set_tuning_frequency(target_frequency() + hidden_offset + tuning_offset());
|
||||
if (enabled_)
|
||||
radio::set_tuning_frequency(target_frequency() + hidden_offset + tuning_offset());
|
||||
}
|
||||
|
||||
void ReceiverModel::set_hidden_offset(rf::Frequency offset) {
|
||||
@@ -315,7 +313,8 @@ void ReceiverModel::set_hidden_offset(rf::Frequency offset) {
|
||||
}
|
||||
|
||||
void ReceiverModel::update_baseband_bandwidth() {
|
||||
radio::set_baseband_filter_bandwidth_rx(baseband_bandwidth());
|
||||
if (enabled_)
|
||||
radio::set_baseband_filter_bandwidth_rx(baseband_bandwidth());
|
||||
}
|
||||
|
||||
void ReceiverModel::update_sampling_rate() {
|
||||
@@ -324,23 +323,31 @@ void ReceiverModel::update_sampling_rate() {
|
||||
// protocols that need quick RX/TX turn-around.
|
||||
|
||||
// Disabling baseband while changing sampling rates seems like a good idea...
|
||||
radio::set_baseband_rate(sampling_rate());
|
||||
if (enabled_)
|
||||
radio::set_baseband_rate(sampling_rate());
|
||||
|
||||
update_tuning_frequency();
|
||||
}
|
||||
|
||||
void ReceiverModel::update_lna() {
|
||||
radio::set_lna_gain(lna());
|
||||
if (enabled_)
|
||||
radio::set_lna_gain(lna());
|
||||
}
|
||||
|
||||
void ReceiverModel::update_vga() {
|
||||
radio::set_vga_gain(vga());
|
||||
if (enabled_)
|
||||
radio::set_vga_gain(vga());
|
||||
}
|
||||
|
||||
void ReceiverModel::update_rf_amp() {
|
||||
radio::set_rf_amp(rf_amp());
|
||||
if (enabled_)
|
||||
radio::set_rf_amp(rf_amp());
|
||||
}
|
||||
|
||||
void ReceiverModel::update_modulation() {
|
||||
if (!enabled_)
|
||||
return;
|
||||
|
||||
switch (modulation()) {
|
||||
default:
|
||||
case Mode::AMAudio:
|
||||
@@ -395,5 +402,6 @@ void ReceiverModel::update_antenna_bias() {
|
||||
}
|
||||
|
||||
void ReceiverModel::update_headphone_volume() {
|
||||
audio::headphone::set_volume(headphone_volume());
|
||||
if (enabled_)
|
||||
audio::headphone::set_volume(headphone_volume());
|
||||
}
|
||||
|
||||
@@ -31,6 +31,8 @@
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
#include "radio.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
using namespace hackrf::one;
|
||||
using namespace portapack;
|
||||
|
||||
@@ -70,7 +72,7 @@ void TransmitterModel::set_channel_bandwidth(uint32_t v) {
|
||||
}
|
||||
|
||||
uint8_t TransmitterModel::tx_gain() const {
|
||||
return settings_.tx_gain_db;
|
||||
return std::min(settings_.tx_gain_db, portapack::persistent_memory::config_tx_gain_max_db());
|
||||
}
|
||||
|
||||
void TransmitterModel::set_tx_gain(uint8_t v_db) {
|
||||
@@ -79,7 +81,7 @@ void TransmitterModel::set_tx_gain(uint8_t v_db) {
|
||||
}
|
||||
|
||||
bool TransmitterModel::rf_amp() const {
|
||||
return settings_.rf_amp;
|
||||
return settings_.rf_amp && !portapack::persistent_memory::config_tx_amp_disabled();
|
||||
}
|
||||
|
||||
void TransmitterModel::set_rf_amp(bool enabled) {
|
||||
@@ -92,6 +94,15 @@ void TransmitterModel::set_antenna_bias() {
|
||||
}
|
||||
|
||||
void TransmitterModel::enable() {
|
||||
if (portapack::persistent_memory::config_tx_disabled()) {
|
||||
radio::disable();
|
||||
|
||||
TXDisabledMessage message;
|
||||
EventDispatcher::send_message(message);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
enabled_ = true;
|
||||
radio::set_direction(rf::Direction::Transmit);
|
||||
update_tuning_frequency();
|
||||
@@ -101,7 +112,6 @@ void TransmitterModel::enable() {
|
||||
update_sampling_rate();
|
||||
update_tx_gain();
|
||||
|
||||
led_tx.on();
|
||||
signal_token_tick_second = rtc_time::signal_tick_second += [this]() {
|
||||
this->on_tick_second();
|
||||
};
|
||||
@@ -118,7 +128,6 @@ void TransmitterModel::disable() {
|
||||
radio::disable();
|
||||
|
||||
rtc_time::signal_tick_second -= signal_token_tick_second;
|
||||
led_tx.off();
|
||||
}
|
||||
|
||||
void TransmitterModel::initialize() {
|
||||
@@ -135,11 +144,13 @@ void TransmitterModel::configure_from_app_settings(
|
||||
}
|
||||
|
||||
void TransmitterModel::update_tuning_frequency() {
|
||||
radio::set_tuning_frequency(target_frequency());
|
||||
if (enabled_)
|
||||
radio::set_tuning_frequency(target_frequency());
|
||||
}
|
||||
|
||||
void TransmitterModel::update_baseband_bandwidth() {
|
||||
radio::set_baseband_filter_bandwidth_tx(baseband_bandwidth());
|
||||
if (enabled_)
|
||||
radio::set_baseband_filter_bandwidth_tx(baseband_bandwidth());
|
||||
}
|
||||
|
||||
void TransmitterModel::update_sampling_rate() {
|
||||
@@ -148,17 +159,20 @@ void TransmitterModel::update_sampling_rate() {
|
||||
// protocols that need quick RX/TX turn-around.
|
||||
|
||||
// Disabling baseband while changing sampling rates seems like a good idea...
|
||||
if (enabled_)
|
||||
radio::set_baseband_rate(sampling_rate());
|
||||
|
||||
radio::set_baseband_rate(sampling_rate());
|
||||
update_tuning_frequency();
|
||||
}
|
||||
|
||||
void TransmitterModel::update_tx_gain() {
|
||||
radio::set_tx_gain(tx_gain());
|
||||
if (enabled_)
|
||||
radio::set_tx_gain(tx_gain());
|
||||
}
|
||||
|
||||
void TransmitterModel::update_rf_amp() {
|
||||
radio::set_rf_amp(rf_amp());
|
||||
if (enabled_)
|
||||
radio::set_rf_amp(rf_amp());
|
||||
}
|
||||
|
||||
void TransmitterModel::update_antenna_bias() {
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -49,10 +49,9 @@
|
||||
#include "ui_rds.hpp"
|
||||
#include "ui_recon.hpp"
|
||||
// #include "ui_scanner.hpp"
|
||||
#include "ui_sd_over_usb.hpp"
|
||||
// #include "ui_sd_over_usb.hpp"
|
||||
#include "ui_search.hpp"
|
||||
#include "ui_settings.hpp"
|
||||
#include "ui_siggen.hpp"
|
||||
#include "ui_sonde.hpp"
|
||||
#include "ui_ss_viewer.hpp"
|
||||
// #include "ui_test.hpp"
|
||||
@@ -114,7 +113,7 @@ const NavigationView::AppList NavigationView::appList = {
|
||||
/* HOME ******************************************************************/
|
||||
{nullptr, "Receive", HOME, Color::cyan(), &bitmap_icon_receivers, new ViewFactory<ReceiversMenuView>()},
|
||||
{nullptr, "Transmit", HOME, Color::cyan(), &bitmap_icon_transmit, new ViewFactory<TransmittersMenuView>()},
|
||||
{nullptr, "Tranceiver", HOME, Color::cyan(), &bitmap_icon_tranceivers, new ViewFactory<TranceiversMenuView>()},
|
||||
{nullptr, "Transceiver", HOME, Color::cyan(), &bitmap_icon_transceivers, new ViewFactory<TransceiversMenuView>()},
|
||||
{"recon", "Recon", HOME, Color::green(), &bitmap_icon_scanner, new ViewFactory<ReconView>()},
|
||||
{"capture", "Capture", HOME, Color::red(), &bitmap_icon_capture, new ViewFactory<CaptureAppView>()},
|
||||
{"replay", "Replay", HOME, Color::green(), &bitmap_icon_replay, new ViewFactory<PlaylistView>()},
|
||||
@@ -140,7 +139,6 @@ const NavigationView::AppList NavigationView::appList = {
|
||||
{"pocsagtx", "POCSAG TX", TX, ui::Color::green(), &bitmap_icon_pocsag, new ViewFactory<POCSAGTXView>()},
|
||||
{"rdstx", "RDS", TX, ui::Color::green(), &bitmap_icon_rds, new ViewFactory<RDSView>()},
|
||||
{"touchtune", "TouchTune", TX, ui::Color::green(), &bitmap_icon_touchtunes, new ViewFactory<TouchTunesView>()},
|
||||
{"signalgen", "SignalGen", TX, Color::green(), &bitmap_icon_cwgen, new ViewFactory<SigGenView>()},
|
||||
/* TRX ********************************************************************/
|
||||
{"microphone", "Mic", TRX, Color::green(), &bitmap_icon_microphone, new ViewFactory<MicTXView>()},
|
||||
/* UTILITIES *************************************************************/
|
||||
@@ -148,7 +146,7 @@ const NavigationView::AppList NavigationView::appList = {
|
||||
{"freqman", "Freq. Manager", UTILITIES, Color::green(), &bitmap_icon_freqman, new ViewFactory<FrequencyManagerView>()},
|
||||
{"iqtrim", "IQ Trim", UTILITIES, Color::orange(), &bitmap_icon_trim, new ViewFactory<IQTrimView>()},
|
||||
{"notepad", "Notepad", UTILITIES, Color::dark_cyan(), &bitmap_icon_notepad, new ViewFactory<TextEditorView>()},
|
||||
{nullptr, "SD Over USB", UTILITIES, Color::yellow(), &bitmap_icon_hackrf, new ViewFactory<SdOverUsbView>()},
|
||||
//{nullptr, "SD Over USB", UTILITIES, Color::yellow(), &bitmap_icon_hackrf, new ViewFactory<SdOverUsbView>()},
|
||||
{nullptr, "Debug", UTILITIES, Color::light_grey(), &bitmap_icon_debug, new ViewFactory<DebugMenuView>()},
|
||||
//{"testapp", "Test App", UTILITIES, Color::dark_grey(), nullptr, new ViewFactory<TestView>()},
|
||||
// Dangerous apps.
|
||||
@@ -320,6 +318,14 @@ SystemStatusView::SystemStatusView(
|
||||
refresh();
|
||||
}
|
||||
|
||||
void SystemStatusView::on_tx_disabled() {
|
||||
if (!nav_.is_valid())
|
||||
return;
|
||||
|
||||
nav_.pop();
|
||||
nav_.display_modal("Error", "RF transmit disabled.\nApplication closed.");
|
||||
}
|
||||
|
||||
// when battery icon / text is clicked
|
||||
void SystemStatusView::on_battery_details() {
|
||||
if (!nav_.is_valid()) return;
|
||||
@@ -601,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;
|
||||
}
|
||||
@@ -837,12 +843,12 @@ void TransmittersMenuView::on_populate() {
|
||||
add_external_items(nav_, app_location_t::TX, *this, return_icon ? 1 : 0);
|
||||
}
|
||||
|
||||
/* TranceiversMenuView **************************************************/
|
||||
/* TransceiversMenuView **************************************************/
|
||||
|
||||
TranceiversMenuView::TranceiversMenuView(NavigationView& nav)
|
||||
TransceiversMenuView::TransceiversMenuView(NavigationView& nav)
|
||||
: nav_(nav) {}
|
||||
|
||||
void TranceiversMenuView::on_populate() {
|
||||
void TransceiversMenuView::on_populate() {
|
||||
bool return_icon = pmem::show_gui_return_icon();
|
||||
if (return_icon) {
|
||||
add_items({{"..", Theme::getInstance()->fg_light->foreground, &bitmap_icon_previous, [this]() { nav_.pop(); }}});
|
||||
|
||||
@@ -303,6 +303,7 @@ class SystemStatusView : public View {
|
||||
void on_title();
|
||||
void refresh();
|
||||
void on_clk();
|
||||
void on_tx_disabled();
|
||||
void rtc_battery_workaround();
|
||||
void on_battery_data(const BatteryStateMessage* msg);
|
||||
void on_battery_details();
|
||||
@@ -314,6 +315,13 @@ class SystemStatusView : public View {
|
||||
this->refresh();
|
||||
}};
|
||||
|
||||
MessageHandlerRegistration message_handler_tx_disabled{
|
||||
Message::ID::TXDisabled,
|
||||
[this](const Message* const p) {
|
||||
(void)p;
|
||||
this->on_tx_disabled();
|
||||
}};
|
||||
|
||||
MessageHandlerRegistration message_handler_battery{
|
||||
Message::ID::BatteryStateData,
|
||||
[this](const Message* const p) {
|
||||
@@ -381,10 +389,10 @@ class TransmittersMenuView : public BtnGridView {
|
||||
void on_populate() override;
|
||||
};
|
||||
|
||||
class TranceiversMenuView : public BtnGridView {
|
||||
class TransceiversMenuView : public BtnGridView {
|
||||
public:
|
||||
TranceiversMenuView(NavigationView& nav);
|
||||
std::string title() const override { return "Tranceiver"; };
|
||||
TransceiversMenuView(NavigationView& nav);
|
||||
std::string title() const override { return "Transceiver"; };
|
||||
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
|
||||
@@ -71,9 +71,9 @@ void nvic_enable_irq(uint8_t irqn) {
|
||||
void usb_configuration_changed(usb_device_t* const device) {
|
||||
(void)device;
|
||||
|
||||
usb_endpoint_init(&usb_endpoint_int_in);
|
||||
usb_endpoint_init(&usb_endpoint_bulk_in);
|
||||
usb_endpoint_init(&usb_endpoint_bulk_out);
|
||||
usb_endpoint_init(&usb_endpoint_int_in, false);
|
||||
usb_endpoint_init(&usb_endpoint_bulk_in, false);
|
||||
usb_endpoint_init(&usb_endpoint_bulk_out, false);
|
||||
}
|
||||
|
||||
void setup_usb_serial_controller(void) {
|
||||
@@ -88,8 +88,8 @@ void setup_usb_serial_controller(void) {
|
||||
usb_queue_init(&usb_endpoint_bulk_out_queue);
|
||||
usb_queue_init(&usb_endpoint_bulk_in_queue);
|
||||
|
||||
usb_endpoint_init(&usb_endpoint_control_out);
|
||||
usb_endpoint_init(&usb_endpoint_control_in);
|
||||
usb_endpoint_init(&usb_endpoint_control_out, false);
|
||||
usb_endpoint_init(&usb_endpoint_control_in, false);
|
||||
|
||||
usb_run(&usb_device);
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -447,14 +447,6 @@ set(MODE_CPPSRC
|
||||
)
|
||||
DeclareTargets(PREP replay)
|
||||
|
||||
### Signal generator
|
||||
|
||||
set(MODE_CPPSRC
|
||||
proc_siggen.cpp
|
||||
)
|
||||
DeclareTargets(PSIG siggen)
|
||||
|
||||
|
||||
|
||||
### Tones
|
||||
|
||||
@@ -505,53 +497,6 @@ set(MODE_CPPSRC
|
||||
)
|
||||
DeclareTargets(PFUT flash_utility)
|
||||
|
||||
### SD over USB
|
||||
|
||||
set(MODE_INCDIR
|
||||
${HACKRF_PATH}/firmware
|
||||
${HACKRF_PATH}/firmware/common
|
||||
${HACKRF_PATH}/firmware/libopencm3/include
|
||||
)
|
||||
set(MODE_CPPSRC
|
||||
sd_over_usb/proc_sd_over_usb.cpp
|
||||
|
||||
sd_over_usb/scsi.c
|
||||
sd_over_usb/diskio.c
|
||||
sd_over_usb/sd_over_usb.c
|
||||
sd_over_usb/usb_descriptor.c
|
||||
sd_over_usb/hackrf_core.c
|
||||
|
||||
${HACKRF_PATH}/firmware/common/usb.c
|
||||
${HACKRF_PATH}/firmware/common/usb_queue.c
|
||||
${HACKRF_PATH}/firmware/hackrf_usb/usb_device.c
|
||||
${HACKRF_PATH}/firmware/hackrf_usb/usb_endpoint.c
|
||||
${HACKRF_PATH}/firmware/common/usb_request.c
|
||||
${HACKRF_PATH}/firmware/common/usb_standard_request.c
|
||||
${HACKRF_PATH}/firmware/common/platform_detect.c
|
||||
${HACKRF_PATH}/firmware/common/gpio_lpc.c
|
||||
${HACKRF_PATH}/firmware/common/firmware_info.c
|
||||
${HACKRF_PATH}/firmware/common/si5351c.c
|
||||
${HACKRF_PATH}/firmware/common/i2c_bus.c
|
||||
${HACKRF_PATH}/firmware/common/mixer.c
|
||||
${HACKRF_PATH}/firmware/common/clkin.c
|
||||
${HACKRF_PATH}/firmware/common/spi_bus.c
|
||||
${HACKRF_PATH}/firmware/common/sgpio.c
|
||||
${HACKRF_PATH}/firmware/common/rf_path.c
|
||||
${HACKRF_PATH}/firmware/common/i2c_lpc.c
|
||||
${HACKRF_PATH}/firmware/common/spi_ssp.c
|
||||
${HACKRF_PATH}/firmware/common/rffc5071_spi.c
|
||||
${HACKRF_PATH}/firmware/common/rffc5071.c
|
||||
${HACKRF_PATH}/firmware/common/clkin.c
|
||||
${HACKRF_PATH}/firmware/common/gpdma.c
|
||||
|
||||
${HACKRF_PATH}/firmware/libopencm3/lib/cm3/nvic.c
|
||||
${HACKRF_PATH}/firmware/libopencm3/lib/cm3/sync.c
|
||||
${HACKRF_PATH}/firmware/libopencm3/lib/lpc43xx/scu.c
|
||||
${HACKRF_PATH}/firmware/libopencm3/lib/lpc43xx/timer.c
|
||||
${HACKRF_PATH}/firmware/libopencm3/lib/lpc43xx/i2c.c
|
||||
)
|
||||
DeclareTargets(PUSB sd_over_usb)
|
||||
|
||||
### Place external app and disabled images below so they don't get added to the firmware
|
||||
set(add_to_firmware FALSE)
|
||||
set(MODE_FLAGS "-O3")
|
||||
@@ -681,6 +626,13 @@ set(MODE_CPPSRC
|
||||
DeclareTargets(POSK ookstream)
|
||||
|
||||
|
||||
### Signal generator
|
||||
|
||||
set(MODE_CPPSRC
|
||||
proc_siggen.cpp
|
||||
)
|
||||
DeclareTargets(PSIG siggen)
|
||||
|
||||
|
||||
### WeFax Rx
|
||||
|
||||
@@ -711,6 +663,70 @@ set(MODE_CPPSRC
|
||||
)
|
||||
DeclareTargets(PSCD subcar)
|
||||
|
||||
### Morse RX Decoder
|
||||
|
||||
set(MODE_CPPSRC
|
||||
proc_morse.cpp
|
||||
)
|
||||
DeclareTargets(PMRS morse)
|
||||
|
||||
### SD over USB
|
||||
|
||||
set(MODE_INCDIR
|
||||
${HACKRF_PATH}/firmware
|
||||
${HACKRF_PATH}/firmware/common
|
||||
${HACKRF_PATH}/firmware/libopencm3/include
|
||||
)
|
||||
|
||||
### Morse TX
|
||||
|
||||
set(MODE_CPPSRC
|
||||
proc_morsetx.cpp
|
||||
)
|
||||
DeclareTargets(PMRT morsetx)
|
||||
|
||||
set(MODE_CPPSRC
|
||||
sd_over_usb/proc_sd_over_usb.cpp
|
||||
|
||||
sd_over_usb/scsi.c
|
||||
sd_over_usb/diskio.c
|
||||
sd_over_usb/sd_over_usb.c
|
||||
sd_over_usb/usb_descriptor.c
|
||||
sd_over_usb/hackrf_core.c
|
||||
|
||||
${HACKRF_PATH}/firmware/common/adc.c
|
||||
${HACKRF_PATH}/firmware/common/selftest.c
|
||||
${HACKRF_PATH}/firmware/common/usb.c
|
||||
${HACKRF_PATH}/firmware/common/usb_queue.c
|
||||
${HACKRF_PATH}/firmware/hackrf_usb/usb_device.c
|
||||
${HACKRF_PATH}/firmware/hackrf_usb/usb_endpoint.c
|
||||
${HACKRF_PATH}/firmware/common/usb_request.c
|
||||
${HACKRF_PATH}/firmware/common/usb_standard_request.c
|
||||
${HACKRF_PATH}/firmware/common/platform_detect.c
|
||||
${HACKRF_PATH}/firmware/common/gpio_lpc.c
|
||||
${HACKRF_PATH}/firmware/common/firmware_info.c
|
||||
${HACKRF_PATH}/firmware/common/si5351c.c
|
||||
${HACKRF_PATH}/firmware/common/i2c_bus.c
|
||||
${HACKRF_PATH}/firmware/common/mixer.c
|
||||
${HACKRF_PATH}/firmware/common/clkin.c
|
||||
${HACKRF_PATH}/firmware/common/spi_bus.c
|
||||
${HACKRF_PATH}/firmware/common/sgpio.c
|
||||
${HACKRF_PATH}/firmware/common/rf_path.c
|
||||
${HACKRF_PATH}/firmware/common/i2c_lpc.c
|
||||
${HACKRF_PATH}/firmware/common/spi_ssp.c
|
||||
${HACKRF_PATH}/firmware/common/rffc5071_spi.c
|
||||
${HACKRF_PATH}/firmware/common/rffc5071.c
|
||||
${HACKRF_PATH}/firmware/common/clkin.c
|
||||
${HACKRF_PATH}/firmware/common/gpdma.c
|
||||
|
||||
${HACKRF_PATH}/firmware/libopencm3/lib/cm3/nvic.c
|
||||
${HACKRF_PATH}/firmware/libopencm3/lib/cm3/sync.c
|
||||
${HACKRF_PATH}/firmware/libopencm3/lib/lpc43xx/scu.c
|
||||
${HACKRF_PATH}/firmware/libopencm3/lib/lpc43xx/timer.c
|
||||
${HACKRF_PATH}/firmware/libopencm3/lib/lpc43xx/i2c.c
|
||||
)
|
||||
DeclareTargets(PUSB sd_over_usb)
|
||||
|
||||
|
||||
### HackRF "factory" firmware
|
||||
|
||||
|
||||
@@ -161,8 +161,6 @@ class FProtoSubCarFiatV0 : public FProtoSubCarBase {
|
||||
}
|
||||
|
||||
if (bit_count > 0x46) {
|
||||
final_count = bit_count;
|
||||
|
||||
endbyte = (uint8_t)data_low;
|
||||
/*
|
||||
generic.data = ((uint64_t)hop << 32) | fix;
|
||||
@@ -192,13 +190,11 @@ class FProtoSubCarFiatV0 : public FProtoSubCarBase {
|
||||
|
||||
ManchesterState manchester_state = ManchesterStateMid1;
|
||||
uint8_t decoder_state = 0;
|
||||
uint8_t bit_count = 0;
|
||||
uint8_t endbyte = 0;
|
||||
uint16_t preamble_count = 0;
|
||||
uint32_t data_low = 0;
|
||||
uint32_t data_high = 0;
|
||||
uint8_t bit_count = 0;
|
||||
uint32_t hop = 0;
|
||||
uint32_t fix = 0;
|
||||
uint8_t endbyte = 0;
|
||||
uint8_t final_count = 0;
|
||||
uint32_t te_last = 0;
|
||||
};
|
||||
|
||||
@@ -59,20 +59,26 @@ class FProtoSubCarKiaV0 : public FProtoSubCarBase {
|
||||
break;
|
||||
case KIADecoderStepSaveDuration:
|
||||
if (level) {
|
||||
if (duration >=
|
||||
(te_long + te_delta * 2UL)) {
|
||||
// Signal ended too early!
|
||||
// FURI_LOG_W(TAG, "Signal ended at %u bits (expected 61). Duration: %lu", decode_count_bit, duration);
|
||||
|
||||
if (duration >= (te_long + te_delta * 2UL)) {
|
||||
// End of transmission detected
|
||||
parser_step = KIADecoderStepReset;
|
||||
|
||||
if (decode_count_bit == min_count_bit_for_found) {
|
||||
// instance->generic.data = decode_data;
|
||||
// data = decode_data;
|
||||
data_count_bit = decode_count_bit;
|
||||
// just used for log yet, so skip
|
||||
// if (kia_verify_crc()) {
|
||||
// FURI_LOG_I(TAG, "Valid signal received with correct CRC");
|
||||
// } else {
|
||||
// FURI_LOG_W(TAG, "Signal received but CRC mismatch!");
|
||||
// }
|
||||
|
||||
if (callback)
|
||||
callback(this);
|
||||
} else {
|
||||
// FURI_LOG_E(TAG, "Incomplete signal: only %u bits", decode_count_bit);
|
||||
// FURI_LOG_E(TAG, "Incomplete signal: only %u bits", instance->decoder.decode_count_bit);
|
||||
}
|
||||
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
break;
|
||||
@@ -80,26 +86,19 @@ class FProtoSubCarKiaV0 : public FProtoSubCarBase {
|
||||
te_last = duration;
|
||||
parser_step = KIADecoderStepCheckDuration;
|
||||
}
|
||||
|
||||
} else {
|
||||
parser_step = KIADecoderStepReset;
|
||||
}
|
||||
break;
|
||||
case KIADecoderStepCheckDuration:
|
||||
if (!level) {
|
||||
if ((DURATION_DIFF(te_last, te_short) < te_delta) &&
|
||||
(DURATION_DIFF(duration, te_short) < te_delta)) {
|
||||
if ((DURATION_DIFF(te_last, te_short) < te_delta) && (DURATION_DIFF(duration, te_short) < te_delta)) {
|
||||
subghz_protocol_blocks_add_bit(0);
|
||||
if (decode_count_bit % 10 == 0) {
|
||||
// FURI_LOG_D(TAG, "Decoded %u bits so far", decode_count_bit);
|
||||
}
|
||||
parser_step = KIADecoderStepSaveDuration;
|
||||
} else if (
|
||||
(DURATION_DIFF(te_last, te_long) < te_delta) &&
|
||||
(DURATION_DIFF(duration, te_long) < te_delta)) {
|
||||
(DURATION_DIFF(te_last, te_long) < te_delta) && (DURATION_DIFF(duration, te_long) < te_delta)) {
|
||||
subghz_protocol_blocks_add_bit(1);
|
||||
if (decode_count_bit % 10 == 0) {
|
||||
// FURI_LOG_D(TAG, "Decoded %u bits so far", decode_count_bit);
|
||||
}
|
||||
parser_step = KIADecoderStepSaveDuration;
|
||||
} else {
|
||||
// FURI_LOG_W(TAG, "Timing mismatch at bit %u. Last: %lu, Current: %lu", decode_count_bit, te_last, duration);
|
||||
|
||||
@@ -5,8 +5,7 @@
|
||||
typedef enum {
|
||||
KiaV1DecoderStepReset = 0,
|
||||
KiaV1DecoderStepCheckPreamble,
|
||||
KiaV1DecoderStepFoundShortLow,
|
||||
KiaV1DecoderStepCollectRawBits,
|
||||
KiaV1DecoderStepDecodeData,
|
||||
} KiaV1DecoderStep;
|
||||
|
||||
class FProtoSubCarKiaV1 : public FProtoSubCarBase {
|
||||
@@ -16,177 +15,74 @@ class FProtoSubCarKiaV1 : public FProtoSubCarBase {
|
||||
te_short = 800;
|
||||
te_long = 1600;
|
||||
te_delta = 200;
|
||||
min_count_bit_for_found = 56;
|
||||
}
|
||||
void kia_v1_add_raw_bit(bool bit) {
|
||||
if (raw_bit_count < 192) {
|
||||
uint16_t byte_idx = raw_bit_count / 8;
|
||||
uint8_t bit_idx = 7 - (raw_bit_count % 8);
|
||||
if (bit) {
|
||||
raw_bits[byte_idx] |= (1 << bit_idx);
|
||||
} else {
|
||||
raw_bits[byte_idx] &= ~(1 << bit_idx);
|
||||
}
|
||||
raw_bit_count++;
|
||||
}
|
||||
}
|
||||
inline bool kia_v1_get_raw_bit(uint16_t idx) {
|
||||
uint16_t byte_idx = idx / 8;
|
||||
uint8_t bit_idx = 7 - (idx % 8);
|
||||
return (raw_bits[byte_idx] >> bit_idx) & 1;
|
||||
}
|
||||
bool kia_v1_manchester_decode() {
|
||||
if (raw_bit_count < 113) {
|
||||
// FURI_LOG_D(TAG, "Not enough raw bits: %u", raw_bit_count);
|
||||
return false;
|
||||
}
|
||||
// Try different offsets to find best alignment (RTL-433 uses -1 bit offset)
|
||||
uint16_t best_bits = 0;
|
||||
uint64_t best_data = 0;
|
||||
// uint16_t best_offset = 0;
|
||||
|
||||
for (uint16_t offset = 0; offset < 8; offset++) {
|
||||
uint64_t data = 0;
|
||||
uint16_t decoded_bits = 0;
|
||||
|
||||
for (uint16_t i = offset; i + 1 < raw_bit_count && decoded_bits < 56; i += 2) {
|
||||
bool bit1 = kia_v1_get_raw_bit(i);
|
||||
bool bit2 = kia_v1_get_raw_bit(i + 1);
|
||||
|
||||
uint8_t two_bits = (bit1 << 1) | bit2;
|
||||
|
||||
// V1 uses: 10=1, 01=0
|
||||
if (two_bits == 0x02) { // 10 = decoded 1
|
||||
data = (data << 1) | 1;
|
||||
decoded_bits++;
|
||||
} else if (two_bits == 0x01) { // 01 = decoded 0
|
||||
data = (data << 1);
|
||||
decoded_bits++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (decoded_bits > best_bits) {
|
||||
best_bits = decoded_bits;
|
||||
best_data = data;
|
||||
// best_offset = offset;
|
||||
}
|
||||
}
|
||||
|
||||
// FURI_LOG_I(TAG, "Best: offset=%u bits=%u data=%014llX", best_offset, best_bits, best_data);
|
||||
|
||||
decode_data = best_data;
|
||||
decode_count_bit = best_bits;
|
||||
|
||||
return best_bits >= min_count_bit_for_found;
|
||||
min_count_bit_for_found = 57;
|
||||
}
|
||||
|
||||
void feed(bool level, uint32_t duration) {
|
||||
ManchesterEvent event = ManchesterEventReset;
|
||||
|
||||
switch (parser_step) {
|
||||
case KiaV1DecoderStepReset:
|
||||
// Preamble 0xCCCCCCCD produces alternating LONG pulses
|
||||
if ((level) && (DURATION_DIFF(duration, te_long) <
|
||||
te_delta)) {
|
||||
if ((level) && (DURATION_DIFF(duration, te_long) < te_delta)) {
|
||||
parser_step = KiaV1DecoderStepCheckPreamble;
|
||||
te_last = duration;
|
||||
header_count = 1;
|
||||
header_count = 0;
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
FProtoGeneral::manchester_advance(manchester_saved_state, ManchesterEventReset, &manchester_saved_state, NULL);
|
||||
}
|
||||
break;
|
||||
|
||||
case KiaV1DecoderStepCheckPreamble:
|
||||
if (level) {
|
||||
if (DURATION_DIFF(duration, te_long) <
|
||||
te_delta) {
|
||||
te_last = duration;
|
||||
if (!level) {
|
||||
if ((DURATION_DIFF(duration, te_long) < te_delta) && (DURATION_DIFF(te_last, te_long) < te_delta)) {
|
||||
header_count++;
|
||||
} else if (
|
||||
DURATION_DIFF(duration, te_short) <
|
||||
te_delta) {
|
||||
te_last = duration;
|
||||
} else {
|
||||
parser_step = KiaV1DecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
// LOW pulse
|
||||
if (DURATION_DIFF(duration, te_long) <
|
||||
te_delta) {
|
||||
header_count++;
|
||||
} else if (
|
||||
DURATION_DIFF(duration, te_short) <
|
||||
te_delta) {
|
||||
// Short LOW - this is the start of sync (0xCD ends: ...long H, short L, short H)
|
||||
if (header_count > 12) {
|
||||
parser_step = KiaV1DecoderStepFoundShortLow;
|
||||
}
|
||||
} else {
|
||||
parser_step = KiaV1DecoderStepReset;
|
||||
}
|
||||
if (header_count > 70) {
|
||||
if ((!level) && (DURATION_DIFF(duration, te_short) < te_delta) && (DURATION_DIFF(te_last, te_long) < te_delta)) {
|
||||
decode_count_bit = 1;
|
||||
subghz_protocol_blocks_add_bit(1);
|
||||
header_count = 0;
|
||||
parser_step = KiaV1DecoderStepDecodeData;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case KiaV1DecoderStepFoundShortLow:
|
||||
// Expecting SHORT HIGH to complete sync
|
||||
if (level && (DURATION_DIFF(duration, te_short) <
|
||||
te_delta)) {
|
||||
// FURI_LOG_I(TAG, "Sync! hdr=%u", header_count);
|
||||
parser_step = KiaV1DecoderStepCollectRawBits;
|
||||
raw_bit_count = 0;
|
||||
memset(raw_bits, 0, sizeof(raw_bits));
|
||||
// Add the sync short HIGH as first raw bit
|
||||
kia_v1_add_raw_bit(true);
|
||||
} else {
|
||||
parser_step = KiaV1DecoderStepReset;
|
||||
case KiaV1DecoderStepDecodeData:
|
||||
if ((DURATION_DIFF(duration, te_short) < te_delta)) {
|
||||
event = level ? ManchesterEventShortLow : ManchesterEventShortHigh;
|
||||
} else if ((DURATION_DIFF(duration, te_long) <
|
||||
te_delta)) {
|
||||
event = level ? ManchesterEventLongLow : ManchesterEventLongHigh;
|
||||
}
|
||||
break;
|
||||
|
||||
case KiaV1DecoderStepCollectRawBits:
|
||||
if (duration > 2400) {
|
||||
// FURI_LOG_I(TAG, "End! raw_bits=%u", raw_bit_count);
|
||||
|
||||
if (kia_v1_manchester_decode()) {
|
||||
// instance->generic.data = decode_data;
|
||||
data_count_bit = raw_bit_count / 8;
|
||||
|
||||
// Extract fields from 56-bit data per RTL-433:
|
||||
// Serial: bits 55-24 (32 bits)
|
||||
// Btn: bits 23-16 (8 bits)
|
||||
// Count: bits 15-8 (8 bits)
|
||||
// CRC: bits 7-0 (8 bits)
|
||||
// instance->generic.serial = (uint32_t)((instance->generic.data >> 24) & 0xFFFFFFFF);
|
||||
// instance->generic.btn = (uint8_t)((instance->generic.data >> 16) & 0xFF);
|
||||
// instance->generic.cnt = (uint8_t)((instance->generic.data >> 8) & 0xFF);
|
||||
|
||||
if (callback)
|
||||
callback(this);
|
||||
if (event != ManchesterEventReset) {
|
||||
bool data;
|
||||
bool data_ok = FProtoGeneral::manchester_advance(manchester_saved_state, event, &manchester_saved_state, &data);
|
||||
if (data_ok) {
|
||||
decode_data = (decode_data << 1) | data;
|
||||
decode_count_bit++;
|
||||
}
|
||||
}
|
||||
|
||||
if (decode_count_bit == min_count_bit_for_found) {
|
||||
// instance->generic.data = decode_data;
|
||||
data_count_bit = decode_count_bit;
|
||||
if (callback)
|
||||
callback(this);
|
||||
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
parser_step = KiaV1DecoderStepReset;
|
||||
break;
|
||||
}
|
||||
|
||||
int num_bits = 0;
|
||||
if (DURATION_DIFF(duration, te_short) <
|
||||
te_delta) {
|
||||
num_bits = 1;
|
||||
} else if (
|
||||
DURATION_DIFF(duration, te_long) <
|
||||
te_delta) {
|
||||
num_bits = 2;
|
||||
} else {
|
||||
parser_step = KiaV1DecoderStepReset;
|
||||
break;
|
||||
}
|
||||
|
||||
for (int i = 0; i < num_bits; i++) {
|
||||
kia_v1_add_raw_bit(level);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t raw_bits[24]{0};
|
||||
uint16_t raw_bit_count = 0;
|
||||
uint16_t header_count = 0;
|
||||
uint8_t header_count = 0;
|
||||
ManchesterState manchester_saved_state = ManchesterStateStart1;
|
||||
};
|
||||
|
||||
@@ -15,65 +15,7 @@ class FProtoSubCarKiaV2 : public FProtoSubCarBase {
|
||||
te_short = 500;
|
||||
te_long = 1000;
|
||||
te_delta = 160;
|
||||
min_count_bit_for_found = 51;
|
||||
}
|
||||
|
||||
void kia_v2_add_raw_bit(bool bit) {
|
||||
if (raw_bit_count < 160) {
|
||||
uint16_t byte_idx = raw_bit_count / 8;
|
||||
uint8_t bit_idx = 7 - (raw_bit_count % 8);
|
||||
if (bit) {
|
||||
raw_bits[byte_idx] |= (1 << bit_idx);
|
||||
} else {
|
||||
raw_bits[byte_idx] &= ~(1 << bit_idx);
|
||||
}
|
||||
raw_bit_count++;
|
||||
}
|
||||
}
|
||||
inline bool kia_v2_get_raw_bit(uint16_t idx) {
|
||||
uint16_t byte_idx = idx / 8;
|
||||
uint8_t bit_idx = 7 - (idx % 8);
|
||||
return (raw_bits[byte_idx] >> bit_idx) & 1;
|
||||
}
|
||||
bool kia_v2_manchester_decode() {
|
||||
if (raw_bit_count < 100) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint16_t best_bits = 0;
|
||||
uint64_t best_data = 0;
|
||||
|
||||
for (uint16_t offset = 0; offset < 8; offset++) {
|
||||
uint64_t data = 0;
|
||||
uint16_t decoded_bits = 0;
|
||||
|
||||
for (uint16_t i = offset; i + 1 < raw_bit_count && decoded_bits < 53; i += 2) {
|
||||
bool bit1 = kia_v2_get_raw_bit(i);
|
||||
bool bit2 = kia_v2_get_raw_bit(i + 1);
|
||||
|
||||
uint8_t two_bits = (bit1 << 1) | bit2;
|
||||
|
||||
if (two_bits == 0x02) {
|
||||
data = (data << 1) | 1;
|
||||
decoded_bits++;
|
||||
} else if (two_bits == 0x01) {
|
||||
data = (data << 1);
|
||||
decoded_bits++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (decoded_bits > best_bits) {
|
||||
best_bits = decoded_bits;
|
||||
best_data = data;
|
||||
}
|
||||
}
|
||||
|
||||
decode_data = best_data;
|
||||
decode_count_bit = best_bits;
|
||||
|
||||
return best_bits >= min_count_bit_for_found;
|
||||
min_count_bit_for_found = 53;
|
||||
}
|
||||
|
||||
void feed(bool level, uint32_t duration) {
|
||||
@@ -82,86 +24,85 @@ class FProtoSubCarKiaV2 : public FProtoSubCarBase {
|
||||
if ((level) && (DURATION_DIFF(duration, te_long) < te_delta)) {
|
||||
parser_step = KiaV2DecoderStepCheckPreamble;
|
||||
te_last = duration;
|
||||
header_count = 1;
|
||||
header_count = 0;
|
||||
FProtoGeneral::manchester_advance(manchester_state, ManchesterEventReset, &manchester_state, NULL);
|
||||
}
|
||||
break;
|
||||
|
||||
case KiaV2DecoderStepCheckPreamble:
|
||||
if (level) {
|
||||
if (DURATION_DIFF(duration, te_long) <
|
||||
te_delta) {
|
||||
if (level) // HIGH pulse
|
||||
{
|
||||
if (DURATION_DIFF(duration, te_long) < te_delta) {
|
||||
te_last = duration;
|
||||
header_count++;
|
||||
} else if (
|
||||
DURATION_DIFF(duration, te_short) <
|
||||
te_delta) {
|
||||
te_last = duration;
|
||||
} else {
|
||||
parser_step = KiaV2DecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
if (DURATION_DIFF(duration, te_long) <
|
||||
te_delta) {
|
||||
header_count++;
|
||||
} else if (
|
||||
DURATION_DIFF(duration, te_short) <
|
||||
te_delta) {
|
||||
if (header_count > 10 &&
|
||||
DURATION_DIFF(te_last, te_short) <
|
||||
te_delta) {
|
||||
DURATION_DIFF(duration, te_short) < te_delta) {
|
||||
if (header_count >= 100) {
|
||||
header_count = 0;
|
||||
decode_data = 0;
|
||||
decode_count_bit = 1;
|
||||
parser_step = KiaV2DecoderStepCollectRawBits;
|
||||
raw_bit_count = 0;
|
||||
memset(raw_bits, 0, sizeof(raw_bits));
|
||||
subghz_protocol_blocks_add_bit(1);
|
||||
} else {
|
||||
te_last = duration;
|
||||
}
|
||||
} else {
|
||||
parser_step = KiaV2DecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
if (DURATION_DIFF(duration, te_long) < te_delta) {
|
||||
header_count++;
|
||||
te_last = duration;
|
||||
} else if (
|
||||
DURATION_DIFF(duration, te_short) < te_delta) {
|
||||
te_last = duration;
|
||||
} else {
|
||||
parser_step = KiaV2DecoderStepReset;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case KiaV2DecoderStepCollectRawBits:
|
||||
if (duration > 1500) {
|
||||
if (kia_v2_manchester_decode()) {
|
||||
/*data = decode_data;
|
||||
data_count_bit = decode_count_bit;
|
||||
case KiaV2DecoderStepCollectRawBits: {
|
||||
ManchesterEvent event;
|
||||
|
||||
serial = (uint32_t)((data >> 20) & 0xFFFFFFFF);
|
||||
btn = (uint8_t)((data >> 16) & 0x0F);
|
||||
|
||||
uint16_t raw_count = (uint16_t)((data >> 4) & 0xFFF);
|
||||
cnt = ((raw_count >> 4) | (raw_count << 8)) & 0xFFF;
|
||||
*/
|
||||
data_count_bit = decode_count_bit;
|
||||
if (callback)
|
||||
callback(this);
|
||||
}
|
||||
|
||||
parser_step = KiaV2DecoderStepReset;
|
||||
break;
|
||||
}
|
||||
|
||||
int num_bits = 0;
|
||||
if (DURATION_DIFF(duration, te_short) <
|
||||
te_delta) {
|
||||
num_bits = 1;
|
||||
if (DURATION_DIFF(duration, te_short) < te_delta) {
|
||||
event = level ? ManchesterEventShortLow : ManchesterEventShortHigh;
|
||||
} else if (
|
||||
DURATION_DIFF(duration, te_long) <
|
||||
te_delta) {
|
||||
num_bits = 2;
|
||||
DURATION_DIFF(duration, te_long) < te_delta) {
|
||||
event = level ? ManchesterEventLongLow : ManchesterEventLongHigh;
|
||||
} else {
|
||||
parser_step = KiaV2DecoderStepReset;
|
||||
break;
|
||||
}
|
||||
|
||||
for (int i = 0; i < num_bits; i++) {
|
||||
kia_v2_add_raw_bit(level);
|
||||
}
|
||||
bool data_bit;
|
||||
if (FProtoGeneral::manchester_advance(manchester_state, event, &manchester_state, &data_bit)) {
|
||||
decode_data = (decode_data << 1) | data_bit;
|
||||
decode_count_bit++;
|
||||
|
||||
if (decode_count_bit == 53) {
|
||||
// instance->generic.data = decode_data;
|
||||
data_count_bit = decode_count_bit;
|
||||
|
||||
// instance->generic.serial = (uint32_t)((instance->generic.data >> 20) & 0xFFFFFFFF);
|
||||
// instance->generic.btn = (uint8_t)((instance->generic.data >> 16) & 0x0F);
|
||||
|
||||
// uint16_t raw_count = (uint16_t)((instance->generic.data >> 4) & 0xFFF);
|
||||
// instance->generic.cnt = ((raw_count >> 4) | (raw_count << 8)) & 0xFFF;
|
||||
|
||||
if (callback)
|
||||
callback(this);
|
||||
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
header_count = 0;
|
||||
parser_step = KiaV2DecoderStepReset;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t raw_bits[20]{0};
|
||||
uint16_t raw_bit_count = 0;
|
||||
uint16_t header_count = 0;
|
||||
ManchesterState manchester_state = ManchesterStateMid1;
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ class FProtoSubCarKiaV3V4 : public FProtoSubCarBase {
|
||||
te_short = 400;
|
||||
te_long = 800;
|
||||
te_delta = 150;
|
||||
min_count_bit_for_found = 64;
|
||||
min_count_bit_for_found = 68;
|
||||
}
|
||||
uint8_t reverse8(uint8_t byte) {
|
||||
byte = (byte & 0xF0) >> 4 | (byte & 0x0F) << 4;
|
||||
@@ -36,7 +36,7 @@ class FProtoSubCarKiaV3V4 : public FProtoSubCarBase {
|
||||
}
|
||||
}
|
||||
bool kia_v3_v4_process_buffer() {
|
||||
if (raw_bit_count < 64) {
|
||||
if (raw_bit_count < 68) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ class FProtoSubCarKiaV3V4 : public FProtoSubCarBase {
|
||||
|
||||
uint8_t btn = (reverse8(b[7]) & 0xF0) >> 4;
|
||||
decode_data = serial;
|
||||
decode_count_bit = 64;
|
||||
decode_count_bit = 68;
|
||||
decode_data2 = btn;
|
||||
data_count_bit = decode_count_bit;
|
||||
if (callback)
|
||||
@@ -93,8 +93,7 @@ class FProtoSubCarKiaV3V4 : public FProtoSubCarBase {
|
||||
void feed(bool level, uint32_t duration) {
|
||||
switch (parser_step) {
|
||||
case KiaV3V4DecoderStepReset:
|
||||
if (level && DURATION_DIFF(duration, te_short) <
|
||||
te_delta) {
|
||||
if (level && DURATION_DIFF(duration, te_short) < te_delta) {
|
||||
parser_step = KiaV3V4DecoderStepCheckPreamble;
|
||||
te_last = duration;
|
||||
header_count = 1;
|
||||
@@ -103,8 +102,7 @@ class FProtoSubCarKiaV3V4 : public FProtoSubCarBase {
|
||||
|
||||
case KiaV3V4DecoderStepCheckPreamble:
|
||||
if (level) {
|
||||
if (DURATION_DIFF(duration, te_short) <
|
||||
te_delta) {
|
||||
if (DURATION_DIFF(duration, te_short) < te_delta) {
|
||||
te_last = duration;
|
||||
} else if (duration > 1000 && duration < 1500) {
|
||||
// V4 style: Sync is LONG HIGH
|
||||
@@ -130,11 +128,7 @@ class FProtoSubCarKiaV3V4 : public FProtoSubCarBase {
|
||||
} else {
|
||||
parser_step = KiaV3V4DecoderStepReset;
|
||||
}
|
||||
} else if (
|
||||
DURATION_DIFF(duration, te_short) <
|
||||
te_delta &&
|
||||
DURATION_DIFF(te_last, te_short) <
|
||||
te_delta) {
|
||||
} else if (DURATION_DIFF(duration, te_short) < te_delta && DURATION_DIFF(te_last, te_short) < te_delta) {
|
||||
header_count++;
|
||||
} else if (duration > 1500) {
|
||||
parser_step = KiaV3V4DecoderStepReset;
|
||||
@@ -149,12 +143,10 @@ class FProtoSubCarKiaV3V4 : public FProtoSubCarBase {
|
||||
kia_v3_v4_process_buffer();
|
||||
parser_step = KiaV3V4DecoderStepReset;
|
||||
} else if (
|
||||
DURATION_DIFF(duration, te_short) <
|
||||
te_delta) {
|
||||
DURATION_DIFF(duration, te_short) < te_delta) {
|
||||
kia_v3_v4_add_raw_bit(false);
|
||||
} else if (
|
||||
DURATION_DIFF(duration, te_long) <
|
||||
te_delta) {
|
||||
DURATION_DIFF(duration, te_long) < te_delta) {
|
||||
kia_v3_v4_add_raw_bit(true);
|
||||
} else {
|
||||
parser_step = KiaV3V4DecoderStepReset;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
#pragma once
|
||||
#include "subcarbase.hpp"
|
||||
#include <cstring>
|
||||
|
||||
typedef enum {
|
||||
KiaV5DecoderStepReset = 0,
|
||||
KiaV5DecoderStepCheckPreamble,
|
||||
KiaV5DecoderStepCollectRawBits,
|
||||
KiaV5DecoderStepData,
|
||||
} KiaV5DecoderStep;
|
||||
|
||||
class FProtoSubCarKiaV5 : public FProtoSubCarBase {
|
||||
@@ -17,74 +18,37 @@ class FProtoSubCarKiaV5 : public FProtoSubCarBase {
|
||||
min_count_bit_for_found = 64;
|
||||
}
|
||||
|
||||
inline bool kia_v5_get_raw_bit(uint16_t idx) {
|
||||
uint16_t byte_idx = idx / 8;
|
||||
uint8_t bit_idx = 7 - (idx % 8);
|
||||
return (raw_bits[byte_idx] >> bit_idx) & 1;
|
||||
}
|
||||
|
||||
void kia_v5_add_raw_bit(bool bit) {
|
||||
if (raw_bit_count < 256) {
|
||||
uint16_t byte_idx = raw_bit_count / 8;
|
||||
uint8_t bit_idx = 7 - (raw_bit_count % 8);
|
||||
if (bit) {
|
||||
raw_bits[byte_idx] |= (1 << bit_idx);
|
||||
} else {
|
||||
raw_bits[byte_idx] &= ~(1 << bit_idx);
|
||||
}
|
||||
raw_bit_count++;
|
||||
}
|
||||
}
|
||||
|
||||
bool kia_v5_manchester_decode() {
|
||||
if (raw_bit_count < 130) {
|
||||
return false;
|
||||
}
|
||||
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
|
||||
// Start at offset 2 for proper Manchester alignment
|
||||
const uint16_t start_bit = 2;
|
||||
|
||||
for (uint16_t i = start_bit;
|
||||
i + 1 < raw_bit_count && decode_count_bit < 64;
|
||||
i += 2) {
|
||||
bool bit1 = kia_v5_get_raw_bit(i);
|
||||
bool bit2 = kia_v5_get_raw_bit(i + 1);
|
||||
|
||||
uint8_t two_bits = (bit1 << 1) | bit2;
|
||||
|
||||
if (two_bits == 0x01) { // 01 = decoded 1
|
||||
decode_data = (decode_data << 1) | 1;
|
||||
decode_count_bit++;
|
||||
} else if (two_bits == 0x02) { // 10 = decoded 0
|
||||
decode_data = (decode_data << 1);
|
||||
decode_count_bit++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return decode_count_bit >= min_count_bit_for_found;
|
||||
void kia_v5_add_bit(bool bit) {
|
||||
decode_data = (decode_data << 1) | (bit ? 1 : 0);
|
||||
decode_count_bit++;
|
||||
}
|
||||
|
||||
void feed(bool level, uint32_t duration) {
|
||||
switch (parser_step) {
|
||||
case KiaV5DecoderStepReset:
|
||||
if ((level) && (DURATION_DIFF(duration, te_short) <
|
||||
te_delta)) {
|
||||
if ((level) && (DURATION_DIFF(duration, te_short) < te_delta)) {
|
||||
parser_step = KiaV5DecoderStepCheckPreamble;
|
||||
te_last = duration;
|
||||
header_count = 1;
|
||||
decode_count_bit = 0;
|
||||
decode_data = 0;
|
||||
FProtoGeneral::manchester_advance(manchester_state, ManchesterEventReset, &manchester_state, NULL);
|
||||
}
|
||||
break;
|
||||
|
||||
case KiaV5DecoderStepCheckPreamble:
|
||||
if (level) {
|
||||
if ((DURATION_DIFF(duration, te_short) <
|
||||
te_delta) ||
|
||||
(DURATION_DIFF(duration, te_long) <
|
||||
te_delta)) {
|
||||
if (DURATION_DIFF(duration, te_long) < te_delta) {
|
||||
if (header_count > 40) {
|
||||
parser_step = KiaV5DecoderStepData;
|
||||
decode_count_bit = 0;
|
||||
decode_data = 0;
|
||||
decode_data2 = 0;
|
||||
header_count = 0;
|
||||
} else {
|
||||
te_last = duration;
|
||||
}
|
||||
} else if (DURATION_DIFF(duration, te_short) < te_delta) {
|
||||
te_last = duration;
|
||||
} else {
|
||||
parser_step = KiaV5DecoderStepReset;
|
||||
@@ -100,13 +64,7 @@ class FProtoSubCarKiaV5 : public FProtoSubCarBase {
|
||||
te_delta) &&
|
||||
(DURATION_DIFF(te_last, te_short) <
|
||||
te_delta)) {
|
||||
if (header_count > 40) {
|
||||
parser_step = KiaV5DecoderStepCollectRawBits;
|
||||
raw_bit_count = 0;
|
||||
memset(raw_bits, 0, sizeof(raw_bits));
|
||||
} else {
|
||||
header_count++;
|
||||
}
|
||||
header_count++;
|
||||
} else if (
|
||||
DURATION_DIFF(te_last, te_long) <
|
||||
te_delta) {
|
||||
@@ -114,33 +72,21 @@ class FProtoSubCarKiaV5 : public FProtoSubCarBase {
|
||||
} else {
|
||||
parser_step = KiaV5DecoderStepReset;
|
||||
}
|
||||
te_last = duration;
|
||||
}
|
||||
break;
|
||||
|
||||
case KiaV5DecoderStepCollectRawBits:
|
||||
if (duration > 1200) {
|
||||
if (kia_v5_manchester_decode()) {
|
||||
// generic.data = decode_data;
|
||||
// generic.data_count_bit = decode_count_bit;
|
||||
data_count_bit = decode_count_bit;
|
||||
// Compute yek (bit-reverse each byte)
|
||||
uint64_t yek = 0;
|
||||
for (int i = 0; i < 8; i++) {
|
||||
uint8_t byte = (decode_data >> (i * 8)) & 0xFF;
|
||||
uint8_t reversed = 0;
|
||||
for (int b = 0; b < 8; b++) {
|
||||
if (byte & (1 << b))
|
||||
reversed |= (1 << (7 - b));
|
||||
}
|
||||
yek |= ((uint64_t)reversed << ((7 - i) * 8));
|
||||
}
|
||||
decode_data = yek;
|
||||
|
||||
// Shift serial right by 1 to correct alignment
|
||||
// generic.serial = (uint32_t)(((yek >> 32) & 0x0FFFFFFF) >> 1);
|
||||
// generic.btn = (uint8_t)((yek >> 61) & 0x07); // Shift btn too
|
||||
// generic.cnt = (uint16_t)(yek & 0xFFFF);
|
||||
case KiaV5DecoderStepData: {
|
||||
ManchesterEvent event;
|
||||
|
||||
if (DURATION_DIFF(duration, te_short) < te_delta) {
|
||||
event = level ? ManchesterEventShortHigh : ManchesterEventShortLow;
|
||||
} else if (DURATION_DIFF(duration, te_long) < te_delta) {
|
||||
event = level ? ManchesterEventLongHigh : ManchesterEventLongLow;
|
||||
} else {
|
||||
if (decode_count_bit >= min_count_bit_for_found) {
|
||||
// instance->generic.data = instance->decode_data2;
|
||||
data_count_bit = (decode_count_bit > 67) ? 67 : decode_count_bit;
|
||||
if (callback)
|
||||
callback(this);
|
||||
}
|
||||
@@ -149,28 +95,22 @@ class FProtoSubCarKiaV5 : public FProtoSubCarBase {
|
||||
break;
|
||||
}
|
||||
|
||||
int num_bits = 0;
|
||||
if (DURATION_DIFF(duration, te_short) <
|
||||
te_delta) {
|
||||
num_bits = 1;
|
||||
} else if (
|
||||
DURATION_DIFF(duration, te_long) <
|
||||
te_delta) {
|
||||
num_bits = 2;
|
||||
} else {
|
||||
parser_step = KiaV5DecoderStepReset;
|
||||
break;
|
||||
}
|
||||
|
||||
for (int i = 0; i < num_bits; i++) {
|
||||
kia_v5_add_raw_bit(level);
|
||||
bool data_bit;
|
||||
if (decode_count_bit <= 66 && FProtoGeneral::manchester_advance(manchester_state, event, &manchester_state, &data_bit)) {
|
||||
kia_v5_add_bit(data_bit);
|
||||
|
||||
if (decode_count_bit == 64) {
|
||||
decode_data2 = decode_data;
|
||||
decode_data = 0;
|
||||
}
|
||||
}
|
||||
|
||||
te_last = duration;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t raw_bits[32]{};
|
||||
uint16_t raw_bit_count = 0;
|
||||
uint16_t header_count = 0;
|
||||
ManchesterState manchester_state = ManchesterStateMid1;
|
||||
};
|
||||
|
||||
@@ -20,6 +20,7 @@ class FProtoSubCarSubaru : public FProtoSubCarBase {
|
||||
te_delta = 260;
|
||||
min_count_bit_for_found = 64;
|
||||
}
|
||||
|
||||
void subghz_protocol_decoder_subaru_reset() {
|
||||
parser_step = SubaruDecoderStepReset;
|
||||
te_last = 0;
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
|
||||
typedef enum {
|
||||
SuzukiDecoderStepReset = 0,
|
||||
SuzukiDecoderStepFoundStartPulse,
|
||||
SuzukiDecoderStepSaveDuration,
|
||||
SuzukiDecoderStepCountPreamble = 1,
|
||||
SuzukiDecoderStepDecodeData = 2,
|
||||
} SuzukiDecoderStep;
|
||||
|
||||
class FProtoSubCarSuzuki : public FProtoSubCarBase {
|
||||
@@ -20,54 +20,45 @@ class FProtoSubCarSuzuki : public FProtoSubCarBase {
|
||||
min_count_bit_for_found = 64;
|
||||
}
|
||||
void suzuki_add_bit(uint32_t bit) {
|
||||
uint32_t carry = data_low >> 31;
|
||||
data_low = (data_low << 1) | bit;
|
||||
data_high = (data_high << 1) | carry;
|
||||
data_count_bit++;
|
||||
decode_data = (decode_data << 1) | bit;
|
||||
decode_count_bit++;
|
||||
}
|
||||
void subghz_protocol_decoder_suzuki_reset() {
|
||||
parser_step = SuzukiDecoderStepReset;
|
||||
header_count = 0;
|
||||
data_count_bit = 0;
|
||||
data_low = 0;
|
||||
data_high = 0;
|
||||
decode_data = 0;
|
||||
}
|
||||
|
||||
void feed(bool level, uint32_t duration) {
|
||||
switch (parser_step) {
|
||||
case SuzukiDecoderStepReset:
|
||||
// Wait for short HIGH pulse (~250µs) to start preamble
|
||||
if (!level)
|
||||
// Wait for HIGH pulse (~250µs) to start preamble
|
||||
if (!level) {
|
||||
return;
|
||||
|
||||
}
|
||||
if (DURATION_DIFF(duration, te_short) > te_delta) {
|
||||
return;
|
||||
}
|
||||
|
||||
data_low = 0;
|
||||
data_high = 0;
|
||||
parser_step = SuzukiDecoderStepFoundStartPulse;
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
parser_step = SuzukiDecoderStepCountPreamble;
|
||||
header_count = 0;
|
||||
data_count_bit = 0;
|
||||
break;
|
||||
|
||||
case SuzukiDecoderStepFoundStartPulse:
|
||||
case SuzukiDecoderStepCountPreamble:
|
||||
if (level) {
|
||||
// HIGH pulse
|
||||
if (header_count < 257) {
|
||||
// Still in preamble - just count
|
||||
return;
|
||||
}
|
||||
|
||||
// After preamble, look for long HIGH to start data
|
||||
if (DURATION_DIFF(duration, te_long) < te_delta) {
|
||||
parser_step = SuzukiDecoderStepSaveDuration;
|
||||
suzuki_add_bit(1);
|
||||
if (header_count >= 300) {
|
||||
if (DURATION_DIFF(duration, te_long) <= te_delta) {
|
||||
parser_step = SuzukiDecoderStepDecodeData;
|
||||
suzuki_add_bit(1);
|
||||
}
|
||||
}
|
||||
// Ignore short HIGHs after preamble until we see a long one
|
||||
} else {
|
||||
// LOW pulse - count as header if short
|
||||
if (DURATION_DIFF(duration, te_short) < te_delta) {
|
||||
if (DURATION_DIFF(duration, te_short) <= te_delta) {
|
||||
te_last = duration;
|
||||
header_count++;
|
||||
} else {
|
||||
@@ -76,45 +67,69 @@ class FProtoSubCarSuzuki : public FProtoSubCarBase {
|
||||
}
|
||||
break;
|
||||
|
||||
case SuzukiDecoderStepSaveDuration:
|
||||
case SuzukiDecoderStepDecodeData:
|
||||
|
||||
if (level) {
|
||||
// HIGH pulse - determines bit value
|
||||
// Long HIGH (~500µs) = 1, Short HIGH (~250µs) = 0
|
||||
if (DURATION_DIFF(duration, te_long) < te_delta) {
|
||||
suzuki_add_bit(1);
|
||||
} else if (DURATION_DIFF(duration, te_short) < te_delta) {
|
||||
suzuki_add_bit(0);
|
||||
if (duration < te_long) {
|
||||
uint32_t diff_long = 500 - duration;
|
||||
if (diff_long > 99) {
|
||||
uint32_t diff_short;
|
||||
if (duration < 250) {
|
||||
diff_short = 250 - duration;
|
||||
} else {
|
||||
diff_short = duration - 250;
|
||||
}
|
||||
|
||||
if (diff_short <= 99) {
|
||||
suzuki_add_bit(0);
|
||||
}
|
||||
} else {
|
||||
suzuki_add_bit(1);
|
||||
}
|
||||
} else {
|
||||
parser_step = SuzukiDecoderStepReset;
|
||||
uint32_t diff_long = duration - 500;
|
||||
if (diff_long <= 99) {
|
||||
suzuki_add_bit(1);
|
||||
}
|
||||
}
|
||||
// Stay in this state for next bit
|
||||
} else {
|
||||
// LOW pulse - check for gap (end of transmission)
|
||||
if (DURATION_DIFF(duration, SUZUKI_GAP_TIME) < SUZUKI_GAP_DELTA) {
|
||||
// Gap found - end of transmission
|
||||
if (data_count_bit == 64) {
|
||||
uint32_t diff_gap;
|
||||
if (duration < SUZUKI_GAP_TIME) {
|
||||
diff_gap = SUZUKI_GAP_TIME - duration;
|
||||
} else {
|
||||
diff_gap = duration - SUZUKI_GAP_TIME;
|
||||
}
|
||||
|
||||
if (diff_gap <= SUZUKI_GAP_DELTA) {
|
||||
if (decode_count_bit == 64) {
|
||||
// instance->generic.data = decode_data;
|
||||
data_count_bit = 64;
|
||||
decode_data = ((uint64_t)data_high << 32) | (uint64_t)data_low;
|
||||
// Check manufacturer nibble (should be 0xF)
|
||||
uint8_t manufacturer = (data_high >> 28) & 0xF;
|
||||
if (manufacturer == 0xF) {
|
||||
// Extract fields
|
||||
decode_data2 = 0; // Not used
|
||||
if (callback) {
|
||||
callback(this);
|
||||
}
|
||||
|
||||
/*uint64_t data = instance->generic.data;
|
||||
uint32_t data_high = (uint32_t)(data >> 32);
|
||||
uint32_t data_low = (uint32_t)data;
|
||||
|
||||
instance->generic.serial = ((data_high & 0xFFF) << 16) | (data_low >> 16);
|
||||
|
||||
instance->generic.btn = (data_low >> 12) & 0xF;
|
||||
instance->generic.cnt = (data_high << 4) >> 16;
|
||||
*/
|
||||
if (callback) {
|
||||
callback(this);
|
||||
}
|
||||
}
|
||||
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
parser_step = SuzukiDecoderStepReset;
|
||||
}
|
||||
// Short LOW pulses are ignored - stay in this state
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t header_count = 0;
|
||||
uint32_t data_high = 0;
|
||||
uint32_t data_low = 0;
|
||||
uint8_t data_count_bit = 0;
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -0,0 +1,341 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Pezsma
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "proc_morse.hpp"
|
||||
#include "audio_dma.hpp"
|
||||
#include "portapack_shared_memory.hpp"
|
||||
#include "event_m4.hpp"
|
||||
#include <cmath>
|
||||
|
||||
void MorseProcessor::configure(uint8_t mode) {
|
||||
configured = false;
|
||||
|
||||
modulation = static_cast<ModulationMode>((uint8_t)mode);
|
||||
|
||||
if (modulation == ModulationMode::FM) { // FM
|
||||
decim_0.configure(taps_11k0_decim_0.taps);
|
||||
decim_1.configure(taps_11k0_decim_1.taps);
|
||||
channel_filter.configure(taps_11k0_channel.taps, 2);
|
||||
demod_cw_fm.configure(24000, 5000);
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
if (modulation == ModulationMode::FM)
|
||||
audio_output.configure(iir_config_passthrough, iir_config_passthrough, (float)user_squelch_level / 100.0f);
|
||||
else
|
||||
audio_output.configure(audio_12k_hpf_300hz_config);
|
||||
|
||||
meas_samples_in_period = 0;
|
||||
meas_last_period_len = 0;
|
||||
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;
|
||||
|
||||
// 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);
|
||||
|
||||
configured = true;
|
||||
}
|
||||
|
||||
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) {
|
||||
// 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;
|
||||
|
||||
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);
|
||||
|
||||
auto audio_buf = demodulate(channel_out);
|
||||
|
||||
for (size_t i = 0; i < audio_buf.count; i++) {
|
||||
float raw_audio = audio_buf.p[i];
|
||||
|
||||
// Squelch Logic
|
||||
int32_t raw_int_abs = (int32_t)(raw_audio * 32768.0f);
|
||||
if (raw_int_abs < 0) raw_int_abs = -raw_int_abs;
|
||||
|
||||
int32_t audio_threshold = (user_squelch_level * user_squelch_level) * 3;
|
||||
|
||||
if (raw_int_abs > audio_threshold || user_squelch_level == 0) {
|
||||
squelch_is_open = true;
|
||||
squelch_hold = (modulation == ModulationMode::FM) ? 2400 : 1200;
|
||||
} else {
|
||||
if (squelch_hold > 0)
|
||||
squelch_hold--;
|
||||
else if (modulation == ModulationMode::FM)
|
||||
squelch_is_open = false;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// 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;
|
||||
}
|
||||
audio_buf.p[i] = decode_audio;
|
||||
}
|
||||
|
||||
// DC BLOCKING
|
||||
if (modulation != ModulationMode::FM) {
|
||||
dc_average = (dc_average * 0.95f) + (decode_audio * 0.05f);
|
||||
measure_frequency((int32_t)((decode_audio - dc_average) * 32768.0f));
|
||||
} else {
|
||||
measure_frequency((int32_t)(raw_audio * 32768.0f));
|
||||
}
|
||||
|
||||
process_decoding((int32_t)(decode_audio * 32768.0f));
|
||||
|
||||
if (modulation == 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>()};
|
||||
event_dispatcher.run();
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Pezsma
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef __PROC_MORSE_H__
|
||||
#define __PROC_MORSE_H__
|
||||
|
||||
#include "baseband_processor.hpp"
|
||||
#include "baseband_thread.hpp"
|
||||
#include "message.hpp"
|
||||
#include "dsp_decimate.hpp"
|
||||
#include "dsp_demodulate.hpp"
|
||||
#include "audio_output.hpp"
|
||||
#include "dsp_fir_taps.hpp"
|
||||
#include "rssi_thread.hpp"
|
||||
|
||||
class MorseProcessor : public BasebandProcessor {
|
||||
public:
|
||||
MorseProcessor() {}
|
||||
|
||||
void execute(const buffer_c8_t& buffer) override;
|
||||
void on_message(const Message* const p) override;
|
||||
|
||||
private:
|
||||
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);
|
||||
|
||||
BasebandThread baseband_thread{3072000, this, baseband::Direction::Receive};
|
||||
RSSIThread rssi_thread{};
|
||||
|
||||
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::AM demod_AM{};
|
||||
dsp::demodulate::FM demod_cw_fm{};
|
||||
dsp::demodulate::SSB demod_ssb{};
|
||||
AudioOutput audio_output{};
|
||||
|
||||
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};
|
||||
|
||||
float dc_average = 0.0f; // DC level tracking
|
||||
int32_t dc_average_int = 0;
|
||||
|
||||
MorseRXDataMessage message{};
|
||||
MorseRXfreqMessage freq_message{};
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,97 @@
|
||||
#include "proc_morsetx.hpp"
|
||||
#include "portapack_shared_memory.hpp"
|
||||
#include "sine_table_int8.hpp"
|
||||
#include "event_m4.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
void MorseTXProcessor::execute(const buffer_c8_t& buffer) {
|
||||
if (!configured) return;
|
||||
|
||||
for (size_t i = 0; i < buffer.count; i++) {
|
||||
int8_t sample_sin;
|
||||
int8_t sample_cos;
|
||||
|
||||
sample_sin = (sine_table_i8[(tone_phase & 0xFF000000) >> 24]);
|
||||
tone_phase += tone_delta;
|
||||
|
||||
im = 0;
|
||||
re = 0;
|
||||
|
||||
// modulation logic
|
||||
if (modulation == 0) { // AM modulation
|
||||
if (key_down) {
|
||||
re = 64 + (sample_sin >> 2);
|
||||
}
|
||||
} else if (modulation == 1) { // FM modulation
|
||||
if (key_down) {
|
||||
delta = sample_sin * fm_delta;
|
||||
} else {
|
||||
delta = 0;
|
||||
}
|
||||
|
||||
phase += delta;
|
||||
sphase = phase + (64 << 24);
|
||||
|
||||
re = (sine_table_i8[(sphase & 0xFF000000) >> 24]);
|
||||
im = (sine_table_i8[(phase & 0xFF000000) >> 24]);
|
||||
|
||||
} else if (modulation == 2) { // DSB
|
||||
if (key_down) {
|
||||
re = sample_sin;
|
||||
}
|
||||
} else if (modulation == 3) { // USB
|
||||
if (key_down) {
|
||||
sample_cos = (sine_table_i8[((tone_phase + 0x40000000) & 0xFF000000) >> 24]);
|
||||
re = sample_cos;
|
||||
im = sample_sin;
|
||||
}
|
||||
} else if (modulation == 4) { // LSB
|
||||
if (key_down) {
|
||||
sample_cos = (sine_table_i8[((tone_phase + 0x40000000) & 0xFF000000) >> 24]);
|
||||
re = sample_cos;
|
||||
im = (sine_table_i8[((tone_phase + 0x80000000) & 0xFF000000) >> 24]);
|
||||
}
|
||||
}
|
||||
buffer.p[i] = {re, im};
|
||||
}
|
||||
}
|
||||
|
||||
void MorseTXProcessor::on_message(const Message* const p) {
|
||||
switch (p->id) {
|
||||
case Message::ID::MorseTXConfigure: {
|
||||
auto message = *reinterpret_cast<const MorseTXConfigureMessage*>(p);
|
||||
tone_delta = message.tone * 1398; // scale
|
||||
tone = message.tone; // audio tone
|
||||
modulation = message.modulation;
|
||||
if (message.fm_delta == 0 && modulation == 1) {
|
||||
fm_delta = 90000;
|
||||
} else {
|
||||
uint64_t scale = 0xFFFFFFFFULL; // 32-bit max
|
||||
fm_delta = (uint32_t)((uint64_t)message.fm_delta * (scale / 1536000));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case Message::ID::MorseTXkey: {
|
||||
auto key = *reinterpret_cast<const MorseTXkeyMessage*>(p);
|
||||
key_down = key.key_down;
|
||||
configured = true;
|
||||
if (key_down)
|
||||
audio::dma::beep_start(tone, 24000, 0);
|
||||
else
|
||||
audio::dma::beep_stop();
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
audio::dma::init_audio_out();
|
||||
EventDispatcher event_dispatcher{std::make_unique<MorseTXProcessor>()};
|
||||
event_dispatcher.run();
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
#ifndef __PROC_MORSETX_H__
|
||||
#define __PROC_MORSETX_H__
|
||||
|
||||
#include "baseband_processor.hpp"
|
||||
#include "baseband_thread.hpp"
|
||||
#include "portapack_shared_memory.hpp"
|
||||
#include "audio_output.hpp"
|
||||
#include "audio_dma.hpp"
|
||||
|
||||
#define AUDIO_OUTPUT_BUFFER_SIZE 32
|
||||
#define AUDIO_SAMPLING_RATE 24000
|
||||
|
||||
class MorseTXProcessor : public BasebandProcessor {
|
||||
public:
|
||||
void execute(const buffer_c8_t& buffer) override;
|
||||
void on_message(const Message* const msg) override;
|
||||
|
||||
private:
|
||||
bool configured{false};
|
||||
bool key_down{false}; // currently the virtual key is pressed or not.
|
||||
|
||||
int8_t sample{0}, re{0}, im{0};
|
||||
uint8_t modulation{0}; // 0=AM, 1=FM, 2=DSB, 3=USB, 4=LSB
|
||||
uint32_t tone_delta{0}; // shifting value by tone
|
||||
uint32_t fm_delta{};
|
||||
uint32_t tone_phase{0};
|
||||
uint32_t tone{0}; // audio tone frequeny
|
||||
int32_t phase{0}, sphase{0}, delta{0}; // sample generation.
|
||||
|
||||
BasebandThread baseband_thread{1536000, this, baseband::Direction::Transmit};
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -241,7 +241,7 @@ w25q80bv_driver_t spi_flash = {
|
||||
|
||||
sgpio_config_t sgpio_config = {
|
||||
.gpio_q_invert = &gpio_q_invert,
|
||||
.gpio_hw_sync_enable = &gpio_hw_sync_enable,
|
||||
.gpio_trigger_enable = &gpio_hw_sync_enable,
|
||||
.slice_mode_multislice = true,
|
||||
};
|
||||
|
||||
@@ -950,7 +950,7 @@ void pin_setup(void) {
|
||||
#ifdef HACKRF_ONE
|
||||
if (detected_platform() == BOARD_ID_HACKRF1_R9) {
|
||||
rf_path.gpio_rx = &gpio_h1r9_rx;
|
||||
sgpio_config.gpio_hw_sync_enable = &gpio_h1r9_hw_sync_enable;
|
||||
sgpio_config.gpio_trigger_enable = &gpio_h1r9_hw_sync_enable;
|
||||
}
|
||||
#endif
|
||||
rf_path_pin_setup(&rf_path);
|
||||
@@ -1041,8 +1041,8 @@ void set_leds(const uint8_t state) {
|
||||
}
|
||||
}
|
||||
|
||||
void hw_sync_enable(const hw_sync_mode_t hw_sync_mode) {
|
||||
gpio_write(sgpio_config.gpio_hw_sync_enable, hw_sync_mode == 1);
|
||||
void trigger_enable(bool hw_sync_mode) {
|
||||
gpio_write(sgpio_config.gpio_trigger_enable, hw_sync_mode == 1);
|
||||
}
|
||||
|
||||
void halt_and_flash(const uint32_t duration) {
|
||||
|
||||
@@ -65,8 +65,8 @@ const usb_request_handlers_t usb_request_handlers = {
|
||||
void usb_configuration_changed(usb_device_t* const device) {
|
||||
(void)device;
|
||||
|
||||
usb_endpoint_init(&usb_endpoint_bulk_in);
|
||||
usb_endpoint_init(&usb_endpoint_bulk_out);
|
||||
usb_endpoint_init(&usb_endpoint_bulk_in, false);
|
||||
usb_endpoint_init(&usb_endpoint_bulk_out, false);
|
||||
}
|
||||
|
||||
void start_usb(void) {
|
||||
@@ -84,8 +84,8 @@ void start_usb(void) {
|
||||
usb_queue_init(&usb_endpoint_bulk_out_queue);
|
||||
usb_queue_init(&usb_endpoint_bulk_in_queue);
|
||||
|
||||
usb_endpoint_init(&usb_endpoint_control_out);
|
||||
usb_endpoint_init(&usb_endpoint_control_in);
|
||||
usb_endpoint_init(&usb_endpoint_control_out, false);
|
||||
usb_endpoint_init(&usb_endpoint_control_in, false);
|
||||
|
||||
nvic_set_priority(NVIC_USB0_IRQ, 255);
|
||||
|
||||
@@ -126,4 +126,4 @@ void usb_transfer(void) {
|
||||
scsi_command(msd_cbw_data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 */
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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__*/
|
||||
|
||||
@@ -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()};
|
||||
|
||||
/////////////////////////////////
|
||||
|
||||
@@ -146,6 +146,12 @@ class Message {
|
||||
SSTVRXPhaseSlant = 88,
|
||||
SSTVRXCalibration = 89,
|
||||
SubCarData = 90,
|
||||
MorseRXData = 91,
|
||||
MorseRXfreq = 92,
|
||||
MorseRXConfig = 93,
|
||||
TXDisabled = 94,
|
||||
MorseTXConfigure = 95,
|
||||
MorseTXkey = 96,
|
||||
MAX
|
||||
};
|
||||
|
||||
@@ -1699,4 +1705,57 @@ class SubCarDataMessage : public Message {
|
||||
uint64_t data2 = 0;
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
class MorseRXConfigureMessage : public Message {
|
||||
public:
|
||||
constexpr MorseRXConfigureMessage(uint8_t mode)
|
||||
: Message{ID::MorseRXConfig},
|
||||
mode{mode} {}
|
||||
uint8_t mode = 0;
|
||||
};
|
||||
|
||||
class TXDisabledMessage : public Message {
|
||||
public:
|
||||
constexpr TXDisabledMessage()
|
||||
: Message{ID::TXDisabled} {
|
||||
}
|
||||
};
|
||||
|
||||
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__*/
|
||||
|
||||
@@ -152,12 +152,12 @@ struct misc_config_t {
|
||||
bool config_sdcard_high_speed_io : 1;
|
||||
bool config_disable_config_mode : 1;
|
||||
bool beep_on_packets : 1;
|
||||
bool UNUSED_6 : 1;
|
||||
bool UNUSED_7 : 1;
|
||||
bool tx_disabled : 1;
|
||||
bool tx_amp_disabled : 1;
|
||||
|
||||
uint8_t tx_gain_max_db;
|
||||
uint8_t PLACEHOLDER_1;
|
||||
uint8_t PLACEHOLDER_2;
|
||||
uint8_t PLACEHOLDER_3;
|
||||
};
|
||||
static_assert(sizeof(misc_config_t) == sizeof(uint32_t));
|
||||
|
||||
@@ -441,6 +441,10 @@ void defaults() {
|
||||
set_recon_repeat_delay(1);
|
||||
|
||||
set_config_sdcard_high_speed_io(false, true);
|
||||
|
||||
set_config_tx_disabled(false);
|
||||
set_config_tx_amp_disabled(false);
|
||||
set_config_tx_gain_max_db(47);
|
||||
}
|
||||
|
||||
void init() {
|
||||
@@ -649,6 +653,18 @@ bool config_sdcard_high_speed_io() {
|
||||
return data->misc_config.config_sdcard_high_speed_io;
|
||||
}
|
||||
|
||||
bool config_tx_disabled() {
|
||||
return data->misc_config.tx_disabled;
|
||||
}
|
||||
|
||||
bool config_tx_amp_disabled() {
|
||||
return data->misc_config.tx_amp_disabled;
|
||||
}
|
||||
|
||||
uint8_t config_tx_gain_max_db() {
|
||||
return data->misc_config.tx_gain_max_db;
|
||||
}
|
||||
|
||||
bool stealth_mode() {
|
||||
return data->ui_config.stealth_mode;
|
||||
}
|
||||
@@ -743,6 +759,18 @@ void set_config_sdcard_high_speed_io(bool v, bool save) {
|
||||
data->misc_config.config_sdcard_high_speed_io = v;
|
||||
}
|
||||
|
||||
void set_config_tx_disabled(bool v) {
|
||||
data->misc_config.tx_disabled = v;
|
||||
}
|
||||
|
||||
void set_config_tx_amp_disabled(bool v) {
|
||||
data->misc_config.tx_amp_disabled = v;
|
||||
}
|
||||
|
||||
void set_config_tx_gain_max_db(uint8_t v) {
|
||||
data->misc_config.tx_gain_max_db = v;
|
||||
}
|
||||
|
||||
void set_stealth_mode(bool v) {
|
||||
data->ui_config.stealth_mode = v;
|
||||
}
|
||||
|
||||
@@ -203,6 +203,9 @@ bool config_disable_external_tcxo();
|
||||
bool config_sdcard_high_speed_io();
|
||||
bool config_disable_config_mode();
|
||||
bool beep_on_packets();
|
||||
bool config_tx_disabled();
|
||||
bool config_tx_amp_disabled();
|
||||
uint8_t config_tx_gain_max_db();
|
||||
|
||||
bool config_splash();
|
||||
bool config_converter();
|
||||
@@ -226,6 +229,9 @@ void set_config_disable_external_tcxo(bool v);
|
||||
void set_config_sdcard_high_speed_io(bool v, bool save);
|
||||
void set_config_disable_config_mode(bool v);
|
||||
void set_beep_on_packets(bool v);
|
||||
void set_config_tx_disabled(bool v);
|
||||
void set_config_tx_amp_disabled(bool v);
|
||||
void set_config_tx_gain_max_db(uint8_t v);
|
||||
|
||||
void set_config_splash(bool v);
|
||||
bool config_converter();
|
||||
|
||||
@@ -124,6 +124,8 @@ constexpr image_tag_t image_tag_protoview{'P', 'P', 'V', 'W'};
|
||||
constexpr image_tag_t image_tag_wefaxrx{'P', 'W', 'F', 'X'};
|
||||
constexpr image_tag_t image_tag_noaaapt_rx{'P', 'N', 'O', 'A'};
|
||||
constexpr image_tag_t image_tag_sstv_rx{'P', 'S', 'R', 'X'};
|
||||
constexpr image_tag_t image_tag_morse{'P', 'M', 'R', 'S'};
|
||||
constexpr image_tag_t image_tag_morsetx{'P', 'M', 'R', 'T'};
|
||||
|
||||
constexpr image_tag_t image_tag_noop{'P', 'N', 'O', 'P'};
|
||||
|
||||
|
||||
+205
-19
@@ -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;
|
||||
}
|
||||
@@ -1358,14 +1362,18 @@ void NewButton::paint(Painter& painter) {
|
||||
}
|
||||
|
||||
if (!text_.empty()) {
|
||||
const auto label_r = style.font.size_of(text_);
|
||||
auto label_r = style.font.size_of(text_);
|
||||
std::string text_to_draw = text_;
|
||||
if (label_r.width() > r.width() - 2) {
|
||||
// Truncate text to fit
|
||||
size_t max_chars = (r.width() - 2) / style.font.char_width();
|
||||
text_to_draw = text_.substr(0, max_chars);
|
||||
label_r = style.font.size_of(text_to_draw);
|
||||
}
|
||||
if (bitmap_) {
|
||||
y += spacing;
|
||||
}
|
||||
painter.draw_string(
|
||||
{r.left() + (r.width() - label_r.width()) / 2, y},
|
||||
style,
|
||||
text_);
|
||||
painter.draw_string({r.left() + (r.width() - label_r.width()) / 2, y}, style, text_to_draw);
|
||||
}
|
||||
} else { // no valign
|
||||
if (bitmap_) {
|
||||
@@ -1379,11 +1387,16 @@ void NewButton::paint(Painter& painter) {
|
||||
}
|
||||
|
||||
if (!text_.empty()) {
|
||||
const auto label_r = style.font.size_of(text_);
|
||||
painter.draw_string(
|
||||
{r.left() + (r.width() - label_r.width()) / 2, y + (r.height() - label_r.height()) / 2},
|
||||
style,
|
||||
text_);
|
||||
auto label_r = style.font.size_of(text_);
|
||||
std::string text_to_draw = text_;
|
||||
if (label_r.width() > r.width() - 2) {
|
||||
// Truncate text to fit
|
||||
size_t max_chars = (r.width() - 2) / style.font.char_width();
|
||||
text_to_draw = text_.substr(0, max_chars);
|
||||
label_r = style.font.size_of(text_to_draw);
|
||||
}
|
||||
painter.draw_string({r.left() + (r.width() - label_r.width()) / 2, y + (r.height() - label_r.height()) / 2}, style,
|
||||
text_to_draw);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1732,6 +1745,14 @@ bool ImageOptionsField::on_keyboard(const KeyboardEvent key) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ImageOptionsField::on_key(const KeyEvent event) {
|
||||
if (event == KeyEvent::Select) {
|
||||
on_encoder(1);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ImageOptionsField::on_touch(const TouchEvent event) {
|
||||
if (event.type == TouchEvent::Type::Start) {
|
||||
focus();
|
||||
@@ -1881,6 +1902,14 @@ bool OptionsField::on_keyboard(const KeyboardEvent key) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool OptionsField::on_key(const KeyEvent event) {
|
||||
if (event == KeyEvent::Select) {
|
||||
on_encoder(1);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool OptionsField::on_touch(const TouchEvent event) {
|
||||
if (event.type == TouchEvent::Type::Start) {
|
||||
focus();
|
||||
@@ -2269,20 +2298,44 @@ void NumberField::getWidgetName(std::string& result) {
|
||||
}
|
||||
|
||||
void NumberField::set_value(int32_t new_value, bool trigger_change) {
|
||||
const int32_t lo = range.first;
|
||||
const int32_t hi = range.second;
|
||||
|
||||
// Helper: floor-div for negatives
|
||||
auto floordiv = [](int64_t a, int64_t b) -> int64_t { // b > 0
|
||||
int64_t q = a / b;
|
||||
int64_t r = a % b;
|
||||
return (r < 0) ? (q - 1) : q;
|
||||
};
|
||||
// Helper: positive modulo
|
||||
auto posmod = [](int64_t a, int64_t m) -> int64_t { // m > 0
|
||||
int64_t r = a % m;
|
||||
return (r < 0) ? (r + m) : r;
|
||||
};
|
||||
|
||||
if (can_loop) {
|
||||
if (new_value >= range.first)
|
||||
new_value = new_value % (range.second + 1);
|
||||
else
|
||||
new_value = range.second + new_value + 1;
|
||||
// number of discrete positions
|
||||
const int64_t count = (int64_t)(hi - lo) / step + 1;
|
||||
|
||||
// map value -> step index (floor so negatives behave)
|
||||
int64_t idx = floordiv((int64_t)new_value - lo, step);
|
||||
|
||||
// wrap index
|
||||
idx = posmod(idx, count);
|
||||
|
||||
// rebuild value
|
||||
new_value = (int32_t)(lo + idx * step);
|
||||
} else {
|
||||
new_value = clip(new_value, lo, hi);
|
||||
// optionally snap to step here too
|
||||
int64_t idx = floordiv((int64_t)new_value - lo, step);
|
||||
new_value = (int32_t)(lo + idx * step);
|
||||
}
|
||||
|
||||
new_value = clip(new_value, range.first, range.second);
|
||||
|
||||
// set final value if needed
|
||||
if (new_value != value()) {
|
||||
value_ = new_value;
|
||||
if (on_change && trigger_change) {
|
||||
on_change(value_);
|
||||
}
|
||||
if (on_change && trigger_change) on_change(value_);
|
||||
set_dirty();
|
||||
}
|
||||
}
|
||||
@@ -2313,6 +2366,8 @@ bool NumberField::on_key(const KeyEvent key) {
|
||||
if (on_select) {
|
||||
on_select(*this);
|
||||
return true;
|
||||
} else {
|
||||
return on_encoder(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2355,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(
|
||||
|
||||
@@ -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;
|
||||
@@ -666,6 +667,8 @@ class ImageOptionsField : public Widget {
|
||||
bool on_encoder(const EncoderEvent delta) override;
|
||||
bool on_touch(const TouchEvent event) override;
|
||||
bool on_keyboard(const KeyboardEvent event) override;
|
||||
bool on_key(const KeyEvent event) override;
|
||||
|
||||
void getAccessibilityText(std::string& result) override;
|
||||
void getWidgetName(std::string& result) override;
|
||||
|
||||
@@ -674,6 +677,7 @@ class ImageOptionsField : public Widget {
|
||||
size_t selected_index_{0};
|
||||
Color foreground_;
|
||||
Color background_;
|
||||
bool is_activated = false;
|
||||
};
|
||||
|
||||
class OptionsField : public Widget {
|
||||
@@ -706,6 +710,7 @@ class OptionsField : public Widget {
|
||||
bool on_encoder(const EncoderEvent delta) override;
|
||||
bool on_touch(const TouchEvent event) override;
|
||||
bool on_keyboard(const KeyboardEvent event) override;
|
||||
bool on_key(const KeyEvent event) override;
|
||||
|
||||
void getAccessibilityText(std::string& result) override;
|
||||
void getWidgetName(std::string& result) override;
|
||||
@@ -715,6 +720,7 @@ class OptionsField : public Widget {
|
||||
options_t options_;
|
||||
size_t selected_index_{0};
|
||||
bool centered_{false}; // e.g.: length as screen_width/8, x position as 0, it will be centered in x axis
|
||||
bool is_activated = false;
|
||||
};
|
||||
|
||||
// A TextEdit is bound to a string reference and allows the string
|
||||
@@ -851,11 +857,11 @@ class NumberField : public Widget {
|
||||
NumberField(Point parent_pos, int length, range_t range, int32_t step, char fill_char, bool can_loop);
|
||||
|
||||
NumberField(Point parent_pos, int length, range_t range, int32_t step, char fill_char)
|
||||
: NumberField{parent_pos, length, range, step, fill_char, false} {
|
||||
: NumberField{parent_pos, length, range, step, fill_char, true} {
|
||||
}
|
||||
|
||||
NumberField()
|
||||
: NumberField{{0, 0}, 1, {0, 1}, 1, ' ', false} {
|
||||
: NumberField{{0, 0}, 1, {0, 1}, 1, ' ', true} {
|
||||
}
|
||||
|
||||
NumberField(const NumberField&) = delete;
|
||||
@@ -885,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:
|
||||
|
||||
@@ -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
|
||||
@@ -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:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user