Commit 566eb489 authored by Eric Duminil's avatar Eric Duminil
Browse files

Merge branch 'develop'

parents aa5e2240 a5807d86
Pipeline #3776 passed with stage
in 1 minute and 45 seconds
......@@ -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.
......
#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();
}
}
}
......@@ -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]
......
......@@ -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() {
......
......@@ -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);
......
......@@ -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
......@@ -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("-");
......
......@@ -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();
}
......
......@@ -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;
}
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(function_name, part1, MAX_COMMAND_SIZE);
arg = strtok(NULL, " ");
uint8_t code = 0;
if (arg) {
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;
argument = strtol(arg, &end, 10);
command_line.int_argument = strtol(command_line.str_argument, &end, 0); // Accepts 123 or 0xFF00FF
if (*end) {
// Second argument isn't a number
code = 2;
}
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;
}
}
}
Serial.print(F("'"));
Serial.print(command);
Serial.print(command_str);
Serial.println(F("' not supported. Available commands :"));
listAvailableCommands();
}
}
......@@ -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
......@@ -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;
......@@ -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;
......
......@@ -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() {
......
......@@ -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
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment