diff --git a/ampel-firmware/ampel-firmware.h b/ampel-firmware/ampel-firmware.h index e38cbfa04e93f2660e3d93a85ce8219249fea872..629e180f589ec0fd08a9e6b0b64ccbf87dcce76a 100644 --- a/ampel-firmware/ampel-firmware.h +++ b/ampel-firmware/ampel-firmware.h @@ -33,11 +33,12 @@ #endif #include "util.h" +#include "sensor_console.h" #include "co2_sensor.h" #include "led_effects.h" - void keepServicesAlive(); void checkFlashButton(); +void checkSerialInput(); #endif diff --git a/ampel-firmware/ampel-firmware.ino b/ampel-firmware/ampel-firmware.ino index 7abda47f3a43f1d53af4b49a6f8d3d6e342d54a1..facb1e3874f36a28d389f9f01c403278691a8d7c 100644 --- a/ampel-firmware/ampel-firmware.ino +++ b/ampel-firmware/ampel-firmware.ino @@ -56,7 +56,6 @@ * and define your credentials and parameters in 'config.h'. */ - /***************************************************************** * PreInit * *****************************************************************/ @@ -68,7 +67,6 @@ void preinit() { #endif } - /***************************************************************** * Setup * *****************************************************************/ @@ -147,6 +145,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); @@ -166,7 +166,13 @@ void loop() { max_loop_duration = duration; Serial.print(F("Debug - Max loop duration : ")); Serial.print(max_loop_duration); - Serial.println(" ms."); + 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 0420890c1deedc6f0c31b3b36d2b75bf5f39a9c1..430b945575fe37c115351a741cf90caffa549e64 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. @@ -17,7 +19,7 @@ namespace config { namespace sensor { SCD30 scd30; - int16_t co2 = 0; + uint16_t co2 = 0; float temperature = 0; float humidity = 0; String timestamp = ""; @@ -68,13 +70,25 @@ namespace sensor { Serial.print(F("Auto-calibration is ")); Serial.println(config::auto_calibrate_sensor ? "ON." : "OFF."); + + sensor_console::defineIntCommand("co2", setCO2forDebugging, " 1500 (Sets co2 level, for debugging purposes)"); + sensor_console::defineIntCommand("timer", setTimer, " 30 (Sets measurement interval, in s)"); + sensor_console::defineCommand("calibrate", startCalibrationProcess, " (Starts calibration process)"); + sensor_console::defineIntCommand("calibrate", calibrateSensorToSpecificPPM, + " 600 (Starts calibration process, to given ppm)"); + sensor_console::defineIntCommand("calibrate!", calibrateSensorRightNow, + " 600 (Calibrates right now, to given ppm)"); + sensor_console::defineCommand("reset", []() { + ESP.restart(); + }, " (Restarts the sensor)"); + sensor_console::defineCommand("night_mode", led_effects::toggleNightMode, " (Toggles night mode on/off)"); } //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 +96,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); @@ -177,7 +192,7 @@ namespace sensor { } if (should_calibrate) { - if (stable_measurements == 60) { + if (stable_measurements == config::enough_stable_measurements) { calibrateAndRestart(); } led_effects::showWaitingLED(waiting_color); @@ -187,4 +202,39 @@ 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 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..dd18bcf312048448ea665ad61b12ccb14b6f27c4 100644 --- a/ampel-firmware/co2_sensor.h +++ b/ampel-firmware/co2_sensor.h @@ -7,6 +7,7 @@ #include "config.h" #include "led_effects.h" #include "util.h" +#include "sensor_console.h" #include <Wire.h> namespace config { @@ -18,7 +19,7 @@ namespace config { namespace sensor { extern SCD30 scd30; - extern int16_t co2; + extern uint16_t co2; extern float temperature; extern float humidity; extern String timestamp; @@ -26,5 +27,10 @@ namespace sensor { 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); } #endif diff --git a/ampel-firmware/csv_writer.cpp b/ampel-firmware/csv_writer.cpp index 653630335527800481fb4907025e494e2de3fd58..2a7f462a4a750ed7565a8ff8318d6a608cbf12a0 100644 --- a/ampel-firmware/csv_writer.cpp +++ b/ampel-firmware/csv_writer.cpp @@ -1,7 +1,5 @@ #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] @@ -113,9 +111,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, " 60 (Sets CSV writing interval, in s)"); + sensor_console::defineCommand("format_filesystem", formatFilesystem, " (Deletes the whole filesystem)"); + sensor_console::defineCommand("show_csv", showCSVContent, " (Displays the complete CSV file on Serial)"); } File openOrCreate() { @@ -161,4 +163,35 @@ namespace csv_writer { 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..080d5cdcc2e4b2e5aa17617118120f5c5213dac6 100644 --- a/ampel-firmware/csv_writer.h +++ b/ampel-firmware/csv_writer.h @@ -14,6 +14,7 @@ #include "config.h" #include "util.h" #include "led_effects.h" +#include "sensor_console.h" namespace config { extern uint16_t csv_interval; // [s] @@ -24,6 +25,10 @@ namespace csv_writer { void logIfTimeHasCome(const String &timeStamp, const int16_t &co2, const float &temperature, const float &humidity); int getAvailableSpace(); extern const String filename; + + void setCSVinterval(int32_t csv_interval); + void showCSVContent(); + void formatFilesystem(); } #endif diff --git a/ampel-firmware/lorawan.cpp b/ampel-firmware/lorawan.cpp index 7e027703ebd6505577005de734c3272beb785789..70a02e1c8325a21a42f63402956d497264baf3d8 100644 --- a/ampel-firmware/lorawan.cpp +++ b/ampel-firmware/lorawan.cpp @@ -47,11 +47,12 @@ namespace lorawan { LMIC_reset(); // Join, but don't send anything yet. LMIC_startJoining(); + sensor_console::defineIntCommand("lora", setLoRaInterval, " 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(); } @@ -189,6 +190,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..6cf1226f72f7385e075ff387c160d0dbdefa2ffd 100644 --- a/ampel-firmware/lorawan.h +++ b/ampel-firmware/lorawan.h @@ -13,7 +13,7 @@ #include <SPI.h> #include "led_effects.h" - +#include "sensor_console.h" #include "util.h" namespace config { @@ -43,6 +43,8 @@ namespace lorawan { 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 143ed736ced5b9d94999147922973d4e919a0484..516090744a5ac7c8cfed9cfdee02391b5bb78d4e 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; @@ -35,6 +35,10 @@ namespace mqtt { #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, " 60 (Sets MQTT sending interval, in s)"); + sensor_console::defineCommand("local_ip", sendInfoAboutLocalNetwork, + " (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) { @@ -55,70 +59,6 @@ namespace mqtt { } } - //TODO: Move all those setters to a separate class, which could be used by Serial/MQTT/WebServer - 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 * @@ -133,48 +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("'."); - - //TODO: Move this logic to a separate class, which could be used by Serial/MQTT/WebServer - - 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(); } @@ -218,7 +123,7 @@ namespace mqtt { void publishIfTimeHasCome(const String &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); } @@ -232,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.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); + } } diff --git a/ampel-firmware/mqtt.h b/ampel-firmware/mqtt.h index 2899a9996bbf8d2a484f5fc79b619d9647ee70a7..aaaa95621d10cdf0a8608700654a89b360563989 100644 --- a/ampel-firmware/mqtt.h +++ b/ampel-firmware/mqtt.h @@ -4,14 +4,11 @@ #include <Arduino.h> #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; @@ -19,5 +16,8 @@ namespace mqtt { void initialize(String &topic); void keepConnection(); void publishIfTimeHasCome(const String &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..ec55534d0ed69c96a820047939ff0b1285db4faa --- /dev/null +++ b/ampel-firmware/sensor_console.cpp @@ -0,0 +1,160 @@ +#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 char *doc) { + 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 char *doc) { + 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); + Serial.print(F("Received : '")); + Serial.print(command); + Serial.println("'"); + char *arg; + char *part1; + part1 = strtok(split_command, " "); + if (!part1) { + // 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("."); + } + led_effects::showKITTWheel(color::red, 1); + } + + /* + * 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("("); + Serial.print(argument); + Serial.println(")"); + commands[i].intFunction(argument); + } else { + Serial.println("()"); + commands[i].voidFunction(); + } + return; + } + } + Serial.println(F("Message 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..8afa40c34838b2c321b55546a75a34d74360ab6b --- /dev/null +++ b/ampel-firmware/sensor_console.h @@ -0,0 +1,14 @@ +#include <Arduino.h> +#include "led_effects.h" + +/** 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 processSerialInput(const byte in_byte); + void runCommand(const char *command); + void defineIntCommand(const char *command, void (*function)(int32_t), const char *doc); + void defineCommand(const char *command, void (*function)(void), const char *doc); +} diff --git a/ampel-firmware/web_server.cpp b/ampel-firmware/web_server.cpp index 9cba04f27e880890b72310241ab985f652cc1737..c9dfeab5e2546703dc2968f5eefa45799d86282a 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(); @@ -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" - "</script>\n"); - - body_template = - PSTR("<div class='pure-g'>\n" - "<div class='pure-u-1' id='graph'></div>\n" // Graph placeholder + "</script>\n" + "<div class='pure-g'>\n" + "<div class='pure-u-1' id='graph'></div>\n"// Graph placeholder "</div>\n" "<div class='pure-g'>\n" - //Sensor table - "<table id='table' class='pure-table-striped pure-u-1 pure-u-md-1-2'>\n" - "<tr><th colspan='2'>%s</th></tr>\n" + "<table id='table' class='pure-table-striped pure-u-1 pure-u-md-1-2'>\n"); + + body_template = + PSTR("<tr><th colspan='2'>%s</th></tr>\n" "<tr><td>CO<sub>2</sub> concentration</td><td>%5d ppm</td></tr>\n" "<tr><td>Temperature</td><td>%.1f℃</td></tr>\n" "<tr><td>Humidity</td><td>%.1f%%</td></tr>\n" @@ -108,6 +108,7 @@ namespace web_server { #endif "<tr><th colspan='2'>Sensor</th></tr>\n" "<tr><td>Temperature offset</td><td>%.1fK</td></tr>\n" //TODO: Read it from sensor? + "<tr><td>Auto-calibration?</td><td>%s</td></tr>\n" "<tr><td>Local address</td><td><a href='http://%s.local/'>%s.local</a></td></tr>\n" "<tr><td>Local IP</td><td><a href='http://%s'>%s</a></td></tr>\n" "<tr><td>Free heap space</td><td>%6d bytes</td></tr>\n" @@ -116,6 +117,7 @@ namespace web_server { "<tr><td>Uptime</td><td>%2d d %4d h %02d min %02d s</td></tr>\n" "</table>\n" "<div id='log' class='pure-u-1 pure-u-md-1-2'></div>\n" + "<form action='/command'><input type='text' id='send' name='send'><input type='submit' value='Send'></form>\n" #ifdef AMPEL_CSV "<form action='/delete_csv' method='POST' onsubmit=\"return confirm('Are you really sure you want to delete all data?') && (document.body.style.cursor = 'wait');\">" "<input type='submit' value='Delete CSV'/>" @@ -173,6 +175,7 @@ namespace web_server { // Web-server http.on("/", handleWebServerRoot); + http.on("/command", handleWebServerCommand); #ifdef AMPEL_CSV http.on("/" + csv_writer::filename, handleWebServerCSV); http.on("/delete_csv", HTTP_POST, handleDeleteCSV); @@ -207,9 +210,8 @@ namespace web_server { //NOTE: Splitting in multiple parts in order to use less RAM char content[2000]; // Update if needed - // Header size : 1611 - Body size : 1800 - Script size : 1920 + // INFO - Header size : 1767 - Body size : 1812 - Script size : 1909 - // Header snprintf_P(content, sizeof(content), header_template, sensor::co2, SENSOR_ID.c_str(), WiFi.localIP().toString().c_str() #ifdef AMPEL_CSV @@ -217,6 +219,8 @@ namespace web_server { #endif ); + Serial.print(F("INFO - Header size : ")); + Serial.print(strlen(content)); http.setContentLength(CONTENT_LENGTH_UNKNOWN); http.send_P(200, PSTR("text/html"), content); @@ -227,16 +231,19 @@ namespace web_server { csv_writer::last_successful_write.c_str(), config::csv_interval, csv_writer::getAvailableSpace() / 1024, #endif #ifdef AMPEL_MQTT - mqtt::connected ? "Yes" : "No", mqtt::last_successful_publish.c_str(), config::sending_interval, + mqtt::connected ? "Yes" : "No", mqtt::last_successful_publish.c_str(), config::mqtt_sending_interval, #endif #if defined(AMPEL_LORAWAN) && defined(ESP32) lorawan::connected ? "Yes" : "No", LMIC_FREQUENCY_PLAN, lorawan::last_transmission.c_str(), config::lorawan_sending_interval, #endif - config::temperature_offset, SENSOR_ID.c_str(), SENSOR_ID.c_str(), WiFi.localIP().toString().c_str(), - WiFi.localIP().toString().c_str(), get_free_heap_size(), max_loop_duration, BOARD, dd, hh, mm, ss); + config::temperature_offset, config::auto_calibrate_sensor ? "Yes" : "No", SENSOR_ID.c_str(), SENSOR_ID.c_str(), + WiFi.localIP().toString().c_str(), WiFi.localIP().toString().c_str(), get_free_heap_size(), max_loop_duration, + BOARD, dd, hh, mm, ss); + Serial.print(F(" - Body size : ")); http.sendContent(content); + Serial.print(strlen(content)); // Script snprintf_P(content, sizeof(content), script_template @@ -245,6 +252,8 @@ namespace web_server { #endif ); + Serial.print(F(" - Script size : ")); + Serial.println(strlen(content)); http.sendContent(content); } @@ -275,6 +284,15 @@ namespace web_server { } #endif + void handleWebServerCommand() { + if (!shouldBeAllowed()) { + return http.requestAuthentication(DIGEST_AUTH); + } + http.sendHeader("Location", "/"); + http.send(303); + sensor_console::runCommand(http.arg("send").c_str()); + } + void handlePageNotFound() { http.send(404, F("text/plain"), F("404: Not found")); } diff --git a/ampel-firmware/web_server.h b/ampel-firmware/web_server.h index 71c17f0578ac1616f464d0456f25afcfa5930c72..ed1b65dc26dee2dc5dd3d5ce27a6e1e5acea66ff 100644 --- a/ampel-firmware/web_server.h +++ b/ampel-firmware/web_server.h @@ -1,5 +1,6 @@ #ifndef WEB_SERVER_H_ #define WEB_SERVER_H_ + #if defined(ESP8266) # include <ESP8266WebServer.h> #elif defined(ESP32) @@ -9,6 +10,7 @@ #include "config.h" #include "util.h" #include "co2_sensor.h" +#include "sensor_console.h" #ifdef AMPEL_CSV # include "csv_writer.h" #endif