mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2026-08-21 23:19:02 +00:00
Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| c46cc431c9 | |||
| a4d23768c1 | |||
| 344aa0c741 |
+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.0
|
||||
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})
|
||||
|
||||
@@ -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 ****",
|
||||
" ",
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,11 @@ 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);
|
||||
}
|
||||
|
||||
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,10 @@ 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_wefax_config(uint8_t lpm, uint8_t ioc);
|
||||
void set_noaaapt_config();
|
||||
void set_flex_config();
|
||||
|
||||
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;
|
||||
|
||||
|
||||
+37
-1
@@ -99,6 +99,10 @@ set(EXTCPPSRC
|
||||
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,27 @@ 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
|
||||
)
|
||||
|
||||
set(EXTAPPLIST
|
||||
@@ -290,6 +319,7 @@ set(EXTAPPLIST
|
||||
adsbtx
|
||||
morse_tx
|
||||
sstvtx
|
||||
sstvrx
|
||||
random_password
|
||||
# acars_rx --not working
|
||||
wefax_rx
|
||||
@@ -319,6 +349,7 @@ set(EXTAPPLIST
|
||||
scanner
|
||||
level
|
||||
gfxeq
|
||||
waterfall_designer
|
||||
detector_rx
|
||||
spaceinv
|
||||
blackjack
|
||||
@@ -330,4 +361,9 @@ set(EXTAPPLIST
|
||||
bht_tx
|
||||
morse_practice
|
||||
adult_toys_controller
|
||||
flex_rx
|
||||
subcarrx
|
||||
siggen
|
||||
sdusb
|
||||
morse_radio
|
||||
)
|
||||
|
||||
+48
-1
@@ -86,7 +86,13 @@ 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
|
||||
}
|
||||
|
||||
SECTIONS
|
||||
@@ -397,6 +403,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 +483,40 @@ 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
|
||||
}
|
||||
|
||||
|
||||
+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__*/
|
||||
+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
|
||||
|
||||
@@ -98,7 +98,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;
|
||||
|
||||
|
||||
@@ -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,274 @@
|
||||
#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,
|
||||
&options_mode,
|
||||
&btn_clear,
|
||||
&console_text,
|
||||
&labels,
|
||||
&chk_log,
|
||||
&field_squelch});
|
||||
field_frequency.set_step(100);
|
||||
|
||||
btn_clear.on_select = [this](Button&) {
|
||||
console_text.clear(true);
|
||||
txt_last.set("");
|
||||
time_stamp = false;
|
||||
if (logger && save_log)
|
||||
logger->init_daily_log(logs_dir);
|
||||
};
|
||||
|
||||
audio::output::start();
|
||||
receiver_model.set_sampling_rate(3072000);
|
||||
receiver_model.set_baseband_bandwidth(1750000);
|
||||
receiver_model.enable();
|
||||
|
||||
options_mode.on_change = [this](size_t, int32_t value) {
|
||||
current_mode = (uint8_t)value;
|
||||
morse_decoder_.resetLearning();
|
||||
if (current_mode == 0) {
|
||||
receiver_model.set_am_configuration(4);
|
||||
receiver_model.set_modulation(ReceiverModel::Mode::NarrowbandFMAudio);
|
||||
field_squelch.set_style(Theme::getInstance()->option_active);
|
||||
field_squelch.set_focusable(true);
|
||||
receiver_model.set_squelch_level(field_squelch.value());
|
||||
audio::set_rate(audio::Rate::Hz_24000);
|
||||
} else {
|
||||
receiver_model.set_modulation(ReceiverModel::Mode::AMAudio);
|
||||
receiver_model.set_am_configuration(current_mode + 7);
|
||||
receiver_model.set_squelch_level(0);
|
||||
field_squelch.set_style(Theme::getInstance()->fg_dark);
|
||||
field_squelch.set_focusable(false);
|
||||
audio::set_rate(audio::Rate::Hz_12000);
|
||||
}
|
||||
baseband::set_moreserx_config(current_mode);
|
||||
};
|
||||
field_squelch.set_value(receiver_model.squelch_level(), false); // will be sent later, no need to send 2x
|
||||
field_squelch.on_change = [this](int32_t v) {
|
||||
if (current_mode == 0)
|
||||
receiver_model.set_squelch_level(v);
|
||||
};
|
||||
options_mode.set_selected_index(current_mode, true);
|
||||
|
||||
auto vol = field_volume.value(); // audio volume fix
|
||||
field_volume.set_value(0);
|
||||
field_volume.set_value(vol);
|
||||
|
||||
logger = std::make_unique<MorseLogger>();
|
||||
chk_log.on_select = [this](Checkbox&, bool save) {
|
||||
save_log = save;
|
||||
if (logger && save_log)
|
||||
logger->init_daily_log(logs_dir);
|
||||
};
|
||||
}
|
||||
|
||||
void MorseLogger::init_daily_log(const std::filesystem::path& log_dir) {
|
||||
auto now = rtc_time::now();
|
||||
std::string pattern = "morse_";
|
||||
uint16_t y = now.year();
|
||||
pattern += (char)('0' + (y / 1000) % 10);
|
||||
pattern += (char)('0' + (y / 100) % 10);
|
||||
pattern += (char)('0' + (y / 10) % 10);
|
||||
pattern += (char)('0' + (y % 10));
|
||||
|
||||
uint8_t m = now.month();
|
||||
pattern += (char)('0' + (m / 10) % 10);
|
||||
pattern += (char)('0' + (m % 10));
|
||||
|
||||
uint8_t d = now.day();
|
||||
pattern += (char)('0' + (d / 10) % 10);
|
||||
pattern += (char)('0' + (d % 10));
|
||||
|
||||
pattern += "_???.TXT";
|
||||
|
||||
auto full_pattern_path = log_dir / pattern;
|
||||
auto final_path = next_filename_matching_pattern(full_pattern_path);
|
||||
if (!final_path.empty()) {
|
||||
this->append(final_path);
|
||||
}
|
||||
}
|
||||
|
||||
void MorseLogger::radio_set_log(uint8_t current_mode) {
|
||||
int64_t freq = receiver_model.target_frequency();
|
||||
std::string header = "Freq:" + to_string_rounded_freq(freq, 4);
|
||||
header += "MHz, ";
|
||||
header += "RX MODE:" + std::string(current_mode == 0 ? "CW/FM" : (current_mode == 1 ? "USB" : "LSB"));
|
||||
header += "\r\nMessage:";
|
||||
log_file.write_raw(header);
|
||||
}
|
||||
|
||||
bool MorseLogger::on_packet(const std::string& content, bool time, uint8_t current_mode) {
|
||||
if (!time) {
|
||||
log_file.write_raw_no_newline("\r\n\r\n");
|
||||
auto timestamp = to_string_datetime(rtc_time::now(), YMDHMS);
|
||||
log_file.write_raw("[" + timestamp + "] ");
|
||||
radio_set_log(current_mode);
|
||||
char_count = 0;
|
||||
time = true;
|
||||
}
|
||||
|
||||
if (content.empty()) return time;
|
||||
std::string s;
|
||||
for (char c : content) {
|
||||
s = c;
|
||||
if ((char_count >= 35 && c == ' ') || (char_count >= 47)) {
|
||||
log_file.write_raw_no_newline("\r\n");
|
||||
|
||||
if (c != ' ') {
|
||||
log_file.write_raw_no_newline(s);
|
||||
char_count = 1;
|
||||
} else {
|
||||
char_count = 0;
|
||||
}
|
||||
} else {
|
||||
log_file.write_raw_no_newline(s);
|
||||
char_count++;
|
||||
}
|
||||
}
|
||||
|
||||
return time;
|
||||
}
|
||||
|
||||
MorseRadioView::~MorseRadioView() {
|
||||
receiver_model.disable();
|
||||
baseband::shutdown();
|
||||
audio::output::stop();
|
||||
}
|
||||
|
||||
void MorseRadioView::focus() {
|
||||
field_frequency.focus();
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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, current_mode);
|
||||
}
|
||||
float dah_time = morse_decoder_.getCurrentTimeUnit() * 3.0f;
|
||||
if (dah_time > 0) {
|
||||
uint16_t wpm = (uint16_t)(3600.0f / (dah_time + 18.0f) + 0.5f);
|
||||
txt_speed.set(to_string_dec_uint(wpm));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MorseRadioView::on_freq(const MorseRXfreqMessage* message) {
|
||||
std::string ret = " ";
|
||||
uint32_t freq = message->measured_frequency;
|
||||
if (freq < 301) {
|
||||
ret = "<";
|
||||
freq = 300;
|
||||
}
|
||||
if (freq > 2299) {
|
||||
freq = 2300;
|
||||
ret = ">";
|
||||
}
|
||||
if (freq < 400 || freq > 1400)
|
||||
txt_freq.set_style(Theme::getInstance()->fg_red);
|
||||
else if (freq <= 580 || freq >= 1220)
|
||||
txt_freq.set_style(Theme::getInstance()->fg_yellow);
|
||||
else
|
||||
txt_freq.set_style(Theme::getInstance()->fg_green);
|
||||
ret += to_string_dec_uint(freq);
|
||||
txt_freq.set(ret);
|
||||
}
|
||||
|
||||
void MorseRadioView::writeCharToConsole(const std::string& ch, double confidence) {
|
||||
if (ch.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
txt_last.set(morse_decoder_.getLastSequence().c_str());
|
||||
|
||||
last_color_id = color_id;
|
||||
std::string color = "";
|
||||
|
||||
if (ch == " ") {
|
||||
color_id = 0;
|
||||
} else if (ch[0] == '{') { // no match
|
||||
color_id = 0;
|
||||
} else {
|
||||
if (confidence < 0.8)
|
||||
color_id = 1;
|
||||
else if (confidence < 0.9)
|
||||
color_id = 2;
|
||||
else
|
||||
color_id = 3;
|
||||
}
|
||||
color = arr_color[color_id];
|
||||
last_color_id = color_id;
|
||||
console_text.write(color + ch);
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::morse_radio
|
||||
@@ -0,0 +1,172 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Pezsma
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef __MORSE_RADIO_H__
|
||||
#define __MORSE_RADIO_H__
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "ui_widget.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "external_app.hpp"
|
||||
#include "ui_freq_field.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "portapack.hpp"
|
||||
#include "message.hpp"
|
||||
#include "audio.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
#include "receiver_model.hpp"
|
||||
#include "tone_key.hpp"
|
||||
#include "log_file.hpp"
|
||||
#include "file_path.hpp"
|
||||
#include "file.hpp"
|
||||
#include "rtc_time.hpp"
|
||||
#include "morsedecoder.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace ui::external_app::morse_radio {
|
||||
|
||||
class MorseRadioView;
|
||||
|
||||
class MorseLogger {
|
||||
public:
|
||||
Optional<File::Error> append(const std::filesystem::path& filename) {
|
||||
return log_file.append(filename);
|
||||
}
|
||||
|
||||
void init_daily_log(const std::filesystem::path& log_dir);
|
||||
bool on_packet(const std::string& content, bool time, uint8_t current_mode);
|
||||
void radio_set_log(uint8_t current_mode);
|
||||
|
||||
private:
|
||||
LogFile log_file{};
|
||||
uint8_t char_count{0}; // log file line break
|
||||
};
|
||||
|
||||
class MorseRadioView : public ui::View {
|
||||
public:
|
||||
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{};
|
||||
|
||||
uint8_t current_mode{0}; // 0=CW/FM, 1=USB, 2=LSB
|
||||
|
||||
app_settings::SettingsManager settings_{
|
||||
"rx_morese_radio",
|
||||
app_settings::Mode::RX,
|
||||
{{"cwmode"sv, ¤t_mode}}};
|
||||
|
||||
RxFrequencyField field_frequency{
|
||||
{UI_POS_X(0), UI_POS_Y(0)},
|
||||
nav_};
|
||||
RFAmpField field_rf_amp{
|
||||
{UI_POS_X(13), UI_POS_Y(0)}};
|
||||
LNAGainField field_lna{
|
||||
{UI_POS_X(15), UI_POS_Y(0)}};
|
||||
VGAGainField field_vga{
|
||||
{UI_POS_X(18), UI_POS_Y(0)}};
|
||||
RSSI rssi{
|
||||
{UI_POS_X(21), UI_POS_Y(0), UI_POS_WIDTH_REMAINING(24), 4}};
|
||||
|
||||
AudioVolumeField field_volume{
|
||||
{UI_POS_X_RIGHT(2), UI_POS_Y(0)}};
|
||||
|
||||
NumberField field_squelch{
|
||||
{UI_POS_X(10), UI_POS_Y(1)},
|
||||
2,
|
||||
{0, 99},
|
||||
1,
|
||||
' ',
|
||||
};
|
||||
ui::Text txt_speed{{UI_POS_X(23), UI_POS_Y(1), UI_POS_WIDTH(3), UI_POS_HEIGHT(1)}, "??"};
|
||||
ui::Text txt_last{{UI_POS_X(12), UI_POS_Y(4), UI_POS_WIDTH_REMAINING(12), UI_POS_HEIGHT(1)}, ""};
|
||||
ui::Text txt_freq{{UI_POS_X(21), UI_POS_Y(2), UI_POS_WIDTH(5), UI_POS_HEIGHT(1)}, "??"};
|
||||
ui::Console console_text{{UI_POS_X(0), UI_POS_Y(5), UI_POS_MAXWIDTH, UI_POS_HEIGHT_REMAINING(7)}};
|
||||
ui::Labels labels{
|
||||
{{UI_POS_X(0), UI_POS_Y(1)}, "Squelch:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(15), UI_POS_Y(1)}, "Speed:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(26), UI_POS_Y(1)}, "wpm", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(4)}, "Last seq.:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(15), UI_POS_Y(2)}, "Tone:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(27), UI_POS_Y(2)}, "Hz", Theme::getInstance()->fg_light->foreground}};
|
||||
|
||||
ui::OptionsField options_mode{
|
||||
{UI_POS_X(9), UI_POS_Y(2)},
|
||||
5,
|
||||
{{"CW/FM", 0}, {"USB", 1}, {"LSB", 2}}};
|
||||
|
||||
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__
|
||||
+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,523 @@
|
||||
/*
|
||||
* 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,
|
||||
&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);
|
||||
};
|
||||
baseband::set_subghzd_config(0, receiver_model.sampling_rate()); // 0=am
|
||||
receiver_model.enable();
|
||||
signal_token_tick_second = rtc_time::signal_tick_second += [this]() {
|
||||
on_tick_second();
|
||||
};
|
||||
}
|
||||
|
||||
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_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);
|
||||
}
|
||||
|
||||
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,222 @@
|
||||
/*
|
||||
* 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;
|
||||
app_settings::SettingsManager settings_{
|
||||
"rx_subcar",
|
||||
app_settings::Mode::RX,
|
||||
{
|
||||
{"log"sv, &logging},
|
||||
}};
|
||||
|
||||
SubCarRecentEntries recent{};
|
||||
|
||||
RFAmpField field_rf_amp{
|
||||
{13 * 8, UI_POS_Y(0)}};
|
||||
LNAGainField field_lna{
|
||||
{15 * 8, UI_POS_Y(0)}};
|
||||
VGAGainField field_vga{
|
||||
{18 * 8, UI_POS_Y(0)}};
|
||||
RSSI rssi{
|
||||
{21 * 8, 0, UI_POS_WIDTH_REMAINING(24), 4}};
|
||||
Channel channel{
|
||||
{21 * 8, 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{
|
||||
{0, 16, 7 * 8, 32},
|
||||
"Clear"};
|
||||
|
||||
Checkbox check_log{
|
||||
{10 * 8, 18},
|
||||
3,
|
||||
"Log",
|
||||
true};
|
||||
|
||||
Labels labels{
|
||||
{{UI_POS_X_RIGHT(14), UI_POS_Y(1)}, "no fm yet :(", Theme::getInstance()->fg_light->foreground},
|
||||
};
|
||||
|
||||
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), 1 * 16, 15 * 8, 16}, "?"};
|
||||
Text text_id{{6 * 8, 2 * 16, 10 * 8, 16}, "?"};
|
||||
|
||||
Console console{
|
||||
{0, 4 * 16, screen_width, screen_height - (4 * 16) - 36}};
|
||||
|
||||
Labels labels{
|
||||
{{UI_POS_X(0), UI_POS_Y(0)}, "Type:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), 2 * 16}, "Serial: ", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), 3 * 16}, "Data:", Theme::getInstance()->fg_light->foreground},
|
||||
};
|
||||
|
||||
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) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -332,6 +332,10 @@ WaterfallView::WaterfallView(const bool cursor) {
|
||||
}
|
||||
};
|
||||
|
||||
load_gradient();
|
||||
}
|
||||
|
||||
void WaterfallView::load_gradient() {
|
||||
if (!waterfall_widget.gradient.load_file(default_gradient_file)) {
|
||||
waterfall_widget.gradient.set_default();
|
||||
}
|
||||
|
||||
@@ -146,6 +146,7 @@ class WaterfallView : public View {
|
||||
|
||||
void set_parent_rect(const Rect new_parent_rect) override;
|
||||
void show_audio_spectrum_view(const bool show);
|
||||
void load_gradient();
|
||||
|
||||
private:
|
||||
void update_widgets_rect();
|
||||
|
||||
@@ -49,10 +49,9 @@
|
||||
#include "ui_rds.hpp"
|
||||
#include "ui_recon.hpp"
|
||||
// #include "ui_scanner.hpp"
|
||||
#include "ui_sd_over_usb.hpp"
|
||||
// #include "ui_sd_over_usb.hpp"
|
||||
#include "ui_search.hpp"
|
||||
#include "ui_settings.hpp"
|
||||
#include "ui_siggen.hpp"
|
||||
#include "ui_sonde.hpp"
|
||||
#include "ui_ss_viewer.hpp"
|
||||
// #include "ui_test.hpp"
|
||||
@@ -114,7 +113,7 @@ const NavigationView::AppList NavigationView::appList = {
|
||||
/* HOME ******************************************************************/
|
||||
{nullptr, "Receive", HOME, Color::cyan(), &bitmap_icon_receivers, new ViewFactory<ReceiversMenuView>()},
|
||||
{nullptr, "Transmit", HOME, Color::cyan(), &bitmap_icon_transmit, new ViewFactory<TransmittersMenuView>()},
|
||||
{nullptr, "Tranceiver", HOME, Color::cyan(), &bitmap_icon_tranceivers, new ViewFactory<TranceiversMenuView>()},
|
||||
{nullptr, "Transceiver", HOME, Color::cyan(), &bitmap_icon_transceivers, new ViewFactory<TransceiversMenuView>()},
|
||||
{"recon", "Recon", HOME, Color::green(), &bitmap_icon_scanner, new ViewFactory<ReconView>()},
|
||||
{"capture", "Capture", HOME, Color::red(), &bitmap_icon_capture, new ViewFactory<CaptureAppView>()},
|
||||
{"replay", "Replay", HOME, Color::green(), &bitmap_icon_replay, new ViewFactory<PlaylistView>()},
|
||||
@@ -140,7 +139,6 @@ const NavigationView::AppList NavigationView::appList = {
|
||||
{"pocsagtx", "POCSAG TX", TX, ui::Color::green(), &bitmap_icon_pocsag, new ViewFactory<POCSAGTXView>()},
|
||||
{"rdstx", "RDS", TX, ui::Color::green(), &bitmap_icon_rds, new ViewFactory<RDSView>()},
|
||||
{"touchtune", "TouchTune", TX, ui::Color::green(), &bitmap_icon_touchtunes, new ViewFactory<TouchTunesView>()},
|
||||
{"signalgen", "SignalGen", TX, Color::green(), &bitmap_icon_cwgen, new ViewFactory<SigGenView>()},
|
||||
/* TRX ********************************************************************/
|
||||
{"microphone", "Mic", TRX, Color::green(), &bitmap_icon_microphone, new ViewFactory<MicTXView>()},
|
||||
/* UTILITIES *************************************************************/
|
||||
@@ -148,7 +146,7 @@ const NavigationView::AppList NavigationView::appList = {
|
||||
{"freqman", "Freq. Manager", UTILITIES, Color::green(), &bitmap_icon_freqman, new ViewFactory<FrequencyManagerView>()},
|
||||
{"iqtrim", "IQ Trim", UTILITIES, Color::orange(), &bitmap_icon_trim, new ViewFactory<IQTrimView>()},
|
||||
{"notepad", "Notepad", UTILITIES, Color::dark_cyan(), &bitmap_icon_notepad, new ViewFactory<TextEditorView>()},
|
||||
{nullptr, "SD Over USB", UTILITIES, Color::yellow(), &bitmap_icon_hackrf, new ViewFactory<SdOverUsbView>()},
|
||||
//{nullptr, "SD Over USB", UTILITIES, Color::yellow(), &bitmap_icon_hackrf, new ViewFactory<SdOverUsbView>()},
|
||||
{nullptr, "Debug", UTILITIES, Color::light_grey(), &bitmap_icon_debug, new ViewFactory<DebugMenuView>()},
|
||||
//{"testapp", "Test App", UTILITIES, Color::dark_grey(), nullptr, new ViewFactory<TestView>()},
|
||||
// Dangerous apps.
|
||||
@@ -320,6 +318,14 @@ SystemStatusView::SystemStatusView(
|
||||
refresh();
|
||||
}
|
||||
|
||||
void SystemStatusView::on_tx_disabled() {
|
||||
if (!nav_.is_valid())
|
||||
return;
|
||||
|
||||
nav_.pop();
|
||||
nav_.display_modal("Error", "RF transmit disabled.\nApplication closed.");
|
||||
}
|
||||
|
||||
// when battery icon / text is clicked
|
||||
void SystemStatusView::on_battery_details() {
|
||||
if (!nav_.is_valid()) return;
|
||||
@@ -601,7 +607,7 @@ bool InformationView::firmware_checksum_error() {
|
||||
|
||||
// only checking firmware checksum once per boot
|
||||
if (!fw_checksum_checked) {
|
||||
fw_checksum_error = (simple_checksum(FLASH_STARTING_ADDRESS, FLASH_ROM_SIZE) != FLASH_EXPECTED_CHECKSUM);
|
||||
fw_checksum_error = (simple_checksum(FLASH_STARTING_ADDRESS, FLASH_SIZE_LIMIT_MB * 1024 * 1024) != FLASH_EXPECTED_CHECKSUM);
|
||||
}
|
||||
return fw_checksum_error;
|
||||
}
|
||||
@@ -837,12 +843,12 @@ void TransmittersMenuView::on_populate() {
|
||||
add_external_items(nav_, app_location_t::TX, *this, return_icon ? 1 : 0);
|
||||
}
|
||||
|
||||
/* TranceiversMenuView **************************************************/
|
||||
/* TransceiversMenuView **************************************************/
|
||||
|
||||
TranceiversMenuView::TranceiversMenuView(NavigationView& nav)
|
||||
TransceiversMenuView::TransceiversMenuView(NavigationView& nav)
|
||||
: nav_(nav) {}
|
||||
|
||||
void TranceiversMenuView::on_populate() {
|
||||
void TransceiversMenuView::on_populate() {
|
||||
bool return_icon = pmem::show_gui_return_icon();
|
||||
if (return_icon) {
|
||||
add_items({{"..", Theme::getInstance()->fg_light->foreground, &bitmap_icon_previous, [this]() { nav_.pop(); }}});
|
||||
|
||||
@@ -303,6 +303,7 @@ class SystemStatusView : public View {
|
||||
void on_title();
|
||||
void refresh();
|
||||
void on_clk();
|
||||
void on_tx_disabled();
|
||||
void rtc_battery_workaround();
|
||||
void on_battery_data(const BatteryStateMessage* msg);
|
||||
void on_battery_details();
|
||||
@@ -314,6 +315,13 @@ class SystemStatusView : public View {
|
||||
this->refresh();
|
||||
}};
|
||||
|
||||
MessageHandlerRegistration message_handler_tx_disabled{
|
||||
Message::ID::TXDisabled,
|
||||
[this](const Message* const p) {
|
||||
(void)p;
|
||||
this->on_tx_disabled();
|
||||
}};
|
||||
|
||||
MessageHandlerRegistration message_handler_battery{
|
||||
Message::ID::BatteryStateData,
|
||||
[this](const Message* const p) {
|
||||
@@ -381,10 +389,10 @@ class TransmittersMenuView : public BtnGridView {
|
||||
void on_populate() override;
|
||||
};
|
||||
|
||||
class TranceiversMenuView : public BtnGridView {
|
||||
class TransceiversMenuView : public BtnGridView {
|
||||
public:
|
||||
TranceiversMenuView(NavigationView& nav);
|
||||
std::string title() const override { return "Tranceiver"; };
|
||||
TransceiversMenuView(NavigationView& nav);
|
||||
std::string title() const override { return "Transceiver"; };
|
||||
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
|
||||
@@ -71,9 +71,9 @@ void nvic_enable_irq(uint8_t irqn) {
|
||||
void usb_configuration_changed(usb_device_t* const device) {
|
||||
(void)device;
|
||||
|
||||
usb_endpoint_init(&usb_endpoint_int_in);
|
||||
usb_endpoint_init(&usb_endpoint_bulk_in);
|
||||
usb_endpoint_init(&usb_endpoint_bulk_out);
|
||||
usb_endpoint_init(&usb_endpoint_int_in, false);
|
||||
usb_endpoint_init(&usb_endpoint_bulk_in, false);
|
||||
usb_endpoint_init(&usb_endpoint_bulk_out, false);
|
||||
}
|
||||
|
||||
void setup_usb_serial_controller(void) {
|
||||
@@ -88,8 +88,8 @@ void setup_usb_serial_controller(void) {
|
||||
usb_queue_init(&usb_endpoint_bulk_out_queue);
|
||||
usb_queue_init(&usb_endpoint_bulk_in_queue);
|
||||
|
||||
usb_endpoint_init(&usb_endpoint_control_out);
|
||||
usb_endpoint_init(&usb_endpoint_control_in);
|
||||
usb_endpoint_init(&usb_endpoint_control_out, false);
|
||||
usb_endpoint_init(&usb_endpoint_control_in, false);
|
||||
|
||||
usb_run(&usb_device);
|
||||
}
|
||||
|
||||
@@ -1383,6 +1383,25 @@ static void cmd_getres(BaseSequentialStream* chp, int argc, char* argv[]) {
|
||||
chprintf(chp, res.c_str());
|
||||
}
|
||||
|
||||
static void cmd_getflash(BaseSequentialStream* chp, int argc, char* argv[]) {
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
uint8_t allowrun = FLASH_SIZE_MB;
|
||||
if (portapack::device_type == portapack::DeviceType::DEV_PORTAPACK) {
|
||||
allowrun = 1;
|
||||
}
|
||||
std::string res = "ALLOWEDFW:" + to_string_dec_uint(FLASH_SIZE_MB) + "\r\nALLOWEDRUNTIME:" + to_string_dec_uint(allowrun) + "\r\nCURRENT:" + to_string_dec_uint(FLASH_SIZE_LIMIT_MB) + "\r\nok\r\n";
|
||||
chprintf(chp, res.c_str());
|
||||
}
|
||||
|
||||
static void cmd_getdevtype(BaseSequentialStream* chp, int argc, char* argv[]) {
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
std::string res = portapack::device_type == portapack::DeviceType::DEV_PORTARF ? "PORTARF" : "PORTAPACK";
|
||||
res += "\r\nok\r\n";
|
||||
chprintf(chp, res.c_str());
|
||||
}
|
||||
|
||||
static const ShellCommand commands[] = {
|
||||
{"reboot", cmd_reboot},
|
||||
{"dfu", cmd_dfu},
|
||||
@@ -1419,6 +1438,8 @@ static const ShellCommand commands[] = {
|
||||
{"asyncmsg", cmd_asyncmsg},
|
||||
{"setfreq", cmd_setfreq},
|
||||
{"getres", cmd_getres},
|
||||
{"getflash", cmd_getflash},
|
||||
{"getdevtype", cmd_getdevtype},
|
||||
{NULL, NULL}};
|
||||
|
||||
static const ShellConfig shell_cfg1 = {
|
||||
|
||||
@@ -447,14 +447,6 @@ set(MODE_CPPSRC
|
||||
)
|
||||
DeclareTargets(PREP replay)
|
||||
|
||||
### Signal generator
|
||||
|
||||
set(MODE_CPPSRC
|
||||
proc_siggen.cpp
|
||||
)
|
||||
DeclareTargets(PSIG siggen)
|
||||
|
||||
|
||||
|
||||
### Tones
|
||||
|
||||
@@ -505,57 +497,17 @@ set(MODE_CPPSRC
|
||||
)
|
||||
DeclareTargets(PFUT flash_utility)
|
||||
|
||||
### SD over USB
|
||||
|
||||
set(MODE_INCDIR
|
||||
${HACKRF_PATH}/firmware
|
||||
${HACKRF_PATH}/firmware/common
|
||||
${HACKRF_PATH}/firmware/libopencm3/include
|
||||
)
|
||||
set(MODE_CPPSRC
|
||||
sd_over_usb/proc_sd_over_usb.cpp
|
||||
|
||||
sd_over_usb/scsi.c
|
||||
sd_over_usb/diskio.c
|
||||
sd_over_usb/sd_over_usb.c
|
||||
sd_over_usb/usb_descriptor.c
|
||||
sd_over_usb/hackrf_core.c
|
||||
|
||||
${HACKRF_PATH}/firmware/common/usb.c
|
||||
${HACKRF_PATH}/firmware/common/usb_queue.c
|
||||
${HACKRF_PATH}/firmware/hackrf_usb/usb_device.c
|
||||
${HACKRF_PATH}/firmware/hackrf_usb/usb_endpoint.c
|
||||
${HACKRF_PATH}/firmware/common/usb_request.c
|
||||
${HACKRF_PATH}/firmware/common/usb_standard_request.c
|
||||
${HACKRF_PATH}/firmware/common/platform_detect.c
|
||||
${HACKRF_PATH}/firmware/common/gpio_lpc.c
|
||||
${HACKRF_PATH}/firmware/common/firmware_info.c
|
||||
${HACKRF_PATH}/firmware/common/si5351c.c
|
||||
${HACKRF_PATH}/firmware/common/i2c_bus.c
|
||||
${HACKRF_PATH}/firmware/common/mixer.c
|
||||
${HACKRF_PATH}/firmware/common/clkin.c
|
||||
${HACKRF_PATH}/firmware/common/spi_bus.c
|
||||
${HACKRF_PATH}/firmware/common/sgpio.c
|
||||
${HACKRF_PATH}/firmware/common/rf_path.c
|
||||
${HACKRF_PATH}/firmware/common/i2c_lpc.c
|
||||
${HACKRF_PATH}/firmware/common/spi_ssp.c
|
||||
${HACKRF_PATH}/firmware/common/rffc5071_spi.c
|
||||
${HACKRF_PATH}/firmware/common/rffc5071.c
|
||||
${HACKRF_PATH}/firmware/common/clkin.c
|
||||
${HACKRF_PATH}/firmware/common/gpdma.c
|
||||
|
||||
${HACKRF_PATH}/firmware/libopencm3/lib/cm3/nvic.c
|
||||
${HACKRF_PATH}/firmware/libopencm3/lib/cm3/sync.c
|
||||
${HACKRF_PATH}/firmware/libopencm3/lib/lpc43xx/scu.c
|
||||
${HACKRF_PATH}/firmware/libopencm3/lib/lpc43xx/timer.c
|
||||
${HACKRF_PATH}/firmware/libopencm3/lib/lpc43xx/i2c.c
|
||||
)
|
||||
DeclareTargets(PUSB sd_over_usb)
|
||||
|
||||
### Place external app and disabled images below so they don't get added to the firmware
|
||||
set(add_to_firmware FALSE)
|
||||
set(MODE_FLAGS "-O3")
|
||||
|
||||
### FLEX RX
|
||||
|
||||
set(MODE_CPPSRC
|
||||
proc_flex.cpp
|
||||
)
|
||||
DeclareTargets(PFLX flex)
|
||||
|
||||
### ACARS RX
|
||||
|
||||
set(MODE_CPPSRC
|
||||
@@ -644,6 +596,13 @@ set(MODE_CPPSRC
|
||||
)
|
||||
DeclareTargets(PSTX sstvtx)
|
||||
|
||||
### SSTV RX
|
||||
|
||||
set(MODE_CPPSRC
|
||||
proc_sstvrx.cpp
|
||||
)
|
||||
DeclareTargets(PSRX sstvrx)
|
||||
|
||||
### TPMS
|
||||
|
||||
set(MODE_CPPSRC
|
||||
@@ -667,6 +626,13 @@ set(MODE_CPPSRC
|
||||
DeclareTargets(POSK ookstream)
|
||||
|
||||
|
||||
### Signal generator
|
||||
|
||||
set(MODE_CPPSRC
|
||||
proc_siggen.cpp
|
||||
)
|
||||
DeclareTargets(PSIG siggen)
|
||||
|
||||
|
||||
### WeFax Rx
|
||||
|
||||
@@ -690,6 +656,68 @@ set(MODE_CPPSRC
|
||||
)
|
||||
DeclareTargets(PATX audio_tx)
|
||||
|
||||
### SubCar Decoders
|
||||
|
||||
set(MODE_CPPSRC
|
||||
proc_subcar.cpp
|
||||
)
|
||||
DeclareTargets(PSCD subcar)
|
||||
|
||||
set(MODE_CPPSRC
|
||||
proc_morse.cpp
|
||||
|
||||
)
|
||||
DeclareTargets(PMRS morse)
|
||||
|
||||
### SD over USB
|
||||
|
||||
set(MODE_INCDIR
|
||||
${HACKRF_PATH}/firmware
|
||||
${HACKRF_PATH}/firmware/common
|
||||
${HACKRF_PATH}/firmware/libopencm3/include
|
||||
)
|
||||
set(MODE_CPPSRC
|
||||
sd_over_usb/proc_sd_over_usb.cpp
|
||||
|
||||
sd_over_usb/scsi.c
|
||||
sd_over_usb/diskio.c
|
||||
sd_over_usb/sd_over_usb.c
|
||||
sd_over_usb/usb_descriptor.c
|
||||
sd_over_usb/hackrf_core.c
|
||||
|
||||
${HACKRF_PATH}/firmware/common/adc.c
|
||||
${HACKRF_PATH}/firmware/common/selftest.c
|
||||
${HACKRF_PATH}/firmware/common/usb.c
|
||||
${HACKRF_PATH}/firmware/common/usb_queue.c
|
||||
${HACKRF_PATH}/firmware/hackrf_usb/usb_device.c
|
||||
${HACKRF_PATH}/firmware/hackrf_usb/usb_endpoint.c
|
||||
${HACKRF_PATH}/firmware/common/usb_request.c
|
||||
${HACKRF_PATH}/firmware/common/usb_standard_request.c
|
||||
${HACKRF_PATH}/firmware/common/platform_detect.c
|
||||
${HACKRF_PATH}/firmware/common/gpio_lpc.c
|
||||
${HACKRF_PATH}/firmware/common/firmware_info.c
|
||||
${HACKRF_PATH}/firmware/common/si5351c.c
|
||||
${HACKRF_PATH}/firmware/common/i2c_bus.c
|
||||
${HACKRF_PATH}/firmware/common/mixer.c
|
||||
${HACKRF_PATH}/firmware/common/clkin.c
|
||||
${HACKRF_PATH}/firmware/common/spi_bus.c
|
||||
${HACKRF_PATH}/firmware/common/sgpio.c
|
||||
${HACKRF_PATH}/firmware/common/rf_path.c
|
||||
${HACKRF_PATH}/firmware/common/i2c_lpc.c
|
||||
${HACKRF_PATH}/firmware/common/spi_ssp.c
|
||||
${HACKRF_PATH}/firmware/common/rffc5071_spi.c
|
||||
${HACKRF_PATH}/firmware/common/rffc5071.c
|
||||
${HACKRF_PATH}/firmware/common/clkin.c
|
||||
${HACKRF_PATH}/firmware/common/gpdma.c
|
||||
|
||||
${HACKRF_PATH}/firmware/libopencm3/lib/cm3/nvic.c
|
||||
${HACKRF_PATH}/firmware/libopencm3/lib/cm3/sync.c
|
||||
${HACKRF_PATH}/firmware/libopencm3/lib/lpc43xx/scu.c
|
||||
${HACKRF_PATH}/firmware/libopencm3/lib/lpc43xx/timer.c
|
||||
${HACKRF_PATH}/firmware/libopencm3/lib/lpc43xx/i2c.c
|
||||
)
|
||||
DeclareTargets(PUSB sd_over_usb)
|
||||
|
||||
|
||||
### HackRF "factory" firmware
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ class BasebandProcessor {
|
||||
|
||||
virtual void execute(const buffer_c8_t& buffer) = 0;
|
||||
|
||||
virtual void on_message(const Message* const){};
|
||||
virtual void on_message(const Message* const) {};
|
||||
|
||||
protected:
|
||||
void feed_channel_stats(const buffer_c16_t& channel);
|
||||
|
||||
@@ -30,8 +30,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;
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
#pragma once
|
||||
#include "subcarbase.hpp"
|
||||
#include <cstring>
|
||||
|
||||
typedef enum {
|
||||
BMWDecoderStepReset = 0,
|
||||
BMWDecoderStepCheckPreambula,
|
||||
BMWDecoderStepSaveDuration,
|
||||
BMWDecoderStepCheckDuration,
|
||||
} BMWDecoderStep;
|
||||
|
||||
class FProtoSubCarBMWV0 : public FProtoSubCarBase {
|
||||
public:
|
||||
FProtoSubCarBMWV0() {
|
||||
sensorType = FPC_BMWV0;
|
||||
te_short = 350;
|
||||
te_long = 700;
|
||||
te_delta = 120;
|
||||
min_count_bit_for_found = 61;
|
||||
}
|
||||
uint8_t subghz_protocol_bmw_crc8(uint8_t* data, size_t len) {
|
||||
uint8_t crc = 0x00;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
crc ^= data[i];
|
||||
for (uint8_t j = 0; j < 8; j++) {
|
||||
if (crc & 0x80)
|
||||
crc = (uint8_t)((crc << 1) ^ 0x31);
|
||||
else
|
||||
crc <<= 1;
|
||||
}
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
uint16_t subghz_protocol_bmw_crc16(uint8_t* data, size_t len) {
|
||||
uint16_t crc = 0xFFFF;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
crc ^= ((uint16_t)data[i] << 8);
|
||||
for (uint8_t j = 0; j < 8; j++) {
|
||||
if (crc & 0x8000)
|
||||
crc = (crc << 1) ^ 0x1021;
|
||||
else
|
||||
crc <<= 1;
|
||||
}
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
void subghz_protocol_decoder_bmw_reset_internal() {
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
decode_data2 = 0;
|
||||
parser_step = BMWDecoderStepReset;
|
||||
header_count = 0;
|
||||
crc_type = 0;
|
||||
}
|
||||
|
||||
void feed(bool level, uint32_t duration) {
|
||||
switch (parser_step) {
|
||||
case BMWDecoderStepReset:
|
||||
if (level && (DURATION_DIFF(duration, te_short) <
|
||||
te_delta)) {
|
||||
parser_step = BMWDecoderStepCheckPreambula;
|
||||
te_last = duration;
|
||||
header_count = 0;
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case BMWDecoderStepCheckPreambula:
|
||||
if (level) {
|
||||
if ((DURATION_DIFF(duration, te_short) <
|
||||
te_delta) ||
|
||||
(DURATION_DIFF(duration, te_long) <
|
||||
te_delta)) {
|
||||
te_last = duration;
|
||||
} else {
|
||||
parser_step = BMWDecoderStepReset;
|
||||
}
|
||||
} else if (
|
||||
(DURATION_DIFF(duration, te_short) <
|
||||
te_delta) &&
|
||||
(DURATION_DIFF(te_last, te_short) <
|
||||
te_delta)) {
|
||||
header_count++;
|
||||
} else if (
|
||||
(DURATION_DIFF(duration, te_long) <
|
||||
te_delta) &&
|
||||
(DURATION_DIFF(te_last, te_long) <
|
||||
te_delta)) {
|
||||
if (header_count > 15) {
|
||||
parser_step = BMWDecoderStepSaveDuration;
|
||||
decode_data = 0ULL;
|
||||
decode_count_bit = 0;
|
||||
} else {
|
||||
parser_step = BMWDecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
parser_step = BMWDecoderStepReset;
|
||||
}
|
||||
break;
|
||||
|
||||
case BMWDecoderStepSaveDuration:
|
||||
if (level) {
|
||||
if (duration >=
|
||||
(te_long + te_delta * 2UL)) {
|
||||
if (decode_count_bit >=
|
||||
min_count_bit_for_found) {
|
||||
// instance->generic.data = decode_data;
|
||||
data_count_bit = decode_count_bit;
|
||||
|
||||
// Perform CRC check with both CRC8 and CRC16
|
||||
uint8_t* raw_bytes = (uint8_t*)decode_data;
|
||||
size_t raw_len = (decode_count_bit + 7) / 8;
|
||||
uint8_t crc8 = subghz_protocol_bmw_crc8(raw_bytes, raw_len - 1);
|
||||
if (crc8 == raw_bytes[raw_len - 1]) {
|
||||
crc_type = 8;
|
||||
} else {
|
||||
uint16_t crc16 = subghz_protocol_bmw_crc16(raw_bytes, raw_len - 2);
|
||||
uint16_t rx_crc16 = (raw_bytes[raw_len - 2] << 8) | raw_bytes[raw_len - 1];
|
||||
if (crc16 == rx_crc16) {
|
||||
crc_type = 16;
|
||||
} else {
|
||||
crc_type = 0; // invalid
|
||||
}
|
||||
}
|
||||
|
||||
if (crc_type != 0 && callback) {
|
||||
callback(this);
|
||||
}
|
||||
}
|
||||
subghz_protocol_decoder_bmw_reset_internal();
|
||||
} else {
|
||||
te_last = duration;
|
||||
parser_step = BMWDecoderStepCheckDuration;
|
||||
}
|
||||
} else {
|
||||
parser_step = BMWDecoderStepReset;
|
||||
}
|
||||
break;
|
||||
|
||||
case BMWDecoderStepCheckDuration:
|
||||
if (!level) {
|
||||
if ((DURATION_DIFF(te_last, te_short) <
|
||||
te_delta) &&
|
||||
(DURATION_DIFF(duration, te_short) <
|
||||
te_delta)) {
|
||||
subghz_protocol_blocks_add_bit(0);
|
||||
parser_step = BMWDecoderStepSaveDuration;
|
||||
} else if (
|
||||
(DURATION_DIFF(te_last, te_long) <
|
||||
te_delta) &&
|
||||
(DURATION_DIFF(duration, te_long) <
|
||||
te_delta)) {
|
||||
subghz_protocol_blocks_add_bit(1);
|
||||
parser_step = BMWDecoderStepSaveDuration;
|
||||
} else {
|
||||
parser_step = BMWDecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
parser_step = BMWDecoderStepReset;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t header_count = 0;
|
||||
uint8_t crc_type = 0;
|
||||
};
|
||||
@@ -0,0 +1,200 @@
|
||||
#pragma once
|
||||
#include "subcarbase.hpp"
|
||||
#include <cstring>
|
||||
|
||||
typedef enum {
|
||||
FiatV0DecoderStepReset = 0,
|
||||
FiatV0DecoderStepPreamble = 1,
|
||||
FiatV0DecoderStepData = 2,
|
||||
} FiatV0DecoderStep;
|
||||
|
||||
class FProtoSubCarFiatV0 : public FProtoSubCarBase {
|
||||
public:
|
||||
FProtoSubCarFiatV0() {
|
||||
sensorType = FPC_FIATV0;
|
||||
te_short = 200;
|
||||
te_long = 400;
|
||||
te_delta = 100;
|
||||
min_count_bit_for_found = 64;
|
||||
}
|
||||
|
||||
void feed(bool level, uint32_t duration) {
|
||||
uint32_t gap_threshold = 800;
|
||||
uint32_t diff;
|
||||
switch (decoder_state) {
|
||||
case FiatV0DecoderStepReset:
|
||||
if (!level) {
|
||||
return;
|
||||
}
|
||||
if (duration < te_short) {
|
||||
diff = te_short - duration;
|
||||
} else {
|
||||
diff = duration - te_short;
|
||||
}
|
||||
if (diff < te_delta) {
|
||||
data_low = 0;
|
||||
data_high = 0;
|
||||
decoder_state = FiatV0DecoderStepPreamble;
|
||||
te_last = duration;
|
||||
preamble_count = 0;
|
||||
bit_count = 0;
|
||||
FProtoGeneral::manchester_advance(
|
||||
manchester_state,
|
||||
ManchesterEventReset,
|
||||
&manchester_state,
|
||||
NULL);
|
||||
}
|
||||
break;
|
||||
case FiatV0DecoderStepPreamble:
|
||||
if (level) {
|
||||
return;
|
||||
}
|
||||
if (duration < te_short) {
|
||||
diff = te_short - duration;
|
||||
if (diff < te_delta) {
|
||||
preamble_count++;
|
||||
te_last = duration;
|
||||
if (preamble_count >= 0x96) {
|
||||
if (duration < gap_threshold) {
|
||||
diff = gap_threshold - duration;
|
||||
} else {
|
||||
diff = duration - gap_threshold;
|
||||
}
|
||||
if (diff < te_delta) {
|
||||
decoder_state = FiatV0DecoderStepData;
|
||||
preamble_count = 0;
|
||||
data_low = 0;
|
||||
data_high = 0;
|
||||
bit_count = 0;
|
||||
te_last = duration;
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
decoder_state = FiatV0DecoderStepReset;
|
||||
if (preamble_count >= 0x96) {
|
||||
if (duration < gap_threshold) {
|
||||
diff = gap_threshold - duration;
|
||||
} else {
|
||||
diff = duration - gap_threshold;
|
||||
}
|
||||
if (diff < te_delta) {
|
||||
decoder_state = FiatV0DecoderStepData;
|
||||
preamble_count = 0;
|
||||
data_low = 0;
|
||||
data_high = 0;
|
||||
bit_count = 0;
|
||||
te_last = duration;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
diff = duration - te_short;
|
||||
if (diff < te_delta) {
|
||||
preamble_count++;
|
||||
te_last = duration;
|
||||
} else {
|
||||
decoder_state = FiatV0DecoderStepReset;
|
||||
}
|
||||
if (preamble_count >= 0x96) {
|
||||
if (duration >= 799) {
|
||||
diff = duration - gap_threshold;
|
||||
} else {
|
||||
diff = gap_threshold - duration;
|
||||
}
|
||||
if (diff < te_delta) {
|
||||
decoder_state = FiatV0DecoderStepData;
|
||||
preamble_count = 0;
|
||||
data_low = 0;
|
||||
data_high = 0;
|
||||
bit_count = 0;
|
||||
te_last = duration;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case FiatV0DecoderStepData:
|
||||
ManchesterEvent event = ManchesterEventReset;
|
||||
if (duration < te_short) {
|
||||
diff = te_short - duration;
|
||||
if (diff < te_delta) {
|
||||
event = level ? ManchesterEventShortLow : ManchesterEventShortHigh;
|
||||
}
|
||||
} else {
|
||||
diff = duration - te_short;
|
||||
if (diff < te_delta) {
|
||||
event = level ? ManchesterEventShortLow : ManchesterEventShortHigh;
|
||||
} else {
|
||||
if (duration < te_long) {
|
||||
diff = te_long - duration;
|
||||
} else {
|
||||
diff = duration - te_long;
|
||||
}
|
||||
if (diff < te_delta) {
|
||||
event = level ? ManchesterEventLongLow : ManchesterEventLongHigh;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (event != ManchesterEventReset) {
|
||||
bool data_bit_bool;
|
||||
if (FProtoGeneral::manchester_advance(
|
||||
manchester_state,
|
||||
event,
|
||||
&manchester_state,
|
||||
&data_bit_bool)) {
|
||||
uint32_t new_bit = data_bit_bool ? 1 : 0;
|
||||
|
||||
uint32_t carry = (data_low >> 31) & 1;
|
||||
data_low = (data_low << 1) | new_bit;
|
||||
data_high = (data_high << 1) | carry;
|
||||
|
||||
bit_count++;
|
||||
|
||||
if (bit_count == 0x40) {
|
||||
fix = data_low;
|
||||
hop = data_high;
|
||||
data_low = 0;
|
||||
data_high = 0;
|
||||
}
|
||||
|
||||
if (bit_count > 0x46) {
|
||||
endbyte = (uint8_t)data_low;
|
||||
/*
|
||||
generic.data = ((uint64_t)hop << 32) | fix;
|
||||
generic.data_count_bit = 64;
|
||||
generic.serial = fix;
|
||||
generic.btn = endbyte; // still exported as btn for UI compatibility
|
||||
generic.cnt = hop;
|
||||
*/
|
||||
decode_data = ((uint64_t)hop << 32) | fix; // this is my own data passer, not the original
|
||||
decode_data2 = endbyte;
|
||||
data_count_bit = 64;
|
||||
if (callback) {
|
||||
callback(this);
|
||||
}
|
||||
|
||||
data_low = 0;
|
||||
data_high = 0;
|
||||
bit_count = 0;
|
||||
decoder_state = FiatV0DecoderStepReset;
|
||||
}
|
||||
}
|
||||
}
|
||||
te_last = duration;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ManchesterState manchester_state = ManchesterStateMid1;
|
||||
uint8_t decoder_state = 0;
|
||||
uint8_t bit_count = 0;
|
||||
uint8_t endbyte = 0;
|
||||
uint16_t preamble_count = 0;
|
||||
uint32_t data_low = 0;
|
||||
uint32_t data_high = 0;
|
||||
uint32_t hop = 0;
|
||||
uint32_t fix = 0;
|
||||
};
|
||||
@@ -0,0 +1,151 @@
|
||||
#pragma once
|
||||
#include "subcarbase.hpp"
|
||||
#include <cstring>
|
||||
|
||||
typedef enum {
|
||||
FordV0DecoderStepReset = 0,
|
||||
FordV0DecoderStepPreamble,
|
||||
FordV0DecoderStepPreambleCheck,
|
||||
FordV0DecoderStepGap,
|
||||
FordV0DecoderStepData,
|
||||
} FordV0DecoderStep;
|
||||
|
||||
class FProtoSubCarFordV0 : public FProtoSubCarBase {
|
||||
public:
|
||||
FProtoSubCarFordV0() {
|
||||
sensorType = FPC_FORDV0;
|
||||
te_short = 250;
|
||||
te_long = 500;
|
||||
te_delta = 100;
|
||||
min_count_bit_for_found = 64;
|
||||
}
|
||||
void ford_v0_add_bit(bool bit) {
|
||||
uint32_t low = (uint32_t)data_low;
|
||||
data_low = (data_low << 1) | (bit ? 1 : 0);
|
||||
data_high = (data_high << 1) | ((low >> 31) & 1);
|
||||
bit_count++;
|
||||
}
|
||||
|
||||
bool ford_v0_process_data() {
|
||||
if (bit_count == 64) {
|
||||
uint64_t combined = ((uint64_t)data_high << 32) | data_low;
|
||||
key1 = ~combined;
|
||||
data_low = 0;
|
||||
data_high = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (bit_count == 80) {
|
||||
uint16_t key2_raw = (uint16_t)(data_low & 0xFFFF);
|
||||
uint16_t key2 = ~key2_raw;
|
||||
decode_data = key1;
|
||||
decode_data2 = key2;
|
||||
// decode_ford_v0(key1, key2, &serial, &button, &count);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void feed(bool level, uint32_t duration) {
|
||||
uint32_t gap_threshold = 3500;
|
||||
|
||||
switch (parser_step) {
|
||||
case FordV0DecoderStepReset:
|
||||
if (level && (DURATION_DIFF(duration, te_short) < te_delta)) {
|
||||
data_low = 0;
|
||||
data_high = 0;
|
||||
parser_step = FordV0DecoderStepPreamble;
|
||||
te_last = duration;
|
||||
header_count = 0;
|
||||
bit_count = 0;
|
||||
FProtoGeneral::manchester_advance(manchester_state, ManchesterEventReset, &manchester_state, NULL);
|
||||
}
|
||||
break;
|
||||
|
||||
case FordV0DecoderStepPreamble:
|
||||
if (!level) {
|
||||
if (DURATION_DIFF(duration, te_long) < te_delta) {
|
||||
te_last = duration;
|
||||
parser_step = FordV0DecoderStepPreambleCheck;
|
||||
} else {
|
||||
parser_step = FordV0DecoderStepReset;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case FordV0DecoderStepPreambleCheck:
|
||||
if (level) {
|
||||
if (DURATION_DIFF(duration, te_long) < te_delta) {
|
||||
header_count++;
|
||||
te_last = duration;
|
||||
parser_step = FordV0DecoderStepPreamble;
|
||||
} else if (DURATION_DIFF(duration, te_short) < te_delta) {
|
||||
parser_step = FordV0DecoderStepGap;
|
||||
} else {
|
||||
parser_step = FordV0DecoderStepReset;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case FordV0DecoderStepGap:
|
||||
if (!level && (DURATION_DIFF(duration, gap_threshold) < 250)) {
|
||||
data_low = 1;
|
||||
data_high = 0;
|
||||
bit_count = 1;
|
||||
parser_step = FordV0DecoderStepData;
|
||||
} else if (!level && duration > gap_threshold + 250) {
|
||||
parser_step = FordV0DecoderStepReset;
|
||||
}
|
||||
break;
|
||||
|
||||
case FordV0DecoderStepData: {
|
||||
ManchesterEvent event;
|
||||
|
||||
if (DURATION_DIFF(duration, te_short) < te_delta) {
|
||||
event = level ? ManchesterEventShortLow : ManchesterEventShortHigh;
|
||||
} else if (DURATION_DIFF(duration, te_long) < te_delta) {
|
||||
event = level ? ManchesterEventLongLow : ManchesterEventLongHigh;
|
||||
} else {
|
||||
parser_step = FordV0DecoderStepReset;
|
||||
break;
|
||||
}
|
||||
|
||||
bool data_bit;
|
||||
if (FProtoGeneral::manchester_advance(manchester_state, event, &manchester_state, &data_bit)) {
|
||||
ford_v0_add_bit(data_bit);
|
||||
if (ford_v0_process_data()) {
|
||||
/* instance->generic.data = instance->key1;
|
||||
instance->generic.data_count_bit = 64;
|
||||
instance->generic.serial = instance->serial;
|
||||
instance->generic.btn = instance->button;
|
||||
instance->generic.cnt = instance->count;
|
||||
*/
|
||||
data_count_bit = 64;
|
||||
if (callback) {
|
||||
callback(this);
|
||||
}
|
||||
|
||||
data_low = 0;
|
||||
data_high = 0;
|
||||
bit_count = 0;
|
||||
parser_step = FordV0DecoderStepReset;
|
||||
}
|
||||
}
|
||||
|
||||
te_last = duration;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ManchesterState manchester_state = ManchesterStateMid1;
|
||||
|
||||
uint64_t data_low = 0;
|
||||
uint64_t data_high = 0;
|
||||
uint8_t bit_count = 0;
|
||||
|
||||
uint16_t header_count = 0;
|
||||
uint64_t key1 = 0;
|
||||
uint16_t key2 = 0;
|
||||
};
|
||||
@@ -0,0 +1,120 @@
|
||||
#pragma once
|
||||
#include "subcarbase.hpp"
|
||||
#include <cstring>
|
||||
|
||||
typedef enum {
|
||||
KIADecoderStepReset = 0,
|
||||
KIADecoderStepCheckPreambula,
|
||||
KIADecoderStepSaveDuration,
|
||||
KIADecoderStepCheckDuration,
|
||||
} KIADecoderStep;
|
||||
|
||||
class FProtoSubCarKiaV0 : public FProtoSubCarBase {
|
||||
public:
|
||||
FProtoSubCarKiaV0() {
|
||||
sensorType = FPC_KIAV0;
|
||||
te_short = 250;
|
||||
te_long = 500;
|
||||
te_delta = 100;
|
||||
min_count_bit_for_found = 61;
|
||||
}
|
||||
|
||||
void feed(bool level, uint32_t duration) {
|
||||
switch (parser_step) {
|
||||
case KIADecoderStepReset:
|
||||
if ((level) && (DURATION_DIFF(duration, te_short) < te_delta)) {
|
||||
parser_step = KIADecoderStepCheckPreambula;
|
||||
te_last = duration;
|
||||
header_count = 0;
|
||||
}
|
||||
break;
|
||||
case KIADecoderStepCheckPreambula:
|
||||
if (level) {
|
||||
if ((DURATION_DIFF(duration, te_short) < te_delta) ||
|
||||
(DURATION_DIFF(duration, te_long) < te_delta)) {
|
||||
te_last = duration;
|
||||
} else {
|
||||
parser_step = KIADecoderStepReset;
|
||||
}
|
||||
} else if (
|
||||
(DURATION_DIFF(duration, te_short) < te_delta) &&
|
||||
(DURATION_DIFF(te_last, te_short) < te_delta)) {
|
||||
header_count++;
|
||||
break;
|
||||
} else if (
|
||||
(DURATION_DIFF(duration, te_long) < te_delta) &&
|
||||
(DURATION_DIFF(te_last, te_long) < te_delta)) {
|
||||
if (header_count > 15) {
|
||||
parser_step = KIADecoderStepSaveDuration;
|
||||
decode_data = 0;
|
||||
decode_count_bit = 1;
|
||||
subghz_protocol_blocks_add_bit(1);
|
||||
// FURI_LOG_I(TAG, "Starting data decode after %u header pulses", header_count);
|
||||
} else {
|
||||
parser_step = KIADecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
parser_step = KIADecoderStepReset;
|
||||
}
|
||||
break;
|
||||
case KIADecoderStepSaveDuration:
|
||||
if (level) {
|
||||
if (duration >= (te_long + te_delta * 2UL)) {
|
||||
// End of transmission detected
|
||||
parser_step = KIADecoderStepReset;
|
||||
|
||||
if (decode_count_bit == min_count_bit_for_found) {
|
||||
// data = decode_data;
|
||||
data_count_bit = decode_count_bit;
|
||||
// just used for log yet, so skip
|
||||
// if (kia_verify_crc()) {
|
||||
// FURI_LOG_I(TAG, "Valid signal received with correct CRC");
|
||||
// } else {
|
||||
// FURI_LOG_W(TAG, "Signal received but CRC mismatch!");
|
||||
// }
|
||||
|
||||
if (callback)
|
||||
callback(this);
|
||||
} else {
|
||||
// FURI_LOG_E(TAG, "Incomplete signal: only %u bits", instance->decoder.decode_count_bit);
|
||||
}
|
||||
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
break;
|
||||
} else {
|
||||
te_last = duration;
|
||||
parser_step = KIADecoderStepCheckDuration;
|
||||
}
|
||||
|
||||
} else {
|
||||
parser_step = KIADecoderStepReset;
|
||||
}
|
||||
break;
|
||||
case KIADecoderStepCheckDuration:
|
||||
if (!level) {
|
||||
if ((DURATION_DIFF(te_last, te_short) < te_delta) && (DURATION_DIFF(duration, te_short) < te_delta)) {
|
||||
subghz_protocol_blocks_add_bit(0);
|
||||
parser_step = KIADecoderStepSaveDuration;
|
||||
} else if (
|
||||
(DURATION_DIFF(te_last, te_long) < te_delta) && (DURATION_DIFF(duration, te_long) < te_delta)) {
|
||||
subghz_protocol_blocks_add_bit(1);
|
||||
parser_step = KIADecoderStepSaveDuration;
|
||||
} else {
|
||||
// FURI_LOG_W(TAG, "Timing mismatch at bit %u. Last: %lu, Current: %lu", decode_count_bit, te_last, duration);
|
||||
parser_step = KIADecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
parser_step = KIADecoderStepReset;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool is_running = false;
|
||||
size_t preamble_count = 0;
|
||||
size_t data_bit_index = 0;
|
||||
uint8_t last_bit = 0;
|
||||
bool send_high = false;
|
||||
uint16_t header_count = 0;
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
#pragma once
|
||||
#include "subcarbase.hpp"
|
||||
#include <cstring>
|
||||
|
||||
typedef enum {
|
||||
KiaV1DecoderStepReset = 0,
|
||||
KiaV1DecoderStepCheckPreamble,
|
||||
KiaV1DecoderStepDecodeData,
|
||||
} KiaV1DecoderStep;
|
||||
|
||||
class FProtoSubCarKiaV1 : public FProtoSubCarBase {
|
||||
public:
|
||||
FProtoSubCarKiaV1() {
|
||||
sensorType = FPC_KIAV1;
|
||||
te_short = 800;
|
||||
te_long = 1600;
|
||||
te_delta = 200;
|
||||
min_count_bit_for_found = 57;
|
||||
}
|
||||
|
||||
void feed(bool level, uint32_t duration) {
|
||||
ManchesterEvent event = ManchesterEventReset;
|
||||
|
||||
switch (parser_step) {
|
||||
case KiaV1DecoderStepReset:
|
||||
if ((level) && (DURATION_DIFF(duration, te_long) < te_delta)) {
|
||||
parser_step = KiaV1DecoderStepCheckPreamble;
|
||||
te_last = duration;
|
||||
header_count = 0;
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
FProtoGeneral::manchester_advance(manchester_saved_state, ManchesterEventReset, &manchester_saved_state, NULL);
|
||||
}
|
||||
break;
|
||||
|
||||
case KiaV1DecoderStepCheckPreamble:
|
||||
if (!level) {
|
||||
if ((DURATION_DIFF(duration, te_long) < te_delta) && (DURATION_DIFF(te_last, te_long) < te_delta)) {
|
||||
header_count++;
|
||||
te_last = duration;
|
||||
} else {
|
||||
parser_step = KiaV1DecoderStepReset;
|
||||
}
|
||||
}
|
||||
if (header_count > 70) {
|
||||
if ((!level) && (DURATION_DIFF(duration, te_short) < te_delta) && (DURATION_DIFF(te_last, te_long) < te_delta)) {
|
||||
decode_count_bit = 1;
|
||||
subghz_protocol_blocks_add_bit(1);
|
||||
header_count = 0;
|
||||
parser_step = KiaV1DecoderStepDecodeData;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case KiaV1DecoderStepDecodeData:
|
||||
if ((DURATION_DIFF(duration, te_short) < te_delta)) {
|
||||
event = level ? ManchesterEventShortLow : ManchesterEventShortHigh;
|
||||
} else if ((DURATION_DIFF(duration, te_long) <
|
||||
te_delta)) {
|
||||
event = level ? ManchesterEventLongLow : ManchesterEventLongHigh;
|
||||
}
|
||||
|
||||
if (event != ManchesterEventReset) {
|
||||
bool data;
|
||||
bool data_ok = FProtoGeneral::manchester_advance(manchester_saved_state, event, &manchester_saved_state, &data);
|
||||
if (data_ok) {
|
||||
decode_data = (decode_data << 1) | data;
|
||||
decode_count_bit++;
|
||||
}
|
||||
}
|
||||
|
||||
if (decode_count_bit == min_count_bit_for_found) {
|
||||
// instance->generic.data = decode_data;
|
||||
data_count_bit = decode_count_bit;
|
||||
if (callback)
|
||||
callback(this);
|
||||
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
parser_step = KiaV1DecoderStepReset;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t header_count = 0;
|
||||
ManchesterState manchester_saved_state = ManchesterStateStart1;
|
||||
};
|
||||
@@ -0,0 +1,108 @@
|
||||
#pragma once
|
||||
#include "subcarbase.hpp"
|
||||
#include <cstring>
|
||||
|
||||
typedef enum {
|
||||
KiaV2DecoderStepReset = 0,
|
||||
KiaV2DecoderStepCheckPreamble,
|
||||
KiaV2DecoderStepCollectRawBits,
|
||||
} KiaV2DecoderStep;
|
||||
|
||||
class FProtoSubCarKiaV2 : public FProtoSubCarBase {
|
||||
public:
|
||||
FProtoSubCarKiaV2() {
|
||||
sensorType = FPC_KIAV2;
|
||||
te_short = 500;
|
||||
te_long = 1000;
|
||||
te_delta = 160;
|
||||
min_count_bit_for_found = 53;
|
||||
}
|
||||
|
||||
void feed(bool level, uint32_t duration) {
|
||||
switch (parser_step) {
|
||||
case KiaV2DecoderStepReset:
|
||||
if ((level) && (DURATION_DIFF(duration, te_long) < te_delta)) {
|
||||
parser_step = KiaV2DecoderStepCheckPreamble;
|
||||
te_last = duration;
|
||||
header_count = 0;
|
||||
FProtoGeneral::manchester_advance(manchester_state, ManchesterEventReset, &manchester_state, NULL);
|
||||
}
|
||||
break;
|
||||
case KiaV2DecoderStepCheckPreamble:
|
||||
if (level) // HIGH pulse
|
||||
{
|
||||
if (DURATION_DIFF(duration, te_long) < te_delta) {
|
||||
te_last = duration;
|
||||
header_count++;
|
||||
} else if (
|
||||
DURATION_DIFF(duration, te_short) < te_delta) {
|
||||
if (header_count >= 100) {
|
||||
header_count = 0;
|
||||
decode_data = 0;
|
||||
decode_count_bit = 1;
|
||||
parser_step = KiaV2DecoderStepCollectRawBits;
|
||||
subghz_protocol_blocks_add_bit(1);
|
||||
} else {
|
||||
te_last = duration;
|
||||
}
|
||||
} else {
|
||||
parser_step = KiaV2DecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
if (DURATION_DIFF(duration, te_long) < te_delta) {
|
||||
header_count++;
|
||||
te_last = duration;
|
||||
} else if (
|
||||
DURATION_DIFF(duration, te_short) < te_delta) {
|
||||
te_last = duration;
|
||||
} else {
|
||||
parser_step = KiaV2DecoderStepReset;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case KiaV2DecoderStepCollectRawBits: {
|
||||
ManchesterEvent event;
|
||||
|
||||
if (DURATION_DIFF(duration, te_short) < te_delta) {
|
||||
event = level ? ManchesterEventShortLow : ManchesterEventShortHigh;
|
||||
} else if (
|
||||
DURATION_DIFF(duration, te_long) < te_delta) {
|
||||
event = level ? ManchesterEventLongLow : ManchesterEventLongHigh;
|
||||
} else {
|
||||
parser_step = KiaV2DecoderStepReset;
|
||||
break;
|
||||
}
|
||||
|
||||
bool data_bit;
|
||||
if (FProtoGeneral::manchester_advance(manchester_state, event, &manchester_state, &data_bit)) {
|
||||
decode_data = (decode_data << 1) | data_bit;
|
||||
decode_count_bit++;
|
||||
|
||||
if (decode_count_bit == 53) {
|
||||
// instance->generic.data = decode_data;
|
||||
data_count_bit = decode_count_bit;
|
||||
|
||||
// instance->generic.serial = (uint32_t)((instance->generic.data >> 20) & 0xFFFFFFFF);
|
||||
// instance->generic.btn = (uint8_t)((instance->generic.data >> 16) & 0x0F);
|
||||
|
||||
// uint16_t raw_count = (uint16_t)((instance->generic.data >> 4) & 0xFFF);
|
||||
// instance->generic.cnt = ((raw_count >> 4) | (raw_count << 8)) & 0xFFF;
|
||||
|
||||
if (callback)
|
||||
callback(this);
|
||||
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
header_count = 0;
|
||||
parser_step = KiaV2DecoderStepReset;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t header_count = 0;
|
||||
ManchesterState manchester_state = ManchesterStateMid1;
|
||||
};
|
||||
@@ -0,0 +1,177 @@
|
||||
#pragma once
|
||||
#include "subcarbase.hpp"
|
||||
#include <cstring>
|
||||
|
||||
typedef enum {
|
||||
KiaV3V4DecoderStepReset = 0,
|
||||
KiaV3V4DecoderStepCheckPreamble,
|
||||
KiaV3V4DecoderStepCollectRawBits,
|
||||
} KiaV3V4DecoderStep;
|
||||
|
||||
class FProtoSubCarKiaV3V4 : public FProtoSubCarBase {
|
||||
public:
|
||||
FProtoSubCarKiaV3V4() {
|
||||
sensorType = FPC_KIAV3V4;
|
||||
te_short = 400;
|
||||
te_long = 800;
|
||||
te_delta = 150;
|
||||
min_count_bit_for_found = 68;
|
||||
}
|
||||
uint8_t reverse8(uint8_t byte) {
|
||||
byte = (byte & 0xF0) >> 4 | (byte & 0x0F) << 4;
|
||||
byte = (byte & 0xCC) >> 2 | (byte & 0x33) << 2;
|
||||
byte = (byte & 0xAA) >> 1 | (byte & 0x55) << 1;
|
||||
return byte;
|
||||
}
|
||||
void kia_v3_v4_add_raw_bit(bool bit) {
|
||||
if (raw_bit_count < 256) {
|
||||
uint16_t byte_idx = raw_bit_count / 8;
|
||||
uint8_t bit_idx = 7 - (raw_bit_count % 8);
|
||||
if (bit) {
|
||||
raw_bits[byte_idx] |= (1 << bit_idx);
|
||||
} else {
|
||||
raw_bits[byte_idx] &= ~(1 << bit_idx);
|
||||
}
|
||||
raw_bit_count++;
|
||||
}
|
||||
}
|
||||
bool kia_v3_v4_process_buffer() {
|
||||
if (raw_bit_count < 68) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t* b = raw_bits;
|
||||
|
||||
// For V3-style (long LOW sync), data is inverted
|
||||
if (is_v3_sync) {
|
||||
uint16_t num_bytes = (raw_bit_count + 7) / 8;
|
||||
for (uint16_t i = 0; i < num_bytes; i++) {
|
||||
b[i] = ~b[i];
|
||||
}
|
||||
}
|
||||
|
||||
// Extract fields
|
||||
// uint32_t encrypted = ((uint32_t)reverse8(b[3]) << 24) | ((uint32_t)reverse8(b[2]) << 16) | ((uint32_t)reverse8(b[1]) << 8) | (uint32_t)reverse8(b[0]);
|
||||
uint32_t serial = ((uint32_t)reverse8(b[7] & 0xF0) << 24) | ((uint32_t)reverse8(b[6]) << 16) | ((uint32_t)reverse8(b[5]) << 8) | (uint32_t)reverse8(b[4]);
|
||||
|
||||
uint8_t btn = (reverse8(b[7]) & 0xF0) >> 4;
|
||||
decode_data = serial;
|
||||
decode_count_bit = 68;
|
||||
decode_data2 = btn;
|
||||
data_count_bit = decode_count_bit;
|
||||
if (callback)
|
||||
callback(this);
|
||||
// uint8_t our_serial_lsb = serial & 0xFF;
|
||||
|
||||
// Decrypt --skipped, no keeloq decoding
|
||||
/* uint32_t decrypted = keeloq_common_decrypt(encrypted, kia_mf_key);
|
||||
uint8_t dec_btn = (decrypted >> 28) & 0x0F;
|
||||
uint8_t dec_serial_lsb = (decrypted >> 16) & 0xFF;
|
||||
|
||||
// Validate
|
||||
if (dec_btn != btn || dec_serial_lsb != our_serial_lsb) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Valid decode - version determined by sync type
|
||||
instance->encrypted = encrypted;
|
||||
instance->decrypted = decrypted;
|
||||
instance->generic.serial = serial;
|
||||
instance->generic.btn = btn;
|
||||
instance->generic.cnt = decrypted & 0xFFFF;
|
||||
instance->version = is_v3_sync ? 1 : 0;
|
||||
|
||||
uint64_t key_data = ((uint64_t)b[0] << 56) | ((uint64_t)b[1] << 48) | ((uint64_t)b[2] << 40) |
|
||||
((uint64_t)b[3] << 32) | ((uint64_t)b[4] << 24) | ((uint64_t)b[5] << 16) |
|
||||
((uint64_t)b[6] << 8) | (uint64_t)b[7];
|
||||
instance->generic.data = key_data;
|
||||
instance->generic.data_count_bit = 64;
|
||||
*/
|
||||
return true;
|
||||
}
|
||||
|
||||
void feed(bool level, uint32_t duration) {
|
||||
switch (parser_step) {
|
||||
case KiaV3V4DecoderStepReset:
|
||||
if (level && DURATION_DIFF(duration, te_short) < te_delta) {
|
||||
parser_step = KiaV3V4DecoderStepCheckPreamble;
|
||||
te_last = duration;
|
||||
header_count = 1;
|
||||
}
|
||||
break;
|
||||
|
||||
case KiaV3V4DecoderStepCheckPreamble:
|
||||
if (level) {
|
||||
if (DURATION_DIFF(duration, te_short) < te_delta) {
|
||||
te_last = duration;
|
||||
} else if (duration > 1000 && duration < 1500) {
|
||||
// V4 style: Sync is LONG HIGH
|
||||
if (header_count >= 8) {
|
||||
parser_step = KiaV3V4DecoderStepCollectRawBits;
|
||||
raw_bit_count = 0;
|
||||
is_v3_sync = false;
|
||||
memset(raw_bits, 0, sizeof(raw_bits));
|
||||
} else {
|
||||
parser_step = KiaV3V4DecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
parser_step = KiaV3V4DecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
if (duration > 1000 && duration < 1500) {
|
||||
// V3 style: Sync is LONG LOW
|
||||
if (header_count >= 8) {
|
||||
parser_step = KiaV3V4DecoderStepCollectRawBits;
|
||||
raw_bit_count = 0;
|
||||
is_v3_sync = true;
|
||||
memset(raw_bits, 0, sizeof(raw_bits));
|
||||
} else {
|
||||
parser_step = KiaV3V4DecoderStepReset;
|
||||
}
|
||||
} else if (DURATION_DIFF(duration, te_short) < te_delta && DURATION_DIFF(te_last, te_short) < te_delta) {
|
||||
header_count++;
|
||||
} else if (duration > 1500) {
|
||||
parser_step = KiaV3V4DecoderStepReset;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case KiaV3V4DecoderStepCollectRawBits:
|
||||
if (level) {
|
||||
if (duration > 1000 && duration < 1500) {
|
||||
// Next sync pulse (V4 style) - end this packet
|
||||
kia_v3_v4_process_buffer();
|
||||
parser_step = KiaV3V4DecoderStepReset;
|
||||
} else if (
|
||||
DURATION_DIFF(duration, te_short) < te_delta) {
|
||||
kia_v3_v4_add_raw_bit(false);
|
||||
} else if (
|
||||
DURATION_DIFF(duration, te_long) < te_delta) {
|
||||
kia_v3_v4_add_raw_bit(true);
|
||||
} else {
|
||||
parser_step = KiaV3V4DecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
if (duration > 1000 && duration < 1500) {
|
||||
// Next sync pulse (V3 style) - end this packet
|
||||
kia_v3_v4_process_buffer();
|
||||
parser_step = KiaV3V4DecoderStepReset;
|
||||
} else if (duration > 1500) {
|
||||
// Long gap - end of transmission
|
||||
kia_v3_v4_process_buffer();
|
||||
parser_step = KiaV3V4DecoderStepReset;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool is_v3_sync = false; // true = V3 (long LOW sync), false = V4 (long HIGH sync)
|
||||
uint8_t version = 0; // 0 = V4, 1 = V3
|
||||
uint8_t raw_bits[32]{0};
|
||||
uint16_t raw_bit_count = 0;
|
||||
uint16_t header_count = 0;
|
||||
|
||||
// uint32_t encrypted;
|
||||
// uint32_t decrypted;
|
||||
};
|
||||
@@ -0,0 +1,116 @@
|
||||
#pragma once
|
||||
#include "subcarbase.hpp"
|
||||
#include <cstring>
|
||||
|
||||
typedef enum {
|
||||
KiaV5DecoderStepReset = 0,
|
||||
KiaV5DecoderStepCheckPreamble,
|
||||
KiaV5DecoderStepData,
|
||||
} KiaV5DecoderStep;
|
||||
|
||||
class FProtoSubCarKiaV5 : public FProtoSubCarBase {
|
||||
public:
|
||||
FProtoSubCarKiaV5() {
|
||||
sensorType = FPC_KIAV5;
|
||||
te_short = 400;
|
||||
te_long = 800;
|
||||
te_delta = 150;
|
||||
min_count_bit_for_found = 64;
|
||||
}
|
||||
|
||||
void kia_v5_add_bit(bool bit) {
|
||||
decode_data = (decode_data << 1) | (bit ? 1 : 0);
|
||||
decode_count_bit++;
|
||||
}
|
||||
|
||||
void feed(bool level, uint32_t duration) {
|
||||
switch (parser_step) {
|
||||
case KiaV5DecoderStepReset:
|
||||
if ((level) && (DURATION_DIFF(duration, te_short) < te_delta)) {
|
||||
parser_step = KiaV5DecoderStepCheckPreamble;
|
||||
te_last = duration;
|
||||
header_count = 1;
|
||||
decode_count_bit = 0;
|
||||
decode_data = 0;
|
||||
FProtoGeneral::manchester_advance(manchester_state, ManchesterEventReset, &manchester_state, NULL);
|
||||
}
|
||||
break;
|
||||
|
||||
case KiaV5DecoderStepCheckPreamble:
|
||||
if (level) {
|
||||
if (DURATION_DIFF(duration, te_long) < te_delta) {
|
||||
if (header_count > 40) {
|
||||
parser_step = KiaV5DecoderStepData;
|
||||
decode_count_bit = 0;
|
||||
decode_data = 0;
|
||||
decode_data2 = 0;
|
||||
header_count = 0;
|
||||
} else {
|
||||
te_last = duration;
|
||||
}
|
||||
} else if (DURATION_DIFF(duration, te_short) < te_delta) {
|
||||
te_last = duration;
|
||||
} else {
|
||||
parser_step = KiaV5DecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
if ((DURATION_DIFF(duration, te_short) <
|
||||
te_delta) &&
|
||||
(DURATION_DIFF(te_last, te_short) <
|
||||
te_delta)) {
|
||||
header_count++;
|
||||
} else if (
|
||||
(DURATION_DIFF(duration, te_long) <
|
||||
te_delta) &&
|
||||
(DURATION_DIFF(te_last, te_short) <
|
||||
te_delta)) {
|
||||
header_count++;
|
||||
} else if (
|
||||
DURATION_DIFF(te_last, te_long) <
|
||||
te_delta) {
|
||||
header_count++;
|
||||
} else {
|
||||
parser_step = KiaV5DecoderStepReset;
|
||||
}
|
||||
te_last = duration;
|
||||
}
|
||||
break;
|
||||
|
||||
case KiaV5DecoderStepData: {
|
||||
ManchesterEvent event;
|
||||
|
||||
if (DURATION_DIFF(duration, te_short) < te_delta) {
|
||||
event = level ? ManchesterEventShortHigh : ManchesterEventShortLow;
|
||||
} else if (DURATION_DIFF(duration, te_long) < te_delta) {
|
||||
event = level ? ManchesterEventLongHigh : ManchesterEventLongLow;
|
||||
} else {
|
||||
if (decode_count_bit >= min_count_bit_for_found) {
|
||||
// instance->generic.data = instance->decode_data2;
|
||||
data_count_bit = (decode_count_bit > 67) ? 67 : decode_count_bit;
|
||||
if (callback)
|
||||
callback(this);
|
||||
}
|
||||
|
||||
parser_step = KiaV5DecoderStepReset;
|
||||
break;
|
||||
}
|
||||
|
||||
bool data_bit;
|
||||
if (decode_count_bit <= 66 && FProtoGeneral::manchester_advance(manchester_state, event, &manchester_state, &data_bit)) {
|
||||
kia_v5_add_bit(data_bit);
|
||||
|
||||
if (decode_count_bit == 64) {
|
||||
decode_data2 = decode_data;
|
||||
decode_data = 0;
|
||||
}
|
||||
}
|
||||
|
||||
te_last = duration;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t header_count = 0;
|
||||
ManchesterState manchester_state = ManchesterStateMid1;
|
||||
};
|
||||
@@ -0,0 +1,173 @@
|
||||
#pragma once
|
||||
#include "subcarbase.hpp"
|
||||
#include <cstring>
|
||||
|
||||
typedef enum {
|
||||
SubaruDecoderStepReset = 0,
|
||||
SubaruDecoderStepCheckPreamble,
|
||||
SubaruDecoderStepFoundGap,
|
||||
SubaruDecoderStepFoundSync,
|
||||
SubaruDecoderStepSaveDuration,
|
||||
SubaruDecoderStepCheckDuration,
|
||||
} SubaruDecoderStep;
|
||||
|
||||
class FProtoSubCarSubaru : public FProtoSubCarBase {
|
||||
public:
|
||||
FProtoSubCarSubaru() {
|
||||
sensorType = FPC_SUBARU;
|
||||
te_short = 800;
|
||||
te_long = 1600;
|
||||
te_delta = 260;
|
||||
min_count_bit_for_found = 64;
|
||||
}
|
||||
|
||||
void subghz_protocol_decoder_subaru_reset() {
|
||||
parser_step = SubaruDecoderStepReset;
|
||||
te_last = 0;
|
||||
header_count = 0;
|
||||
bit_count = 0;
|
||||
memset(data, 0, sizeof(data));
|
||||
}
|
||||
|
||||
void subaru_add_bit(bool bit) {
|
||||
if (bit_count < 64) {
|
||||
uint8_t byte_idx = bit_count / 8;
|
||||
uint8_t bit_idx = 7 - (bit_count % 8);
|
||||
if (bit) {
|
||||
data[byte_idx] |= (1 << bit_idx);
|
||||
|
||||
} else {
|
||||
data[byte_idx] &= ~(1 << bit_idx);
|
||||
}
|
||||
bit_count++;
|
||||
}
|
||||
}
|
||||
|
||||
bool subaru_process_data() {
|
||||
if (bit_count < 64) {
|
||||
return false;
|
||||
}
|
||||
uint8_t* b = data;
|
||||
|
||||
uint64_t key = ((uint64_t)b[0] << 56) | ((uint64_t)b[1] << 48) |
|
||||
((uint64_t)b[2] << 40) | ((uint64_t)b[3] << 32) |
|
||||
((uint64_t)b[4] << 24) | ((uint64_t)b[5] << 16) |
|
||||
((uint64_t)b[6] << 8) | ((uint64_t)b[7]);
|
||||
decode_data = key;
|
||||
// uint32_t serial = ((uint32_t)b[1] << 16) | ((uint32_t)b[2] << 8) | b[3];
|
||||
// uint8_t button = b[0] & 0x0F;
|
||||
// uint16_t cnt;
|
||||
// subaru_decode_count(b, &cnt);
|
||||
data_count_bit = bit_count;
|
||||
if (callback) {
|
||||
callback(this);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void feed(bool level, uint32_t duration) {
|
||||
switch (parser_step) {
|
||||
case SubaruDecoderStepReset:
|
||||
if (level && DURATION_DIFF(duration, te_long) < te_delta) {
|
||||
parser_step = SubaruDecoderStepCheckPreamble;
|
||||
te_last = duration;
|
||||
header_count = 1;
|
||||
}
|
||||
break;
|
||||
|
||||
case SubaruDecoderStepCheckPreamble:
|
||||
if (!level) {
|
||||
if (DURATION_DIFF(duration, te_long) < te_delta) {
|
||||
header_count++;
|
||||
} else if (duration > 2000 && duration < 3500) {
|
||||
if (header_count > 20) {
|
||||
parser_step = SubaruDecoderStepFoundGap;
|
||||
} else {
|
||||
parser_step = SubaruDecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
parser_step = SubaruDecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
if (DURATION_DIFF(duration, te_long) < te_delta) {
|
||||
te_last = duration;
|
||||
header_count++;
|
||||
} else {
|
||||
parser_step = SubaruDecoderStepReset;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case SubaruDecoderStepFoundGap:
|
||||
if (level && duration > 2000 && duration < 3500) {
|
||||
parser_step = SubaruDecoderStepFoundSync;
|
||||
} else {
|
||||
parser_step = SubaruDecoderStepReset;
|
||||
}
|
||||
break;
|
||||
|
||||
case SubaruDecoderStepFoundSync:
|
||||
if (!level && DURATION_DIFF(duration, te_long) < te_delta) {
|
||||
parser_step = SubaruDecoderStepSaveDuration;
|
||||
bit_count = 0;
|
||||
memset(data, 0, sizeof(data));
|
||||
} else {
|
||||
parser_step = SubaruDecoderStepReset;
|
||||
}
|
||||
break;
|
||||
|
||||
case SubaruDecoderStepSaveDuration:
|
||||
if (level) {
|
||||
// HIGH pulse duration encodes the bit:
|
||||
// Short HIGH (~800µs) = 1
|
||||
// Long HIGH (~1600µs) = 0
|
||||
if (DURATION_DIFF(duration, te_short) < te_delta) {
|
||||
// Short HIGH = bit 1
|
||||
subaru_add_bit(true);
|
||||
te_last = duration;
|
||||
parser_step = SubaruDecoderStepCheckDuration;
|
||||
} else if (DURATION_DIFF(duration, te_long) < te_delta) {
|
||||
// Long HIGH = bit 0
|
||||
subaru_add_bit(false);
|
||||
te_last = duration;
|
||||
parser_step = SubaruDecoderStepCheckDuration;
|
||||
} else if (duration > 3000) {
|
||||
// End of transmission
|
||||
if (bit_count >= 64) {
|
||||
subaru_process_data();
|
||||
}
|
||||
parser_step = SubaruDecoderStepReset;
|
||||
} else {
|
||||
parser_step = SubaruDecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
parser_step = SubaruDecoderStepReset;
|
||||
}
|
||||
break;
|
||||
|
||||
case SubaruDecoderStepCheckDuration:
|
||||
if (!level) {
|
||||
// LOW pulse - just validates timing, doesn't encode bit
|
||||
if (DURATION_DIFF(duration, te_short) < te_delta ||
|
||||
DURATION_DIFF(duration, te_long) < te_delta) {
|
||||
parser_step = SubaruDecoderStepSaveDuration;
|
||||
} else if (duration > 3000) {
|
||||
// Gap - end of packet
|
||||
if (bit_count >= 64) {
|
||||
subaru_process_data();
|
||||
}
|
||||
parser_step = SubaruDecoderStepReset;
|
||||
} else {
|
||||
parser_step = SubaruDecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
parser_step = SubaruDecoderStepReset;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t header_count = 0;
|
||||
uint8_t data[8];
|
||||
uint8_t bit_count = 0;
|
||||
};
|
||||
@@ -0,0 +1,135 @@
|
||||
#pragma once
|
||||
#include "subcarbase.hpp"
|
||||
|
||||
#define SUZUKI_GAP_TIME 2000
|
||||
#define SUZUKI_GAP_DELTA 400
|
||||
|
||||
typedef enum {
|
||||
SuzukiDecoderStepReset = 0,
|
||||
SuzukiDecoderStepCountPreamble = 1,
|
||||
SuzukiDecoderStepDecodeData = 2,
|
||||
} SuzukiDecoderStep;
|
||||
|
||||
class FProtoSubCarSuzuki : public FProtoSubCarBase {
|
||||
public:
|
||||
FProtoSubCarSuzuki() {
|
||||
sensorType = FPC_SUZUKI;
|
||||
te_short = 250;
|
||||
te_long = 500;
|
||||
te_delta = 110;
|
||||
min_count_bit_for_found = 64;
|
||||
}
|
||||
void suzuki_add_bit(uint32_t bit) {
|
||||
decode_data = (decode_data << 1) | bit;
|
||||
decode_count_bit++;
|
||||
}
|
||||
void subghz_protocol_decoder_suzuki_reset() {
|
||||
parser_step = SuzukiDecoderStepReset;
|
||||
header_count = 0;
|
||||
data_count_bit = 0;
|
||||
decode_data = 0;
|
||||
}
|
||||
|
||||
void feed(bool level, uint32_t duration) {
|
||||
switch (parser_step) {
|
||||
case SuzukiDecoderStepReset:
|
||||
// Wait for HIGH pulse (~250µs) to start preamble
|
||||
if (!level) {
|
||||
return;
|
||||
}
|
||||
if (DURATION_DIFF(duration, te_short) > te_delta) {
|
||||
return;
|
||||
}
|
||||
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
parser_step = SuzukiDecoderStepCountPreamble;
|
||||
header_count = 0;
|
||||
break;
|
||||
|
||||
case SuzukiDecoderStepCountPreamble:
|
||||
if (level) {
|
||||
// HIGH pulse
|
||||
|
||||
if (header_count >= 300) {
|
||||
if (DURATION_DIFF(duration, te_long) <= te_delta) {
|
||||
parser_step = SuzukiDecoderStepDecodeData;
|
||||
suzuki_add_bit(1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (DURATION_DIFF(duration, te_short) <= te_delta) {
|
||||
te_last = duration;
|
||||
header_count++;
|
||||
} else {
|
||||
parser_step = SuzukiDecoderStepReset;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case SuzukiDecoderStepDecodeData:
|
||||
|
||||
if (level) {
|
||||
// HIGH pulse - determines bit value
|
||||
if (duration < te_long) {
|
||||
uint32_t diff_long = 500 - duration;
|
||||
if (diff_long > 99) {
|
||||
uint32_t diff_short;
|
||||
if (duration < 250) {
|
||||
diff_short = 250 - duration;
|
||||
} else {
|
||||
diff_short = duration - 250;
|
||||
}
|
||||
|
||||
if (diff_short <= 99) {
|
||||
suzuki_add_bit(0);
|
||||
}
|
||||
} else {
|
||||
suzuki_add_bit(1);
|
||||
}
|
||||
} else {
|
||||
uint32_t diff_long = duration - 500;
|
||||
if (diff_long <= 99) {
|
||||
suzuki_add_bit(1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// LOW pulse - check for gap (end of transmission)
|
||||
uint32_t diff_gap;
|
||||
if (duration < SUZUKI_GAP_TIME) {
|
||||
diff_gap = SUZUKI_GAP_TIME - duration;
|
||||
} else {
|
||||
diff_gap = duration - SUZUKI_GAP_TIME;
|
||||
}
|
||||
|
||||
if (diff_gap <= SUZUKI_GAP_DELTA) {
|
||||
if (decode_count_bit == 64) {
|
||||
// instance->generic.data = decode_data;
|
||||
data_count_bit = 64;
|
||||
|
||||
/*uint64_t data = instance->generic.data;
|
||||
uint32_t data_high = (uint32_t)(data >> 32);
|
||||
uint32_t data_low = (uint32_t)data;
|
||||
|
||||
instance->generic.serial = ((data_high & 0xFFF) << 16) | (data_low >> 16);
|
||||
|
||||
instance->generic.btn = (data_low >> 12) & 0xF;
|
||||
instance->generic.cnt = (data_high << 4) >> 16;
|
||||
*/
|
||||
if (callback) {
|
||||
callback(this);
|
||||
}
|
||||
}
|
||||
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
parser_step = SuzukiDecoderStepReset;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t header_count = 0;
|
||||
uint8_t data_count_bit = 0;
|
||||
};
|
||||
@@ -0,0 +1,236 @@
|
||||
#pragma once
|
||||
#include "subcarbase.hpp"
|
||||
|
||||
typedef enum {
|
||||
VwDecoderStepReset = 0,
|
||||
VwDecoderStepFoundSync,
|
||||
VwDecoderStepFoundStart1,
|
||||
VwDecoderStepFoundStart2,
|
||||
VwDecoderStepFoundStart3,
|
||||
VwDecoderStepFoundData,
|
||||
} VwDecoderStep;
|
||||
|
||||
class FProtoSubCarVW : public FProtoSubCarBase {
|
||||
public:
|
||||
FProtoSubCarVW() {
|
||||
sensorType = FPC_VW;
|
||||
te_short = 500;
|
||||
te_long = 1000;
|
||||
te_delta = 130;
|
||||
min_count_bit_for_found = 80;
|
||||
}
|
||||
uint8_t vw_get_bit_index(uint8_t bit) {
|
||||
uint8_t bit_index = 0;
|
||||
if (bit < 72 && bit >= 8) {
|
||||
// use generic.data (bytes 1-8)
|
||||
bit_index = bit - 8;
|
||||
} else {
|
||||
// use data_2
|
||||
if (bit >= 72) {
|
||||
bit_index = bit - 64; // byte 0 = type
|
||||
}
|
||||
if (bit < 8) {
|
||||
bit_index = bit; // byte 9 = check digit
|
||||
}
|
||||
bit_index |= 0x80; // mark for data_2
|
||||
}
|
||||
return bit_index;
|
||||
}
|
||||
|
||||
void vw_add_bit(bool level) {
|
||||
if (data_count_bit >= min_count_bit_for_found) {
|
||||
return;
|
||||
}
|
||||
uint8_t bit_index_full = min_count_bit_for_found - 1 - data_count_bit;
|
||||
uint8_t bit_index_masked = vw_get_bit_index(bit_index_full);
|
||||
uint8_t bit_index = bit_index_masked & 0x7F;
|
||||
if (bit_index_masked & 0x80) {
|
||||
// use data_2
|
||||
if (level) {
|
||||
decode_data2 |= (1ULL << bit_index);
|
||||
} else {
|
||||
decode_data2 &= ~(1ULL << bit_index);
|
||||
}
|
||||
} else {
|
||||
// use data
|
||||
if (level) {
|
||||
decode_data |= (1ULL << bit_index);
|
||||
} else {
|
||||
decode_data &= ~(1ULL << bit_index);
|
||||
}
|
||||
}
|
||||
|
||||
data_count_bit++;
|
||||
if (data_count_bit >= min_count_bit_for_found) {
|
||||
if (callback) {
|
||||
callback(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void subghz_protocol_decoder_vw_reset() {
|
||||
parser_step = VwDecoderStepReset;
|
||||
data_count_bit = 0;
|
||||
decode_data = 0;
|
||||
decode_data2 = 0;
|
||||
manchester_state = ManchesterStateMid1;
|
||||
}
|
||||
|
||||
bool vw_manchester_advance(
|
||||
ManchesterState state,
|
||||
ManchesterEvent event,
|
||||
ManchesterState* next_state,
|
||||
bool* data) {
|
||||
bool result = false;
|
||||
ManchesterState new_state = ManchesterStateMid1;
|
||||
|
||||
if (event == ManchesterEventReset) {
|
||||
new_state = ManchesterStateMid1;
|
||||
} else if (state == ManchesterStateMid0 || state == ManchesterStateMid1) {
|
||||
if (event == ManchesterEventShortHigh) {
|
||||
new_state = ManchesterStateStart1;
|
||||
} else if (event == ManchesterEventShortLow) {
|
||||
new_state = ManchesterStateStart0;
|
||||
} else {
|
||||
new_state = ManchesterStateMid1;
|
||||
}
|
||||
} else if (state == ManchesterStateStart1) {
|
||||
if (event == ManchesterEventShortLow) {
|
||||
new_state = ManchesterStateMid1;
|
||||
result = true;
|
||||
if (data)
|
||||
*data = true;
|
||||
} else if (event == ManchesterEventLongLow) {
|
||||
new_state = ManchesterStateStart0;
|
||||
result = true;
|
||||
if (data)
|
||||
*data = true;
|
||||
} else {
|
||||
new_state = ManchesterStateMid1;
|
||||
}
|
||||
} else if (state == ManchesterStateStart0) {
|
||||
if (event == ManchesterEventShortHigh) {
|
||||
new_state = ManchesterStateMid0;
|
||||
result = true;
|
||||
if (data)
|
||||
*data = false;
|
||||
} else if (event == ManchesterEventLongHigh) {
|
||||
new_state = ManchesterStateStart1;
|
||||
result = true;
|
||||
if (data)
|
||||
*data = false;
|
||||
} else {
|
||||
new_state = ManchesterStateMid1;
|
||||
}
|
||||
}
|
||||
|
||||
*next_state = new_state;
|
||||
return result;
|
||||
}
|
||||
|
||||
void feed(bool level, uint32_t duration) {
|
||||
uint32_t te_med = (te_long + te_short) / 2;
|
||||
uint32_t te_end = te_long * 5;
|
||||
|
||||
ManchesterEvent event = ManchesterEventReset;
|
||||
|
||||
switch (parser_step) {
|
||||
case VwDecoderStepReset:
|
||||
if (DURATION_DIFF(duration, te_short) < te_delta) {
|
||||
parser_step = VwDecoderStepFoundSync;
|
||||
}
|
||||
break;
|
||||
|
||||
case VwDecoderStepFoundSync:
|
||||
if (DURATION_DIFF(duration, te_short) < te_delta) {
|
||||
// Stay - sync pattern repeats ~43 times
|
||||
break;
|
||||
}
|
||||
|
||||
if (level && DURATION_DIFF(duration, te_long) < te_delta) {
|
||||
parser_step = VwDecoderStepFoundStart1;
|
||||
break;
|
||||
}
|
||||
|
||||
parser_step = VwDecoderStepReset;
|
||||
break;
|
||||
|
||||
case VwDecoderStepFoundStart1:
|
||||
if (!level && DURATION_DIFF(duration, te_short) < te_delta) {
|
||||
parser_step = VwDecoderStepFoundStart2;
|
||||
break;
|
||||
}
|
||||
|
||||
parser_step = VwDecoderStepReset;
|
||||
break;
|
||||
|
||||
case VwDecoderStepFoundStart2:
|
||||
if (level && DURATION_DIFF(duration, te_med) < te_delta) {
|
||||
parser_step = VwDecoderStepFoundStart3;
|
||||
break;
|
||||
}
|
||||
|
||||
parser_step = VwDecoderStepReset;
|
||||
break;
|
||||
|
||||
case VwDecoderStepFoundStart3:
|
||||
if (DURATION_DIFF(duration, te_med) < te_delta) {
|
||||
// Stay - med pattern repeats
|
||||
break;
|
||||
}
|
||||
|
||||
if (level && DURATION_DIFF(duration, te_short) < te_delta) {
|
||||
// Start data collection
|
||||
vw_manchester_advance(
|
||||
manchester_state,
|
||||
ManchesterEventReset,
|
||||
&manchester_state,
|
||||
NULL);
|
||||
vw_manchester_advance(
|
||||
manchester_state,
|
||||
ManchesterEventShortHigh,
|
||||
&manchester_state,
|
||||
NULL);
|
||||
data_count_bit = 0;
|
||||
decode_data = 0;
|
||||
decode_data2 = 0;
|
||||
parser_step = VwDecoderStepFoundData;
|
||||
break;
|
||||
}
|
||||
|
||||
parser_step = VwDecoderStepReset;
|
||||
break;
|
||||
|
||||
case VwDecoderStepFoundData:
|
||||
if (DURATION_DIFF(duration, te_short) < te_delta) {
|
||||
event = level ? ManchesterEventShortHigh : ManchesterEventShortLow;
|
||||
}
|
||||
|
||||
if (DURATION_DIFF(duration, te_long) < te_delta) {
|
||||
event = level ? ManchesterEventLongHigh : ManchesterEventLongLow;
|
||||
}
|
||||
|
||||
// Last bit can be arbitrarily long
|
||||
if (data_count_bit == min_count_bit_for_found - 1 &&
|
||||
!level && duration > te_end) {
|
||||
event = ManchesterEventShortLow;
|
||||
}
|
||||
|
||||
if (event == ManchesterEventReset) {
|
||||
subghz_protocol_decoder_vw_reset();
|
||||
} else {
|
||||
bool new_level;
|
||||
if (vw_manchester_advance(
|
||||
manchester_state,
|
||||
event,
|
||||
&manchester_state,
|
||||
&new_level)) {
|
||||
vw_add_bit(new_level);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ManchesterState manchester_state = ManchesterStateMid1;
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
Base class for all weather protocols.
|
||||
This and most of the weather protocols uses code from Flipper XTreme codebase ( https://github.com/Flipper-XFW/Xtreme-Firmware/tree/dev/lib/subghz ). Thanks for their work!
|
||||
For comments in a protocol implementation check w-nexus-th.hpp
|
||||
*/
|
||||
|
||||
#ifndef __FPROTO_SCARBASE_H__
|
||||
#define __FPROTO_SCARBASE_H__
|
||||
|
||||
#include "fprotogeneral.hpp"
|
||||
#include "subcartypes.hpp"
|
||||
|
||||
#include <string>
|
||||
// default values to indicate 'no value'
|
||||
|
||||
class FProtoSubCarBase;
|
||||
typedef void (*SubCarProtocolDecoderBaseRxCallback)(FProtoSubCarBase* instance);
|
||||
|
||||
class FProtoSubCarBase {
|
||||
public:
|
||||
FProtoSubCarBase() {}
|
||||
virtual ~FProtoSubCarBase() {}
|
||||
virtual void feed(bool level, uint32_t duration) = 0; // need to be implemented on each protocol handler.
|
||||
void setCallback(SubCarProtocolDecoderBaseRxCallback cb) { callback = cb; } // this is called when there is a hit.
|
||||
|
||||
// General data holder, these will be passed
|
||||
uint8_t sensorType = FPC_Invalid;
|
||||
uint16_t data_count_bit = 0;
|
||||
uint64_t decode_data = 0;
|
||||
uint64_t decode_data2 = 0;
|
||||
|
||||
protected:
|
||||
// Helper functions to keep it as compatible with flipper as we can, so adding new protos will be easy.
|
||||
void subghz_protocol_blocks_add_bit(uint8_t bit) {
|
||||
decode_data = decode_data << 1 | bit;
|
||||
decode_count_bit++;
|
||||
}
|
||||
|
||||
// inner logic stuff, also for flipper compatibility.
|
||||
uint32_t te_short = UINT32_MAX;
|
||||
uint32_t te_long = UINT32_MAX;
|
||||
uint32_t te_delta = UINT32_MAX;
|
||||
uint32_t min_count_bit_for_found = UINT32_MAX;
|
||||
|
||||
SubCarProtocolDecoderBaseRxCallback callback = NULL;
|
||||
|
||||
uint8_t parser_step = 0;
|
||||
uint32_t te_last = 0;
|
||||
uint32_t decode_count_bit = 0;
|
||||
|
||||
//
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
This is the protocol list handler. It holds an instance of all known protocols.
|
||||
So include here the .hpp, and add a new element to the protos vector in the constructor. That's all you need to do here if you wanna add a new proto.
|
||||
@htotoo
|
||||
*/
|
||||
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include "portapack_shared_memory.hpp"
|
||||
|
||||
#include "fprotolistgeneral.hpp"
|
||||
#include "subcarbase.hpp"
|
||||
#include "c-suzuki.hpp"
|
||||
#include "c-vw.hpp"
|
||||
#include "c-subaru.hpp"
|
||||
#include "c-kia_v5.hpp"
|
||||
#include "c-kia_v3v4.hpp"
|
||||
#include "c-kia_v2.hpp"
|
||||
#include "c-kia_v1.hpp"
|
||||
#include "c-kia_v0.hpp"
|
||||
#include "c-ford_v0.hpp"
|
||||
#include "c-fiat_v0.hpp"
|
||||
#include "c-bmw_v0.hpp"
|
||||
|
||||
#ifndef __FPROTO_PROTOLISTCAR_H__
|
||||
#define __FPROTO_PROTOLISTCAR_H__
|
||||
|
||||
class SubCarProtos : public FProtoListGeneral {
|
||||
public:
|
||||
SubCarProtos(const SubCarProtos&) { SubCarProtos(); }; // won't use, but makes compiler happy
|
||||
SubCarProtos& operator=(const SubCarProtos&) { return *this; } // won't use, but makes compiler happy
|
||||
SubCarProtos() {
|
||||
// add protos
|
||||
protos[FPC_SUZUKI] = new FProtoSubCarSuzuki();
|
||||
protos[FPC_VW] = new FProtoSubCarVW();
|
||||
protos[FPC_SUBARU] = new FProtoSubCarSubaru();
|
||||
protos[FPC_KIAV5] = new FProtoSubCarKiaV5();
|
||||
protos[FPC_KIAV3V4] = new FProtoSubCarKiaV3V4();
|
||||
protos[FPC_KIAV2] = new FProtoSubCarKiaV2();
|
||||
protos[FPC_KIAV1] = new FProtoSubCarKiaV1();
|
||||
protos[FPC_KIAV0] = new FProtoSubCarKiaV0();
|
||||
protos[FPC_FORDV0] = new FProtoSubCarFordV0();
|
||||
protos[FPC_FIATV0] = new FProtoSubCarFiatV0();
|
||||
protos[FPC_BMWV0] = new FProtoSubCarBMWV0();
|
||||
|
||||
for (uint8_t i = 0; i < FPC_COUNT; ++i) {
|
||||
if (protos[i] != NULL) protos[i]->setCallback(callbackTarget);
|
||||
}
|
||||
}
|
||||
|
||||
~SubCarProtos() { // not needed for current operation logic, but a bit more elegant :)
|
||||
for (uint8_t i = 0; i < FPC_COUNT; ++i) {
|
||||
if (protos[i] != NULL) {
|
||||
free(protos[i]);
|
||||
protos[i] = NULL;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
static void callbackTarget(FProtoSubCarBase* instance) {
|
||||
SubCarDataMessage packet_message{instance->sensorType, instance->data_count_bit, instance->decode_data, instance->decode_data2};
|
||||
shared_memory.application_queue.push(packet_message);
|
||||
}
|
||||
|
||||
void feed(bool level, uint32_t duration) {
|
||||
for (uint8_t i = 0; i < FPC_COUNT; ++i) {
|
||||
if (protos[i] != NULL) protos[i]->feed(level, duration);
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
FProtoSubCarBase* protos[FPC_COUNT] = {NULL};
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,30 @@
|
||||
|
||||
#ifndef __FPROTO_SUBCARTYPES_H__
|
||||
#define __FPROTO_SUBCARTYPES_H__
|
||||
|
||||
/*
|
||||
Define known protocols.
|
||||
These values must be present on the protocol's constructor, like FProtoWeatherAcurite592TXR() { sensorType = FPS_ANSONIC; }
|
||||
Also it must have a switch-case element in the getSubGhzDSensorTypeName() function, to display it's name.
|
||||
*/
|
||||
|
||||
#define FPM_AM 0
|
||||
#define FPM_FM 1
|
||||
|
||||
enum FPROTO_SUBCAR_SENSOR : uint8_t {
|
||||
FPC_Invalid = 0,
|
||||
FPC_SUZUKI = 1,
|
||||
FPC_VW = 2,
|
||||
FPC_SUBARU = 3,
|
||||
FPC_KIAV5 = 4,
|
||||
FPC_KIAV3V4 = 5,
|
||||
FPC_KIAV2 = 6,
|
||||
FPC_KIAV1 = 7,
|
||||
FPC_KIAV0 = 8,
|
||||
FPC_FORDV0 = 9,
|
||||
FPC_FIATV0 = 10,
|
||||
FPC_BMWV0 = 11,
|
||||
FPC_COUNT
|
||||
};
|
||||
|
||||
#endif
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user