mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2026-08-21 23:19:02 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 472571b1ed | |||
| 106e56abc3 | |||
| 4129d57c09 | |||
| caac5e1041 | |||
| 6b02ba6e5d | |||
| c01597baf2 | |||
| 43a5163b77 | |||
| c46cc431c9 | |||
| a4d23768c1 | |||
| 344aa0c741 |
@@ -1 +1 @@
|
||||
v2.3.0
|
||||
v2.3.1
|
||||
|
||||
@@ -56,6 +56,7 @@ POCSAGSettingsView::POCSAGSettingsView(
|
||||
: settings_{settings} {
|
||||
add_children(
|
||||
{&labels,
|
||||
&opt_baud_rate,
|
||||
&check_log,
|
||||
&check_log_raw,
|
||||
&check_small_font,
|
||||
@@ -65,6 +66,7 @@ POCSAGSettingsView::POCSAGSettingsView(
|
||||
&field_filter_address,
|
||||
&button_save});
|
||||
|
||||
opt_baud_rate.set_by_value(settings_.baud_rate);
|
||||
check_log.set_value(settings_.enable_logging);
|
||||
check_log_raw.set_value(settings_.enable_raw_log);
|
||||
check_small_font.set_value(settings_.enable_small_font);
|
||||
@@ -81,7 +83,7 @@ POCSAGSettingsView::POCSAGSettingsView(
|
||||
settings_.hide_addr_only = check_hide_addr_only.value();
|
||||
settings_.filter_mode = opt_filter_mode.selected_index_value();
|
||||
settings_.filter_address = field_filter_address.to_integer();
|
||||
|
||||
settings_.baud_rate = opt_baud_rate.selected_index_value();
|
||||
nav.pop();
|
||||
};
|
||||
}
|
||||
@@ -142,7 +144,7 @@ POCSAGAppView::POCSAGAppView(NavigationView& nav)
|
||||
|
||||
audio::output::start();
|
||||
receiver_model.enable();
|
||||
baseband::set_pocsag();
|
||||
baseband::set_pocsag((int8_t)settings_.baud_rate);
|
||||
}
|
||||
|
||||
void POCSAGAppView::focus() {
|
||||
@@ -182,6 +184,7 @@ void POCSAGAppView::refresh_ui() {
|
||||
btn_text = "Filter Last";
|
||||
break;
|
||||
}
|
||||
baseband::set_pocsag((int8_t)settings_.baud_rate);
|
||||
button_filter_last.set_text(btn_text);
|
||||
}
|
||||
|
||||
|
||||
@@ -126,6 +126,7 @@ struct POCSAGSettings {
|
||||
bool hide_bad_data = false;
|
||||
bool hide_addr_only = false;
|
||||
uint8_t filter_mode = false;
|
||||
int32_t baud_rate = -1;
|
||||
uint32_t filter_address = 0;
|
||||
};
|
||||
|
||||
@@ -139,7 +140,16 @@ class POCSAGSettingsView : public View {
|
||||
private:
|
||||
POCSAGSettings& settings_;
|
||||
|
||||
OptionsField opt_baud_rate{
|
||||
{8 * 8, 0 * 16},
|
||||
4,
|
||||
{{"Auto", -1},
|
||||
{" 512", 0},
|
||||
{"1200", 1},
|
||||
{"2400", 2}}};
|
||||
|
||||
Labels labels{
|
||||
{{2 * 8, 0 * 16}, "Baud:", Theme::getInstance()->fg_light->foreground},
|
||||
{{2 * 8, 12 * 16}, "Filter Mode:", Theme::getInstance()->fg_light->foreground},
|
||||
{{2 * 8, 13 * 16}, "Filter Addr:", Theme::getInstance()->fg_light->foreground},
|
||||
};
|
||||
@@ -221,6 +231,7 @@ class POCSAGAppView : public View {
|
||||
{"filter_address"sv, &settings_.filter_address},
|
||||
{"hide_bad_data"sv, &settings_.hide_bad_data},
|
||||
{"hide_addr_only"sv, &settings_.hide_addr_only},
|
||||
{"baud_rate"sv, &settings_.baud_rate},
|
||||
}};
|
||||
|
||||
void refresh_ui();
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace ui {
|
||||
constexpr std::string_view mayhem_information_list[] = {
|
||||
"#****** Mayhem Community ******",
|
||||
" ",
|
||||
" https://discord.mayhem.app",
|
||||
" https://discord.hackrf.app",
|
||||
" ",
|
||||
"#**** List of contributors ****",
|
||||
" ",
|
||||
|
||||
@@ -30,11 +30,10 @@
|
||||
#include "portapack.hpp"
|
||||
#include <cstring>
|
||||
#include <stdio.h>
|
||||
#include "rtc_time.hpp"
|
||||
|
||||
using namespace portapack;
|
||||
namespace pmem = portapack::persistent_memory;
|
||||
|
||||
#include "string_format.hpp"
|
||||
#include "complex.hpp"
|
||||
|
||||
void SondeLogger::on_packet(const sonde::Packet& packet) {
|
||||
@@ -65,6 +64,8 @@ SondeView::SondeView(NavigationView& nav)
|
||||
&text_frame,
|
||||
&text_temp,
|
||||
&text_humid,
|
||||
&text_press,
|
||||
&text_vspeed,
|
||||
&geopos,
|
||||
&button_see_qr,
|
||||
&button_see_map});
|
||||
@@ -107,7 +108,7 @@ SondeView::SondeView(NavigationView& nav)
|
||||
|
||||
logger = std::make_unique<SondeLogger>();
|
||||
if (logger)
|
||||
logger->append(logs_dir / u"SONDE.TXT");
|
||||
logger->append(logs_dir / u"SONDE_" + to_string_timestamp(rtc_time::now()) + u".TXT");
|
||||
|
||||
if (pmem::beep_on_packets()) {
|
||||
audio::set_rate(audio::Rate::Hz_24000);
|
||||
@@ -155,7 +156,7 @@ void SondeView::on_packet(const sonde::Packet& packet) {
|
||||
sonde_id = packet.serial_number(); // used also as tag on the geomap
|
||||
text_serial.set(sonde_id);
|
||||
|
||||
text_timestamp.set(to_string_timestamp(packet.received_at()));
|
||||
text_timestamp.set(to_string_datetime(packet.received_at(), TimeFormat::YMDHMS));
|
||||
|
||||
text_voltage.set(unit_auto_scale(packet.battery_voltage(), 2, 2) + "V");
|
||||
|
||||
@@ -163,21 +164,42 @@ void SondeView::on_packet(const sonde::Packet& packet) {
|
||||
|
||||
temp_humid_info = packet.get_temp_humid();
|
||||
if (temp_humid_info.humid != 0) {
|
||||
double decimals = abs(get_decimals(temp_humid_info.humid, 10, true));
|
||||
text_humid.set(to_string_dec_int((int)temp_humid_info.humid) + "." + to_string_dec_uint(decimals, 1) + "%");
|
||||
text_humid.set(to_string_decimal(temp_humid_info.humid, 1) + "%");
|
||||
}
|
||||
|
||||
if (temp_humid_info.temp != 0) {
|
||||
double decimals = abs(get_decimals(temp_humid_info.temp, 10, true));
|
||||
text_temp.set(to_string_dec_int((int)temp_humid_info.temp) + "." + to_string_dec_uint(decimals, 1) + STR_DEGREES_C);
|
||||
text_temp.set(to_string_decimal(temp_humid_info.temp, 1) + STR_DEGREES_C);
|
||||
}
|
||||
|
||||
if (packet.get_pressure() != 0) {
|
||||
text_press.set(to_string_decimal(packet.get_pressure(), 1) + " hPa");
|
||||
}
|
||||
|
||||
gps_info = packet.get_GPS_data();
|
||||
|
||||
geopos.set_altitude(gps_info.alt);
|
||||
geopos.set_lat(gps_info.lat);
|
||||
geopos.set_lon(gps_info.lon);
|
||||
|
||||
if (last_timestamp_update_ != 0 && last_altitude_ != 0) {
|
||||
// calculate speeds
|
||||
float vspeed = 0;
|
||||
time_t currpackettime = rtc_time::rtcToUnixUTC(packet.received_at());
|
||||
int32_t time_diff = (currpackettime - last_timestamp_update_);
|
||||
if (time_diff >= 10) { // update only every 10 seconds
|
||||
vspeed = (static_cast<int>(gps_info.alt) - static_cast<int>(last_altitude_)) / (float)time_diff;
|
||||
last_timestamp_update_ = currpackettime;
|
||||
last_altitude_ = gps_info.alt;
|
||||
text_vspeed.set(to_string_decimal(vspeed, 1) + " m/s");
|
||||
}
|
||||
} else { // save first valid packet time + altitude
|
||||
last_timestamp_update_ = rtc_time::rtcToUnixUTC(packet.received_at());
|
||||
last_altitude_ = geopos.altitude();
|
||||
}
|
||||
if (gps_info.is_valid()) { // only update when valid, to prevent flashing
|
||||
geopos.set_altitude(gps_info.alt);
|
||||
geopos.set_lat(gps_info.lat);
|
||||
geopos.set_lon(gps_info.lon);
|
||||
if (geomap_view_) {
|
||||
geomap_view_->update_position(gps_info.lat, gps_info.lon, 400, gps_info.alt, 0);
|
||||
}
|
||||
}
|
||||
if (logger && logging) {
|
||||
logger->on_packet(packet);
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include "ui_rssi.hpp"
|
||||
#include "ui_qrcode.hpp"
|
||||
#include "ui_geomap.hpp"
|
||||
#include "string_format.hpp"
|
||||
|
||||
#include "event_m0.hpp"
|
||||
|
||||
@@ -94,32 +95,33 @@ class SondeView : public View {
|
||||
// AudioOutput audio_output { };
|
||||
|
||||
Labels labels{
|
||||
{{4 * 8, 2 * 16}, "Type:", Theme::getInstance()->fg_light->foreground},
|
||||
{{6 * 8, 3 * 16}, "ID:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), 4 * 16}, "DateTime:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(4), UI_POS_Y(2)}, "Type:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(6), UI_POS_Y(3)}, "ID:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(4)}, "DateTime:", Theme::getInstance()->fg_light->foreground},
|
||||
|
||||
{{3 * 8, 5 * 16}, "Vbatt:", Theme::getInstance()->fg_light->foreground},
|
||||
{{3 * 8, 6 * 16}, "Frame:", Theme::getInstance()->fg_light->foreground},
|
||||
{{4 * 8, 7 * 16}, "Temp:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), 8 * 16}, "Humidity:", Theme::getInstance()->fg_light->foreground}};
|
||||
{{UI_POS_X(3), UI_POS_Y(5)}, "Vbatt:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(3), UI_POS_Y(6)}, "Frame:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(4), UI_POS_Y(7)}, "Temp:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(8)}, "Humidity:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(0), UI_POS_Y(9)}, "Pressure:", Theme::getInstance()->fg_light->foreground},
|
||||
{{UI_POS_X(2), UI_POS_Y(10)}, "VSpeed:", Theme::getInstance()->fg_light->foreground}};
|
||||
|
||||
RxFrequencyField field_frequency{
|
||||
{UI_POS_X(0), 0 * 8},
|
||||
{UI_POS_X(0), UI_POS_Y(0)},
|
||||
nav_};
|
||||
|
||||
RFAmpField field_rf_amp{
|
||||
{13 * 8, UI_POS_Y(0)}};
|
||||
|
||||
{UI_POS_X(13), UI_POS_Y(0)}};
|
||||
LNAGainField field_lna{
|
||||
{15 * 8, UI_POS_Y(0)}};
|
||||
{UI_POS_X(15), UI_POS_Y(0)}};
|
||||
|
||||
VGAGainField field_vga{
|
||||
{18 * 8, UI_POS_Y(0)}};
|
||||
{UI_POS_X(18), UI_POS_Y(0)}};
|
||||
|
||||
RSSI rssi{
|
||||
{21 * 8, 0, UI_POS_WIDTH_REMAINING(24), 4}};
|
||||
{UI_POS_X(21), UI_POS_Y(0), UI_POS_WIDTH_REMAINING(24), 4}};
|
||||
Channel channel{
|
||||
{21 * 8, 5, UI_POS_WIDTH_REMAINING(24), 4},
|
||||
{UI_POS_X(21), UI_POS_Y(0) + 5, UI_POS_WIDTH_REMAINING(24), 4},
|
||||
};
|
||||
|
||||
AudioVolumeField field_volume{
|
||||
@@ -136,47 +138,57 @@ class SondeView : public View {
|
||||
"CRC"};
|
||||
|
||||
Text text_signature{
|
||||
{9 * 8, 2 * 16, 10 * 8, 16},
|
||||
{UI_POS_X(9), UI_POS_Y(2), UI_POS_WIDTH_REMAINING(10), UI_POS_HEIGHT(1)},
|
||||
"..."};
|
||||
|
||||
Text text_serial{
|
||||
{9 * 8, 3 * 16, 11 * 8, 16},
|
||||
{UI_POS_X(9), UI_POS_Y(3), UI_POS_WIDTH_REMAINING(10), UI_POS_HEIGHT(1)},
|
||||
"..."};
|
||||
|
||||
Text text_timestamp{
|
||||
{9 * 8, 4 * 16, 11 * 8, 16},
|
||||
{UI_POS_X(9), UI_POS_Y(4), UI_POS_WIDTH_REMAINING(9), UI_POS_HEIGHT(1)},
|
||||
"..."};
|
||||
|
||||
Text text_voltage{
|
||||
{9 * 8, 5 * 16, 10 * 8, 16},
|
||||
{UI_POS_X(9), UI_POS_Y(5), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)},
|
||||
"..."};
|
||||
|
||||
Text text_frame{
|
||||
{9 * 8, 6 * 16, 10 * 8, 16},
|
||||
{UI_POS_X(9), UI_POS_Y(6), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)},
|
||||
"..."};
|
||||
|
||||
Text text_temp{
|
||||
{9 * 8, 7 * 16, 10 * 8, 16},
|
||||
{UI_POS_X(9), UI_POS_Y(7), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)},
|
||||
"..."};
|
||||
|
||||
Text text_humid{
|
||||
{9 * 8, 8 * 16, 10 * 8, 16},
|
||||
{UI_POS_X(9), UI_POS_Y(8), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)},
|
||||
"..."};
|
||||
|
||||
Text text_press{
|
||||
{UI_POS_X(9), UI_POS_Y(9), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)},
|
||||
"..."};
|
||||
|
||||
Text text_vspeed{
|
||||
{UI_POS_X(9), UI_POS_Y(10), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)},
|
||||
"..."};
|
||||
|
||||
GeoPos geopos{
|
||||
{0, 12 * 16},
|
||||
{UI_POS_X(0), UI_POS_Y(12)},
|
||||
GeoPos::alt_unit::METERS,
|
||||
GeoPos::spd_unit::HIDDEN};
|
||||
|
||||
Button button_see_qr{
|
||||
{UI_POS_X_CENTER(12) - UI_POS_WIDTH(8), UI_POS_Y_BOTTOM(4), 12 * 8, 3 * 16},
|
||||
{UI_POS_X_CENTER(12) - UI_POS_WIDTH(8), UI_POS_Y_BOTTOM(4), UI_POS_WIDTH(12), UI_POS_HEIGHT(3)},
|
||||
"See QR"};
|
||||
|
||||
Button button_see_map{
|
||||
{UI_POS_X_CENTER(12) + UI_POS_WIDTH(8), UI_POS_Y_BOTTOM(4), 12 * 8, 3 * 16},
|
||||
{UI_POS_X_CENTER(12) + UI_POS_WIDTH(8), UI_POS_Y_BOTTOM(4), UI_POS_WIDTH(12), UI_POS_HEIGHT(3)},
|
||||
"See on map"};
|
||||
|
||||
GeoMapView* geomap_view_{nullptr};
|
||||
time_t last_timestamp_update_{0};
|
||||
uint32_t last_altitude_{0};
|
||||
|
||||
MessageHandlerRegistration message_handler_packet{
|
||||
Message::ID::SondePacket,
|
||||
|
||||
@@ -288,8 +288,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 +328,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)};
|
||||
|
||||
@@ -80,7 +80,7 @@ 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 +91,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 +102,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();
|
||||
|
||||
+1
-1
@@ -104,7 +104,7 @@ class AdultToysView : public ui::View {
|
||||
/*short_ui*/ true};
|
||||
|
||||
app_settings::SettingsManager settings_{
|
||||
"Adult Toys", app_settings::Mode::TX};
|
||||
"tx_adult_toys", app_settings::Mode::TX};
|
||||
|
||||
OptionsField options_target{
|
||||
{UI_POS_X(6), UI_POS_Y(1)},
|
||||
|
||||
@@ -264,6 +264,10 @@ set(EXTCPPSRC
|
||||
#adult_toys_controller 144 bytes
|
||||
external/adult_toys_controller/main.cpp
|
||||
external/adult_toys_controller/ui_adult_toys_controller.cpp
|
||||
|
||||
#flex_rx
|
||||
external/flex_rx/main.cpp
|
||||
external/flex_rx/ui_flex_rx.cpp
|
||||
)
|
||||
|
||||
set(EXTAPPLIST
|
||||
@@ -330,4 +334,5 @@ set(EXTAPPLIST
|
||||
bht_tx
|
||||
morse_practice
|
||||
adult_toys_controller
|
||||
flex_rx
|
||||
)
|
||||
|
||||
+6
@@ -86,6 +86,7 @@ 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
|
||||
|
||||
}
|
||||
|
||||
@@ -471,5 +472,10 @@ 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
|
||||
}
|
||||
|
||||
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright (C) 2025 timelf123
|
||||
* with barely any help from RocketGod but I exist.
|
||||
*
|
||||
* 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::cyan().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
|
||||
};
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright (C) 2025 HTotoo
|
||||
*
|
||||
* This file is part of PortaPack.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2, or (at your option)
|
||||
* any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; see the file COPYING. If not, write to
|
||||
* the Free Software Foundation, Inc., 51 Franklin Street,
|
||||
* Boston, MA 02110-1301, USA.
|
||||
*/
|
||||
|
||||
#include "ui.hpp"
|
||||
#include "ui_flex_rx.hpp"
|
||||
#include "ui_navigation.hpp"
|
||||
#include "external_app.hpp"
|
||||
|
||||
namespace ui::external_app::flex_rx {
|
||||
void initialize_app(ui::NavigationView& nav) {
|
||||
nav.push<FlexAppView>();
|
||||
}
|
||||
} // namespace ui::external_app::flex_rx
|
||||
|
||||
extern "C" {
|
||||
|
||||
__attribute__((section(".external_app.app_flex_rx.application_information"), used)) application_information_t _application_information_flex_rx = {
|
||||
/*.memory_location = */ (uint8_t*)0x00000000,
|
||||
/*.externalAppEntry = */ ui::external_app::flex_rx::initialize_app,
|
||||
/*.header_version = */ CURRENT_HEADER_VERSION,
|
||||
/*.app_version = */ VERSION_MD5,
|
||||
|
||||
/*.app_name = */ "FLEX RX",
|
||||
/*.bitmap_data = */ {
|
||||
0x00,
|
||||
0x00,
|
||||
0xFE,
|
||||
0x7F,
|
||||
0x02,
|
||||
0x40,
|
||||
0xFA,
|
||||
0x5F,
|
||||
0x02,
|
||||
0x40,
|
||||
0xF2,
|
||||
0x4F,
|
||||
0x02,
|
||||
0x40,
|
||||
0xE2,
|
||||
0x47,
|
||||
0x02,
|
||||
0x40,
|
||||
0xC2,
|
||||
0x43,
|
||||
0x02,
|
||||
0x40,
|
||||
0x82,
|
||||
0x41,
|
||||
0x02,
|
||||
0x40,
|
||||
0xFE,
|
||||
0x7F,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
},
|
||||
/*.icon_color = */ ui::Color::orange().v,
|
||||
/*.menu_location = */ app_location_t::RX,
|
||||
/*.desired_menu_position = */ -1,
|
||||
|
||||
/*.m4_app_tag = portapack::spi_flash::image_tag_flex */ {'P', 'F', 'L', 'X'},
|
||||
/*.m4_app_offset = */ 0x00000000, // will be filled at compile time
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
#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,
|
||||
&button_color,
|
||||
&rssi,
|
||||
&menu_view});
|
||||
|
||||
// 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);
|
||||
};
|
||||
|
||||
// Color button cycles through available colors
|
||||
button_color.on_select = [this](Button&) {
|
||||
cycle_color();
|
||||
};
|
||||
|
||||
// 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();
|
||||
|
||||
log_message("FLEX RX Ready");
|
||||
}
|
||||
|
||||
FlexAppView::~FlexAppView() {
|
||||
receiver_model.disable();
|
||||
baseband::shutdown();
|
||||
}
|
||||
|
||||
void FlexAppView::focus() {
|
||||
field_frequency.focus();
|
||||
}
|
||||
|
||||
// Cycle to next text color and refresh display
|
||||
void FlexAppView::cycle_color() {
|
||||
current_color_index = (current_color_index + 1) % text_colors.size();
|
||||
rebuild_menu();
|
||||
}
|
||||
|
||||
// Rebuild entire menu with current color
|
||||
void FlexAppView::rebuild_menu() {
|
||||
menu_view.clear();
|
||||
Color current_color = text_colors[current_color_index];
|
||||
for (const auto& msg : log_messages) {
|
||||
menu_view.add_item({msg,
|
||||
current_color,
|
||||
nullptr,
|
||||
[](KeyEvent) {}});
|
||||
}
|
||||
if (menu_view.item_count() > 0) {
|
||||
menu_view.set_highlighted(menu_view.item_count() - 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Add message to log with automatic line wrapping
|
||||
void FlexAppView::log_message(const std::string& message) {
|
||||
// Calculate characters per line based on screen width (8 pixels per char)
|
||||
const size_t chars_per_line = screen_width / 8;
|
||||
Color current_color = text_colors[current_color_index];
|
||||
|
||||
std::string remaining = message;
|
||||
bool first_line = true;
|
||||
bool needs_rebuild = false;
|
||||
size_t lines_added = 0;
|
||||
|
||||
// Split message into screen-width chunks
|
||||
while (!remaining.empty()) {
|
||||
std::string line;
|
||||
if (remaining.length() <= chars_per_line) {
|
||||
line = remaining;
|
||||
remaining.clear();
|
||||
} else {
|
||||
line = remaining.substr(0, chars_per_line);
|
||||
remaining = remaining.substr(chars_per_line);
|
||||
}
|
||||
|
||||
// Indent continuation lines
|
||||
if (!first_line) {
|
||||
line = " " + line;
|
||||
}
|
||||
first_line = false;
|
||||
|
||||
// Remove oldest line if at limit
|
||||
if (log_messages.size() >= MAX_LOG_LINES) {
|
||||
log_messages.erase(log_messages.begin());
|
||||
needs_rebuild = true;
|
||||
}
|
||||
|
||||
log_messages.push_back(line);
|
||||
lines_added++;
|
||||
}
|
||||
|
||||
// Either rebuild all or just add new lines
|
||||
if (needs_rebuild) {
|
||||
rebuild_menu();
|
||||
} else {
|
||||
size_t start_idx = log_messages.size() - lines_added;
|
||||
for (size_t i = start_idx; i < log_messages.size(); i++) {
|
||||
menu_view.add_item({log_messages[i],
|
||||
current_color,
|
||||
nullptr,
|
||||
[](KeyEvent) {}});
|
||||
}
|
||||
if (menu_view.item_count() > 0) {
|
||||
menu_view.set_highlighted(menu_view.item_count() - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
std::string text = "FLEX ";
|
||||
text += to_string_dec_uint(message->packet.bitrate);
|
||||
text += " ";
|
||||
text += to_string_dec_uint(message->packet.capcode);
|
||||
text += ": ";
|
||||
text += message->packet.message;
|
||||
|
||||
log_message(text);
|
||||
}
|
||||
|
||||
// Handle stats message (currently unused)
|
||||
void FlexAppView::on_stats(const FlexStatsMessage* /* message */) {
|
||||
}
|
||||
|
||||
// Handle debug message from baseband
|
||||
void FlexAppView::on_debug(const FlexDebugMessage* message) {
|
||||
std::string text = "DBG: ";
|
||||
text += message->text;
|
||||
text += " " + to_string_hex(message->val1, 8);
|
||||
text += " " + to_string_hex(message->val2, 8);
|
||||
log_message(text);
|
||||
}
|
||||
|
||||
} // namespace ui::external_app::flex_rx
|
||||
@@ -0,0 +1,116 @@
|
||||
#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>
|
||||
#include <array>
|
||||
|
||||
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
|
||||
uint32_t current_color_index{0}; // Current text color selection
|
||||
|
||||
// Available text colors for message display
|
||||
static constexpr std::array<Color, 7> text_colors = {{Color::green(),
|
||||
Color::white(),
|
||||
Color::cyan(),
|
||||
Color::magenta(),
|
||||
Color::yellow(),
|
||||
Color::blue(),
|
||||
Color::red()}};
|
||||
|
||||
RxRadioState radio_state_{};
|
||||
|
||||
// Message log settings
|
||||
static constexpr size_t MAX_LOG_LINES = 32; // Limit to prevent memory issues
|
||||
std::vector<std::string> log_messages{}; // Stored log lines
|
||||
|
||||
// Helper methods
|
||||
void log_message(const std::string& message); // Add message with word wrap
|
||||
void rebuild_menu(); // Rebuild menu after color change or overflow
|
||||
void update_freq(rf::Frequency f); // Update tuned frequency
|
||||
void cycle_color(); // Cycle through text colors
|
||||
|
||||
// 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)}};
|
||||
|
||||
// Color cycle button
|
||||
Button button_color{
|
||||
{UI_POS_X(21), UI_POS_Y(0), UI_POS_WIDTH(5), UI_POS_HEIGHT(1)},
|
||||
"COLOR"};
|
||||
|
||||
RSSI rssi{
|
||||
{UI_POS_X(26), 0, UI_POS_WIDTH(4), 4}};
|
||||
|
||||
// Message display area - scrollable menu view
|
||||
MenuView menu_view{
|
||||
{0, 1 * 16, screen_width, screen_height - 1 * 16},
|
||||
true};
|
||||
|
||||
// Persistent settings manager
|
||||
app_settings::SettingsManager settings_{
|
||||
"rx_flex",
|
||||
app_settings::Mode::RX,
|
||||
{{"frequency", &frequency_value},
|
||||
{"color_index", ¤t_color_index}}};
|
||||
|
||||
// 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__*/
|
||||
@@ -265,4 +265,31 @@ uint8_t day_of_week(uint16_t year, uint8_t month, uint8_t day) {
|
||||
return (day - 1 + (13 * m / 5) + y + (y / 4) - (y / 100) + (y / 400)) % 7;
|
||||
}
|
||||
|
||||
bool isLeap(int year) {
|
||||
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
|
||||
}
|
||||
|
||||
time_t rtcToUnixUTC(const rtc::RTC& rtc) {
|
||||
const uint8_t daysOfMonth[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
|
||||
uint16_t y = rtc.year();
|
||||
uint8_t m = rtc.month();
|
||||
uint8_t d = rtc.day();
|
||||
uint32_t totalDays = 0;
|
||||
for (int i = 1970; i < y; i++) {
|
||||
totalDays += isLeap(i) ? 366 : 365;
|
||||
}
|
||||
for (int i = 1; i < m; i++) {
|
||||
totalDays += daysOfMonth[i];
|
||||
if (i == 2 && isLeap(y)) {
|
||||
totalDays++;
|
||||
}
|
||||
}
|
||||
totalDays += (d - 1);
|
||||
time_t totalSeconds = totalDays * 86400; // 24 * 60 * 60
|
||||
totalSeconds += rtc.hour() * 3600;
|
||||
totalSeconds += rtc.minute() * 60;
|
||||
totalSeconds += rtc.second();
|
||||
return totalSeconds;
|
||||
}
|
||||
|
||||
} /* namespace rtc_time */
|
||||
|
||||
@@ -56,6 +56,9 @@ bool leap_year(uint16_t year);
|
||||
uint16_t day_of_year(uint16_t year, uint8_t month, uint8_t day);
|
||||
uint16_t day_of_year_of_nth_weekday(uint16_t year, uint8_t month, uint8_t n, uint8_t weekday);
|
||||
|
||||
bool isLeap(int year);
|
||||
time_t rtcToUnixUTC(const rtc::RTC& rtc);
|
||||
|
||||
} /* namespace rtc_time */
|
||||
|
||||
#endif /*__RTC_TIME_H__*/
|
||||
|
||||
@@ -199,7 +199,7 @@ bool GeoMap::on_encoder(const EncoderEvent delta) {
|
||||
}
|
||||
}
|
||||
map_osm_zoom++;
|
||||
if (has_osm) set_osm_max_zoom();
|
||||
if (has_osm) set_osm_max_zoom(true);
|
||||
} else if (delta < 0) {
|
||||
if (map_zoom > -MAX_MAP_ZOOM_OUT) {
|
||||
if (map_zoom == 1) {
|
||||
@@ -212,6 +212,7 @@ bool GeoMap::on_encoder(const EncoderEvent delta) {
|
||||
}
|
||||
}
|
||||
if (map_osm_zoom > 0) map_osm_zoom--;
|
||||
if (has_osm) set_osm_max_zoom(true);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
@@ -299,8 +300,8 @@ ui::Point GeoMap::item_rect_pixel(GeoMarker& item) {
|
||||
return {(int16_t)x, (int16_t)y};
|
||||
}
|
||||
// osm calculation
|
||||
double y = lat_to_pixel_y_tile(item.lat, map_osm_zoom) - viewport_top_left_py;
|
||||
double x = lon_to_pixel_x_tile(item.lon, map_osm_zoom) - viewport_top_left_px;
|
||||
double y = lat_to_pixel_y_tile(item.lat, map_osm_real_zoom) - viewport_top_left_py;
|
||||
double x = lon_to_pixel_x_tile(item.lon, map_osm_real_zoom) - viewport_top_left_px;
|
||||
return {(int16_t)x, (int16_t)y};
|
||||
}
|
||||
|
||||
@@ -327,7 +328,7 @@ int GeoMap::lat2tile(double lat, int zoom) {
|
||||
return (int)floor((1.0 - log(tan(lat_rad) + 1.0 / cos(lat_rad)) / M_PI) / 2.0 * pow(2.0, zoom));
|
||||
}
|
||||
|
||||
void GeoMap::set_osm_max_zoom() {
|
||||
void GeoMap::set_osm_max_zoom(bool changeboth) {
|
||||
if (map_osm_zoom > 20) map_osm_zoom = 20;
|
||||
for (uint8_t i = map_osm_zoom; i > 0; i--) {
|
||||
int tile_x = lon2tile(lon_, i);
|
||||
@@ -335,11 +336,13 @@ void GeoMap::set_osm_max_zoom() {
|
||||
std::string filename = "/OSM/" + to_string_dec_int(i) + "/" + to_string_dec_int(tile_x) + "/" + to_string_dec_int(tile_y) + ".bmp";
|
||||
std::filesystem::path file_path(filename);
|
||||
if (file_exists(file_path)) {
|
||||
map_osm_zoom = i;
|
||||
map_osm_real_zoom = i;
|
||||
if (changeboth) map_osm_zoom = i;
|
||||
return;
|
||||
}
|
||||
}
|
||||
map_osm_zoom = 0; // should not happen
|
||||
if (changeboth) map_osm_zoom = 0; // should not happen
|
||||
map_osm_real_zoom = 0; // should not happen
|
||||
}
|
||||
|
||||
// checks if the tile file presents or not. to determine if we got osm or not
|
||||
@@ -454,14 +457,22 @@ bool GeoMap::draw_osm_file(int zoom, int tile_x, int tile_y, int relative_x, int
|
||||
return false;
|
||||
}
|
||||
std::vector<ui::Color> line(clip_w);
|
||||
for (int y = 0; y < clip_h; ++y) {
|
||||
int source_row = src_y + y;
|
||||
int dest_row = dest_y + y;
|
||||
bmp.seek(src_x, source_row);
|
||||
for (int x = 0; x < clip_w; ++x) {
|
||||
bmp.read_next_px(line[x], true);
|
||||
if (bmp.is_bottomup()) {
|
||||
for (int y = clip_h - 1; y >= 0; --y) {
|
||||
int source_row = src_y + y;
|
||||
int dest_row = dest_y + y;
|
||||
bmp.seek(src_x, source_row);
|
||||
bmp.read_next_px_cnt(line.data(), clip_w, false);
|
||||
display.draw_pixels({dest_x + r.left(), dest_row + r.top(), clip_w, 1}, line);
|
||||
}
|
||||
} else {
|
||||
for (int y = 0; y < clip_h; ++y) {
|
||||
int source_row = src_y + y;
|
||||
int dest_row = dest_y + y;
|
||||
bmp.seek(src_x, source_row);
|
||||
bmp.read_next_px_cnt(line.data(), clip_w, false);
|
||||
display.draw_pixels({dest_x + r.left(), dest_row + r.top(), clip_w, 1}, line);
|
||||
}
|
||||
display.draw_pixels({dest_x + r.left(), dest_row + r.top(), clip_w, 1}, line);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -522,8 +533,8 @@ void GeoMap::paint(Painter& painter) {
|
||||
} else {
|
||||
// display osm tiles
|
||||
// Convert center GPS to a global pixel coordinate
|
||||
double global_center_px = lon_to_pixel_x_tile(lon_, map_osm_zoom);
|
||||
double global_center_py = lat_to_pixel_y_tile(lat_, map_osm_zoom);
|
||||
double global_center_px = lon_to_pixel_x_tile(lon_, map_osm_real_zoom);
|
||||
double global_center_py = lat_to_pixel_y_tile(lat_, map_osm_real_zoom);
|
||||
|
||||
// Find the top-left corner of the screen (viewport) in global pixel coordinates
|
||||
viewport_top_left_px = global_center_px - (r.width() / 2.0);
|
||||
@@ -552,7 +563,7 @@ void GeoMap::paint(Painter& painter) {
|
||||
// For the first tile (x=0, y=0), this will be the negative offset.
|
||||
int draw_pos_x = round(render_offset_x + x * TILE_SIZE);
|
||||
int draw_pos_y = round(render_offset_y + y * TILE_SIZE);
|
||||
if (!draw_osm_file(map_osm_zoom, current_tile_x, current_tile_y, draw_pos_x, draw_pos_y)) {
|
||||
if (!draw_osm_file(map_osm_real_zoom, current_tile_x, current_tile_y, draw_pos_x, draw_pos_y)) {
|
||||
// already blanked it.
|
||||
}
|
||||
}
|
||||
@@ -615,7 +626,7 @@ bool GeoMap::on_touch(const TouchEvent event) {
|
||||
on_move(p.x() / 2.0 * lon_ratio, p.y() / 2.0 * lat_ratio, false);
|
||||
} else {
|
||||
p = event.point - screen_rect().location();
|
||||
on_move(tile_pixel_x_to_lon(p.x() + viewport_top_left_px, map_osm_zoom), tile_pixel_y_to_lat(p.y() + viewport_top_left_py, map_osm_zoom), true);
|
||||
on_move(tile_pixel_x_to_lon(p.x() + viewport_top_left_px, map_osm_real_zoom), tile_pixel_y_to_lat(p.y() + viewport_top_left_py, map_osm_real_zoom), true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -255,7 +255,7 @@ class GeoMap : public Widget {
|
||||
void map_read_line_bin(ui::Color* buffer, uint16_t pixels);
|
||||
// open street map related
|
||||
uint8_t find_osm_file_tile();
|
||||
void set_osm_max_zoom();
|
||||
void set_osm_max_zoom(bool changeboth = false);
|
||||
bool draw_osm_file(int zoom, int tile_x, int tile_y, int relative_x, int relative_y);
|
||||
int lon2tile(double lon, int zoom);
|
||||
int lat2tile(double lat, int zoom);
|
||||
@@ -263,7 +263,8 @@ class GeoMap : public Widget {
|
||||
double lat_to_pixel_y_tile(double lat, int zoom);
|
||||
double tile_pixel_x_to_lon(int x, int zoom);
|
||||
double tile_pixel_y_to_lat(int y, int zoom);
|
||||
uint8_t map_osm_zoom{3};
|
||||
uint8_t map_osm_zoom{5};
|
||||
uint8_t map_osm_real_zoom{5};
|
||||
double viewport_top_left_px = 0;
|
||||
double viewport_top_left_py = 0;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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__*/
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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__*/
|
||||
@@ -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,10 @@ class Message {
|
||||
NoaaAptRxImageData = 79,
|
||||
FSKPacket = 80,
|
||||
EPIRBPacket = 81,
|
||||
FlexPacket = 82,
|
||||
FlexStats = 83,
|
||||
FlexConfigure = 84,
|
||||
FlexDebug = 85,
|
||||
MAX
|
||||
};
|
||||
|
||||
@@ -1187,9 +1192,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 +1574,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__*/
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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'};
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
|
||||
bool notouch(int, int, uint32_t) {
|
||||
// do nothing
|
||||
return false;
|
||||
}
|
||||
void nothing() {
|
||||
// do nothing
|
||||
|
||||
Reference in New Issue
Block a user