diff --git a/ampel-firmware/ampel-firmware.ino b/ampel-firmware/ampel-firmware.ino index 65cf8fedcc6bddc98dd858f35bd5f2c871690d8d..a69c42a46670f7484c07cc5c9a1eae2824a588dc 100644 --- a/ampel-firmware/ampel-firmware.ino +++ b/ampel-firmware/ampel-firmware.ino @@ -1,32 +1,20 @@ -/** - * IotWebConf03TypedParameters.ino -- IotWebConf is an ESP8266/ESP32 - * non blocking WiFi/AP web configuration library for Arduino. - * https://github.com/prampec/IotWebConf - * - * Copyright (C) 2020 Balazs Kelemen <prampec+arduino@gmail.com> - * - * This software may be modified and distributed under the terms - * of the MIT license. See the LICENSE file for details. - */ +#include <IotWebConf.h> +#include <IotWebConfTParameter.h> -/** - * Example: Custom parameters - * Description: - * Typed Parameters are a new approach to store/handle parameter data. - * This part of the software is very experimental, and certainly - * not recommended for beginners. - * The goal of this particular example is to compare the original - * approach of IotWebConf03CustomParameters to this new typed - * parameters, as both examples should work the same. - * - * Hardware setup for this example: - * - An LED is attached to LED_BUILTIN pin with setup On=LOW. - * - [Optional] A push button is attached to pin D2, the other leg of the - * button should be attached to GND. +/** 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, led on/off, ...) + * The callbacks can either have no parameter, one int32_t parameter or one char pointer. */ -#include <IotWebConf.h> -#include <IotWebConfTParameter.h> +namespace sensor_console { + void defineCommand(const char *name, void (*function)(), const __FlashStringHelper *doc_fstring); + void defineIntCommand(const char *name, void (*function)(int32_t), const __FlashStringHelper *doc_fstring); + void defineStringCommand(const char *name, void (*function)(char*), const __FlashStringHelper *doc_fstring); + + void checkSerialInput(); + + void execute(const char *command_line); +} // -- Initial name of the Thing. Used e.g. as SSID of the own Access Point. const char thingName[] = "testThing"; @@ -38,16 +26,7 @@ const char wifiInitialApPassword[] = "smrtTHNG8266"; #define NUMBER_LEN 32 // -- Configuration specific key. The value should be modified if config structure was changed. -#define CONFIG_VERSION "dem4" - -// -- When CONFIG_PIN is pulled to ground on startup, the Thing will use the initial -// password to buld an AP. (E.g. in case of lost password) -#define CONFIG_PIN 2 // D2 - -// -- Status indicator pin. -// First it will light up (kept LOW), on Wifi connection it will blink, -// when connected to the Wifi it will turn off (kept HIGH). -#define STATUS_PIN LED_BUILTIN +#define CONFIG_VERSION "dem99" // -- Method declarations. void handleRoot(); @@ -129,8 +108,6 @@ void setup() group2.addItem(&dateParam); group2.addItem(&timeParam); - iotWebConf.setStatusPin(STATUS_PIN); - iotWebConf.setConfigPin(CONFIG_PIN); iotWebConf.addSystemParameter(&stringParam); iotWebConf.addParameterGroup(&group1); iotWebConf.addParameterGroup(&group2); @@ -144,6 +121,34 @@ void setup() server.on("/", handleRoot); server.on("/config", []{ iotWebConf.handleConfig(); }); server.onNotFound([](){ iotWebConf.handleNotFound(); }); + + sensor_console::defineCommand("reset", [](){ ESP.restart(); }, F("(Restarts the ESP)")); + + sensor_console::defineCommand("save_config", [](){ iotWebConf.saveConfig(); }, F("(Saves the config to EEPROM)")); + + sensor_console::defineCommand("reset_config", []() { + Serial.println(F("Resetting config...")); + iotWebConf.getRootParameterGroup()->applyDefaultValue(); + Serial.println(F("Done!")); + }, F("(Resets the complete IotWeb config)")); + + sensor_console::defineStringCommand("ssid", [](char *ssid) { + Serial.print(F("Setting WiFi ssid to ")); + Serial.println(ssid); + strlcpy(iotWebConf.getWifiSsidParameter()->valueBuffer, ssid, iotWebConf.getWifiSsidParameter()->getLength()); + }, F("name (Sets SSID to name)")); + + sensor_console::defineStringCommand("wifi_pwd", [](char *pwd) { + Serial.print(F("Setting WiFi password to ")); + Serial.println(pwd); + strlcpy(iotWebConf.getWifiPasswordParameter()->valueBuffer, pwd, iotWebConf.getWifiPasswordParameter()->getLength()); + }, F("abc (Sets WiFi password to abc)")); + + sensor_console::defineStringCommand("ap_pwd", [](char *pwd) { + Serial.print(F("Setting AP password to ")); + Serial.println(pwd); + strlcpy(iotWebConf.getWifiPasswordParameter()->valueBuffer, pwd, iotWebConf.getWifiPasswordParameter()->getLength()); + }, F("abc (Sets AP password to abc)")); Serial.println("Ready."); } @@ -152,6 +157,7 @@ void loop() { // -- doLoop should be called as frequently as possible. iotWebConf.doLoop(); + sensor_console::checkSerialInput(); } /** @@ -198,3 +204,199 @@ void configSaved() { Serial.println("Configuration was updated."); } + +/*** + * Sensor console + */ + +namespace sensor_console { + const uint8_t MAX_COMMANDS = 10; + const uint8_t MAX_COMMAND_SIZE = 40; + + uint8_t commands_count = 0; + + enum input_type { + NONE, + INT32, + STRING + }; + + struct Command { + const char *name; + union { + void (*voidFunction)(); + void (*intFunction)(int32_t); + void (*strFunction)(char*); + }; + const char *doc; + input_type parameter_type; + }; + + struct CommandLine { + char function_name[MAX_COMMAND_SIZE]; + input_type argument_type; + int32_t int_argument; + char str_argument[MAX_COMMAND_SIZE]; + }; + + Command commands[MAX_COMMANDS]; + + bool addCommand(const char *name, const __FlashStringHelper *doc_fstring) { + if (commands_count < MAX_COMMANDS) { + commands[commands_count].name = name; + commands[commands_count].doc = (const char*) doc_fstring; + return true; + } else { + Serial.println(F("Too many commands have been defined.")); + return false; + } + } + + void defineCommand(const char *name, void (*function)(), const __FlashStringHelper *doc_fstring) { + if (addCommand(name, doc_fstring)) { + commands[commands_count].voidFunction = function; + commands[commands_count++].parameter_type = NONE; + } + } + + void defineIntCommand(const char *name, void (*function)(int32_t), const __FlashStringHelper *doc_fstring) { + if (addCommand(name, doc_fstring)) { + commands[commands_count].intFunction = function; + commands[commands_count++].parameter_type = INT32; + } + } + + void defineStringCommand(const char *name, void (*function)(char*), const __FlashStringHelper *doc_fstring) { + if (addCommand(name, doc_fstring)) { + commands[commands_count].strFunction = function; + commands[commands_count++].parameter_type = STRING; + } + } + + /* + * Tries to split a string command (e.g. 'mqtt 60' or 'show_csv') into + * a CommandLine struct (function_name, argument_type and argument) + */ + void parseCommand(const char *command, CommandLine &command_line) { + if (strlen(command) == 0) { + Serial.println(F("Received empty command")); + command_line.argument_type = NONE; + return; + } + + char *first_space; + first_space = strchr(command, ' '); + + if (first_space == NULL) { + command_line.argument_type = NONE; + strlcpy(command_line.function_name, command, MAX_COMMAND_SIZE); + return; + } + + strlcpy(command_line.function_name, command, first_space - command + 1); + strlcpy(command_line.str_argument, first_space + 1, MAX_COMMAND_SIZE - (first_space - command) - 1); + + char *end; + command_line.int_argument = strtol(command_line.str_argument, &end, 0); // Accepts 123 or 0xFF00FF + + if (*end) { + command_line.argument_type = STRING; + } else { + command_line.argument_type = INT32; + } + } + + int compareCommandNames(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]), compareCommandNames); + for (uint8_t i = 0; i < commands_count; i++) { + Serial.print(F(" ")); + Serial.print(commands[i].name); + Serial.print(F(" ")); + Serial.print(commands[i].doc); + Serial.println(F(".")); + } + } + + /* + * 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; + execute(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; + } + } + + void checkSerialInput() { + while (Serial.available() > 0) { + sensor_console::processSerialInput(Serial.read()); + } + } + + /* + * Tries to find the corresponding callback for a given command. Name and parameter type should fit. + */ + void execute(const char *command_str) { + CommandLine input; + parseCommand(command_str, input); + for (uint8_t i = 0; i < commands_count; i++) { + if (!strcmp(input.function_name, commands[i].name) && input.argument_type == commands[i].parameter_type) { + Serial.print(F("Calling : ")); + Serial.print(input.function_name); + switch (input.argument_type) { + case NONE: + Serial.println(F("()")); + commands[i].voidFunction(); + return; + case INT32: + Serial.print(F("(")); + Serial.print(input.int_argument); + Serial.println(F(")")); + commands[i].intFunction(input.int_argument); + return; + case STRING: + Serial.print(F("('")); + Serial.print(input.str_argument); + Serial.println(F("')")); + commands[i].strFunction(input.str_argument); + return; + } + } + } + Serial.print(F("'")); + Serial.print(command_str); + Serial.println(F("' not supported. Available commands :")); + listAvailableCommands(); + } +}