Compare commits

..

23 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
gullradriel fd7637187a updated version number (#2855)
Co-authored-by: gullradriel <gullradriel@no-mail.com>
2025-11-07 20:52:28 +01:00
Totoo 674b865ec0 Refactor OnTouchEvent functions to return boolean values for better event handling (#2854) 2025-11-05 20:53:23 +01:00
Totoo 00c9c6b40e Fix shift key position (#2850) 2025-10-31 18:25:23 +01:00
RocketGod 113cbc42c9 Add sound effects (#2849)
* Add sound effects

Adding sound effects and volume adjuster in start menu. Sounds are a little clicky but I tinkered a long time and couldn't solve that. Maybe someone else wants to tinker a bit with me.

* Format code
2025-10-30 07:38:45 +01:00
RocketGod 454b8776b6 Refactor gfxEQView UI layout for consistency (#2848)
* Refactor gfxEQView UI layout for consistency

Updated control and graph positioning in gfxEQView to use a standard layout approach, aligning with other applications. Screen dimensions are now used for dynamic placement, and hardcoded values have been replaced for improved maintainability and consistency.

Basically, centered for smaller and larger screens with other visual improvements.

* Fixed screen to Macros - looks good now

Better methods for dynamic screen size
2025-10-30 07:33:38 +01:00
Matej Sochan efdf043d62 fix typo: wps -> wpm in morse_tx (#2847) 2025-10-28 20:55:21 +01:00
gullradriel 0f9d9e589d Revert "Additional debounce control parameters for rotary encoder settings (f…" (#2846)
This reverts commit bf18851b6b.
2025-10-28 22:34:14 +13:00
Totoo 3b57672dda sht4x support,, typo fix (#2843) 2025-10-28 10:14:11 +08:00
Totoo b739dd883c Free up some fw space (#2844) 2025-10-28 08:10:09 +08:00
rmckay bf18851b6b Additional debounce control parameters for rotary encoder settings (for bad/noisy encoders) (#2841)
* Added debounce control options for rotary encoder settings

* ran format-code.sh to adjust whitespace

---------

Co-authored-by: Robert McKay <robert.mckay@ubermorgen.land>
2025-10-27 23:56:27 +01:00
103 changed files with 4328 additions and 319 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.1.0
v2.2.0
+1 -1
View File
@@ -1 +1 @@
v2.2.0
v2.3.1
-2
View File
@@ -192,7 +192,6 @@ set(CPPSRC
core_control.cpp
database.cpp
gradient.cpp
rfm69.cpp
event_m0.cpp
file_reader.cpp
file.cpp
@@ -261,7 +260,6 @@ set(CPPSRC
ui/ui_receiver.cpp
ui/ui_rssi.cpp
ui/ui_freqlist.cpp
ui/ui_tv.cpp
ui/ui_spectrum.cpp
ui/ui_tabview.cpp
ui/ui_textentry.cpp
+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 ****",
" ",
+1 -1
View File
@@ -171,7 +171,7 @@ void IQTrimView::update_range_controls(iq::TrimRange trim_range) {
}
void IQTrimView::profile_capture() {
power_buckets_ = {};
power_buckets_.clear();
iq::PowerBuckets buckets{
.p = power_buckets_.data(),
.size = power_buckets_.size()};
+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,
@@ -285,9 +285,9 @@ bool StandaloneView::on_encoder(const EncoderEvent event) {
bool StandaloneView::on_touch(const TouchEvent event) {
if (get_application_information()->header_version > 1) {
get_application_information()->OnTouchEvent(event.point.x(), event.point.y(), (uint32_t)event.type);
return get_application_information()->OnTouchEvent(event.point.x(), event.point.y(), (uint32_t)event.type);
}
return true;
return false;
}
bool StandaloneView::on_keyboard(const KeyboardEvent event) {
+1 -1
View File
@@ -339,7 +339,7 @@ void SubGhzDRecentEntryDetailView::parseProtocol() {
if (entry_.sensorType == FPS_CAMEATOMO) {
entry_.data ^= 0xFFFFFFFFFFFFFFFF;
entry_.data <<= 4;
uint8_t pack[8] = {};
uint8_t pack[8];
pack[0] = (entry_.data >> 56);
pack[1] = ((entry_.data >> 48) & 0xFF);
pack[2] = ((entry_.data >> 40) & 0xFF);
+1 -1
View File
@@ -612,7 +612,7 @@ void TextEditorView::open_file(const fs::path& path) {
viewer.clear_file();
delete_temp_file(path_);
path_ = {};
path_ = "";
file_dirty_ = false;
has_temp_file_ = false;
auto result = FileWrapper::open(
+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)},
@@ -34,7 +34,7 @@ using namespace portapack;
#include <cmath>
#include <array>
namespace ui {
namespace ui::external_app::analogtv {
namespace tv {
/* TimeScopeView******************************************************/
@@ -235,4 +235,4 @@ void TVWidget::on_audio_spectrum() {
}
} /* namespace tv */
} /* namespace ui */
} // namespace ui::external_app::analogtv
@@ -33,7 +33,7 @@
#include <cstdint>
#include <cstddef>
namespace ui {
namespace ui::external_app::analogtv {
namespace tv {
class TimeScopeView : public View {
@@ -167,6 +167,6 @@ class TVWidget : public View {
};
} /* namespace tv */
} /* namespace ui */
} // namespace ui::external_app::analogtv
#endif /*__UI_TV_H__*/
-1
View File
@@ -21,7 +21,6 @@
*/
#include "bht.hpp"
#include "portapack_persistent_memory.hpp"
namespace ui::external_app::bht_tx {
+1 -1
View File
@@ -207,7 +207,7 @@ class BHTView : public View {
"Scan"};
Checkbox checkbox_flashing{
{16 * 8, 25 * 8},
{UI_POS_X(16), 25 * 8},
8,
"Flashing"};
NumberField field_speed{
+12 -12
View File
@@ -20,13 +20,13 @@ void initialize_app(ui::NavigationView& nav) {
extern "C" {
__attribute__((section(".external_app.app_breakout.application_information"), used)) application_information_t _application_information_breakout = {
(uint8_t*)0x00000000,
ui::external_app::breakout::initialize_app,
CURRENT_HEADER_VERSION,
VERSION_MD5,
/*.memory_location = */ (uint8_t*)0x00000000,
/*.externalAppEntry = */ ui::external_app::breakout::initialize_app,
/*.header_version = */ CURRENT_HEADER_VERSION,
/*.app_version = */ VERSION_MD5,
"Breakout",
{
/*.app_name = */ "Breakout",
/*.bitmap_data = */ {
0x00,
0x00,
0x7F,
@@ -60,11 +60,11 @@ __attribute__((section(".external_app.app_breakout.application_information"), us
0xF7,
0xF7,
},
ui::Color::green().v,
app_location_t::GAMES,
-1,
/*.icon_color = */ ui::Color::green().v,
/*.menu_location = */ app_location_t::GAMES,
/*.desired_menu_position = */ -1,
{0, 0, 0, 0},
0x00000000,
/*.m4_app_tag = */ {'P', 'A', 'B', 'P'}, // This is the audio beep baseband tag!
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
};
} // namespace ui::external_app::breakout
}
+279 -10
View File
@@ -7,6 +7,12 @@
*/
#include "ui_breakout.hpp"
#include "baseband_api.hpp"
#include "audio.hpp"
#include "portapack.hpp"
#include "portapack_persistent_memory.hpp"
using namespace portapack;
namespace ui::external_app::breakout {
@@ -92,6 +98,37 @@ void BreakoutView::init_game() {
game_state = STATE_MENU;
menu_blink_state = true;
menu_blink_counter = 0;
// Play startup sound
if (sound_enabled) {
baseband::request_audio_beep(262, 24000, 150); // C4 -
chThdSleepMilliseconds(200);
baseband::request_audio_beep(262, 24000, 150); // C4
chThdSleepMilliseconds(200);
baseband::request_audio_beep(392, 24000, 150); // G4
chThdSleepMilliseconds(200);
baseband::request_audio_beep(392, 24000, 150); // G4
chThdSleepMilliseconds(200);
baseband::request_audio_beep(440, 24000, 150); // A4
chThdSleepMilliseconds(200);
baseband::request_audio_beep(440, 24000, 150); // A4
chThdSleepMilliseconds(200);
baseband::request_audio_beep(392, 24000, 300); // G4
chThdSleepMilliseconds(350);
baseband::request_audio_beep(349, 24000, 150); // F4
chThdSleepMilliseconds(200);
baseband::request_audio_beep(349, 24000, 150); // F4
chThdSleepMilliseconds(200);
baseband::request_audio_beep(330, 24000, 150); // E4
chThdSleepMilliseconds(200);
baseband::request_audio_beep(330, 24000, 150); // E4
chThdSleepMilliseconds(200);
baseband::request_audio_beep(294, 24000, 150); // D4
chThdSleepMilliseconds(200);
baseband::request_audio_beep(294, 24000, 150); // D4
chThdSleepMilliseconds(200);
baseband::request_audio_beep(262, 24000, 400); // C4
}
}
void BreakoutView::init_level() {
@@ -126,6 +163,34 @@ void BreakoutView::draw_screen() {
draw_level();
}
void BreakoutView::draw_menu_volume_bar() {
// Clear the entire volume area first to handle bar shrinking
painter.fill_rectangle({40, 259, 220, 40}, Color::black());
// Draw volume label
auto style = *ui::Theme::getInstance()->fg_light;
painter.draw_string({40, 260}, style, "Volume:");
// Draw volume bar background
painter.draw_rectangle({100, 259, 102, 10}, Color::grey());
// Draw volume level
int bar_width = (volume_level * 100) / 99;
if (bar_width > 0) {
painter.fill_rectangle({101, 260, bar_width, 8}, Color::green());
}
// Draw percentage
painter.draw_string({210, 260}, style, std::to_string(volume_level) + "%");
// Draw sound status
if (sound_enabled) {
painter.draw_string({UI_POS_X_CENTER(11), 280}, *ui::Theme::getInstance()->fg_green, "Sound: ON");
} else {
painter.draw_string({UI_POS_X_CENTER(11), 280}, *ui::Theme::getInstance()->fg_red, "Sound: OFF");
}
}
void BreakoutView::draw_borders() {
rect(0, GAME_AREA_TOP - 1, SCREEN_WIDTH, GAME_AREA_TOP, COLOR_BORDER);
}
@@ -166,6 +231,22 @@ void BreakoutView::draw_level() {
painter.draw_string({80, 30}, style, "Level: " + std::to_string(level));
}
void BreakoutView::adjust_volume(int delta) {
volume_level += delta;
if (volume_level < 0) volume_level = 0;
if (volume_level > 99) volume_level = 99;
// Update the actual audio volume using decibel conversion
// Map 0-99 to roughly -60dB to 0dB range
int db = (volume_level - 99) * 60 / 99; // Maps 0->-60dB, 99->0dB
audio::headphone::set_volume(volume_t::decibel(db));
// If we're in the menu, redraw the volume bar
if (game_state == STATE_MENU) {
draw_menu_volume_bar();
}
}
void BreakoutView::move_paddle_left() {
if (paddle_x > 0) {
fillrect(paddle_x, PADDLE_Y, paddle_x + PADDLE_WIDTH, PADDLE_Y + PADDLE_HEIGHT, COLOR_BACKGROUND);
@@ -231,19 +312,23 @@ void BreakoutView::update_game() {
if (ball_x < 0) {
ball_x = 0;
ball_dx = -ball_dx;
play_wall_hit();
} else if (ball_x > SCREEN_WIDTH - BALL_SIZE) {
ball_x = SCREEN_WIDTH - BALL_SIZE;
ball_dx = -ball_dx;
play_wall_hit();
}
if (ball_y < GAME_AREA_TOP) {
ball_y = GAME_AREA_TOP;
ball_dy = -ball_dy;
play_wall_hit();
}
// Bottom edge - lose life
if (ball_y > GAME_AREA_BOTTOM) {
lives--;
play_death_sound(); // Play death sound
draw_lives();
if (lives <= 0) {
handle_game_over();
@@ -262,6 +347,8 @@ void BreakoutView::update_game() {
ball_y = PADDLE_Y - BALL_SIZE;
ball_dy = -ball_dy;
play_paddle_hit();
// Add some angle based on hit position
float hit_position = (ball_x + (BALL_SIZE / 2)) - paddle_x;
float angle = (hit_position / PADDLE_WIDTH) - 0.5f;
@@ -301,6 +388,8 @@ bool BreakoutView::check_brick_collision(int row, int col) {
score += (5 - row) * 10;
draw_score();
play_brick_hit();
ball_dy = -ball_dy;
return true;
}
@@ -314,6 +403,8 @@ bool BreakoutView::check_level_complete() {
void BreakoutView::next_level() {
level++;
play_level_complete();
chThdSleepMilliseconds(500); // Give time for sound to play
init_level();
draw_screen();
}
@@ -322,9 +413,11 @@ void BreakoutView::handle_game_over() {
game_state = STATE_GAME_OVER;
gameover_blink_state = true;
gameover_blink_counter = 0;
play_game_over();
}
void BreakoutView::show_menu() {
// Only draw the static parts once
if (menu_blink_counter == 0) {
cls();
background(COLOR_BACKGROUND);
@@ -338,23 +431,30 @@ void BreakoutView::show_menu() {
painter.draw_string({UI_POS_X_CENTER(20), 130}, style_cyan, "ROTARY: MOVE PADDLE");
painter.draw_string({UI_POS_X_CENTER(21), 160}, style_cyan, "SELECT: START/LAUNCH");
painter.draw_string({UI_POS_X_CENTER(25), 190}, style_blue, "========================");
// Volume adjustment instructions
painter.draw_string({UI_POS_X_CENTER(24), 220}, style_cyan, "Press UP/DOWN for Volume");
// Draw volume control at bottom
draw_menu_volume_bar();
}
auto style_red = *ui::Theme::getInstance()->fg_red;
// Only flash the "PRESS SELECT" text
if (++menu_blink_counter >= 30) {
menu_blink_counter = 0;
menu_blink_counter = 1; // Keep it at 1 so we don't redraw everything
menu_blink_state = !menu_blink_state;
painter.fill_rectangle({UI_POS_X_CENTER(17), 228, 128, 20}, Color::black());
auto style_red = *ui::Theme::getInstance()->fg_red;
painter.fill_rectangle({UI_POS_X_CENTER(17), 238, 128, 20}, Color::black());
if (menu_blink_state) {
painter.draw_string({UI_POS_X_CENTER(17), 230}, style_red, "* PRESS SELECT *");
painter.draw_string({UI_POS_X_CENTER(17), 240}, style_red, "* PRESS SELECT *");
}
}
}
void BreakoutView::show_game_over() {
// Only draw the static parts once
if (gameover_blink_counter == 0) {
cls();
background(COLOR_BACKGROUND);
@@ -366,12 +466,12 @@ void BreakoutView::show_game_over() {
painter.draw_string({UI_POS_X_CENTER(11), 150}, style_yellow, "SCORE: " + std::to_string(score));
}
auto style_green = *ui::Theme::getInstance()->fg_green;
// Only flash the "PRESS SELECT" text
if (++gameover_blink_counter >= 30) {
gameover_blink_counter = 0;
gameover_blink_counter = 1; // Keep it at 1 so we don't redraw everything
gameover_blink_state = !gameover_blink_state;
auto style_green = *ui::Theme::getInstance()->fg_green;
painter.fill_rectangle({UI_POS_X_CENTER(13), 198, 96, 20}, Color::black());
if (gameover_blink_state) {
@@ -385,17 +485,169 @@ void BreakoutView::reset_game() {
score = 0;
lives = 3;
game_state = STATE_PLAYING;
dummy.focus();
init_level();
draw_screen();
}
// Sound effect implementations with timing control
void BreakoutView::play_paddle_hit() {
if (sound_enabled) {
uint32_t current_time = chTimeNow();
if (current_time - last_sound_time >= min_sound_interval) {
baseband::request_beep_stop(); // Stop any previous sound
chThdSleepMilliseconds(5); // Small delay
baseband::request_audio_beep(440, 24000, 40); // 440Hz A4, 40ms
last_sound_time = current_time;
}
}
}
void BreakoutView::play_brick_hit() {
if (sound_enabled) {
uint32_t current_time = chTimeNow();
if (current_time - last_sound_time >= min_sound_interval) {
baseband::request_beep_stop(); // Stop any previous sound
chThdSleepMilliseconds(5); // Small delay
// Higher pitch that varies with remaining bricks
uint32_t freq = 880 + ((25 - brick_count) * 20); // Gets higher as bricks are destroyed
baseband::request_audio_beep(freq, 24000, 30);
last_sound_time = current_time;
}
}
}
void BreakoutView::play_wall_hit() {
if (sound_enabled) {
uint32_t current_time = chTimeNow();
if (current_time - last_sound_time >= min_sound_interval) {
baseband::request_beep_stop(); // Stop any previous sound
chThdSleepMilliseconds(5); // Small delay
baseband::request_audio_beep(220, 24000, 120); // 220Hz A3, 120ms
last_sound_time = current_time;
}
}
}
void BreakoutView::play_death_sound() {
if (sound_enabled) {
baseband::request_audio_beep(493, 24000, 150); // B4
chThdSleepMilliseconds(180);
baseband::request_audio_beep(466, 24000, 150); // Bb4
chThdSleepMilliseconds(180);
baseband::request_audio_beep(440, 24000, 150); // A4
chThdSleepMilliseconds(180);
baseband::request_audio_beep(415, 24000, 150); // Ab4
chThdSleepMilliseconds(180);
baseband::request_audio_beep(392, 24000, 150); // G4
chThdSleepMilliseconds(180);
baseband::request_audio_beep(369, 24000, 150); // Gb4
chThdSleepMilliseconds(180);
baseband::request_audio_beep(349, 24000, 150); // F4
chThdSleepMilliseconds(180);
baseband::request_audio_beep(329, 24000, 150); // E4
chThdSleepMilliseconds(180);
baseband::request_audio_beep(293, 24000, 150); // D4
chThdSleepMilliseconds(180);
baseband::request_audio_beep(261, 24000, 150); // C4
chThdSleepMilliseconds(180);
baseband::request_audio_beep(196, 24000, 200); // G3
chThdSleepMilliseconds(230);
baseband::request_audio_beep(130, 24000, 300); // C3
}
}
void BreakoutView::play_game_over() {
if (sound_enabled) {
// First phrase
baseband::request_audio_beep(392, 24000, 300); // G4
chThdSleepMilliseconds(350);
baseband::request_audio_beep(392, 24000, 150); // G4
chThdSleepMilliseconds(200);
baseband::request_audio_beep(392, 24000, 150); // G4
chThdSleepMilliseconds(200);
baseband::request_audio_beep(311, 24000, 500); // Eb4 - long
chThdSleepMilliseconds(600);
// Second phrase
baseband::request_audio_beep(349, 24000, 300); // F4
chThdSleepMilliseconds(350);
baseband::request_audio_beep(349, 24000, 150); // F4
chThdSleepMilliseconds(200);
baseband::request_audio_beep(349, 24000, 150); // F4
chThdSleepMilliseconds(200);
baseband::request_audio_beep(294, 24000, 500); // D4 - long
chThdSleepMilliseconds(600);
// Final phrase - slower and more dramatic
baseband::request_audio_beep(262, 24000, 200); // C4
chThdSleepMilliseconds(250);
baseband::request_audio_beep(247, 24000, 200); // B3
chThdSleepMilliseconds(250);
baseband::request_audio_beep(233, 24000, 200); // Bb3
chThdSleepMilliseconds(250);
baseband::request_audio_beep(220, 24000, 200); // A3
chThdSleepMilliseconds(250);
baseband::request_audio_beep(196, 24000, 200); // G3
chThdSleepMilliseconds(250);
baseband::request_audio_beep(175, 24000, 200); // F3
chThdSleepMilliseconds(250);
baseband::request_audio_beep(131, 24000, 600); // C3
}
}
void BreakoutView::play_level_complete() {
if (sound_enabled) {
baseband::request_beep_stop(); // Stop any previous sound
chThdSleepMilliseconds(10);
baseband::request_audio_beep(1047, 24000, 300); // 1047Hz C6, 300ms
}
}
BreakoutView::BreakoutView(NavigationView& nav)
: nav_{nav}, bricks{} { // Add bricks{} here
: nav_{nav}, bricks{} {
// Initialize audio system for beeps FIRST
baseband::run_prepared_image(portapack::memory::map::m4_code.base()); // Load the audio beep baseband
add_children({&dummy});
// Initialize audio
audio::set_rate(audio::Rate::Hz_24000);
audio::output::start();
// Set initial volume to 80% and ensure sound is ON
volume_level = 80;
sound_enabled = true; // Make sure this is explicitly set to ON
// Set the actual volume
int db = (volume_level - 99) * 60 / 99; // Maps to roughly -12dB for 80%
audio::headphone::set_volume(volume_t::decibel(db));
attach(1.0 / 60.0);
}
BreakoutView::~BreakoutView() {
// Clean shutdown
baseband::request_beep_stop();
receiver_model.disable();
baseband::shutdown();
audio::output::stop();
}
void BreakoutView::on_show() {
// Nothing needed here, initialization done in constructor
}
void BreakoutView::on_hide() {
// Additional cleanup
baseband::request_beep_stop();
audio::output::stop();
}
void BreakoutView::focus() {
dummy.focus();
}
void BreakoutView::paint(Painter& painter) {
@@ -433,16 +685,33 @@ bool BreakoutView::on_key(const KeyEvent key) {
} else if (game_state == STATE_PLAYING && ball_attached) {
launch_ball();
} else if (game_state == STATE_GAME_OVER) {
reset_game();
// Return to menu after game over
game_state = STATE_MENU;
menu_blink_counter = 0;
init_game();
}
} else if (key == KeyEvent::Left && game_state == STATE_PLAYING) {
move_paddle_left();
} else if (key == KeyEvent::Right && game_state == STATE_PLAYING) {
move_paddle_right();
} else if (key == KeyEvent::Up && game_state == STATE_MENU) {
// Only adjust volume in menu
adjust_volume(5);
if (!sound_enabled) sound_enabled = true;
set_dirty();
} else if (key == KeyEvent::Down && game_state == STATE_MENU) {
// Only adjust volume in menu
adjust_volume(-5);
set_dirty();
}
set_dirty();
return true;
}
bool BreakoutView::on_touch(const TouchEvent event) {
(void)event; // Intentionally unused - touch disabled to avoid nav bar issues
return true;
}
} // namespace ui::external_app::breakout
+21 -1
View File
@@ -11,6 +11,7 @@
#include "ui.hpp"
#include "ui_navigation.hpp"
#include "ui_receiver.hpp"
#include "event_m0.hpp"
#include "message.hpp"
#include "irq_controls.hpp"
@@ -56,15 +57,18 @@ enum {
class BreakoutView : public View {
public:
BreakoutView(NavigationView& nav);
~BreakoutView();
void on_show() override;
void on_hide() override;
std::string title() const override { return "Breakout"; }
void focus() override { dummy.focus(); }
void focus() override;
void paint(Painter& painter) override;
void frame_sync();
bool on_encoder(const EncoderEvent event) override;
bool on_key(KeyEvent key) override;
bool on_touch(const TouchEvent event) override;
void cls();
void background(int color);
@@ -81,6 +85,7 @@ class BreakoutView : public View {
void draw_lives();
void draw_level();
void draw_borders();
void draw_menu_volume_bar();
void move_paddle_left();
void move_paddle_right();
void launch_ball();
@@ -96,6 +101,15 @@ class BreakoutView : public View {
void check_game_timer();
void attach(double delay_sec);
void detach();
void adjust_volume(int delta);
// Sound effect methods
void play_paddle_hit();
void play_brick_hit();
void play_wall_hit();
void play_death_sound();
void play_game_over();
void play_level_complete();
private:
const Color pp_colors[9] = {
@@ -147,6 +161,12 @@ class BreakoutView : public View {
double game_update_timeout = 0;
uint32_t game_update_counter = 0;
// Audio control
bool sound_enabled = true;
int volume_level = 80; // 0-99
uint32_t last_sound_time = 0;
uint32_t min_sound_interval = 20; // Minimum ms between sounds
Button dummy{
{screen_width, 0, 0, 0},
""};
+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;
+85 -73
View File
@@ -1,267 +1,277 @@
set(EXTCPPSRC
#afsk_rx
#afsk_rx 16 byte
external/afsk_rx/main.cpp
external/afsk_rx/ui_afsk_rx.cpp
#calculator
#calculator 632 bytes
external/calculator/main.cpp
external/calculator/ui_calculator.cpp
#font_viewer
#font_viewer 8 byte?!
external/font_viewer/main.cpp
external/font_viewer/ui_font_viewer.cpp
#blespam
#blespam 336 bytes - array initializers?
external/blespam/main.cpp
external/blespam/ui_blespam.cpp
#analogtv
#analogtv 552 bytes
external/analogtv/main.cpp
external/analogtv/analog_tv_app.cpp
external/analogtv/ui_tv.cpp
#nrf_rx
#nrf_rx 40 byte
external/nrf_rx/main.cpp
external/nrf_rx/ui_nrf_rx.cpp
#coasterp
#coasterp 0 byte
external/coasterp/main.cpp
external/coasterp/ui_coasterp.cpp
#lge
#lge 120 byte
external/lge/main.cpp
external/lge/lge_app.cpp
external/lge/rfm69.cpp
#lcr
#lcr - 460 byte flash
external/lcr/main.cpp
external/lcr/ui_lcr.cpp
#jammer
#jammer 144 byte
external/jammer/main.cpp
external/jammer/ui_jammer.cpp
#gpssim
#gpssim 160 byte
external/gpssim/main.cpp
external/gpssim/gps_sim_app.cpp
#spainter
#spainter 464 byte
external/spainter/main.cpp
external/spainter/ui_spectrum_painter.cpp
external/spainter/ui_spectrum_painter_text.cpp
external/spainter/ui_spectrum_painter_image.cpp
#keyfob
#keyfob 216 byte
external/keyfob/main.cpp
external/keyfob/ui_keyfob.cpp
external/keyfob/ui_keyfob.hpp
#tetris
#tetris 88 byte
external/tetris/main.cpp
external/tetris/ui_tetris.cpp
#extsensors
#extsensors 192 byte
external/extsensors/main.cpp
external/extsensors/ui_extsensors.cpp
external/extsensors/ui_extsensors.hpp
#foxhunt
#foxhunt 0
external/foxhunt/main.cpp
external/foxhunt/ui_foxhunt_rx.cpp
external/foxhunt/ui_foxhunt_rx.hpp
#audio_test
#audio_test 192 byte
external/audio_test/main.cpp
external/audio_test/ui_audio_test.cpp
#wardrivemap
#wardrivemap 64 byte
external/wardrivemap/main.cpp
external/wardrivemap/ui_wardrivemap.cpp
#tpmsrx
#tpmsrx 920 byte- possible shared part with baseband
external/tpmsrx/main.cpp
external/tpmsrx/tpms_app.cpp
#protoview
#protoview 8 byte
external/protoview/main.cpp
external/protoview/ui_protoview.cpp
#adsbtx
#adsbtx 3544 byte - adsb shared part
external/adsbtx/main.cpp
external/adsbtx/ui_adsb_tx.cpp
#morse_tx
#morse_tx 768 bytes
external/morse_tx/main.cpp
external/morse_tx/ui_morse.cpp
#sstvtx
#sstvtx 456 bytes
external/sstvtx/main.cpp
external/sstvtx/ui_sstvtx.cpp
#random
#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
external/random_password/sha512.cpp
external/random_password/sha512.h
#acars
external/acars_rx/main.cpp
external/acars_rx/acars_app.cpp
#external/acars_rx/main.cpp
#external/acars_rx/acars_app.cpp
#wefax_rx
#wefax_rx 192 bytes
external/wefax_rx/main.cpp
external/wefax_rx/ui_wefax_rx.cpp
#noaaapt_rx
#noaaapt_rx 72 bytes
external/noaaapt_rx/main.cpp
external/noaaapt_rx/ui_noaaapt_rx.cpp
#shoppingcart_lock
#shoppingcart_lock 272 bytes
external/shoppingcart_lock/main.cpp
external/shoppingcart_lock/shoppingcart_lock.cpp
#ookbrute
#ookbrute 80 byte
external/ookbrute/main.cpp
external/ookbrute/ui_ookbrute.cpp
#ook_editor
#ook_editor 1808 bytes
external/ook_editor/main.cpp
external/ook_editor/ui_ook_editor.cpp
#cvs_spam
#cvs_spam 24 byte
external/cvs_spam/main.cpp
external/cvs_spam/cvs_spam.cpp
#flippertx
#flippertx 712 bytes
external/flippertx/main.cpp
external/flippertx/ui_flippertx.cpp
#remote
#remote 1664 bytes
external/remote/main.cpp
external/remote/ui_remote.cpp
#mcu_temperature
#mcu_temperature 112
external/mcu_temperature/main.cpp
external/mcu_temperature/mcu_temperature.cpp
#fmradio
#fmradio 640
external/fmradio/main.cpp
external/fmradio/ui_fmradio.cpp
#tuner
#tuner 384
external/tuner/main.cpp
external/tuner/ui_tuner.cpp
#metronome
#metronome 696 bytes
external/metronome/main.cpp
external/metronome/ui_metronome.cpp
#app_manager
#app_manager 40 bytes
external/app_manager/main.cpp
external/app_manager/ui_app_manager.cpp
#hopper
#hopper 472 bytes
external/hopper/main.cpp
external/hopper/ui_hopper.cpp
# whip calculator
# whip calculator 48 bytes
external/antenna_length/main.cpp
external/antenna_length/ui_whipcalc.cpp
# wav viewer
# wav viewer 1232 bytes
external/wav_view/main.cpp
external/wav_view/ui_view_wav.cpp
# wipe sdcard
# wipe sdcard 16 byte
external/sd_wipe/main.cpp
external/sd_wipe/ui_sd_wipe.cpp
# playlist editor
# playlist editor 232 bytes
external/playlist_editor/main.cpp
external/playlist_editor/ui_playlist_editor.cpp
#snake
#snake 240 bytes
external/snake/main.cpp
external/snake/ui_snake.cpp
#stopwatch
#stopwatch 0
external/stopwatch/main.cpp
external/stopwatch/ui_stopwatch.cpp
#breakout
#breakout 1144 bytes
external/breakout/main.cpp
external/breakout/ui_breakout.cpp
#dinogame
#dinogame 0
external/dinogame/main.cpp
external/dinogame/ui_dinogame.cpp
#doom
#doom 224
external/doom/main.cpp
external/doom/ui_doom.cpp
#debug_pmem
#debug_pmem 944 byte
external/debug_pmem/main.cpp
external/debug_pmem/ui_debug_pmem.cpp
#scanner
#scanner 520 byte
external/scanner/main.cpp
external/scanner/ui_scanner.cpp
#level
#level 24 byte
external/level/main.cpp
external/level/ui_level.cpp
#gfxEQ
#gfxEQ 80 byte
external/gfxeq/main.cpp
external/gfxeq/ui_gfxeq.cpp
#detector_rx
#detector_rx 168 byte
external/detector_rx/main.cpp
external/detector_rx/ui_detector_rx.cpp
#space_invaders
#space_invaders 0 byte
external/spaceinv/main.cpp
external/spaceinv/ui_spaceinv.cpp
#blackjack
#blackjack 24 byte
external/blackjack/main.cpp
external/blackjack/ui_blackjack.cpp
#battleship
#battleship 256 byte
external/battleship/main.cpp
external/battleship/ui_battleship.cpp
#ert
#ert 3040 bytes - has common with baseband, could be renamed the namespace, so both could have it, but not kept in fw.
external/ert/main.cpp
external/ert/ert_app.cpp
#epirb_rx
#epirb_rx 168 byte flash
external/epirb_rx/main.cpp
external/epirb_rx/ui_epirb_rx.cpp
#soundboard
#soundboard 272byte - 1236 bytes
external/soundboard/main.cpp
external/soundboard/soundboard_app.cpp
#game2048
#game2048 - 168 byte flash
external/game2048/main.cpp
external/game2048/ui_game2048.cpp
#bht_tx
#bht_tx - 3920 byte flash, unknown
external/bht_tx/main.cpp
external/bht_tx/ui_bht_tx.cpp
external/bht_tx/bht.cpp
#morse_practice
#morse_practice - 80 byte flash - bc of array initializers
external/morse_practice/main.cpp
external/morse_practice/ui_morse_practice.cpp
#adult_toys_controller
#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
@@ -269,8 +279,8 @@ set(EXTAPPLIST
calculator
font_viewer
blespam
nrf_rx
analogtv
nrf_rx
coasterp
lge
lcr
@@ -283,19 +293,20 @@ set(EXTAPPLIST
foxhunt_rx
audio_test
wardrivemap
cvs_spam
tpmsrx
protoview
adsbtx
morse_tx
sstvtx
sstvrx
random_password
# acars_rx
ookbrute
ook_editor
# acars_rx --not working
wefax_rx
noaaapt_rx
shoppingcart_lock
ookbrute
ook_editor
cvs_spam
flippertx
remote
mcu_temperature
@@ -328,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__*/
+1 -1
View File
@@ -79,7 +79,7 @@ class FlipperTxView : public View {
void on_tx_progress(const bool done);
bool on_file_changed(std::filesystem::path new_file_path);
std::filesystem::path filename = {};
std::filesystem::path filename = "";
FlipperProto proto = FLIPPER_PROTO_UNSUPPORTED;
FlipperPreset preset = FLIPPER_PRESET_UNK;
uint16_t te = 0; // for binraw
-1
View File
@@ -41,7 +41,6 @@ gfxEQView::gfxEQView(NavigationView& nav)
audio::set_rate(audio::Rate::Hz_48000);
audio::output::start();
receiver_model.set_headphone_volume(receiver_model.headphone_volume()); // WM8731 hack
//
receiver_model.enable();
receiver_model.set_target_frequency(frequency_value); // Retune to actual freq
+9 -7
View File
@@ -46,6 +46,7 @@ class gfxEQView : public View {
NavigationView& nav_;
uint32_t current_theme{0};
const std::array<ColorTheme, 20> themes{
ColorTheme{Color(255, 0, 255), Color(255, 255, 255)},
ColorTheme{Color(0, 255, 0), Color(255, 0, 0)},
@@ -68,13 +69,14 @@ class gfxEQView : public View {
ColorTheme{Color(64, 64, 64), Color(255, 0, 0)},
ColorTheme{Color(255, 192, 0), Color(0, 64, 128)}};
ButtonWithEncoder button_frequency{{UI_POS_X(0), UI_POS_Y(0) + 4, 11 * 8, 1 * 8}, ""};
RFAmpField field_rf_amp{{13 * 8, UI_POS_Y(0)}};
LNAGainField field_lna{{15 * 8, UI_POS_Y(0)}};
VGAGainField field_vga{{18 * 8, UI_POS_Y(0)}};
Button button_mood{{21 * 8, 0, 6 * 8, 16}, "MOOD"};
AudioVolumeField field_volume{{screen_width - 2 * 8, UI_POS_Y(0)}};
GraphEq gr{{2, UI_POS_DEFAULT_HEIGHT, UI_POS_MAXWIDTH - 4, UI_POS_HEIGHT_REMAINING(2)}, false};
// Widgets with positions using UI_POS macros
ButtonWithEncoder button_frequency{{UI_POS_X(0), UI_POS_Y(0) + 4, UI_POS_WIDTH(11), UI_POS_HEIGHT(1)}, ""};
RFAmpField field_rf_amp{{UI_POS_X(12), UI_POS_Y(0) + 4}};
LNAGainField field_lna{{UI_POS_X(14), UI_POS_Y(0) + 4}};
VGAGainField field_vga{{UI_POS_X(17), UI_POS_Y(0) + 4}};
Button button_mood{{UI_POS_X(21), UI_POS_Y(0) + 4, UI_POS_WIDTH(4), UI_POS_HEIGHT(1)}, "MOOD"};
AudioVolumeField field_volume{{UI_POS_X_RIGHT(2), UI_POS_Y(0) + 4}};
GraphEq gr{{2, UI_POS_Y(1) + 8, UI_POS_MAXWIDTH, UI_POS_Y_BOTTOM(1) - 28}, false};
rf::Frequency frequency_value{93100000};
@@ -24,6 +24,8 @@
#include "crc.hpp"
#include "portapack_shared_memory.hpp"
namespace ui::external_app::lge {
uint32_t RFM69::gen_frame(std::vector<uint8_t>& payload) {
CRC<16> crc{0x1021, 0x1D0F, 0xFFFF};
std::vector<uint8_t> frame{};
@@ -74,3 +76,5 @@ uint32_t RFM69::gen_frame(std::vector<uint8_t>& payload) {
// Copy for baseband
return frame.size();
}
} /* namespace ui::external_app::lge */
@@ -28,6 +28,8 @@
#include "utility.hpp"
namespace ui::external_app::lge {
class RFM69 {
public:
RFM69(const uint8_t num_preamble, const uint16_t sync_word, const bool CRC, const bool manchester)
@@ -54,4 +56,6 @@ class RFM69 {
bool manchester_{false};
};
} /* namespace ui::external_app::lge */
#endif /*__RFM69_H__*/
+1 -1
View File
@@ -110,7 +110,7 @@ class MorseView : public View {
bool run{false};
Labels labels{
{{4 * 8, 6 * 8}, "Speed: wps", Theme::getInstance()->fg_light->foreground},
{{4 * 8, 6 * 8}, "Speed: wpm", Theme::getInstance()->fg_light->foreground},
{{4 * 8, 8 * 8}, "Tone: Hz", Theme::getInstance()->fg_light->foreground},
{{4 * 8, 10 * 8}, "Modulation:", Theme::getInstance()->fg_light->foreground},
{{4 * 8, 12 * 8}, "Loop:", Theme::getInstance()->fg_light->foreground},
@@ -231,7 +231,7 @@ void RandomPasswordView::on_data(uint32_t value, bool is_data) {
}
void RandomPasswordView::clean_buffer() {
seeds_deque = {};
seeds_deque.clear();
}
void RandomPasswordView::on_freqchg(int64_t freq) {
@@ -247,6 +247,18 @@ void RandomPasswordView::set_random_freq() {
field_frequency.set_value(random_freq);
}
bool RandomPasswordView::islower(char c) {
return (c >= 'a' && c <= 'z');
}
bool RandomPasswordView::isupper(char c) {
return (c >= 'A' && c <= 'Z');
}
bool RandomPasswordView::isdigit(char c) {
return (c >= '0' && c <= '9');
}
void RandomPasswordView::new_password() {
if (seeds_deque.size() < MAX_DIGITS * 2) {
seeds_buffer_not_full = true;
@@ -261,21 +273,21 @@ void RandomPasswordView::new_password() {
int password_length = field_digits.value();
/// charset worker
if (check_digits.value())
charset += "0123456789";
if (check_latin_lower.value())
charset += "abcdefghijklmnopqrstuvwxyz";
if (check_latin_upper.value())
charset += "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
if (check_digits.value()) {
charset += "23456789";
if (check_allow_confusable_chars.value()) charset += "01";
}
if (check_latin_lower.value()) {
charset += "abcdefghijkmnpqrstuvwxyz";
if (check_allow_confusable_chars.value()) charset += "ol";
}
if (check_latin_upper.value()) {
charset += "ABCDEFGHIJKLMNPQRSTUVWXYZ";
if (check_allow_confusable_chars.value()) charset += "O";
}
if (check_punctuation.value())
charset += ".,-!?";
if (!check_allow_confusable_chars.value()) {
charset.erase(std::remove_if(charset.begin(), charset.end(),
[](char c) { return c == '0' || c == 'O' || c == 'o' || c == '1' || c == 'l'; }),
charset.end());
}
if (charset.empty()) {
text_generated_passwd.set("generate failed,");
text_char_type_hints.set("select at least 1 type");
@@ -306,11 +318,11 @@ void RandomPasswordView::new_password() {
/// hint text worker
for (char c : password_chars) {
password += c;
if (std::isdigit(c)) {
if (isdigit(c)) {
char_type_hints += "1";
} else if (std::islower(c)) {
} else if (islower(c)) {
char_type_hints += "a";
} else if (std::isupper(c)) {
} else if (isupper(c)) {
char_type_hints += "A";
} else {
char_type_hints += ",";
@@ -360,11 +372,11 @@ void RandomPasswordView::paint_password_hints() {
for (size_t i = 0; i < password.length(); i++) {
char c = password[i];
Color color;
if (std::isdigit(c)) {
if (isdigit(c)) {
color = Color::red();
} else if (std::islower(c)) {
} else if (islower(c)) {
color = Color::green();
} else if (std::isupper(c)) {
} else if (isupper(c)) {
color = Color::blue();
} else {
color = Color::white();
@@ -379,8 +391,8 @@ void RandomPasswordView::paint_password_hints() {
std::string RandomPasswordView::generate_log_line() {
std::string seeds_set = "";
for (auto seed : seeds_deque) {
seeds_set += std::to_string(seed);
for (unsigned int seed : seeds_deque) {
seeds_set += to_string_dec_uint(seed);
seeds_set += " ";
}
std::string line = "\npassword=" + password +
@@ -92,6 +92,10 @@ class RandomPasswordView : public View {
std::string generate_log_line();
void paint_password_hints();
bool islower(char c);
bool isupper(char c);
bool isdigit(char c);
NavigationView& nav_;
RxRadioState radio_state_{};
app_settings::SettingsManager settings_{
+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() {
+1 -1
View File
@@ -114,7 +114,7 @@ Optional<capture_metadata> read_metadata_file(const fs::path& path) {
}
bool parse_float_meta(std::string_view str, float& out_val) {
out_val = {};
out_val = 0.0f;
if (str.size() > max_parse_int_length)
return false;
+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__*/
+1 -1
View File
@@ -81,7 +81,7 @@ class AlphanumView : public TextEntryView {
std::array<Button, 29> buttons{};
NewButton button_shift{
{192, 214, screen_width / 5, 38},
{screen_width - screen_width / 5, 214, screen_width / 5, 38},
{},
&bitmap_icon_shift,
Theme::getInstance()->bg_dark->background,
+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;
+3 -4
View File
@@ -769,9 +769,9 @@ void add_apps(NavigationView& nav, BtnGridView& grid, app_location_t loc) {
}
// clang-format off
void add_external_items(NavigationView& nav, app_location_t location, BtnGridView& grid, uint8_t error_tile_pos) {
void add_external_items(NavigationView& nav, app_location_t location, BtnGridView& grid, uint8_t error_tile_pos, bool show_error_tile ) {
auto externalItems = ExternalItemsMenuLoader::load_external_items(location, nav);
if (externalItems.empty()) {
if (externalItems.empty() && show_error_tile) {
grid.insert_item({"ExtAppErr",
Theme::getInstance()->error_dark->foreground,
nullptr,
@@ -848,8 +848,7 @@ void TranceiversMenuView::on_populate() {
add_items({{"..", Theme::getInstance()->fg_light->foreground, &bitmap_icon_previous, [this]() { nav_.pop(); }}});
}
add_apps(nav_, *this, TRX);
// add_external_items(nav_, app_location_t::TRX, *this, return_icon ? 1 : 0);
// this folder doesn't have external apps, comment to prevent pop the err msg.
add_external_items(nav_, app_location_t::TRX, *this, return_icon ? 1 : 0, false); // this folder doesn't have external apps, so don't show error for it.
// NB: when has external app someday, uncomment this.
}
+1 -1
View File
@@ -61,7 +61,7 @@ using namespace sd_card;
namespace ui {
void add_apps(NavigationView& nav, BtnGridView& grid, app_location_t loc);
void add_external_items(NavigationView& nav, app_location_t location, BtnGridView& grid, uint8_t error_tile_pos);
void add_external_items(NavigationView& nav, app_location_t location, BtnGridView& grid, uint8_t error_tile_pos, bool show_error_tile = true);
bool verify_sdcard_format();
enum modal_t {
+2 -2
View File
@@ -175,7 +175,7 @@ void RecordView::start() {
text_record_filename.set("");
text_record_dropped.set("");
trim_path = {};
trim_path = "";
if (sampling_rate == 0) {
return;
@@ -348,7 +348,7 @@ void RecordView::trim_capture() {
trim_ui.clear();
}
trim_path = {};
trim_path = "";
}
void RecordView::on_gps(const GPSPosDataMessage* msg) {
+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
@@ -32,7 +32,7 @@ namespace i2cdev {
bool I2cDev_BH1750::init(uint8_t addr_) {
if (addr_ != I2CDEV_BH1750_ADDR_1) return false;
addr = addr_;
model = I2CDECMDL_BH1750;
model = I2CDEVMDL_BH1750;
query_interval = 3;
// power up
+61
View File
@@ -0,0 +1,61 @@
/*
* Copyright (C) 2024 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 "i2cdev_sht4x.hpp"
namespace i2cdev {
bool I2cDev_SHT4x::init(uint8_t addr_) {
if (addr_ != I2CDEV_SHT4X_ADDR_1 && addr_ != I2CDEV_SHT4X_ADDR_2) return false;
addr = addr_; // store the addr
model = I2C_DEVMDL::I2CDEVMDL_SHT4X; // set the device model!!!!!!!!!!!!!!!!!!
query_interval = 10; // set update interval in sec
chThdSleepMilliseconds(50);
uint8_t tmp[1]; // command buffer
uint8_t rx[6]; // response buffer
tmp[0] = SHT4X_SERIAL_NUMBER_CMD_ID;
if (!i2c_write(nullptr, 0, tmp, 1)) return false; // get serial number
chThdSleepMilliseconds(10);
if (!i2c_read(nullptr, 0, rx, 6)) return false; // read serial number
return true;
}
void I2cDev_SHT4x::update() {
float temp = 0;
float hum = 0;
uint8_t cmd[1];
cmd[0] = SHT4X_MEASURE_MEDIUM_PRECISION_TICKS_CMD_ID;
uint8_t res[6];
if (!i2c_write(nullptr, 0, cmd, 1)) return;
chThdSleepMilliseconds(5); // wait for measurement, 10 for high precision, 5 for medium, 2 for low
if (!i2c_read(nullptr, 0, res, 6)) return;
uint16_t temptick = (uint16_t)res[0] << 8 | (uint16_t)res[1];
uint16_t humtick = (uint16_t)res[3] << 8 | (uint16_t)res[4];
temp = -45 + (((float)temptick * 175.0) / 65535.0);
hum = -6 + 125.0 * (float)humtick / 65535.0;
if (hum < 0) hum = 0;
if (hum > 100) hum = 100;
EnvironmentDataMessage msg{temp, hum}; // create the system message
EventDispatcher::send_message(msg); // and send it
}
} // namespace i2cdev
+53
View File
@@ -0,0 +1,53 @@
/*
* 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.
*/
#ifndef __I2CDEV_SHT4X_H__
#define __I2CDEV_SHT4X_H__
#include "i2cdevmanager.hpp"
namespace i2cdev {
class I2cDev_SHT4x : public I2cDev {
public:
typedef enum {
SHT4X_MEASURE_HIGH_PRECISION_TICKS_CMD_ID = 0xfd,
SHT4X_MEASURE_MEDIUM_PRECISION_TICKS_CMD_ID = 0xf6,
SHT4X_MEASURE_LOWEST_PRECISION_TICKS_CMD_ID = 0xe0,
SHT4X_ACTIVATE_HIGHEST_HEATER_POWER_LONG_TICKS_CMD_ID = 0x39,
SHT4X_ACTIVATE_HIGHEST_HEATER_POWER_SHORT_TICKS_CMD_ID = 0x32,
SHT4X_ACTIVATE_MEDIUM_HEATER_POWER_LONG_TICKS_CMD_ID = 0x2f,
SHT4X_ACTIVATE_MEDIUM_HEATER_POWER_SHORT_TICKS_CMD_ID = 0x24,
SHT4X_ACTIVATE_LOWEST_HEATER_POWER_LONG_TICKS_CMD_ID = 0x1e,
SHT4X_ACTIVATE_LOWEST_HEATER_POWER_SHORT_TICKS_CMD_ID = 0x15,
SHT4X_SERIAL_NUMBER_CMD_ID = 0x89,
SHT4X_SOFT_RESET_CMD_ID = 0x94,
} SHT4xCmdId;
bool init(uint8_t addr_) override; // sets the addr to our local variable, set the model, try to init the module, and only return true if it is really that module, and inited ok
void update() override; // query the module for recent data, and send it to the system via the corresponding Message
private:
float read_temperature();
float read_humidity();
};
} // namespace i2cdev
#endif
+5 -1
View File
@@ -34,14 +34,18 @@ enum I2C_DEVMDL {
I2CDEVMDL_SHT3X,
I2CDEVMDL_BMP280,
I2CDEVMDL_BME280,
I2CDECMDL_BH1750,
I2CDEVMDL_BH1750,
I2CDECMDL_PPMOD,
I2CDEVMDL_SHT4X,
};
#define I2CDEV_BMX280_ADDR_1 0x76
#define I2CDEV_BMX280_ADDR_2 0x77
#define I2CDEV_SHT3X_ADDR_1 0x44
#define I2CDEV_SHT3X_ADDR_2 0x45
// sadly sht4x uses the same addr as sht3x, but the init script can decide which is which
#define I2CDEV_SHT4X_ADDR_1 0x44
#define I2CDEV_SHT4X_ADDR_2 0x45
#define I2CDEV_MAX17055_ADDR_1 0x36
#define I2CDEV_ADS1110_ADDR_1 0x48
+6
View File
@@ -32,6 +32,7 @@
#include "i2cdev_ads1110.hpp"
#include "i2cdev_bh1750.hpp"
#include "i2cdev_ppmod.hpp"
#include "i2cdev_sht4x.hpp"
namespace i2cdev {
@@ -73,6 +74,11 @@ bool I2CDevManager::found(uint8_t addr) {
if (!item.dev->init(addr)) item.dev = nullptr;
}
if (!item.dev && (addr == I2CDEV_SHT4X_ADDR_1 || addr == I2CDEV_SHT4X_ADDR_2)) {
item.dev = std::make_unique<I2cDev_SHT4x>();
if (!item.dev->init(addr)) item.dev = nullptr;
}
if (!item.dev && (addr == I2CDEV_MAX17055_ADDR_1)) {
item.dev = std::make_unique<I2cDev_MAX17055>();
if (!item.dev->init(addr)) item.dev = nullptr;
+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)
+1 -1
View File
@@ -148,7 +148,7 @@ struct standalone_application_information_t {
void (*shutdown)();
void (*PaintViewMirror)();
void (*OnTouchEvent)(int x, int y, uint32_t type);
bool (*OnTouchEvent)(int x, int y, uint32_t type);
void (*OnFocus)();
bool (*OnKeyEvent)(uint8_t key);
bool (*OnEncoder)(int32_t delta);
+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;
}
@@ -77,7 +77,7 @@ ui::Widget* touch_widget(ui::Widget* const w, ui::TouchEvent event) {
ui::Widget* captured_widget{nullptr};
void OnTouchEvent(int, int, uint32_t) {
bool OnTouchEvent(int, int, uint32_t) {
if (standaloneViewMirror) {
_api->exit_app();
/* //left here for example, but not used in digital rain
@@ -96,6 +96,7 @@ void OnTouchEvent(int, int, uint32_t) {
captured_widget->on_touch(event);
*/
}
return false;
}
void OnFocus() {

Some files were not shown because too many files have changed in this diff Show More