Compare commits

...

16 Commits

Author SHA1 Message Date
gullradriel 70ac1a03a1 Make stable release (#2895)
* updated submodule
* updated versions
2025-12-21 11:39:47 +01:00
Totoo 2261e53981 fix bit counts (#2894) 2025-12-21 11:38:35 +01:00
Totoo f86d3e51f1 Subcar (#2893) 2025-12-21 11:21:55 +01:00
StarVore Labs f71f19e719 Addition of SSTV RX application (#2888) 2025-12-17 13:14:19 +01:00
RocketGod c53adfc765 Refactor FLEX RX UI to use console for messages (#2890) 2025-12-14 10:08:39 -08:00
gullradriel 7e8ad83537 Update clang format to 18 (#2891)
* move to clang-format-18
* clang-format-18 indendation
2025-12-14 11:26:21 +01:00
RocketGod 472571b1ed Refactor FLEX RX UI to add color cycling and message log (#2885)
Replaces the console with a scrollable menu view for displaying FLEX messages, adds support for cycling text colors, and implements message logging with line wrapping and persistence. Also refactors frequency handling for persistence and updates UI element layout and initialization.
2025-12-12 19:40:15 +01:00
Tim Elfelt 106e56abc3 FLEX pager app (#2883)
* Add FLEX pager support

- Introduced a new FLEX configuration function in baseband_api.
- Added FLEX application view and associated UI elements.
- Implemented FLEX processing logic in proc_flex, including demodulation and message handling.
- Updated CMakeLists to include new FLEX source files and headers.
- Enhanced message system to support FLEX-specific messages and statistics.

This commit lays the groundwork for FLEX pager functionality, allowing for the reception and processing of FLEX messages.

* Fixed baseband and moved app to external with some other fixes.

* Format code

---------

Co-authored-by: RocketGod <57732082+RocketGod-git@users.noreply.github.com>
2025-12-12 17:43:51 +01:00
zxkmm 4129d57c09 fix dead link (#2882)
* fix dead link

* submodule
2025-12-06 07:34:10 -08:00
Totoo caac5e1041 Sonde + map (#2879)
* Update geomap view position on GPS data reception

* Refactor timestamp and temperature/humidity display formatting in SondeView

* Implement battery voltage reading for Meteomodem M20

* Enhance GPS data validation and update OSM zoom handling

- Introduced a new method to validate GPS data in the Packet structure.
- Updated the SondeView to use the new GPS validation method.
- Modified GeoMap to improve handling of OSM zoom levels and ensure consistent usage of real zoom values.
- Adjusted battery voltage calculation for Meteomodem M20 to ensure correct scaling.

* Add serial number extraction for Meteomodem M20 support

* Add support for Meteomodem M20 temperature and humidity readings

* Update log file naming to include timestamp in SondeView

* Add pressure reading support for Meteomodem M20 and update UI

* Update SondeView UI layout and enhance Meteomodem M20 packet handling

* Add vertical speed calculation and display to SondeView

* Fix set_fsk function parameter type for samplesPerSymbol
2025-11-26 19:13:21 +01:00
Totoo 6b02ba6e5d Faster osm (#2874)
Much faster OSM map handler
2025-11-21 09:30:01 +01:00
Totoo c01597baf2 Pocsag manual baud option (#2870)
* baseband part of manual baud control of pocsag

* added manual pocsag baud to ui, to fix #2546

* fix adult toys settings filename
2025-11-18 14:05:27 +01:00
Totoo 43a5163b77 prevent sonde pos to change to 0 when the frame is invalid. fixes #2862 (#2869) 2025-11-18 19:07:40 +08:00
jLynx c46cc431c9 Revert "resolve conflicts merge" (#2860)
This reverts commit 344aa0c741.
2025-11-08 10:21:33 +13:00
jLynx a4d23768c1 Update version.txt (#2858) 2025-11-08 10:13:35 +13:00
gullradriel 344aa0c741 resolve conflicts merge 2025-11-07 21:36:41 +01:00
93 changed files with 6901 additions and 169 deletions
+1
View File
@@ -19,5 +19,6 @@ jobs:
- name: clang-format Check
uses: jidicula/clang-format-action@v4.11.0
with:
clang-format-version: '18'
check-path: ${{ matrix.path }}
fallback-style: Chromium
+1 -1
View File
@@ -1 +1 @@
v2.2.0
v2.3.1
+1 -1
View File
@@ -1 +1 @@
v2.3.0
v2.3.2
+1 -1
View File
@@ -159,7 +159,7 @@ class AISAppView : public View {
~AISAppView();
void set_parent_rect(const Rect new_parent_rect) override;
void paint(Painter&) override{};
void paint(Painter&) override {};
void focus() override;
+1 -1
View File
@@ -59,7 +59,7 @@ class BLECommView : public View {
~BLECommView();
void set_parent_rect(const Rect new_parent_rect) override;
void paint(Painter&) override{};
void paint(Painter&) override {};
void focus() override;
+1 -1
View File
@@ -209,7 +209,7 @@ class BLERxView : public View {
~BLERxView();
void set_parent_rect(const Rect new_parent_rect) override;
void paint(Painter&) override{};
void paint(Painter&) override {};
void focus() override;
+1 -1
View File
@@ -105,7 +105,7 @@ class BLETxView : public View {
~BLETxView();
void set_parent_rect(const Rect new_parent_rect) override;
void paint(Painter&) override{};
void paint(Painter&) override {};
void focus() override;
+5 -2
View File
@@ -56,6 +56,7 @@ POCSAGSettingsView::POCSAGSettingsView(
: settings_{settings} {
add_children(
{&labels,
&opt_baud_rate,
&check_log,
&check_log_raw,
&check_small_font,
@@ -65,6 +66,7 @@ POCSAGSettingsView::POCSAGSettingsView(
&field_filter_address,
&button_save});
opt_baud_rate.set_by_value(settings_.baud_rate);
check_log.set_value(settings_.enable_logging);
check_log_raw.set_value(settings_.enable_raw_log);
check_small_font.set_value(settings_.enable_small_font);
@@ -81,7 +83,7 @@ POCSAGSettingsView::POCSAGSettingsView(
settings_.hide_addr_only = check_hide_addr_only.value();
settings_.filter_mode = opt_filter_mode.selected_index_value();
settings_.filter_address = field_filter_address.to_integer();
settings_.baud_rate = opt_baud_rate.selected_index_value();
nav.pop();
};
}
@@ -142,7 +144,7 @@ POCSAGAppView::POCSAGAppView(NavigationView& nav)
audio::output::start();
receiver_model.enable();
baseband::set_pocsag();
baseband::set_pocsag((int8_t)settings_.baud_rate);
}
void POCSAGAppView::focus() {
@@ -182,6 +184,7 @@ void POCSAGAppView::refresh_ui() {
btn_text = "Filter Last";
break;
}
baseband::set_pocsag((int8_t)settings_.baud_rate);
button_filter_last.set_text(btn_text);
}
+11
View File
@@ -126,6 +126,7 @@ struct POCSAGSettings {
bool hide_bad_data = false;
bool hide_addr_only = false;
uint8_t filter_mode = false;
int32_t baud_rate = -1;
uint32_t filter_address = 0;
};
@@ -139,7 +140,16 @@ class POCSAGSettingsView : public View {
private:
POCSAGSettings& settings_;
OptionsField opt_baud_rate{
{8 * 8, 0 * 16},
4,
{{"Auto", -1},
{" 512", 0},
{"1200", 1},
{"2400", 2}}};
Labels labels{
{{2 * 8, 0 * 16}, "Baud:", Theme::getInstance()->fg_light->foreground},
{{2 * 8, 12 * 16}, "Filter Mode:", Theme::getInstance()->fg_light->foreground},
{{2 * 8, 13 * 16}, "Filter Addr:", Theme::getInstance()->fg_light->foreground},
};
@@ -221,6 +231,7 @@ class POCSAGAppView : public View {
{"filter_address"sv, &settings_.filter_address},
{"hide_bad_data"sv, &settings_.hide_bad_data},
{"hide_addr_only"sv, &settings_.hide_addr_only},
{"baud_rate"sv, &settings_.baud_rate},
}};
void refresh_ui();
@@ -10,7 +10,7 @@ namespace ui {
constexpr std::string_view mayhem_information_list[] = {
"#****** Mayhem Community ******",
" ",
" https://discord.mayhem.app",
" https://discord.hackrf.app",
" ",
"#**** List of contributors ****",
" ",
+2 -2
View File
@@ -1086,8 +1086,8 @@ void ReconView::on_statistics_update(const ChannelStatistics& statistics) {
if (stepper < 0) stepper++;
if (stepper > 0) stepper--;
} // if( recon || stepper != 0 || index_stepper != 0 )
} // if (frequency_list.size() > 0 )
} /* on_statistics_updates */
} // if (frequency_list.size() > 0 )
} /* on_statistics_updates */
}
handle_retune();
recon_redraw();
+34 -12
View File
@@ -30,11 +30,10 @@
#include "portapack.hpp"
#include <cstring>
#include <stdio.h>
#include "rtc_time.hpp"
using namespace portapack;
namespace pmem = portapack::persistent_memory;
#include "string_format.hpp"
#include "complex.hpp"
void SondeLogger::on_packet(const sonde::Packet& packet) {
@@ -65,6 +64,8 @@ SondeView::SondeView(NavigationView& nav)
&text_frame,
&text_temp,
&text_humid,
&text_press,
&text_vspeed,
&geopos,
&button_see_qr,
&button_see_map});
@@ -107,7 +108,7 @@ SondeView::SondeView(NavigationView& nav)
logger = std::make_unique<SondeLogger>();
if (logger)
logger->append(logs_dir / u"SONDE.TXT");
logger->append(logs_dir / u"SONDE_" + to_string_timestamp(rtc_time::now()) + u".TXT");
if (pmem::beep_on_packets()) {
audio::set_rate(audio::Rate::Hz_24000);
@@ -155,7 +156,7 @@ void SondeView::on_packet(const sonde::Packet& packet) {
sonde_id = packet.serial_number(); // used also as tag on the geomap
text_serial.set(sonde_id);
text_timestamp.set(to_string_timestamp(packet.received_at()));
text_timestamp.set(to_string_datetime(packet.received_at(), TimeFormat::YMDHMS));
text_voltage.set(unit_auto_scale(packet.battery_voltage(), 2, 2) + "V");
@@ -163,21 +164,42 @@ void SondeView::on_packet(const sonde::Packet& packet) {
temp_humid_info = packet.get_temp_humid();
if (temp_humid_info.humid != 0) {
double decimals = abs(get_decimals(temp_humid_info.humid, 10, true));
text_humid.set(to_string_dec_int((int)temp_humid_info.humid) + "." + to_string_dec_uint(decimals, 1) + "%");
text_humid.set(to_string_decimal(temp_humid_info.humid, 1) + "%");
}
if (temp_humid_info.temp != 0) {
double decimals = abs(get_decimals(temp_humid_info.temp, 10, true));
text_temp.set(to_string_dec_int((int)temp_humid_info.temp) + "." + to_string_dec_uint(decimals, 1) + STR_DEGREES_C);
text_temp.set(to_string_decimal(temp_humid_info.temp, 1) + STR_DEGREES_C);
}
if (packet.get_pressure() != 0) {
text_press.set(to_string_decimal(packet.get_pressure(), 1) + " hPa");
}
gps_info = packet.get_GPS_data();
geopos.set_altitude(gps_info.alt);
geopos.set_lat(gps_info.lat);
geopos.set_lon(gps_info.lon);
if (last_timestamp_update_ != 0 && last_altitude_ != 0) {
// calculate speeds
float vspeed = 0;
time_t currpackettime = rtc_time::rtcToUnixUTC(packet.received_at());
int32_t time_diff = (currpackettime - last_timestamp_update_);
if (time_diff >= 10) { // update only every 10 seconds
vspeed = (static_cast<int>(gps_info.alt) - static_cast<int>(last_altitude_)) / (float)time_diff;
last_timestamp_update_ = currpackettime;
last_altitude_ = gps_info.alt;
text_vspeed.set(to_string_decimal(vspeed, 1) + " m/s");
}
} else { // save first valid packet time + altitude
last_timestamp_update_ = rtc_time::rtcToUnixUTC(packet.received_at());
last_altitude_ = geopos.altitude();
}
if (gps_info.is_valid()) { // only update when valid, to prevent flashing
geopos.set_altitude(gps_info.alt);
geopos.set_lat(gps_info.lat);
geopos.set_lon(gps_info.lon);
if (geomap_view_) {
geomap_view_->update_position(gps_info.lat, gps_info.lon, 400, gps_info.alt, 0);
}
}
if (logger && logging) {
logger->on_packet(packet);
}
+36 -24
View File
@@ -30,6 +30,7 @@
#include "ui_rssi.hpp"
#include "ui_qrcode.hpp"
#include "ui_geomap.hpp"
#include "string_format.hpp"
#include "event_m0.hpp"
@@ -94,32 +95,33 @@ class SondeView : public View {
// AudioOutput audio_output { };
Labels labels{
{{4 * 8, 2 * 16}, "Type:", Theme::getInstance()->fg_light->foreground},
{{6 * 8, 3 * 16}, "ID:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), 4 * 16}, "DateTime:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(4), UI_POS_Y(2)}, "Type:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(6), UI_POS_Y(3)}, "ID:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(4)}, "DateTime:", Theme::getInstance()->fg_light->foreground},
{{3 * 8, 5 * 16}, "Vbatt:", Theme::getInstance()->fg_light->foreground},
{{3 * 8, 6 * 16}, "Frame:", Theme::getInstance()->fg_light->foreground},
{{4 * 8, 7 * 16}, "Temp:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), 8 * 16}, "Humidity:", Theme::getInstance()->fg_light->foreground}};
{{UI_POS_X(3), UI_POS_Y(5)}, "Vbatt:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(3), UI_POS_Y(6)}, "Frame:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(4), UI_POS_Y(7)}, "Temp:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(8)}, "Humidity:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(9)}, "Pressure:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(2), UI_POS_Y(10)}, "VSpeed:", Theme::getInstance()->fg_light->foreground}};
RxFrequencyField field_frequency{
{UI_POS_X(0), 0 * 8},
{UI_POS_X(0), UI_POS_Y(0)},
nav_};
RFAmpField field_rf_amp{
{13 * 8, UI_POS_Y(0)}};
{UI_POS_X(13), UI_POS_Y(0)}};
LNAGainField field_lna{
{15 * 8, UI_POS_Y(0)}};
{UI_POS_X(15), UI_POS_Y(0)}};
VGAGainField field_vga{
{18 * 8, UI_POS_Y(0)}};
{UI_POS_X(18), UI_POS_Y(0)}};
RSSI rssi{
{21 * 8, 0, UI_POS_WIDTH_REMAINING(24), 4}};
{UI_POS_X(21), UI_POS_Y(0), UI_POS_WIDTH_REMAINING(24), 4}};
Channel channel{
{21 * 8, 5, UI_POS_WIDTH_REMAINING(24), 4},
{UI_POS_X(21), UI_POS_Y(0) + 5, UI_POS_WIDTH_REMAINING(24), 4},
};
AudioVolumeField field_volume{
@@ -136,47 +138,57 @@ class SondeView : public View {
"CRC"};
Text text_signature{
{9 * 8, 2 * 16, 10 * 8, 16},
{UI_POS_X(9), UI_POS_Y(2), UI_POS_WIDTH_REMAINING(10), UI_POS_HEIGHT(1)},
"..."};
Text text_serial{
{9 * 8, 3 * 16, 11 * 8, 16},
{UI_POS_X(9), UI_POS_Y(3), UI_POS_WIDTH_REMAINING(10), UI_POS_HEIGHT(1)},
"..."};
Text text_timestamp{
{9 * 8, 4 * 16, 11 * 8, 16},
{UI_POS_X(9), UI_POS_Y(4), UI_POS_WIDTH_REMAINING(9), UI_POS_HEIGHT(1)},
"..."};
Text text_voltage{
{9 * 8, 5 * 16, 10 * 8, 16},
{UI_POS_X(9), UI_POS_Y(5), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)},
"..."};
Text text_frame{
{9 * 8, 6 * 16, 10 * 8, 16},
{UI_POS_X(9), UI_POS_Y(6), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)},
"..."};
Text text_temp{
{9 * 8, 7 * 16, 10 * 8, 16},
{UI_POS_X(9), UI_POS_Y(7), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)},
"..."};
Text text_humid{
{9 * 8, 8 * 16, 10 * 8, 16},
{UI_POS_X(9), UI_POS_Y(8), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)},
"..."};
Text text_press{
{UI_POS_X(9), UI_POS_Y(9), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)},
"..."};
Text text_vspeed{
{UI_POS_X(9), UI_POS_Y(10), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)},
"..."};
GeoPos geopos{
{0, 12 * 16},
{UI_POS_X(0), UI_POS_Y(12)},
GeoPos::alt_unit::METERS,
GeoPos::spd_unit::HIDDEN};
Button button_see_qr{
{UI_POS_X_CENTER(12) - UI_POS_WIDTH(8), UI_POS_Y_BOTTOM(4), 12 * 8, 3 * 16},
{UI_POS_X_CENTER(12) - UI_POS_WIDTH(8), UI_POS_Y_BOTTOM(4), UI_POS_WIDTH(12), UI_POS_HEIGHT(3)},
"See QR"};
Button button_see_map{
{UI_POS_X_CENTER(12) + UI_POS_WIDTH(8), UI_POS_Y_BOTTOM(4), 12 * 8, 3 * 16},
{UI_POS_X_CENTER(12) + UI_POS_WIDTH(8), UI_POS_Y_BOTTOM(4), UI_POS_WIDTH(12), UI_POS_HEIGHT(3)},
"See on map"};
GeoMapView* geomap_view_{nullptr};
time_t last_timestamp_update_{0};
uint32_t last_altitude_{0};
MessageHandlerRegistration message_handler_packet{
Message::ID::SondePacket,
+20 -2
View File
@@ -147,6 +147,19 @@ void set_sstv_data(const uint8_t vis_code, const uint32_t pixel_duration) {
send_message(&message);
}
void set_sstvrx_data(const uint8_t code) {
const SSTVRXConfigureMessage message{
code};
send_message(&message);
}
void set_sstvrx_phase_slant(const int16_t phase, const int16_t slant) {
const SSTVRXPhaseSlantMessage message{
phase,
slant};
send_message(&message);
}
void set_afsk(const uint32_t baudrate, const uint32_t word_length, const uint32_t trigger_value, const bool trigger_word) {
const AFSKRxConfigureMessage message{
baudrate,
@@ -288,8 +301,8 @@ void set_fsk_data(const uint32_t stream_length, const uint32_t samples_per_bit,
send_message(&message);
}
void set_pocsag() {
const POCSAGConfigureMessage message{};
void set_pocsag(int8_t baud_config) {
const POCSAGConfigureMessage message{baud_config};
send_message(&message);
}
@@ -328,6 +341,11 @@ void set_noaaapt_config() {
send_message(&message);
}
void set_flex_config() {
const FlexConfigureMessage message{};
send_message(&message);
}
void set_siggen_tone(const uint32_t tone) {
const SigGenToneMessage message{
TONES_F2D(tone, TONES_SAMPLERATE)};
+5 -2
View File
@@ -74,13 +74,15 @@ void set_tone(const uint32_t index, const uint32_t delta, const uint32_t duratio
void set_tones_config(const uint32_t bw, const uint32_t pre_silence, const uint16_t tone_count, const bool dual_tone, const bool audio_out);
void kill_tone();
void set_sstv_data(const uint8_t vis_code, const uint32_t pixel_duration);
void set_sstvrx_data(const uint8_t code);
void set_sstvrx_phase_slant(const int16_t phase, const int16_t slant);
void set_audiotx_config(const uint32_t divider, const float deviation_hz, const float audio_gain, uint8_t audio_shift_bits_s16, uint8_t bits_per_sample, const uint32_t tone_key_delta, const bool am_enabled, const bool dsb_enabled, const bool usb_enabled, const bool lsb_enabled);
void set_fifo_data(const int8_t* data);
void set_pitch_rssi(int32_t avg, bool enabled);
void set_afsk_data(const uint32_t afsk_samples_per_bit, const uint32_t afsk_phase_inc_mark, const uint32_t afsk_phase_inc_space, const uint8_t afsk_repeat, const uint32_t afsk_bw, const uint8_t symbol_count);
void kill_afsk();
void set_afsk(const uint32_t baudrate, const uint32_t word_length, const uint32_t trigger_value, const bool trigger_word);
void set_fsk(const uint32_t samplesPerSymbol, const uint32_t syncWord, const uint8_t syncWordLength, const uint32_t preamble, const uint8_t preambleLength, uint16_t numDataBytes);
void set_fsk(const uint8_t samplesPerSymbol, const uint32_t syncWord, const uint8_t syncWordLength, const uint32_t preamble, const uint8_t preambleLength, uint16_t numDataBytes);
void set_aprs(const uint32_t baudrate);
void set_btlerx(uint8_t channel_number);
@@ -91,7 +93,7 @@ void set_nrf(const uint32_t baudrate, const uint32_t word_length, const uint32_t
void set_ook_data(const uint32_t stream_length, const uint32_t samples_per_bit, const uint8_t repeat, const uint32_t pause_symbols, const uint8_t de_bruijn_length = 0);
void kill_ook();
void set_fsk_data(const uint32_t stream_length, const uint32_t samples_per_bit, const uint32_t shift, const uint32_t progress_notice);
void set_pocsag();
void set_pocsag(int8_t baud_config = -1);
void set_adsb();
void set_jammer(const bool run, const jammer::JammerType type, const uint32_t speed);
void set_rds_data(const uint16_t message_length);
@@ -102,6 +104,7 @@ void set_spectrum_painter_config(const uint16_t width, const uint16_t height, bo
void set_subghzd_config(uint8_t modulation, uint32_t sampling_rate);
void set_wefax_config(uint8_t lpm, uint8_t ioc);
void set_noaaapt_config();
void set_flex_config();
void request_roger_beep();
void request_rssi_beep();
+6 -12
View File
@@ -244,8 +244,7 @@ static void portapack_tcxo_enable() {
/* Delay >10ms at 96MHz clock speed for reference oscillator to start. */
/* Delay an additional 1ms (arbitrary) for the clock generator to detect a signal. */
volatile uint32_t delay = 240000 + 24000;
while (delay--)
;
while (delay--);
}
static void portapack_tcxo_disable() {
@@ -329,8 +328,7 @@ void ClockManager::init_clock_generator() {
: (ref_pll == ClockControl::MultiSynthSource::PLLB)
? 0x40
: 0x20;
while ((clock_generator.device_status() & device_status_mask) != 0)
;
while ((clock_generator.device_status() & device_status_mask) != 0);
clock_generator.set_clock_control(
clock_generator_output_mcu_clkin,
@@ -377,8 +375,7 @@ ClockManager::Reference ClockManager::choose_reference() {
if (hackrf_r9) {
gpio_r9_clkin_en.write(1);
volatile uint32_t delay = 240000 + 24000;
while (delay--)
;
while (delay--);
}
const auto detected_reference = detect_reference_source();
@@ -513,8 +510,7 @@ void ClockManager::start_frequency_monitor_measurement(const cgu::CLK_SEL clk_se
void ClockManager::wait_For_frequency_monitor_measurement_done() {
// FREQ_MON mechanism fails to finish if there's no clock present on selected input?!
while (LPC_CGU->FREQ_MON.MEAS == 1)
;
while (LPC_CGU->FREQ_MON.MEAS == 1);
}
uint32_t ClockManager::get_frequency_monitor_measurement_in_hertz() {
@@ -559,8 +555,7 @@ void ClockManager::start_audio_pll() {
});
cgu::pll0audio::power_up();
while (!cgu::pll0audio::is_locked())
;
while (!cgu::pll0audio::is_locked());
cgu::pll0audio::clock_enable();
set_base_audio_clock_divider(1);
@@ -577,8 +572,7 @@ void ClockManager::set_base_audio_clock_divider(const size_t divisor) {
void ClockManager::stop_audio_pll() {
cgu::pll0audio::clock_disable();
cgu::pll0audio::power_down();
while (cgu::pll0audio::is_locked())
;
while (cgu::pll0audio::is_locked());
}
void ClockManager::enable_clock_output(bool enable) {
+3 -6
View File
@@ -139,13 +139,11 @@ void runtime_error(uint8_t source) {
led.off();
// wait for DFU button release if pressed, so we don't immediately jump into stack dump
while (swizzled_switches() & (1 << (int)Switch::Dfu))
;
while (swizzled_switches() & (1 << (int)Switch::Dfu));
while (true) {
volatile size_t n = 1000000U;
while (n--)
;
while (n--);
led.toggle();
// Stack dump will cover entire screen, so wait for DFU button press to attempt it
@@ -225,8 +223,7 @@ void draw_stack_dump() {
// Out of room on the screen or end of stack - allow Up/Down paging.
// First wait for button release from previous press.
// NOTE: can't call swizzle_switches() with interrupted enabled!
while (swizzled_switches() & ((1 << (int)Switch::Right) | (1 << (int)Switch::Left) | (1 << (int)Switch::Down) | (1 << (int)Switch::Up) | (1 << (int)Switch::Sel) | (1 << (int)Switch::Dfu)))
;
while (swizzled_switches() & ((1 << (int)Switch::Right) | (1 << (int)Switch::Left) | (1 << (int)Switch::Down) | (1 << (int)Switch::Up) | (1 << (int)Switch::Sel) | (1 << (int)Switch::Dfu)));
painter.draw_string({border, portapack::display.height() - border - 8}, *Theme::getInstance()->bg_darkest_small, "Use UP/DOWN key");
+1 -2
View File
@@ -41,8 +41,7 @@ extern uint32_t __process_stack_end__;
inline uint32_t get_free_stack_space() {
uint32_t* p;
for (p = &__process_stack_base__; *p == CRT0_STACKS_FILL_PATTERN && p < &__process_stack_end__; p++)
;
for (p = &__process_stack_base__; *p == CRT0_STACKS_FILL_PATTERN && p < &__process_stack_end__; p++);
auto stack_space_left = p - &__process_stack_base__;
return stack_space_left;
@@ -104,7 +104,7 @@ class AdultToysView : public ui::View {
/*short_ui*/ true};
app_settings::SettingsManager settings_{
"Adult Toys", app_settings::Mode::TX};
"tx_adult_toys", app_settings::Mode::TX};
OptionsField options_target{
{UI_POS_X(6), UI_POS_Y(1)},
+1 -1
View File
@@ -118,7 +118,7 @@ class ERTAppView : public View {
// Prevent painting of region covered entirely by a child.
// TODO: Add flag to View that specifies view does not need to be cleared before painting.
void paint(Painter&) override{};
void paint(Painter&) override {};
void focus() override;
+15
View File
@@ -99,6 +99,10 @@ set(EXTCPPSRC
external/sstvtx/main.cpp
external/sstvtx/ui_sstvtx.cpp
#sstvrx
external/sstvrx/main.cpp
external/sstvrx/ui_sstvrx.cpp
#random 464 bytes.
external/random_password/main.cpp
external/random_password/ui_random_password.cpp
@@ -264,6 +268,14 @@ set(EXTCPPSRC
#adult_toys_controller 144 bytes
external/adult_toys_controller/main.cpp
external/adult_toys_controller/ui_adult_toys_controller.cpp
#flex_rx
external/flex_rx/main.cpp
external/flex_rx/ui_flex_rx.cpp
#subcarrx
external/subcarrx/main.cpp
external/subcarrx/ui_subcar.cpp
)
set(EXTAPPLIST
@@ -290,6 +302,7 @@ set(EXTAPPLIST
adsbtx
morse_tx
sstvtx
sstvrx
random_password
# acars_rx --not working
wefax_rx
@@ -330,4 +343,6 @@ set(EXTAPPLIST
bht_tx
morse_practice
adult_toys_controller
flex_rx
subcarrx
)
+22
View File
@@ -86,6 +86,9 @@ MEMORY
ram_external_app_bht_tx (rwx) : org = 0xADED0000, len = 32k
ram_external_app_morse_practice (rwx) : org = 0xADEE0000, len = 32k
ram_external_app_adult_toys_controller (rwx) : org = 0xADEF0000, len = 32k
ram_external_app_flex_rx (rwx) : org = 0xADF00000, len = 32k
ram_external_app_sstvrx (rwx) : org = 0xADF10000, len = 32k
ram_external_app_subcarrx (rwx) : org = 0xADF20000, len = 32k
}
@@ -471,5 +474,24 @@ SECTIONS
*(*ui*external_app*adult_toys_controller*);
} > ram_external_app_adult_toys_controller
.external_app_flex_rx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_flex_rx.application_information));
*(*ui*external_app*flex_rx*);
} > ram_external_app_flex_rx
.external_app_sstvrx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_sstvrx.application_information));
*(*ui*external_app*sstvrx*);
} > ram_external_app_sstvrx
.external_app_subcarrx : ALIGN(4) SUBALIGN(4)
{
KEEP(*(.external_app.app_subcarrx.application_information));
*(*ui*external_app*subcarrx*);
} > ram_external_app_subcarrx
}
+83
View File
@@ -0,0 +1,83 @@
/*
* Copyright (C) 2025 HTotoo
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#include "ui.hpp"
#include "ui_flex_rx.hpp"
#include "ui_navigation.hpp"
#include "external_app.hpp"
namespace ui::external_app::flex_rx {
void initialize_app(ui::NavigationView& nav) {
nav.push<FlexAppView>();
}
} // namespace ui::external_app::flex_rx
extern "C" {
__attribute__((section(".external_app.app_flex_rx.application_information"), used)) application_information_t _application_information_flex_rx = {
/*.memory_location = */ (uint8_t*)0x00000000,
/*.externalAppEntry = */ ui::external_app::flex_rx::initialize_app,
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
/*.app_name = */ "FLEX RX",
/*.bitmap_data = */ {
0x00,
0x00,
0xFE,
0x7F,
0x02,
0x40,
0xFA,
0x5F,
0x02,
0x40,
0xF2,
0x4F,
0x02,
0x40,
0xE2,
0x47,
0x02,
0x40,
0xC2,
0x43,
0x02,
0x40,
0x82,
0x41,
0x02,
0x40,
0xFE,
0x7F,
0x00,
0x00,
0x00,
0x00,
},
/*.icon_color = */ ui::Color::orange().v,
/*.menu_location = */ app_location_t::RX,
/*.desired_menu_position = */ -1,
/*.m4_app_tag = portapack::spi_flash::image_tag_flex */ {'P', 'F', 'L', 'X'},
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
};
}
+129
View File
@@ -0,0 +1,129 @@
#include "ui_flex_rx.hpp"
#include "baseband_api.hpp"
#include "portapack_persistent_memory.hpp"
#include "string_format.hpp"
#include "memory_map.hpp"
using namespace portapack;
namespace ui::external_app::flex_rx {
FlexAppView::FlexAppView(NavigationView& nav)
: nav_{nav} {
// Load baseband image for FLEX decoding
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
add_children({&field_frequency,
&field_rf_amp,
&field_lna,
&field_vga,
&rssi,
&console});
// Restore saved frequency
field_frequency.set_value(frequency_value);
receiver_model.set_target_frequency(frequency_value);
// Frequency change callback
field_frequency.updated = [this](rf::Frequency f) {
update_freq(f);
};
// Configure receiver
receiver_model.set_sampling_rate(3072000);
receiver_model.set_baseband_bandwidth(1750000);
receiver_model.enable();
receiver_model.set_squelch_level(0);
// Initialize FLEX baseband
baseband::set_flex_config();
console.writeln("Ready");
}
FlexAppView::~FlexAppView() {
receiver_model.disable();
baseband::shutdown();
}
void FlexAppView::focus() {
field_frequency.focus();
}
// Redraw all messages to console
void FlexAppView::redraw_console() {
console.clear(true);
bool first = true;
for (const auto& msg : messages) {
if (!first) {
console.writeln(""); // Blank line between messages
}
first = false;
console.writeln(msg);
}
}
// Add message to log with automatic line wrapping
void FlexAppView::log_message(const std::string& message) {
const size_t chars_per_line = screen_width / 8;
// Console height accounts for status bar and controls row
const size_t console_lines = (screen_height - 2 * 16) / 16;
messages.push_back(message);
// Calculate total lines used (messages + blank lines between them)
size_t total_lines = 0;
for (size_t i = 0; i < messages.size(); i++) {
if (i > 0) total_lines++; // Count blank line separator
size_t msg_lines = (messages[i].length() + chars_per_line - 1) / chars_per_line;
if (msg_lines == 0) msg_lines = 1;
total_lines += msg_lines;
}
// If console would overflow, remove oldest messages and redraw
if (total_lines > console_lines) {
while (total_lines > console_lines && !messages.empty()) {
const auto& oldest = messages.front();
size_t oldest_lines = (oldest.length() + chars_per_line - 1) / chars_per_line;
if (oldest_lines == 0) oldest_lines = 1;
total_lines -= oldest_lines;
if (messages.size() > 1) total_lines--; // Remove separator line too
messages.erase(messages.begin());
}
redraw_console();
} else {
// Just append new message
if (messages.size() > 1) {
console.writeln(""); // Blank line before new message
}
console.writeln(message);
}
}
// Update frequency and save for persistence
void FlexAppView::update_freq(rf::Frequency f) {
frequency_value = f;
receiver_model.set_target_frequency(f);
}
// Handle decoded FLEX packet from baseband
void FlexAppView::on_packet(const FlexPacketMessage* message) {
log_message(message->packet.message);
}
// Handle stats message (currently unused)
void FlexAppView::on_stats(const FlexStatsMessage*) {
}
// Debug handler - uncomment to see baseband debug messages
void FlexAppView::on_debug(const FlexDebugMessage* message) {
(void)message; // Suppress unused parameter warning
// std::string text = "DBG: ";
// text += message->text;
// text += " " + to_string_hex(message->val1, 8);
// text += " " + to_string_hex(message->val2, 8);
// log_message(text);
}
} // namespace ui::external_app::flex_rx
+97
View File
@@ -0,0 +1,97 @@
#ifndef __UI_FLEX_RX_H__
#define __UI_FLEX_RX_H__
#include "ui_widget.hpp"
#include "ui_navigation.hpp"
#include "ui_receiver.hpp"
#include "ui_freq_field.hpp"
#include "ui_rssi.hpp"
#include "app_settings.hpp"
#include "radio_state.hpp"
#include <string>
#include <vector>
namespace ui::external_app::flex_rx {
class FlexAppView : public View {
public:
FlexAppView(NavigationView& nav);
~FlexAppView();
void focus() override;
std::string title() const override { return "FLEX RX"; };
private:
NavigationView& nav_;
// Saved settings
rf::Frequency frequency_value{931740000}; // Default FLEX frequency
RxRadioState radio_state_{};
// Message storage for console redraw
static constexpr size_t MAX_MESSAGES = 20;
std::vector<std::string> messages{};
// Helper methods
void log_message(const std::string& message);
void redraw_console();
void update_freq(rf::Frequency f);
// UI Elements - Row 0, dynamically positioned
RxFrequencyField field_frequency{
{UI_POS_X(0), UI_POS_Y(0)},
nav_};
RFAmpField field_rf_amp{
{UI_POS_X(13), UI_POS_Y(0)}};
LNAGainField field_lna{
{UI_POS_X(15), UI_POS_Y(0)}};
VGAGainField field_vga{
{UI_POS_X(18), UI_POS_Y(0)}};
RSSI rssi{
{UI_POS_X(21), 0, UI_POS_WIDTH(9), 4}};
// Message display area (below controls, account for status bar)
Console console{
{0, 1 * 16, screen_width, screen_height - 2 * 16}};
// Persistent settings manager
app_settings::SettingsManager settings_{
"rx_flex",
app_settings::Mode::RX,
{{"frequency", &frequency_value}}};
// Message handlers
void on_packet(const FlexPacketMessage* message);
void on_stats(const FlexStatsMessage* message);
void on_debug(const FlexDebugMessage* message);
// Message handler registrations
MessageHandlerRegistration message_handler_packet{
Message::ID::FlexPacket,
[this](const Message* const p) {
const auto message = *static_cast<const FlexPacketMessage*>(p);
this->on_packet(&message);
}};
MessageHandlerRegistration message_handler_stats{
Message::ID::FlexStats,
[this](const Message* const p) {
const auto message = *static_cast<const FlexStatsMessage*>(p);
this->on_stats(&message);
}};
MessageHandlerRegistration message_handler_debug{
Message::ID::FlexDebug,
[this](const Message* const p) {
const auto message = *static_cast<const FlexDebugMessage*>(p);
this->on_debug(&message);
}};
};
} // namespace ui::external_app::flex_rx
#endif /*__UI_FLEX_RX_H__*/
+88
View File
@@ -0,0 +1,88 @@
/*
* Copyright (C) 2025 StarVore Labs
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#include "ui.hpp"
#include "ui_sstvrx.hpp"
#include "ui_navigation.hpp"
#include "external_app.hpp"
namespace ui::external_app::sstvrx {
void initialize_app(NavigationView& nav) {
nav.push<SstvRxView>();
}
} // namespace ui::external_app::sstvrx
extern "C" {
// Az alkalmazás információ C-linkage-ként, hogy a firmware hívhassa
__attribute__((section(".external_app.app_sstvrx.application_information"), used))
application_information_t _application_information_sstvrx = {
/*.memory_location = */ (uint8_t*)0x00000000,
/*.externalAppEntry = */ ui::external_app::sstvrx::initialize_app,
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
/*.app_name = */ "SSTV RX",
/*.bitmap_data = */ {
0x00,
0x00,
0x00,
0x00,
0xFE,
0x7F,
0x03,
0xC0,
0x53,
0xD5,
0xAB,
0xCA,
0x53,
0xD5,
0xAB,
0xCA,
0x53,
0xD5,
0xAB,
0xCA,
0x53,
0xD5,
0x03,
0xC0,
0xFF,
0xFF,
0xFB,
0xD7,
0xFE,
0x7F,
0x00,
0x00,
},
/*.icon_color = */ ui::Color::yellow().v,
/*.menu_location = */ app_location_t::RX,
/*.desired_menu_position = */ -1,
/*.m4_app_tag = portapack::spi_flash::image_tag_none */ {'P', 'S', 'R', 'X'},
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
};
} // extern "C"
+510
View File
@@ -0,0 +1,510 @@
/*
* Copyright (C) 2025 StarVore Labs
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#include "ui_sstvrx.hpp"
#include "portapack_persistent_memory.hpp"
#include "portapack.hpp"
#include "hackrf_hal.hpp"
#include "file_path.hpp"
#include "message.hpp"
#include <algorithm>
#include <array>
#include <cstdint>
#include <cstring>
#include <stdio.h>
using namespace portapack;
using namespace modems;
using namespace ui;
#if SSTVRX_ENABLE_LOGGER
#define SSTVRX_LOG_INFO(msg) \
do { \
if (logger) { \
logger->log_info(msg); \
} \
} while (0)
#define SSTVRX_LOG_ERROR(msg) \
do { \
if (logger) { \
logger->log_error(msg); \
} \
} while (0)
#else
#define SSTVRX_LOG_INFO(msg) \
do { \
(void)sizeof(msg); \
} while (0)
#define SSTVRX_LOG_ERROR(msg) \
do { \
(void)sizeof(msg); \
} while (0)
#endif
// SSTV RX View Implementation
namespace ui::external_app::sstvrx {
static_assert(sizeof(shared_memory.bb_data.data) == 512,
"SSTV shared buffer size mismatch");
#if SSTVRX_ENABLE_LOGGER
void SstvRxLogger::log_error(const std::string& error_message) {
log_file.write_entry(rtc_time::now(), "ERROR: " + error_message);
}
void SstvRxLogger::log_info(const std::string& info_message) {
log_file.write_entry(rtc_time::now(), "INFO: " + info_message);
}
#endif
SstvRxView::SstvRxView(ui::NavigationView& nav)
: nav_(nav) {
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
DISPLAY_HEIGHT = screen_height - SSTV_IMG_START_ROW * 16 - 16;
DISPLAY_WIDTH = screen_width;
add_children({&field_rf_amp,
&field_lna,
&field_vga,
&rssi,
&channel,
&field_frequency,
&field_volume,
&audio,
&start_stop_btn,
&options_mode,
&field_phase,
&field_slant,
&labels,
&text_calibration});
// Initialize audio with proper rate for SSTV
audio::set_rate(audio::Rate::Hz_48000);
audio::output::start();
// Configure receiver with optimal settings for SSTV
// NOTE: Do NOT set modulation mode - SSTV uses a custom baseband processor
// Standard sampling rate (3.072MHz) with wide bandwidth to capture full SSTV audio spectrum
// SSTV uses 1200-2300 Hz tones, so we need wide baseband to avoid distortion
receiver_model.set_sampling_rate(3072000);
receiver_model.set_baseband_bandwidth(1750000); // Standard wideband setting
receiver_model.set_squelch_level(1);
receiver_model.set_hidden_offset(0); // No offset needed
// Field values will be set in on_show() to ensure proper initialization
using option_t = std::pair<std::string, int32_t>;
using options_t = std::vector<option_t>;
options_t mode_options;
uint32_t c;
// Start/Stop button handler - toggles between start and stop
start_stop_btn.on_select = [this](Button&) {
start_stop_btn.focus();
on_start_stop();
};
// Initialize frequency field from settings or use default
if (settings_.loaded() && settings_.raw().rx_frequency != 0) {
field_frequency.set_value(settings_.raw().rx_frequency);
} else if (field_frequency.value() == 0) {
field_frequency.set_value(145800000); // Default to 145.800 MHz (ISS)
}
field_frequency.set_step(25000);
// Populate mode list
for (c = 0; c < SSTV_MODES_NB; c++)
mode_options.emplace_back(sstv_modes[c].name, c);
options_mode.set_options(mode_options);
options_mode.on_change = [this](size_t i, int32_t) {
this->on_mode_changed(i);
};
options_mode.set_selected_index(1); // Scottie 2
on_mode_changed(1);
// Initialize phase and slant controls from loaded settings
field_phase.set_value(phase_adjustment);
field_phase.on_change = [this](int32_t v) {
phase_adjustment = v;
if (is_receiving) {
baseband::set_sstvrx_phase_slant(phase_adjustment, slant_adjustment);
} else if (max_received_line > 0) {
// Auto-redraw when adjusting after reception
redraw_image();
}
};
field_slant.set_value(slant_adjustment);
field_slant.on_change = [this](int32_t v) {
slant_adjustment = v;
if (is_receiving) {
baseband::set_sstvrx_phase_slant(phase_adjustment, slant_adjustment);
} else if (max_received_line > 0) {
// Auto-redraw when adjusting after reception
redraw_image();
}
};
#if SSTVRX_ENABLE_LOGGER
logger = std::make_unique<SstvRxLogger>();
if (logger) {
logger->append(logs_dir / "SSTVRX.txt");
logger->log_info("----------SSTV RX Started----------");
}
#endif
}
// Destructor: Ensure reception is stopped
SstvRxView::~SstvRxView() {
is_receiving = true;
on_stop();
baseband::shutdown();
SSTVRX_LOG_INFO("SSTV RX Stopped");
}
void SstvRxView::on_show() {
// Update field values from receiver model to reflect loaded settings
field_lna.set_value(receiver_model.lna());
field_vga.set_value(receiver_model.vga());
field_rf_amp.set_value(receiver_model.rf_amp());
field_volume.set_value(receiver_model.normalized_headphone_volume());
}
void SstvRxView::focus() {
field_frequency.focus();
}
// Combined start/stop handler - toggles based on current state
void SstvRxView::on_start_stop() {
if (is_receiving) {
// Currently receiving - stop it
on_stop();
start_stop_btn.set_text("Start RX");
} else {
// Currently stopped - start reception
SSTVRX_LOG_INFO("Starting SSTV RX Reception");
start_audio();
start_stop_btn.set_text("Stop RX");
}
}
// Stop NFM audio reception
void SstvRxView::on_stop() {
SSTVRX_LOG_INFO("Stopping SSTV RX Reception");
if (is_receiving) {
// Stop in reverse order of start
receiver_model.disable();
audio::output::stop();
// Reset state
is_receiving = false;
// Close image file if still open
bmp.close();
pending_line_valid = false;
pending_chunk_mask = 0;
std::fill(pending_line_rgb.begin(), pending_line_rgb.end(), 0);
*reinterpret_cast<volatile uint8_t*>(&shared_memory.bb_data.data[CHUNK_FLAG_INDEX]) = 0;
SSTVRX_LOG_INFO("SSTV RX Reception Stopped");
} else {
SSTVRX_LOG_ERROR("SSTV RX Reception Not Running");
}
}
// Start NFM audio reception
void SstvRxView::start_audio() {
SSTVRX_LOG_INFO("Configuring SSTV RX Audio Reception");
// Configure the baseband processor with VIS code
if (rx_sstv_mode) {
baseband::set_sstvrx_data(rx_sstv_mode->vis_code);
SSTVRX_LOG_INFO("Sent VIS code to processor: " + to_string_dec_uint(rx_sstv_mode->vis_code));
}
// Send phase and slant adjustments
baseband::set_sstvrx_phase_slant(phase_adjustment, slant_adjustment);
// Initialize audio path
audio::output::stop();
audio::set_rate(audio::Rate::Hz_48000);
// Set audio routing and volume
// audio::output::start();
// Clear display area and reset line counter
portapack::display.fill_rectangle(
{0, SSTV_IMG_START_ROW * 16, DISPLAY_WIDTH, DISPLAY_HEIGHT},
{0, 0, 0});
line_num = 0;
file_line_num = 0;
max_received_line = 0;
pending_line_valid = false;
pending_chunk_mask = 0;
std::fill(pending_line_rgb.begin(), pending_line_rgb.end(), 0);
*reinterpret_cast<volatile uint8_t*>(&shared_memory.bb_data.data[CHUNK_FLAG_INDEX]) = 0;
// Clear calibration display
text_calibration.set("Calibrating...");
// Initialize new image file
current_line_rx = 0;
auto timestamp = to_string_timestamp(rtc_time::now());
auto dir_error = ensure_directory(sstv_dir / "RX");
if (!dir_error.ok()) {
SSTVRX_LOG_ERROR("Failed to create directory: SSTV/RX");
}
current_image_path = sstv_dir / ("RX/SSTV_" + timestamp + ".bmp");
auto ok = bmp.create(current_image_path, IMAGE_WIDTH, 1);
if (!ok) {
SSTVRX_LOG_ERROR("Failed to create file: " + current_image_path.string());
bmp.close();
}
// Start audio output
audio::output::start();
audio::headphone::set_volume(persistent_memory::headphone_volume());
// Keep ReceiverModel in capture mode so it doesn't override our custom baseband/audio configuration.
receiver_model.set_modulation(ReceiverModel::Mode::Capture);
// Enable receiver last
receiver_model.enable();
is_receiving = true;
SSTVRX_LOG_INFO("SSTV RX Started");
}
void SstvRxView::on_mode_changed(const size_t index) {
rx_sstv_mode = &sstv_modes[index];
}
void SstvRxView::write_line_to_file(uint16_t line_num, const uint8_t* rgb_line) {
(void)line_num;
if (!bmp.is_loaded()) return;
// Ensure BMP height is sufficient
if (bmp.get_real_height() <= file_line_num) {
bmp.expand_y(file_line_num + 1);
}
bmp.seek(0, file_line_num);
// Write RGB data in BGR order
for (uint16_t x = 0; x < IMAGE_WIDTH; x++) {
uint8_t r = rgb_line[x * 3 + 0];
uint8_t g = rgb_line[x * 3 + 1];
uint8_t b = rgb_line[x * 3 + 2];
Color px(g, b, r);
bmp.write_next_px(px);
}
file_line_num++;
}
void SstvRxView::update_display(uint16_t current_line, const uint8_t* rgb_line) {
if (current_line >= IMAGE_HEIGHT) return;
// Reset line counter if we reach the bottom of display
if (line_num >= DISPLAY_HEIGHT) {
line_num = 0;
}
// Scale line to display width
for (uint16_t x = 0; x < DISPLAY_WIDTH; x++) {
// Scale x coordinate
uint16_t src_x = (x * IMAGE_WIDTH) / DISPLAY_WIDTH;
if (src_x >= IMAGE_WIDTH) continue;
// Get RGB values from interleaved data [R,G,B,R,G,B,...]
uint8_t r = rgb_line[src_x * 3 + 0];
uint8_t g = rgb_line[src_x * 3 + 1];
uint8_t b = rgb_line[src_x * 3 + 2];
// Display uses BGR order like BMP format
// line_buffer[x] = Color(b, r, g);
line_buffer[x] = Color(g, b, r);
}
// Render the line at the current position
portapack::display.render_line(
{0, line_num + SSTV_IMG_START_ROW * 16},
DISPLAY_WIDTH,
line_buffer);
// Increment line counter
line_num++;
}
void SstvRxView::redraw_image() {
// Disabled: Post-reception redraw requires 245KB buffer which exceeds M0 memory
// Phase and slant adjustments must be set before reception starts
}
void SstvRxView::on_progress(uint16_t line, uint16_t total_lines) {
if (!is_receiving) {
if (line < 0xFFF0) {
*reinterpret_cast<volatile uint8_t*>(&shared_memory.bb_data.data[CHUNK_FLAG_INDEX]) = 0;
}
return;
}
// Handle debug messages
if (line == 0xFFFF) {
SSTVRX_LOG_ERROR("Processor not configured");
return;
}
if (line == 0xFFFE) {
SSTVRX_LOG_INFO("Sync pulse duration: " + to_string_dec_uint(total_lines) + " samples");
return;
}
if (line == 0xFFFD) {
SSTVRX_LOG_INFO("Sync detected, count=" + to_string_dec_uint(total_lines));
text_calibration.set("Syncs: " + to_string_dec_uint(total_lines));
return;
}
if (line == 0xFFFC) {
SSTVRX_LOG_INFO("Expected interval: " + to_string_dec_uint(total_lines) + " samples");
return;
}
if (line == 0xFFFB) {
SSTVRX_LOG_INFO("Actual interval: " + to_string_dec_uint(total_lines) + " samples");
return;
}
if (line == 0xFFFA) {
SSTVRX_LOG_INFO("SYNC TIMEOUT after " + to_string_dec_uint(total_lines) + " samples - continuing without sync");
return;
}
if (line == 0xFFF9) {
SSTVRX_LOG_INFO("Detected frequency at sync: " + to_string_dec_uint(total_lines) + " Hz");
return;
}
if (line == 0xFFF8) {
SSTVRX_LOG_INFO("OUTLIER REJECTED: interval=" + to_string_dec_uint(total_lines) + " samples (expected 10000-15000)");
return;
}
if (line == 0xFFF7) {
SSTVRX_LOG_INFO("PRE-RECORD: sync_history_count=" + to_string_dec_uint(total_lines));
return;
}
if (line == 0xFFF6) {
SSTVRX_LOG_INFO("MAX_SYNC_HISTORY EXCEEDED: count=" + to_string_dec_uint(total_lines));
return;
}
if (line == 0xFFF5) {
// Decode rejection reason bit flags: 0x1=interval, 0x2=freq, 0x4=duration
std::string reasons = "";
if (total_lines & 0x1) reasons += "interval ";
if (total_lines & 0x2) reasons += "freq ";
if (total_lines & 0x4) reasons += "duration ";
if (reasons.empty()) reasons = "unknown";
SSTVRX_LOG_INFO("FALSE SYNC PAIR REJECTED on Line 0: " + reasons);
return;
}
if (line == 0xFFF4) {
SSTVRX_LOG_INFO("STARTING LINE 0 DECODE after sync_sample_count=" + to_string_dec_uint(total_lines));
return;
}
std::array<uint8_t, CHUNK_COPY_BYTES> chunk{};
memcpy(chunk.data(), shared_memory.bb_data.data, chunk.size());
*reinterpret_cast<volatile uint8_t*>(&shared_memory.bb_data.data[CHUNK_FLAG_INDEX]) = 0;
const uint16_t line_num_encoded = chunk[0] | (chunk[1] << 8);
const bool is_second_chunk = (line_num_encoded & 1) == 1;
const uint16_t actual_line_num = line_num_encoded / 2;
if (actual_line_num >= IMAGE_HEIGHT) {
return;
}
const bool multi_chunk_line = PIXELS_PER_LINE > MAX_CHUNK_PIXELS;
if (!pending_line_valid || pending_line_number != actual_line_num) {
pending_line_number = actual_line_num;
pending_line_valid = true;
pending_chunk_mask = 0;
std::fill(pending_line_rgb.begin(), pending_line_rgb.end(), 0);
}
const uint16_t chunk_pixels = multi_chunk_line
? (is_second_chunk ? (PIXELS_PER_LINE - MAX_CHUNK_PIXELS) : MAX_CHUNK_PIXELS)
: PIXELS_PER_LINE;
const uint16_t dest_pixel_offset = (multi_chunk_line && is_second_chunk) ? MAX_CHUNK_PIXELS : 0;
const uint16_t chunk_bytes = chunk_pixels * 3;
const uint16_t max_copy_bytes = static_cast<uint16_t>(chunk.size() > CHUNK_HEADER_BYTES ? (chunk.size() - CHUNK_HEADER_BYTES) : 0);
const uint16_t bytes_to_copy = (chunk_bytes < max_copy_bytes) ? chunk_bytes : max_copy_bytes;
const uint16_t dest_byte_offset = dest_pixel_offset * 3;
if (bytes_to_copy > 0 && (dest_byte_offset + bytes_to_copy) <= pending_line_rgb.size()) {
memcpy(pending_line_rgb.data() + dest_byte_offset, chunk.data() + CHUNK_HEADER_BYTES, bytes_to_copy);
}
const uint8_t chunk_bit = (multi_chunk_line && is_second_chunk) ? 0x2 : 0x1;
pending_chunk_mask |= chunk_bit;
const uint8_t required_mask = multi_chunk_line ? 0x3 : 0x1;
if (pending_chunk_mask != required_mask) {
return;
}
pending_line_valid = false;
pending_chunk_mask = 0;
current_line_rx = actual_line_num;
if (actual_line_num < IMAGE_HEIGHT) {
write_line_to_file(actual_line_num, pending_line_rgb.data());
update_display(actual_line_num, pending_line_rgb.data());
max_received_line = max_received_line > (actual_line_num + 1) ? max_received_line : (actual_line_num + 1);
}
#if SSTVRX_ENABLE_LOGGER
if (logger && (actual_line_num % 10 == 0)) {
logger->log_info("Line " + to_string_dec_uint(actual_line_num) + "/" + to_string_dec_uint(total_lines));
}
#endif
// if (actual_line_num >= (total_lines - 1)) { //don't auto finish image upon end, user need to manually stop. this method is not reliable enough.
// finish_image();
// }
}
void SstvRxView::finish_image() {
bmp.close();
SSTVRX_LOG_INFO("Image completed: " + current_image_path.string());
}
void SstvRxView::on_calibration(int16_t suggested_phase, int16_t suggested_slant, uint16_t sync_count) {
if (!is_receiving) return;
// Display calibration suggestions to the user
if (sync_count >= 4) {
std::string cal_text = "Try Slant=" + to_string_dec_int(suggested_slant);
text_calibration.set(cal_text);
text_calibration.set_dirty();
// Don't auto-apply yet - just suggest for now until we verify the values are correct
// User can manually adjust if needed
// Log the suggestion
SSTVRX_LOG_INFO("Calibration suggestion: phase=" + to_string_dec_int(suggested_phase) +
" slant=" + to_string_dec_int(suggested_slant) +
" (from " + to_string_dec_uint(sync_count) + " syncs)");
} else {
// Log that we received calibration but not enough syncs yet
SSTVRX_LOG_INFO("Calibration received: " + to_string_dec_uint(sync_count) + " syncs (need 4+)");
}
}
} // namespace ui::external_app::sstvrx
+196
View File
@@ -0,0 +1,196 @@
/*
* Copyright (C) 2025 StarVore Labs
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#ifndef __SSTVRX_H__
#define __SSTVRX_H__
#include "ui.hpp"
#include "ui_widget.hpp"
#include "ui_receiver.hpp"
#include "ui_navigation.hpp"
#include "ui_receiver.hpp"
#include "ui_freq_field.hpp"
#include "ui_freqman.hpp"
#include "ui_channel.hpp"
#include "baseband_api.hpp"
#include "event_m0.hpp"
#include "message.hpp"
#include "sstv.hpp"
#include "file.hpp"
#include "bmpfile.hpp"
#include "app_settings.hpp"
#include "radio_state.hpp"
#include "oversample.hpp"
#include "string_format.hpp"
#include "log_file.hpp"
#include "utility.hpp"
#include "audio.hpp"
#include "portapack.hpp"
#include <array>
#include <ch.h>
#ifndef SSTVRX_ENABLE_LOGGER
#define SSTVRX_ENABLE_LOGGER 0
#endif
using namespace sstv;
namespace ui::external_app::sstvrx {
#define FMR_BTNGRID_TOP 60
#if SSTVRX_ENABLE_LOGGER
class SstvRxLogger {
public:
Optional<File::Error> append(const std::filesystem::path& filename) {
return log_file.append(filename);
}
void log_error(const std::string& error_message);
void log_info(const std::string& info_message);
private:
LogFile log_file{};
};
#endif
class SstvRxView : public ui::View {
public:
SstvRxView(ui::NavigationView& nav);
SstvRxView& operator=(const SstvRxView&) = delete;
SstvRxView(const SstvRxView&) = delete;
~SstvRxView();
std::string title() { return "SSTV RX"; }
void focus() override;
void on_show() override;
private:
ui::NavigationView& nav_;
#if SSTVRX_ENABLE_LOGGER
std::unique_ptr<SstvRxLogger> logger{};
#endif
// Phase and slant adjustments (runtime only, not persisted)
int16_t phase_adjustment{0}; // Horizontal offset in pixels (-50 to +50)
int16_t slant_adjustment{0}; // Timing adjustment in 0.1% units (-100 to +100)
// Settings must be declared before UI controls
app_settings::SettingsManager settings_{
"rx_sstv",
app_settings::Mode::RX};
ReceiverModel::Mode receiver_mode = ReceiverModel::Mode::WidebandFMAudio;
AudioSpectrum* audio_spectrum_data{nullptr};
int16_t audio_spectrum[128]{0};
RxRadioState radio_state_{};
audio::Rate audio_sampling_rate = audio::Rate::Hz_48000;
uint8_t radio_bw = 0;
bool is_receiving = false;
const sstv_mode* rx_sstv_mode{};
// Image data storage - only store current line to save memory
static constexpr uint16_t IMAGE_WIDTH = 320;
static constexpr uint16_t IMAGE_HEIGHT = 256;
static constexpr uint16_t PIXELS_PER_LINE = 320;
uint16_t DISPLAY_WIDTH = 240; // Scaled display width
uint16_t DISPLAY_HEIGHT = 192; // Scaled display height
static constexpr uint16_t SSTV_IMG_START_ROW = 7; // Start drawing at row 7 (after controls)
static constexpr size_t SHARED_BUFFER_BYTES = 512;
static constexpr size_t CHUNK_FLAG_INDEX = SHARED_BUFFER_BYTES - 1;
static constexpr size_t CHUNK_HEADER_BYTES = 2;
static constexpr size_t CHUNK_COPY_BYTES = CHUNK_FLAG_INDEX; // Exclude flag byte
static constexpr uint16_t MAX_CHUNK_PIXELS = (CHUNK_COPY_BYTES - CHUNK_HEADER_BYTES) / 3;
uint16_t current_line_rx{0};
BMPFile bmp{};
std::filesystem::path current_image_path{};
ui::Color line_buffer[320];
uint16_t line_num{0}, file_line_num{0};
std::array<uint8_t, IMAGE_WIDTH * 3> pending_line_rgb{};
uint16_t pending_line_number{0};
uint8_t pending_chunk_mask{0};
bool pending_line_valid{false};
// Note: Post-reception phase/slant adjustment disabled due to M0 memory constraints
// The 245KB image buffer exceeds available heap memory
uint16_t max_received_line{0};
MessageHandlerRegistration message_handler_progress{
Message::ID::SSTVRXProgress,
[this](const Message* const p) {
const auto message = *reinterpret_cast<const SSTVRXProgressMessage*>(p);
this->on_progress(message.line, message.total_lines);
}};
MessageHandlerRegistration message_handler_calibration{
Message::ID::SSTVRXCalibration,
[this](const Message* const p) {
const auto message = *reinterpret_cast<const SSTVRXCalibrationMessage*>(p);
this->on_calibration(message.suggested_phase, message.suggested_slant, message.sync_count);
}};
// UI Elements
RFAmpField field_rf_amp{{UI_POS_X(13), UI_POS_Y(0)}};
LNAGainField field_lna{{UI_POS_X(15), UI_POS_Y(0)}};
VGAGainField field_vga{{UI_POS_X(18), UI_POS_Y(0)}};
RSSI rssi{{UI_POS_X(21), 0, UI_POS_WIDTH_REMAINING(24), 4}};
Channel channel{{UI_POS_X(21), 5, UI_POS_WIDTH_REMAINING(24), 4}};
RxFrequencyField field_frequency{{UI_POS_X(0), UI_POS_Y(0)}, nav_};
AudioVolumeField field_volume{{UI_POS_X_RIGHT(2), UI_POS_Y(0)}};
OptionsField options_mode{{UI_POS_X(6), UI_POS_Y(1)}, 16, {}};
NumberField field_phase{{UI_POS_X(4), UI_POS_Y(2)}, 3, {-50, 50}, 1, ' '};
NumberField field_slant{{UI_POS_X(13), UI_POS_Y(2)}, 4, {-100, 100}, 1, ' '};
Labels labels{
{{UI_POS_X(1), UI_POS_Y(1)}, "Mode:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(1), UI_POS_Y(2)}, "Ph:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(8), UI_POS_Y(2)}, "Slnt:", Theme::getInstance()->fg_light->foreground}};
Audio audio{{UI_POS_X(21), 10, UI_POS_WIDTH(6), 4}};
ui::Button start_stop_btn{{UI_POS_X_RIGHT(12), UI_POS_Y(3), UI_POS_WIDTH(11), UI_POS_HEIGHT(2)}, "Start RX"};
// ui::Button redraw_btn{{16 * 8, UI_POS_Y(5), UI_POS_WIDTH(12), UI_POS_HEIGHT(3)}, "Redraw"};
// Calibration suggestion display
Text text_calibration{
{UI_POS_X(1), UI_POS_Y(3), UI_POS_WIDTH(17), UI_POS_HEIGHT(1)},
"Calib: N/A"};
void on_audio_spectrum();
void update_display(uint16_t line_num, const uint8_t* rgb_line);
void redraw_image(); // Disabled due to memory constraints
void start_audio();
void on_start_stop(); // Combined start/stop handler
void on_stop();
void on_mode_changed(const size_t index);
void on_progress(uint16_t line, uint16_t total_lines);
void on_calibration(int16_t suggested_phase, int16_t suggested_slant, uint16_t sync_count);
void write_bmp_header();
void write_line_to_file(uint16_t line_num, const uint8_t* rgb_line);
void finish_image();
};
} // namespace ui::external_app::sstvrx
#endif // __SSTVRX_H__
+81
View File
@@ -0,0 +1,81 @@
/*
* Copyright (C) 2026 HTotoo
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#include "ui_subcar.hpp"
#include "ui_navigation.hpp"
#include "external_app.hpp"
namespace ui::external_app::subcarrx {
void initialize_app(ui::NavigationView& nav) {
nav.push<SubCarView>();
}
} // namespace ui::external_app::subcarrx
extern "C" {
__attribute__((section(".external_app.app_subcarrx.application_information"), used)) application_information_t _application_information_subcarrx = {
/*.memory_location = */ (uint8_t*)0x00000000,
/*.externalAppEntry = */ ui::external_app::subcarrx::initialize_app,
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
/*.app_name = */ "SubCar",
/*.bitmap_data = */ {
0xC0,
0x03,
0xE0,
0x07,
0x30,
0x0C,
0x30,
0x0C,
0x30,
0x0C,
0x30,
0x0C,
0xE0,
0x07,
0xC0,
0x03,
0x80,
0x01,
0x80,
0x01,
0x80,
0x01,
0x80,
0x01,
0x80,
0x07,
0x80,
0x03,
0x80,
0x07,
0x80,
0x01,
},
/*.icon_color = */ ui::Color::orange().v,
/*.menu_location = */ app_location_t::RX,
/*.desired_menu_position = */ -1,
/*.m4_app_tag = portapack::spi_flash::image_tag_acars */ {'P', 'S', 'C', 'D'},
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
};
}
+459
View File
@@ -0,0 +1,459 @@
/*
* Copyright (C) 2026 HTotoo
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#include "ui_subcar.hpp"
#include "audio.hpp"
#include "baseband_api.hpp"
#include "string_format.hpp"
#include "file_path.hpp"
#include "portapack_persistent_memory.hpp"
using namespace portapack;
using namespace ui;
namespace ui::external_app::subcarrx {
std::string SubCarRecentEntry::to_csv() {
std::string csv = ";";
csv += SubCarView::getSensorTypeName((FPROTO_SUBCAR_SENSOR)sensorType);
csv += ";" + to_string_dec_uint(bits) + ";";
csv += to_string_hex(data, 64 / 4) + ";" + to_string_hex(data2, 64 / 4);
return csv;
}
void SubCarLogger::log_data(SubCarRecentEntry& data) {
log_file.write_entry(data.to_csv());
}
void SubCarRecentEntryDetailView::update_data() {
// process protocol data
parseProtocol();
// set text elements
text_type.set(SubCarView::getSensorTypeName((FPROTO_SUBCAR_SENSOR)entry_.sensorType));
text_id.set("0x" + to_string_hex(serial));
if (entry_.bits > 0) console.writeln("Bits: " + to_string_dec_uint(entry_.bits));
if (!btn.empty()) console.writeln("Btn: " + btn);
if (cnt != SD_NO_CNT) console.writeln("Cnt: " + to_string_dec_uint(cnt));
if (entry_.data != 0) console.writeln("Data : " + to_string_hex(entry_.data));
if (entry_.data2 != 0) console.writeln("Data2: " + to_string_hex(entry_.data2));
}
SubCarRecentEntryDetailView::SubCarRecentEntryDetailView(NavigationView& nav, const SubCarRecentEntry& entry)
: nav_{nav},
entry_{entry} {
add_children({&button_done,
&text_type,
&text_id,
&console,
&labels});
button_done.on_select = [&nav](const ui::Button&) {
nav.pop();
};
update_data();
}
void SubCarRecentEntryDetailView::focus() {
button_done.focus();
}
void SubCarView::focus() {
field_frequency.focus();
}
SubCarView::SubCarView(NavigationView& nav)
: nav_{nav} {
add_children({&rssi,
&channel,
&field_rf_amp,
&field_lna,
&field_vga,
&field_frequency,
&button_clear_list,
&check_log,
&labels,
&recent_entries_view});
baseband::run_prepared_image(portapack::memory::map::m4_code.base());
logger = std::make_unique<SubCarLogger>();
button_clear_list.on_select = [this](Button&) {
recent.clear();
recent_entries_view.set_dirty();
};
field_frequency.set_step(10000);
check_log.on_select = [this](Checkbox&, bool v) {
logging = v;
if (logger && logging) {
logger->append(logs_dir.string() + "/SubCarLOG_" + to_string_timestamp(rtc_time::now()) + ".CSV");
logger->write_header();
}
};
check_log.set_value(logging);
const Rect content_rect{0, header_height, screen_width, screen_height - header_height};
recent_entries_view.set_parent_rect(content_rect);
recent_entries_view.on_select = [this](const SubCarRecentEntry& entry) {
nav_.push<SubCarRecentEntryDetailView>(entry);
};
baseband::set_subghzd_config(0, receiver_model.sampling_rate()); // 0=am
receiver_model.enable();
signal_token_tick_second = rtc_time::signal_tick_second += [this]() {
on_tick_second();
};
}
void SubCarView::on_tick_second() {
for (auto& entry : recent) {
entry.inc_age(1);
}
recent_entries_view.set_dirty();
}
void SubCarView::on_data(const SubCarDataMessage* data) {
SubCarRecentEntry key{data->sensorType, data->data, data->data2, data->bits};
if (logger && logging) {
logger->log_data(key);
}
auto matching_recent = find(recent, key.key());
if (matching_recent != std::end(recent)) {
// Found within. Move to front of list, increment counter.
(*matching_recent).reset_age();
recent.push_front(*matching_recent);
recent.erase(matching_recent);
} else {
recent.emplace_front(key);
truncate_entries(recent, 64);
}
recent_entries_view.set_dirty();
}
SubCarView::~SubCarView() {
rtc_time::signal_tick_second -= signal_token_tick_second;
receiver_model.disable();
baseband::shutdown();
}
const char* SubCarView::getSensorTypeName(FPROTO_SUBCAR_SENSOR type) {
switch (type) {
case FPC_SUZUKI:
return "Suzuki";
case FPC_VW:
return "VW";
case FPC_SUBARU:
return "Subaru";
case FPC_KIAV5:
return "Kia V5";
case FPC_KIAV3V4:
return "Kia V3/V4";
case FPC_KIAV2:
return "Kia V2";
case FPC_KIAV1:
return "Kia V1";
case FPC_KIAV0:
return "Kia V0";
case FPC_FORDV0:
return "Ford V0";
case FPC_FIATV0:
return "Fiat V0";
case FPC_BMWV0:
return "BMW V0";
case FPC_Invalid:
default:
return "Unknown";
}
}
std::string SubCarView::pad_string_with_spaces(int snakes) {
std::string paddedStr(snakes, ' ');
return paddedStr;
}
void SubCarView::on_freqchg(int64_t freq) {
field_frequency.set_value(freq);
}
void subaru_decode_count(const uint8_t* KB, uint16_t* count) {
uint8_t lo = 0;
if ((KB[4] & 0x40) == 0)
lo |= 0x01;
if ((KB[4] & 0x80) == 0)
lo |= 0x02;
if ((KB[5] & 0x01) == 0)
lo |= 0x04;
if ((KB[5] & 0x02) == 0)
lo |= 0x08;
if ((KB[6] & 0x01) == 0)
lo |= 0x10;
if ((KB[6] & 0x02) == 0)
lo |= 0x20;
if ((KB[5] & 0x40) == 0)
lo |= 0x40;
if ((KB[5] & 0x80) == 0)
lo |= 0x80;
uint8_t REG_SH1 = (KB[7] << 4) & 0xF0;
if (KB[5] & 0x04)
REG_SH1 |= 0x04;
if (KB[5] & 0x08)
REG_SH1 |= 0x08;
if (KB[6] & 0x80)
REG_SH1 |= 0x02;
if (KB[6] & 0x40)
REG_SH1 |= 0x01;
uint8_t REG_SH2 = ((KB[6] << 2) & 0xF0) | ((KB[7] >> 4) & 0x0F);
uint8_t SER0 = KB[3];
uint8_t SER1 = KB[1];
uint8_t SER2 = KB[2];
uint8_t total_rot = 4 + lo;
for (uint8_t i = 0; i < total_rot; ++i) {
uint8_t t_bit = (SER0 >> 7) & 1;
SER0 = ((SER0 << 1) & 0xFE) | ((SER1 >> 7) & 1);
SER1 = ((SER1 << 1) & 0xFE) | ((SER2 >> 7) & 1);
SER2 = ((SER2 << 1) & 0xFE) | t_bit;
}
uint8_t T1 = SER1 ^ REG_SH1;
uint8_t T2 = SER2 ^ REG_SH2;
uint8_t hi = 0;
if ((T1 & 0x10) == 0)
hi |= 0x04;
if ((T1 & 0x20) == 0)
hi |= 0x08;
if ((T2 & 0x80) == 0)
hi |= 0x02;
if ((T2 & 0x40) == 0)
hi |= 0x01;
if ((T1 & 0x01) == 0)
hi |= 0x40;
if ((T1 & 0x02) == 0)
hi |= 0x80;
if ((T2 & 0x08) == 0)
hi |= 0x20;
if ((T2 & 0x04) == 0)
hi |= 0x10;
*count = ((hi << 8) | lo) & 0xFFFF;
}
void SubCarRecentEntryDetailView::parseProtocol() {
btn = "";
cnt = SD_NO_CNT;
serial = 0;
if (entry_.sensorType == FPC_Invalid) return;
if (entry_.sensorType == FPC_SUZUKI) {
uint32_t 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;
btn = to_string_dec_uint(buttonid);
return;
}
if (entry_.sensorType == FPC_VW) {
// uint32_t key_high = (entry_.data >> 32) & 0xFFFFFFFF;
uint32_t key_low = entry_.data & 0xFFFFFFFF;
serial = key_low; // trimmed to 32 bits for VW
uint8_t check = entry_.data2 & 0xFF;
uint8_t btnid = (check >> 4) & 0xF;
switch (btnid) {
case 0x1:
btn = "UNLOCK";
break;
case 0x2:
btn = "LOCK";
break;
case 0x3:
btn = "Un+Lk";
break;
case 0x4:
btn = "TRUNK";
break;
case 0x5:
btn = "Un+Tr";
break;
case 0x6:
btn = "Lk+Tr";
break;
case 0x7:
btn = "Un+Lk+Tr";
break;
case 0x8:
btn = "PANIC";
break;
default:
btn = "Unknown";
break;
}
}
if (entry_.sensorType == FPC_SUBARU) {
uint8_t* data_bytes = (uint8_t*)entry_.data;
serial = ((uint32_t)data_bytes[1] << 16) | ((uint32_t)data_bytes[2] << 8) | data_bytes[3];
uint8_t button = data_bytes[0] & 0x0F;
btn = to_string_dec_uint(button);
uint16_t cnttmp = 0;
subaru_decode_count(data_bytes, &cnttmp);
cnt = cnttmp;
}
if (entry_.sensorType == FPC_KIAV5) {
serial = (uint32_t)(((entry_.data >> 32) & 0x0FFFFFFF) >> 1);
uint8_t button = (entry_.data >> 61) & 0x07;
btn = to_string_dec_uint(button);
cnt = (uint16_t)(entry_.data & 0xFFFF);
}
if (entry_.sensorType == FPC_KIAV3V4) {
// not decrypted!
serial = SD_NO_SERIAL; //(uint32_t)entry_.data;
// uint8_t button = entry_.data2 & 0xFF;
btn = "?"; // to_string_dec_uint(button);
}
if (entry_.sensorType == FPC_KIAV2) {
serial = (uint32_t)((entry_.data >> 20) & 0xFFFFFFFF);
uint8_t button = (uint8_t)((entry_.data >> 16) & 0x0F);
uint16_t raw_count = (uint16_t)((entry_.data >> 4) & 0xFFF);
cnt = ((raw_count >> 4) | (raw_count << 8)) & 0xFFF;
btn = to_string_dec_uint(button);
}
if (entry_.sensorType == FPC_KIAV1) {
serial = (uint32_t)((entry_.data >> 24) & 0xFFFFFFFF);
uint8_t button = (uint8_t)((entry_.data >> 16) & 0xFF);
cnt = (uint8_t)((entry_.data >> 8) & 0xFF);
btn = to_string_dec_uint(button);
}
if (entry_.sensorType == FPC_KIAV0) {
serial = (uint32_t)((entry_.data >> 12) & 0x0FFFFFFF);
uint8_t button = (entry_.data >> 8) & 0x0F;
cnt = (entry_.data >> 40) & 0xFFFF;
btn = to_string_dec_uint(button);
}
if (entry_.sensorType == FPC_FORDV0) {
uint8_t buf[13] = {0};
for (int i = 0; i < 8; ++i) {
buf[i] = (uint8_t)(entry_.data >> (56 - i * 8));
}
buf[8] = (uint8_t)(entry_.data2 >> 8);
buf[9] = (uint8_t)(entry_.data2 & 0xFF);
uint8_t tmp = buf[8];
uint8_t parity = 0;
uint8_t parity_any = (tmp != 0);
while (tmp) {
parity ^= (tmp & 1);
tmp >>= 1;
}
buf[11] = parity_any ? parity : 0;
uint8_t xor_byte;
uint8_t limit;
if (buf[11]) {
xor_byte = buf[7];
limit = 7;
} else {
xor_byte = buf[6];
limit = 6;
}
for (int idx = 1; idx < limit; ++idx) {
buf[idx] ^= xor_byte;
}
if (buf[11] == 0) {
buf[7] ^= xor_byte;
}
uint8_t orig_b7 = buf[7];
buf[7] = (orig_b7 & 0xAA) | (buf[6] & 0x55);
uint8_t mixed = (buf[6] & 0xAA) | (orig_b7 & 0x55);
buf[12] = mixed;
buf[6] = mixed;
uint32_t serial_le = ((uint32_t)buf[1]) |
((uint32_t)buf[2] << 8) |
((uint32_t)buf[3] << 16) |
((uint32_t)buf[4] << 24);
serial = ((serial_le & 0xFF) << 24) |
(((serial_le >> 8) & 0xFF) << 16) |
(((serial_le >> 16) & 0xFF) << 8) |
((serial_le >> 24) & 0xFF);
uint8_t button = (buf[5] >> 4) & 0x0F;
cnt = ((buf[5] & 0x0F) << 16) |
(buf[6] << 8) |
buf[7];
btn = to_string_dec_uint(button);
}
if (entry_.sensorType == FPC_FIATV0) {
serial = (uint32_t)(entry_.data & 0xFFFFFFFF);
cnt = (uint32_t)((entry_.data >> 32) & 0xFFFFFFFF);
uint8_t button = (uint8_t)(entry_.data2 & 0xFF);
btn = to_string_dec_uint(button);
}
if (entry_.sensorType == FPC_BMWV0) {
serial = (uint32_t)((entry_.data >> 12) & 0x0FFFFFFF);
uint8_t button = (entry_.data >> 8) & 0x0F;
cnt = (entry_.data >> 40) & 0xFFFF;
btn = to_string_dec_uint(button);
}
return;
}
} // namespace ui::external_app::subcarrx
namespace ui {
template <>
void RecentEntriesTable<ui::external_app::subcarrx::SubCarRecentEntries>::draw(
const Entry& entry,
const Rect& target_rect,
Painter& painter,
const Style& style,
ui::RecentEntriesColumns& columns) {
std::string line{};
line.reserve(30);
line = ui::external_app::subcarrx::SubCarView::getSensorTypeName((FPROTO_SUBCAR_SENSOR)entry.sensorType);
line = line + " " + to_string_hex(entry.data << 32);
line.resize(columns.at(0).second, ' ');
std::string ageStr = to_string_dec_uint(entry.age);
std::string bitsStr = to_string_dec_uint(entry.bits);
line += ui::external_app::subcarrx::SubCarView::pad_string_with_spaces(5 - bitsStr.length()) + bitsStr;
line += ui::external_app::subcarrx::SubCarView::pad_string_with_spaces(4 - ageStr.length()) + ageStr;
line.resize(target_rect.width() / 8, ' ');
painter.draw_string(target_rect.location(), style, line);
}
} // namespace ui
+222
View File
@@ -0,0 +1,222 @@
/*
* Copyright (C) 2026 HTotoo
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
/*
This and The other files related to this is based on a lot of great people's work. https://github.com/RocketGod-git/ProtoPirate Check the repo, and the credits inside.
*/
#ifndef __UI_SubCar_H__
#define __UI_SubCar_H__
#define SD_NO_SERIAL 0xFFFFFFFF
#define SD_NO_BTN 0xFF
#define SD_NO_CNT 0xFF
#include "ui.hpp"
#include "ui_navigation.hpp"
#include "ui_receiver.hpp"
#include "ui_freq_field.hpp"
#include "app_settings.hpp"
#include "radio_state.hpp"
#include "utility.hpp"
#include "log_file.hpp"
#include "recent_entries.hpp"
#include "../../baseband/fprotos/subcartypes.hpp"
using namespace ui;
namespace ui::external_app::subcarrx {
struct SubCarRecentEntry {
using Key = uint64_t;
static constexpr Key invalid_key = 0x0fffffff;
uint8_t sensorType = FPC_Invalid;
uint16_t bits = 0;
uint16_t age = 0; // updated on each seconds, show how long the signal was last seen
uint64_t data = 0;
uint64_t data2 = 0;
SubCarRecentEntry() {}
SubCarRecentEntry(
uint8_t sensorType,
uint64_t data = 0,
uint64_t data2 = 0,
uint16_t bits = 0)
: sensorType{sensorType},
bits{bits},
data{data},
data2{data2} {
}
Key key() const {
return (data ^ ((static_cast<uint64_t>(sensorType) & 0xFF) << 0)); // should be optimized...
}
void inc_age(int delta) {
if (UINT16_MAX - delta > age) age += delta;
}
void reset_age() {
age = 0;
}
std::string to_csv();
};
class SubCarLogger {
public:
Optional<File::Error> append(const std::filesystem::path& filename) {
return log_file.append(filename);
}
void log_data(SubCarRecentEntry& data);
void write_header() {
log_file.write_entry(";Type; Bits; Data;");
}
private:
LogFile log_file{};
};
using SubCarRecentEntries = RecentEntries<SubCarRecentEntry>;
using SubCarRecentEntriesView = RecentEntriesView<SubCarRecentEntries>;
class SubCarView : public View {
public:
SubCarView(NavigationView& nav);
~SubCarView();
void focus() override;
std::string title() const override { return "SubCar"; };
static const char* getSensorTypeName(FPROTO_SUBCAR_SENSOR type);
static std::string pad_string_with_spaces(int snakes);
private:
void on_tick_second();
void on_data(const SubCarDataMessage* data);
NavigationView& nav_;
RxRadioState radio_state_{
433'920'000 /* frequency */,
1'750'000 /* bandwidth */,
4'000'000 /* sampling rate */,
ReceiverModel::Mode::AMAudio};
bool logging = false;
app_settings::SettingsManager settings_{
"rx_subcar",
app_settings::Mode::RX,
{
{"log"sv, &logging},
}};
SubCarRecentEntries recent{};
RFAmpField field_rf_amp{
{13 * 8, UI_POS_Y(0)}};
LNAGainField field_lna{
{15 * 8, UI_POS_Y(0)}};
VGAGainField field_vga{
{18 * 8, UI_POS_Y(0)}};
RSSI rssi{
{21 * 8, 0, UI_POS_WIDTH_REMAINING(24), 4}};
Channel channel{
{21 * 8, 5, UI_POS_WIDTH_REMAINING(24), 4},
};
RxFrequencyField field_frequency{
{UI_POS_X(0), UI_POS_Y(0)},
nav_};
SignalToken signal_token_tick_second{};
Button button_clear_list{
{0, 16, 7 * 8, 32},
"Clear"};
Checkbox check_log{
{10 * 8, 18},
3,
"Log",
true};
Labels labels{
{{UI_POS_X_RIGHT(14), UI_POS_Y(1)}, "no fm yet :(", Theme::getInstance()->fg_light->foreground},
};
static constexpr auto header_height = 3 * 16;
std::unique_ptr<SubCarLogger> logger{};
ui::RecentEntriesColumns columns{{
{"Type", 0},
{"Bits", 4},
{"Age", 3},
}};
SubCarRecentEntriesView recent_entries_view{columns, recent};
void on_freqchg(int64_t freq);
MessageHandlerRegistration message_handler_freqchg{
Message::ID::FreqChangeCommand,
[this](Message* const p) {
const auto message = static_cast<const FreqChangeCommandMessage*>(p);
this->on_freqchg(message->freq);
}};
MessageHandlerRegistration message_handler_packet{
Message::ID::SubCarData,
[this](Message* const p) {
const auto message = static_cast<const SubCarDataMessage*>(p);
this->on_data(message);
}};
};
class SubCarRecentEntryDetailView : public View {
public:
SubCarRecentEntryDetailView(NavigationView& nav, const SubCarRecentEntry& entry);
void update_data();
void focus() override;
private:
NavigationView& nav_;
SubCarRecentEntry entry_{};
uint32_t serial = 0;
std::string btn = "";
uint32_t cnt = SD_NO_CNT;
Text text_type{{UI_POS_X(0), 1 * 16, 15 * 8, 16}, "?"};
Text text_id{{6 * 8, 2 * 16, 10 * 8, 16}, "?"};
Console console{
{0, 4 * 16, screen_width, screen_height - (4 * 16) - 36}};
Labels labels{
{{UI_POS_X(0), UI_POS_Y(0)}, "Type:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), 2 * 16}, "Serial: ", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), 3 * 16}, "Data:", Theme::getInstance()->fg_light->foreground},
};
Button button_done{
{screen_width - 96 - 4, screen_height - 32 - 12, 96, 32},
"Done"};
void parseProtocol();
};
} // namespace ui::external_app::subcarrx
#endif /*__UI_SubCar_H__*/
+1 -2
View File
@@ -598,8 +598,7 @@ void pause_game() {
joystick.detach();
locate((INFO_LEFT + SCREEN_WIDTH) / 2 - 25, 200);
printf("PAUSED");
while ((get_switches_state().to_ulong() & 0x10) == 0)
;
while ((get_switches_state().to_ulong() & 0x10) == 0);
fillrect(INFO_LEFT, 195, SCREEN_WIDTH, 210, Black);
joystick.attach(&ReadJoystickForFigure, 0.3);
game.attach(&PlayGame, delays[level]);
+1 -1
View File
@@ -99,7 +99,7 @@ class TPMSAppView : public View {
// Prevent painting of region covered entirely by a child.
// TODO: Add flag to View that specifies view does not need to be cleared before painting.
void paint(Painter&) override{};
void paint(Painter&) override {};
void focus() override;
+1 -1
View File
@@ -315,7 +315,7 @@ class File {
template <typename T>
using Result = Result<T, Error>;
File(){};
File() {};
~File();
File(File&& other) {
+1 -2
View File
@@ -361,8 +361,7 @@ class Si5351 {
}
void wait_for_device_ready() {
while (device_status() & 0x80)
;
while (device_status() & 0x80);
}
bool plla_loss_of_signal() {
+3 -6
View File
@@ -322,8 +322,7 @@ static void shutdown_base() {
});
cgu::pll1::enable();
while (!cgu::pll1::is_locked())
;
while (!cgu::pll1::is_locked());
set_clock_config(clock_config_pll1_boot);
@@ -361,15 +360,13 @@ static void set_cpu_clock_speed() {
});
cgu::pll1::enable();
while (!cgu::pll1::is_locked())
;
while (!cgu::pll1::is_locked());
set_clock_config(clock_config_pll1_step);
/* Delay >50us at 90-110MHz clock speed */
volatile uint32_t delay = 1400;
while (delay--)
;
while (delay--);
set_clock_config(clock_config_pll1);
+27
View File
@@ -265,4 +265,31 @@ uint8_t day_of_week(uint16_t year, uint8_t month, uint8_t day) {
return (day - 1 + (13 * m / 5) + y + (y / 4) - (y / 100) + (y / 400)) % 7;
}
bool isLeap(int year) {
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}
time_t rtcToUnixUTC(const rtc::RTC& rtc) {
const uint8_t daysOfMonth[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
uint16_t y = rtc.year();
uint8_t m = rtc.month();
uint8_t d = rtc.day();
uint32_t totalDays = 0;
for (int i = 1970; i < y; i++) {
totalDays += isLeap(i) ? 366 : 365;
}
for (int i = 1; i < m; i++) {
totalDays += daysOfMonth[i];
if (i == 2 && isLeap(y)) {
totalDays++;
}
}
totalDays += (d - 1);
time_t totalSeconds = totalDays * 86400; // 24 * 60 * 60
totalSeconds += rtc.hour() * 3600;
totalSeconds += rtc.minute() * 60;
totalSeconds += rtc.second();
return totalSeconds;
}
} /* namespace rtc_time */
+3
View File
@@ -56,6 +56,9 @@ bool leap_year(uint16_t year);
uint16_t day_of_year(uint16_t year, uint8_t month, uint8_t day);
uint16_t day_of_year_of_nth_weekday(uint16_t year, uint8_t month, uint8_t n, uint8_t weekday);
bool isLeap(int year);
time_t rtcToUnixUTC(const rtc::RTC& rtc);
} /* namespace rtc_time */
#endif /*__RTC_TIME_H__*/
+28 -17
View File
@@ -199,7 +199,7 @@ bool GeoMap::on_encoder(const EncoderEvent delta) {
}
}
map_osm_zoom++;
if (has_osm) set_osm_max_zoom();
if (has_osm) set_osm_max_zoom(true);
} else if (delta < 0) {
if (map_zoom > -MAX_MAP_ZOOM_OUT) {
if (map_zoom == 1) {
@@ -212,6 +212,7 @@ bool GeoMap::on_encoder(const EncoderEvent delta) {
}
}
if (map_osm_zoom > 0) map_osm_zoom--;
if (has_osm) set_osm_max_zoom(true);
} else {
return false;
}
@@ -299,8 +300,8 @@ ui::Point GeoMap::item_rect_pixel(GeoMarker& item) {
return {(int16_t)x, (int16_t)y};
}
// osm calculation
double y = lat_to_pixel_y_tile(item.lat, map_osm_zoom) - viewport_top_left_py;
double x = lon_to_pixel_x_tile(item.lon, map_osm_zoom) - viewport_top_left_px;
double y = lat_to_pixel_y_tile(item.lat, map_osm_real_zoom) - viewport_top_left_py;
double x = lon_to_pixel_x_tile(item.lon, map_osm_real_zoom) - viewport_top_left_px;
return {(int16_t)x, (int16_t)y};
}
@@ -327,7 +328,7 @@ int GeoMap::lat2tile(double lat, int zoom) {
return (int)floor((1.0 - log(tan(lat_rad) + 1.0 / cos(lat_rad)) / M_PI) / 2.0 * pow(2.0, zoom));
}
void GeoMap::set_osm_max_zoom() {
void GeoMap::set_osm_max_zoom(bool changeboth) {
if (map_osm_zoom > 20) map_osm_zoom = 20;
for (uint8_t i = map_osm_zoom; i > 0; i--) {
int tile_x = lon2tile(lon_, i);
@@ -335,11 +336,13 @@ void GeoMap::set_osm_max_zoom() {
std::string filename = "/OSM/" + to_string_dec_int(i) + "/" + to_string_dec_int(tile_x) + "/" + to_string_dec_int(tile_y) + ".bmp";
std::filesystem::path file_path(filename);
if (file_exists(file_path)) {
map_osm_zoom = i;
map_osm_real_zoom = i;
if (changeboth) map_osm_zoom = i;
return;
}
}
map_osm_zoom = 0; // should not happen
if (changeboth) map_osm_zoom = 0; // should not happen
map_osm_real_zoom = 0; // should not happen
}
// checks if the tile file presents or not. to determine if we got osm or not
@@ -454,14 +457,22 @@ bool GeoMap::draw_osm_file(int zoom, int tile_x, int tile_y, int relative_x, int
return false;
}
std::vector<ui::Color> line(clip_w);
for (int y = 0; y < clip_h; ++y) {
int source_row = src_y + y;
int dest_row = dest_y + y;
bmp.seek(src_x, source_row);
for (int x = 0; x < clip_w; ++x) {
bmp.read_next_px(line[x], true);
if (bmp.is_bottomup()) {
for (int y = clip_h - 1; y >= 0; --y) {
int source_row = src_y + y;
int dest_row = dest_y + y;
bmp.seek(src_x, source_row);
bmp.read_next_px_cnt(line.data(), clip_w, false);
display.draw_pixels({dest_x + r.left(), dest_row + r.top(), clip_w, 1}, line);
}
} else {
for (int y = 0; y < clip_h; ++y) {
int source_row = src_y + y;
int dest_row = dest_y + y;
bmp.seek(src_x, source_row);
bmp.read_next_px_cnt(line.data(), clip_w, false);
display.draw_pixels({dest_x + r.left(), dest_row + r.top(), clip_w, 1}, line);
}
display.draw_pixels({dest_x + r.left(), dest_row + r.top(), clip_w, 1}, line);
}
return true;
}
@@ -522,8 +533,8 @@ void GeoMap::paint(Painter& painter) {
} else {
// display osm tiles
// Convert center GPS to a global pixel coordinate
double global_center_px = lon_to_pixel_x_tile(lon_, map_osm_zoom);
double global_center_py = lat_to_pixel_y_tile(lat_, map_osm_zoom);
double global_center_px = lon_to_pixel_x_tile(lon_, map_osm_real_zoom);
double global_center_py = lat_to_pixel_y_tile(lat_, map_osm_real_zoom);
// Find the top-left corner of the screen (viewport) in global pixel coordinates
viewport_top_left_px = global_center_px - (r.width() / 2.0);
@@ -552,7 +563,7 @@ void GeoMap::paint(Painter& painter) {
// For the first tile (x=0, y=0), this will be the negative offset.
int draw_pos_x = round(render_offset_x + x * TILE_SIZE);
int draw_pos_y = round(render_offset_y + y * TILE_SIZE);
if (!draw_osm_file(map_osm_zoom, current_tile_x, current_tile_y, draw_pos_x, draw_pos_y)) {
if (!draw_osm_file(map_osm_real_zoom, current_tile_x, current_tile_y, draw_pos_x, draw_pos_y)) {
// already blanked it.
}
}
@@ -615,7 +626,7 @@ bool GeoMap::on_touch(const TouchEvent event) {
on_move(p.x() / 2.0 * lon_ratio, p.y() / 2.0 * lat_ratio, false);
} else {
p = event.point - screen_rect().location();
on_move(tile_pixel_x_to_lon(p.x() + viewport_top_left_px, map_osm_zoom), tile_pixel_y_to_lat(p.y() + viewport_top_left_py, map_osm_zoom), true);
on_move(tile_pixel_x_to_lon(p.x() + viewport_top_left_px, map_osm_real_zoom), tile_pixel_y_to_lat(p.y() + viewport_top_left_py, map_osm_real_zoom), true);
}
return true;
}
+3 -2
View File
@@ -255,7 +255,7 @@ class GeoMap : public Widget {
void map_read_line_bin(ui::Color* buffer, uint16_t pixels);
// open street map related
uint8_t find_osm_file_tile();
void set_osm_max_zoom();
void set_osm_max_zoom(bool changeboth = false);
bool draw_osm_file(int zoom, int tile_x, int tile_y, int relative_x, int relative_y);
int lon2tile(double lon, int zoom);
int lat2tile(double lat, int zoom);
@@ -263,7 +263,8 @@ class GeoMap : public Widget {
double lat_to_pixel_y_tile(double lat, int zoom);
double tile_pixel_x_to_lon(int x, int zoom);
double tile_pixel_y_to_lat(int y, int zoom);
uint8_t map_osm_zoom{3};
uint8_t map_osm_zoom{5};
uint8_t map_osm_real_zoom{5};
double viewport_top_left_px = 0;
double viewport_top_left_py = 0;
+21
View File
@@ -556,6 +556,13 @@ DeclareTargets(PUSB sd_over_usb)
set(add_to_firmware FALSE)
set(MODE_FLAGS "-O3")
### FLEX RX
set(MODE_CPPSRC
proc_flex.cpp
)
DeclareTargets(PFLX flex)
### ACARS RX
set(MODE_CPPSRC
@@ -644,6 +651,13 @@ set(MODE_CPPSRC
)
DeclareTargets(PSTX sstvtx)
### SSTV RX
set(MODE_CPPSRC
proc_sstvrx.cpp
)
DeclareTargets(PSRX sstvrx)
### TPMS
set(MODE_CPPSRC
@@ -690,6 +704,13 @@ set(MODE_CPPSRC
)
DeclareTargets(PATX audio_tx)
### SubCar Decoders
set(MODE_CPPSRC
proc_subcar.cpp
)
DeclareTargets(PSCD subcar)
### HackRF "factory" firmware
+1 -1
View File
@@ -34,7 +34,7 @@ class BasebandProcessor {
virtual void execute(const buffer_c8_t& buffer) = 0;
virtual void on_message(const Message* const){};
virtual void on_message(const Message* const) {};
protected:
void feed_channel_stats(const buffer_c16_t& channel);
+1 -2
View File
@@ -30,8 +30,7 @@ extern uint32_t __process_stack_end__;
inline uint32_t get_free_stack_space() {
uint32_t* p;
for (p = &__process_stack_base__; *p == CRT0_STACKS_FILL_PATTERN && p < &__process_stack_end__; p++)
;
for (p = &__process_stack_base__; *p == CRT0_STACKS_FILL_PATTERN && p < &__process_stack_end__; p++);
auto stack_space_left = p - &__process_stack_base__;
return stack_space_left;
+169
View File
@@ -0,0 +1,169 @@
#pragma once
#include "subcarbase.hpp"
#include <cstring>
typedef enum {
BMWDecoderStepReset = 0,
BMWDecoderStepCheckPreambula,
BMWDecoderStepSaveDuration,
BMWDecoderStepCheckDuration,
} BMWDecoderStep;
class FProtoSubCarBMWV0 : public FProtoSubCarBase {
public:
FProtoSubCarBMWV0() {
sensorType = FPC_BMWV0;
te_short = 350;
te_long = 700;
te_delta = 120;
min_count_bit_for_found = 61;
}
uint8_t subghz_protocol_bmw_crc8(uint8_t* data, size_t len) {
uint8_t crc = 0x00;
for (size_t i = 0; i < len; i++) {
crc ^= data[i];
for (uint8_t j = 0; j < 8; j++) {
if (crc & 0x80)
crc = (uint8_t)((crc << 1) ^ 0x31);
else
crc <<= 1;
}
}
return crc;
}
uint16_t subghz_protocol_bmw_crc16(uint8_t* data, size_t len) {
uint16_t crc = 0xFFFF;
for (size_t i = 0; i < len; i++) {
crc ^= ((uint16_t)data[i] << 8);
for (uint8_t j = 0; j < 8; j++) {
if (crc & 0x8000)
crc = (crc << 1) ^ 0x1021;
else
crc <<= 1;
}
}
return crc;
}
void subghz_protocol_decoder_bmw_reset_internal() {
decode_data = 0;
decode_count_bit = 0;
decode_data2 = 0;
parser_step = BMWDecoderStepReset;
header_count = 0;
crc_type = 0;
}
void feed(bool level, uint32_t duration) {
switch (parser_step) {
case BMWDecoderStepReset:
if (level && (DURATION_DIFF(duration, te_short) <
te_delta)) {
parser_step = BMWDecoderStepCheckPreambula;
te_last = duration;
header_count = 0;
decode_data = 0;
decode_count_bit = 0;
}
break;
case BMWDecoderStepCheckPreambula:
if (level) {
if ((DURATION_DIFF(duration, te_short) <
te_delta) ||
(DURATION_DIFF(duration, te_long) <
te_delta)) {
te_last = duration;
} else {
parser_step = BMWDecoderStepReset;
}
} else if (
(DURATION_DIFF(duration, te_short) <
te_delta) &&
(DURATION_DIFF(te_last, te_short) <
te_delta)) {
header_count++;
} else if (
(DURATION_DIFF(duration, te_long) <
te_delta) &&
(DURATION_DIFF(te_last, te_long) <
te_delta)) {
if (header_count > 15) {
parser_step = BMWDecoderStepSaveDuration;
decode_data = 0ULL;
decode_count_bit = 0;
} else {
parser_step = BMWDecoderStepReset;
}
} else {
parser_step = BMWDecoderStepReset;
}
break;
case BMWDecoderStepSaveDuration:
if (level) {
if (duration >=
(te_long + te_delta * 2UL)) {
if (decode_count_bit >=
min_count_bit_for_found) {
// instance->generic.data = decode_data;
data_count_bit = decode_count_bit;
// Perform CRC check with both CRC8 and CRC16
uint8_t* raw_bytes = (uint8_t*)decode_data;
size_t raw_len = (decode_count_bit + 7) / 8;
uint8_t crc8 = subghz_protocol_bmw_crc8(raw_bytes, raw_len - 1);
if (crc8 == raw_bytes[raw_len - 1]) {
crc_type = 8;
} else {
uint16_t crc16 = subghz_protocol_bmw_crc16(raw_bytes, raw_len - 2);
uint16_t rx_crc16 = (raw_bytes[raw_len - 2] << 8) | raw_bytes[raw_len - 1];
if (crc16 == rx_crc16) {
crc_type = 16;
} else {
crc_type = 0; // invalid
}
}
if (crc_type != 0 && callback) {
callback(this);
}
}
subghz_protocol_decoder_bmw_reset_internal();
} else {
te_last = duration;
parser_step = BMWDecoderStepCheckDuration;
}
} else {
parser_step = BMWDecoderStepReset;
}
break;
case BMWDecoderStepCheckDuration:
if (!level) {
if ((DURATION_DIFF(te_last, te_short) <
te_delta) &&
(DURATION_DIFF(duration, te_short) <
te_delta)) {
subghz_protocol_blocks_add_bit(0);
parser_step = BMWDecoderStepSaveDuration;
} else if (
(DURATION_DIFF(te_last, te_long) <
te_delta) &&
(DURATION_DIFF(duration, te_long) <
te_delta)) {
subghz_protocol_blocks_add_bit(1);
parser_step = BMWDecoderStepSaveDuration;
} else {
parser_step = BMWDecoderStepReset;
}
} else {
parser_step = BMWDecoderStepReset;
}
break;
}
}
uint16_t header_count = 0;
uint8_t crc_type = 0;
};
+204
View File
@@ -0,0 +1,204 @@
#pragma once
#include "subcarbase.hpp"
#include <cstring>
typedef enum {
FiatV0DecoderStepReset = 0,
FiatV0DecoderStepPreamble = 1,
FiatV0DecoderStepData = 2,
} FiatV0DecoderStep;
class FProtoSubCarFiatV0 : public FProtoSubCarBase {
public:
FProtoSubCarFiatV0() {
sensorType = FPC_FIATV0;
te_short = 200;
te_long = 400;
te_delta = 100;
min_count_bit_for_found = 64;
}
void feed(bool level, uint32_t duration) {
uint32_t gap_threshold = 800;
uint32_t diff;
switch (decoder_state) {
case FiatV0DecoderStepReset:
if (!level) {
return;
}
if (duration < te_short) {
diff = te_short - duration;
} else {
diff = duration - te_short;
}
if (diff < te_delta) {
data_low = 0;
data_high = 0;
decoder_state = FiatV0DecoderStepPreamble;
te_last = duration;
preamble_count = 0;
bit_count = 0;
FProtoGeneral::manchester_advance(
manchester_state,
ManchesterEventReset,
&manchester_state,
NULL);
}
break;
case FiatV0DecoderStepPreamble:
if (level) {
return;
}
if (duration < te_short) {
diff = te_short - duration;
if (diff < te_delta) {
preamble_count++;
te_last = duration;
if (preamble_count >= 0x96) {
if (duration < gap_threshold) {
diff = gap_threshold - duration;
} else {
diff = duration - gap_threshold;
}
if (diff < te_delta) {
decoder_state = FiatV0DecoderStepData;
preamble_count = 0;
data_low = 0;
data_high = 0;
bit_count = 0;
te_last = duration;
return;
}
}
} else {
decoder_state = FiatV0DecoderStepReset;
if (preamble_count >= 0x96) {
if (duration < gap_threshold) {
diff = gap_threshold - duration;
} else {
diff = duration - gap_threshold;
}
if (diff < te_delta) {
decoder_state = FiatV0DecoderStepData;
preamble_count = 0;
data_low = 0;
data_high = 0;
bit_count = 0;
te_last = duration;
return;
}
}
}
} else {
diff = duration - te_short;
if (diff < te_delta) {
preamble_count++;
te_last = duration;
} else {
decoder_state = FiatV0DecoderStepReset;
}
if (preamble_count >= 0x96) {
if (duration >= 799) {
diff = duration - gap_threshold;
} else {
diff = gap_threshold - duration;
}
if (diff < te_delta) {
decoder_state = FiatV0DecoderStepData;
preamble_count = 0;
data_low = 0;
data_high = 0;
bit_count = 0;
te_last = duration;
return;
}
}
}
break;
case FiatV0DecoderStepData:
ManchesterEvent event = ManchesterEventReset;
if (duration < te_short) {
diff = te_short - duration;
if (diff < te_delta) {
event = level ? ManchesterEventShortLow : ManchesterEventShortHigh;
}
} else {
diff = duration - te_short;
if (diff < te_delta) {
event = level ? ManchesterEventShortLow : ManchesterEventShortHigh;
} else {
if (duration < te_long) {
diff = te_long - duration;
} else {
diff = duration - te_long;
}
if (diff < te_delta) {
event = level ? ManchesterEventLongLow : ManchesterEventLongHigh;
}
}
}
if (event != ManchesterEventReset) {
bool data_bit_bool;
if (FProtoGeneral::manchester_advance(
manchester_state,
event,
&manchester_state,
&data_bit_bool)) {
uint32_t new_bit = data_bit_bool ? 1 : 0;
uint32_t carry = (data_low >> 31) & 1;
data_low = (data_low << 1) | new_bit;
data_high = (data_high << 1) | carry;
bit_count++;
if (bit_count == 0x40) {
fix = data_low;
hop = data_high;
data_low = 0;
data_high = 0;
}
if (bit_count > 0x46) {
final_count = bit_count;
endbyte = (uint8_t)data_low;
/*
generic.data = ((uint64_t)hop << 32) | fix;
generic.data_count_bit = 64;
generic.serial = fix;
generic.btn = endbyte; // still exported as btn for UI compatibility
generic.cnt = hop;
*/
decode_data = ((uint64_t)hop << 32) | fix; // this is my own data passer, not the original
decode_data2 = endbyte;
data_count_bit = 64;
if (callback) {
callback(this);
}
data_low = 0;
data_high = 0;
bit_count = 0;
decoder_state = FiatV0DecoderStepReset;
}
}
}
te_last = duration;
break;
}
}
ManchesterState manchester_state = ManchesterStateMid1;
uint8_t decoder_state = 0;
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;
};
+151
View File
@@ -0,0 +1,151 @@
#pragma once
#include "subcarbase.hpp"
#include <cstring>
typedef enum {
FordV0DecoderStepReset = 0,
FordV0DecoderStepPreamble,
FordV0DecoderStepPreambleCheck,
FordV0DecoderStepGap,
FordV0DecoderStepData,
} FordV0DecoderStep;
class FProtoSubCarFordV0 : public FProtoSubCarBase {
public:
FProtoSubCarFordV0() {
sensorType = FPC_FORDV0;
te_short = 250;
te_long = 500;
te_delta = 100;
min_count_bit_for_found = 64;
}
void ford_v0_add_bit(bool bit) {
uint32_t low = (uint32_t)data_low;
data_low = (data_low << 1) | (bit ? 1 : 0);
data_high = (data_high << 1) | ((low >> 31) & 1);
bit_count++;
}
bool ford_v0_process_data() {
if (bit_count == 64) {
uint64_t combined = ((uint64_t)data_high << 32) | data_low;
key1 = ~combined;
data_low = 0;
data_high = 0;
return false;
}
if (bit_count == 80) {
uint16_t key2_raw = (uint16_t)(data_low & 0xFFFF);
uint16_t key2 = ~key2_raw;
decode_data = key1;
decode_data2 = key2;
// decode_ford_v0(key1, key2, &serial, &button, &count);
return true;
}
return false;
}
void feed(bool level, uint32_t duration) {
uint32_t gap_threshold = 3500;
switch (parser_step) {
case FordV0DecoderStepReset:
if (level && (DURATION_DIFF(duration, te_short) < te_delta)) {
data_low = 0;
data_high = 0;
parser_step = FordV0DecoderStepPreamble;
te_last = duration;
header_count = 0;
bit_count = 0;
FProtoGeneral::manchester_advance(manchester_state, ManchesterEventReset, &manchester_state, NULL);
}
break;
case FordV0DecoderStepPreamble:
if (!level) {
if (DURATION_DIFF(duration, te_long) < te_delta) {
te_last = duration;
parser_step = FordV0DecoderStepPreambleCheck;
} else {
parser_step = FordV0DecoderStepReset;
}
}
break;
case FordV0DecoderStepPreambleCheck:
if (level) {
if (DURATION_DIFF(duration, te_long) < te_delta) {
header_count++;
te_last = duration;
parser_step = FordV0DecoderStepPreamble;
} else if (DURATION_DIFF(duration, te_short) < te_delta) {
parser_step = FordV0DecoderStepGap;
} else {
parser_step = FordV0DecoderStepReset;
}
}
break;
case FordV0DecoderStepGap:
if (!level && (DURATION_DIFF(duration, gap_threshold) < 250)) {
data_low = 1;
data_high = 0;
bit_count = 1;
parser_step = FordV0DecoderStepData;
} else if (!level && duration > gap_threshold + 250) {
parser_step = FordV0DecoderStepReset;
}
break;
case FordV0DecoderStepData: {
ManchesterEvent event;
if (DURATION_DIFF(duration, te_short) < te_delta) {
event = level ? ManchesterEventShortLow : ManchesterEventShortHigh;
} else if (DURATION_DIFF(duration, te_long) < te_delta) {
event = level ? ManchesterEventLongLow : ManchesterEventLongHigh;
} else {
parser_step = FordV0DecoderStepReset;
break;
}
bool data_bit;
if (FProtoGeneral::manchester_advance(manchester_state, event, &manchester_state, &data_bit)) {
ford_v0_add_bit(data_bit);
if (ford_v0_process_data()) {
/* instance->generic.data = instance->key1;
instance->generic.data_count_bit = 64;
instance->generic.serial = instance->serial;
instance->generic.btn = instance->button;
instance->generic.cnt = instance->count;
*/
data_count_bit = 64;
if (callback) {
callback(this);
}
data_low = 0;
data_high = 0;
bit_count = 0;
parser_step = FordV0DecoderStepReset;
}
}
te_last = duration;
break;
}
}
}
ManchesterState manchester_state = ManchesterStateMid1;
uint64_t data_low = 0;
uint64_t data_high = 0;
uint8_t bit_count = 0;
uint16_t header_count = 0;
uint64_t key1 = 0;
uint16_t key2 = 0;
};
+121
View File
@@ -0,0 +1,121 @@
#pragma once
#include "subcarbase.hpp"
#include <cstring>
typedef enum {
KIADecoderStepReset = 0,
KIADecoderStepCheckPreambula,
KIADecoderStepSaveDuration,
KIADecoderStepCheckDuration,
} KIADecoderStep;
class FProtoSubCarKiaV0 : public FProtoSubCarBase {
public:
FProtoSubCarKiaV0() {
sensorType = FPC_KIAV0;
te_short = 250;
te_long = 500;
te_delta = 100;
min_count_bit_for_found = 61;
}
void feed(bool level, uint32_t duration) {
switch (parser_step) {
case KIADecoderStepReset:
if ((level) && (DURATION_DIFF(duration, te_short) < te_delta)) {
parser_step = KIADecoderStepCheckPreambula;
te_last = duration;
header_count = 0;
}
break;
case KIADecoderStepCheckPreambula:
if (level) {
if ((DURATION_DIFF(duration, te_short) < te_delta) ||
(DURATION_DIFF(duration, te_long) < te_delta)) {
te_last = duration;
} else {
parser_step = KIADecoderStepReset;
}
} else if (
(DURATION_DIFF(duration, te_short) < te_delta) &&
(DURATION_DIFF(te_last, te_short) < te_delta)) {
header_count++;
break;
} else if (
(DURATION_DIFF(duration, te_long) < te_delta) &&
(DURATION_DIFF(te_last, te_long) < te_delta)) {
if (header_count > 15) {
parser_step = KIADecoderStepSaveDuration;
decode_data = 0;
decode_count_bit = 1;
subghz_protocol_blocks_add_bit(1);
// FURI_LOG_I(TAG, "Starting data decode after %u header pulses", header_count);
} else {
parser_step = KIADecoderStepReset;
}
} else {
parser_step = KIADecoderStepReset;
}
break;
case KIADecoderStepSaveDuration:
if (level) {
if (duration >=
(te_long + te_delta * 2UL)) {
// Signal ended too early!
// FURI_LOG_W(TAG, "Signal ended at %u bits (expected 61). Duration: %lu", decode_count_bit, duration);
parser_step = KIADecoderStepReset;
if (decode_count_bit == min_count_bit_for_found) {
// instance->generic.data = decode_data;
data_count_bit = decode_count_bit;
if (callback)
callback(this);
} else {
// FURI_LOG_E(TAG, "Incomplete signal: only %u bits", decode_count_bit);
}
decode_data = 0;
decode_count_bit = 0;
break;
} else {
te_last = duration;
parser_step = KIADecoderStepCheckDuration;
}
} else {
parser_step = KIADecoderStepReset;
}
break;
case KIADecoderStepCheckDuration:
if (!level) {
if ((DURATION_DIFF(te_last, te_short) < te_delta) &&
(DURATION_DIFF(duration, te_short) < te_delta)) {
subghz_protocol_blocks_add_bit(0);
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)) {
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);
parser_step = KIADecoderStepReset;
}
} else {
parser_step = KIADecoderStepReset;
}
break;
}
}
bool is_running = false;
size_t preamble_count = 0;
size_t data_bit_index = 0;
uint8_t last_bit = 0;
bool send_high = false;
uint16_t header_count = 0;
};
+192
View File
@@ -0,0 +1,192 @@
#pragma once
#include "subcarbase.hpp"
#include <cstring>
typedef enum {
KiaV1DecoderStepReset = 0,
KiaV1DecoderStepCheckPreamble,
KiaV1DecoderStepFoundShortLow,
KiaV1DecoderStepCollectRawBits,
} KiaV1DecoderStep;
class FProtoSubCarKiaV1 : public FProtoSubCarBase {
public:
FProtoSubCarKiaV1() {
sensorType = FPC_KIAV1;
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;
}
void feed(bool level, uint32_t duration) {
switch (parser_step) {
case KiaV1DecoderStepReset:
// Preamble 0xCCCCCCCD produces alternating LONG pulses
if ((level) && (DURATION_DIFF(duration, te_long) <
te_delta)) {
parser_step = KiaV1DecoderStepCheckPreamble;
te_last = duration;
header_count = 1;
}
break;
case KiaV1DecoderStepCheckPreamble:
if (level) {
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 = 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;
}
}
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;
}
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);
}
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;
};
+167
View File
@@ -0,0 +1,167 @@
#pragma once
#include "subcarbase.hpp"
#include <cstring>
typedef enum {
KiaV2DecoderStepReset = 0,
KiaV2DecoderStepCheckPreamble,
KiaV2DecoderStepCollectRawBits,
} KiaV2DecoderStep;
class FProtoSubCarKiaV2 : public FProtoSubCarBase {
public:
FProtoSubCarKiaV2() {
sensorType = FPC_KIAV2;
te_short = 500;
te_long = 1000;
te_delta = 160;
min_count_bit_for_found = 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;
}
void feed(bool level, uint32_t duration) {
switch (parser_step) {
case KiaV2DecoderStepReset:
if ((level) && (DURATION_DIFF(duration, te_long) < te_delta)) {
parser_step = KiaV2DecoderStepCheckPreamble;
te_last = duration;
header_count = 1;
}
break;
case KiaV2DecoderStepCheckPreamble:
if (level) {
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) {
parser_step = KiaV2DecoderStepCollectRawBits;
raw_bit_count = 0;
memset(raw_bits, 0, sizeof(raw_bits));
}
} else {
parser_step = KiaV2DecoderStepReset;
}
}
break;
case KiaV2DecoderStepCollectRawBits:
if (duration > 1500) {
if (kia_v2_manchester_decode()) {
/*data = decode_data;
data_count_bit = decode_count_bit;
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;
} else if (
DURATION_DIFF(duration, te_long) <
te_delta) {
num_bits = 2;
} else {
parser_step = KiaV2DecoderStepReset;
break;
}
for (int i = 0; i < num_bits; i++) {
kia_v2_add_raw_bit(level);
}
break;
}
}
uint8_t raw_bits[20]{0};
uint16_t raw_bit_count = 0;
uint16_t header_count = 0;
};
+185
View File
@@ -0,0 +1,185 @@
#pragma once
#include "subcarbase.hpp"
#include <cstring>
typedef enum {
KiaV3V4DecoderStepReset = 0,
KiaV3V4DecoderStepCheckPreamble,
KiaV3V4DecoderStepCollectRawBits,
} KiaV3V4DecoderStep;
class FProtoSubCarKiaV3V4 : public FProtoSubCarBase {
public:
FProtoSubCarKiaV3V4() {
sensorType = FPC_KIAV3V4;
te_short = 400;
te_long = 800;
te_delta = 150;
min_count_bit_for_found = 64;
}
uint8_t reverse8(uint8_t byte) {
byte = (byte & 0xF0) >> 4 | (byte & 0x0F) << 4;
byte = (byte & 0xCC) >> 2 | (byte & 0x33) << 2;
byte = (byte & 0xAA) >> 1 | (byte & 0x55) << 1;
return byte;
}
void kia_v3_v4_add_raw_bit(bool bit) {
if (raw_bit_count < 256) {
uint16_t byte_idx = raw_bit_count / 8;
uint8_t bit_idx = 7 - (raw_bit_count % 8);
if (bit) {
raw_bits[byte_idx] |= (1 << bit_idx);
} else {
raw_bits[byte_idx] &= ~(1 << bit_idx);
}
raw_bit_count++;
}
}
bool kia_v3_v4_process_buffer() {
if (raw_bit_count < 64) {
return false;
}
uint8_t* b = raw_bits;
// For V3-style (long LOW sync), data is inverted
if (is_v3_sync) {
uint16_t num_bytes = (raw_bit_count + 7) / 8;
for (uint16_t i = 0; i < num_bytes; i++) {
b[i] = ~b[i];
}
}
// Extract fields
// uint32_t encrypted = ((uint32_t)reverse8(b[3]) << 24) | ((uint32_t)reverse8(b[2]) << 16) | ((uint32_t)reverse8(b[1]) << 8) | (uint32_t)reverse8(b[0]);
uint32_t serial = ((uint32_t)reverse8(b[7] & 0xF0) << 24) | ((uint32_t)reverse8(b[6]) << 16) | ((uint32_t)reverse8(b[5]) << 8) | (uint32_t)reverse8(b[4]);
uint8_t btn = (reverse8(b[7]) & 0xF0) >> 4;
decode_data = serial;
decode_count_bit = 64;
decode_data2 = btn;
data_count_bit = decode_count_bit;
if (callback)
callback(this);
// uint8_t our_serial_lsb = serial & 0xFF;
// Decrypt --skipped, no keeloq decoding
/* uint32_t decrypted = keeloq_common_decrypt(encrypted, kia_mf_key);
uint8_t dec_btn = (decrypted >> 28) & 0x0F;
uint8_t dec_serial_lsb = (decrypted >> 16) & 0xFF;
// Validate
if (dec_btn != btn || dec_serial_lsb != our_serial_lsb) {
return false;
}
// Valid decode - version determined by sync type
instance->encrypted = encrypted;
instance->decrypted = decrypted;
instance->generic.serial = serial;
instance->generic.btn = btn;
instance->generic.cnt = decrypted & 0xFFFF;
instance->version = is_v3_sync ? 1 : 0;
uint64_t key_data = ((uint64_t)b[0] << 56) | ((uint64_t)b[1] << 48) | ((uint64_t)b[2] << 40) |
((uint64_t)b[3] << 32) | ((uint64_t)b[4] << 24) | ((uint64_t)b[5] << 16) |
((uint64_t)b[6] << 8) | (uint64_t)b[7];
instance->generic.data = key_data;
instance->generic.data_count_bit = 64;
*/
return true;
}
void feed(bool level, uint32_t duration) {
switch (parser_step) {
case KiaV3V4DecoderStepReset:
if (level && DURATION_DIFF(duration, te_short) <
te_delta) {
parser_step = KiaV3V4DecoderStepCheckPreamble;
te_last = duration;
header_count = 1;
}
break;
case KiaV3V4DecoderStepCheckPreamble:
if (level) {
if (DURATION_DIFF(duration, te_short) <
te_delta) {
te_last = duration;
} else if (duration > 1000 && duration < 1500) {
// V4 style: Sync is LONG HIGH
if (header_count >= 8) {
parser_step = KiaV3V4DecoderStepCollectRawBits;
raw_bit_count = 0;
is_v3_sync = false;
memset(raw_bits, 0, sizeof(raw_bits));
} else {
parser_step = KiaV3V4DecoderStepReset;
}
} else {
parser_step = KiaV3V4DecoderStepReset;
}
} else {
if (duration > 1000 && duration < 1500) {
// V3 style: Sync is LONG LOW
if (header_count >= 8) {
parser_step = KiaV3V4DecoderStepCollectRawBits;
raw_bit_count = 0;
is_v3_sync = true;
memset(raw_bits, 0, sizeof(raw_bits));
} else {
parser_step = KiaV3V4DecoderStepReset;
}
} else if (
DURATION_DIFF(duration, te_short) <
te_delta &&
DURATION_DIFF(te_last, te_short) <
te_delta) {
header_count++;
} else if (duration > 1500) {
parser_step = KiaV3V4DecoderStepReset;
}
}
break;
case KiaV3V4DecoderStepCollectRawBits:
if (level) {
if (duration > 1000 && duration < 1500) {
// Next sync pulse (V4 style) - end this packet
kia_v3_v4_process_buffer();
parser_step = KiaV3V4DecoderStepReset;
} else if (
DURATION_DIFF(duration, te_short) <
te_delta) {
kia_v3_v4_add_raw_bit(false);
} else if (
DURATION_DIFF(duration, te_long) <
te_delta) {
kia_v3_v4_add_raw_bit(true);
} else {
parser_step = KiaV3V4DecoderStepReset;
}
} else {
if (duration > 1000 && duration < 1500) {
// Next sync pulse (V3 style) - end this packet
kia_v3_v4_process_buffer();
parser_step = KiaV3V4DecoderStepReset;
} else if (duration > 1500) {
// Long gap - end of transmission
kia_v3_v4_process_buffer();
parser_step = KiaV3V4DecoderStepReset;
}
}
break;
}
}
bool is_v3_sync = false; // true = V3 (long LOW sync), false = V4 (long HIGH sync)
uint8_t version = 0; // 0 = V4, 1 = V3
uint8_t raw_bits[32]{0};
uint16_t raw_bit_count = 0;
uint16_t header_count = 0;
// uint32_t encrypted;
// uint32_t decrypted;
};
+176
View File
@@ -0,0 +1,176 @@
#pragma once
#include "subcarbase.hpp"
#include <cstring>
typedef enum {
KiaV5DecoderStepReset = 0,
KiaV5DecoderStepCheckPreamble,
KiaV5DecoderStepCollectRawBits,
} KiaV5DecoderStep;
class FProtoSubCarKiaV5 : public FProtoSubCarBase {
public:
FProtoSubCarKiaV5() {
sensorType = FPC_KIAV5;
te_short = 400;
te_long = 800;
te_delta = 150;
min_count_bit_for_found = 64;
}
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 feed(bool level, uint32_t duration) {
switch (parser_step) {
case KiaV5DecoderStepReset:
if ((level) && (DURATION_DIFF(duration, te_short) <
te_delta)) {
parser_step = KiaV5DecoderStepCheckPreamble;
te_last = duration;
header_count = 1;
}
break;
case KiaV5DecoderStepCheckPreamble:
if (level) {
if ((DURATION_DIFF(duration, te_short) <
te_delta) ||
(DURATION_DIFF(duration, te_long) <
te_delta)) {
te_last = duration;
} else {
parser_step = KiaV5DecoderStepReset;
}
} else {
if ((DURATION_DIFF(duration, te_short) <
te_delta) &&
(DURATION_DIFF(te_last, te_short) <
te_delta)) {
header_count++;
} else if (
(DURATION_DIFF(duration, te_long) <
te_delta) &&
(DURATION_DIFF(te_last, te_short) <
te_delta)) {
if (header_count > 40) {
parser_step = KiaV5DecoderStepCollectRawBits;
raw_bit_count = 0;
memset(raw_bits, 0, sizeof(raw_bits));
} else {
header_count++;
}
} else if (
DURATION_DIFF(te_last, te_long) <
te_delta) {
header_count++;
} else {
parser_step = KiaV5DecoderStepReset;
}
}
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);
if (callback)
callback(this);
}
parser_step = KiaV5DecoderStepReset;
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);
}
break;
}
}
uint8_t raw_bits[32]{};
uint16_t raw_bit_count = 0;
uint16_t header_count = 0;
};
+172
View File
@@ -0,0 +1,172 @@
#pragma once
#include "subcarbase.hpp"
#include <cstring>
typedef enum {
SubaruDecoderStepReset = 0,
SubaruDecoderStepCheckPreamble,
SubaruDecoderStepFoundGap,
SubaruDecoderStepFoundSync,
SubaruDecoderStepSaveDuration,
SubaruDecoderStepCheckDuration,
} SubaruDecoderStep;
class FProtoSubCarSubaru : public FProtoSubCarBase {
public:
FProtoSubCarSubaru() {
sensorType = FPC_SUBARU;
te_short = 800;
te_long = 1600;
te_delta = 260;
min_count_bit_for_found = 64;
}
void subghz_protocol_decoder_subaru_reset() {
parser_step = SubaruDecoderStepReset;
te_last = 0;
header_count = 0;
bit_count = 0;
memset(data, 0, sizeof(data));
}
void subaru_add_bit(bool bit) {
if (bit_count < 64) {
uint8_t byte_idx = bit_count / 8;
uint8_t bit_idx = 7 - (bit_count % 8);
if (bit) {
data[byte_idx] |= (1 << bit_idx);
} else {
data[byte_idx] &= ~(1 << bit_idx);
}
bit_count++;
}
}
bool subaru_process_data() {
if (bit_count < 64) {
return false;
}
uint8_t* b = data;
uint64_t key = ((uint64_t)b[0] << 56) | ((uint64_t)b[1] << 48) |
((uint64_t)b[2] << 40) | ((uint64_t)b[3] << 32) |
((uint64_t)b[4] << 24) | ((uint64_t)b[5] << 16) |
((uint64_t)b[6] << 8) | ((uint64_t)b[7]);
decode_data = key;
// uint32_t serial = ((uint32_t)b[1] << 16) | ((uint32_t)b[2] << 8) | b[3];
// uint8_t button = b[0] & 0x0F;
// uint16_t cnt;
// subaru_decode_count(b, &cnt);
data_count_bit = bit_count;
if (callback) {
callback(this);
}
return true;
}
void feed(bool level, uint32_t duration) {
switch (parser_step) {
case SubaruDecoderStepReset:
if (level && DURATION_DIFF(duration, te_long) < te_delta) {
parser_step = SubaruDecoderStepCheckPreamble;
te_last = duration;
header_count = 1;
}
break;
case SubaruDecoderStepCheckPreamble:
if (!level) {
if (DURATION_DIFF(duration, te_long) < te_delta) {
header_count++;
} else if (duration > 2000 && duration < 3500) {
if (header_count > 20) {
parser_step = SubaruDecoderStepFoundGap;
} else {
parser_step = SubaruDecoderStepReset;
}
} else {
parser_step = SubaruDecoderStepReset;
}
} else {
if (DURATION_DIFF(duration, te_long) < te_delta) {
te_last = duration;
header_count++;
} else {
parser_step = SubaruDecoderStepReset;
}
}
break;
case SubaruDecoderStepFoundGap:
if (level && duration > 2000 && duration < 3500) {
parser_step = SubaruDecoderStepFoundSync;
} else {
parser_step = SubaruDecoderStepReset;
}
break;
case SubaruDecoderStepFoundSync:
if (!level && DURATION_DIFF(duration, te_long) < te_delta) {
parser_step = SubaruDecoderStepSaveDuration;
bit_count = 0;
memset(data, 0, sizeof(data));
} else {
parser_step = SubaruDecoderStepReset;
}
break;
case SubaruDecoderStepSaveDuration:
if (level) {
// HIGH pulse duration encodes the bit:
// Short HIGH (~800µs) = 1
// Long HIGH (~1600µs) = 0
if (DURATION_DIFF(duration, te_short) < te_delta) {
// Short HIGH = bit 1
subaru_add_bit(true);
te_last = duration;
parser_step = SubaruDecoderStepCheckDuration;
} else if (DURATION_DIFF(duration, te_long) < te_delta) {
// Long HIGH = bit 0
subaru_add_bit(false);
te_last = duration;
parser_step = SubaruDecoderStepCheckDuration;
} else if (duration > 3000) {
// End of transmission
if (bit_count >= 64) {
subaru_process_data();
}
parser_step = SubaruDecoderStepReset;
} else {
parser_step = SubaruDecoderStepReset;
}
} else {
parser_step = SubaruDecoderStepReset;
}
break;
case SubaruDecoderStepCheckDuration:
if (!level) {
// LOW pulse - just validates timing, doesn't encode bit
if (DURATION_DIFF(duration, te_short) < te_delta ||
DURATION_DIFF(duration, te_long) < te_delta) {
parser_step = SubaruDecoderStepSaveDuration;
} else if (duration > 3000) {
// Gap - end of packet
if (bit_count >= 64) {
subaru_process_data();
}
parser_step = SubaruDecoderStepReset;
} else {
parser_step = SubaruDecoderStepReset;
}
} else {
parser_step = SubaruDecoderStepReset;
}
break;
}
}
uint16_t header_count = 0;
uint8_t data[8];
uint8_t bit_count = 0;
};
+120
View File
@@ -0,0 +1,120 @@
#pragma once
#include "subcarbase.hpp"
#define SUZUKI_GAP_TIME 2000
#define SUZUKI_GAP_DELTA 400
typedef enum {
SuzukiDecoderStepReset = 0,
SuzukiDecoderStepFoundStartPulse,
SuzukiDecoderStepSaveDuration,
} SuzukiDecoderStep;
class FProtoSubCarSuzuki : public FProtoSubCarBase {
public:
FProtoSubCarSuzuki() {
sensorType = FPC_SUZUKI;
te_short = 250;
te_long = 500;
te_delta = 110;
min_count_bit_for_found = 64;
}
void suzuki_add_bit(uint32_t bit) {
uint32_t carry = data_low >> 31;
data_low = (data_low << 1) | bit;
data_high = (data_high << 1) | carry;
data_count_bit++;
}
void subghz_protocol_decoder_suzuki_reset() {
parser_step = SuzukiDecoderStepReset;
header_count = 0;
data_count_bit = 0;
data_low = 0;
data_high = 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)
return;
if (DURATION_DIFF(duration, te_short) > te_delta) {
return;
}
data_low = 0;
data_high = 0;
parser_step = SuzukiDecoderStepFoundStartPulse;
header_count = 0;
data_count_bit = 0;
break;
case SuzukiDecoderStepFoundStartPulse:
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);
}
// 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) {
te_last = duration;
header_count++;
} else {
parser_step = SuzukiDecoderStepReset;
}
}
break;
case SuzukiDecoderStepSaveDuration:
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);
} else {
parser_step = SuzukiDecoderStepReset;
}
// 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) {
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);
}
}
}
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;
};
+236
View File
@@ -0,0 +1,236 @@
#pragma once
#include "subcarbase.hpp"
typedef enum {
VwDecoderStepReset = 0,
VwDecoderStepFoundSync,
VwDecoderStepFoundStart1,
VwDecoderStepFoundStart2,
VwDecoderStepFoundStart3,
VwDecoderStepFoundData,
} VwDecoderStep;
class FProtoSubCarVW : public FProtoSubCarBase {
public:
FProtoSubCarVW() {
sensorType = FPC_VW;
te_short = 500;
te_long = 1000;
te_delta = 130;
min_count_bit_for_found = 80;
}
uint8_t vw_get_bit_index(uint8_t bit) {
uint8_t bit_index = 0;
if (bit < 72 && bit >= 8) {
// use generic.data (bytes 1-8)
bit_index = bit - 8;
} else {
// use data_2
if (bit >= 72) {
bit_index = bit - 64; // byte 0 = type
}
if (bit < 8) {
bit_index = bit; // byte 9 = check digit
}
bit_index |= 0x80; // mark for data_2
}
return bit_index;
}
void vw_add_bit(bool level) {
if (data_count_bit >= min_count_bit_for_found) {
return;
}
uint8_t bit_index_full = min_count_bit_for_found - 1 - data_count_bit;
uint8_t bit_index_masked = vw_get_bit_index(bit_index_full);
uint8_t bit_index = bit_index_masked & 0x7F;
if (bit_index_masked & 0x80) {
// use data_2
if (level) {
decode_data2 |= (1ULL << bit_index);
} else {
decode_data2 &= ~(1ULL << bit_index);
}
} else {
// use data
if (level) {
decode_data |= (1ULL << bit_index);
} else {
decode_data &= ~(1ULL << bit_index);
}
}
data_count_bit++;
if (data_count_bit >= min_count_bit_for_found) {
if (callback) {
callback(this);
}
}
}
void subghz_protocol_decoder_vw_reset() {
parser_step = VwDecoderStepReset;
data_count_bit = 0;
decode_data = 0;
decode_data2 = 0;
manchester_state = ManchesterStateMid1;
}
bool vw_manchester_advance(
ManchesterState state,
ManchesterEvent event,
ManchesterState* next_state,
bool* data) {
bool result = false;
ManchesterState new_state = ManchesterStateMid1;
if (event == ManchesterEventReset) {
new_state = ManchesterStateMid1;
} else if (state == ManchesterStateMid0 || state == ManchesterStateMid1) {
if (event == ManchesterEventShortHigh) {
new_state = ManchesterStateStart1;
} else if (event == ManchesterEventShortLow) {
new_state = ManchesterStateStart0;
} else {
new_state = ManchesterStateMid1;
}
} else if (state == ManchesterStateStart1) {
if (event == ManchesterEventShortLow) {
new_state = ManchesterStateMid1;
result = true;
if (data)
*data = true;
} else if (event == ManchesterEventLongLow) {
new_state = ManchesterStateStart0;
result = true;
if (data)
*data = true;
} else {
new_state = ManchesterStateMid1;
}
} else if (state == ManchesterStateStart0) {
if (event == ManchesterEventShortHigh) {
new_state = ManchesterStateMid0;
result = true;
if (data)
*data = false;
} else if (event == ManchesterEventLongHigh) {
new_state = ManchesterStateStart1;
result = true;
if (data)
*data = false;
} else {
new_state = ManchesterStateMid1;
}
}
*next_state = new_state;
return result;
}
void feed(bool level, uint32_t duration) {
uint32_t te_med = (te_long + te_short) / 2;
uint32_t te_end = te_long * 5;
ManchesterEvent event = ManchesterEventReset;
switch (parser_step) {
case VwDecoderStepReset:
if (DURATION_DIFF(duration, te_short) < te_delta) {
parser_step = VwDecoderStepFoundSync;
}
break;
case VwDecoderStepFoundSync:
if (DURATION_DIFF(duration, te_short) < te_delta) {
// Stay - sync pattern repeats ~43 times
break;
}
if (level && DURATION_DIFF(duration, te_long) < te_delta) {
parser_step = VwDecoderStepFoundStart1;
break;
}
parser_step = VwDecoderStepReset;
break;
case VwDecoderStepFoundStart1:
if (!level && DURATION_DIFF(duration, te_short) < te_delta) {
parser_step = VwDecoderStepFoundStart2;
break;
}
parser_step = VwDecoderStepReset;
break;
case VwDecoderStepFoundStart2:
if (level && DURATION_DIFF(duration, te_med) < te_delta) {
parser_step = VwDecoderStepFoundStart3;
break;
}
parser_step = VwDecoderStepReset;
break;
case VwDecoderStepFoundStart3:
if (DURATION_DIFF(duration, te_med) < te_delta) {
// Stay - med pattern repeats
break;
}
if (level && DURATION_DIFF(duration, te_short) < te_delta) {
// Start data collection
vw_manchester_advance(
manchester_state,
ManchesterEventReset,
&manchester_state,
NULL);
vw_manchester_advance(
manchester_state,
ManchesterEventShortHigh,
&manchester_state,
NULL);
data_count_bit = 0;
decode_data = 0;
decode_data2 = 0;
parser_step = VwDecoderStepFoundData;
break;
}
parser_step = VwDecoderStepReset;
break;
case VwDecoderStepFoundData:
if (DURATION_DIFF(duration, te_short) < te_delta) {
event = level ? ManchesterEventShortHigh : ManchesterEventShortLow;
}
if (DURATION_DIFF(duration, te_long) < te_delta) {
event = level ? ManchesterEventLongHigh : ManchesterEventLongLow;
}
// Last bit can be arbitrarily long
if (data_count_bit == min_count_bit_for_found - 1 &&
!level && duration > te_end) {
event = ManchesterEventShortLow;
}
if (event == ManchesterEventReset) {
subghz_protocol_decoder_vw_reset();
} else {
bool new_level;
if (vw_manchester_advance(
manchester_state,
event,
&manchester_state,
&new_level)) {
vw_add_bit(new_level);
}
}
break;
}
}
ManchesterState manchester_state = ManchesterStateMid1;
};
+54
View File
@@ -0,0 +1,54 @@
/*
Base class for all weather protocols.
This and most of the weather protocols uses code from Flipper XTreme codebase ( https://github.com/Flipper-XFW/Xtreme-Firmware/tree/dev/lib/subghz ). Thanks for their work!
For comments in a protocol implementation check w-nexus-th.hpp
*/
#ifndef __FPROTO_SCARBASE_H__
#define __FPROTO_SCARBASE_H__
#include "fprotogeneral.hpp"
#include "subcartypes.hpp"
#include <string>
// default values to indicate 'no value'
class FProtoSubCarBase;
typedef void (*SubCarProtocolDecoderBaseRxCallback)(FProtoSubCarBase* instance);
class FProtoSubCarBase {
public:
FProtoSubCarBase() {}
virtual ~FProtoSubCarBase() {}
virtual void feed(bool level, uint32_t duration) = 0; // need to be implemented on each protocol handler.
void setCallback(SubCarProtocolDecoderBaseRxCallback cb) { callback = cb; } // this is called when there is a hit.
// General data holder, these will be passed
uint8_t sensorType = FPC_Invalid;
uint16_t data_count_bit = 0;
uint64_t decode_data = 0;
uint64_t decode_data2 = 0;
protected:
// Helper functions to keep it as compatible with flipper as we can, so adding new protos will be easy.
void subghz_protocol_blocks_add_bit(uint8_t bit) {
decode_data = decode_data << 1 | bit;
decode_count_bit++;
}
// inner logic stuff, also for flipper compatibility.
uint32_t te_short = UINT32_MAX;
uint32_t te_long = UINT32_MAX;
uint32_t te_delta = UINT32_MAX;
uint32_t min_count_bit_for_found = UINT32_MAX;
SubCarProtocolDecoderBaseRxCallback callback = NULL;
uint8_t parser_step = 0;
uint32_t te_last = 0;
uint32_t decode_count_bit = 0;
//
};
#endif
@@ -0,0 +1,75 @@
/*
This is the protocol list handler. It holds an instance of all known protocols.
So include here the .hpp, and add a new element to the protos vector in the constructor. That's all you need to do here if you wanna add a new proto.
@htotoo
*/
#include <vector>
#include <memory>
#include "portapack_shared_memory.hpp"
#include "fprotolistgeneral.hpp"
#include "subcarbase.hpp"
#include "c-suzuki.hpp"
#include "c-vw.hpp"
#include "c-subaru.hpp"
#include "c-kia_v5.hpp"
#include "c-kia_v3v4.hpp"
#include "c-kia_v2.hpp"
#include "c-kia_v1.hpp"
#include "c-kia_v0.hpp"
#include "c-ford_v0.hpp"
#include "c-fiat_v0.hpp"
#include "c-bmw_v0.hpp"
#ifndef __FPROTO_PROTOLISTCAR_H__
#define __FPROTO_PROTOLISTCAR_H__
class SubCarProtos : public FProtoListGeneral {
public:
SubCarProtos(const SubCarProtos&) { SubCarProtos(); }; // won't use, but makes compiler happy
SubCarProtos& operator=(const SubCarProtos&) { return *this; } // won't use, but makes compiler happy
SubCarProtos() {
// add protos
protos[FPC_SUZUKI] = new FProtoSubCarSuzuki();
protos[FPC_VW] = new FProtoSubCarVW();
protos[FPC_SUBARU] = new FProtoSubCarSubaru();
protos[FPC_KIAV5] = new FProtoSubCarKiaV5();
protos[FPC_KIAV3V4] = new FProtoSubCarKiaV3V4();
protos[FPC_KIAV2] = new FProtoSubCarKiaV2();
protos[FPC_KIAV1] = new FProtoSubCarKiaV1();
protos[FPC_KIAV0] = new FProtoSubCarKiaV0();
protos[FPC_FORDV0] = new FProtoSubCarFordV0();
protos[FPC_FIATV0] = new FProtoSubCarFiatV0();
protos[FPC_BMWV0] = new FProtoSubCarBMWV0();
for (uint8_t i = 0; i < FPC_COUNT; ++i) {
if (protos[i] != NULL) protos[i]->setCallback(callbackTarget);
}
}
~SubCarProtos() { // not needed for current operation logic, but a bit more elegant :)
for (uint8_t i = 0; i < FPC_COUNT; ++i) {
if (protos[i] != NULL) {
free(protos[i]);
protos[i] = NULL;
}
}
};
static void callbackTarget(FProtoSubCarBase* instance) {
SubCarDataMessage packet_message{instance->sensorType, instance->data_count_bit, instance->decode_data, instance->decode_data2};
shared_memory.application_queue.push(packet_message);
}
void feed(bool level, uint32_t duration) {
for (uint8_t i = 0; i < FPC_COUNT; ++i) {
if (protos[i] != NULL) protos[i]->feed(level, duration);
}
}
protected:
FProtoSubCarBase* protos[FPC_COUNT] = {NULL};
};
#endif
+30
View File
@@ -0,0 +1,30 @@
#ifndef __FPROTO_SUBCARTYPES_H__
#define __FPROTO_SUBCARTYPES_H__
/*
Define known protocols.
These values must be present on the protocol's constructor, like FProtoWeatherAcurite592TXR() { sensorType = FPS_ANSONIC; }
Also it must have a switch-case element in the getSubGhzDSensorTypeName() function, to display it's name.
*/
#define FPM_AM 0
#define FPM_FM 1
enum FPROTO_SUBCAR_SENSOR : uint8_t {
FPC_Invalid = 0,
FPC_SUZUKI = 1,
FPC_VW = 2,
FPC_SUBARU = 3,
FPC_KIAV5 = 4,
FPC_KIAV3V4 = 5,
FPC_KIAV2 = 6,
FPC_KIAV1 = 7,
FPC_KIAV0 = 8,
FPC_FORDV0 = 9,
FPC_FIATV0 = 10,
FPC_BMWV0 = 11,
FPC_COUNT
};
#endif
+1 -1
View File
@@ -76,7 +76,7 @@ static inline int16_t q15_mul(const int16_t j, const int16_t k) {
return intermediate >> 15;
#elif 0 // biased rounding
return (intermediate + 0x4000) >> 15;
#else // unbiased rounding
#else // unbiased rounding
return (intermediate + ((intermediate & 0x7FFF) == 0x4000 ? 0 : 0x4000)) >> 15;
#endif
}
+810
View File
@@ -0,0 +1,810 @@
#include "proc_flex.hpp"
#include "event_m4.hpp"
#include "audio_dma.hpp"
#include "pocsag.hpp"
#include "dsp_fir_taps.hpp"
#include "portapack_shared_memory.hpp"
#include <cmath>
#include <cstring>
#include <cstdio> // for snprintf
// Constants from demod_flex.c
#define FREQ_SAMP 24000 // Our sample rate
#define DC_OFFSET_FILTER 0.010
#define PHASE_LOCKED_RATE 0.045
#define PHASE_UNLOCKED_RATE 0.050
#define LOCK_LEN 24
#define IDLE_THRESHOLD 0
#define DEMOD_TIMEOUT 100
#define FLEX_SYNC_MARKER 0xA6C6AAAAul
#define SLICE_THRESHOLD 0.667
// Implement EccContainer here to avoid linking pocsag.cpp which pulls in app headers
using namespace pocsag;
EccContainer::EccContainer() {
setup_ecc();
}
void EccContainer::setup_ecc() {
unsigned int srr = 0x3b4;
unsigned int i, n, j, k;
for (i = 0; i <= 20; i++) {
ecs[i] = srr;
if ((srr & 0x01) != 0)
srr = (srr >> 1) ^ 0x3B4;
else
srr = srr >> 1;
}
for (i = 0; i < 1024; i++) bch[i] = 0;
for (n = 0; n <= 20; n++) {
for (i = 0; i <= 20; i++) {
j = (i << 5) + n;
k = ecs[n] ^ ecs[i];
bch[k] = j + 0x2000;
}
}
for (n = 0; n <= 20; n++) {
k = ecs[n];
j = n + (0x1f << 5);
bch[k] = j + 0x1000;
}
for (n = 0; n <= 20; n++) {
for (i = 0; i < 10; i++) {
k = ecs[n] ^ (1 << i);
j = n + (0x1f << 5);
bch[k] = j + 0x2000;
}
}
for (n = 0; n < 10; n++) {
k = 1 << n;
bch[k] = 0x3ff + 0x1000;
}
for (n = 0; n < 10; n++) {
for (i = 0; i < 10; i++) {
if (i != n) {
k = (1 << n) ^ (1 << i);
bch[k] = 0x3ff + 0x2000;
}
}
}
}
int EccContainer::error_correct(uint32_t& val) {
int i, synd, errl, acc, pari, ecc, b1, b2;
errl = 0;
pari = 0;
ecc = 0;
for (i = 31; i >= 11; --i) {
if (val & (1 << i)) {
ecc = ecc ^ ecs[31 - i];
pari = pari ^ 0x01;
}
}
acc = 0;
for (i = 10; i >= 1; --i) {
acc = acc << 1;
if (val & (1 << i)) {
acc = acc ^ 0x01;
}
}
synd = ecc ^ acc;
errl = 0;
if (synd != 0) {
if (bch[synd] != 0) {
b1 = bch[synd] & 0x1f;
b2 = bch[synd] >> 5;
b2 = b2 & 0x1f;
if (b2 != 0x1f) {
val ^= 0x01 << (31 - b2);
ecc = ecc ^ ecs[b2];
}
if (b1 != 0x1f) {
val ^= 0x01 << (31 - b1);
ecc = ecc ^ ecs[b1];
}
errl = bch[synd] >> 12;
} else {
errl = 3;
}
if (errl == 1) pari = pari ^ 0x01;
}
if (errl == 4) errl = 3;
return errl;
}
namespace {
// Helpers
unsigned int popcount(unsigned int n) {
// Simple popcount for 32-bit integer
n = n - ((n >> 1) & 0x55555555);
n = (n & 0x33333333) + ((n >> 2) & 0x33333333);
return (((n + (n >> 4)) & 0x0F0F0F0F) * 0x01010101) >> 24;
}
uint32_t bit_reverse_32(uint32_t x) {
x = ((x >> 1) & 0x55555555) | ((x & 0x55555555) << 1);
x = ((x >> 2) & 0x33333333) | ((x & 0x33333333) << 2);
x = ((x >> 4) & 0x0F0F0F0F) | ((x & 0x0F0F0F0F) << 4);
x = ((x >> 8) & 0x00FF00FF) | ((x & 0x00FF00FF) << 8);
x = (x >> 16) | (x << 16);
return x;
}
} // namespace
void FlexProcessor::send_debug(const char* text, uint32_t v1, uint32_t v2) {
if (shared_memory.application_queue.is_empty()) return;
FlexDebugMessage message(v1, v2, text);
shared_memory.application_queue.push(message);
}
void FlexProcessor::execute(const buffer_c8_t& buffer) {
if (!configured) return;
// Heartbeat debug every ~1 second (24000Hz / 4096 buffer size * ~6)
static int debug_count = 0;
debug_count++;
if (debug_count > 1000) {
send_debug("Running", 0, 0);
debug_count = 0;
}
// Decimate and demodulate: 3.072MHz -> 24kHz
auto decim_0_out = decim_0_iq.execute(buffer, dst_buffer);
auto decim_1_out = decim_1_iq.execute(decim_0_out, dst_buffer);
auto channel_out = channel_filter.execute(decim_1_out, dst_buffer);
auto audio = demod.execute(channel_out, audio_buffer);
process_audio(audio);
}
void FlexProcessor::process_audio(const buffer_f32_t& audio) {
for (size_t i = 0; i < audio.count; ++i) {
flex_demodulate(audio.p[i]);
}
}
void FlexProcessor::flex_demodulate(double sample) {
if (build_symbol(sample) == 1) {
demodulator.nonconsec = 0;
demodulator.symbol_count++;
// modulation.symbol_rate = ... // Unused in main logic usually, just stats
/*Determine the modal symbol*/
int j;
int decmax = 0;
int modal_symbol = 0;
for (j = 0; j < 4; j++) {
if (demodulator.symcount[j] > decmax) {
modal_symbol = j;
decmax = demodulator.symcount[j];
}
}
demodulator.symcount[0] = 0;
demodulator.symcount[1] = 0;
demodulator.symcount[2] = 0;
demodulator.symcount[3] = 0;
if (demodulator.locked) {
/*Process the symbol*/
flex_sym(modal_symbol);
} else {
/*Check for lock pattern*/
/*Shift symbols into buffer, symbols are converted so that the max and min symbols map to 1 and 2 i.e each contain a single 1 */
demodulator.lock_buf = (demodulator.lock_buf << 2) | (modal_symbol ^ 0x1);
uint64_t lock_pattern = demodulator.lock_buf ^ 0x6666666666666666ull;
uint64_t lock_mask = (1ull << (2 * LOCK_LEN)) - 1;
if ((lock_pattern & lock_mask) == 0 || ((~lock_pattern) & lock_mask) == 0) {
demodulator.locked = 1;
demodulator.lock_buf = 0;
demodulator.symbol_count = 0;
demodulator.sample_count = 0;
}
}
/*Time out after X periods with no zero crossing*/
demodulator.timeout++;
if (demodulator.timeout > DEMOD_TIMEOUT) {
demodulator.locked = 0;
}
}
}
int FlexProcessor::build_symbol(double sample) {
const int64_t phase_max = 100 * demodulator.sample_freq;
const int64_t phase_rate = phase_max * demodulator.baud / demodulator.sample_freq;
const double phasepercent = 100.0 * demodulator.phase / phase_max;
demodulator.sample_count++;
/*Remove DC offset (FIR filter)*/
if (state.Current == flex::State::SYNC1) {
modulation.zero = (modulation.zero * (FREQ_SAMP * DC_OFFSET_FILTER) + sample) / ((FREQ_SAMP * DC_OFFSET_FILTER) + 1);
}
sample -= modulation.zero;
if (demodulator.locked) {
if (state.Current == flex::State::SYNC1) {
demodulator.envelope_sum += std::abs(sample);
demodulator.envelope_count++;
modulation.envelope = demodulator.envelope_sum / demodulator.envelope_count;
}
} else {
modulation.envelope = 0;
demodulator.envelope_sum = 0;
demodulator.envelope_count = 0;
demodulator.baud = 1600;
demodulator.timeout = 0;
demodulator.nonconsec = 0;
state.Current = flex::State::SYNC1;
}
/* MID 80% SYMBOL PERIOD */
if (phasepercent > 10 && phasepercent < 90) {
if (sample > 0) {
if (sample > modulation.envelope * SLICE_THRESHOLD)
demodulator.symcount[3]++;
else
demodulator.symcount[2]++;
} else {
if (sample < -modulation.envelope * SLICE_THRESHOLD)
demodulator.symcount[0]++;
else
demodulator.symcount[1]++;
}
}
/* ZERO CROSSING */
if ((demodulator.sample_last < 0 && sample >= 0) || (demodulator.sample_last >= 0 && sample < 0)) {
double phase_error = 0.0;
if (phasepercent < 50) {
phase_error = demodulator.phase;
} else {
phase_error = demodulator.phase - phase_max;
}
if (demodulator.locked) {
demodulator.phase -= phase_error * PHASE_LOCKED_RATE;
} else {
demodulator.phase -= phase_error * PHASE_UNLOCKED_RATE;
}
if (phasepercent > 10 && phasepercent < 90) {
demodulator.nonconsec++;
if (demodulator.nonconsec > 20 && demodulator.locked) {
demodulator.locked = 0;
}
} else {
demodulator.nonconsec = 0;
}
demodulator.timeout = 0;
}
demodulator.sample_last = sample;
/* END OF SYMBOL PERIOD */
demodulator.phase += phase_rate;
if (demodulator.phase > phase_max) {
demodulator.phase -= phase_max;
return 1;
} else {
return 0;
}
}
unsigned int FlexProcessor::flex_sync(unsigned char sym) {
int retval = 0;
sync.syncbuf = (sync.syncbuf << 1) | ((sym < 2) ? 1 : 0);
retval = flex_sync_check(sync.syncbuf);
if (retval != 0) {
sync.polarity = 0;
} else {
retval = flex_sync_check(~sync.syncbuf);
if (retval != 0) {
sync.polarity = 1;
}
}
return retval;
}
unsigned int FlexProcessor::flex_sync_check(uint64_t buf) {
// 64-bit FLEX sync code: AAAA:BBBBBBBB:CCCC
unsigned int marker = (buf & 0x0000FFFFFFFF0000ULL) >> 16;
unsigned short codehigh = (buf & 0xFFFF000000000000ULL) >> 48;
unsigned short codelow = ~(buf & 0x000000000000FFFFULL);
int retval = 0;
// Hamming distance check (popcount of XOR)
unsigned int diff_marker = popcount(marker ^ FLEX_SYNC_MARKER);
unsigned int diff_code = popcount(codelow ^ codehigh);
if (diff_marker < 4 && diff_code < 4) {
retval = codehigh;
} else {
retval = 0;
}
return retval;
}
void FlexProcessor::decode_mode(unsigned int sync_code) {
struct FlexModeDef {
int sync;
unsigned int baud;
unsigned int levels;
} flex_modes[] = {
{0x870C, 1600, 2},
{0xB068, 1600, 4},
{0x7B18, 3200, 2},
{0xDEA0, 3200, 4},
{0x4C7C, 3200, 4},
{0, 0, 0}};
for (int i = 0; flex_modes[i].sync != 0; i++) {
unsigned int diff = popcount((unsigned int)flex_modes[i].sync ^ sync_code);
if (diff < 4) {
sync.sync = sync_code;
sync.baud = flex_modes[i].baud;
sync.levels = flex_modes[i].levels;
return;
}
}
// Default
sync.baud = 1600;
sync.levels = 2;
}
void FlexProcessor::read_2fsk(unsigned int sym, uint32_t* dat) {
*dat = (*dat >> 1) | ((sym > 1) ? 0x80000000 : 0);
}
int FlexProcessor::bch_fix_errors(uint32_t* data_to_fix) {
// Reverse bits for EccContainer (POCSAG MSB-first expectation vs FLEX LSB-first in our representation)
uint32_t reversed = bit_reverse_32(*data_to_fix);
int result = ecc.error_correct(reversed);
if (result == 0 || result == 1 || result == 2) {
*data_to_fix = bit_reverse_32(reversed);
}
return result;
}
int FlexProcessor::decode_fiw() {
uint32_t fiw_val = fiw.rawdata;
int decode_error = bch_fix_errors(&fiw_val);
if (decode_error > 2) {
return 1;
}
fiw.checksum = fiw_val & 0xF;
fiw.cycleno = (fiw_val >> 4) & 0xF;
fiw.frameno = (fiw_val >> 8) & 0x7F;
fiw.fix3 = (fiw_val >> 15) & 0x3F;
unsigned int checksum = (fiw_val & 0xF);
checksum += ((fiw_val >> 4) & 0xF);
checksum += ((fiw_val >> 8) & 0xF);
checksum += ((fiw_val >> 12) & 0xF);
checksum += ((fiw_val >> 16) & 0xF);
checksum += ((fiw_val >> 20) & 0x01);
checksum &= 0xF;
if (checksum == 0xF) {
return 0;
} else {
return 1;
}
}
int FlexProcessor::read_data(unsigned char sym) {
int bit_a = (sym > 1);
int bit_b = 0;
if (sync.levels == 4) {
bit_b = (sym == 1) || (sym == 2);
}
if (sync.baud == 1600) {
data.phase_toggle = 0;
}
unsigned int idx = ((data.data_bit_counter >> 5) & 0xFFF8) | (data.data_bit_counter & 0x0007);
if (idx >= 88) return 0; // Boundary check
if (data.phase_toggle == 0) {
data.PhaseA.buf[idx] = (data.PhaseA.buf[idx] >> 1) | (bit_a ? 0x80000000 : 0);
data.PhaseB.buf[idx] = (data.PhaseB.buf[idx] >> 1) | (bit_b ? 0x80000000 : 0);
data.phase_toggle = 1;
if ((data.data_bit_counter & 0xFF) == 0xFF) {
if (data.PhaseA.buf[idx] == 0x00000000 || data.PhaseA.buf[idx] == 0xffffffff) data.PhaseA.idle_count++;
if (data.PhaseB.buf[idx] == 0x00000000 || data.PhaseB.buf[idx] == 0xffffffff) data.PhaseB.idle_count++;
}
} else {
data.PhaseC.buf[idx] = (data.PhaseC.buf[idx] >> 1) | (bit_a ? 0x80000000 : 0);
data.PhaseD.buf[idx] = (data.PhaseD.buf[idx] >> 1) | (bit_b ? 0x80000000 : 0);
data.phase_toggle = 0;
if ((data.data_bit_counter & 0xFF) == 0xFF) {
if (data.PhaseC.buf[idx] == 0x00000000 || data.PhaseC.buf[idx] == 0xffffffff) data.PhaseC.idle_count++;
if (data.PhaseD.buf[idx] == 0x00000000 || data.PhaseD.buf[idx] == 0xffffffff) data.PhaseD.idle_count++;
}
}
if (sync.baud == 1600 || data.phase_toggle == 0) {
data.data_bit_counter++;
}
int idle = 0;
if (sync.baud == 1600) {
if (sync.levels == 2) {
idle = (data.PhaseA.idle_count > IDLE_THRESHOLD);
} else {
idle = ((data.PhaseA.idle_count > IDLE_THRESHOLD) && (data.PhaseB.idle_count > IDLE_THRESHOLD));
}
} else {
if (sync.levels == 2) {
idle = ((data.PhaseA.idle_count > IDLE_THRESHOLD) && (data.PhaseC.idle_count > IDLE_THRESHOLD));
} else {
idle = ((data.PhaseA.idle_count > IDLE_THRESHOLD) && (data.PhaseB.idle_count > IDLE_THRESHOLD) && (data.PhaseC.idle_count > IDLE_THRESHOLD) && (data.PhaseD.idle_count > IDLE_THRESHOLD));
}
}
return idle;
}
void FlexProcessor::flex_sym(unsigned char sym) {
unsigned char sym_rectified;
if (sync.polarity) {
sym_rectified = 3 - sym;
} else {
sym_rectified = sym;
}
switch (state.Current) {
case flex::State::SYNC1: {
unsigned int sync_code = flex_sync(sym);
if (sync_code != 0) {
decode_mode(sync_code);
if (sync.baud != 0 && sync.levels != 0) {
state.Current = flex::State::FIW;
send_debug("SYNC1 Found", sync.baud, sync_code);
} else {
state.Current = flex::State::SYNC1;
}
} else {
state.Current = flex::State::SYNC1;
}
state.fiwcount = 0;
fiw.rawdata = 0;
break;
}
case flex::State::FIW: {
state.fiwcount++;
if (state.fiwcount >= 16) {
read_2fsk(sym_rectified, &fiw.rawdata);
}
if (state.fiwcount == 48) {
if (decode_fiw() == 0) {
state.sync2_count = 0;
demodulator.baud = sync.baud;
state.Current = flex::State::SYNC2;
send_debug("FIW OK", fiw.frameno, fiw.cycleno);
} else {
state.Current = flex::State::SYNC1;
send_debug("FIW Fail", fiw.rawdata, 0);
}
}
break;
}
case flex::State::SYNC2: {
if (++state.sync2_count == sync.baud * 25 / 1000) {
state.data_count = 0;
// Clear phase data
for (int i = 0; i < 88; i++) {
data.PhaseA.buf[i] = 0;
data.PhaseB.buf[i] = 0;
data.PhaseC.buf[i] = 0;
data.PhaseD.buf[i] = 0;
}
data.PhaseA.idle_count = 0;
data.PhaseB.idle_count = 0;
data.PhaseC.idle_count = 0;
data.PhaseD.idle_count = 0;
data.phase_toggle = 0;
data.data_bit_counter = 0;
state.Current = flex::State::DATA;
}
break;
}
case flex::State::DATA: {
int idle = read_data(sym_rectified);
if (++state.data_count == sync.baud * 1760 / 1000 || idle) {
decode_data();
demodulator.baud = 1600;
state.Current = flex::State::SYNC1;
state.data_count = 0;
}
break;
}
}
}
void FlexProcessor::decode_data() {
if (sync.baud == 1600) {
if (sync.levels == 2) {
decode_phase('A');
} else {
decode_phase('A');
decode_phase('B');
}
} else {
if (sync.levels == 2) {
decode_phase('A');
decode_phase('C');
} else {
decode_phase('A');
decode_phase('B');
decode_phase('C');
decode_phase('D');
}
}
}
void FlexProcessor::decode_phase(char PhaseNo) {
uint32_t* phaseptr = nullptr;
switch (PhaseNo) {
case 'A':
phaseptr = data.PhaseA.buf;
break;
case 'B':
phaseptr = data.PhaseB.buf;
break;
case 'C':
phaseptr = data.PhaseC.buf;
break;
case 'D':
phaseptr = data.PhaseD.buf;
break;
default:
return;
}
for (int i = 0; i < 88; i++) {
int decode_error = bch_fix_errors(&phaseptr[i]);
if (decode_error > 2) return;
phaseptr[i] &= 0x001FFFFF; // Extract message bits
}
uint32_t biw = phaseptr[0];
if (biw == 0 || biw == 0x001FFFFF) return;
int voffset = (biw >> 10) & 0x3f;
int aoffset = ((biw >> 8) & 0x03) + 1;
for (int i = aoffset; i < voffset; i++) {
int j = voffset + i - aoffset;
if (phaseptr[i] == 0x00000000 || phaseptr[i] == 0x001FFFFF) continue;
parse_capcode(phaseptr[i]);
if (decode.long_address) continue; // Skip long addresses for now
if (decode.capcode > 4297068542ll || decode.capcode < 0) continue;
uint32_t viw = phaseptr[j];
int type_val = (viw >> 4) & 0x07;
switch (type_val) {
case 0:
decode.type = flex::PageType::SECURE;
break;
case 1:
decode.type = flex::PageType::SHORT_INSTRUCTION;
break;
case 2:
decode.type = flex::PageType::TONE;
break;
case 3:
decode.type = flex::PageType::STANDARD_NUMERIC;
break;
case 4:
decode.type = flex::PageType::SPECIAL_NUMERIC;
break;
case 5:
decode.type = flex::PageType::ALPHANUMERIC;
break;
case 6:
decode.type = flex::PageType::BINARY;
break;
case 7:
decode.type = flex::PageType::NUMBERED_NUMERIC;
break;
}
int mw1 = (viw >> 7) & 0x7F;
int len = (viw >> 14) & 0x7F;
int mw2 = mw1 + (len - 1);
if (mw1 == 0 && mw2 == 0) continue;
if (decode.type == flex::PageType::TONE) mw1 = mw2 = 0;
if (decode.type == flex::PageType::ALPHANUMERIC || decode.type == flex::PageType::SECURE) {
if (mw1 > 87 || mw2 > 87) continue;
parse_alphanumeric(phaseptr, PhaseNo, mw1, mw2, 0);
} else if (decode.type == flex::PageType::STANDARD_NUMERIC || decode.type == flex::PageType::SPECIAL_NUMERIC || decode.type == flex::PageType::NUMBERED_NUMERIC) {
parse_numeric(phaseptr, PhaseNo, j);
} else if (decode.type == flex::PageType::TONE) {
parse_tone_only(phaseptr, PhaseNo, j);
} else {
// Unknown or unsupported
}
}
}
void FlexProcessor::parse_capcode(uint32_t aw1) {
decode.long_address = (aw1 < 0x008001L) || (aw1 > 0x1E0000L) || (aw1 > 0x1E7FFEL);
decode.capcode = aw1 - 0x8000;
}
void FlexProcessor::parse_alphanumeric(uint32_t* phaseptr, char, int mw1, int mw2, int) {
char message[128] = {0}; // Fixed buffer for message
int currentChar = 0;
// int frag = (phaseptr[mw1] >> 11) & 0x03;
// int cont = (phaseptr[mw1] >> 0x0A) & 0x01;
// Helper logic for fragmentation (ignored for basic display)
mw1++;
for (int i = mw1; i <= mw2; i++) {
unsigned int dw = phaseptr[i];
unsigned char ch;
// Extract chars (7-bit ASCII)
// If i > mw1 (not first word) or fragment check (simplified here)
if (i > mw1) {
ch = dw & 0x7F;
if (ch != 0x03 && currentChar < 127) message[currentChar++] = ch;
}
ch = (dw >> 7) & 0x7F;
if (ch != 0x03 && currentChar < 127) message[currentChar++] = ch;
ch = (dw >> 14) & 0x7F;
if (ch != 0x03 && currentChar < 127) message[currentChar++] = ch;
}
message[currentChar] = '\0';
flex::FlexPacket packet;
packet.bitrate = sync.baud;
packet.capcode = decode.capcode;
packet.function = 0; // TODO extract function if available
packet.type = 5; // ALPHANUMERIC
packet.status = 0; // OK
memcpy(packet.message, message, currentChar + 1);
send_packet(packet);
}
void FlexProcessor::parse_numeric(uint32_t* phaseptr, char, int j) {
// Simplified numeric parsing
char message[128] = {0};
const char flex_bcd[] = "0123456789 U -][";
int w1 = phaseptr[j] >> 7;
int w2 = w1 >> 7;
w1 = w1 & 0x7f;
w2 = (w2 & 0x07) + w1;
int dw;
// Handle short vs long logic if needed (simplified)
dw = phaseptr[w1];
w1++;
w2++;
unsigned char digit = 0;
int count = 4; // Standard numeric skip
if (decode.type == flex::PageType::NUMBERED_NUMERIC)
count += 10;
else
count += 2;
int idx = 0;
for (int i = w1; i <= w2; i++) {
for (int k = 0; k < 21; k++) {
digit = (digit >> 1) & 0x0F;
if (dw & 0x01) digit ^= 0x08;
dw >>= 1;
if (--count == 0) {
if (digit != 0x0C && idx < 127) {
message[idx++] = flex_bcd[digit];
}
count = 4;
}
}
dw = phaseptr[i];
}
message[idx] = '\0';
flex::FlexPacket packet;
packet.bitrate = sync.baud;
packet.capcode = decode.capcode;
packet.function = 0;
packet.type = 3; // NUMERIC
packet.status = 0;
memcpy(packet.message, message, idx + 1);
send_packet(packet);
}
void FlexProcessor::parse_tone_only(uint32_t*, char, int) {
flex::FlexPacket packet;
packet.bitrate = sync.baud;
packet.capcode = decode.capcode;
packet.function = 0;
packet.type = 2; // TONE
packet.status = 0;
snprintf(packet.message, sizeof(packet.message), "Tone Only");
send_packet(packet);
}
void FlexProcessor::parse_unknown(uint32_t*, char, int, int) {
// Ignored
}
void FlexProcessor::on_message(const Message* const message) {
if (message->id == Message::ID::FlexConfigure) {
configure();
}
}
void FlexProcessor::configure() {
decim_0_iq.configure(taps_11k0_decim_0.taps);
decim_1_iq.configure(taps_11k0_decim_1.taps);
channel_filter.configure(taps_11k0_channel.taps, 2); // Decim 2 -> 24kHz output
demod.configure(24000, 4800);
demodulator.sample_freq = 24000;
configured = true;
send_debug("Configured", 0, 0);
}
void FlexProcessor::send_packet(const flex::FlexPacket& packet) {
FlexPacketMessage message(packet);
shared_memory.application_queue.push(message);
}
void FlexProcessor::send_stats() {
// Stats
}
int main() {
EventDispatcher event_dispatcher{std::make_unique<FlexProcessor>()};
event_dispatcher.run();
return 0;
}
+177
View File
@@ -0,0 +1,177 @@
#ifndef __PROC_FLEX_H__
#define __PROC_FLEX_H__
#include "baseband_processor.hpp"
#include "baseband_thread.hpp"
#include "dsp_decimate.hpp"
#include "dsp_demodulate.hpp"
#include "message.hpp"
#include "flex_defs.hpp"
#include "pocsag.hpp" // For EccContainer
#include <cstdint>
#include <array>
namespace flex {
enum class PageType {
SECURE,
SHORT_INSTRUCTION,
TONE,
STANDARD_NUMERIC,
SPECIAL_NUMERIC,
ALPHANUMERIC,
BINARY,
NUMBERED_NUMERIC
};
enum class State {
SYNC1,
FIW,
SYNC2,
DATA
};
struct FlexDemodParams {
unsigned int sample_freq = 24000;
double sample_last = 0.0;
int locked = 0;
int phase = 0;
unsigned int sample_count = 0;
unsigned int symbol_count = 0;
double envelope_sum = 0.0;
int envelope_count = 0;
uint64_t lock_buf = 0;
int symcount[4] = {0};
int timeout = 0;
int nonconsec = 0;
unsigned int baud = 1600;
};
struct FlexGroupHandler {
int64_t GroupCodes[17][100]; // Reduced size from 1000 to save RAM
int GroupCycle[17];
int GroupFrame[17];
};
struct FlexModulation {
double symbol_rate = 0.0;
double envelope = 0.0;
double zero = 0.0;
};
struct FlexStateInfo {
unsigned int sync2_count = 0;
unsigned int data_count = 0;
unsigned int fiwcount = 0;
State Current = State::SYNC1;
State Previous = State::SYNC1;
};
struct FlexSync {
unsigned int sync = 0;
unsigned int baud = 0;
unsigned int levels = 0;
unsigned int polarity = 0;
uint64_t syncbuf = 0;
};
struct FlexFIW {
uint32_t rawdata = 0;
unsigned int checksum = 0;
unsigned int cycleno = 0;
unsigned int frameno = 0;
unsigned int fix3 = 0;
};
struct FlexPhase {
uint32_t buf[88] = {0};
int idle_count = 0;
};
struct FlexData {
int phase_toggle = 0;
unsigned int data_bit_counter = 0;
FlexPhase PhaseA;
FlexPhase PhaseB;
FlexPhase PhaseC;
FlexPhase PhaseD;
};
struct FlexDecode {
PageType type = PageType::ALPHANUMERIC;
int long_address = 0;
int64_t capcode = 0;
};
} // namespace flex
class FlexProcessor : public BasebandProcessor {
public:
void execute(const buffer_c8_t& buffer) override;
void on_message(const Message* const message) override;
private:
bool configured{false};
// DSP components
// 3.072MHz -> 24kHz (Decim 128)
// decim_0: 8, decim_1: 8, channel: 2. Total 128.
dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0_iq{};
dsp::decimate::FIRC16xR16x32Decim8 decim_1_iq{};
dsp::decimate::FIRAndDecimateComplex channel_filter{};
dsp::demodulate::FM demod{};
// Buffers
std::array<complex16_t, 256> dst{};
const buffer_c16_t dst_buffer{dst.data(), dst.size()};
std::array<float, 16> audio{};
const buffer_f32_t audio_buffer{audio.data(), audio.size()};
// Flex State
flex::FlexDemodParams demodulator{};
flex::FlexModulation modulation{};
flex::FlexStateInfo state{};
flex::FlexSync sync{};
flex::FlexFIW fiw{};
flex::FlexData data{};
flex::FlexDecode decode{};
flex::FlexGroupHandler group_handler{};
pocsag::EccContainer ecc{};
// Methods
void configure();
void process_audio(const buffer_f32_t& audio);
// Internal Flex logic
int build_symbol(double sample);
void flex_demodulate(double sample);
void flex_sym(unsigned char sym);
unsigned int flex_sync_check(uint64_t buf);
unsigned int flex_sync(unsigned char sym);
void decode_mode(unsigned int sync_code);
void read_2fsk(unsigned int sym, uint32_t* dat); // Changed to uint32_t*
int decode_fiw();
int read_data(unsigned char sym);
void decode_data();
void decode_phase(char PhaseNo);
int bch_fix_errors(uint32_t* data_to_fix);
// Parsing
void parse_capcode(uint32_t aw1);
void parse_alphanumeric(uint32_t* phaseptr, char PhaseNo, int mw1, int mw2, int flex_groupmessage);
void parse_numeric(uint32_t* phaseptr, char PhaseNo, int j);
void parse_tone_only(uint32_t* phaseptr, char PhaseNo, int j);
void parse_unknown(uint32_t* phaseptr, char PhaseNo, int mw1, int mw2);
void send_packet(const flex::FlexPacket& packet);
void send_stats();
void send_debug(const char* text, uint32_t v1, uint32_t v2);
// Threads
BasebandThread baseband_thread{3072000, this, baseband::Direction::Receive};
};
#endif /*__PROC_FLEX_H__*/
+1 -2
View File
@@ -108,8 +108,7 @@ inline size_t OOKProcessor::duval_algo_step() {
for (unsigned int j = 0; j < w - idx; j++)
v[idx + j] = v[j];
for (idx = w; (idx > 0) && (v[idx - 1] >= duval_symbols - 1); idx--)
;
for (idx = w; (idx > 0) && (v[idx - 1] >= duval_symbols - 1); idx--);
if (idx)
v[idx - 1]++;
+19 -5
View File
@@ -159,13 +159,27 @@ void BitExtractor::configure(uint32_t sample_rate) {
// without needing to know exact transition boundaries.
for (auto& rate : known_rates_)
rate.sample_interval = sample_rate / (2.0 * rate.baud_rate);
if (baud_config_ >= 0 && baud_config_ < static_cast<int8_t>(known_rates_.size())) {
current_rate_ = &known_rates_[baud_config_];
} else {
current_rate_ = nullptr;
}
}
void BitExtractor::reset() {
current_rate_ = nullptr;
for (auto& rate : known_rates_)
rate.reset();
if (baud_config_ >= 0 && baud_config_ < static_cast<int8_t>(known_rates_.size())) {
current_rate_ = &known_rates_[baud_config_];
} else {
current_rate_ = nullptr;
}
}
void BitExtractor::set_baud_config(int8_t baud_config) {
baud_config_ = baud_config;
}
uint16_t BitExtractor::baud_rate() const {
@@ -352,7 +366,7 @@ void POCSAGProcessor::execute(const buffer_c8_t& buffer) {
void POCSAGProcessor::on_message(const Message* const message) {
switch (message->id) {
case Message::ID::POCSAGConfigure:
configure();
configure(reinterpret_cast<const POCSAGConfigureMessage*>(message)->baud_config);
break;
case Message::ID::NBFMConfigure: {
@@ -370,7 +384,7 @@ void POCSAGProcessor::on_message(const Message* const message) {
}
}
void POCSAGProcessor::configure() {
void POCSAGProcessor::configure(int8_t baud_config) {
constexpr size_t decim_0_output_fs = baseband_fs / decim_0.decimation_factor;
constexpr size_t decim_1_output_fs = decim_0_output_fs / decim_1.decimation_factor;
constexpr size_t channel_filter_output_fs = decim_1_output_fs / 2;
@@ -383,7 +397,7 @@ void POCSAGProcessor::configure() {
// Don't process the audio stream.
audio_output.configure(false);
bit_extractor.set_baud_config(baud_config);
bit_extractor.configure(demod_input_fs);
// Set ready to process data.
+3 -2
View File
@@ -84,6 +84,7 @@ class BitExtractor {
void extract_bits(const buffer_f32_t& audio);
void configure(uint32_t sample_rate);
void reset();
void set_baud_config(int8_t baud_config);
uint16_t baud_rate() const;
private:
@@ -117,7 +118,7 @@ class BitExtractor {
RateInfo{2400}};
BitQueue& bits_;
int8_t baud_config_ = -1;
uint32_t sample_rate_ = 0;
RateInfo* current_rate_ = nullptr;
};
@@ -207,7 +208,7 @@ class POCSAGProcessor : public BasebandProcessor {
static constexpr uint32_t stat_update_threshold =
baseband_fs / stat_update_interval;
void configure();
void configure(int8_t baud_config = -1);
void flush();
void reset();
void send_stats() const;
+1 -1
View File
@@ -109,4 +109,4 @@ int main() {
EventDispatcher event_dispatcher{std::make_unique<ProtoViewProcessor>()};
event_dispatcher.run();
return 0;
}
}
+1 -1
View File
@@ -71,4 +71,4 @@ class ProtoViewProcessor : public BasebandProcessor {
RSSIThread rssi_thread{};
};
#endif /*__PROC_PROTOVIEW_H__*/
#endif /*__PROC_PROTOVIEW_H__*/
+719
View File
@@ -0,0 +1,719 @@
/*
* Copyright (C) 2025 StarVore Labs
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#include "proc_sstvrx.hpp"
#include "event_m4.hpp"
#include "portapack_shared_memory.hpp"
#include "audio_dma.hpp"
#include "sine_table_int8.hpp"
#include "fxpt_atan2.hpp"
#include "message.hpp"
#include <cstdint>
#include <cmath>
#include <cstddef>
#include <cstring>
#include <algorithm>
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
namespace {
constexpr size_t sstv_shared_buffer_bytes = sizeof(shared_memory.bb_data.data);
constexpr size_t sstv_chunk_flag_index = sstv_shared_buffer_bytes - 1; // Reserve last byte as ownership flag
constexpr size_t sstv_chunk_header_bytes = 2;
constexpr size_t sstv_chunk_copy_bytes = sstv_shared_buffer_bytes - 1; // Bytes copied to M0 (excludes flag)
constexpr uint16_t sstv_max_chunk_pixels = (sstv_chunk_copy_bytes - sstv_chunk_header_bytes) / 3;
inline volatile uint8_t& chunk_flag() {
return *reinterpret_cast<volatile uint8_t*>(&shared_memory.bb_data.data[sstv_chunk_flag_index]);
}
inline void wait_for_chunk_slot() {
while (chunk_flag() != 0) {
__asm__ volatile("nop");
}
}
inline void mark_chunk_ready() {
chunk_flag() = 1;
}
inline const sstv_mode* find_mode_by_vis_code(const uint8_t vis_code) {
for (const auto& mode : sstv_modes) {
if (mode.vis_code == vis_code) {
return &mode;
}
}
return nullptr;
}
inline std::array<uint8_t, 3> color_order_for_mode(const sstv_mode& mode) {
switch (mode.color_sequence) {
case SSTV_COLOR_RGB:
return {0, 1, 2};
case SSTV_COLOR_GBR:
return {1, 2, 0};
default:
return {0, 1, 2};
}
}
} // namespace
void SSTVRXProcessor::execute(const buffer_c8_t& buffer) {
if (!configured) {
// Just return silently if not configured
return;
}
// Decimation chain (same as NFM)
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 = channel_filter.execute(decim_1_out, dst_buffer);
feed_channel_stats(channel);
// FM demodulation and audio processing
// Demodulator outputs 24kHz audio after channel filter decimation
auto audio = demod.execute(channel, work_audio_buffer);
// Feed audio samples to output and use for frequency estimation
audio_output.write(audio);
// Process each audio sample for SSTV decoding
// audio is buffer_s16_t, so audio.p[i] is int16_t
for (size_t i = 0; i < audio.count; i++) {
// Get int16 audio sample directly (no float conversion needed)
int32_t audio_sample = audio.p[i];
// Increment global sample counter for calibration
global_sample_count++;
// Estimate frequency using Goertzel algorithm on the audio tones
estimate_frequency_goertzel(audio_sample);
// Process based on current state
switch (state) {
case STATE_SYNC_SEARCH:
// Before Line 0: wait for initial sync pulses to establish timing
if (current_line == 0) {
detect_sync(current_freq);
}
// After Line 0 started: we're at end of a line, waiting for next sync
// Just wait - the sync will be detected and we'll transition to separator
else {
detect_sync(current_freq);
}
break;
case STATE_VIS_DECODE:
// VIS code detection not implemented yet
// Skip directly to separator wait
state = STATE_SEPARATOR;
sample_count = 0;
break;
case STATE_SEPARATOR:
// Wait for separator/porch tone to finish before resuming pixels
sample_count++;
if (separator_target == 0 || sample_count >= separator_target) {
sample_count = 0;
state = STATE_IMAGE_DATA;
}
break;
case STATE_IMAGE_DATA:
// Process pixels continuously
process_pixel_sample(current_freq);
break;
}
}
}
// Estimate frequency from audio samples using Goertzel algorithm
void SSTVRXProcessor::estimate_frequency_goertzel(int32_t audio_sample) {
// Normalize sample to float [-1.0, 1.0]
float sample = audio_sample / 32768.0f;
// Update Goertzel filters for each target frequency
for (int f = 0; f < 4; f++) {
float Q0 = goertzel_coeff[f] * goertzel_Q1[f] - goertzel_Q2[f] + sample;
goertzel_Q2[f] = goertzel_Q1[f];
goertzel_Q1[f] = Q0;
}
goertzel_count++;
// Calculate magnitudes every N samples
if (goertzel_count >= GOERTZEL_N) {
float magnitudes[4];
for (int f = 0; f < 4; f++) {
// Calculate magnitude^2 (we don't need sqrt for comparison)
magnitudes[f] = goertzel_Q1[f] * goertzel_Q1[f] +
goertzel_Q2[f] * goertzel_Q2[f] -
goertzel_Q1[f] * goertzel_Q2[f] * goertzel_coeff[f];
// Reset for next block
goertzel_Q1[f] = 0;
goertzel_Q2[f] = 0;
}
// Find which frequency has the strongest response
int max_idx = 0;
float max_mag = magnitudes[0];
for (int f = 1; f < 4; f++) {
if (magnitudes[f] > max_mag) {
max_mag = magnitudes[f];
max_idx = f;
}
}
// Map index to frequency
// 0=1200Hz, 1=1500Hz, 2=1900Hz, 3=2300Hz
const int freqs[4] = {1200, 1500, 1900, 2300};
// Check if we have a strong enough signal
// Lowered threshold for weak signals (SSTV often has low audio levels)
if (max_mag > 0.001f) { // Very low threshold - accept weak signals
int freq_est = freqs[max_idx];
// Improved linear interpolation between bins
if (max_idx > 0 && magnitudes[max_idx - 1] > 0.0005f) {
float ratio = magnitudes[max_idx - 1] / max_mag;
if (ratio > 0.2f) {
freq_est -= (int)((freqs[max_idx] - freqs[max_idx - 1]) * ratio * 0.5f);
}
}
if (max_idx < 3 && magnitudes[max_idx + 1] > 0.0005f) {
float ratio = magnitudes[max_idx + 1] / max_mag;
if (ratio > 0.2f) {
freq_est += (int)((freqs[max_idx + 1] - freqs[max_idx]) * ratio * 0.5f);
}
}
// Light smoothing to reduce noise while maintaining responsiveness
current_freq = (current_freq + freq_est) / 2;
} else {
// Signal too weak - don't update frequency (keeps last valid estimate)
// This prevents spurious detections from noise
}
goertzel_count = 0;
}
}
// Convert frequency to pixel value (0-255)
int32_t SSTVRXProcessor::freq_to_pixel(int32_t freq) {
// SSTV standard: 1500 Hz = black (0), 2300 Hz = white (255)
if (freq < FREQ_BLACK) freq = FREQ_BLACK;
if (freq > FREQ_WHITE) freq = FREQ_WHITE;
// Linear mapping
int32_t pixel = ((freq - FREQ_BLACK) * 255) / (FREQ_WHITE - FREQ_BLACK);
if (pixel < 0) pixel = 0;
if (pixel > 255) pixel = 255;
return pixel;
}
// Detect horizontal sync pulses
void SSTVRXProcessor::detect_sync(int32_t freq) {
// Sync pulse is 1200 Hz for ~9ms
const int32_t sync_tolerance = 150; // Hz - tolerance for sync detection
// Check for sync frequency (1200 Hz ± 150 Hz)
if (freq > (FREQ_SYNC - sync_tolerance) && freq < (FREQ_SYNC + sync_tolerance)) {
sync_sample_count++;
in_sync = true;
} else {
// Not sync frequency - check if we just finished a valid sync
// Require at least 1/3 of expected sync duration (more lenient for weak signals)
if (in_sync && sync_sample_count >= (samples_per_sync / 3)) {
// Valid sync pulse detected - always record it for timing tracking
// Debug: log current history count before recording
SSTVRXProgressMessage pre_count_msg{0xFFF7, sync_history_count};
shared_memory.application_queue.push(pre_count_msg);
if (sync_history_count < MAX_SYNC_HISTORY) {
sync_positions[sync_history_count] = global_sample_count;
sync_history_count++;
// Send debug message with sync count
SSTVRXProgressMessage sync_debug{0xFFFD, sync_history_count};
shared_memory.application_queue.push(sync_debug);
// Check if this sync should be used for calibration (reject outliers)
bool use_for_calibration = true;
if (sync_history_count > 1) {
uint32_t interval = sync_positions[sync_history_count - 1] - sync_positions[sync_history_count - 2];
const uint32_t nominal_interval = compute_nominal_line_interval();
if (nominal_interval == 0) {
use_for_calibration = false;
} else {
const uint32_t tolerance = nominal_interval / 4;
const uint32_t min_interval = (nominal_interval > tolerance) ? (nominal_interval - tolerance) : 0;
const uint32_t max_interval = nominal_interval + tolerance;
if (interval < min_interval || interval > max_interval) {
use_for_calibration = false; // Don't use this sync for calibration
// Debug: Send outlier rejection message (use 0xFFF8 for interval value)
SSTVRXProgressMessage outlier_msg{0xFFF8, (uint16_t)(interval & 0xFFFF)};
shared_memory.application_queue.push(outlier_msg);
}
}
}
// Calculate calibration after collecting enough syncs for accuracy
// Wait for 8 syncs to get better statistics, then update every 8 syncs
if (use_for_calibration && sync_history_count >= 8 && pixel_time_frac != 0.0f && sync_history_count % 8 == 0) {
calculate_calibration();
}
} else {
// Debug: MAX_SYNC_HISTORY exceeded
SSTVRXProgressMessage max_reached_msg{0xFFF6, sync_history_count};
shared_memory.application_queue.push(max_reached_msg);
}
// Debug: Send sync detection info with timing data
// Also send current frequency estimate for debugging
SSTVRXProgressMessage debug_msg{0xFFFE, (uint16_t)sync_sample_count};
shared_memory.application_queue.push(debug_msg);
// Send frequency estimate for debugging (use 0xFFF9)
SSTVRXProgressMessage freq_msg{0xFFF9, (uint16_t)current_freq};
shared_memory.application_queue.push(freq_msg);
bool ready_for_line = false;
if (waiting_for_first_line) {
if (sync_history_count >= 2) {
waiting_for_first_line = false;
ready_for_line = true;
SSTVRXProgressMessage start_msg{0xFFF4, static_cast<uint16_t>(sync_sample_count)};
shared_memory.application_queue.push(start_msg);
}
} else if (state == STATE_SYNC_SEARCH) {
ready_for_line = true;
}
if (ready_for_line) {
begin_line_after_sync();
}
// else: Line 0 without enough syncs, or mid-image but not in SYNC_SEARCH - just track the sync
}
in_sync = false;
sync_sample_count = 0;
}
}
// Calculate phase and slant calibration from sync timing
void SSTVRXProcessor::calculate_calibration() {
if (sync_history_count < 2 || pixel_time_frac == 0.0f) return;
expected_sync_interval = compute_nominal_line_interval();
if (expected_sync_interval == 0) {
return;
}
// Send debug info about expected interval
SSTVRXProgressMessage debug_interval{0xFFFC, (uint16_t)(expected_sync_interval & 0xFFFF)};
shared_memory.application_queue.push(debug_interval);
// Calculate average timing error (slant) from recent intervals
// Use last 8 intervals for more responsive calibration, but filter outliers
int32_t total_timing_error = 0;
uint32_t last_interval = 0;
uint16_t start_idx = (sync_history_count > 8) ? (sync_history_count - 8) : 1;
uint16_t interval_count = 0;
for (uint16_t i = start_idx; i < sync_history_count; i++) {
uint32_t actual_interval = sync_positions[i] - sync_positions[i - 1];
last_interval = actual_interval;
// Filter out outliers: reject intervals >20% off expected value
// These are likely missed syncs, not actual timing drift
int32_t timing_error = (int32_t)actual_interval - (int32_t)expected_sync_interval;
int32_t max_deviation = (int32_t)expected_sync_interval / 5; // 20% threshold
// Only include intervals within ±20% of expected
if (timing_error >= -max_deviation && timing_error <= max_deviation) {
total_timing_error += timing_error;
interval_count++;
}
}
// Send debug info about last actual interval
SSTVRXProgressMessage debug_actual{0xFFFB, (uint16_t)(last_interval & 0xFFFF)};
shared_memory.application_queue.push(debug_actual);
if (interval_count == 0) return; // Safety check - no valid intervals
// Average error per line
int32_t avg_error = total_timing_error / interval_count;
// Convert to slant adjustment (0.1% units)
// Error in samples / expected_sync_interval = fractional error
// Multiply by 1000 to get 0.1% units
int16_t suggested_slant = (int16_t)(((int64_t)avg_error * 1000) / expected_sync_interval);
// Clamp to reasonable range (±10% = ±100 in 0.1% units)
if (suggested_slant > 100) suggested_slant = 100;
if (suggested_slant < -100) suggested_slant = -100;
// Phase is harder to detect automatically without knowing absolute position
// For now, we only suggest slant correction
int16_t suggested_phase = 0;
// Send calibration suggestion
SSTVRXCalibrationMessage cal_msg{suggested_phase, suggested_slant, sync_history_count};
shared_memory.application_queue.push(cal_msg);
}
uint32_t SSTVRXProcessor::compute_nominal_line_interval() const {
const uint32_t channel_sections = (channel_count > 0) ? channel_count : 1U;
const uint32_t gap_sections = (samples_per_gap == 0)
? 0U
: ((active_mode && active_mode->gaps) ? channel_sections : 1U);
const float samples_per_channel_f = pixel_time_frac * static_cast<float>(PIXELS_PER_LINE);
const float rounded_channel = std::round(samples_per_channel_f);
const uint32_t samples_per_channel = static_cast<uint32_t>(std::max(1.0f, rounded_channel));
const uint32_t total_channel_samples = samples_per_channel * channel_sections;
const uint32_t total_gap_samples = samples_per_gap * gap_sections;
return samples_per_sync + total_gap_samples + total_channel_samples;
}
// Process pixel samples during image data state
void SSTVRXProcessor::process_pixel_sample(int32_t freq) {
// Accumulate frequency samples for averaging
pixel_accumulator += freq;
pixel_sample_count++;
// Advance pixel phase (1.0 per sample, adjusted by slant)
pixel_phase += slant_factor;
// Check if we've accumulated enough samples for one or more pixels
// pixel_time_frac is the number of audio samples per pixel for the current mode
// Use a loop to handle cases where pixel_phase exceeds pixel_time_frac by more than one pixel
while (pixel_phase >= pixel_time_frac && pixel_index < PIXELS_PER_LINE) {
// Pixel complete - calculate average frequency
// Prevent division by zero
int32_t avg_freq;
if (pixel_sample_count > 0) {
avg_freq = pixel_accumulator / pixel_sample_count;
} else {
avg_freq = freq; // Use current frequency if no samples accumulated
}
// Convert to pixel value
uint8_t pixel_value = freq_to_pixel(avg_freq);
// Apply phase offset (horizontal shift) and clamp to prevent out-of-bounds writes
// Clamping prevents pixels from wrapping around and causing duplication
int32_t adjusted_pixel_index = (int32_t)pixel_index + phase_offset;
if (adjusted_pixel_index < 0) {
adjusted_pixel_index = 0;
} else if (adjusted_pixel_index >= PIXELS_PER_LINE) {
adjusted_pixel_index = PIXELS_PER_LINE - 1;
}
store_pixel_value(channel_index, static_cast<uint16_t>(adjusted_pixel_index), pixel_value);
pixel_index++;
// Reset accumulator for next pixel
// If this is not the last pixel in the loop, subsequent pixels will use current sample
pixel_accumulator = freq;
pixel_sample_count = 1;
pixel_phase -= pixel_time_frac; // Keep fractional part for next pixel
// Check if we finished a color channel
if (pixel_index >= PIXELS_PER_LINE) {
pixel_index = 0;
const bool last_channel = ((channel_index + 1) >= channel_count);
if (last_channel) {
process_line();
channel_index = 0;
state = STATE_SYNC_SEARCH;
sync_sample_count = 0;
in_sync = false;
reset_pixel_state();
break;
} else {
channel_index++;
reset_pixel_state();
if (channel_gap_samples > 0) {
start_gap(channel_gap_samples);
} else {
state = STATE_IMAGE_DATA;
}
break;
}
}
}
}
void SSTVRXProcessor::process_line() {
if (current_line >= mode_total_lines) current_line = 1; // reset, maybe a new image
if (mode_total_lines == 0) return; // not set
const uint16_t first_chunk_pixels = (PIXELS_PER_LINE < sstv_max_chunk_pixels) ? PIXELS_PER_LINE : sstv_max_chunk_pixels;
const uint16_t remaining_pixels = (PIXELS_PER_LINE > sstv_max_chunk_pixels) ? (PIXELS_PER_LINE - sstv_max_chunk_pixels) : 0;
auto write_chunk = [&](const uint16_t encoded_line, const uint16_t start_pixel, const uint16_t pixel_count) {
if (pixel_count == 0) {
return;
}
wait_for_chunk_slot();
uint8_t* data_ptr = shared_memory.bb_data.data;
data_ptr[0] = encoded_line & 0xFF;
data_ptr[1] = (encoded_line >> 8) & 0xFF;
for (uint16_t i = 0; i < pixel_count; i++) {
const uint16_t src_idx = start_pixel + i;
const size_t dst = sstv_chunk_header_bytes + i * 3;
data_ptr[dst + 0] = line_buffer_r[src_idx];
data_ptr[dst + 1] = line_buffer_g[src_idx];
data_ptr[dst + 2] = line_buffer_b[src_idx];
}
mark_chunk_ready();
SSTVRXProgressMessage progress_message{encoded_line, mode_total_lines};
shared_memory.application_queue.push(progress_message);
};
write_chunk(static_cast<uint16_t>(current_line * 2), 0, first_chunk_pixels);
if (remaining_pixels) {
write_chunk(static_cast<uint16_t>(current_line * 2 + 1), first_chunk_pixels, remaining_pixels);
}
current_line++;
}
void SSTVRXProcessor::on_message(const Message* const msg) {
switch (msg->id) {
case Message::ID::CaptureConfig:
capture_config(*reinterpret_cast<const CaptureConfigMessage*>(msg));
break;
case Message::ID::SSTVRXPhaseSlant: {
const auto message = *reinterpret_cast<const SSTVRXPhaseSlantMessage*>(msg);
phase_offset = message.phase;
slant_rate = message.slant;
// Convert slant from 0.1% units to a multiplier
// slant_rate of +10 = +1% faster = multiply by 1.01
slant_factor = 1.0f + (slant_rate / 1000.0f);
break;
}
case Message::ID::SSTVRXConfigure: {
const auto message = *reinterpret_cast<const SSTVRXConfigureMessage*>(msg);
vis_code = message.code;
active_mode = find_mode_by_vis_code(message.code);
if (!active_mode) {
configured = false;
SSTVRXProgressMessage error_msg{0xFFFF, 0};
shared_memory.application_queue.push(error_msg);
break;
}
if (active_mode->pixels != PIXELS_PER_LINE) {
configured = false;
SSTVRXProgressMessage error_msg{0xFFFF, 0};
shared_memory.application_queue.push(error_msg);
break;
}
mode_total_lines = active_mode->lines;
if (mode_total_lines == 0) {
mode_total_lines = 1;
}
channel_count = static_cast<uint8_t>(active_mode->color ? 3U : 1U);
if (channel_count == 0) {
channel_count = 1;
}
color_order = color_order_for_mode(*active_mode);
waiting_for_first_line = true;
// Configure decimation chain using NFM filters (narrower than WFMAM)
decim_0.configure(taps_11k0_decim_0.taps); // NFM decim0 filter
decim_1.configure(taps_11k0_decim_1.taps); // NFM decim1 filter
channel_filter.configure(taps_11k0_channel.taps, 1); // Keep 48kHz audio for better pixel resolution
// Calculate filter parameters
const size_t decim_0_input_fs = baseband_fs;
const size_t decim_0_output_fs = decim_0_input_fs / decim_0.decimation_factor;
const size_t decim_1_input_fs = decim_0_output_fs;
const size_t decim_1_output_fs = decim_1_input_fs / decim_1.decimation_factor;
const size_t channel_filter_output_fs = decim_1_output_fs; // Final rate: 48kHz
// Configure demodulator for SSTV - use moderate NFM deviation
// SSTV needs wider deviation than voice NFM to capture 1200-2300 Hz tone range
demod.configure(channel_filter_output_fs, 7500); // 7.5kHz deviation (wider for SSTV tones)
// No audio filter needed - we want clean SSTV tones without filtering
// Enable audio output for monitoring with passthrough filters
audio_output.configure(iir_config_passthrough, iir_config_passthrough, 0.0f);
// Initialize Goertzel coefficients for 24kHz sample rate
// coeff = 2 * cos(2 * PI * freq / sample_rate)
const float sample_rate = static_cast<float>(channel_filter_output_fs);
const float target_freqs[4] = {1200.0f, 1500.0f, 1900.0f, 2300.0f};
for (int f = 0; f < 4; f++) {
float k = (GOERTZEL_N * target_freqs[f]) / sample_rate;
float omega = (2.0f * M_PI * k) / GOERTZEL_N;
goertzel_coeff[f] = 2.0f * cosf(omega);
goertzel_Q1[f] = 0;
goertzel_Q2[f] = 0;
}
goertzel_count = 0;
// Initialize state variables
current_freq = 1200; // Default to sync frequency
configured = true;
current_line = 0;
sample_count = 0;
pixel_index = 0;
channel_index = 0;
pixel_accumulator = 0;
pixel_sample_count = 0;
sync_sample_count = 0;
in_sync = false;
state = STATE_SYNC_SEARCH;
separator_target = 0;
clear_line_buffers();
// Reset frequency offset calibration
freq_offset = 0;
freq_offset_calibrated = false;
sync_freq_accumulator = 0;
sync_freq_count = 0;
// Reset sync history for calibration
sync_history_count = 0;
memset(sync_positions, 0, sizeof(sync_positions));
// Translate SSTV timing constants (expressed for 3.072MHz TX) to 48kHz RX domain
const float conversion = sample_rate / static_cast<float>(SSTV_SAMPLERATE);
pixel_time_frac = static_cast<float>(active_mode->samples_per_pixel) * conversion;
if (pixel_time_frac < 1.0f) {
pixel_time_frac = 1.0f;
}
samples_per_pixel = static_cast<uint32_t>(pixel_time_frac + 0.5f);
const auto convert_interval = [conversion](uint32_t value) -> uint32_t {
const float samples = static_cast<float>(value) * conversion;
const float rounded = std::round(samples);
const float clamped = std::max(1.0f, rounded);
return static_cast<uint32_t>(clamped);
};
samples_per_sync = convert_interval(active_mode->samples_per_sync);
samples_per_gap = convert_interval(active_mode->samples_per_gap);
channel_gap_samples = active_mode->gaps ? samples_per_gap : 0;
pixel_phase = 0.0f;
reset_pixel_state();
shared_memory.bb_data.data[sstv_chunk_flag_index] = 0;
break;
}
default:
break;
}
}
void SSTVRXProcessor::reset_pixel_state() {
pixel_accumulator = 0;
pixel_sample_count = 0;
pixel_phase = 0.0f;
}
void SSTVRXProcessor::start_gap(const uint32_t duration) {
reset_pixel_state();
separator_target = duration;
sample_count = 0;
if (duration == 0) {
state = STATE_IMAGE_DATA;
} else {
state = STATE_SEPARATOR;
}
}
void SSTVRXProcessor::clear_line_buffers() {
std::fill_n(line_buffer_r, PIXELS_PER_LINE, uint8_t{0});
std::fill_n(line_buffer_g, PIXELS_PER_LINE, uint8_t{0});
std::fill_n(line_buffer_b, PIXELS_PER_LINE, uint8_t{0});
}
void SSTVRXProcessor::begin_line_after_sync() {
pixel_index = 0;
channel_index = 0;
clear_line_buffers();
start_gap(samples_per_gap);
}
void SSTVRXProcessor::store_pixel_value(const uint32_t channel, const uint16_t pixel, const uint8_t value) {
if (!active_mode) {
return;
}
if (!active_mode->color) {
line_buffer_r[pixel] = value;
line_buffer_g[pixel] = value;
line_buffer_b[pixel] = value;
return;
}
if (channel >= channel_count || channel >= color_order.size()) {
return;
}
switch (color_order[channel]) {
case 0:
line_buffer_r[pixel] = value;
break;
case 1:
line_buffer_g[pixel] = value;
break;
case 2:
line_buffer_b[pixel] = value;
break;
default:
break;
}
}
void SSTVRXProcessor::capture_config(const CaptureConfigMessage& message) {
if (message.config) {
audio_output.set_stream(std::make_unique<StreamInput>(message.config));
} else {
audio_output.set_stream(nullptr);
}
}
int main() {
// Initialize audio DMA
audio::dma::init_audio_out();
EventDispatcher event_dispatcher{std::make_unique<SSTVRXProcessor>()};
event_dispatcher.run();
return 0;
}
+175
View File
@@ -0,0 +1,175 @@
/*
* Copyright (C) 2025 StarVore Labs
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
#ifndef __PROC_SSTV_RX__
#define __PROC_SSTV_RX__
#include "portapack_shared_memory.hpp"
#include "baseband_processor.hpp"
#include "baseband_thread.hpp"
#include "sstv.hpp"
#include "dsp_decimate.hpp"
#include "dsp_demodulate.hpp"
#include "dsp_iir.hpp"
#include "audio_output.hpp"
#include <array>
using namespace sstv;
class SSTVRXProcessor : public BasebandProcessor {
public:
void execute(const buffer_c8_t& buffer) override;
void on_message(const Message* const p) override;
private:
enum state_t {
STATE_SYNC_SEARCH = 0,
STATE_VIS_DECODE,
STATE_SEPARATOR, // Wait for separator pulse (1500Hz)
STATE_IMAGE_DATA
};
static constexpr uint32_t MAX_SAMPLES_PER_LINE = 4096;
static constexpr uint16_t PIXELS_PER_LINE = 320;
// Frequency ranges for SSTV (in Hz)
static constexpr int32_t FREQ_BLACK = 1500;
static constexpr int32_t FREQ_WHITE = 2300;
static constexpr int32_t FREQ_SYNC = 1200;
static constexpr int32_t FREQ_VIS_BIT0 = 1300;
static constexpr int32_t FREQ_VIS_BIT1 = 1100;
state_t state{STATE_SYNC_SEARCH};
bool configured{false};
uint8_t vis_code{0};
const sstv_mode* active_mode{nullptr};
uint16_t mode_total_lines{256};
static constexpr size_t baseband_fs = 3072000;
// DSP chain components (using NFM-style decimation for SSTV)
dsp::decimate::FIRC8xR16x24FS4Decim8 decim_0{}; // Decimate by 8 (NFM style)
dsp::decimate::FIRC16xR16x32Decim8 decim_1{}; // Decimate by 8
dsp::decimate::FIRAndDecimateComplex channel_filter{}; // Decimate by 2 -> 24kHz
dsp::demodulate::FM demod{}; // FM demodulator
AudioOutput audio_output{};
// Buffers
std::array<complex16_t, 512> dst{};
const buffer_c16_t dst_buffer{
dst.data(),
dst.size()};
// work_audio_buffer and dst_buffer use the same data pointer
const buffer_s16_t work_audio_buffer{
(int16_t*)dst.data(),
sizeof(dst) / sizeof(int16_t)};
// State variables for Goertzel frequency estimation
int32_t current_freq{1200}; // Current frequency in Hz
// Goertzel filters for SSTV frequencies; configured at runtime (48kHz today)
// We'll detect 1200Hz, 1500Hz, 1900Hz, and 2300Hz
// Larger block size = better frequency discrimination but slower response
// At 48kHz: 48 samples ≈ 1ms, a little over one cycle of a 1200Hz tone
static constexpr size_t GOERTZEL_N = 48; // Increased from 16 for better accuracy
float goertzel_Q1[4]{0, 0, 0, 0};
float goertzel_Q2[4]{0, 0, 0, 0};
float goertzel_coeff[4]; // Calculated in configure
size_t goertzel_count{0};
// Line decoding state
uint8_t line_buffer_r[PIXELS_PER_LINE];
uint8_t line_buffer_g[PIXELS_PER_LINE];
uint8_t line_buffer_b[PIXELS_PER_LINE];
uint32_t sample_count{0};
uint32_t pixel_index{0};
uint32_t channel_index{0};
uint8_t channel_count{3};
std::array<uint8_t, 3> color_order{{1, 2, 0}};
uint16_t current_line{0};
bool waiting_for_first_line{true};
// Pixel accumulation for averaging
int32_t pixel_accumulator{0};
uint32_t pixel_sample_count{0};
// Fractional pixel timing for accuracy
float pixel_time_frac{0.0f}; // Fractional samples per pixel
float pixel_phase{0.0f}; // Accumulated phase for current pixel
// Phase and slant adjustments
int16_t phase_offset{0}; // Horizontal offset in pixels
int16_t slant_rate{0}; // Timing adjustment in 0.1% units
float slant_factor{1.0f}; // Calculated slant multiplier
// Timing parameters (will be set based on mode)
uint32_t samples_per_pixel{7}; // Integer part for quick checks
uint32_t samples_per_sync{216}; // 9ms
uint32_t samples_per_gap{36}; // Gap after sync or between channels
uint32_t channel_gap_samples{36}; // Separators between color sections
uint32_t separator_target{0};
// Sync detection
uint32_t sync_sample_count{0};
bool in_sync{false};
int32_t sync_freq_sum{0}; // Accumulated frequency during sync pulse
uint32_t sync_freq_samples{0}; // Number of samples in sync pulse for averaging
// Sync pulse timing tracking for auto-calibration
static constexpr uint32_t MAX_SYNC_HISTORY = 256; // Track all syncs in image
uint32_t sync_positions[MAX_SYNC_HISTORY]; // Sample positions when sync detected
uint16_t sync_history_count{0};
uint32_t expected_sync_interval{0}; // Expected samples between syncs
int32_t accumulated_phase_error{0}; // Accumulated phase offset in samples
int32_t accumulated_slant_error{0}; // Accumulated timing drift
uint32_t global_sample_count{0}; // Never-reset counter for timing calibration
// Frequency offset compensation (auto-calibrated from sync pulses)
int32_t freq_offset{0};
bool freq_offset_calibrated{false};
int32_t sync_freq_accumulator{0};
uint32_t sync_freq_count{0};
// Helper functions
int32_t freq_to_pixel(int32_t freq);
void process_pixel_sample(int32_t freq);
void process_line();
void detect_sync(int32_t freq);
void calculate_calibration();
uint32_t compute_nominal_line_interval() const;
void estimate_frequency_goertzel(int32_t audio_sample);
void capture_config(const CaptureConfigMessage& message);
void reset_pixel_state();
void start_gap(uint32_t duration);
void begin_line_after_sync();
void clear_line_buffers();
void store_pixel_value(uint32_t channel, uint16_t pixel, uint8_t value);
RequestSignalMessage sig_message{RequestSignalMessage::Signal::FillRequest};
/* NB: Threads should be the last members in the class definition. */
BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive};
};
#endif
+206
View File
@@ -0,0 +1,206 @@
/*
* Copyright (C) 2026 HTotoo
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
/*
This and The other files related to this is based on a lot of great people's work. https://github.com/RocketGod-git/ProtoPirate Check the repo, and the credits inside.
*/
#include "proc_subcar.hpp"
#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;
// SR = 4Mhz , and we are decimating by /8 in total , decim1_out clock 4Mhz /8= 500khz samples/sec.
// buffer has 2048 complex i8 I,Q signed samples
// decim0 out: 2048/4 = 512 complex i16 I,Q signed samples
// decim1 out: 512/2 = 256 complex i16 I,Q signed samples
// Regarding Filters, we are re-using existing FIR filters, @4Mhz, FIR decim1 ilter, BW =+-220Khz (at -3dB's). BW = 440kHZ.
const auto decim_0_out = decim_0.execute(buffer, dst_buffer); // Input:2048 complex/4 (decim factor) = 512_output complex (1024 I/Q samples)
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;
int32_t const hysteresis = threshold / 8; // +-12%
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 {
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;
}
// 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;
}
}
void SubCarProcessor::on_message(const Message* const message) {
if (message->id == Message::ID::SubGhzFPRxConfigure)
configure(*reinterpret_cast<const SubGhzFPRxConfigureMessage*>(message));
}
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
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
decim_0.configure(taps_200k_wfm_decim_0.taps);
decim_1.configure(taps_200k_wfm_decim_1.taps);
configured = true;
}
int main() {
EventDispatcher event_dispatcher{std::make_unique<SubCarProcessor>()};
event_dispatcher.run();
return 0;
}
+98
View File
@@ -0,0 +1,98 @@
/*
* Copyright (C) 2026 HTotoo
*
* This file is part of PortaPack.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street,
* Boston, MA 02110-1301, USA.
*/
/*
This and The other files related to this is based on a lot of great people's work. https://github.com/RocketGod-git/ProtoPirate Check the repo, and the credits inside.
*/
#ifndef __PROC_SUBCAR_H__
#define __PROC_SUBCAR_H__
#include "baseband_processor.hpp"
#include "baseband_thread.hpp"
#include "rssi_thread.hpp"
#include "message.hpp"
#include "dsp_decimate.hpp"
#pragma GCC push_options
#pragma GCC optimize("Os")
#include "fprotos/subcarprotos.hpp"
#pragma GCC pop_options
#define OOK_EST_HIGH_RATIO 3 // Constant for slowness of OOK high level estimator
#define OOK_EST_LOW_RATIO 5 // Constant for slowness of OOK low level (noise) estimator (very slow)
#define OOK_MAX_HIGH_LEVEL 450000
class SubCarProcessor : public BasebandProcessor {
public:
void execute(const buffer_c8_t& buffer) override;
void on_message(const Message* const message) override;
private:
enum {
STATE_IDLE = 0,
STATE_PULSE = 1,
STATE_GAP_START = 2,
STATE_GAP = 3,
} sig_state = STATE_IDLE;
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
uint32_t nsPerDecSamp = 0;
/* Array Buffer aux. used in decim0 and decim1 IQ c16 signed data ; (decim0 defines the max length of the array) */
std::array<complex16_t, 512> dst{}; // decim0 /4 , 2048/4 = 512 complex I,Q
const buffer_c16_t dst_buffer{
dst.data(),
dst.size()};
/* Decimates */
dsp::decimate::FIRC8xR16x24FS4Decim4 decim_0{};
dsp::decimate::FIRC16xR16x16Decim2 decim_1{};
uint32_t currentDuration = 0;
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;
};
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)
void configure(const SubGhzFPRxConfigureMessage& message);
/* NB: Threads should be the last members in the class definition. */
BasebandThread baseband_thread{baseband_fs, this, baseband::Direction::Receive};
RSSIThread rssi_thread{};
};
#endif /*__PROC_WEATHER_H__*/
+2 -2
View File
@@ -23,8 +23,8 @@
Creator: @htotoo
*/
#ifndef __PROC_WEATHER_H__
#define __PROC_WEATHER_H__
#ifndef __PROC_SUBGHZD_H__
#define __PROC_SUBGHZD_H__
#include "baseband_processor.hpp"
#include "baseband_thread.hpp"
+2 -4
View File
@@ -45,8 +45,7 @@ void usb_send_bulk(void* const data, const uint32_t maximum_length) {
usb_bulk_block_cb,
NULL);
while (!usb_bulk_block_done)
;
while (!usb_bulk_block_done);
}
void usb_receive_bulk(void* const data, const uint32_t maximum_length) {
@@ -59,8 +58,7 @@ void usb_receive_bulk(void* const data, const uint32_t maximum_length) {
usb_bulk_block_cb,
NULL);
while (!usb_bulk_block_done)
;
while (!usb_bulk_block_done);
}
void usb_send_csw(msd_cbw_t* msd_cbw_data, uint8_t status) {
+1 -2
View File
@@ -118,8 +118,7 @@ void usb_transfer(void) {
scsi_bulk_transfer_complete,
NULL);
while (!transfer_complete)
;
while (!transfer_complete);
msd_cbw_t* msd_cbw_data = (msd_cbw_t*)&usb_bulk_buffer[0x4000];
+48 -1
View File
@@ -173,7 +173,7 @@ bool BMPFile::read_next_px(ui::Color& px, bool seek = true) {
//*a = (val >> 15) & 0x01; // 1-bit alpha
uint8_t r = (val >> 10) & 0x1F; // 5-bit red
uint8_t g = (val >> 5) & 0x1F; // 5-bit green
uint8_t b = (val)&0x1F; // 5-bit blue
uint8_t b = (val) & 0x1F; // 5-bit blue
// expand
r = (r << 3) | (r >> 2);
g = (g << 3) | (g >> 2);
@@ -200,6 +200,53 @@ bool BMPFile::read_next_px(ui::Color& px, bool seek = true) {
return true;
}
bool BMPFile::read_next_px_cnt(ui::Color* px, uint32_t count, bool seek) {
if (!is_opened) return false;
size_t bytesneeded = byte_per_px * count;
while (bytesneeded > 0) { // read in batches
size_t currusedbytes = bytesneeded > 512 ? 170 * byte_per_px : bytesneeded; // don't mind this magic number.
uint8_t buffer[currusedbytes];
auto res = bmpimage.read(buffer, currusedbytes);
if (res.is_error()) return false;
for (uint32_t i = 0; i < currusedbytes; i += byte_per_px, px++) {
switch (type) {
case 5: {
// ARGB1555
uint16_t val = buffer[i] | (buffer[i + 1] << 8);
// Extract components
//*a = (val >> 15) & 0x01; // 1-bit alpha
uint8_t r = (val >> 10) & 0x1F; // 5-bit red
uint8_t g = (val >> 5) & 0x1F; // 5-bit green
uint8_t b = (val) & 0x1F; // 5-bit blue
// expand
r = (r << 3) | (r >> 2);
g = (g << 3) | (g >> 2);
b = (b << 3) | (b >> 2);
*px = ui::Color(r, g, b);
break;
}
case 2: // 32
*px = ui::Color(buffer[i + 2], buffer[i + 1], buffer[i]);
break;
case 4: { // 8-bit
// uint8_t index = buffer[0];
// px = ui::Color(color_palette[index][2], color_palette[index][1], color_palette[index][0]); // Palette is BGR
// px = ui::Color(buffer[0]); // niy, since needs a lot of ram for the palette
break;
}
case 1: // 24
default:
*px = ui::Color(buffer[i + 2], buffer[i + 1], buffer[i]);
break;
}
}
bytesneeded -= currusedbytes;
}
if (seek) advance_curr_px(count);
return true;
}
// if you set this, then the expanded part (or the newly created) will be filled with this color. but the expansion or the creation will be slower.
void BMPFile::set_bg_color(ui::Color background) {
bg = background;
+1
View File
@@ -42,6 +42,7 @@ class BMPFile {
uint32_t getbpr() { return byte_per_row; };
bool read_next_px(ui::Color& px, bool seek);
bool read_next_px_cnt(ui::Color* px, uint32_t count, bool seek);
bool write_next_px(ui::Color& px);
uint32_t get_real_height();
uint32_t get_width();
+34
View File
@@ -0,0 +1,34 @@
#ifndef __FLEX_DEFS_H__
#define __FLEX_DEFS_H__
#include <cstdint>
#include <array>
#include "baseband.hpp"
namespace flex {
enum class FlexMode : uint8_t {
FLEX_1600_2FSK,
FLEX_3200_2FSK,
FLEX_3200_4FSK,
FLEX_6400_4FSK
};
struct FlexStats {
uint32_t symbols_processed;
uint32_t total_frames;
uint32_t correct_frames;
};
struct FlexPacket {
uint32_t bitrate; // 1600, 3200, 6400
uint32_t capcode;
uint32_t function; // 0-3
uint32_t type; // Message type (e.g. ALN, NUM, etc - could use enum)
char message[128]; // Decoded message text
uint32_t status; // 0=OK, other=Errors
};
} /* namespace flex */
#endif /*__FLEX_DEFS_H__*/
+1 -1
View File
@@ -40,7 +40,7 @@ namespace i2cdev {
// The device class. You'll derive your from this. Override init() and update();
class I2cDev {
public:
virtual ~I2cDev(){};
virtual ~I2cDev() {};
virtual bool init(uint8_t addr) = 0; // returns true if it is that that device we are looking for.
virtual void update() = 0; // override this, and you'll be able to query your device and broadcast the result to the system
+1 -1
View File
@@ -47,7 +47,7 @@ class ManchesterBase {
virtual size_t symbols_count() const;
virtual ~ManchesterBase(){};
virtual ~ManchesterBase() {};
protected:
const baseband::Packet& packet;
+133 -2
View File
@@ -35,6 +35,7 @@
#include "adsb_frame.hpp"
#include "ert_packet.hpp"
#include "pocsag_packet.hpp"
#include "flex_defs.hpp"
#include "aprs_packet.hpp"
#include "sonde_packet.hpp"
#include "tpms_packet.hpp"
@@ -136,6 +137,15 @@ class Message {
NoaaAptRxImageData = 79,
FSKPacket = 80,
EPIRBPacket = 81,
FlexPacket = 82,
FlexStats = 83,
FlexConfigure = 84,
FlexDebug = 85,
SSTVRXConfigure = 86,
SSTVRXProgress = 87,
SSTVRXPhaseSlant = 88,
SSTVRXCalibration = 89,
SubCarData = 90,
MAX
};
@@ -1139,6 +1149,62 @@ class SSTVConfigureMessage : public Message {
const uint32_t pixel_duration;
};
class SSTVRXConfigureMessage : public Message {
public:
constexpr SSTVRXConfigureMessage(
const uint8_t code)
: Message{id : ID::SSTVRXConfigure},
code(code) {
}
const uint8_t code;
};
class SSTVRXProgressMessage : public Message {
public:
constexpr SSTVRXProgressMessage(
const uint16_t line,
const uint16_t total_lines)
: Message{ID::SSTVRXProgress},
line(line),
total_lines(total_lines) {
}
const uint16_t line;
const uint16_t total_lines;
};
class SSTVRXPhaseSlantMessage : public Message {
public:
constexpr SSTVRXPhaseSlantMessage(
const int16_t phase,
const int16_t slant)
: Message{ID::SSTVRXPhaseSlant},
phase(phase),
slant(slant) {
}
const int16_t phase;
const int16_t slant;
};
class SSTVRXCalibrationMessage : public Message {
public:
constexpr SSTVRXCalibrationMessage(
const int16_t suggested_phase,
const int16_t suggested_slant,
const uint16_t sync_count)
: Message{ID::SSTVRXCalibration},
suggested_phase(suggested_phase),
suggested_slant(suggested_slant),
sync_count(sync_count) {
}
const int16_t suggested_phase; // Suggested phase correction in pixels
const int16_t suggested_slant; // Suggested slant correction in 0.1% units
const uint16_t sync_count; // Number of syncs analyzed
};
class FSKConfigureMessage : public Message {
public:
constexpr FSKConfigureMessage(
@@ -1187,9 +1253,10 @@ class FSKRxConfigureMessage : public Message {
class POCSAGConfigureMessage : public Message {
public:
constexpr POCSAGConfigureMessage()
: Message{ID::POCSAGConfigure} {
constexpr POCSAGConfigureMessage(int8_t baud_config = -1)
: Message{ID::POCSAGConfigure}, baud_config(baud_config) {
}
int8_t baud_config; //-1 auto, 0=512,1=1200,2=2400
};
class APRSPacketMessage : public Message {
@@ -1568,4 +1635,68 @@ class NoaaAptRxImageDataMessage : public Message {
uint32_t cnt = 0;
};
class FlexPacketMessage : public Message {
public:
constexpr FlexPacketMessage(const flex::FlexPacket& packet)
: Message{ID::FlexPacket},
packet{packet} {
}
flex::FlexPacket packet;
};
class FlexStatsMessage : public Message {
public:
constexpr FlexStatsMessage(const flex::FlexStats& stats)
: Message{ID::FlexStats},
stats{stats} {
}
flex::FlexStats stats;
};
class FlexConfigureMessage : public Message {
public:
constexpr FlexConfigureMessage()
: Message{ID::FlexConfigure} {
}
};
class FlexDebugMessage : public Message {
public:
constexpr FlexDebugMessage(const uint32_t val1, const uint32_t val2, const char* msg)
: Message{ID::FlexDebug},
val1{val1},
val2{val2},
text{} {
size_t i = 0;
while (i < sizeof(text) - 1 && msg[i] != '\0') {
text[i] = msg[i];
i++;
}
text[i] = '\0';
}
uint32_t val1;
uint32_t val2;
char text[64];
};
class SubCarDataMessage : public Message {
public:
constexpr SubCarDataMessage(
uint8_t sensorType = 0,
uint16_t bits = 0,
uint64_t data = 0,
uint64_t data2 = 0)
: Message{ID::SubCarData},
sensorType{sensorType},
bits{bits},
data{data},
data2{data2} {
}
uint8_t sensorType = 0;
uint16_t bits = 0;
uint64_t data = 0;
uint64_t data2 = 0;
};
#endif /*__MESSAGE_H__*/
+1 -2
View File
@@ -55,8 +55,7 @@ class MessageQueue {
const bool result = push(message);
if (result) {
// TODO: More graceful method of waiting for empty? Maybe sleep for a bit?
while (!is_empty())
;
while (!is_empty());
}
return result;
}
+2 -2
View File
@@ -28,8 +28,8 @@
#define MATRIX_A 0x9908b0dfUL /* constant vector a */
#define UMASK 0x80000000UL /* most significant w-r bits */
#define LMASK 0x7fffffffUL /* least significant r bits */
#define MIXBITS(u, v) (((u)&UMASK) | ((v)&LMASK))
#define TWIST(u, v) ((MIXBITS(u, v) >> 1) ^ ((v)&1UL ? MATRIX_A : 0UL))
#define MIXBITS(u, v) (((u) & UMASK) | ((v) & LMASK))
#define TWIST(u, v) ((MIXBITS(u, v) >> 1) ^ ((v) & 1UL ? MATRIX_A : 0UL))
/* initializes state[N] with a seed */
extern void init_genrand(unsigned long s);
+126 -2
View File
@@ -64,7 +64,7 @@ Packet::Packet(
type_ = Type::Meteomodem_M10;
else if (id_byte == 0x648F)
type_ = Type::Meteomodem_M2K2;
else if (id_byte == 0x4520) // https://raw.githubusercontent.com/projecthorus/radiosonde_auto_rx/master/demod/mod/m20mod.c
else if (id_byte == 0x4520 || id_byte == 0x4320) // https://raw.githubusercontent.com/projecthorus/radiosonde_auto_rx/master/demod/mod/m20mod.c
type_ = Type::Meteomodem_M20;
}
}
@@ -145,7 +145,7 @@ uint32_t Packet::battery_voltage() const {
if (type_ == Type::Meteomodem_M10)
return (reader_bi_m.read(69 * 8, 8) + (reader_bi_m.read(70 * 8, 8) << 8)) * 1000 / 150;
else if (type_ == Type::Meteomodem_M20) {
return 0; // NOT SUPPPORTED YET
return reader_bi_m.read(0x26 * 8, 8) * (3.3f / 255.0) * 1000; // based on https://raw.githubusercontent.com/projecthorus/radiosonde_auto_rx/master/demod/mod/m20mod.c
} else if (type_ == Type::Meteomodem_M2K2)
return reader_bi_m.read(69 * 8, 8) * 66; // Actually 65.8
else if (type_ == Type::Vaisala_RS41_SG) {
@@ -160,10 +160,46 @@ uint32_t Packet::frame() const {
if (type_ == Type::Vaisala_RS41_SG) {
uint32_t frame_number = vaisala_descramble(pos_FrameNb) | (vaisala_descramble(pos_FrameNb + 1) << 8);
return frame_number;
} else if (type_ == Type::Meteomodem_M20) {
return reader_bi_m.read(0x15 * 8, 8);
} else {
return 0; // Unknown
}
}
uint8_t Packet::getFwVerM20() const {
size_t pos_fw = 0x43;
int flen = reader_bi_m.read(0, 8);
if (flen != 0x45) {
int auxLen = flen - 0x45;
if (auxLen < 0) {
pos_fw = flen - 2;
}
}
return reader_bi_m.read(pos_fw, 8);
}
float Packet::get_pressure() const {
float pressure = 0.0f;
if (type_ == Type::Meteomodem_M20) {
float hPa = 0.0f;
uint32_t val = ((uint32_t)reader_bi_m.read(0x25 * 8, 8) << 8) | (uint32_t)reader_bi_m.read(0x24 * 8, 8); // cf. DF9DQ
uint8_t p0 = 0x00;
uint8_t fwVer = getFwVerM20();
if (fwVer >= 0x07) { // SPI1_P[0]
p0 = reader_bi_m.read(0x16 * 8, 8);
}
val = (val << 8) | p0;
if (val > 0) {
hPa = val / (float)(16 * 256); // 4096=0x1000
}
if (hPa > 2560.0f) { // val > 0xA00000
hPa = -1.0f;
}
pressure = hPa;
}
return pressure;
}
temp_humid Packet::get_temp_humid() const {
temp_humid result;
@@ -331,6 +367,78 @@ temp_humid Packet::get_temp_humid() const {
result.humid = rh;
}
}
if (type_ == Type::Meteomodem_M20) {
float p0 = 1.07303516e-03,
p1 = 2.41296733e-04,
p2 = 2.26744154e-06,
p3 = 6.52855181e-08;
float Rs[3] = {12.1e3, 36.5e3, 475.0e3}; // bias/series
float Rp[3] = {1e20, 330.0e3, 2000.0e3}; // parallel, Rp[0]=inf
uint8_t scT = 0; // {0,1,2}, range/scale voltage divider
uint16_t ADC_RT; // ADC12
// ui16_t Tcal[2];
float x, R;
float T = 0; // T/Kelvin
uint32_t b2 = reader_bi_m.read(0x5 * 8, 8);
uint32_t b1 = reader_bi_m.read(0x4 * 8, 8);
ADC_RT = (b2 << 8) | b1;
if (ADC_RT > 8191) {
scT = 2;
ADC_RT -= 8192;
} else if (ADC_RT > 4095) {
scT = 1;
ADC_RT -= 4096;
} else {
scT = 0;
} // also if (ADC_RT>>12)&3 == 3
// ADC12 , 4096 = 1<<12, max: 4095
x = (4095.0 - ADC_RT) / ADC_RT; // (Vcc-Vout)/Vout = Vcc/Vout - 1
R = Rs[scT] / (x - Rs[scT] / Rp[scT]);
if (R > 0) T = 1.0 / (p0 + p1 * log(R) + p2 * log(R) * log(R) + p3 * log(R) * log(R) * log(R));
if (T - 273.15 < -120.0 || T - 273.15 > 60.0) T = 0; // T < -120C, T > 60C invalid
result.temp = T - 273.15; // celsius
// humidity
// humi helper tntc2:
float Rsq = 22.1e3; // P5.6=Vcc
float R25 = 2.2e3; // 0.119e3; //2.2e3;
float b = 3650.0; // B/Kelvin
float T25 = 25.0 + 273.15; // T0=25C, R0=R25=5k
// -> Steinhart-Hart coefficients (polyfit):
T = 0.0; // T/Kelvin
uint16_t ADC_ntc0; // M10: ADC12 P6.4(A4)
float xq, Rq;
uint32_t bq2 = reader_bi_m.read(0x7 * 8, 8);
uint32_t bq1 = reader_bi_m.read(0x6 * 8, 8);
ADC_ntc0 = (bq2 << 8) | bq1; // M10: 0x40,0x3F
xq = (4095.0 - ADC_ntc0) / ADC_ntc0; // (Vcc-Vout)/Vout
Rq = Rsq / xq;
if (Rq > 0) T = 1.0 / (1.0 / T25 + 1.0 / b * log(Rq / R25));
// really the humidity
float TU = T - 273.15;
float RH = -1.0f;
float xqq;
uint16_t humval = ((uint32_t)reader_bi_m.read(0x03 * 8, 8) << 8) | (uint32_t)reader_bi_m.read(0x02 * 8, 8);
uint16_t rh_cal = ((uint32_t)reader_bi_m.read(0x30 * 8, 8) << 8) | (uint32_t)reader_bi_m.read(0x2F * 8, 8);
float humidityCalibration = 6.4e8f / (rh_cal + 80000.0f);
xqq = (humval + 80000.0f) * humidityCalibration * (1.0f - 5.8e-4f * (TU - 25.0f));
xqq = 4.16e9f / xqq;
xqq = 10.087f * xqq * xqq * xqq - 211.62f * xqq * xqq + 1388.2f * xqq - 2797.0f;
RH = -1.0f;
if (humval < 48000) {
if (xqq > -20.0f && xqq < 120.f) {
RH = xqq;
if (RH < 0.0f) RH = 0.0f;
if (RH > 100.0f) RH = 100.0f;
}
}
result.humid = RH;
}
return result;
}
@@ -378,6 +486,10 @@ std::string Packet::serial_number() const {
}
}
return serial_id;
} else if (type_ == Type::Meteomodem_M20) {
// Inspired by https://raw.githubusercontent.com/projecthorus/radiosonde_auto_rx/master/demod/mod/m20mod.c
uint32_t sn = reader_bi_m.read(0x12 * 8, 8) | (reader_bi_m.read(0x13 * 8, 8) << 8) | (reader_bi_m.read(0x14 * 8, 8) << 16);
return to_string_dec_uint(sn); // Serial is 3 bytes at byte #12
} else {
return "?";
}
@@ -404,11 +516,23 @@ bool Packet::crc_ok() const {
return crc_ok_M10();
case Type::Vaisala_RS41_SG:
return crc_ok_RS41();
case Type::Meteomodem_M20:
return check_ok_M20();
default:
return true; // euquiq: it was false, but if no crc routine, then no way to check
}
}
bool Packet::check_ok_M20() const {
uint8_t b1 = reader_bi_m.read(0, 8);
uint8_t b2 = reader_bi_m.read(8, 8);
if ((b1 != 0x45 && b1 != 0x43) || b2 != 0x20)
return false;
if (packet_.size() / 8 < b1)
return false;
return true;
}
// each data block has a 2 byte header, data, and 2 byte tail:
// 1st byte: block ID
// 2nd byte: data length (without header or tail)
+77 -8
View File
@@ -36,6 +36,16 @@ struct GPS_data {
uint32_t alt{0};
float lat{0};
float lon{0};
bool is_valid() const {
if (lat >= -0.01 && lat <= 0.01 && lon >= -0.01 && lon <= 0.01)
return false;
if (lat < -90.0 || lat > 90.0)
return false;
if (lon < -180.0 || lon > 180.0)
return false;
return true;
}
};
struct temp_humid {
@@ -68,21 +78,79 @@ class Packet {
GPS_data get_GPS_data() const;
uint32_t frame() const;
temp_humid get_temp_humid() const;
float get_pressure() const;
FormattedSymbols symbols_formatted() const;
bool crc_ok() const;
private:
uint8_t getFwVerM20() const;
static constexpr uint8_t vaisala_mask[64] = {
0x96, 0x83, 0x3E, 0x51, 0xB1, 0x49, 0x08, 0x98,
0x32, 0x05, 0x59, 0x0E, 0xF9, 0x44, 0xC6, 0x26,
0x21, 0x60, 0xC2, 0xEA, 0x79, 0x5D, 0x6D, 0xA1,
0x54, 0x69, 0x47, 0x0C, 0xDC, 0xE8, 0x5C, 0xF1,
0xF7, 0x76, 0x82, 0x7F, 0x07, 0x99, 0xA2, 0x2C,
0x93, 0x7C, 0x30, 0x63, 0xF5, 0x10, 0x2E, 0x61,
0xD0, 0xBC, 0xB4, 0xB6, 0x06, 0xAA, 0xF4, 0x23,
0x78, 0x6E, 0x3B, 0xAE, 0xBF, 0x7B, 0x4C, 0xC1};
0x96,
0x83,
0x3E,
0x51,
0xB1,
0x49,
0x08,
0x98,
0x32,
0x05,
0x59,
0x0E,
0xF9,
0x44,
0xC6,
0x26,
0x21,
0x60,
0xC2,
0xEA,
0x79,
0x5D,
0x6D,
0xA1,
0x54,
0x69,
0x47,
0x0C,
0xDC,
0xE8,
0x5C,
0xF1,
0xF7,
0x76,
0x82,
0x7F,
0x07,
0x99,
0xA2,
0x2C,
0x93,
0x7C,
0x30,
0x63,
0xF5,
0x10,
0x2E,
0x61,
0xD0,
0xBC,
0xB4,
0xB6,
0x06,
0xAA,
0xF4,
0x23,
0x78,
0x6E,
0x3B,
0xAE,
0xBF,
0x7B,
0x4C,
0xC1};
GPS_data ecef_to_gps() const;
@@ -97,6 +165,7 @@ class Packet {
bool crc_ok_M10() const;
bool crc_ok_RS41() const;
bool check_ok_M20() const;
bool crc16rs41(uint32_t field_start) const;
};
+3
View File
@@ -91,6 +91,7 @@ constexpr image_tag_t image_tag_epirb_rx{'P', 'E', 'P', 'I'};
constexpr image_tag_t image_tag_nfm_audio{'P', 'N', 'F', 'M'};
constexpr image_tag_t image_tag_pocsag{'P', 'P', 'O', 'C'};
constexpr image_tag_t image_tag_pocsag2{'P', 'P', 'O', '2'};
constexpr image_tag_t image_tag_flex{'P', 'F', 'L', 'X'};
constexpr image_tag_t image_tag_sonde{'P', 'S', 'O', 'N'};
constexpr image_tag_t image_tag_tpms{'P', 'T', 'P', 'M'};
constexpr image_tag_t image_tag_wfm_audio{'P', 'W', 'F', 'M'};
@@ -118,9 +119,11 @@ constexpr image_tag_t image_tag_usb_sd{'P', 'U', 'S', 'B'};
constexpr image_tag_t image_tag_weather{'P', 'W', 'T', 'H'};
constexpr image_tag_t image_tag_subghzd{'P', 'S', 'G', 'D'};
constexpr image_tag_t image_tag_subcar{'P', 'S', 'C', 'D'};
constexpr image_tag_t image_tag_protoview{'P', 'P', 'V', 'W'};
constexpr image_tag_t image_tag_wefaxrx{'P', 'W', 'F', 'X'};
constexpr image_tag_t image_tag_noaaapt_rx{'P', 'N', 'O', 'A'};
constexpr image_tag_t image_tag_sstv_rx{'P', 'S', 'R', 'X'};
constexpr image_tag_t image_tag_noop{'P', 'N', 'O', 'P'};
+1 -1
View File
@@ -28,7 +28,7 @@ namespace sstv {
#define SSTV_SAMPLERATE 3072000
#define SSTV_DELTA_COEF ((1ULL << 32) / SSTV_SAMPLERATE)
#define SSTV_F2D(f) (uint32_t)((f)*SSTV_DELTA_COEF)
#define SSTV_F2D(f) (uint32_t)((f) * SSTV_DELTA_COEF)
#define SSTV_MS2S(d) (uint32_t)((d) / 1000.0 * (float)SSTV_SAMPLERATE)
#define SSTV_VIS_SS SSTV_F2D(1200)
+9 -9
View File
@@ -36,29 +36,29 @@ namespace ui {
// default font width
#define UI_POS_DEFAULT_WIDTH 8
// px position of the linenum-th character (Y)
#define UI_POS_Y(linenum) ((int)((linenum)*UI_POS_DEFAULT_HEIGHT))
#define UI_POS_Y(linenum) ((int)((linenum) * UI_POS_DEFAULT_HEIGHT))
// px position of the linenum-th character from the bottom of the screen (Y) (please calculate the +1 line top-bar to it too if that is visible!)
#define UI_POS_Y_BOTTOM(linenum) ((int)(screen_height - (linenum)*UI_POS_DEFAULT_HEIGHT))
#define UI_POS_Y_BOTTOM(linenum) ((int)(screen_height - (linenum) * UI_POS_DEFAULT_HEIGHT))
// px position of the linenum-th character from the left of the screen (X)
#define UI_POS_X(charnum) ((int)((charnum)*UI_POS_DEFAULT_WIDTH))
#define UI_POS_X(charnum) ((int)((charnum) * UI_POS_DEFAULT_WIDTH))
// px position of the linenum-th character from the right of the screen (X)
#define UI_POS_X_RIGHT(charnum) ((int)(screen_width - ((charnum)*UI_POS_DEFAULT_WIDTH)))
#define UI_POS_X_RIGHT(charnum) ((int)(screen_width - ((charnum) * UI_POS_DEFAULT_WIDTH)))
// px position of the left character from the center of the screen (X) (for N character wide string)
#define UI_POS_X_CENTER(charnum) ((int)((screen_width / 2) - ((charnum)*UI_POS_DEFAULT_WIDTH / 2)))
#define UI_POS_X_CENTER(charnum) ((int)((screen_width / 2) - ((charnum) * UI_POS_DEFAULT_WIDTH / 2)))
// px position of the currcol in a table with colnum number of columns, where one coloumn is charnum characters wide maximum
#define UI_POS_X_TABLE(colnum, currcol) ((currcol) * (screen_width / (colnum)))
// px width of N characters
#define UI_POS_WIDTH(charnum) ((int)((charnum)*UI_POS_DEFAULT_WIDTH))
#define UI_POS_WIDTH(charnum) ((int)((charnum) * UI_POS_DEFAULT_WIDTH))
// px width of the screen
#define UI_POS_MAXWIDTH (screen_width)
// px height of N line
#define UI_POS_HEIGHT(linecount) ((int)((linecount)*UI_POS_DEFAULT_HEIGHT))
#define UI_POS_HEIGHT(linecount) ((int)((linecount) * UI_POS_DEFAULT_HEIGHT))
// px height of the screen's percent
#define UI_POS_HEIGHT_PERCENT(percent) ((int)(screen_height * (percent) / 100))
// remaining px from the linenum-th line to the bottom of the screen. (please calculate the +1 line top-bar to it too if that is visible!)
#define UI_POS_HEIGHT_REMAINING(linenum) ((int)(screen_height - ((linenum)*UI_POS_DEFAULT_HEIGHT)))
#define UI_POS_HEIGHT_REMAINING(linenum) ((int)(screen_height - ((linenum) * UI_POS_DEFAULT_HEIGHT)))
// remaining px from the charnum-th character to the right of the screen
#define UI_POS_WIDTH_REMAINING(charnum) ((int)(screen_width - ((charnum)*UI_POS_DEFAULT_WIDTH)))
#define UI_POS_WIDTH_REMAINING(charnum) ((int)(screen_width - ((charnum) * UI_POS_DEFAULT_WIDTH)))
// px width of the screen's percent
#define UI_POS_WIDTH_PERCENT(percent) ((int)(screen_width * (percent) / 100))
// px width of the screen
+1 -1
View File
@@ -65,7 +65,7 @@ class Widget;
class Painter {
public:
Painter(){};
Painter() {};
Painter(const Painter&) = delete;
Painter(Painter&&) = delete;
+2 -2
View File
@@ -343,8 +343,8 @@ class WM8731 : public audio::Codec {
headphone_mute();
}
void speaker_enable(){};
void speaker_disable(){};
void speaker_enable() {};
void speaker_disable() {};
bool speaker_disable_supported() const override {
return false;
}
+48 -1
View File
@@ -173,7 +173,7 @@ bool BMPFile::read_next_px(ui::Color& px, bool seek = true) {
//*a = (val >> 15) & 0x01; // 1-bit alpha
uint8_t r = (val >> 10) & 0x1F; // 5-bit red
uint8_t g = (val >> 5) & 0x1F; // 5-bit green
uint8_t b = (val)&0x1F; // 5-bit blue
uint8_t b = (val) & 0x1F; // 5-bit blue
// expand
r = (r << 3) | (r >> 2);
g = (g << 3) | (g >> 2);
@@ -200,6 +200,53 @@ bool BMPFile::read_next_px(ui::Color& px, bool seek = true) {
return true;
}
bool BMPFile::read_next_px_cnt(ui::Color* px, uint32_t count, bool seek) {
if (!is_opened) return false;
size_t bytesneeded = byte_per_px * count;
while (bytesneeded > 0) { // read in batches
size_t currusedbytes = bytesneeded > 256 ? 85 * byte_per_px : bytesneeded; // don't mind this magic number.
uint8_t buffer[currusedbytes];
auto res = bmpimage.read(buffer, currusedbytes);
if (res.is_error()) return false;
for (uint32_t i = 0; i < currusedbytes; i += byte_per_px, px++) {
switch (type) {
case 5: {
// ARGB1555
uint16_t val = buffer[i] | (buffer[i + 1] << 8);
// Extract components
//*a = (val >> 15) & 0x01; // 1-bit alpha
uint8_t r = (val >> 10) & 0x1F; // 5-bit red
uint8_t g = (val >> 5) & 0x1F; // 5-bit green
uint8_t b = (val) & 0x1F; // 5-bit blue
// expand
r = (r << 3) | (r >> 2);
g = (g << 3) | (g >> 2);
b = (b << 3) | (b >> 2);
*px = ui::Color(r, g, b);
break;
}
case 2: // 32
*px = ui::Color(buffer[i + 2], buffer[i + 1], buffer[i]);
break;
case 4: { // 8-bit
// uint8_t index = buffer[0];
// px = ui::Color(color_palette[index][2], color_palette[index][1], color_palette[index][0]); // Palette is BGR
// px = ui::Color(buffer[0]); // niy, since needs a lot of ram for the palette
break;
}
case 1: // 24
default:
*px = ui::Color(buffer[i + 2], buffer[i + 1], buffer[i]);
break;
}
}
bytesneeded -= currusedbytes;
}
if (seek) advance_curr_px(count);
return true;
}
// if you set this, then the expanded part (or the newly created) will be filled with this color. but the expansion or the creation will be slower.
void BMPFile::set_bg_color(ui::Color background) {
bg = background;
@@ -42,6 +42,7 @@ class BMPFile {
uint32_t getbpr() { return byte_per_row; };
bool read_next_px(ui::Color& px, bool seek);
bool read_next_px_cnt(ui::Color* px, uint32_t count, bool seek);
bool write_next_px(ui::Color& px);
uint32_t get_real_height();
uint32_t get_width();
+15 -7
View File
@@ -451,14 +451,22 @@ bool GeoMap::draw_osm_file(int zoom, int tile_x, int tile_y, int relative_x, int
return false;
}
std::vector<ui::Color> line(clip_w);
for (int y = 0; y < clip_h; ++y) {
int source_row = src_y + y;
int dest_row = dest_y + y;
bmp.seek(src_x, source_row);
for (int x = 0; x < clip_w; ++x) {
bmp.read_next_px(line[x], true);
if (bmp.is_bottomup()) {
for (int y = clip_h - 1; y >= 0; --y) {
int source_row = src_y + y;
int dest_row = dest_y + y;
bmp.seek(src_x, source_row);
bmp.read_next_px_cnt(line.data(), clip_w, false);
painter.draw_pixels({dest_x + r.left(), dest_row + r.top(), clip_w, 1}, line);
}
} else {
for (int y = 0; y < clip_h; ++y) {
int source_row = src_y + y;
int dest_row = dest_y + y;
bmp.seek(src_x, source_row);
bmp.read_next_px_cnt(line.data(), clip_w, false);
painter.draw_pixels({dest_x + r.left(), dest_row + r.top(), clip_w, 1}, line);
}
painter.draw_pixels({dest_x + r.left(), dest_row + r.top(), clip_w, 1}, line);
}
return true;
}
+1
View File
@@ -25,6 +25,7 @@
bool notouch(int, int, uint32_t) {
// do nothing
return false;
}
void nothing() {
// do nothing
+1 -1
View File
@@ -1,2 +1,2 @@
#!/bin/sh
find firmware/common firmware/baseband firmware/application firmware/test/application firmware/test/baseband -iname '*.h' -o -iname '*.hpp' -o -iname '*.cpp' -o -iname '*.c' | xargs clang-format-13 -style=file -i
find firmware/common firmware/baseband firmware/application firmware/test/application firmware/test/baseband -iname '*.h' -o -iname '*.hpp' -o -iname '*.cpp' -o -iname '*.c' | xargs clang-format-18 -style=file -i
+1 -1
Submodule hackrf updated: cf6815aaf9...c0b15549cb