diff --git a/README.md b/README.md index e4d9f16..cbe56cc 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,8 @@ Raspberry Pi OS no longer includes WiringPi, so you must install Wiring Pi from MQTT ======= +MQTT support was added recently, and needs the following library installed:: + cd ~ mkdir MQTTClients cd MQTTClients @@ -91,6 +93,7 @@ The configuration is in the file gateway.txt. Example: tracker=M0RPI EnableHabitat=N + EnableSondehub=N EnableSSDV=Y LogTelemetry=Y LogPackets=Y @@ -121,6 +124,8 @@ The global options are: EnableHabitat=. Enables uploading of telemetry packets to Habitat. + EnableSondhub=. Enables uploading of telemetry packets to the amateur Sondehub system. + EnableSSDV=. Enables uploading of SSDV image packets to the SSDV server. EnableHABLink=. Enables uploading of telemetry packets to the hab.link server. @@ -175,8 +180,8 @@ and the channel-specific options are: ​ 4 = Test mode not for normal use. ​ 5 = (normal for calling mode) Explicit mode, Error coding 4:8, Bandwidth 41.7kHz, SF 11, Low data rate optimize off ​ - SF_= e.g. SF_0=7 - +​ SF_= e.g. SF_0=7 +​ Bandwidth_=. e.g. Bandwidth_0=41K7. Options are 7K8, 10K4, 15K6, 20K8, 31K25, 41K7, 62K5, 125K, 250K, 500K Implicit_=. e.g. Implicit_0=Y @@ -290,11 +295,15 @@ Many thanks to David Brooke for coding this feature and the AFC. Change History ============== -16/02/2022 - V1.8.46 +## 04/09/2022 - V1.9.0 + + Added support for uploading telemetry and listener details to the amateur Sondehub system. + +## 16/02/2022 - V1.8.46 Added flexible MQTT topic - $GATEWAY$ gets replaced by gateway callsign; $PAYLOAD$ gets replaced by payload callsign -14/02/2022 - V1.8.45 +## 14/02/2022 - V1.8.45 Added MQTT support (coded by David Johnson G4DPZ) diff --git a/gateway.c b/gateway.c index 6375344..24f03c1 100644 --- a/gateway.c +++ b/gateway.c @@ -33,6 +33,7 @@ #include "ssdv.h" #include "ftp.h" #include "habitat.h" +#include "sondehub.h" #include "mqtt.h" #include "hablink.h" #include "network.h" @@ -47,7 +48,7 @@ #include "udpclient.h" #include "lifo_buffer.h" -#define VERSION "V1.8.46" +#define VERSION "V1.9.0" bool run = TRUE; // RFM98 @@ -1155,6 +1156,11 @@ int ProcessTelemetryMessage(int Channel, received_t *Received) SetHablinkSentence(startmessage); } + if (Config.EnableSondehub) + { + SetSondehubSentence(Channel, startmessage); + } + tm = localtime( &Received->Metadata.Timestamp ); LogMessage("%02d:%02d:%02d Ch%d: %s%s\n", tm->tm_hour, tm->tm_min, tm->tm_sec, Channel, startmessage, Repeated ? " (repeated)" : ""); @@ -2009,6 +2015,7 @@ void LoadConfigFile(void) Config.InternetLED = -1; Config.LoRaDevices[0].ActivityLED = -1; Config.LoRaDevices[1].ActivityLED = -1; + strcpy(Config.radio, "Uputronics LoRa HAT"); // Default pin allocations Config.LoRaDevices[0].DIO0 = 6; @@ -2040,6 +2047,7 @@ void LoadConfigFile(void) RegisterConfigBoolean(MainSection, -1, "EnableHabitat", &Config.EnableHabitat, NULL); RegisterConfigBoolean(MainSection, -1, "EnableSSDV", &Config.EnableSSDV, NULL); RegisterConfigBoolean(MainSection, -1, "EnableHablink", &Config.EnableHablink, NULL); + RegisterConfigBoolean(MainSection, -1, "EnableSondehub", &Config.EnableSondehub, NULL); RegisterConfigString(MainSection, -1, "HablinkAddress", Config.HablinkAddress, sizeof(Config.HablinkAddress), NULL); @@ -2109,6 +2117,7 @@ void LoadConfigFile(void) // Listener RegisterConfigDouble(MainSection, -1, "Latitude", &Config.latitude, NULL); RegisterConfigDouble(MainSection, -1, "Longitude", &Config.longitude, NULL); + RegisterConfigDouble(MainSection, -1, "Altitude", &Config.altitude, NULL); RegisterConfigString(MainSection, -1, "radio", Config.radio, sizeof(Config.radio), NULL); RegisterConfigString(MainSection, -1, "antenna", Config.antenna, sizeof(Config.antenna), NULL); @@ -2635,7 +2644,7 @@ int main( int argc, char **argv ) int ch; int LoopPeriod, MSPerLoop; int Channel; - pthread_t SSDVThread, FTPThread, NetworkThread, HabitatThread, HablinkThread, ServerThread, TelnetThread, ListenerThread, DataportThread, ChatportThread, MQTTThread; + pthread_t SSDVThread, FTPThread, NetworkThread, HabitatThread, HablinkThread, SondehubThread, ServerThread, TelnetThread, ListenerThread, DataportThread, ChatportThread, MQTTThread; struct TServerInfo JSONInfo, TelnetInfo, DataportInfo, ChatportInfo; atexit(bye); @@ -2735,21 +2744,21 @@ int main( int argc, char **argv ) if (Config.EnableMQTT) { lifo_buffer_init(&MQTT_Upload_Buffer, 1024); - mqtt_connect_t *mqttConnection = malloc(sizeof *mqttConnection); + mqtt_connect_t *mqttConnection = malloc(sizeof *mqttConnection); - strcpy(mqttConnection->host, Config.MQTTHost); + strcpy(mqttConnection->host, Config.MQTTHost); strcpy(mqttConnection->port, Config.MQTTPort); strcpy(mqttConnection->user, Config.MQTTUser); strcpy(mqttConnection->pass, Config.MQTTPass); strcpy(mqttConnection->topic, Config.MQTTTopic); strcpy(mqttConnection->clientId, Config.MQTTClient); - if ( pthread_create (&MQTTThread, NULL, MQTTLoop, mqttConnection)) - { - fprintf( stderr, "Error creating MQTT thread\n" ); + if ( pthread_create (&MQTTThread, NULL, MQTTLoop, mqttConnection)) + { + fprintf( stderr, "Error creating MQTT thread\n" ); free(mqttConnection); - return 1; - } + return 1; + } } if (Config.EnableHablink && Config.HablinkAddress[0]) @@ -2761,6 +2770,15 @@ int main( int argc, char **argv ) } } + if (Config.EnableSondehub) + { + if (pthread_create (&SondehubThread, NULL, SondehubLoop, NULL)) + { + fprintf( stderr, "Error creating Sondehub thread\n" ); + return 1; + } + } + if (Config.ServerPort > 0) { JSONInfo.Port = Config.ServerPort; diff --git a/global.h b/global.h index 71c5b22..ad07e2c 100644 --- a/global.h +++ b/global.h @@ -110,11 +110,12 @@ struct TLoRaDevice struct TConfig { char Tracker[16]; // Callsign or name of receiver - double latitude, longitude; // Receiver's location + double latitude, longitude, altitude; // Receiver's location int EnableHabitat; int EnableSSDV; int EnableHablink; + int EnableSondehub; char HablinkAddress[32]; int EnableTelemetryLogging; int EnablePacketLogging; diff --git a/sondehub.c b/sondehub.c new file mode 100755 index 0000000..50ad09b --- /dev/null +++ b/sondehub.c @@ -0,0 +1,261 @@ +#include +#include +#include +#include +#include // Standard input/output definitions +#include // String function definitions +#include // UNIX standard function definitions +#include // File control definitions +#include // Error number definitions +#include // POSIX terminal control definitions +#include +#include +#include +#include +#include +#include +#include +#include + +#include "global.h" +#include "gateway.h" +#include "sondehub.h" + +struct TPayload SondehubPayloads[2]; + +void SetSondehubSentence(int Channel, char *tmp) +{ + sscanf(tmp + 2, "%31[^,],%u,%8[^,],%lf,%lf,%d", + SondehubPayloads[Channel].Payload, + &SondehubPayloads[Channel].Counter, + SondehubPayloads[Channel].Time, + &SondehubPayloads[Channel].Latitude, + &SondehubPayloads[Channel].Longitude, + &SondehubPayloads[Channel].Altitude); + + // LogMessage("Sondehub: %s,%d,%s,%.5lf,%.5lf,%d\n", + // SondehubPayloads[Channel].Payload, + // SondehubPayloads[Channel].Counter, + // SondehubPayloads[Channel].Time, + // SondehubPayloads[Channel].Latitude, + // SondehubPayloads[Channel].Longitude, + // SondehubPayloads[Channel].Altitude); + + SondehubPayloads[Channel].InUse = 1; +} + +size_t sondehub_write_data( void *buffer, size_t size, size_t nmemb, void *userp ) +{ + // LogMessage("%s\n", (char *)buffer); + return size * nmemb; +} + +int UploadJSONToServer(char *url, char *json) +{ + CURL *curl; + CURLcode res; + char curl_error[CURL_ERROR_SIZE]; + + /* get a curl handle */ + curl = curl_easy_init( ); + if ( curl ) + { + bool result; + struct curl_slist *headers = NULL; + int retries; + long int http_resp; + + // So that the response to the curl PUT doesn't mess up my finely crafted display! + curl_easy_setopt( curl, CURLOPT_WRITEFUNCTION, sondehub_write_data ); + + // Set the timeout + curl_easy_setopt( curl, CURLOPT_TIMEOUT, 15 ); + + // RJH capture http errors and report + // curl_easy_setopt( curl, CURLOPT_FAILONERROR, 1 ); + curl_easy_setopt( curl, CURLOPT_ERRORBUFFER, curl_error ); + + // Avoid curl library bug that happens if above timeout occurs (sigh) + curl_easy_setopt( curl, CURLOPT_NOSIGNAL, 1 ); + + // Set the headers + headers = NULL; + headers = curl_slist_append(headers, "Accept: application/json"); + headers = curl_slist_append(headers, "Content-Type: application/json"); + headers = curl_slist_append(headers, "charsets: utf-8" ); + + // PUT https://api.v2.sondehub.org/amateur/telemetry with content-type application/json + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); + curl_easy_setopt(curl, CURLOPT_URL, url); + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT"); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json); + + retries = 0; + do + { + // Perform the request, res will get the return code + res = curl_easy_perform( curl ); + + // Check for errors + if ( res == CURLE_OK ) + { + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_resp); + if (http_resp == 200) + { + // LogMessage("Saved OK to sondehub\n"); + // Everything performing nominally (even if we didn't successfully insert this time) + result = true; + } + else + { + LogMessage("Unexpected HTTP response %ld for URL '%s'\n", http_resp, url); + result = false; + } + } + else + { + http_resp = 0; + LogMessage("Failed for URL '%s'\n", url); + LogMessage("curl_easy_perform() failed: %s\n", curl_easy_strerror(res)); + LogMessage("error: %s\n", curl_error); + // Likely a network error, so return false to requeue + result = false; + } + } while ((!result) && (++retries < 5)); + + // always cleanup + curl_slist_free_all( headers ); + curl_easy_cleanup( curl ); + + return result; + } + else + { + /* CURL error, return false so we requeue */ + return false; + } + +} + +int UploadSondehubPosition(int Channel) +{ + char url[200]; + char json[1000], now[32], doc_time[32]; + time_t rawtime; + struct tm *tm, *doc_tm; + + // Get formatted timestamp for now + time( &rawtime ); + tm = gmtime( &rawtime ); + strftime( now, sizeof( now ), "%Y-%0m-%0dT%H:%M:%SZ", tm ); + + // Get formatted timestamp for doc timestamp + doc_tm = gmtime( &rawtime ); + strftime(doc_time, sizeof( doc_time ), "%Y-%0m-%0dT%H:%M:%SZ", doc_tm); + + // Create json as required by sondehub-amateur + sprintf(json, "[{\"software_name\": \"LoRa Gateway\"," // Fixed software name + "\"software_version\": \"%s\"," // Version + "\"uploader_callsign\": \"%s\"," // User callsign + "\"time_received\": \"%s\"," // UTC + "\"payload_callsign\": \"%s\"," // Payload callsign + "\"datetime\":\"%s\"," // UTC from payload + "\"lat\": %.5lf," // Latitude + "\"lon\": %.5lf," // Longitude + "\"alt\": %.d," // Altitude + "\"frequency\": %.4lf," // Frequency + "\"modulation\": \"LoRa\"," // Modulation + // DoubleToString('snr', SNR, HasSNR) + + // DoubleToString('rssi', PacketRSSI, HasPacketRSSI) + + "\"uploader_position\": [" + " %.3lf," // Listener Latitude + " %.3lf," // Listener Longitude + " %.0lf" // Listener Altitude + "]," + "\"uploader_antenna\": \"%s\"" + "}]", + Config.Version, Config.Tracker, now, + SondehubPayloads[Channel].Payload, doc_time, + SondehubPayloads[Channel].Latitude, SondehubPayloads[Channel].Longitude, SondehubPayloads[Channel].Altitude, + Config.LoRaDevices[Channel].Frequency + Config.LoRaDevices[Channel].FrequencyOffset, + Config.latitude, Config.longitude, Config.altitude, Config.antenna); + + // Set the URL that is about to receive our PUT + strcpy(url, "https://api.v2.sondehub.org/amateur/telemetry"); + + return UploadJSONToServer(url, json); +} + +int UploadListenerToSondehub(void) +{ + char url[200]; + char json[1000], now[32], doc_time[32]; + time_t rawtime; + struct tm *tm, *doc_tm; + + // Get formatted timestamp for now + time( &rawtime ); + tm = gmtime( &rawtime ); + strftime( now, sizeof( now ), "%Y-%0m-%0dT%H:%M:%SZ", tm ); + + // Get formatted timestamp for doc timestamp + doc_tm = gmtime( &rawtime ); + strftime(doc_time, sizeof( doc_time ), "%Y-%0m-%0dT%H:%M:%SZ", doc_tm); + + // Create json as required by sondehub-amateur + sprintf(json, "{\"software_name\": \"LoRa Gateway\"," // Fixed software name + "\"software_version\": \"%s\"," // Version + "\"uploader_callsign\": \"%s\"," // User callsign + "\"uploader_position\": [" + " %.3lf," // Listener Latitude + " %.3lf," // Listener Longitude + " %.0lf" // Listener Altitude + "]," + "\"uploader_radio\": \"%s\"," + "\"uploader_antenna\": \"%s\"" + "}", + Config.Version, Config.Tracker, + Config.latitude, Config.longitude, Config.altitude, + Config.radio, Config.antenna); + + strcpy(url, "https://api.v2.sondehub.org/amateur/listeners"); + + return UploadJSONToServer(url, json); +} + + +void *SondehubLoop( void *vars ) +{ + while (1) + { + static long ListenerCountdown = 0; + int Channel; + + for (Channel=0; Channel<=1; Channel++) + { + if (SondehubPayloads[Channel].InUse) + { + if (UploadSondehubPosition(Channel)) + { + SondehubPayloads[Channel].InUse = 0; + } + } + } + + if (--ListenerCountdown <= 0) + { + if (UploadListenerToSondehub()) + { + LogMessage("Uploaded listener info to sondhub"); + ListenerCountdown = 216000; // Every 6 hours + } + else + { + LogMessage("Failed to upload listener info to Sondehub/amateur"); + ListenerCountdown = 600; // Try again in 1 minute + } + } + + usleep(100000); + } +} diff --git a/sondehub.h b/sondehub.h new file mode 100755 index 0000000..2e9af17 --- /dev/null +++ b/sondehub.h @@ -0,0 +1,2 @@ +void *SondehubLoop( void *some_void_ptr ); +void SetSondehubSentence(int Channel, char *tmp);