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
This commit is contained in:
Totoo
2025-11-26 19:13:21 +01:00
committed by GitHub
parent 6b02ba6e5d
commit caac5e1041
9 changed files with 316 additions and 56 deletions
+30 -9
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,41 @@ 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();
if (gps_info.lat != 0 && gps_info.lon != 0) { // only update when valid, to prevent flashing
if (last_timestamp_update_ != 0 && last_altitude_ != 0) {
// calculate speeds
float vspeed = 0;
time_t currpackettime = rtc_time::rtcToUnixUTC(packet.received_at());
int32_t time_diff = (currpackettime - last_timestamp_update_);
if (time_diff >= 10) { // update only every 10 seconds
vspeed = (static_cast<int>(gps_info.alt) - static_cast<int>(last_altitude_)) / (float)time_diff;
last_timestamp_update_ = currpackettime;
last_altitude_ = gps_info.alt;
text_vspeed.set(to_string_decimal(vspeed, 1) + " m/s");
}
} else { // save first valid packet time + altitude
last_timestamp_update_ = rtc_time::rtcToUnixUTC(packet.received_at());
last_altitude_ = geopos.altitude();
}
if (gps_info.is_valid()) { // only update when valid, to prevent flashing
geopos.set_altitude(gps_info.alt);
geopos.set_lat(gps_info.lat);
geopos.set_lon(gps_info.lon);
if (geomap_view_) {
geomap_view_->update_position(gps_info.lat, gps_info.lon, 400, gps_info.alt, 0);
}
}
if (logger && logging) {
logger->on_packet(packet);
+36 -24
View File
@@ -30,6 +30,7 @@
#include "ui_rssi.hpp"
#include "ui_qrcode.hpp"
#include "ui_geomap.hpp"
#include "string_format.hpp"
#include "event_m0.hpp"
@@ -94,32 +95,33 @@ class SondeView : public View {
// AudioOutput audio_output { };
Labels labels{
{{4 * 8, 2 * 16}, "Type:", Theme::getInstance()->fg_light->foreground},
{{6 * 8, 3 * 16}, "ID:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), 4 * 16}, "DateTime:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(4), UI_POS_Y(2)}, "Type:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(6), UI_POS_Y(3)}, "ID:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(4)}, "DateTime:", Theme::getInstance()->fg_light->foreground},
{{3 * 8, 5 * 16}, "Vbatt:", Theme::getInstance()->fg_light->foreground},
{{3 * 8, 6 * 16}, "Frame:", Theme::getInstance()->fg_light->foreground},
{{4 * 8, 7 * 16}, "Temp:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), 8 * 16}, "Humidity:", Theme::getInstance()->fg_light->foreground}};
{{UI_POS_X(3), UI_POS_Y(5)}, "Vbatt:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(3), UI_POS_Y(6)}, "Frame:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(4), UI_POS_Y(7)}, "Temp:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(8)}, "Humidity:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(0), UI_POS_Y(9)}, "Pressure:", Theme::getInstance()->fg_light->foreground},
{{UI_POS_X(2), UI_POS_Y(10)}, "VSpeed:", Theme::getInstance()->fg_light->foreground}};
RxFrequencyField field_frequency{
{UI_POS_X(0), 0 * 8},
{UI_POS_X(0), UI_POS_Y(0)},
nav_};
RFAmpField field_rf_amp{
{13 * 8, UI_POS_Y(0)}};
{UI_POS_X(13), UI_POS_Y(0)}};
LNAGainField field_lna{
{15 * 8, UI_POS_Y(0)}};
{UI_POS_X(15), UI_POS_Y(0)}};
VGAGainField field_vga{
{18 * 8, UI_POS_Y(0)}};
{UI_POS_X(18), UI_POS_Y(0)}};
RSSI rssi{
{21 * 8, 0, UI_POS_WIDTH_REMAINING(24), 4}};
{UI_POS_X(21), UI_POS_Y(0), UI_POS_WIDTH_REMAINING(24), 4}};
Channel channel{
{21 * 8, 5, UI_POS_WIDTH_REMAINING(24), 4},
{UI_POS_X(21), UI_POS_Y(0) + 5, UI_POS_WIDTH_REMAINING(24), 4},
};
AudioVolumeField field_volume{
@@ -136,47 +138,57 @@ class SondeView : public View {
"CRC"};
Text text_signature{
{9 * 8, 2 * 16, 10 * 8, 16},
{UI_POS_X(9), UI_POS_Y(2), UI_POS_WIDTH_REMAINING(10), UI_POS_HEIGHT(1)},
"..."};
Text text_serial{
{9 * 8, 3 * 16, 11 * 8, 16},
{UI_POS_X(9), UI_POS_Y(3), UI_POS_WIDTH_REMAINING(10), UI_POS_HEIGHT(1)},
"..."};
Text text_timestamp{
{9 * 8, 4 * 16, 11 * 8, 16},
{UI_POS_X(9), UI_POS_Y(4), UI_POS_WIDTH_REMAINING(9), UI_POS_HEIGHT(1)},
"..."};
Text text_voltage{
{9 * 8, 5 * 16, 10 * 8, 16},
{UI_POS_X(9), UI_POS_Y(5), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)},
"..."};
Text text_frame{
{9 * 8, 6 * 16, 10 * 8, 16},
{UI_POS_X(9), UI_POS_Y(6), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)},
"..."};
Text text_temp{
{9 * 8, 7 * 16, 10 * 8, 16},
{UI_POS_X(9), UI_POS_Y(7), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)},
"..."};
Text text_humid{
{9 * 8, 8 * 16, 10 * 8, 16},
{UI_POS_X(9), UI_POS_Y(8), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)},
"..."};
Text text_press{
{UI_POS_X(9), UI_POS_Y(9), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)},
"..."};
Text text_vspeed{
{UI_POS_X(9), UI_POS_Y(10), UI_POS_WIDTH(10), UI_POS_HEIGHT(1)},
"..."};
GeoPos geopos{
{0, 12 * 16},
{UI_POS_X(0), UI_POS_Y(12)},
GeoPos::alt_unit::METERS,
GeoPos::spd_unit::HIDDEN};
Button button_see_qr{
{UI_POS_X_CENTER(12) - UI_POS_WIDTH(8), UI_POS_Y_BOTTOM(4), 12 * 8, 3 * 16},
{UI_POS_X_CENTER(12) - UI_POS_WIDTH(8), UI_POS_Y_BOTTOM(4), UI_POS_WIDTH(12), UI_POS_HEIGHT(3)},
"See QR"};
Button button_see_map{
{UI_POS_X_CENTER(12) + UI_POS_WIDTH(8), UI_POS_Y_BOTTOM(4), 12 * 8, 3 * 16},
{UI_POS_X_CENTER(12) + UI_POS_WIDTH(8), UI_POS_Y_BOTTOM(4), UI_POS_WIDTH(12), UI_POS_HEIGHT(3)},
"See on map"};
GeoMapView* geomap_view_{nullptr};
time_t last_timestamp_update_{0};
uint32_t last_altitude_{0};
MessageHandlerRegistration message_handler_packet{
Message::ID::SondePacket,
+1 -1
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);
+27
View File
@@ -265,4 +265,31 @@ uint8_t day_of_week(uint16_t year, uint8_t month, uint8_t day) {
return (day - 1 + (13 * m / 5) + y + (y / 4) - (y / 100) + (y / 400)) % 7;
}
bool isLeap(int year) {
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}
time_t rtcToUnixUTC(const rtc::RTC& rtc) {
const uint8_t daysOfMonth[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
uint16_t y = rtc.year();
uint8_t m = rtc.month();
uint8_t d = rtc.day();
uint32_t totalDays = 0;
for (int i = 1970; i < y; i++) {
totalDays += isLeap(i) ? 366 : 365;
}
for (int i = 1; i < m; i++) {
totalDays += daysOfMonth[i];
if (i == 2 && isLeap(y)) {
totalDays++;
}
}
totalDays += (d - 1);
time_t totalSeconds = totalDays * 86400; // 24 * 60 * 60
totalSeconds += rtc.hour() * 3600;
totalSeconds += rtc.minute() * 60;
totalSeconds += rtc.second();
return totalSeconds;
}
} /* namespace rtc_time */
+3
View File
@@ -56,6 +56,9 @@ bool leap_year(uint16_t year);
uint16_t day_of_year(uint16_t year, uint8_t month, uint8_t day);
uint16_t day_of_year_of_nth_weekday(uint16_t year, uint8_t month, uint8_t n, uint8_t weekday);
bool isLeap(int year);
time_t rtcToUnixUTC(const rtc::RTC& rtc);
} /* namespace rtc_time */
#endif /*__RTC_TIME_H__*/
+13 -10
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
@@ -530,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);
@@ -560,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.
}
}
@@ -623,7 +626,7 @@ bool GeoMap::on_touch(const TouchEvent event) {
on_move(p.x() / 2.0 * lon_ratio, p.y() / 2.0 * lat_ratio, false);
} else {
p = event.point - screen_rect().location();
on_move(tile_pixel_x_to_lon(p.x() + viewport_top_left_px, map_osm_zoom), tile_pixel_y_to_lat(p.y() + viewport_top_left_py, map_osm_zoom), true);
on_move(tile_pixel_x_to_lon(p.x() + viewport_top_left_px, map_osm_real_zoom), tile_pixel_y_to_lat(p.y() + viewport_top_left_py, map_osm_real_zoom), true);
}
return true;
}
+3 -2
View File
@@ -255,7 +255,7 @@ class GeoMap : public Widget {
void map_read_line_bin(ui::Color* buffer, uint16_t pixels);
// open street map related
uint8_t find_osm_file_tile();
void set_osm_max_zoom();
void set_osm_max_zoom(bool changeboth = false);
bool draw_osm_file(int zoom, int tile_x, int tile_y, int relative_x, int relative_y);
int lon2tile(double lon, int zoom);
int lat2tile(double lat, int zoom);
@@ -263,7 +263,8 @@ class GeoMap : public Widget {
double lat_to_pixel_y_tile(double lat, int zoom);
double tile_pixel_x_to_lon(int x, int zoom);
double tile_pixel_y_to_lat(int y, int zoom);
uint8_t map_osm_zoom{3};
uint8_t map_osm_zoom{5};
uint8_t map_osm_real_zoom{5};
double viewport_top_left_px = 0;
double viewport_top_left_py = 0;