Compare commits

...

13 Commits

Author SHA1 Message Date
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
69 changed files with 3735 additions and 163 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.3.0
v2.3.1
+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;
+10
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,10 @@ 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
)
set(EXTAPPLIST
@@ -290,6 +298,7 @@ set(EXTAPPLIST
adsbtx
morse_tx
sstvtx
sstvrx
random_password
# acars_rx --not working
wefax_rx
@@ -330,4 +339,5 @@ set(EXTAPPLIST
bht_tx
morse_practice
adult_toys_controller
flex_rx
)
+13
View File
@@ -86,6 +86,8 @@ 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
}
@@ -471,5 +473,16 @@ 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
}
+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__
+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;
+14
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
+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;
+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;
+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
+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;
+113 -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,14 @@ class Message {
NoaaAptRxImageData = 79,
FSKPacket = 80,
EPIRBPacket = 81,
FlexPacket = 82,
FlexStats = 83,
FlexConfigure = 84,
FlexDebug = 85,
SSTVRXConfigure = 86,
SSTVRXProgress = 87,
SSTVRXPhaseSlant = 88,
SSTVRXCalibration = 89,
MAX
};
@@ -1139,6 +1148,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 +1252,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 +1634,49 @@ 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];
};
#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;
};
+2
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'};
@@ -121,6 +122,7 @@ constexpr image_tag_t image_tag_subghzd{'P', 'S', 'G', '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