mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2026-08-21 15:09:03 +00:00
Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 56d965f3ae | |||
| fe9f0c228f | |||
| 6562e596e8 | |||
| c27192bdbb | |||
| 927e3b11e7 | |||
| 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 | |||
| 70ac1a03a1 | |||
| 2261e53981 | |||
| f86d3e51f1 | |||
| f71f19e719 | |||
| c53adfc765 | |||
| 7e8ad83537 | |||
| 472571b1ed | |||
| 106e56abc3 | |||
| 4129d57c09 | |||
| caac5e1041 | |||
| 6b02ba6e5d | |||
| c01597baf2 | |||
| 43a5163b77 |
+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
|
||||
|
||||
...
|
||||
|
||||
@@ -19,5 +19,6 @@ jobs:
|
||||
- name: clang-format Check
|
||||
uses: jidicula/clang-format-action@v4.11.0
|
||||
with:
|
||||
clang-format-version: '18'
|
||||
check-path: ${{ matrix.path }}
|
||||
fallback-style: Chromium
|
||||
|
||||
@@ -1 +1 @@
|
||||
v2.2.0
|
||||
v2.3.1
|
||||
|
||||
@@ -1 +1 @@
|
||||
v2.3.1
|
||||
v2.3.2
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -159,7 +159,7 @@ class AISAppView : public View {
|
||||
~AISAppView();
|
||||
|
||||
void set_parent_rect(const Rect new_parent_rect) override;
|
||||
void paint(Painter&) override{};
|
||||
void paint(Painter&) override {};
|
||||
|
||||
void focus() override;
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ class BLECommView : public View {
|
||||
~BLECommView();
|
||||
|
||||
void set_parent_rect(const Rect new_parent_rect) override;
|
||||
void paint(Painter&) override{};
|
||||
void paint(Painter&) override {};
|
||||
|
||||
void focus() override;
|
||||
|
||||
|
||||
@@ -209,7 +209,7 @@ class BLERxView : public View {
|
||||
~BLERxView();
|
||||
|
||||
void set_parent_rect(const Rect new_parent_rect) override;
|
||||
void paint(Painter&) override{};
|
||||
void paint(Painter&) override {};
|
||||
|
||||
void focus() override;
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ class BLETxView : public View {
|
||||
~BLETxView();
|
||||
|
||||
void set_parent_rect(const Rect new_parent_rect) override;
|
||||
void paint(Painter&) override{};
|
||||
void paint(Painter&) override {};
|
||||
|
||||
void focus() override;
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ POCSAGSettingsView::POCSAGSettingsView(
|
||||
: settings_{settings} {
|
||||
add_children(
|
||||
{&labels,
|
||||
&opt_baud_rate,
|
||||
&check_log,
|
||||
&check_log_raw,
|
||||
&check_small_font,
|
||||
@@ -65,6 +66,7 @@ POCSAGSettingsView::POCSAGSettingsView(
|
||||
&field_filter_address,
|
||||
&button_save});
|
||||
|
||||
opt_baud_rate.set_by_value(settings_.baud_rate);
|
||||
check_log.set_value(settings_.enable_logging);
|
||||
check_log_raw.set_value(settings_.enable_raw_log);
|
||||
check_small_font.set_value(settings_.enable_small_font);
|
||||
@@ -81,7 +83,7 @@ POCSAGSettingsView::POCSAGSettingsView(
|
||||
settings_.hide_addr_only = check_hide_addr_only.value();
|
||||
settings_.filter_mode = opt_filter_mode.selected_index_value();
|
||||
settings_.filter_address = field_filter_address.to_integer();
|
||||
|
||||
settings_.baud_rate = opt_baud_rate.selected_index_value();
|
||||
nav.pop();
|
||||
};
|
||||
}
|
||||
@@ -142,7 +144,7 @@ POCSAGAppView::POCSAGAppView(NavigationView& nav)
|
||||
|
||||
audio::output::start();
|
||||
receiver_model.enable();
|
||||
baseband::set_pocsag();
|
||||
baseband::set_pocsag((int8_t)settings_.baud_rate);
|
||||
}
|
||||
|
||||
void POCSAGAppView::focus() {
|
||||
@@ -182,6 +184,7 @@ void POCSAGAppView::refresh_ui() {
|
||||
btn_text = "Filter Last";
|
||||
break;
|
||||
}
|
||||
baseband::set_pocsag((int8_t)settings_.baud_rate);
|
||||
button_filter_last.set_text(btn_text);
|
||||
}
|
||||
|
||||
|
||||
@@ -126,6 +126,7 @@ struct POCSAGSettings {
|
||||
bool hide_bad_data = false;
|
||||
bool hide_addr_only = false;
|
||||
uint8_t filter_mode = false;
|
||||
int32_t baud_rate = -1;
|
||||
uint32_t filter_address = 0;
|
||||
};
|
||||
|
||||
@@ -139,7 +140,16 @@ class POCSAGSettingsView : public View {
|
||||
private:
|
||||
POCSAGSettings& settings_;
|
||||
|
||||
OptionsField opt_baud_rate{
|
||||
{8 * 8, 0 * 16},
|
||||
4,
|
||||
{{"Auto", -1},
|
||||
{" 512", 0},
|
||||
{"1200", 1},
|
||||
{"2400", 2}}};
|
||||
|
||||
Labels labels{
|
||||
{{2 * 8, 0 * 16}, "Baud:", Theme::getInstance()->fg_light->foreground},
|
||||
{{2 * 8, 12 * 16}, "Filter Mode:", Theme::getInstance()->fg_light->foreground},
|
||||
{{2 * 8, 13 * 16}, "Filter Addr:", Theme::getInstance()->fg_light->foreground},
|
||||
};
|
||||
@@ -221,6 +231,7 @@ class POCSAGAppView : public View {
|
||||
{"filter_address"sv, &settings_.filter_address},
|
||||
{"hide_bad_data"sv, &settings_.hide_bad_data},
|
||||
{"hide_addr_only"sv, &settings_.hide_addr_only},
|
||||
{"baud_rate"sv, &settings_.baud_rate},
|
||||
}};
|
||||
|
||||
void refresh_ui();
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace ui {
|
||||
constexpr std::string_view mayhem_information_list[] = {
|
||||
"#****** Mayhem Community ******",
|
||||
" ",
|
||||
" https://discord.mayhem.app",
|
||||
" https://discord.hackrf.app",
|
||||
" ",
|
||||
"#**** List of contributors ****",
|
||||
" ",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1086,8 +1086,8 @@ void ReconView::on_statistics_update(const ChannelStatistics& statistics) {
|
||||
if (stepper < 0) stepper++;
|
||||
if (stepper > 0) stepper--;
|
||||
} // if( recon || stepper != 0 || index_stepper != 0 )
|
||||
} // if (frequency_list.size() > 0 )
|
||||
} /* on_statistics_updates */
|
||||
} // if (frequency_list.size() > 0 )
|
||||
} /* on_statistics_updates */
|
||||
}
|
||||
handle_retune();
|
||||
recon_redraw();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -30,11 +30,10 @@
|
||||
#include "portapack.hpp"
|
||||
#include <cstring>
|
||||
#include <stdio.h>
|
||||
#include "rtc_time.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
namespace pmem = portapack::persistent_memory;
|
||||
|
||||
#include "string_format.hpp"
|
||||
#include "complex.hpp"
|
||||
|
||||
void SondeLogger::on_packet(const sonde::Packet& packet) {
|
||||
@@ -65,6 +64,8 @@ SondeView::SondeView(NavigationView& nav)
|
||||
&text_frame,
|
||||
&text_temp,
|
||||
&text_humid,
|
||||
&text_press,
|
||||
&text_vspeed,
|
||||
&geopos,
|
||||
&button_see_qr,
|
||||
&button_see_map});
|
||||
@@ -107,7 +108,7 @@ SondeView::SondeView(NavigationView& nav)
|
||||
|
||||
logger = std::make_unique<SondeLogger>();
|
||||
if (logger)
|
||||
logger->append(logs_dir / u"SONDE.TXT");
|
||||
logger->append(logs_dir / u"SONDE_" + to_string_timestamp(rtc_time::now()) + u".TXT");
|
||||
|
||||
if (pmem::beep_on_packets()) {
|
||||
audio::set_rate(audio::Rate::Hz_24000);
|
||||
@@ -155,7 +156,7 @@ void SondeView::on_packet(const sonde::Packet& packet) {
|
||||
sonde_id = packet.serial_number(); // used also as tag on the geomap
|
||||
text_serial.set(sonde_id);
|
||||
|
||||
text_timestamp.set(to_string_timestamp(packet.received_at()));
|
||||
text_timestamp.set(to_string_datetime(packet.received_at(), TimeFormat::YMDHMS));
|
||||
|
||||
text_voltage.set(unit_auto_scale(packet.battery_voltage(), 2, 2) + "V");
|
||||
|
||||
@@ -163,21 +164,42 @@ void SondeView::on_packet(const sonde::Packet& packet) {
|
||||
|
||||
temp_humid_info = packet.get_temp_humid();
|
||||
if (temp_humid_info.humid != 0) {
|
||||
double decimals = abs(get_decimals(temp_humid_info.humid, 10, true));
|
||||
text_humid.set(to_string_dec_int((int)temp_humid_info.humid) + "." + to_string_dec_uint(decimals, 1) + "%");
|
||||
text_humid.set(to_string_decimal(temp_humid_info.humid, 1) + "%");
|
||||
}
|
||||
|
||||
if (temp_humid_info.temp != 0) {
|
||||
double decimals = abs(get_decimals(temp_humid_info.temp, 10, true));
|
||||
text_temp.set(to_string_dec_int((int)temp_humid_info.temp) + "." + to_string_dec_uint(decimals, 1) + STR_DEGREES_C);
|
||||
text_temp.set(to_string_decimal(temp_humid_info.temp, 1) + STR_DEGREES_C);
|
||||
}
|
||||
|
||||
if (packet.get_pressure() != 0) {
|
||||
text_press.set(to_string_decimal(packet.get_pressure(), 1) + " hPa");
|
||||
}
|
||||
|
||||
gps_info = packet.get_GPS_data();
|
||||
|
||||
geopos.set_altitude(gps_info.alt);
|
||||
geopos.set_lat(gps_info.lat);
|
||||
geopos.set_lon(gps_info.lon);
|
||||
|
||||
if (last_timestamp_update_ != 0 && last_altitude_ != 0) {
|
||||
// calculate speeds
|
||||
float vspeed = 0;
|
||||
time_t currpackettime = rtc_time::rtcToUnixUTC(packet.received_at());
|
||||
int32_t time_diff = (currpackettime - last_timestamp_update_);
|
||||
if (time_diff >= 10) { // update only every 10 seconds
|
||||
vspeed = (static_cast<int>(gps_info.alt) - static_cast<int>(last_altitude_)) / (float)time_diff;
|
||||
last_timestamp_update_ = currpackettime;
|
||||
last_altitude_ = gps_info.alt;
|
||||
text_vspeed.set(to_string_decimal(vspeed, 1) + " m/s");
|
||||
}
|
||||
} else { // save first valid packet time + altitude
|
||||
last_timestamp_update_ = rtc_time::rtcToUnixUTC(packet.received_at());
|
||||
last_altitude_ = geopos.altitude();
|
||||
}
|
||||
if (gps_info.is_valid()) { // only update when valid, to prevent flashing
|
||||
geopos.set_altitude(gps_info.alt);
|
||||
geopos.set_lat(gps_info.lat);
|
||||
geopos.set_lon(gps_info.lon);
|
||||
if (geomap_view_) {
|
||||
geomap_view_->update_position(gps_info.lat, gps_info.lon, 400, gps_info.alt, 0);
|
||||
}
|
||||
}
|
||||
if (logger && logging) {
|
||||
logger->on_packet(packet);
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include "ui_rssi.hpp"
|
||||
#include "ui_qrcode.hpp"
|
||||
#include "ui_geomap.hpp"
|
||||
#include "string_format.hpp"
|
||||
|
||||
#include "event_m0.hpp"
|
||||
|
||||
@@ -94,32 +95,33 @@ class SondeView : public View {
|
||||
// AudioOutput audio_output { };
|
||||
|
||||
Labels labels{
|
||||
{{4 * 8, 2 * 16}, "Type:", Theme::getInstance()->fg_light->foreground},
|
||||
{{6 * 8, 3 * 16}, "ID:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), 4 * 16}, "DateTime:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(4), UI_POS_Y(2)}, "Type:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(6), UI_POS_Y(3)}, "ID:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(4)}, "DateTime:", Theme::getInstance()->fg_light->foreground},
|
||||
|
||||
{{3 * 8, 5 * 16}, "Vbatt:", Theme::getInstance()->fg_light->foreground},
|
||||
{{3 * 8, 6 * 16}, "Frame:", Theme::getInstance()->fg_light->foreground},
|
||||
{{4 * 8, 7 * 16}, "Temp:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), 8 * 16}, "Humidity:", Theme::getInstance()->fg_light->foreground}};
|
||||
{{UI_POS_X(3), UI_POS_Y(5)}, "Vbatt:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(3), UI_POS_Y(6)}, "Frame:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(4), UI_POS_Y(7)}, "Temp:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(8)}, "Humidity:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(9)}, "Pressure:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(2), UI_POS_Y(10)}, "VSpeed:", Theme::getInstance()->fg_light->foreground}};
|
||||
|
||||
RxFrequencyField field_frequency{
|
||||
{UI_POS_X(0), 0 * 8},
|
||||
{UI_POS_X(0), UI_POS_Y(0)},
|
||||
nav_};
|
||||
|
||||
RFAmpField field_rf_amp{
|
||||
{13 * 8, UI_POS_Y(0)}};
|
||||
|
||||
{UI_POS_X(13), UI_POS_Y(0)}};
|
||||
LNAGainField field_lna{
|
||||
{15 * 8, UI_POS_Y(0)}};
|
||||
{UI_POS_X(15), UI_POS_Y(0)}};
|
||||
|
||||
VGAGainField field_vga{
|
||||
{18 * 8, UI_POS_Y(0)}};
|
||||
{UI_POS_X(18), UI_POS_Y(0)}};
|
||||
|
||||
RSSI rssi{
|
||||
{21 * 8, 0, UI_POS_WIDTH_REMAINING(24), 4}};
|
||||
{UI_POS_X(21), UI_POS_Y(0), UI_POS_WIDTH_REMAINING(24), 4}};
|
||||
Channel channel{
|
||||
{21 * 8, 5, UI_POS_WIDTH_REMAINING(24), 4},
|
||||
{UI_POS_X(21), UI_POS_Y(0) + 5, UI_POS_WIDTH_REMAINING(24), 4},
|
||||
};
|
||||
|
||||
AudioVolumeField field_volume{
|
||||
@@ -136,47 +138,57 @@ class SondeView : public View {
|
||||
"CRC"};
|
||||
|
||||
Text text_signature{
|
||||
{9 * 8, 2 * 16, 10 * 8, 16},
|
||||
{UI_POS_X(9), UI_POS_Y(2), UI_POS_WIDTH_REMAINING(10), UI_POS_HEIGHT(1)},
|
||||
"..."};
|
||||
|
||||
Text text_serial{
|
||||
{9 * 8, 3 * 16, 11 * 8, 16},
|
||||
{UI_POS_X(9), UI_POS_Y(3), UI_POS_WIDTH_REMAINING(10), UI_POS_HEIGHT(1)},
|
||||
"..."};
|
||||
|
||||
Text text_timestamp{
|
||||
{9 * 8, 4 * 16, 11 * 8, 16},
|
||||
{UI_POS_X(9), UI_POS_Y(4), UI_POS_WIDTH_REMAINING(9), UI_POS_HEIGHT(1)},
|
||||
"..."};
|
||||
|
||||
Text text_voltage{
|
||||
{9 * 8, 5 * 16, 10 * 8, 16},
|
||||
{UI_POS_X(9), UI_POS_Y(5), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)},
|
||||
"..."};
|
||||
|
||||
Text text_frame{
|
||||
{9 * 8, 6 * 16, 10 * 8, 16},
|
||||
{UI_POS_X(9), UI_POS_Y(6), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)},
|
||||
"..."};
|
||||
|
||||
Text text_temp{
|
||||
{9 * 8, 7 * 16, 10 * 8, 16},
|
||||
{UI_POS_X(9), UI_POS_Y(7), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)},
|
||||
"..."};
|
||||
|
||||
Text text_humid{
|
||||
{9 * 8, 8 * 16, 10 * 8, 16},
|
||||
{UI_POS_X(9), UI_POS_Y(8), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)},
|
||||
"..."};
|
||||
|
||||
Text text_press{
|
||||
{UI_POS_X(9), UI_POS_Y(9), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)},
|
||||
"..."};
|
||||
|
||||
Text text_vspeed{
|
||||
{UI_POS_X(9), UI_POS_Y(10), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)},
|
||||
"..."};
|
||||
|
||||
GeoPos geopos{
|
||||
{0, 12 * 16},
|
||||
{UI_POS_X(0), UI_POS_Y(12)},
|
||||
GeoPos::alt_unit::METERS,
|
||||
GeoPos::spd_unit::HIDDEN};
|
||||
|
||||
Button button_see_qr{
|
||||
{UI_POS_X_CENTER(12) - UI_POS_WIDTH(8), UI_POS_Y_BOTTOM(4), 12 * 8, 3 * 16},
|
||||
{UI_POS_X_CENTER(12) - UI_POS_WIDTH(8), UI_POS_Y_BOTTOM(4), UI_POS_WIDTH(12), UI_POS_HEIGHT(3)},
|
||||
"See QR"};
|
||||
|
||||
Button button_see_map{
|
||||
{UI_POS_X_CENTER(12) + UI_POS_WIDTH(8), UI_POS_Y_BOTTOM(4), 12 * 8, 3 * 16},
|
||||
{UI_POS_X_CENTER(12) + UI_POS_WIDTH(8), UI_POS_Y_BOTTOM(4), UI_POS_WIDTH(12), UI_POS_HEIGHT(3)},
|
||||
"See on map"};
|
||||
|
||||
GeoMapView* geomap_view_{nullptr};
|
||||
time_t last_timestamp_update_{0};
|
||||
uint32_t last_altitude_{0};
|
||||
|
||||
MessageHandlerRegistration message_handler_packet{
|
||||
Message::ID::SondePacket,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -147,6 +147,19 @@ void set_sstv_data(const uint8_t vis_code, const uint32_t pixel_duration) {
|
||||
send_message(&message);
|
||||
}
|
||||
|
||||
void set_sstvrx_data(const uint8_t code) {
|
||||
const SSTVRXConfigureMessage message{
|
||||
code};
|
||||
send_message(&message);
|
||||
}
|
||||
|
||||
void set_sstvrx_phase_slant(const int16_t phase, const int16_t slant) {
|
||||
const SSTVRXPhaseSlantMessage message{
|
||||
phase,
|
||||
slant};
|
||||
send_message(&message);
|
||||
}
|
||||
|
||||
void set_afsk(const uint32_t baudrate, const uint32_t word_length, const uint32_t trigger_value, const bool trigger_word) {
|
||||
const AFSKRxConfigureMessage message{
|
||||
baudrate,
|
||||
@@ -288,8 +301,8 @@ void set_fsk_data(const uint32_t stream_length, const uint32_t samples_per_bit,
|
||||
send_message(&message);
|
||||
}
|
||||
|
||||
void set_pocsag() {
|
||||
const POCSAGConfigureMessage message{};
|
||||
void set_pocsag(int8_t baud_config) {
|
||||
const POCSAGConfigureMessage message{baud_config};
|
||||
send_message(&message);
|
||||
}
|
||||
|
||||
@@ -328,6 +341,11 @@ void set_noaaapt_config() {
|
||||
send_message(&message);
|
||||
}
|
||||
|
||||
void set_flex_config() {
|
||||
const FlexConfigureMessage message{};
|
||||
send_message(&message);
|
||||
}
|
||||
|
||||
void set_siggen_tone(const uint32_t tone) {
|
||||
const SigGenToneMessage message{
|
||||
TONES_F2D(tone, TONES_SAMPLERATE)};
|
||||
@@ -350,6 +368,26 @@ 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);
|
||||
}
|
||||
|
||||
void set_bitstream_config(uint32_t deviation, uint8_t mode) {
|
||||
const StreamTXConfigurationMessage message{deviation, mode};
|
||||
send_message(&message);
|
||||
}
|
||||
|
||||
static bool baseband_image_running = false;
|
||||
|
||||
void run_image(const spi_flash::image_tag_t image_tag) {
|
||||
|
||||
@@ -74,13 +74,15 @@ void set_tone(const uint32_t index, const uint32_t delta, const uint32_t duratio
|
||||
void set_tones_config(const uint32_t bw, const uint32_t pre_silence, const uint16_t tone_count, const bool dual_tone, const bool audio_out);
|
||||
void kill_tone();
|
||||
void set_sstv_data(const uint8_t vis_code, const uint32_t pixel_duration);
|
||||
void set_sstvrx_data(const uint8_t code);
|
||||
void set_sstvrx_phase_slant(const int16_t phase, const int16_t slant);
|
||||
void set_audiotx_config(const uint32_t divider, const float deviation_hz, const float audio_gain, uint8_t audio_shift_bits_s16, uint8_t bits_per_sample, const uint32_t tone_key_delta, const bool am_enabled, const bool dsb_enabled, const bool usb_enabled, const bool lsb_enabled);
|
||||
void set_fifo_data(const int8_t* data);
|
||||
void set_pitch_rssi(int32_t avg, bool enabled);
|
||||
void set_afsk_data(const uint32_t afsk_samples_per_bit, const uint32_t afsk_phase_inc_mark, const uint32_t afsk_phase_inc_space, const uint8_t afsk_repeat, const uint32_t afsk_bw, const uint8_t symbol_count);
|
||||
void kill_afsk();
|
||||
void set_afsk(const uint32_t baudrate, const uint32_t word_length, const uint32_t trigger_value, const bool trigger_word);
|
||||
void set_fsk(const uint32_t samplesPerSymbol, const uint32_t syncWord, const uint8_t syncWordLength, const uint32_t preamble, const uint8_t preambleLength, uint16_t numDataBytes);
|
||||
void set_fsk(const uint8_t samplesPerSymbol, const uint32_t syncWord, const uint8_t syncWordLength, const uint32_t preamble, const uint8_t preambleLength, uint16_t numDataBytes);
|
||||
void set_aprs(const uint32_t baudrate);
|
||||
|
||||
void set_btlerx(uint8_t channel_number);
|
||||
@@ -91,7 +93,7 @@ void set_nrf(const uint32_t baudrate, const uint32_t word_length, const uint32_t
|
||||
void set_ook_data(const uint32_t stream_length, const uint32_t samples_per_bit, const uint8_t repeat, const uint32_t pause_symbols, const uint8_t de_bruijn_length = 0);
|
||||
void kill_ook();
|
||||
void set_fsk_data(const uint32_t stream_length, const uint32_t samples_per_bit, const uint32_t shift, const uint32_t progress_notice);
|
||||
void set_pocsag();
|
||||
void set_pocsag(int8_t baud_config = -1);
|
||||
void set_adsb();
|
||||
void set_jammer(const bool run, const jammer::JammerType type, const uint32_t speed);
|
||||
void set_rds_data(const uint16_t message_length);
|
||||
@@ -100,8 +102,13 @@ void set_siggen_tone(const uint32_t tone);
|
||||
void set_siggen_config(const uint32_t bw, const uint32_t shape, const uint32_t duration);
|
||||
void set_spectrum_painter_config(const uint16_t width, const uint16_t height, bool update, int32_t bw);
|
||||
void set_subghzd_config(uint8_t modulation, uint32_t sampling_rate);
|
||||
void set_moreserx_config(uint8_t mode);
|
||||
void set_morsetx_config(uint8_t mode, uint32_t tone, float fm_delta);
|
||||
void set_morsetx_key(bool key_down);
|
||||
void set_wefax_config(uint8_t lpm, uint8_t ioc);
|
||||
void set_noaaapt_config();
|
||||
void set_flex_config();
|
||||
void set_bitstream_config(uint32_t deviation, uint8_t mode); // mode 0 for am, 1 for 2fsk
|
||||
|
||||
void request_roger_beep();
|
||||
void request_rssi_beep();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -244,8 +244,7 @@ static void portapack_tcxo_enable() {
|
||||
/* Delay >10ms at 96MHz clock speed for reference oscillator to start. */
|
||||
/* Delay an additional 1ms (arbitrary) for the clock generator to detect a signal. */
|
||||
volatile uint32_t delay = 240000 + 24000;
|
||||
while (delay--)
|
||||
;
|
||||
while (delay--);
|
||||
}
|
||||
|
||||
static void portapack_tcxo_disable() {
|
||||
@@ -329,8 +328,7 @@ void ClockManager::init_clock_generator() {
|
||||
: (ref_pll == ClockControl::MultiSynthSource::PLLB)
|
||||
? 0x40
|
||||
: 0x20;
|
||||
while ((clock_generator.device_status() & device_status_mask) != 0)
|
||||
;
|
||||
while ((clock_generator.device_status() & device_status_mask) != 0);
|
||||
|
||||
clock_generator.set_clock_control(
|
||||
clock_generator_output_mcu_clkin,
|
||||
@@ -377,8 +375,7 @@ ClockManager::Reference ClockManager::choose_reference() {
|
||||
if (hackrf_r9) {
|
||||
gpio_r9_clkin_en.write(1);
|
||||
volatile uint32_t delay = 240000 + 24000;
|
||||
while (delay--)
|
||||
;
|
||||
while (delay--);
|
||||
}
|
||||
const auto detected_reference = detect_reference_source();
|
||||
|
||||
@@ -513,8 +510,7 @@ void ClockManager::start_frequency_monitor_measurement(const cgu::CLK_SEL clk_se
|
||||
|
||||
void ClockManager::wait_For_frequency_monitor_measurement_done() {
|
||||
// FREQ_MON mechanism fails to finish if there's no clock present on selected input?!
|
||||
while (LPC_CGU->FREQ_MON.MEAS == 1)
|
||||
;
|
||||
while (LPC_CGU->FREQ_MON.MEAS == 1);
|
||||
}
|
||||
|
||||
uint32_t ClockManager::get_frequency_monitor_measurement_in_hertz() {
|
||||
@@ -559,8 +555,7 @@ void ClockManager::start_audio_pll() {
|
||||
});
|
||||
|
||||
cgu::pll0audio::power_up();
|
||||
while (!cgu::pll0audio::is_locked())
|
||||
;
|
||||
while (!cgu::pll0audio::is_locked());
|
||||
cgu::pll0audio::clock_enable();
|
||||
|
||||
set_base_audio_clock_divider(1);
|
||||
@@ -577,8 +572,7 @@ void ClockManager::set_base_audio_clock_divider(const size_t divisor) {
|
||||
void ClockManager::stop_audio_pll() {
|
||||
cgu::pll0audio::clock_disable();
|
||||
cgu::pll0audio::power_down();
|
||||
while (cgu::pll0audio::is_locked())
|
||||
;
|
||||
while (cgu::pll0audio::is_locked());
|
||||
}
|
||||
|
||||
void ClockManager::enable_clock_output(bool enable) {
|
||||
|
||||
@@ -139,13 +139,11 @@ void runtime_error(uint8_t source) {
|
||||
led.off();
|
||||
|
||||
// wait for DFU button release if pressed, so we don't immediately jump into stack dump
|
||||
while (swizzled_switches() & (1 << (int)Switch::Dfu))
|
||||
;
|
||||
while (swizzled_switches() & (1 << (int)Switch::Dfu));
|
||||
|
||||
while (true) {
|
||||
volatile size_t n = 1000000U;
|
||||
while (n--)
|
||||
;
|
||||
while (n--);
|
||||
led.toggle();
|
||||
|
||||
// Stack dump will cover entire screen, so wait for DFU button press to attempt it
|
||||
@@ -225,8 +223,7 @@ void draw_stack_dump() {
|
||||
// Out of room on the screen or end of stack - allow Up/Down paging.
|
||||
// First wait for button release from previous press.
|
||||
// NOTE: can't call swizzle_switches() with interrupted enabled!
|
||||
while (swizzled_switches() & ((1 << (int)Switch::Right) | (1 << (int)Switch::Left) | (1 << (int)Switch::Down) | (1 << (int)Switch::Up) | (1 << (int)Switch::Sel) | (1 << (int)Switch::Dfu)))
|
||||
;
|
||||
while (swizzled_switches() & ((1 << (int)Switch::Right) | (1 << (int)Switch::Left) | (1 << (int)Switch::Down) | (1 << (int)Switch::Up) | (1 << (int)Switch::Sel) | (1 << (int)Switch::Dfu)));
|
||||
|
||||
painter.draw_string({border, portapack::display.height() - border - 8}, *Theme::getInstance()->bg_darkest_small, "Use UP/DOWN key");
|
||||
|
||||
|
||||
@@ -41,8 +41,7 @@ extern uint32_t __process_stack_end__;
|
||||
|
||||
inline uint32_t get_free_stack_space() {
|
||||
uint32_t* p;
|
||||
for (p = &__process_stack_base__; *p == CRT0_STACKS_FILL_PATTERN && p < &__process_stack_end__; p++)
|
||||
;
|
||||
for (p = &__process_stack_base__; *p == CRT0_STACKS_FILL_PATTERN && p < &__process_stack_end__; p++);
|
||||
auto stack_space_left = p - &__process_stack_base__;
|
||||
|
||||
return stack_space_left;
|
||||
|
||||
+1
-1
@@ -104,7 +104,7 @@ class AdultToysView : public ui::View {
|
||||
/*short_ui*/ true};
|
||||
|
||||
app_settings::SettingsManager settings_{
|
||||
"Adult Toys", app_settings::Mode::TX};
|
||||
"tx_adult_toys", app_settings::Mode::TX};
|
||||
|
||||
OptionsField options_target{
|
||||
{UI_POS_X(6), UI_POS_Y(1)},
|
||||
|
||||
+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++;
|
||||
});
|
||||
|
||||
+1
-1
@@ -118,7 +118,7 @@ class ERTAppView : public View {
|
||||
|
||||
// Prevent painting of region covered entirely by a child.
|
||||
// TODO: Add flag to View that specifies view does not need to be cleared before painting.
|
||||
void paint(Painter&) override{};
|
||||
void paint(Painter&) override {};
|
||||
|
||||
void focus() override;
|
||||
|
||||
|
||||
+46
-5
@@ -91,14 +91,18 @@ 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
|
||||
external/sstvtx/ui_sstvtx.cpp
|
||||
|
||||
#sstvrx
|
||||
external/sstvrx/main.cpp
|
||||
external/sstvrx/ui_sstvrx.cpp
|
||||
|
||||
#random 464 bytes.
|
||||
external/random_password/main.cpp
|
||||
external/random_password/ui_random_password.cpp
|
||||
@@ -218,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
|
||||
@@ -264,6 +272,31 @@ set(EXTCPPSRC
|
||||
#adult_toys_controller 144 bytes
|
||||
external/adult_toys_controller/main.cpp
|
||||
external/adult_toys_controller/ui_adult_toys_controller.cpp
|
||||
|
||||
#flex_rx
|
||||
external/flex_rx/main.cpp
|
||||
external/flex_rx/ui_flex_rx.cpp
|
||||
|
||||
#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
|
||||
@@ -288,8 +321,9 @@ set(EXTAPPLIST
|
||||
tpmsrx
|
||||
protoview
|
||||
adsbtx
|
||||
morse_tx
|
||||
#morse_tx
|
||||
sstvtx
|
||||
sstvrx
|
||||
random_password
|
||||
# acars_rx --not working
|
||||
wefax_rx
|
||||
@@ -319,6 +353,7 @@ set(EXTAPPLIST
|
||||
scanner
|
||||
level
|
||||
gfxeq
|
||||
waterfall_designer
|
||||
detector_rx
|
||||
spaceinv
|
||||
blackjack
|
||||
@@ -330,4 +365,10 @@ set(EXTAPPLIST
|
||||
bht_tx
|
||||
morse_practice
|
||||
adult_toys_controller
|
||||
flex_rx
|
||||
subcarrx
|
||||
siggen
|
||||
sdusb
|
||||
morse_radio
|
||||
morseradiotx
|
||||
)
|
||||
|
||||
+56
-1
@@ -86,7 +86,14 @@ MEMORY
|
||||
ram_external_app_bht_tx (rwx) : org = 0xADED0000, len = 32k
|
||||
ram_external_app_morse_practice (rwx) : org = 0xADEE0000, len = 32k
|
||||
ram_external_app_adult_toys_controller (rwx) : org = 0xADEF0000, len = 32k
|
||||
|
||||
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
|
||||
@@ -397,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));
|
||||
@@ -471,5 +484,47 @@ SECTIONS
|
||||
*(*ui*external_app*adult_toys_controller*);
|
||||
} > ram_external_app_adult_toys_controller
|
||||
|
||||
.external_app_flex_rx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_flex_rx.application_information));
|
||||
*(*ui*external_app*flex_rx*);
|
||||
} > ram_external_app_flex_rx
|
||||
|
||||
.external_app_sstvrx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_sstvrx.application_information));
|
||||
*(*ui*external_app*sstvrx*);
|
||||
} > ram_external_app_sstvrx
|
||||
|
||||
.external_app_subcarrx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_subcarrx.application_information));
|
||||
*(*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
|
||||
|
||||
}
|
||||
|
||||
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright (C) 2025 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.hpp"
|
||||
#include "ui_flex_rx.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "external_app.hpp"
|
||||
|
||||
namespace ui::external_app::flex_rx {
|
||||
void initialize_app(ui::NavigationView& nav) {
|
||||
nav.push<FlexAppView>();
|
||||
}
|
||||
} // namespace ui::external_app::flex_rx
|
||||
|
||||
extern "C" {
|
||||
|
||||
__attribute__((section(".external_app.app_flex_rx.application_information"), used)) application_information_t _application_information_flex_rx = {
|
||||
/*.memory_location = */ (uint8_t*)0x00000000,
|
||||
/*.externalAppEntry = */ ui::external_app::flex_rx::initialize_app,
|
||||
/*.header_version = */ CURRENT_HEADER_VERSION,
|
||||
/*.app_version = */ VERSION_MD5,
|
||||
|
||||
/*.app_name = */ "FLEX RX",
|
||||
/*.bitmap_data = */ {
|
||||
0x00,
|
||||
0x00,
|
||||
0xFE,
|
||||
0x7F,
|
||||
0x02,
|
||||
0x40,
|
||||
0xFA,
|
||||
0x5F,
|
||||
0x02,
|
||||
0x40,
|
||||
0xF2,
|
||||
0x4F,
|
||||
0x02,
|
||||
0x40,
|
||||
0xE2,
|
||||
0x47,
|
||||
0x02,
|
||||
0x40,
|
||||
0xC2,
|
||||
0x43,
|
||||
0x02,
|
||||
0x40,
|
||||
0x82,
|
||||
0x41,
|
||||
0x02,
|
||||
0x40,
|
||||
0xFE,
|
||||
0x7F,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
},
|
||||
/*.icon_color = */ ui::Color::orange().v,
|
||||
/*.menu_location = */ app_location_t::RX,
|
||||
/*.desired_menu_position = */ -1,
|
||||
|
||||
/*.m4_app_tag = portapack::spi_flash::image_tag_flex */ {'P', 'F', 'L', 'X'},
|
||||
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
#include "ui_flex_rx.hpp"
|
||||
|
||||
#include "baseband_api.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "memory_map.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
|
||||
namespace ui::external_app::flex_rx {
|
||||
|
||||
FlexAppView::FlexAppView(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
// Load baseband image for FLEX decoding
|
||||
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
|
||||
|
||||
add_children({&field_frequency,
|
||||
&field_rf_amp,
|
||||
&field_lna,
|
||||
&field_vga,
|
||||
&rssi,
|
||||
&console});
|
||||
|
||||
// Restore saved frequency
|
||||
field_frequency.set_value(frequency_value);
|
||||
receiver_model.set_target_frequency(frequency_value);
|
||||
|
||||
// Frequency change callback
|
||||
field_frequency.updated = [this](rf::Frequency f) {
|
||||
update_freq(f);
|
||||
};
|
||||
|
||||
// Configure receiver
|
||||
receiver_model.set_sampling_rate(3072000);
|
||||
receiver_model.set_baseband_bandwidth(1750000);
|
||||
receiver_model.enable();
|
||||
receiver_model.set_squelch_level(0);
|
||||
|
||||
// Initialize FLEX baseband
|
||||
baseband::set_flex_config();
|
||||
|
||||
console.writeln("Ready");
|
||||
}
|
||||
|
||||
FlexAppView::~FlexAppView() {
|
||||
receiver_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
void FlexAppView::focus() {
|
||||
field_frequency.focus();
|
||||
}
|
||||
|
||||
// Redraw all messages to console
|
||||
void FlexAppView::redraw_console() {
|
||||
console.clear(true);
|
||||
bool first = true;
|
||||
for (const auto& msg : messages) {
|
||||
if (!first) {
|
||||
console.writeln(""); // Blank line between messages
|
||||
}
|
||||
first = false;
|
||||
console.writeln(msg);
|
||||
}
|
||||
}
|
||||
|
||||
// Add message to log with automatic line wrapping
|
||||
void FlexAppView::log_message(const std::string& message) {
|
||||
const size_t chars_per_line = screen_width / 8;
|
||||
// Console height accounts for status bar and controls row
|
||||
const size_t console_lines = (screen_height - 2 * 16) / 16;
|
||||
|
||||
messages.push_back(message);
|
||||
|
||||
// Calculate total lines used (messages + blank lines between them)
|
||||
size_t total_lines = 0;
|
||||
for (size_t i = 0; i < messages.size(); i++) {
|
||||
if (i > 0) total_lines++; // Count blank line separator
|
||||
size_t msg_lines = (messages[i].length() + chars_per_line - 1) / chars_per_line;
|
||||
if (msg_lines == 0) msg_lines = 1;
|
||||
total_lines += msg_lines;
|
||||
}
|
||||
|
||||
// If console would overflow, remove oldest messages and redraw
|
||||
if (total_lines > console_lines) {
|
||||
while (total_lines > console_lines && !messages.empty()) {
|
||||
const auto& oldest = messages.front();
|
||||
size_t oldest_lines = (oldest.length() + chars_per_line - 1) / chars_per_line;
|
||||
if (oldest_lines == 0) oldest_lines = 1;
|
||||
total_lines -= oldest_lines;
|
||||
if (messages.size() > 1) total_lines--; // Remove separator line too
|
||||
messages.erase(messages.begin());
|
||||
}
|
||||
redraw_console();
|
||||
} else {
|
||||
// Just append new message
|
||||
if (messages.size() > 1) {
|
||||
console.writeln(""); // Blank line before new message
|
||||
}
|
||||
console.writeln(message);
|
||||
}
|
||||
}
|
||||
|
||||
// Update frequency and save for persistence
|
||||
void FlexAppView::update_freq(rf::Frequency f) {
|
||||
frequency_value = f;
|
||||
receiver_model.set_target_frequency(f);
|
||||
}
|
||||
|
||||
// Handle decoded FLEX packet from baseband
|
||||
void FlexAppView::on_packet(const FlexPacketMessage* message) {
|
||||
log_message(message->packet.message);
|
||||
}
|
||||
|
||||
// Handle stats message (currently unused)
|
||||
void FlexAppView::on_stats(const FlexStatsMessage*) {
|
||||
}
|
||||
|
||||
// Debug handler - uncomment to see baseband debug messages
|
||||
void FlexAppView::on_debug(const FlexDebugMessage* message) {
|
||||
(void)message; // Suppress unused parameter warning
|
||||
// std::string text = "DBG: ";
|
||||
// text += message->text;
|
||||
// text += " " + to_string_hex(message->val1, 8);
|
||||
// text += " " + to_string_hex(message->val2, 8);
|
||||
// log_message(text);
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::flex_rx
|
||||
@@ -0,0 +1,97 @@
|
||||
#ifndef __UI_FLEX_RX_H__
|
||||
#define __UI_FLEX_RX_H__
|
||||
|
||||
#include "ui_widget.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_freq_field.hpp"
|
||||
#include "ui_rssi.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace ui::external_app::flex_rx {
|
||||
|
||||
class FlexAppView : public View {
|
||||
public:
|
||||
FlexAppView(NavigationView& nav);
|
||||
~FlexAppView();
|
||||
|
||||
void focus() override;
|
||||
std::string title() const override { return "FLEX RX"; };
|
||||
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
|
||||
// Saved settings
|
||||
rf::Frequency frequency_value{931740000}; // Default FLEX frequency
|
||||
|
||||
RxRadioState radio_state_{};
|
||||
|
||||
// Message storage for console redraw
|
||||
static constexpr size_t MAX_MESSAGES = 20;
|
||||
std::vector<std::string> messages{};
|
||||
|
||||
// Helper methods
|
||||
void log_message(const std::string& message);
|
||||
void redraw_console();
|
||||
void update_freq(rf::Frequency f);
|
||||
|
||||
// UI Elements - Row 0, dynamically positioned
|
||||
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), 0, UI_POS_WIDTH(9), 4}};
|
||||
|
||||
// Message display area (below controls, account for status bar)
|
||||
Console console{
|
||||
{0, 1 * 16, screen_width, screen_height - 2 * 16}};
|
||||
|
||||
// Persistent settings manager
|
||||
app_settings::SettingsManager settings_{
|
||||
"rx_flex",
|
||||
app_settings::Mode::RX,
|
||||
{{"frequency", &frequency_value}}};
|
||||
|
||||
// Message handlers
|
||||
void on_packet(const FlexPacketMessage* message);
|
||||
void on_stats(const FlexStatsMessage* message);
|
||||
void on_debug(const FlexDebugMessage* message);
|
||||
|
||||
// Message handler registrations
|
||||
MessageHandlerRegistration message_handler_packet{
|
||||
Message::ID::FlexPacket,
|
||||
[this](const Message* const p) {
|
||||
const auto message = *static_cast<const FlexPacketMessage*>(p);
|
||||
this->on_packet(&message);
|
||||
}};
|
||||
|
||||
MessageHandlerRegistration message_handler_stats{
|
||||
Message::ID::FlexStats,
|
||||
[this](const Message* const p) {
|
||||
const auto message = *static_cast<const FlexStatsMessage*>(p);
|
||||
this->on_stats(&message);
|
||||
}};
|
||||
|
||||
MessageHandlerRegistration message_handler_debug{
|
||||
Message::ID::FlexDebug,
|
||||
[this](const Message* const p) {
|
||||
const auto message = *static_cast<const FlexDebugMessage*>(p);
|
||||
this->on_debug(&message);
|
||||
}};
|
||||
};
|
||||
|
||||
} // namespace ui::external_app::flex_rx
|
||||
|
||||
#endif /*__UI_FLEX_RX_H__*/
|
||||
@@ -80,7 +80,7 @@ bool FlipperTxView::on_file_changed(std::filesystem::path new_file_path) {
|
||||
return false;
|
||||
}
|
||||
preset = submeta.value().preset;
|
||||
if (preset != FLIPPER_PRESET_OOK) {
|
||||
if (preset != FLIPPER_PRESET_OOK && preset != FLIPPER_PRESET_2FSK) {
|
||||
field_filename.set_text("File: err, not supp. preset");
|
||||
return false;
|
||||
}
|
||||
@@ -108,8 +108,13 @@ void FlipperTxView::stop() {
|
||||
bool FlipperTxView::start() {
|
||||
if (filename.empty()) return false;
|
||||
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
|
||||
transmitter_model.set_sampling_rate(OOK_SAMPLERATE);
|
||||
transmitter_model.enable();
|
||||
button_startstop.set_text(LanguageHelper::currentMessages[LANG_STOP]);
|
||||
uint8_t mode = 0;
|
||||
if (preset == FLIPPER_PRESET_2FSK)
|
||||
mode = 1;
|
||||
baseband::set_bitstream_config(FM_DEVIATION, mode);
|
||||
// start thread
|
||||
replay_thread = std::make_unique<FlipperPlayThread>(
|
||||
filename,
|
||||
|
||||
@@ -28,6 +28,8 @@ using namespace ui;
|
||||
namespace ui::external_app::flippertx {
|
||||
|
||||
#define OOK_SAMPLERATE 2280000U
|
||||
#define FM_DEVIATION 60000U
|
||||
|
||||
class FlipperPlayThread;
|
||||
class FlipperTxView : public View {
|
||||
public:
|
||||
|
||||
+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,282 @@
|
||||
#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);
|
||||
};
|
||||
|
||||
field_squelch.on_change = [this](int32_t v) {
|
||||
receiver_model.set_squelch_level(v);
|
||||
};
|
||||
|
||||
options_mode.on_change = [this](size_t, int32_t mode) {
|
||||
audio::output::stop();
|
||||
receiver_model.disable();
|
||||
morse_decoder_.resetLearning();
|
||||
if (mode == MORSE_NFM) {
|
||||
receiver_model.set_am_configuration(4);
|
||||
receiver_model.set_modulation(ReceiverModel::Mode::NarrowbandFMAudio);
|
||||
field_squelch.set_style(Theme::getInstance()->option_active);
|
||||
field_squelch.set_focusable(true);
|
||||
field_squelch.set_value(receiver_model.squelch_level());
|
||||
audio::set_rate(audio::Rate::Hz_24000);
|
||||
} else {
|
||||
audio::set_rate(audio::Rate::Hz_12000);
|
||||
receiver_model.set_modulation(ReceiverModel::Mode::AMAudio);
|
||||
if (mode == MORSE_AM_CW || mode == MORSE_AM_DSB)
|
||||
receiver_model.set_am_configuration(7);
|
||||
else if (mode == MORSE_AM_USB)
|
||||
receiver_model.set_am_configuration(9);
|
||||
else // LSB
|
||||
receiver_model.set_am_configuration(10);
|
||||
field_squelch.set_style(Theme::getInstance()->fg_dark);
|
||||
field_squelch.set_focusable(false);
|
||||
}
|
||||
baseband::set_moreserx_config(mode);
|
||||
saved_mode = mode;
|
||||
|
||||
audio::output::start();
|
||||
receiver_model.set_headphone_volume(receiver_model.headphone_volume()); // WM8731 hack.
|
||||
|
||||
receiver_model.set_sampling_rate(3072000);
|
||||
receiver_model.set_baseband_bandwidth(1750000);
|
||||
receiver_model.enable();
|
||||
};
|
||||
options_mode.set_selected_index(saved_mode);
|
||||
|
||||
logger = std::make_unique<MorseLogger>();
|
||||
chk_log.on_select = [this](Checkbox&, bool save) {
|
||||
save_log = save;
|
||||
if (logger && save_log)
|
||||
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(const std::string& morse_mode) {
|
||||
int64_t freq = receiver_model.target_frequency();
|
||||
std::string header = "Freq:" + to_string_rounded_freq(freq, 4);
|
||||
header += "MHz, ";
|
||||
header += "RX MODE:" + morse_mode;
|
||||
header += "\r\nMessage:";
|
||||
log_file.write_raw(header);
|
||||
}
|
||||
|
||||
bool MorseLogger::on_packet(const std::string& content, bool time, const std::string& morse_mode) {
|
||||
if (!time) {
|
||||
log_file.write_raw_no_newline("\r\n\r\n");
|
||||
auto timestamp = to_string_datetime(rtc_time::now(), YMDHMS);
|
||||
log_file.write_raw("[" + timestamp + "] ");
|
||||
radio_set_log(morse_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, options_mode.selected_index_name());
|
||||
}
|
||||
float dah_time = morse_decoder_.getCurrentTimeUnit() * 3.0f;
|
||||
if (dah_time > 0) {
|
||||
uint16_t wpm = (uint16_t)(3600.0f / (dah_time + 18.0f) + 0.5f);
|
||||
txt_speed.set(to_string_dec_uint(wpm));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MorseRadioView::on_freq(const MorseRXfreqMessage* message) {
|
||||
std::string ret = " ";
|
||||
uint32_t freq = message->measured_frequency;
|
||||
if (freq < 301) {
|
||||
ret = "<";
|
||||
freq = 300;
|
||||
}
|
||||
if (freq > 2299) {
|
||||
freq = 2300;
|
||||
ret = ">";
|
||||
}
|
||||
if (freq < 400 || freq > 1400)
|
||||
txt_freq.set_style(Theme::getInstance()->fg_red);
|
||||
else if (freq <= 580 || freq >= 1220)
|
||||
txt_freq.set_style(Theme::getInstance()->fg_yellow);
|
||||
else
|
||||
txt_freq.set_style(Theme::getInstance()->fg_green);
|
||||
ret += to_string_dec_uint(freq);
|
||||
txt_freq.set(ret);
|
||||
}
|
||||
|
||||
void MorseRadioView::writeCharToConsole(const std::string& ch, double confidence) {
|
||||
if (ch.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
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,188 @@
|
||||
/*
|
||||
* 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, const std::string& morse_mode);
|
||||
void radio_set_log(const std::string& morse_mode);
|
||||
|
||||
private:
|
||||
LogFile log_file{};
|
||||
uint8_t char_count{0}; // log file line break
|
||||
};
|
||||
|
||||
class MorseRadioView : public ui::View {
|
||||
public:
|
||||
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{};
|
||||
|
||||
enum morse_modes : uint8_t {
|
||||
MORSE_AM_CW = 0,
|
||||
MORSE_NFM,
|
||||
MORSE_AM_DSB,
|
||||
MORSE_AM_USB,
|
||||
MORSE_AM_LSB,
|
||||
};
|
||||
uint8_t saved_mode = MORSE_AM_CW;
|
||||
|
||||
app_settings::SettingsManager settings_{
|
||||
"rx_morse_radio",
|
||||
app_settings::Mode::RX,
|
||||
{
|
||||
{"cwmode"sv, &saved_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(8), UI_POS_Y(2) + 4}, // +4 to align with 'Log' checkbox text
|
||||
6,
|
||||
{
|
||||
{"AM/CW", MORSE_AM_CW},
|
||||
{"NFM", MORSE_NFM},
|
||||
{"AM/DSB", MORSE_AM_DSB},
|
||||
{"AM/USB", MORSE_AM_USB},
|
||||
{"AM/LSB", MORSE_AM_LSB},
|
||||
}};
|
||||
|
||||
Checkbox chk_log{{UI_POS_X(0), UI_POS_Y(2)}, 12, "Log", false};
|
||||
ui::Button btn_clear{{UI_POS_X(0), UI_POS_Y_BOTTOM(2), UI_POS_WIDTH(6), UI_POS_HEIGHT(1)}, "CLR"};
|
||||
|
||||
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__*/
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright (C) 2025 StarVore Labs
|
||||
*
|
||||
* 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_sstvrx.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "external_app.hpp"
|
||||
|
||||
namespace ui::external_app::sstvrx {
|
||||
|
||||
void initialize_app(NavigationView& nav) {
|
||||
nav.push<SstvRxView>();
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::sstvrx
|
||||
|
||||
extern "C" {
|
||||
|
||||
// Az alkalmazás információ C-linkage-ként, hogy a firmware hívhassa
|
||||
__attribute__((section(".external_app.app_sstvrx.application_information"), used))
|
||||
application_information_t _application_information_sstvrx = {
|
||||
/*.memory_location = */ (uint8_t*)0x00000000,
|
||||
/*.externalAppEntry = */ ui::external_app::sstvrx::initialize_app,
|
||||
/*.header_version = */ CURRENT_HEADER_VERSION,
|
||||
/*.app_version = */ VERSION_MD5,
|
||||
|
||||
/*.app_name = */ "SSTV RX",
|
||||
/*.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::yellow().v,
|
||||
/*.menu_location = */ app_location_t::RX,
|
||||
/*.desired_menu_position = */ -1,
|
||||
|
||||
/*.m4_app_tag = portapack::spi_flash::image_tag_none */ {'P', 'S', 'R', 'X'},
|
||||
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
|
||||
};
|
||||
|
||||
} // extern "C"
|
||||
+510
@@ -0,0 +1,510 @@
|
||||
/*
|
||||
* Copyright (C) 2025 StarVore Labs
|
||||
*
|
||||
* 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_sstvrx.hpp"
|
||||
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
#include "portapack.hpp"
|
||||
#include "hackrf_hal.hpp"
|
||||
#include "file_path.hpp"
|
||||
#include "message.hpp"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <stdio.h>
|
||||
|
||||
using namespace portapack;
|
||||
using namespace modems;
|
||||
using namespace ui;
|
||||
|
||||
#if SSTVRX_ENABLE_LOGGER
|
||||
#define SSTVRX_LOG_INFO(msg) \
|
||||
do { \
|
||||
if (logger) { \
|
||||
logger->log_info(msg); \
|
||||
} \
|
||||
} while (0)
|
||||
#define SSTVRX_LOG_ERROR(msg) \
|
||||
do { \
|
||||
if (logger) { \
|
||||
logger->log_error(msg); \
|
||||
} \
|
||||
} while (0)
|
||||
#else
|
||||
#define SSTVRX_LOG_INFO(msg) \
|
||||
do { \
|
||||
(void)sizeof(msg); \
|
||||
} while (0)
|
||||
#define SSTVRX_LOG_ERROR(msg) \
|
||||
do { \
|
||||
(void)sizeof(msg); \
|
||||
} while (0)
|
||||
#endif
|
||||
|
||||
// SSTV RX View Implementation
|
||||
namespace ui::external_app::sstvrx {
|
||||
|
||||
static_assert(sizeof(shared_memory.bb_data.data) == 512,
|
||||
"SSTV shared buffer size mismatch");
|
||||
|
||||
#if SSTVRX_ENABLE_LOGGER
|
||||
void SstvRxLogger::log_error(const std::string& error_message) {
|
||||
log_file.write_entry(rtc_time::now(), "ERROR: " + error_message);
|
||||
}
|
||||
|
||||
void SstvRxLogger::log_info(const std::string& info_message) {
|
||||
log_file.write_entry(rtc_time::now(), "INFO: " + info_message);
|
||||
}
|
||||
#endif
|
||||
|
||||
SstvRxView::SstvRxView(ui::NavigationView& nav)
|
||||
: nav_(nav) {
|
||||
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
|
||||
DISPLAY_HEIGHT = screen_height - SSTV_IMG_START_ROW * 16 - 16;
|
||||
DISPLAY_WIDTH = screen_width;
|
||||
add_children({&field_rf_amp,
|
||||
&field_lna,
|
||||
&field_vga,
|
||||
&rssi,
|
||||
&channel,
|
||||
&field_frequency,
|
||||
&field_volume,
|
||||
&audio,
|
||||
&start_stop_btn,
|
||||
&options_mode,
|
||||
&field_phase,
|
||||
&field_slant,
|
||||
&labels,
|
||||
&text_calibration});
|
||||
|
||||
// Initialize audio with proper rate for SSTV
|
||||
audio::set_rate(audio::Rate::Hz_48000);
|
||||
audio::output::start();
|
||||
|
||||
// Configure receiver with optimal settings for SSTV
|
||||
// NOTE: Do NOT set modulation mode - SSTV uses a custom baseband processor
|
||||
// Standard sampling rate (3.072MHz) with wide bandwidth to capture full SSTV audio spectrum
|
||||
// SSTV uses 1200-2300 Hz tones, so we need wide baseband to avoid distortion
|
||||
receiver_model.set_sampling_rate(3072000);
|
||||
receiver_model.set_baseband_bandwidth(1750000); // Standard wideband setting
|
||||
receiver_model.set_squelch_level(1);
|
||||
receiver_model.set_hidden_offset(0); // No offset needed
|
||||
|
||||
// Field values will be set in on_show() to ensure proper initialization
|
||||
|
||||
using option_t = std::pair<std::string, int32_t>;
|
||||
using options_t = std::vector<option_t>;
|
||||
options_t mode_options;
|
||||
uint32_t c;
|
||||
|
||||
// Start/Stop button handler - toggles between start and stop
|
||||
start_stop_btn.on_select = [this](Button&) {
|
||||
start_stop_btn.focus();
|
||||
on_start_stop();
|
||||
};
|
||||
|
||||
// Initialize frequency field from settings or use default
|
||||
if (settings_.loaded() && settings_.raw().rx_frequency != 0) {
|
||||
field_frequency.set_value(settings_.raw().rx_frequency);
|
||||
} else if (field_frequency.value() == 0) {
|
||||
field_frequency.set_value(145800000); // Default to 145.800 MHz (ISS)
|
||||
}
|
||||
field_frequency.set_step(25000);
|
||||
|
||||
// Populate mode list
|
||||
for (c = 0; c < SSTV_MODES_NB; c++)
|
||||
mode_options.emplace_back(sstv_modes[c].name, c);
|
||||
options_mode.set_options(mode_options);
|
||||
|
||||
options_mode.on_change = [this](size_t i, int32_t) {
|
||||
this->on_mode_changed(i);
|
||||
};
|
||||
options_mode.set_selected_index(1); // Scottie 2
|
||||
on_mode_changed(1);
|
||||
|
||||
// Initialize phase and slant controls from loaded settings
|
||||
field_phase.set_value(phase_adjustment);
|
||||
field_phase.on_change = [this](int32_t v) {
|
||||
phase_adjustment = v;
|
||||
if (is_receiving) {
|
||||
baseband::set_sstvrx_phase_slant(phase_adjustment, slant_adjustment);
|
||||
} else if (max_received_line > 0) {
|
||||
// Auto-redraw when adjusting after reception
|
||||
redraw_image();
|
||||
}
|
||||
};
|
||||
|
||||
field_slant.set_value(slant_adjustment);
|
||||
field_slant.on_change = [this](int32_t v) {
|
||||
slant_adjustment = v;
|
||||
if (is_receiving) {
|
||||
baseband::set_sstvrx_phase_slant(phase_adjustment, slant_adjustment);
|
||||
} else if (max_received_line > 0) {
|
||||
// Auto-redraw when adjusting after reception
|
||||
redraw_image();
|
||||
}
|
||||
};
|
||||
|
||||
#if SSTVRX_ENABLE_LOGGER
|
||||
logger = std::make_unique<SstvRxLogger>();
|
||||
if (logger) {
|
||||
logger->append(logs_dir / "SSTVRX.txt");
|
||||
logger->log_info("----------SSTV RX Started----------");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Destructor: Ensure reception is stopped
|
||||
SstvRxView::~SstvRxView() {
|
||||
is_receiving = true;
|
||||
on_stop();
|
||||
baseband::shutdown();
|
||||
SSTVRX_LOG_INFO("SSTV RX Stopped");
|
||||
}
|
||||
|
||||
void SstvRxView::on_show() {
|
||||
// Update field values from receiver model to reflect loaded settings
|
||||
field_lna.set_value(receiver_model.lna());
|
||||
field_vga.set_value(receiver_model.vga());
|
||||
field_rf_amp.set_value(receiver_model.rf_amp());
|
||||
field_volume.set_value(receiver_model.normalized_headphone_volume());
|
||||
}
|
||||
|
||||
void SstvRxView::focus() {
|
||||
field_frequency.focus();
|
||||
}
|
||||
|
||||
// Combined start/stop handler - toggles based on current state
|
||||
void SstvRxView::on_start_stop() {
|
||||
if (is_receiving) {
|
||||
// Currently receiving - stop it
|
||||
on_stop();
|
||||
start_stop_btn.set_text("Start RX");
|
||||
} else {
|
||||
// Currently stopped - start reception
|
||||
SSTVRX_LOG_INFO("Starting SSTV RX Reception");
|
||||
start_audio();
|
||||
start_stop_btn.set_text("Stop RX");
|
||||
}
|
||||
}
|
||||
|
||||
// Stop NFM audio reception
|
||||
void SstvRxView::on_stop() {
|
||||
SSTVRX_LOG_INFO("Stopping SSTV RX Reception");
|
||||
if (is_receiving) {
|
||||
// Stop in reverse order of start
|
||||
receiver_model.disable();
|
||||
audio::output::stop();
|
||||
|
||||
// Reset state
|
||||
is_receiving = false;
|
||||
|
||||
// Close image file if still open
|
||||
bmp.close();
|
||||
|
||||
pending_line_valid = false;
|
||||
pending_chunk_mask = 0;
|
||||
std::fill(pending_line_rgb.begin(), pending_line_rgb.end(), 0);
|
||||
*reinterpret_cast<volatile uint8_t*>(&shared_memory.bb_data.data[CHUNK_FLAG_INDEX]) = 0;
|
||||
|
||||
SSTVRX_LOG_INFO("SSTV RX Reception Stopped");
|
||||
} else {
|
||||
SSTVRX_LOG_ERROR("SSTV RX Reception Not Running");
|
||||
}
|
||||
}
|
||||
|
||||
// Start NFM audio reception
|
||||
void SstvRxView::start_audio() {
|
||||
SSTVRX_LOG_INFO("Configuring SSTV RX Audio Reception");
|
||||
|
||||
// Configure the baseband processor with VIS code
|
||||
if (rx_sstv_mode) {
|
||||
baseband::set_sstvrx_data(rx_sstv_mode->vis_code);
|
||||
SSTVRX_LOG_INFO("Sent VIS code to processor: " + to_string_dec_uint(rx_sstv_mode->vis_code));
|
||||
}
|
||||
|
||||
// Send phase and slant adjustments
|
||||
baseband::set_sstvrx_phase_slant(phase_adjustment, slant_adjustment);
|
||||
|
||||
// Initialize audio path
|
||||
audio::output::stop();
|
||||
audio::set_rate(audio::Rate::Hz_48000);
|
||||
|
||||
// Set audio routing and volume
|
||||
// audio::output::start();
|
||||
|
||||
// Clear display area and reset line counter
|
||||
portapack::display.fill_rectangle(
|
||||
{0, SSTV_IMG_START_ROW * 16, DISPLAY_WIDTH, DISPLAY_HEIGHT},
|
||||
{0, 0, 0});
|
||||
line_num = 0;
|
||||
file_line_num = 0;
|
||||
max_received_line = 0;
|
||||
pending_line_valid = false;
|
||||
pending_chunk_mask = 0;
|
||||
std::fill(pending_line_rgb.begin(), pending_line_rgb.end(), 0);
|
||||
*reinterpret_cast<volatile uint8_t*>(&shared_memory.bb_data.data[CHUNK_FLAG_INDEX]) = 0;
|
||||
|
||||
// Clear calibration display
|
||||
text_calibration.set("Calibrating...");
|
||||
|
||||
// Initialize new image file
|
||||
current_line_rx = 0;
|
||||
auto timestamp = to_string_timestamp(rtc_time::now());
|
||||
auto dir_error = ensure_directory(sstv_dir / "RX");
|
||||
if (!dir_error.ok()) {
|
||||
SSTVRX_LOG_ERROR("Failed to create directory: SSTV/RX");
|
||||
}
|
||||
current_image_path = sstv_dir / ("RX/SSTV_" + timestamp + ".bmp");
|
||||
auto ok = bmp.create(current_image_path, IMAGE_WIDTH, 1);
|
||||
if (!ok) {
|
||||
SSTVRX_LOG_ERROR("Failed to create file: " + current_image_path.string());
|
||||
bmp.close();
|
||||
}
|
||||
|
||||
// Start audio output
|
||||
audio::output::start();
|
||||
audio::headphone::set_volume(persistent_memory::headphone_volume());
|
||||
|
||||
// Keep ReceiverModel in capture mode so it doesn't override our custom baseband/audio configuration.
|
||||
receiver_model.set_modulation(ReceiverModel::Mode::Capture);
|
||||
|
||||
// Enable receiver last
|
||||
receiver_model.enable();
|
||||
is_receiving = true;
|
||||
SSTVRX_LOG_INFO("SSTV RX Started");
|
||||
}
|
||||
|
||||
void SstvRxView::on_mode_changed(const size_t index) {
|
||||
rx_sstv_mode = &sstv_modes[index];
|
||||
}
|
||||
|
||||
void SstvRxView::write_line_to_file(uint16_t line_num, const uint8_t* rgb_line) {
|
||||
(void)line_num;
|
||||
if (!bmp.is_loaded()) return;
|
||||
// Ensure BMP height is sufficient
|
||||
if (bmp.get_real_height() <= file_line_num) {
|
||||
bmp.expand_y(file_line_num + 1);
|
||||
}
|
||||
bmp.seek(0, file_line_num);
|
||||
|
||||
// Write RGB data in BGR order
|
||||
for (uint16_t x = 0; x < IMAGE_WIDTH; x++) {
|
||||
uint8_t r = rgb_line[x * 3 + 0];
|
||||
uint8_t g = rgb_line[x * 3 + 1];
|
||||
uint8_t b = rgb_line[x * 3 + 2];
|
||||
Color px(g, b, r);
|
||||
bmp.write_next_px(px);
|
||||
}
|
||||
file_line_num++;
|
||||
}
|
||||
|
||||
void SstvRxView::update_display(uint16_t current_line, const uint8_t* rgb_line) {
|
||||
if (current_line >= IMAGE_HEIGHT) return;
|
||||
|
||||
// Reset line counter if we reach the bottom of display
|
||||
if (line_num >= DISPLAY_HEIGHT) {
|
||||
line_num = 0;
|
||||
}
|
||||
|
||||
// Scale line to display width
|
||||
for (uint16_t x = 0; x < DISPLAY_WIDTH; x++) {
|
||||
// Scale x coordinate
|
||||
uint16_t src_x = (x * IMAGE_WIDTH) / DISPLAY_WIDTH;
|
||||
if (src_x >= IMAGE_WIDTH) continue;
|
||||
|
||||
// Get RGB values from interleaved data [R,G,B,R,G,B,...]
|
||||
uint8_t r = rgb_line[src_x * 3 + 0];
|
||||
uint8_t g = rgb_line[src_x * 3 + 1];
|
||||
uint8_t b = rgb_line[src_x * 3 + 2];
|
||||
// Display uses BGR order like BMP format
|
||||
// line_buffer[x] = Color(b, r, g);
|
||||
line_buffer[x] = Color(g, b, r);
|
||||
}
|
||||
|
||||
// Render the line at the current position
|
||||
portapack::display.render_line(
|
||||
{0, line_num + SSTV_IMG_START_ROW * 16},
|
||||
DISPLAY_WIDTH,
|
||||
line_buffer);
|
||||
|
||||
// Increment line counter
|
||||
line_num++;
|
||||
}
|
||||
void SstvRxView::redraw_image() {
|
||||
// Disabled: Post-reception redraw requires 245KB buffer which exceeds M0 memory
|
||||
// Phase and slant adjustments must be set before reception starts
|
||||
}
|
||||
|
||||
void SstvRxView::on_progress(uint16_t line, uint16_t total_lines) {
|
||||
if (!is_receiving) {
|
||||
if (line < 0xFFF0) {
|
||||
*reinterpret_cast<volatile uint8_t*>(&shared_memory.bb_data.data[CHUNK_FLAG_INDEX]) = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle debug messages
|
||||
if (line == 0xFFFF) {
|
||||
SSTVRX_LOG_ERROR("Processor not configured");
|
||||
return;
|
||||
}
|
||||
if (line == 0xFFFE) {
|
||||
SSTVRX_LOG_INFO("Sync pulse duration: " + to_string_dec_uint(total_lines) + " samples");
|
||||
return;
|
||||
}
|
||||
if (line == 0xFFFD) {
|
||||
SSTVRX_LOG_INFO("Sync detected, count=" + to_string_dec_uint(total_lines));
|
||||
text_calibration.set("Syncs: " + to_string_dec_uint(total_lines));
|
||||
return;
|
||||
}
|
||||
if (line == 0xFFFC) {
|
||||
SSTVRX_LOG_INFO("Expected interval: " + to_string_dec_uint(total_lines) + " samples");
|
||||
return;
|
||||
}
|
||||
if (line == 0xFFFB) {
|
||||
SSTVRX_LOG_INFO("Actual interval: " + to_string_dec_uint(total_lines) + " samples");
|
||||
return;
|
||||
}
|
||||
if (line == 0xFFFA) {
|
||||
SSTVRX_LOG_INFO("SYNC TIMEOUT after " + to_string_dec_uint(total_lines) + " samples - continuing without sync");
|
||||
return;
|
||||
}
|
||||
if (line == 0xFFF9) {
|
||||
SSTVRX_LOG_INFO("Detected frequency at sync: " + to_string_dec_uint(total_lines) + " Hz");
|
||||
return;
|
||||
}
|
||||
if (line == 0xFFF8) {
|
||||
SSTVRX_LOG_INFO("OUTLIER REJECTED: interval=" + to_string_dec_uint(total_lines) + " samples (expected 10000-15000)");
|
||||
return;
|
||||
}
|
||||
if (line == 0xFFF7) {
|
||||
SSTVRX_LOG_INFO("PRE-RECORD: sync_history_count=" + to_string_dec_uint(total_lines));
|
||||
return;
|
||||
}
|
||||
if (line == 0xFFF6) {
|
||||
SSTVRX_LOG_INFO("MAX_SYNC_HISTORY EXCEEDED: count=" + to_string_dec_uint(total_lines));
|
||||
return;
|
||||
}
|
||||
if (line == 0xFFF5) {
|
||||
// Decode rejection reason bit flags: 0x1=interval, 0x2=freq, 0x4=duration
|
||||
std::string reasons = "";
|
||||
if (total_lines & 0x1) reasons += "interval ";
|
||||
if (total_lines & 0x2) reasons += "freq ";
|
||||
if (total_lines & 0x4) reasons += "duration ";
|
||||
if (reasons.empty()) reasons = "unknown";
|
||||
SSTVRX_LOG_INFO("FALSE SYNC PAIR REJECTED on Line 0: " + reasons);
|
||||
return;
|
||||
}
|
||||
if (line == 0xFFF4) {
|
||||
SSTVRX_LOG_INFO("STARTING LINE 0 DECODE after sync_sample_count=" + to_string_dec_uint(total_lines));
|
||||
return;
|
||||
}
|
||||
|
||||
std::array<uint8_t, CHUNK_COPY_BYTES> chunk{};
|
||||
memcpy(chunk.data(), shared_memory.bb_data.data, chunk.size());
|
||||
*reinterpret_cast<volatile uint8_t*>(&shared_memory.bb_data.data[CHUNK_FLAG_INDEX]) = 0;
|
||||
|
||||
const uint16_t line_num_encoded = chunk[0] | (chunk[1] << 8);
|
||||
const bool is_second_chunk = (line_num_encoded & 1) == 1;
|
||||
const uint16_t actual_line_num = line_num_encoded / 2;
|
||||
|
||||
if (actual_line_num >= IMAGE_HEIGHT) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool multi_chunk_line = PIXELS_PER_LINE > MAX_CHUNK_PIXELS;
|
||||
if (!pending_line_valid || pending_line_number != actual_line_num) {
|
||||
pending_line_number = actual_line_num;
|
||||
pending_line_valid = true;
|
||||
pending_chunk_mask = 0;
|
||||
std::fill(pending_line_rgb.begin(), pending_line_rgb.end(), 0);
|
||||
}
|
||||
|
||||
const uint16_t chunk_pixels = multi_chunk_line
|
||||
? (is_second_chunk ? (PIXELS_PER_LINE - MAX_CHUNK_PIXELS) : MAX_CHUNK_PIXELS)
|
||||
: PIXELS_PER_LINE;
|
||||
const uint16_t dest_pixel_offset = (multi_chunk_line && is_second_chunk) ? MAX_CHUNK_PIXELS : 0;
|
||||
const uint16_t chunk_bytes = chunk_pixels * 3;
|
||||
const uint16_t max_copy_bytes = static_cast<uint16_t>(chunk.size() > CHUNK_HEADER_BYTES ? (chunk.size() - CHUNK_HEADER_BYTES) : 0);
|
||||
const uint16_t bytes_to_copy = (chunk_bytes < max_copy_bytes) ? chunk_bytes : max_copy_bytes;
|
||||
const uint16_t dest_byte_offset = dest_pixel_offset * 3;
|
||||
|
||||
if (bytes_to_copy > 0 && (dest_byte_offset + bytes_to_copy) <= pending_line_rgb.size()) {
|
||||
memcpy(pending_line_rgb.data() + dest_byte_offset, chunk.data() + CHUNK_HEADER_BYTES, bytes_to_copy);
|
||||
}
|
||||
|
||||
const uint8_t chunk_bit = (multi_chunk_line && is_second_chunk) ? 0x2 : 0x1;
|
||||
pending_chunk_mask |= chunk_bit;
|
||||
const uint8_t required_mask = multi_chunk_line ? 0x3 : 0x1;
|
||||
if (pending_chunk_mask != required_mask) {
|
||||
return;
|
||||
}
|
||||
|
||||
pending_line_valid = false;
|
||||
pending_chunk_mask = 0;
|
||||
current_line_rx = actual_line_num;
|
||||
if (actual_line_num < IMAGE_HEIGHT) {
|
||||
write_line_to_file(actual_line_num, pending_line_rgb.data());
|
||||
update_display(actual_line_num, pending_line_rgb.data());
|
||||
max_received_line = max_received_line > (actual_line_num + 1) ? max_received_line : (actual_line_num + 1);
|
||||
}
|
||||
|
||||
#if SSTVRX_ENABLE_LOGGER
|
||||
if (logger && (actual_line_num % 10 == 0)) {
|
||||
logger->log_info("Line " + to_string_dec_uint(actual_line_num) + "/" + to_string_dec_uint(total_lines));
|
||||
}
|
||||
#endif
|
||||
|
||||
// if (actual_line_num >= (total_lines - 1)) { //don't auto finish image upon end, user need to manually stop. this method is not reliable enough.
|
||||
// finish_image();
|
||||
// }
|
||||
}
|
||||
|
||||
void SstvRxView::finish_image() {
|
||||
bmp.close();
|
||||
SSTVRX_LOG_INFO("Image completed: " + current_image_path.string());
|
||||
}
|
||||
|
||||
void SstvRxView::on_calibration(int16_t suggested_phase, int16_t suggested_slant, uint16_t sync_count) {
|
||||
if (!is_receiving) return;
|
||||
|
||||
// Display calibration suggestions to the user
|
||||
if (sync_count >= 4) {
|
||||
std::string cal_text = "Try Slant=" + to_string_dec_int(suggested_slant);
|
||||
text_calibration.set(cal_text);
|
||||
text_calibration.set_dirty();
|
||||
|
||||
// Don't auto-apply yet - just suggest for now until we verify the values are correct
|
||||
// User can manually adjust if needed
|
||||
|
||||
// Log the suggestion
|
||||
SSTVRX_LOG_INFO("Calibration suggestion: phase=" + to_string_dec_int(suggested_phase) +
|
||||
" slant=" + to_string_dec_int(suggested_slant) +
|
||||
" (from " + to_string_dec_uint(sync_count) + " syncs)");
|
||||
} else {
|
||||
// Log that we received calibration but not enough syncs yet
|
||||
SSTVRX_LOG_INFO("Calibration received: " + to_string_dec_uint(sync_count) + " syncs (need 4+)");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::sstvrx
|
||||
+196
@@ -0,0 +1,196 @@
|
||||
/*
|
||||
* Copyright (C) 2025 StarVore Labs
|
||||
*
|
||||
* 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 __SSTVRX_H__
|
||||
#define __SSTVRX_H__
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "ui_widget.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_freq_field.hpp"
|
||||
#include "ui_freqman.hpp"
|
||||
#include "ui_channel.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
#include "event_m0.hpp"
|
||||
#include "message.hpp"
|
||||
#include "sstv.hpp"
|
||||
#include "file.hpp"
|
||||
#include "bmpfile.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "oversample.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "log_file.hpp"
|
||||
#include "utility.hpp"
|
||||
#include "audio.hpp"
|
||||
#include "portapack.hpp"
|
||||
#include <array>
|
||||
#include <ch.h>
|
||||
|
||||
#ifndef SSTVRX_ENABLE_LOGGER
|
||||
#define SSTVRX_ENABLE_LOGGER 0
|
||||
#endif
|
||||
|
||||
using namespace sstv;
|
||||
|
||||
namespace ui::external_app::sstvrx {
|
||||
|
||||
#define FMR_BTNGRID_TOP 60
|
||||
|
||||
#if SSTVRX_ENABLE_LOGGER
|
||||
class SstvRxLogger {
|
||||
public:
|
||||
Optional<File::Error> append(const std::filesystem::path& filename) {
|
||||
return log_file.append(filename);
|
||||
}
|
||||
|
||||
void log_error(const std::string& error_message);
|
||||
void log_info(const std::string& info_message);
|
||||
|
||||
private:
|
||||
LogFile log_file{};
|
||||
};
|
||||
#endif
|
||||
|
||||
class SstvRxView : public ui::View {
|
||||
public:
|
||||
SstvRxView(ui::NavigationView& nav);
|
||||
SstvRxView& operator=(const SstvRxView&) = delete;
|
||||
SstvRxView(const SstvRxView&) = delete;
|
||||
~SstvRxView();
|
||||
|
||||
std::string title() { return "SSTV RX"; }
|
||||
void focus() override;
|
||||
void on_show() override;
|
||||
|
||||
private:
|
||||
ui::NavigationView& nav_;
|
||||
#if SSTVRX_ENABLE_LOGGER
|
||||
std::unique_ptr<SstvRxLogger> logger{};
|
||||
#endif
|
||||
|
||||
// Phase and slant adjustments (runtime only, not persisted)
|
||||
int16_t phase_adjustment{0}; // Horizontal offset in pixels (-50 to +50)
|
||||
int16_t slant_adjustment{0}; // Timing adjustment in 0.1% units (-100 to +100)
|
||||
|
||||
// Settings must be declared before UI controls
|
||||
app_settings::SettingsManager settings_{
|
||||
"rx_sstv",
|
||||
app_settings::Mode::RX};
|
||||
|
||||
ReceiverModel::Mode receiver_mode = ReceiverModel::Mode::WidebandFMAudio;
|
||||
AudioSpectrum* audio_spectrum_data{nullptr};
|
||||
int16_t audio_spectrum[128]{0};
|
||||
RxRadioState radio_state_{};
|
||||
audio::Rate audio_sampling_rate = audio::Rate::Hz_48000;
|
||||
uint8_t radio_bw = 0;
|
||||
bool is_receiving = false;
|
||||
const sstv_mode* rx_sstv_mode{};
|
||||
|
||||
// Image data storage - only store current line to save memory
|
||||
static constexpr uint16_t IMAGE_WIDTH = 320;
|
||||
static constexpr uint16_t IMAGE_HEIGHT = 256;
|
||||
static constexpr uint16_t PIXELS_PER_LINE = 320;
|
||||
uint16_t DISPLAY_WIDTH = 240; // Scaled display width
|
||||
uint16_t DISPLAY_HEIGHT = 192; // Scaled display height
|
||||
static constexpr uint16_t SSTV_IMG_START_ROW = 7; // Start drawing at row 7 (after controls)
|
||||
static constexpr size_t SHARED_BUFFER_BYTES = 512;
|
||||
static constexpr size_t CHUNK_FLAG_INDEX = SHARED_BUFFER_BYTES - 1;
|
||||
static constexpr size_t CHUNK_HEADER_BYTES = 2;
|
||||
static constexpr size_t CHUNK_COPY_BYTES = CHUNK_FLAG_INDEX; // Exclude flag byte
|
||||
static constexpr uint16_t MAX_CHUNK_PIXELS = (CHUNK_COPY_BYTES - CHUNK_HEADER_BYTES) / 3;
|
||||
|
||||
uint16_t current_line_rx{0};
|
||||
BMPFile bmp{};
|
||||
std::filesystem::path current_image_path{};
|
||||
ui::Color line_buffer[320];
|
||||
uint16_t line_num{0}, file_line_num{0};
|
||||
std::array<uint8_t, IMAGE_WIDTH * 3> pending_line_rgb{};
|
||||
uint16_t pending_line_number{0};
|
||||
uint8_t pending_chunk_mask{0};
|
||||
bool pending_line_valid{false};
|
||||
|
||||
// Note: Post-reception phase/slant adjustment disabled due to M0 memory constraints
|
||||
// The 245KB image buffer exceeds available heap memory
|
||||
uint16_t max_received_line{0};
|
||||
|
||||
MessageHandlerRegistration message_handler_progress{
|
||||
Message::ID::SSTVRXProgress,
|
||||
[this](const Message* const p) {
|
||||
const auto message = *reinterpret_cast<const SSTVRXProgressMessage*>(p);
|
||||
this->on_progress(message.line, message.total_lines);
|
||||
}};
|
||||
|
||||
MessageHandlerRegistration message_handler_calibration{
|
||||
Message::ID::SSTVRXCalibration,
|
||||
[this](const Message* const p) {
|
||||
const auto message = *reinterpret_cast<const SSTVRXCalibrationMessage*>(p);
|
||||
this->on_calibration(message.suggested_phase, message.suggested_slant, message.sync_count);
|
||||
}};
|
||||
|
||||
// UI Elements
|
||||
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), 0, UI_POS_WIDTH_REMAINING(24), 4}};
|
||||
Channel channel{{UI_POS_X(21), 5, UI_POS_WIDTH_REMAINING(24), 4}};
|
||||
RxFrequencyField field_frequency{{UI_POS_X(0), UI_POS_Y(0)}, nav_};
|
||||
AudioVolumeField field_volume{{UI_POS_X_RIGHT(2), UI_POS_Y(0)}};
|
||||
|
||||
OptionsField options_mode{{UI_POS_X(6), UI_POS_Y(1)}, 16, {}};
|
||||
|
||||
NumberField field_phase{{UI_POS_X(4), UI_POS_Y(2)}, 3, {-50, 50}, 1, ' '};
|
||||
NumberField field_slant{{UI_POS_X(13), UI_POS_Y(2)}, 4, {-100, 100}, 1, ' '};
|
||||
|
||||
Labels labels{
|
||||
{{UI_POS_X(1), UI_POS_Y(1)}, "Mode:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(1), UI_POS_Y(2)}, "Ph:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(8), UI_POS_Y(2)}, "Slnt:", Theme::getInstance()->fg_light->foreground}};
|
||||
|
||||
Audio audio{{UI_POS_X(21), 10, UI_POS_WIDTH(6), 4}};
|
||||
ui::Button start_stop_btn{{UI_POS_X_RIGHT(12), UI_POS_Y(3), UI_POS_WIDTH(11), UI_POS_HEIGHT(2)}, "Start RX"};
|
||||
// ui::Button redraw_btn{{16 * 8, UI_POS_Y(5), UI_POS_WIDTH(12), UI_POS_HEIGHT(3)}, "Redraw"};
|
||||
|
||||
// Calibration suggestion display
|
||||
Text text_calibration{
|
||||
{UI_POS_X(1), UI_POS_Y(3), UI_POS_WIDTH(17), UI_POS_HEIGHT(1)},
|
||||
"Calib: N/A"};
|
||||
|
||||
void on_audio_spectrum();
|
||||
void update_display(uint16_t line_num, const uint8_t* rgb_line);
|
||||
void redraw_image(); // Disabled due to memory constraints
|
||||
void start_audio();
|
||||
void on_start_stop(); // Combined start/stop handler
|
||||
void on_stop();
|
||||
void on_mode_changed(const size_t index);
|
||||
void on_progress(uint16_t line, uint16_t total_lines);
|
||||
void on_calibration(int16_t suggested_phase, int16_t suggested_slant, uint16_t sync_count);
|
||||
void write_bmp_header();
|
||||
void write_line_to_file(uint16_t line_num, const uint8_t* rgb_line);
|
||||
void finish_image();
|
||||
};
|
||||
|
||||
} // namespace ui::external_app::sstvrx
|
||||
|
||||
#endif // __SSTVRX_H__
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright (C) 2026 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_subcar.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "external_app.hpp"
|
||||
|
||||
namespace ui::external_app::subcarrx {
|
||||
void initialize_app(ui::NavigationView& nav) {
|
||||
nav.push<SubCarView>();
|
||||
}
|
||||
} // namespace ui::external_app::subcarrx
|
||||
extern "C" {
|
||||
|
||||
__attribute__((section(".external_app.app_subcarrx.application_information"), used)) application_information_t _application_information_subcarrx = {
|
||||
/*.memory_location = */ (uint8_t*)0x00000000,
|
||||
/*.externalAppEntry = */ ui::external_app::subcarrx::initialize_app,
|
||||
/*.header_version = */ CURRENT_HEADER_VERSION,
|
||||
/*.app_version = */ VERSION_MD5,
|
||||
|
||||
/*.app_name = */ "SubCar",
|
||||
/*.bitmap_data = */ {
|
||||
0xC0,
|
||||
0x03,
|
||||
0xE0,
|
||||
0x07,
|
||||
0x30,
|
||||
0x0C,
|
||||
0x30,
|
||||
0x0C,
|
||||
0x30,
|
||||
0x0C,
|
||||
0x30,
|
||||
0x0C,
|
||||
0xE0,
|
||||
0x07,
|
||||
0xC0,
|
||||
0x03,
|
||||
0x80,
|
||||
0x01,
|
||||
0x80,
|
||||
0x01,
|
||||
0x80,
|
||||
0x01,
|
||||
0x80,
|
||||
0x01,
|
||||
0x80,
|
||||
0x07,
|
||||
0x80,
|
||||
0x03,
|
||||
0x80,
|
||||
0x07,
|
||||
0x80,
|
||||
0x01,
|
||||
},
|
||||
/*.icon_color = */ ui::Color::orange().v,
|
||||
/*.menu_location = */ app_location_t::RX,
|
||||
/*.desired_menu_position = */ -1,
|
||||
|
||||
/*.m4_app_tag = portapack::spi_flash::image_tag_acars */ {'P', 'S', 'C', 'D'},
|
||||
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,539 @@
|
||||
/*
|
||||
* Copyright (C) 2026 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_subcar.hpp"
|
||||
#include "audio.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "file_path.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
using namespace ui;
|
||||
|
||||
namespace ui::external_app::subcarrx {
|
||||
|
||||
std::string SubCarRecentEntry::to_csv() {
|
||||
std::string csv = ";";
|
||||
csv += SubCarView::getSensorTypeName((FPROTO_SUBCAR_SENSOR)sensorType);
|
||||
csv += ";" + to_string_dec_uint(bits) + ";";
|
||||
csv += to_string_hex(data, 64 / 4) + ";" + to_string_hex(data2, 64 / 4);
|
||||
return csv;
|
||||
}
|
||||
|
||||
void SubCarLogger::log_data(SubCarRecentEntry& data) {
|
||||
log_file.write_entry(data.to_csv());
|
||||
}
|
||||
|
||||
void SubCarRecentEntryDetailView::update_data() {
|
||||
// process protocol data
|
||||
parseProtocol();
|
||||
// set text elements
|
||||
text_type.set(SubCarView::getSensorTypeName((FPROTO_SUBCAR_SENSOR)entry_.sensorType));
|
||||
|
||||
text_id.set("0x" + to_string_hex(serial));
|
||||
if (entry_.bits > 0) console.writeln("Bits: " + to_string_dec_uint(entry_.bits));
|
||||
if (!btn.empty()) console.writeln("Btn: " + btn);
|
||||
if (cnt != SD_NO_CNT) console.writeln("Cnt: " + to_string_dec_uint(cnt));
|
||||
if (entry_.data != 0) console.writeln("Data : " + to_string_hex(entry_.data));
|
||||
if (entry_.data2 != 0) console.writeln("Data2: " + to_string_hex(entry_.data2));
|
||||
}
|
||||
|
||||
SubCarRecentEntryDetailView::SubCarRecentEntryDetailView(NavigationView& nav, const SubCarRecentEntry& entry)
|
||||
: nav_{nav},
|
||||
entry_{entry} {
|
||||
add_children({&button_done,
|
||||
&text_type,
|
||||
&text_id,
|
||||
&console,
|
||||
&labels});
|
||||
|
||||
button_done.on_select = [&nav](const ui::Button&) {
|
||||
nav.pop();
|
||||
};
|
||||
update_data();
|
||||
}
|
||||
|
||||
void SubCarRecentEntryDetailView::focus() {
|
||||
button_done.focus();
|
||||
}
|
||||
|
||||
void SubCarView::focus() {
|
||||
field_frequency.focus();
|
||||
}
|
||||
|
||||
SubCarView::SubCarView(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
add_children({&rssi,
|
||||
&channel,
|
||||
&field_rf_amp,
|
||||
&field_lna,
|
||||
&field_vga,
|
||||
&field_frequency,
|
||||
&button_clear_list,
|
||||
&check_log,
|
||||
&labels,
|
||||
&options_mode,
|
||||
&recent_entries_view});
|
||||
|
||||
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
|
||||
logger = std::make_unique<SubCarLogger>();
|
||||
|
||||
button_clear_list.on_select = [this](Button&) {
|
||||
recent.clear();
|
||||
recent_entries_view.set_dirty();
|
||||
};
|
||||
field_frequency.set_step(10000);
|
||||
check_log.on_select = [this](Checkbox&, bool v) {
|
||||
logging = v;
|
||||
if (logger && logging) {
|
||||
logger->append(logs_dir.string() + "/SubCarLOG_" + to_string_timestamp(rtc_time::now()) + ".CSV");
|
||||
logger->write_header();
|
||||
}
|
||||
};
|
||||
check_log.set_value(logging);
|
||||
const Rect content_rect{0, header_height, screen_width, screen_height - header_height};
|
||||
recent_entries_view.set_parent_rect(content_rect);
|
||||
recent_entries_view.on_select = [this](const SubCarRecentEntry& entry) {
|
||||
nav_.push<SubCarRecentEntryDetailView>(entry);
|
||||
};
|
||||
|
||||
options_mode.on_change = [this](size_t, int32_t v) {
|
||||
modulation = v;
|
||||
chThdSleepMilliseconds(100); // wait for the baseband thread to process the previous config, to avoid glitchy output when switching modes
|
||||
baseband::set_subghzd_config(modulation, receiver_model.sampling_rate());
|
||||
};
|
||||
signal_token_tick_second = rtc_time::signal_tick_second += [this]() {
|
||||
on_tick_second();
|
||||
};
|
||||
options_mode.set_selected_index(modulation, true);
|
||||
receiver_model.enable();
|
||||
}
|
||||
|
||||
void SubCarView::on_tick_second() {
|
||||
for (auto& entry : recent) {
|
||||
entry.inc_age(1);
|
||||
}
|
||||
recent_entries_view.set_dirty();
|
||||
}
|
||||
|
||||
void SubCarView::on_data(const SubCarDataMessage* data) {
|
||||
SubCarRecentEntry key{data->sensorType, data->data, data->data2, data->bits};
|
||||
if (logger && logging) {
|
||||
logger->log_data(key);
|
||||
}
|
||||
auto matching_recent = find(recent, key.key());
|
||||
if (matching_recent != std::end(recent)) {
|
||||
// Found within. Move to front of list, increment counter.
|
||||
(*matching_recent).reset_age();
|
||||
recent.push_front(*matching_recent);
|
||||
recent.erase(matching_recent);
|
||||
} else {
|
||||
recent.emplace_front(key);
|
||||
truncate_entries(recent, 64);
|
||||
}
|
||||
recent_entries_view.set_dirty();
|
||||
}
|
||||
|
||||
SubCarView::~SubCarView() {
|
||||
rtc_time::signal_tick_second -= signal_token_tick_second;
|
||||
receiver_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
const char* SubCarView::getSensorTypeName(FPROTO_SUBCAR_SENSOR type) {
|
||||
switch (type) {
|
||||
case FPC_SUZUKI:
|
||||
return "Suzuki";
|
||||
case FPC_VW:
|
||||
return "VW";
|
||||
case FPC_SUBARU:
|
||||
return "Subaru";
|
||||
case FPC_KIAV5:
|
||||
return "Kia V5";
|
||||
case FPC_KIAV3V4:
|
||||
return "Kia V3/V4";
|
||||
case FPC_KIAV2:
|
||||
return "Kia V2";
|
||||
case FPC_KIAV1:
|
||||
return "Kia V1";
|
||||
case FPC_KIAV0:
|
||||
return "Kia V0";
|
||||
case FPC_FORDV0:
|
||||
return "Ford V0";
|
||||
case FPC_FIATV0:
|
||||
return "Fiat V0";
|
||||
case FPC_BMWV0:
|
||||
return "BMW V0";
|
||||
/* case FPC_KIAV6:
|
||||
return "Kia V6";*/
|
||||
|
||||
case FPC_Invalid:
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
std::string SubCarView::pad_string_with_spaces(int snakes) {
|
||||
std::string paddedStr(snakes, ' ');
|
||||
return paddedStr;
|
||||
}
|
||||
|
||||
void SubCarView::on_freqchg(int64_t freq) {
|
||||
field_frequency.set_value(freq);
|
||||
}
|
||||
|
||||
void subaru_decode_count(const uint8_t* KB, uint16_t* count) {
|
||||
uint8_t lo = 0;
|
||||
if ((KB[4] & 0x40) == 0)
|
||||
lo |= 0x01;
|
||||
if ((KB[4] & 0x80) == 0)
|
||||
lo |= 0x02;
|
||||
if ((KB[5] & 0x01) == 0)
|
||||
lo |= 0x04;
|
||||
if ((KB[5] & 0x02) == 0)
|
||||
lo |= 0x08;
|
||||
if ((KB[6] & 0x01) == 0)
|
||||
lo |= 0x10;
|
||||
if ((KB[6] & 0x02) == 0)
|
||||
lo |= 0x20;
|
||||
if ((KB[5] & 0x40) == 0)
|
||||
lo |= 0x40;
|
||||
if ((KB[5] & 0x80) == 0)
|
||||
lo |= 0x80;
|
||||
|
||||
uint8_t REG_SH1 = (KB[7] << 4) & 0xF0;
|
||||
if (KB[5] & 0x04)
|
||||
REG_SH1 |= 0x04;
|
||||
if (KB[5] & 0x08)
|
||||
REG_SH1 |= 0x08;
|
||||
if (KB[6] & 0x80)
|
||||
REG_SH1 |= 0x02;
|
||||
if (KB[6] & 0x40)
|
||||
REG_SH1 |= 0x01;
|
||||
|
||||
uint8_t REG_SH2 = ((KB[6] << 2) & 0xF0) | ((KB[7] >> 4) & 0x0F);
|
||||
|
||||
uint8_t SER0 = KB[3];
|
||||
uint8_t SER1 = KB[1];
|
||||
uint8_t SER2 = KB[2];
|
||||
|
||||
uint8_t total_rot = 4 + lo;
|
||||
for (uint8_t i = 0; i < total_rot; ++i) {
|
||||
uint8_t t_bit = (SER0 >> 7) & 1;
|
||||
SER0 = ((SER0 << 1) & 0xFE) | ((SER1 >> 7) & 1);
|
||||
SER1 = ((SER1 << 1) & 0xFE) | ((SER2 >> 7) & 1);
|
||||
SER2 = ((SER2 << 1) & 0xFE) | t_bit;
|
||||
}
|
||||
|
||||
uint8_t T1 = SER1 ^ REG_SH1;
|
||||
uint8_t T2 = SER2 ^ REG_SH2;
|
||||
|
||||
uint8_t hi = 0;
|
||||
if ((T1 & 0x10) == 0)
|
||||
hi |= 0x04;
|
||||
if ((T1 & 0x20) == 0)
|
||||
hi |= 0x08;
|
||||
if ((T2 & 0x80) == 0)
|
||||
hi |= 0x02;
|
||||
if ((T2 & 0x40) == 0)
|
||||
hi |= 0x01;
|
||||
if ((T1 & 0x01) == 0)
|
||||
hi |= 0x40;
|
||||
if ((T1 & 0x02) == 0)
|
||||
hi |= 0x80;
|
||||
if ((T2 & 0x08) == 0)
|
||||
hi |= 0x20;
|
||||
if ((T2 & 0x04) == 0)
|
||||
hi |= 0x10;
|
||||
*count = ((hi << 8) | lo) & 0xFFFF;
|
||||
}
|
||||
|
||||
void SubCarRecentEntryDetailView::parseProtocol() {
|
||||
btn = "";
|
||||
cnt = SD_NO_CNT;
|
||||
serial = 0;
|
||||
|
||||
if (entry_.sensorType == FPC_Invalid) return;
|
||||
|
||||
if (entry_.sensorType == FPC_SUZUKI) {
|
||||
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;
|
||||
}
|
||||
if (entry_.sensorType == FPC_VW) {
|
||||
// uint32_t key_high = (entry_.data >> 32) & 0xFFFFFFFF;
|
||||
uint32_t key_low = entry_.data & 0xFFFFFFFF;
|
||||
serial = key_low; // trimmed to 32 bits for VW
|
||||
uint8_t check = entry_.data2 & 0xFF;
|
||||
uint8_t btnid = (check >> 4) & 0xF;
|
||||
switch (btnid) {
|
||||
case 0x1:
|
||||
btn = "UNLOCK";
|
||||
break;
|
||||
case 0x2:
|
||||
btn = "LOCK";
|
||||
break;
|
||||
case 0x3:
|
||||
btn = "Un+Lk";
|
||||
break;
|
||||
case 0x4:
|
||||
btn = "TRUNK";
|
||||
break;
|
||||
case 0x5:
|
||||
btn = "Un+Tr";
|
||||
break;
|
||||
case 0x6:
|
||||
btn = "Lk+Tr";
|
||||
break;
|
||||
case 0x7:
|
||||
btn = "Un+Lk+Tr";
|
||||
break;
|
||||
case 0x8:
|
||||
btn = "PANIC";
|
||||
break;
|
||||
default:
|
||||
btn = "Unknown";
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (entry_.sensorType == FPC_SUBARU) {
|
||||
uint8_t* data_bytes = (uint8_t*)entry_.data;
|
||||
serial = ((uint32_t)data_bytes[1] << 16) | ((uint32_t)data_bytes[2] << 8) | data_bytes[3];
|
||||
uint8_t button = data_bytes[0] & 0x0F;
|
||||
btn = to_string_dec_uint(button);
|
||||
uint16_t cnttmp = 0;
|
||||
subaru_decode_count(data_bytes, &cnttmp);
|
||||
cnt = cnttmp;
|
||||
}
|
||||
|
||||
if (entry_.sensorType == FPC_KIAV5) {
|
||||
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);
|
||||
// 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 = (uint32_t)entry_.data;
|
||||
// uint8_t button = entry_.data2 & 0xFF;
|
||||
btn = "?"; // to_string_dec_uint(button);
|
||||
}
|
||||
|
||||
if (entry_.sensorType == FPC_KIAV2) {
|
||||
serial = (uint32_t)((entry_.data >> 20) & 0xFFFFFFFF);
|
||||
uint8_t button = (uint8_t)((entry_.data >> 16) & 0x0F);
|
||||
uint16_t raw_count = (uint16_t)((entry_.data >> 4) & 0xFFF);
|
||||
cnt = ((raw_count >> 4) | (raw_count << 8)) & 0xFFF;
|
||||
btn = to_string_dec_uint(button);
|
||||
}
|
||||
|
||||
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 >> 4) & 0xF) << 8 | ((entry_.data >> 8) & 0xFF);
|
||||
btn = to_string_dec_uint(button);
|
||||
}
|
||||
|
||||
if (entry_.sensorType == FPC_KIAV0) {
|
||||
serial = (uint32_t)((entry_.data >> 12) & 0x0FFFFFFF);
|
||||
uint8_t button = (entry_.data >> 8) & 0x0F;
|
||||
cnt = (entry_.data >> 40) & 0xFFFF;
|
||||
btn = to_string_dec_uint(button);
|
||||
}
|
||||
if (entry_.sensorType == FPC_FORDV0) {
|
||||
uint8_t buf[13] = {0};
|
||||
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
buf[i] = (uint8_t)(entry_.data >> (56 - i * 8));
|
||||
}
|
||||
|
||||
buf[8] = (uint8_t)(entry_.data2 >> 8);
|
||||
buf[9] = (uint8_t)(entry_.data2 & 0xFF);
|
||||
uint8_t tmp = buf[8];
|
||||
uint8_t parity = 0;
|
||||
uint8_t parity_any = (tmp != 0);
|
||||
while (tmp) {
|
||||
parity ^= (tmp & 1);
|
||||
tmp >>= 1;
|
||||
}
|
||||
buf[11] = parity_any ? parity : 0;
|
||||
uint8_t xor_byte;
|
||||
uint8_t limit;
|
||||
if (buf[11]) {
|
||||
xor_byte = buf[7];
|
||||
limit = 7;
|
||||
} else {
|
||||
xor_byte = buf[6];
|
||||
limit = 6;
|
||||
}
|
||||
|
||||
for (int idx = 1; idx < limit; ++idx) {
|
||||
buf[idx] ^= xor_byte;
|
||||
}
|
||||
|
||||
if (buf[11] == 0) {
|
||||
buf[7] ^= xor_byte;
|
||||
}
|
||||
|
||||
uint8_t orig_b7 = buf[7];
|
||||
buf[7] = (orig_b7 & 0xAA) | (buf[6] & 0x55);
|
||||
uint8_t mixed = (buf[6] & 0xAA) | (orig_b7 & 0x55);
|
||||
buf[12] = mixed;
|
||||
buf[6] = mixed;
|
||||
|
||||
uint32_t serial_le = ((uint32_t)buf[1]) |
|
||||
((uint32_t)buf[2] << 8) |
|
||||
((uint32_t)buf[3] << 16) |
|
||||
((uint32_t)buf[4] << 24);
|
||||
|
||||
serial = ((serial_le & 0xFF) << 24) |
|
||||
(((serial_le >> 8) & 0xFF) << 16) |
|
||||
(((serial_le >> 16) & 0xFF) << 8) |
|
||||
((serial_le >> 24) & 0xFF);
|
||||
|
||||
uint8_t button = (buf[5] >> 4) & 0x0F;
|
||||
|
||||
cnt = ((buf[5] & 0x0F) << 16) |
|
||||
(buf[6] << 8) |
|
||||
buf[7];
|
||||
btn = to_string_dec_uint(button);
|
||||
}
|
||||
|
||||
if (entry_.sensorType == FPC_FIATV0) {
|
||||
serial = (uint32_t)(entry_.data & 0xFFFFFFFF);
|
||||
cnt = (uint32_t)((entry_.data >> 32) & 0xFFFFFFFF);
|
||||
uint8_t button = (uint8_t)(entry_.data2 & 0xFF);
|
||||
btn = to_string_dec_uint(button);
|
||||
}
|
||||
|
||||
if (entry_.sensorType == FPC_BMWV0) {
|
||||
serial = (uint32_t)((entry_.data >> 12) & 0x0FFFFFFF);
|
||||
uint8_t button = (entry_.data >> 8) & 0x0F;
|
||||
cnt = (entry_.data >> 40) & 0xFFFF;
|
||||
btn = to_string_dec_uint(button);
|
||||
}
|
||||
|
||||
/*if (entry_.sensorType == FPC_KIAV6) {
|
||||
// not decrypted!
|
||||
serial = 0;
|
||||
btn = "?";
|
||||
cnt = 0;
|
||||
}*/
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::subcarrx
|
||||
|
||||
namespace ui {
|
||||
|
||||
template <>
|
||||
void RecentEntriesTable<ui::external_app::subcarrx::SubCarRecentEntries>::draw(
|
||||
const Entry& entry,
|
||||
const Rect& target_rect,
|
||||
Painter& painter,
|
||||
const Style& style,
|
||||
ui::RecentEntriesColumns& columns) {
|
||||
std::string line{};
|
||||
line.reserve(30);
|
||||
|
||||
line = ui::external_app::subcarrx::SubCarView::getSensorTypeName((FPROTO_SUBCAR_SENSOR)entry.sensorType);
|
||||
line = line + " " + to_string_hex(entry.data << 32);
|
||||
line.resize(columns.at(0).second, ' ');
|
||||
std::string ageStr = to_string_dec_uint(entry.age);
|
||||
std::string bitsStr = to_string_dec_uint(entry.bits);
|
||||
line += ui::external_app::subcarrx::SubCarView::pad_string_with_spaces(5 - bitsStr.length()) + bitsStr;
|
||||
line += ui::external_app::subcarrx::SubCarView::pad_string_with_spaces(4 - ageStr.length()) + ageStr;
|
||||
|
||||
line.resize(target_rect.width() / 8, ' ');
|
||||
painter.draw_string(target_rect.location(), style, line);
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
@@ -0,0 +1,228 @@
|
||||
/*
|
||||
* Copyright (C) 2026 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.
|
||||
*/
|
||||
/*
|
||||
This and The other files related to this is based on a lot of great people's work. https://github.com/RocketGod-git/ProtoPirate Check the repo, and the credits inside.
|
||||
*/
|
||||
|
||||
#ifndef __UI_SubCar_H__
|
||||
#define __UI_SubCar_H__
|
||||
|
||||
#define SD_NO_SERIAL 0xFFFFFFFF
|
||||
#define SD_NO_BTN 0xFF
|
||||
#define SD_NO_CNT 0xFF
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_freq_field.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "utility.hpp"
|
||||
#include "log_file.hpp"
|
||||
#include "recent_entries.hpp"
|
||||
|
||||
#include "../../baseband/fprotos/subcartypes.hpp"
|
||||
|
||||
using namespace ui;
|
||||
|
||||
namespace ui::external_app::subcarrx {
|
||||
|
||||
struct SubCarRecentEntry {
|
||||
using Key = uint64_t;
|
||||
static constexpr Key invalid_key = 0x0fffffff;
|
||||
uint8_t sensorType = FPC_Invalid;
|
||||
uint16_t bits = 0;
|
||||
uint16_t age = 0; // updated on each seconds, show how long the signal was last seen
|
||||
uint64_t data = 0;
|
||||
uint64_t data2 = 0;
|
||||
SubCarRecentEntry() {}
|
||||
SubCarRecentEntry(
|
||||
uint8_t sensorType,
|
||||
uint64_t data = 0,
|
||||
uint64_t data2 = 0,
|
||||
uint16_t bits = 0)
|
||||
: sensorType{sensorType},
|
||||
bits{bits},
|
||||
data{data},
|
||||
data2{data2} {
|
||||
}
|
||||
Key key() const {
|
||||
return (data ^ ((static_cast<uint64_t>(sensorType) & 0xFF) << 0)); // should be optimized...
|
||||
}
|
||||
void inc_age(int delta) {
|
||||
if (UINT16_MAX - delta > age) age += delta;
|
||||
}
|
||||
void reset_age() {
|
||||
age = 0;
|
||||
}
|
||||
|
||||
std::string to_csv();
|
||||
};
|
||||
|
||||
class SubCarLogger {
|
||||
public:
|
||||
Optional<File::Error> append(const std::filesystem::path& filename) {
|
||||
return log_file.append(filename);
|
||||
}
|
||||
|
||||
void log_data(SubCarRecentEntry& data);
|
||||
void write_header() {
|
||||
log_file.write_entry(";Type; Bits; Data;");
|
||||
}
|
||||
|
||||
private:
|
||||
LogFile log_file{};
|
||||
};
|
||||
using SubCarRecentEntries = RecentEntries<SubCarRecentEntry>;
|
||||
using SubCarRecentEntriesView = RecentEntriesView<SubCarRecentEntries>;
|
||||
|
||||
class SubCarView : public View {
|
||||
public:
|
||||
SubCarView(NavigationView& nav);
|
||||
~SubCarView();
|
||||
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "SubCar"; };
|
||||
static const char* getSensorTypeName(FPROTO_SUBCAR_SENSOR type);
|
||||
static std::string pad_string_with_spaces(int snakes);
|
||||
|
||||
private:
|
||||
void on_tick_second();
|
||||
void on_data(const SubCarDataMessage* data);
|
||||
|
||||
NavigationView& nav_;
|
||||
RxRadioState radio_state_{
|
||||
433'920'000 /* frequency */,
|
||||
1'750'000 /* bandwidth */,
|
||||
4'000'000 /* sampling rate */,
|
||||
ReceiverModel::Mode::AMAudio};
|
||||
bool logging = false;
|
||||
uint8_t modulation = 0;
|
||||
app_settings::SettingsManager settings_{
|
||||
"rx_subcar",
|
||||
app_settings::Mode::RX,
|
||||
{
|
||||
{"log"sv, &logging},
|
||||
{"modulationmode"sv, &modulation},
|
||||
}};
|
||||
|
||||
SubCarRecentEntries recent{};
|
||||
|
||||
RFAmpField field_rf_amp{
|
||||
{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), 0, UI_POS_WIDTH_REMAINING(24), 4}};
|
||||
Channel channel{
|
||||
{UI_POS_X(21), 5, UI_POS_WIDTH_REMAINING(24), 4},
|
||||
};
|
||||
RxFrequencyField field_frequency{
|
||||
{UI_POS_X(0), UI_POS_Y(0)},
|
||||
nav_};
|
||||
|
||||
SignalToken signal_token_tick_second{};
|
||||
|
||||
Button button_clear_list{
|
||||
{UI_POS_X(0), UI_POS_Y(1), UI_POS_WIDTH(7), UI_POS_HEIGHT(2)},
|
||||
"Clear"};
|
||||
|
||||
Checkbox check_log{
|
||||
{UI_POS_X(8), UI_POS_Y(1)},
|
||||
3,
|
||||
"Log",
|
||||
true};
|
||||
|
||||
Labels labels{
|
||||
{{UI_POS_X(15), UI_POS_Y(1)}, "Mode:", Theme::getInstance()->fg_light->foreground},
|
||||
};
|
||||
ui::OptionsField options_mode{
|
||||
{UI_POS_X(22), UI_POS_Y(1)},
|
||||
3,
|
||||
{{"AM", 0}, {"FM", 1}}};
|
||||
|
||||
static constexpr auto header_height = 3 * 16;
|
||||
|
||||
std::unique_ptr<SubCarLogger> logger{};
|
||||
|
||||
ui::RecentEntriesColumns columns{{
|
||||
{"Type", 0},
|
||||
{"Bits", 4},
|
||||
{"Age", 3},
|
||||
}};
|
||||
SubCarRecentEntriesView recent_entries_view{columns, recent};
|
||||
|
||||
void on_freqchg(int64_t freq);
|
||||
MessageHandlerRegistration message_handler_freqchg{
|
||||
Message::ID::FreqChangeCommand,
|
||||
[this](Message* const p) {
|
||||
const auto message = static_cast<const FreqChangeCommandMessage*>(p);
|
||||
this->on_freqchg(message->freq);
|
||||
}};
|
||||
|
||||
MessageHandlerRegistration message_handler_packet{
|
||||
Message::ID::SubCarData,
|
||||
[this](Message* const p) {
|
||||
const auto message = static_cast<const SubCarDataMessage*>(p);
|
||||
this->on_data(message);
|
||||
}};
|
||||
};
|
||||
|
||||
class SubCarRecentEntryDetailView : public View {
|
||||
public:
|
||||
SubCarRecentEntryDetailView(NavigationView& nav, const SubCarRecentEntry& entry);
|
||||
|
||||
void update_data();
|
||||
void focus() override;
|
||||
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
SubCarRecentEntry entry_{};
|
||||
|
||||
uint32_t serial = 0;
|
||||
std::string btn = "";
|
||||
uint32_t cnt = SD_NO_CNT;
|
||||
|
||||
Text text_type{{UI_POS_X(0), UI_POS_Y(1), UI_POS_WIDTH(15), UI_POS_HEIGHT(1)}, "?"};
|
||||
Text text_id{{UI_POS_X(6), UI_POS_Y(2), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)}, "?"};
|
||||
|
||||
Console console{
|
||||
{UI_POS_X(0), UI_POS_Y(4), UI_POS_MAXWIDTH, screen_height - (4 * 16) - 36}};
|
||||
|
||||
Labels labels{
|
||||
{{UI_POS_X(0), UI_POS_Y(0)}, "Type:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(2)}, "Serial: ", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(3)}, "Data:", Theme::getInstance()->fg_light->foreground},
|
||||
};
|
||||
|
||||
Button button_done{
|
||||
{screen_width - 96 - 4, screen_height - 32 - 12, 96, 32},
|
||||
"Done"};
|
||||
|
||||
void parseProtocol();
|
||||
};
|
||||
|
||||
} // namespace ui::external_app::subcarrx
|
||||
|
||||
#endif /*__UI_SubCar_H__*/
|
||||
+1
-2
@@ -598,8 +598,7 @@ void pause_game() {
|
||||
joystick.detach();
|
||||
locate((INFO_LEFT + SCREEN_WIDTH) / 2 - 25, 200);
|
||||
printf("PAUSED");
|
||||
while ((get_switches_state().to_ulong() & 0x10) == 0)
|
||||
;
|
||||
while ((get_switches_state().to_ulong() & 0x10) == 0);
|
||||
fillrect(INFO_LEFT, 195, SCREEN_WIDTH, 210, Black);
|
||||
joystick.attach(&ReadJoystickForFigure, 0.3);
|
||||
game.attach(&PlayGame, delays[level]);
|
||||
|
||||
+1
-1
@@ -99,7 +99,7 @@ class TPMSAppView : public View {
|
||||
|
||||
// Prevent painting of region covered entirely by a child.
|
||||
// TODO: Add flag to View that specifies view does not need to be cleared before painting.
|
||||
void paint(Painter&) override{};
|
||||
void paint(Painter&) override {};
|
||||
|
||||
void focus() override;
|
||||
|
||||
|
||||
@@ -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__*/
|
||||
@@ -315,7 +315,7 @@ class File {
|
||||
template <typename T>
|
||||
using Result = Result<T, Error>;
|
||||
|
||||
File(){};
|
||||
File() {};
|
||||
~File();
|
||||
|
||||
File(File&& other) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -361,8 +361,7 @@ class Si5351 {
|
||||
}
|
||||
|
||||
void wait_for_device_ready() {
|
||||
while (device_status() & 0x80)
|
||||
;
|
||||
while (device_status() & 0x80);
|
||||
}
|
||||
|
||||
bool plla_loss_of_signal() {
|
||||
|
||||
@@ -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{};
|
||||
|
||||
@@ -322,8 +322,7 @@ static void shutdown_base() {
|
||||
});
|
||||
|
||||
cgu::pll1::enable();
|
||||
while (!cgu::pll1::is_locked())
|
||||
;
|
||||
while (!cgu::pll1::is_locked());
|
||||
|
||||
set_clock_config(clock_config_pll1_boot);
|
||||
|
||||
@@ -361,15 +360,13 @@ static void set_cpu_clock_speed() {
|
||||
});
|
||||
|
||||
cgu::pll1::enable();
|
||||
while (!cgu::pll1::is_locked())
|
||||
;
|
||||
while (!cgu::pll1::is_locked());
|
||||
|
||||
set_clock_config(clock_config_pll1_step);
|
||||
|
||||
/* Delay >50us at 90-110MHz clock speed */
|
||||
volatile uint32_t delay = 1400;
|
||||
while (delay--)
|
||||
;
|
||||
while (delay--);
|
||||
|
||||
set_clock_config(clock_config_pll1);
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -265,4 +265,31 @@ uint8_t day_of_week(uint16_t year, uint8_t month, uint8_t day) {
|
||||
return (day - 1 + (13 * m / 5) + y + (y / 4) - (y / 100) + (y / 400)) % 7;
|
||||
}
|
||||
|
||||
bool isLeap(int year) {
|
||||
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
|
||||
}
|
||||
|
||||
time_t rtcToUnixUTC(const rtc::RTC& rtc) {
|
||||
const uint8_t daysOfMonth[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
|
||||
uint16_t y = rtc.year();
|
||||
uint8_t m = rtc.month();
|
||||
uint8_t d = rtc.day();
|
||||
uint32_t totalDays = 0;
|
||||
for (int i = 1970; i < y; i++) {
|
||||
totalDays += isLeap(i) ? 366 : 365;
|
||||
}
|
||||
for (int i = 1; i < m; i++) {
|
||||
totalDays += daysOfMonth[i];
|
||||
if (i == 2 && isLeap(y)) {
|
||||
totalDays++;
|
||||
}
|
||||
}
|
||||
totalDays += (d - 1);
|
||||
time_t totalSeconds = totalDays * 86400; // 24 * 60 * 60
|
||||
totalSeconds += rtc.hour() * 3600;
|
||||
totalSeconds += rtc.minute() * 60;
|
||||
totalSeconds += rtc.second();
|
||||
return totalSeconds;
|
||||
}
|
||||
|
||||
} /* namespace rtc_time */
|
||||
|
||||
@@ -56,6 +56,9 @@ bool leap_year(uint16_t year);
|
||||
uint16_t day_of_year(uint16_t year, uint8_t month, uint8_t day);
|
||||
uint16_t day_of_year_of_nth_weekday(uint16_t year, uint8_t month, uint8_t n, uint8_t weekday);
|
||||
|
||||
bool isLeap(int year);
|
||||
time_t rtcToUnixUTC(const rtc::RTC& rtc);
|
||||
|
||||
} /* namespace rtc_time */
|
||||
|
||||
#endif /*__RTC_TIME_H__*/
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -199,7 +199,7 @@ bool GeoMap::on_encoder(const EncoderEvent delta) {
|
||||
}
|
||||
}
|
||||
map_osm_zoom++;
|
||||
if (has_osm) set_osm_max_zoom();
|
||||
if (has_osm) set_osm_max_zoom(true);
|
||||
} else if (delta < 0) {
|
||||
if (map_zoom > -MAX_MAP_ZOOM_OUT) {
|
||||
if (map_zoom == 1) {
|
||||
@@ -212,6 +212,7 @@ bool GeoMap::on_encoder(const EncoderEvent delta) {
|
||||
}
|
||||
}
|
||||
if (map_osm_zoom > 0) map_osm_zoom--;
|
||||
if (has_osm) set_osm_max_zoom(true);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
@@ -299,8 +300,8 @@ ui::Point GeoMap::item_rect_pixel(GeoMarker& item) {
|
||||
return {(int16_t)x, (int16_t)y};
|
||||
}
|
||||
// osm calculation
|
||||
double y = lat_to_pixel_y_tile(item.lat, map_osm_zoom) - viewport_top_left_py;
|
||||
double x = lon_to_pixel_x_tile(item.lon, map_osm_zoom) - viewport_top_left_px;
|
||||
double y = lat_to_pixel_y_tile(item.lat, map_osm_real_zoom) - viewport_top_left_py;
|
||||
double x = lon_to_pixel_x_tile(item.lon, map_osm_real_zoom) - viewport_top_left_px;
|
||||
return {(int16_t)x, (int16_t)y};
|
||||
}
|
||||
|
||||
@@ -327,7 +328,7 @@ int GeoMap::lat2tile(double lat, int zoom) {
|
||||
return (int)floor((1.0 - log(tan(lat_rad) + 1.0 / cos(lat_rad)) / M_PI) / 2.0 * pow(2.0, zoom));
|
||||
}
|
||||
|
||||
void GeoMap::set_osm_max_zoom() {
|
||||
void GeoMap::set_osm_max_zoom(bool changeboth) {
|
||||
if (map_osm_zoom > 20) map_osm_zoom = 20;
|
||||
for (uint8_t i = map_osm_zoom; i > 0; i--) {
|
||||
int tile_x = lon2tile(lon_, i);
|
||||
@@ -335,11 +336,13 @@ void GeoMap::set_osm_max_zoom() {
|
||||
std::string filename = "/OSM/" + to_string_dec_int(i) + "/" + to_string_dec_int(tile_x) + "/" + to_string_dec_int(tile_y) + ".bmp";
|
||||
std::filesystem::path file_path(filename);
|
||||
if (file_exists(file_path)) {
|
||||
map_osm_zoom = i;
|
||||
map_osm_real_zoom = i;
|
||||
if (changeboth) map_osm_zoom = i;
|
||||
return;
|
||||
}
|
||||
}
|
||||
map_osm_zoom = 0; // should not happen
|
||||
if (changeboth) map_osm_zoom = 0; // should not happen
|
||||
map_osm_real_zoom = 0; // should not happen
|
||||
}
|
||||
|
||||
// checks if the tile file presents or not. to determine if we got osm or not
|
||||
@@ -454,14 +457,22 @@ bool GeoMap::draw_osm_file(int zoom, int tile_x, int tile_y, int relative_x, int
|
||||
return false;
|
||||
}
|
||||
std::vector<ui::Color> line(clip_w);
|
||||
for (int y = 0; y < clip_h; ++y) {
|
||||
int source_row = src_y + y;
|
||||
int dest_row = dest_y + y;
|
||||
bmp.seek(src_x, source_row);
|
||||
for (int x = 0; x < clip_w; ++x) {
|
||||
bmp.read_next_px(line[x], true);
|
||||
if (bmp.is_bottomup()) {
|
||||
for (int y = clip_h - 1; y >= 0; --y) {
|
||||
int source_row = src_y + y;
|
||||
int dest_row = dest_y + y;
|
||||
bmp.seek(src_x, source_row);
|
||||
bmp.read_next_px_cnt(line.data(), clip_w, false);
|
||||
display.draw_pixels({dest_x + r.left(), dest_row + r.top(), clip_w, 1}, line);
|
||||
}
|
||||
} else {
|
||||
for (int y = 0; y < clip_h; ++y) {
|
||||
int source_row = src_y + y;
|
||||
int dest_row = dest_y + y;
|
||||
bmp.seek(src_x, source_row);
|
||||
bmp.read_next_px_cnt(line.data(), clip_w, false);
|
||||
display.draw_pixels({dest_x + r.left(), dest_row + r.top(), clip_w, 1}, line);
|
||||
}
|
||||
display.draw_pixels({dest_x + r.left(), dest_row + r.top(), clip_w, 1}, line);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -522,8 +533,8 @@ void GeoMap::paint(Painter& painter) {
|
||||
} else {
|
||||
// display osm tiles
|
||||
// Convert center GPS to a global pixel coordinate
|
||||
double global_center_px = lon_to_pixel_x_tile(lon_, map_osm_zoom);
|
||||
double global_center_py = lat_to_pixel_y_tile(lat_, map_osm_zoom);
|
||||
double global_center_px = lon_to_pixel_x_tile(lon_, map_osm_real_zoom);
|
||||
double global_center_py = lat_to_pixel_y_tile(lat_, map_osm_real_zoom);
|
||||
|
||||
// Find the top-left corner of the screen (viewport) in global pixel coordinates
|
||||
viewport_top_left_px = global_center_px - (r.width() / 2.0);
|
||||
@@ -552,7 +563,7 @@ void GeoMap::paint(Painter& painter) {
|
||||
// For the first tile (x=0, y=0), this will be the negative offset.
|
||||
int draw_pos_x = round(render_offset_x + x * TILE_SIZE);
|
||||
int draw_pos_y = round(render_offset_y + y * TILE_SIZE);
|
||||
if (!draw_osm_file(map_osm_zoom, current_tile_x, current_tile_y, draw_pos_x, draw_pos_y)) {
|
||||
if (!draw_osm_file(map_osm_real_zoom, current_tile_x, current_tile_y, draw_pos_x, draw_pos_y)) {
|
||||
// already blanked it.
|
||||
}
|
||||
}
|
||||
@@ -615,7 +626,7 @@ bool GeoMap::on_touch(const TouchEvent event) {
|
||||
on_move(p.x() / 2.0 * lon_ratio, p.y() / 2.0 * lat_ratio, false);
|
||||
} else {
|
||||
p = event.point - screen_rect().location();
|
||||
on_move(tile_pixel_x_to_lon(p.x() + viewport_top_left_px, map_osm_zoom), tile_pixel_y_to_lat(p.y() + viewport_top_left_py, map_osm_zoom), true);
|
||||
on_move(tile_pixel_x_to_lon(p.x() + viewport_top_left_px, map_osm_real_zoom), tile_pixel_y_to_lat(p.y() + viewport_top_left_py, map_osm_real_zoom), true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -646,7 +657,13 @@ void GeoMap::move(const float lon, const float lat) {
|
||||
} else {
|
||||
if (is_changed) {
|
||||
set_osm_max_zoom();
|
||||
redraw_map = true;
|
||||
double global_center_px = lon_to_pixel_x_tile(lon_, map_osm_real_zoom);
|
||||
double global_center_py = lat_to_pixel_y_tile(lat_, map_osm_real_zoom);
|
||||
// Redraw if viewport moved by at least 1 pixel (includes zoom level changes)
|
||||
if (abs(global_center_px - (r.width() / 2.0) - viewport_top_left_px) >= 1.0 ||
|
||||
abs(global_center_py - (r.height() / 2.0) - viewport_top_left_py) >= 1.0) {
|
||||
redraw_map = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,7 +255,7 @@ class GeoMap : public Widget {
|
||||
void map_read_line_bin(ui::Color* buffer, uint16_t pixels);
|
||||
// open street map related
|
||||
uint8_t find_osm_file_tile();
|
||||
void set_osm_max_zoom();
|
||||
void set_osm_max_zoom(bool changeboth = false);
|
||||
bool draw_osm_file(int zoom, int tile_x, int tile_y, int relative_x, int relative_y);
|
||||
int lon2tile(double lon, int zoom);
|
||||
int lat2tile(double lat, int zoom);
|
||||
@@ -263,7 +263,8 @@ class GeoMap : public Widget {
|
||||
double lat_to_pixel_y_tile(double lat, int zoom);
|
||||
double tile_pixel_x_to_lon(int x, int zoom);
|
||||
double tile_pixel_y_to_lat(int y, int zoom);
|
||||
uint8_t map_osm_zoom{3};
|
||||
uint8_t map_osm_zoom{5};
|
||||
uint8_t map_osm_real_zoom{5};
|
||||
double viewport_top_left_px = 0;
|
||||
double viewport_top_left_py = 0;
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user