Compare commits

...

4 Commits

Author SHA1 Message Date
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
22 changed files with 493 additions and 89 deletions

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);
}

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();

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);
}

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,

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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)},

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 */

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__*/

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;
}

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;

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.

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;

View File

@@ -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;

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();

View File

@@ -1187,9 +1187,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 {

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)

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;
};

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;

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();

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;
}

View File

@@ -25,6 +25,7 @@
bool notouch(int, int, uint32_t) {
// do nothing
return false;
}
void nothing() {
// do nothing