diff --git a/RX_FSK/RX_FSK.ino b/RX_FSK/RX_FSK.ino index 3b86e18..c026fe8 100644 --- a/RX_FSK/RX_FSK.ino +++ b/RX_FSK/RX_FSK.ino @@ -1,5 +1,6 @@ #include "features.h" #include "version.h" +#include "core.h" #include #include @@ -42,6 +43,9 @@ #if FEATURE_APRS #include "src/conn-aprs.h" #endif +#if FEATURE_SONDEHUB +#include "src/conn-sondehub.h" +#endif //#define ESP_MEM_DEBUG 1 //int e; @@ -78,14 +82,15 @@ WiFiClient client; const char *sondeTypeStrSH[NSondeTypes] = { "DFM", "RS41", "RS92", "Mxx"/*never sent*/, "M10", "M20", "MRZ" }; -#if FEATURE_SONDEHUB -#define SONDEHUB_STATION_UPDATE_TIME (60*60*1000) // 60 min -#define SONDEHUB_MOBILE_STATION_UPDATE_TIME (30*1000) // 30 sec -WiFiClient shclient; // Sondehub v2 -int shImportInterval = 0; -char shImport = 0; -unsigned long time_last_update = 0; -#endif +// moved to connSondehub.cpp +//#if FEATURE_SONDEHUB +//#define SONDEHUB_STATION_UPDATE_TIME (60*60*1000) // 60 min +//#define SONDEHUB_MOBILE_STATION_UPDATE_TIME (30*1000) // 30 sec +//WiFiClient shclient; // Sondehub v2 +//int shImportInterval = 0; +//char shImport = 0; +//unsigned long time_last_update = 0; +//#endif // JSON over TCP for communicating with the rdzSonde (rdzwx-go) Android app WiFiServer rdzserver(14570); @@ -438,32 +443,6 @@ const char *createWIFIForm() { return message; } -#if 0 -// moved to map.html (active warning is still TODO -const char *createSondeHubMap() { - SondeInfo *s = &sonde.sondeList[0]; - char *ptr = message; - strcpy(ptr, HTMLHEAD); strcat(ptr, ""); - HTMLBODY(ptr, "map.html"); - if (!sonde.config.sondehub.active) { - strcat(ptr, "
NOTE: SondeHub uploading is not enabled, detected sonde will not be visable on map
"); - if ((*s->d.ser == 0) && ( !isnan(sonde.config.rxlat))) { - sprintf(ptr + strlen(ptr), "", sonde.config.rxlat, sonde.config.rxlon); - } else { - sprintf(ptr + strlen(ptr), "", s->d.ser); - } - } else { - if ((*s->d.ser == 0) && (!isnan(sonde.config.rxlat))) { - sprintf(ptr, "", sonde.config.rxlat, sonde.config.rxlon); - } else { - sprintf(ptr, "", s->d.ser); - } - } - HTMLBODYEND(ptr); - return message; -} -#endif - const char *handleWIFIPost(AsyncWebServerRequest * request) { char label[10]; // parameters: a_i, f_1, t_i (active/frequency/type) @@ -602,9 +581,6 @@ void setupConfigData() { sonde.setConfig(line.c_str()); } sonde.checkConfig(); // eliminate invalid entries -#if FEATURE_SONDEHUB - shImportInterval = 5; // refresh now in 5 seconds -#endif } @@ -2062,10 +2038,6 @@ void loopDecoder() { Serial.println(""); } -#if FEATURE_SONDEHUB - sondehub_reply_handler(&shclient); -#endif - // wifi active and good packet received => send packet SondeInfo *s = &sonde.sondeList[rxtask.receiveSonde]; if ((res & 0xff) == 0 && connected) { @@ -2076,40 +2048,6 @@ void loopDecoder() { #if FEATURE_APRS connAPRS.updateSonde(s); #endif -#if 0 - // moved to conn-aprs.cpp - char *str = aprs_senddata(s, sonde.config.call, sonde.config.objcall, sonde.config.udpfeed.symbol); - char raw[201]; - int rawlen = aprsstr_mon2raw(str, raw, APRS_MAXLEN); - Serial.println("Sending AXUDP"); - //Serial.println(raw); - udp.beginPacket(sonde.config.udpfeed.host, sonde.config.udpfeed.port); - udp.write((const uint8_t *)raw, rawlen); - udp.endPacket(); - if (tncclient.connected()) { - Serial.println("Sending position via TCP"); - char raw[201]; - int rawlen = aprsstr_mon2kiss(str, raw, APRS_MAXLEN); - Serial.print("sending: "); Serial.println(raw); - tncclient.write(raw, rawlen); - } - if (sonde.config.tcpfeed.active) { - static unsigned long lasttcp = 0; - if ( tcpclient.disconnected()) { - tcpclient.connect(sonde.config.tcpfeed.host, sonde.config.tcpfeed.port); - } - else if ( tcpclient.connected() ) { - unsigned long now = millis(); - Serial.printf("aprs: now-last = %ld\n", (now - lasttcp)); - if ( (now - lasttcp) > sonde.config.tcpfeed.highrate * 1000L ) { - strcat(str, "\r\n"); - Serial.print(str); - tcpclient.write(str, strlen(str)); - lasttcp = now; - } - } - } -#endif #if FEATURE_CHASEMAPPER if (sonde.config.cm.active) { @@ -2119,7 +2057,8 @@ void loopDecoder() { } #if FEATURE_SONDEHUB if (sonde.config.sondehub.active) { - sondehub_send_data(&shclient, s, &sonde.config.sondehub); + connSondehub.updateSonde( s ); // invoke sh_send_data.... + // sondehub_send_data(&shclient, s, &sonde.config.sondehub); } #endif @@ -2129,20 +2068,17 @@ void loopDecoder() { } else { #if FEATURE_SONDEHUB - sondehub_finish_data(&shclient, s, &sonde.config.sondehub); + connSondehub.updateSonde( NULL ); + // sondehub_finish_data(&shclient, s, &sonde.config.sondehub); #endif } - // Send own position periodically #if FEATURE_MQTT connMQTT.updateStation( NULL ); #endif #if FEATURE_APRS connAPRS.updateStation( NULL ); - //if (sonde.config.tcpfeed.active) { - // aprs_station_update(); - //} #endif // always send data, even if not valid.... if (rdzclient.connected()) { @@ -2279,9 +2215,10 @@ String translateEncryptionType(wifi_auth_mode_t encryptionType) { } } -enum t_wifi_state { WIFI_DISABLED, WIFI_SCAN, WIFI_CONNECT, WIFI_CONNECTED, WIFI_APMODE }; +// in core.h +//enum t_wifi_state { WIFI_DISABLED, WIFI_SCAN, WIFI_CONNECT, WIFI_CONNECTED, WIFI_APMODE }; -static t_wifi_state wifi_state = WIFI_DISABLED; +t_wifi_state wifi_state = WIFI_DISABLED; void enableNetwork(bool enable) { if (enable) { @@ -2299,21 +2236,22 @@ void enableNetwork(bool enable) { connMQTT.netsetup(); #endif #if FEATURE_SONDEHUB - if (sonde.config.sondehub.active && wifi_state != WIFI_APMODE) { - time_last_update = millis() + 1000; /* force sending update */ - sondehub_station_update(&shclient, &sonde.config.sondehub); - } + //if (sonde.config.sondehub.active && wifi_state != WIFI_APMODE) { + // time_last_update = millis() + 1000; /* force sending update */ + // sondehub_station_update(&shclient, &sonde.config.sondehub); + //} + connSondehub.netsetup(); #endif configTime(gmtOffset_sec, daylightOffset_sec, ntpServer); connected = true; +#if FEATURE_APRS + connAPRS.netsetup(); +#endif } else { MDNS.end(); connected = false; } -#if FEATURE_APRS - connAPRS.netsetup(); -#endif Serial.println("enableNetwork done"); } @@ -2962,7 +2900,8 @@ void loop() { } -#if FEATURE_SONDEHUB +#if 0 +// removed here, now in connSondehub // Sondehub v2 DB related codes /* diff --git a/RX_FSK/core.h b/RX_FSK/core.h new file mode 100644 index 0000000..8fbd0d7 --- /dev/null +++ b/RX_FSK/core.h @@ -0,0 +1,4 @@ +enum t_wifi_state { WIFI_DISABLED, WIFI_SCAN, WIFI_CONNECT, WIFI_CONNECTED, WIFI_APMODE }; +extern t_wifi_state wifi_state; + +extern const char *sondeTypeStrSH[]; diff --git a/RX_FSK/src/conn-aprs.cpp b/RX_FSK/src/conn-aprs.cpp index a4d4d4f..5fae85b 100644 --- a/RX_FSK/src/conn-aprs.cpp +++ b/RX_FSK/src/conn-aprs.cpp @@ -129,6 +129,10 @@ void ConnAPRS::updateStation( PosInfo *pi ) { } } +String ConnAPRS::getStatus() { + return String(""); +} + void ConnAPRS::aprs_station_update() { int chase = sonde.config.chase; // automatically decided if CHASE or FIXED mode is used (for config AUTO) diff --git a/RX_FSK/src/conn-aprs.h b/RX_FSK/src/conn-aprs.h index 9d832fd..6f1708a 100644 --- a/RX_FSK/src/conn-aprs.h +++ b/RX_FSK/src/conn-aprs.h @@ -29,6 +29,8 @@ public: /* Called approx 1x / second* */ void updateStation( PosInfo *pi ); + String getStatus(); + private: void aprs_station_update(); }; diff --git a/RX_FSK/src/conn-chasemapper.cpp b/RX_FSK/src/conn-chasemapper.cpp index 0e46eb8..b28ab00 100644 --- a/RX_FSK/src/conn-chasemapper.cpp +++ b/RX_FSK/src/conn-chasemapper.cpp @@ -64,5 +64,9 @@ void ConnChasemapper::updateSonde(SondeInfo *si) { void ConnChasemapper::updateStation(PosInfo *pi) { } +String ConnChasemapper::getStatus() { + return String(""); +} + ConnChasemapper connChasemapper; #endif diff --git a/RX_FSK/src/conn-chasemapper.h b/RX_FSK/src/conn-chasemapper.h index e17b307..ed5a228 100644 --- a/RX_FSK/src/conn-chasemapper.h +++ b/RX_FSK/src/conn-chasemapper.h @@ -10,6 +10,7 @@ public: void netsetup(); void updateSonde( SondeInfo *si ); void updateStation( PosInfo *pi ); + String getStatus(); }; extern ConnChasemapper connChasemapper; diff --git a/RX_FSK/src/conn-mqtt.cpp b/RX_FSK/src/conn-mqtt.cpp index cc1ef63..de194a5 100644 --- a/RX_FSK/src/conn-mqtt.cpp +++ b/RX_FSK/src/conn-mqtt.cpp @@ -82,6 +82,10 @@ void MQTT::updateStation( PosInfo *pi ) { } } +String MQTT::getStatus() { + return String(""); +} + // Internal (private) functions //void MQTT::connectToMqtt() { // Serial.println("Connecting to MQTT..."); diff --git a/RX_FSK/src/conn-mqtt.h b/RX_FSK/src/conn-mqtt.h index 75e41ee..2884499 100644 --- a/RX_FSK/src/conn-mqtt.h +++ b/RX_FSK/src/conn-mqtt.h @@ -25,6 +25,7 @@ public: /* Called approx 1x / second* */ virtual void updateStation( PosInfo *pi ); + virtual String getStatus(); private: WiFiClient mqttWifiClient; diff --git a/RX_FSK/src/conn-sondehub.cpp b/RX_FSK/src/conn-sondehub.cpp new file mode 100644 index 0000000..e494f85 --- /dev/null +++ b/RX_FSK/src/conn-sondehub.cpp @@ -0,0 +1,585 @@ +#include "../features.h" + +#if FEATURE_SONDEHUB + +#include "conn-sondehub.h" +#include "posinfo.h" +#include "../core.h" +#include "DFM.h" +#include "RS41.h" +#include +#include +#include "ShFreqImport.h" + +#include +#include + +#include + +extern const char *version_name; +extern const char *version_id; + +#define SONDEHUB_STATION_UPDATE_TIME (60*60*1000) // 60 min +#define SONDEHUB_MOBILE_STATION_UPDATE_TIME (30*1000) // 30 sec +WiFiClient shclient; // Sondehub v2 +int shImportInterval = 0; +char shImport = 0; +unsigned long time_last_update = 0; + +void ConnSondehub::init() { +} + +void ConnSondehub::netsetup() { + if (sonde.config.sondehub.active && wifi_state != WIFI_APMODE) { + time_last_update = millis() + 1000; /* force sending update */ + sondehub_station_update(); + + // SH import: initial refresh on connect, even if configured interval is longer + shImportInterval = 5; // refresh now in 5 seconds + } +} + +// Imitating the old non-modular code +// updateSonde is called once per second +// old code called +// each second: sondehub_reply_handler +// each second, if good decode: sondehub_send_data +// each second, if no good decode: sondehub_finish_data +void ConnSondehub::updateSonde( SondeInfo *si ) { + sondehub_reply_handler(); + if(si==NULL) { + sondehub_finish_data(); + } else { + sondehub_send_data(si); + } +} + + +void ConnSondehub::updateStation( PosInfo *pi ) { + // TODO: station_update should be here and not in netsetup() + // Currently, interlnal reply_handler does this, using gpsInfo global variable instead of this pi +} + +String ConnSondehub::getStatus() { + return String(""); +} + +/*** Code moved from RX_FSK to here ****/ + +// Sondehub v2 DB related codes +/* + Update station data to the sondehub v2 DB +*/ +/* which_pos: 0=none, 1=fixed, 2=gps */ +void ConnSondehub::sondehub_station_update() { + struct st_sondehub *conf = &sonde.config.sondehub; +#define STATION_DATA_LEN 300 + char data[STATION_DATA_LEN]; + char *w; + + // If there is no connection to some WiFi AP, we cannot upload any data at all.... + if ( wifi_state != WIFI_CONNECTED ) return; + + unsigned long time_now = millis(); + // time_delta will be correct, even if time_now overflows + unsigned long time_delta = time_now - time_last_update; + + int chase = conf->chase; + // automatically decided if CHASE or FIXED mode is used (for config AUTO) + if (chase == SH_LOC_AUTO) { + if (posInfo.chase) chase = SH_LOC_CHASE; else chase = SH_LOC_FIXED; + } + + // Use 30sec update time in chase mode, 60 min in station mode. + unsigned long update_time = (chase == SH_LOC_CHASE) ? SONDEHUB_MOBILE_STATION_UPDATE_TIME : SONDEHUB_STATION_UPDATE_TIME; + + // If it is not yet time to send another update. do nothing.... + if ( (time_delta <= update_time) ) return; + + Serial.println("sondehub_station_update()"); + time_last_update = time_now; + + if (!shclient.connected()) { + if (!shclient.connect(conf->host, 80)) { + Serial.println("Connection FAILED"); + return; + } + } + + w = data; + // not necessary... memset(w, 0, STATION_DATA_LEN); + + sprintf(w, + "{" + "\"software_name\": \"%s\"," + "\"software_version\": \"%s\"," + "\"uploader_callsign\": \"%s\",", + version_name, version_id, conf->callsign); + w += strlen(w); + + // Only send email if provided + if (strlen(conf->email) != 0) { + sprintf(w, "\"uploader_contact_email\": \"%s\",", conf->email); + w += strlen(w); + } + + // Only send antenna if provided + if (strlen(conf->antenna) != 0) { + sprintf(w, "\"uploader_antenna\": \"%s\",", conf->antenna); + w += strlen(w); + } + + // We send GPS position: (a) in CHASE mode, (b) in AUTO mode if no fixed location has been specified in config + if (chase == SH_LOC_CHASE) { + if (gpsPos.valid) { + sprintf(w, + "\"uploader_position\": [%.6f,%.6f,%d]," + "\"mobile\": true", + gpsPos.lat, gpsPos.lon, gpsPos.alt); + } else { + sprintf(w, "\"uploader_position\": [null,null,null]"); + } + w += strlen(w); + } + // Otherweise, in FIXED mode we send the fixed position from config (if specified) + else if (chase == SH_LOC_FIXED) { + if ((!isnan(sonde.config.rxlat)) && (!isnan(sonde.config.rxlon))) { + if (isnan(sonde.config.rxalt)) + sprintf(w, "\"uploader_position\": [%.6f,%.6f,null]", sonde.config.rxlat, sonde.config.rxlon); + else + sprintf(w, "\"uploader_position\": [%.6f,%.6f,%d]", sonde.config.rxlat, sonde.config.rxlon, (int)sonde.config.rxalt); + } else { + sprintf(w, "\"uploader_position\": [null,null,null]"); + } + w += strlen(w); + } else { + sprintf(w, "\"uploader_position\": [null,null,null]"); + w += strlen(w); + } + + // otherwise (in SH_LOC_NONE mode) we dont include any position info + sprintf(w, "}"); + + shclient.println("PUT /listeners HTTP/1.1"); + shclient.print("Host: "); + shclient.println(conf->host); + shclient.println("accept: text/plain"); + shclient.println("Content-Type: application/json"); + shclient.print("Content-Length: "); + shclient.println(strlen(data)); + shclient.println(); + shclient.println(data); + Serial.println(strlen(data)); + Serial.println(data); + Serial.println("Waiting for response"); + // TODO: better do this asynchronously + // At least, do this safely. See Notes-on-Using-WiFiClient.txt for details + // If any of the shclient.print failed before (remote end closed connection), + // then calling client->read will cause a LoadProhibited exception + if (shclient.connected()) { + String response = shclient.readString(); + Serial.println(response); + Serial.println("Response done..."); + } else { + Serial.println("SH client connection closed\n"); + } + //client->stop(); +} + +/* + Update sonde data to the sondehub v2 DB +*/ +enum SHState { SH_DISCONNECTED, SH_CONNECTING, SH_CONN_IDLE, SH_CONN_APPENDING, SH_CONN_WAITACK }; + +SHState shState = SH_DISCONNECTED; +time_t shStart = 0; + + + +void ConnSondehub::sondehub_reply_handler() { + // sondehub handler for tasks to be done even if no data is to be sent: + // process response messages from sondehub + // request frequency list (if active) +#define MSG_SIZE 1000 + char rs_msg[MSG_SIZE]; + + if (shImport == 1) { // we are waiting for a reply to a sondehub frequency import request + // while we are waiting, we do nothing else with sondehub... + int res = ShFreqImport::shImportHandleReply(&shclient); + Serial.printf("ret: %d\n", res); + // res==0 means more data is expected, res==1 means complete reply received (or error) + if (res == 1) { + shImport = 2; // finished + shImportInterval = sonde.config.sondehub.fiinterval * 60; + } + } + else { + // any reply here belongs to normal telemetry upload, lets just print it. + // and wait for a valid HTTP response + int cnt = 0; + while (shclient.available() > 0) { + // data is available from remote server, process it... + // readBytesUntil may wait for up to 1 second if enough data is not available... + // int cnt = shclient.readBytesUntil('\n', rs_msg, MSG_SIZE - 1); + int c = shclient.read(); + if (c < 0) break; // should never happen in available() returned >0 right before.... + rs_msg[cnt++] = c; + if (c == '\n') { + rs_msg[cnt] = 0; + Serial.println(rs_msg); + // If something that looks like a valid HTTP response is received, we are ready to send the next data item + if (shState == SH_CONN_WAITACK && cnt > 11 && strncmp(rs_msg, "HTTP/1", 6) == 0) { + shState = SH_CONN_IDLE; + } + cnt = 0; + } + if (cnt >= MSG_SIZE - 1) { + cnt = 0; + Serial.println("(overlong line from network, ignoring)"); + } + } + if (cnt > 0) { + rs_msg[cnt + 1] = 0; + Serial.println(rs_msg); + } + } + // send import requests if needed + if (sonde.config.sondehub.fiactive) { + if (shImport == 2) { + Serial.printf("next sondehub frequncy import in %d seconds\n", shImportInterval); + shImportInterval --; + if (shImportInterval <= 0) { + shImport = 0; + } + } + else if (shImport == 0) { + if (shState == SH_CONN_APPENDING || shState == SH_CONN_WAITACK) + Serial.printf("Time to request next sondehub import.... but still busy with upload request"); + else + sondehub_send_fimport(); + } + } + + // also handle periodic station updates here... + // interval check moved to sondehub_station_update to avoid having to calculate distance in auto mode twice + if (sonde.config.sondehub.active) { + if (shState == SH_CONN_IDLE || shState == SH_DISCONNECTED ) { + // (do not set station update while a telemetry report is being sent + sondehub_station_update(); + } + } +} + +void ConnSondehub::sondehub_send_fimport() { + if (shState == SH_CONN_APPENDING || shState == SH_CONN_WAITACK) { + // Currently busy with SondeHub data upload + // So do nothing here. + // sond_fimport will be re-sent later, when shState becomes SH_CONN_IDLE + return; + } + // It's time to run, so check prerequisites + float lat = sonde.config.rxlat, lon = sonde.config.rxlon; + if (gpsPos.valid) { + lat = gpsPos.lat; + lon = gpsPos.lon; + } + + int maxdist = sonde.config.sondehub.fimaxdist; // km + int maxage = sonde.config.sondehub.fimaxage * 60; // fimaxage is hours, shImportSendRequest uses minutes + int fiinterval = sonde.config.sondehub.fiinterval; + Serial.printf("shimp : %f %f %d %d %d\n", lat, lon, maxdist, maxage, shImportInterval); + if ( !isnan(lat) && !isnan(lon) && maxdist > 0 && maxage > 0 && fiinterval > 0 ) { + int res = ShFreqImport::shImportSendRequest(&shclient, lat, lon, maxdist, maxage); + if (res == 0) shImport = 1; // Request OK: wait for response + else shImport = 2; // Request failed: wait interval, then retry + } +} + + +// in hours.... max allowed diff UTC <-> sonde time +#define SONDEHUB_TIME_THRESHOLD (3) +void ConnSondehub::sondehub_send_data(SondeInfo * s) { + struct st_sondehub *conf = &sonde.config.sondehub; + + Serial.println("sondehub_send_data()"); + Serial.printf("shState = %d\n", shState); + + // max age of data in JSON request (in seconds) +#define SONDEHUB_MAXAGE 15 + + char rs_msg[MSG_SIZE]; + char *w; + struct tm ts; + // config setting M10 and M20 will both decode both types, so use the real type that was decoded + uint8_t realtype = sonde.realType(s); + + // For DFM, s->d.time is data from subframe DAT8 (gps date/hh/mm), and sec is from DAT1 (gps sec/usec) + // For all others, sec should always be 0 and time the exact time in seconds + time_t t = s->d.time; + + int chase = conf->chase; + // automatically decided if CHASE or FIXED mode is used (for config AUTO) + if (chase == SH_LOC_AUTO) { + if (posInfo.chase) chase = SH_LOC_CHASE; else chase = SH_LOC_FIXED; + } + + + struct tm timeinfo; + time_t now; + time(&now); + gmtime_r(&now, &timeinfo); + if (timeinfo.tm_year <= (2016 - 1900)) { + Serial.println("Failed to obtain time"); + return; + } + + // Check if current sonde data is valid. If not, don't do anything.... + if (*s->d.ser == 0 || s->d.validID == 0 ) return; // Don't send anything without serial number + if (((int)s->d.lat == 0) && ((int)s->d.lon == 0)) return; // Sometimes these values are zeroes. Don't send those to the sondehub + if ((int)s->d.alt > 50000) return; // If alt is too high don't send to SondeHub + // M20 data does not include #sat information + if ( realtype != STYPE_M20 && (int)s->d.sats < 4) return; // If not enough sats don't send to SondeHub + + // If not connected to sondehub, try reconnecting. + // TODO: do this outside of main loop + if (!shclient.connected()) { + Serial.println("NO CONNECTION"); + shState = SH_DISCONNECTED; + if (!shclient.connect(conf->host, 80)) { + Serial.println("Connection FAILED"); + return; + } + shclient.Client::setTimeout(0); // does this work? + shState = SH_CONN_IDLE; + } + + if ( shState == SH_CONN_WAITACK ) { + Serial.println("Previous SH-frame not yet ack'ed, not sending new data"); + return; + } + if ( abs(now - (time_t)s->d.time) > (3600 * SONDEHUB_TIME_THRESHOLD) ) { + Serial.printf("Sonde time %d too far from current UTC time %ld", s->d.time, now); + return; + } + + // DFM uses UTC. Most of the other radiosondes use GPS time + // SondeHub expect datetime to be the same time sytem as the sonde transmits as time stamp + if ( realtype == STYPE_RS41 || realtype == STYPE_RS92 || realtype == STYPE_M20 ) { + t += 18; // convert back to GPS time from UTC time +18s + } + + gmtime_r(&t, &ts); + + memset(rs_msg, 0, MSG_SIZE); + w = rs_msg; + + sprintf(w, + " {" + "\"software_name\": \"%s\"," + "\"software_version\": \"%s\"," + "\"uploader_callsign\": \"%s\"," + "\"time_received\": \"%04d-%02d-%02dT%02d:%02d:%02d.000Z\"," + "\"manufacturer\": \"%s\"," + "\"serial\": \"%s\"," + "\"datetime\": \"%04d-%02d-%02dT%02d:%02d:%02d.000Z\"," + "\"lat\": %.5f," + "\"lon\": %.5f," + "\"alt\": %.5f," + "\"frequency\": %.3f," + "\"vel_h\": %.5f," + "\"vel_v\": %.5f," + "\"heading\": %.5f," + "\"rssi\": %.1f," + "\"frame\": %d," + "\"type\": \"%s\",", + version_name, version_id, conf->callsign, + timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday, timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec, + manufacturer_string[realtype], s->d.ser, + ts.tm_year + 1900, ts.tm_mon + 1, ts.tm_mday, ts.tm_hour, ts.tm_min, ts.tm_sec, + (float)s->d.lat, (float)s->d.lon, (float)s->d.alt, (float)s->freq, (float)s->d.hs, (float)s->d.vs, + (float)s->d.dir, -((float)s->rssi / 2), s->d.vframe, sondeTypeStrSH[realtype] + ); + w += strlen(w); + + // Only send sats if not M20 + if (realtype != STYPE_M20) { + sprintf(w, "\"sats\": %d,", (int)s->d.sats); + w += strlen(w); + } + + /* if there is a subtype (DFM only) */ + if ( TYPE_IS_DFM(s->type) && s->d.subtype > 0 ) { + if ( (s->d.subtype & 0xF) != DFM_UNK) { + const char *t = dfmSubtypeLong[s->d.subtype & 0xF]; + sprintf(w, "\"subtype\": \"%s\",", t); + } + else { + sprintf(w, "\"subtype\": \"DFMx%X\",", s->d.subtype >> 4); // Unknown subtype + } + w += strlen(w); + } else if ( s->type == STYPE_RS41 ) { + char buf[11]; + if (RS41::getSubtype(buf, 11, s) == 0) { + sprintf(w, "\"subtype\": \"%s\",", buf); + w += strlen(w); + } + } + + // Only send temp if provided + if (!isnan(s->d.temperature)) { + sprintf(w, "\"temp\": %.1f,", s->d.temperature); + w += strlen(w); + } + + // Only send humidity if provided + if (!isnan(s->d.relativeHumidity)) { + sprintf(w, "\"humidity\": %.1f,", s->d.relativeHumidity); + w += strlen(w); + } + + // Only send pressure if provided + if (!isnan(s->d.pressure)) { + sprintf(w, "\"pressure\": %.2f,", s->d.pressure); + w += strlen(w); + } + + // Only send burst timer if RS41 and fresh within the last 51s + if ((realtype == STYPE_RS41) && (s->d.crefKT > 0) && (s->d.vframe - s->d.crefKT < 51)) { + sprintf(w, "\"burst_timer\": %d,", (int)s->d.countKT); + w += strlen(w); + } + + // Only send battery if provided + if (s->d.batteryVoltage > 0) { + sprintf(w, "\"batt\": %.2f,", s->d.batteryVoltage); + w += strlen(w); + } + + // Only send antenna if provided + if (strlen(conf->antenna) != 0) { + sprintf(w, "\"uploader_antenna\": \"%s\",", conf->antenna); + w += strlen(w); + } + + // We send GPS position: (a) in CHASE mode, (b) in AUTO mode if no fixed location has been specified in config + if (chase == SH_LOC_CHASE) { + if (gpsPos.valid) { + sprintf(w, "\"uploader_position\": [%.6f,%.6f,%d]", gpsPos.lat, gpsPos.lon, gpsPos.alt); + } else { + sprintf(w, "\"uploader_position\": [null,null,null]"); + } + w += strlen(w); + } + // Otherweise, in FIXED mode we send the fixed position from config (if specified) + else if (chase == SH_LOC_FIXED) { + if ((!isnan(sonde.config.rxlat)) && (!isnan(sonde.config.rxlon))) { + if (isnan(sonde.config.rxalt)) + sprintf(w, "\"uploader_position\": [%.6f,%.6f,null]", sonde.config.rxlat, sonde.config.rxlon); + else + sprintf(w, "\"uploader_position\": [%.6f,%.6f,%d]", sonde.config.rxlat, sonde.config.rxlon, (int)sonde.config.rxalt); + } else { + sprintf(w, "\"uploader_position\": [null,null,null]"); + } + w += strlen(w); + } else { + sprintf(w, "\"uploader_position\": [null,null,null]"); + w += strlen(w); + } + + // otherwise (in SH_LOC_NONE mode) we dont include any position info + sprintf(w, "}"); + + if (shState != SH_CONN_APPENDING) { + sondehub_send_header(s, &timeinfo); + sondehub_send_next(s, rs_msg, strlen(rs_msg), 1); + shState = SH_CONN_APPENDING; + shStart = now; + } else { + sondehub_send_next(s, rs_msg, strlen(rs_msg), 0); + } + if (now - shStart > SONDEHUB_MAXAGE) { // after MAXAGE seconds + sondehub_send_last(); + shState = SH_CONN_WAITACK; + shStart = 0; + } + //client->println(rs_msg); + //Serial.println(rs_msg); + //String response = client->readString(); + //Serial.println(response); +} + +void ConnSondehub::sondehub_finish_data() { + // If there is an "old" pending collection of JSON data sets, send it even if no now data is received + if (shState == SH_CONN_APPENDING) { + time_t now; + time(&now); + if (now - shStart > SONDEHUB_MAXAGE + 3) { // after MAXAGE seconds + sondehub_send_last(); + shState = SH_CONN_WAITACK; + shStart = 0; + } + } +} + +static const char *DAYS[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}; +static const char *MONTHS[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Noc", "Dec"}; + +void ConnSondehub::sondehub_send_header(SondeInfo * s, struct tm * now) { + struct st_sondehub *conf = &sonde.config.sondehub; + Serial.print("PUT /sondes/telemetry HTTP/1.1\r\n" + "Host: "); + Serial.println(conf->host); + Serial.print("accept: text/plain\r\n" + "Content-Type: application/json\r\n" + "Transfer-Encoding: chunked\r\n"); + + shclient.print("PUT /sondes/telemetry HTTP/1.1\r\n" + "Host: "); + shclient.println(conf->host); + shclient.print("accept: text/plain\r\n" + "Content-Type: application/json\r\n" + "Transfer-Encoding: chunked\r\n"); + if (now) { + Serial.printf("Date: %s, %02d %s %04d %02d:%02d:%02d GMT\r\n", + DAYS[now->tm_wday], now->tm_mday, MONTHS[now->tm_mon], now->tm_year + 1900, + now->tm_hour, now->tm_min, now->tm_sec); + shclient.printf("Date: %s, %02d %s %04d %02d:%02d:%02d GMT\r\n", + DAYS[now->tm_wday], now->tm_mday, MONTHS[now->tm_mon], now->tm_year + 1900, + now->tm_hour, now->tm_min, now->tm_sec); + } + shclient.print("User-agent: "); + shclient.print(version_name); + shclient.print("/"); + shclient.println(version_id); + shclient.println(""); // another cr lf as indication of end of header +} +void ConnSondehub::sondehub_send_next(SondeInfo * s, char *chunk, int chunklen, int first) { + // send next chunk of JSON request + shclient.printf("%x\r\n", chunklen + 1); + shclient.write(first ? "[" : ",", 1); + shclient.write(chunk, chunklen); + shclient.print("\r\n"); + + Serial.printf("%x\r\n", chunklen + 1); + Serial.write((const uint8_t *)(first ? "[" : ","), 1); + Serial.write((const uint8_t *)chunk, chunklen); + Serial.print("\r\n"); +} +void ConnSondehub::sondehub_send_last() { + // last chunk. just the closing "]" of the json request + shclient.printf("1\r\n]\r\n0\r\n\r\n"); + Serial.printf("1\r\n]\r\n0\r\n\r\n"); +} + + + + + + +ConnSondehub connSondehub; + +#endif + + + + diff --git a/RX_FSK/src/conn-sondehub.h b/RX_FSK/src/conn-sondehub.h new file mode 100644 index 0000000..32d8afc --- /dev/null +++ b/RX_FSK/src/conn-sondehub.h @@ -0,0 +1,42 @@ +#include "../features.h" +#if FEATURE_SONDEHUB + +#ifndef conn_sondehub_h +#define conn_sondehub_h + +#include "conn.h" + + +class ConnSondehub : public Conn +{ +public: + /* Called once on startup */ + void init(); + + /* Called whenever the network becomes available */ + void netsetup(); + + /* Called approx 1x / second (maybe only if good data is available) */ + void updateSonde( SondeInfo *si ); + + /* Called approx 1x / second* */ + void updateStation( PosInfo *pi ); + + String getStatus(); + +private: + void sondehub_station_update(); + void sondehub_reply_handler(); + void sondehub_send_fimport(); + void sondehub_send_data(SondeInfo * s); + void sondehub_finish_data(); + void sondehub_send_header(SondeInfo * s, struct tm * now); + void sondehub_send_next(SondeInfo * s, char *chunk, int chunklen, int first); + void sondehub_send_last(); +}; + + +extern ConnSondehub connSondehub; +#endif + +#endif diff --git a/RX_FSK/src/conn.h b/RX_FSK/src/conn.h index cd001e4..d6cc1c9 100644 --- a/RX_FSK/src/conn.h +++ b/RX_FSK/src/conn.h @@ -34,5 +34,7 @@ public: /* Called approx 1x / second* */ virtual void updateStation( PosInfo *pi ); + /* Called to retrieve status (used for Info in about tab) */ + virtual String getStatus(); }; #endif diff --git a/RX_FSK/version.h b/RX_FSK/version.h index 63a55f6..0a4d4c5 100644 --- a/RX_FSK/version.h +++ b/RX_FSK/version.h @@ -1,4 +1,4 @@ const char *version_name = "rdzTTGOsonde"; -const char *version_id = "devel20240110"; +const char *version_id = "devel20240116"; const int SPIFFS_MAJOR=2; const int SPIFFS_MINOR=17; diff --git a/platformio.ini b/platformio.ini index 089385f..1653205 100644 --- a/platformio.ini +++ b/platformio.ini @@ -28,6 +28,7 @@ platform = https://github.com/platformio/platform-espressif32.git#v6.3.0 board = ttgo-lora32-v1 framework = arduino monitor_speed = 115200 +monitor_filters = esp32_exception_decoder lib_deps = ${extra.lib_deps_builtin} ${extra.lib_deps_external}