mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2026-08-21 06:59:02 +00:00
Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 89b878d3bb | |||
| 56d965f3ae | |||
| fe9f0c228f | |||
| 6562e596e8 | |||
| c27192bdbb | |||
| 927e3b11e7 | |||
| a44a00acf0 | |||
| 6739b43914 | |||
| b306a0d490 | |||
| c6130c6e8d | |||
| 97f2ca42f1 | |||
| e2b642ae25 | |||
| 106d9b1207 | |||
| c8ae419e0b | |||
| 2319dcd0d6 | |||
| 44d9ce00cd | |||
| 5cc5f8faa6 | |||
| 3a0ca480b0 | |||
| 7a2b432bfa | |||
| 86d7b345d0 | |||
| 877ede5f86 | |||
| 19eb1c40ab | |||
| 9c03a18893 | |||
| cd504eb4a7 | |||
| dc976c7f38 | |||
| 266d90a600 | |||
| 2884bb9ab7 | |||
| 133b2d2065 | |||
| 2f2671f76c | |||
| f5c2c78733 | |||
| e4ab7227fc | |||
| 93799bde6d | |||
| 3d979a613a | |||
| ee97b51eb3 |
+6
-7
@@ -1,14 +1,13 @@
|
||||
---
|
||||
BasedOnStyle: Chromium
|
||||
IndentWidth: '4'
|
||||
SortIncludes: 'false'
|
||||
AllowAllArgumentsOnNextLine: 'true'
|
||||
IndentWidth: 4
|
||||
SortIncludes: false
|
||||
AllowAllArgumentsOnNextLine: true
|
||||
ColumnLimit: 0
|
||||
Cpp11BracedListStyle: 'true'
|
||||
AllowShortLoopsOnASingleLine: 'true'
|
||||
AllowShortIfStatementsOnASingleLine: 'true'
|
||||
Cpp11BracedListStyle: true
|
||||
AllowShortLoopsOnASingleLine: true
|
||||
AllowShortIfStatementsOnASingleLine: true
|
||||
SpacesInLineCommentPrefix:
|
||||
Minimum: 1
|
||||
Maximum: 1
|
||||
|
||||
...
|
||||
|
||||
@@ -83,3 +83,4 @@ venv/
|
||||
# generated bitmap arr file
|
||||
# TODO: generate bitmap during build, since we use python during build anyway, lemme know if this is a bad idea @zxkmm
|
||||
/firmware/tools/bitmap.hpp
|
||||
firmware/flashsize.h
|
||||
|
||||
@@ -27,6 +27,27 @@ set(CMAKE_COLOR_MAKEFILE ON)
|
||||
|
||||
add_compile_options(-fdiagnostics-color=always)
|
||||
|
||||
#Flash size related options
|
||||
#This is the size of the flash memory we SUPPORT. Don't worry, the internl flash app has harder limits based on device type.
|
||||
if(NOT DEFINED FLASH_MB_SIZE)
|
||||
set(FLASH_MB_SIZE 2) # Default to 2MB if not provided. For PortaRf this should be 2MB passed to cmake with -DFLASH_MB_SIZE=2
|
||||
endif()
|
||||
message("Configuring for FLASH_MB_SIZE=${FLASH_MB_SIZE}MB")
|
||||
#This is the flash memory size we build. This can be less, so the FW size will be less, so can be flashed with older utils.
|
||||
if(NOT DEFINED FLASH_MB_LIMIT_SIZE)
|
||||
set(FLASH_MB_LIMIT_SIZE 1) #TODO: should be ${FLASH_MB_SIZE} remove once we got a stable build for all devices we support with bigger than 1 mb flash
|
||||
endif()
|
||||
message("Configuring for FLASH_MB_LIMIT_SIZE=${FLASH_MB_LIMIT_SIZE}MB")
|
||||
|
||||
if(FLASH_MB_SIZE LESS FLASH_MB_LIMIT_SIZE)
|
||||
message(FATAL_ERROR "Error: The supported flash size is lower than the generated flash file. Build halted.")
|
||||
endif()
|
||||
|
||||
math(EXPR FLASH_BYTES_SIZE "${FLASH_MB_SIZE} * 1024 * 1024")
|
||||
math(EXPR FLASH_BYTES_LIMIT_SIZE "${FLASH_MB_LIMIT_SIZE} * 1024 * 1024")
|
||||
#generate h files
|
||||
configure_file(flashsize.h.in ${CMAKE_CURRENT_SOURCE_DIR}/firmware/flashsize.h)
|
||||
|
||||
option(USE_CCACHE "Enable ccache, please keep an eye on this because sometimes it's not quite stable and generated unusable binary" OFF)
|
||||
|
||||
project(portapack-h1)
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
portapack.ried.cl
|
||||
@@ -56,7 +56,7 @@ add_subdirectory(test)
|
||||
# NOTE: Dependencies break if the .bin files aren't included in DEPENDS. WTF, CMake?
|
||||
add_custom_command(
|
||||
OUTPUT ${FIRMWARE_FILENAME}
|
||||
COMMAND ${MAKE_SPI_IMAGE} ${application_BINARY_DIR}/application.bin ${baseband_BINARY_DIR}/baseband.img ${FIRMWARE_FILENAME}
|
||||
COMMAND ${MAKE_SPI_IMAGE} ${application_BINARY_DIR}/application.bin ${baseband_BINARY_DIR}/baseband.img ${FIRMWARE_FILENAME} ${FLASH_BYTES_LIMIT_SIZE}
|
||||
DEPENDS baseband application ${MAKE_SPI_IMAGE}
|
||||
${baseband_BINARY_DIR}/baseband.img ${application_BINARY_DIR}/application.bin
|
||||
VERBATIM
|
||||
|
||||
@@ -118,11 +118,16 @@ set(CSRC
|
||||
usb_serial_endpoints.c
|
||||
usb_serial_device_to_host.c
|
||||
i2c_device_to_host.c
|
||||
hackrf_core_mini.c
|
||||
${HACKRF_PATH}/firmware/common/adc.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/common/usb_request.c
|
||||
${HACKRF_PATH}/firmware/common/usb_standard_request.c
|
||||
${HACKRF_PATH}/firmware/libopencm3/lib/lpc43xx/scu.c
|
||||
${HACKRF_PATH}/firmware/common/gpio_lpc.c
|
||||
${HACKRF_PATH}/firmware/common/platform_detect.c
|
||||
${CHIBIOS}/os/various/chprintf.c
|
||||
)
|
||||
|
||||
@@ -296,10 +301,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 +472,9 @@ add_custom_command(
|
||||
)
|
||||
|
||||
add_executable(${PROJECT_NAME}.elf ${CSRC} ${CPPSRC} ${ASMSRC})
|
||||
target_link_options(${PROJECT_NAME}.elf PRIVATE
|
||||
"LINKER:--defsym,LD_FLASH_SIZE=${FLASH_BYTES_LIMIT_SIZE}"
|
||||
)
|
||||
set_target_properties(${PROJECT_NAME}.elf PROPERTIES LINK_DEPENDS ${LDSCRIPT})
|
||||
add_definitions(${DEFS})
|
||||
include_directories(. ${INCDIR})
|
||||
|
||||
@@ -68,6 +68,9 @@ void BoundSetting::parse(std::string_view value) {
|
||||
as<bool>() = (parsed != 0);
|
||||
break;
|
||||
}
|
||||
case SettingType::Float:
|
||||
as<float>() = atof(value.data());
|
||||
break;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -101,6 +104,11 @@ void BoundSetting::write(File& file) const {
|
||||
case SettingType::Bool:
|
||||
file.write(as<bool>() ? "1" : "0", 1);
|
||||
break;
|
||||
|
||||
case SettingType::Float: {
|
||||
auto float_str = to_string_decimal(as<float>(), 6);
|
||||
file.write(float_str.c_str(), float_str.length());
|
||||
}
|
||||
}
|
||||
|
||||
file.write("\r\n", 2);
|
||||
|
||||
@@ -52,6 +52,7 @@ class BoundSetting {
|
||||
U8,
|
||||
String,
|
||||
Bool,
|
||||
Float
|
||||
};
|
||||
|
||||
public:
|
||||
@@ -73,6 +74,9 @@ class BoundSetting {
|
||||
BoundSetting(std::string_view name, bool* target)
|
||||
: name_{name}, target_{target}, type_{SettingType::Bool} {}
|
||||
|
||||
BoundSetting(std::string_view name, float* target)
|
||||
: name_{name}, target_{target}, type_{SettingType::Float} {}
|
||||
|
||||
std::string_view name() const { return name_; }
|
||||
void parse(std::string_view value);
|
||||
void write(File& file) const;
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
#include "baseband_api.hpp"
|
||||
#include "portapack_shared_memory.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
#include "ui_geomap.hpp"
|
||||
|
||||
#include <cstring>
|
||||
#include <stdio.h>
|
||||
@@ -49,10 +50,24 @@ APRSTXView::~APRSTXView() {
|
||||
|
||||
void APRSTXView::start_tx() {
|
||||
// TODO: Clean up this API to take string_views to avoid allocations.
|
||||
std::string new_payload = payload;
|
||||
|
||||
std::string gps = text_gps_coord.get();
|
||||
std::string token = "?GPS?";
|
||||
size_t pos = 0;
|
||||
while ((pos = new_payload.find(token, pos)) != std::string::npos) {
|
||||
new_payload.replace(pos, token.size(), gps);
|
||||
pos += gps.size();
|
||||
}
|
||||
|
||||
text_payload.set(new_payload);
|
||||
|
||||
std::string path = text_path.get();
|
||||
|
||||
make_aprs_frame(
|
||||
sym_source.to_string().c_str(), num_ssid_source.value(),
|
||||
sym_dest.to_string().c_str(), num_ssid_dest.value(),
|
||||
payload);
|
||||
new_payload, path);
|
||||
|
||||
// uint8_t * bb_data_ptr = shared_memory.bb_data.data;
|
||||
// text_payload.set(to_string_hex_array(bb_data_ptr + 56, 15));
|
||||
@@ -77,6 +92,61 @@ void APRSTXView::on_tx_progress(const uint32_t progress, const bool done) {
|
||||
}
|
||||
}
|
||||
|
||||
void APRSTXView::process_coordinates(float lat_, float lon_) {
|
||||
std::string s;
|
||||
|
||||
char ns = lat_ >= 0 ? 'N' : 'S';
|
||||
float lat = (lat_ >= 0) ? lat_ : -lat_;
|
||||
uint32_t lat_d = (uint32_t)lat;
|
||||
uint32_t lat_m = (uint32_t)((lat - lat_d) * 6000.0f + 0.5f);
|
||||
|
||||
if (lat_m >= 6000) {
|
||||
lat_m = 0;
|
||||
lat_d++;
|
||||
}
|
||||
|
||||
s += to_string_dec_uint(lat_d, 2, '0');
|
||||
s += to_string_dec_uint(lat_m / 100, 2, '0');
|
||||
s += ".";
|
||||
s += to_string_dec_uint(lat_m % 100, 2, '0');
|
||||
s += ns;
|
||||
|
||||
s += "/";
|
||||
|
||||
char ew = lon_ >= 0 ? 'E' : 'W';
|
||||
float lon = (lon_ >= 0) ? lon_ : -lon_;
|
||||
uint32_t lon_d = (uint32_t)lon;
|
||||
uint32_t lon_m = (uint32_t)((lon - lon_d) * 6000.0f + 0.5f);
|
||||
|
||||
if (lon_m >= 6000) {
|
||||
lon_m = 0;
|
||||
lon_d++;
|
||||
}
|
||||
|
||||
s += to_string_dec_uint(lon_d, 3, '0');
|
||||
s += to_string_dec_uint(lon_m / 100, 2, '0');
|
||||
s += ".";
|
||||
s += to_string_dec_uint(lon_m % 100, 2, '0');
|
||||
s += ew;
|
||||
text_gps_coord.set(s);
|
||||
}
|
||||
|
||||
void APRSTXView::on_gps(const GPSPosDataMessage* message) {
|
||||
if (manual_gps_mode) {
|
||||
return; // no more update once set manually
|
||||
}
|
||||
if (message->lat < -90.0 || message->lat > 90.0 ||
|
||||
message->lon < -180.0 || message->lon > 180.0) {
|
||||
return;
|
||||
}
|
||||
if (message->lat == 0.0f && message->lon == 0.0f) {
|
||||
return;
|
||||
}
|
||||
last_lat = message->lat;
|
||||
last_lon = message->lon;
|
||||
process_coordinates(message->lat, message->lon);
|
||||
}
|
||||
|
||||
APRSTXView::APRSTXView(NavigationView& nav) {
|
||||
baseband::run_image(portapack::spi_flash::image_tag_afsk);
|
||||
|
||||
@@ -87,8 +157,33 @@ APRSTXView::APRSTXView(NavigationView& nav) {
|
||||
&num_ssid_dest,
|
||||
&text_payload,
|
||||
&button_set,
|
||||
&text_gps_coord,
|
||||
&button_mangps,
|
||||
&gps_is_manual,
|
||||
&text_path,
|
||||
&button_setpath,
|
||||
&tx_view});
|
||||
|
||||
sym_source.set_value(symsrc);
|
||||
num_ssid_source.set_value(ssidsrc);
|
||||
sym_dest.set_value(symdst);
|
||||
num_ssid_dest.set_value(ssiddst);
|
||||
text_payload.set(payload);
|
||||
text_path.set(path_cache);
|
||||
|
||||
sym_source.on_change = [this](SymField&) {
|
||||
symsrc = sym_source.to_string();
|
||||
};
|
||||
num_ssid_source.on_change = [this](int32_t v) {
|
||||
ssidsrc = v;
|
||||
};
|
||||
sym_dest.on_change = [this](SymField&) {
|
||||
symdst = sym_dest.to_string();
|
||||
};
|
||||
num_ssid_dest.on_change = [this](int32_t v) {
|
||||
ssiddst = v;
|
||||
};
|
||||
|
||||
button_set.on_select = [this, &nav](Button&) {
|
||||
text_prompt(
|
||||
nav,
|
||||
@@ -99,6 +194,17 @@ APRSTXView::APRSTXView(NavigationView& nav) {
|
||||
text_payload.set(s);
|
||||
});
|
||||
};
|
||||
button_setpath.on_select = [this, &nav](Button&) {
|
||||
text_prompt(
|
||||
nav,
|
||||
path_cache,
|
||||
60,
|
||||
ENTER_KEYBOARD_MODE_ALPHA,
|
||||
[this](std::string& s) {
|
||||
text_path.set(s);
|
||||
path_cache = s;
|
||||
});
|
||||
};
|
||||
|
||||
tx_view.on_edit_frequency = [this, &nav]() {
|
||||
auto new_view = nav.push<FrequencyKeypadView>(transmitter_model.target_frequency());
|
||||
@@ -116,6 +222,23 @@ APRSTXView::APRSTXView(NavigationView& nav) {
|
||||
tx_view.set_transmitting(false);
|
||||
transmitter_model.disable();
|
||||
};
|
||||
|
||||
button_mangps.on_select = [this, &nav](Button&) {
|
||||
nav.push<GeoMapView>(
|
||||
0,
|
||||
GeoPos::alt_unit::METERS,
|
||||
GeoPos::spd_unit::HIDDEN,
|
||||
last_lat,
|
||||
last_lon,
|
||||
[this](int32_t, float lat, float lon, int32_t) {
|
||||
last_lat = lat;
|
||||
last_lon = lon;
|
||||
manual_gps_mode = true;
|
||||
gps_is_manual.set("MANUAL");
|
||||
process_coordinates(lat, lon);
|
||||
});
|
||||
};
|
||||
// process_coordinates(last_lat, last_lon); //don't load last, so won't confuse users
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
|
||||
@@ -50,59 +50,109 @@ class APRSTXView : public View {
|
||||
1750000 /* bandwidth */,
|
||||
AFSK_TX_SAMPLERATE /* sampling rate */
|
||||
};
|
||||
app_settings::SettingsManager settings_{
|
||||
"tx_aprs", app_settings::Mode::TX};
|
||||
|
||||
std::string symsrc = "";
|
||||
std::string symdst = "";
|
||||
std::string payload{""};
|
||||
std::string path_cache = "";
|
||||
int32_t ssidsrc = 0;
|
||||
int32_t ssiddst = 0;
|
||||
|
||||
bool manual_gps_mode = false;
|
||||
float last_lat = 0.0f;
|
||||
float last_lon = 0.0f;
|
||||
|
||||
app_settings::SettingsManager settings_{
|
||||
"tx_aprs",
|
||||
app_settings::Mode::TX,
|
||||
{{"symsrc"sv, &symsrc},
|
||||
{"symdst"sv, &symdst},
|
||||
{"ssidsrc"sv, &ssidsrc},
|
||||
{"ssiddst"sv, &ssiddst},
|
||||
{"payload"sv, &payload},
|
||||
{"last_lat"sv, &last_lat},
|
||||
{"last_lon"sv, &last_lon},
|
||||
{"path"sv, &path_cache}}};
|
||||
|
||||
void start_tx();
|
||||
void generate_frame();
|
||||
void generate_frame_pos();
|
||||
void on_tx_progress(const uint32_t progress, const bool done);
|
||||
void on_gps(const GPSPosDataMessage* message);
|
||||
void process_coordinates(float lat, float lon);
|
||||
|
||||
Labels labels{
|
||||
{{UI_POS_X(0), 1 * 16}, "Source: SSID:", Theme::getInstance()->fg_light->foreground}, // 6 alphanum + SSID
|
||||
{{UI_POS_X(0), 2 * 16}, " Dest.: SSID:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), 4 * 16}, "Info field:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(0)}, "Source: SSID:", Theme::getInstance()->fg_light->foreground}, // 6 alphanum + SSID
|
||||
{{UI_POS_X(0), UI_POS_Y(1)}, " Dest.: SSID:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(2)}, "Path: (ex.: WIDE1-1,WIDE2-1)", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(6)}, "Info field:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(10)}, "GPS:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X_CENTER(22), UI_POS_Y(14)}, "Use ?GPS? in the info", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X_CENTER(17), UI_POS_Y(15)}, "as a placeholder.", Theme::getInstance()->fg_light->foreground},
|
||||
};
|
||||
|
||||
Text text_path{
|
||||
{UI_POS_X(0), UI_POS_Y(3), UI_POS_MAXWIDTH, UI_POS_HEIGHT(1)},
|
||||
"WIDE1-1",
|
||||
};
|
||||
Text gps_is_manual{
|
||||
{UI_POS_X(6), UI_POS_Y(10), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)},
|
||||
"",
|
||||
};
|
||||
|
||||
SymField sym_source{
|
||||
{7 * 8, 1 * 16},
|
||||
{UI_POS_X(7), UI_POS_Y(0)},
|
||||
6,
|
||||
SymField::Type::Alpha};
|
||||
|
||||
NumberField num_ssid_source{
|
||||
{19 * 8, 1 * 16},
|
||||
{UI_POS_X(19), UI_POS_Y(0)},
|
||||
2,
|
||||
{0, 15},
|
||||
1,
|
||||
' '};
|
||||
|
||||
SymField sym_dest{
|
||||
{7 * 8, 2 * 16},
|
||||
{UI_POS_X(7), UI_POS_Y(1)},
|
||||
6,
|
||||
SymField::Type::Alpha};
|
||||
|
||||
NumberField num_ssid_dest{
|
||||
{19 * 8, 2 * 16},
|
||||
{UI_POS_X(19), UI_POS_Y(1)},
|
||||
2,
|
||||
{0, 15},
|
||||
1,
|
||||
' '};
|
||||
|
||||
Text text_payload{
|
||||
{UI_POS_X(0), 5 * 16, screen_width, 16},
|
||||
"-"};
|
||||
{UI_POS_X(0), UI_POS_Y(7), UI_POS_MAXWIDTH, UI_POS_HEIGHT(1)},
|
||||
""};
|
||||
Button button_set{
|
||||
{UI_POS_X(0), 6 * 16, 80, 32},
|
||||
{UI_POS_X(0), UI_POS_Y(8), UI_POS_WIDTH(10), UI_POS_HEIGHT(2)},
|
||||
"Set"};
|
||||
|
||||
Text text_gps_coord{
|
||||
{UI_POS_X(0), UI_POS_Y(11), UI_POS_WIDTH(20), UI_POS_HEIGHT(1)},
|
||||
"-"};
|
||||
|
||||
Button button_mangps{
|
||||
{UI_POS_X(0), UI_POS_Y(12), UI_POS_WIDTH(14), UI_POS_HEIGHT(2)},
|
||||
"Manual GPS"};
|
||||
Button button_setpath{
|
||||
{UI_POS_X(0), UI_POS_Y(4), UI_POS_WIDTH(14), UI_POS_HEIGHT(2)},
|
||||
"Set Path"};
|
||||
TransmitterView tx_view{
|
||||
(int16_t)UI_POS_Y_BOTTOM(4),
|
||||
5000,
|
||||
0 // disable setting bandwith, since APRS used fixed 10k bandwidth
|
||||
};
|
||||
|
||||
MessageHandlerRegistration message_handler_gps{
|
||||
Message::ID::GPSPosData,
|
||||
[this](Message* const p) {
|
||||
const auto message = static_cast<const GPSPosDataMessage*>(p);
|
||||
this->on_gps(message);
|
||||
}};
|
||||
MessageHandlerRegistration message_handler_tx_progress{
|
||||
Message::ID::TXProgress,
|
||||
[this](const Message* const p) {
|
||||
|
||||
@@ -98,7 +98,7 @@ class BattinfoView : public View {
|
||||
"Volt"};
|
||||
|
||||
Button button_exit{
|
||||
{UI_POS_X_CENTER(12), 17 * 16, UI_POS_WIDTH(12), UI_POS_HEIGHT(3)},
|
||||
{UI_POS_X_CENTER(12), UI_POS_Y_BOTTOM(4), UI_POS_WIDTH(12), UI_POS_HEIGHT(2)},
|
||||
"Back"};
|
||||
static msg_t static_fn(void* arg);
|
||||
Thread* thread{nullptr};
|
||||
|
||||
@@ -45,10 +45,15 @@ bool valid_firmware_file(std::filesystem::path::string_type path) {
|
||||
// test read of the whole file just to validate checksum (baseband flash code will re-read when flashing)
|
||||
auto result = firmware_file.open(path.c_str());
|
||||
if (!result.is_valid()) {
|
||||
uint64_t file_size = firmware_file.size();
|
||||
if (portapack::device_type == portapack::DeviceType::DEV_PORTAPACK && file_size > 1 * 1024 * 1024) {
|
||||
// Portapack firmware files must not be larger than 1MB
|
||||
return false;
|
||||
}
|
||||
// May need to add a check to portarf and portarf pro too.
|
||||
checksum = 0;
|
||||
for (uint32_t offset = 0; offset < FLASH_ROM_SIZE; offset += sizeof(read_buffer)) {
|
||||
for (uint64_t offset = 0; offset < FLASH_ROM_SIZE && offset < file_size; offset += sizeof(read_buffer)) {
|
||||
auto readResult = firmware_file.read(&read_buffer, sizeof(read_buffer));
|
||||
|
||||
if ((!readResult) || (readResult.value() != sizeof(read_buffer))) {
|
||||
// File was smaller than 1MB:
|
||||
// If version is such that the file SHOULD have been 1MB, call it a checksum error (otherwise say it's OK).
|
||||
|
||||
@@ -32,7 +32,9 @@
|
||||
#include "untar.hpp"
|
||||
#include <cstdint>
|
||||
|
||||
#define FLASH_ROM_SIZE 1048576
|
||||
#include "../../flashsize.h"
|
||||
|
||||
#define FLASH_ROM_SIZE (FLASH_SIZE_MB * 1024 * 1024)
|
||||
#define FLASH_STARTING_ADDRESS 0x00000000
|
||||
#define FLASH_EXPECTED_CHECKSUM 0x00000000
|
||||
#define FLASH_CHECKSUM_ERROR 0xFFFFFFFF
|
||||
|
||||
@@ -69,7 +69,7 @@ void GlassView::manage_beep_audio() {
|
||||
}
|
||||
}
|
||||
|
||||
void GlassView::get_max_power(const ChannelSpectrum& spectrum, uint8_t bin, uint8_t& max_power) {
|
||||
void GlassView::get_max_power(const ChannelSpectrum& spectrum, uint16_t bin, uint8_t& max_power) {
|
||||
if (mode == LOOKING_GLASS_SINGLEPASS) {
|
||||
// <20MHz spectrum mode
|
||||
if (bin < 120) {
|
||||
@@ -91,7 +91,7 @@ void GlassView::get_max_power(const ChannelSpectrum& spectrum, uint8_t bin, uint
|
||||
}
|
||||
}
|
||||
|
||||
rf::Frequency GlassView::get_freq_from_bin_pos(uint8_t pos) {
|
||||
rf::Frequency GlassView::get_freq_from_bin_pos(uint16_t pos) {
|
||||
rf::Frequency freq_at_pos = 0;
|
||||
if (mode == LOOKING_GLASS_SINGLEPASS) {
|
||||
// starting from the middle, minus 8 ignored bin on each side. Since pos is [-120,120] after the (pos - 120), it's divided by screen_width(240)/2 => 120
|
||||
@@ -101,7 +101,14 @@ rf::Frequency GlassView::get_freq_from_bin_pos(uint8_t pos) {
|
||||
return freq_at_pos;
|
||||
}
|
||||
|
||||
void GlassView::on_marker_change() {
|
||||
void GlassView::on_marker_change(int8_t delta) {
|
||||
if ((marker_pixel_index + delta) < 0)
|
||||
marker_pixel_index = marker_pixel_index + delta + screen_width;
|
||||
else if ((marker_pixel_index + delta) > screen_width)
|
||||
marker_pixel_index = marker_pixel_index + delta - screen_width;
|
||||
else
|
||||
marker_pixel_index = marker_pixel_index + delta;
|
||||
|
||||
marker = get_freq_from_bin_pos(marker_pixel_index);
|
||||
field_marker.set_text(to_string_short_freq(marker));
|
||||
plot_marker(marker_pixel_index); // Refresh marker on screen
|
||||
@@ -200,7 +207,7 @@ void GlassView::on_channel_spectrum(const ChannelSpectrum& spectrum) {
|
||||
baseband::spectrum_streaming_stop();
|
||||
// Convert bins of this spectrum slice into a representative max_power and when enough, into pixels
|
||||
// we actually need screen_width (240) of those bins
|
||||
for (uint8_t bin = 0; bin < bin_length; bin++) {
|
||||
for (uint16_t bin = 0; bin < bin_length; bin++) {
|
||||
get_max_power(spectrum, bin, max_power);
|
||||
if (max_power > range_max_power)
|
||||
range_max_power = max_power;
|
||||
@@ -208,7 +215,7 @@ void GlassView::on_channel_spectrum(const ChannelSpectrum& spectrum) {
|
||||
if (bin == 119) {
|
||||
uint8_t next_max_power = 0;
|
||||
get_max_power(spectrum, bin + 1, next_max_power);
|
||||
for (uint8_t it = 0; it < ignore_dc; it++) {
|
||||
for (uint16_t it = 0; it < ignore_dc; it++) {
|
||||
uint8_t med_max_power = (max_power + next_max_power) / 2; // due to the way process_bins works we have to keep resetting the color
|
||||
if (process_bins(&med_max_power) == true)
|
||||
return; // new line signaled, return
|
||||
@@ -285,7 +292,7 @@ void GlassView::on_range_changed() {
|
||||
max_power = 0;
|
||||
bins_hz_size = 0;
|
||||
|
||||
on_marker_change();
|
||||
on_marker_change(0);
|
||||
update_range_field();
|
||||
|
||||
// set the sample rate and bandwidth
|
||||
@@ -298,8 +305,8 @@ void GlassView::on_range_changed() {
|
||||
receiver_model.set_target_frequency(f_center); // tune rx for this slice
|
||||
}
|
||||
|
||||
void GlassView::plot_marker(uint8_t pos) {
|
||||
uint8_t shift_y = 0;
|
||||
void GlassView::plot_marker(uint16_t pos) {
|
||||
uint16_t shift_y = 0;
|
||||
if (live_frequency_view > 0) // plot one line down when in live view
|
||||
{
|
||||
shift_y = 16;
|
||||
@@ -381,9 +388,10 @@ GlassView::GlassView(
|
||||
&button_beep_squelch,
|
||||
&field_marker,
|
||||
&field_trigger,
|
||||
&button_marker_minus,
|
||||
&button_marker_plus,
|
||||
&button_jump,
|
||||
&button_rst,
|
||||
&field_rx_iq_phase_cal,
|
||||
&freq_stats});
|
||||
|
||||
load_presets(); // Load available presets from TXT files (or default).
|
||||
@@ -491,13 +499,7 @@ GlassView::GlassView(
|
||||
range_presets.set_selected_index(preset_index);
|
||||
|
||||
field_marker.on_encoder_change = [this](TextField&, EncoderEvent delta) {
|
||||
if ((marker_pixel_index + delta) < 0)
|
||||
marker_pixel_index = marker_pixel_index + delta + screen_width;
|
||||
else if ((marker_pixel_index + delta) > screen_width)
|
||||
marker_pixel_index = marker_pixel_index + delta - screen_width;
|
||||
else
|
||||
marker_pixel_index = marker_pixel_index + delta;
|
||||
on_marker_change();
|
||||
on_marker_change(delta);
|
||||
};
|
||||
|
||||
field_marker.on_select = [this](TextField&) {
|
||||
@@ -516,6 +518,14 @@ GlassView::GlassView(
|
||||
update_range_field();
|
||||
};
|
||||
|
||||
button_marker_minus.on_select = [this](Button&) {
|
||||
on_marker_change(-1);
|
||||
};
|
||||
|
||||
button_marker_plus.on_select = [this](Button&) {
|
||||
on_marker_change(1);
|
||||
};
|
||||
|
||||
button_jump.on_select = [this](Button&) {
|
||||
// Launch Audio with peak frequency.
|
||||
launch_audio(max_freq_hold);
|
||||
@@ -525,13 +535,6 @@ GlassView::GlassView(
|
||||
reset_live_view();
|
||||
};
|
||||
|
||||
field_rx_iq_phase_cal.set_range(0, hackrf_r9 ? 63 : 31); // max2839 has 6 bits [0..63], max2837 has 5 bits [0..31]
|
||||
field_rx_iq_phase_cal.set_value(get_spec_iq_phase_calibration_value()); // using accessor function of AnalogAudioView to read iq_phase_calibration_value from rx_audio.ini
|
||||
field_rx_iq_phase_cal.on_change = [this](int32_t v) {
|
||||
set_spec_iq_phase_calibration_value(v); // using accessor function of AnalogAudioView to write inside SPEC submenu, register value to max283x and save it to rx_audio.ini
|
||||
};
|
||||
set_spec_iq_phase_calibration_value(get_spec_iq_phase_calibration_value()); // initialize iq_phase_calibration in radio
|
||||
|
||||
display.scroll_set_area(109, screen_height - 1); // Restart scroll on the correct coordinates
|
||||
|
||||
// trigger:
|
||||
@@ -581,15 +584,6 @@ void GlassView::on_freqchg(int64_t freq) {
|
||||
on_range_changed();
|
||||
}
|
||||
|
||||
uint8_t GlassView::get_spec_iq_phase_calibration_value() { // define accessor functions inside AnalogAudioView to read & write real iq_phase_calibration_value
|
||||
return iq_phase_calibration_value;
|
||||
}
|
||||
|
||||
void GlassView::set_spec_iq_phase_calibration_value(uint8_t cal_value) { // define accessor functions
|
||||
iq_phase_calibration_value = cal_value;
|
||||
radio::set_rx_max283x_iq_phase_calibration(iq_phase_calibration_value);
|
||||
}
|
||||
|
||||
void GlassView::load_presets() {
|
||||
File presets_file;
|
||||
auto error = presets_file.open(looking_glass_dir / u"PRESETS.TXT");
|
||||
|
||||
@@ -66,9 +66,6 @@ class GlassView : public View {
|
||||
void on_hide() override;
|
||||
void focus() override;
|
||||
|
||||
uint8_t get_spec_iq_phase_calibration_value();
|
||||
void set_spec_iq_phase_calibration_value(uint8_t cal_value);
|
||||
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
Gradient gradient{};
|
||||
@@ -80,11 +77,10 @@ class GlassView : public View {
|
||||
uint8_t filter_index = 0; // OFF
|
||||
uint8_t trigger = 32;
|
||||
uint8_t mode = LOOKING_GLASS_FASTSCAN;
|
||||
uint8_t live_frequency_view = 0; // Spectrum
|
||||
uint8_t live_frequency_integrate = 3; // Default (3 * old value + new_value) / 4
|
||||
uint8_t iq_phase_calibration_value{15}; // initial default RX IQ phase calibration value , used for both max2837 & max2839
|
||||
int32_t beep_squelch = 20; // range from -100 to +20, >=20 disabled
|
||||
bool beep_enabled = false; // activate on bip button click
|
||||
uint8_t live_frequency_view = 0; // Spectrum
|
||||
uint8_t live_frequency_integrate = 3; // Default (3 * old value + new_value) / 4
|
||||
int32_t beep_squelch = 20; // range from -100 to +20, >=20 disabled
|
||||
bool beep_enabled = false; // activate on bip button click
|
||||
app_settings::SettingsManager settings_{
|
||||
"rx_glass"sv,
|
||||
app_settings::Mode::RX,
|
||||
@@ -97,7 +93,6 @@ class GlassView : public View {
|
||||
{"scan_mode"sv, &mode},
|
||||
{"freq_view"sv, &live_frequency_view},
|
||||
{"freq_integrate"sv, &live_frequency_integrate},
|
||||
{"iq_phase_calibration"sv, &iq_phase_calibration_value}, // we are saving and restoring that CAL from Settings.
|
||||
{"beep_squelch"sv, &beep_squelch},
|
||||
{"beep_enabled"sv, &beep_enabled},
|
||||
}};
|
||||
@@ -116,9 +111,9 @@ class GlassView : public View {
|
||||
void update_min(int32_t v);
|
||||
void update_max(int32_t v);
|
||||
void update_range_field();
|
||||
void get_max_power(const ChannelSpectrum& spectrum, uint8_t bin, uint8_t& max_power);
|
||||
rf::Frequency get_freq_from_bin_pos(uint8_t pos);
|
||||
void on_marker_change();
|
||||
void get_max_power(const ChannelSpectrum& spectrum, uint16_t bin, uint8_t& max_power);
|
||||
rf::Frequency get_freq_from_bin_pos(uint16_t pos);
|
||||
void on_marker_change(int8_t delta);
|
||||
void retune();
|
||||
bool process_bins(uint8_t* powerlevel);
|
||||
void on_channel_spectrum(const ChannelSpectrum& spectrum);
|
||||
@@ -128,7 +123,7 @@ class GlassView : public View {
|
||||
void on_range_changed();
|
||||
void reset_live_view();
|
||||
void add_spectrum_pixel(uint8_t power);
|
||||
void plot_marker(uint8_t pos);
|
||||
void plot_marker(uint16_t pos);
|
||||
void load_presets();
|
||||
void populate_presets();
|
||||
void launch_audio(rf::Frequency center_freq);
|
||||
@@ -138,7 +133,7 @@ class GlassView : public View {
|
||||
rf::Frequency f_center_ini{0};
|
||||
|
||||
rf::Frequency marker{0};
|
||||
uint8_t marker_pixel_index{0};
|
||||
uint16_t marker_pixel_index{0};
|
||||
rf::Frequency marker_pixel_step{0};
|
||||
|
||||
// Size of one spectrum bin in Hz.
|
||||
@@ -173,8 +168,7 @@ class GlassView : public View {
|
||||
{{0, UI_POS_Y(0)}, "MIN: MAX: LNA VGA ", Theme::getInstance()->fg_light->foreground},
|
||||
{{0, 1 * 16}, "RANGE: FILTER: AMP:", Theme::getInstance()->fg_light->foreground},
|
||||
{{0, 2 * 16}, "P:", Theme::getInstance()->fg_light->foreground},
|
||||
{{0, 3 * 16}, "MARKER: MHz RXIQCAL", Theme::getInstance()->fg_light->foreground},
|
||||
//{{0, 4 * 16}, "RES: STEPS:", Theme::getInstance()->fg_light->foreground}};
|
||||
{{0, 3 * 16}, "MARKER( / ): MHz", Theme::getInstance()->fg_light->foreground},
|
||||
{{0, 4 * 16}, "RES: VOL:", Theme::getInstance()->fg_light->foreground}};
|
||||
|
||||
NumberField field_frequency_min{
|
||||
@@ -223,16 +217,16 @@ class GlassView : public View {
|
||||
""};
|
||||
|
||||
TextField field_marker{
|
||||
{7 * 8, 3 * 16, 9 * 8, 16},
|
||||
{12 * 8, 3 * 16, 9 * 8, 16},
|
||||
""};
|
||||
|
||||
NumberField field_rx_iq_phase_cal{
|
||||
{28 * 8, 3 * 16},
|
||||
2,
|
||||
{0, 63}, // 5 or 6 bits IQ CAL phase adjustment (range updated later)
|
||||
1,
|
||||
' ',
|
||||
};
|
||||
Button button_marker_minus{
|
||||
{7 * 8, 3 * 16 + 4, 1 * 8, 1 * 8},
|
||||
"-"};
|
||||
|
||||
Button button_marker_plus{
|
||||
{9 * 8, 3 * 16 + 4, 1 * 8, 1 * 8},
|
||||
"+"};
|
||||
|
||||
NumberField field_trigger{
|
||||
{4 * 8, 4 * 16},
|
||||
|
||||
@@ -350,6 +350,12 @@ MicTXView::MicTXView(
|
||||
&tx_button,
|
||||
&tx_icon});
|
||||
|
||||
// disable key repeat on select
|
||||
initial_switch_config_ = get_switches_repeat_config();
|
||||
SwitchesState config = initial_switch_config_;
|
||||
config[toUType(Switch::Sel)] = false;
|
||||
set_switches_repeat_config(config);
|
||||
|
||||
set_rxbw_options();
|
||||
set_rxbw_defaults(settings_.loaded());
|
||||
|
||||
@@ -640,6 +646,9 @@ MicTXView::MicTXView(
|
||||
}
|
||||
|
||||
MicTXView::~MicTXView() {
|
||||
// restore select key repeat mode
|
||||
set_switches_repeat_config(initial_switch_config_);
|
||||
|
||||
audio::input::stop();
|
||||
if (rx_enabled) { // Also turn off both (audio rx if enabled, and disable mic_loop to HP)
|
||||
rxaudio(false);
|
||||
|
||||
@@ -90,6 +90,9 @@ class MicTXView : public View {
|
||||
sampling_rate /* sampling rate */
|
||||
};
|
||||
|
||||
// structure used to control key repeat
|
||||
SwitchesState initial_switch_config_{};
|
||||
|
||||
enum Mic_Modulation : uint32_t {
|
||||
MIC_MOD_NFM = 0,
|
||||
MIC_MOD_WFM = 1,
|
||||
|
||||
@@ -309,6 +309,39 @@ SetFrequencyCorrectionModel SetRadioView::form_collect() {
|
||||
};
|
||||
}
|
||||
|
||||
/* SetTXLimitView ************************************/
|
||||
|
||||
SetTXLimitView::SetTXLimitView(NavigationView& nav) {
|
||||
add_children({
|
||||
&labels,
|
||||
&tx_gain_max_db,
|
||||
&tx_disable_switch,
|
||||
&tx_amp_disable_switch,
|
||||
&button_save,
|
||||
&button_cancel,
|
||||
});
|
||||
|
||||
tx_disable_switch.set_value(pmem::config_tx_disabled());
|
||||
tx_amp_disable_switch.set_value(pmem::config_tx_amp_disabled());
|
||||
tx_gain_max_db.set_value(pmem::config_tx_gain_max_db());
|
||||
|
||||
button_save.on_select = [&nav, this](Button&) {
|
||||
pmem::set_config_tx_disabled(tx_disable_switch.value());
|
||||
pmem::set_config_tx_amp_disabled(tx_amp_disable_switch.value());
|
||||
pmem::set_config_tx_gain_max_db(tx_gain_max_db.value());
|
||||
send_system_refresh();
|
||||
nav.pop();
|
||||
};
|
||||
|
||||
button_cancel.on_select = [&nav, this](Button&) {
|
||||
nav.pop();
|
||||
};
|
||||
}
|
||||
|
||||
void SetTXLimitView::focus() {
|
||||
button_save.focus();
|
||||
}
|
||||
|
||||
/* SetUIView *********************************************/
|
||||
|
||||
SetUIView::SetUIView(NavigationView& nav) {
|
||||
@@ -1107,6 +1140,7 @@ void SettingsMenuView::on_populate() {
|
||||
{"Freq. Correct", ui::Color::dark_cyan(), &bitmap_icon_options_radio, [this]() { nav_.push<SetFrequencyCorrectionView>(); }},
|
||||
{"P.Memory Mgmt", ui::Color::dark_cyan(), &bitmap_icon_memory, [this]() { nav_.push<SetPersistentMemoryView>(); }},
|
||||
{"Radio", ui::Color::dark_cyan(), &bitmap_icon_options_radio, [this]() { nav_.push<SetRadioView>(); }},
|
||||
{"TX Limit", ui::Color::dark_cyan(), &bitmap_icon_options_radio, [this]() { nav_.push<SetTXLimitView>(); }},
|
||||
{"SD Card", ui::Color::dark_cyan(), &bitmap_icon_sdcard, [this]() { nav_.push<SetSDCardView>(); }},
|
||||
{"User Interface", ui::Color::dark_cyan(), &bitmap_icon_options_ui, [this]() { nav_.push<SetUIView>(); }},
|
||||
{"Display", ui::Color::dark_cyan(), &bitmap_icon_brightness, [this]() { nav_.push<SetDisplayView>(); }},
|
||||
|
||||
@@ -264,6 +264,51 @@ class SetRadioView : public View {
|
||||
SetFrequencyCorrectionModel form_collect();
|
||||
};
|
||||
|
||||
class SetTXLimitView : public View {
|
||||
public:
|
||||
SetTXLimitView(NavigationView& nav);
|
||||
|
||||
void focus() override;
|
||||
|
||||
std::string title() const override { return "TX Limit"; };
|
||||
|
||||
private:
|
||||
Labels labels{
|
||||
{{1 * 8, 1 * 16}, "Limits RF TX Gain", Theme::getInstance()->fg_light->foreground},
|
||||
{{1 * 8, 2 * 16}, "(This may affect", Theme::getInstance()->fg_light->foreground},
|
||||
{{1 * 8, 3 * 16}, "all applications.)", Theme::getInstance()->fg_light->foreground},
|
||||
{{2 * 8, 12 * 16}, "TX Max Gain:", Theme::getInstance()->fg_light->foreground},
|
||||
|
||||
};
|
||||
|
||||
Checkbox tx_disable_switch{
|
||||
{1 * 8, 6 * 16},
|
||||
23,
|
||||
"Disable TX"};
|
||||
|
||||
Checkbox tx_amp_disable_switch{
|
||||
{1 * 8, 8 * 16},
|
||||
23,
|
||||
"Disable TX Amp"};
|
||||
|
||||
NumberField tx_gain_max_db{
|
||||
{20 * 8, 12 * 16},
|
||||
6,
|
||||
{0, 47},
|
||||
1,
|
||||
' ',
|
||||
};
|
||||
|
||||
Button button_save{
|
||||
{UI_POS_X_CENTER(12) - UI_POS_WIDTH(8), UI_POS_Y_BOTTOM(4), UI_POS_WIDTH(12), UI_POS_HEIGHT(2)},
|
||||
"Save"};
|
||||
|
||||
Button button_cancel{
|
||||
{UI_POS_X_CENTER(16) + UI_POS_WIDTH(8), UI_POS_Y_BOTTOM(4), UI_POS_WIDTH(12), UI_POS_HEIGHT(2)},
|
||||
"Cancel",
|
||||
};
|
||||
};
|
||||
|
||||
using portapack::persistent_memory::backlight_timeout_t;
|
||||
|
||||
class SetUIView : public View {
|
||||
|
||||
@@ -242,6 +242,8 @@ const char* SubGhzDView::getSensorTypeName(FPROTO_SUBGHZD_SENSOR type) {
|
||||
return "GangQi";
|
||||
case FPS_MARANTEC24:
|
||||
return "Marantec24";
|
||||
case FPS_HOLTEKHT6P20B:
|
||||
return "Holtek HT6P20B";
|
||||
case FPS_Invalid:
|
||||
default:
|
||||
return "Unknown";
|
||||
@@ -756,5 +758,11 @@ void SubGhzDRecentEntryDetailView::parseProtocol() {
|
||||
btn = entry_.data & 0xf;
|
||||
return;
|
||||
}
|
||||
|
||||
if (entry_.sensorType == FPS_HOLTEKHT6P20B) {
|
||||
serial = entry_.data >> 8;
|
||||
btn = (entry_.data >> 4) & 0xF;
|
||||
return;
|
||||
}
|
||||
}
|
||||
} // namespace ui
|
||||
} // namespace ui
|
||||
|
||||
@@ -368,6 +368,26 @@ void set_subghzd_config(uint8_t modulation = 0, uint32_t sampling_rate = 0) {
|
||||
send_message(&message);
|
||||
}
|
||||
|
||||
void set_moreserx_config(uint8_t mode) {
|
||||
const MorseRXConfigureMessage message{mode};
|
||||
send_message(&message);
|
||||
}
|
||||
|
||||
void set_morsetx_config(uint8_t mode, uint32_t tone, float fm_delta) {
|
||||
const MorseTXConfigureMessage message{mode, tone, fm_delta};
|
||||
send_message(&message);
|
||||
}
|
||||
|
||||
void set_morsetx_key(bool key_down) {
|
||||
const MorseTXkeyMessage message{key_down};
|
||||
send_message(&message);
|
||||
}
|
||||
|
||||
void set_bitstream_config(uint32_t deviation, uint8_t mode) {
|
||||
const StreamTXConfigurationMessage message{deviation, mode};
|
||||
send_message(&message);
|
||||
}
|
||||
|
||||
static bool baseband_image_running = false;
|
||||
|
||||
void run_image(const spi_flash::image_tag_t image_tag) {
|
||||
|
||||
@@ -102,9 +102,13 @@ void set_siggen_tone(const uint32_t tone);
|
||||
void set_siggen_config(const uint32_t bw, const uint32_t shape, const uint32_t duration);
|
||||
void set_spectrum_painter_config(const uint16_t width, const uint16_t height, bool update, int32_t bw);
|
||||
void set_subghzd_config(uint8_t modulation, uint32_t sampling_rate);
|
||||
void set_moreserx_config(uint8_t mode);
|
||||
void set_morsetx_config(uint8_t mode, uint32_t tone, float fm_delta);
|
||||
void set_morsetx_key(bool key_down);
|
||||
void set_wefax_config(uint8_t lpm, uint8_t ioc);
|
||||
void set_noaaapt_config();
|
||||
void set_flex_config();
|
||||
void set_bitstream_config(uint32_t deviation, uint8_t mode); // mode 0 for am, 1 for 2fsk
|
||||
|
||||
void request_roger_beep();
|
||||
void request_rssi_beep();
|
||||
|
||||
@@ -4297,7 +4297,7 @@ static constexpr Bitmap bitmap_icon_tpms{
|
||||
{16, 16},
|
||||
bitmap_icon_tpms_data};
|
||||
|
||||
static constexpr uint8_t bitmap_icon_tranceivers_data[] = {
|
||||
static constexpr uint8_t bitmap_icon_transceivers_data[] = {
|
||||
0x80,
|
||||
0x01,
|
||||
0xC0,
|
||||
@@ -4331,9 +4331,9 @@ static constexpr uint8_t bitmap_icon_tranceivers_data[] = {
|
||||
0xFF,
|
||||
0xFF,
|
||||
};
|
||||
static constexpr Bitmap bitmap_icon_tranceivers{
|
||||
static constexpr Bitmap bitmap_icon_transceivers{
|
||||
{16, 16},
|
||||
bitmap_icon_tranceivers_data};
|
||||
bitmap_icon_transceivers_data};
|
||||
|
||||
static constexpr uint8_t bitmap_icon_transmit_data[] = {
|
||||
0x80,
|
||||
|
||||
+38
-25
@@ -62,19 +62,24 @@ AppManagerView::AppManagerView(NavigationView& nav)
|
||||
auto app_name = get_app_info(menu_view.highlighted_index(), true);
|
||||
auto app_id = get_app_info(menu_view.highlighted_index(), false);
|
||||
|
||||
info += "Hidden:";
|
||||
if (!app_name.empty()) {
|
||||
info += "Hidden:";
|
||||
|
||||
if (is_blacklisted(app_name)) {
|
||||
info += "Yes ";
|
||||
} else {
|
||||
info += "No ";
|
||||
if (is_blacklisted(app_name)) {
|
||||
info += "Yes ";
|
||||
} else {
|
||||
info += "No ";
|
||||
}
|
||||
}
|
||||
|
||||
info += "Autostart:";
|
||||
if (is_autostart_app(app_id)) {
|
||||
info += "Yes";
|
||||
} else {
|
||||
info += "No";
|
||||
if (!app_id.empty()) {
|
||||
info += "Autostart:";
|
||||
|
||||
if (is_autostart_app(app_id)) {
|
||||
info += "Yes";
|
||||
} else {
|
||||
info += "No";
|
||||
}
|
||||
}
|
||||
|
||||
if (info.empty()) {
|
||||
@@ -82,6 +87,8 @@ AppManagerView::AppManagerView(NavigationView& nav)
|
||||
}
|
||||
|
||||
text_app_info.set(info);
|
||||
button_hide_unhide.set_focusable(!app_name.empty());
|
||||
button_set_cancel_autostart.set_focusable(!app_id.empty());
|
||||
};
|
||||
|
||||
button_hide_unhide.on_select = [this](Button&) {
|
||||
@@ -120,27 +127,23 @@ void AppManagerView::refresh_list() {
|
||||
};
|
||||
|
||||
for (auto& app : NavigationView::appList) {
|
||||
if (app.id == nullptr) continue;
|
||||
|
||||
app_list_index++;
|
||||
|
||||
menu_view.add_item({app.displayName + std::string(is_autostart_app(app.id) ? padding(app.displayName) : ""),
|
||||
menu_view.add_item({app.displayName + std::string((app.id != nullptr && is_autostart_app(app.id)) ? padding(app.displayName) : ""),
|
||||
app.iconColor,
|
||||
app.icon,
|
||||
[this, app_id = std::string(app.id)](KeyEvent) {
|
||||
[this, app_id = std::string(app.id != nullptr ? app.id : "")](KeyEvent) {
|
||||
button_hide_unhide.focus();
|
||||
}});
|
||||
}
|
||||
|
||||
ExternalItemsMenuLoader::load_all_external_items_callback([this, &index, &padding](ui::AppInfoConsole& app) {
|
||||
if (app.appCallName == nullptr) return;
|
||||
|
||||
app_list_index++;
|
||||
|
||||
menu_view.add_item({app.appFriendlyName + std::string(is_autostart_app(app.appCallName) ? padding(app.appFriendlyName) : ""),
|
||||
menu_view.add_item({app.appFriendlyName + std::string((app.appCallName != nullptr && is_autostart_app(app.appCallName)) ? padding(app.appFriendlyName) : ""),
|
||||
ui::Color::light_grey(),
|
||||
&bitmap_icon_sdcard,
|
||||
[this, app_id = std::string(app.appCallName)](KeyEvent) {
|
||||
[this, app_id = std::string(app.appCallName != nullptr ? app.appCallName : "")](KeyEvent) {
|
||||
button_hide_unhide.focus();
|
||||
}});
|
||||
});
|
||||
@@ -156,19 +159,29 @@ void AppManagerView::hide_app() {
|
||||
std::vector<std::string> blacklist;
|
||||
get_blacklist(blacklist);
|
||||
|
||||
blacklist.push_back(get_app_info(menu_view.highlighted_index(), true));
|
||||
auto app_name = get_app_info(menu_view.highlighted_index(), true);
|
||||
if (app_name.empty()) return;
|
||||
|
||||
blacklist.push_back(app_name);
|
||||
write_blacklist(blacklist);
|
||||
}
|
||||
|
||||
void AppManagerView::unhide_app() {
|
||||
std::vector<std::string> blacklist;
|
||||
get_blacklist(blacklist);
|
||||
blacklist.erase(std::remove(blacklist.begin(), blacklist.end(), get_app_info(menu_view.highlighted_index(), true)), blacklist.end());
|
||||
|
||||
auto app_name = get_app_info(menu_view.highlighted_index(), true);
|
||||
if (app_name.empty()) return;
|
||||
|
||||
blacklist.erase(std::remove(blacklist.begin(), blacklist.end(), app_name), blacklist.end());
|
||||
write_blacklist(blacklist);
|
||||
}
|
||||
|
||||
void AppManagerView::hide_unhide_app() {
|
||||
if (is_blacklisted(get_app_info(menu_view.highlighted_index(), true)))
|
||||
auto app_name = get_app_info(menu_view.highlighted_index(), true);
|
||||
if (app_name.empty()) return;
|
||||
|
||||
if (is_blacklisted(app_name))
|
||||
unhide_app();
|
||||
else
|
||||
hide_app();
|
||||
@@ -218,6 +231,7 @@ void AppManagerView::set_auto_start() {
|
||||
if (app_index >= app_list_index) return;
|
||||
|
||||
auto id_aka_app_call_name = get_app_info(app_index, false);
|
||||
if (id_aka_app_call_name.empty()) return;
|
||||
|
||||
autostart_app = id_aka_app_call_name;
|
||||
|
||||
@@ -234,6 +248,7 @@ void AppManagerView::set_unset_autostart_app() {
|
||||
if (app_index >= app_list_index) return;
|
||||
|
||||
auto id_aka_friendly_name = get_app_info(app_index, false);
|
||||
if (id_aka_friendly_name.empty()) return;
|
||||
|
||||
if (is_autostart_app(id_aka_friendly_name))
|
||||
unset_auto_start();
|
||||
@@ -254,17 +269,15 @@ std::string AppManagerView::get_app_info(uint16_t index, bool is_display_name) {
|
||||
std::string result;
|
||||
|
||||
for (auto& app : NavigationView::appList) {
|
||||
if (app.id == nullptr) continue;
|
||||
if (current_index == index) {
|
||||
return is_display_name ? std::string(app.displayName) : std::string(app.id);
|
||||
return is_display_name ? std::string(app.displayName != nullptr ? app.displayName : "") : std::string(app.id != nullptr ? app.id : "");
|
||||
}
|
||||
current_index++;
|
||||
}
|
||||
|
||||
ExternalItemsMenuLoader::load_all_external_items_callback([¤t_index, &index, &result, &is_display_name](ui::AppInfoConsole& app) {
|
||||
if (app.appCallName == nullptr) return;
|
||||
if (current_index == index) {
|
||||
result = is_display_name ? app.appFriendlyName : app.appCallName;
|
||||
result = is_display_name ? (app.appFriendlyName != nullptr ? app.appFriendlyName : "") : (app.appCallName != nullptr ? app.appCallName : "");
|
||||
}
|
||||
current_index++;
|
||||
});
|
||||
|
||||
+31
-5
@@ -91,9 +91,9 @@ set(EXTCPPSRC
|
||||
external/adsbtx/main.cpp
|
||||
external/adsbtx/ui_adsb_tx.cpp
|
||||
|
||||
#morse_tx 768 bytes
|
||||
external/morse_tx/main.cpp
|
||||
external/morse_tx/ui_morse.cpp
|
||||
#morse_tx 768 bytes -- disabled because of the new morse tx app with more functions
|
||||
#external/morse_tx/main.cpp
|
||||
#external/morse_tx/ui_morse.cpp
|
||||
|
||||
#sstvtx 456 bytes
|
||||
external/sstvtx/main.cpp
|
||||
@@ -222,7 +222,11 @@ set(EXTCPPSRC
|
||||
|
||||
#gfxEQ 80 byte
|
||||
external/gfxeq/main.cpp
|
||||
external/gfxeq/ui_gfxeq.cpp
|
||||
external/gfxeq/ui_gfxeq.cpp
|
||||
|
||||
#waterfall designer
|
||||
external/waterfall_designer/main.cpp
|
||||
external/waterfall_designer/ui_waterfall_designer.cpp
|
||||
|
||||
#detector_rx 168 byte
|
||||
external/detector_rx/main.cpp
|
||||
@@ -276,6 +280,23 @@ set(EXTCPPSRC
|
||||
#subcarrx
|
||||
external/subcarrx/main.cpp
|
||||
external/subcarrx/ui_subcar.cpp
|
||||
|
||||
#siggen
|
||||
external/siggen/main.cpp
|
||||
external/siggen/ui_siggen.cpp
|
||||
|
||||
#sdusb
|
||||
external/sdusb/main.cpp
|
||||
external/sdusb/ui_sd_over_usb.cpp
|
||||
|
||||
|
||||
#morse_radio
|
||||
external/morse_radio/main.cpp
|
||||
external/morse_radio/ui_morse_radio.cpp
|
||||
|
||||
#morseradiotx
|
||||
external/morseradiotx/main.cpp
|
||||
external/morseradiotx/ui_morse_radiotx.cpp
|
||||
)
|
||||
|
||||
set(EXTAPPLIST
|
||||
@@ -300,7 +321,7 @@ set(EXTAPPLIST
|
||||
tpmsrx
|
||||
protoview
|
||||
adsbtx
|
||||
morse_tx
|
||||
#morse_tx
|
||||
sstvtx
|
||||
sstvrx
|
||||
random_password
|
||||
@@ -332,6 +353,7 @@ set(EXTAPPLIST
|
||||
scanner
|
||||
level
|
||||
gfxeq
|
||||
waterfall_designer
|
||||
detector_rx
|
||||
spaceinv
|
||||
blackjack
|
||||
@@ -345,4 +367,8 @@ set(EXTAPPLIST
|
||||
adult_toys_controller
|
||||
flex_rx
|
||||
subcarrx
|
||||
siggen
|
||||
sdusb
|
||||
morse_radio
|
||||
morseradiotx
|
||||
)
|
||||
|
||||
+35
-2
@@ -89,7 +89,11 @@ MEMORY
|
||||
ram_external_app_flex_rx (rwx) : org = 0xADF00000, len = 32k
|
||||
ram_external_app_sstvrx (rwx) : org = 0xADF10000, len = 32k
|
||||
ram_external_app_subcarrx (rwx) : org = 0xADF20000, len = 32k
|
||||
|
||||
ram_external_app_siggen (rwx) : org = 0xADF30000, len = 32k
|
||||
ram_external_app_sdusb (rwx) : org = 0xADF40000, len = 32k
|
||||
ram_external_app_morse_radio (rwx) : org = 0xADF50000, len = 32k
|
||||
ram_external_app_waterfall_designer (rwx) : org = 0xADF60000, len = 32k
|
||||
ram_external_app_morseradiotx (rwx) : org = 0xADF70000, len = 32k
|
||||
}
|
||||
|
||||
SECTIONS
|
||||
@@ -400,6 +404,12 @@ SECTIONS
|
||||
*(*ui*external_app*gfxeq*);
|
||||
} > ram_external_app_gfxeq
|
||||
|
||||
.external_app_waterfall_designer : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_waterfall_designer.application_information));
|
||||
*(*ui*external_app*waterfall_designer*);
|
||||
} > ram_external_app_waterfall_designer
|
||||
|
||||
.external_app_detector_rx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_detector_rx.application_information));
|
||||
@@ -492,6 +502,29 @@ SECTIONS
|
||||
*(*ui*external_app*subcarrx*);
|
||||
} > ram_external_app_subcarrx
|
||||
|
||||
|
||||
.external_app_siggen : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_siggen.application_information));
|
||||
*(*ui*external_app*siggen*);
|
||||
} > ram_external_app_siggen
|
||||
|
||||
.external_app_sdusb : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_sdusb.application_information));
|
||||
*(*ui*external_app*sdusb*);
|
||||
} > ram_external_app_sdusb
|
||||
|
||||
.external_app_morse_radio : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_morse_radio.application_information));
|
||||
*(*ui*external_app*morse_radio*);
|
||||
} > ram_external_app_morse_radio
|
||||
|
||||
.external_app_morseradiotx : ALIGN(4) SUBALIGN(4)
|
||||
{
|
||||
KEEP(*(.external_app.app_morseradiotx.application_information));
|
||||
*(*ui*external_app*morseradiotx*);
|
||||
} > ram_external_app_morseradiotx
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ bool FlipperTxView::on_file_changed(std::filesystem::path new_file_path) {
|
||||
return false;
|
||||
}
|
||||
preset = submeta.value().preset;
|
||||
if (preset != FLIPPER_PRESET_OOK) {
|
||||
if (preset != FLIPPER_PRESET_OOK && preset != FLIPPER_PRESET_2FSK) {
|
||||
field_filename.set_text("File: err, not supp. preset");
|
||||
return false;
|
||||
}
|
||||
@@ -108,8 +108,13 @@ void FlipperTxView::stop() {
|
||||
bool FlipperTxView::start() {
|
||||
if (filename.empty()) return false;
|
||||
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
|
||||
transmitter_model.set_sampling_rate(OOK_SAMPLERATE);
|
||||
transmitter_model.enable();
|
||||
button_startstop.set_text(LanguageHelper::currentMessages[LANG_STOP]);
|
||||
uint8_t mode = 0;
|
||||
if (preset == FLIPPER_PRESET_2FSK)
|
||||
mode = 1;
|
||||
baseband::set_bitstream_config(FM_DEVIATION, mode);
|
||||
// start thread
|
||||
replay_thread = std::make_unique<FlipperPlayThread>(
|
||||
filename,
|
||||
|
||||
@@ -28,6 +28,8 @@ using namespace ui;
|
||||
namespace ui::external_app::flippertx {
|
||||
|
||||
#define OOK_SAMPLERATE 2280000U
|
||||
#define FM_DEVIATION 60000U
|
||||
|
||||
class FlipperPlayThread;
|
||||
class FlipperTxView : public View {
|
||||
public:
|
||||
|
||||
+27
-12
@@ -24,8 +24,7 @@
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
#define MORSEDEC_ERROR "<ERR>"
|
||||
#include "string_format.hpp"
|
||||
|
||||
namespace ui::external_app::morse_practice {
|
||||
|
||||
@@ -89,21 +88,28 @@ class MorseDecoder {
|
||||
};
|
||||
|
||||
MorseDecoder() {}
|
||||
DecodeResult handleInput(int32_t duration_ms) {
|
||||
|
||||
DecodeResult
|
||||
handleInput(int32_t duration_ms) {
|
||||
DecodeResult result = {"", 0.0};
|
||||
|
||||
if (duration_ms < 5 && duration_ms > -5) return result;
|
||||
|
||||
if (duration_ms > 0) {
|
||||
pulse_history_.push_back(duration_ms);
|
||||
double dah_prob = getDahProbability(duration_ms);
|
||||
current_sequence_ += (dah_prob > 0.5) ? '-' : '.';
|
||||
last_confidence_ = (dah_prob > 0.5) ? dah_prob : (1.0 - dah_prob);
|
||||
last_sequence_ = current_sequence_;
|
||||
|
||||
} else {
|
||||
uint32_t gap_duration = -duration_ms;
|
||||
pulse_gaps_.push_back(gap_duration);
|
||||
|
||||
if (gap_duration >= getInterCharThreshold() && !current_sequence_.empty()) {
|
||||
result.text = lookupMorse(current_sequence_);
|
||||
result.confidence = (result.text != MORSEDEC_ERROR) ? last_confidence_ : 0.0;
|
||||
|
||||
result.confidence = (result.text[0] != '{') ? last_confidence_ : 0.0;
|
||||
if (gap_duration >= getInterWordThreshold()) {
|
||||
result.text += " ";
|
||||
}
|
||||
@@ -116,9 +122,9 @@ class MorseDecoder {
|
||||
return result;
|
||||
}
|
||||
|
||||
inline double getInterElementThreshold() { return time_unit_ms_ * 2.0; }
|
||||
inline double getInterCharThreshold() { return time_unit_ms_ * 4.0; }
|
||||
inline double getInterWordThreshold() { return time_unit_ms_ * 7.0; }
|
||||
inline double getInterElementThreshold() { return time_unit_ms_ * 0.8; }
|
||||
inline double getInterCharThreshold() { return time_unit_ms_ * 2.5; }
|
||||
inline double getInterWordThreshold() { return time_unit_ms_ * 6.0; }
|
||||
|
||||
inline double getCurrentTimeUnit() { return time_unit_ms_; }
|
||||
inline std::string getLastSequence() { return last_sequence_; }
|
||||
@@ -126,7 +132,7 @@ class MorseDecoder {
|
||||
private:
|
||||
std::string current_sequence_ = "";
|
||||
std::string last_sequence_ = "";
|
||||
double time_unit_ms_ = 160.0;
|
||||
double time_unit_ms_ = 119.0;
|
||||
double last_confidence_ = 0.0;
|
||||
MorseRingBuffer pulse_history_{};
|
||||
MorseRingBuffer pulse_gaps_{};
|
||||
@@ -136,7 +142,7 @@ class MorseDecoder {
|
||||
if (seq == morse_table_[i].code)
|
||||
return morse_table_[i].letter;
|
||||
}
|
||||
return MORSEDEC_ERROR; // not found
|
||||
return "{" + seq + "}"; // not found
|
||||
}
|
||||
|
||||
double getDahProbability(uint32_t duration_ms) {
|
||||
@@ -298,8 +304,8 @@ class MorseDecoder {
|
||||
time_unit_ms_ = (time_unit_ms_ * (1.0 - learning_factor)) + (new_time_unit * learning_factor);
|
||||
}
|
||||
|
||||
size_t morse_table_size_ = 41;
|
||||
MorseEntry morse_table_[41] = {
|
||||
size_t morse_table_size_ = 50;
|
||||
MorseEntry morse_table_[50] = {
|
||||
{".-", "A"},
|
||||
{"-...", "B"},
|
||||
{"-.-.", "C"},
|
||||
@@ -340,7 +346,16 @@ class MorseDecoder {
|
||||
{"..--..", "?"},
|
||||
{"-.-.--", "!"},
|
||||
{"--..--", ","},
|
||||
{"-...-", "="}};
|
||||
{"-...-", "="},
|
||||
{"-..-.", "/"},
|
||||
{".--.-.", "@"},
|
||||
{"---...", ":"},
|
||||
{"-....-", "-"},
|
||||
{".----.", "'"},
|
||||
{".-..-.", "\""},
|
||||
{"-.--.", "("},
|
||||
{"-.--.-", ")"},
|
||||
{".-.-.", "+"}};
|
||||
};
|
||||
|
||||
} // namespace ui::external_app::morse_practice
|
||||
|
||||
@@ -12,6 +12,13 @@ MorsePracticeView::MorsePracticeView(ui::NavigationView& nav)
|
||||
&btn_clear,
|
||||
&console_text,
|
||||
&field_volume});
|
||||
|
||||
initial_switch_config_ = get_switches_repeat_config();
|
||||
|
||||
SwitchesState config = initial_switch_config_;
|
||||
config[toUType(Switch::Sel)] = false;
|
||||
set_switches_repeat_config(config);
|
||||
|
||||
audio::set_rate(audio::Rate::Hz_24000);
|
||||
|
||||
btn_tt.on_select = [this](Button&) {
|
||||
@@ -43,6 +50,7 @@ MorsePracticeView::MorsePracticeView(ui::NavigationView& nav)
|
||||
}
|
||||
|
||||
MorsePracticeView::~MorsePracticeView() {
|
||||
set_switches_repeat_config(initial_switch_config_);
|
||||
receiver_model.disable();
|
||||
baseband::shutdown();
|
||||
audio::output::stop();
|
||||
@@ -98,7 +106,7 @@ void MorsePracticeView::writeCharToConsole(const std::string& ch, double confide
|
||||
|
||||
if (ch == " ") {
|
||||
color_id = 0;
|
||||
} else if (ch == MORSEDEC_ERROR) {
|
||||
} else if (ch[0] == '{') { // no match
|
||||
color_id = 0;
|
||||
} else {
|
||||
if (confidence < 0.8)
|
||||
|
||||
@@ -47,7 +47,7 @@ class MorsePracticeView : public ui::View {
|
||||
MorsePracticeView(ui::NavigationView& nav);
|
||||
~MorsePracticeView();
|
||||
|
||||
std::string title() { return "Morse P"; }
|
||||
std::string title() const override { return "Morse Practice"; }
|
||||
void focus() override;
|
||||
void on_show() override;
|
||||
|
||||
@@ -68,7 +68,7 @@ class MorsePracticeView : public ui::View {
|
||||
ui::Text txt_last{{UI_POS_X(0), UI_POS_Y(6), UI_POS_MAXWIDTH, UI_POS_HEIGHT(1)}, ""};
|
||||
ui::Button btn_clear{{UI_POS_X(0), UI_POS_Y_BOTTOM(2), UI_POS_WIDTH(6), UI_POS_HEIGHT(1)}, "CLR"};
|
||||
ui::Console console_text{{UI_POS_X(0), UI_POS_Y(7), UI_POS_MAXWIDTH, UI_POS_HEIGHT_REMAINING(10)}};
|
||||
AudioVolumeField field_volume{{UI_POS_X_RIGHT(2), UI_POS_X(0)}};
|
||||
AudioVolumeField field_volume{{UI_POS_X_RIGHT(2), UI_POS_Y(0)}};
|
||||
|
||||
uint8_t last_color_id = 255;
|
||||
uint8_t color_id = 255;
|
||||
@@ -77,6 +77,7 @@ class MorsePracticeView : public ui::View {
|
||||
bool button_touch = false;
|
||||
bool button_was_selected = false;
|
||||
bool decode_timeout_calc = false;
|
||||
SwitchesState initial_switch_config_{};
|
||||
|
||||
MessageHandlerRegistration message_handler_framesync{
|
||||
Message::ID::DisplayFrameSync,
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Pezsma
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "ui_morse_radio.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "external_app.hpp"
|
||||
|
||||
namespace ui::external_app::morse_radio {
|
||||
|
||||
void initialize_app(NavigationView& nav) {
|
||||
nav.push<MorseRadioView>();
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::morse_radio
|
||||
|
||||
extern "C" {
|
||||
|
||||
__attribute__((section(".external_app.app_morse_radio.application_information"), used))
|
||||
application_information_t _application_information_morse_radio = {
|
||||
/*.memory_location = */ (uint8_t*)0x00000000,
|
||||
/*.externalAppEntry = */ ui::external_app::morse_radio::initialize_app,
|
||||
/*.header_version = */ CURRENT_HEADER_VERSION,
|
||||
/*.app_version = */ VERSION_MD5,
|
||||
|
||||
/*.app_name = */ "Morse",
|
||||
/*.bitmap_data = */ {
|
||||
0x00,
|
||||
0x00,
|
||||
0xFE,
|
||||
0x7F,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xBB,
|
||||
0xD0,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0x0B,
|
||||
0xE1,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xEB,
|
||||
0xD0,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFE,
|
||||
0x7F,
|
||||
0x70,
|
||||
0x00,
|
||||
0x30,
|
||||
0x00,
|
||||
0x10,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
},
|
||||
/*.icon_color = */ ui::Color::yellow().v,
|
||||
/*.menu_location = */ app_location_t::RX,
|
||||
/*.desired_menu_position = */ -1,
|
||||
|
||||
/*.m4_app_tag = portapack::spi_flash::image_tag_morse_radio */ {'P', 'M', 'R', 'S'},
|
||||
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,378 @@
|
||||
#ifndef __MORSEDECODER_HPP__
|
||||
#define __MORSEDECODER_HPP__
|
||||
|
||||
/*
|
||||
* Copyright (C) 2025 Pezsma
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include "string_format.hpp"
|
||||
|
||||
namespace ui::external_app::morse_radio {
|
||||
|
||||
class MorseRingBuffer {
|
||||
public:
|
||||
MorseRingBuffer()
|
||||
: head_(0), tail_(0), count_(0) {}
|
||||
|
||||
void push_back(const uint32_t& value) {
|
||||
data_[head_] = value;
|
||||
head_ = (head_ + 1) % 40;
|
||||
if (count_ < 40) {
|
||||
count_++;
|
||||
} else {
|
||||
// overwrite oldest element
|
||||
tail_ = (tail_ + 1) % 40;
|
||||
}
|
||||
}
|
||||
|
||||
void pop_front() {
|
||||
if (count_ > 0) {
|
||||
tail_ = (tail_ + 1) % 40;
|
||||
count_--;
|
||||
}
|
||||
}
|
||||
|
||||
size_t size() const { return count_; }
|
||||
bool empty() const { return count_ == 0; }
|
||||
const uint32_t& front() const { return data_[tail_]; }
|
||||
// Access by index (0 = oldest)
|
||||
uint32_t operator[](size_t idx) const {
|
||||
return data_[(tail_ + idx) % 40];
|
||||
}
|
||||
// Convert to vector-like access for sorting etc.
|
||||
void copy_to_array(uint32_t* out) const {
|
||||
for (size_t i = 0; i < count_; ++i)
|
||||
out[i] = (*this)[i];
|
||||
}
|
||||
|
||||
void clear() {
|
||||
head_ = 0;
|
||||
tail_ = 0;
|
||||
count_ = 0;
|
||||
}
|
||||
|
||||
private:
|
||||
uint32_t data_[40];
|
||||
size_t head_;
|
||||
size_t tail_;
|
||||
size_t count_;
|
||||
};
|
||||
|
||||
class MorseDecoder {
|
||||
public:
|
||||
struct DecodeResult {
|
||||
std::string text = "";
|
||||
double confidence = 0.0;
|
||||
|
||||
bool isValid() const {
|
||||
return !text.empty();
|
||||
}
|
||||
};
|
||||
|
||||
struct MorseEntry {
|
||||
std::string code;
|
||||
std::string letter;
|
||||
};
|
||||
|
||||
MorseDecoder() {}
|
||||
|
||||
void resetLearning() {
|
||||
time_unit_ms_ = 119.0;
|
||||
current_sequence_ = "";
|
||||
last_sequence_ = "";
|
||||
last_confidence_ = 0.0;
|
||||
pulse_history_.clear();
|
||||
pulse_gaps_.clear();
|
||||
}
|
||||
|
||||
DecodeResult
|
||||
handleInput(int32_t duration_ms) {
|
||||
DecodeResult result = {"", 0.0};
|
||||
|
||||
if (duration_ms < 5 && duration_ms > -5) return result;
|
||||
|
||||
if (duration_ms > 0) {
|
||||
pulse_history_.push_back(duration_ms);
|
||||
double dah_prob = getDahProbability(duration_ms);
|
||||
current_sequence_ += (dah_prob > 0.5) ? '-' : '.';
|
||||
last_confidence_ = (dah_prob > 0.5) ? dah_prob : (1.0 - dah_prob);
|
||||
last_sequence_ = current_sequence_;
|
||||
|
||||
} else {
|
||||
uint32_t gap_duration = -duration_ms;
|
||||
pulse_gaps_.push_back(gap_duration);
|
||||
|
||||
if (gap_duration >= getInterCharThreshold() && !current_sequence_.empty()) {
|
||||
result.text = lookupMorse(current_sequence_);
|
||||
|
||||
result.confidence = (result.text[0] != '{') ? last_confidence_ : 0.0;
|
||||
if (gap_duration >= getInterWordThreshold()) {
|
||||
result.text += " ";
|
||||
}
|
||||
current_sequence_ = "";
|
||||
}
|
||||
}
|
||||
|
||||
updateLearning();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
inline double getInterElementThreshold() { return time_unit_ms_ * 0.8; }
|
||||
inline double getInterCharThreshold() { return time_unit_ms_ * 2.5; }
|
||||
inline double getInterWordThreshold() { return time_unit_ms_ * 6.0; }
|
||||
|
||||
inline double getCurrentTimeUnit() { return time_unit_ms_; }
|
||||
inline std::string getLastSequence() { return last_sequence_; }
|
||||
|
||||
private:
|
||||
std::string current_sequence_ = "";
|
||||
std::string last_sequence_ = "";
|
||||
double time_unit_ms_ = 119.0;
|
||||
double last_confidence_ = 0.0;
|
||||
MorseRingBuffer pulse_history_{};
|
||||
MorseRingBuffer pulse_gaps_{};
|
||||
|
||||
std::string lookupMorse(const std::string& seq) {
|
||||
for (size_t i = 0; i < morse_table_size_; i++) {
|
||||
if (seq == morse_table_[i].code)
|
||||
return morse_table_[i].letter;
|
||||
}
|
||||
return "{" + seq + "}"; // not found
|
||||
}
|
||||
|
||||
double getDahProbability(uint32_t duration_ms) {
|
||||
double start_interp = 1.5 * time_unit_ms_;
|
||||
double end_interp = 2.5 * time_unit_ms_;
|
||||
|
||||
if (duration_ms <= start_interp) return 0.0;
|
||||
if (duration_ms >= end_interp) return 1.0;
|
||||
return ((double)(duration_ms)-start_interp) / (end_interp - start_interp);
|
||||
}
|
||||
|
||||
size_t findDecisionBoundary(uint32_t* sorted_data, size_t sorted_data_size) {
|
||||
if (sorted_data_size < 4) return 0;
|
||||
|
||||
size_t best_split_index = 0;
|
||||
uint32_t max_diff = 0;
|
||||
|
||||
for (size_t i = 1; i < sorted_data_size; ++i) {
|
||||
uint32_t diff = sorted_data[i] - sorted_data[i - 1];
|
||||
if (diff > sorted_data[i - 1] * 0.5 && diff > max_diff) {
|
||||
max_diff = diff;
|
||||
best_split_index = i;
|
||||
}
|
||||
}
|
||||
return best_split_index;
|
||||
}
|
||||
|
||||
bool calculatePulseUnit(double& unit, double& confidence) {
|
||||
if (pulse_history_.size() < 10) return false;
|
||||
uint32_t sorted_pulses[pulse_history_.size()];
|
||||
pulse_history_.copy_to_array(sorted_pulses);
|
||||
sort_uint32(sorted_pulses, pulse_history_.size());
|
||||
|
||||
size_t split_index = findDecisionBoundary(sorted_pulses, pulse_history_.size());
|
||||
if (split_index == 0 || split_index < 3 || (pulse_history_.size() - split_index) < 2) {
|
||||
return false;
|
||||
}
|
||||
double dit_sum = sum_uint32_range(sorted_pulses, 0, split_index);
|
||||
double dah_sum = sum_uint32_range(sorted_pulses, split_index, pulse_history_.size());
|
||||
|
||||
double avg_dit = dit_sum / split_index;
|
||||
double avg_dah = dah_sum / (pulse_history_.size() - split_index);
|
||||
if (avg_dah <= avg_dit) return false;
|
||||
|
||||
double ratio = avg_dah / avg_dit;
|
||||
if (ratio > 1.5 && ratio < 5.0) {
|
||||
unit = avg_dit;
|
||||
double tmpabs = ratio - 3.0;
|
||||
if (tmpabs < 0) tmpabs *= -1;
|
||||
tmpabs /= 3.0;
|
||||
tmpabs = 1.0 - tmpabs;
|
||||
if (tmpabs < 0) tmpabs = 0;
|
||||
confidence = tmpabs; // 0..1
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool calculateGapUnit(double& unit, double& confidence) {
|
||||
if (pulse_gaps_.size() < 10) return false;
|
||||
double threshold = getInterElementThreshold();
|
||||
double valid_gaps[pulse_gaps_.size()];
|
||||
size_t valid_count = 0;
|
||||
for (size_t i = 0; i < pulse_gaps_.size(); i++) {
|
||||
double gap = pulse_gaps_[i];
|
||||
if (gap <= threshold) {
|
||||
valid_gaps[valid_count++] = gap;
|
||||
}
|
||||
}
|
||||
if (valid_count < 2) {
|
||||
return false;
|
||||
}
|
||||
double sum = sum_double_range(valid_gaps, 0, valid_count);
|
||||
size_t count_to_average = valid_count;
|
||||
|
||||
if (count_to_average > 0) {
|
||||
unit = sum / count_to_average;
|
||||
confidence = 0.8;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
double sum_uint32_range(const uint32_t* data, size_t start, size_t end) {
|
||||
double sum = 0.0;
|
||||
for (size_t i = start; i < end; i++) {
|
||||
sum += data[i];
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
double sum_double_range(const double* data, size_t start, size_t end) {
|
||||
double sum = 0.0;
|
||||
for (size_t i = start; i < end; i++) {
|
||||
sum += data[i];
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
void sort_uint32(uint32_t* data, size_t size) {
|
||||
if (size < 2)
|
||||
return;
|
||||
|
||||
for (size_t i = 1; i < size; i++) {
|
||||
uint32_t key = data[i];
|
||||
size_t j = i;
|
||||
|
||||
while (j > 0 && data[j - 1] > key) {
|
||||
data[j] = data[j - 1];
|
||||
j--;
|
||||
}
|
||||
|
||||
data[j] = key;
|
||||
}
|
||||
}
|
||||
|
||||
double clamp_double(double value, double min_val, double max_val) {
|
||||
if (value < min_val)
|
||||
return min_val;
|
||||
else if (value > max_val)
|
||||
return max_val;
|
||||
else
|
||||
return value;
|
||||
}
|
||||
|
||||
void updateLearning() {
|
||||
double pulse_unit = -1.0, pulse_confidence = 0.0;
|
||||
double gap_unit = -1.0, gap_confidence = 0.0;
|
||||
|
||||
bool pulse_success = calculatePulseUnit(pulse_unit, pulse_confidence);
|
||||
bool gap_success = calculateGapUnit(gap_unit, gap_confidence);
|
||||
|
||||
double new_time_unit = -1.0;
|
||||
if (pulse_success && pulse_confidence > 0.5) {
|
||||
new_time_unit = pulse_unit;
|
||||
} else if (pulse_success && gap_success) {
|
||||
gap_confidence = 0.2;
|
||||
double total_confidence = pulse_confidence + gap_confidence;
|
||||
new_time_unit = (pulse_unit * pulse_confidence + gap_unit * gap_confidence) / total_confidence;
|
||||
} else if (gap_success) {
|
||||
new_time_unit = gap_unit;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
double max_change = time_unit_ms_ * 0.25;
|
||||
new_time_unit = clamp_double(new_time_unit, time_unit_ms_ - max_change, time_unit_ms_ + max_change);
|
||||
double DEFAULT_TIME_UNIT = 160.0;
|
||||
double BASE_LEARNING_RATE = 0.05;
|
||||
double MAX_LEARNING_RATE = 0.25;
|
||||
double tudeltaabs = new_time_unit - DEFAULT_TIME_UNIT;
|
||||
if (tudeltaabs < 0) tudeltaabs *= -1;
|
||||
double deviation_from_default = tudeltaabs / DEFAULT_TIME_UNIT;
|
||||
double tpp = deviation_from_default * 2.0;
|
||||
if (tpp > 1) tpp = 1;
|
||||
double learning_factor = BASE_LEARNING_RATE + (MAX_LEARNING_RATE - BASE_LEARNING_RATE) * tpp;
|
||||
|
||||
time_unit_ms_ = (time_unit_ms_ * (1.0 - learning_factor)) + (new_time_unit * learning_factor);
|
||||
}
|
||||
|
||||
size_t morse_table_size_ = 50;
|
||||
MorseEntry morse_table_[50] = {
|
||||
{".-", "A"},
|
||||
{"-...", "B"},
|
||||
{"-.-.", "C"},
|
||||
{"-..", "D"},
|
||||
{".", "E"},
|
||||
{"..-.", "F"},
|
||||
{"--.", "G"},
|
||||
{"....", "H"},
|
||||
{"..", "I"},
|
||||
{".---", "J"},
|
||||
{"-.-", "K"},
|
||||
{".-..", "L"},
|
||||
{"--", "M"},
|
||||
{"-.", "N"},
|
||||
{"---", "O"},
|
||||
{".--.", "P"},
|
||||
{"--.-", "Q"},
|
||||
{".-.", "R"},
|
||||
{"...", "S"},
|
||||
{"-", "T"},
|
||||
{"..-", "U"},
|
||||
{"...-", "V"},
|
||||
{".--", "W"},
|
||||
{"-..-", "X"},
|
||||
{"-.--", "Y"},
|
||||
{"--..", "Z"},
|
||||
{".----", "1"},
|
||||
{"..---", "2"},
|
||||
{"...--", "3"},
|
||||
{"....-", "4"},
|
||||
{".....", "5"},
|
||||
{"-....", "6"},
|
||||
{"--...", "7"},
|
||||
{"---..", "8"},
|
||||
{"----.", "9"},
|
||||
{"-----", "0"},
|
||||
{".-.-.-", "."},
|
||||
{"..--..", "?"},
|
||||
{"-.-.--", "!"},
|
||||
{"--..--", ","},
|
||||
{"-...-", "="},
|
||||
{"-..-.", "/"},
|
||||
{".--.-.", "@"},
|
||||
{"---...", ":"},
|
||||
{"-....-", "-"},
|
||||
{".----.", "'"},
|
||||
{".-..-.", "\""},
|
||||
{"-.--.", "("},
|
||||
{"-.--.-", ")"},
|
||||
{".-.-.", "+"}};
|
||||
};
|
||||
|
||||
} // namespace ui::external_app::morse_radio
|
||||
|
||||
#endif // __MORSEDECODER_HPP__
|
||||
@@ -0,0 +1,282 @@
|
||||
#include "ui_morse_radio.hpp"
|
||||
#include "portapack_persistent_memory.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
|
||||
namespace ui::external_app::morse_radio {
|
||||
|
||||
MorseRadioView::MorseRadioView(ui::NavigationView& nav)
|
||||
: nav_(nav) {
|
||||
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
|
||||
add_children({&rssi,
|
||||
&field_rf_amp,
|
||||
&field_lna,
|
||||
&field_vga,
|
||||
&field_volume,
|
||||
&field_frequency,
|
||||
&txt_last,
|
||||
&txt_speed,
|
||||
&txt_freq,
|
||||
&txt_clip,
|
||||
&options_mode,
|
||||
&btn_clear,
|
||||
&console_text,
|
||||
&labels,
|
||||
&chk_log,
|
||||
&field_squelch});
|
||||
field_frequency.set_step(100);
|
||||
|
||||
btn_clear.on_select = [this](Button&) {
|
||||
console_text.clear(true);
|
||||
txt_last.set("");
|
||||
time_stamp = false;
|
||||
if (logger && save_log)
|
||||
logger->init_daily_log(logs_dir);
|
||||
};
|
||||
|
||||
field_squelch.on_change = [this](int32_t v) {
|
||||
receiver_model.set_squelch_level(v);
|
||||
};
|
||||
|
||||
options_mode.on_change = [this](size_t, int32_t mode) {
|
||||
audio::output::stop();
|
||||
receiver_model.disable();
|
||||
morse_decoder_.resetLearning();
|
||||
if (mode == MORSE_NFM) {
|
||||
receiver_model.set_am_configuration(4);
|
||||
receiver_model.set_modulation(ReceiverModel::Mode::NarrowbandFMAudio);
|
||||
field_squelch.set_style(Theme::getInstance()->option_active);
|
||||
field_squelch.set_focusable(true);
|
||||
field_squelch.set_value(receiver_model.squelch_level());
|
||||
audio::set_rate(audio::Rate::Hz_24000);
|
||||
} else {
|
||||
audio::set_rate(audio::Rate::Hz_12000);
|
||||
receiver_model.set_modulation(ReceiverModel::Mode::AMAudio);
|
||||
if (mode == MORSE_AM_CW || mode == MORSE_AM_DSB)
|
||||
receiver_model.set_am_configuration(7);
|
||||
else if (mode == MORSE_AM_USB)
|
||||
receiver_model.set_am_configuration(9);
|
||||
else // LSB
|
||||
receiver_model.set_am_configuration(10);
|
||||
field_squelch.set_style(Theme::getInstance()->fg_dark);
|
||||
field_squelch.set_focusable(false);
|
||||
}
|
||||
baseband::set_moreserx_config(mode);
|
||||
saved_mode = mode;
|
||||
|
||||
audio::output::start();
|
||||
receiver_model.set_headphone_volume(receiver_model.headphone_volume()); // WM8731 hack.
|
||||
|
||||
receiver_model.set_sampling_rate(3072000);
|
||||
receiver_model.set_baseband_bandwidth(1750000);
|
||||
receiver_model.enable();
|
||||
};
|
||||
options_mode.set_selected_index(saved_mode);
|
||||
|
||||
logger = std::make_unique<MorseLogger>();
|
||||
chk_log.on_select = [this](Checkbox&, bool save) {
|
||||
save_log = save;
|
||||
if (logger && save_log)
|
||||
logger->init_daily_log(logs_dir);
|
||||
};
|
||||
}
|
||||
|
||||
void MorseLogger::init_daily_log(const std::filesystem::path& log_dir) {
|
||||
auto now = rtc_time::now();
|
||||
std::string pattern = "morse_";
|
||||
uint16_t y = now.year();
|
||||
pattern += (char)('0' + (y / 1000) % 10);
|
||||
pattern += (char)('0' + (y / 100) % 10);
|
||||
pattern += (char)('0' + (y / 10) % 10);
|
||||
pattern += (char)('0' + (y % 10));
|
||||
|
||||
uint8_t m = now.month();
|
||||
pattern += (char)('0' + (m / 10) % 10);
|
||||
pattern += (char)('0' + (m % 10));
|
||||
|
||||
uint8_t d = now.day();
|
||||
pattern += (char)('0' + (d / 10) % 10);
|
||||
pattern += (char)('0' + (d % 10));
|
||||
|
||||
pattern += "_???.TXT";
|
||||
|
||||
auto full_pattern_path = log_dir / pattern;
|
||||
auto final_path = next_filename_matching_pattern(full_pattern_path);
|
||||
if (!final_path.empty()) {
|
||||
this->append(final_path);
|
||||
}
|
||||
}
|
||||
|
||||
void MorseLogger::radio_set_log(const std::string& morse_mode) {
|
||||
int64_t freq = receiver_model.target_frequency();
|
||||
std::string header = "Freq:" + to_string_rounded_freq(freq, 4);
|
||||
header += "MHz, ";
|
||||
header += "RX MODE:" + morse_mode;
|
||||
header += "\r\nMessage:";
|
||||
log_file.write_raw(header);
|
||||
}
|
||||
|
||||
bool MorseLogger::on_packet(const std::string& content, bool time, const std::string& morse_mode) {
|
||||
if (!time) {
|
||||
log_file.write_raw_no_newline("\r\n\r\n");
|
||||
auto timestamp = to_string_datetime(rtc_time::now(), YMDHMS);
|
||||
log_file.write_raw("[" + timestamp + "] ");
|
||||
radio_set_log(morse_mode);
|
||||
char_count = 0;
|
||||
time = true;
|
||||
}
|
||||
|
||||
if (content.empty()) return time;
|
||||
std::string s;
|
||||
for (char c : content) {
|
||||
s = c;
|
||||
if ((char_count >= 35 && c == ' ') || (char_count >= 47)) {
|
||||
log_file.write_raw_no_newline("\r\n");
|
||||
|
||||
if (c != ' ') {
|
||||
log_file.write_raw_no_newline(s);
|
||||
char_count = 1;
|
||||
} else {
|
||||
char_count = 0;
|
||||
}
|
||||
} else {
|
||||
log_file.write_raw_no_newline(s);
|
||||
char_count++;
|
||||
}
|
||||
}
|
||||
|
||||
return time;
|
||||
}
|
||||
|
||||
MorseRadioView::~MorseRadioView() {
|
||||
receiver_model.disable();
|
||||
baseband::shutdown();
|
||||
audio::output::stop();
|
||||
}
|
||||
|
||||
void MorseRadioView::focus() {
|
||||
field_frequency.focus();
|
||||
txt_clip.set_style(Theme::getInstance()->fg_red);
|
||||
txt_clip.hidden(true);
|
||||
}
|
||||
|
||||
void MorseRadioView::check_for_timeout() {
|
||||
uint64_t now = chTimeNow();
|
||||
if (last_activity_time == 0) {
|
||||
last_activity_time = now;
|
||||
return;
|
||||
}
|
||||
uint64_t quiet_duration = now - last_activity_time;
|
||||
if (quiet_duration >= 60000) {
|
||||
morse_decoder_.resetLearning();
|
||||
time_stamp = false;
|
||||
last_activity_time = now;
|
||||
}
|
||||
}
|
||||
|
||||
int32_t MorseRadioView::ProcessSignal(int32_t sig_time_us) {
|
||||
int8_t sign = (sig_time_us > 0) ? 1 : ((sig_time_us < 0) ? -1 : 0);
|
||||
int32_t result = 0;
|
||||
|
||||
if (last_sign_ == 0) {
|
||||
last_sign_ = sign;
|
||||
accumulator_us_ = sig_time_us;
|
||||
long_pause_sent_ = false;
|
||||
return 0;
|
||||
}
|
||||
if (sign == last_sign_) {
|
||||
accumulator_us_ += sig_time_us;
|
||||
|
||||
if (sign < 0) {
|
||||
int32_t threshold_us = morse_decoder_.getInterWordThreshold() * 1000;
|
||||
if (!long_pause_sent_ && accumulator_us_ <= -threshold_us) {
|
||||
result = accumulator_us_ / 1000;
|
||||
long_pause_sent_ = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// state change
|
||||
if (!long_pause_sent_) {
|
||||
result = accumulator_us_ / 1000;
|
||||
} else {
|
||||
result = 0;
|
||||
}
|
||||
accumulator_us_ = sig_time_us;
|
||||
last_sign_ = sign;
|
||||
long_pause_sent_ = false;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void MorseRadioView::on_data(const MorseRXDataMessage* message) {
|
||||
int32_t r;
|
||||
|
||||
txt_clip.hidden(!message->clipped);
|
||||
|
||||
for (uint8_t i = 0; i <= message->state_cnt; ++i) {
|
||||
r = ProcessSignal(message->state_durations[i]);
|
||||
auto result = morse_decoder_.handleInput((r != 0) ? r : 0);
|
||||
if (result.isValid()) {
|
||||
last_activity_time = chTimeNow(); // start reset timer on valid input
|
||||
writeCharToConsole(result.text, result.confidence);
|
||||
if (logger && save_log) {
|
||||
time_stamp = logger->on_packet(result.text, time_stamp, options_mode.selected_index_name());
|
||||
}
|
||||
float dah_time = morse_decoder_.getCurrentTimeUnit() * 3.0f;
|
||||
if (dah_time > 0) {
|
||||
uint16_t wpm = (uint16_t)(3600.0f / (dah_time + 18.0f) + 0.5f);
|
||||
txt_speed.set(to_string_dec_uint(wpm));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MorseRadioView::on_freq(const MorseRXfreqMessage* message) {
|
||||
std::string ret = " ";
|
||||
uint32_t freq = message->measured_frequency;
|
||||
if (freq < 301) {
|
||||
ret = "<";
|
||||
freq = 300;
|
||||
}
|
||||
if (freq > 2299) {
|
||||
freq = 2300;
|
||||
ret = ">";
|
||||
}
|
||||
if (freq < 400 || freq > 1400)
|
||||
txt_freq.set_style(Theme::getInstance()->fg_red);
|
||||
else if (freq <= 580 || freq >= 1220)
|
||||
txt_freq.set_style(Theme::getInstance()->fg_yellow);
|
||||
else
|
||||
txt_freq.set_style(Theme::getInstance()->fg_green);
|
||||
ret += to_string_dec_uint(freq);
|
||||
txt_freq.set(ret);
|
||||
}
|
||||
|
||||
void MorseRadioView::writeCharToConsole(const std::string& ch, double confidence) {
|
||||
if (ch.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
txt_last.set(morse_decoder_.getLastSequence().c_str());
|
||||
|
||||
last_color_id = color_id;
|
||||
std::string color = "";
|
||||
|
||||
if (ch == " ") {
|
||||
color_id = 0;
|
||||
} else if (ch[0] == '{') { // no match
|
||||
color_id = 0;
|
||||
} else {
|
||||
if (confidence < 0.8)
|
||||
color_id = 1;
|
||||
else if (confidence < 0.9)
|
||||
color_id = 2;
|
||||
else
|
||||
color_id = 3;
|
||||
}
|
||||
color = arr_color[color_id];
|
||||
last_color_id = color_id;
|
||||
console_text.write(color + ch);
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::morse_radio
|
||||
@@ -0,0 +1,188 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Pezsma
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef __MORSE_RADIO_H__
|
||||
#define __MORSE_RADIO_H__
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "ui_widget.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "external_app.hpp"
|
||||
#include "ui_freq_field.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "portapack.hpp"
|
||||
#include "message.hpp"
|
||||
#include "audio.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
#include "receiver_model.hpp"
|
||||
#include "tone_key.hpp"
|
||||
#include "log_file.hpp"
|
||||
#include "file_path.hpp"
|
||||
#include "file.hpp"
|
||||
#include "rtc_time.hpp"
|
||||
#include "morsedecoder.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace ui::external_app::morse_radio {
|
||||
|
||||
class MorseRadioView;
|
||||
|
||||
class MorseLogger {
|
||||
public:
|
||||
Optional<File::Error> append(const std::filesystem::path& filename) {
|
||||
return log_file.append(filename);
|
||||
}
|
||||
|
||||
void init_daily_log(const std::filesystem::path& log_dir);
|
||||
bool on_packet(const std::string& content, bool time, const std::string& morse_mode);
|
||||
void radio_set_log(const std::string& morse_mode);
|
||||
|
||||
private:
|
||||
LogFile log_file{};
|
||||
uint8_t char_count{0}; // log file line break
|
||||
};
|
||||
|
||||
class MorseRadioView : public ui::View {
|
||||
public:
|
||||
MorseRadioView(ui::NavigationView& nav);
|
||||
~MorseRadioView();
|
||||
std::string title() const override {
|
||||
return "Morse";
|
||||
}
|
||||
void focus() override;
|
||||
|
||||
private:
|
||||
void writeCharToConsole(const std::string& ch, double confidence);
|
||||
void on_data(const MorseRXDataMessage* message);
|
||||
void on_freq(const MorseRXfreqMessage* message);
|
||||
void on_message(const Message* const p);
|
||||
void check_for_timeout();
|
||||
int32_t ProcessSignal(int32_t sig_time_us);
|
||||
ui::NavigationView& nav_;
|
||||
MorseDecoder morse_decoder_{};
|
||||
RxRadioState radio_state_{};
|
||||
std::unique_ptr<MorseLogger> logger{};
|
||||
|
||||
enum morse_modes : uint8_t {
|
||||
MORSE_AM_CW = 0,
|
||||
MORSE_NFM,
|
||||
MORSE_AM_DSB,
|
||||
MORSE_AM_USB,
|
||||
MORSE_AM_LSB,
|
||||
};
|
||||
uint8_t saved_mode = MORSE_AM_CW;
|
||||
|
||||
app_settings::SettingsManager settings_{
|
||||
"rx_morse_radio",
|
||||
app_settings::Mode::RX,
|
||||
{
|
||||
{"cwmode"sv, &saved_mode},
|
||||
}};
|
||||
|
||||
RxFrequencyField field_frequency{
|
||||
{UI_POS_X(0), UI_POS_Y(0)},
|
||||
nav_};
|
||||
RFAmpField field_rf_amp{
|
||||
{UI_POS_X(13), UI_POS_Y(0)}};
|
||||
LNAGainField field_lna{
|
||||
{UI_POS_X(15), UI_POS_Y(0)}};
|
||||
VGAGainField field_vga{
|
||||
{UI_POS_X(18), UI_POS_Y(0)}};
|
||||
RSSI rssi{
|
||||
{UI_POS_X(21), UI_POS_Y(0), UI_POS_WIDTH_REMAINING(24), 4}};
|
||||
|
||||
AudioVolumeField field_volume{
|
||||
{UI_POS_X_RIGHT(2), UI_POS_Y(0)}};
|
||||
|
||||
NumberField field_squelch{
|
||||
{UI_POS_X(10), UI_POS_Y(1)},
|
||||
2,
|
||||
{0, 99},
|
||||
1,
|
||||
' ',
|
||||
};
|
||||
ui::Text txt_speed{{UI_POS_X(23), UI_POS_Y(1), UI_POS_WIDTH(3), UI_POS_HEIGHT(1)}, "??"};
|
||||
ui::Text txt_last{{UI_POS_X(12), UI_POS_Y(4), UI_POS_WIDTH_REMAINING(12), UI_POS_HEIGHT(1)}, ""};
|
||||
ui::Text txt_freq{{UI_POS_X(21), UI_POS_Y(2), UI_POS_WIDTH(5), UI_POS_HEIGHT(1)}, "??"};
|
||||
ui::Text txt_clip{{UI_POS_X(15), UI_POS_Y(3), UI_POS_WIDTH(5), UI_POS_HEIGHT(1)}, "clipping"};
|
||||
ui::Console console_text{{UI_POS_X(0), UI_POS_Y(5), UI_POS_MAXWIDTH, UI_POS_HEIGHT_REMAINING(7)}};
|
||||
ui::Labels labels{
|
||||
{{UI_POS_X(0), UI_POS_Y(1)}, "Squelch:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(15), UI_POS_Y(1)}, "Speed:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(26), UI_POS_Y(1)}, "wpm", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(4)}, "Last seq.:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(15), UI_POS_Y(2)}, "Tone:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(27), UI_POS_Y(2)}, "Hz", Theme::getInstance()->fg_light->foreground}};
|
||||
|
||||
ui::OptionsField options_mode{
|
||||
{UI_POS_X(8), UI_POS_Y(2) + 4}, // +4 to align with 'Log' checkbox text
|
||||
6,
|
||||
{
|
||||
{"AM/CW", MORSE_AM_CW},
|
||||
{"NFM", MORSE_NFM},
|
||||
{"AM/DSB", MORSE_AM_DSB},
|
||||
{"AM/USB", MORSE_AM_USB},
|
||||
{"AM/LSB", MORSE_AM_LSB},
|
||||
}};
|
||||
|
||||
Checkbox chk_log{{UI_POS_X(0), UI_POS_Y(2)}, 12, "Log", false};
|
||||
ui::Button btn_clear{{UI_POS_X(0), UI_POS_Y_BOTTOM(2), UI_POS_WIDTH(6), UI_POS_HEIGHT(1)}, "CLR"};
|
||||
|
||||
std::string arr_color[4] = {STR_COLOR_WHITE, STR_COLOR_RED, STR_COLOR_YELLOW, STR_COLOR_GREEN};
|
||||
|
||||
bool long_pause_sent_{false};
|
||||
bool save_log{false};
|
||||
bool reset_triggered_{false};
|
||||
bool time_stamp{false};
|
||||
uint8_t last_color_id{255};
|
||||
uint8_t color_id{255};
|
||||
int8_t last_sign_{0};
|
||||
uint8_t space_timer{0};
|
||||
int32_t accumulator_us_{0};
|
||||
uint64_t last_activity_time{0};
|
||||
|
||||
MessageHandlerRegistration message_handler_packet{
|
||||
Message::ID::MorseRXData,
|
||||
[this](Message* const p) {
|
||||
const auto message = static_cast<const MorseRXDataMessage*>(p);
|
||||
this->on_data(message);
|
||||
}};
|
||||
|
||||
MessageHandlerRegistration message_handler_freq{
|
||||
Message::ID::MorseRXfreq,
|
||||
[this](Message* const p) {
|
||||
const auto message = static_cast<const MorseRXfreqMessage*>(p);
|
||||
this->on_freq(message);
|
||||
}};
|
||||
|
||||
MessageHandlerRegistration message_handler_framesync{
|
||||
Message::ID::DisplayFrameSync,
|
||||
[this](const Message* const p) {
|
||||
(void)p;
|
||||
this->check_for_timeout();
|
||||
}};
|
||||
};
|
||||
|
||||
} // namespace ui::external_app::morse_radio
|
||||
|
||||
#endif // __MORSE_RADIO_H__
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Pezsma
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "ui_morse_radiotx.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "external_app.hpp"
|
||||
|
||||
namespace ui::external_app::morseradiotx {
|
||||
|
||||
void initialize_app(NavigationView& nav) {
|
||||
nav.push<MorseRadiotxView>();
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::morseradiotx
|
||||
|
||||
extern "C" {
|
||||
|
||||
__attribute__((section(".external_app.app_morseradiotx.application_information"), used))
|
||||
application_information_t _application_information_morseradiotx = {
|
||||
/*.memory_location = */ (uint8_t*)0x00000000,
|
||||
/*.externalAppEntry = */ ui::external_app::morseradiotx::initialize_app,
|
||||
/*.header_version = */ CURRENT_HEADER_VERSION,
|
||||
/*.app_version = */ VERSION_MD5,
|
||||
|
||||
/*.app_name = */ "Morse TX",
|
||||
/*.bitmap_data = */ {
|
||||
0x00,
|
||||
0x00,
|
||||
0xFE,
|
||||
0x7F,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xBB,
|
||||
0xD0,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0x0B,
|
||||
0xE1,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xEB,
|
||||
0xD0,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFE,
|
||||
0x7F,
|
||||
0x70,
|
||||
0x00,
|
||||
0x30,
|
||||
0x00,
|
||||
0x10,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
},
|
||||
/*.icon_color = */ ui::Color::yellow().v,
|
||||
/*.menu_location = */ app_location_t::TX,
|
||||
/*.desired_menu_position = */ -1,
|
||||
|
||||
/*.m4_app_tag = portapack::spi_flash::image_tag_none */ {'P', 'M', 'R', 'T'},
|
||||
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
|
||||
};
|
||||
|
||||
} // extern "C"
|
||||
@@ -0,0 +1,389 @@
|
||||
#ifndef __MORSEDECODER_HPP__
|
||||
#define __MORSEDECODER_HPP__
|
||||
|
||||
/*
|
||||
* Copyright (C) 2025 Pezsma
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include "string_format.hpp"
|
||||
|
||||
namespace ui::external_app::morseradiotx {
|
||||
|
||||
class MorseRingBuffer {
|
||||
public:
|
||||
MorseRingBuffer()
|
||||
: head_(0), tail_(0), count_(0) {}
|
||||
|
||||
void push_back(const uint32_t& value) {
|
||||
data_[head_] = value;
|
||||
head_ = (head_ + 1) % 40;
|
||||
if (count_ < 40) {
|
||||
count_++;
|
||||
} else {
|
||||
// overwrite oldest element
|
||||
tail_ = (tail_ + 1) % 40;
|
||||
}
|
||||
}
|
||||
|
||||
void pop_front() {
|
||||
if (count_ > 0) {
|
||||
tail_ = (tail_ + 1) % 40;
|
||||
count_--;
|
||||
}
|
||||
}
|
||||
|
||||
size_t size() const { return count_; }
|
||||
bool empty() const { return count_ == 0; }
|
||||
const uint32_t& front() const { return data_[tail_]; }
|
||||
// Access by index (0 = oldest)
|
||||
uint32_t operator[](size_t idx) const {
|
||||
return data_[(tail_ + idx) % 40];
|
||||
}
|
||||
// Convert to vector-like access for sorting etc.
|
||||
void copy_to_array(uint32_t* out) const {
|
||||
for (size_t i = 0; i < count_; ++i)
|
||||
out[i] = (*this)[i];
|
||||
}
|
||||
|
||||
void clear() {
|
||||
head_ = 0;
|
||||
tail_ = 0;
|
||||
count_ = 0;
|
||||
}
|
||||
|
||||
private:
|
||||
uint32_t data_[40];
|
||||
size_t head_;
|
||||
size_t tail_;
|
||||
size_t count_;
|
||||
};
|
||||
|
||||
class MorseDecoder {
|
||||
public:
|
||||
struct DecodeResult {
|
||||
std::string text = "";
|
||||
double confidence = 0.0;
|
||||
|
||||
bool isValid() const {
|
||||
return !text.empty();
|
||||
}
|
||||
};
|
||||
|
||||
struct MorseEntry {
|
||||
std::string code;
|
||||
std::string letter;
|
||||
};
|
||||
|
||||
MorseDecoder() {}
|
||||
|
||||
void resetLearning() {
|
||||
time_unit_ms_ = 119.0;
|
||||
current_sequence_ = "";
|
||||
last_sequence_ = "";
|
||||
last_confidence_ = 0.0;
|
||||
pulse_history_.clear();
|
||||
pulse_gaps_.clear();
|
||||
}
|
||||
|
||||
const char* get_morse_pattern(char c) {
|
||||
char upper_c = (c >= 'a' && c <= 'z') ? c - 32 : c;
|
||||
|
||||
for (size_t i = 0; i < morse_table_size_; ++i) {
|
||||
if (morse_table_[i].letter[0] == upper_c) {
|
||||
return morse_table_[i].code.c_str();
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
DecodeResult
|
||||
handleInput(int32_t duration_ms) {
|
||||
DecodeResult result = {"", 0.0};
|
||||
|
||||
if (duration_ms < 5 && duration_ms > -5) return result;
|
||||
|
||||
if (duration_ms > 0) {
|
||||
pulse_history_.push_back(duration_ms);
|
||||
double dah_prob = getDahProbability(duration_ms);
|
||||
current_sequence_ += (dah_prob > 0.5) ? '-' : '.';
|
||||
last_confidence_ = (dah_prob > 0.5) ? dah_prob : (1.0 - dah_prob);
|
||||
last_sequence_ = current_sequence_;
|
||||
|
||||
} else {
|
||||
uint32_t gap_duration = -duration_ms;
|
||||
pulse_gaps_.push_back(gap_duration);
|
||||
|
||||
if (gap_duration >= getInterCharThreshold() && !current_sequence_.empty()) {
|
||||
result.text = lookupMorse(current_sequence_);
|
||||
|
||||
result.confidence = (result.text[0] != '{') ? last_confidence_ : 0.0;
|
||||
if (gap_duration >= getInterWordThreshold()) {
|
||||
result.text += " ";
|
||||
}
|
||||
current_sequence_ = "";
|
||||
}
|
||||
}
|
||||
|
||||
updateLearning();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
inline double getInterElementThreshold() { return time_unit_ms_ * 0.8; }
|
||||
inline double getInterCharThreshold() { return time_unit_ms_ * 2.5; }
|
||||
inline double getInterWordThreshold() { return time_unit_ms_ * 6.0; }
|
||||
|
||||
inline double getCurrentTimeUnit() { return time_unit_ms_; }
|
||||
inline std::string getLastSequence() { return last_sequence_; }
|
||||
|
||||
private:
|
||||
std::string current_sequence_ = "";
|
||||
std::string last_sequence_ = "";
|
||||
double time_unit_ms_ = 119.0;
|
||||
double last_confidence_ = 0.0;
|
||||
MorseRingBuffer pulse_history_{};
|
||||
MorseRingBuffer pulse_gaps_{};
|
||||
|
||||
std::string lookupMorse(const std::string& seq) {
|
||||
for (size_t i = 0; i < morse_table_size_; i++) {
|
||||
if (seq == morse_table_[i].code)
|
||||
return morse_table_[i].letter;
|
||||
}
|
||||
return "{" + seq + "}"; // not found
|
||||
}
|
||||
|
||||
double getDahProbability(uint32_t duration_ms) {
|
||||
double start_interp = 1.5 * time_unit_ms_;
|
||||
double end_interp = 2.5 * time_unit_ms_;
|
||||
|
||||
if (duration_ms <= start_interp) return 0.0;
|
||||
if (duration_ms >= end_interp) return 1.0;
|
||||
return ((double)(duration_ms)-start_interp) / (end_interp - start_interp);
|
||||
}
|
||||
|
||||
size_t findDecisionBoundary(uint32_t* sorted_data, size_t sorted_data_size) {
|
||||
if (sorted_data_size < 4) return 0;
|
||||
|
||||
size_t best_split_index = 0;
|
||||
uint32_t max_diff = 0;
|
||||
|
||||
for (size_t i = 1; i < sorted_data_size; ++i) {
|
||||
uint32_t diff = sorted_data[i] - sorted_data[i - 1];
|
||||
if (diff > sorted_data[i - 1] * 0.5 && diff > max_diff) {
|
||||
max_diff = diff;
|
||||
best_split_index = i;
|
||||
}
|
||||
}
|
||||
return best_split_index;
|
||||
}
|
||||
|
||||
bool calculatePulseUnit(double& unit, double& confidence) {
|
||||
if (pulse_history_.size() < 10) return false;
|
||||
uint32_t sorted_pulses[pulse_history_.size()];
|
||||
pulse_history_.copy_to_array(sorted_pulses);
|
||||
sort_uint32(sorted_pulses, pulse_history_.size());
|
||||
|
||||
size_t split_index = findDecisionBoundary(sorted_pulses, pulse_history_.size());
|
||||
if (split_index == 0 || split_index < 3 || (pulse_history_.size() - split_index) < 2) {
|
||||
return false;
|
||||
}
|
||||
double dit_sum = sum_uint32_range(sorted_pulses, 0, split_index);
|
||||
double dah_sum = sum_uint32_range(sorted_pulses, split_index, pulse_history_.size());
|
||||
|
||||
double avg_dit = dit_sum / split_index;
|
||||
double avg_dah = dah_sum / (pulse_history_.size() - split_index);
|
||||
if (avg_dah <= avg_dit) return false;
|
||||
|
||||
double ratio = avg_dah / avg_dit;
|
||||
if (ratio > 1.5 && ratio < 5.0) {
|
||||
unit = avg_dit;
|
||||
double tmpabs = ratio - 3.0;
|
||||
if (tmpabs < 0) tmpabs *= -1;
|
||||
tmpabs /= 3.0;
|
||||
tmpabs = 1.0 - tmpabs;
|
||||
if (tmpabs < 0) tmpabs = 0;
|
||||
confidence = tmpabs; // 0..1
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool calculateGapUnit(double& unit, double& confidence) {
|
||||
if (pulse_gaps_.size() < 10) return false;
|
||||
double threshold = getInterElementThreshold();
|
||||
double valid_gaps[pulse_gaps_.size()];
|
||||
size_t valid_count = 0;
|
||||
for (size_t i = 0; i < pulse_gaps_.size(); i++) {
|
||||
double gap = pulse_gaps_[i];
|
||||
if (gap <= threshold) {
|
||||
valid_gaps[valid_count++] = gap;
|
||||
}
|
||||
}
|
||||
if (valid_count < 2) {
|
||||
return false;
|
||||
}
|
||||
double sum = sum_double_range(valid_gaps, 0, valid_count);
|
||||
size_t count_to_average = valid_count;
|
||||
|
||||
if (count_to_average > 0) {
|
||||
unit = sum / count_to_average;
|
||||
confidence = 0.8;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
double sum_uint32_range(const uint32_t* data, size_t start, size_t end) {
|
||||
double sum = 0.0;
|
||||
for (size_t i = start; i < end; i++) {
|
||||
sum += data[i];
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
double sum_double_range(const double* data, size_t start, size_t end) {
|
||||
double sum = 0.0;
|
||||
for (size_t i = start; i < end; i++) {
|
||||
sum += data[i];
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
void sort_uint32(uint32_t* data, size_t size) {
|
||||
if (size < 2)
|
||||
return;
|
||||
|
||||
for (size_t i = 1; i < size; i++) {
|
||||
uint32_t key = data[i];
|
||||
size_t j = i;
|
||||
|
||||
while (j > 0 && data[j - 1] > key) {
|
||||
data[j] = data[j - 1];
|
||||
j--;
|
||||
}
|
||||
|
||||
data[j] = key;
|
||||
}
|
||||
}
|
||||
|
||||
double clamp_double(double value, double min_val, double max_val) {
|
||||
if (value < min_val)
|
||||
return min_val;
|
||||
else if (value > max_val)
|
||||
return max_val;
|
||||
else
|
||||
return value;
|
||||
}
|
||||
|
||||
void updateLearning() {
|
||||
double pulse_unit = -1.0, pulse_confidence = 0.0;
|
||||
double gap_unit = -1.0, gap_confidence = 0.0;
|
||||
|
||||
bool pulse_success = calculatePulseUnit(pulse_unit, pulse_confidence);
|
||||
bool gap_success = calculateGapUnit(gap_unit, gap_confidence);
|
||||
|
||||
double new_time_unit = -1.0;
|
||||
if (pulse_success && pulse_confidence > 0.5) {
|
||||
new_time_unit = pulse_unit;
|
||||
} else if (pulse_success && gap_success) {
|
||||
gap_confidence = 0.2;
|
||||
double total_confidence = pulse_confidence + gap_confidence;
|
||||
new_time_unit = (pulse_unit * pulse_confidence + gap_unit * gap_confidence) / total_confidence;
|
||||
} else if (gap_success) {
|
||||
new_time_unit = gap_unit;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
double max_change = time_unit_ms_ * 0.25;
|
||||
new_time_unit = clamp_double(new_time_unit, time_unit_ms_ - max_change, time_unit_ms_ + max_change);
|
||||
double DEFAULT_TIME_UNIT = 160.0;
|
||||
double BASE_LEARNING_RATE = 0.05;
|
||||
double MAX_LEARNING_RATE = 0.25;
|
||||
double tudeltaabs = new_time_unit - DEFAULT_TIME_UNIT;
|
||||
if (tudeltaabs < 0) tudeltaabs *= -1;
|
||||
double deviation_from_default = tudeltaabs / DEFAULT_TIME_UNIT;
|
||||
double tpp = deviation_from_default * 2.0;
|
||||
if (tpp > 1) tpp = 1;
|
||||
double learning_factor = BASE_LEARNING_RATE + (MAX_LEARNING_RATE - BASE_LEARNING_RATE) * tpp;
|
||||
|
||||
time_unit_ms_ = (time_unit_ms_ * (1.0 - learning_factor)) + (new_time_unit * learning_factor);
|
||||
}
|
||||
|
||||
size_t morse_table_size_ = 50;
|
||||
MorseEntry morse_table_[50] = {
|
||||
{".-", "A"},
|
||||
{"-...", "B"},
|
||||
{"-.-.", "C"},
|
||||
{"-..", "D"},
|
||||
{".", "E"},
|
||||
{"..-.", "F"},
|
||||
{"--.", "G"},
|
||||
{"....", "H"},
|
||||
{"..", "I"},
|
||||
{".---", "J"},
|
||||
{"-.-", "K"},
|
||||
{".-..", "L"},
|
||||
{"--", "M"},
|
||||
{"-.", "N"},
|
||||
{"---", "O"},
|
||||
{".--.", "P"},
|
||||
{"--.-", "Q"},
|
||||
{".-.", "R"},
|
||||
{"...", "S"},
|
||||
{"-", "T"},
|
||||
{"..-", "U"},
|
||||
{"...-", "V"},
|
||||
{".--", "W"},
|
||||
{"-..-", "X"},
|
||||
{"-.--", "Y"},
|
||||
{"--..", "Z"},
|
||||
{".----", "1"},
|
||||
{"..---", "2"},
|
||||
{"...--", "3"},
|
||||
{"....-", "4"},
|
||||
{".....", "5"},
|
||||
{"-....", "6"},
|
||||
{"--...", "7"},
|
||||
{"---..", "8"},
|
||||
{"----.", "9"},
|
||||
{"-----", "0"},
|
||||
{".-.-.-", "."},
|
||||
{"..--..", "?"},
|
||||
{"-.-.--", "!"},
|
||||
{"--..--", ","},
|
||||
{"-...-", "="},
|
||||
{"-..-.", "/"},
|
||||
{".--.-.", "@"},
|
||||
{"---...", ":"},
|
||||
{"-....-", "-"},
|
||||
{".----.", "'"},
|
||||
{".-..-.", "\""},
|
||||
{"-.--.", "("},
|
||||
{"-.--.-", ")"},
|
||||
{".-.-.", "+"}};
|
||||
};
|
||||
|
||||
} // namespace ui::external_app::morseradiotx
|
||||
|
||||
#endif // __MORSEDECODER_HPP__
|
||||
@@ -0,0 +1,437 @@
|
||||
#include "ui_morse_radiotx.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
|
||||
namespace ui::external_app::morseradiotx {
|
||||
|
||||
static msg_t tx_thread_fn(void* arg) {
|
||||
auto view = reinterpret_cast<MorseRadiotxView*>(arg);
|
||||
chRegSetThreadName("morse_tx_thread");
|
||||
view->transmit_morse_message();
|
||||
return 0;
|
||||
}
|
||||
|
||||
MorseRadiotxView::MorseRadiotxView(ui::NavigationView& nav)
|
||||
: current_timings(calculate_morse_timings(20)),
|
||||
nav_(nav) {
|
||||
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
|
||||
add_children({&tx_view,
|
||||
&field_volume,
|
||||
&labels,
|
||||
&options_mode,
|
||||
&tone_,
|
||||
&wpm_,
|
||||
&txt_msg,
|
||||
&btn_message,
|
||||
&btn_calls,
|
||||
&chk_trans,
|
||||
&bandwidth,
|
||||
&chk_callsgn,
|
||||
&txt_last,
|
||||
&console_text,
|
||||
&btn_clear,
|
||||
&btn_ptt});
|
||||
|
||||
initial_switch_config_ = get_switches_repeat_config();
|
||||
|
||||
SwitchesState config = initial_switch_config_;
|
||||
config[toUType(Switch::Sel)] = false;
|
||||
set_switches_repeat_config(config);
|
||||
|
||||
audio::set_rate(audio::Rate::Hz_24000);
|
||||
|
||||
btn_clear.on_select = [this](Button&) {
|
||||
console_text.clear(true);
|
||||
txt_last.set("");
|
||||
};
|
||||
|
||||
options_mode.on_change = [this](size_t, int32_t value) {
|
||||
current_mode = (uint8_t)value;
|
||||
baseband::set_morsetx_config(current_mode, tone, band);
|
||||
ui_toggle();
|
||||
};
|
||||
|
||||
tone_.on_change = [this](int32_t v) {
|
||||
tone = static_cast<uint32_t>(v);
|
||||
baseband::set_morsetx_config(current_mode, tone, band);
|
||||
};
|
||||
|
||||
wpm_.on_change = [this](int16_t v) {
|
||||
wpm = v;
|
||||
current_timings = calculate_morse_timings(wpm);
|
||||
};
|
||||
|
||||
bandwidth.on_change = [this](float v) {
|
||||
band = v;
|
||||
baseband::set_morsetx_config(current_mode, tone, band);
|
||||
};
|
||||
|
||||
options_mode.set_selected_index(current_mode, true);
|
||||
tone_.set_value(tone, true);
|
||||
wpm_.set_value(wpm, true);
|
||||
bandwidth.set_value(band, true);
|
||||
|
||||
btn_ptt.on_select = [this](Button&) {
|
||||
if (button_touch) {
|
||||
button_touch = false;
|
||||
return;
|
||||
}
|
||||
button_was_selected = true;
|
||||
onPress();
|
||||
};
|
||||
|
||||
btn_ptt.on_touch_press = [this](Button&) {
|
||||
button_touch = true;
|
||||
button_was_selected = false;
|
||||
onPress();
|
||||
};
|
||||
btn_ptt.on_touch_release = [this](Button&) {
|
||||
if (chk_trans.value() && transmit) {
|
||||
button_touch = true;
|
||||
button_was_selected = false;
|
||||
onRelease();
|
||||
}
|
||||
};
|
||||
|
||||
btn_message.on_select = [this, &nav](Button&) {
|
||||
if (!transmit)
|
||||
text_prompt(nav, msg_buffer, 27, ENTER_KEYBOARD_MODE_ALPHA, [this](std::string& buffer) {
|
||||
msg_buffer = buffer;
|
||||
txt_msg.set("[" + msg_buffer + "] ");
|
||||
});
|
||||
};
|
||||
|
||||
btn_calls.on_select = [this, &nav](Button&) {
|
||||
if (!transmit) {
|
||||
text_prompt(nav, call_sign, 10, ENTER_KEYBOARD_MODE_ALPHA, [this](std::string& buffer) {
|
||||
call_sign = buffer;
|
||||
if (call_sign.empty())
|
||||
btn_calls.set_text("call sign?");
|
||||
else
|
||||
btn_calls.set_text(call_sign);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
audio::output::start();
|
||||
auto vol = field_volume.value();
|
||||
field_volume.set_value(0);
|
||||
field_volume.set_value(vol);
|
||||
|
||||
tx_view.on_start = [this]() {
|
||||
if (!chk_trans.value() && msg_buffer.empty()) {
|
||||
tx_view.set_transmitting(false);
|
||||
return;
|
||||
}
|
||||
tx_view.focus();
|
||||
if (!tx_thread && !thread_running) {
|
||||
thread_running = true;
|
||||
tx_thread = chThdCreateFromHeap(NULL, 1024, NORMALPRIO + 5, tx_thread_fn, this);
|
||||
tx_view.set_transmitting(true);
|
||||
}
|
||||
|
||||
else
|
||||
tx_view.set_transmitting(false);
|
||||
};
|
||||
|
||||
tx_view.on_stop = [this]() {
|
||||
baseband::set_morsetx_key(false);
|
||||
transmitter_model.disable();
|
||||
|
||||
if (tx_thread) {
|
||||
chThdTerminate(tx_thread);
|
||||
chThdWait(tx_thread);
|
||||
}
|
||||
transmit = false;
|
||||
transmit_time = 0;
|
||||
tx_thread = nullptr;
|
||||
thread_running = false;
|
||||
tx_view.set_transmitting(false);
|
||||
ui_toggle();
|
||||
};
|
||||
|
||||
tx_view.on_edit_frequency = [this, &nav]() {
|
||||
auto new_view = nav.push<FrequencyKeypadView>(transmitter_model.target_frequency());
|
||||
new_view->on_changed = [this](rf::Frequency f) {
|
||||
transmitter_model.set_target_frequency(f);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
MorseRadiotxView::~MorseRadiotxView() {
|
||||
set_switches_repeat_config(initial_switch_config_);
|
||||
if (tx_thread) {
|
||||
chThdTerminate(tx_thread);
|
||||
chThdWait(tx_thread);
|
||||
if (!thread_running) tx_thread = nullptr;
|
||||
}
|
||||
|
||||
transmitter_model.disable();
|
||||
baseband::shutdown();
|
||||
audio::output::stop();
|
||||
}
|
||||
|
||||
void MorseRadiotxView::on_show() {
|
||||
ptt_button_visibility(true);
|
||||
start_time = 0;
|
||||
end_time = 0;
|
||||
transmit_time = 0;
|
||||
}
|
||||
|
||||
void MorseRadiotxView::focus() {
|
||||
options_mode.focus();
|
||||
}
|
||||
|
||||
MorseRadiotxView::MorseTimings MorseRadiotxView::calculate_morse_timings(uint32_t wpm) {
|
||||
MorseRadiotxView::MorseTimings t;
|
||||
if (wpm < 10) wpm = 10;
|
||||
if (wpm > 45) wpm = 45;
|
||||
t.dot_ms = 1200 / wpm;
|
||||
|
||||
t.dash_ms = 3 * t.dot_ms;
|
||||
t.symbol_gap = t.dot_ms;
|
||||
t.char_gap = 3 * t.dot_ms;
|
||||
t.word_gap = 7 * t.dot_ms;
|
||||
return t;
|
||||
}
|
||||
|
||||
void MorseRadiotxView::transmit_morse_message() {
|
||||
std::string full_message = "";
|
||||
// cal sign
|
||||
if (chk_callsgn.value() && !call_sign.empty()) {
|
||||
full_message = call_sign;
|
||||
if (!chk_trans.value() && !msg_buffer.empty()) {
|
||||
full_message += " " + msg_buffer;
|
||||
}
|
||||
} else {
|
||||
if (!chk_trans.value()) full_message = msg_buffer;
|
||||
}
|
||||
|
||||
if (full_message.empty() && !chk_trans.value()) {
|
||||
ptt_button_visibility(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// enable transmit
|
||||
if (!transmit) {
|
||||
transmit = true;
|
||||
transmitter_model.enable();
|
||||
ui_toggle();
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < full_message.length(); i++) {
|
||||
if (chThdShouldTerminate()) break;
|
||||
|
||||
char c = full_message[i];
|
||||
|
||||
std::string s_char(1, c);
|
||||
// space
|
||||
if (c == ' ') {
|
||||
console_text.write(" ");
|
||||
chThdSleepMilliseconds(current_timings.word_gap);
|
||||
continue;
|
||||
}
|
||||
|
||||
const char* pattern = morse_decoder_.get_morse_pattern(c);
|
||||
|
||||
if (pattern != nullptr) {
|
||||
txt_last.set(pattern);
|
||||
|
||||
// write blue char to console
|
||||
console_text.write(STR_COLOR_BLUE + s_char);
|
||||
|
||||
// Morze (dih/dah) send
|
||||
for (int j = 0; pattern[j] != '\0'; j++) {
|
||||
baseband::set_morsetx_key(true);
|
||||
if (pattern[j] == '.') {
|
||||
chThdSleepMilliseconds(current_timings.dot_ms);
|
||||
} else if (pattern[j] == '-') {
|
||||
chThdSleepMilliseconds(current_timings.dash_ms);
|
||||
}
|
||||
if (chThdShouldTerminate()) break;
|
||||
// pause between signs
|
||||
baseband::set_morsetx_key(false);
|
||||
chThdSleepMilliseconds(current_timings.symbol_gap);
|
||||
}
|
||||
if (chThdShouldTerminate()) break;
|
||||
if (current_timings.char_gap > current_timings.symbol_gap) {
|
||||
chThdSleepMilliseconds(current_timings.char_gap - current_timings.symbol_gap);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (chk_trans.value()) ptt_button_visibility(false);
|
||||
baseband::set_morsetx_key(false);
|
||||
transmit_time = chTimeNow();
|
||||
decode_timeout_calc = false;
|
||||
thread_running = false;
|
||||
}
|
||||
|
||||
void MorseRadiotxView::onPress() {
|
||||
start_time = chTimeNow();
|
||||
if (!transmit) {
|
||||
transmit = true;
|
||||
transmitter_model.enable();
|
||||
}
|
||||
baseband::set_morsetx_key(true);
|
||||
if (end_time != 0) {
|
||||
int64_t gap_delta = (chTimeNow() - end_time);
|
||||
auto result = morse_decoder_.handleInput(-gap_delta);
|
||||
if (result.isValid()) {
|
||||
writeCharToConsole(result.text, result.confidence);
|
||||
}
|
||||
}
|
||||
end_time = 0;
|
||||
transmit_time = 0;
|
||||
decode_timeout_calc = false;
|
||||
}
|
||||
|
||||
void MorseRadiotxView::onRelease() {
|
||||
end_time = chTimeNow();
|
||||
transmit_time = end_time;
|
||||
baseband::set_morsetx_key(false);
|
||||
if (start_time != 0) {
|
||||
int32_t press_delta = (end_time - start_time);
|
||||
auto result = morse_decoder_.handleInput(press_delta);
|
||||
if (result.isValid()) {
|
||||
writeCharToConsole(result.text, result.confidence);
|
||||
}
|
||||
}
|
||||
start_time = 0;
|
||||
decode_timeout_calc = true;
|
||||
baseband::request_beep_stop();
|
||||
}
|
||||
|
||||
void MorseRadiotxView::writeCharToConsole(const std::string& ch, double confidence) {
|
||||
if (ch.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
txt_last.set(morse_decoder_.getLastSequence().c_str());
|
||||
|
||||
last_color_id = color_id;
|
||||
std::string color = "";
|
||||
|
||||
if (ch == " ") {
|
||||
color_id = 0;
|
||||
} else if (ch[0] == '{') { // no match
|
||||
color_id = 0;
|
||||
} else {
|
||||
if (confidence == 1.5)
|
||||
color_id = 4;
|
||||
else if (confidence < 0.8)
|
||||
color_id = 1;
|
||||
else if (confidence < 0.9)
|
||||
color_id = 2;
|
||||
else
|
||||
color_id = 3;
|
||||
}
|
||||
color = arr_color[color_id];
|
||||
last_color_id = color_id;
|
||||
console_text.write(color + ch);
|
||||
}
|
||||
|
||||
void MorseRadiotxView::ptt_button_visibility(bool hidden) {
|
||||
bool hiddenorig = btn_ptt.hidden();
|
||||
if (hiddenorig != hidden) {
|
||||
btn_ptt.hidden(hidden);
|
||||
if (!hidden) btn_ptt.focus();
|
||||
if (hidden) {
|
||||
// hack fix until widget hidig problem is not solved.
|
||||
auto r = btn_ptt.screen_rect();
|
||||
Painter p;
|
||||
p.fill_rectangle_unrolled8(r, Theme::getInstance()->fg_light->background);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MorseRadiotxView::ui_toggle() {
|
||||
// 0=AM, 1=FM, 2=DSB, 3=USB, 4=LSB
|
||||
|
||||
if (transmit) {
|
||||
options_mode.set_style(Theme::getInstance()->fg_dark);
|
||||
options_mode.set_focusable(false);
|
||||
tone_.set_style(Theme::getInstance()->fg_dark);
|
||||
tone_.set_focusable(false);
|
||||
wpm_.set_style(Theme::getInstance()->fg_dark);
|
||||
wpm_.set_focusable(false);
|
||||
btn_message.set_style(Theme::getInstance()->fg_dark);
|
||||
btn_calls.set_style(Theme::getInstance()->fg_dark);
|
||||
chk_trans.set_style(Theme::getInstance()->fg_dark);
|
||||
chk_trans.set_focusable(false);
|
||||
bandwidth.set_style(Theme::getInstance()->fg_dark);
|
||||
bandwidth.set_focusable(false);
|
||||
chk_callsgn.set_style(Theme::getInstance()->fg_dark);
|
||||
chk_callsgn.set_focusable(false);
|
||||
|
||||
} else {
|
||||
options_mode.set_style(Theme::getInstance()->bg_darker);
|
||||
options_mode.set_focusable(true);
|
||||
wpm_.set_style(Theme::getInstance()->bg_darker);
|
||||
wpm_.set_focusable(true);
|
||||
btn_message.set_style(Theme::getInstance()->bg_darker);
|
||||
btn_calls.set_style(Theme::getInstance()->bg_darker);
|
||||
chk_trans.set_style(Theme::getInstance()->bg_darker);
|
||||
chk_trans.set_focusable(true);
|
||||
chk_callsgn.set_style(Theme::getInstance()->bg_darker);
|
||||
chk_callsgn.set_focusable(true);
|
||||
ptt_button_visibility(true);
|
||||
tone_.set_style(Theme::getInstance()->bg_darker);
|
||||
tone_.set_focusable(true);
|
||||
|
||||
if (current_mode == 1) { // FM mode
|
||||
bandwidth.set_style(Theme::getInstance()->bg_darker);
|
||||
bandwidth.set_focusable(true);
|
||||
} else {
|
||||
bandwidth.set_style(Theme::getInstance()->fg_dark);
|
||||
bandwidth.set_focusable(false);
|
||||
}
|
||||
} // transmit false
|
||||
}
|
||||
|
||||
inline bool MorseRadiotxView::tx_button_held() {
|
||||
const auto switches_state = get_switches_state();
|
||||
return switches_state[(size_t)ui::KeyEvent::Select];
|
||||
}
|
||||
|
||||
void MorseRadiotxView::on_framesync() {
|
||||
if (button_was_selected && !button_touch && !tx_button_held()) {
|
||||
button_was_selected = false;
|
||||
onRelease();
|
||||
}
|
||||
|
||||
if (end_time != 0 && decode_timeout_calc) {
|
||||
int64_t gap_delta = (chTimeNow() - end_time);
|
||||
|
||||
if (gap_delta >= morse_decoder_.getInterCharThreshold()) {
|
||||
auto result = morse_decoder_.handleInput(-(int32_t)gap_delta);
|
||||
if (result.isValid()) {
|
||||
writeCharToConsole(result.text, result.confidence);
|
||||
}
|
||||
}
|
||||
if (gap_delta >= morse_decoder_.getInterWordThreshold()) {
|
||||
writeCharToConsole(" ", 1.0);
|
||||
end_time = 0;
|
||||
decode_timeout_calc = false;
|
||||
}
|
||||
}
|
||||
if (transmit_time != 0 && transmit) { // Tx disable if time is up
|
||||
int64_t gap_delta = (chTimeNow() - transmit_time);
|
||||
if (gap_delta >= ((morse_decoder_.getInterWordThreshold() * ((chk_trans.value()) ? 10 : 1)))) {
|
||||
if (tx_thread) {
|
||||
chThdTerminate(tx_thread);
|
||||
chThdWait(tx_thread);
|
||||
}
|
||||
transmit = false;
|
||||
tx_view.set_transmitting(false);
|
||||
transmitter_model.disable();
|
||||
thread_running = false;
|
||||
tx_thread = nullptr;
|
||||
transmit_time = 0;
|
||||
if (!chk_trans.value()) writeCharToConsole(" ", 1.0);
|
||||
ui_toggle();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::morseradiotx
|
||||
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Pezsma
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef __MORSE_RADIOTX_H__
|
||||
#define __MORSE_RADIOTX_H__
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "ui_widget.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_language.hpp"
|
||||
#include "ui_painter.hpp"
|
||||
#include "ui_freq_field.hpp"
|
||||
#include "ui_transmitter.hpp"
|
||||
#include "ui_textentry.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include "morsedecoder.hpp"
|
||||
#include "irq_controls.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "portapack.hpp"
|
||||
#include "message.hpp"
|
||||
#include "volume.hpp"
|
||||
#include "audio.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
#include "external_app.hpp"
|
||||
#include "string_format.hpp"
|
||||
#include <ch.h>
|
||||
#include <hal.h>
|
||||
|
||||
namespace ui::external_app::morseradiotx {
|
||||
|
||||
class MorseRadiotxView : public ui::View {
|
||||
public:
|
||||
MorseRadiotxView(ui::NavigationView& nav);
|
||||
~MorseRadiotxView();
|
||||
|
||||
MorseRadiotxView(const MorseRadiotxView&) = delete;
|
||||
MorseRadiotxView(MorseRadiotxView&&) = delete;
|
||||
MorseRadiotxView& operator=(const MorseRadiotxView&) = delete;
|
||||
MorseRadiotxView& operator=(MorseRadiotxView&&) = delete;
|
||||
|
||||
std::string title() const override { return "Morse Tx"; }
|
||||
void focus() override;
|
||||
void on_show() override;
|
||||
void transmit_morse_message();
|
||||
|
||||
private:
|
||||
struct MorseTimings {
|
||||
uint32_t dot_ms;
|
||||
uint32_t dash_ms;
|
||||
uint32_t symbol_gap;
|
||||
uint32_t char_gap;
|
||||
uint32_t word_gap;
|
||||
};
|
||||
|
||||
MorseTimings current_timings{};
|
||||
|
||||
std::string msg_indicator{""};
|
||||
MorseTimings calculate_morse_timings(uint32_t wpm);
|
||||
void onPress();
|
||||
void onRelease();
|
||||
void on_framesync();
|
||||
void writeCharToConsole(const std::string& ch, double confidence);
|
||||
void ui_toggle();
|
||||
bool tx_button_held();
|
||||
void ptt_button_visibility(bool hidden);
|
||||
|
||||
ui::NavigationView& nav_;
|
||||
MorseDecoder morse_decoder_{};
|
||||
TxRadioState radio_state_{};
|
||||
Thread* tx_thread{nullptr};
|
||||
|
||||
std::string msg_buffer{"PORTAPACK"};
|
||||
uint8_t current_mode{0}; // 0=AM, 1=FM, 2=DSB, 3=USB, 4=LSB
|
||||
uint8_t wpm{20};
|
||||
uint32_t tone{700};
|
||||
float band{5.8};
|
||||
std::string call_sign{""};
|
||||
|
||||
app_settings::SettingsManager settings_{
|
||||
"tx_morseradio",
|
||||
app_settings::Mode::TX,
|
||||
{
|
||||
{"cmode"sv, ¤t_mode},
|
||||
{"audtone"sv, &tone},
|
||||
{"wpm"sv, &wpm},
|
||||
{"message"sv, &msg_buffer},
|
||||
{"bandwith"sv, &band},
|
||||
{"call_sign"sv, &call_sign},
|
||||
}};
|
||||
|
||||
TransmitterView tx_view{
|
||||
(int16_t)UI_POS_Y_BOTTOM(4),
|
||||
10000,
|
||||
0, false};
|
||||
|
||||
AudioVolumeField field_volume{{UI_POS_X_RIGHT(2), UI_POS_Y_BOTTOM(5)}};
|
||||
ui::OptionsField options_mode{
|
||||
{UI_POS_X(5), UI_POS_Y(0)},
|
||||
3,
|
||||
{{"AM", 0}, {"FM", 1}, {"DSB", 2}, {"USB", 3}, {"LSB", 4}}};
|
||||
NumberField tone_{{UI_POS_X(14), UI_POS_Y(0)}, 4, {400, 1400}, 10, ' ', true};
|
||||
NumberField wpm_{{UI_POS_X(25), UI_POS_Y(0)}, 2, {10, 45}, 1, ' ', true};
|
||||
FloatField bandwidth{{UI_POS_X(20), UI_POS_Y(3)}, 4, {1.0, 16.0}, 0.1, ' ', true, 1};
|
||||
|
||||
ui::Text txt_msg{{UI_POS_X(0), UI_POS_Y(1), UI_POS_MAXWIDTH, UI_POS_HEIGHT(1)}, "[" + msg_buffer + "] "};
|
||||
ui::Button btn_message{{UI_POS_X(0), UI_POS_Y(2), UI_POS_WIDTH(11), UI_POS_HEIGHT(1)}, "Message"};
|
||||
ui::Button btn_calls{{UI_POS_X(0), UI_POS_Y(4), UI_POS_WIDTH(11), UI_POS_HEIGHT(1)}, (call_sign.empty()) ? "call sign?" : call_sign};
|
||||
Checkbox chk_trans{{UI_POS_X(14), UI_POS_Y(2)}, 13, "Manual trans.", true};
|
||||
Checkbox chk_callsgn{{UI_POS_X(14), UI_POS_Y(4)}, 13, "Call sign", true};
|
||||
ui::Text txt_last{{UI_POS_X(10), UI_POS_Y(5), UI_POS_WIDTH_REMAINING(10), UI_POS_HEIGHT(1)}, ""};
|
||||
ui::Console console_text{{UI_POS_X(0), UI_POS_Y(7), UI_POS_MAXWIDTH, UI_POS_HEIGHT_REMAINING(14)}};
|
||||
ui::Button btn_clear{{UI_POS_X(0), UI_POS_Y_BOTTOM(5), UI_POS_WIDTH(5), UI_POS_HEIGHT(1)}, "CLR"};
|
||||
ui::Button btn_ptt{{UI_POS_X_CENTER(12), UI_POS_Y_BOTTOM(7), UI_POS_WIDTH(12), UI_POS_HEIGHT(3)}, "PTT"};
|
||||
|
||||
ui::Labels labels{
|
||||
{{UI_POS_X(0), UI_POS_Y(0)}, "Mode:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(9), UI_POS_Y(0)}, "Tone:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(18), UI_POS_Y(0)}, "Hz", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(21), UI_POS_Y(0)}, "WPM:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(14), UI_POS_Y(3)}, "BandW:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(24), UI_POS_Y(3)}, "kHz", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(5)}, "Last seq:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(6)}, "Sent Message:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X_RIGHT(7), UI_POS_Y_BOTTOM(5)}, "Vol.:", Theme::getInstance()->fg_light->foreground},
|
||||
};
|
||||
|
||||
uint8_t last_color_id{255};
|
||||
uint8_t color_id{255};
|
||||
std::string arr_color[4] = {STR_COLOR_WHITE, STR_COLOR_RED, STR_COLOR_YELLOW, STR_COLOR_GREEN};
|
||||
|
||||
bool button_touch{false};
|
||||
bool button_was_selected{false};
|
||||
bool decode_timeout_calc{false};
|
||||
bool transmit{false};
|
||||
bool thread_running = false;
|
||||
|
||||
uint8_t send_indicator{5};
|
||||
int64_t start_time{0};
|
||||
int64_t end_time{0};
|
||||
int64_t transmit_time{0};
|
||||
SwitchesState initial_switch_config_{};
|
||||
|
||||
MessageHandlerRegistration message_handler_framesync{
|
||||
Message::ID::DisplayFrameSync,
|
||||
[this](const Message* const p) {
|
||||
(void)p;
|
||||
this->on_framesync();
|
||||
}};
|
||||
};
|
||||
|
||||
} // namespace ui::external_app::morseradiotx
|
||||
|
||||
#endif // __MORSE_RADIOTX_H__
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright (C) 2024 Bernd Herzog
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "ui_sd_over_usb.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "external_app.hpp"
|
||||
|
||||
namespace ui::external_app::sdusb {
|
||||
void initialize_app(ui::NavigationView& nav) {
|
||||
nav.push<SdOverUsbView>();
|
||||
}
|
||||
} // namespace ui::external_app::sdusb
|
||||
|
||||
extern "C" {
|
||||
|
||||
__attribute__((section(".external_app.app_sdusb.application_information"), used)) application_information_t _application_information_sdusb = {
|
||||
/*.memory_location = */ (uint8_t*)0x00000000,
|
||||
/*.externalAppEntry = */ ui::external_app::sdusb::initialize_app,
|
||||
/*.header_version = */ CURRENT_HEADER_VERSION,
|
||||
/*.app_version = */ VERSION_MD5,
|
||||
|
||||
/*.app_name = */ "SD Over USB",
|
||||
/*.bitmap_data = */ {
|
||||
0xF0,
|
||||
0x0F,
|
||||
0x10,
|
||||
0x08,
|
||||
0x50,
|
||||
0x0A,
|
||||
0x10,
|
||||
0x08,
|
||||
0x10,
|
||||
0x08,
|
||||
0x10,
|
||||
0x08,
|
||||
0xF8,
|
||||
0x1F,
|
||||
0xF8,
|
||||
0x1F,
|
||||
0xF8,
|
||||
0x1F,
|
||||
0xF8,
|
||||
0x1F,
|
||||
0xF8,
|
||||
0x1F,
|
||||
0xF8,
|
||||
0x1F,
|
||||
0xF8,
|
||||
0x1F,
|
||||
0xF0,
|
||||
0x0F,
|
||||
0x80,
|
||||
0x01,
|
||||
0x80,
|
||||
0x01,
|
||||
},
|
||||
/*.icon_color = */ ui::Color::yellow().v,
|
||||
/*.menu_location = */ app_location_t::UTILITIES,
|
||||
/*.desired_menu_position = */ -1,
|
||||
|
||||
/*.m4_app_tag = portapack::spi_flash::image_tag_usb_sd */ {'P', 'U', 'S', 'B'},
|
||||
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
|
||||
};
|
||||
}
|
||||
+3
-3
@@ -23,7 +23,7 @@
|
||||
#include "ui_sd_over_usb.hpp"
|
||||
#include "portapack_shared_memory.hpp"
|
||||
|
||||
namespace ui {
|
||||
namespace ui::external_app::sdusb {
|
||||
|
||||
SdOverUsbView::SdOverUsbView(NavigationView& nav)
|
||||
: nav_(nav) {
|
||||
@@ -46,7 +46,7 @@ SdOverUsbView::SdOverUsbView(NavigationView& nav)
|
||||
sdcStop(&SDCD1);
|
||||
|
||||
portapack::shutdown(true);
|
||||
m4_init(portapack::spi_flash::image_tag_usb_sd, portapack::memory::map::m4_code, false);
|
||||
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
|
||||
m0_halt();
|
||||
/* will not return*/
|
||||
};
|
||||
@@ -56,4 +56,4 @@ void SdOverUsbView::focus() {
|
||||
button_run.focus();
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
} // namespace ui::external_app::sdusb
|
||||
+2
-2
@@ -32,7 +32,7 @@
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace ui {
|
||||
namespace ui::external_app::sdusb {
|
||||
|
||||
class SdOverUsbView : public View {
|
||||
public:
|
||||
@@ -58,6 +58,6 @@ class SdOverUsbView : public View {
|
||||
"Run"};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
} // namespace ui::external_app::sdusb
|
||||
|
||||
#endif /*__UI_SD_OVER_USB_H__*/
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Bernd Herzog
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "ui_siggen.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "external_app.hpp"
|
||||
|
||||
namespace ui::external_app::siggen {
|
||||
void initialize_app(ui::NavigationView& nav) {
|
||||
nav.push<SigGenView>();
|
||||
}
|
||||
} // namespace ui::external_app::siggen
|
||||
extern "C" {
|
||||
|
||||
__attribute__((section(".external_app.app_siggen.application_information"), used)) application_information_t _application_information_siggen = {
|
||||
/*.memory_location = */ (uint8_t*)0x00000000,
|
||||
/*.externalAppEntry = */ ui::external_app::siggen::initialize_app,
|
||||
/*.header_version = */ CURRENT_HEADER_VERSION,
|
||||
/*.app_version = */ VERSION_MD5,
|
||||
|
||||
/*.app_name = */ "Signal gen",
|
||||
/*.bitmap_data = */ {
|
||||
0x18,
|
||||
0x00,
|
||||
0x24,
|
||||
0x00,
|
||||
0x42,
|
||||
0x00,
|
||||
0x42,
|
||||
0x00,
|
||||
0x42,
|
||||
0x00,
|
||||
0x42,
|
||||
0x00,
|
||||
0x81,
|
||||
0x00,
|
||||
0xAB,
|
||||
0x6A,
|
||||
0x80,
|
||||
0x40,
|
||||
0x00,
|
||||
0x21,
|
||||
0x00,
|
||||
0x21,
|
||||
0x00,
|
||||
0x21,
|
||||
0x00,
|
||||
0x21,
|
||||
0x00,
|
||||
0x12,
|
||||
0x00,
|
||||
0x0C,
|
||||
0x00,
|
||||
0x00,
|
||||
},
|
||||
/*.icon_color = */ ui::Color::green().v,
|
||||
/*.menu_location = */ app_location_t::TX,
|
||||
/*.desired_menu_position = */ -1,
|
||||
|
||||
/*.m4_app_tag = portapack::spi_flash::image_tag_siggen */ {'P', 'S', 'I', 'G'},
|
||||
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
|
||||
};
|
||||
}
|
||||
Vendored
+3
-3
@@ -31,7 +31,7 @@
|
||||
|
||||
using namespace portapack;
|
||||
|
||||
namespace ui {
|
||||
namespace ui::external_app::siggen {
|
||||
|
||||
void SigGenView::focus() {
|
||||
options_shape.focus();
|
||||
@@ -75,7 +75,7 @@ void SigGenView::on_tx_progress(const uint32_t progress, const bool done) {
|
||||
|
||||
SigGenView::SigGenView(
|
||||
NavigationView& nav) {
|
||||
baseband::run_image(portapack::spi_flash::image_tag_siggen);
|
||||
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
|
||||
|
||||
add_children({&labels,
|
||||
&options_mod,
|
||||
@@ -172,4 +172,4 @@ SigGenView::SigGenView(
|
||||
};
|
||||
}
|
||||
|
||||
} /* namespace ui */
|
||||
} // namespace ui::external_app::siggen
|
||||
Vendored
+2
-2
@@ -33,7 +33,7 @@
|
||||
#include "portapack.hpp"
|
||||
#include "message.hpp"
|
||||
|
||||
namespace ui {
|
||||
namespace ui::external_app::siggen {
|
||||
|
||||
class SigGenView : public View {
|
||||
public:
|
||||
@@ -139,6 +139,6 @@ class SigGenView : public View {
|
||||
}};
|
||||
};
|
||||
|
||||
} /* namespace ui */
|
||||
} // namespace ui::external_app::siggen
|
||||
|
||||
#endif /*__SIGGEN_H__*/
|
||||
+91
-11
@@ -91,6 +91,7 @@ SubCarView::SubCarView(NavigationView& nav)
|
||||
&button_clear_list,
|
||||
&check_log,
|
||||
&labels,
|
||||
&options_mode,
|
||||
&recent_entries_view});
|
||||
|
||||
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
|
||||
@@ -114,11 +115,17 @@ SubCarView::SubCarView(NavigationView& nav)
|
||||
recent_entries_view.on_select = [this](const SubCarRecentEntry& entry) {
|
||||
nav_.push<SubCarRecentEntryDetailView>(entry);
|
||||
};
|
||||
baseband::set_subghzd_config(0, receiver_model.sampling_rate()); // 0=am
|
||||
receiver_model.enable();
|
||||
|
||||
options_mode.on_change = [this](size_t, int32_t v) {
|
||||
modulation = v;
|
||||
chThdSleepMilliseconds(100); // wait for the baseband thread to process the previous config, to avoid glitchy output when switching modes
|
||||
baseband::set_subghzd_config(modulation, receiver_model.sampling_rate());
|
||||
};
|
||||
signal_token_tick_second = rtc_time::signal_tick_second += [this]() {
|
||||
on_tick_second();
|
||||
};
|
||||
options_mode.set_selected_index(modulation, true);
|
||||
receiver_model.enable();
|
||||
}
|
||||
|
||||
void SubCarView::on_tick_second() {
|
||||
@@ -176,6 +183,8 @@ const char* SubCarView::getSensorTypeName(FPROTO_SUBCAR_SENSOR type) {
|
||||
return "Fiat V0";
|
||||
case FPC_BMWV0:
|
||||
return "BMW V0";
|
||||
/* case FPC_KIAV6:
|
||||
return "Kia V6";*/
|
||||
|
||||
case FPC_Invalid:
|
||||
default:
|
||||
@@ -266,10 +275,11 @@ void SubCarRecentEntryDetailView::parseProtocol() {
|
||||
if (entry_.sensorType == FPC_Invalid) return;
|
||||
|
||||
if (entry_.sensorType == FPC_SUZUKI) {
|
||||
uint32_t serial_button = (((entry_.data >> 32) & 0xFFF) << 20) | (entry_.data >> 12);
|
||||
serial = serial_button >> 4;
|
||||
uint8_t buttonid = serial_button & 0xF;
|
||||
cnt = (entry_.data >> 44) & 0xFFFF;
|
||||
uint32_t data_high = (uint32_t)(entry_.data >> 32);
|
||||
uint32_t data_low = (uint32_t)entry_.data;
|
||||
serial = ((data_high & 0xFFF) << 16) | (data_low >> 16);
|
||||
uint8_t buttonid = (data_low >> 12) & 0xF;
|
||||
cnt = (data_high << 4) >> 16;
|
||||
btn = to_string_dec_uint(buttonid);
|
||||
return;
|
||||
}
|
||||
@@ -320,15 +330,78 @@ void SubCarRecentEntryDetailView::parseProtocol() {
|
||||
}
|
||||
|
||||
if (entry_.sensorType == FPC_KIAV5) {
|
||||
serial = (uint32_t)(((entry_.data >> 32) & 0x0FFFFFFF) >> 1);
|
||||
uint8_t button = (entry_.data >> 61) & 0x07;
|
||||
uint64_t yek = 0;
|
||||
for (int i = 0; i < 8; i++) {
|
||||
uint8_t byte = (entry_.data2 >> (i * 8)) & 0xFF;
|
||||
uint8_t reversed = 0;
|
||||
for (int b = 0; b < 8; b++) {
|
||||
if (byte & (1 << b))
|
||||
reversed |= (1 << (7 - b));
|
||||
}
|
||||
yek |= ((uint64_t)reversed << ((7 - i) * 8));
|
||||
}
|
||||
serial = (uint32_t)((yek >> 32) & 0x0FFFFFFF);
|
||||
uint8_t button = (uint8_t)((yek >> 60) & 0x0F);
|
||||
uint32_t encrypted = (uint32_t)(yek & 0xFFFFFFFF);
|
||||
btn = to_string_dec_uint(button);
|
||||
cnt = (uint16_t)(entry_.data & 0xFFFF);
|
||||
// decode
|
||||
uint8_t keystore_bytes[] = {0x53, 0x54, 0x46, 0x52, 0x4b, 0x45, 0x30, 0x30};
|
||||
uint8_t s0 = (encrypted & 0xFF);
|
||||
uint8_t s1 = (encrypted >> 8) & 0xFF;
|
||||
uint8_t s2 = (encrypted >> 16) & 0xFF;
|
||||
uint8_t s3 = (encrypted >> 24) & 0xFF;
|
||||
int round_index = 1;
|
||||
for (size_t i = 0; i < 18; i++) {
|
||||
uint8_t r = keystore_bytes[round_index] & 0xFF;
|
||||
int steps = 8;
|
||||
while (steps > 0) {
|
||||
uint8_t base;
|
||||
if ((s3 & 0x40) == 0) {
|
||||
base = (s3 & 0x02) == 0 ? 0x74 : 0x2E;
|
||||
} else {
|
||||
base = (s3 & 0x02) == 0 ? 0x3A : 0x5C;
|
||||
}
|
||||
|
||||
if (s2 & 0x08) {
|
||||
base = (((base >> 4) & 0x0F) | ((base & 0x0F) << 4)) & 0xFF;
|
||||
}
|
||||
if (s1 & 0x01) {
|
||||
base = ((base & 0x3F) << 2) & 0xFF;
|
||||
}
|
||||
if (s0 & 0x01) {
|
||||
base = (base << 1) & 0xFF;
|
||||
}
|
||||
|
||||
uint8_t temp = (s3 ^ s1) & 0xFF;
|
||||
s3 = ((s3 & 0x7F) << 1) & 0xFF;
|
||||
if (s2 & 0x80) {
|
||||
s3 |= 0x01;
|
||||
}
|
||||
s2 = ((s2 & 0x7F) << 1) & 0xFF;
|
||||
if (s1 & 0x80) {
|
||||
s2 |= 0x01;
|
||||
}
|
||||
s1 = ((s1 & 0x7F) << 1) & 0xFF;
|
||||
if (s0 & 0x80) {
|
||||
s1 |= 0x01;
|
||||
}
|
||||
s0 = ((s0 & 0x7F) << 1) & 0xFF;
|
||||
|
||||
uint8_t chk = (base ^ (r ^ temp)) & 0xFF;
|
||||
if (chk & 0x80) {
|
||||
s0 |= 0x01;
|
||||
}
|
||||
r = ((r & 0x7F) << 1) & 0xFF;
|
||||
steps--;
|
||||
}
|
||||
round_index = (round_index - 1) & 0x7;
|
||||
}
|
||||
cnt = (s0 + (s1 << 8)) & 0xFFFF;
|
||||
}
|
||||
|
||||
if (entry_.sensorType == FPC_KIAV3V4) {
|
||||
// not decrypted!
|
||||
serial = SD_NO_SERIAL; //(uint32_t)entry_.data;
|
||||
serial = (uint32_t)entry_.data;
|
||||
// uint8_t button = entry_.data2 & 0xFF;
|
||||
btn = "?"; // to_string_dec_uint(button);
|
||||
}
|
||||
@@ -344,7 +417,7 @@ void SubCarRecentEntryDetailView::parseProtocol() {
|
||||
if (entry_.sensorType == FPC_KIAV1) {
|
||||
serial = (uint32_t)((entry_.data >> 24) & 0xFFFFFFFF);
|
||||
uint8_t button = (uint8_t)((entry_.data >> 16) & 0xFF);
|
||||
cnt = (uint8_t)((entry_.data >> 8) & 0xFF);
|
||||
cnt = (uint8_t)((entry_.data >> 4) & 0xF) << 8 | ((entry_.data >> 8) & 0xFF);
|
||||
btn = to_string_dec_uint(button);
|
||||
}
|
||||
|
||||
@@ -427,6 +500,13 @@ void SubCarRecentEntryDetailView::parseProtocol() {
|
||||
btn = to_string_dec_uint(button);
|
||||
}
|
||||
|
||||
/*if (entry_.sensorType == FPC_KIAV6) {
|
||||
// not decrypted!
|
||||
serial = 0;
|
||||
btn = "?";
|
||||
cnt = 0;
|
||||
}*/
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
+19
-13
@@ -116,25 +116,27 @@ class SubCarView : public View {
|
||||
4'000'000 /* sampling rate */,
|
||||
ReceiverModel::Mode::AMAudio};
|
||||
bool logging = false;
|
||||
uint8_t modulation = 0;
|
||||
app_settings::SettingsManager settings_{
|
||||
"rx_subcar",
|
||||
app_settings::Mode::RX,
|
||||
{
|
||||
{"log"sv, &logging},
|
||||
{"modulationmode"sv, &modulation},
|
||||
}};
|
||||
|
||||
SubCarRecentEntries recent{};
|
||||
|
||||
RFAmpField field_rf_amp{
|
||||
{13 * 8, UI_POS_Y(0)}};
|
||||
{UI_POS_X(13), UI_POS_Y(0)}};
|
||||
LNAGainField field_lna{
|
||||
{15 * 8, UI_POS_Y(0)}};
|
||||
{UI_POS_X(15), UI_POS_Y(0)}};
|
||||
VGAGainField field_vga{
|
||||
{18 * 8, UI_POS_Y(0)}};
|
||||
{UI_POS_X(18), UI_POS_Y(0)}};
|
||||
RSSI rssi{
|
||||
{21 * 8, 0, UI_POS_WIDTH_REMAINING(24), 4}};
|
||||
{UI_POS_X(21), 0, UI_POS_WIDTH_REMAINING(24), 4}};
|
||||
Channel channel{
|
||||
{21 * 8, 5, UI_POS_WIDTH_REMAINING(24), 4},
|
||||
{UI_POS_X(21), 5, UI_POS_WIDTH_REMAINING(24), 4},
|
||||
};
|
||||
RxFrequencyField field_frequency{
|
||||
{UI_POS_X(0), UI_POS_Y(0)},
|
||||
@@ -143,18 +145,22 @@ class SubCarView : public View {
|
||||
SignalToken signal_token_tick_second{};
|
||||
|
||||
Button button_clear_list{
|
||||
{0, 16, 7 * 8, 32},
|
||||
{UI_POS_X(0), UI_POS_Y(1), UI_POS_WIDTH(7), UI_POS_HEIGHT(2)},
|
||||
"Clear"};
|
||||
|
||||
Checkbox check_log{
|
||||
{10 * 8, 18},
|
||||
{UI_POS_X(8), UI_POS_Y(1)},
|
||||
3,
|
||||
"Log",
|
||||
true};
|
||||
|
||||
Labels labels{
|
||||
{{UI_POS_X_RIGHT(14), UI_POS_Y(1)}, "no fm yet :(", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(15), UI_POS_Y(1)}, "Mode:", Theme::getInstance()->fg_light->foreground},
|
||||
};
|
||||
ui::OptionsField options_mode{
|
||||
{UI_POS_X(22), UI_POS_Y(1)},
|
||||
3,
|
||||
{{"AM", 0}, {"FM", 1}}};
|
||||
|
||||
static constexpr auto header_height = 3 * 16;
|
||||
|
||||
@@ -198,16 +204,16 @@ class SubCarRecentEntryDetailView : public View {
|
||||
std::string btn = "";
|
||||
uint32_t cnt = SD_NO_CNT;
|
||||
|
||||
Text text_type{{UI_POS_X(0), 1 * 16, 15 * 8, 16}, "?"};
|
||||
Text text_id{{6 * 8, 2 * 16, 10 * 8, 16}, "?"};
|
||||
Text text_type{{UI_POS_X(0), UI_POS_Y(1), UI_POS_WIDTH(15), UI_POS_HEIGHT(1)}, "?"};
|
||||
Text text_id{{UI_POS_X(6), UI_POS_Y(2), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)}, "?"};
|
||||
|
||||
Console console{
|
||||
{0, 4 * 16, screen_width, screen_height - (4 * 16) - 36}};
|
||||
{UI_POS_X(0), UI_POS_Y(4), UI_POS_MAXWIDTH, screen_height - (4 * 16) - 36}};
|
||||
|
||||
Labels labels{
|
||||
{{UI_POS_X(0), UI_POS_Y(0)}, "Type:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), 2 * 16}, "Serial: ", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), 3 * 16}, "Data:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(2)}, "Serial: ", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(3)}, "Data:", Theme::getInstance()->fg_light->foreground},
|
||||
};
|
||||
|
||||
Button button_done{
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright (C) 2025 Bernd Herzog
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "ui_waterfall_designer.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "external_app.hpp"
|
||||
|
||||
namespace ui::external_app::waterfall_designer {
|
||||
void initialize_app(ui::NavigationView& nav) {
|
||||
nav.push<WaterfallDesignerView>();
|
||||
}
|
||||
} // namespace ui::external_app::waterfall_designer
|
||||
|
||||
extern "C" {
|
||||
|
||||
__attribute__((section(".external_app.app_waterfall_designer.application_information"), used)) application_information_t _application_information_waterfall_designer = {
|
||||
/*.memory_location = */ (uint8_t*)0x00000000,
|
||||
/*.externalAppEntry = */ ui::external_app::waterfall_designer::initialize_app,
|
||||
/*.header_version = */ CURRENT_HEADER_VERSION,
|
||||
/*.app_version = */ VERSION_MD5,
|
||||
|
||||
/*.app_name = */ "Wt Design",
|
||||
/*.bitmap_data = */ {
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0xFE,
|
||||
0x7F,
|
||||
0x03,
|
||||
0xC0,
|
||||
0x53,
|
||||
0xD5,
|
||||
0xAB,
|
||||
0xCA,
|
||||
0x53,
|
||||
0xD5,
|
||||
0xAB,
|
||||
0xCA,
|
||||
0x53,
|
||||
0xD5,
|
||||
0xAB,
|
||||
0xCA,
|
||||
0x53,
|
||||
0xD5,
|
||||
0x03,
|
||||
0xC0,
|
||||
0xFF,
|
||||
0xFF,
|
||||
0xFB,
|
||||
0xD7,
|
||||
0xFE,
|
||||
0x7F,
|
||||
0x00,
|
||||
0x00,
|
||||
},
|
||||
/*.icon_color = */ ui::Color::cyan().v,
|
||||
/*.menu_location = */ app_location_t::UTILITIES,
|
||||
/*.desired_menu_position = */ -1,
|
||||
|
||||
/*.m4_app_tag = portapack::spi_flash::image_tag_none */ {'P', 'C', 'A', 'P'},
|
||||
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,503 @@
|
||||
/*
|
||||
* Copyleft Mr. Robot
|
||||
* Copyright HTotoo
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "ui_waterfall_designer.hpp"
|
||||
#include "baseband_api.hpp"
|
||||
#include "portapack.hpp"
|
||||
#include "ui_freqman.hpp"
|
||||
#include "file_path.hpp"
|
||||
#include "ui_fileman.hpp"
|
||||
#include "file_reader.hpp"
|
||||
#include "ui_textentry.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
|
||||
namespace ui::external_app::waterfall_designer {
|
||||
|
||||
WaterfallDesignerView::WaterfallDesignerView(NavigationView& nav)
|
||||
: nav_{nav} {
|
||||
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
|
||||
|
||||
add_children({&labels,
|
||||
&field_frequency,
|
||||
&field_frequency_step,
|
||||
&field_rf_amp,
|
||||
&field_lna,
|
||||
&field_vga,
|
||||
&option_bandwidth,
|
||||
&record_view,
|
||||
&menu_view,
|
||||
&button_new,
|
||||
&button_open,
|
||||
&button_save,
|
||||
&button_add_level,
|
||||
&button_remove_level,
|
||||
&button_edit_color,
|
||||
&button_apply_setting,
|
||||
&waterfall});
|
||||
ensure_directory(waterfalls_dir);
|
||||
ui::Rect waterfall_rect{0, header_height, screen_rect().width(), screen_rect().height() - header_height};
|
||||
waterfall.set_parent_rect(waterfall_rect);
|
||||
|
||||
menu_view.set_parent_rect({0, 1 * 16, screen_width, 7 * 16});
|
||||
|
||||
field_frequency_step.set_by_value(receiver_model.frequency_step());
|
||||
field_frequency_step.on_change = [this](size_t, OptionsField::value_t v) {
|
||||
receiver_model.set_frequency_step(v);
|
||||
this->field_frequency.set_step(v);
|
||||
};
|
||||
|
||||
freqman_set_bandwidth_option(SPEC_MODULATION, option_bandwidth);
|
||||
option_bandwidth.on_change = [this](size_t, uint32_t new_capture_rate) {
|
||||
/* Nyquist would imply a sample rate of 2x bandwidth, but because the ADC
|
||||
* provides 2 values (I,Q), the sample_rate is equal to bandwidth here. */
|
||||
|
||||
/* capture_rate (bandwidth) is used for FFT calculation and display LCD, and also in recording writing SD Card rate. */
|
||||
/* ex. sampling_rate values, 4Mhz, when recording 500 kHz (BW) and fs 8 Mhz, when selected 1 Mhz BW ... */
|
||||
/* ex. recording 500kHz BW to .C16 file, base_rate clock 500kHz x2(I,Q) x 2 bytes (int signed) =2MB/sec rate SD Card. */
|
||||
|
||||
waterfall.stop();
|
||||
|
||||
// record_view determines the correct oversampling to apply and returns the actual sample rate.
|
||||
// NB: record_view is what actually updates proc_capture baseband settings.
|
||||
auto actual_sample_rate = record_view.set_sampling_rate(new_capture_rate);
|
||||
|
||||
// Update the radio model with the actual sampling rate.
|
||||
receiver_model.set_sampling_rate(actual_sample_rate);
|
||||
|
||||
// Get suitable anti-aliasing BPF bandwidth for MAX2837 given the actual sample rate.
|
||||
auto anti_alias_filter_bandwidth = filter_bandwidth_for_sampling_rate(actual_sample_rate);
|
||||
receiver_model.set_baseband_bandwidth(anti_alias_filter_bandwidth);
|
||||
|
||||
capture_rate = new_capture_rate;
|
||||
|
||||
waterfall.start();
|
||||
};
|
||||
|
||||
button_new.on_select = [this]() {
|
||||
on_create_new_profile();
|
||||
};
|
||||
|
||||
button_open.on_select = [this]() {
|
||||
on_open_profile();
|
||||
};
|
||||
|
||||
button_save.on_select = [this]() {
|
||||
on_save_profile();
|
||||
};
|
||||
|
||||
button_add_level.on_select = [this]() {
|
||||
on_add_level();
|
||||
};
|
||||
|
||||
button_remove_level.on_select = [this]() {
|
||||
on_remove_level();
|
||||
};
|
||||
|
||||
button_edit_color.on_select = [this]() {
|
||||
on_edit_color();
|
||||
};
|
||||
|
||||
button_apply_setting.on_select = [this]() {
|
||||
if_apply_setting = true;
|
||||
on_apply_current_to_wtf();
|
||||
// nav_.pop();
|
||||
};
|
||||
|
||||
menu_view.on_highlight = [this]() {
|
||||
highlighted_index_ = menu_view.highlighted_index();
|
||||
};
|
||||
|
||||
receiver_model.enable();
|
||||
option_bandwidth.set_by_value(capture_rate);
|
||||
|
||||
record_view.on_error = [&nav](std::string message) {
|
||||
nav.display_modal("Error", message);
|
||||
};
|
||||
|
||||
button_save.hidden(true);
|
||||
button_add_level.hidden(true);
|
||||
button_remove_level.hidden(true);
|
||||
button_edit_color.hidden(true);
|
||||
button_apply_setting.hidden(true);
|
||||
|
||||
refresh_menu_view();
|
||||
}
|
||||
|
||||
WaterfallDesignerView::WaterfallDesignerView(
|
||||
NavigationView& nav,
|
||||
ReceiverModel::settings_t override)
|
||||
: WaterfallDesignerView(nav) {
|
||||
// Settings to override when launched from another app (versus from AppSettings .ini file)
|
||||
field_frequency.set_value(override.frequency_app_override);
|
||||
}
|
||||
|
||||
WaterfallDesignerView::~WaterfallDesignerView() {
|
||||
if (!if_apply_setting) restore_current_profile();
|
||||
receiver_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
void WaterfallDesignerView::set_parent_rect(const Rect new_parent_rect) {
|
||||
View::set_parent_rect(new_parent_rect);
|
||||
|
||||
ui::Rect waterfall_rect{0, header_height, new_parent_rect.width(), new_parent_rect.height() - header_height};
|
||||
waterfall.set_parent_rect(waterfall_rect);
|
||||
}
|
||||
|
||||
void WaterfallDesignerView::focus() {
|
||||
button_open.focus();
|
||||
}
|
||||
|
||||
void WaterfallDesignerView::on_freqchg(int64_t freq) {
|
||||
field_frequency.set_value(freq);
|
||||
}
|
||||
|
||||
void WaterfallDesignerView::on_open_profile() {
|
||||
auto open_view = nav_.push<FileLoadView>(".txt");
|
||||
open_view->push_dir(waterfalls_dir);
|
||||
open_view->on_changed = [this](std::filesystem::path new_file_path) {
|
||||
on_profile_changed(new_file_path);
|
||||
};
|
||||
}
|
||||
|
||||
void WaterfallDesignerView::on_profile_changed(std::filesystem::path new_profile_path) {
|
||||
current_profile_path = new_profile_path;
|
||||
profile_levels.clear();
|
||||
|
||||
File playlist_file;
|
||||
auto error = playlist_file.open(new_profile_path.string());
|
||||
|
||||
if (error) return;
|
||||
|
||||
menu_view.clear();
|
||||
auto reader = FileLineReader(playlist_file);
|
||||
|
||||
for (const auto& line : reader) {
|
||||
profile_levels.push_back(line);
|
||||
}
|
||||
|
||||
for (auto& line : profile_levels) {
|
||||
// remove empty lines
|
||||
if (line == "\n" || line == "\r\n" || line == "\r") {
|
||||
profile_levels.erase(std::remove(profile_levels.begin(), profile_levels.end(), line), profile_levels.end());
|
||||
}
|
||||
|
||||
// remove line end \n etc
|
||||
if (line.length() > 0 && (line[line.length() - 1] == '\n' || line[line.length() - 1] == '\r')) {
|
||||
line = line.substr(0, line.length() - 1);
|
||||
}
|
||||
}
|
||||
|
||||
button_save.hidden(false);
|
||||
button_add_level.hidden(false);
|
||||
button_remove_level.hidden(false);
|
||||
button_edit_color.hidden(false);
|
||||
button_apply_setting.hidden(false);
|
||||
|
||||
refresh_menu_view();
|
||||
on_apply_current_to_wtf();
|
||||
}
|
||||
|
||||
void WaterfallDesignerView::refresh_menu_view() {
|
||||
menu_view.clear();
|
||||
|
||||
for (const auto& line : profile_levels) {
|
||||
if (line.length() == 0 || line[0] == '#') {
|
||||
menu_view.add_item({line,
|
||||
ui::Color::grey(),
|
||||
&bitmap_icon_notepad,
|
||||
[this](KeyEvent) {
|
||||
button_add_level.focus();
|
||||
}});
|
||||
} else {
|
||||
// index,R,G,B
|
||||
size_t pos = 0;
|
||||
size_t next_pos = 0;
|
||||
|
||||
// pass index
|
||||
next_pos = line.find(',', pos);
|
||||
if (next_pos == std::string::npos) continue;
|
||||
pos = next_pos + 1;
|
||||
|
||||
// r
|
||||
next_pos = line.find(',', pos);
|
||||
if (next_pos == std::string::npos) continue;
|
||||
uint8_t r = static_cast<uint8_t>(std::stoi(line.substr(pos, next_pos - pos)));
|
||||
pos = next_pos + 1;
|
||||
|
||||
// g
|
||||
next_pos = line.find(',', pos);
|
||||
if (next_pos == std::string::npos) continue;
|
||||
uint8_t g = static_cast<uint8_t>(std::stoi(line.substr(pos, next_pos - pos)));
|
||||
pos = next_pos + 1;
|
||||
|
||||
// b
|
||||
uint8_t b = static_cast<uint8_t>(std::stoi(line.substr(pos)));
|
||||
|
||||
ui::Color color = ui::Color(r, g, b);
|
||||
menu_view.add_item({line,
|
||||
color,
|
||||
&bitmap_icon_cwgen,
|
||||
[this](KeyEvent) {
|
||||
button_remove_level.focus();
|
||||
}});
|
||||
}
|
||||
}
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
void WaterfallDesignerView::on_apply_current_to_wtf() {
|
||||
std::filesystem::path system_read_path = "waterfall.txt";
|
||||
copy_file(current_profile_path, system_read_path);
|
||||
|
||||
waterfall.load_gradient();
|
||||
|
||||
set_dirty();
|
||||
}
|
||||
|
||||
void WaterfallDesignerView::on_save_profile() {
|
||||
if (current_profile_path.empty()) {
|
||||
nav_.display_modal("Err", "No profile file loaded");
|
||||
return;
|
||||
} else if (profile_levels.empty()) {
|
||||
nav_.display_modal("Err", "List is empty");
|
||||
return;
|
||||
}
|
||||
|
||||
File profile_file;
|
||||
auto error = profile_file.open(current_profile_path.string(), false, false);
|
||||
|
||||
if (error) {
|
||||
nav_.display_modal("Err", "open err");
|
||||
return;
|
||||
}
|
||||
|
||||
// clear file
|
||||
profile_file.seek(0);
|
||||
profile_file.truncate();
|
||||
|
||||
// write new data
|
||||
for (const auto& entry : profile_levels) {
|
||||
profile_file.write_line(entry);
|
||||
}
|
||||
|
||||
nav_.display_modal("Save", "Saved profile\n" + current_profile_path.string());
|
||||
}
|
||||
|
||||
void WaterfallDesignerView::on_add_level() {
|
||||
if (highlighted_index_ >= profile_levels.size()) return;
|
||||
if (profile_levels[highlighted_index_].empty()) return;
|
||||
if (profile_levels[highlighted_index_][0] == '#') return;
|
||||
if (profile_levels[highlighted_index_].find(',') == std::string::npos) return;
|
||||
size_t insert_pos = highlighted_index_;
|
||||
std::string new_entry = "0,128,128,128";
|
||||
profile_levels.insert(profile_levels.begin() + insert_pos, new_entry);
|
||||
refresh_menu_view();
|
||||
on_edit_color();
|
||||
}
|
||||
|
||||
void WaterfallDesignerView::on_remove_level() {
|
||||
if (highlighted_index_ >= profile_levels.size()) return;
|
||||
if (profile_levels[highlighted_index_].empty()) return;
|
||||
if (profile_levels[highlighted_index_][0] == '#') return;
|
||||
if (profile_levels[highlighted_index_].find(',') == std::string::npos) return;
|
||||
profile_levels.erase(profile_levels.begin() + highlighted_index_);
|
||||
refresh_menu_view();
|
||||
}
|
||||
|
||||
void WaterfallDesignerView::on_edit_color() {
|
||||
if (highlighted_index_ >= profile_levels.size()) return;
|
||||
if (profile_levels[highlighted_index_].empty()) return;
|
||||
if (profile_levels[highlighted_index_][0] == '#') return;
|
||||
if (profile_levels[highlighted_index_].find(',') == std::string::npos) return;
|
||||
|
||||
auto color_picker_view = nav_.push<WaterfallDesignerColorPickerView>(profile_levels[highlighted_index_]);
|
||||
color_picker_view->on_save = [this](std::string new_color) {
|
||||
profile_levels[highlighted_index_] = new_color;
|
||||
refresh_menu_view();
|
||||
on_apply_current_to_wtf();
|
||||
};
|
||||
}
|
||||
|
||||
void WaterfallDesignerView::backup_current_profile() {
|
||||
std::filesystem::path curren_wtf_path = "waterfall.txt";
|
||||
std::filesystem::path backup_path = waterfalls_dir / "wtf_des_bk.bk";
|
||||
copy_file(curren_wtf_path, backup_path);
|
||||
}
|
||||
|
||||
void WaterfallDesignerView::restore_current_profile() {
|
||||
std::filesystem::path backup_path = waterfalls_dir / "wtf_des_bk.bk";
|
||||
std::filesystem::path put_back_path = "waterfall.txt";
|
||||
copy_file(backup_path, put_back_path);
|
||||
delete_file(backup_path);
|
||||
}
|
||||
|
||||
void WaterfallDesignerView::on_apply_setting() {
|
||||
}
|
||||
|
||||
void WaterfallDesignerView::on_create_new_profile() {
|
||||
text_prompt(
|
||||
nav_,
|
||||
file_name_buffer,
|
||||
32 - 4, // fat32
|
||||
ENTER_KEYBOARD_MODE_ALPHA,
|
||||
[this](std::string& buffer) {
|
||||
if (buffer.empty()) return;
|
||||
|
||||
if (buffer.length() < 4 || buffer.substr(buffer.length() - 4) != ".txt") {
|
||||
buffer += ".txt";
|
||||
}
|
||||
|
||||
File new_file;
|
||||
auto error = new_file.create(waterfalls_dir / buffer);
|
||||
if (error) {
|
||||
nav_.display_modal("Err", "create file err");
|
||||
return;
|
||||
}
|
||||
|
||||
profile_levels.clear();
|
||||
current_profile_path = waterfalls_dir / buffer;
|
||||
on_profile_changed(current_profile_path);
|
||||
});
|
||||
}
|
||||
|
||||
WaterfallDesignerColorPickerView::WaterfallDesignerColorPickerView(NavigationView& nav, std::string color_str)
|
||||
: nav_{nav},
|
||||
color_str_{color_str} {
|
||||
add_children({&labels,
|
||||
&field_red,
|
||||
&field_green,
|
||||
&field_blue,
|
||||
&field_step,
|
||||
&field_index,
|
||||
&progressbar,
|
||||
&button_save});
|
||||
|
||||
progressbar.set_max(UINT8_MAX);
|
||||
|
||||
size_t pos = 0;
|
||||
size_t next_pos = 0;
|
||||
|
||||
// index
|
||||
next_pos = color_str.find(',', pos);
|
||||
if (next_pos != std::string::npos) {
|
||||
index_ = static_cast<uint8_t>(std::stoi(color_str.substr(pos, next_pos - pos)));
|
||||
pos = next_pos + 1;
|
||||
}
|
||||
|
||||
// r
|
||||
next_pos = color_str.find(',', pos);
|
||||
if (next_pos != std::string::npos) {
|
||||
red_ = static_cast<uint8_t>(std::stoi(color_str.substr(pos, next_pos - pos)));
|
||||
pos = next_pos + 1;
|
||||
}
|
||||
|
||||
// g
|
||||
next_pos = color_str.find(',', pos);
|
||||
if (next_pos != std::string::npos) {
|
||||
green_ = static_cast<uint8_t>(std::stoi(color_str.substr(pos, next_pos - pos)));
|
||||
pos = next_pos + 1;
|
||||
}
|
||||
|
||||
// b
|
||||
blue_ = static_cast<uint8_t>(std::stoi(color_str.substr(pos)));
|
||||
|
||||
field_red.set_value(red_);
|
||||
field_green.set_value(green_);
|
||||
field_blue.set_value(blue_);
|
||||
field_index.set_value(index_);
|
||||
|
||||
// cb
|
||||
field_red.on_change = [this](int32_t) {
|
||||
update_color_index();
|
||||
};
|
||||
|
||||
field_green.on_change = [this](int32_t) {
|
||||
update_color_index();
|
||||
};
|
||||
|
||||
field_blue.on_change = [this](int32_t) {
|
||||
update_color_index();
|
||||
};
|
||||
|
||||
button_save.on_select = [this](Button&) {
|
||||
if (on_save) on_save(build_color_str());
|
||||
nav_.pop();
|
||||
};
|
||||
|
||||
field_index.on_change = [this](int32_t) {
|
||||
update_color_index();
|
||||
};
|
||||
|
||||
field_step.on_change = [this](int32_t) {
|
||||
field_red.set_step(field_step.value());
|
||||
field_green.set_step(field_step.value());
|
||||
field_blue.set_step(field_step.value());
|
||||
field_index.set_step(field_step.value());
|
||||
};
|
||||
|
||||
update_color_index();
|
||||
}
|
||||
|
||||
void WaterfallDesignerColorPickerView::focus() {
|
||||
button_save.focus();
|
||||
}
|
||||
|
||||
void WaterfallDesignerColorPickerView::update_color_index() {
|
||||
index_ = static_cast<uint8_t>(field_index.value());
|
||||
red_ = static_cast<uint8_t>(field_red.value());
|
||||
green_ = static_cast<uint8_t>(field_green.value());
|
||||
blue_ = static_cast<uint8_t>(field_blue.value());
|
||||
|
||||
const Rect preview_rect{screen_width - 48, 1 * 16, 40, 40};
|
||||
|
||||
Painter painter_instance_2;
|
||||
painter_instance_2.fill_rectangle(
|
||||
{preview_rect.left(), preview_rect.top(), preview_rect.width(), preview_rect.height()},
|
||||
ui::Color(red_, green_, blue_));
|
||||
}
|
||||
|
||||
void WaterfallDesignerColorPickerView::paint(Painter& painter) {
|
||||
// this is not duplicated code.
|
||||
// because need to display color when enter,
|
||||
// but it is too early to call update_color() in the constructor.
|
||||
|
||||
const Rect preview_rect{screen_width - 48, 1 * 16, 40, 40};
|
||||
|
||||
painter.fill_rectangle(
|
||||
{preview_rect.left(), preview_rect.top(), preview_rect.width(), preview_rect.height()},
|
||||
ui::Color(red_, green_, blue_));
|
||||
}
|
||||
|
||||
std::string WaterfallDesignerColorPickerView::build_color_str() {
|
||||
size_t index_pos = color_str_.find(',');
|
||||
if (index_pos != std::string::npos) {
|
||||
return color_str_.substr(0, index_pos + 1) +
|
||||
to_string_dec_uint(red_) + "," +
|
||||
to_string_dec_uint(green_) + "," +
|
||||
to_string_dec_uint(blue_);
|
||||
}
|
||||
return to_string_dec_uint(index_) + "," + to_string_dec_uint(red_) + "," + to_string_dec_uint(green_) + "," + to_string_dec_uint(blue_);
|
||||
}
|
||||
|
||||
} /* namespace ui::external_app::waterfall_designer */
|
||||
@@ -0,0 +1,265 @@
|
||||
/*
|
||||
* Copyleft Mr. Robot
|
||||
* Copyright HTotoo
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef __WATERFALL_DESIGNER_APP_HPP__
|
||||
#define __WATERFALL_DESIGNER_APP_HPP__
|
||||
|
||||
#include "ui_widget.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_freq_field.hpp"
|
||||
#include "ui_record_view.hpp"
|
||||
#include "ui_spectrum.hpp"
|
||||
#include "app_settings.hpp"
|
||||
#include "radio_state.hpp"
|
||||
#include "file_path.hpp"
|
||||
|
||||
namespace ui::external_app::waterfall_designer {
|
||||
|
||||
enum class ColorComponent {
|
||||
RED,
|
||||
GREEN,
|
||||
BLUE
|
||||
};
|
||||
|
||||
class WaterfallDesignerColorPickerView : public View {
|
||||
public:
|
||||
std::function<void(std::string)> on_save{};
|
||||
|
||||
WaterfallDesignerColorPickerView(NavigationView& nav, std::string color_str);
|
||||
|
||||
std::string title() const override { return "Color Picker"; };
|
||||
void focus() override;
|
||||
void paint(Painter& painter) override;
|
||||
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
std::string color_str_;
|
||||
uint8_t index_{0};
|
||||
uint8_t red_{0};
|
||||
uint8_t green_{0};
|
||||
uint8_t blue_{0};
|
||||
|
||||
void update_color_index();
|
||||
std::string build_color_str();
|
||||
|
||||
Labels labels{
|
||||
{{0 * 8, 0 * 16}, "Index", Theme::getInstance()->fg_light->foreground},
|
||||
{{0 * 8, 2 * 16}, "Red", Theme::getInstance()->fg_light->foreground},
|
||||
{{0 * 8, 4 * 16}, "Green", Theme::getInstance()->fg_light->foreground},
|
||||
{{0 * 8, 6 * 16}, "Blue", Theme::getInstance()->fg_light->foreground},
|
||||
{{0 * 8, 8 * 16}, "Step", Theme::getInstance()->fg_light->foreground}};
|
||||
|
||||
NumberField field_index{
|
||||
{0 * 8, 1 * 16},
|
||||
3,
|
||||
{0, 255},
|
||||
1,
|
||||
' '};
|
||||
|
||||
NumberField field_red{
|
||||
{0 * 8, 3 * 16},
|
||||
3,
|
||||
{0, 255},
|
||||
1,
|
||||
' '};
|
||||
|
||||
NumberField field_green{
|
||||
{0 * 8, 5 * 16},
|
||||
3,
|
||||
{0, 255},
|
||||
1,
|
||||
' '};
|
||||
|
||||
NumberField field_blue{
|
||||
{0 * 8, 7 * 16},
|
||||
3,
|
||||
{0, 255},
|
||||
1,
|
||||
' '};
|
||||
|
||||
NumberField field_step{
|
||||
{0 * 8, 9 * 16},
|
||||
3,
|
||||
{0, 255},
|
||||
1,
|
||||
' '};
|
||||
|
||||
ProgressBar progressbar{
|
||||
{0 * 8,
|
||||
screen_height - 4 * 16 - 2 * 16 - 1 * 16,
|
||||
screen_width,
|
||||
2 * 16}};
|
||||
|
||||
Button button_save{
|
||||
{0, screen_height - 4 * 16, screen_width, 4 * 16},
|
||||
"Save"};
|
||||
};
|
||||
|
||||
class WaterfallDesignerView : public View {
|
||||
public:
|
||||
WaterfallDesignerView(NavigationView& nav);
|
||||
WaterfallDesignerView(NavigationView& nav, ReceiverModel::settings_t override);
|
||||
~WaterfallDesignerView();
|
||||
|
||||
void focus() override;
|
||||
void set_parent_rect(const Rect new_parent_rect) override;
|
||||
|
||||
std::string title() const override { return "Wtf Design"; };
|
||||
|
||||
private:
|
||||
uint32_t capture_rate{500000};
|
||||
uint32_t file_format{0};
|
||||
uint32_t highlighted_index_{0};
|
||||
bool trim{false};
|
||||
std::filesystem::path current_profile_path = "";
|
||||
NavigationView& nav_;
|
||||
RxRadioState radio_state_{ReceiverModel::Mode::Capture};
|
||||
app_settings::SettingsManager settings_{
|
||||
"rx_wtf_designer",
|
||||
app_settings::Mode::RX,
|
||||
{
|
||||
{"capture_rate"sv, &capture_rate},
|
||||
{"file_format"sv, &file_format},
|
||||
{"trim"sv, &trim},
|
||||
}};
|
||||
|
||||
Labels labels{
|
||||
{{0 * 8, 1 * 16}, "Rate:", Theme::getInstance()->fg_light->foreground},
|
||||
{{11 * 8, 1 * 16}, "Format:", Theme::getInstance()->fg_light->foreground},
|
||||
};
|
||||
|
||||
RxFrequencyField field_frequency{
|
||||
{0 * 8, 0 * 16},
|
||||
nav_};
|
||||
|
||||
FrequencyStepView field_frequency_step{
|
||||
{10 * 8, 0 * 16}};
|
||||
|
||||
RFAmpField field_rf_amp{
|
||||
{16 * 8, 0 * 16}};
|
||||
|
||||
LNAGainField field_lna{
|
||||
{18 * 8, 0 * 16}};
|
||||
|
||||
VGAGainField field_vga{
|
||||
{21 * 8, 0 * 16}};
|
||||
|
||||
OptionsField option_bandwidth{
|
||||
{24 * 8, 0 * 16},
|
||||
5,
|
||||
{}};
|
||||
|
||||
MenuView menu_view{};
|
||||
|
||||
NewButton button_new{
|
||||
{0 * 8, 8 * 16, 4 * 8, 32},
|
||||
{},
|
||||
&bitmap_icon_file,
|
||||
Theme::getInstance()->fg_blue->foreground};
|
||||
|
||||
NewButton button_open{
|
||||
{4 * 8, 8 * 16, 4 * 8, 32},
|
||||
{},
|
||||
&bitmap_icon_load,
|
||||
Theme::getInstance()->fg_blue->foreground};
|
||||
|
||||
NewButton button_save{
|
||||
{8 * 8, 8 * 16, 4 * 8, 32},
|
||||
{},
|
||||
&bitmap_icon_save,
|
||||
Theme::getInstance()->fg_blue->foreground};
|
||||
|
||||
NewButton button_add_level{
|
||||
{12 * 8, 8 * 16, 4 * 8, 32},
|
||||
{},
|
||||
&bitmap_icon_add,
|
||||
Theme::getInstance()->fg_blue->foreground};
|
||||
|
||||
NewButton button_remove_level{
|
||||
{16 * 8, 8 * 16, 4 * 8, 32},
|
||||
{},
|
||||
&bitmap_icon_delete,
|
||||
Theme::getInstance()->fg_blue->foreground};
|
||||
|
||||
NewButton button_edit_color{
|
||||
{20 * 8, 8 * 16, 4 * 8, 32},
|
||||
{},
|
||||
&bitmap_icon_notepad,
|
||||
Theme::getInstance()->fg_blue->foreground};
|
||||
|
||||
NewButton button_apply_setting{
|
||||
{24 * 8, 8 * 16, 4 * 8, 32},
|
||||
{},
|
||||
&bitmap_icon_replay,
|
||||
Theme::getInstance()->fg_blue->foreground};
|
||||
|
||||
void backup_current_profile();
|
||||
void restore_current_profile();
|
||||
void on_create_new_profile();
|
||||
void on_open_profile();
|
||||
void on_profile_changed(std::filesystem::path new_profile_path);
|
||||
void on_save_profile();
|
||||
void on_add_level();
|
||||
void on_remove_level();
|
||||
void on_edit_color();
|
||||
|
||||
void refresh_menu_view();
|
||||
|
||||
void on_apply_current_to_wtf(); // will restore if didn't apple, when distruct
|
||||
void on_apply_setting(); // apply set
|
||||
|
||||
bool if_apply_setting{false};
|
||||
/*NB:
|
||||
this works as:
|
||||
each time you change color, it apply as file realtime
|
||||
however if you don't push the apply (play) btn, it would resotore in distructor,
|
||||
if you push apply, it would apply and exit*/
|
||||
|
||||
std::vector<std::string> profile_levels{};
|
||||
|
||||
static constexpr ui::Dim header_height = 10 * 16;
|
||||
|
||||
RecordView record_view{// we still need it cuz it make waterfall correct
|
||||
{screen_width, screen_height, 30 * 8, 1 * 16},
|
||||
u"BBD_????.*",
|
||||
captures_dir,
|
||||
RecordView::FileType::RawS16,
|
||||
16384,
|
||||
3};
|
||||
|
||||
spectrum::WaterfallView waterfall{};
|
||||
std::string file_name_buffer{}; // needed by text_prompy
|
||||
|
||||
MessageHandlerRegistration message_handler_freqchg{
|
||||
Message::ID::FreqChangeCommand,
|
||||
[this](Message* const p) {
|
||||
const auto message = static_cast<const FreqChangeCommandMessage*>(p);
|
||||
this->on_freqchg(message->freq);
|
||||
}};
|
||||
|
||||
void on_freqchg(int64_t freq);
|
||||
};
|
||||
|
||||
} /* namespace ui::external_app::waterfall_designer */
|
||||
|
||||
#endif /*__WATERFALL_DESIGNER_APP_HPP__*/
|
||||
@@ -122,7 +122,7 @@ options_t freqman_steps = {
|
||||
{"1kHz ", 1000},
|
||||
{"5kHz (SA AM)", 5000},
|
||||
{"6.25kHz(NFM)", 6250},
|
||||
{"8.33kHz(AIR)", 8330},
|
||||
{"8.33kHz(AIR)", 8333},
|
||||
{"9kHz (EU AM)", 9000},
|
||||
{"10kHz(US AM)", 10000},
|
||||
{"12.5kHz(NFM)", 12500},
|
||||
@@ -144,7 +144,7 @@ options_t freqman_steps_short = {
|
||||
{"1kHz", 1000},
|
||||
{"5kHz", 5000},
|
||||
{"6.25kHz", 6250},
|
||||
{"8.33kHz", 8330},
|
||||
{"8.33kHz", 8333},
|
||||
{"9kHz", 9000},
|
||||
{"10kHz", 10000},
|
||||
{"12.5kHz", 12500},
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
#include "gpio_lpc.h"
|
||||
|
||||
typedef enum {
|
||||
LED1 = 0,
|
||||
LED2 = 1,
|
||||
LED3 = 2,
|
||||
LED4 = 3,
|
||||
} led_t;
|
||||
|
||||
/* GPIO Output PinMux */
|
||||
static struct gpio_t gpio_led[] = {
|
||||
GPIO(2, 1),
|
||||
GPIO(2, 2),
|
||||
GPIO(2, 8),
|
||||
#ifdef RAD1O
|
||||
GPIO(5, 26),
|
||||
#endif
|
||||
};
|
||||
|
||||
void delay(uint32_t duration) {
|
||||
while (duration--) {
|
||||
/* cannot be optimized out */
|
||||
__asm__ volatile("nop");
|
||||
}
|
||||
}
|
||||
|
||||
void delay_us_at_mhz(uint32_t us, uint32_t mhz) {
|
||||
/* overflow-safe multiply */
|
||||
uint64_t cycles64 = (uint64_t)us * (uint64_t)mhz;
|
||||
if (cycles64 > UINT32_MAX) {
|
||||
cycles64 = UINT32_MAX;
|
||||
}
|
||||
delay((uint32_t)cycles64);
|
||||
}
|
||||
|
||||
void led_on(const led_t led) {
|
||||
gpio_set(&gpio_led[led]);
|
||||
}
|
||||
|
||||
void led_off(const led_t led) {
|
||||
gpio_clear(&gpio_led[led]);
|
||||
}
|
||||
|
||||
void halt_and_flash(const uint32_t duration) {
|
||||
/* blink LED1, LED2, and LED3 */
|
||||
while (1) {
|
||||
led_on(LED1);
|
||||
led_on(LED2);
|
||||
led_on(LED3);
|
||||
delay(duration);
|
||||
led_off(LED1);
|
||||
led_off(LED2);
|
||||
led_off(LED3);
|
||||
delay(duration);
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,14 @@ void Debounce::enable_repeat() {
|
||||
repeat_enabled_ = true;
|
||||
}
|
||||
|
||||
void Debounce::set_enable_repeat(bool enabled) {
|
||||
repeat_enabled_ = enabled;
|
||||
}
|
||||
|
||||
bool Debounce::get_repeat_enabled() {
|
||||
return repeat_enabled_;
|
||||
}
|
||||
|
||||
bool Debounce::get_long_press_enabled() const {
|
||||
return long_press_enabled_;
|
||||
}
|
||||
|
||||
@@ -43,6 +43,8 @@ class Debounce {
|
||||
bool feed(const uint8_t bit);
|
||||
uint8_t state();
|
||||
void enable_repeat();
|
||||
void set_enable_repeat(bool enabled);
|
||||
bool get_repeat_enabled();
|
||||
bool get_long_press_enabled() const;
|
||||
void set_long_press_enabled(bool v);
|
||||
bool long_press_occurred();
|
||||
|
||||
@@ -227,8 +227,8 @@ void controls_init() {
|
||||
gptStart(&GPTD1, &timer0_config);
|
||||
gptStartContinuous(&GPTD1, timer0_match_count);
|
||||
|
||||
// Enable repeat for directional switches only
|
||||
for (auto i = Switch::Right; i <= Switch::Up; incr(i))
|
||||
// Enable repeat for directional and Select switches only
|
||||
for (auto i = Switch::Right; i <= Switch::Sel; incr(i))
|
||||
switch_debounce[toUType(i)].enable_repeat();
|
||||
}
|
||||
|
||||
@@ -250,6 +250,22 @@ SwitchesState get_switches_state() {
|
||||
return result;
|
||||
}
|
||||
|
||||
/* Gets the repeat enabled state for all the switches. */
|
||||
SwitchesState get_switches_repeat_config() {
|
||||
SwitchesState result;
|
||||
|
||||
for (size_t i = 0; i < result.size(); i++)
|
||||
result[i] = switch_debounce[i].get_repeat_enabled();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/* Configures which switches support repeat.*/
|
||||
void set_switches_repeat_config(SwitchesState switch_config) {
|
||||
for (size_t i = 0; i < switch_config.size(); i++)
|
||||
switch_debounce[i].set_enable_repeat(switch_config[i]);
|
||||
}
|
||||
|
||||
/* Gets the long press enabled state for all the switches. */
|
||||
SwitchesState get_switches_long_press_config() {
|
||||
SwitchesState result;
|
||||
|
||||
@@ -48,6 +48,8 @@ uint8_t swizzled_switches();
|
||||
SwitchesState get_switches_state();
|
||||
EncoderPosition get_encoder_position();
|
||||
touch::Frame get_touch_frame();
|
||||
SwitchesState get_switches_repeat_config();
|
||||
void set_switches_repeat_config(SwitchesState switch_config);
|
||||
|
||||
SwitchesState get_switches_long_press_config();
|
||||
void set_switches_long_press_config(SwitchesState switch_config);
|
||||
|
||||
@@ -38,3 +38,11 @@ Optional<File::Error> LogFile::write_raw(const std::string& message) {
|
||||
}
|
||||
return error;
|
||||
}
|
||||
|
||||
Optional<File::Error> LogFile::write_raw_no_newline(const std::string& message) {
|
||||
auto result_s = file.write(message.c_str(), message.size());
|
||||
if (!result_s.is_error()) {
|
||||
file.sync();
|
||||
}
|
||||
return {result_s.error()};
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ class LogFile {
|
||||
Optional<File::Error> write_entry(const std::string& entry);
|
||||
Optional<File::Error> write_entry(const rtc::RTC& datetime, const std::string& entry);
|
||||
Optional<File::Error> write_raw(const std::string& message);
|
||||
Optional<File::Error> write_raw_no_newline(const std::string& message);
|
||||
|
||||
private:
|
||||
File file{};
|
||||
|
||||
@@ -31,20 +31,67 @@ using namespace ax25;
|
||||
|
||||
namespace aprs {
|
||||
|
||||
void make_aprs_frame(const char* src_address, const uint32_t src_ssid, const char* dest_address, const uint32_t dest_ssid, const std::string& payload) {
|
||||
void make_aprs_frame(const char* src_address, const uint32_t src_ssid, const char* dest_address, const uint32_t dest_ssid, const std::string& payload, const std::string& path) {
|
||||
AX25Frame frame;
|
||||
|
||||
char address[14] = {0};
|
||||
memset(address, ' ', 14);
|
||||
|
||||
memcpy(&address[0], dest_address, 6);
|
||||
memcpy(&address[7], src_address, 6);
|
||||
size_t dest_len = strlen(dest_address);
|
||||
if (dest_len > 6) dest_len = 6;
|
||||
memcpy(&address[0], dest_address, dest_len);
|
||||
size_t src_len = strlen(src_address);
|
||||
if (src_len > 6) src_len = 6;
|
||||
memcpy(&address[7], src_address, src_len);
|
||||
// euquiq: According to ax.25 doc section 2.2.13.x.x and 2.4.1.2
|
||||
// SSID need bits 5.6 set, so later when shifted it will end up being 011xxxx0 (xxxx = SSID number)
|
||||
// Notice that if need to signal usage of AX.25 V2.0, (dest_ssid | 112); (MSb will need to be set at the end)
|
||||
address[6] = (dest_ssid | 48);
|
||||
address[13] = (src_ssid | 48);
|
||||
// SSID need bits 5.6 set, so later when shifted it will end up being 011xxxx0 (xxxx = SSID number)
|
||||
// Notice that if need to signal usage of AX.25 V2.0, (dest_ssid | 112); (MSb will need to be set at the end)
|
||||
address[6] = (dest_ssid | 0x30);
|
||||
address[13] = (src_ssid | 0x30);
|
||||
|
||||
frame.make_ui_frame(address, 0x03, protocol_id_t::NO_LAYER3, payload);
|
||||
// fix path ssid bits. the result will be the same as above
|
||||
std::string fixed_path = "";
|
||||
|
||||
const char* p = path.c_str();
|
||||
|
||||
while (*p) {
|
||||
while (*p == ' ') p++;
|
||||
if (!*p) break;
|
||||
|
||||
char call[6] = {' ', ' ', ' ', ' ', ' ', ' '};
|
||||
int idx = 0;
|
||||
|
||||
while (*p && *p != '-' && *p != ',') {
|
||||
if (*p != ' ') {
|
||||
if (idx < 6) {
|
||||
char c = *p;
|
||||
if (c >= 'a' && c <= 'z') c -= 32;
|
||||
call[idx++] = c;
|
||||
}
|
||||
}
|
||||
p++;
|
||||
}
|
||||
|
||||
int ssid = 0;
|
||||
if (*p == '-') {
|
||||
p++;
|
||||
while (*p == ' ') p++;
|
||||
while (*p >= '0' && *p <= '9') {
|
||||
ssid = ssid * 10 + (*p - '0');
|
||||
p++;
|
||||
}
|
||||
}
|
||||
|
||||
if (ssid >= 0 && ssid <= 15) {
|
||||
for (int i = 0; i < 6; i++) fixed_path += call[i];
|
||||
fixed_path += (char)(ssid | 0x30);
|
||||
}
|
||||
|
||||
while (*p && *p != ',') p++;
|
||||
if (*p == ',') p++;
|
||||
}
|
||||
|
||||
frame.make_ui_frame(address, 0x03, protocol_id_t::NO_LAYER3, payload, fixed_path);
|
||||
}
|
||||
|
||||
} /* namespace aprs */
|
||||
|
||||
@@ -33,7 +33,8 @@ void make_aprs_frame(
|
||||
const uint32_t src_ssid,
|
||||
const char* dest_address,
|
||||
const uint32_t dest_ssid,
|
||||
const std::string& payload);
|
||||
const std::string& payload,
|
||||
const std::string& path = "");
|
||||
|
||||
} /* namespace aprs */
|
||||
|
||||
|
||||
@@ -26,13 +26,16 @@
|
||||
|
||||
namespace ax25 {
|
||||
|
||||
void AX25Frame::make_extended_field(char* const data, size_t length) {
|
||||
void AX25Frame::make_extended_field(char* const data, size_t length, bool is_last) {
|
||||
size_t i = 0;
|
||||
|
||||
for (i = 0; i < length - 1; i++)
|
||||
add_data(data[i] << 1);
|
||||
|
||||
add_data((data[i] << 1) | 1);
|
||||
if (is_last)
|
||||
add_data((data[i] << 1) | 1);
|
||||
else
|
||||
add_data((data[i] << 1));
|
||||
}
|
||||
|
||||
void AX25Frame::NRZI_add_bit(const uint32_t bit) {
|
||||
@@ -92,7 +95,7 @@ void AX25Frame::add_checksum() {
|
||||
add_byte(checksum >> 8, false, false);
|
||||
}
|
||||
|
||||
void AX25Frame::make_ui_frame(char* const address, const uint8_t control, const uint8_t protocol, const std::string& info) {
|
||||
void AX25Frame::make_ui_frame(char* const address, const uint8_t control, const uint8_t protocol, const std::string& info, const std::string& path) {
|
||||
size_t i;
|
||||
|
||||
bb_data_ptr = (uint16_t*)shared_memory.bb_data.data;
|
||||
@@ -103,22 +106,27 @@ void AX25Frame::make_ui_frame(char* const address, const uint8_t control, const
|
||||
ones_counter = 0;
|
||||
crc_ccitt.reset();
|
||||
|
||||
add_flag();
|
||||
add_flag();
|
||||
add_flag();
|
||||
add_flag();
|
||||
add_flag(); // 0x73
|
||||
add_flag(); // 0x73
|
||||
add_flag(); // 0x73
|
||||
add_flag(); // 0x73
|
||||
|
||||
make_extended_field(address, 14);
|
||||
add_data(control);
|
||||
add_data(protocol);
|
||||
// path must not contain the - character! just the callsign and SSID next to each other
|
||||
bool has_path = (path.size() > 0 && path.size() <= 63 && path.size() % 7 == 0);
|
||||
make_extended_field(address, 14, !has_path);
|
||||
if (has_path) {
|
||||
make_extended_field(const_cast<char*>(path.data()), path.size(), true);
|
||||
}
|
||||
add_data(control); // 0x03
|
||||
add_data(protocol); // 0xf0
|
||||
|
||||
for (i = 0; i < info.size(); i++)
|
||||
add_data(info[i]);
|
||||
|
||||
add_checksum();
|
||||
add_checksum(); // fcs
|
||||
|
||||
add_flag();
|
||||
add_flag();
|
||||
add_flag(); // 0x73
|
||||
add_flag(); // 0x73
|
||||
|
||||
flush();
|
||||
}
|
||||
|
||||
@@ -43,11 +43,11 @@ enum protocol_id_t {
|
||||
|
||||
class AX25Frame {
|
||||
public:
|
||||
void make_ui_frame(char* const address, const uint8_t control, const uint8_t protocol, const std::string& info);
|
||||
void make_ui_frame(char* const address, const uint8_t control, const uint8_t protocol, const std::string& info, const std::string& path = "");
|
||||
|
||||
private:
|
||||
void NRZI_add_bit(const uint32_t bit);
|
||||
void make_extended_field(char* const data, size_t length);
|
||||
void make_extended_field(char* const data, size_t length, bool is_last = false);
|
||||
void add_byte(uint8_t byte, bool is_flag, bool is_data);
|
||||
void add_data(uint8_t byte);
|
||||
void add_checksum();
|
||||
|
||||
@@ -206,13 +206,6 @@ bool set_tuning_frequency(const rf::Frequency frequency) {
|
||||
|
||||
void set_rf_amp(const bool rf_amp) {
|
||||
rf_path.set_rf_amp(rf_amp);
|
||||
|
||||
if (direction == rf::Direction::Transmit) {
|
||||
if (rf_amp)
|
||||
led_tx.on();
|
||||
else
|
||||
led_tx.off();
|
||||
}
|
||||
}
|
||||
|
||||
void set_lna_gain(const int_fast8_t db) {
|
||||
|
||||
@@ -252,8 +252,6 @@ void ReceiverModel::enable() {
|
||||
|
||||
// TODO: maybe not the perfect place for this, but it's reasonable.
|
||||
update_headphone_volume();
|
||||
|
||||
led_rx.on();
|
||||
}
|
||||
|
||||
void ReceiverModel::disable() {
|
||||
@@ -262,7 +260,6 @@ void ReceiverModel::disable() {
|
||||
// TODO: Responsibility for enabling/disabling the radio is muddy.
|
||||
// Some happens in ReceiverModel, some inside radio namespace.
|
||||
radio::disable();
|
||||
led_rx.off();
|
||||
}
|
||||
|
||||
void ReceiverModel::initialize() {
|
||||
@@ -306,7 +303,8 @@ int32_t ReceiverModel::tuning_offset() {
|
||||
|
||||
void ReceiverModel::update_tuning_frequency() {
|
||||
// TODO: use positive offset if freq < offset.
|
||||
radio::set_tuning_frequency(target_frequency() + hidden_offset + tuning_offset());
|
||||
if (enabled_)
|
||||
radio::set_tuning_frequency(target_frequency() + hidden_offset + tuning_offset());
|
||||
}
|
||||
|
||||
void ReceiverModel::set_hidden_offset(rf::Frequency offset) {
|
||||
@@ -315,7 +313,8 @@ void ReceiverModel::set_hidden_offset(rf::Frequency offset) {
|
||||
}
|
||||
|
||||
void ReceiverModel::update_baseband_bandwidth() {
|
||||
radio::set_baseband_filter_bandwidth_rx(baseband_bandwidth());
|
||||
if (enabled_)
|
||||
radio::set_baseband_filter_bandwidth_rx(baseband_bandwidth());
|
||||
}
|
||||
|
||||
void ReceiverModel::update_sampling_rate() {
|
||||
@@ -324,23 +323,31 @@ void ReceiverModel::update_sampling_rate() {
|
||||
// protocols that need quick RX/TX turn-around.
|
||||
|
||||
// Disabling baseband while changing sampling rates seems like a good idea...
|
||||
radio::set_baseband_rate(sampling_rate());
|
||||
if (enabled_)
|
||||
radio::set_baseband_rate(sampling_rate());
|
||||
|
||||
update_tuning_frequency();
|
||||
}
|
||||
|
||||
void ReceiverModel::update_lna() {
|
||||
radio::set_lna_gain(lna());
|
||||
if (enabled_)
|
||||
radio::set_lna_gain(lna());
|
||||
}
|
||||
|
||||
void ReceiverModel::update_vga() {
|
||||
radio::set_vga_gain(vga());
|
||||
if (enabled_)
|
||||
radio::set_vga_gain(vga());
|
||||
}
|
||||
|
||||
void ReceiverModel::update_rf_amp() {
|
||||
radio::set_rf_amp(rf_amp());
|
||||
if (enabled_)
|
||||
radio::set_rf_amp(rf_amp());
|
||||
}
|
||||
|
||||
void ReceiverModel::update_modulation() {
|
||||
if (!enabled_)
|
||||
return;
|
||||
|
||||
switch (modulation()) {
|
||||
default:
|
||||
case Mode::AMAudio:
|
||||
@@ -395,5 +402,6 @@ void ReceiverModel::update_antenna_bias() {
|
||||
}
|
||||
|
||||
void ReceiverModel::update_headphone_volume() {
|
||||
audio::headphone::set_volume(headphone_volume());
|
||||
if (enabled_)
|
||||
audio::headphone::set_volume(headphone_volume());
|
||||
}
|
||||
|
||||
@@ -32,6 +32,10 @@
|
||||
#include "chprintf.h"
|
||||
#include "portapack.hpp"
|
||||
|
||||
extern "C" {
|
||||
#include "platform_detect.h"
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Shell termination event source.
|
||||
*/
|
||||
@@ -65,8 +69,42 @@ static void list_commands(BaseSequentialStream* chp, const ShellCommand* scp) {
|
||||
}
|
||||
}
|
||||
|
||||
static const char* get_board_revision_string(board_rev_t rev) {
|
||||
switch (rev) {
|
||||
case BOARD_REV_HACKRF1_OLD:
|
||||
return "HackRF R1-R5";
|
||||
case BOARD_REV_HACKRF1_R6:
|
||||
return "HackRF R6";
|
||||
case BOARD_REV_HACKRF1_R7:
|
||||
return "HackRF R7";
|
||||
case BOARD_REV_HACKRF1_R8:
|
||||
return "HackRF R8";
|
||||
case BOARD_REV_HACKRF1_R9:
|
||||
return "HackRF R9";
|
||||
case BOARD_REV_HACKRF1_R10:
|
||||
return "HackRF R10";
|
||||
case BOARD_REV_GSG_HACKRF1_R6:
|
||||
return "GSG HackRF R6";
|
||||
case BOARD_REV_GSG_HACKRF1_R7:
|
||||
return "GSG HackRF R7";
|
||||
case BOARD_REV_GSG_HACKRF1_R8:
|
||||
return "GSG HackRF R8";
|
||||
case BOARD_REV_GSG_HACKRF1_R9:
|
||||
return "GSG HackRF R9";
|
||||
case BOARD_REV_GSG_HACKRF1_R10:
|
||||
return "GSG HackRF R10";
|
||||
case BOARD_REV_UNRECOGNIZED:
|
||||
return "Unrecognized";
|
||||
case BOARD_REV_UNDETECTED:
|
||||
return "Undetected";
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
static void cmd_info(BaseSequentialStream* chp, int argc, char* argv[]) {
|
||||
(void)argv;
|
||||
|
||||
if (argc > 0) {
|
||||
usage(chp, const_cast<char*>("info"));
|
||||
return;
|
||||
@@ -92,7 +130,11 @@ static void cmd_info(BaseSequentialStream* chp, int argc, char* argv[]) {
|
||||
#ifdef VERSION_STRING
|
||||
chprintf(chp, "Mayhem Version: %s\r\n", VERSION_STRING);
|
||||
#endif
|
||||
chprintf(chp, "HackRF Board Rev: %s\r\n", hackrf_r9 ? "R9" : "R1-R8");
|
||||
|
||||
detect_hardware_platform();
|
||||
board_rev_t revision = detected_revision();
|
||||
const char* revision_string = get_board_revision_string(revision);
|
||||
chprintf(chp, "HackRF Board Rev: %s\r\n", revision_string);
|
||||
chprintf(chp, "Reference Source: %s\r\n", portapack::clock_manager.get_source().c_str());
|
||||
chprintf(chp, "Reference Freq: %s\r\n", portapack::clock_manager.get_freq().c_str());
|
||||
#ifdef __DATE__
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -657,7 +657,13 @@ void GeoMap::move(const float lon, const float lat) {
|
||||
} else {
|
||||
if (is_changed) {
|
||||
set_osm_max_zoom();
|
||||
redraw_map = true;
|
||||
double global_center_px = lon_to_pixel_x_tile(lon_, map_osm_real_zoom);
|
||||
double global_center_py = lat_to_pixel_y_tile(lat_, map_osm_real_zoom);
|
||||
// Redraw if viewport moved by at least 1 pixel (includes zoom level changes)
|
||||
if (abs(global_center_px - (r.width() / 2.0) - viewport_top_left_px) >= 1.0 ||
|
||||
abs(global_center_py - (r.height() / 2.0) - viewport_top_left_py) >= 1.0) {
|
||||
redraw_map = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include "string_format.hpp"
|
||||
#include "ui_receiver.hpp"
|
||||
#include "ui_freqman.hpp"
|
||||
#include "audio.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
|
||||
@@ -591,8 +592,13 @@ AudioVolumeField::AudioVolumeField(
|
||||
/* fill char */ ' '} {
|
||||
set_value(receiver_model.normalized_headphone_volume());
|
||||
|
||||
on_change = [](int32_t v) {
|
||||
receiver_model.set_normalized_headphone_volume(v);
|
||||
on_change = [](int32_t vol) {
|
||||
// don't call receiver model, because this widget shuld be able to handle volume settinsg from any app, regardless of the receiver model's enables state. like the tx apps should be able to set volume too.
|
||||
// this is identical to the receiver model's method
|
||||
uint8_t v = clip<uint8_t>(vol, 0, 99);
|
||||
auto new_volume = volume_t::decibel(v - 99) + audio::headphone::volume_range().max;
|
||||
persistent_memory::set_headphone_volume(new_volume);
|
||||
audio::headphone::set_volume(new_volume);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -332,6 +332,10 @@ WaterfallView::WaterfallView(const bool cursor) {
|
||||
}
|
||||
};
|
||||
|
||||
load_gradient();
|
||||
}
|
||||
|
||||
void WaterfallView::load_gradient() {
|
||||
if (!waterfall_widget.gradient.load_file(default_gradient_file)) {
|
||||
waterfall_widget.gradient.set_default();
|
||||
}
|
||||
|
||||
@@ -146,6 +146,7 @@ class WaterfallView : public View {
|
||||
|
||||
void set_parent_rect(const Rect new_parent_rect) override;
|
||||
void show_audio_spectrum_view(const bool show);
|
||||
void load_gradient();
|
||||
|
||||
private:
|
||||
void update_widgets_rect();
|
||||
|
||||
@@ -49,10 +49,9 @@
|
||||
#include "ui_rds.hpp"
|
||||
#include "ui_recon.hpp"
|
||||
// #include "ui_scanner.hpp"
|
||||
#include "ui_sd_over_usb.hpp"
|
||||
// #include "ui_sd_over_usb.hpp"
|
||||
#include "ui_search.hpp"
|
||||
#include "ui_settings.hpp"
|
||||
#include "ui_siggen.hpp"
|
||||
#include "ui_sonde.hpp"
|
||||
#include "ui_ss_viewer.hpp"
|
||||
// #include "ui_test.hpp"
|
||||
@@ -114,7 +113,7 @@ const NavigationView::AppList NavigationView::appList = {
|
||||
/* HOME ******************************************************************/
|
||||
{nullptr, "Receive", HOME, Color::cyan(), &bitmap_icon_receivers, new ViewFactory<ReceiversMenuView>()},
|
||||
{nullptr, "Transmit", HOME, Color::cyan(), &bitmap_icon_transmit, new ViewFactory<TransmittersMenuView>()},
|
||||
{nullptr, "Tranceiver", HOME, Color::cyan(), &bitmap_icon_tranceivers, new ViewFactory<TranceiversMenuView>()},
|
||||
{nullptr, "Transceiver", HOME, Color::cyan(), &bitmap_icon_transceivers, new ViewFactory<TransceiversMenuView>()},
|
||||
{"recon", "Recon", HOME, Color::green(), &bitmap_icon_scanner, new ViewFactory<ReconView>()},
|
||||
{"capture", "Capture", HOME, Color::red(), &bitmap_icon_capture, new ViewFactory<CaptureAppView>()},
|
||||
{"replay", "Replay", HOME, Color::green(), &bitmap_icon_replay, new ViewFactory<PlaylistView>()},
|
||||
@@ -140,7 +139,6 @@ const NavigationView::AppList NavigationView::appList = {
|
||||
{"pocsagtx", "POCSAG TX", TX, ui::Color::green(), &bitmap_icon_pocsag, new ViewFactory<POCSAGTXView>()},
|
||||
{"rdstx", "RDS", TX, ui::Color::green(), &bitmap_icon_rds, new ViewFactory<RDSView>()},
|
||||
{"touchtune", "TouchTune", TX, ui::Color::green(), &bitmap_icon_touchtunes, new ViewFactory<TouchTunesView>()},
|
||||
{"signalgen", "SignalGen", TX, Color::green(), &bitmap_icon_cwgen, new ViewFactory<SigGenView>()},
|
||||
/* TRX ********************************************************************/
|
||||
{"microphone", "Mic", TRX, Color::green(), &bitmap_icon_microphone, new ViewFactory<MicTXView>()},
|
||||
/* UTILITIES *************************************************************/
|
||||
@@ -148,7 +146,7 @@ const NavigationView::AppList NavigationView::appList = {
|
||||
{"freqman", "Freq. Manager", UTILITIES, Color::green(), &bitmap_icon_freqman, new ViewFactory<FrequencyManagerView>()},
|
||||
{"iqtrim", "IQ Trim", UTILITIES, Color::orange(), &bitmap_icon_trim, new ViewFactory<IQTrimView>()},
|
||||
{"notepad", "Notepad", UTILITIES, Color::dark_cyan(), &bitmap_icon_notepad, new ViewFactory<TextEditorView>()},
|
||||
{nullptr, "SD Over USB", UTILITIES, Color::yellow(), &bitmap_icon_hackrf, new ViewFactory<SdOverUsbView>()},
|
||||
//{nullptr, "SD Over USB", UTILITIES, Color::yellow(), &bitmap_icon_hackrf, new ViewFactory<SdOverUsbView>()},
|
||||
{nullptr, "Debug", UTILITIES, Color::light_grey(), &bitmap_icon_debug, new ViewFactory<DebugMenuView>()},
|
||||
//{"testapp", "Test App", UTILITIES, Color::dark_grey(), nullptr, new ViewFactory<TestView>()},
|
||||
// Dangerous apps.
|
||||
@@ -320,6 +318,14 @@ SystemStatusView::SystemStatusView(
|
||||
refresh();
|
||||
}
|
||||
|
||||
void SystemStatusView::on_tx_disabled() {
|
||||
if (!nav_.is_valid())
|
||||
return;
|
||||
|
||||
nav_.pop();
|
||||
nav_.display_modal("Error", "RF transmit disabled.\nApplication closed.");
|
||||
}
|
||||
|
||||
// when battery icon / text is clicked
|
||||
void SystemStatusView::on_battery_details() {
|
||||
if (!nav_.is_valid()) return;
|
||||
@@ -601,7 +607,7 @@ bool InformationView::firmware_checksum_error() {
|
||||
|
||||
// only checking firmware checksum once per boot
|
||||
if (!fw_checksum_checked) {
|
||||
fw_checksum_error = (simple_checksum(FLASH_STARTING_ADDRESS, FLASH_ROM_SIZE) != FLASH_EXPECTED_CHECKSUM);
|
||||
fw_checksum_error = (simple_checksum(FLASH_STARTING_ADDRESS, FLASH_SIZE_LIMIT_MB * 1024 * 1024) != FLASH_EXPECTED_CHECKSUM);
|
||||
}
|
||||
return fw_checksum_error;
|
||||
}
|
||||
@@ -837,12 +843,12 @@ void TransmittersMenuView::on_populate() {
|
||||
add_external_items(nav_, app_location_t::TX, *this, return_icon ? 1 : 0);
|
||||
}
|
||||
|
||||
/* TranceiversMenuView **************************************************/
|
||||
/* TransceiversMenuView **************************************************/
|
||||
|
||||
TranceiversMenuView::TranceiversMenuView(NavigationView& nav)
|
||||
TransceiversMenuView::TransceiversMenuView(NavigationView& nav)
|
||||
: nav_(nav) {}
|
||||
|
||||
void TranceiversMenuView::on_populate() {
|
||||
void TransceiversMenuView::on_populate() {
|
||||
bool return_icon = pmem::show_gui_return_icon();
|
||||
if (return_icon) {
|
||||
add_items({{"..", Theme::getInstance()->fg_light->foreground, &bitmap_icon_previous, [this]() { nav_.pop(); }}});
|
||||
|
||||
@@ -303,6 +303,7 @@ class SystemStatusView : public View {
|
||||
void on_title();
|
||||
void refresh();
|
||||
void on_clk();
|
||||
void on_tx_disabled();
|
||||
void rtc_battery_workaround();
|
||||
void on_battery_data(const BatteryStateMessage* msg);
|
||||
void on_battery_details();
|
||||
@@ -314,6 +315,13 @@ class SystemStatusView : public View {
|
||||
this->refresh();
|
||||
}};
|
||||
|
||||
MessageHandlerRegistration message_handler_tx_disabled{
|
||||
Message::ID::TXDisabled,
|
||||
[this](const Message* const p) {
|
||||
(void)p;
|
||||
this->on_tx_disabled();
|
||||
}};
|
||||
|
||||
MessageHandlerRegistration message_handler_battery{
|
||||
Message::ID::BatteryStateData,
|
||||
[this](const Message* const p) {
|
||||
@@ -381,10 +389,10 @@ class TransmittersMenuView : public BtnGridView {
|
||||
void on_populate() override;
|
||||
};
|
||||
|
||||
class TranceiversMenuView : public BtnGridView {
|
||||
class TransceiversMenuView : public BtnGridView {
|
||||
public:
|
||||
TranceiversMenuView(NavigationView& nav);
|
||||
std::string title() const override { return "Tranceiver"; };
|
||||
TransceiversMenuView(NavigationView& nav);
|
||||
std::string title() const override { return "Transceiver"; };
|
||||
|
||||
private:
|
||||
NavigationView& nav_;
|
||||
|
||||
@@ -71,9 +71,9 @@ void nvic_enable_irq(uint8_t irqn) {
|
||||
void usb_configuration_changed(usb_device_t* const device) {
|
||||
(void)device;
|
||||
|
||||
usb_endpoint_init(&usb_endpoint_int_in);
|
||||
usb_endpoint_init(&usb_endpoint_bulk_in);
|
||||
usb_endpoint_init(&usb_endpoint_bulk_out);
|
||||
usb_endpoint_init(&usb_endpoint_int_in, false);
|
||||
usb_endpoint_init(&usb_endpoint_bulk_in, false);
|
||||
usb_endpoint_init(&usb_endpoint_bulk_out, false);
|
||||
}
|
||||
|
||||
void setup_usb_serial_controller(void) {
|
||||
@@ -88,8 +88,8 @@ void setup_usb_serial_controller(void) {
|
||||
usb_queue_init(&usb_endpoint_bulk_out_queue);
|
||||
usb_queue_init(&usb_endpoint_bulk_in_queue);
|
||||
|
||||
usb_endpoint_init(&usb_endpoint_control_out);
|
||||
usb_endpoint_init(&usb_endpoint_control_in);
|
||||
usb_endpoint_init(&usb_endpoint_control_out, false);
|
||||
usb_endpoint_init(&usb_endpoint_control_in, false);
|
||||
|
||||
usb_run(&usb_device);
|
||||
}
|
||||
|
||||
@@ -1383,6 +1383,25 @@ static void cmd_getres(BaseSequentialStream* chp, int argc, char* argv[]) {
|
||||
chprintf(chp, res.c_str());
|
||||
}
|
||||
|
||||
static void cmd_getflash(BaseSequentialStream* chp, int argc, char* argv[]) {
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
uint8_t allowrun = FLASH_SIZE_MB;
|
||||
if (portapack::device_type == portapack::DeviceType::DEV_PORTAPACK) {
|
||||
allowrun = 1;
|
||||
}
|
||||
std::string res = "ALLOWEDFW:" + to_string_dec_uint(FLASH_SIZE_MB) + "\r\nALLOWEDRUNTIME:" + to_string_dec_uint(allowrun) + "\r\nCURRENT:" + to_string_dec_uint(FLASH_SIZE_LIMIT_MB) + "\r\nok\r\n";
|
||||
chprintf(chp, res.c_str());
|
||||
}
|
||||
|
||||
static void cmd_getdevtype(BaseSequentialStream* chp, int argc, char* argv[]) {
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
std::string res = portapack::device_type == portapack::DeviceType::DEV_PORTARF ? "PORTARF" : "PORTAPACK";
|
||||
res += "\r\nok\r\n";
|
||||
chprintf(chp, res.c_str());
|
||||
}
|
||||
|
||||
static const ShellCommand commands[] = {
|
||||
{"reboot", cmd_reboot},
|
||||
{"dfu", cmd_dfu},
|
||||
@@ -1419,6 +1438,8 @@ static const ShellCommand commands[] = {
|
||||
{"asyncmsg", cmd_asyncmsg},
|
||||
{"setfreq", cmd_setfreq},
|
||||
{"getres", cmd_getres},
|
||||
{"getflash", cmd_getflash},
|
||||
{"getdevtype", cmd_getdevtype},
|
||||
{NULL, NULL}};
|
||||
|
||||
static const ShellConfig shell_cfg1 = {
|
||||
|
||||
@@ -447,14 +447,6 @@ set(MODE_CPPSRC
|
||||
)
|
||||
DeclareTargets(PREP replay)
|
||||
|
||||
### Signal generator
|
||||
|
||||
set(MODE_CPPSRC
|
||||
proc_siggen.cpp
|
||||
)
|
||||
DeclareTargets(PSIG siggen)
|
||||
|
||||
|
||||
|
||||
### Tones
|
||||
|
||||
@@ -505,53 +497,6 @@ set(MODE_CPPSRC
|
||||
)
|
||||
DeclareTargets(PFUT flash_utility)
|
||||
|
||||
### SD over USB
|
||||
|
||||
set(MODE_INCDIR
|
||||
${HACKRF_PATH}/firmware
|
||||
${HACKRF_PATH}/firmware/common
|
||||
${HACKRF_PATH}/firmware/libopencm3/include
|
||||
)
|
||||
set(MODE_CPPSRC
|
||||
sd_over_usb/proc_sd_over_usb.cpp
|
||||
|
||||
sd_over_usb/scsi.c
|
||||
sd_over_usb/diskio.c
|
||||
sd_over_usb/sd_over_usb.c
|
||||
sd_over_usb/usb_descriptor.c
|
||||
sd_over_usb/hackrf_core.c
|
||||
|
||||
${HACKRF_PATH}/firmware/common/usb.c
|
||||
${HACKRF_PATH}/firmware/common/usb_queue.c
|
||||
${HACKRF_PATH}/firmware/hackrf_usb/usb_device.c
|
||||
${HACKRF_PATH}/firmware/hackrf_usb/usb_endpoint.c
|
||||
${HACKRF_PATH}/firmware/common/usb_request.c
|
||||
${HACKRF_PATH}/firmware/common/usb_standard_request.c
|
||||
${HACKRF_PATH}/firmware/common/platform_detect.c
|
||||
${HACKRF_PATH}/firmware/common/gpio_lpc.c
|
||||
${HACKRF_PATH}/firmware/common/firmware_info.c
|
||||
${HACKRF_PATH}/firmware/common/si5351c.c
|
||||
${HACKRF_PATH}/firmware/common/i2c_bus.c
|
||||
${HACKRF_PATH}/firmware/common/mixer.c
|
||||
${HACKRF_PATH}/firmware/common/clkin.c
|
||||
${HACKRF_PATH}/firmware/common/spi_bus.c
|
||||
${HACKRF_PATH}/firmware/common/sgpio.c
|
||||
${HACKRF_PATH}/firmware/common/rf_path.c
|
||||
${HACKRF_PATH}/firmware/common/i2c_lpc.c
|
||||
${HACKRF_PATH}/firmware/common/spi_ssp.c
|
||||
${HACKRF_PATH}/firmware/common/rffc5071_spi.c
|
||||
${HACKRF_PATH}/firmware/common/rffc5071.c
|
||||
${HACKRF_PATH}/firmware/common/clkin.c
|
||||
${HACKRF_PATH}/firmware/common/gpdma.c
|
||||
|
||||
${HACKRF_PATH}/firmware/libopencm3/lib/cm3/nvic.c
|
||||
${HACKRF_PATH}/firmware/libopencm3/lib/cm3/sync.c
|
||||
${HACKRF_PATH}/firmware/libopencm3/lib/lpc43xx/scu.c
|
||||
${HACKRF_PATH}/firmware/libopencm3/lib/lpc43xx/timer.c
|
||||
${HACKRF_PATH}/firmware/libopencm3/lib/lpc43xx/i2c.c
|
||||
)
|
||||
DeclareTargets(PUSB sd_over_usb)
|
||||
|
||||
### Place external app and disabled images below so they don't get added to the firmware
|
||||
set(add_to_firmware FALSE)
|
||||
set(MODE_FLAGS "-O3")
|
||||
@@ -673,14 +618,21 @@ set(MODE_CPPSRC
|
||||
)
|
||||
DeclareTargets(PADT adsbtx)
|
||||
|
||||
### OOKStream
|
||||
### Binary Timed stream TX
|
||||
|
||||
set(MODE_CPPSRC
|
||||
proc_ook_stream_tx.cpp
|
||||
proc_bint_stream_tx.cpp
|
||||
)
|
||||
DeclareTargets(POSK ookstream)
|
||||
DeclareTargets(POSK bintstream)
|
||||
|
||||
|
||||
### Signal generator
|
||||
|
||||
set(MODE_CPPSRC
|
||||
proc_siggen.cpp
|
||||
)
|
||||
DeclareTargets(PSIG siggen)
|
||||
|
||||
|
||||
### WeFax Rx
|
||||
|
||||
@@ -711,6 +663,70 @@ set(MODE_CPPSRC
|
||||
)
|
||||
DeclareTargets(PSCD subcar)
|
||||
|
||||
### Morse RX Decoder
|
||||
|
||||
set(MODE_CPPSRC
|
||||
proc_morse.cpp
|
||||
)
|
||||
DeclareTargets(PMRS morse)
|
||||
|
||||
### SD over USB
|
||||
|
||||
set(MODE_INCDIR
|
||||
${HACKRF_PATH}/firmware
|
||||
${HACKRF_PATH}/firmware/common
|
||||
${HACKRF_PATH}/firmware/libopencm3/include
|
||||
)
|
||||
|
||||
### Morse TX
|
||||
|
||||
set(MODE_CPPSRC
|
||||
proc_morsetx.cpp
|
||||
)
|
||||
DeclareTargets(PMRT morsetx)
|
||||
|
||||
set(MODE_CPPSRC
|
||||
sd_over_usb/proc_sd_over_usb.cpp
|
||||
|
||||
sd_over_usb/scsi.c
|
||||
sd_over_usb/diskio.c
|
||||
sd_over_usb/sd_over_usb.c
|
||||
sd_over_usb/usb_descriptor.c
|
||||
sd_over_usb/hackrf_core.c
|
||||
|
||||
${HACKRF_PATH}/firmware/common/adc.c
|
||||
${HACKRF_PATH}/firmware/common/selftest.c
|
||||
${HACKRF_PATH}/firmware/common/usb.c
|
||||
${HACKRF_PATH}/firmware/common/usb_queue.c
|
||||
${HACKRF_PATH}/firmware/hackrf_usb/usb_device.c
|
||||
${HACKRF_PATH}/firmware/hackrf_usb/usb_endpoint.c
|
||||
${HACKRF_PATH}/firmware/common/usb_request.c
|
||||
${HACKRF_PATH}/firmware/common/usb_standard_request.c
|
||||
${HACKRF_PATH}/firmware/common/platform_detect.c
|
||||
${HACKRF_PATH}/firmware/common/gpio_lpc.c
|
||||
${HACKRF_PATH}/firmware/common/firmware_info.c
|
||||
${HACKRF_PATH}/firmware/common/si5351c.c
|
||||
${HACKRF_PATH}/firmware/common/i2c_bus.c
|
||||
${HACKRF_PATH}/firmware/common/mixer.c
|
||||
${HACKRF_PATH}/firmware/common/clkin.c
|
||||
${HACKRF_PATH}/firmware/common/spi_bus.c
|
||||
${HACKRF_PATH}/firmware/common/sgpio.c
|
||||
${HACKRF_PATH}/firmware/common/rf_path.c
|
||||
${HACKRF_PATH}/firmware/common/i2c_lpc.c
|
||||
${HACKRF_PATH}/firmware/common/spi_ssp.c
|
||||
${HACKRF_PATH}/firmware/common/rffc5071_spi.c
|
||||
${HACKRF_PATH}/firmware/common/rffc5071.c
|
||||
${HACKRF_PATH}/firmware/common/clkin.c
|
||||
${HACKRF_PATH}/firmware/common/gpdma.c
|
||||
|
||||
${HACKRF_PATH}/firmware/libopencm3/lib/cm3/nvic.c
|
||||
${HACKRF_PATH}/firmware/libopencm3/lib/cm3/sync.c
|
||||
${HACKRF_PATH}/firmware/libopencm3/lib/lpc43xx/scu.c
|
||||
${HACKRF_PATH}/firmware/libopencm3/lib/lpc43xx/timer.c
|
||||
${HACKRF_PATH}/firmware/libopencm3/lib/lpc43xx/i2c.c
|
||||
)
|
||||
DeclareTargets(PUSB sd_over_usb)
|
||||
|
||||
|
||||
### HackRF "factory" firmware
|
||||
|
||||
|
||||
@@ -161,8 +161,6 @@ class FProtoSubCarFiatV0 : public FProtoSubCarBase {
|
||||
}
|
||||
|
||||
if (bit_count > 0x46) {
|
||||
final_count = bit_count;
|
||||
|
||||
endbyte = (uint8_t)data_low;
|
||||
/*
|
||||
generic.data = ((uint64_t)hop << 32) | fix;
|
||||
@@ -192,13 +190,11 @@ class FProtoSubCarFiatV0 : public FProtoSubCarBase {
|
||||
|
||||
ManchesterState manchester_state = ManchesterStateMid1;
|
||||
uint8_t decoder_state = 0;
|
||||
uint8_t bit_count = 0;
|
||||
uint8_t endbyte = 0;
|
||||
uint16_t preamble_count = 0;
|
||||
uint32_t data_low = 0;
|
||||
uint32_t data_high = 0;
|
||||
uint8_t bit_count = 0;
|
||||
uint32_t hop = 0;
|
||||
uint32_t fix = 0;
|
||||
uint8_t endbyte = 0;
|
||||
uint8_t final_count = 0;
|
||||
uint32_t te_last = 0;
|
||||
};
|
||||
|
||||
@@ -59,20 +59,26 @@ class FProtoSubCarKiaV0 : public FProtoSubCarBase {
|
||||
break;
|
||||
case KIADecoderStepSaveDuration:
|
||||
if (level) {
|
||||
if (duration >=
|
||||
(te_long + te_delta * 2UL)) {
|
||||
// Signal ended too early!
|
||||
// FURI_LOG_W(TAG, "Signal ended at %u bits (expected 61). Duration: %lu", decode_count_bit, duration);
|
||||
|
||||
if (duration >= (te_long + te_delta * 2UL)) {
|
||||
// End of transmission detected
|
||||
parser_step = KIADecoderStepReset;
|
||||
|
||||
if (decode_count_bit == min_count_bit_for_found) {
|
||||
// instance->generic.data = decode_data;
|
||||
// data = decode_data;
|
||||
data_count_bit = decode_count_bit;
|
||||
// just used for log yet, so skip
|
||||
// if (kia_verify_crc()) {
|
||||
// FURI_LOG_I(TAG, "Valid signal received with correct CRC");
|
||||
// } else {
|
||||
// FURI_LOG_W(TAG, "Signal received but CRC mismatch!");
|
||||
// }
|
||||
|
||||
if (callback)
|
||||
callback(this);
|
||||
} else {
|
||||
// FURI_LOG_E(TAG, "Incomplete signal: only %u bits", decode_count_bit);
|
||||
// FURI_LOG_E(TAG, "Incomplete signal: only %u bits", instance->decoder.decode_count_bit);
|
||||
}
|
||||
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
break;
|
||||
@@ -80,26 +86,19 @@ class FProtoSubCarKiaV0 : public FProtoSubCarBase {
|
||||
te_last = duration;
|
||||
parser_step = KIADecoderStepCheckDuration;
|
||||
}
|
||||
|
||||
} else {
|
||||
parser_step = KIADecoderStepReset;
|
||||
}
|
||||
break;
|
||||
case KIADecoderStepCheckDuration:
|
||||
if (!level) {
|
||||
if ((DURATION_DIFF(te_last, te_short) < te_delta) &&
|
||||
(DURATION_DIFF(duration, te_short) < te_delta)) {
|
||||
if ((DURATION_DIFF(te_last, te_short) < te_delta) && (DURATION_DIFF(duration, te_short) < te_delta)) {
|
||||
subghz_protocol_blocks_add_bit(0);
|
||||
if (decode_count_bit % 10 == 0) {
|
||||
// FURI_LOG_D(TAG, "Decoded %u bits so far", decode_count_bit);
|
||||
}
|
||||
parser_step = KIADecoderStepSaveDuration;
|
||||
} else if (
|
||||
(DURATION_DIFF(te_last, te_long) < te_delta) &&
|
||||
(DURATION_DIFF(duration, te_long) < te_delta)) {
|
||||
(DURATION_DIFF(te_last, te_long) < te_delta) && (DURATION_DIFF(duration, te_long) < te_delta)) {
|
||||
subghz_protocol_blocks_add_bit(1);
|
||||
if (decode_count_bit % 10 == 0) {
|
||||
// FURI_LOG_D(TAG, "Decoded %u bits so far", decode_count_bit);
|
||||
}
|
||||
parser_step = KIADecoderStepSaveDuration;
|
||||
} else {
|
||||
// FURI_LOG_W(TAG, "Timing mismatch at bit %u. Last: %lu, Current: %lu", decode_count_bit, te_last, duration);
|
||||
|
||||
@@ -5,8 +5,7 @@
|
||||
typedef enum {
|
||||
KiaV1DecoderStepReset = 0,
|
||||
KiaV1DecoderStepCheckPreamble,
|
||||
KiaV1DecoderStepFoundShortLow,
|
||||
KiaV1DecoderStepCollectRawBits,
|
||||
KiaV1DecoderStepDecodeData,
|
||||
} KiaV1DecoderStep;
|
||||
|
||||
class FProtoSubCarKiaV1 : public FProtoSubCarBase {
|
||||
@@ -16,177 +15,74 @@ class FProtoSubCarKiaV1 : public FProtoSubCarBase {
|
||||
te_short = 800;
|
||||
te_long = 1600;
|
||||
te_delta = 200;
|
||||
min_count_bit_for_found = 56;
|
||||
}
|
||||
void kia_v1_add_raw_bit(bool bit) {
|
||||
if (raw_bit_count < 192) {
|
||||
uint16_t byte_idx = raw_bit_count / 8;
|
||||
uint8_t bit_idx = 7 - (raw_bit_count % 8);
|
||||
if (bit) {
|
||||
raw_bits[byte_idx] |= (1 << bit_idx);
|
||||
} else {
|
||||
raw_bits[byte_idx] &= ~(1 << bit_idx);
|
||||
}
|
||||
raw_bit_count++;
|
||||
}
|
||||
}
|
||||
inline bool kia_v1_get_raw_bit(uint16_t idx) {
|
||||
uint16_t byte_idx = idx / 8;
|
||||
uint8_t bit_idx = 7 - (idx % 8);
|
||||
return (raw_bits[byte_idx] >> bit_idx) & 1;
|
||||
}
|
||||
bool kia_v1_manchester_decode() {
|
||||
if (raw_bit_count < 113) {
|
||||
// FURI_LOG_D(TAG, "Not enough raw bits: %u", raw_bit_count);
|
||||
return false;
|
||||
}
|
||||
// Try different offsets to find best alignment (RTL-433 uses -1 bit offset)
|
||||
uint16_t best_bits = 0;
|
||||
uint64_t best_data = 0;
|
||||
// uint16_t best_offset = 0;
|
||||
|
||||
for (uint16_t offset = 0; offset < 8; offset++) {
|
||||
uint64_t data = 0;
|
||||
uint16_t decoded_bits = 0;
|
||||
|
||||
for (uint16_t i = offset; i + 1 < raw_bit_count && decoded_bits < 56; i += 2) {
|
||||
bool bit1 = kia_v1_get_raw_bit(i);
|
||||
bool bit2 = kia_v1_get_raw_bit(i + 1);
|
||||
|
||||
uint8_t two_bits = (bit1 << 1) | bit2;
|
||||
|
||||
// V1 uses: 10=1, 01=0
|
||||
if (two_bits == 0x02) { // 10 = decoded 1
|
||||
data = (data << 1) | 1;
|
||||
decoded_bits++;
|
||||
} else if (two_bits == 0x01) { // 01 = decoded 0
|
||||
data = (data << 1);
|
||||
decoded_bits++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (decoded_bits > best_bits) {
|
||||
best_bits = decoded_bits;
|
||||
best_data = data;
|
||||
// best_offset = offset;
|
||||
}
|
||||
}
|
||||
|
||||
// FURI_LOG_I(TAG, "Best: offset=%u bits=%u data=%014llX", best_offset, best_bits, best_data);
|
||||
|
||||
decode_data = best_data;
|
||||
decode_count_bit = best_bits;
|
||||
|
||||
return best_bits >= min_count_bit_for_found;
|
||||
min_count_bit_for_found = 57;
|
||||
}
|
||||
|
||||
void feed(bool level, uint32_t duration) {
|
||||
ManchesterEvent event = ManchesterEventReset;
|
||||
|
||||
switch (parser_step) {
|
||||
case KiaV1DecoderStepReset:
|
||||
// Preamble 0xCCCCCCCD produces alternating LONG pulses
|
||||
if ((level) && (DURATION_DIFF(duration, te_long) <
|
||||
te_delta)) {
|
||||
if ((level) && (DURATION_DIFF(duration, te_long) < te_delta)) {
|
||||
parser_step = KiaV1DecoderStepCheckPreamble;
|
||||
te_last = duration;
|
||||
header_count = 1;
|
||||
header_count = 0;
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
FProtoGeneral::manchester_advance(manchester_saved_state, ManchesterEventReset, &manchester_saved_state, NULL);
|
||||
}
|
||||
break;
|
||||
|
||||
case KiaV1DecoderStepCheckPreamble:
|
||||
if (level) {
|
||||
if (DURATION_DIFF(duration, te_long) <
|
||||
te_delta) {
|
||||
te_last = duration;
|
||||
if (!level) {
|
||||
if ((DURATION_DIFF(duration, te_long) < te_delta) && (DURATION_DIFF(te_last, te_long) < te_delta)) {
|
||||
header_count++;
|
||||
} else if (
|
||||
DURATION_DIFF(duration, te_short) <
|
||||
te_delta) {
|
||||
te_last = duration;
|
||||
} else {
|
||||
parser_step = KiaV1DecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
// LOW pulse
|
||||
if (DURATION_DIFF(duration, te_long) <
|
||||
te_delta) {
|
||||
header_count++;
|
||||
} else if (
|
||||
DURATION_DIFF(duration, te_short) <
|
||||
te_delta) {
|
||||
// Short LOW - this is the start of sync (0xCD ends: ...long H, short L, short H)
|
||||
if (header_count > 12) {
|
||||
parser_step = KiaV1DecoderStepFoundShortLow;
|
||||
}
|
||||
} else {
|
||||
parser_step = KiaV1DecoderStepReset;
|
||||
}
|
||||
if (header_count > 70) {
|
||||
if ((!level) && (DURATION_DIFF(duration, te_short) < te_delta) && (DURATION_DIFF(te_last, te_long) < te_delta)) {
|
||||
decode_count_bit = 1;
|
||||
subghz_protocol_blocks_add_bit(1);
|
||||
header_count = 0;
|
||||
parser_step = KiaV1DecoderStepDecodeData;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case KiaV1DecoderStepFoundShortLow:
|
||||
// Expecting SHORT HIGH to complete sync
|
||||
if (level && (DURATION_DIFF(duration, te_short) <
|
||||
te_delta)) {
|
||||
// FURI_LOG_I(TAG, "Sync! hdr=%u", header_count);
|
||||
parser_step = KiaV1DecoderStepCollectRawBits;
|
||||
raw_bit_count = 0;
|
||||
memset(raw_bits, 0, sizeof(raw_bits));
|
||||
// Add the sync short HIGH as first raw bit
|
||||
kia_v1_add_raw_bit(true);
|
||||
} else {
|
||||
parser_step = KiaV1DecoderStepReset;
|
||||
case KiaV1DecoderStepDecodeData:
|
||||
if ((DURATION_DIFF(duration, te_short) < te_delta)) {
|
||||
event = level ? ManchesterEventShortLow : ManchesterEventShortHigh;
|
||||
} else if ((DURATION_DIFF(duration, te_long) <
|
||||
te_delta)) {
|
||||
event = level ? ManchesterEventLongLow : ManchesterEventLongHigh;
|
||||
}
|
||||
break;
|
||||
|
||||
case KiaV1DecoderStepCollectRawBits:
|
||||
if (duration > 2400) {
|
||||
// FURI_LOG_I(TAG, "End! raw_bits=%u", raw_bit_count);
|
||||
|
||||
if (kia_v1_manchester_decode()) {
|
||||
// instance->generic.data = decode_data;
|
||||
data_count_bit = raw_bit_count / 8;
|
||||
|
||||
// Extract fields from 56-bit data per RTL-433:
|
||||
// Serial: bits 55-24 (32 bits)
|
||||
// Btn: bits 23-16 (8 bits)
|
||||
// Count: bits 15-8 (8 bits)
|
||||
// CRC: bits 7-0 (8 bits)
|
||||
// instance->generic.serial = (uint32_t)((instance->generic.data >> 24) & 0xFFFFFFFF);
|
||||
// instance->generic.btn = (uint8_t)((instance->generic.data >> 16) & 0xFF);
|
||||
// instance->generic.cnt = (uint8_t)((instance->generic.data >> 8) & 0xFF);
|
||||
|
||||
if (callback)
|
||||
callback(this);
|
||||
if (event != ManchesterEventReset) {
|
||||
bool data;
|
||||
bool data_ok = FProtoGeneral::manchester_advance(manchester_saved_state, event, &manchester_saved_state, &data);
|
||||
if (data_ok) {
|
||||
decode_data = (decode_data << 1) | data;
|
||||
decode_count_bit++;
|
||||
}
|
||||
}
|
||||
|
||||
if (decode_count_bit == min_count_bit_for_found) {
|
||||
// instance->generic.data = decode_data;
|
||||
data_count_bit = decode_count_bit;
|
||||
if (callback)
|
||||
callback(this);
|
||||
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
parser_step = KiaV1DecoderStepReset;
|
||||
break;
|
||||
}
|
||||
|
||||
int num_bits = 0;
|
||||
if (DURATION_DIFF(duration, te_short) <
|
||||
te_delta) {
|
||||
num_bits = 1;
|
||||
} else if (
|
||||
DURATION_DIFF(duration, te_long) <
|
||||
te_delta) {
|
||||
num_bits = 2;
|
||||
} else {
|
||||
parser_step = KiaV1DecoderStepReset;
|
||||
break;
|
||||
}
|
||||
|
||||
for (int i = 0; i < num_bits; i++) {
|
||||
kia_v1_add_raw_bit(level);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t raw_bits[24]{0};
|
||||
uint16_t raw_bit_count = 0;
|
||||
uint16_t header_count = 0;
|
||||
uint8_t header_count = 0;
|
||||
ManchesterState manchester_saved_state = ManchesterStateStart1;
|
||||
};
|
||||
|
||||
@@ -15,65 +15,7 @@ class FProtoSubCarKiaV2 : public FProtoSubCarBase {
|
||||
te_short = 500;
|
||||
te_long = 1000;
|
||||
te_delta = 160;
|
||||
min_count_bit_for_found = 51;
|
||||
}
|
||||
|
||||
void kia_v2_add_raw_bit(bool bit) {
|
||||
if (raw_bit_count < 160) {
|
||||
uint16_t byte_idx = raw_bit_count / 8;
|
||||
uint8_t bit_idx = 7 - (raw_bit_count % 8);
|
||||
if (bit) {
|
||||
raw_bits[byte_idx] |= (1 << bit_idx);
|
||||
} else {
|
||||
raw_bits[byte_idx] &= ~(1 << bit_idx);
|
||||
}
|
||||
raw_bit_count++;
|
||||
}
|
||||
}
|
||||
inline bool kia_v2_get_raw_bit(uint16_t idx) {
|
||||
uint16_t byte_idx = idx / 8;
|
||||
uint8_t bit_idx = 7 - (idx % 8);
|
||||
return (raw_bits[byte_idx] >> bit_idx) & 1;
|
||||
}
|
||||
bool kia_v2_manchester_decode() {
|
||||
if (raw_bit_count < 100) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint16_t best_bits = 0;
|
||||
uint64_t best_data = 0;
|
||||
|
||||
for (uint16_t offset = 0; offset < 8; offset++) {
|
||||
uint64_t data = 0;
|
||||
uint16_t decoded_bits = 0;
|
||||
|
||||
for (uint16_t i = offset; i + 1 < raw_bit_count && decoded_bits < 53; i += 2) {
|
||||
bool bit1 = kia_v2_get_raw_bit(i);
|
||||
bool bit2 = kia_v2_get_raw_bit(i + 1);
|
||||
|
||||
uint8_t two_bits = (bit1 << 1) | bit2;
|
||||
|
||||
if (two_bits == 0x02) {
|
||||
data = (data << 1) | 1;
|
||||
decoded_bits++;
|
||||
} else if (two_bits == 0x01) {
|
||||
data = (data << 1);
|
||||
decoded_bits++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (decoded_bits > best_bits) {
|
||||
best_bits = decoded_bits;
|
||||
best_data = data;
|
||||
}
|
||||
}
|
||||
|
||||
decode_data = best_data;
|
||||
decode_count_bit = best_bits;
|
||||
|
||||
return best_bits >= min_count_bit_for_found;
|
||||
min_count_bit_for_found = 53;
|
||||
}
|
||||
|
||||
void feed(bool level, uint32_t duration) {
|
||||
@@ -82,86 +24,85 @@ class FProtoSubCarKiaV2 : public FProtoSubCarBase {
|
||||
if ((level) && (DURATION_DIFF(duration, te_long) < te_delta)) {
|
||||
parser_step = KiaV2DecoderStepCheckPreamble;
|
||||
te_last = duration;
|
||||
header_count = 1;
|
||||
header_count = 0;
|
||||
FProtoGeneral::manchester_advance(manchester_state, ManchesterEventReset, &manchester_state, NULL);
|
||||
}
|
||||
break;
|
||||
|
||||
case KiaV2DecoderStepCheckPreamble:
|
||||
if (level) {
|
||||
if (DURATION_DIFF(duration, te_long) <
|
||||
te_delta) {
|
||||
if (level) // HIGH pulse
|
||||
{
|
||||
if (DURATION_DIFF(duration, te_long) < te_delta) {
|
||||
te_last = duration;
|
||||
header_count++;
|
||||
} else if (
|
||||
DURATION_DIFF(duration, te_short) <
|
||||
te_delta) {
|
||||
te_last = duration;
|
||||
} else {
|
||||
parser_step = KiaV2DecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
if (DURATION_DIFF(duration, te_long) <
|
||||
te_delta) {
|
||||
header_count++;
|
||||
} else if (
|
||||
DURATION_DIFF(duration, te_short) <
|
||||
te_delta) {
|
||||
if (header_count > 10 &&
|
||||
DURATION_DIFF(te_last, te_short) <
|
||||
te_delta) {
|
||||
DURATION_DIFF(duration, te_short) < te_delta) {
|
||||
if (header_count >= 100) {
|
||||
header_count = 0;
|
||||
decode_data = 0;
|
||||
decode_count_bit = 1;
|
||||
parser_step = KiaV2DecoderStepCollectRawBits;
|
||||
raw_bit_count = 0;
|
||||
memset(raw_bits, 0, sizeof(raw_bits));
|
||||
subghz_protocol_blocks_add_bit(1);
|
||||
} else {
|
||||
te_last = duration;
|
||||
}
|
||||
} else {
|
||||
parser_step = KiaV2DecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
if (DURATION_DIFF(duration, te_long) < te_delta) {
|
||||
header_count++;
|
||||
te_last = duration;
|
||||
} else if (
|
||||
DURATION_DIFF(duration, te_short) < te_delta) {
|
||||
te_last = duration;
|
||||
} else {
|
||||
parser_step = KiaV2DecoderStepReset;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case KiaV2DecoderStepCollectRawBits:
|
||||
if (duration > 1500) {
|
||||
if (kia_v2_manchester_decode()) {
|
||||
/*data = decode_data;
|
||||
data_count_bit = decode_count_bit;
|
||||
case KiaV2DecoderStepCollectRawBits: {
|
||||
ManchesterEvent event;
|
||||
|
||||
serial = (uint32_t)((data >> 20) & 0xFFFFFFFF);
|
||||
btn = (uint8_t)((data >> 16) & 0x0F);
|
||||
|
||||
uint16_t raw_count = (uint16_t)((data >> 4) & 0xFFF);
|
||||
cnt = ((raw_count >> 4) | (raw_count << 8)) & 0xFFF;
|
||||
*/
|
||||
data_count_bit = decode_count_bit;
|
||||
if (callback)
|
||||
callback(this);
|
||||
}
|
||||
|
||||
parser_step = KiaV2DecoderStepReset;
|
||||
break;
|
||||
}
|
||||
|
||||
int num_bits = 0;
|
||||
if (DURATION_DIFF(duration, te_short) <
|
||||
te_delta) {
|
||||
num_bits = 1;
|
||||
if (DURATION_DIFF(duration, te_short) < te_delta) {
|
||||
event = level ? ManchesterEventShortLow : ManchesterEventShortHigh;
|
||||
} else if (
|
||||
DURATION_DIFF(duration, te_long) <
|
||||
te_delta) {
|
||||
num_bits = 2;
|
||||
DURATION_DIFF(duration, te_long) < te_delta) {
|
||||
event = level ? ManchesterEventLongLow : ManchesterEventLongHigh;
|
||||
} else {
|
||||
parser_step = KiaV2DecoderStepReset;
|
||||
break;
|
||||
}
|
||||
|
||||
for (int i = 0; i < num_bits; i++) {
|
||||
kia_v2_add_raw_bit(level);
|
||||
}
|
||||
bool data_bit;
|
||||
if (FProtoGeneral::manchester_advance(manchester_state, event, &manchester_state, &data_bit)) {
|
||||
decode_data = (decode_data << 1) | data_bit;
|
||||
decode_count_bit++;
|
||||
|
||||
if (decode_count_bit == 53) {
|
||||
// instance->generic.data = decode_data;
|
||||
data_count_bit = decode_count_bit;
|
||||
|
||||
// instance->generic.serial = (uint32_t)((instance->generic.data >> 20) & 0xFFFFFFFF);
|
||||
// instance->generic.btn = (uint8_t)((instance->generic.data >> 16) & 0x0F);
|
||||
|
||||
// uint16_t raw_count = (uint16_t)((instance->generic.data >> 4) & 0xFFF);
|
||||
// instance->generic.cnt = ((raw_count >> 4) | (raw_count << 8)) & 0xFFF;
|
||||
|
||||
if (callback)
|
||||
callback(this);
|
||||
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
header_count = 0;
|
||||
parser_step = KiaV2DecoderStepReset;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t raw_bits[20]{0};
|
||||
uint16_t raw_bit_count = 0;
|
||||
uint16_t header_count = 0;
|
||||
ManchesterState manchester_state = ManchesterStateMid1;
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ class FProtoSubCarKiaV3V4 : public FProtoSubCarBase {
|
||||
te_short = 400;
|
||||
te_long = 800;
|
||||
te_delta = 150;
|
||||
min_count_bit_for_found = 64;
|
||||
min_count_bit_for_found = 68;
|
||||
}
|
||||
uint8_t reverse8(uint8_t byte) {
|
||||
byte = (byte & 0xF0) >> 4 | (byte & 0x0F) << 4;
|
||||
@@ -36,7 +36,7 @@ class FProtoSubCarKiaV3V4 : public FProtoSubCarBase {
|
||||
}
|
||||
}
|
||||
bool kia_v3_v4_process_buffer() {
|
||||
if (raw_bit_count < 64) {
|
||||
if (raw_bit_count < 68) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ class FProtoSubCarKiaV3V4 : public FProtoSubCarBase {
|
||||
|
||||
uint8_t btn = (reverse8(b[7]) & 0xF0) >> 4;
|
||||
decode_data = serial;
|
||||
decode_count_bit = 64;
|
||||
decode_count_bit = 68;
|
||||
decode_data2 = btn;
|
||||
data_count_bit = decode_count_bit;
|
||||
if (callback)
|
||||
@@ -93,8 +93,7 @@ class FProtoSubCarKiaV3V4 : public FProtoSubCarBase {
|
||||
void feed(bool level, uint32_t duration) {
|
||||
switch (parser_step) {
|
||||
case KiaV3V4DecoderStepReset:
|
||||
if (level && DURATION_DIFF(duration, te_short) <
|
||||
te_delta) {
|
||||
if (level && DURATION_DIFF(duration, te_short) < te_delta) {
|
||||
parser_step = KiaV3V4DecoderStepCheckPreamble;
|
||||
te_last = duration;
|
||||
header_count = 1;
|
||||
@@ -103,8 +102,7 @@ class FProtoSubCarKiaV3V4 : public FProtoSubCarBase {
|
||||
|
||||
case KiaV3V4DecoderStepCheckPreamble:
|
||||
if (level) {
|
||||
if (DURATION_DIFF(duration, te_short) <
|
||||
te_delta) {
|
||||
if (DURATION_DIFF(duration, te_short) < te_delta) {
|
||||
te_last = duration;
|
||||
} else if (duration > 1000 && duration < 1500) {
|
||||
// V4 style: Sync is LONG HIGH
|
||||
@@ -130,11 +128,7 @@ class FProtoSubCarKiaV3V4 : public FProtoSubCarBase {
|
||||
} else {
|
||||
parser_step = KiaV3V4DecoderStepReset;
|
||||
}
|
||||
} else if (
|
||||
DURATION_DIFF(duration, te_short) <
|
||||
te_delta &&
|
||||
DURATION_DIFF(te_last, te_short) <
|
||||
te_delta) {
|
||||
} else if (DURATION_DIFF(duration, te_short) < te_delta && DURATION_DIFF(te_last, te_short) < te_delta) {
|
||||
header_count++;
|
||||
} else if (duration > 1500) {
|
||||
parser_step = KiaV3V4DecoderStepReset;
|
||||
@@ -149,12 +143,10 @@ class FProtoSubCarKiaV3V4 : public FProtoSubCarBase {
|
||||
kia_v3_v4_process_buffer();
|
||||
parser_step = KiaV3V4DecoderStepReset;
|
||||
} else if (
|
||||
DURATION_DIFF(duration, te_short) <
|
||||
te_delta) {
|
||||
DURATION_DIFF(duration, te_short) < te_delta) {
|
||||
kia_v3_v4_add_raw_bit(false);
|
||||
} else if (
|
||||
DURATION_DIFF(duration, te_long) <
|
||||
te_delta) {
|
||||
DURATION_DIFF(duration, te_long) < te_delta) {
|
||||
kia_v3_v4_add_raw_bit(true);
|
||||
} else {
|
||||
parser_step = KiaV3V4DecoderStepReset;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
#pragma once
|
||||
#include "subcarbase.hpp"
|
||||
#include <cstring>
|
||||
|
||||
typedef enum {
|
||||
KiaV5DecoderStepReset = 0,
|
||||
KiaV5DecoderStepCheckPreamble,
|
||||
KiaV5DecoderStepCollectRawBits,
|
||||
KiaV5DecoderStepData,
|
||||
} KiaV5DecoderStep;
|
||||
|
||||
class FProtoSubCarKiaV5 : public FProtoSubCarBase {
|
||||
@@ -17,74 +18,37 @@ class FProtoSubCarKiaV5 : public FProtoSubCarBase {
|
||||
min_count_bit_for_found = 64;
|
||||
}
|
||||
|
||||
inline bool kia_v5_get_raw_bit(uint16_t idx) {
|
||||
uint16_t byte_idx = idx / 8;
|
||||
uint8_t bit_idx = 7 - (idx % 8);
|
||||
return (raw_bits[byte_idx] >> bit_idx) & 1;
|
||||
}
|
||||
|
||||
void kia_v5_add_raw_bit(bool bit) {
|
||||
if (raw_bit_count < 256) {
|
||||
uint16_t byte_idx = raw_bit_count / 8;
|
||||
uint8_t bit_idx = 7 - (raw_bit_count % 8);
|
||||
if (bit) {
|
||||
raw_bits[byte_idx] |= (1 << bit_idx);
|
||||
} else {
|
||||
raw_bits[byte_idx] &= ~(1 << bit_idx);
|
||||
}
|
||||
raw_bit_count++;
|
||||
}
|
||||
}
|
||||
|
||||
bool kia_v5_manchester_decode() {
|
||||
if (raw_bit_count < 130) {
|
||||
return false;
|
||||
}
|
||||
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
|
||||
// Start at offset 2 for proper Manchester alignment
|
||||
const uint16_t start_bit = 2;
|
||||
|
||||
for (uint16_t i = start_bit;
|
||||
i + 1 < raw_bit_count && decode_count_bit < 64;
|
||||
i += 2) {
|
||||
bool bit1 = kia_v5_get_raw_bit(i);
|
||||
bool bit2 = kia_v5_get_raw_bit(i + 1);
|
||||
|
||||
uint8_t two_bits = (bit1 << 1) | bit2;
|
||||
|
||||
if (two_bits == 0x01) { // 01 = decoded 1
|
||||
decode_data = (decode_data << 1) | 1;
|
||||
decode_count_bit++;
|
||||
} else if (two_bits == 0x02) { // 10 = decoded 0
|
||||
decode_data = (decode_data << 1);
|
||||
decode_count_bit++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return decode_count_bit >= min_count_bit_for_found;
|
||||
void kia_v5_add_bit(bool bit) {
|
||||
decode_data = (decode_data << 1) | (bit ? 1 : 0);
|
||||
decode_count_bit++;
|
||||
}
|
||||
|
||||
void feed(bool level, uint32_t duration) {
|
||||
switch (parser_step) {
|
||||
case KiaV5DecoderStepReset:
|
||||
if ((level) && (DURATION_DIFF(duration, te_short) <
|
||||
te_delta)) {
|
||||
if ((level) && (DURATION_DIFF(duration, te_short) < te_delta)) {
|
||||
parser_step = KiaV5DecoderStepCheckPreamble;
|
||||
te_last = duration;
|
||||
header_count = 1;
|
||||
decode_count_bit = 0;
|
||||
decode_data = 0;
|
||||
FProtoGeneral::manchester_advance(manchester_state, ManchesterEventReset, &manchester_state, NULL);
|
||||
}
|
||||
break;
|
||||
|
||||
case KiaV5DecoderStepCheckPreamble:
|
||||
if (level) {
|
||||
if ((DURATION_DIFF(duration, te_short) <
|
||||
te_delta) ||
|
||||
(DURATION_DIFF(duration, te_long) <
|
||||
te_delta)) {
|
||||
if (DURATION_DIFF(duration, te_long) < te_delta) {
|
||||
if (header_count > 40) {
|
||||
parser_step = KiaV5DecoderStepData;
|
||||
decode_count_bit = 0;
|
||||
decode_data = 0;
|
||||
decode_data2 = 0;
|
||||
header_count = 0;
|
||||
} else {
|
||||
te_last = duration;
|
||||
}
|
||||
} else if (DURATION_DIFF(duration, te_short) < te_delta) {
|
||||
te_last = duration;
|
||||
} else {
|
||||
parser_step = KiaV5DecoderStepReset;
|
||||
@@ -100,13 +64,7 @@ class FProtoSubCarKiaV5 : public FProtoSubCarBase {
|
||||
te_delta) &&
|
||||
(DURATION_DIFF(te_last, te_short) <
|
||||
te_delta)) {
|
||||
if (header_count > 40) {
|
||||
parser_step = KiaV5DecoderStepCollectRawBits;
|
||||
raw_bit_count = 0;
|
||||
memset(raw_bits, 0, sizeof(raw_bits));
|
||||
} else {
|
||||
header_count++;
|
||||
}
|
||||
header_count++;
|
||||
} else if (
|
||||
DURATION_DIFF(te_last, te_long) <
|
||||
te_delta) {
|
||||
@@ -114,33 +72,21 @@ class FProtoSubCarKiaV5 : public FProtoSubCarBase {
|
||||
} else {
|
||||
parser_step = KiaV5DecoderStepReset;
|
||||
}
|
||||
te_last = duration;
|
||||
}
|
||||
break;
|
||||
|
||||
case KiaV5DecoderStepCollectRawBits:
|
||||
if (duration > 1200) {
|
||||
if (kia_v5_manchester_decode()) {
|
||||
// generic.data = decode_data;
|
||||
// generic.data_count_bit = decode_count_bit;
|
||||
data_count_bit = decode_count_bit;
|
||||
// Compute yek (bit-reverse each byte)
|
||||
uint64_t yek = 0;
|
||||
for (int i = 0; i < 8; i++) {
|
||||
uint8_t byte = (decode_data >> (i * 8)) & 0xFF;
|
||||
uint8_t reversed = 0;
|
||||
for (int b = 0; b < 8; b++) {
|
||||
if (byte & (1 << b))
|
||||
reversed |= (1 << (7 - b));
|
||||
}
|
||||
yek |= ((uint64_t)reversed << ((7 - i) * 8));
|
||||
}
|
||||
decode_data = yek;
|
||||
|
||||
// Shift serial right by 1 to correct alignment
|
||||
// generic.serial = (uint32_t)(((yek >> 32) & 0x0FFFFFFF) >> 1);
|
||||
// generic.btn = (uint8_t)((yek >> 61) & 0x07); // Shift btn too
|
||||
// generic.cnt = (uint16_t)(yek & 0xFFFF);
|
||||
case KiaV5DecoderStepData: {
|
||||
ManchesterEvent event;
|
||||
|
||||
if (DURATION_DIFF(duration, te_short) < te_delta) {
|
||||
event = level ? ManchesterEventShortHigh : ManchesterEventShortLow;
|
||||
} else if (DURATION_DIFF(duration, te_long) < te_delta) {
|
||||
event = level ? ManchesterEventLongHigh : ManchesterEventLongLow;
|
||||
} else {
|
||||
if (decode_count_bit >= min_count_bit_for_found) {
|
||||
// instance->generic.data = instance->decode_data2;
|
||||
data_count_bit = (decode_count_bit > 67) ? 67 : decode_count_bit;
|
||||
if (callback)
|
||||
callback(this);
|
||||
}
|
||||
@@ -149,28 +95,22 @@ class FProtoSubCarKiaV5 : public FProtoSubCarBase {
|
||||
break;
|
||||
}
|
||||
|
||||
int num_bits = 0;
|
||||
if (DURATION_DIFF(duration, te_short) <
|
||||
te_delta) {
|
||||
num_bits = 1;
|
||||
} else if (
|
||||
DURATION_DIFF(duration, te_long) <
|
||||
te_delta) {
|
||||
num_bits = 2;
|
||||
} else {
|
||||
parser_step = KiaV5DecoderStepReset;
|
||||
break;
|
||||
}
|
||||
|
||||
for (int i = 0; i < num_bits; i++) {
|
||||
kia_v5_add_raw_bit(level);
|
||||
bool data_bit;
|
||||
if (decode_count_bit <= 66 && FProtoGeneral::manchester_advance(manchester_state, event, &manchester_state, &data_bit)) {
|
||||
kia_v5_add_bit(data_bit);
|
||||
|
||||
if (decode_count_bit == 64) {
|
||||
decode_data2 = decode_data;
|
||||
decode_data = 0;
|
||||
}
|
||||
}
|
||||
|
||||
te_last = duration;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t raw_bits[32]{};
|
||||
uint16_t raw_bit_count = 0;
|
||||
uint16_t header_count = 0;
|
||||
ManchesterState manchester_state = ManchesterStateMid1;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
#pragma once
|
||||
#include "subcarbase.hpp"
|
||||
#include <cstring>
|
||||
|
||||
typedef enum {
|
||||
KiaV6DecoderStepReset = 0,
|
||||
KiaV6DecoderStepWaitFirstHigh,
|
||||
KiaV6DecoderStepCountPreamble,
|
||||
KiaV6DecoderStepWaitLongHigh,
|
||||
KiaV6DecoderStepData,
|
||||
} KiaV6DecoderStep;
|
||||
|
||||
#define KIA_V6_XOR_MASK_LOW 0x84AF25FB
|
||||
#define KIA_V6_XOR_MASK_HIGH 0x638766AB
|
||||
|
||||
class FProtoSubCarKiaV6 : public FProtoSubCarBase {
|
||||
public:
|
||||
FProtoSubCarKiaV6() {
|
||||
sensorType = FPC_KIAV6;
|
||||
te_short = 200;
|
||||
te_long = 400;
|
||||
te_delta = 100;
|
||||
min_count_bit_for_found = 144;
|
||||
}
|
||||
|
||||
void feed(bool level, uint32_t duration) {
|
||||
uint32_t uVar4, uVar5;
|
||||
ManchesterEvent event;
|
||||
bool data_bit;
|
||||
uint8_t bit_count_inc;
|
||||
uint32_t step_value;
|
||||
|
||||
switch (parser_step) {
|
||||
case KiaV6DecoderStepReset: // case 0
|
||||
if (level == 0) {
|
||||
return;
|
||||
}
|
||||
if (DURATION_DIFF(duration, te_short) <
|
||||
te_delta) {
|
||||
parser_step = KiaV6DecoderStepWaitFirstHigh;
|
||||
te_last = duration;
|
||||
header_count = 0;
|
||||
FProtoGeneral::manchester_advance(
|
||||
manchester_state,
|
||||
ManchesterEventReset,
|
||||
&manchester_state,
|
||||
NULL);
|
||||
}
|
||||
return;
|
||||
|
||||
case KiaV6DecoderStepWaitFirstHigh: { // case 1
|
||||
if (level != 0) {
|
||||
return;
|
||||
}
|
||||
uint32_t diff_short = DURATION_DIFF(duration, te_short);
|
||||
uint32_t diff_long = DURATION_DIFF(duration, te_long);
|
||||
|
||||
uint32_t diff = (diff_long < diff_short) ? diff_long : diff_short;
|
||||
|
||||
if (diff_long < te_delta && diff_long < diff_short) {
|
||||
if (header_count >= 0x259) { // 601 decimal
|
||||
header_count = 0;
|
||||
te_last = duration;
|
||||
parser_step = KiaV6DecoderStepWaitLongHigh;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (diff >= te_delta) {
|
||||
step_value = KiaV6DecoderStepReset;
|
||||
goto LAB_reset;
|
||||
}
|
||||
|
||||
if (DURATION_DIFF(te_last, te_short) <
|
||||
te_delta) {
|
||||
te_last = duration;
|
||||
header_count++;
|
||||
return;
|
||||
} else {
|
||||
step_value = KiaV6DecoderStepReset;
|
||||
goto LAB_reset;
|
||||
}
|
||||
}
|
||||
case KiaV6DecoderStepWaitLongHigh: { // case 2
|
||||
if (level == 0) {
|
||||
step_value = KiaV6DecoderStepReset;
|
||||
goto LAB_reset;
|
||||
}
|
||||
uint32_t diff_long_check = DURATION_DIFF(duration, te_long);
|
||||
uint32_t diff_short_check = DURATION_DIFF(duration, te_short);
|
||||
|
||||
if (diff_long_check >= te_delta) {
|
||||
if (diff_short_check >= te_delta) {
|
||||
step_value = KiaV6DecoderStepReset;
|
||||
goto LAB_reset;
|
||||
}
|
||||
}
|
||||
|
||||
if (DURATION_DIFF(te_last, te_long) >=
|
||||
te_delta) {
|
||||
step_value = KiaV6DecoderStepReset;
|
||||
goto LAB_reset;
|
||||
}
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
|
||||
subghz_protocol_blocks_add_bit(1);
|
||||
subghz_protocol_blocks_add_bit(1);
|
||||
subghz_protocol_blocks_add_bit(0);
|
||||
subghz_protocol_blocks_add_bit(1);
|
||||
|
||||
data_part1_low = (uint32_t)(decode_data & 0xFFFFFFFF);
|
||||
data_part1_high = (uint32_t)((decode_data >> 32) & 0xFFFFFFFF);
|
||||
bit_count = decode_count_bit;
|
||||
|
||||
parser_step = KiaV6DecoderStepData;
|
||||
return;
|
||||
}
|
||||
case KiaV6DecoderStepData: // case 3
|
||||
if (DURATION_DIFF(duration, te_short) <
|
||||
te_delta) {
|
||||
event = (ManchesterEvent)((level & 0x7F) << 1);
|
||||
goto manchester_process;
|
||||
} else if (
|
||||
DURATION_DIFF(duration, te_long) <
|
||||
te_delta) {
|
||||
event = (ManchesterEvent)(level ? 6 : 4);
|
||||
goto manchester_process;
|
||||
}
|
||||
step_value = KiaV6DecoderStepReset;
|
||||
goto LAB_reset;
|
||||
|
||||
manchester_process:
|
||||
if (FProtoGeneral::manchester_advance(
|
||||
manchester_state, event, &manchester_state, &data_bit)) {
|
||||
uVar4 = data_part1_low;
|
||||
uVar5 = (uVar4 << 1) | (data_bit ? 1 : 0);
|
||||
|
||||
uint32_t carry = (uVar4 >> 31) & 1;
|
||||
uVar4 = (data_part1_high << 1) | carry;
|
||||
|
||||
data_part1_low = uVar5;
|
||||
data_part1_high = uVar4;
|
||||
|
||||
decode_data = ((uint64_t)uVar4 << 32) | uVar5;
|
||||
|
||||
bit_count_inc = bit_count + 1;
|
||||
bit_count = bit_count_inc;
|
||||
|
||||
if (bit_count_inc == 0x40) {
|
||||
// stored_part1_low = ~uVar5;
|
||||
// stored_part1_high = ~uVar4;
|
||||
data_part1_low = 0;
|
||||
data_part1_high = 0;
|
||||
} else if (bit_count_inc == 0x80) {
|
||||
// stored_part2_low = ~uVar5;
|
||||
// stored_part2_high = ~uVar4;
|
||||
data_part1_low = 0;
|
||||
data_part1_high = 0;
|
||||
}
|
||||
}
|
||||
|
||||
te_last = duration;
|
||||
|
||||
if (bit_count != min_count_bit_for_found) {
|
||||
return;
|
||||
}
|
||||
data_count_bit = min_count_bit_for_found;
|
||||
// data_part3 = ~((uint16_t)data_part1_low);
|
||||
|
||||
// kia_v6_decrypt(); --won't
|
||||
decode_data = data_part1_low | ((uint64_t)data_part1_high << 32);
|
||||
if (callback) {
|
||||
callback(this);
|
||||
}
|
||||
|
||||
data_part1_low = 0;
|
||||
data_part1_high = 0;
|
||||
bit_count = 0;
|
||||
step_value = KiaV6DecoderStepReset;
|
||||
goto LAB_reset;
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
LAB_reset:
|
||||
parser_step = step_value;
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t bit_count = 0;
|
||||
|
||||
uint16_t header_count = 0;
|
||||
ManchesterState manchester_state = ManchesterStateMid1;
|
||||
uint32_t data_part1_low = 0;
|
||||
uint32_t data_part1_high = 0;
|
||||
|
||||
// uint32_t stored_part1_low = 0;
|
||||
// uint32_t stored_part1_high = 0;
|
||||
// uint32_t stored_part2_low = 0;
|
||||
// uint32_t stored_part2_high = 0;
|
||||
// uint16_t data_part3 = 0;
|
||||
};
|
||||
@@ -20,6 +20,7 @@ class FProtoSubCarSubaru : public FProtoSubCarBase {
|
||||
te_delta = 260;
|
||||
min_count_bit_for_found = 64;
|
||||
}
|
||||
|
||||
void subghz_protocol_decoder_subaru_reset() {
|
||||
parser_step = SubaruDecoderStepReset;
|
||||
te_last = 0;
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
|
||||
typedef enum {
|
||||
SuzukiDecoderStepReset = 0,
|
||||
SuzukiDecoderStepFoundStartPulse,
|
||||
SuzukiDecoderStepSaveDuration,
|
||||
SuzukiDecoderStepCountPreamble = 1,
|
||||
SuzukiDecoderStepDecodeData = 2,
|
||||
} SuzukiDecoderStep;
|
||||
|
||||
class FProtoSubCarSuzuki : public FProtoSubCarBase {
|
||||
@@ -20,54 +20,45 @@ class FProtoSubCarSuzuki : public FProtoSubCarBase {
|
||||
min_count_bit_for_found = 64;
|
||||
}
|
||||
void suzuki_add_bit(uint32_t bit) {
|
||||
uint32_t carry = data_low >> 31;
|
||||
data_low = (data_low << 1) | bit;
|
||||
data_high = (data_high << 1) | carry;
|
||||
data_count_bit++;
|
||||
decode_data = (decode_data << 1) | bit;
|
||||
decode_count_bit++;
|
||||
}
|
||||
void subghz_protocol_decoder_suzuki_reset() {
|
||||
parser_step = SuzukiDecoderStepReset;
|
||||
header_count = 0;
|
||||
data_count_bit = 0;
|
||||
data_low = 0;
|
||||
data_high = 0;
|
||||
decode_data = 0;
|
||||
}
|
||||
|
||||
void feed(bool level, uint32_t duration) {
|
||||
switch (parser_step) {
|
||||
case SuzukiDecoderStepReset:
|
||||
// Wait for short HIGH pulse (~250µs) to start preamble
|
||||
if (!level)
|
||||
// Wait for HIGH pulse (~250µs) to start preamble
|
||||
if (!level) {
|
||||
return;
|
||||
|
||||
}
|
||||
if (DURATION_DIFF(duration, te_short) > te_delta) {
|
||||
return;
|
||||
}
|
||||
|
||||
data_low = 0;
|
||||
data_high = 0;
|
||||
parser_step = SuzukiDecoderStepFoundStartPulse;
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
parser_step = SuzukiDecoderStepCountPreamble;
|
||||
header_count = 0;
|
||||
data_count_bit = 0;
|
||||
break;
|
||||
|
||||
case SuzukiDecoderStepFoundStartPulse:
|
||||
case SuzukiDecoderStepCountPreamble:
|
||||
if (level) {
|
||||
// HIGH pulse
|
||||
if (header_count < 257) {
|
||||
// Still in preamble - just count
|
||||
return;
|
||||
}
|
||||
|
||||
// After preamble, look for long HIGH to start data
|
||||
if (DURATION_DIFF(duration, te_long) < te_delta) {
|
||||
parser_step = SuzukiDecoderStepSaveDuration;
|
||||
suzuki_add_bit(1);
|
||||
if (header_count >= 300) {
|
||||
if (DURATION_DIFF(duration, te_long) <= te_delta) {
|
||||
parser_step = SuzukiDecoderStepDecodeData;
|
||||
suzuki_add_bit(1);
|
||||
}
|
||||
}
|
||||
// Ignore short HIGHs after preamble until we see a long one
|
||||
} else {
|
||||
// LOW pulse - count as header if short
|
||||
if (DURATION_DIFF(duration, te_short) < te_delta) {
|
||||
if (DURATION_DIFF(duration, te_short) <= te_delta) {
|
||||
te_last = duration;
|
||||
header_count++;
|
||||
} else {
|
||||
@@ -76,45 +67,69 @@ class FProtoSubCarSuzuki : public FProtoSubCarBase {
|
||||
}
|
||||
break;
|
||||
|
||||
case SuzukiDecoderStepSaveDuration:
|
||||
case SuzukiDecoderStepDecodeData:
|
||||
|
||||
if (level) {
|
||||
// HIGH pulse - determines bit value
|
||||
// Long HIGH (~500µs) = 1, Short HIGH (~250µs) = 0
|
||||
if (DURATION_DIFF(duration, te_long) < te_delta) {
|
||||
suzuki_add_bit(1);
|
||||
} else if (DURATION_DIFF(duration, te_short) < te_delta) {
|
||||
suzuki_add_bit(0);
|
||||
if (duration < te_long) {
|
||||
uint32_t diff_long = 500 - duration;
|
||||
if (diff_long > 99) {
|
||||
uint32_t diff_short;
|
||||
if (duration < 250) {
|
||||
diff_short = 250 - duration;
|
||||
} else {
|
||||
diff_short = duration - 250;
|
||||
}
|
||||
|
||||
if (diff_short <= 99) {
|
||||
suzuki_add_bit(0);
|
||||
}
|
||||
} else {
|
||||
suzuki_add_bit(1);
|
||||
}
|
||||
} else {
|
||||
parser_step = SuzukiDecoderStepReset;
|
||||
uint32_t diff_long = duration - 500;
|
||||
if (diff_long <= 99) {
|
||||
suzuki_add_bit(1);
|
||||
}
|
||||
}
|
||||
// Stay in this state for next bit
|
||||
} else {
|
||||
// LOW pulse - check for gap (end of transmission)
|
||||
if (DURATION_DIFF(duration, SUZUKI_GAP_TIME) < SUZUKI_GAP_DELTA) {
|
||||
// Gap found - end of transmission
|
||||
if (data_count_bit == 64) {
|
||||
uint32_t diff_gap;
|
||||
if (duration < SUZUKI_GAP_TIME) {
|
||||
diff_gap = SUZUKI_GAP_TIME - duration;
|
||||
} else {
|
||||
diff_gap = duration - SUZUKI_GAP_TIME;
|
||||
}
|
||||
|
||||
if (diff_gap <= SUZUKI_GAP_DELTA) {
|
||||
if (decode_count_bit == 64) {
|
||||
// instance->generic.data = decode_data;
|
||||
data_count_bit = 64;
|
||||
decode_data = ((uint64_t)data_high << 32) | (uint64_t)data_low;
|
||||
// Check manufacturer nibble (should be 0xF)
|
||||
uint8_t manufacturer = (data_high >> 28) & 0xF;
|
||||
if (manufacturer == 0xF) {
|
||||
// Extract fields
|
||||
decode_data2 = 0; // Not used
|
||||
if (callback) {
|
||||
callback(this);
|
||||
}
|
||||
|
||||
/*uint64_t data = instance->generic.data;
|
||||
uint32_t data_high = (uint32_t)(data >> 32);
|
||||
uint32_t data_low = (uint32_t)data;
|
||||
|
||||
instance->generic.serial = ((data_high & 0xFFF) << 16) | (data_low >> 16);
|
||||
|
||||
instance->generic.btn = (data_low >> 12) & 0xF;
|
||||
instance->generic.cnt = (data_high << 4) >> 16;
|
||||
*/
|
||||
if (callback) {
|
||||
callback(this);
|
||||
}
|
||||
}
|
||||
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
parser_step = SuzukiDecoderStepReset;
|
||||
}
|
||||
// Short LOW pulses are ignored - stay in this state
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t header_count = 0;
|
||||
uint32_t data_high = 0;
|
||||
uint32_t data_low = 0;
|
||||
uint8_t data_count_bit = 0;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
|
||||
#ifndef __FPROTO_HOLTEKHT6P20B_H__
|
||||
#define __FPROTO_HOLTEKHT6P20B_H__
|
||||
|
||||
#include "subghzdbase.hpp"
|
||||
|
||||
typedef enum : uint8_t {
|
||||
Holtek_HT6P20BDecoderStepReset = 0,
|
||||
Holtek_HT6P20BDecoderStepFoundStartBit,
|
||||
Holtek_HT6P20BDecoderStepSaveDuration,
|
||||
Holtek_HT6P20BDecoderStepCheckDuration,
|
||||
} Holtek_HT6P20BDecoderStep;
|
||||
|
||||
class FProtoSubGhzDHoltekHt6p20b : public FProtoSubGhzDBase {
|
||||
public:
|
||||
FProtoSubGhzDHoltekHt6p20b() {
|
||||
sensorType = FPS_HOLTEKHT6P20B;
|
||||
te_short = 450;
|
||||
te_long = 900;
|
||||
te_delta = 270;
|
||||
min_count_bit_for_found = 28;
|
||||
}
|
||||
|
||||
void feed(bool level, uint32_t duration) {
|
||||
switch (parser_step) {
|
||||
case Holtek_HT6P20BDecoderStepReset:
|
||||
if ((!level) && (DURATION_DIFF(duration, te_short * 23) < te_delta * 23)) {
|
||||
// Found Preambula
|
||||
parser_step = Holtek_HT6P20BDecoderStepFoundStartBit;
|
||||
}
|
||||
break;
|
||||
case Holtek_HT6P20BDecoderStepFoundStartBit:
|
||||
if ((level) && (DURATION_DIFF(duration, te_short) < te_delta)) {
|
||||
// Found StartBit
|
||||
parser_step = Holtek_HT6P20BDecoderStepSaveDuration;
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
} else {
|
||||
parser_step = Holtek_HT6P20BDecoderStepReset;
|
||||
}
|
||||
break;
|
||||
case Holtek_HT6P20BDecoderStepSaveDuration:
|
||||
// save duration
|
||||
if (!level) {
|
||||
if (duration >= ((uint32_t)te_short * 10 + te_delta)) {
|
||||
if (decode_count_bit == min_count_bit_for_found) {
|
||||
data_count_bit = decode_count_bit;
|
||||
if (callback) callback(this);
|
||||
}
|
||||
decode_data = 0;
|
||||
decode_count_bit = 0;
|
||||
parser_step = Holtek_HT6P20BDecoderStepFoundStartBit;
|
||||
break;
|
||||
} else {
|
||||
te_last = duration;
|
||||
parser_step = Holtek_HT6P20BDecoderStepCheckDuration;
|
||||
}
|
||||
} else {
|
||||
parser_step = Holtek_HT6P20BDecoderStepReset;
|
||||
}
|
||||
break;
|
||||
case Holtek_HT6P20BDecoderStepCheckDuration:
|
||||
if (level) {
|
||||
if ((DURATION_DIFF(te_last, te_long) < te_delta * 2) &&
|
||||
(DURATION_DIFF(duration, te_short) < te_delta)) {
|
||||
subghz_protocol_blocks_add_bit(1);
|
||||
parser_step = Holtek_HT6P20BDecoderStepSaveDuration;
|
||||
} else if (
|
||||
(DURATION_DIFF(te_last, te_short) < te_delta) &&
|
||||
(DURATION_DIFF(duration, te_long) < te_delta * 2)) {
|
||||
subghz_protocol_blocks_add_bit(0);
|
||||
parser_step = Holtek_HT6P20BDecoderStepSaveDuration;
|
||||
} else {
|
||||
parser_step = Holtek_HT6P20BDecoderStepReset;
|
||||
}
|
||||
} else {
|
||||
parser_step = Holtek_HT6P20BDecoderStepReset;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -76,7 +76,7 @@ class FProtoSubGhzDSecPlusV1 : public FProtoSubGhzDBase {
|
||||
packet_accepted |= SECPLUS_V1_PACKET_2_ACCEPTED;
|
||||
|
||||
if (packet_accepted == (SECPLUS_V1_PACKET_1_ACCEPTED | SECPLUS_V1_PACKET_2_ACCEPTED)) {
|
||||
// subghz_protocol_secplus_v1_decode(); // disabled doe to lack of flash
|
||||
// subghz_protocol_secplus_v1_decode(); // disabled due to lack of flash
|
||||
// controller
|
||||
// uint32_t fixed = (data >> 32) & 0xFFFFFFFF;
|
||||
// cnt = data & 0xFFFFFFFF;
|
||||
|
||||
@@ -21,6 +21,7 @@ So include here the .hpp, and add a new element to the protos vector in the cons
|
||||
#include "c-ford_v0.hpp"
|
||||
#include "c-fiat_v0.hpp"
|
||||
#include "c-bmw_v0.hpp"
|
||||
// #include "c-kia_v6.hpp"
|
||||
|
||||
#ifndef __FPROTO_PROTOLISTCAR_H__
|
||||
#define __FPROTO_PROTOLISTCAR_H__
|
||||
@@ -42,6 +43,7 @@ class SubCarProtos : public FProtoListGeneral {
|
||||
protos[FPC_FORDV0] = new FProtoSubCarFordV0();
|
||||
protos[FPC_FIATV0] = new FProtoSubCarFiatV0();
|
||||
protos[FPC_BMWV0] = new FProtoSubCarBMWV0();
|
||||
// protos[FPC_KIAV6] = new FProtoSubCarKiaV6(); //-- disabled, due to whole encrypted.
|
||||
|
||||
for (uint8_t i = 0; i < FPC_COUNT; ++i) {
|
||||
if (protos[i] != NULL) protos[i]->setCallback(callbackTarget);
|
||||
|
||||
@@ -8,9 +8,6 @@ These values must be present on the protocol's constructor, like FProtoWeatherAc
|
||||
Also it must have a switch-case element in the getSubGhzDSensorTypeName() function, to display it's name.
|
||||
*/
|
||||
|
||||
#define FPM_AM 0
|
||||
#define FPM_FM 1
|
||||
|
||||
enum FPROTO_SUBCAR_SENSOR : uint8_t {
|
||||
FPC_Invalid = 0,
|
||||
FPC_SUZUKI = 1,
|
||||
@@ -24,6 +21,8 @@ enum FPROTO_SUBCAR_SENSOR : uint8_t {
|
||||
FPC_FORDV0 = 9,
|
||||
FPC_FIATV0 = 10,
|
||||
FPC_BMWV0 = 11,
|
||||
// FPC_KIAV6 = 12, //disabled, due to whole encrypted.
|
||||
// FPC_PSA = 13, // IS whole encrypted
|
||||
FPC_COUNT
|
||||
};
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ So include here the .hpp, and add a new element to the protos vector in the cons
|
||||
#include "s-gangqi.hpp"
|
||||
#include "s-marantec24.hpp"
|
||||
// GENIE FROM PR
|
||||
#include "s-holtek_ht6p20b.hpp"
|
||||
|
||||
#ifndef __FPROTO_PROTOLISTSGZ_H__
|
||||
#define __FPROTO_PROTOLISTSGZ_H__
|
||||
@@ -107,6 +108,7 @@ class SubGhzDProtos : public FProtoListGeneral {
|
||||
protos[FPS_LEGRAND] = new FProtoSubGhzDLegrand();
|
||||
protos[FPS_GANGQI] = new FProtoSubGhzDGangqi();
|
||||
protos[FPS_MARANTEC24] = new FProtoSubGhzDMarantec24();
|
||||
protos[FPS_HOLTEKHT6P20B] = new FProtoSubGhzDHoltekHt6p20b();
|
||||
|
||||
for (uint8_t i = 0; i < FPS_COUNT; ++i) {
|
||||
if (protos[i] != NULL) protos[i]->setCallback(callbackTarget);
|
||||
|
||||
@@ -58,6 +58,7 @@ enum FPROTO_SUBGHZD_SENSOR : uint8_t {
|
||||
FPS_SOMIFY_TELIS,
|
||||
FPS_GANGQI,
|
||||
FPS_MARANTEC24,
|
||||
FPS_HOLTEKHT6P20B,
|
||||
FPS_COUNT
|
||||
};
|
||||
|
||||
|
||||
+33
-14
@@ -19,7 +19,7 @@
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "proc_ook_stream_tx.hpp"
|
||||
#include "proc_bint_stream_tx.hpp"
|
||||
#include "sine_table_int8.hpp"
|
||||
#include "portapack_shared_memory.hpp"
|
||||
|
||||
@@ -27,27 +27,35 @@
|
||||
|
||||
#include "utility.hpp"
|
||||
|
||||
OOKProcessorStreamed::OOKProcessorStreamed() {
|
||||
BinaryTimedProcessorStreamed::BinaryTimedProcessorStreamed() {
|
||||
configured = false;
|
||||
baseband_thread.start();
|
||||
}
|
||||
|
||||
inline void OOKProcessorStreamed::write_sample(const buffer_c8_t& buffer, bool bit_value, size_t i) {
|
||||
int8_t re, im;
|
||||
|
||||
if (bit_value) {
|
||||
phase = (phase + 200); // What ?
|
||||
inline void BinaryTimedProcessorStreamed::write_sample(const buffer_c8_t& buffer, bool bit_value, size_t i) {
|
||||
int8_t re = 0, im = 0;
|
||||
if (mode == 0) {
|
||||
if (bit_value) {
|
||||
phase = (phase + 200);
|
||||
sphase = phase + (64 << 18);
|
||||
re = (sine_table_i8[(sphase & 0x03FC0000) >> 18]);
|
||||
im = (sine_table_i8[(phase & 0x03FC0000) >> 18]);
|
||||
}
|
||||
} else if (mode == 1) {
|
||||
// calculate the re, im based on the deviation uint32_t variable to get the re, im, to send out 2fsk signal. based on the bit_value
|
||||
if (bit_value) {
|
||||
phase += deviation_delta;
|
||||
} else {
|
||||
phase -= deviation_delta;
|
||||
}
|
||||
sphase = phase + (64 << 18);
|
||||
re = (sine_table_i8[(sphase & 0x03FC0000) >> 18]);
|
||||
im = (sine_table_i8[(phase & 0x03FC0000) >> 18]);
|
||||
} else {
|
||||
re = 0;
|
||||
im = 0;
|
||||
}
|
||||
buffer.p[i] = {re, im};
|
||||
}
|
||||
|
||||
void OOKProcessorStreamed::execute(const buffer_c8_t& buffer) {
|
||||
void BinaryTimedProcessorStreamed::execute(const buffer_c8_t& buffer) {
|
||||
if (!configured || !stream) return;
|
||||
|
||||
for (size_t i = 0; i < buffer.count; i++) {
|
||||
@@ -87,7 +95,7 @@ void OOKProcessorStreamed::execute(const buffer_c8_t& buffer) {
|
||||
}
|
||||
}
|
||||
|
||||
void OOKProcessorStreamed::on_message(const Message* const message) {
|
||||
void BinaryTimedProcessorStreamed::on_message(const Message* const message) {
|
||||
switch (message->id) {
|
||||
case Message::ID::ReplayConfig:
|
||||
configured = false;
|
||||
@@ -100,12 +108,23 @@ void OOKProcessorStreamed::on_message(const Message* const message) {
|
||||
shared_memory.application_queue.push(txprogress_message);
|
||||
break;
|
||||
|
||||
case Message::ID::StreamTXConfiguration:
|
||||
streamtx_config(*reinterpret_cast<const StreamTXConfigurationMessage*>(message));
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void OOKProcessorStreamed::replay_config(const ReplayConfigMessage& message) {
|
||||
void BinaryTimedProcessorStreamed::streamtx_config(const StreamTXConfigurationMessage& message) {
|
||||
mode = message.mode;
|
||||
deviation = message.deviation;
|
||||
uint64_t big_calc = (uint64_t)message.deviation << 26;
|
||||
deviation_delta = (uint32_t)(big_calc / OOK_SAMPLERATE);
|
||||
}
|
||||
|
||||
void BinaryTimedProcessorStreamed::replay_config(const ReplayConfigMessage& message) {
|
||||
if (message.config) {
|
||||
txprogress_message.progress = -2;
|
||||
shared_memory.application_queue.push(txprogress_message);
|
||||
@@ -121,7 +140,7 @@ void OOKProcessorStreamed::replay_config(const ReplayConfigMessage& message) {
|
||||
}
|
||||
|
||||
int main() {
|
||||
EventDispatcher event_dispatcher{std::make_unique<OOKProcessorStreamed>()};
|
||||
EventDispatcher event_dispatcher{std::make_unique<BinaryTimedProcessorStreamed>()};
|
||||
event_dispatcher.run();
|
||||
return 0;
|
||||
}
|
||||
+8
-2
@@ -34,10 +34,11 @@
|
||||
#include <memory>
|
||||
|
||||
#define OOK_SAMPLERATE 2280000U
|
||||
#define FM_DEVIATION 60000U
|
||||
|
||||
class OOKProcessorStreamed : public BasebandProcessor {
|
||||
class BinaryTimedProcessorStreamed : public BasebandProcessor {
|
||||
public:
|
||||
OOKProcessorStreamed();
|
||||
BinaryTimedProcessorStreamed();
|
||||
|
||||
void execute(const buffer_c8_t& buffer) override;
|
||||
void on_message(const Message* const message) override;
|
||||
@@ -53,6 +54,11 @@ class OOKProcessorStreamed : public BasebandProcessor {
|
||||
std::unique_ptr<StreamOutput> stream{};
|
||||
bool configured{false};
|
||||
void replay_config(const ReplayConfigMessage& message);
|
||||
void streamtx_config(const StreamTXConfigurationMessage& message);
|
||||
|
||||
uint8_t mode = 0; // am = 0, 2fsk = 1
|
||||
uint32_t deviation = FM_DEVIATION; // used in 2fsk
|
||||
uint32_t deviation_delta = (FM_DEVIATION * 4294967296ULL) / OOK_SAMPLERATE;
|
||||
|
||||
int32_t endsignals[3] = {0, 42069, 613379}; // 0 is skipped, count from 1, don't ask...
|
||||
uint8_t readerrs = 0; // to count in the array
|
||||
@@ -28,8 +28,10 @@
|
||||
#include "debug.hpp"
|
||||
#include "portapack_shared_memory.hpp"
|
||||
|
||||
#include "../flashsize.h"
|
||||
|
||||
#define PAGE_LEN 256U
|
||||
#define NUM_PAGES 4096U
|
||||
#define NUM_PAGES (4096U * FLASH_SIZE_MB)
|
||||
|
||||
void initialize_flash();
|
||||
void erase_flash();
|
||||
|
||||
@@ -0,0 +1,341 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Pezsma
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "proc_morse.hpp"
|
||||
#include "audio_dma.hpp"
|
||||
#include "portapack_shared_memory.hpp"
|
||||
#include "event_m4.hpp"
|
||||
#include <cmath>
|
||||
|
||||
void MorseProcessor::configure(uint8_t mode) {
|
||||
configured = false;
|
||||
|
||||
modulation = static_cast<ModulationMode>((uint8_t)mode);
|
||||
|
||||
if (modulation == ModulationMode::FM) { // FM
|
||||
decim_0.configure(taps_11k0_decim_0.taps);
|
||||
decim_1.configure(taps_11k0_decim_1.taps);
|
||||
channel_filter.configure(taps_11k0_channel.taps, 2);
|
||||
demod_cw_fm.configure(24000, 5000);
|
||||
} else {
|
||||
decim_0.configure(taps_4k25_decim_0.taps);
|
||||
decim_1.configure(taps_4k25_decim_1.taps);
|
||||
if (modulation == ModulationMode::AM) { // AM
|
||||
channel_filter.configure(taps_2k0_am_lpf_channel.taps, 4);
|
||||
} else { // SSB, DSB
|
||||
if (modulation == ModulationMode::DSB) // DSB
|
||||
channel_filter.configure(taps_1k5_dsb_lpf.taps, 4);
|
||||
else if (modulation == ModulationMode::USB) // USB
|
||||
channel_filter.configure(taps_1k5_USB_channel.taps, 4);
|
||||
else // LSB
|
||||
channel_filter.configure(taps_1k5_LSB_channel.taps, 4);
|
||||
}
|
||||
}
|
||||
|
||||
if (modulation == ModulationMode::FM)
|
||||
audio_output.configure(iir_config_passthrough, iir_config_passthrough, (float)user_squelch_level / 100.0f);
|
||||
else
|
||||
audio_output.configure(audio_12k_hpf_300hz_config);
|
||||
|
||||
meas_samples_in_period = 0;
|
||||
meas_last_period_len = 0;
|
||||
meas_consistency_count = 0;
|
||||
meas_signal_state_high = false;
|
||||
meas_freq_accumulator = 0.0f;
|
||||
meas_freq_count = 0;
|
||||
ui_update_timer = 0;
|
||||
|
||||
// Decoding reset
|
||||
s_prev_i = 0;
|
||||
s_prev2_i = 0;
|
||||
goertzel_count = 0;
|
||||
duration_samples = 0;
|
||||
was_signaling = false;
|
||||
|
||||
// Thresholds
|
||||
noise_floor = 5000;
|
||||
startup_delay = 20;
|
||||
squelch_is_open = true;
|
||||
squelch_hold = 0;
|
||||
|
||||
dc_average = 0.0f;
|
||||
|
||||
current_freq = 700.0f;
|
||||
update_goertzel_coeff(current_freq);
|
||||
|
||||
configured = true;
|
||||
}
|
||||
|
||||
inline buffer_f32_t MorseProcessor::demodulate(const buffer_c16_t& channel) {
|
||||
// AM,SSB,DSB always keeps squelch "technically" open for the demodulator
|
||||
if (modulation == ModulationMode::AM || modulation == ModulationMode::DSB) {
|
||||
squelch_is_open = true;
|
||||
return demod_AM.execute(channel, audio_buffer);
|
||||
} else {
|
||||
if (modulation == ModulationMode::USB || modulation == ModulationMode::LSB) {
|
||||
squelch_is_open = true;
|
||||
return demod_ssb.execute(channel, audio_buffer);
|
||||
}
|
||||
}
|
||||
return demod_cw_fm.execute(channel, audio_buffer);
|
||||
}
|
||||
|
||||
void MorseProcessor::update_goertzel_coeff(float freq) {
|
||||
// limit to algo capacity min/max
|
||||
if (freq < 300.0f) freq = 300.0f;
|
||||
if (freq > 2300.0f) freq = 2300.0f;
|
||||
|
||||
float sample_rate = (modulation == ModulationMode::FM) ? 24000.0f : 12000.0f;
|
||||
float omega = 2.0f * M_PI * freq / sample_rate;
|
||||
float omega_sq = omega * omega;
|
||||
float cos_approx = 1.0f - (omega_sq * 0.5f);
|
||||
// 16384 is the scaling factor for the Goertzel integer math
|
||||
coeff_int = (int32_t)(2.0f * cos_approx * 16384.0f);
|
||||
}
|
||||
|
||||
void MorseProcessor::measure_frequency(int32_t sample) {
|
||||
// Noise gate threshold
|
||||
const int32_t gate_threshold = (modulation == ModulationMode::FM) ? 4000 : 2000;
|
||||
|
||||
if (sample > gate_threshold || sample < -gate_threshold) {
|
||||
if (sample > 0 && !meas_signal_state_high) {
|
||||
// Rising Edge (End of Period)
|
||||
meas_signal_state_high = true;
|
||||
|
||||
if (meas_samples_in_period > 0) {
|
||||
bool period_is_stable = false;
|
||||
if (meas_last_period_len > 0) {
|
||||
int32_t diff = std::abs((int)meas_samples_in_period - (int)meas_last_period_len);
|
||||
if (diff <= 2) {
|
||||
meas_consistency_count++;
|
||||
period_is_stable = true;
|
||||
} else {
|
||||
meas_consistency_count = 0;
|
||||
}
|
||||
}
|
||||
meas_last_period_len = meas_samples_in_period;
|
||||
|
||||
// Wait for AT LEAST 3 STABLE CYCLES
|
||||
if (period_is_stable && meas_consistency_count > 5) {
|
||||
float base_rate = (modulation == ModulationMode::FM) ? 24000.0f : 12000.0f;
|
||||
float inst_freq = base_rate / (float)meas_samples_in_period;
|
||||
if (modulation == ModulationMode::DSB) {
|
||||
inst_freq /= 2.0f;
|
||||
}
|
||||
// Check for overflows
|
||||
if (inst_freq > 250 && inst_freq < 3000) {
|
||||
meas_freq_accumulator += inst_freq;
|
||||
meas_freq_count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
meas_samples_in_period = 0;
|
||||
|
||||
} else if (sample < 0) {
|
||||
meas_signal_state_high = false;
|
||||
}
|
||||
} else {
|
||||
// Silence detection
|
||||
if (meas_samples_in_period > 200) {
|
||||
meas_last_period_len = 0;
|
||||
meas_consistency_count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
meas_samples_in_period++;
|
||||
|
||||
ui_update_timer++;
|
||||
uint32_t update_limit = (modulation == ModulationMode::FM) ? 4800 : 2400; // ~200ms
|
||||
|
||||
if (ui_update_timer > update_limit) {
|
||||
if (meas_freq_count > 0) {
|
||||
float avg_freq = meas_freq_accumulator / (float)meas_freq_count;
|
||||
current_freq = avg_freq;
|
||||
// Round to 5Hz for display
|
||||
uint32_t stable_disp = (uint32_t)avg_freq;
|
||||
stable_disp = (stable_disp / 5) * 5;
|
||||
|
||||
freq_message.measured_frequency = stable_disp;
|
||||
shared_memory.application_queue.push(freq_message);
|
||||
}
|
||||
|
||||
meas_freq_accumulator = 0.0f;
|
||||
meas_freq_count = 0;
|
||||
ui_update_timer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void MorseProcessor::process_decoding(int32_t sample) {
|
||||
update_goertzel_coeff(current_freq);
|
||||
|
||||
// 1. Goertzel Algorithm
|
||||
int64_t s = (int64_t)sample + (((int64_t)coeff_int * s_prev_i) >> 14) - s_prev2_i;
|
||||
s_prev2_i = s_prev_i;
|
||||
s_prev_i = (int32_t)s;
|
||||
goertzel_count++;
|
||||
|
||||
// 2. Evaluation every 60 samples
|
||||
if (goertzel_count >= 60) {
|
||||
if (startup_delay > 0) {
|
||||
startup_delay--;
|
||||
// Fast learning phase
|
||||
int64_t pwr = (int64_t)s_prev_i * s_prev_i + (int64_t)s_prev2_i * s_prev2_i -
|
||||
(((int64_t)s_prev_i * s_prev2_i * coeff_int) >> 14);
|
||||
noise_floor = (noise_floor * 15 + pwr) / 16;
|
||||
} else {
|
||||
// Power calculation
|
||||
int64_t power = (int64_t)s_prev_i * s_prev_i + (int64_t)s_prev2_i * s_prev2_i -
|
||||
(((int64_t)s_prev_i * s_prev2_i * coeff_int) >> 14);
|
||||
|
||||
// Adaptive Noise Floor
|
||||
if (!was_signaling) {
|
||||
noise_floor = (noise_floor * 127 + power) / 128;
|
||||
}
|
||||
|
||||
// Threshold Calculation
|
||||
int64_t sensitivity = 4 + (user_squelch_level / 10);
|
||||
int64_t base_pwr_threshold = noise_floor * sensitivity;
|
||||
int64_t current_pwr_threshold = was_signaling ? (base_pwr_threshold / 2) : base_pwr_threshold;
|
||||
|
||||
// Tone Detection
|
||||
bool is_tone = squelch_is_open && (power > current_pwr_threshold) && (power > 150000);
|
||||
int32_t time_base = 250;
|
||||
// State Change Logic
|
||||
if (is_tone != was_signaling) {
|
||||
int32_t duration_us = (int32_t)((int64_t)duration_samples * time_base / 3);
|
||||
|
||||
// Send message if significant
|
||||
if (duration_us > 10000 || was_signaling) {
|
||||
message.state_durations[0] = was_signaling ? duration_us : -duration_us;
|
||||
message.state_cnt = 1; // 1 indicates valid state duration data
|
||||
shared_memory.application_queue.push(message);
|
||||
}
|
||||
was_signaling = is_tone;
|
||||
duration_samples = 0;
|
||||
}
|
||||
|
||||
// Timeout Logic
|
||||
if (!was_signaling && duration_samples > 28800) {
|
||||
int32_t duration_us = (int32_t)((int64_t)duration_samples * time_base / 3);
|
||||
message.state_durations[0] = -duration_us;
|
||||
message.state_cnt = 1;
|
||||
shared_memory.application_queue.push(message);
|
||||
duration_samples = 0;
|
||||
}
|
||||
}
|
||||
|
||||
duration_samples += 60;
|
||||
s_prev_i = 0;
|
||||
s_prev2_i = 0;
|
||||
goertzel_count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void MorseProcessor::execute(const buffer_c8_t& buffer) {
|
||||
if (!configured) return;
|
||||
|
||||
const auto decim_0_out = decim_0.execute(buffer, dst_buffer);
|
||||
const auto decim_1_out = decim_1.execute(decim_0_out, dst_buffer);
|
||||
const auto channel_out = channel_filter.execute(decim_1_out, dst_buffer);
|
||||
|
||||
auto audio_buf = demodulate(channel_out);
|
||||
|
||||
for (size_t i = 0; i < audio_buf.count; i++) {
|
||||
float raw_audio = audio_buf.p[i];
|
||||
|
||||
// Squelch Logic
|
||||
int32_t raw_int_abs = (int32_t)(raw_audio * 32768.0f);
|
||||
if (raw_int_abs < 0) raw_int_abs = -raw_int_abs;
|
||||
|
||||
int32_t audio_threshold = (user_squelch_level * user_squelch_level) * 3;
|
||||
|
||||
if (raw_int_abs > audio_threshold || user_squelch_level == 0) {
|
||||
squelch_is_open = true;
|
||||
squelch_hold = (modulation == ModulationMode::FM) ? 2400 : 1200;
|
||||
} else {
|
||||
if (squelch_hold > 0)
|
||||
squelch_hold--;
|
||||
else if (modulation == ModulationMode::FM)
|
||||
squelch_is_open = false;
|
||||
}
|
||||
|
||||
float decode_audio = raw_audio;
|
||||
if (modulation != ModulationMode::FM) {
|
||||
float gain = 16.0f;
|
||||
if (modulation == ModulationMode::USB || modulation == ModulationMode::LSB)
|
||||
gain = 5.0f;
|
||||
decode_audio *= gain;
|
||||
|
||||
// Hard Limiting / Clipping
|
||||
message.clipped = false;
|
||||
if (decode_audio > 1.0f) {
|
||||
decode_audio = 1.0f;
|
||||
message.clipped = true;
|
||||
} else if (decode_audio < -1.0f) {
|
||||
decode_audio = -1.0f;
|
||||
}
|
||||
audio_buf.p[i] = decode_audio;
|
||||
}
|
||||
|
||||
// DC BLOCKING
|
||||
if (modulation != ModulationMode::FM) {
|
||||
dc_average = (dc_average * 0.95f) + (decode_audio * 0.05f);
|
||||
measure_frequency((int32_t)((decode_audio - dc_average) * 32768.0f));
|
||||
} else {
|
||||
measure_frequency((int32_t)(raw_audio * 32768.0f));
|
||||
}
|
||||
|
||||
process_decoding((int32_t)(decode_audio * 32768.0f));
|
||||
|
||||
if (modulation == ModulationMode::FM && !squelch_is_open) {
|
||||
audio_buf.p[i] = 0.0f; // mute nfm on squelch
|
||||
}
|
||||
}
|
||||
|
||||
audio_output.write(audio_buf);
|
||||
}
|
||||
|
||||
void MorseProcessor::on_message(const Message* const p) {
|
||||
switch (p->id) {
|
||||
case Message::ID::MorseRXConfig: {
|
||||
auto morse_rx_msg = *reinterpret_cast<const MorseRXConfigureMessage*>(p);
|
||||
configure(morse_rx_msg.mode);
|
||||
break;
|
||||
}
|
||||
|
||||
case Message::ID::NBFMConfigure: {
|
||||
auto nbfm_msg = *reinterpret_cast<const NBFMConfigureMessage*>(p);
|
||||
user_squelch_level = nbfm_msg.squelch_level;
|
||||
audio_output.configure(iir_config_passthrough, iir_config_passthrough, (float)user_squelch_level / 100.0f);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
audio::dma::init_audio_out();
|
||||
EventDispatcher event_dispatcher{std::make_unique<MorseProcessor>()};
|
||||
event_dispatcher.run();
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Pezsma
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#ifndef __PROC_MORSE_H__
|
||||
#define __PROC_MORSE_H__
|
||||
|
||||
#include "baseband_processor.hpp"
|
||||
#include "baseband_thread.hpp"
|
||||
#include "message.hpp"
|
||||
#include "dsp_decimate.hpp"
|
||||
#include "dsp_demodulate.hpp"
|
||||
#include "audio_output.hpp"
|
||||
#include "dsp_fir_taps.hpp"
|
||||
#include "rssi_thread.hpp"
|
||||
|
||||
class MorseProcessor : public BasebandProcessor {
|
||||
public:
|
||||
MorseProcessor() {}
|
||||
|
||||
void execute(const buffer_c8_t& buffer) override;
|
||||
void on_message(const Message* const p) override;
|
||||
|
||||
private:
|
||||
void configure(uint8_t mode);
|
||||
buffer_f32_t demodulate(const buffer_c16_t& channel);
|
||||
void update_goertzel_coeff(float freq);
|
||||
void measure_frequency(int32_t sample);
|
||||
void process_decoding(int32_t sample);
|
||||
|
||||
BasebandThread baseband_thread{3072000, this, baseband::Direction::Receive};
|
||||
RSSIThread rssi_thread{};
|
||||
|
||||
std::array<complex16_t, 512> dst{};
|
||||
const buffer_c16_t dst_buffer{
|
||||
dst.data(),
|
||||
dst.size()};
|
||||
std::array<float, 32> audio{};
|
||||
const buffer_f32_t audio_buffer{
|
||||
audio.data(),
|
||||
audio.size()};
|
||||
|
||||
dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0{};
|
||||
dsp::decimate::FIRC16xR16x32Decim8 decim_1{};
|
||||
dsp::decimate::FIRAndDecimateComplex channel_filter{};
|
||||
dsp::demodulate::AM demod_AM{};
|
||||
dsp::demodulate::FM demod_cw_fm{};
|
||||
dsp::demodulate::SSB demod_ssb{};
|
||||
AudioOutput audio_output{};
|
||||
|
||||
bool configured{false};
|
||||
|
||||
enum class ModulationMode : uint8_t {
|
||||
AM = 0,
|
||||
FM = 1,
|
||||
DSB = 2,
|
||||
USB = 3,
|
||||
LSB = 4
|
||||
};
|
||||
|
||||
ModulationMode modulation = ModulationMode::AM;
|
||||
int32_t user_squelch_level{0};
|
||||
bool squelch_is_open{true};
|
||||
int32_t squelch_hold{0};
|
||||
|
||||
// frequency measurement variables
|
||||
uint32_t meas_samples_in_period{0};
|
||||
bool meas_signal_state_high{false};
|
||||
uint32_t meas_last_period_len{0};
|
||||
uint32_t meas_consistency_count{0};
|
||||
float meas_freq_accumulator{0.0f};
|
||||
uint32_t meas_freq_count{0};
|
||||
uint32_t ui_update_timer{0};
|
||||
float current_freq{700.0f};
|
||||
|
||||
// --- Decoding variables (Goertzel) ---
|
||||
int32_t coeff_int{0};
|
||||
int32_t s_prev_i{0};
|
||||
int32_t s_prev2_i{0};
|
||||
uint32_t goertzel_count{0};
|
||||
uint32_t duration_samples{0};
|
||||
bool was_signaling{false};
|
||||
int64_t noise_floor{5000};
|
||||
int32_t startup_delay{20};
|
||||
|
||||
float dc_average = 0.0f; // DC level tracking
|
||||
int32_t dc_average_int = 0;
|
||||
|
||||
MorseRXDataMessage message{};
|
||||
MorseRXfreqMessage freq_message{};
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,97 @@
|
||||
#include "proc_morsetx.hpp"
|
||||
#include "portapack_shared_memory.hpp"
|
||||
#include "sine_table_int8.hpp"
|
||||
#include "event_m4.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
void MorseTXProcessor::execute(const buffer_c8_t& buffer) {
|
||||
if (!configured) return;
|
||||
|
||||
for (size_t i = 0; i < buffer.count; i++) {
|
||||
int8_t sample_sin;
|
||||
int8_t sample_cos;
|
||||
|
||||
sample_sin = (sine_table_i8[(tone_phase & 0xFF000000) >> 24]);
|
||||
tone_phase += tone_delta;
|
||||
|
||||
im = 0;
|
||||
re = 0;
|
||||
|
||||
// modulation logic
|
||||
if (modulation == 0) { // AM modulation
|
||||
if (key_down) {
|
||||
re = 64 + (sample_sin >> 2);
|
||||
}
|
||||
} else if (modulation == 1) { // FM modulation
|
||||
if (key_down) {
|
||||
delta = sample_sin * fm_delta;
|
||||
} else {
|
||||
delta = 0;
|
||||
}
|
||||
|
||||
phase += delta;
|
||||
sphase = phase + (64 << 24);
|
||||
|
||||
re = (sine_table_i8[(sphase & 0xFF000000) >> 24]);
|
||||
im = (sine_table_i8[(phase & 0xFF000000) >> 24]);
|
||||
|
||||
} else if (modulation == 2) { // DSB
|
||||
if (key_down) {
|
||||
re = sample_sin;
|
||||
}
|
||||
} else if (modulation == 3) { // USB
|
||||
if (key_down) {
|
||||
sample_cos = (sine_table_i8[((tone_phase + 0x40000000) & 0xFF000000) >> 24]);
|
||||
re = sample_cos;
|
||||
im = sample_sin;
|
||||
}
|
||||
} else if (modulation == 4) { // LSB
|
||||
if (key_down) {
|
||||
sample_cos = (sine_table_i8[((tone_phase + 0x40000000) & 0xFF000000) >> 24]);
|
||||
re = sample_cos;
|
||||
im = (sine_table_i8[((tone_phase + 0x80000000) & 0xFF000000) >> 24]);
|
||||
}
|
||||
}
|
||||
buffer.p[i] = {re, im};
|
||||
}
|
||||
}
|
||||
|
||||
void MorseTXProcessor::on_message(const Message* const p) {
|
||||
switch (p->id) {
|
||||
case Message::ID::MorseTXConfigure: {
|
||||
auto message = *reinterpret_cast<const MorseTXConfigureMessage*>(p);
|
||||
tone_delta = message.tone * 1398; // scale
|
||||
tone = message.tone; // audio tone
|
||||
modulation = message.modulation;
|
||||
if (message.fm_delta == 0 && modulation == 1) {
|
||||
fm_delta = 90000;
|
||||
} else {
|
||||
uint64_t scale = 0xFFFFFFFFULL; // 32-bit max
|
||||
fm_delta = (uint32_t)((uint64_t)message.fm_delta * (scale / 1536000));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case Message::ID::MorseTXkey: {
|
||||
auto key = *reinterpret_cast<const MorseTXkeyMessage*>(p);
|
||||
key_down = key.key_down;
|
||||
configured = true;
|
||||
if (key_down)
|
||||
audio::dma::beep_start(tone, 24000, 0);
|
||||
else
|
||||
audio::dma::beep_stop();
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
audio::dma::init_audio_out();
|
||||
EventDispatcher event_dispatcher{std::make_unique<MorseTXProcessor>()};
|
||||
event_dispatcher.run();
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
#ifndef __PROC_MORSETX_H__
|
||||
#define __PROC_MORSETX_H__
|
||||
|
||||
#include "baseband_processor.hpp"
|
||||
#include "baseband_thread.hpp"
|
||||
#include "portapack_shared_memory.hpp"
|
||||
#include "audio_output.hpp"
|
||||
#include "audio_dma.hpp"
|
||||
|
||||
#define AUDIO_OUTPUT_BUFFER_SIZE 32
|
||||
#define AUDIO_SAMPLING_RATE 24000
|
||||
|
||||
class MorseTXProcessor : public BasebandProcessor {
|
||||
public:
|
||||
void execute(const buffer_c8_t& buffer) override;
|
||||
void on_message(const Message* const msg) override;
|
||||
|
||||
private:
|
||||
bool configured{false};
|
||||
bool key_down{false}; // currently the virtual key is pressed or not.
|
||||
|
||||
int8_t sample{0}, re{0}, im{0};
|
||||
uint8_t modulation{0}; // 0=AM, 1=FM, 2=DSB, 3=USB, 4=LSB
|
||||
uint32_t tone_delta{0}; // shifting value by tone
|
||||
uint32_t fm_delta{};
|
||||
uint32_t tone_phase{0};
|
||||
uint32_t tone{0}; // audio tone frequeny
|
||||
int32_t phase{0}, sphase{0}, delta{0}; // sample generation.
|
||||
|
||||
BasebandThread baseband_thread{1536000, this, baseband::Direction::Transmit};
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -25,14 +25,6 @@
|
||||
#include "portapack_shared_memory.hpp"
|
||||
#include "event_m4.hpp"
|
||||
|
||||
static inline int get_quadrant(int16_t i, int16_t q) {
|
||||
if (i >= 0) {
|
||||
return (q >= 0) ? 0 : 3;
|
||||
} else {
|
||||
return (q >= 0) ? 1 : 2;
|
||||
}
|
||||
}
|
||||
|
||||
void SubCarProcessor::execute(const buffer_c8_t& buffer) {
|
||||
if (!configured) return;
|
||||
|
||||
@@ -46,10 +38,6 @@ void SubCarProcessor::execute(const buffer_c8_t& buffer) {
|
||||
const auto decim_1_out = decim_1.execute(decim_0_out, dst_buffer); // Input:512 complex/2 (decim factor) = 256_output complex ( 512 I/Q samples)
|
||||
feed_channel_stats(decim_1_out);
|
||||
|
||||
// for fm
|
||||
const int32_t DC_ALPHA = 5; // Auto-centering speed
|
||||
int32_t buffer_rotation_sum = 0;
|
||||
|
||||
for (size_t i = 0; i < decim_1_out.count; i++) {
|
||||
// am
|
||||
threshold = (low_estimate + high_estimate) / 2;
|
||||
@@ -57,126 +45,103 @@ void SubCarProcessor::execute(const buffer_c8_t& buffer) {
|
||||
int16_t re = decim_1_out.p[i].real();
|
||||
int16_t im = decim_1_out.p[i].imag();
|
||||
uint32_t mag = ((uint32_t)re * (uint32_t)re) + ((uint32_t)im * (uint32_t)im);
|
||||
|
||||
mag = (mag >> 10);
|
||||
int32_t const ook_low_delta = mag - low_estimate;
|
||||
bool meashl = currentHiLow;
|
||||
if (sig_state == STATE_IDLE) {
|
||||
if (mag > (threshold + hysteresis)) { // just become high
|
||||
meashl = true;
|
||||
sig_state = STATE_PULSE;
|
||||
numg = 0;
|
||||
} else {
|
||||
meashl = false; // still low
|
||||
low_estimate += ook_low_delta / OOK_EST_LOW_RATIO;
|
||||
low_estimate += ((ook_low_delta > 0) ? 1 : -1); // Hack to compensate for lack of fixed-point scaling
|
||||
// Calculate default OOK high level estimate
|
||||
high_estimate = 1.35 * low_estimate; // Default is a ratio of low level
|
||||
high_estimate = std::max(high_estimate, min_high_level);
|
||||
high_estimate = std::min(high_estimate, (uint32_t)OOK_MAX_HIGH_LEVEL);
|
||||
}
|
||||
|
||||
} else if (sig_state == STATE_PULSE) {
|
||||
++numg;
|
||||
if (numg > 100) numg = 100;
|
||||
if (mag < (threshold - hysteresis)) {
|
||||
// check if really a bad value
|
||||
if (numg < 3) {
|
||||
// susp
|
||||
sig_state = STATE_GAP;
|
||||
} else {
|
||||
if (modulation == 0) {
|
||||
int32_t const ook_low_delta = mag - low_estimate;
|
||||
bool meashl = currentHiLow;
|
||||
if (sig_state == STATE_IDLE) {
|
||||
if (mag > (threshold + hysteresis)) { // just become high
|
||||
meashl = true;
|
||||
sig_state = STATE_PULSE;
|
||||
numg = 0;
|
||||
sig_state = STATE_GAP_START;
|
||||
} else {
|
||||
meashl = false; // still low
|
||||
low_estimate += ook_low_delta / OOK_EST_LOW_RATIO;
|
||||
low_estimate += ((ook_low_delta > 0) ? 1 : -1); // Hack to compensate for lack of fixed-point scaling
|
||||
// Calculate default OOK high level estimate
|
||||
high_estimate = 1.35 * low_estimate; // Default is a ratio of low level
|
||||
high_estimate = std::max(high_estimate, min_high_level);
|
||||
high_estimate = std::min(high_estimate, (uint32_t)OOK_MAX_HIGH_LEVEL);
|
||||
}
|
||||
|
||||
} else if (sig_state == STATE_PULSE) {
|
||||
++numg;
|
||||
if (numg > 100) numg = 100;
|
||||
if (mag < (threshold - hysteresis)) {
|
||||
// check if really a bad value
|
||||
if (numg < 3) {
|
||||
// susp
|
||||
sig_state = STATE_GAP;
|
||||
} else {
|
||||
numg = 0;
|
||||
sig_state = STATE_GAP_START;
|
||||
}
|
||||
meashl = false; // low
|
||||
} else {
|
||||
high_estimate += mag / OOK_EST_HIGH_RATIO - high_estimate / OOK_EST_HIGH_RATIO;
|
||||
high_estimate = std::max(high_estimate, min_high_level);
|
||||
high_estimate = std::min(high_estimate, (uint32_t)OOK_MAX_HIGH_LEVEL);
|
||||
meashl = true; // still high
|
||||
}
|
||||
} else if (sig_state == STATE_GAP_START) {
|
||||
++numg;
|
||||
if (mag > (threshold + hysteresis)) { // New pulse?
|
||||
sig_state = STATE_PULSE;
|
||||
meashl = true;
|
||||
} else if (numg >= 3) {
|
||||
sig_state = STATE_GAP;
|
||||
meashl = false; // gap
|
||||
}
|
||||
} else if (sig_state == STATE_GAP) {
|
||||
++numg;
|
||||
if (mag > (threshold + hysteresis)) { // New pulse?
|
||||
numg = 0;
|
||||
sig_state = STATE_PULSE;
|
||||
meashl = true;
|
||||
} else {
|
||||
meashl = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (meashl == currentHiLow && currentDuration < 30'000'000) // allow pass 'end' signal
|
||||
{
|
||||
currentDuration += nsPerDecSamp;
|
||||
} else { // called on change, so send the last duration and dir.
|
||||
if (currentDuration >= 30'000'000) sig_state = STATE_IDLE;
|
||||
if (protoList) protoList->feed(currentHiLow, currentDuration / 1000);
|
||||
currentDuration = nsPerDecSamp;
|
||||
currentHiLow = meashl;
|
||||
}
|
||||
}
|
||||
if (modulation == 1) {
|
||||
int32_t discrim = ((int32_t)im * fm_state.last_re) - ((int32_t)re * fm_state.last_im);
|
||||
fm_state.last_re = re;
|
||||
fm_state.last_im = im;
|
||||
fm_state.smoothed_discrim += (discrim - fm_state.smoothed_discrim) >> 4;
|
||||
|
||||
// --- FM Part (Simple 2-FSK) ---
|
||||
if (mag > (threshold / 2)) {
|
||||
const int32_t fm_hysteresis = 2000;
|
||||
bool new_level = fm_state.current_logic_level;
|
||||
if (fm_state.smoothed_discrim > fm_hysteresis) {
|
||||
new_level = true;
|
||||
} else if (fm_state.smoothed_discrim < -fm_hysteresis) {
|
||||
new_level = false;
|
||||
}
|
||||
if (new_level == fm_state.current_logic_level) {
|
||||
fm_state.buffer_count++;
|
||||
} else {
|
||||
int32_t duration_us = (fm_state.buffer_count * nsPerDecSamp) / 1000;
|
||||
if (duration_us > 15) {
|
||||
if (protoList) protoList->feed(fm_state.current_logic_level, duration_us);
|
||||
}
|
||||
fm_state.current_logic_level = new_level;
|
||||
fm_state.buffer_count = 1;
|
||||
}
|
||||
meashl = false; // low
|
||||
} else {
|
||||
high_estimate += mag / OOK_EST_HIGH_RATIO - high_estimate / OOK_EST_HIGH_RATIO;
|
||||
high_estimate = std::max(high_estimate, min_high_level);
|
||||
high_estimate = std::min(high_estimate, (uint32_t)OOK_MAX_HIGH_LEVEL);
|
||||
meashl = true; // still high
|
||||
}
|
||||
} else if (sig_state == STATE_GAP_START) {
|
||||
++numg;
|
||||
if (mag > (threshold + hysteresis)) { // New pulse?
|
||||
sig_state = STATE_PULSE;
|
||||
meashl = true;
|
||||
} else if (numg >= 3) {
|
||||
sig_state = STATE_GAP;
|
||||
meashl = false; // gap
|
||||
}
|
||||
} else if (sig_state == STATE_GAP) {
|
||||
++numg;
|
||||
if (mag > (threshold + hysteresis)) { // New pulse?
|
||||
numg = 0;
|
||||
sig_state = STATE_PULSE;
|
||||
meashl = true;
|
||||
} else {
|
||||
meashl = false;
|
||||
fm_state.buffer_count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (meashl == currentHiLow && currentDuration < 30'000'000) // allow pass 'end' signal
|
||||
{
|
||||
currentDuration += nsPerDecSamp;
|
||||
} else { // called on change, so send the last duration and dir.
|
||||
if (currentDuration >= 30'000'000) sig_state = STATE_IDLE;
|
||||
if (protoList) protoList->feed(currentHiLow, currentDuration / 1000);
|
||||
currentDuration = nsPerDecSamp;
|
||||
currentHiLow = meashl;
|
||||
}
|
||||
|
||||
// fm part: -- NOT WORKING!!!! TODO FIX. AI code ;)
|
||||
int current_quad = get_quadrant(re, im);
|
||||
// Calculate Step (Current - Previous)
|
||||
int diff = current_quad - fm_state.prev_quad;
|
||||
// Handle Wrap-Around (crossing from Q3 to Q0 or Q0 to Q3)
|
||||
// 3 -> 0 should be +1 (CCW)
|
||||
// 0 -> 3 should be -1 (CW)
|
||||
if (diff == -3)
|
||||
diff = 1;
|
||||
else if (diff == 3)
|
||||
diff = -1;
|
||||
// Update History
|
||||
fm_state.prev_quad = current_quad;
|
||||
// Accumulate Rotation
|
||||
buffer_rotation_sum += diff;
|
||||
}
|
||||
|
||||
// fm finish:
|
||||
// 3. AUTO-CENTERING (DC BLOCKER)
|
||||
// Even with quadrant counting, "drift" (hand effect) makes the wheel spin
|
||||
// faster or slower. We need to subtract the average speed.
|
||||
// Update our "Average Speed" estimate
|
||||
// Note: buffer_rotation_sum is roughly proportional to frequency.
|
||||
fm_state.dc_offset = (fm_state.dc_offset * ((1 << DC_ALPHA) - 1) + buffer_rotation_sum) >> DC_ALPHA;
|
||||
// Remove the drift
|
||||
int32_t centered_rotation = buffer_rotation_sum - fm_state.dc_offset;
|
||||
// 4. LOW PASS FILTER
|
||||
const int32_t LPF_ALPHA = 4;
|
||||
fm_state.smoothed_error = (fm_state.smoothed_error * (LPF_ALPHA - 1) + centered_rotation) / LPF_ALPHA;
|
||||
// 5. DECISION LOGIC
|
||||
// Threshold is small now because we are counting quadrant steps.
|
||||
// Max steps per buffer (256 samples) is 256.
|
||||
// Typical FSK deviation might give you +/- 10 to 50 steps per buffer.
|
||||
const int32_t THRESHOLD = 3;
|
||||
bool new_level = fm_state.current_logic_level;
|
||||
if (fm_state.smoothed_error > THRESHOLD) {
|
||||
new_level = true;
|
||||
} else if (fm_state.smoothed_error < -THRESHOLD) {
|
||||
new_level = false;
|
||||
}
|
||||
// 6. TIMING OUTPUT
|
||||
if (new_level == fm_state.current_logic_level) {
|
||||
fm_state.buffer_count++;
|
||||
} else {
|
||||
// Output pulse duration
|
||||
int32_t duration_us = fm_state.buffer_count * 512;
|
||||
|
||||
if (duration_us > 250) {
|
||||
if (protoListFm) protoListFm->feed(fm_state.current_logic_level, duration_us);
|
||||
}
|
||||
fm_state.current_logic_level = new_level;
|
||||
fm_state.buffer_count = 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,6 +154,14 @@ void SubCarProcessor::configure(const SubGhzFPRxConfigureMessage& message) {
|
||||
// constexpr size_t decim_0_output_fs = baseband_fs / decim_0.decimation_factor; //unused
|
||||
// constexpr size_t decim_1_output_fs = decim_0_output_fs / decim_1.decimation_factor; //unused
|
||||
|
||||
if (modulation != message.modulation) {
|
||||
// reload protos to reset them all
|
||||
if (protoList) {
|
||||
delete protoList;
|
||||
}
|
||||
protoList = new SubCarProtos();
|
||||
}
|
||||
modulation = message.modulation;
|
||||
baseband_fs = message.sampling_rate;
|
||||
baseband_thread.set_sampling_rate(baseband_fs);
|
||||
nsPerDecSamp = 1'000'000'000 / baseband_fs * 8; // Scaled it due to less array buffer sampes due to /8 decimation. 250 nseg (4Mhz) * 8
|
||||
|
||||
@@ -56,8 +56,8 @@ class SubCarProcessor : public BasebandProcessor {
|
||||
uint32_t low_estimate = 100;
|
||||
uint32_t high_estimate = 12000;
|
||||
uint32_t min_high_level = 10;
|
||||
uint8_t numg = 0; // count of matched signals to filter spikes
|
||||
size_t baseband_fs = 0; // will be set later by configure message
|
||||
uint8_t numg = 0; // count of matched signals to filter spikes
|
||||
size_t baseband_fs = 4'000'000; // will be set later by configure message
|
||||
uint32_t nsPerDecSamp = 0;
|
||||
|
||||
/* Array Buffer aux. used in decim0 and decim1 IQ c16 signed data ; (decim0 defines the max length of the array) */
|
||||
@@ -74,20 +74,20 @@ class SubCarProcessor : public BasebandProcessor {
|
||||
uint32_t threshold = 0x0630;
|
||||
bool currentHiLow = false;
|
||||
bool configured{false};
|
||||
uint8_t mode = 0; // 0 = am, 1 = fm
|
||||
|
||||
// fm part:
|
||||
struct DemodFMState {
|
||||
int prev_quad = 0; // Stores 0, 1, 2, or 3
|
||||
int32_t dc_offset = 0;
|
||||
int32_t smoothed_error = 0;
|
||||
bool current_logic_level = false;
|
||||
uint32_t buffer_count = 0;
|
||||
int16_t last_re = 0; // Store previous Real sample
|
||||
int16_t last_im = 0;
|
||||
int32_t smoothed_discrim = 0;
|
||||
};
|
||||
DemodFMState fm_state{};
|
||||
|
||||
FProtoListGeneral* protoList = new SubCarProtos(); // holds all the protocols we can parse
|
||||
FProtoListGeneral* protoListFm = new SubCarProtos(); // holds all the protocols we can parse, but for fm (dupe, bc most of it is dual)
|
||||
uint8_t modulation = 0; // 0 am, 1 fm
|
||||
|
||||
FProtoListGeneral* protoList = new SubCarProtos(); // holds all the protocols we can parse
|
||||
void configure(const SubGhzFPRxConfigureMessage& message);
|
||||
|
||||
/* NB: Threads should be the last members in the class definition. */
|
||||
|
||||
@@ -241,7 +241,7 @@ w25q80bv_driver_t spi_flash = {
|
||||
|
||||
sgpio_config_t sgpio_config = {
|
||||
.gpio_q_invert = &gpio_q_invert,
|
||||
.gpio_hw_sync_enable = &gpio_hw_sync_enable,
|
||||
.gpio_trigger_enable = &gpio_hw_sync_enable,
|
||||
.slice_mode_multislice = true,
|
||||
};
|
||||
|
||||
@@ -950,7 +950,7 @@ void pin_setup(void) {
|
||||
#ifdef HACKRF_ONE
|
||||
if (detected_platform() == BOARD_ID_HACKRF1_R9) {
|
||||
rf_path.gpio_rx = &gpio_h1r9_rx;
|
||||
sgpio_config.gpio_hw_sync_enable = &gpio_h1r9_hw_sync_enable;
|
||||
sgpio_config.gpio_trigger_enable = &gpio_h1r9_hw_sync_enable;
|
||||
}
|
||||
#endif
|
||||
rf_path_pin_setup(&rf_path);
|
||||
@@ -1041,8 +1041,8 @@ void set_leds(const uint8_t state) {
|
||||
}
|
||||
}
|
||||
|
||||
void hw_sync_enable(const hw_sync_mode_t hw_sync_mode) {
|
||||
gpio_write(sgpio_config.gpio_hw_sync_enable, hw_sync_mode == 1);
|
||||
void trigger_enable(bool hw_sync_mode) {
|
||||
gpio_write(sgpio_config.gpio_trigger_enable, hw_sync_mode == 1);
|
||||
}
|
||||
|
||||
void halt_and_flash(const uint32_t duration) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user