diff --git a/README.md b/README.md index 9389e016950113580ab266e21e5178403af5d2ae..744c18fd15511019511627056c40b7f2b2ab7b31 100644 --- a/README.md +++ b/README.md @@ -69,19 +69,21 @@ make upload board=esp32 && make monitor # For ESP32 ## Authors - * Eric Duminil - * Robert Otto - * Myriam Guedey - * Tobias Gabriel Erhart - * Jonas Stave - -Hochschule für Technik Stuttgart + * Eric Duminil (HfT Stuttgart) + * Robert Otto (HfT Stuttgart) + * Myriam Guedey (HfT Stuttgart) + * Tobias Gabriel Erhart (HfT Stuttgart) + * Jonas Stave (HfT Stuttgart) + * Michael Käppler ## Contributing -Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. + +* Merge requests are welcome, and should be based on the `develop` branch. +* The `develop` branch gets merged into the `master` once it has been sufficiently tested. +* For major changes, please open an issue first to discuss what you would like to change. ## License -Copyright © 2020, [HfT Stuttgart](https://www.hft-stuttgart.de/) +Copyright © 2021, [HfT Stuttgart](https://www.hft-stuttgart.de/) [GPLv3](https://choosealicense.com/licenses/gpl-3.0/) diff --git a/ampel-firmware/ampel-firmware.h b/ampel-firmware/ampel-firmware.h index 5c47dec1cc21cbb38963d9f31fe2a08743ea851a..41fe46bd593aa82a1a3c133e08f1fabe88e7b490 100644 --- a/ampel-firmware/ampel-firmware.h +++ b/ampel-firmware/ampel-firmware.h @@ -20,6 +20,12 @@ # ifdef AMPEL_HTTP # include "web_server.h" # endif +# if defined(ESP8266) + //allows sensor to be seen as SENSOR_ID.local, from the local network. For example : espd03cc5.local +# include +# elif defined(ESP32) +# include +# endif #endif #ifdef AMPEL_LORAWAN @@ -27,17 +33,8 @@ #endif #include "util.h" +#include "sensor_console.h" #include "co2_sensor.h" #include "led_effects.h" -#if defined(ESP8266) -//allows sensor to be seen as SENSOR_ID.local, from the local network. For example : espd03cc5.local -# include -#elif defined(ESP32) -# include -#endif - -void keepServicesAlive(); -void checkFlashButton(); - #endif diff --git a/ampel-firmware/ampel-firmware.ino b/ampel-firmware/ampel-firmware.ino index c5e82c868a13ce2eca3fd5365e9364c903a50d52..8d04504665e014bd52e1d66f38c0e14864d2fc9e 100644 --- a/ampel-firmware/ampel-firmware.ino +++ b/ampel-firmware/ampel-firmware.ino @@ -43,6 +43,7 @@ * Myriam Guedey * Tobias Gabriel Erhart * Jonas Stave + * Michael Käppler */ /***************************************************************** @@ -55,6 +56,17 @@ * and define your credentials and parameters in 'config.h'. */ +/***************************************************************** + * PreInit * + *****************************************************************/ +void preinit() { +#if !defined(AMPEL_WIFI) && defined(ESP8266) + // WiFi would be initialized otherwise (on ESP8266), even if unused. + // see https://github.com/esp8266/Arduino/issues/2111#issuecomment-224251391 + ESP8266WiFiClass::preinitWiFiOff(); +#endif +} + /***************************************************************** * Setup * *****************************************************************/ @@ -71,12 +83,16 @@ void setup() { sensor::initialize(); Serial.print(F("Sensor ID: ")); - Serial.println(SENSOR_ID); + Serial.println(ampel.sensorId); Serial.print(F("Board : ")); - Serial.println(BOARD); + Serial.println(ampel.board); + +#ifdef AMPEL_CSV + csv_writer::initialize(ampel.sensorId); +#endif #ifdef AMPEL_WIFI - WiFiConnect(SENSOR_ID); + wifi::connect(ampel.sensorId); Serial.print(F("WiFi - Status: ")); Serial.println(WiFi.status()); @@ -88,7 +104,7 @@ void setup() { ntp::initialize(); - if (MDNS.begin(SENSOR_ID.c_str())) { // Start the mDNS responder for SENSOR_ID.local + if (MDNS.begin(ampel.sensorId)) { // Start the mDNS responder for SENSOR_ID.local MDNS.addService("http", "tcp", 80); Serial.println(F("mDNS responder started")); } else { @@ -96,20 +112,23 @@ void setup() { } # ifdef AMPEL_MQTT - mqtt::initialize("CO2sensors/" + SENSOR_ID); + mqtt::initialize(ampel.sensorId); # endif } #endif -#ifdef AMPEL_CSV - csv_writer::initialize(); -#endif - #if defined(AMPEL_LORAWAN) && defined(ESP32) lorawan::initialize(); #endif } +/***************************************************************** + * Helper functions * + *****************************************************************/ +void keepServicesAlive(); +void checkFlashButton(); +void checkSerialInput(); + /***************************************************************** * Main loop * *****************************************************************/ @@ -133,6 +152,8 @@ void loop() { // Short press for night mode, Long press for calibration. checkFlashButton(); + checkSerialInput(); + if (sensor::processData()) { #ifdef AMPEL_CSV csv_writer::logIfTimeHasCome(sensor::timestamp, sensor::co2, sensor::temperature, sensor::humidity); @@ -148,11 +169,17 @@ void loop() { } uint32_t duration = millis() - t0; - if (duration > max_loop_duration) { - max_loop_duration = duration; + if (duration > ampel.max_loop_duration) { + ampel.max_loop_duration = duration; Serial.print(F("Debug - Max loop duration : ")); - Serial.print(max_loop_duration); - Serial.println(" ms."); + Serial.print(ampel.max_loop_duration); + Serial.println(F(" ms.")); + } +} + +void checkSerialInput() { + while (Serial.available() > 0) { + sensor_console::processSerialInput(Serial.read()); } } diff --git a/ampel-firmware/co2_sensor.cpp b/ampel-firmware/co2_sensor.cpp index 7110776f7ab9362e431917afa8ebc84cd8bcb653..5dff6765fb7c4a6b19d9dc631af15c3474d9840b 100644 --- a/ampel-firmware/co2_sensor.cpp +++ b/ampel-firmware/co2_sensor.cpp @@ -5,6 +5,8 @@ namespace config { uint16_t measurement_timestep = MEASUREMENT_TIMESTEP; // [s] Value between 2 and 1800 (range for SCD30 sensor) const uint16_t altitude_above_sea_level = ALTITUDE_ABOVE_SEA_LEVEL; // [m] uint16_t co2_calibration_level = ATMOSPHERIC_CO2_CONCENTRATION; // [ppm] + int8_t max_deviation_during_calibration = 30; // [ppm] + int8_t enough_stable_measurements = 60; #ifdef TEMPERATURE_OFFSET // Residual heat from CO2 sensor seems to be high enough to change the temperature reading. How much should it be offset? // NOTE: Sign isn't relevant. The returned temperature will always be shifted down. @@ -12,15 +14,15 @@ namespace config { #else const float temperature_offset = -3.0; // [K] Temperature measured by sensor is usually at least 3K too high. #endif - const bool auto_calibrate_sensor = AUTO_CALIBRATE_SENSOR; // [true / false] + bool auto_calibrate_sensor = AUTO_CALIBRATE_SENSOR; // [true / false] } namespace sensor { SCD30 scd30; - int16_t co2 = 0; + uint16_t co2 = 0; float temperature = 0; float humidity = 0; - String timestamp = ""; + char timestamp[23]; int16_t stable_measurements = 0; uint32_t waiting_color = color::blue; bool should_calibrate = false; @@ -42,10 +44,9 @@ namespace sensor { // CO2 if (scd30.begin(config::auto_calibrate_sensor) == false) { - Serial.println("Air sensor not detected. Please check wiring. Freezing..."); - while (1) { - led_effects::showWaitingLED(color::red); - } + Serial.println(F("ERROR - CO2 sensor not detected. Please check wiring!")); + led_effects::showKITTWheel(color::red, 30); + ESP.restart(); } // SCD30 has its own timer. @@ -68,13 +69,23 @@ namespace sensor { Serial.print(F("Auto-calibration is ")); Serial.println(config::auto_calibrate_sensor ? "ON." : "OFF."); + + sensor_console::defineIntCommand("co2", setCO2forDebugging, F(" 1500 (Sets co2 level, for debugging purposes)")); + sensor_console::defineIntCommand("timer", setTimer, F(" 30 (Sets measurement interval, in s)")); + sensor_console::defineCommand("calibrate", startCalibrationProcess, F(" (Starts calibration process)")); + sensor_console::defineIntCommand("calibrate", calibrateSensorToSpecificPPM, + F(" 600 (Starts calibration process, to given ppm)")); + sensor_console::defineIntCommand("calibrate!", calibrateSensorRightNow, + F(" 600 (Calibrates right now, to given ppm)")); + sensor_console::defineIntCommand("auto_calibrate", setAutoCalibration, + F(" 0/1 (Disables/enables autocalibration)")); } //NOTE: should timer deviation be used to adjust measurement_timestep? void checkTimerDeviation() { static int32_t previous_measurement_at = 0; int32_t now = millis(); - Serial.print("Measurement time offset : "); + Serial.print(F("Measurement time offset : ")); Serial.print(now - previous_measurement_at - config::measurement_timestep * 1000); Serial.println(" ms."); previous_measurement_at = now; @@ -82,7 +93,8 @@ namespace sensor { void countStableMeasurements() { static int16_t previous_co2 = 0; - if (co2 > (previous_co2 - 30) && co2 < (previous_co2 + 30)) { + if (co2 > (previous_co2 - config::max_deviation_during_calibration) + && co2 < (previous_co2 + config::max_deviation_during_calibration)) { stable_measurements++; Serial.print(F("Number of stable measurements : ")); Serial.println(stable_measurements); @@ -133,11 +145,12 @@ namespace sensor { } /** * Display data, even if it's "old" (with breathing). - * Those effects include a short delay. + * A short delay is required in order to let background tasks run on the ESP8266. + * see https://github.com/esp8266/Arduino/issues/3241#issuecomment-301290392 */ if (co2 < 2000) { led_effects::displayCO2color(co2); - led_effects::breathe(co2); + delay(100); } else { // >= 2000: entire ring blinks red led_effects::redAlert(); @@ -152,7 +165,7 @@ namespace sensor { if (freshData) { // checkTimerDeviation(); - timestamp = ntp::getLocalTime(); + ntp::getLocalTime(timestamp); co2 = scd30.getCO2(); temperature = scd30.getTemperature(); humidity = scd30.getHumidity(); @@ -176,7 +189,7 @@ namespace sensor { } if (should_calibrate) { - if (stable_measurements == 60) { + if (stable_measurements == config::enough_stable_measurements) { calibrateAndRestart(); } led_effects::showWaitingLED(waiting_color); @@ -186,4 +199,46 @@ namespace sensor { displayCO2OnLedRing(); return freshData; } + + /***************************************************************** + * Callbacks for sensor commands * + *****************************************************************/ + void setCO2forDebugging(int32_t fakeCo2) { + Serial.print(F("DEBUG. Setting CO2 to ")); + co2 = fakeCo2; + Serial.println(co2); + } + + void setAutoCalibration(int32_t autoCalibration) { + config::auto_calibrate_sensor = autoCalibration; + scd30.setAutoSelfCalibration(autoCalibration); + Serial.print(F("Setting auto-calibration to : ")); + Serial.println(autoCalibration ? F("On.") : F("Off.")); + } + + void setTimer(int32_t timestep) { + if (timestep >= 2 && timestep <= 1800) { + Serial.print(F("Setting Measurement Interval to : ")); + Serial.print(timestep); + Serial.println("s."); + sensor::scd30.setMeasurementInterval(timestep); + config::measurement_timestep = timestep; + led_effects::showKITTWheel(color::green, 1); + } + } + + void calibrateSensorToSpecificPPM(int32_t calibrationLevel) { + if (calibrationLevel >= 400 && calibrationLevel <= 2000) { + Serial.print(F("Force calibration, at ")); + config::co2_calibration_level = calibrationLevel; + Serial.print(config::co2_calibration_level); + Serial.println(" ppm."); + sensor::startCalibrationProcess(); + } + } + + void calibrateSensorRightNow(int32_t calibrationLevel) { + stable_measurements = config::enough_stable_measurements; + calibrateSensorToSpecificPPM(calibrationLevel); + } } diff --git a/ampel-firmware/co2_sensor.h b/ampel-firmware/co2_sensor.h index f11eb815582358a2732b34ec4afe790773683c0d..318b9596ae50ce15476366c3481d45e238895291 100644 --- a/ampel-firmware/co2_sensor.h +++ b/ampel-firmware/co2_sensor.h @@ -7,24 +7,31 @@ #include "config.h" #include "led_effects.h" #include "util.h" +#include "sensor_console.h" #include namespace config { extern uint16_t measurement_timestep; // [s] Value between 2 and 1800 (range for SCD30 sensor) - extern const bool auto_calibrate_sensor; // [true / false] + extern bool auto_calibrate_sensor; // [true / false] extern uint16_t co2_calibration_level; // [ppm] extern const float temperature_offset; // [K] Sign isn't relevant. } namespace sensor { extern SCD30 scd30; - extern int16_t co2; + extern uint16_t co2; extern float temperature; extern float humidity; - extern String timestamp; + extern char timestamp[]; void initialize(); bool processData(); void startCalibrationProcess(); + + void setCO2forDebugging(int32_t fakeCo2); + void setTimer(int32_t timestep); + void calibrateSensorToSpecificPPM(int32_t calibrationLevel); + void calibrateSensorRightNow(int32_t calibrationLevel); + void setAutoCalibration(int32_t autoCalibration); } #endif diff --git a/ampel-firmware/config.public.h b/ampel-firmware/config.public.h index 00339350cf751165bbd1c1ac46f78ef859e46ed5..1ca67666cb3f76cb20ca1a9d03dc1b38d6bc3558 100644 --- a/ampel-firmware/config.public.h +++ b/ampel-firmware/config.public.h @@ -59,9 +59,10 @@ */ // LED brightness, which can vary between min and max brightness ("LED breathing") -// max_brightness should be between 0 and 255. -// min_brightness should be between 0 and max_brightness +// MAX_BRIGHTNESS must be defined, and should be between 0 and 255. # define MAX_BRIGHTNESS 255 +// MIN_BRIGHTNESS, if defined, should be between 0 and MAX_BRIGHTNESS - 1 +// If MIN_BRIGHTNESS is not set, or if it is set to MAX_BRIGHTNESS, breathing is disabled. # define MIN_BRIGHTNESS 60 /** @@ -143,7 +144,7 @@ */ # define NTP_SERVER "pool.ntp.org" -# define UTC_OFFSET_IN_SECONDS 3600 // [s] 3600 for UTC+1 +# define UTC_OFFSET_IN_SECONDS 7200 // [s] 3600 for UTC+1, 7200 for UTC+1 and daylight saving time /** * Others diff --git a/ampel-firmware/csv_writer.cpp b/ampel-firmware/csv_writer.cpp index 653630335527800481fb4907025e494e2de3fd58..7e30d44da9531d5a4022ce3aec2a3a5a9d51c987 100644 --- a/ampel-firmware/csv_writer.cpp +++ b/ampel-firmware/csv_writer.cpp @@ -1,14 +1,12 @@ #include "csv_writer.h" -//TODO: Allow CSV download via USB Serial, when requested (e.g. via a Python script) - namespace config { // Values should be defined in config.h uint16_t csv_interval = CSV_INTERVAL; // [s] } namespace csv_writer { unsigned long last_written_at = 0; - String last_successful_write = ""; + char last_successful_write[23]; #if defined(ESP8266) /** @@ -80,13 +78,15 @@ namespace csv_writer { } #endif - const String filename = "/" + SENSOR_ID + ".csv"; + char filename[15]; // "/ESPxxxxxx.csv\0" int getAvailableSpace() { return getTotalSpace() - getUsedSpace(); } - void initialize() { + void initialize(const char *sensorId) { + snprintf(filename, sizeof(filename), "/%s.csv", sensorId); + Serial.print(F("Initializing FS...")); if (mountFS()) { Serial.println(F("done.")); @@ -113,9 +113,13 @@ namespace csv_writer { Serial.println(); // Open dir folder - Serial.println("Filesystem content:"); + Serial.println(F("Filesystem content:")); showFilesystemContent(); Serial.println(); + + sensor_console::defineIntCommand("csv", setCSVinterval, F(" 60 (Sets CSV writing interval, in s)")); + sensor_console::defineCommand("format_filesystem", formatFilesystem, F(" (Deletes the whole filesystem)")); + sensor_console::defineCommand("show_csv", showCSVContent, F(" (Displays the complete CSV file on Serial)")); } File openOrCreate() { @@ -130,11 +134,11 @@ namespace csv_writer { return csv_file; } - void log(const String &timeStamp, const int16_t &co2, const float &temperature, const float &humidity) { + void log(const char *timestamp, const int16_t &co2, const float &temperature, const float &humidity) { led_effects::onBoardLEDOn(); File csv_file = openOrCreate(); char csv_line[42]; - snprintf(csv_line, sizeof(csv_line), "%s;%d;%.1f;%.1f\r\n", timeStamp.c_str(), co2, temperature, humidity); + snprintf(csv_line, sizeof(csv_line), "%s;%d;%.1f;%.1f\r\n", timestamp, co2, temperature, humidity); if (csv_file) { size_t written_bytes = csv_file.print(csv_line); csv_file.close(); @@ -143,7 +147,7 @@ namespace csv_writer { } else { Serial.print(F("CSV - Wrote : ")); Serial.print(csv_line); - last_successful_write = ntp::getLocalTime(); + ntp::getLocalTime(last_successful_write); } updateFsInfo(); delay(50); @@ -154,11 +158,42 @@ namespace csv_writer { led_effects::onBoardLEDOff(); } - void logIfTimeHasCome(const String &timeStamp, const int16_t &co2, const float &temperature, const float &humidity) { + void logIfTimeHasCome(const char *timeStamp, const int16_t &co2, const float &temperature, const float &humidity) { unsigned long now = seconds(); if (now - last_written_at > config::csv_interval) { last_written_at = now; log(timeStamp, co2, temperature, humidity); } } + + /***************************************************************** + * Callbacks for sensor commands * + *****************************************************************/ + void setCSVinterval(int32_t csv_interval) { + config::csv_interval = csv_interval; + Serial.print(F("Setting CSV Interval to : ")); + Serial.print(config::csv_interval); + Serial.println("s."); + led_effects::showKITTWheel(color::green, 1); + } + + void showCSVContent() { + Serial.print(F("### ")); + Serial.print(filename); + Serial.println(F(" ###")); + File csv_file; + if (FS_LIB.exists(filename)) { + csv_file = FS_LIB.open(filename, "r"); + while (csv_file.available()) { + Serial.write(csv_file.read()); + } + csv_file.close(); + } + Serial.println(F("######################")); + } + + void formatFilesystem() { + FS_LIB.format(); + led_effects::showKITTWheel(color::blue, 2); + } } diff --git a/ampel-firmware/csv_writer.h b/ampel-firmware/csv_writer.h index e0e0d5314ae9060ca8c39315737da2a73beb5d99..f85f34641aaf4f1915e773ad7c1675f846ecfa90 100644 --- a/ampel-firmware/csv_writer.h +++ b/ampel-firmware/csv_writer.h @@ -14,16 +14,21 @@ #include "config.h" #include "util.h" #include "led_effects.h" +#include "sensor_console.h" namespace config { extern uint16_t csv_interval; // [s] } namespace csv_writer { - extern String last_successful_write; - void initialize(); - void logIfTimeHasCome(const String &timeStamp, const int16_t &co2, const float &temperature, const float &humidity); + extern char last_successful_write[]; + void initialize(const char *sensorId); + void logIfTimeHasCome(const char *timestamp, const int16_t &co2, const float &temperature, const float &humidity); int getAvailableSpace(); - extern const String filename; + extern char filename[]; + + void setCSVinterval(int32_t csv_interval); + void showCSVContent(); + void formatFilesystem(); } #endif diff --git a/ampel-firmware/led_effects.cpp b/ampel-firmware/led_effects.cpp index f654e77e581ec88bfce7c3dd8be9b648740e9583..ee0fccde5222ecf935dc444d86ed8663d5291a77 100644 --- a/ampel-firmware/led_effects.cpp +++ b/ampel-firmware/led_effects.cpp @@ -4,16 +4,15 @@ *****************************************************************/ namespace config { const uint8_t max_brightness = MAX_BRIGHTNESS; +#if defined(MIN_BRIGHTNESS) const uint8_t min_brightness = MIN_BRIGHTNESS; - const int kitt_tail = 3; // How many dimmer LEDs follow in K.I.T.T. wheel -} - -/***************************************************************** - * Configuration (calculated from above values) * - *****************************************************************/ -namespace config //NOTE: Use a class instead? NightMode could then be another state. -{ +#else + const uint8_t min_brightness = MAX_BRIGHTNESS; +#endif const uint8_t brightness_amplitude = config::max_brightness - config::min_brightness; + const int kitt_tail = 3; // How many dimmer LEDs follow in K.I.T.T. wheel + const uint16_t poor_air_quality_ppm = 1600; // Above this threshold, LED breathing effect is faster. + //NOTE: Use a class instead? NightMode could then be another state. bool night_mode = false; } @@ -75,6 +74,7 @@ namespace led_effects { pixels.begin(); pixels.setBrightness(config::max_brightness); LEDsOff(); + sensor_console::defineCommand("night_mode", toggleNightMode, F(" (Toggles night mode on/off)")); } void toggleNightMode() { @@ -131,6 +131,17 @@ namespace led_effects { } } + /** + * If enabled, slowly varies the brightness between MAX_BRIGHTNESS & MIN_BRIGHTNESS. + */ + void breathe(int16_t co2) { + static uint8_t breathing_offset = 0; + uint16_t brightness = config::min_brightness + pixels.sine8(breathing_offset) * config::brightness_amplitude / 255; + pixels.setBrightness(brightness); + pixels.show(); + breathing_offset += co2 > config::poor_air_quality_ppm ? 6 : 3; // breathing speed. +3 looks like slow human breathing. + } + /** * Fills the whole ring with green, yellow, orange or black, depending on co2 input and CO2_TICKS. */ @@ -144,6 +155,9 @@ namespace led_effects { pixels.setPixelColor(ledId, pixels.ColorHSV(LED_HUES[ledId], 255, brightness)); } pixels.show(); + if (config::brightness_amplitude > 0) { + breathe(co2); + } } void showRainbowWheel(uint16_t duration_ms, uint16_t hue_increment) { @@ -179,18 +193,6 @@ namespace led_effects { } } - void breathe(int16_t co2) { - if (!config::night_mode) { - static uint16_t breathing_offset = 0; - uint16_t brightness = config::min_brightness - + pixels.sine8(breathing_offset) * config::brightness_amplitude / 255; - pixels.setBrightness(brightness); - pixels.show(); - breathing_offset += 3; // breathing speed. +3 looks like slow human breathing. - } - delay(co2 > 1600 ? 50 : 100); // faster breathing for higher CO2 values - } - /** * Displays a complete blue circle, and starts removing LEDs one by one. Returns the number of remaining LEDs. * Can be used for calibration, e.g. when countdown is 0. Does not work in night mode. diff --git a/ampel-firmware/led_effects.h b/ampel-firmware/led_effects.h index d34a9bfb7d42eeca5fd8cf0ca55b037dec604482..96367ef5f2909d38377e2441f91d4e2e1abe2198 100644 --- a/ampel-firmware/led_effects.h +++ b/ampel-firmware/led_effects.h @@ -2,6 +2,7 @@ #define LED_EFFECTS_H_INCLUDED #include #include "config.h" +#include "sensor_console.h" // Adafruit NeoPixel (Arduino library for controlling single-wire-based LED pixels and strip) // https://github.com/adafruit/Adafruit_NeoPixel @@ -25,7 +26,6 @@ namespace led_effects { void setupRing(); void redAlert(); - void breathe(int16_t co2); int countdownToZero(); void showWaitingLED(uint32_t color); void showKITTWheel(uint32_t color, uint16_t duration_s = 2); diff --git a/ampel-firmware/lorawan.cpp b/ampel-firmware/lorawan.cpp index 7e027703ebd6505577005de734c3272beb785789..784b46fb61b25bc8d3868a3a541baa14f241150a 100644 --- a/ampel-firmware/lorawan.cpp +++ b/ampel-firmware/lorawan.cpp @@ -33,7 +33,7 @@ void os_getDevKey(u1_t *buf) { namespace lorawan { bool waiting_for_confirmation = false; bool connected = false; - String last_transmission = ""; + char last_transmission[23] = ""; void initialize() { Serial.println(F("Starting LoRaWAN. Frequency plan : " LMIC_FREQUENCY_PLAN " MHz.")); @@ -47,11 +47,12 @@ namespace lorawan { LMIC_reset(); // Join, but don't send anything yet. LMIC_startJoining(); + sensor_console::defineIntCommand("lora", setLoRaInterval, F(" 300 (Sets LoRaWAN sending interval, in s)")); } // Checks if OTAA is connected, or if payload should be sent. // NOTE: while a transaction is in process (i.e. until the TXcomplete event has been received, no blocking code (e.g. delay loops etc.) are allowed, otherwise the LMIC/OS code might miss the event. - // If this rule is not followed, a typical symptom is that the first send is ok and all following ones end with the TX not complete failure. + // If this rule is not followed, a typical symptom is that the first send is ok and all following ones end with the 'TX not complete' failure. void process() { os_runloop_once(); } @@ -64,8 +65,10 @@ namespace lorawan { } void onEvent(ev_t ev) { + char current_time[23]; + ntp::getLocalTime(current_time); Serial.print("LoRa - "); - Serial.print(ntp::getLocalTime()); + Serial.print(current_time); Serial.print(" - "); switch (ev) { case EV_JOINING: @@ -112,7 +115,7 @@ namespace lorawan { Serial.println(F("EV_REJOIN_FAILED")); break; case EV_TXCOMPLETE: - last_transmission = ntp::getLocalTime(); + ntp::getLocalTime(last_transmission); Serial.println(F("EV_TXCOMPLETE")); break; case EV_TXSTART: @@ -189,6 +192,17 @@ namespace lorawan { preparePayload(co2, temperature, humidity); } } + + /***************************************************************** + * Callbacks for sensor commands * + *****************************************************************/ + void setLoRaInterval(int32_t sending_interval) { + config::lorawan_sending_interval = sending_interval; + Serial.print(F("Setting LoRa sending interval to : ")); + Serial.print(config::lorawan_sending_interval); + Serial.println("s."); + led_effects::showKITTWheel(color::green, 1); + } } void onEvent(ev_t ev) { diff --git a/ampel-firmware/lorawan.h b/ampel-firmware/lorawan.h index a4e29ab2c89019202aceddb920c26f49978b085b..45f462b2ef761066797ccd2ff114762088b5c628 100644 --- a/ampel-firmware/lorawan.h +++ b/ampel-firmware/lorawan.h @@ -13,7 +13,7 @@ #include #include "led_effects.h" - +#include "sensor_console.h" #include "util.h" namespace config { @@ -39,10 +39,12 @@ namespace config { namespace lorawan { extern bool waiting_for_confirmation; extern bool connected; - extern String last_transmission; + extern char last_transmission[]; void initialize(); void process(); void preparePayloadIfTimeHasCome(const int16_t &co2, const float &temp, const float &hum); + + void setLoRaInterval(int32_t sending_interval); } #endif diff --git a/ampel-firmware/mqtt.cpp b/ampel-firmware/mqtt.cpp index 2e71ae2417fefb24f52761072d9ba20fb3c2d423..22a0e2def350a38b77336b954395ae5d55005a8e 100644 --- a/ampel-firmware/mqtt.cpp +++ b/ampel-firmware/mqtt.cpp @@ -2,7 +2,7 @@ namespace config { // Values should be defined in config.h - uint16_t sending_interval = MQTT_SENDING_INTERVAL; // [s] + uint16_t mqtt_sending_interval = MQTT_SENDING_INTERVAL; // [s] //INFO: Listen to every CO2 sensor which is connected to the server: // mosquitto_sub -h MQTT_SERVER -t 'CO2sensors/#' -p 443 --capath /etc/ssl/certs/ -u "MQTT_USER" -P "MQTT_PASSWORD" -v const char *mqtt_server = MQTT_SERVER; @@ -23,31 +23,35 @@ namespace mqtt { unsigned long last_failed_at = 0; bool connected = false; - String publish_topic; + char publish_topic[21]; // e.g. "CO2sensors/ESPxxxxxx\0" const char *json_sensor_format; - String last_successful_publish = ""; + char last_successful_publish[23] = ""; - void initialize(String &topic) { + void initialize(const char *sensorId) { json_sensor_format = PSTR("{\"time\":\"%s\", \"co2\":%d, \"temp\":%.1f, \"rh\":%.1f}"); - publish_topic = topic; + snprintf(publish_topic, sizeof(publish_topic), "CO2sensors/%s", sensorId); #if defined(ESP8266) espClient.setInsecure(); // Sorry, we don't want to flash the sensors every 3 months. #endif // mqttClient.setSocketTimeout(config::mqtt_timeout); //NOTE: somehow doesn't seem to have any effect on connect() mqttClient.setServer(config::mqtt_server, config::mqtt_port); + + sensor_console::defineIntCommand("mqtt", setMQTTinterval, F(" 60 (Sets MQTT sending interval, in s)")); + sensor_console::defineCommand("local_ip", sendInfoAboutLocalNetwork, + F(" (Sends local IP and SSID via MQTT. Can be useful to find sensor)")); } - void publish(const String ×tamp, int16_t co2, float temperature, float humidity) { + void publish(const char *timestamp, int16_t co2, float temperature, float humidity) { if (WiFi.status() == WL_CONNECTED && mqttClient.connected()) { led_effects::onBoardLEDOn(); Serial.print(F("MQTT - Publishing message ... ")); char payload[75]; // Should be enough for json... - snprintf(payload, sizeof(payload), json_sensor_format, timestamp.c_str(), co2, temperature, humidity); + snprintf(payload, sizeof(payload), json_sensor_format, timestamp, co2, temperature, humidity); // Topic is the same as clientID. e.g. 'CO2sensors/ESP3d03da' - if (mqttClient.publish(publish_topic.c_str(), payload)) { + if (mqttClient.publish(publish_topic, payload)) { Serial.println(F("OK")); - last_successful_publish = ntp::getLocalTime(); + ntp::getLocalTime(last_successful_publish); } else { Serial.println(F("Failed.")); } @@ -55,69 +59,6 @@ namespace mqtt { } } - void setTimer(String messageString) { - messageString.replace("timer ", ""); - int timestep = messageString.toInt(); - if (timestep >= 2 && timestep <= 1800) { - Serial.print(F("Setting Measurement Interval to : ")); - Serial.print(timestep); - Serial.println("s."); - sensor::scd30.setMeasurementInterval(messageString.toInt()); - config::measurement_timestep = messageString.toInt(); - led_effects::showKITTWheel(color::green, 1); - } - } - - void setMQTTinterval(String messageString) { - messageString.replace("mqtt ", ""); - config::sending_interval = messageString.toInt(); - Serial.print(F("Setting Sending Interval to : ")); - Serial.print(config::sending_interval); - Serial.println("s."); - led_effects::showKITTWheel(color::green, 1); - } - -#ifdef AMPEL_CSV - void setCSVinterval(String messageString) { - messageString.replace("csv ", ""); - config::csv_interval = messageString.toInt(); - Serial.print(F("Setting CSV Interval to : ")); - Serial.print(config::csv_interval); - Serial.println("s."); - led_effects::showKITTWheel(color::green, 1); - } -#endif - - void calibrateSensorToSpecificPPM(String messageString) { - messageString.replace("calibrate ", ""); - long int calibrationLevel = messageString.toInt(); - if (calibrationLevel >= 400 && calibrationLevel <= 2000) { - Serial.print(F("Force calibration, at ")); - config::co2_calibration_level = messageString.toInt(); - Serial.print(config::co2_calibration_level); - Serial.println(" ppm."); - sensor::startCalibrationProcess(); - } - } - - void setCO2forDebugging(String messageString) { - Serial.print(F("DEBUG. Setting CO2 to ")); - messageString.replace("co2 ", ""); - sensor::co2 = messageString.toInt(); - Serial.println(sensor::co2); - } - - void sendInfoAboutLocalNetwork() { - char info_topic[60]; // Should be enough for "CO2sensors/ESPd03cc5/info" - snprintf(info_topic, sizeof(info_topic), "%s/info", publish_topic.c_str()); - - char payload[75]; // Should be enough for info json... - const char *json_info_format = PSTR("{\"local_ip\":\"%s\", \"ssid\":\"%s\"}"); - snprintf(payload, sizeof(payload), json_info_format, WiFi.localIP().toString().c_str(), WiFi.SSID().c_str()); - - mqttClient.publish(info_topic, payload); - } - /** * Allows sensor to be controlled by commands over MQTT * @@ -132,46 +73,13 @@ namespace mqtt { } led_effects::onBoardLEDOn(); Serial.print(F("Message arrived on topic: ")); - Serial.print(sub_topic); - Serial.print(F(". Message: '")); - String messageString; + Serial.println(sub_topic); + char command[length + 1]; for (unsigned int i = 0; i < length; i++) { - Serial.print((char) message[i]); - messageString += (char) message[i]; + command[i] = message[i]; } - Serial.println("'."); - - if (messageString.startsWith("co2 ")) { - setCO2forDebugging(messageString); - } else if (messageString.startsWith("timer ")) { - setTimer(messageString); - } else if (messageString == "calibrate") { - sensor::startCalibrationProcess(); - } else if (messageString.startsWith("calibrate ")) { - calibrateSensorToSpecificPPM(messageString); - } else if (messageString.startsWith("mqtt ")) { - setMQTTinterval(messageString); - } else if (messageString == "publish") { - Serial.println(F("Forcing MQTT publish now.")); - publish(sensor::timestamp, sensor::co2, sensor::temperature, sensor::humidity); -#ifdef AMPEL_CSV - } else if (messageString.startsWith("csv ")) { - setCSVinterval(messageString); - } else if (messageString == "format_filesystem") { - FS_LIB.format(); - led_effects::showKITTWheel(color::blue, 2); -#endif - } else if (messageString == "night_mode") { - led_effects::toggleNightMode(); - } else if (messageString == "local_ip") { - sendInfoAboutLocalNetwork(); - } else if (messageString == "reset") { - ESP.restart(); // softer than ESP.reset() - } else { - led_effects::showKITTWheel(color::red, 1); - Serial.println(F("Message not supported. Doing nothing.")); - } - delay(50); + command[length] = 0; + sensor_console::runCommand(command); led_effects::onBoardLEDOff(); } @@ -188,7 +96,7 @@ namespace mqtt { led_effects::onBoardLEDOn(); // Wait for connection, at most 15s (default) - mqttClient.connect(publish_topic.c_str(), config::mqtt_user, config::mqtt_password); + mqttClient.connect(publish_topic, config::mqtt_user, config::mqtt_password); led_effects::onBoardLEDOff(); connected = mqttClient.connected(); @@ -196,7 +104,7 @@ namespace mqtt { if (connected) { if (config::allow_mqtt_commands) { char control_topic[60]; // Should be enough for "CO2sensors/ESPd03cc5/control" - snprintf(control_topic, sizeof(control_topic), "%s/control", publish_topic.c_str()); + snprintf(control_topic, sizeof(control_topic), "%s/control", publish_topic); mqttClient.subscribe(control_topic); mqttClient.setCallback(controlSensorCallback); } @@ -212,12 +120,12 @@ namespace mqtt { } } - void publishIfTimeHasCome(const String &timeStamp, const int16_t &co2, const float &temp, const float &hum) { + void publishIfTimeHasCome(const char *timestamp, const int16_t &co2, const float &temp, const float &hum) { // Send message via MQTT according to sending interval unsigned long now = seconds(); - if (now - last_sent_at > config::sending_interval) { + if (now - last_sent_at > config::mqtt_sending_interval) { last_sent_at = now; - publish(timeStamp, co2, temp, hum); + publish(timestamp, co2, temp, hum); } } @@ -229,4 +137,28 @@ namespace mqtt { mqttClient.loop(); } + /***************************************************************** + * Callbacks for sensor commands * + *****************************************************************/ + void setMQTTinterval(int32_t sending_interval) { + config::mqtt_sending_interval = sending_interval; + Serial.print(F("Setting MQTT sending interval to : ")); + Serial.print(config::mqtt_sending_interval); + Serial.println("s."); + led_effects::showKITTWheel(color::green, 1); + } + + // It can be hard to find the local IP of a sensor if it isn't connected to Serial port, and if mDNS is disabled. + // If the sensor can be reach by MQTT, it can answer with info about local_ip and ssid. + // The sensor will send the info to "CO2sensors/ESP123456/info". + void sendInfoAboutLocalNetwork() { + char info_topic[60]; // Should be enough for "CO2sensors/ESP123456/info" + snprintf(info_topic, sizeof(info_topic), "%s/info", publish_topic); + + char payload[75]; // Should be enough for info json... + const char *json_info_format = PSTR("{\"local_ip\":\"%s\", \"ssid\":\"%s\"}"); + snprintf(payload, sizeof(payload), json_info_format, wifi::local_ip, WIFI_SSID); + + mqttClient.publish(info_topic, payload); + } } diff --git a/ampel-firmware/mqtt.h b/ampel-firmware/mqtt.h index 2899a9996bbf8d2a484f5fc79b619d9647ee70a7..4d1088345f061646e9c05fb95e378fbe7feec252 100644 --- a/ampel-firmware/mqtt.h +++ b/ampel-firmware/mqtt.h @@ -4,20 +4,22 @@ #include #include "config.h" #include "led_effects.h" -#ifdef AMPEL_CSV -# include "csv_writer.h" -#endif -#include "co2_sensor.h" +#include "sensor_console.h" #include "src/lib/PubSubClient/src/PubSubClient.h" #include "wifi_util.h" + namespace config { - extern uint16_t sending_interval; // [s] + extern uint16_t mqtt_sending_interval; // [s] } + namespace mqtt { - extern String last_successful_publish; + extern char last_successful_publish[]; extern bool connected; - void initialize(String &topic); + void initialize(const char *sensorId); void keepConnection(); - void publishIfTimeHasCome(const String &timeStamp, const int16_t &co2, const float &temp, const float &hum); + void publishIfTimeHasCome(const char *timestamp, const int16_t &co2, const float &temp, const float &hum); + + void setMQTTinterval(int32_t sending_interval); + void sendInfoAboutLocalNetwork(); } #endif diff --git a/ampel-firmware/sensor_console.cpp b/ampel-firmware/sensor_console.cpp new file mode 100644 index 0000000000000000000000000000000000000000..5f1876ddda8f2405b4f1a2eea253408123408824 --- /dev/null +++ b/ampel-firmware/sensor_console.cpp @@ -0,0 +1,161 @@ +#include "sensor_console.h" + +namespace sensor_console { + const uint8_t MAX_COMMANDS = 20; + const uint8_t MAX_COMMAND_SIZE = 30; + + uint8_t commands_count = 0; + + struct Command { + const char *name; + union { + void (*intFunction)(int32_t); + void (*voidFunction)(void); + }; + const char *doc; + bool has_parameter; + }; + + Command commands[MAX_COMMANDS]; + + //NOTE: Probably possible to DRY (with templates?) + void defineCommand(const char *name, void (*function)(void), const __FlashStringHelper *doc_fstring) { + const char *doc = (const char*) doc_fstring; + if (commands_count < MAX_COMMANDS) { + commands[commands_count].name = name; + commands[commands_count].voidFunction = function; + commands[commands_count].doc = doc; + commands[commands_count].has_parameter = false; + commands_count++; + } else { + Serial.println(F("Too many commands have been defined.")); + } + } + + void defineIntCommand(const char *name, void (*function)(int32_t), const __FlashStringHelper *doc_fstring) { + const char *doc = (const char*) doc_fstring; + if (commands_count < MAX_COMMANDS) { + commands[commands_count].name = name; + commands[commands_count].intFunction = function; + commands[commands_count].doc = doc; + commands[commands_count].has_parameter = true; + commands_count++; + } else { + Serial.println(F("Too many commands have been defined.")); + } + } + + /* + * Tries to split a string command (e.g. 'mqtt 60' or 'show_csv') into a function_name and an argument. + * Returns 0 if both are found, 1 if there is a problem and 2 if no argument is found. + */ + uint8_t parseCommand(const char *command, char *function_name, int32_t &argument) { + char split_command[MAX_COMMAND_SIZE]; + strlcpy(split_command, command, MAX_COMMAND_SIZE); + char *arg; + char *part1; + part1 = strtok(split_command, " "); + if (!part1) { + Serial.println(F("Received empty command")); + // Empty string + return 1; + } + strlcpy(function_name, part1, MAX_COMMAND_SIZE); + arg = strtok(NULL, " "); + uint8_t code = 0; + if (arg) { + char *end; + argument = strtol(arg, &end, 10); + if (*end) { + // Second argument isn't a number + code = 2; + } + } else { + // No argument + code = 2; + } + return code; + } + + /* + * Saves bytes from Serial.read() until enter is pressed, and tries to run the corresponding command. + * http://www.gammon.com.au/serial + */ + void processSerialInput(const byte input_byte) { + static char input_line[MAX_COMMAND_SIZE]; + static unsigned int input_pos = 0; + switch (input_byte) { + case '\n': // end of text + Serial.println(); + input_line[input_pos] = 0; + runCommand(input_line); + input_pos = 0; + break; + case '\r': // discard carriage return + break; + case '\b': // backspace + if (input_pos > 0) { + input_pos--; + Serial.print(F("\b \b")); + } + break; + default: + if (input_pos == 0) { + Serial.print(F("> ")); + } + // keep adding if not full ... allow for terminating null byte + if (input_pos < (MAX_COMMAND_SIZE - 1)) { + input_line[input_pos++] = input_byte; + Serial.print((char) input_byte); + } + break; + } + } + + int compareName(const void *s1, const void *s2) { + struct Command *c1 = (struct Command*) s1; + struct Command *c2 = (struct Command*) s2; + return strcmp(c1->name, c2->name); + } + + void listAvailableCommands() { + qsort(commands, commands_count, sizeof(commands[0]), compareName); + for (uint8_t i = 0; i < commands_count; i++) { + Serial.print(" "); + Serial.print(commands[i].name); + Serial.print(commands[i].doc); + Serial.println("."); + } + } + + /* + * Tries to find the corresponding callback for a given command. Name and number of argument should fit. + */ + void runCommand(const char *command) { + char function_name[MAX_COMMAND_SIZE]; + int32_t argument = 0; + bool has_argument; + has_argument = (parseCommand(command, function_name, argument) == 0); + + for (uint8_t i = 0; i < commands_count; i++) { + if (!strcmp(function_name, commands[i].name) && has_argument == commands[i].has_parameter) { + Serial.print(F("Calling : ")); + Serial.print(function_name); + if (has_argument) { + Serial.print(F("(")); + Serial.print(argument); + Serial.println(F(")")); + commands[i].intFunction(argument); + } else { + Serial.println(F("()")); + commands[i].voidFunction(); + } + return; + } + } + Serial.print(F("'")); + Serial.print(command); + Serial.println(F("' not supported. Available commands :")); + listAvailableCommands(); + } +} diff --git a/ampel-firmware/sensor_console.h b/ampel-firmware/sensor_console.h new file mode 100644 index 0000000000000000000000000000000000000000..b46e212e06c308e15ba7497509c33ffd104932ad --- /dev/null +++ b/ampel-firmware/sensor_console.h @@ -0,0 +1,18 @@ +#ifndef SENSOR_CONSOLE_H_INCLUDED +#define SENSOR_CONSOLE_H_INCLUDED +#include + +/** Other scripts can use this namespace, in order to define commands, via callbacks. + * Those callbacks can then be used to send commands to the sensor (reset, calibrate, night mode, ...) + * The callbacks can either have no parameter, or one int32_t parameter. + */ + +namespace sensor_console { + void defineCommand(const char *command, void (*function)(void), const __FlashStringHelper *ifsh); + void defineIntCommand(const char *command, void (*function)(int32_t), const __FlashStringHelper *ifsh); + + void processSerialInput(const byte in_byte); + void runCommand(const char *command); +} + +#endif diff --git a/ampel-firmware/src/lib/NTPClient-master/NTPClient.cpp b/ampel-firmware/src/lib/NTPClient-master/NTPClient.cpp index 048a82a4bcec1d3885070fbb83db764f60369080..1b52de5af6eefae690044fa9138222fb3aa78d1b 100644 --- a/ampel-firmware/src/lib/NTPClient-master/NTPClient.cpp +++ b/ampel-firmware/src/lib/NTPClient-master/NTPClient.cpp @@ -152,19 +152,17 @@ int NTPClient::getSeconds() { return (this->getEpochTime() % 60); } -String NTPClient::getFormattedTime(unsigned long secs) { +void NTPClient::getFormattedTime(char *formatted_time, unsigned long secs) { unsigned long rawTime = secs ? secs : this->getEpochTime(); unsigned int hours = (rawTime % 86400L) / 3600; unsigned int minutes = (rawTime % 3600) / 60; unsigned int seconds = rawTime % 60; - char formatted_time[9]; - snprintf(formatted_time, sizeof(formatted_time), "%02d:%02d:%02d", hours, minutes, seconds); - return String(formatted_time); + snprintf(formatted_time, 9, "%02d:%02d:%02d", hours, minutes, seconds); } // Based on https://github.com/PaulStoffregen/Time/blob/master/Time.cpp -String NTPClient::getFormattedDate(unsigned long secs) { +void NTPClient::getFormattedDate(char *formatted_date, unsigned long secs) { unsigned long rawTime = (secs ? secs : this->getEpochTime()) / 86400L; // in days unsigned long days = 0, year = 1970; uint8_t month; @@ -187,11 +185,9 @@ String NTPClient::getFormattedDate(unsigned long secs) { month++; // jan is month 1 rawTime++; // first day is day 1 - char formatted_date[23]; - snprintf(formatted_date, sizeof(formatted_date), "%4lu-%02d-%02lu %s%+03d", - year, month, rawTime, this->getFormattedTime(secs).c_str(), this->_timeOffset / 3600); - - return String(formatted_date); + char formatted_time[9]; + this->getFormattedTime(formatted_time, secs); + snprintf(formatted_date, 23, "%4lu-%02d-%02lu %s%+03d", year, month, rawTime, formatted_time, this->_timeOffset / 3600); } void NTPClient::end() { diff --git a/ampel-firmware/src/lib/NTPClient-master/NTPClient.h b/ampel-firmware/src/lib/NTPClient-master/NTPClient.h index ad450706490679ee8a459c5471efec167e419d21..3349dbb7df998fc931874c3f1ec86cda07fc3f80 100644 --- a/ampel-firmware/src/lib/NTPClient-master/NTPClient.h +++ b/ampel-firmware/src/lib/NTPClient-master/NTPClient.h @@ -80,7 +80,7 @@ class NTPClient { /** * @return secs argument (or 0 for current time) formatted like `hh:mm:ss` */ - String getFormattedTime(unsigned long secs = 0); + void getFormattedTime(char *formatted_time, unsigned long secs = 0); /** * @return time in seconds since Jan. 1, 1970 @@ -91,7 +91,7 @@ class NTPClient { * @return secs argument (or 0 for current date) formatted to ISO 8601 * like `2004-02-12T15:19:21+00:00` */ - String getFormattedDate(unsigned long secs = 0); + void getFormattedDate(char *formatted_date, unsigned long secs = 0); /** * Stops the underlying UDP client diff --git a/ampel-firmware/util.cpp b/ampel-firmware/util.cpp index 31df19dd856e5c3f17b5eb929979660eb27db715..e3b2d6e8913172cce6e4dd868aace8c03e8be02f 100644 --- a/ampel-firmware/util.cpp +++ b/ampel-firmware/util.cpp @@ -5,41 +5,80 @@ namespace config { const long utc_offset_in_seconds = UTC_OFFSET_IN_SECONDS; // UTC+1 } -// Get last 3 bytes of ESP MAC (worldwide unique) -String macToID() { - uint8_t mac[6]; - WiFi.macAddress(mac); - String result; - for (int i = 3; i < 6; i++) { - if (mac[i] < 16) - result += '0'; - result += String(mac[i], HEX); - } - result.toLowerCase(); - return result; -} +#if defined(ESP8266) +const char *current_board = "ESP8266"; +#elif defined(ESP32) +const char *current_board = "ESP32"; +#else +const char *current_board = "UNKNOWN"; +#endif //NOTE: ESP32 sometimes couldn't access the NTP server, and every loop would take +1000ms // ifdefs could be used to define functions specific to ESP32, e.g. with configTime namespace ntp { WiFiUDP ntpUDP; NTPClient timeClient(ntpUDP, config::ntp_server, config::utc_offset_in_seconds, 60000UL); + bool connected_at_least_once = false; void initialize() { timeClient.begin(); } void update() { - timeClient.update(); + connected_at_least_once |= timeClient.update(); } - String getLocalTime() { - return timeClient.getFormattedDate(); + void getLocalTime(char *timestamp) { + timeClient.getFormattedDate(timestamp); } + + void setLocalTime(int32_t unix_seconds) { + char time[23]; + timeClient.getFormattedDate(time); + Serial.print(F("Current time : ")); + Serial.println(time); + if (connected_at_least_once) { + Serial.println(F("NTP update already happened. Not changing anything.")); + return; + } + Serial.print(F("Setting UNIX time to : ")); + Serial.println(unix_seconds); + timeClient.setEpochTime(unix_seconds - seconds()); + timeClient.getFormattedDate(time); + Serial.print(F("Current time : ")); + Serial.println(time); + } +} + +void Ampel::showFreeSpace() { + Serial.print(F("Free heap space : ")); + Serial.print(ESP.getFreeHeap()); + Serial.println(F(" bytes.")); + Serial.print(F("Max free block size : ")); + Serial.print(esp_get_max_free_block_size()); + Serial.println(F(" bytes.")); + Serial.print(F("Heap fragmentation : ")); + Serial.print(esp_get_heap_fragmentation()); + Serial.println(F(" %")); } -uint32_t max_loop_duration = 0; +char sensorId[10]; // e.g "ESPxxxxxx\0" + +char* getSensorId() { + uint8_t mac[6]; + WiFi.macAddress(mac); + // Get last 3 bytes of ESP MAC (worldwide unique) + snprintf(sensorId, sizeof(sensorId), "ESP%02x%02x%02x", mac[3], mac[4], mac[5]); + return sensorId; +} + +Ampel::Ampel() : + board(current_board), sensorId(getSensorId()), max_loop_duration(0) { + sensor_console::defineIntCommand("set_time", ntp::setLocalTime, F(" 1618829570 (Sets time to the given UNIX time)")); + sensor_console::defineCommand("free", Ampel::showFreeSpace, F(" (Displays available heap space)")); + sensor_console::defineCommand("reset", []() { + ESP.restart(); + }, F(" (Restarts the sensor)")); +} -//FIXME: Remove every instance of Strings, to avoid heap fragmentation problems. (Start: "Free heap space : 17104 bytes") -// See more https://cpp4arduino.com/2020/02/07/how-to-format-strings-without-the-string-class.html -const String SENSOR_ID = "ESP" + macToID(); +Ampel ampel; diff --git a/ampel-firmware/util.h b/ampel-firmware/util.h index 2997933480b06db67b18a09bafa2d156d8b7e7b5..db424db6892d1beea14f3e4151c91356af464e4e 100644 --- a/ampel-firmware/util.h +++ b/ampel-firmware/util.h @@ -2,26 +2,25 @@ #define AMPEL_UTIL_H_INCLUDED #include #include "config.h" +#include "sensor_console.h" #include // required for NTP #include "src/lib/NTPClient-master/NTPClient.h" // NTP #if defined(ESP8266) -# define BOARD "ESP8266" # include // required to get MAC address -# define get_free_heap_size() system_get_free_heap_size() +# define esp_get_max_free_block_size() ESP.getMaxFreeBlockSize() +# define esp_get_heap_fragmentation() ESP.getHeapFragmentation() #elif defined(ESP32) -# define BOARD "ESP32" # include // required to get MAC address -# define get_free_heap_size() esp_get_free_heap_size() -#else -# define BOARD "Unknown" +# define esp_get_max_free_block_size() ESP.getMaxAllocHeap() //largest block of heap that can be allocated. +# define esp_get_heap_fragmentation() "?" // apparently not available for ESP32 #endif namespace ntp { void initialize(); void update(); - String getLocalTime(); + void getLocalTime(char *timestamp); } namespace util { @@ -35,10 +34,19 @@ namespace util { return b > a ? b : a; } } +class Ampel { +private: + static void showFreeSpace(); +public: + const char *board; + const char *sensorId; + uint32_t max_loop_duration; + Ampel(); +}; + +extern Ampel ampel; //NOTE: Only use seconds() for duration comparison, not timestamps comparison. Otherwise, problems happen when millis roll over. #define seconds() (millis() / 1000UL) -extern uint32_t max_loop_duration; -const extern String SENSOR_ID; #endif diff --git a/ampel-firmware/web_server.cpp b/ampel-firmware/web_server.cpp index 9cba04f27e880890b72310241ab985f652cc1737..40cd15b3e7e5a1dae0777a36dfe85138f58bb7a5 100644 --- a/ampel-firmware/web_server.cpp +++ b/ampel-firmware/web_server.cpp @@ -22,6 +22,7 @@ namespace web_server { const char *script_template; void handleWebServerRoot(); void handlePageNotFound(); + void handleWebServerCommand(); #ifdef AMPEL_CSV void handleDeleteCSV(); @@ -64,7 +65,7 @@ namespace web_server { #ifdef AMPEL_CSV "
  • Graph
  • \n" "
  • Log
  • \n" - "
  • Download CSV
  • \n" + "
  • Download CSV
  • \n" #endif "
  • ⬤
  • \n" // LED "\n" @@ -72,16 +73,15 @@ namespace web_server { // Show a colored dot on the webpage, with a similar color than on LED Ring. "hue=(1-(Math.min(Math.max(parseInt(document.title),500),1600)-500)/1100)*120;\n" "document.getElementById('led').style.color=['hsl(',hue,',100%%,50%%)'].join('');\n" - "\n"); - - body_template = - PSTR("
    \n" - "
    \n" // Graph placeholder + "\n" + "
    \n" + "
    \n"// Graph placeholder "
    \n" "
    \n" - //Sensor table - "\n" - "\n" + "
    %s
    \n"); + + body_template = + PSTR("\n" "\n" "\n" "\n" @@ -108,14 +108,17 @@ namespace web_server { #endif "\n" "\n" //TODO: Read it from sensor? + "\n" "\n" "\n" "\n" + "\n" "\n" "\n" "\n" "
    %s
    CO2 concentration%5d ppm
    Temperature%.1f℃
    Humidity%.1f%%
    Sensor
    Temperature offset%.1fK
    Auto-calibration?%s
    Local address%s.local
    Local IP%s
    Free heap space%6d bytes
    Largest heap block%6d bytes
    Max loop duration%5d ms
    Board%s
    Uptime%2d d %4d h %02d min %02d s
    \n" "
    \n" + "
    \n" #ifdef AMPEL_CSV "
    " "" @@ -130,7 +133,7 @@ namespace web_server { #ifdef AMPEL_CSV "