diff --git a/ampel-firmware/ampel-firmware.ino b/ampel-firmware/ampel-firmware.ino index 8d04504665e014bd52e1d66f38c0e14864d2fc9e..39f6b0c3b9ede61f313a190e4c292eb6c9bde408 100644 --- a/ampel-firmware/ampel-firmware.ino +++ b/ampel-firmware/ampel-firmware.ino @@ -56,17 +56,6 @@ * 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 * *****************************************************************/ @@ -78,14 +67,17 @@ void setup() { pinMode(0, INPUT); // Flash button (used for forced calibration) - led_effects::setupRing(); - - sensor::initialize(); - + Serial.println(); Serial.print(F("Sensor ID: ")); Serial.println(ampel.sensorId); Serial.print(F("Board : ")); Serial.println(ampel.board); + Serial.print(F("Firmware : ")); + Serial.println(ampel.version); + + led_effects::setupRing(); + + sensor::initialize(); #ifdef AMPEL_CSV csv_writer::initialize(ampel.sensorId); @@ -132,7 +124,6 @@ void checkSerialInput(); /***************************************************************** * Main loop * *****************************************************************/ - void loop() { #if defined(AMPEL_LORAWAN) && defined(ESP32) //LMIC Library seems to be very sensitive to timing issues, so run it first. diff --git a/ampel-firmware/co2_sensor.cpp b/ampel-firmware/co2_sensor.cpp index 5795697b05eb325ae2f4e8822f5f994bcffa043a..e68b2ea43514e685332229e33eab2f0ea54d4a57 100644 --- a/ampel-firmware/co2_sensor.cpp +++ b/ampel-firmware/co2_sensor.cpp @@ -1,12 +1,16 @@ #include "co2_sensor.h" namespace config { - // Values should be defined in config.h - uint16_t measurement_timestep = MEASUREMENT_TIMESTEP; // [s] Value between 2 and 1800 (range for SCD30 sensor) + // UPPERCASE values should be defined in config.h + 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; + const uint16_t measurement_timestep_bootup = 5; // [s] Measurement timestep during acclimatization. + const uint8_t max_deviation_during_bootup = 20; // [%] + const int8_t max_deviation_during_calibration = 30; // [ppm] + const int16_t timestep_during_calibration = 10; // [s] WARNING: Measurements can be unreliable for timesteps shorter than 10s. + const int8_t stable_measurements_before_calibration = 120 / timestep_during_calibration; // [-] Stable measurements during at least 2 minutes. + #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. @@ -28,31 +32,27 @@ namespace sensor { /** * Define sensor states - * INITIAL -> initial state - * BOOTUP -> state after initializing the sensor, i.e. after scd.begin() + * BOOTUP -> initial state, until first >0 ppm values are returned * READY -> sensor does output valid information (> 0 ppm) and no other condition takes place * NEEDS_CALIBRATION -> sensor measurements are too low (< 250 ppm) - * PREPARE_CALIBRATION -> forced calibration was initiated, waiting for stable measurements - * CALIBRATION -> the sensor does calibrate itself + * PREPARE_CALIBRATION_UNSTABLE -> forced calibration was initiated, last measurements were too far apart + * PREPARE_CALIBRATION_STABLE -> forced calibration was initiated, last measurements were close to each others */ enum state { - INITIAL, BOOTUP, READY, NEEDS_CALIBRATION, PREPARE_CALIBRATION_UNSTABLE, - PREPARE_CALIBRATION_STABLE, - CALIBRATION + PREPARE_CALIBRATION_STABLE }; const char *state_names[] = { - "INITIAL", "BOOTUP", "READY", "NEEDS_CALIBRATION", "PREPARE_CALIBRATION_UNSTABLE", - "PREPARE_CALIBRATION_STABLE", - "CALIBRATION" }; - state current_state = INITIAL; + "PREPARE_CALIBRATION_STABLE" }; + + state current_state = BOOTUP; void switchState(state); void initialize() { @@ -78,47 +78,51 @@ namespace sensor { ESP.restart(); } - switchState(BOOTUP); - - // SCD30 has its own timer. - //NOTE: The timer seems to be inaccurate, though, possibly depending on voltage. Should it be offset? - Serial.print(F("Setting SCD30 timestep to ")); - Serial.print(config::measurement_timestep); - Serial.println(" s."); - scd30.setMeasurementInterval(config::measurement_timestep); // [s] + // Changes of the SCD30's measurement timestep do not come into effect + // before the next measurement takes place. That means that after a hard reset + // of the ESP the SCD30 sometimes needs a long time until switching back to 2 s + // for acclimatization. Resetting it after startup seems to fix this behaviour. + scd30.reset(); Serial.print(F("Setting temperature offset to -")); Serial.print(abs(config::temperature_offset)); - Serial.println(" K."); + Serial.println(F(" K.")); scd30.setTemperatureOffset(abs(config::temperature_offset)); // setTemperatureOffset only accepts positive numbers, but shifts the temperature down. delay(100); Serial.print(F("Temperature offset is : -")); Serial.print(scd30.getTemperatureOffset()); - Serial.println(" K"); + Serial.println(F(" K")); 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)")); + // SCD30 has its own timer. + //NOTE: The timer seems to be inaccurate, though, possibly depending on voltage. Should it be offset? + Serial.println(); + Serial.print(F("Setting SCD30 timestep to ")); + Serial.print(config::measurement_timestep_bootup); + Serial.println(F(" s during acclimatization.")); + scd30.setMeasurementInterval(config::measurement_timestep_bootup); // [s] + + 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)")); + 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)")); + 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(F("Measurement time offset : ")); - Serial.print(now - previous_measurement_at - config::measurement_timestep * 1000); - Serial.println(" ms."); - previous_measurement_at = now; + bool hasSensorSettled() { + static uint16_t last_co2 = 0; + uint16_t delta; + delta = abs(co2 - last_co2); + last_co2 = co2; + // We assume the sensor has acclimated to the environment if measurements + // change less than a specified percentage of the current value. + return (co2 > 0 && delta < ((uint32_t) co2 * config::max_deviation_during_bootup / 100)); } bool countStableMeasurements() { @@ -128,30 +132,32 @@ namespace sensor { && co2 < (previous_co2 + config::max_deviation_during_calibration)) { stable_measurements++; Serial.print(F("Number of stable measurements : ")); - Serial.println(stable_measurements); + Serial.print(stable_measurements); + Serial.print(F(" / ")); + Serial.println(config::stable_measurements_before_calibration); switchState(PREPARE_CALIBRATION_STABLE); } else { stable_measurements = 0; switchState(PREPARE_CALIBRATION_UNSTABLE); } previous_co2 = co2; - return (stable_measurements == config::enough_stable_measurements); + return (stable_measurements == config::stable_measurements_before_calibration); } void startCalibrationProcess() { /** From the sensor documentation: - * For best results, the sensor has to be run in a stable environment in continuous mode at - * a measurement rate of 2s for at least two minutes before applying the FRC command and sending the reference value. + * Before applying FRC, SCD30 needs to be operated for 2 minutes with the desired measurement period in continuous mode. */ - Serial.println(F("Setting SCD30 timestep to 2s, prior to calibration.")); - scd30.setMeasurementInterval(2); // [s] The change will only take effect after next measurement. + Serial.print(F("Setting SCD30 timestep to ")); + Serial.print(config::timestep_during_calibration); + Serial.println(F("s, prior to calibration.")); + scd30.setMeasurementInterval(config::timestep_during_calibration); // [s] The change will only take effect after next measurement. Serial.println(F("Waiting until the measurements are stable for at least 2 minutes.")); Serial.println(F("It could take a very long time.")); switchState(PREPARE_CALIBRATION_UNSTABLE); } void calibrateAndRestart() { - switchState(CALIBRATION); Serial.print(F("Calibrating SCD30 now...")); scd30.setAltitudeCompensation(config::altitude_above_sea_level); scd30.setForcedRecalibrationFactor(config::co2_calibration_level); @@ -177,19 +183,28 @@ namespace sensor { if (config::debug_sensor_states) { Serial.print(F("Changing sensor state: ")); Serial.print(state_names[current_state]); - Serial.print(" -> "); + Serial.print(F(" -> ")); Serial.println(state_names[new_state]); } current_state = new_state; } void switchStateForCurrentPPM() { - if (co2 == 0) { - // NOTE: Data is available, but it's sometimes erroneous: the sensor outputs - // zero ppm but non-zero temperature and non-zero humidity. - Serial.println(F("Invalid sensor data - CO2 concentration supposedly 0 ppm")); - switchState(BOOTUP); - } else if ((current_state == PREPARE_CALIBRATION_UNSTABLE) || (current_state == PREPARE_CALIBRATION_STABLE)) { + if (current_state == BOOTUP) { + if (!hasSensorSettled()) { + return; + } + switchState(READY); + Serial.println(F("Sensor acclimatization finished.")); + Serial.print(F("Setting SCD30 timestep to ")); + Serial.print(config::measurement_timestep); + Serial.println(F(" s.")); + if (config::measurement_timestep < 10) { + Serial.println(F("WARNING: Timesteps shorter than 10s can lead to unreliable measurements!")); + } + scd30.setMeasurementInterval(config::measurement_timestep); // [s] + } + if ((current_state == PREPARE_CALIBRATION_UNSTABLE) || (current_state == PREPARE_CALIBRATION_STABLE)) { // Check for pre-calibration states first, because we do not want to // leave them before calibration is done. bool ready_for_calibration = countStableMeasurements(); @@ -236,8 +251,6 @@ namespace sensor { case PREPARE_CALIBRATION_STABLE: led_effects::showWaitingLED(color::green); break; - case CALIBRATION: // Nothing to do, will restart soon. - break; default: Serial.println(F("Encountered unknown sensor state")); // This should not happen. } @@ -250,7 +263,6 @@ namespace sensor { bool freshData = scd30.dataAvailable(); if (freshData) { - // checkTimerDeviation(); ntp::getLocalTime(timestamp); co2 = scd30.getCO2(); temperature = scd30.getTemperature(); @@ -264,7 +276,9 @@ namespace sensor { showState(); - return freshData; + // Report data for further processing only if the data is reliable + // (state 'READY') or manual calibration is necessary (state 'NEEDS_CALIBRATION'). + return freshData && (current_state == READY || current_state == NEEDS_CALIBRATION); } /***************************************************************** @@ -288,8 +302,8 @@ namespace sensor { if (timestep >= 2 && timestep <= 1800) { Serial.print(F("Setting Measurement Interval to : ")); Serial.print(timestep); - Serial.println("s."); - sensor::scd30.setMeasurementInterval(timestep); + Serial.println(F("s.")); + scd30.setMeasurementInterval(timestep); config::measurement_timestep = timestep; led_effects::showKITTWheel(color::green, 1); } @@ -300,13 +314,18 @@ namespace sensor { Serial.print(F("Force calibration, at ")); config::co2_calibration_level = calibrationLevel; Serial.print(config::co2_calibration_level); - Serial.println(" ppm."); - sensor::startCalibrationProcess(); + Serial.println(F(" ppm.")); + startCalibrationProcess(); } } void calibrateSensorRightNow(int32_t calibrationLevel) { - stable_measurements = config::enough_stable_measurements; - calibrateSensorToSpecificPPM(calibrationLevel); + if (calibrationLevel >= 400 && calibrationLevel <= 2000) { + Serial.print(F("Force calibration, right now, at ")); + config::co2_calibration_level = calibrationLevel; + Serial.print(config::co2_calibration_level); + Serial.println(F(" ppm.")); + calibrateAndRestart(); + } } } diff --git a/ampel-firmware/config.public.h b/ampel-firmware/config.public.h index 1ca67666cb3f76cb20ca1a9d03dc1b38d6bc3558..44a2bc95b176e1fe6d281a5a1d72dfa7d9c8fd86 100644 --- a/ampel-firmware/config.public.h +++ b/ampel-firmware/config.public.h @@ -27,7 +27,9 @@ */ // How often should measurement be performed, and displayed? -//NOTE: SCD30 timer does not seem to be very precise. Variations may occur. +//WARNING: On some sensors, measurements become very unreliable when timestep is set to 2s. +//NOTE: 10s or longer should be fine in order to get reliable results. +//NOTE: SCD30 timer does not seem to be very precise. Time variations may occur. # define MEASUREMENT_TIMESTEP 60 // [s] Value between 2 and 1800 (range for SCD30 sensor) // How often should measurements be appended to CSV ? @@ -64,6 +66,8 @@ // 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 +// How many LEDs in the ring? 12 and 16 are currently supported. If undefined, 12 is used as default. +# define LED_COUNT 12 /** * WEB SERVER @@ -101,7 +105,7 @@ */ # define ALLOW_MQTT_COMMANDS false -// How often measurements should be sent to MQTT server? +// How often should measurements be sent to MQTT server? // Probably a good idea to use a multiple of MEASUREMENT_TIMESTEP, so that averages can be calculated // Set to 0 if you want to send values after each measurement // # define MQTT_SENDING_INTERVAL MEASUREMENT_TIMESTEP * 5 // [s] diff --git a/ampel-firmware/csv_writer.cpp b/ampel-firmware/csv_writer.cpp index a83bed666455545d2d5cb7c68a3d583d88f7b40a..d0828a40999dc7326d1ce19f9a4808be10dd6852 100644 --- a/ampel-firmware/csv_writer.cpp +++ b/ampel-firmware/csv_writer.cpp @@ -118,9 +118,9 @@ namespace csv_writer { 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)")); + 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() { diff --git a/ampel-firmware/led_effects.cpp b/ampel-firmware/led_effects.cpp index ee0fccde5222ecf935dc444d86ed8663d5291a77..f0b672c9d0c76e1cac6916d94783e85577f387fd 100644 --- a/ampel-firmware/led_effects.cpp +++ b/ampel-firmware/led_effects.cpp @@ -12,8 +12,28 @@ namespace config { 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; + bool night_mode = false; //NOTE: Use a class instead? NightMode could then be another state. + +#if !defined(LED_COUNT) +# define LED_COUNT 12 +#endif + + const uint16_t led_count = LED_COUNT; + +#if LED_COUNT == 12 + //NOTE: One value has been prepended, to make calculations easier and avoid out of bounds index. + const uint16_t co2_ticks[led_count + 1] = { 0, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000, 2200 }; // [ppm] + // For a given LED, which color should be displayed? First LED will be pure green (hue angle 120°), + // last 4 LEDs will be pure red (hue angle 0°), LEDs in-between will be yellowish. + const uint16_t led_hues[led_count] = { 21845U, 19114U, 16383U, 13653U, 10922U, 8191U, 5461U, 2730U, 0, 0, 0, 0 }; // [hue angle] +#elif LED_COUNT == 16 + const uint16_t co2_ticks[led_count + 1] = { 0, 400, 500, 600, 700, 800, 900, 1000, 1100, + 1200, 1300, 1400, 1500, 1600, 1800, 2000, 2200 }; // [ppm] + const uint16_t led_hues[led_count] = { 21845U, 20024U, 18204U, 16383U, 14563U, 12742U, 10922U, 9102U, + 7281U, 5461U, 3640U, 1820U, 0, 0, 0, 0 }; // [hue angle] +#else +# error "Only 12 and 16 LEDs rings are currently supported." +#endif } #if defined(ESP8266) @@ -24,20 +44,7 @@ const int NEOPIXELS_PIN = 5; const int NEOPIXELS_PIN = 23; #endif -const int NUMPIXELS = 12; -//NOTE: One value has been prepended, to make calculations easier and avoid out of bounds index. -const uint16_t CO2_TICKS[NUMPIXELS + 1] = { 0, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000, 2200 }; // [ppm] -// const uint16_t CO2_TICKS[NUMPIXELS + 1] = { 0, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400, 1500, 1600, 1800, 2000, 2200 }; // [ppm] -// For a given LED, which color should be displayed? First LED will be pure green (hue angle 120°), -// last 4 LEDs will be pure red (hue angle 0°), LEDs in-between will be yellowish. -// For reference, this python code can be used to generate the array -// NUMPIXELS = 12 -// RED_LEDS = 4 -// hues = [ (2**16-1) // 3 * max(NUMPIXELS - RED_LEDS - i, 0) // (NUMPIXELS - RED_LEDS) for i in range(NUMPIXELS) ] -// '{' + ', '.join([str(hue) + ('U' if hue else '') for hue in hues]) + '}; // [hue angle]' -const uint16_t LED_HUES[NUMPIXELS] = { 21845U, 19114U, 16383U, 13653U, 10922U, 8191U, 5461U, 2730U, 0, 0, 0, 0 }; // [hue angle] -// const uint16_t LED_HUES[NUMPIXELS] = { 21845U, 20024U, 18204U, 16383U, 14563U, 12742U, 10922U, 9102U, 7281U, 5461U, 3640U, 1820U, 0, 0, 0, 0 }; // [hue angle] -Adafruit_NeoPixel pixels(NUMPIXELS, NEOPIXELS_PIN, NEO_GRB + NEO_KHZ800); +Adafruit_NeoPixel pixels(config::led_count, NEOPIXELS_PIN, NEO_GRB + NEO_KHZ800); namespace led_effects { //On-board LED on D4, aka GPIO02 @@ -70,11 +77,19 @@ namespace led_effects { onBoardLEDOff(); } + void showColor(int32_t color) { + config::night_mode = true; // In order to avoid overwriting the desired color next time CO2 is displayed + pixels.setBrightness(255); + pixels.fill(color); + pixels.show(); + } + void setupRing() { pixels.begin(); pixels.setBrightness(config::max_brightness); LEDsOff(); - sensor_console::defineCommand("night_mode", toggleNightMode, F(" (Toggles night mode on/off)")); + sensor_console::defineCommand("night_mode", toggleNightMode, F("(Toggles night mode on/off)")); + sensor_console::defineIntCommand("color", showColor, F("0xFF0015 (Shows color, specified as RGB, for debugging)")); } void toggleNightMode() { @@ -89,14 +104,15 @@ namespace led_effects { //NOTE: basically one iteration of KITT wheel void showWaitingLED(uint32_t color) { + using namespace config; delay(80); - if (config::night_mode) { + if (night_mode) { return; } static uint16_t kitt_offset = 0; pixels.clear(); - for (int j = config::kitt_tail; j >= 0; j--) { - int ledNumber = abs((kitt_offset - j + NUMPIXELS) % (2 * NUMPIXELS) - NUMPIXELS) % NUMPIXELS; // Triangular function + for (int j = kitt_tail; j >= 0; j--) { + int ledNumber = abs((kitt_offset - j + led_count) % (2 * led_count) - led_count) % led_count; // Triangular function pixels.setPixelColor(ledNumber, color * pixels.gamma8(255 - j * 76) / 255); } pixels.show(); @@ -108,7 +124,7 @@ namespace led_effects { // Takes approximately 1s for each direction. void showKITTWheel(uint32_t color, uint16_t duration_s) { pixels.setBrightness(config::max_brightness); - for (int i = 0; i < duration_s * NUMPIXELS; ++i) { + for (int i = 0; i < duration_s * config::led_count; ++i) { showWaitingLED(color); } } @@ -118,10 +134,10 @@ namespace led_effects { * For example, for 1500ppm, every LED between 0 and 7 (500 -> 1400ppm) should be on, LED at 8 (1600ppm) should be half-on. */ uint8_t getLedBrightness(uint16_t co2, int ledId) { - if (co2 >= CO2_TICKS[ledId + 1]) { + if (co2 >= config::co2_ticks[ledId + 1]) { return 255; } else { - if (2 * co2 >= CO2_TICKS[ledId] + CO2_TICKS[ledId + 1]) { + if (2 * co2 >= config::co2_ticks[ledId] + config::co2_ticks[ledId + 1]) { // Show partial LED if co2 more than halfway between ticks. return 27; // Brightness isn't linear, so 27 / 255 looks much brighter than 10% } else { @@ -150,9 +166,9 @@ namespace led_effects { return; } pixels.setBrightness(config::max_brightness); - for (int ledId = 0; ledId < NUMPIXELS; ++ledId) { + for (int ledId = 0; ledId < config::led_count; ++ledId) { uint8_t brightness = getLedBrightness(co2, ledId); - pixels.setPixelColor(ledId, pixels.ColorHSV(LED_HUES[ledId], 255, brightness)); + pixels.setPixelColor(ledId, pixels.ColorHSV(config::led_hues[ledId], 255, brightness)); } pixels.show(); if (config::brightness_amplitude > 0) { @@ -160,17 +176,18 @@ namespace led_effects { } } - void showRainbowWheel(uint16_t duration_ms, uint16_t hue_increment) { + void showRainbowWheel(uint16_t duration_ms) { if (config::night_mode) { return; } static uint16_t wheel_offset = 0; + static uint16_t sine_offset = 0; unsigned long t0 = millis(); pixels.setBrightness(config::max_brightness); while (millis() - t0 < duration_ms) { - for (int i = 0; i < NUMPIXELS; i++) { - pixels.setPixelColor(i, pixels.ColorHSV(i * 65535 / NUMPIXELS + wheel_offset)); - wheel_offset += hue_increment; + for (int i = 0; i < config::led_count; i++) { + pixels.setPixelColor(i, pixels.ColorHSV(i * 65535 / config::led_count + wheel_offset)); + wheel_offset += (pixels.sine8(sine_offset++ / 50) - 127) / 2; } pixels.show(); delay(10); @@ -206,7 +223,7 @@ namespace led_effects { pixels.fill(color::blue); pixels.show(); int countdown; - for (countdown = NUMPIXELS; countdown >= 0 && !digitalRead(0); countdown--) { + for (countdown = config::led_count; countdown >= 0 && !digitalRead(0); countdown--) { pixels.setPixelColor(countdown, color::black); pixels.show(); Serial.println(countdown); diff --git a/ampel-firmware/led_effects.h b/ampel-firmware/led_effects.h index 96367ef5f2909d38377e2441f91d4e2e1abe2198..a910d307f7c6294db889e704f745c3777136d19b 100644 --- a/ampel-firmware/led_effects.h +++ b/ampel-firmware/led_effects.h @@ -29,7 +29,7 @@ namespace led_effects { int countdownToZero(); void showWaitingLED(uint32_t color); void showKITTWheel(uint32_t color, uint16_t duration_s = 2); - void showRainbowWheel(uint16_t duration_ms = 1000, uint16_t hue_increment = 50); + void showRainbowWheel(uint16_t duration_ms = 1000); void displayCO2color(uint16_t co2); } #endif diff --git a/ampel-firmware/lorawan.cpp b/ampel-firmware/lorawan.cpp index 784b46fb61b25bc8d3868a3a541baa14f241150a..746893b3698137ff14417158e5a2728fd8ad4a25 100644 --- a/ampel-firmware/lorawan.cpp +++ b/ampel-firmware/lorawan.cpp @@ -47,7 +47,7 @@ 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)")); + sensor_console::defineIntCommand("lora", setLoRaInterval, F("300 (Sets LoRaWAN sending interval, in s)")); } // Checks if OTAA is connected, or if payload should be sent. @@ -96,7 +96,7 @@ namespace lorawan { printHex2(artKey[i]); } Serial.println(); - Serial.print(" NwkSKey: "); + Serial.print(F(" NwkSKey: ")); for (size_t i = 0; i < sizeof(nwkKey); ++i) { if (i != 0) Serial.print("-"); diff --git a/ampel-firmware/mqtt.cpp b/ampel-firmware/mqtt.cpp index 22a0e2def350a38b77336b954395ae5d55005a8e..cb0c5019c00d14094d6389684bb120eb061197f6 100644 --- a/ampel-firmware/mqtt.cpp +++ b/ampel-firmware/mqtt.cpp @@ -36,9 +36,9 @@ namespace mqtt { // 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)")); + sensor_console::defineIntCommand("mqtt", setMQTTinterval, F("60 (Sets MQTT sending interval, in s)")); + sensor_console::defineCommand("send_local_ip", sendInfoAboutLocalNetwork, + F("(Sends local IP and SSID via MQTT. Can be useful to find sensor)")); } void publish(const char *timestamp, int16_t co2, float temperature, float humidity) { @@ -79,7 +79,7 @@ namespace mqtt { command[i] = message[i]; } command[length] = 0; - sensor_console::runCommand(command); + sensor_console::execute(command); led_effects::onBoardLEDOff(); } diff --git a/ampel-firmware/sensor_console.cpp b/ampel-firmware/sensor_console.cpp index 5f1876ddda8f2405b4f1a2eea253408123408824..5fc3377ced8a4d77b9337fad19c71483159ac74c 100644 --- a/ampel-firmware/sensor_console.cpp +++ b/ampel-firmware/sensor_console.cpp @@ -6,75 +6,112 @@ namespace sensor_console { uint8_t commands_count = 0; + enum input_type { + NONE, + INT32, + STRING + }; + struct Command { const char *name; union { + void (*voidFunction)(); void (*intFunction)(int32_t); - void (*voidFunction)(void); + void (*strFunction)(char*); }; const char *doc; - bool has_parameter; + 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]; - //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; + bool addCommand(const char *name, const __FlashStringHelper *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++; + 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) { - const char *doc = (const char*) doc_fstring; - if (commands_count < MAX_COMMANDS) { - commands[commands_count].name = name; + if (addCommand(name, doc_fstring)) { 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.")); + 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 function_name and an argument. - * Returns 0 if both are found, 1 if there is a problem and 2 if no argument is found. + * Tries to split a string command (e.g. 'mqtt 60' or 'show_csv') into + * a CommandLine struct (function_name, argument_type and argument) */ - 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) { + void parseCommand(const char *command, CommandLine &command_line) { + if (strlen(command) == 0) { Serial.println(F("Received empty command")); - // Empty string - return 1; + command_line.argument_type = NONE; + return; } - 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; - } + + 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 { - // No argument - code = 2; + 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(".")); } - return code; } /* @@ -88,7 +125,7 @@ namespace sensor_console { case '\n': // end of text Serial.println(); input_line[input_pos] = 0; - runCommand(input_line); + execute(input_line); input_pos = 0; break; case '\r': // discard carriage return @@ -112,50 +149,40 @@ namespace sensor_console { } } - 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. + * Tries to find the corresponding callback for a given command. Name and parameter type 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); - + void execute(const char *command_str) { + CommandLine input; + parseCommand(command_str, input); for (uint8_t i = 0; i < commands_count; i++) { - if (!strcmp(function_name, commands[i].name) && has_argument == commands[i].has_parameter) { + if (!strcmp(input.function_name, commands[i].name) && input.argument_type == commands[i].parameter_type) { 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.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; } - return; } } Serial.print(F("'")); - Serial.print(command); + Serial.print(command_str); Serial.println(F("' not supported. Available commands :")); listAvailableCommands(); } + } diff --git a/ampel-firmware/sensor_console.h b/ampel-firmware/sensor_console.h index b46e212e06c308e15ba7497509c33ffd104932ad..5cf4450d19535d19e871f5d4d48b11d09521070e 100644 --- a/ampel-firmware/sensor_console.h +++ b/ampel-firmware/sensor_console.h @@ -8,11 +8,13 @@ */ 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 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 processSerialInput(const byte in_byte); - void runCommand(const char *command); + + void execute(const char *command_line); } #endif diff --git a/ampel-firmware/util.cpp b/ampel-firmware/util.cpp index e3b2d6e8913172cce6e4dd868aace8c03e8be02f..5fcb36cb454c42b606fd632e7db9be5faf9108b8 100644 --- a/ampel-firmware/util.cpp +++ b/ampel-firmware/util.cpp @@ -7,6 +7,13 @@ namespace config { #if defined(ESP8266) const char *current_board = "ESP8266"; +# if !defined(AMPEL_WIFI) +void preinit() { + // WiFi would be initialized otherwise (on ESP8266), even if unused. + // see https://github.com/esp8266/Arduino/issues/2111#issuecomment-224251391 + ESP8266WiFiClass::preinitWiFiOff(); +} +# endif #elif defined(ESP32) const char *current_board = "ESP32"; #else @@ -74,11 +81,11 @@ char* getSensorId() { 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::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)")); + }, F("(Restarts the sensor)")); } Ampel ampel; diff --git a/ampel-firmware/util.h b/ampel-firmware/util.h index db424db6892d1beea14f3e4151c91356af464e4e..60406a891a776132bf936ee3b454304b5b74b5b7 100644 --- a/ampel-firmware/util.h +++ b/ampel-firmware/util.h @@ -38,6 +38,7 @@ class Ampel { private: static void showFreeSpace(); public: + const char *version = "v0.1.0"; // Update manually after significant changes. const char *board; const char *sensorId; uint32_t max_loop_duration; diff --git a/ampel-firmware/web_server.cpp b/ampel-firmware/web_server.cpp index 40cd15b3e7e5a1dae0777a36dfe85138f58bb7a5..b17e666d08ca932290d15f56061fd1d65ac4f8ea 100644 --- a/ampel-firmware/web_server.cpp +++ b/ampel-firmware/web_server.cpp @@ -115,6 +115,7 @@ namespace web_server { "<tr><td>Largest heap block</td><td>%6d bytes</td></tr>\n" "<tr><td>Max loop duration</td><td>%5d ms</td></tr>\n" "<tr><td>Board</td><td>%s</td></tr>\n" + "<tr><td>Ampel firmware</td><td>%s</td></tr>\n" "<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" @@ -219,8 +220,8 @@ namespace web_server { #endif ); - Serial.print(F("INFO - Header size : ")); - Serial.print(strlen(content)); + // Serial.print(F("INFO - Header size : ")); + // Serial.print(strlen(content)); http.setContentLength(CONTENT_LENGTH_UNKNOWN); http.send_P(200, PSTR("text/html"), content); @@ -239,11 +240,11 @@ namespace web_server { #endif config::temperature_offset, config::auto_calibrate_sensor ? "Yes" : "No", ampel.sensorId, ampel.sensorId, wifi::local_ip, wifi::local_ip, ESP.getFreeHeap(), esp_get_max_free_block_size(), ampel.max_loop_duration, - ampel.board, dd, hh, mm, ss); + ampel.board, ampel.version, dd, hh, mm, ss); - Serial.print(F(" - Body size : ")); + // Serial.print(F(" - Body size : ")); + // Serial.print(strlen(content)); http.sendContent(content); - Serial.print(strlen(content)); // Script snprintf_P(content, sizeof(content), script_template @@ -252,8 +253,8 @@ namespace web_server { #endif ); - Serial.print(F(" - Script size : ")); - Serial.println(strlen(content)); + // Serial.print(F(" - Script size : ")); + // Serial.println(strlen(content)); http.sendContent(content); } @@ -292,7 +293,7 @@ namespace web_server { } http.sendHeader("Location", "/"); http.send(303); - sensor_console::runCommand(http.arg("send").c_str()); + sensor_console::execute(http.arg("send").c_str()); } void handlePageNotFound() { diff --git a/ampel-firmware/wifi_util.cpp b/ampel-firmware/wifi_util.cpp index a845140a8aa80e66c0279c7b74e486847aaf6355..4cdc7a2a597d3d66523c6e37b837346e5231311e 100644 --- a/ampel-firmware/wifi_util.cpp +++ b/ampel-firmware/wifi_util.cpp @@ -14,8 +14,38 @@ namespace config { namespace wifi { char local_ip[16]; // "255.255.255.255\0" + + void scanNetworks() { + Serial.println(); + Serial.println(F("WiFi - Scanning...")); + bool async = false; + bool showHidden = true; + int n = WiFi.scanNetworks(async, showHidden); + for (int i = 0; i < n; ++i) { + Serial.print(F(" * '")); + Serial.print(WiFi.SSID(i)); + Serial.print(F("' (")); + int16_t quality = 2 * (100 + WiFi.RSSI(i)); + Serial.print(util::min(util::max(quality, 0), 100)); + Serial.println(F(" %)")); + } + Serial.println(F("Done!")); + Serial.println(); + } + + void showLocalIp() { + Serial.print(F("WiFi - Local IP : ")); + Serial.println(wifi::local_ip); + Serial.print(F("WiFi - SSID : ")); + Serial.println(WIFI_SSID); + } + // Initialize Wi-Fi void connect(const char *hostname) { + + sensor_console::defineCommand("wifi_scan", scanNetworks, F("(Scans available WiFi networks)")); + sensor_console::defineCommand("local_ip", showLocalIp, F("(Displays local IP and current SSID)")); + //NOTE: WiFi Multi could allow multiple SSID and passwords. WiFi.persistent(false); // Don't write user & password to Flash. WiFi.mode(WIFI_STA); // Set ESP to be a WiFi-client only