From 1dfbec146822c656235c6507d7d8b994ab412d51 Mon Sep 17 00:00:00 2001 From: tom-r Date: Thu, 31 Mar 2022 22:25:12 +0200 Subject: [PATCH 1/7] implements ... * a web page with actual sensor data, incl. wifi signal quality, firmware version... * a debug web page, showing the serial log, change of log level * adds a influx-db option in the configuration * fixes a bug in the configuration page : on first time call, text attributes were filled with ???? --- multigeiger/display.cpp | 5 +- multigeiger/display.h | 6 + multigeiger/log.cpp | 20 ++- multigeiger/log.h | 17 +- multigeiger/multigeiger.ino | 26 ++- multigeiger/transmission.cpp | 64 ++++++- multigeiger/transmission.h | 2 +- multigeiger/userdefines-example.h | 4 + multigeiger/utils.cpp | 76 ++++++++- multigeiger/utils.h | 27 +++ multigeiger/webconf.cpp | 274 +++++++++++++++++++++++++++--- multigeiger/webconf.h | 128 +++++++++++++- 12 files changed, 596 insertions(+), 53 deletions(-) diff --git a/multigeiger/display.cpp b/multigeiger/display.cpp index b9abe1e3..708b7856 100644 --- a/multigeiger/display.cpp +++ b/multigeiger/display.cpp @@ -87,9 +87,10 @@ static const char *status_chars[STATUS_MAX] = { ".t3T?", // ST_TTN_OFF, ST_TTN_IDLE, ST_TTN_ERROR, ST_TTN_SENDING, ST_TTN_INIT // group BlueTooth ".B4b?", // ST_BLE_OFF, ST_BLE_CONNECTED, ST_BLE_ERROR, ST_BLE_CONNECTABLE, ST_BLE_INIT + // group InfluxDB + ".i5I?", // ST_INFLUX_OFF, ST_INFLUX_IDLE, ST_INFLUX_ERROR, ST_INFLUX_SENDING,ST_INFLUX_INIT // group other - ".", // ST_NODISPLAY - ".", // ST_NODISPLAY + ".", // ST_NODISPLAY ".H7", // ST_NODISPLAY, ST_HV_OK, ST_HV_ERROR }; diff --git a/multigeiger/display.h b/multigeiger/display.h index 26c67be5..86db5857 100644 --- a/multigeiger/display.h +++ b/multigeiger/display.h @@ -52,6 +52,12 @@ void display_statusline(String txt); #define ST_BLE_INIT 4 // status index 5 is still free +#define STATUS_INFLUX 5 +#define ST_INFLUX_OFF 0 +#define ST_INFLUX_IDLE 1 +#define ST_INFLUX_ERROR 2 +#define ST_INFLUX_SENDING 3 +#define ST_INFLUX_INIT 4 // status index 6 is still free diff --git a/multigeiger/log.cpp b/multigeiger/log.cpp index 3615ce4d..0a38e0f5 100644 --- a/multigeiger/log.cpp +++ b/multigeiger/log.cpp @@ -1,18 +1,19 @@ // low level log() call - outputs to serial/usb #include - +#include "string.h" #include "log.h" #include "clock.h" +#include "utils.h" // the GEIGER: prefix is is to easily differentiate our output from other esp32 output (e.g. wifi messages) #define LOG_PREFIX_FORMAT "GEIGER: %s " #define LOG_PREFIX_LEN (7+1+19+1) // chars, without the terminating \0 -static int log_level = NOLOG; // messages at level >= log_level will be output +int log_level = NOLOG; // messages at level >= log_level will be output void log(int level, const char *format, ...) { - if (level < log_level) + if (level > log_level) return; va_list args; @@ -21,12 +22,19 @@ void log(int level, const char *format, ...) { sprintf(buf, LOG_PREFIX_FORMAT, utctime()); vsprintf(buf + LOG_PREFIX_LEN, format, args); va_end(args); - Serial.println(buf); + Debug.println(buf); } void setup_log(int level) { - Serial.begin(115200); - while (!Serial) {}; +// Serial.begin(115200); //started in multigeiger.ino as Debug.begin().... + + while (!Debug) {}; log(NOLOG, "Logging initialized at level %d.", level); // this will always be output log_level = level; } +int getloglevel(void){ + return log_level; +} +void setloglevel(int level){ + log_level=level; +} diff --git a/multigeiger/log.h b/multigeiger/log.h index bbaf8e25..3b6d60c4 100644 --- a/multigeiger/log.h +++ b/multigeiger/log.h @@ -4,14 +4,17 @@ #define _LOG_H_ // log levels -#define DEBUG 0 -#define INFO 1 -#define WARNING 2 -#define ERROR 3 -#define CRITICAL 4 -#define NOLOG 999 // only to set log_level, so log() never creates output +// reverted the numbers to meet logic in log_data.h (see #define Serial_None ...) +// DEBUG is max info, reducing step by step till NOLOG +#define DEBUG 5 +#define INFO 4 +#define WARNING 3 +#define ERROR 2 +#define CRITICAL 1 +#define NOLOG 0 // only to set log_level, so log() never creates output void log(int level, const char *format, ...); void setup_log(int level); - +int getloglevel(void); +void setloglevel(int level); #endif // _LOG_H_ diff --git a/multigeiger/multigeiger.ino b/multigeiger/multigeiger.ino index 8031f205..fa22e875 100644 --- a/multigeiger/multigeiger.ino +++ b/multigeiger/multigeiger.ino @@ -18,6 +18,7 @@ #include "ble.h" #include "chkhardware.h" #include "clock.h" +#include "utils.h" // Measurement interval (default 2.5min) [sec] #define MEASUREMENT_INTERVAL 150 @@ -37,10 +38,17 @@ // DIP switches static Switches switches; - +float Count_Rate; +float Dose_Rate; +unsigned long starttime; +bool have_thp = false; +float temperature = 0.0, humidity = 0.0, pressure = 0.0; void setup() { bool isLoraBoard = init_hwtest(); + starttime = millis(); // store the start time + Debug.begin(115200); // Output to Serial at 115200 baud + while (!Debug) {}; setup_log(DEFAULT_LOG_LEVEL); setup_display(isLoraBoard); setup_switches(isLoraBoard); @@ -136,8 +144,8 @@ void publish(unsigned long current_ms, unsigned long current_counts, unsigned lo float GMC_factor_uSvph = tubes[TUBE_TYPE].cps_to_uSvph; // calculate the current count rate and dose rate - float Count_Rate = (dt != 0) ? (float)counts * 1000.0 / (float)dt : 0.0; - float Dose_Rate = Count_Rate * GMC_factor_uSvph; + Count_Rate = (dt != 0) ? (float)counts * 1000.0 / (float)dt : 0.0; + Dose_Rate = Count_Rate * GMC_factor_uSvph; // calculate the count rate and dose rate over the complete time from start accumulated_Count_Rate = (accumulated_time != 0) ? (float)accumulated_GMC_counts * 1000.0 / (float)accumulated_time : 0.0; @@ -212,6 +220,7 @@ void read_THP(unsigned long current_ms, } void transmit(unsigned long current_ms, unsigned long current_counts, unsigned long gm_count_timestamp, unsigned long current_hv_pulses, + bool have_thp, float temperature, float humidity, float pressure, int wifi_status) { static unsigned long last_counts = 0; static unsigned long last_hv_pulses = 0; @@ -233,10 +242,14 @@ void transmit(unsigned long current_ms, unsigned long current_counts, unsigned l int hv_pulses = current_hv_pulses - last_hv_pulses; last_hv_pulses = current_hv_pulses; + // calculate the current count rate and dose rate + float GMC_factor_uSvph = tubes[TUBE_TYPE].cps_to_uSvph; + float Count_Rate = (dt != 0) ? (float)counts * 1000.0 / (float)dt : 0.0; + float dose_rate = Count_Rate * GMC_factor_uSvph; log(DEBUG, "Measured GM: cpm= %d HV=%d", current_cpm, hv_pulses); - transmit_data(tubes[TUBE_TYPE].type, tubes[TUBE_TYPE].nbr, dt, hv_pulses, counts, current_cpm, + transmit_data(tubes[TUBE_TYPE].type, tubes[TUBE_TYPE].nbr, dt, hv_pulses, counts, current_cpm, dose_rate, have_thp, temperature, humidity, pressure, wifi_status); } } @@ -244,8 +257,9 @@ void transmit(unsigned long current_ms, unsigned long current_counts, unsigned l void loop() { static bool hv_error = false; // true means a HV capacitor charging issue - static bool have_thp = false; - static float temperature = 0.0, humidity = 0.0, pressure = 0.0; + //static bool have_thp = false; + have_thp = false; + temperature = 0.0; humidity = 0.0; pressure = 0.0; unsigned long current_ms = millis(); // to save multiple calls to millis() diff --git a/multigeiger/transmission.cpp b/multigeiger/transmission.cpp index fc278f6d..3a51fefa 100644 --- a/multigeiger/transmission.cpp +++ b/multigeiger/transmission.cpp @@ -15,7 +15,7 @@ #include "transmission.h" #include "ca_certs.h" - +#include "utils.h" // Hosts for data delivery // use http for now, could we use https? @@ -30,7 +30,7 @@ #define CUSTOMSRV "https://ptsv2.com/t/xxxxx-yyyyyyyyyy/post" // Get your own toilet URL and put it here before setting this to true. #define SEND2CUSTOMSRV false - +extern uint16_t influxPort; static String http_software_version; static unsigned int lora_software_version; static String chipID; @@ -41,7 +41,7 @@ typedef struct https_client { HTTPClient *hc; } HttpsClient; -static HttpsClient c_madavi, c_sensorc, c_customsrv; +static HttpsClient c_madavi, c_sensorc, c_customsrv, c_influxsrv; void setup_transmission(const char *version, char *ssid, bool loraHardware) { chipID = String(ssid); @@ -69,9 +69,14 @@ void setup_transmission(const char *version, char *ssid, bool loraHardware) { c_customsrv.wc->setCACert(ca_certs); c_customsrv.hc = new HTTPClient; + c_influxsrv.wc = new WiFiClientSecure; + c_influxsrv.wc->setCACert(ca_certs); + c_influxsrv.hc = new HTTPClient; + set_status(STATUS_SCOMM, sendToCommunity ? ST_SCOMM_INIT : ST_SCOMM_OFF); set_status(STATUS_MADAVI, sendToMadavi ? ST_MADAVI_INIT : ST_MADAVI_OFF); set_status(STATUS_TTN, sendToLora ? ST_TTN_INIT : ST_TTN_OFF); + set_status(STATUS_INFLUX, sendToInflux ? ST_INFLUX_INIT : ST_INFLUX_OFF); } void poll_transmission() { @@ -97,12 +102,11 @@ void prepare_http(HttpsClient *client, const char *host) { int send_http(HttpsClient *client, String body) { if (DEBUG_SERVER_SEND) log(DEBUG, "http request body: %s", body.c_str()); - int httpResponseCode = client->hc->POST(body); if (httpResponseCode > 0) { - String response = client->hc->getString(); if (DEBUG_SERVER_SEND) { log(DEBUG, "http code: %d", httpResponseCode); + String response = client->hc->getString(); log(DEBUG, "http response: %s", response.c_str()); } } else { @@ -243,8 +247,44 @@ int send_ttn_thp(float temperature, float humidity, float pressure) { ttnData[4] = ((int)(pressure / 10)) & 0xFF; return lorawan_send(2, ttnData, 5, false, NULL, NULL, NULL); } +//************influx-DB******************** +int send_http_influx(HttpsClient *client, String body) { + if (DEBUG_SERVER_SEND) + log(DEBUG, "http request body: %s", body.c_str()); + if(strlen(influxUser) >0 || strlen(influxPassword)>0) + client->hc->setAuthorization(influxUser,influxPassword); + int httpResponseCode = client->hc->POST(body); + if (httpResponseCode >= HTTP_CODE_OK && httpResponseCode <= HTTP_CODE_ALREADY_REPORTED) { + log(DEBUG,"Sent to influx-db, status: ok, http: %d", httpResponseCode); + } else if (httpResponseCode >= HTTP_CODE_BAD_REQUEST) { + log(DEBUG,"Sent to influx-db, status: error, http: %d", httpResponseCode); + log(DEBUG,"Details: %s", client->hc->getString().c_str()); + } + client->hc->end(); + return httpResponseCode; +} -void transmit_data(String tube_type, int tube_nbr, unsigned int dt, unsigned int hv_pulses, unsigned int gm_counts, unsigned int cpm, +int send_http_geiger_2_influx(HttpsClient *client, const char *host, unsigned int timediff, unsigned int hv_pulses, + unsigned int gm_counts, unsigned int cpm, float Dose_Rate) { + char body[1000]; + if (host[4] == 's') // https + client->hc->begin(*client->wc, host); + else // http + client->hc->begin(influxServer,influxPort,influxPath); + client->hc->addHeader("Content-Type", "application/x-www-form-urlencoded"); + client->hc->addHeader("X-Sensor", chipID); + + client->hc->addHeader("", String(influxMeasurement)); + + snprintf(body, 1000, "%s cpm=%d,hv_pulses=%d,gm_count=%d,timediff=%d,dose_rate=%f\n", influxMeasurement, + cpm, + hv_pulses, + gm_counts, + timediff,Dose_Rate); + return send_http_influx(client, body); + +} +void transmit_data(String tube_type, int tube_nbr, unsigned int dt, unsigned int hv_pulses, unsigned int gm_counts, unsigned int cpm,float Dose_Rate, int have_thp, float temperature, float humidity, float pressure, int wifi_status) { int rc1, rc2; @@ -296,5 +336,17 @@ void transmit_data(String tube_type, int tube_nbr, unsigned int dt, unsigned int set_status(STATUS_TTN, ttn_ok ? ST_TTN_IDLE : ST_TTN_ERROR); display_status(); } +//InfuxDB + if (sendToInflux && (wifi_status == ST_WIFI_CONNECTED)) { + bool influx_ok; + log(INFO, "Sending to Influx-DB ..."); + log(DEBUG,"Measured data: cpm=%d, HV=%d, DoseRate=%f,gm_count=%d,timediff=%d", cpm, hv_pulses,Dose_Rate,gm_counts,dt); + rc1 = send_http_geiger_2_influx(&c_influxsrv, influxServer, dt, hv_pulses, gm_counts, cpm, Dose_Rate); + + influx_ok = (rc1 >= HTTP_CODE_OK && rc1 <= HTTP_CODE_ALREADY_REPORTED); + log(INFO, "Sent to influx server %s, status: %s, http: %d", influxServer, influx_ok ? "ok" : "error", rc1); + set_status(STATUS_INFLUX, influx_ok ? ST_INFLUX_IDLE : ST_INFLUX_ERROR); + display_status(); + } } diff --git a/multigeiger/transmission.h b/multigeiger/transmission.h index 428d18dc..820c4b0e 100644 --- a/multigeiger/transmission.h +++ b/multigeiger/transmission.h @@ -15,7 +15,7 @@ #define XPIN_BME280 11 void setup_transmission(const char *version, char *ssid, bool lora); -void transmit_data(String tube_type, int tube_nbr, unsigned int dt, unsigned int hv_pulses, unsigned int gm_counts, unsigned int cpm, +void transmit_data(String tube_type, int tube_nbr, unsigned int dt, unsigned int hv_pulses, unsigned int gm_counts, unsigned int cpm,float Dose_Rate, int have_thp, float temperature, float humidity, float pressure, int wifi_status); // The Arduino LMIC wants to be polled from loop(). This takes care of that on LoRa boards. diff --git a/multigeiger/userdefines-example.h b/multigeiger/userdefines-example.h index def914bd..03ced6be 100644 --- a/multigeiger/userdefines-example.h +++ b/multigeiger/userdefines-example.h @@ -64,6 +64,10 @@ // 0x2A39: Heart Rate Control Point --> allows to reset "energy expenditure", as required by service definition #define SEND2BLE false +// Send data to an influx-db ? +#define SEND2INFLUX false +#define INFLUX_PORT 8086 + // Play an alarm sound when radiation level is too high? // Activates when either accumulated dose rate reaches the set threshold (see below) // or when the current dose rate is higher than the accumulated dose rate by the set factor (see below). diff --git a/multigeiger/utils.cpp b/multigeiger/utils.cpp index d44e3734..7bc0fc1a 100644 --- a/multigeiger/utils.cpp +++ b/multigeiger/utils.cpp @@ -1,7 +1,7 @@ // Misc. utilities #include - +#include #include "log.h" #include "utils.h" @@ -44,3 +44,77 @@ void reverseByteArray(unsigned char *data, int len) { } } +/***************************************************************** + * Debug output * + *****************************************************************/ + +LoggingSerial Debug; + +LoggingSerial::LoggingSerial() + : HardwareSerial(0) +{ + m_buffer = xQueueCreate(XLARGE_STR, sizeof(uint8_t)); +} + +size_t LoggingSerial::write(uint8_t c) +{ + xQueueSendToBack(m_buffer, ( void * ) &c, ( TickType_t ) 1); + return HardwareSerial::write(c); +} + +size_t LoggingSerial::write(const uint8_t *buffer, size_t size) +{ + for(int i = 0; i < size; i++) { + xQueueSendToBack(m_buffer, ( void * ) &buffer[i], ( TickType_t ) 1); + } + return HardwareSerial::write(buffer, size); +} + +String LoggingSerial::popLines() +{ + String r; + uint8_t c; + while (xQueueReceive(m_buffer, &(c ), (TickType_t) 1 )) { + r += (char) c; + + if (c == '\n' && r.length() > 10) + break; + } + return r; +} + +//taken from Luftdaten.info +String delayToString(unsigned time_ms) { + + char buf[64]; + String s; + + if (time_ms > 2 * 1000 * 60 * 60 * 24) { + sprintf_P(buf, PSTR("%d days, "), time_ms / (1000 * 60 * 60 * 24)); + s += buf; + time_ms %= 1000 * 60 * 60 * 24; + } + + if (time_ms > 2 * 1000 * 60 * 60) { + sprintf_P(buf, PSTR("%d hours, "), time_ms / (1000 * 60 * 60)); + s += buf; + time_ms %= 1000 * 60 * 60; + } + + if (time_ms > 2 * 1000 * 60) { + sprintf_P(buf, PSTR("%d min, "), time_ms / (1000 * 60)); + s += buf; + time_ms %= 1000 * 60; + } + + if (time_ms > 2 * 1000) { + sprintf_P(buf, PSTR("%ds, "), time_ms / 1000); + s += buf; + } + + if (s.length() > 2) { + s = s.substring(0, s.length() - 2); + } + + return s; +} \ No newline at end of file diff --git a/multigeiger/utils.h b/multigeiger/utils.h index 65eb7298..bce5486e 100644 --- a/multigeiger/utils.h +++ b/multigeiger/utils.h @@ -2,9 +2,36 @@ #ifndef UTILS_H #define UTILS_H +#include // Prototypes int hex2data(unsigned char *data, const char *hexstring, unsigned int len); void reverseByteArray(unsigned char *data, int len); +String delayToString(unsigned time_ms); + +constexpr unsigned SMALL_STR = 64-1; +constexpr unsigned MED_STR = 256-1; +constexpr unsigned LARGE_STR = 512-1; +constexpr unsigned XLARGE_STR = 1024-1; + +#define RESERVE_STRING(name, size) String name((const char*)nullptr); name.reserve(size) + +/***************************************************************** + * Debug output * + *****************************************************************/ + +class LoggingSerial : public HardwareSerial { + +public: + LoggingSerial(); + size_t write(uint8_t c) override; + size_t write(const uint8_t *buffer, size_t size) override; + String popLines(); + +private: + QueueHandle_t m_buffer; +}; + +extern class LoggingSerial Debug; #endif diff --git a/multigeiger/webconf.cpp b/multigeiger/webconf.cpp index 1ca60ac6..a647617e 100644 --- a/multigeiger/webconf.cpp +++ b/multigeiger/webconf.cpp @@ -1,6 +1,6 @@ // Web Configuration related code // also: OTA updates - +#include "version.h" #include "log.h" #include "speaker.h" @@ -8,6 +8,9 @@ #include "IotWebConfTParameter.h" #include #include "userdefines.h" +#include "utils.h" +#include "tube.h" +#include "webconf.h" // Checkboxes have 'selected' if checked, so we need 9 byte for this string. #define CHECKBOX_LEN 9 @@ -20,6 +23,7 @@ bool sendToCommunity = SEND2SENSORCOMMUNITY; bool sendToMadavi = SEND2MADAVI; bool sendToLora = SEND2LORA; bool sendToBle = SEND2BLE; +bool sendToInflux = SEND2INFLUX; bool soundLocalAlarm = LOCAL_ALARM_SOUND; char speakerTick_c[CHECKBOX_LEN]; @@ -29,6 +33,7 @@ char showDisplay_c[CHECKBOX_LEN]; char sendToCommunity_c[CHECKBOX_LEN]; char sendToMadavi_c[CHECKBOX_LEN]; char sendToLora_c[CHECKBOX_LEN]; +char sendToInflux_c[CHECKBOX_LEN]; char sendToBle_c[CHECKBOX_LEN]; char soundLocalAlarm_c[CHECKBOX_LEN]; @@ -40,6 +45,23 @@ static bool isLoraBoard; float localAlarmThreshold = LOCAL_ALARM_THRESHOLD; int localAlarmFactor = (int)LOCAL_ALARM_FACTOR; +uint16_t influxPort = INFLUX_PORT; + +char influxServer[100] = ""; +char influxPath[100] = ""; +char influxUser[65] = ""; +char influxPassword[IOTWEBCONF_PASSWORD_LEN+1] = ""; +char influxMeasurement[100] = ""; + +extern float Count_Rate; +extern float Dose_Rate; +extern unsigned long starttime; +extern int log_level; +extern float temperature; +extern float humidity; +extern float pressure; + + iotwebconf::ParameterGroup grpMisc = iotwebconf::ParameterGroup("misc", "Misc. Settings"); iotwebconf::CheckboxParameter startSoundParam = iotwebconf::CheckboxParameter("Start sound", "startSound", playSound_c, CHECKBOX_LEN, playSound); iotwebconf::CheckboxParameter speakerTickParam = iotwebconf::CheckboxParameter("Speaker tick", "speakerTick", speakerTick_c, CHECKBOX_LEN, speakerTick); @@ -50,12 +72,26 @@ iotwebconf::ParameterGroup grpTransmission = iotwebconf::ParameterGroup("transmi iotwebconf::CheckboxParameter sendToCommunityParam = iotwebconf::CheckboxParameter("Send to sensor.community", "send2Community", sendToCommunity_c, CHECKBOX_LEN, sendToCommunity); iotwebconf::CheckboxParameter sendToMadaviParam = iotwebconf::CheckboxParameter("Send to madavi.de", "send2Madavi", sendToMadavi_c, CHECKBOX_LEN, sendToMadavi); iotwebconf::CheckboxParameter sendToBleParam = iotwebconf::CheckboxParameter("Send to BLE (Reboot required!)", "send2ble", sendToBle_c, CHECKBOX_LEN, sendToBle); +iotwebconf::CheckboxParameter sendToInfluxParam = iotwebconf::CheckboxParameter("Send to influx-db", "send2Influx", sendToInflux_c, CHECKBOX_LEN, sendToInflux); +// influx-db parameters +iotwebconf::ParameterGroup grpInfluxDB = iotwebconf::ParameterGroup("influxdb", "Influx-DB Settings"); +iotwebconf::TextParameter influxServerParam = iotwebconf::TextParameter("Server", "influxserver", influxServer, 99,'\0',"influx-Server name"); +iotwebconf::TextParameter influxPathParam = iotwebconf::TextParameter("Path", "influxpath", influxPath, 99,'\0',"e.g. /write?db=myInfluxDB&precision=s"); +iotwebconf::IntTParameter influxPortParam = + iotwebconf::Builder>("influxPort"). + label("Port"). + defaultValue(influxPort). + min(1).max(65535). + step(1).placeholder("1..65535").build(); +iotwebconf::TextParameter influxUserParam = iotwebconf::TextParameter("User", "influxuser", influxUser, 64,'\0',"Username"); +iotwebconf::PasswordParameter influxPasswordParam = iotwebconf::PasswordParameter("Password", "influxpassword", influxPassword, IOTWEBCONF_PASSWORD_LEN,'\0',"Password"); +iotwebconf::TextParameter influxMeasurementParam = iotwebconf::TextParameter("Measurement", "influxmeasurement", influxMeasurement, 99,'\0',"Measurement"); iotwebconf::ParameterGroup grpLoRa = iotwebconf::ParameterGroup("lora", "LoRa Settings"); iotwebconf::CheckboxParameter sendToLoraParam = iotwebconf::CheckboxParameter("Send to LoRa (=>TTN)", "send2lora", sendToLora_c, CHECKBOX_LEN, sendToLora); -iotwebconf::TextParameter deveuiParam = iotwebconf::TextParameter("DEVEUI", "deveui", deveui, 17); -iotwebconf::TextParameter appeuiParam = iotwebconf::TextParameter("APPEUI", "appeui", appeui, 17); -iotwebconf::TextParameter appkeyParam = iotwebconf::TextParameter("APPKEY", "appkey", appkey, 33); +iotwebconf::TextParameter deveuiParam = iotwebconf::TextParameter("DEVEUI", "deveui", deveui, 17,'\0',"Device EUI"); +iotwebconf::TextParameter appeuiParam = iotwebconf::TextParameter("APPEUI", "appeui", appeui, 17,'\0',"Application EUI"); +iotwebconf::TextParameter appkeyParam = iotwebconf::TextParameter("APPKEY", "appkey", appkey, 33,'\0',"App Key"); iotwebconf::ParameterGroup grpAlarm = iotwebconf::ParameterGroup("alarm", "Local Alarm Setting"); iotwebconf::CheckboxParameter soundLocalAlarmParam = iotwebconf::CheckboxParameter("Enable local alarm sound", "soundLocalAlarm", soundLocalAlarm_c, CHECKBOX_LEN, soundLocalAlarm); @@ -100,8 +136,9 @@ unsigned long getESPchipID() { pid[0] = (uint8_t)pespid[5]; pid[1] = (uint8_t)pespid[4]; pid[2] = (uint8_t)pespid[3]; - log(INFO, "ID: %08X", id); - log(INFO, "MAC: %04X%08X", (uint16_t)(espid >> 32), (uint32_t)espid); + /* removed, is shown now in the web header, MAC is wrong/reverted here, by the way */ + //log(INFO, "ID: %08X", id); + //log(INFO, "MAC: %04X%08X", (uint16_t)(espid >> 32), (uint32_t)espid); return id; } @@ -111,34 +148,106 @@ char *buildSSID() { sprintf(ssid, "ESP32-%d", id); return ssid; } +int32_t calcWiFiSignalQuality(int32_t rssi) { + // Treat 0 or positive values as 0% + if (rssi >= 0 || rssi < -100) { + rssi = -100; + } + if (rssi > -50) { + rssi = -50; + } + return (rssi + 100) * 2; +} +//****************************************************************************** +// output of one line with on the startpage with "|sensor|datatype|value unit|" +//****************************************************************************** +void add_value_to_table(String& content, const __FlashStringHelper* sensor, const __FlashStringHelper* param, const String& value, const char* unit) { + RESERVE_STRING(s, MED_STR); + s = FPSTR(WEB_PAGE_DATA_LINE); + s.replace("{s}", sensor); + s.replace("{d}", param); + s.replace("{val}", value); + s.replace("{u}", String(unit)); + content += s; +} +//****************************************************************************** +// Start page +//****************************************************************************** void handleRoot(void) { // Handle web requests to "/" path. // -- Let IotWebConf test and handle captive portal requests. if (iotWebConf.handleCaptivePortal()) { // -- Captive portal requests were already served. return; } - const char *index = - "" - "" - "" - "" - "MultiGeiger Configuration" - "" - "" - "

Configuration

" - "

" - "Go to the config page to change settings or update firmware." - "

" - "" - "\n"; - server.send(200, "text/html;charset=UTF-8", index); + server.sendHeader(F("Cache-Control"), F("no-cache, no-store, must-revalidate")); + server.sendHeader(F("Pragma"), F("no-cache")); + server.sendHeader(F("Expires"), F("0")); + + // Enable Pagination (Chunked Transfer) + server.setContentLength(CONTENT_LENGTH_UNKNOWN); + server.send(200, FPSTR(CONTENT_TYPE_TXT_HTML), ""); + RESERVE_STRING (index,XLARGE_STR); + char tmp[50]; + const int last_signal_strength = WiFi.RSSI(); + index = FPSTR(WEB_PAGE_HEAD); + index.replace("{t}", FPSTR(CURRENT_DATA)); + +// Paginate page after ~ 1500 Bytes + server.sendContent(index); + index = emptyString; + index = FPSTR(WEB_PAGE_HEADLINE); + index.replace("{id}", theName); + index.replace("{mac}", WiFi.macAddress()); + index.replace("{fw}", VERSION_STR); + server.sendContent(index); + index = emptyString; + +index +=F("

Aktuelle Werte

" +"" +"" +"" +"" +"" +""); + +sprintf(tmp,"%.3f",Count_Rate); +add_value_to_table(index,F(tubes[TUBE_TYPE].type),F("cps"),tmp,"c/s"); + +sprintf(tmp,"%.3f",Dose_Rate); + +add_value_to_table(index,F(tubes[TUBE_TYPE].type),F("Dosisleistung"),tmp,"µSv/h"); + +index +=F(""); +// Paginate page after ~ 1500 Bytes + server.sendContent(index); + index = emptyString; +if(have_thp){ + sprintf(tmp,"%.1f",temperature); + add_value_to_table(index,F("BMEx80"),F("Temperatur"),tmp,"°C"); + sprintf(tmp,"%.1f",pressure); + add_value_to_table(index,F("BMEx80"),F("Luftdruck"),tmp,"hPa"); + sprintf(tmp,"%.1f",humidity); + add_value_to_table(index,F("BMEx80"),F("rel. Luftfeuchte"),tmp,"%"); + index +=F(""); +} +add_value_to_table(index,F("WiFi"),F("Signal"),String(last_signal_strength),"dBm"); +add_value_to_table(index,F("WiFi"),F("Qualität"),String(calcWiFiSignalQuality(last_signal_strength)),"%"); +add_value_to_table(index,F("ESP32"),F("Freier Speicher"),String(ESP.getFreeHeap()),"Byte"); +add_value_to_table(index,F("ESP32"),F("Uptime"),delayToString(millis() - starttime),""); + +index += F("
SensorParameterWert
 
 

"); +index += FPSTR(WEB_PAGE_START_BUTTONS); +index +=F("
\n"); + + server.sendContent(index); + //server.send(200, FPSTR(CONTENT_TYPE_TXT_HTML), index); // looks like user wants to do some configuration or maybe flash firmware. // while accessing the flash, we need to turn ticking off to avoid exceptions. // user needs to save the config (or flash firmware + reboot) to turn it on again. // note: it didn't look like there is an easy way to put this call at the right place // (start of fw flash / start of config save) - this is why it is here. - tick_enable(false); + //tick_enable(false);TR : move to setup_webconf(). Then it's only off while in configution mode, and not during 'browsing' ... } static char lastWiFiSSID[IOTWEBCONF_WORD_LEN] = ""; @@ -159,6 +268,13 @@ void loadConfigVariables(void) { sendToMadavi = sendToMadaviParam.isChecked(); sendToLora = sendToLoraParam.isChecked(); sendToBle = sendToBleParam.isChecked(); + sendToInflux = sendToInfluxParam.isChecked(); + influxPort = influxPortParam.value(); + strcpy(influxServer,influxServerParam.valueBuffer); + strcpy(influxPath,influxPathParam.valueBuffer); + strcpy(influxUser,influxUserParam.valueBuffer); + strcpy(influxPassword,influxPasswordParam.valueBuffer); + strcpy(influxMeasurement,influxMeasurementParam.valueBuffer); soundLocalAlarm = soundLocalAlarmParam.isChecked(); localAlarmThreshold = localAlarmThresholdParam.value(); localAlarmFactor = localAlarmFactorParam.value(); @@ -170,8 +286,106 @@ void configSaved(void) { tick_enable(true); } +//****************************************************************************** +// Aufruf original iotWebConf.handleConfig, aber vorher tick abschalten ... +//****************************************************************************** +void handleConfig(void){ + tick_enable(false); + iotWebConf.handleConfig(); +} + +//****************************************************************************** +// Ausgabe der seriellen Daten auf der Debugseite +//****************************************************************************** +void handleSerial(void){ + String s(Debug.popLines()); + server.send(s.length() ? 200 : 204, "text/plain", s); +} + +//****************************************************************************** +// Debug-Seite +//****************************************************************************** +void handleDebug(void){ + server.sendHeader(F("Cache-Control"), F("no-cache, no-store, must-revalidate")); + server.sendHeader(F("Pragma"), F("no-cache")); + server.sendHeader(F("Expires"), F("0")); + // Enable Pagination (Chunked Transfer) + server.setContentLength(CONTENT_LENGTH_UNKNOWN); + server.send(200, FPSTR(CONTENT_TYPE_TXT_HTML), ""); + char s[10]; + RESERVE_STRING(page_content, XLARGE_STR); + + page_content = FPSTR(WEB_PAGE_HEAD); + page_content.replace("{t}", FPSTR(DEBUG_DATA)); + //page_content.replace("{lng}", F("DE")); + + server.sendContent(page_content); + + page_content = emptyString; + page_content = FPSTR(WEB_PAGE_HEADLINE); + page_content.replace("{id}", theName); + page_content.replace("{mac}", WiFi.macAddress()); + page_content.replace("{fw}", VERSION_STR); + + server.sendContent(page_content); + page_content = emptyString; + + page_content += FPSTR(LOGLEVEL_IS); + int lvl=NOLOG; + if (server.hasArg("lvl")) { + lvl = server.arg("lvl").toInt(); + setloglevel(lvl); + }else { lvl=log_level;} + if (lvl == 5) strcpy(s,"DEBUG"); + else if (lvl == 4) strcpy(s,"INFO"); + else if (lvl == 3) strcpy(s,"WARNING"); + else if (lvl == 2) strcpy(s,"ERROR"); + else if (lvl == 1) strcpy(s,"CRITICAL"); + else strcpy(s,"NOLOG"); + + page_content.replace("{lvl}", String(s)); +// page_content += F(".
");
+  page_content += F("

");
+	page_content += Debug.popLines();
+	/*page_content += F("
");*/ + page_content += FPSTR(WEB_PAGE_DBG_SCRIPT); + page_content += F("
"); + server.sendContent(page_content); + page_content = emptyString; + + page_content += FPSTR(SET_LOGLEVEL_TO); + page_content.replace("{lvl}", F("...")); + page_content += FPSTR(WEB_PAGE_DBG_BUTTONS); + page_content += F("Zurück zur Startseite
"); + server.sendContent(page_content); +} +/***************************************************************** + * Webserver Images * + *****************************************************************/ +static void webserver_static() { + server.sendHeader(F("Cache-Control"), F("max-age=2592000, public")); + + if (server.arg(String('r')) == F("logo")) { + server.send_P(200, CONTENT_TYPE_IMAGE_PNG, + LOGO_PNG, LOGO_PNG_SIZE); + } + else if (server.arg(String('r')) == F("css")) { + server.send_P(200, CONTENT_TYPE_TEXT_CSS, + WEB_PAGE_STATIC_CSS, sizeof(WEB_PAGE_STATIC_CSS)-1); + } else { + iotWebConf.handleNotFound(); + } +} + +//****************************************************************************** +// Config settings +//****************************************************************************** void setup_webconf(bool loraHardware) { isLoraBoard = loraHardware; + iotWebConf.setConfigSavedCallback(&configSaved); // *INDENT-OFF* <- for 'astyle' to not format the following 3 lines iotWebConf.setupUpdateServer( @@ -194,6 +408,14 @@ void setup_webconf(bool loraHardware) { grpTransmission.addItem(&sendToMadaviParam); grpTransmission.addItem(&sendToBleParam); iotWebConf.addParameterGroup(&grpTransmission); + grpInfluxDB.addItem(&sendToInfluxParam); + grpInfluxDB.addItem(&influxServerParam); + grpInfluxDB.addItem(&influxPathParam); + grpInfluxDB.addItem(&influxPortParam); + grpInfluxDB.addItem(&influxUserParam); + grpInfluxDB.addItem(&influxPasswordParam); + grpInfluxDB.addItem(&influxMeasurementParam); + iotWebConf.addParameterGroup(&grpInfluxDB); if (isLoraBoard) { grpLoRa.addItem(&sendToLoraParam); grpLoRa.addItem(&deveuiParam); @@ -216,7 +438,13 @@ void setup_webconf(bool loraHardware) { // -- Set up required URL handlers on the web server. server.on("/", handleRoot); - server.on("/config", [] { iotWebConf.handleConfig(); }); + // using our own /config which simply turns of ticks first, then calls iotWebConf.handleConfig() + // server.on("/config", [] { iotWebConf.handleConfig(); }); //TR : original code + //server.on("/config", [] { handleConfig(); }); //works, but slow ??? + server.on("/config", handleConfig); + server.on("/debug", handleDebug); //debug page + server.on("/serial", handleSerial ); //needed for the serial ring buffer on the debug page + server.on(F(STATIC_PREFIX), webserver_static); // need to copy static data (logo,css) for speed reasons server.onNotFound([]() { iotWebConf.handleNotFound(); }); diff --git a/multigeiger/webconf.h b/multigeiger/webconf.h index ad68aa37..56d9ab4f 100644 --- a/multigeiger/webconf.h +++ b/multigeiger/webconf.h @@ -13,12 +13,19 @@ extern bool showDisplay; extern bool sendToCommunity; extern bool sendToMadavi; extern bool sendToLora; +extern bool sendToInflux; extern bool sendToBle; extern bool soundLocalAlarm; +extern bool have_thp; extern char appeui[]; extern char deveui[]; extern char appkey[]; +extern char influxServer[]; +extern char influxPath[]; +extern char influxUser[]; +extern char influxPassword[]; +extern char influxMeasurement[]; extern float localAlarmThreshold; extern int localAlarmFactor; @@ -28,4 +35,123 @@ extern IotWebConf iotWebConf; void setup_webconf(bool loraHardware); -#endif // _WEBCONF_H_ +#define LANGUAGE "DE" + +const char CURRENT_DATA[] PROGMEM = "Aktuelle Werte"; +const char DEBUG_DATA[] PROGMEM = "Debug Info"; +const char SET_LOGLEVEL_TO[] PROGMEM = "

Setze Loglevel auf {lvl}

"; +const char LOGLEVEL_IS[] PROGMEM = "

Loglevel ist: {lvl}

"; +const char CONTENT_TYPE_TXT_HTML[] PROGMEM = "text/html;charset=UTF-8"; +const char CONTENT_TYPE_IMAGE_PNG[] PROGMEM = "image/png"; +const char CONTENT_TYPE_TEXT_CSS[] PROGMEM = "text/css"; +#define STATIC_PREFIX "/" LANGUAGE "_s1" + +// gets preloaded +const char WEB_PAGE_STATIC_CSS[] PROGMEM ="body{font-family:Arial;margin:0}\ +.button{background-color:#1c87c9;border:none;color:white;padding:8px 16px;text-align:center;text-decoration:none;display:inline-block;font-size:16px;margin:4px 2px;cursor:pointer;border-radius:8px;box-shadow:5px 5px #999;}\ +.button:active{background-color:#3e8e41;box-shadow:3px 3px #666;transform:translateY(4px);}\ +div.debuglist{margin:10px;border:3px outset blue;background-color:GhostWhite;box-shadow:5px 5px #999;text-align:left;width:95%;height:300px;overflow-x:auto;overflow-y:auto;padding:5px;box-shadow:5px 5px #999;}\ +div.canvas{background-color:LightCyan;box-shadow:5px 5px #999;width:97%;}\ +.content{margin:10px}.r{text-align:right}.v{box-shadow:5px 5px #999}\ +.b{padding:10px}"; +const char WEB_PAGE_HEAD[] PROGMEM = "{t}\ +\ +\ +"; +/* styles inside the string. slow ... +const char WEB_PAGE_HEAD[] PROGMEM = "{t}\ +\ +"; +*/ + +/* no logo, but text eco curious text only, styles inside string +const char WEB_PAGE_HEADLINE[] PROGMEM = "
\ +\ +\ +\ +\ +
ecoMulti-Geiger
curious
ID: {id}
MAC: {mac}
Firmware: {fw}
"; +*/ + const char WEB_PAGE_DBG_BUTTONS[] PROGMEM = "\ +\ +\ +\ +\ +\ +\ +
NOLOGKRITISCHFEHLERWARNUNGINFODEBUG
"; + +const char WEB_PAGE_START_BUTTONS[] PROGMEM = "\ +
KonfigurationLogInfo Seite öffnenAktualisieren
"; + +const char WEB_PAGE_DBG_SCRIPT[] PROGMEM = "
"; + +const char WEB_PAGE_DATA_LINE[] PROGMEM = "{s}{d}{val} {u}"; + + +/*const char WEB_PAGE_HEADLINE[] PROGMEM = "
\ +

Multi-Geiger


\ +ID: {id}
MAC: {mac}
Firmware: {fw}
"; +*/ + +const char WEB_PAGE_HEADLINE[] PROGMEM = "
\ +Zurück zur Startseite

Multi-Geiger


\ +ID: {id}
MAC: {mac}
Firmware: {fw} (" __DATE__ " " __TIME__ ")
"; +constexpr const unsigned int LOGO_PNG_SIZE = 692; + +// gets preloaded +const char LOGO_PNG[] PROGMEM = { +0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, +0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0x24, 0x04, 0x03, 0x00, 0x00, 0x00, 0x8D, 0x94, 0x82, +0x0B, 0x00, 0x00, 0x00, 0x1B, 0x50, 0x4C, 0x54, 0x45, 0xD0, 0x5D, 0x17, 0xF8, 0xFC, 0xF8, 0x0A, +0xB4, 0xEC, 0x89, 0xBF, 0x2C, 0xA1, 0xDD, 0xCD, 0x61, 0xCE, 0xF4, 0x4F, 0x35, 0x8D, 0x60, 0x6F, +0x97, 0x7C, 0xA2, 0x44, 0xDB, 0xB1, 0x52, 0xEF, 0x00, 0x00, 0x00, 0x01, 0x74, 0x52, 0x4E, 0x53, +0x00, 0x40, 0xE6, 0xD8, 0x66, 0x00, 0x00, 0x02, 0x47, 0x49, 0x44, 0x41, 0x54, 0x38, 0x8D, 0x8D, +0x53, 0xC1, 0x8A, 0xDC, 0x30, 0x0C, 0x35, 0x13, 0x87, 0xED, 0x67, 0x04, 0x91, 0xB4, 0xD7, 0x80, +0x43, 0x77, 0x8F, 0x26, 0x93, 0xB9, 0x87, 0x10, 0xDF, 0x4B, 0x0F, 0xDE, 0x1E, 0xCB, 0x1E, 0x42, +0x8F, 0x0B, 0x81, 0xFD, 0xEE, 0xEA, 0x49, 0xB1, 0x93, 0x42, 0x93, 0xD6, 0xEC, 0x8E, 0x35, 0xF6, +0x7B, 0x7A, 0x4F, 0x92, 0xC7, 0x98, 0xBC, 0xAC, 0x73, 0x9D, 0x4F, 0x81, 0x9C, 0x14, 0x29, 0x38, +0x5B, 0x0E, 0x4B, 0x19, 0x1A, 0x14, 0xD8, 0xDB, 0x0B, 0x86, 0x00, 0x80, 0xB8, 0xA7, 0x40, 0x76, +0xE7, 0x2F, 0x45, 0xEE, 0x92, 0x5D, 0x91, 0xDD, 0x96, 0xE3, 0x42, 0xC6, 0x22, 0x1F, 0x7F, 0x00, +0xD9, 0x5A, 0x26, 0x43, 0xCD, 0x5B, 0xB5, 0x78, 0xE6, 0xAB, 0x13, 0xA9, 0xF6, 0x2E, 0xA0, 0xD5, +0x79, 0x11, 0x28, 0x2E, 0x9C, 0x15, 0xE2, 0xE0, 0xEE, 0x5A, 0xB5, 0xD2, 0xA9, 0xAC, 0x24, 0x39, +0xA3, 0x68, 0xD1, 0x2E, 0x51, 0xC4, 0xA9, 0xF9, 0x17, 0x65, 0x7D, 0x71, 0x2F, 0x42, 0xF1, 0x9B, +0xEC, 0xAA, 0xE7, 0xFF, 0xA3, 0xE2, 0x93, 0x8A, 0x0D, 0xCD, 0x78, 0x41, 0x29, 0xD2, 0xD5, 0x6E, +0xEC, 0x99, 0x78, 0xFD, 0xBA, 0xA0, 0xAC, 0x1D, 0x3E, 0x93, 0x13, 0xD7, 0x16, 0x2E, 0x82, 0xB2, +0xAC, 0x17, 0x2A, 0x4E, 0x25, 0x8A, 0x6D, 0x40, 0xC3, 0x57, 0xA5, 0xFC, 0x38, 0xA5, 0x60, 0xE8, +0x32, 0x41, 0x2E, 0xDB, 0x0D, 0xF8, 0xF2, 0x11, 0x69, 0x7E, 0x50, 0x8C, 0xE7, 0x94, 0xAD, 0xFE, +0xFC, 0x60, 0x8A, 0x18, 0x6B, 0x63, 0x6E, 0x4B, 0x7C, 0x3F, 0xA5, 0xE4, 0x67, 0xA9, 0x81, 0xB7, +0x31, 0x8E, 0xDC, 0x84, 0x25, 0xFE, 0x3C, 0xA5, 0x08, 0xB4, 0x4B, 0x7A, 0xAD, 0xF9, 0x14, 0x23, +0xBE, 0x5C, 0x52, 0x8C, 0xBD, 0xFB, 0x2D, 0x18, 0x38, 0x78, 0x4A, 0x94, 0x6F, 0x7F, 0x81, 0x0E, +0x41, 0xA0, 0xF6, 0xCF, 0xD3, 0xA7, 0xF8, 0x8A, 0xED, 0xE3, 0x48, 0xB1, 0x61, 0x90, 0x7D, 0xE2, +0x56, 0x56, 0xC6, 0x12, 0x49, 0x5C, 0x4D, 0x34, 0x4E, 0x34, 0x13, 0xED, 0xC6, 0x70, 0x6D, 0x6E, +0xD4, 0x98, 0x92, 0x91, 0xB5, 0x41, 0x8C, 0xE5, 0xCB, 0x8D, 0xC2, 0xF8, 0x40, 0x34, 0x10, 0x95, +0x31, 0x7A, 0xA5, 0xF0, 0xA1, 0x52, 0x90, 0x1C, 0xB1, 0x30, 0xA8, 0x66, 0x15, 0xBF, 0xA9, 0xF0, +0x9A, 0x85, 0xD2, 0x72, 0x4F, 0x44, 0x45, 0x29, 0x56, 0x91, 0x22, 0xC2, 0x69, 0xE9, 0x60, 0x8C, +0x54, 0x65, 0x89, 0xAE, 0x1D, 0xDC, 0x12, 0xF9, 0x44, 0x8D, 0x41, 0x08, 0xFF, 0x3D, 0xB2, 0x97, +0x02, 0xCA, 0x2A, 0xCD, 0xC0, 0x07, 0xE5, 0xE7, 0x88, 0xF9, 0x2C, 0xDF, 0xAB, 0xA4, 0x72, 0x03, +0xB5, 0x99, 0x19, 0x84, 0x82, 0x68, 0x3E, 0xD4, 0xC2, 0x01, 0x6B, 0x96, 0xA4, 0x94, 0xB7, 0xA4, +0x52, 0x33, 0x4B, 0x5A, 0x2B, 0xED, 0x30, 0xDE, 0x1C, 0x6A, 0xE1, 0x1C, 0x50, 0xA1, 0x2F, 0xA0, +0xBC, 0x51, 0x56, 0x41, 0xC3, 0x66, 0x0F, 0xCA, 0x28, 0xAD, 0x2E, 0xF7, 0x5A, 0xAA, 0x4D, 0x85, +0xAC, 0xEB, 0xBC, 0xB6, 0x7D, 0x2F, 0xBF, 0xD9, 0x29, 0x07, 0x95, 0x6A, 0x53, 0x21, 0x9D, 0x5A, +0x95, 0xE7, 0xD2, 0x93, 0x8C, 0xF0, 0x40, 0x49, 0xB5, 0x64, 0x15, 0x93, 0x87, 0xAB, 0x14, 0x13, +0x44, 0x46, 0x10, 0xE6, 0x81, 0x51, 0x8A, 0x4D, 0x05, 0x58, 0xE9, 0xBA, 0x97, 0x5A, 0x47, 0x51, +0xE9, 0x41, 0x31, 0x16, 0xCD, 0x99, 0x10, 0x5A, 0x69, 0xF2, 0x88, 0x3D, 0x1B, 0xF3, 0x94, 0x02, +0x69, 0x08, 0xE3, 0x6C, 0x10, 0x04, 0x46, 0x59, 0xDB, 0x5E, 0x10, 0x0D, 0x5F, 0xEE, 0xC6, 0xF0, +0x3C, 0x46, 0x54, 0x8C, 0x60, 0x78, 0x30, 0xAC, 0xE7, 0x1B, 0xD8, 0xD5, 0x57, 0xC0, 0x52, 0xBA, +0xEF, 0xC6, 0xFC, 0x8D, 0x0E, 0x0F, 0x04, 0x10, 0xDE, 0x67, 0x31, 0x35, 0x29, 0x32, 0x5D, 0x64, +0x63, 0x29, 0xD9, 0x28, 0x0F, 0x18, 0x94, 0xF4, 0xC6, 0x4C, 0xEA, 0x36, 0xF6, 0xE6, 0x30, 0x17, +0xAF, 0x49, 0x6A, 0xA3, 0x59, 0x43, 0x6A, 0x32, 0x7A, 0x52, 0x4E, 0xFA, 0x10, 0x50, 0xE9, 0x23, +0xE0, 0x0F, 0xBF, 0xA5, 0x80, 0x3E, 0x2A, 0x03, 0x7D, 0x9A, 0x6D, 0x98, 0x85, 0x3B, 0x9A, 0xDF, +0x39, 0xC4, 0xB9, 0x22, 0x26, 0xEB, 0x96, 0x83, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, +0xAE, 0x42, 0x60, 0x82 +}; +#endif // _WEBCONF_H_ \ No newline at end of file From a864988d44e7fc7b0152f3df071bfaa5e6569310 Mon Sep 17 00:00:00 2001 From: tom-r Date: Fri, 1 Apr 2022 22:58:51 +0200 Subject: [PATCH 2/7] extendable translation system implemented and languages DE and EN introduced. See platformio-example.ini, use [env:geiger] to compile in DE, [env:geger_en] for compilation with EN texts Translations not yet fully done (log messages and config page still only in EN, as before) --- multigeiger/transl.h | 14 +++++++++++ multigeiger/translations_de.h | 47 +++++++++++++++++++++++++++++++++++ multigeiger/translations_en.h | 45 +++++++++++++++++++++++++++++++++ multigeiger/webconf.cpp | 41 +++++++++++++++--------------- multigeiger/webconf.h | 35 +++++++++++--------------- platformio-example.ini | 23 +++++++++++++++++ 6 files changed, 165 insertions(+), 40 deletions(-) create mode 100644 multigeiger/transl.h create mode 100644 multigeiger/translations_de.h create mode 100644 multigeiger/translations_en.h diff --git a/multigeiger/transl.h b/multigeiger/transl.h new file mode 100644 index 00000000..d46a7d47 --- /dev/null +++ b/multigeiger/transl.h @@ -0,0 +1,14 @@ +#ifndef transl_h +#define transl_h + + +#if defined(TRANSL_DE) +#include "translations_de.h" +#elif defined(TRANSL_EN) +#include "translations_en.h" +#else +#warning No language defined +#include "translations_en.h" +#endif + +#endif diff --git a/multigeiger/translations_de.h b/multigeiger/translations_de.h new file mode 100644 index 00000000..b7a84606 --- /dev/null +++ b/multigeiger/translations_de.h @@ -0,0 +1,47 @@ +/* + add new translation strings to ALL files translations_xx.h !!! + to add a new language : + 1. create a new file translation_xx.h where xx is the language code + 2. add this file name to transl.h + 3. add a new chapter into platformio.ini (like [env:geiger_en], and adopt parameter lang = xx and the buildflag '-DTRANSL_XX') +*/ +#include + +#define LANGUAGE "DE" + +const char TRA_CURRENT_DATA[] PROGMEM = "Aktuelle Werte"; +const char TRA_DEBUG_DATA[] PROGMEM = "Debug Info"; +const char TRA_SET_LOGLEVEL_TO[] PROGMEM = "

Setze Loglevel auf {lvl}

"; +const char TRA_LOGLEVEL_IS[] PROGMEM = "

Loglevel ist: {lvl}

"; +#define TRA_ACT_VAL_HEADLINE "Aktuelle Werte" +#define TRA_BUTTON_NOLOG "NOLOG" +#define TRA_BUTTON_CRITICAL "KRITISCH" +#define TRA_BUTTON_ERROR "FEHLER" +#define TRA_BUTTON_WARNING "WARNUNG" +#define TRA_BUTTON_INFO "INFO" +#define TRA_BUTTON_DEBUG "DEBUG" +#define TRA_BUTTON_CONFIG "Konfiguration" +#define TRA_BUTTON_BACK "Zurück zur Startseite" +#define TRA_BUTTON_LOG_PAGE "LogInfo Seite" +#define TRA_BUTTON_REFRESH "Aktualisieren" +#define TRA_REFRESH_INFO "man.Aktualisieren" +#define TRA_LOG_PAGE_INFO "LogInfo Seite öffnen" +#define TRA_CONFIG_INFO "Konfiguration öffnen" +#define TRA_NOLOG_INFO "min. Info" +#define TRA_DEBUG_INFO "max. Info" + +#define TRA_SENSOR "Sensor" +#define TRA_PARAMETER "Parameter" +#define TRA_VALUE "Wert" +#define TRA_MES_RESTART "Starte neu..." +#define TRA_MES_CONF_SAVED "Konfig. gespeichert" +const char TRA_CPS[] PROGMEM="cps"; +const char TRA_DOSERATE[] PROGMEM="Dosisleistung"; +const char TRA_TEMP[] PROGMEM="Temperatur"; +const char TRA_PRESSURE[] PROGMEM = "Luftdruck"; +const char TRA_HUMIDITY[] PROGMEM = "rel. Luftfeuchte"; +const char TRA_WIFISIGNAL[] PROGMEM = "Signal"; +const char TRA_WIFIQUALITY[] PROGMEM = "Qualität"; +const char TRA_ESP_FREE_MEM[] PROGMEM = "Freier Speicher"; +const char TRA_ESP_UPTIME[] PROGMEM = "Laufzeit"; + diff --git a/multigeiger/translations_en.h b/multigeiger/translations_en.h new file mode 100644 index 00000000..555bd362 --- /dev/null +++ b/multigeiger/translations_en.h @@ -0,0 +1,45 @@ +/* + add new translation strings to ALL files translations_xx.h !!! + to add a new language : + 1. create a new file translation_xx.h where xx is the language code + 2. add this file name to transl.h + 3. add a new chapter into platformio.ini (like [env:geiger_en], and adopt parameter lang = xx and the buildflag '-DTRANSL_XX') +*/ +#include + +#define LANGUAGE "EN" + +const char TRA_CURRENT_DATA[] PROGMEM = "Actual values"; +const char TRA_DEBUG_DATA[] PROGMEM = "Debug Info"; +const char TRA_SET_LOGLEVEL_TO[] PROGMEM = "

Seting Loglevel to {lvl}

"; +const char TRA_LOGLEVEL_IS[] PROGMEM = "

Loglevel is: {lvl}

"; +#define TRA_ACT_VAL_HEADLINE "Actual Values" +#define TRA_BUTTON_NOLOG "NOLOG" +#define TRA_BUTTON_CRITICAL "CRITICAL" +#define TRA_BUTTON_ERROR "ERROR" +#define TRA_BUTTON_WARNING "WARNING" +#define TRA_BUTTON_INFO "INFO" +#define TRA_BUTTON_DEBUG "DEBUG" +#define TRA_BUTTON_CONFIG "Configuration" +#define TRA_BUTTON_BACK "Back to Homepage" +#define TRA_BUTTON_LOG_PAGE "LogInfo page" +#define TRA_BUTTON_REFRESH "Refresh" +#define TRA_REFRESH_INFO "manual Refresh" +#define TRA_LOG_PAGE_INFO "open LogInfo page" +#define TRA_CONFIG_INFO "open Configuration" +#define TRA_NOLOG_INFO "min. Info" +#define TRA_DEBUG_INFO "max. Info" +#define TRA_SENSOR "Sensor" +#define TRA_PARAMETER "Parameter" +#define TRA_VALUE "Value" +#define TRA_MES_RESTART "Restarting..." +#define TRA_MES_CONF_SAVED "Config saved." +const char TRA_CPS[] PROGMEM="cps"; +const char TRA_DOSERATE[] PROGMEM="Dose rate"; +const char TRA_TEMP[] PROGMEM="Temperature"; +const char TRA_PRESSURE[] PROGMEM = "Air pressure"; +const char TRA_HUMIDITY[] PROGMEM = "rel. Humidity"; +const char TRA_WIFISIGNAL[] PROGMEM = "Signal"; +const char TRA_WIFIQUALITY[] PROGMEM = "Quality"; +const char TRA_ESP_FREE_MEM[] PROGMEM = "Free Memory"; +const char TRA_ESP_UPTIME[] PROGMEM = "Uptime"; diff --git a/multigeiger/webconf.cpp b/multigeiger/webconf.cpp index a647617e..0a5c5508 100644 --- a/multigeiger/webconf.cpp +++ b/multigeiger/webconf.cpp @@ -3,6 +3,7 @@ #include "version.h" #include "log.h" #include "speaker.h" +#include "transl.h" #include "IotWebConf.h" #include "IotWebConfTParameter.h" @@ -191,7 +192,7 @@ void handleRoot(void) { // Handle web requests to "/" path. char tmp[50]; const int last_signal_strength = WiFi.RSSI(); index = FPSTR(WEB_PAGE_HEAD); - index.replace("{t}", FPSTR(CURRENT_DATA)); + index.replace("{t}", FPSTR(TRA_CURRENT_DATA)); // Paginate page after ~ 1500 Bytes server.sendContent(index); @@ -203,20 +204,20 @@ void handleRoot(void) { // Handle web requests to "/" path. server.sendContent(index); index = emptyString; -index +=F("

Aktuelle Werte

" +index =F("

" TRA_ACT_VAL_HEADLINE "

" "" "" "" -"" -"" -""); +"" +"" +""); sprintf(tmp,"%.3f",Count_Rate); -add_value_to_table(index,F(tubes[TUBE_TYPE].type),F("cps"),tmp,"c/s"); +add_value_to_table(index,F(tubes[TUBE_TYPE].type),FPSTR(TRA_CPS),tmp,"c/s"); sprintf(tmp,"%.3f",Dose_Rate); -add_value_to_table(index,F(tubes[TUBE_TYPE].type),F("Dosisleistung"),tmp,"µSv/h"); +add_value_to_table(index,F(tubes[TUBE_TYPE].type),FPSTR(TRA_DOSERATE),tmp,"µSv/h"); index +=F(""); // Paginate page after ~ 1500 Bytes @@ -224,17 +225,17 @@ index +=F(""); index = emptyString; if(have_thp){ sprintf(tmp,"%.1f",temperature); - add_value_to_table(index,F("BMEx80"),F("Temperatur"),tmp,"°C"); + add_value_to_table(index,F("BMEx80"),FPSTR(TRA_TEMP),tmp,"°C"); sprintf(tmp,"%.1f",pressure); - add_value_to_table(index,F("BMEx80"),F("Luftdruck"),tmp,"hPa"); + add_value_to_table(index,F("BMEx80"),FPSTR(TRA_PRESSURE),tmp,"hPa"); sprintf(tmp,"%.1f",humidity); - add_value_to_table(index,F("BMEx80"),F("rel. Luftfeuchte"),tmp,"%"); + add_value_to_table(index,F("BMEx80"),FPSTR(TRA_HUMIDITY),tmp,"%"); index +=F(""); } -add_value_to_table(index,F("WiFi"),F("Signal"),String(last_signal_strength),"dBm"); -add_value_to_table(index,F("WiFi"),F("Qualität"),String(calcWiFiSignalQuality(last_signal_strength)),"%"); -add_value_to_table(index,F("ESP32"),F("Freier Speicher"),String(ESP.getFreeHeap()),"Byte"); -add_value_to_table(index,F("ESP32"),F("Uptime"),delayToString(millis() - starttime),""); +add_value_to_table(index,F("WiFi"),FPSTR(TRA_WIFISIGNAL),String(last_signal_strength),"dBm"); +add_value_to_table(index,F("WiFi"),FPSTR(TRA_WIFIQUALITY),String(calcWiFiSignalQuality(last_signal_strength)),"%"); +add_value_to_table(index,F("ESP32"),FPSTR(TRA_ESP_FREE_MEM),String(ESP.getFreeHeap()),"Byte"); +add_value_to_table(index,F("ESP32"),FPSTR(TRA_ESP_UPTIME),delayToString(millis() - starttime),""); index += F("
SensorParameterWert
" TRA_SENSOR "" TRA_PARAMETER "" TRA_VALUE "
 
 
 

"); index += FPSTR(WEB_PAGE_START_BUTTONS); @@ -255,7 +256,7 @@ static char lastWiFiSSID[IOTWEBCONF_WORD_LEN] = ""; void loadConfigVariables(void) { // check if WiFi SSID has changed. If so, restart cpu. Otherwise, the program will not use the new SSID if ((strcmp(lastWiFiSSID, "") != 0) && (strcmp(lastWiFiSSID, iotWebConf.getWifiSsidParameter()->valueBuffer) != 0)) { - log(INFO, "Doing restart..."); + log(INFO, TRA_MES_RESTART); ESP.restart(); } strcpy(lastWiFiSSID, iotWebConf.getWifiSsidParameter()->valueBuffer); @@ -281,7 +282,7 @@ void loadConfigVariables(void) { } void configSaved(void) { - log(INFO, "Config saved. "); + log(INFO, TRA_MES_CONF_SAVED); loadConfigVariables(); tick_enable(true); } @@ -316,7 +317,7 @@ void handleDebug(void){ RESERVE_STRING(page_content, XLARGE_STR); page_content = FPSTR(WEB_PAGE_HEAD); - page_content.replace("{t}", FPSTR(DEBUG_DATA)); + page_content.replace("{t}", FPSTR(TRA_DEBUG_DATA)); //page_content.replace("{lng}", F("DE")); server.sendContent(page_content); @@ -330,7 +331,7 @@ void handleDebug(void){ server.sendContent(page_content); page_content = emptyString; - page_content += FPSTR(LOGLEVEL_IS); + page_content += FPSTR(TRA_LOGLEVEL_IS); int lvl=NOLOG; if (server.hasArg("lvl")) { lvl = server.arg("lvl").toInt(); @@ -356,10 +357,10 @@ void handleDebug(void){ server.sendContent(page_content); page_content = emptyString; - page_content += FPSTR(SET_LOGLEVEL_TO); + page_content += FPSTR(TRA_SET_LOGLEVEL_TO); page_content.replace("{lvl}", F("...")); page_content += FPSTR(WEB_PAGE_DBG_BUTTONS); - page_content += F("Zurück zur Startseite
"); + page_content += F("" TRA_BUTTON_BACK "
"); server.sendContent(page_content); } /***************************************************************** diff --git a/multigeiger/webconf.h b/multigeiger/webconf.h index 56d9ab4f..bfb6be02 100644 --- a/multigeiger/webconf.h +++ b/multigeiger/webconf.h @@ -5,6 +5,7 @@ #define _WEBCONF_H_ #include "IotWebConf.h" +#include "transl.h" extern bool speakerTick; extern bool playSound; @@ -35,12 +36,12 @@ extern IotWebConf iotWebConf; void setup_webconf(bool loraHardware); -#define LANGUAGE "DE" +//#define LANGUAGE "DE" -const char CURRENT_DATA[] PROGMEM = "Aktuelle Werte"; -const char DEBUG_DATA[] PROGMEM = "Debug Info"; -const char SET_LOGLEVEL_TO[] PROGMEM = "

Setze Loglevel auf {lvl}

"; -const char LOGLEVEL_IS[] PROGMEM = "

Loglevel ist: {lvl}

"; +//const char CURRENT_DATA[] PROGMEM = "Aktuelle Werte"; +//const char DEBUG_DATA[] PROGMEM = "Debug Info"; +//const char SET_LOGLEVEL_TO[] PROGMEM = "

Setze Loglevel auf {lvl}

"; +//const char LOGLEVEL_IS[] PROGMEM = "

Loglevel ist: {lvl}

"; const char CONTENT_TYPE_TXT_HTML[] PROGMEM = "text/html;charset=UTF-8"; const char CONTENT_TYPE_IMAGE_PNG[] PROGMEM = "image/png"; const char CONTENT_TYPE_TEXT_CSS[] PROGMEM = "text/css"; @@ -79,16 +80,16 @@ const char WEB_PAGE_HEADLINE[] PROGMEM = "
\ ID: {id}
MAC: {mac}
Firmware: {fw}
"; */ const char WEB_PAGE_DBG_BUTTONS[] PROGMEM = "\ -\ -\ -\ -\ -\ -\ +\ +\ +\ +\ +\ +\
NOLOGKRITISCHFEHLERWARNUNGINFODEBUG" TRA_BUTTON_NOLOG "" TRA_BUTTON_CRITICAL "" TRA_BUTTON_ERROR "" TRA_BUTTON_WARNING "" TRA_BUTTON_INFO "" TRA_BUTTON_DEBUG "
"; -const char WEB_PAGE_START_BUTTONS[] PROGMEM = "\ -
KonfigurationLogInfo Seite öffnenAktualisieren
"; +const char WEB_PAGE_START_BUTTONS[] PROGMEM = "\ +
" TRA_BUTTON_CONFIG "" TRA_BUTTON_LOG_PAGE "" TRA_BUTTON_REFRESH "
"; const char WEB_PAGE_DBG_SCRIPT[] PROGMEM = ""; @@ -96,14 +97,8 @@ document.getElementById('slog').innerText+=r;}).catch(err=>console.log(err));};s const char WEB_PAGE_DATA_LINE[] PROGMEM = "{s}{d}{val} {u}"; -/*const char WEB_PAGE_HEADLINE[] PROGMEM = "
\ -

Multi-Geiger


\ -ID: {id}
MAC: {mac}
Firmware: {fw}
"; -*/ - const char WEB_PAGE_HEADLINE[] PROGMEM = "
\ -Zurück zur Startseite

Multi-Geiger


\ +" TRA_BUTTON_BACK "

Multi-Geiger


\ ID: {id}
MAC: {mac}
Firmware: {fw} (" __DATE__ " " __TIME__ ")
"; constexpr const unsigned int LOGO_PNG_SIZE = 692; diff --git a/platformio-example.ini b/platformio-example.ini index 765db244..ac1cff94 100644 --- a/platformio-example.ini +++ b/platformio-example.ini @@ -15,11 +15,13 @@ src_dir = multigeiger [env:geiger] +lang = DE board = heltec_wireless_stick build_flags = -D CFG_eu868=1 -D CFG_sx1276_radio=1 -D ARDUINO_LMIC_PROJECT_CONFIG_H_SUPPRESS=1 + '-DTRANSL_DE' platform = espressif32 framework = arduino monitor_speed=115200 @@ -31,3 +33,24 @@ lib_deps= IotWebConf@^3.1.0 MCCI LoRaWAN LMIC library h2zero/NimBLE-Arduino + +[env:geiger_en] +lang = EN +board = heltec_wireless_stick +build_flags = + -D CFG_eu868=1 + -D CFG_sx1276_radio=1 + -D ARDUINO_LMIC_PROJECT_CONFIG_H_SUPPRESS=1 + '-DTRANSL_EN' +platform = espressif32 +framework = arduino +monitor_speed=115200 +lib_deps= + U8g2 + Adafruit BME680 Library@^2.0.0 + Adafruit BME280 Library + Adafruit Unified Sensor + IotWebConf@^3.1.0 + MCCI LoRaWAN LMIC library + h2zero/NimBLE-Arduino + EspSoftwareSerial \ No newline at end of file From 6b51ff1ab50d796636b90d4da65eab389917ec19 Mon Sep 17 00:00:00 2001 From: tom-r Date: Thu, 7 Apr 2022 18:20:42 +0200 Subject: [PATCH 3/7] translations completed various smaller fixes --- multigeiger/log.cpp | 3 +- multigeiger/log_data.cpp | 15 ++-- multigeiger/log_data.h | 2 +- multigeiger/multigeiger.ino | 7 +- multigeiger/transl.h | 10 ++- multigeiger/translations_de.h | 11 ++- multigeiger/translations_en.h | 12 ++- multigeiger/transmission.cpp | 136 ++++++++++++++++++++++++---------- multigeiger/utils.cpp | 23 +++--- multigeiger/utils.h | 1 + multigeiger/webconf.cpp | 17 +++-- multigeiger/webconf.h | 2 +- platformio-example.ini | 62 ++++++++-------- 13 files changed, 194 insertions(+), 107 deletions(-) diff --git a/multigeiger/log.cpp b/multigeiger/log.cpp index 0a38e0f5..e3f2b973 100644 --- a/multigeiger/log.cpp +++ b/multigeiger/log.cpp @@ -26,8 +26,7 @@ void log(int level, const char *format, ...) { } void setup_log(int level) { -// Serial.begin(115200); //started in multigeiger.ino as Debug.begin().... - + Debug.begin(115200); // Output to Serial at 115200 baud while (!Debug) {}; log(NOLOG, "Logging initialized at level %d.", level); // this will always be output log_level = level; diff --git a/multigeiger/log_data.cpp b/multigeiger/log_data.cpp index c7cb19f1..ad22dc74 100644 --- a/multigeiger/log_data.cpp +++ b/multigeiger/log_data.cpp @@ -8,7 +8,6 @@ int Serial_Print_Mode; static const char *Serial_Logging_Name = "Simple Multi-Geiger"; static const char *dashes = "------------------------------------------------------------------------------------------------------------------------"; - static const char *Serial_Logging_Header = " %10s %15s %10s %9s %9s %8s %9s %9s %9s %5s %5s %6s"; static const char *Serial_Logging_Body = "DATA %10d %15d %10f %9f %9d %8d %9d %9f %9f %5.1f %5.1f %6.0f"; static const char *Serial_One_Minute_Log_Header = " %4s %10s %29s"; @@ -31,11 +30,7 @@ void log_data(int GMC_counts, int time_difference, float Count_Rate, float Dose_ float t, float h, float p) { static int counter = 0; if (counter++ % 20 == 0) { // output the header now and then, so table is better readable - log(INFO, Serial_Logging_Header, - "GMC_counts", "Time_difference", "Count_Rate", "Dose_Rate", "HV Pulses", "Accu_GMC", "Accu_Time", "Accu_Rate", "Accu_Dose", "Temp", "Humi", "Press"); - log(INFO, Serial_Logging_Header, - "[Counts]", "[ms]", "[cps]", "[uSv/h]", "[-]", "[Counts]", "[ms]", "[cps]", "[uSv/h]", "[C]", "[%]", "[hPa]"); - log(INFO, dashes); + write_log_header(); } log(INFO, Serial_Logging_Body, GMC_counts, time_difference, Count_Rate, Dose_Rate, HV_pulse_count, @@ -65,3 +60,11 @@ void log_data_statistics(int count_time_between) { } log(INFO, "%d", count_time_between); } + +void write_log_header(){ + log(INFO, Serial_Logging_Header, + "GMC_counts", "Time_difference", "Count_Rate", "Dose_Rate", "HV Pulses", "Accu_GMC", "Accu_Time", "Accu_Rate", "Accu_Dose", "Temp", "Humi", "Press"); + log(INFO, Serial_Logging_Header, + "[Counts]", "[ms]", "[cps]", "[uSv/h]", "[-]", "[Counts]", "[ms]", "[cps]", "[uSv/h]", "[C]", "[%]", "[hPa]"); + log(INFO, dashes); +} \ No newline at end of file diff --git a/multigeiger/log_data.h b/multigeiger/log_data.h index 46ad12d8..ff28efd7 100644 --- a/multigeiger/log_data.h +++ b/multigeiger/log_data.h @@ -18,5 +18,5 @@ void log_data(int GMC_counts, int time_difference, float Count_Rate, float Dose_ float t, float h, float p); void log_data_one_minute(int time_s, int cpm, int counts); void log_data_statistics(int count_time_between); - +void write_log_header(void); #endif // _LOG_DATA_H_ diff --git a/multigeiger/multigeiger.ino b/multigeiger/multigeiger.ino index fa22e875..2e4ad0d9 100644 --- a/multigeiger/multigeiger.ino +++ b/multigeiger/multigeiger.ino @@ -40,6 +40,7 @@ static Switches switches; float Count_Rate; float Dose_Rate; +int hv_pulses; unsigned long starttime; bool have_thp = false; float temperature = 0.0, humidity = 0.0, pressure = 0.0; @@ -47,8 +48,8 @@ float temperature = 0.0, humidity = 0.0, pressure = 0.0; void setup() { bool isLoraBoard = init_hwtest(); starttime = millis(); // store the start time - Debug.begin(115200); // Output to Serial at 115200 baud - while (!Debug) {}; + //Debug.begin(115200); // Output to Serial at 115200 baud + //while (!Debug) {}; setup_log(DEFAULT_LOG_LEVEL); setup_display(isLoraBoard); setup_switches(isLoraBoard); @@ -131,7 +132,7 @@ void publish(unsigned long current_ms, unsigned long current_counts, unsigned lo return; } last_timestamp = current_ms; - int hv_pulses = current_hv_pulses - last_hv_pulses; + hv_pulses = current_hv_pulses - last_hv_pulses; last_hv_pulses = current_hv_pulses; int counts = current_counts - last_counts; last_counts = current_counts; diff --git a/multigeiger/transl.h b/multigeiger/transl.h index d46a7d47..dd56d283 100644 --- a/multigeiger/transl.h +++ b/multigeiger/transl.h @@ -1,6 +1,14 @@ #ifndef transl_h #define transl_h - +/* TR, 07.04.2022 + add new translation strings to ALL files translations_xx.h !!! + do NOT add translation_xx.h to any of your routines, but transl.h + Steps to add a new language : + 1. create a new file translation_xx.h where xx is the language code + 2. add this file name to transl.h + 3. add a new chapter into platformio.ini (like [env:geiger_en] (copy&paste), and only adopt the buildflag '-DTRANSL_XX' to the new language + this defines TRANSL_XX which can be queried in transl.h during runtime in its if-elif and includes the corresponding translation_xx.h. +*/ #if defined(TRANSL_DE) #include "translations_de.h" diff --git a/multigeiger/translations_de.h b/multigeiger/translations_de.h index b7a84606..3cff1209 100644 --- a/multigeiger/translations_de.h +++ b/multigeiger/translations_de.h @@ -1,9 +1,11 @@ -/* +/* TR, 07.04.2022 add new translation strings to ALL files translations_xx.h !!! - to add a new language : + do NOT add translation_xx.h to any of your routines, but transl.h + Steps to add a new language : 1. create a new file translation_xx.h where xx is the language code 2. add this file name to transl.h - 3. add a new chapter into platformio.ini (like [env:geiger_en], and adopt parameter lang = xx and the buildflag '-DTRANSL_XX') + 3. add a new chapter into platformio.ini (like [env:geiger_en] (copy&paste), and only adopt the buildflag '-DTRANSL_XX' to the new language + this defines TRANSL_XX which can be queried in transl.h during runtime in its if-elif and includes the corresponding translation_xx.h. */ #include @@ -29,6 +31,8 @@ const char TRA_LOGLEVEL_IS[] PROGMEM = "

Loglevel ist: {lvl}

"; #define TRA_CONFIG_INFO "Konfiguration öffnen" #define TRA_NOLOG_INFO "min. Info" #define TRA_DEBUG_INFO "max. Info" +const char TRA_SEND_TO_INFO [] PROGMEM = "Sende an {ext} ..."; +const char TRA_SENT_TO_INFO [] PROGMEM = "An {ext} gesandt, Status: {s}, http: "; #define TRA_SENSOR "Sensor" #define TRA_PARAMETER "Parameter" @@ -37,6 +41,7 @@ const char TRA_LOGLEVEL_IS[] PROGMEM = "

Loglevel ist: {lvl}

"; #define TRA_MES_CONF_SAVED "Konfig. gespeichert" const char TRA_CPS[] PROGMEM="cps"; const char TRA_DOSERATE[] PROGMEM="Dosisleistung"; +const char TRA_HV_PULSES[] PROGMEM="HochVolt Pulse"; const char TRA_TEMP[] PROGMEM="Temperatur"; const char TRA_PRESSURE[] PROGMEM = "Luftdruck"; const char TRA_HUMIDITY[] PROGMEM = "rel. Luftfeuchte"; diff --git a/multigeiger/translations_en.h b/multigeiger/translations_en.h index 555bd362..a668cdd8 100644 --- a/multigeiger/translations_en.h +++ b/multigeiger/translations_en.h @@ -1,9 +1,11 @@ -/* +/* TR, 07.04.2022 add new translation strings to ALL files translations_xx.h !!! - to add a new language : + do NOT add translation_xx.h to any of your routines, but transl.h + Steps to add a new language : 1. create a new file translation_xx.h where xx is the language code 2. add this file name to transl.h - 3. add a new chapter into platformio.ini (like [env:geiger_en], and adopt parameter lang = xx and the buildflag '-DTRANSL_XX') + 3. add a new chapter into platformio.ini (like [env:geiger_en] (copy&paste), and only adopt the buildflag '-DTRANSL_XX' to the new language + this defines TRANSL_XX which can be queried in transl.h during runtime in its if-elif and includes the corresponding translation_xx.h. */ #include @@ -29,6 +31,9 @@ const char TRA_LOGLEVEL_IS[] PROGMEM = "

Loglevel is: {lvl}

"; #define TRA_CONFIG_INFO "open Configuration" #define TRA_NOLOG_INFO "min. Info" #define TRA_DEBUG_INFO "max. Info" +const char TRA_SEND_TO_INFO [] PROGMEM = "Sending to {ext} ..."; +const char TRA_SENT_TO_INFO [] PROGMEM = "Sent to {ext}, status: {s}, http: "; + #define TRA_SENSOR "Sensor" #define TRA_PARAMETER "Parameter" #define TRA_VALUE "Value" @@ -36,6 +41,7 @@ const char TRA_LOGLEVEL_IS[] PROGMEM = "

Loglevel is: {lvl}

"; #define TRA_MES_CONF_SAVED "Config saved." const char TRA_CPS[] PROGMEM="cps"; const char TRA_DOSERATE[] PROGMEM="Dose rate"; +const char TRA_HV_PULSES[] PROGMEM="High Voltage pulses"; const char TRA_TEMP[] PROGMEM="Temperature"; const char TRA_PRESSURE[] PROGMEM = "Air pressure"; const char TRA_HUMIDITY[] PROGMEM = "rel. Humidity"; diff --git a/multigeiger/transmission.cpp b/multigeiger/transmission.cpp index 3a51fefa..12be77e8 100644 --- a/multigeiger/transmission.cpp +++ b/multigeiger/transmission.cpp @@ -118,7 +118,7 @@ int send_http(HttpsClient *client, String body) { int send_http_geiger(HttpsClient *client, const char *host, unsigned int timediff, unsigned int hv_pulses, unsigned int gm_counts, unsigned int cpm, int xpin) { - char body[1000]; + char body[500]; prepare_http(client, host); if (xpin != XPIN_NO_XPIN) { client->hc->addHeader("X-PIN", String(xpin)); @@ -134,7 +134,7 @@ int send_http_geiger(HttpsClient *client, const char *host, unsigned int timedif ] } )====="; - snprintf(body, 1000, json_format, + snprintf(body, 500, json_format, http_software_version.c_str(), cpm, hv_pulses, @@ -144,7 +144,7 @@ int send_http_geiger(HttpsClient *client, const char *host, unsigned int timedif } int send_http_thp(HttpsClient *client, const char *host, float temperature, float humidity, float pressure, int xpin) { - char body[1000]; + char body[300]; prepare_http(client, host); if(xpin != XPIN_NO_XPIN) { client->hc->addHeader("X-PIN", String(xpin)); @@ -159,7 +159,7 @@ int send_http_thp(HttpsClient *client, const char *host, float temperature, floa ] } )====="; - snprintf(body, 1000, json_format, + snprintf(body, 300, json_format, http_software_version.c_str(), temperature, humidity, @@ -170,7 +170,7 @@ int send_http_thp(HttpsClient *client, const char *host, float temperature, floa // two extra functions for MADAVI, because MADAVI needs the sensorname in value_type to recognize the sensors int send_http_geiger_2_madavi(HttpsClient *client, String tube_type, unsigned int timediff, unsigned int hv_pulses, unsigned int gm_counts, unsigned int cpm) { - char body[1000]; + char body[500]; prepare_http(client, MADAVI); tube_type = tube_type.substring(10); const char *json_format = R"=====( @@ -184,7 +184,7 @@ int send_http_geiger_2_madavi(HttpsClient *client, String tube_type, unsigned in ] } )====="; - snprintf(body, 1000, json_format, + snprintf(body, 500, json_format, http_software_version.c_str(), tube_type.c_str(), cpm, tube_type.c_str(), hv_pulses, @@ -194,7 +194,7 @@ int send_http_geiger_2_madavi(HttpsClient *client, String tube_type, unsigned in } int send_http_thp_2_madavi(HttpsClient *client, float temperature, float humidity, float pressure) { - char body[1000]; + char body[500]; prepare_http(client, MADAVI); const char *json_format = R"=====( { @@ -206,7 +206,7 @@ int send_http_thp_2_madavi(HttpsClient *client, float temperature, float humidit ] } )====="; - snprintf(body, 1000, json_format, + snprintf(body, 500, json_format, http_software_version.c_str(), temperature, humidity, @@ -265,8 +265,8 @@ int send_http_influx(HttpsClient *client, String body) { } int send_http_geiger_2_influx(HttpsClient *client, const char *host, unsigned int timediff, unsigned int hv_pulses, - unsigned int gm_counts, unsigned int cpm, float Dose_Rate) { - char body[1000]; + unsigned int gm_counts, unsigned int cpm, float Dose_Rate, int have_thp, float temperature, float humidity, float pressure) { + char body[300]; if (host[4] == 's') // https client->hc->begin(*client->wc, host); else // http @@ -275,77 +275,135 @@ int send_http_geiger_2_influx(HttpsClient *client, const char *host, unsigned in client->hc->addHeader("X-Sensor", chipID); client->hc->addHeader("", String(influxMeasurement)); - - snprintf(body, 1000, "%s cpm=%d,hv_pulses=%d,gm_count=%d,timediff=%d,dose_rate=%f\n", influxMeasurement, + if(!have_thp){ + snprintf(body, 300, "%s cpm=%d,hv_pulses=%d,gm_count=%d,timediff=%d,dose_rate=%f\n", influxMeasurement, cpm, hv_pulses, gm_counts, timediff,Dose_Rate); + } + else{ + snprintf(body, 300, "%s cpm=%d,hv_pulses=%d,gm_count=%d,timediff=%d,dose_rate=%f,temperature=%f,humidity=%f,pressure=%f\n", influxMeasurement, + cpm, + hv_pulses, + gm_counts, + timediff,Dose_Rate,temperature,humidity,pressure); + + } + log(DEBUG,"Influx-Body: %s",body); return send_http_influx(client, body); } void transmit_data(String tube_type, int tube_nbr, unsigned int dt, unsigned int hv_pulses, unsigned int gm_counts, unsigned int cpm,float Dose_Rate, int have_thp, float temperature, float humidity, float pressure, int wifi_status) { int rc1, rc2; + RESERVE_STRING (logstr,SMALL_STR); + bool transfer_ok; #if SEND2CUSTOMSRV - bool customsrv_ok; - log(INFO, "Sending to CUSTOMSRV ..."); - rc1 = send_http_geiger(&c_customsrv, CUSTOMSRV, dt, hv_pulses, gm_counts, cpm, XPIN_NO_XPIN); - rc2 = have_thp ? send_http_thp(&c_customsrv, CUSTOMSRV, temperature, humidity, pressure, XPIN_NO_XPIN) : 200; - customsrv_ok = (rc1 == 200) && (rc2 == 200); - log(INFO, "Sent to CUSTOMSRV, status: %s, http: %d %d", customsrv_ok ? "ok" : "error", rc1, rc2); + //bool customsrv_ok; + //log(INFO, "Sending to CUSTOMSRV ..."); + logstr=FPSTR(TRA_SEND_TO_INFO); + logstr.replace("{ext}", F("CUSTOMSRV")); + log(INFO, logstr.c_str()); + + rc1 = send_http_geiger(&c_customsrv, CUSTOMSRV, dt, hv_pulses, gm_counts, cpm, XPIN_NO_XPIN); + rc2 = have_thp ? send_http_thp(&c_customsrv, CUSTOMSRV, temperature, humidity, pressure, XPIN_NO_XPIN) : 200; + transfer_ok = (rc1 == 200) && (rc2 == 200):true:false; + //log(INFO, "Sent to CUSTOMSRV, status: %s, http: %d %d", customsrv_ok ? "ok" : "error", rc1, rc2); + logstr=FPSTR(TRA_SENT_TO_INFO); + logstr.replace("{ext}", F("CUSTOMSRV")); + logstr.replace("{s}", transfer_ok ? "ok" : "error"); + logstr += rc1; + logstr += F(", "); + logstr += rc2; + log(INFO, logstr.c_str()); #endif if(sendToMadavi && (wifi_status == ST_WIFI_CONNECTED)) { - bool madavi_ok; - log(INFO, "Sending to Madavi ..."); + //bool madavi_ok; + //log(INFO, "Sending to Madavi ..."); + logstr=FPSTR(TRA_SEND_TO_INFO); + logstr.replace("{ext}", F("Madavi")); + log(INFO, logstr.c_str()); + set_status(STATUS_MADAVI, ST_MADAVI_SENDING); display_status(); rc1 = send_http_geiger_2_madavi(&c_madavi, tube_type, dt, hv_pulses, gm_counts, cpm); rc2 = have_thp ? send_http_thp_2_madavi(&c_madavi, temperature, humidity, pressure) : 200; delay(300); - madavi_ok = (rc1 == 200) && (rc2 == 200); - log(INFO, "Sent to Madavi, status: %s, http: %d %d", madavi_ok ? "ok" : "error", rc1, rc2); - set_status(STATUS_MADAVI, madavi_ok ? ST_MADAVI_IDLE : ST_MADAVI_ERROR); + transfer_ok = (rc1 == 200) && (rc2 == 200)?true:false; + //log(INFO, "Sent to Madavi, status: %s, http: %d %d", madavi_ok ? "ok" : "error", rc1, rc2); + logstr=FPSTR(TRA_SENT_TO_INFO); + logstr.replace("{ext}", F("Madavi")); + logstr.replace("{s}", transfer_ok ? "ok" : "error"); + logstr += rc1; + logstr += F(", "); + logstr += rc2; + log(INFO, logstr.c_str()); + set_status(STATUS_MADAVI, transfer_ok ? ST_MADAVI_IDLE : ST_MADAVI_ERROR); display_status(); } if(sendToCommunity && (wifi_status == ST_WIFI_CONNECTED)) { - bool scomm_ok; - log(INFO, "Sending to sensor.community ..."); + //bool scomm_ok; + //log(INFO, "Sending to sensor.community ..."); + logstr=FPSTR(TRA_SEND_TO_INFO); + logstr.replace("{ext}", F("sensor.community")); + log(INFO, logstr.c_str()); set_status(STATUS_SCOMM, ST_SCOMM_SENDING); display_status(); rc1 = send_http_geiger(&c_sensorc, SENSORCOMMUNITY, dt, hv_pulses, gm_counts, cpm, XPIN_RADIATION); rc2 = have_thp ? send_http_thp(&c_sensorc, SENSORCOMMUNITY, temperature, humidity, pressure, XPIN_BME280) : 201; delay(300); - scomm_ok = (rc1 == 201) && (rc2 == 201); - log(INFO, "Sent to sensor.community, status: %s, http: %d %d", scomm_ok ? "ok" : "error", rc1, rc2); - set_status(STATUS_SCOMM, scomm_ok ? ST_SCOMM_IDLE : ST_SCOMM_ERROR); + transfer_ok = (rc1 == 201) && (rc2 == 201)?true:false; + //log(INFO, "Sent to sensor.community, status: %s, http: %d %d", scomm_ok ? "ok" : "error", rc1, rc2); + logstr=FPSTR(TRA_SENT_TO_INFO); + logstr.replace("{ext}", F("sensor.community")); + logstr.replace("{s}", transfer_ok ? "ok" : "error"); + logstr += rc1; + logstr += F(", "); + logstr += rc2; + log(INFO, logstr.c_str()); + set_status(STATUS_SCOMM, transfer_ok ? ST_SCOMM_IDLE : ST_SCOMM_ERROR); display_status(); } if(isLoraBoard && sendToLora && (strcmp(appeui, "") != 0)) { // send only, if we have LoRa credentials - bool ttn_ok; - log(INFO, "Sending to TTN ..."); + //bool ttn_ok; + //log(INFO, "Sending to TTN ..."); + logstr=FPSTR(TRA_SEND_TO_INFO); + logstr.replace("{ext}", "TTN"); + log(INFO, logstr.c_str()); set_status(STATUS_TTN, ST_TTN_SENDING); display_status(); rc1 = send_ttn_geiger(tube_nbr, dt, gm_counts); rc2 = have_thp ? send_ttn_thp(temperature, humidity, pressure) : TX_STATUS_UPLINK_SUCCESS; - ttn_ok = (rc1 == TX_STATUS_UPLINK_SUCCESS) && (rc2 == TX_STATUS_UPLINK_SUCCESS); - set_status(STATUS_TTN, ttn_ok ? ST_TTN_IDLE : ST_TTN_ERROR); + transfer_ok = (rc1 == TX_STATUS_UPLINK_SUCCESS) && (rc2 == TX_STATUS_UPLINK_SUCCESS)?true:false; + set_status(STATUS_TTN, transfer_ok ? ST_TTN_IDLE : ST_TTN_ERROR); display_status(); } //InfuxDB if (sendToInflux && (wifi_status == ST_WIFI_CONNECTED)) { - bool influx_ok; - log(INFO, "Sending to Influx-DB ..."); - log(DEBUG,"Measured data: cpm=%d, HV=%d, DoseRate=%f,gm_count=%d,timediff=%d", cpm, hv_pulses,Dose_Rate,gm_counts,dt); - rc1 = send_http_geiger_2_influx(&c_influxsrv, influxServer, dt, hv_pulses, gm_counts, cpm, Dose_Rate); + //bool influx_ok; + //log(INFO, "Sending to Influx-DB ..."); + logstr=FPSTR(TRA_SEND_TO_INFO); + logstr.replace("{ext}", F("Influx-DB")); + log(INFO, logstr.c_str()); + set_status(STATUS_INFLUX, ST_INFLUX_SENDING); + display_status(); - influx_ok = (rc1 >= HTTP_CODE_OK && rc1 <= HTTP_CODE_ALREADY_REPORTED); - log(INFO, "Sent to influx server %s, status: %s, http: %d", influxServer, influx_ok ? "ok" : "error", rc1); - set_status(STATUS_INFLUX, influx_ok ? ST_INFLUX_IDLE : ST_INFLUX_ERROR); + log(DEBUG,"Measured data: cpm=%d, HV=%d, DoseRate=%f,gm_count=%d,timediff=%d", cpm, hv_pulses,Dose_Rate,gm_counts,dt); + rc1 = send_http_geiger_2_influx(&c_influxsrv, influxServer, dt, hv_pulses, gm_counts, cpm, Dose_Rate,have_thp,temperature, humidity, pressure); + + transfer_ok = (rc1 >= HTTP_CODE_OK && rc1 <= HTTP_CODE_ALREADY_REPORTED)?true:false; + //log(INFO, "Sent to influx server %s, status: %s, http: %d", influxServer, influx_ok ? "ok" : "error", rc1); + logstr=FPSTR(TRA_SENT_TO_INFO); + logstr.replace("{ext}", F("Influx-DB")); + logstr.replace("{s}", transfer_ok ? "ok" : "error"); + logstr += rc1; + log(INFO, logstr.c_str()); + set_status(STATUS_INFLUX, transfer_ok ? ST_INFLUX_IDLE : ST_INFLUX_ERROR); display_status(); } } diff --git a/multigeiger/utils.cpp b/multigeiger/utils.cpp index 7bc0fc1a..d1bbc414 100644 --- a/multigeiger/utils.cpp +++ b/multigeiger/utils.cpp @@ -3,6 +3,7 @@ #include #include #include "log.h" +#include "log_data.h" #include "utils.h" // ** convert hexstring to len bytes of data @@ -51,30 +52,26 @@ void reverseByteArray(unsigned char *data, int len) { LoggingSerial Debug; LoggingSerial::LoggingSerial() - : HardwareSerial(0) -{ + : HardwareSerial(0) { m_buffer = xQueueCreate(XLARGE_STR, sizeof(uint8_t)); } -size_t LoggingSerial::write(uint8_t c) -{ - xQueueSendToBack(m_buffer, ( void * ) &c, ( TickType_t ) 1); +size_t LoggingSerial::write(uint8_t c){ + xQueueSendToBack(m_buffer, ( void * ) &c, ( TickType_t ) 0); return HardwareSerial::write(c); } -size_t LoggingSerial::write(const uint8_t *buffer, size_t size) -{ +size_t LoggingSerial::write(const uint8_t *buffer, size_t size){ for(int i = 0; i < size; i++) { - xQueueSendToBack(m_buffer, ( void * ) &buffer[i], ( TickType_t ) 1); + xQueueSendToBack(m_buffer, ( void * ) &buffer[i], ( TickType_t ) 0); } return HardwareSerial::write(buffer, size); } -String LoggingSerial::popLines() -{ +String LoggingSerial::popLines(){ String r; uint8_t c; - while (xQueueReceive(m_buffer, &(c ), (TickType_t) 1 )) { + while (xQueueReceive(m_buffer, &(c ), (TickType_t) 0 )) { r += (char) c; if (c == '\n' && r.length() > 10) @@ -82,6 +79,10 @@ String LoggingSerial::popLines() } return r; } +void LoggingSerial::Reset(){ + xQueueReset(m_buffer); + write_log_header(); +} //taken from Luftdaten.info String delayToString(unsigned time_ms) { diff --git a/multigeiger/utils.h b/multigeiger/utils.h index bce5486e..4e7893ab 100644 --- a/multigeiger/utils.h +++ b/multigeiger/utils.h @@ -27,6 +27,7 @@ class LoggingSerial : public HardwareSerial { size_t write(uint8_t c) override; size_t write(const uint8_t *buffer, size_t size) override; String popLines(); + void Reset(); private: QueueHandle_t m_buffer; diff --git a/multigeiger/webconf.cpp b/multigeiger/webconf.cpp index 0a5c5508..968e08e0 100644 --- a/multigeiger/webconf.cpp +++ b/multigeiger/webconf.cpp @@ -58,6 +58,7 @@ extern float Count_Rate; extern float Dose_Rate; extern unsigned long starttime; extern int log_level; +extern int hv_pulses; extern float temperature; extern float humidity; extern float pressure; @@ -216,9 +217,11 @@ sprintf(tmp,"%.3f",Count_Rate); add_value_to_table(index,F(tubes[TUBE_TYPE].type),FPSTR(TRA_CPS),tmp,"c/s"); sprintf(tmp,"%.3f",Dose_Rate); - add_value_to_table(index,F(tubes[TUBE_TYPE].type),FPSTR(TRA_DOSERATE),tmp,"µSv/h"); +sprintf(tmp,"%d",hv_pulses); +add_value_to_table(index,F(tubes[TUBE_TYPE].type),FPSTR(TRA_HV_PULSES),tmp,""); + index +=F(" "); // Paginate page after ~ 1500 Bytes server.sendContent(index); @@ -314,7 +317,8 @@ void handleDebug(void){ server.setContentLength(CONTENT_LENGTH_UNKNOWN); server.send(200, FPSTR(CONTENT_TYPE_TXT_HTML), ""); char s[10]; - RESERVE_STRING(page_content, XLARGE_STR); + RESERVE_STRING(page_content, LARGE_STR); + Debug.Reset(); page_content = FPSTR(WEB_PAGE_HEAD); page_content.replace("{t}", FPSTR(TRA_DEBUG_DATA)); @@ -345,13 +349,10 @@ void handleDebug(void){ else strcpy(s,"NOLOG"); page_content.replace("{lvl}", String(s)); -// page_content += F(".
");
-  page_content += F("

");
+
+  page_content += F("
");
 	page_content += Debug.popLines();
-	/*page_content += F("
");*/ + page_content += FPSTR(WEB_PAGE_DBG_SCRIPT); page_content += F("
"); server.sendContent(page_content); diff --git a/multigeiger/webconf.h b/multigeiger/webconf.h index bfb6be02..97c881cb 100644 --- a/multigeiger/webconf.h +++ b/multigeiger/webconf.h @@ -92,7 +92,7 @@ const char WEB_PAGE_START_BUTTONS[] PROGMEM = "
" TRA_BUTTON_LOG_PAGE "" TRA_BUTTON_REFRESH "
"; const char WEB_PAGE_DBG_SCRIPT[] PROGMEM = "
"; +document.getElementById('slog').innerText+=r;}).catch(err=>console.log(err));};setInterval(slog_update,2000);"; const char WEB_PAGE_DATA_LINE[] PROGMEM = "{s}{d}{val} {u}"; diff --git a/platformio-example.ini b/platformio-example.ini index ac1cff94..6e3e19c8 100644 --- a/platformio-example.ini +++ b/platformio-example.ini @@ -8,49 +8,53 @@ ; Please visit documentation for the other options and examples ; https://docs.platformio.org/page/projectconf.html ; -; ******* DON'T EDIT THIS FILE! ******** -; ***** Copy it to platformio.ini and edit that! ***** -; +;TR 07.04.2022 +; Steps to add a new translation language for the internal sensor pages: +; 1. create a new file translation_xx.h where xx is the language code +; 2. add this file name to transl.h +; 3. add a new chapter in platformio.ini, like [env:geiger_en] (copy&paste), and +; - adopt headline [env:geiger_xx] +; - the buildflag '-DTRANSL_XX' to the new language +; this defines TRANSL_XX which can be queried in transl.h during runtime in its if-elsif and includes the corresponding translation_xx.h. +;--> add new translation strings to ALL files translations_xx.h !!! +;--> do NOT add translation_xx.h to any of your routines, but transl.h + [platformio] src_dir = multigeiger - -[env:geiger] -lang = DE -board = heltec_wireless_stick +[common] build_flags = -D CFG_eu868=1 -D CFG_sx1276_radio=1 -D ARDUINO_LMIC_PROJECT_CONFIG_H_SUPPRESS=1 - '-DTRANSL_DE' -platform = espressif32 -framework = arduino -monitor_speed=115200 lib_deps= U8g2 - Adafruit BME680 Library@^2.0.0 + Adafruit BME680 Library Adafruit BME280 Library Adafruit Unified Sensor - IotWebConf@^3.1.0 + IotWebConf MCCI LoRaWAN LMIC library h2zero/NimBLE-Arduino + ;Adafruit BME680 Library@^2.0.0 using latest now + ;IotWebConf@^3.1.0 using latest now + ;EspSoftwareSerial using hardwareserial instead +[env] +monitor_speed=115200 +;set/adopt your upload and monitor port here, like ... +;upload_port = /dev/ttyUSB0 +;monitor_port = /dev/ttyUSB0 + +[env:geiger] +;lang = DE +board = heltec_wireless_stick +build_flags = ${common.build_flags} '-DTRANSL_DE' +platform = espressif32 +framework = arduino +lib_deps= ${common.lib_deps} [env:geiger_en] -lang = EN +;lang = EN board = heltec_wireless_stick -build_flags = - -D CFG_eu868=1 - -D CFG_sx1276_radio=1 - -D ARDUINO_LMIC_PROJECT_CONFIG_H_SUPPRESS=1 - '-DTRANSL_EN' +build_flags = ${common.build_flags} '-DTRANSL_EN' platform = espressif32 framework = arduino -monitor_speed=115200 -lib_deps= - U8g2 - Adafruit BME680 Library@^2.0.0 - Adafruit BME280 Library - Adafruit Unified Sensor - IotWebConf@^3.1.0 - MCCI LoRaWAN LMIC library - h2zero/NimBLE-Arduino - EspSoftwareSerial \ No newline at end of file +lib_deps= ${common.lib_deps} From 68ea55c92f784e1abdbd9d1f9ac82e9ece0b575a Mon Sep 17 00:00:00 2001 From: tom-r Date: Fri, 8 Apr 2022 12:11:11 +0200 Subject: [PATCH 4/7] Italian translations added Readme file adopted with changes in this fork --- README.rst | 41 ++++++++++++++++++++++++++++ multigeiger/transl.h | 2 ++ multigeiger/translations_it.h | 51 +++++++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+) create mode 100644 multigeiger/translations_it.h diff --git a/README.rst b/README.rst index 666f5497..fcdbe36b 100644 --- a/README.rst +++ b/README.rst @@ -7,6 +7,47 @@ It has a lot of interesting properties, which you can find on the first page of Der MultiGeiger ist ein Messgerät für Radioaktivität. Er hat viele interessante Eigenschaften, die man auf der ersten Seite der Doku nachlesen kann. +Adoptions in this fork : +~~~~~~~~~~~~~~~~~~~~~~~~ +this fork is based on the master branch of the original MultiGeiger software (https://github.com/ecocurious2/MultiGeiger.git) as of 08.04.2022 + +Motivation: + Having a Air Quality sensor from the Luftdaten project (sensor.community) installed, I know about problems with dropping Wifi connections, memory leaks, + interrupted data transfer to the internet portals, etc. + And I also know, without being able to monitor my sensor outside at its final installation position, I would have been lost. + E.g. to search a good installation position outside, I use a power bank for the sensor and my cellphone to display the 'Actual Values' page, + and I directly see the Wifi quality. Then you install it there in a provisional manner and watch the values from inside via Wifi for a couple of days. + If sending to the portals fails, you can see the http return code on the log page. + You can actually monitor all values which are normally sent to the serial port via the log info page via WiFi now, + no need to connect a laptop anymore via USB (which would imply a restart, resetting the sensor, which could solve/hide an actual problem ) ... + I have a influx-db running here on a raspberry, where I log all my weather data locally. Of course I wanted to include this sensor as well. + +This fork bascically ports some functionality from the 'Luftdaten-Sensor' (sensor.community) to the MultiGeiger, +with some simplifications and enhancements in the code ... + +It does NOT touch the original code concerning gathering the data from the tube, etc. This is all untouched ! + +This implementation adds : + 1. 2 local html pages which are accessible via WiFi. Just connect to the sensor in your WLAN and you're on the first page ... + * first page is showing actual sensor values (Dose rate, cps, HV pulses), Wifi-Data (Signal/Quality), free Memory, Firmware Version & Date ... + If BME280/680 is available, it also shows temperature, humidity & pressure + * second page shows log infos which are normally only available through serial connection + The loglevel can be changed temporarily (runtime, not saved) + 2. a configurable http connection to a influx database included in the original configuration page + 3. a (extendable) translation system for the texts displayed on the local WLAN pages. Languages DE, EN and IT are available already. + Preferred language to be set on compile time + 4. bugfix in the configuration page where Text attributes were showing only '?????' upon first run. + +Compilation : + +I use 'MS code' and platformio for compilation, upload and monitoring (I'm working under Fedora). +You can copy the platformio-example.ini to platformio.ini and compile one of the (currently) 3 language options, and upload it. +The only thing to be adopted in platformio.ini is probably upload_port & monitor_port +Necessary libraries should be pulled in automatically. +Furthermore you have to copy userdefines-example.h to userdefines.h and adopt the content before compilation. + +I have only experience with the WiFi version of this sensor, so the pages will not be visible on the LoRaWan sensor option due to missing WiFi (I guess). + Documentation ~~~~~~~~~~~~~ diff --git a/multigeiger/transl.h b/multigeiger/transl.h index dd56d283..23524a88 100644 --- a/multigeiger/transl.h +++ b/multigeiger/transl.h @@ -14,6 +14,8 @@ #include "translations_de.h" #elif defined(TRANSL_EN) #include "translations_en.h" +#elif defined(TRANSL_IT) +#include "translations_it.h" #else #warning No language defined #include "translations_en.h" diff --git a/multigeiger/translations_it.h b/multigeiger/translations_it.h new file mode 100644 index 00000000..054963dd --- /dev/null +++ b/multigeiger/translations_it.h @@ -0,0 +1,51 @@ +/* TR, 07.04.2022 + add new translation strings to ALL files translations_xx.h !!! + do NOT add translation_xx.h to any of your routines, but transl.h + Steps to add a new language : + 1. create a new file translation_xx.h where xx is the language code + 2. add this file name to transl.h + 3. add a new chapter into platformio.ini (like [env:geiger_en] (copy&paste), and only adopt the buildflag '-DTRANSL_XX' to the new language + this defines TRANSL_XX which can be queried in transl.h during runtime in its if-elif and includes the corresponding translation_xx.h. +*/ +#include + +#define LANGUAGE "IT" + +const char TRA_CURRENT_DATA[] PROGMEM = "Valori attuali"; +const char TRA_DEBUG_DATA[] PROGMEM = "Informazioni Debug"; +const char TRA_SET_LOGLEVEL_TO[] PROGMEM = "

Cambia livello log a {lvl}

"; +const char TRA_LOGLEVEL_IS[] PROGMEM = "

Livello log è: {lvl}

"; +#define TRA_ACT_VAL_HEADLINE "Valori Attuali" +#define TRA_BUTTON_NOLOG "NESSUNO" +#define TRA_BUTTON_CRITICAL "CRITICI" +#define TRA_BUTTON_ERROR "ERRORI" +#define TRA_BUTTON_WARNING "AVVISI" +#define TRA_BUTTON_INFO "INFO" +#define TRA_BUTTON_DEBUG "DEBUG" +#define TRA_BUTTON_CONFIG "Configurazione" +#define TRA_BUTTON_BACK "Ritorno a Homepage" +#define TRA_BUTTON_LOG_PAGE "Pagina LogInfo" +#define TRA_BUTTON_REFRESH "Riavviare" +#define TRA_REFRESH_INFO "Riavviare adesso" +#define TRA_LOG_PAGE_INFO "Apri pagina Loginfo" +#define TRA_CONFIG_INFO "Apri configurazione" +#define TRA_NOLOG_INFO "min. info" +#define TRA_DEBUG_INFO "max. info" +const char TRA_SEND_TO_INFO [] PROGMEM = "Inviando a {ext} ..."; +const char TRA_SENT_TO_INFO [] PROGMEM = "Inviato a {ext}, status: {s}, http: "; + +#define TRA_SENSOR "Sensore" +#define TRA_PARAMETER "Parametro" +#define TRA_VALUE "Valore" +#define TRA_MES_RESTART "Avviando..." +#define TRA_MES_CONF_SAVED "Configurazione salvata" +const char TRA_CPS[] PROGMEM="cps"; +const char TRA_DOSERATE[] PROGMEM="Rateo di dose"; +const char TRA_HV_PULSES[] PROGMEM="numero pulsioni ad alto voltaggio"; +const char TRA_TEMP[] PROGMEM="Temperatura"; +const char TRA_PRESSURE[] PROGMEM = "Pressione d'aria; +const char TRA_HUMIDITY[] PROGMEM = "rel. Humidity"; +const char TRA_WIFISIGNAL[] PROGMEM = "Segnale"; +const char TRA_WIFIQUALITY[] PROGMEM = "Qualità"; +const char TRA_ESP_FREE_MEM[] PROGMEM = "memoria disponibile"; +const char TRA_ESP_UPTIME[] PROGMEM = "Tempo di esercizio"; From 9c86022ee5d11d298eed89ae7cee0d2d9d0b7401 Mon Sep 17 00:00:00 2001 From: tom-r Date: Fri, 8 Apr 2022 15:36:36 +0200 Subject: [PATCH 5/7] README.rst reverted back README_Fork_changes.md added platformio-example.ini updated with IT language --- README.rst | 41 ---- README_Fork_changes.md | 215 ++++++++++++++++++ README_Fork_changes_html_2076c273428ddb7e.png | Bin 0 -> 27494 bytes README_Fork_changes_html_36ca21603972074b.png | Bin 0 -> 36396 bytes README_Fork_changes_html_f3d2df7ae646bb5a.png | Bin 0 -> 14156 bytes platformio-example.ini | 8 + 6 files changed, 223 insertions(+), 41 deletions(-) create mode 100644 README_Fork_changes.md create mode 100644 README_Fork_changes_html_2076c273428ddb7e.png create mode 100644 README_Fork_changes_html_36ca21603972074b.png create mode 100644 README_Fork_changes_html_f3d2df7ae646bb5a.png diff --git a/README.rst b/README.rst index fcdbe36b..666f5497 100644 --- a/README.rst +++ b/README.rst @@ -7,47 +7,6 @@ It has a lot of interesting properties, which you can find on the first page of Der MultiGeiger ist ein Messgerät für Radioaktivität. Er hat viele interessante Eigenschaften, die man auf der ersten Seite der Doku nachlesen kann. -Adoptions in this fork : -~~~~~~~~~~~~~~~~~~~~~~~~ -this fork is based on the master branch of the original MultiGeiger software (https://github.com/ecocurious2/MultiGeiger.git) as of 08.04.2022 - -Motivation: - Having a Air Quality sensor from the Luftdaten project (sensor.community) installed, I know about problems with dropping Wifi connections, memory leaks, - interrupted data transfer to the internet portals, etc. - And I also know, without being able to monitor my sensor outside at its final installation position, I would have been lost. - E.g. to search a good installation position outside, I use a power bank for the sensor and my cellphone to display the 'Actual Values' page, - and I directly see the Wifi quality. Then you install it there in a provisional manner and watch the values from inside via Wifi for a couple of days. - If sending to the portals fails, you can see the http return code on the log page. - You can actually monitor all values which are normally sent to the serial port via the log info page via WiFi now, - no need to connect a laptop anymore via USB (which would imply a restart, resetting the sensor, which could solve/hide an actual problem ) ... - I have a influx-db running here on a raspberry, where I log all my weather data locally. Of course I wanted to include this sensor as well. - -This fork bascically ports some functionality from the 'Luftdaten-Sensor' (sensor.community) to the MultiGeiger, -with some simplifications and enhancements in the code ... - -It does NOT touch the original code concerning gathering the data from the tube, etc. This is all untouched ! - -This implementation adds : - 1. 2 local html pages which are accessible via WiFi. Just connect to the sensor in your WLAN and you're on the first page ... - * first page is showing actual sensor values (Dose rate, cps, HV pulses), Wifi-Data (Signal/Quality), free Memory, Firmware Version & Date ... - If BME280/680 is available, it also shows temperature, humidity & pressure - * second page shows log infos which are normally only available through serial connection - The loglevel can be changed temporarily (runtime, not saved) - 2. a configurable http connection to a influx database included in the original configuration page - 3. a (extendable) translation system for the texts displayed on the local WLAN pages. Languages DE, EN and IT are available already. - Preferred language to be set on compile time - 4. bugfix in the configuration page where Text attributes were showing only '?????' upon first run. - -Compilation : - -I use 'MS code' and platformio for compilation, upload and monitoring (I'm working under Fedora). -You can copy the platformio-example.ini to platformio.ini and compile one of the (currently) 3 language options, and upload it. -The only thing to be adopted in platformio.ini is probably upload_port & monitor_port -Necessary libraries should be pulled in automatically. -Furthermore you have to copy userdefines-example.h to userdefines.h and adopt the content before compilation. - -I have only experience with the WiFi version of this sensor, so the pages will not be visible on the LoRaWan sensor option due to missing WiFi (I guess). - Documentation ~~~~~~~~~~~~~ diff --git a/README_Fork_changes.md b/README_Fork_changes.md new file mode 100644 index 00000000..c68f99b8 --- /dev/null +++ b/README_Fork_changes.md @@ -0,0 +1,215 @@ + + + + + + + + + + +

+Fork +of MultiGeiger

+

Adoptions in this fork:

+

+this +fork is based on the master branch of the original MultiGeiger +software (https://github.com/ecocurious2/MultiGeiger.git) as of +08.04.2022

+


+ +

+

Motivation:

+

+Having +a Air Quality sensor from the Luftdaten project (sensor.community) +installed, I know about problems with dropping Wifi connections, +memory leaks,

+

+interrupted +data transfer to the internet portals, etc.

+

+And +I also know, without being able to monitor my sensor outside at its +final installation position, I would have been lost.

+

+E.g. +to search a good installation position outside, I use a power bank +for the sensor and my cellphone to display the 'Actual Values' page,

+

+and +I directly see the Wifi quality. Then you install it there in a +provisional manner and watch the values from inside via Wifi for a +couple of days.

+

+If +sending to the portals fails, you can see the http return code on the +log page.

+

+You +can actually monitor all values which are normally sent to the serial +port via the log info page via WiFi now,

+

+no +need to connect a laptop anymore via USB (which would imply a +restart, resetting the sensor, which could solve/hide an actual +problem ) ...

+

+I +have a influx-db running here on a raspberry, where I log all my +weather data locally. Of course I wanted to include this sensor as +well.

+


+ +

+

+This +fork bascically ports some functionality from the 'Luftdaten-Sensor' +(sensor.community) to the MultiGeiger,

+

+with +some simplifications and enhancements in the code ...

+


+ +

+

+It +does NOT touch the original code concerning gathering the data from +the tube, etc. This is all untouched !

+


+ +

+

This implementation adds :

+
    +
  1. + 2 + local html pages which are accessible via WiFi. Just connect to the + sensor in your WLAN and you're on the first page ...

    +
+
    +
      +
    • + first + page is showing actual sensor values (Dose rate, cps, HV + pulses), Wifi-Data (Signal/Quality), free Memory, Firmware Version + & Date …
      +
      +
      +
      + +
      + +

      +
    • + If + BME280/680 is available, it also shows temperature, humidity & + pressure

      +
    • + the + page refreshes automatically every 10s

      +
    • + second + page shows log infos which are normally only available through + serial connection
      +
      +
      +
      + +

      +
    • + the + loginfo is updated automatically in the central frame and can be + scrolled

      +
    • + The + loglevel can be changed temporarily (runtime, not saved)
      +

      + +

      +
    +
+
    +
  1. a + configurable http connection to a influx database included in the + original configuration page
    +
    +
    +
    + +

    +
  2. a + (extendable) translation system for the texts displayed on the local + WLAN pages.

    +
+
    +
      +
    • Languages + DE, EN and IT are available already.

      +
    • + Preferred + language to be set on compile time

      +
    +
+
    +
  1. + in + the configuration page where Text attributes were showing only + '?????' upon first run.

    +

    +
+

Compilation :

+


+ +

+

+I +use 'MS code' and platformio for compilation, upload and monitoring +(I'm working under Fedora).

+
    +
  • + You + can copy the platformio-example.ini to platformio.ini, compile one + of the (currently) 3 language options, and upload it.

    +
  • + The + only thing to be adopted in platformio.ini is probably upload_port & + monitor_port

    +
  • + Necessary + libraries should be pulled in automatically.

    +
  • + Furthermore + you have to copy userdefines-example.h to userdefines.h and adopt + the content before compilation.

    +
+


+ +

+

+I +have only experience with the WiFi version of this sensor, so the +pages will not be visible on the LoRaWan sensor option due to missing +WiFi (I guess).

+

+
+ +

+


+ +

+ + \ No newline at end of file diff --git a/README_Fork_changes_html_2076c273428ddb7e.png b/README_Fork_changes_html_2076c273428ddb7e.png new file mode 100644 index 0000000000000000000000000000000000000000..44bcb21721fd345893b4f420b0ce6430a8471b2e GIT binary patch literal 27494 zcmb@tcT`i~w=Wt3LJ$yQ0R$4zAXpHjL#Rp#NKxr1O_APvZz3WB38K=QfOLUKZ=n;B zF1;h6_t1OZ_Iu7f=iJ{o_q_YYc>E!fWUsx~T64}%nJZyW)lqa8SuTP=AUb6wc?}SV z91jA4MSVu!3 z5D+LAc~|s453iAt30r782y{*&k%a5S)`82(Ua6qu8NU3W;{pDla#dDSpjx1yf-+u% z$Y@D{K-O24Z-z6ui@&*(lQ)z>|ev3$>BD)cTs&#l`%+LDIu7X5O1 z9x%rL_A^61b9taF@d6HiaCq*5t0#9lS_!2Ff%r0TNHP$pmUfF7^oz=q7Bo*TNg22u zbb#N_siT8}vu;gam5aunDeAJ41ET z(w`tlMt9;!nWRt(2dx>)u-m=h*Qk17=ij=)75X5@i|)iqE|wFS_}{-DJ(=dWd%=C> zWxuk^6%!DMEyQqD)>`hdebkk7^)dg1ae#G~JKbPC(Kb!g zz+{qwq|R1c&BwMXaWX2>L3ofO96jfhxE<%e^~(h3SJ-F@5~Hc{oao-ER4$C3I#}O7 z^Q-9-{^!cGoxx?@_Cv88drCOzgz=OdLpH-xD`_mX_hawgAE(2?fSsPLLv|xcvIgam zX?Vc5fPmH%;d&!`Zk88tl0>PLyWv6V`J_j44C-w?r6G8RY4)32yt2yclA1!ZeIDGax;7%NVz5X}_zb>ZAL`&K>(Aywy;A>KcTL>BsPQF^n zDwK*O`As*~%#C}@5M9o!dyK4QPW0gBkAVwx_$CAG@xfk>}j8y$~v}Y(YkVN`g4A+8r+`M;Q~m()Zx?#7H{Cfx;U#V zJr*`*noKv2Cwx7*6hIqZnO!*Lxqh! z4w5`i=dUkN&O4>$Z;Lr@N+d5j`wb0p8HEn%7R7gGnU9BX`5$|*god7bM(Uc%295kQ zt>+0JG7B+2z^59&7Gh1$$f{3#?k&D8VX zr~DkIwqvkY5#-=YzGHB@-j+$rJni=a^wVYW=*oMv4DFtSuGGvygCmlF+tAQ5Il9Hl$=eL$tm{7>r%OH~MwpE%-*f$fOxSB2m!th4y&;N-2ZXfR*WL~pD-sy{x#NpUp> zE!^8WJW0Vj{}5TDG!|gr(}ZHGm?pn>xrAI2j<=!`VHjI{ypX?V#em2E(cNOc&B*F$ zkceIF+VU)y+85OIzD$f`7_#Ig#>JR*2qnyPbl#)E1Q>XZzVxX=50Sm#rFUAS5Qg*$$}6l)}YN@e|W@eoPZ9jjSFvrd{MbL(@ZhQ9!vq;?)^5bY!)f;ZEoMo|U#*mIZo#Lg=|4;H6(exLQMC*^Pg}0i zdoItvCGXYfRs`X%-M>IBSvWT3vU6`+OjRtP2d}&W?H2U+(N~ z2I$pXIK6DEN9A1q$dq{IWQO|@ykz|mFZ>;&&{OFLJs18Rs>uFA(LbO3=Zywa&~0|0T7Rbh2egKQK>z-3nlasfy+Ut} zI2=&>Ro_AQTm3AfMQOfcm&H+ilkU0F_*HJ*0sr9U;!yr|X;tpMT5>0EhzvQX_5V=jPLm z*KOl{^Tv{?+8wfD^VkZHGF?y_k8RT8<=dEMU#%IV%%kD@51vUsl36E*o^^Cf+0POk z@unvOAdtxwc)iE*wu7HrN=N;3=PQrs$IH$F%KkWsmUZ@v`iqRE-ZT!iuVVFYI^hi1 zl{q%GOh_i|G=EKKo7jH^0>Mp_@h?MA%)fv6a2rtx(1q7~!j!JDJ~g7+{$``Fl)!%U zjI!$wK4!6MDV$we3`-AcFz1+}2H4P$?*9~7|H+lyIDmkR@hte-L>_YN!KSjLtC_d2 z(=gmica?ALIKWw)+Ld^-CpONId#m4f-}3$RAd^L3gS!-Kn*14- z>P?tGTvJKJ0hrRxbf3_>E`Hf%vEk*n!Zc+Y47Lw+qm9sHp|$_oTH%E(w8$X zvY)GHtg~2uc!+k=ke1YMt&Z>}YrRt;MaoKrF)n1xsS`9fjQF3 zl5H*@9`29%q&OLq+Bmi{Bx8zE?9I%cteo{?Z$Gp?1fL`(&O51Lz|wvB_R+0;K=Sd# zc%81Bk>UIwjU&&aT=B!TvzLUiG8^G1gZ{Sb7iu~M8rra7yz$_X90*jfJ+fD}`GkaDBL;NtjJOAqdk5@(x6qmw^vV_x zdK=g_Lhq9lr3}9JS*8*{?#4#tiptG!gpLy7w*I(k!Oi4Xi%CO448~RA1+NTc%B41H ziYq=lG|p2Td|79s0)c?YZQ%R+@BdI4|B2ttzZF(;4qKz7U~xl!TkZ`fhZ)HCs*bCl z=XJBX8asD37v}U#p{UB}v{yChnyocgRU|J#uRP=9Ar0D(-WBosVmYs9G%lV{mfRou z;#$BJK~629-Vom`dc5loJ0AI_K>DWb;}y<1M|Rxn(Y=-+Y0Aq7a);wi4BCh2)|!pN zu0ML*wsDKkirZs9s7ZO(6*=Y1GxJ)8Iz@TXh5h>6a$FhV-Yb9M239q4(KVTG5YLM@LcQ#=A z%{dT0H1F`0jb@L!;?4Dd8iv*-rN&i>6z09A+MLCKiHh zG;KrwbK09X-)U;LLV9D=!|;X1$ZK>7Hl>+W?w(|FA4_s{xhQNqtx@^c!+mdwCkh^W z1k>@%lBmrA_Nao>{P*}9x5qw~bAWs);oU9@aKUHX@EY9tv1pkA&M*Zz=06q6ocPP> z`n#T;k=>$ryz%)qU(7+Y@;#<+^-CXenA)Nf)*no>1uXZ6jNFxTl(>pncgip{8;5sX zd*1%0?+HJo$Y`R zkWvdh;`LbS(&(^QSKaMfH!}C+X)&3fXjJ#craX;fg|jU492B^(H))C1^*#3VC1pdV zxd{%R2JcJi9AOuXL-v^u#n$GCy(jAH^3CU|iPb|godk8xg)b^+vb3&c+{V`yJ{&o~ z#4(~{%8EwBx0BqblCiq$EZGl#tnRJTM?MQl^>^>G@zdY7t}vo#lwS-NNpkX@i}-ET zFW&E0ZIgXJwB;C#eZu39SUgGKLIv`}%)Uss8}H|Cw?9fBo2>Jlu=v z#-+Z^1zJ(p_AGO|yK9TcF;!t(>+`|0Y>6cIKR?`)&Kn9XL0gjOd8gEkYZKWu`Qr6V zM5dDcd{zy3$26(Xof>pCRL(1De1p-Gt(=-0zIxqXWoPV&t_$0E10)&t8K0uqEOTyC z8G_?GI~P9N?(r&jGF`bVx)&^WVl!q+vN=3w7cC|ok-NC9xck8A$%X4lgYWSD-@ew^ zsRK+IaT)%EEkS9)r`&Fl#CH1R&%tu6Q(WA_k}YYo<=0EvQgtuMvTC`_p0fniJ(33e zgh&kN4>%8FIETV_$eT{dKn*&2@S31IK8QDdZcT&-a@X`&5-d}Sk1-bc03;zZ*uRyz z5oEGu0;taSeO0{-66HalEeak?mTN} z{ViKE_>nZ~TZZb;?Dwiq2<_>=MZPn#{7t}E$^-|nMY$Pl#(ccsjsdAtQg=^y3j8M% zyFa`~GLV+uST-D}kYrku9FELgl^6(~$C9#&l8yUA?1e#+)IE=`XZ-TXW?#7eeI)8Y zy`S&vx6c>!&+9ac1jk-SQ1)nsN+gZHJNVh|!@Z$Gi8U=EEf>CC#0#RjFq`v^>}dgGnrDA?M;MWT4h?vqUnuL-009AdaP7ZZV*md}{eL%F44y;|N~8Uo zi<0Mnx0L33dJu@$_1GZ;dA=w86*lny$VPk%p$*|r4rm{%f3%P7ypx1$v!1rv;-4ik z)oIcGgZ@^(L!9+lMc0|(#IDcgNT;i9R*&g5%!g;vHHD(aJ7%VpDg@-4ofr8P{XOK_ zkDsJ={xY*mU~q`(T8a(ksy&36NKBODc^Ip0>Jl?-;9-_`B7HS8ERcrTVtIcy+n zu4(>N35{IzAdMvR#?{t5+CzImd>w8pVp+L`ip%0#XOd ziAg(FR(AtzpD#9<24VQyyP=K8Q({KEpI#gW_z6tPTUCu(_BbS>`D|)lC+8-V88NdA zzta+%FL|9U;rJyl(|Q-v*soiU)>KQkRqdhgG41vg^mDxD;V?F(!>j4sZOmoASRqE5n3-5%66ThADL=t;VlJK3~;+m)kJ;Xs`ShxBfwDtvK}hk8j6G3>-Wm zR?c1>dn{lkHZZWK^)22dIHof0BHK{s{p&S8Z>=m^bK|eFdhxsygs=9uZ^f}_d3bPd zhAihwJywfOx^AJ?ZaZdTs>J5NOGug^#=%5J7KXOsG_G`U?eV&L*M9Y%+Z0zb9A_KF zq9xrIEAaAS({DtyXPiWzZrZNsV)8xe^9^uw?{ck(ehMNIQY<-{j{@pasy~>Dc4|F- zF+jH( zN5CH5UT9jk!*Dl$-z@7t6l3OwhjwnqInxpdAz54uW^CEWhB_(r59>K%q>Ku2FTOuc zwGm-%ctR(_ZcUd}qR+OeXU5dQ@;FsP#`!WmS5nLhw(FFKi&70X>z${iJ%hDro_WdQ zkF4-NIURoLjN>}mA5L7ad9Lz{_0kR(p)*6`j`+jd*uwb79H-ZKhk?+DXrp({d3tkh@mA0mzKB22tuf;t*vEHcejXv5u z`{bRZ#=2)K=iX#|J<LoHuRb zdWKklShSNyo{oQ&ryViAEIrMWxH3zT?>_|= zR^7%DfAqI!o<5ry$Az@{$}n+HdzqYYVyfm8|2VnHNbkFeY#pZDd(nnh6GGg&s+j)7 zBV>3h3CrJ&)ua-y&NTL`UXrsu&i~d~+Q;T`$8@25N9@p2&%QM^z9{7nQEWh-teLQ- zaxCOF7V&%6tOGME!b;wHwBmZMZy|MyMjW$qiE8lUn8$s?=mc?6yRY!U;oLzCl9kH6 zV4RbFvFQMvDF~%Z_aAcMqyl2JB6u&!VNA-Ql_&PIjc=mhZ-Ze~au$_Bm~9x*x6Gi> z{H_&d{1qy4Vkm(qWq+x`j4`6Z7S?XmMdg{3?_Arxw(2cdK?{ zL6^Z&;TXNTVZ1^glJu+cZ!Ai>hn^<`iX4Fh{vzOaC(HSbeNYAasIotRF`Sxp#Om#T&;dk%sj z;Yib!BEo0LjpEqFmEx6ox3TH{kB}qcv(GJlV{mvI&FHOH#=+ypbJ+eHVKGm)rXQ$$F+iDEjzzJ2LCk4WpSbe16p~J2e zdcJGe>WArwhFDbB1p!lGCdOk$dRGch>T0RQC(DphmLh?C#Ece7Hrn%@BmSM`u3SUo z8Vj~3)z}_m*!7Ze);k6lx9F6G;K-#m?2$*O&3eW*9Ki*TEOOiPUfMF z{}N|njE7Pw(~9$(~fr|(6Tl%CHeS=MyK0aCK0{ty3_2UVduWQ zs5Fhf#abDEHB{OHLC%dl12JQGKh}wBkk*Fx_8rDfo+XH-c{K9vo&jJP{)<)p-y~x# z3e~V-J^ca}^yey!ER4KI_sZ5{xGM$c8`6yMC6$?Zt~Sw(+hnx&_$6qNU}y1Fh?1kv z)AvzloC`yzK+rM>&a#Gm3@Fx{*RRb%a11(SqXWasq%t4*4tCN!mi!IupGQ%Lsw>57 z!}W1jig%A4pl%c^`Y@Ps0U| zi{nzu<1X>rf%hLWyE_fzi_1xOW0mw z4%8ocPjUC#4J{1bFepSU?4eDpB;5BQy;{U_eER?bMz)E0_(py?LeR5T7bPno|97IY zl@WJ!CJZ}64JS4g;^X)o6;ZO9&nnTf-*Hp)jtYGHj#KNyWO`iuIYmNi1xEN-q) z{-kuJYt?&FSLIYD`k=?OT5<*A=;$M7*QJpKUxLIjA4TAsd)Xh)*CD|JAV)X0L~b_< z%HL4O_F2EZ!QPENXm5wdcI9nYkfRqo(q6h}1#(F0vaiRlLf$`aZ{*+Jc{&N&R+B2$ z0Uub#@^=s%#ibtUqXOTId#x=B4t5efl~HL2mLT7X{BR zN;rBDmiK%R+I##d$t4pVc)|t?>Qnqi)48~g1f$#oGsb$4B+-p)bF4DL0#LkE4Msd* zp@ZD|I+TdR6DpYxNwO$eVCcfUU__VE->s?CL`luQG@YTIx5iH&8-*OJF{P0cToHi=dFX{h>MS3{P2MuUqy)j1l0E`{~~lC;yPIE z!pW~xjiPRsu2sm^y#14U?`NnqFCcwwU*;d;-~4l{#OEJ{Ki%HbhXska`yC7ZlQmMC zm`HS>KT}FVI;cJn*B@3Ok4j4)Yej<9-;A4u27ObN`kOV1M~$&bS6V}Zw-V5@#T*0z z3<Bf7m zVsRq$L|F;@TJcH`5}Xcd%pEOzGA`jp@klV>9a`3G#6KAwXud0Cng0ED$VIZsm-kg~ z(%TlN)}^{SJPykY_o41?wJvH^ej2}(;6k7u6F4+UtxX5;%?o&FgS%PI0hN(N^_!z6 zyrVT3FICj(>O~k?>>TMjj7&3$X$4Ynb^77b(X-)ashQbRi4d~gbr#G{L%@|ab$TmC z(3T05T>P2B% zDLj?bR|JQ2^nj3rmq8XbQfnEm9Fnis{TI29lnTV&~48`*8dqb__oV2M|y?{9}Xb1|0zoMoLL(5*v=QdV= zrat3r>RY{gB|ra0oNp zh04LZcAHaDjfbyeq>Iin|BzO_mPtPSwXfuKDGvtI$`)FE5pyDT=9FXp>mCwZm_I9L z(|jnl0{O`h-F2P)!maVv_Duj#_WNob09G0J+%FUHASD;!Sk0(Z`H=9aL59vev_*eC z>&W`zDW-A5m7fM=8BrTEa;n-Cjh2g=D<}fts_8zd8?=;;goOOSo16M9e_CigrgM+Bu z*{xdX_`EDI9d9DtZ)nD&1KWP?cry$_XSDVTu)Xlr41g@kgJzz!n<2p~to#;&_(2zY zY=;gkh!a(`0-^0$g}CZ2Lq;Iy)A@+BbI00dmSxEM!Ul3!P_9z6TL%A#U%G+evZCLQ z*;FRY~HXYS5096tY=EAhEd_>C%mz*bx8o9YhE;yDx zeZ;!|;`0-JqetM4P-bvHPr^6`2qmr!mAmKO?YH7t6FiN-a9Wl=FKj7y=z}T|@vamb zg^QX2Ux4!PiwOg)^*>K<{Z6LT*z!ubmrZpLW0?x~6de{R3a%kIHC%L(jh5iV@lB~(m^f6tXUHpb?kdwfNL*PkK5UZHuS$9Kzr z%A*=YR=)vvB2U_p`lC)rR??jbLN}<+K57r64YIMKZMv~9(3XMT>Xcrn;t5jx5|#qG z#g+9-xa&ui?gW8XKA)9;7oNCT)nJKhf- zsH_Bnpn*3nFrW{*m6J`?Ka(=Y|7D_gYR|7l=4;ED>YUgxe6brHRFx&AK0v4R*kWjc zX37(&dAFFxmIH<3DqELWS$if#2P)v6Qzom@3xY^6^?3|B(D!f>32r|-ev9Auh;A_4 zX4_=8OTSNtiG)L-51QMEPSJO(0s`jPnt+^q2AX-<4xr#t;-MCV@XFpO!NLa!)&3-f1s23GvoT+<<#Ja| zAq(+zd8ZN`9J|COBLN40Vd`ARt02J|>+G=xK&ipKbonek`|;o_P4Hlu|6U$iHc^u7 z;vh8VUVxl8%U{0UVLW1JI{oQ&hJsTq#b)Wo;M2MFRfbU!7k>|)iP7@aV35o3k z8cA=@bmZ2O+hFYMsdZ0K99a>f^{nh8*oPc{1h$zRE_Il{1s&kp{W1g5vgwF4sp#n< z1V`f<@Lz8*!1sDz&6wD0!N~64F}3H_1U7SU@=Dyzws`ywcDu*~;L^h`BdU-eoq6-L z1!xd)gwoB)Ey;j+->T+7Z3EE+B;M>g{UFp5{Tr%ufbl@PR0BlbuXP)kTHK^A*|ZOh zH+b?AEXYcJK+y%Li?_@=yUeVWQ4MTvHb){WRB!c;R`=Du>?ojQvlxF^iwDc2ICk?9 z8113-ZzhG;r{|=jZ&vn0AubyGpMjbPgxSng8Y0Sr7l7mzpDKcn%{xb6LT_9ztU}^U z_89^sfBG-{o!5NhMA~Rnw7`P7VA)?Z#(I5`)Yq3Wk=8jE_K@^AHsSr;K+?O<1SK$1 zT~d8KM({lA?L6W-`1nc@l{VT;Fb2X=+$$5&8w(7d9P!bs(;Ex~kCyg;gun^6i?V{t zPQ166v`$mY#Ys^bV6kSd0pbaWlD`WTSml6}URaYBl*aW#mrCyZhy}!z+CMp$i%5I6 z0tngTf0n4u3>Fk~GN@gKmC9-$DI$Yx)mP3R&H2=n=jHTnum<1)$aex&eqn5;cs>D`Py9FEIZ;68w-Hp~b~# zyvc1=TQvpfnCSG_NSeQ)IFs@D^IbZZn@pTz2LK8Hk6r}d66=RrDtM&Wi0k4L46Ap~ zJu}d<7Yl%6zuW&^KK<_mm;e|U#iM{kwKWHLZ^H>H0#=0lNdaQiw8(GJYS58sxCn|+ z1ex4vIqzos_Ccl706qs{A)Yo$BV|z>JmhOSnr3bkiWUZ4DE(c|+|hhQL*Y~j4xslY zw}E(xtNX_~l9I81rbE0S{y}5~5_jQ?N^VK`g>iJ#{ny&KN`p7!85QPoYnxqz)o(Q) ztBO<%eA`+7BkipQ=s#1}+va}UNbs7sQyH6ZTlt{Lm=qE`yYki?z|+i^!KsA^hAtz3 zr3t#6v#eJBcAbgp>ukN3D>!KR85FO_);G`9>3^fezdK3t(Wli(|iK8H=b=Egy^fGd_pQ=5VfACZ$H)XQ)(Ldm& zZ(KjFa#-|AieF7=-{EmHGRB7Y%*N)o4z6tLhz8$+19gTW zNhII=uLw$Bf@rZ9B1~o**q0zbqt;DeWMKTS)X3$dl?=jZi|Pz^2{MpR_Qa!c9S3Dr z4NT8{t>*iKH)LdS;pZUkS@dO1sf*>^8a$|zrE36v5k4dM3)dJKB(LpB3ne5ZBf&d` z=idPaH*X2|>|0Ynf}yKre{R2h+@je~fY@3)j7#F_o88SVThwtf_uy*r_wu!U%g^}( zbN5qL7{dwc{pNza74uR{oMfLzZ+1o3Q6k47MA(AaTz>)ZOT3P zWrf-{rD&mnRPOBWuC+@(GR!hzpVDGV8Zk&J(y@b9)kZd#UGd%h0LYvy9oPqjs2si; z>BJ(&GmvtZ5n!$1)GiVMLdr~U^c7*UJ(8+!TC|n58CIkE#H9bkv=A#*N27fPa`&RO z$2dTa=@t&purYeIN!-KQBfCoMe{GK3XRr^0tF`V*S!J>)K;cCv*df8Z!%!35VV@~> z;2dS+|M5)Ye;7po)g-F`_;jr{parY?Z|{jNL$=CytNWUj;{+UTvDiA+@PXA^o$oHX zQOr5_U3p+m$;wLVH85a#sr$__EZ1S|tAsOIw>n@gO~iWdn)qY`YWRb+h6@|-tsfKy z8sX-u)<7Bi#V`{7m%A5Uu+4AtC2_&XhDBOTV<*lmh3?x`brlZI><0~*4cV2n5ddhc zGMJ6ZxPBR=_kI9adZ?%Jb^}5=3bZH-yNQ=_p2m!}9&y*xGs@+Gi*=iNGz8;26A}?B0nR1&a*|T$Sib zWs;BRO$JJSt9K#Wdp)M%m9$7ioQ_v*8iFGhB}ImQqCpnu1qb3ON<7~>>Ze=S?U|P& z83>M#CwZto>4>Ii|1#E34_*|aOaJ74yfrcc&j)P0AmTvLRHNyNk>PhlZ(SOKVMa7+ zL!+*=0?=A zu=2FdEe;x(p}&OS>m%{mkwCb92uOG@R{0Jxg!*r6%ON_4IO z!$9YApIX3RTu{xayCd@(Cslf{R0Y*wJg3>R_Y*?Ka|5WdhIHT2UvDaYd`l&1^c$Mi zdARKtgCAan>;hzONpZ{c*nJxiGm>(sw8+hoVYwq?@+aCkHn%GMGCM2ijo$DArENZ) z+obbph06_%fQKmA&Rdt){dUzP*jCbk8g;phHvwc(h^xiE>z3KvSiKGHKpO z3~jJCpSjFVmrqkrGUKja+uPDb+w=y`s1Q|_&+0~z+4AT@am@?Ih}x^CpMppjphgF> zR^cQcd|HB3T5=5o$o@i)a$5_PTS(O-^F}1MpvMQBMVm|+mPI7^>U>E!J{$>N=5-E*$fI5dH8U|n;M8;jbRB~3)c8^K zVjIQL&L!1R>AC2yYEGB;KiMmY#SIoc-wNG61V(@CUecligEepYLrk>m9xfKfZT*82zQ1=($jFX})qoE(S@RUTGE@jfHu~LA1-v+c1glFfG z;4h!!XrQ?oGb{*p8eCNzUJ(`ed$l|caG-A<0i^(mADTV2Ynzt z+Co@z{{Bt}l_q@6z zN_O^={YM;0-SZYdp7&rdtG%5&V(}|EcMes&l2#ykttD!bjGLBhF6oGn5UPE4wM)`0XoM~St{4w#EqiK+qK)E5Wz97V1{e&vYZ8o`&N7$ zpksQ5p~b9B_?_Q-gQrIVupnl2|6?P&gj~)Xzmcn7Uk2kNiWo0|ni!{dB#l0ND$Zpd z;ch&@&R`j z%teEA1FuhWnyY#N-M_)FBkcr07wzuI!N`!d|uox^3pEO~(2tUO&C#ru9j2l@m9KFZ=OAe)ZX5MI1}Q0Dnj4_B?JwJv8^ zP)>iD^e>eJ$h|)g5FiG~?OW3Qh7cxsh$Bl}7Q0HaBqd*W`vHO!k)Ro7ub+<2gvarAwy1nj07fHaL0 z3{YvmTqzU>p<>0Rc1`Wm4~&z6(t^HuAg6WgtirS?ERT}q)?Tjrq;diDNN;1NEXzRf zQ!ZfoEgJ;H16vZ{<;z(S8Bpe>?*kY^T0Ld>we8y=Kv5Tq7#nH>3UqhjC4*Pd3ZRje z)Hh0yV8ycqpdZPq?tgv)++6nN!HNCe#&eC!hkS*`3?Ro{TvZBgV=*BH$Tj02Zzj{Z z@k885O`U(#FAE|7RGEKUjO`N<=!UTq0u>VcD(h0P&|iT$`+p%YuK+67QEHira}m5ypNU{dv9=THKPh{MILWFrKn1R1GS!2f z5(c$^nHq6SP{Auouj-sO$4`7k$mUw5=Le*s8;SwNJ~0bqW99TLz!p85Tb5=OCN?XU zhE36cIV3g^ml!^1H09s~aqF|)v6F-3tz-bgI?|92;0O(LP9+D}$FFvb2B0G~)3a2| zkb#8jiN%pfuM+p-uE+TZ#96UdlN3Sg=YH#gQu$pp6Rbe$fXl?mGU1 z1b>gf^la+5Jn+f6&+Gb9N)SwJBVLD>6jhsVODgOP1l3<1D7>Z9bsz>1=hK^{r=sE- z(-D|&vFF#3;6_%SXxXzBZ8@L?BG|@D8;1Tk^v05Q18~t+dL9C+E(9aXazY+g4?~yA zH2rhXpo9EGkF@|GG!K6bz9wtlQ92ov6yaijc8>uNt`7>_1~~Fvg6@CJKlI%}6 zrmnE-?SE&Hd48%41WwWEEub@LF@$mGj@{!2_H|3fIg=E8fUm2Cn8TNAJO_2^eeO#Y z9$En%iD+zQTU%(w_*!BQ(6W|uHQa<70tUj}e>*F54e;nRv~sKjRFKSj zz$Le;?FSB|bh7~-LE6%;6Wf*KZ%&q;)DNVreA#42aNbFftD=?R;$O=)>>wy%={=j7 z$+~!kU%y_M|Hc_gUlMCK?6rS+A-SC@8I$)hW7|_{w(PZy;YCr|{!>w+EMqa=E^tJgLff&v*I;rp$x#%zNnxEC=tp^)WvVXxx zZhW|(C+95|z^XHYaEkgjiv`dr1Hc&_t$w>yG)U^^yA@t(?$WoKD7QFT%C_Ab#woYd z4#(VgvhAJlr{zJh{?EJe8#2x1-w3>$$i4zm=R_zzo&@}KDNTAekAo<(^Na4Qkj@AU z&2MNv=-mi5RT*nfEtnIVhp3yrq ztG!B&_+zM@NOWp54XAO7!3TeqM|RwTo{VveYZYHQ@d3zAqVr(m@(06Gx^C`6V}+<;&^|vnv*P`!6uup!3oc zeI;-*3|_7BhhDnYK=v5ydFta>&?fTqas$CQ*ejBI<_YL;GwxsUgz%*4Wk*Jju|?fg z{g7)@u`@sbyA>jOWdIF5y$Y!WM4m0z(VySeo+E9qOT`CkMK`TdyHQnLY@-psdHIQ+g7;rTbHL=VW`p}&Cbsbkqy=u7T`aV84 zEW>Z)r|KpBKX89|nS!l8AUM9&td{3~{3z0AU&H*~3Mw*+Uf{g;(B^A0KmG!{M}R@h zBZhgZyVhGHvw#>KSry;AIzE>w)NTimT2~6>)F*yhE<-C5^81b=`(QO8-gMaMh2&2e zf3TdXSNd(DlXC}BgyrxxrmXs49YF>`-0ilqeNW3zDN33y1C%GmnuG5;Tg}G!ubp@I z50<=Oa<6^pFa+21jjR#GU)Zcpxab>#M~gVXH&X^0*nwkd+hq}-K0bW&Nq|yYdjJYy zyA%xh&=8Ze#B->x>X>v>L4vl7?MmBIyCYc4?a4O|5dSnfz;lNS9iB%+c0|p%6 zqYd)Dc(wNhcc1Rpq8y+%H3;+L3nD46>bJ(3cGK=ud@+3;0eri$_woa~So=Mx!`EW# z#w7_san!l`1Sf@8)X~l57EW&?@OF7#oN2tZi%v4EF<;~3{}62>Ept~Y#R&YV3zZ@F z#|S>Bz?rDp-@5|TsXKAMf#Uzsb08bN?zA#~vS0ZUF8acA;!>D%kR8S@Pp_+Cdqir+ zsnz@5?8>{Wo7weaO0b6GJ%E z>nv5~Qs8+gS8;YeQOB5>^*E!dKP>lwG~AQ(TGANjT7+aw3IirZG0WCk{h~Bu<(8-F z^B|+CM=LZmK=?SULNe>pLef6FI#Yg=5 zKt1fX{&uvG-Bzcsn`NT7c7b_Xu60(=u84Y<-o3jqPy2+bWs-eW3Fhiwu6#fDef6RB zlC>X#KU0(T`04D+%2ei5%ID2uHtIA+Q|vYhN>lXx!=?owNqX`J#VM=7PJD^O8`S*L zRr4zeblg(OTchd{#S~kI!5?wn9_#9lQ##(;(fa*Aoqcy$Q``1!1Vj);0v-f}pdunj zFVX@iMGzDeq$o{#7a^2{7Ep@PLhn@+qy-|qgCIc&Akuq@bfowCmdEqm^X`53ci;Vf ze*quad#~(0*P3gNG3M9__(xM-XD8P1#)SQl7fSdx1tV=ml7WBEABs+W*weVACH>u6 z86pOo2CzH_(29&2L)PiwTB%J3_YL}xOZL+1)2vYP^HHzIv1}75Kng~4zzFY!joXDu zvo<-n22`%h8|S@(r2JY3;>9R84z5a&q{pK{);%8hHq_{~bO!CDf#^7pWUrWZ!l6dQVEi6U20qw!*BwHX!8+(E$WRW zIDF4JSv?9HiQG-z&L!5))Px*8hVdOxLuMR6TGs@!FaCbrxmVBcT$ZfYe9j1f?z%G|3<$t=2s~G@p5I6=9O4cs1)z54Ze)=mlhLZdiiYi`={P< zq?O-Oe7i{{fHS>lzT?sc4&nl zR7m;_s#GRRM9R`a-dsT#@OirkPld$2!GGcv;tsxjf$uxR1k3sJM8s2)x@h%so-NuZ zG2T0fg`-jPo9UgjtSPcpgF z*)e9RDy%+i`$Sf&CYV=(_e>NE)jS`OtSq^JeB^6YX?S^sWL`+(l51CvnCuV?vwpFI z;NYL7a&I;{Ihy$;ZyMY4x~=!bfj71mrc(~>C8$vRapKi^a=ZVCtU-h_Dz9lwn zI8h039I)_VF!w!Mq#<$(+?d6|$q+HZZ0)>7W0rBIAZ#ay@~m#XYcCDCK(`z@+>2GU z3t(PAKL}|d1=#yo_l_4UVg~14Z!$(#oZe?JOuRAgJNA_pDf^xLnhN4co8Ku@wiXf|qQu9E1` z&XDqA1NS-D)uS#6zka+MikCh}f4X@2hJo2w${?r?Cf~#t#PkpNe6JDhPOvIU&kE3z z{FXal`_4M7|NCZZjOKaE*P2<`>l=ed1UN7{$=t*J_yhF=N0%Bv6x%MEY3e;bauCvR ztk14@!`c+uB*r7QJW8#88l%@RWXMA=vDj+!u~39Uewn~U__2~VennC8Vl|o$c^Ljb z(OURk0PRLgkX-GWIsGl|Ewvj-RBTwNoS)24N^G@dPcP?$=5XzsLr3UZc%2QOv=fHU zLd?alQitI9HRK5&JqOY3G$xMNBm5~XWidQ1l2!ugY+(R}RXys1y2`ajdwIlhEiT{! zvE~SC|6AVWE?vO@n$e)sW9qx9eP)5e34of9@`3u^=x?CU3p}N`(HjBGXM`1y1^K|T|zeQbZR`6 zLe-+wf#7=JD&$(|MC_pW)4*Zr2ho*oT+dTE^#@yLn$BrM`tt#TUKAuQ!Yd!+#;OU1 zA^dg4Y>q+);mBEVZdQ!uwooXZ_rqCHhgsgmu%3fMgcyY;@A2WT!s<1QS#1B;2Q9v2 z?dO0_R$?|-zM%m=hSPCkj{I02A58G63}~;(Wf!w6%-!s7*EuI3a+TdE z-)pNMXwq(@+?3MN=O~-PUYCj&lQPqaiyRGl*($PKZ2s2Mji}LL?Y-N>_Avxb18GIV z&t-(m8W+h|uP!A@1b9703RUT}(~Gffr|lXIA{@vOda?#*ryp_2v=gs7@smH+$@pXA zf`%D#>9bPi+nFC%{-`f^=?i|P$DIv@X{YyZWjT{2@G|-+P1Ma%z7erOH<9e+DA_hzhwB+Qol<12tV)ww&Fgi0MwYU~ zi`nJJ!=o)HLagp*ZZtlwclDc=4v+wl|ixoO*R#IR1%GQ6|~PHHeI~o^O8MVYU^S>te4nXj8XYn%B=AZX`8Zp9rC`P zo%mEg+M@_<_>#oLN-ny*q@4>;)Q&O51qyEZ)C(@LZZ85ef4)ocZf0o%y;E<9L=BSz zb3A@%{$t7v4o;ENla~-5m^DAs&Krs;nJ^J=nRYy(LJ_tUQMUf)bdh>y(h*neE@_Tk zm)ek92A#_{ME)qd*IOb%G72Wt@zbXq8+y#Uoo*ytOjFb;(eKp5Dh9j52-@1fib*2` zYc0B!J3P@;y1g&g^~DP-^Wb&lEq#8)5WhsmEqsK{8C~ONs|1VNitHCFldu(7L!K=T zB?;f1j2XX^hCrqSdU5zO`6Ty_ za0`?#3j)+%G83P5*v`qp%r;@~yz2Sfz9?QZ9CuxOtj$%c*2)?3k-n*)Y7)`L=T;-m z$lAHE{q?}4kDu6M*?`&i?@WxO%?Wt&YEsiuVSqeaCS9V{^4d+VyewrssixS;5D|`i z#*k~hB~<3}pYDP%V>IKr=Lb41Rxkhm={S>4i7+gZ<&=PXJ`j($rk*npNvj>qV!OiL z-y(q-Id`UPmOV2*r61ZdP0=Ju%Z4uO`_+xa6|s7!;ukGb3q^d|RtSx^Una!$5aub! z&@3O=_&qX1y%T$(&x&epPkZ>6tn@I&AF}puE^IB+w@OE+9QGg@X+Ez`dD1Ot0#Yf5^s7_wdn_vK>uL?-P zDP@g)cOcPPP4Ki%@-gGm7F9_Sm+RLERyuHDK6|0M=~`kU4_?7a&I@0^+eNHLFqHxE9Dd*`pID zTccy+CK`5iU?ZB_!IB0xlR*6iy{|*7(jvq$_!x#qBHM_dnNkRhxcN^Ky%q_Wzb);B zMxcz-IyP0}uPTKeIFT_elPTYy2KU;l*1uq$Vz*7!<1no@oMDexLJ#^hXu^Hjab%=` zSd>$I?b$dZ?|%nF^;`0lHY6ks5MYLJ5N*|1wzOYQbCY8Mph(yDFh1R7q1=22)uG-N zFNEgIuD{~{K5Q={WX*rbtqG`-r32!aLdfa2e@7e`xf0eUV{y;%HisY?mtx`SF)}yw z4#U!O&~Jb7x1-iS)F=Ont^6|HD#$101Y#7eWW8LrRntcYMKo6-Pp!gFm)Shof87?c zc<> zYRO?LKG^o#17S*aYQ(-DDi)2ub-0aB^x29Qg|;(1CgTh`z{@RL-;%2*|2&pT@1rb^AYgo8zN5_EL{baRD6@S}zT&`cb;{`sKHw znsPn&hl*Do3*7|$WzvAuA*ldYt#qBEs1Cqi%;u9`uOwmfBNh{hZM_&=lNi$U+VhVJ zt;7%4T6nV%?3&z)S7H;_1e`5pfI|7&&UE=JLhr)mTB*GUHf74fgoL+BPGqzquYW-! z0e~K{O%rfH8gBrYVr3Gy>|+0VxRJ7b+soNW3M76d^8}-rPX}%Jxi-}3;8^#u(2RS( zhU+cAyy(CfpR4I`H&nR7CZY&uX!qw?WwL=p#g75o2Q4@DuwAK>ex}2~ikAn95hrW@ z#_zjgtL zwA%Fn4L zT=cw`*TL_BHiS3^lt9&E#XHImFg4hNxnj4sHAD}bn&KTH&e)w%7nW0z;!jN3&`dt;N_la$%jKUHO}#&cVa3cubetzew?4G-<`J}XGZbhQvgj9 zuQ~ZB)f2K)BT2c_Q-Et!eh})FJXy~~@*3VC0~+cCIb+MPLpf|TzEH6a|J)1%ta(d` zin;I_ptnCskhmicRas62D(VAlZ=?JRpaWIYgL2o{LpNnve1s7kcLmwWlAsWsJ2A7k z5_pHn56!*w93Z8SQ)6O>er{#*q&-jpah~=)YHs?%J5DN`4zBi{OWfWRAC?@zJZLWRGV5Z zDO~KZ@GjKbu21bdv_;UB_U1(0)?EW0yD83wpPg&S1;v0_Jz z=c%Jrb)>jVP>^%G+q-wtu@(lYibpNePNx05@o(e$p-LTlK(TWF4{K)`>(~$s;E8^T z9;Cia&2Pd*UdY!1E~gbq*gthu)gf3wHZ`5?(U)V3Y7dn| zfrA!P>lVJ1#3r!eJqHrkMIa8a^pe(7tp8$)mtSo$j-tLM*DREw>b?V##E*DMsGX>* zM!oYXvkvau(h(1E;wRsmJH@-Z=9YQlP-$!v+nc#TJ^APUB1G(s(krC6@Ko9HJzH|X% zD|cjjbiy<6nV3s;22wg@Ev_3e~REZjo@aI|pR< z^xqXPY<*V-0y$gHg(*;FhCmVvn~AyIII&)*WIG>yS9VOmi#!Nq;GDLzd_v|H!PRK4pz8EbS zxjSx|h)_4uAmMA+m1uW=zmk8)gYQ(yy?KH*#R+G4)`9zI($bSA^k z%kpD>DEuB+qEmb`q!E{Nl$N$UG+`HNwI7h#PFJIleUXgIqmPh^>S9wO1MhU(%mrisWpa`t^Z0bY7KSMYlud#dkI$J z+k#M$v!f)q1TR!dZhY#j<77m$nu&IA@?Icn+)4OV%;I@WqZHn&G7sNM1JRLv4)u96 zeBrRqaw>wgO_ieh^3=2YEWDWQnn<*6&w218tAN9Fx7+aJj9s0ZAbfqoW3D+EKWW9^ zBF_kc6s;EN6xT}7sA!q_;%q-?Xckk;Gs!m+6<kaep25hybcksfRjkJiuNXNq+_} zDP<5r38|kH3w1V9z?YZuUWRxhUNU1)t3hLYkZP7Q+pijdR}U9y{ee#x_kTTQ|BD^! z|9*n}%XB-3Ssd=9Ebqn8Idmk;@bPGmpHx1vtHA5RaU56K@1XS1shVoFtmVtql4yfx z*xD24Y&R!N8ER_KH|o@+gQvOF)2)$g*W>D9Jz^dY?G{tq-Fs^! zexFjRAr&FDM)j5em#&hmNb)Xoy3t9fe&S-_E^q{MjzmO4hmEq#j5@)h>4 z|0NDL6iKZeFquxR%_H8IxlEq!AGWY4Q2Tvk&V8Xp!IVSLViw|#q-by!Lg|CtM?+QMbX#tqjKBljp7q|(}XwxqPa{dhTt0At#Uk` z@qM;z!+RKGe@p++M4e3|1)-7J`CftU%Kf#kv=_+6=UA2)^%O^JYG3|w>5D7(+r%Ue zPL;Y%N=NlqgBPk}zP5Lg+wUkVDXGi6ODvX8sl?Ajq`ktn9|dRF3(7Q$SSp$aeL0JX zub-T_5iqiQtC2X=pYcq&J!SK(t2I~I%Z>fp>C~*UHA(!V&q+20_Jpk6CU0g}95Lff zT-lV=4^xBSp%&$V-A568arL%-%Q?3>tv1&(4HL)&FvJVT-9N5~j*m&Tn`nT`&%rup zJZ$b;p0A!ilC`7Z(}AtD+TwJGI3Yh5lgu%0jJ=+j^%`&LrqoKgxyf6o%+`zmRK`|@ znFRBp><+qLARL1{eDKW6R{dJWGK#q~?6yPS+{}Rr)q{|;gL&4a?K@_oFC&&wwqCn4 zx9p4Zr(FstFeXR*L|2!S-(^2#D*L5dxJ=d)Eo8-EJ+|p0v)b?wo zCd>V35v8}{M_v8#GUK;JYB!ukFu9wAN?$2syQhO=uJmVV!%iM&g;c7u_F#<J(nh0O|Q*k}@)>AZ5(INkl6eVZvcm7zTGRJrE(=7y*Hp^4W^nOn#N zJkEKp&(DJMrD&DYF@i3&x_sQ|ch1nqHSYvR)0)=DyS zT_g%;$TE{?m5`Ik`Kp@rkxiY1ux3xzY;o06fbEcxYYrjL#6U(hwcuFQCM{;41+!yT zsso?Ep-$R|yY25X^oTkdt`3WNoaa4{*A9@14;fexqjC2|Rw_^>Q%$7sH(~LZ@*!(7ecCekfyHu)*o+2UMIM1+EVv*qF_yR6!6Zc*` zFA581qMV*67vzvx9h$wQaubyZBm~N)W=}T^VA=xnCE^0mI4YGKO{;F+n(_EB(h+gw5C1jg=pN=k9lZ!29Xya>P}U9$2E*(e_NMyYpXZJy6QRZOXMvY}#R3b+Hvb z1UdeHbG`?B{jhwStiMEe#HID`{X!*P5{|odl{IucmJPD&{&%VHZ(2s=i1d7aM5ByG z-H+6_PfcihJ9}cEyhB3Ap9`+eLXjTX{5^}1aG$!+!nVO)d9v_R6YNOXrIG9#9)>>^ zAm#MSmxS?_VLo-#uDHWJf%2oz6!ee*YS%57>-dTOFMhzieOCld9_}L(Jl;qg|By%y z>73YEWjHUm>be$$3?^sXEsC4P9ZCpA>-WkyJKjHPdhOl!Nc!NNO(RRa$%L9o3cd^? ziYM-GBUIk(p2K*;oAa?Fn^}oC&qUEbmdm_i(^6eZ_)bl5NUeAoaSx;}X%* z1ZlRIhoj&t>olFkH1f&w%z6Y2(Mi6Pr0L>&w;p0GMr+R(cuBTLM zM2&Rb(Cxlo+~>YP{-j5z|BtPPthU3{Na)G1tuH(B+@TfsM!ESbN=!1MEOBy8zW%M& z_+;>alL~rdeZVlbY&DXy6uL~Ox=+o5T5wZCm#>=StG%{I*OXBMTg^ZP7=p?M$sCsr z&m6bvTa=vlIa4~e$@wS|=47kdxH6tTx^>>p!}1&Hgo_II%esW2t%_yoKd-tTcknp| zuY65S;3u0JTB&Yu6R$ykFEoI9oyVmqGBN_V{YL1bVCZg&&QqwhbzRg6&$wj1O^pvPwrhSAZs5fZ2S9KEGc;MK)^Doq5 zQHOMulao8cJ;G>oRTTcSlMH1oF;Z&DhMSuk-b*FmxQ#eo)7T~JPi(YwOW4pZX)={ytGGz8spkVfF zl9Q+x=84Us!2!0o)-`7?QWZTaD|4u&4R+%11ZBFCGS1t@Z`giQd;c`)Pa}w(5wYbC4-pjB>88RUk^CxZ=4#> z*Oh(-<0BI?j^jHA9*f;!py$v>U0@H7rmr&|?*TyD@ole0>6xBf|7D(4*Sjj&ixE|Q zBTTRH_2YiN@;!bX&&JCInrr#%4gIx_OF5Y>Lw)L~rg2pX0R<%O0;{f28}y_+d5Dk9 zIgKOz2K_~PVe(_ai2aqJYuO{OzSOgxVSxPKFP!*~Q~Q5a&s!d{s_NG?m ysQ>wZih7%EZ~)D(D`?o8JGmM;nn5%yot>P_98F$*1Fw;SK<+E3%421pzxqG&uq*ih literal 0 HcmV?d00001 diff --git a/README_Fork_changes_html_36ca21603972074b.png b/README_Fork_changes_html_36ca21603972074b.png new file mode 100644 index 0000000000000000000000000000000000000000..e6eac469827902d8d621118239a20694ebcebaf4 GIT binary patch literal 36396 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 OLrGp;u0rPT&;JWIf7_h^ literal 0 HcmV?d00001 diff --git a/README_Fork_changes_html_f3d2df7ae646bb5a.png b/README_Fork_changes_html_f3d2df7ae646bb5a.png new file mode 100644 index 0000000000000000000000000000000000000000..f6038427cadae1f8323fdfd280f4774ccb8ea9f6 GIT binary patch literal 14156 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 NML|>ky{tL%{{T@~a;^XX literal 0 HcmV?d00001 diff --git a/platformio-example.ini b/platformio-example.ini index 6e3e19c8..36dcc40f 100644 --- a/platformio-example.ini +++ b/platformio-example.ini @@ -58,3 +58,11 @@ build_flags = ${common.build_flags} '-DTRANSL_EN' platform = espressif32 framework = arduino lib_deps= ${common.lib_deps} + +[env:geiger_it] +;lang = IT +board = heltec_wireless_stick +build_flags = ${common.build_flags} '-DTRANSL_IT' +platform = espressif32 +framework = arduino +lib_deps= ${common.lib_deps} From 146672e15ae152d051d624d0676dec704d390481 Mon Sep 17 00:00:00 2001 From: tom-r Date: Fri, 8 Apr 2022 16:44:10 +0200 Subject: [PATCH 6/7] README_FORK.md format fixes, pictures added --- ..._f3d2df7ae646bb5a.png => Actual_values.png | Bin ..._36ca21603972074b.png => Config_Influx.png | Bin ...s_html_2076c273428ddb7e.png => Loginfo.png | Bin README_FORK.md | 71 ++++++ README_Fork_changes.md | 215 ------------------ 5 files changed, 71 insertions(+), 215 deletions(-) rename README_Fork_changes_html_f3d2df7ae646bb5a.png => Actual_values.png (100%) rename README_Fork_changes_html_36ca21603972074b.png => Config_Influx.png (100%) rename README_Fork_changes_html_2076c273428ddb7e.png => Loginfo.png (100%) create mode 100644 README_FORK.md delete mode 100644 README_Fork_changes.md diff --git a/README_Fork_changes_html_f3d2df7ae646bb5a.png b/Actual_values.png similarity index 100% rename from README_Fork_changes_html_f3d2df7ae646bb5a.png rename to Actual_values.png diff --git a/README_Fork_changes_html_36ca21603972074b.png b/Config_Influx.png similarity index 100% rename from README_Fork_changes_html_36ca21603972074b.png rename to Config_Influx.png diff --git a/README_Fork_changes_html_2076c273428ddb7e.png b/Loginfo.png similarity index 100% rename from README_Fork_changes_html_2076c273428ddb7e.png rename to Loginfo.png diff --git a/README_FORK.md b/README_FORK.md new file mode 100644 index 00000000..f5e6458d --- /dev/null +++ b/README_FORK.md @@ -0,0 +1,71 @@ +# Fork of MultiGeiger +# Adoptions in this fork: +this fork is based on the master branch of the original MultiGeiger software (https://github.com/ecocurious2/MultiGeiger.git) as of 08.04.2022 (Firmware V1.17.0-dev) + +# Motivation: +Having a Air Quality sensor from the Luftdaten project (sensor.community) installed, I know about problems with dropping Wifi connections, memory leaks, +interrupted data transfer to the internet portals, etc. +And I also know, without being able to monitor my sensor outside at its final installation position, I would have been lost. + +E.g. to search a good installation position outside, I use a power bank for the sensor and my cellphone to display the 'Actual Values' page, and I directly see the Wifi quality. + +Then you install it there in a provisional manner and watch the values from inside via Wifi for a couple of days. + +If sending to the portals fails, you can see the http return code on the log page. + +You can actually monitor all values which are normally sent to the serial port via the log info page via WiFi now, +no need to connect a laptop anymore via USB (which would imply a restart, resetting the sensor, which could solve/hide an actual problem ) ... + +As long as you have WiFi, you can monitor your Sensor, even if your Internet connection is broken down. + +I have an Influx-DB running here on a raspberry, where I log all my weather data locally. Of course I wanted to include this sensor as well. + +This fork bascically ports some functionality from the 'Luftdaten-Sensor' (sensor.community) to the MultiGeiger, +with some simplifications and enhancements in the code ... + +It does NOT touch the original code concerning gathering the data from the tube, etc. This is all untouched ! + +Nicola from the ecocurious team was so nice to share the ecocurious logo for this fork. + +# This implementation adds : + * local html pages which are accessible via WiFi. Just connect to the sensor in your WLAN and you're on the first page ... + + * first page is showing actual sensor values (Dose rate, cps, HV pulses), Wifi-Data (Signal/Quality), free Memory, Firmware Version & Date … + + ![Alt text](./Actual_values.png) + + * if BME280/680 is available, it also shows temperature, humidity & pressure + + * the page refreshes automatically every 10s + + * second page shows log infos which are normally only available through serial connection + + ![Alt text](./Loginfo.png) + + * the loginfo is updated automatically in the central frame and can be scrolled + + * The loglevel can be changed temporarily (runtime, not saved) + + * a configurable http connection to a influx database included in the original configuration page + + ![Alt text](./Config_Influx.png) + + * (extendable) translation system for the texts displayed on the local WLAN pages. + + * Languages DE, EN and IT are available already. + + * Preferred language to be set on compile time + + * in the configuration page where Text attributes were showing only '?????' upon first run. + +# Compilation : + +I use 'VS code' and platformio for compilation, upload and monitoring (I'm working under Fedora). +* You can copy the platformio-example.ini to platformio.ini, compile one of the (currently) 3 language options, and upload it. +* The only thing to be adopted in platformio.ini is probably upload_port & monitor_port +* Necessary libraries should be pulled in automatically. +* Furthermore you have to copy userdefines-example.h to userdefines.h and adopt the content before compilation. + +I have only experience with the WiFi version of this sensor, so the pages will not be visible on the LoRaWan sensor option due to missing WiFi (I guess). + + diff --git a/README_Fork_changes.md b/README_Fork_changes.md deleted file mode 100644 index c68f99b8..00000000 --- a/README_Fork_changes.md +++ /dev/null @@ -1,215 +0,0 @@ - - - - - - - - - - -

-Fork -of MultiGeiger

-

Adoptions in this fork:

-

-this -fork is based on the master branch of the original MultiGeiger -software (https://github.com/ecocurious2/MultiGeiger.git) as of -08.04.2022

-


- -

-

Motivation:

-

-Having -a Air Quality sensor from the Luftdaten project (sensor.community) -installed, I know about problems with dropping Wifi connections, -memory leaks,

-

-interrupted -data transfer to the internet portals, etc.

-

-And -I also know, without being able to monitor my sensor outside at its -final installation position, I would have been lost.

-

-E.g. -to search a good installation position outside, I use a power bank -for the sensor and my cellphone to display the 'Actual Values' page,

-

-and -I directly see the Wifi quality. Then you install it there in a -provisional manner and watch the values from inside via Wifi for a -couple of days.

-

-If -sending to the portals fails, you can see the http return code on the -log page.

-

-You -can actually monitor all values which are normally sent to the serial -port via the log info page via WiFi now,

-

-no -need to connect a laptop anymore via USB (which would imply a -restart, resetting the sensor, which could solve/hide an actual -problem ) ...

-

-I -have a influx-db running here on a raspberry, where I log all my -weather data locally. Of course I wanted to include this sensor as -well.

-


- -

-

-This -fork bascically ports some functionality from the 'Luftdaten-Sensor' -(sensor.community) to the MultiGeiger,

-

-with -some simplifications and enhancements in the code ...

-


- -

-

-It -does NOT touch the original code concerning gathering the data from -the tube, etc. This is all untouched !

-


- -

-

This implementation adds :

-
    -
  1. - 2 - local html pages which are accessible via WiFi. Just connect to the - sensor in your WLAN and you're on the first page ...

    -
-
    -
      -
    • - first - page is showing actual sensor values (Dose rate, cps, HV - pulses), Wifi-Data (Signal/Quality), free Memory, Firmware Version - & Date …
      -
      -
      -
      - -
      - -

      -
    • - If - BME280/680 is available, it also shows temperature, humidity & - pressure

      -
    • - the - page refreshes automatically every 10s

      -
    • - second - page shows log infos which are normally only available through - serial connection
      -
      -
      -
      - -

      -
    • - the - loginfo is updated automatically in the central frame and can be - scrolled

      -
    • - The - loglevel can be changed temporarily (runtime, not saved)
      -

      - -

      -
    -
-
    -
  1. a - configurable http connection to a influx database included in the - original configuration page
    -
    -
    -
    - -

    -
  2. a - (extendable) translation system for the texts displayed on the local - WLAN pages.

    -
-
    -
      -
    • Languages - DE, EN and IT are available already.

      -
    • - Preferred - language to be set on compile time

      -
    -
-
    -
  1. - in - the configuration page where Text attributes were showing only - '?????' upon first run.

    -

    -
-

Compilation :

-


- -

-

-I -use 'MS code' and platformio for compilation, upload and monitoring -(I'm working under Fedora).

-
    -
  • - You - can copy the platformio-example.ini to platformio.ini, compile one - of the (currently) 3 language options, and upload it.

    -
  • - The - only thing to be adopted in platformio.ini is probably upload_port & - monitor_port

    -
  • - Necessary - libraries should be pulled in automatically.

    -
  • - Furthermore - you have to copy userdefines-example.h to userdefines.h and adopt - the content before compilation.

    -
-


- -

-

-I -have only experience with the WiFi version of this sensor, so the -pages will not be visible on the LoRaWan sensor option due to missing -WiFi (I guess).

-

-
- -

-


- -

- - \ No newline at end of file From 3cb343c373743494233893743d780647e7d09823 Mon Sep 17 00:00:00 2001 From: tom-r Date: Thu, 21 Apr 2022 20:14:39 +0200 Subject: [PATCH 7/7] rework of the debug levels : * removed CRITICAL and WARNING as they are not used at all * integrated the second compile time logdata selection behind Serial_Print_Mode --> see log_data.h for details Log-Levels are now NOLOG 0 --> incl. Serial_None, display ALARMS, else turn off logging ERROR 1 --> incl. 0 (=alarms) MIN_INFO 2 --> incl. Serial_Statistics_Log + 0 + 1 MED_INFO 3 --> incl. Serial_One_Minute_Log + 0 + 1 MAX_INFO 4 --> incl. Serial_Logging + 0 + 1 DEBUG 5 --> incl. Serial_Debug + 0 + 1 + 4 That means it is possible to switch the serial log output by changing the log level on the debug page. Be aware that Serial_One_Minute_Log only creates output 1/min. Until then the frame is empty ... * log levels also changed on debug page * rollover messages added on some of the log level buttons * minor fixes --- multigeiger/log.h | 31 +++++++++++++++++++++++-------- multigeiger/log_data.cpp | 30 +++++++++++++++--------------- multigeiger/log_data.h | 25 ++++++++++++++++++++++--- multigeiger/multigeiger.ino | 17 ++++++++++------- multigeiger/translations_de.h | 19 +++++++++++-------- multigeiger/translations_en.h | 19 +++++++++++-------- multigeiger/translations_it.h | 23 +++++++++++++---------- multigeiger/utils.cpp | 12 ++++++------ multigeiger/webconf.cpp | 12 ++++++------ multigeiger/webconf.h | 8 ++++---- 10 files changed, 121 insertions(+), 75 deletions(-) diff --git a/multigeiger/log.h b/multigeiger/log.h index 3b6d60c4..a0f96a37 100644 --- a/multigeiger/log.h +++ b/multigeiger/log.h @@ -4,14 +4,29 @@ #define _LOG_H_ // log levels -// reverted the numbers to meet logic in log_data.h (see #define Serial_None ...) -// DEBUG is max info, reducing step by step till NOLOG -#define DEBUG 5 -#define INFO 4 -#define WARNING 3 -#define ERROR 2 -#define CRITICAL 1 -#define NOLOG 0 // only to set log_level, so log() never creates output +// reverted the numbers to meet logic in log_data.h (see #define Serial_None 0 ...) +// DEBUG is max info, reducing output step by step till NOLOG +/* TR, 20.04.2022 : +1) there's no CRITICAL and no WARNING . remove both and use numbers for Serial_Print_Mode? +and NOLOG really logs nothing then ... +2) Serial_Print_Mode should be included in this sequence (--> settable through the loglevel!). + It's not clear to me, why Serial_Print_Mode is set fix at compilation time ?!? + Org: +#define DEBUG 5 +#define INFO 4 +#define WARNING 3 // there is nothing with WARNING ... +#define ERROR 2 +#define CRITICAL 1 // there is nothing with CRITICAL ... +#define NOLOG 0 // minimum info, only display alarms +*/ + +#define DEBUG 5 +#define INFO 4 +#define MED_INFO 3 +#define MIN_INFO 2 +#define ERROR 1 +#define NOLOG 0 + void log(int level, const char *format, ...); void setup_log(int level); diff --git a/multigeiger/log_data.cpp b/multigeiger/log_data.cpp index ad22dc74..11dbe8c3 100644 --- a/multigeiger/log_data.cpp +++ b/multigeiger/log_data.cpp @@ -19,12 +19,12 @@ void setup_log_data(int mode) { // Write Header of Table, depending on the logging mode: bool data_log_enabled = (Serial_Print_Mode == Serial_Logging) || (Serial_Print_Mode == Serial_One_Minute_Log) || (Serial_Print_Mode == Serial_Statistics_Log); if (data_log_enabled) { - log(INFO, dashes); - log(INFO, "%s, Version %s", Serial_Logging_Name, VERSION_STR); - log(INFO, dashes); + log(Serial_Print_Mode, dashes); + log(Serial_Print_Mode, "%s, Version %s", Serial_Logging_Name, VERSION_STR); + log(Serial_Print_Mode, dashes); } } - +//Serial_Logging void log_data(int GMC_counts, int time_difference, float Count_Rate, float Dose_Rate, int HV_pulse_count, int accumulated_GMC_counts, int accumulated_time, float accumulated_Count_Rate, float accumulated_Dose_Rate, float t, float h, float p) { @@ -37,30 +37,30 @@ void log_data(int GMC_counts, int time_difference, float Count_Rate, float Dose_ accumulated_GMC_counts, accumulated_time, accumulated_Count_Rate, accumulated_Dose_Rate, t, h, p); } - +//Serial_One_Minute_Log void log_data_one_minute(int time_s, int cpm, int counts) { static int counter = 0; if (counter++ % 20 == 0) { // output the header now and then, so table is better readable - log(INFO, Serial_One_Minute_Log_Header, + log(MED_INFO, Serial_One_Minute_Log_Header, "Time", "Count_Rate", "Counts"); - log(INFO, Serial_One_Minute_Log_Header, + log(MED_INFO, Serial_One_Minute_Log_Header, "[s]", "[cpm]", "[Counts per last measurement]"); - log(INFO, dashes); + log(MED_INFO, dashes); } - log(INFO, Serial_One_Minute_Log_Body, + log(MED_INFO, Serial_One_Minute_Log_Body, time_s, cpm, counts); } - +// Serial_Statistics_Log void log_data_statistics(int count_time_between) { static int counter = 0; if (counter++ % 20 == 0) { // output the header now and then, so table is better readable - log(INFO, "Time between two impacts"); - log(INFO, "[usec]"); - log(INFO, dashes); + log(MIN_INFO, "Time between two impacts"); + log(MIN_INFO, "[usec]"); + log(MIN_INFO, dashes); } - log(INFO, "%d", count_time_between); + log(MIN_INFO, "%d", count_time_between); } - +//Header for Serial_Logging void write_log_header(){ log(INFO, Serial_Logging_Header, "GMC_counts", "Time_difference", "Count_Rate", "Dose_Rate", "HV Pulses", "Accu_GMC", "Accu_Time", "Accu_Rate", "Accu_Dose", "Temp", "Humi", "Press"); diff --git a/multigeiger/log_data.h b/multigeiger/log_data.h index ff28efd7..fe92b991 100644 --- a/multigeiger/log_data.h +++ b/multigeiger/log_data.h @@ -4,11 +4,30 @@ #define _LOG_DATA_H_ // Values for Serial_Print_Mode to configure Serial (USB) output mode. +/* TR, 20.04.2022 : +Serial_Print_Mode should be included in NOLOG...DEBUG sequence (--> settable through the loglevel!). + It's not clear to me, why Serial_Print_Mode is set fix at compilation time ... + Current settings are : +Serial_None 0 // No Serial output +Serial_Debug 1 // Only debug and error messages +Serial_Logging 2 // Log measurements as a table (default) +Serial_One_Minute_Log 3 // One Minute logging +Serial_Statistics_Log 4 // Logs time [us] between two events, 1/s + +Proposal: +rename standard log levels and include Serial_xxx : +NOLOG 0 --> incl. Serial_None, display ALARMS, else turn off logging +ERROR 1 --> incl. 0 +MIN_INFO 2 --> incl. Serial_Statistics_Log + 0 + 1 +MED_INFO 3 --> incl. Serial_One_Minute_Log + 0 + 1 +MAX_INFO 4 --> incl. Serial_Logging + 0 + 1 +DEBUG 5 --> incl. Serial_Debug + 0 + 1 + 4 +*/ #define Serial_None 0 // No Serial output -#define Serial_Debug 1 // Only debug and error messages -#define Serial_Logging 2 // Log measurements as a table +#define Serial_Debug 5 // Only debug and error messages +#define Serial_Logging 4 // Log measurements as a table #define Serial_One_Minute_Log 3 // One Minute logging -#define Serial_Statistics_Log 4 // Logs time [us] between two events +#define Serial_Statistics_Log 2 // Logs time [us] between two events extern int Serial_Print_Mode; diff --git a/multigeiger/multigeiger.ino b/multigeiger/multigeiger.ino index 2e4ad0d9..e7643cc1 100644 --- a/multigeiger/multigeiger.ino +++ b/multigeiger/multigeiger.ino @@ -48,8 +48,6 @@ float temperature = 0.0, humidity = 0.0, pressure = 0.0; void setup() { bool isLoraBoard = init_hwtest(); starttime = millis(); // store the start time - //Debug.begin(115200); // Output to Serial at 115200 baud - //while (!Debug) {}; setup_log(DEFAULT_LOG_LEVEL); setup_display(isLoraBoard); setup_switches(isLoraBoard); @@ -59,7 +57,8 @@ void setup() { setup_speaker(playSound, ledTick && switches.led_on, speakerTick && switches.speaker_on); setup_transmission(VERSION_STR, ssid, isLoraBoard); setup_ble(ssid, sendToBle && switches.ble_on); - setup_log_data(SERIAL_DEBUG); + //setup_log_data(SERIAL_DEBUG); + setup_log_data(DEFAULT_LOG_LEVEL); setup_tube(); } @@ -160,15 +159,19 @@ void publish(unsigned long current_ms, unsigned long current_counts, unsigned lo // Sound local alarm? if (soundLocalAlarm && GMC_factor_uSvph > 0) { if (accumulated_Dose_Rate > localAlarmThreshold) { - log(WARNING, "Local alarm: Accumulated dose of %.3f µSv/h above threshold at %.3f µSv/h", accumulated_Dose_Rate, localAlarmThreshold); +// the following is not a WARNING in the sense of a possible system failure ! This is a safety alarm and should really be displayed anytime !!!! +// log(WARNING, "Local alarm: Accumulated dose of %.3f µSv/h above threshold at %.3f µSv/h", accumulated_Dose_Rate, localAlarmThreshold); + log(NOLOG, "Local alarm: Accumulated dose of %.3f µSv/h above threshold at %.3f µSv/h", accumulated_Dose_Rate, localAlarmThreshold); alarm(); } else if (Dose_Rate > (accumulated_Dose_Rate * localAlarmFactor)) { - log(WARNING, "Local alarm: Current dose of %.3f > %d x accumulated dose of %.3f µSv/h", Dose_Rate, localAlarmFactor, accumulated_Dose_Rate); +// same as above +// log(WARNING, "Local alarm: Current dose of %.3f > %d x accumulated dose of %.3f µSv/h", Dose_Rate, localAlarmFactor, accumulated_Dose_Rate); + log(NOLOG, "Local alarm: Current dose of %.3f > %d x accumulated dose of %.3f µSv/h", Dose_Rate, localAlarmFactor, accumulated_Dose_Rate); alarm(); } } - if (Serial_Print_Mode == Serial_Logging) { + if (Serial_Print_Mode >= Serial_Logging) { log_data(counts, dt, Count_Rate, Dose_Rate, hv_pulses, accumulated_GMC_counts, accumulated_time, accumulated_Count_Rate, accumulated_Dose_Rate, temperature, humidity, pressure); @@ -297,7 +300,7 @@ void loop() { // do any other periodic updates for uplinks poll_transmission(); - + Serial_Print_Mode = getloglevel(); publish(current_ms, gm_counts, gm_count_timestamp, hv_pulses, temperature, humidity, pressure); if (Serial_Print_Mode == Serial_One_Minute_Log) diff --git a/multigeiger/translations_de.h b/multigeiger/translations_de.h index 3cff1209..194217aa 100644 --- a/multigeiger/translations_de.h +++ b/multigeiger/translations_de.h @@ -16,12 +16,15 @@ const char TRA_DEBUG_DATA[] PROGMEM = "Debug Info"; const char TRA_SET_LOGLEVEL_TO[] PROGMEM = "

Setze Loglevel auf {lvl}

"; const char TRA_LOGLEVEL_IS[] PROGMEM = "

Loglevel ist: {lvl}

"; #define TRA_ACT_VAL_HEADLINE "Aktuelle Werte" -#define TRA_BUTTON_NOLOG "NOLOG" -#define TRA_BUTTON_CRITICAL "KRITISCH" -#define TRA_BUTTON_ERROR "FEHLER" -#define TRA_BUTTON_WARNING "WARNUNG" -#define TRA_BUTTON_INFO "INFO" -#define TRA_BUTTON_DEBUG "DEBUG" +#define TRA_BUTTON_NOLOG "NoLog" +//#define TRA_BUTTON_CRITICAL "KRITISCH" +#define TRA_BUTTON_ERROR "Fehler" +//#define TRA_BUTTON_WARNING "WARNUNG" +//#define TRA_BUTTON_INFO "INFO" +#define TRA_BUTTON_MININFO "min.Info" +#define TRA_BUTTON_MEDINFO "med.Info" +#define TRA_BUTTON_MAXINFO "max.Info" +#define TRA_BUTTON_DEBUG "Debug" #define TRA_BUTTON_CONFIG "Konfiguration" #define TRA_BUTTON_BACK "Zurück zur Startseite" #define TRA_BUTTON_LOG_PAGE "LogInfo Seite" @@ -29,8 +32,8 @@ const char TRA_LOGLEVEL_IS[] PROGMEM = "

Loglevel ist: {lvl}

"; #define TRA_REFRESH_INFO "man.Aktualisieren" #define TRA_LOG_PAGE_INFO "LogInfo Seite öffnen" #define TRA_CONFIG_INFO "Konfiguration öffnen" -#define TRA_NOLOG_INFO "min. Info" -#define TRA_DEBUG_INFO "max. Info" +#define TRA_NOLOG_INFO "nur Alarme" +#define TRA_DEBUG_INFO "alles, mit Debuginfo" const char TRA_SEND_TO_INFO [] PROGMEM = "Sende an {ext} ..."; const char TRA_SENT_TO_INFO [] PROGMEM = "An {ext} gesandt, Status: {s}, http: "; diff --git a/multigeiger/translations_en.h b/multigeiger/translations_en.h index a668cdd8..96715400 100644 --- a/multigeiger/translations_en.h +++ b/multigeiger/translations_en.h @@ -16,12 +16,15 @@ const char TRA_DEBUG_DATA[] PROGMEM = "Debug Info"; const char TRA_SET_LOGLEVEL_TO[] PROGMEM = "

Seting Loglevel to {lvl}

"; const char TRA_LOGLEVEL_IS[] PROGMEM = "

Loglevel is: {lvl}

"; #define TRA_ACT_VAL_HEADLINE "Actual Values" -#define TRA_BUTTON_NOLOG "NOLOG" -#define TRA_BUTTON_CRITICAL "CRITICAL" -#define TRA_BUTTON_ERROR "ERROR" -#define TRA_BUTTON_WARNING "WARNING" -#define TRA_BUTTON_INFO "INFO" -#define TRA_BUTTON_DEBUG "DEBUG" +#define TRA_BUTTON_NOLOG "NoLog" +//#define TRA_BUTTON_CRITICAL "CRITICAL" +#define TRA_BUTTON_ERROR "Error" +//#define TRA_BUTTON_WARNING "WARNING" +//#define TRA_BUTTON_INFO "INFO" +#define TRA_BUTTON_MININFO "min.Info" +#define TRA_BUTTON_MEDINFO "med.Info" +#define TRA_BUTTON_MAXINFO "max.Info" +#define TRA_BUTTON_DEBUG "Debug" #define TRA_BUTTON_CONFIG "Configuration" #define TRA_BUTTON_BACK "Back to Homepage" #define TRA_BUTTON_LOG_PAGE "LogInfo page" @@ -29,8 +32,8 @@ const char TRA_LOGLEVEL_IS[] PROGMEM = "

Loglevel is: {lvl}

"; #define TRA_REFRESH_INFO "manual Refresh" #define TRA_LOG_PAGE_INFO "open LogInfo page" #define TRA_CONFIG_INFO "open Configuration" -#define TRA_NOLOG_INFO "min. Info" -#define TRA_DEBUG_INFO "max. Info" +#define TRA_NOLOG_INFO "alarms only" +#define TRA_DEBUG_INFO "all, incl. Debug info" const char TRA_SEND_TO_INFO [] PROGMEM = "Sending to {ext} ..."; const char TRA_SENT_TO_INFO [] PROGMEM = "Sent to {ext}, status: {s}, http: "; diff --git a/multigeiger/translations_it.h b/multigeiger/translations_it.h index 054963dd..c665abc3 100644 --- a/multigeiger/translations_it.h +++ b/multigeiger/translations_it.h @@ -16,12 +16,15 @@ const char TRA_DEBUG_DATA[] PROGMEM = "Informazioni Debug"; const char TRA_SET_LOGLEVEL_TO[] PROGMEM = "

Cambia livello log a {lvl}

"; const char TRA_LOGLEVEL_IS[] PROGMEM = "

Livello log è: {lvl}

"; #define TRA_ACT_VAL_HEADLINE "Valori Attuali" -#define TRA_BUTTON_NOLOG "NESSUNO" -#define TRA_BUTTON_CRITICAL "CRITICI" -#define TRA_BUTTON_ERROR "ERRORI" -#define TRA_BUTTON_WARNING "AVVISI" -#define TRA_BUTTON_INFO "INFO" -#define TRA_BUTTON_DEBUG "DEBUG" +#define TRA_BUTTON_NOLOG "Nessuno" +//#define TRA_BUTTON_CRITICAL "CRITICI" +#define TRA_BUTTON_ERROR "Errori" +//#define TRA_BUTTON_WARNING "AVVISI" +//#define TRA_BUTTON_INFO "INFO" +#define TRA_BUTTON_MININFO "min.Info" +#define TRA_BUTTON_MEDINFO "med.Info" +#define TRA_BUTTON_MAXINFO "max.Info" +#define TRA_BUTTON_DEBUG "Debug" #define TRA_BUTTON_CONFIG "Configurazione" #define TRA_BUTTON_BACK "Ritorno a Homepage" #define TRA_BUTTON_LOG_PAGE "Pagina LogInfo" @@ -29,8 +32,8 @@ const char TRA_LOGLEVEL_IS[] PROGMEM = "

Livello log è: {lvl}

"; #define TRA_REFRESH_INFO "Riavviare adesso" #define TRA_LOG_PAGE_INFO "Apri pagina Loginfo" #define TRA_CONFIG_INFO "Apri configurazione" -#define TRA_NOLOG_INFO "min. info" -#define TRA_DEBUG_INFO "max. info" +#define TRA_NOLOG_INFO "Solo allerte" +#define TRA_DEBUG_INFO "Tutto, debug incluso" const char TRA_SEND_TO_INFO [] PROGMEM = "Inviando a {ext} ..."; const char TRA_SENT_TO_INFO [] PROGMEM = "Inviato a {ext}, status: {s}, http: "; @@ -43,8 +46,8 @@ const char TRA_CPS[] PROGMEM="cps"; const char TRA_DOSERATE[] PROGMEM="Rateo di dose"; const char TRA_HV_PULSES[] PROGMEM="numero pulsioni ad alto voltaggio"; const char TRA_TEMP[] PROGMEM="Temperatura"; -const char TRA_PRESSURE[] PROGMEM = "Pressione d'aria; -const char TRA_HUMIDITY[] PROGMEM = "rel. Humidity"; +const char TRA_PRESSURE[] PROGMEM = "Pressione d'aria"; +const char TRA_HUMIDITY[] PROGMEM = "Umidità rel."; const char TRA_WIFISIGNAL[] PROGMEM = "Segnale"; const char TRA_WIFIQUALITY[] PROGMEM = "Qualità"; const char TRA_ESP_FREE_MEM[] PROGMEM = "memoria disponibile"; diff --git a/multigeiger/utils.cpp b/multigeiger/utils.cpp index d1bbc414..931cf936 100644 --- a/multigeiger/utils.cpp +++ b/multigeiger/utils.cpp @@ -45,9 +45,9 @@ void reverseByteArray(unsigned char *data, int len) { } } -/***************************************************************** - * Debug output * - *****************************************************************/ +/*********************************************************************** + * Log output via Hardwareserial to the serial port AND on log webpage * + ***********************************************************************/ LoggingSerial Debug; @@ -81,7 +81,7 @@ String LoggingSerial::popLines(){ } void LoggingSerial::Reset(){ xQueueReset(m_buffer); - write_log_header(); + //write_log_header(); } //taken from Luftdaten.info @@ -97,7 +97,7 @@ String delayToString(unsigned time_ms) { } if (time_ms > 2 * 1000 * 60 * 60) { - sprintf_P(buf, PSTR("%d hours, "), time_ms / (1000 * 60 * 60)); + sprintf_P(buf, PSTR("%d h, "), time_ms / (1000 * 60 * 60)); s += buf; time_ms %= 1000 * 60 * 60; } @@ -109,7 +109,7 @@ String delayToString(unsigned time_ms) { } if (time_ms > 2 * 1000) { - sprintf_P(buf, PSTR("%ds, "), time_ms / 1000); + sprintf_P(buf, PSTR("%d s, "), time_ms / 1000); s += buf; } diff --git a/multigeiger/webconf.cpp b/multigeiger/webconf.cpp index 968e08e0..dd2c772b 100644 --- a/multigeiger/webconf.cpp +++ b/multigeiger/webconf.cpp @@ -341,12 +341,12 @@ void handleDebug(void){ lvl = server.arg("lvl").toInt(); setloglevel(lvl); }else { lvl=log_level;} - if (lvl == 5) strcpy(s,"DEBUG"); - else if (lvl == 4) strcpy(s,"INFO"); - else if (lvl == 3) strcpy(s,"WARNING"); - else if (lvl == 2) strcpy(s,"ERROR"); - else if (lvl == 1) strcpy(s,"CRITICAL"); - else strcpy(s,"NOLOG"); + if (lvl == 5) strcpy(s,"Debug"); + else if (lvl == 4) strcpy(s,"max.Info"); + else if (lvl == 3) strcpy(s,"med.Info"); + else if (lvl == 2) strcpy(s,"min.Info"); + else if (lvl == 1) strcpy(s,"Error"); + else strcpy(s,"NoLog"); page_content.replace("{lvl}", String(s)); diff --git a/multigeiger/webconf.h b/multigeiger/webconf.h index 97c881cb..314be4ea 100644 --- a/multigeiger/webconf.h +++ b/multigeiger/webconf.h @@ -81,10 +81,10 @@ const char WEB_PAGE_HEADLINE[] PROGMEM = "