diff --git a/firmware/application/apps/ui_sonde.cpp b/firmware/application/apps/ui_sonde.cpp index ff927076b..ae97d925d 100644 --- a/firmware/application/apps/ui_sonde.cpp +++ b/firmware/application/apps/ui_sonde.cpp @@ -30,11 +30,10 @@ #include "portapack.hpp" #include #include +#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(); 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(gps_info.alt) - static_cast(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); diff --git a/firmware/application/apps/ui_sonde.hpp b/firmware/application/apps/ui_sonde.hpp index 11f4b942d..2f4a3f354 100644 --- a/firmware/application/apps/ui_sonde.hpp +++ b/firmware/application/apps/ui_sonde.hpp @@ -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, diff --git a/firmware/application/baseband_api.hpp b/firmware/application/baseband_api.hpp index 3f2e7d972..c642cc1c7 100644 --- a/firmware/application/baseband_api.hpp +++ b/firmware/application/baseband_api.hpp @@ -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); diff --git a/firmware/application/rtc_time.cpp b/firmware/application/rtc_time.cpp index 68e8824df..55f241a5f 100644 --- a/firmware/application/rtc_time.cpp +++ b/firmware/application/rtc_time.cpp @@ -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 */ diff --git a/firmware/application/rtc_time.hpp b/firmware/application/rtc_time.hpp index 207b2a100..19c126db5 100644 --- a/firmware/application/rtc_time.hpp +++ b/firmware/application/rtc_time.hpp @@ -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__*/ diff --git a/firmware/application/ui/ui_geomap.cpp b/firmware/application/ui/ui_geomap.cpp index 59c8a9f2a..fca90d717 100644 --- a/firmware/application/ui/ui_geomap.cpp +++ b/firmware/application/ui/ui_geomap.cpp @@ -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; } diff --git a/firmware/application/ui/ui_geomap.hpp b/firmware/application/ui/ui_geomap.hpp index 414474b21..38ebd0ca8 100644 --- a/firmware/application/ui/ui_geomap.hpp +++ b/firmware/application/ui/ui_geomap.hpp @@ -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; diff --git a/firmware/common/sonde_packet.cpp b/firmware/common/sonde_packet.cpp index 0878f51f9..8bcb73779 100644 --- a/firmware/common/sonde_packet.cpp +++ b/firmware/common/sonde_packet.cpp @@ -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) diff --git a/firmware/common/sonde_packet.hpp b/firmware/common/sonde_packet.hpp index f5475df7b..bbe5d04fa 100644 --- a/firmware/common/sonde_packet.hpp +++ b/firmware/common/sonde_packet.hpp @@ -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; };