Commit 208eadd3 authored by Eric Duminil's avatar Eric Duminil
Browse files

Merge branch 'develop'

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