Commit 983003b4 authored by Eric Duminil's avatar Eric Duminil
Browse files

Merge branch 'develop'

parents a200c6f8 86309b4a
......@@ -6,6 +6,18 @@ It measures the current CO<sub>2</sub> concentration (in ppm), and displays it o
The room should be ventilated as soon as one LED turns red.
## Features
The *CO<sub>2</sub> Ampel* can:
* Display CO2 concentration on LED ring.
* Allow calibration.
* Get current time over NTP
* Send data over MQTT.
* Send data over LoRaWAN.
* Display measurements and configuration on a small website.
* Log data to a CSV file, directly on the ESP flash memory.
## Hardware Requirements
* [ESP8266](https://en.wikipedia.org/wiki/ESP8266) or [ESP32](https://en.wikipedia.org/wiki/ESP32) microcontroller (this project has been tested with *ESP8266 ESP-12 WIFI* and *TTGO ESP32 SX1276 LoRa*)
......
......@@ -5,25 +5,33 @@
*****************************************************************/
#include "config.h"
#ifndef MEASUREMENT_TIMESTEP
# error Missing config.h file. Please copy config.example.h to config.h.
# error Missing config.h file. Please copy config.public.h to config.h.
#endif
#ifdef MQTT
# include "mqtt.h"
#ifdef AMPEL_CSV
# include "csv_writer.h"
#endif
#include "util.h"
#include "wifi_util.h"
#include "co2_sensor.h"
#ifdef AMPEL_WIFI
# include "wifi_util.h"
# ifdef AMPEL_MQTT
# include "mqtt.h"
# endif
# ifdef AMPEL_HTTP
# include "web_server.h"
# endif
#endif
#ifdef HTTP
# include "web_server.h"
#ifdef AMPEL_LORAWAN
# include "lorawan.h"
#endif
#include "util.h"
#include "co2_sensor.h"
#include "led_effects.h"
#include "csv_writer.h"
#if defined(ESP8266)
//allows sensor to be seen as SENSOR_ID.local, from the local network. For example : espd05cc9.local
//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>
......
......@@ -59,14 +59,14 @@
* Setup *
*****************************************************************/
void setup() {
LedEffects::setupOnBoardLED();
LedEffects::onBoardLEDOff();
led_effects::setupOnBoardLED();
led_effects::onBoardLEDOff();
Serial.begin(BAUDS);
pinMode(0, INPUT); // Flash button (used for forced calibration)
pinMode(0, INPUT); // Flash button (used for forced calibration)
LedEffects::setupRing();
led_effects::setupRing();
sensor::initialize();
......@@ -75,6 +75,7 @@ void setup() {
Serial.print(F("Board : "));
Serial.println(BOARD);
#ifdef AMPEL_WIFI
// Try to connect to Wi-Fi
WiFiConnect(SENSOR_ID);
......@@ -82,9 +83,9 @@ void setup() {
Serial.println(WiFi.status());
if (WiFi.status() == WL_CONNECTED) {
#ifdef HTTP
# ifdef AMPEL_HTTP
web_server::initialize();
#endif
# endif
ntp::initialize();
......@@ -95,11 +96,19 @@ void setup() {
Serial.println(F("Error setting up MDNS responder!"));
}
#ifdef MQTT
# ifdef AMPEL_MQTT
mqtt::initialize("CO2sensors/" + SENSOR_ID);
#endif
# endif
}
#endif
#ifdef AMPEL_CSV
csv_writer::initialize();
#endif
#if defined(AMPEL_LORAWAN) && defined(ESP32)
lorawan::initialize();
#endif
}
/*****************************************************************
......@@ -107,73 +116,36 @@ void setup() {
*****************************************************************/
void loop() {
#if defined(AMPEL_LORAWAN) && defined(ESP32)
//LMIC Library seems to be very sensitive to timing issues, so run it first.
lorawan::process();
if (lorawan::waiting_for_confirmation) {
// If node is waiting for join confirmation from Gateway, nothing else should run.
return;
}
#endif
//NOTE: Loop should never take more than 1000ms. Split in smaller methods and logic if needed.
//TODO: Restart every day or week, in order to not let t0 overflow?
uint32_t t0 = millis();
/**
* USER INTERACTION
*/
keepServicesAlive();
// Short press for night mode, Long press for calibration.
checkFlashButton();
/**
* GET DATA
*/
bool freshData = sensor::scd30.dataAvailable(); // Alternative : close to time-step AND dataAvailable, to avoid asking the sensor too often.
if (freshData) {
sensor::co2 = sensor::scd30.getCO2();
sensor::temperature = sensor::scd30.getTemperature();
sensor::humidity = sensor::scd30.getHumidity();
}
//NOTE: Data is available, but it's sometimes erroneous: the sensor outputs zero ppm but non-zero temperature and non-zero humidity.
if (sensor::co2 <= 0) {
// No measurement yet. Waiting.
LedEffects::showWaitingLED(color::blue);
return;
}
/**
* Fresh data. Show it and send it if needed.
*/
if (freshData) {
sensor::timestamp = ntp::getLocalTime();
Serial.println(sensor::timestamp);
Serial.print(F("co2(ppm): "));
Serial.print(sensor::co2);
Serial.print(F(" temp(C): "));
Serial.print(sensor::temperature);
Serial.print(F(" humidity(%): "));
Serial.println(sensor::humidity);
if (sensor::processData()) {
#ifdef AMPEL_CSV
csv_writer::logIfTimeHasCome(sensor::timestamp, sensor::co2, sensor::temperature, sensor::humidity);
#endif
#ifdef MQTT
#if defined(AMPEL_WIFI) && defined(AMPEL_MQTT)
mqtt::publishIfTimeHasCome(sensor::timestamp, sensor::co2, sensor::temperature, sensor::humidity);
#endif
}
if (sensor::co2 < 250) {
// Sensor should be calibrated.
LedEffects::showWaitingLED(color::magenta);
return;
}
/**
* Display data, even if it's "old" (with breathing).
* Those effects include a short delay.
*/
if (sensor::co2 < 2000) {
LedEffects::displayCO2color(sensor::co2);
LedEffects::breathe(sensor::co2);
} else { // >= 2000: entire ring blinks red
LedEffects::redAlert();
#if defined(AMPEL_LORAWAN) && defined(ESP32)
lorawan::preparePayloadIfTimeHasCome(sensor::co2, sensor::temperature, sensor::humidity);
#endif
}
uint32_t duration = millis() - t0;
......@@ -193,34 +165,36 @@ void loop() {
*/
void checkFlashButton() {
if (!digitalRead(0)) { // Button has been pressed
LedEffects::onBoardLEDOn();
led_effects::onBoardLEDOn();
delay(300);
if (digitalRead(0)) {
Serial.println(F("Flash has been pressed for a short time. Should toggle night mode."));
LedEffects::toggleNightMode();
led_effects::toggleNightMode();
} else {
Serial.println(F("Flash has been pressed for a long time. Keep it pressed for calibration."));
if (LedEffects::countdownToZero() < 0) {
if (led_effects::countdownToZero() < 0) {
sensor::startCalibrationProcess();
}
}
LedEffects::onBoardLEDOff();
led_effects::onBoardLEDOff();
}
}
void keepServicesAlive() {
#ifdef AMPEL_WIFI
if (WiFi.status() == WL_CONNECTED) {
#if defined(ESP8266)
# if defined(ESP8266)
//NOTE: Sadly, there seems to be a bug in the current MDNS implementation.
// It stops working after 2 minutes. And forcing a restart leads to a memory leak.
MDNS.update();
#endif
# endif
ntp::update(); // NTP client has its own timer. It will connect to NTP server every 60s.
#ifdef HTTP
# ifdef AMPEL_HTTP
web_server::update();
#endif
#ifdef MQTT
# endif
# ifdef AMPEL_MQTT
mqtt::keepConnection(); // MQTT client has its own timer. It will keep alive every 15s.
#endif
# endif
}
#endif
}
......@@ -2,17 +2,17 @@
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)
const uint16_t altitude_above_sea_level = ALTITUDE_ABOVE_SEA_LEVEL; // [m]
uint16_t co2_calibration_level = ATMOSPHERIC_CO2_CONCENTRATION; // [ppm]
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]
#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.
const float temperature_offset = TEMPERATURE_OFFSET; // [K]
const float temperature_offset = TEMPERATURE_OFFSET; // [K]
#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]
const bool auto_calibrate_sensor = AUTO_CALIBRATE_SENSOR; // [true / false]
}
namespace sensor {
......@@ -21,13 +21,16 @@ namespace sensor {
float temperature = 0;
float humidity = 0;
String timestamp = "";
int16_t stable_measurements = 0;
uint32_t waiting_color = color::blue;
bool should_calibrate = false;
void initialize() {
#if defined(ESP8266)
Wire.begin(12, 14); // ESP8266 - D6, D5;
#endif
#if defined(ESP32)
Wire.begin(21, 22); // ESP32
Wire.begin(21, 22); // ESP32
/**
* SCD30 ESP32
* VCC --- 3V3
......@@ -41,46 +44,146 @@ namespace sensor {
if (scd30.begin(config::auto_calibrate_sensor) == false) {
Serial.println("Air sensor not detected. Please check wiring. Freezing...");
while (1) {
LedEffects::showWaitingLED(color::red);
led_effects::showWaitingLED(color::red);
}
}
// SCD30 has its own timer.
Serial.println("\nSetting SCD30 timestep to " + String(config::measurement_timestep) + " s.");
scd30.setMeasurementInterval(config::measurement_timestep); // [s]
//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);
Serial.println(" s.");
scd30.setMeasurementInterval(config::measurement_timestep); // [s]
Serial.print("Setting temperature offset to -");
Serial.print(F("Setting temperature offset to -"));
Serial.print(abs(config::temperature_offset));
Serial.println(" K.");
scd30.setTemperatureOffset(abs(config::temperature_offset)); // setTemperatureOffset only accepts positive numbers, but shifts the temperature down.
delay(100);
Serial.print("Temperature offset is : -");
Serial.print(F("Temperature offset is : -"));
Serial.print(scd30.getTemperatureOffset());
Serial.println(" K");
Serial.print("Auto-calibration is ");
Serial.print(F("Auto-calibration is "));
Serial.println(config::auto_calibrate_sensor ? "ON." : "OFF.");
}
// Force SCD30 calibration with countdown.
//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(now - previous_measurement_at - config::measurement_timestep * 1000);
Serial.println(" ms.");
previous_measurement_at = now;
}
void countStableMeasurements() {
static int16_t previous_co2 = 0;
if (co2 > (previous_co2 - 30) && co2 < (previous_co2 + 30)) {
stable_measurements++;
Serial.print(F("Number of stable measurements : "));
Serial.println(stable_measurements);
waiting_color = color::green;
} else {
stable_measurements = 0;
waiting_color = color::red;
}
previous_co2 = co2;
}
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.
*/
Serial.println("Setting SCD30 timestep to 2s, prior to calibration.");
scd30.setMeasurementInterval(2); // [s] The change will only take effect after next measurement.
LedEffects::showKITTWheel(color::blue, config::measurement_timestep);
Serial.println("Waiting 2 minutes.");
LedEffects::showKITTWheel(color::blue, 120);
Serial.print("Starting SCD30 calibration...");
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.println(F("Waiting until the measurements are stable for at least 2 minutes."));
Serial.println(F("It could take a very long time."));
should_calibrate = true;
}
void calibrateAndRestart() {
Serial.print(F("Calibrating SCD30 now..."));
scd30.setAltitudeCompensation(config::altitude_above_sea_level);
scd30.setForcedRecalibrationFactor(config::co2_calibration_level);
Serial.println(" Done!");
Serial.println("Sensor calibrated.");
Serial.println("Sensor will now restart.");
LedEffects::showKITTWheel(color::green, 5);
FS_LIB.end();
ESP.restart();
Serial.println(F(" Done!"));
Serial.println(F("Sensor calibrated."));
ESP.restart(); // softer than ESP.reset
}
void logToSerial() {
Serial.println(timestamp);
Serial.print(F("co2(ppm): "));
Serial.print(co2);
Serial.print(F(" temp(C): "));
Serial.print(temperature, 1);
Serial.print(F(" humidity(%): "));
Serial.println(humidity, 1);
}
void displayCO2OnLedRing() {
if (co2 < 250) {
// Sensor should be calibrated.
led_effects::showWaitingLED(color::magenta);
return;
}
/**
* Display data, even if it's "old" (with breathing).
* Those effects include a short delay.
*/
if (co2 < 2000) {
led_effects::displayCO2color(co2);
led_effects::breathe(co2);
} else {
// >= 2000: entire ring blinks red
led_effects::redAlert();
}
}
/** Gets fresh data if available, checks calibration status, displays CO2 levels.
* Returns true if fresh data is available, for further processing (e.g. MQTT, CSV or LoRa)
*/
bool processData() {
bool freshData = scd30.dataAvailable();
if (freshData) {
// checkTimerDeviation();
timestamp = ntp::getLocalTime();
co2 = scd30.getCO2();
temperature = scd30.getTemperature();
humidity = scd30.getHumidity();
}
//NOTE: Data is available, but it's sometimes erroneous: the sensor outputs zero ppm but non-zero temperature and non-zero humidity.
if (co2 <= 0) {
// No measurement yet. Waiting.
led_effects::showWaitingLED(color::blue);
return false;
}
/**
* Fresh data. Log it and send it if needed.
*/
if (freshData) {
if (should_calibrate) {
countStableMeasurements();
}
logToSerial();
}
if (should_calibrate) {
if (stable_measurements == 60) {
calibrateAndRestart();
}
led_effects::showWaitingLED(waiting_color);
return false;
}
displayCO2OnLedRing();
return freshData;
}
}
......@@ -6,13 +6,13 @@
#include "src/lib/SparkFun_SCD30_Arduino_Library/src/SparkFun_SCD30_Arduino_Library.h" // From: http://librarymanager/All#SparkFun_SCD30
#include "config.h"
#include "led_effects.h"
#include "csv_writer.h" // To close filesystem before restart.
#include "util.h"
#include <Wire.h>
namespace config {
extern uint16_t measurement_timestep; // [s] Value between 2 and 1800 (range for SCD30 sensor)
extern uint16_t measurement_timestep; // [s] Value between 2 and 1800 (range for SCD30 sensor)
extern const bool auto_calibrate_sensor; // [true / false]
extern uint16_t co2_calibration_level; // [ppm]
extern uint16_t co2_calibration_level; // [ppm]
extern const float temperature_offset; // [K] Sign isn't relevant.
}
......@@ -24,6 +24,7 @@ namespace sensor {
extern String timestamp;
void initialize();
bool processData();
void startCalibrationProcess();
}
#endif
......@@ -3,14 +3,24 @@
// This file is a config template, and can be copied to config.h. Please don't save any important password in this template.
/**
* SERVICES
*/
// Comment or remove those lines if you want to disable the corresponding services
# define AMPEL_WIFI // Should ESP connect to WiFi? It allows the Ampel to get time from an NTP server.
# define AMPEL_HTTP // Should HTTP web server be started? (AMPEL_WIFI should be enabled too)
# define AMPEL_MQTT // Should data be sent over MQTT? (AMPEL_WIFI should be enabled too)
# define AMPEL_CSV // Should data be logged as CSV, on the ESP flash memory?
// # define AMPEL_LORAWAN // Should data be sent over LoRaWAN? (Requires ESP32 + LoRa modem, and "MCCI LoRaWAN LMIC library")
/**
* WIFI
*/
// Setting WIFI_SSID to "NO_WIFI" will disable WiFi completely, and all other dependent services (MQTT, HTTP, NTP, ...)
# define WIFI_SSID "NO_WIFI"
# define WIFI_SSID "MY_SSID"
# define WIFI_PASSWORD "P4SSW0RD"
# define WIFI_TIMEOUT 20 // [s]
# define WIFI_TIMEOUT 30 // [s]
/**
* Sensor
......@@ -20,15 +30,10 @@
//NOTE: SCD30 timer does not seem to be very precise. Variations may occur.
# define MEASUREMENT_TIMESTEP 60 // [s] Value between 2 and 1800 (range for SCD30 sensor)
// How often measurements should 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 SENDING_INTERVAL MEASUREMENT_TIMESTEP * 5 // [s]
# define SENDING_INTERVAL 300 // [s]
// How often should measurements be appended to CSV ?
// 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
// WARNING: Writing too often might damage the ESP memory
# define CSV_INTERVAL 300 // [s]
// Residual heat from CO2 sensor seems to be high enough to change the temperature reading. How much should it be offset?
......@@ -64,29 +69,26 @@
* available at http://local_ip, with user HTTP_USER and password HTTP_PASSWORD
*/
# define HTTP // Comment or remove this line if you want to disable HTTP webserver
// Define empty strings in order to disable authentication, or remove the constants altogether.
# define HTTP_USER "co2ampel"
# define HTTP_PASSWORD "my_password"
/**
* MQTT SERVER
* MQTT
*/
# define MQTT // Comment or remove this line if you want to disable MQTT
/*
* If MQTT is enabled, co2ampel will publish data every SENDING_INTERVAL seconds.
* If AMPEL_MQTT is enabled, co2ampel will publish data every MQTT_SENDING_INTERVAL seconds.
* An MQTT subscriber can then get the data from the corresponding broker, either encrypted or unencrypted:
*
* ❯ mosquitto_sub -h 'test.mosquitto.org' -p 8883 -t 'CO2sensors/#' --cafile mosquitto.org.crt -v
* CO2sensors/ESPd05cc9 {"time":"2020-12-13 13:14:37+01", "co2":571, "temp":18.9, "rh":50.9}
* CO2sensors/ESPd05cc9 {"time":"2020-12-13 13:14:48+01", "co2":573, "temp":18.9, "rh":50.2}
* CO2sensors/ESPd03cc5 {"time":"2020-12-13 13:14:37+01", "co2":571, "temp":18.9, "rh":50.9}
* CO2sensors/ESPd03cc5 {"time":"2020-12-13 13:14:48+01", "co2":573, "temp":18.9, "rh":50.2}
* ...
*
* ❯ mosquitto_sub -h 'test.mosquitto.org' -t 'CO2sensors/#' -v
* CO2sensors/ESPd05cc9 {"time":"2020-12-13 13:15:09+01", "co2":568, "temp":18.9, "rh":50.1}
* CO2sensors/ESPd05cc9 {"time":"2020-12-13 13:15:20+01", "co2":572, "temp":18.9, "rh":50.3}
* CO2sensors/ESPd03cc5 {"time":"2020-12-13 13:15:09+01", "co2":568, "temp":18.9, "rh":50.1}
* CO2sensors/ESPd03cc5 {"time":"2020-12-13 13:15:20+01", "co2":572, "temp":18.9, "rh":50.3}
* ...
*/
......@@ -98,12 +100,45 @@
*/
# define ALLOW_MQTT_COMMANDS false
// How often measurements should 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]
# define MQTT_SENDING_INTERVAL 60 // [s]
# define MQTT_SERVER "test.mosquitto.org" // MQTT server URL or IP address
# define MQTT_PORT 8883
# define MQTT_USER ""
# define MQTT_PASSWORD ""
# define MQTT_SERVER_FINGERPRINT "EE BC 4B F8 57 E3 D3 E4 07 54 23 1E F0 C8 A1 56 E0 D3 1A 1C" // SHA1 for test.mosquitto.org
/**
* LoRaWAN
*/
// 1) Requires "MCCI LoRaWAN LMIC library", which will be automatically used with PlatformIO but should be added in "Arduino IDE".
// 2) Region and transceiver type should be specified in:
// * Arduino/libraries/MCCI_LoRaWAN_LMIC_library/project_config/lmic_project_config.h for Arduino IDE
// * platformio.ini for PlatformIO
// See https://github.com/mcci-catena/arduino-lmic#configuration for more information
// 3) It has been tested with "TTGO ESP32 SX1276 LoRa 868" and will only work with an ESP32 + LoRa modem
// 4) In order to use LoRaWAN, a gateway should be close to the co2ampel, and an account, an application and a device should be registered,
// e.g. on https://www.thethingsnetwork.org/docs/applications/
// 5) The corresponding keys should be defined in LORAWAN_DEVICE_EUI, LORAWAN_APPLICATION_EUI and LORAWAN_APPLICATION_KEY
// How often should measurements be sent over LoRaWAN?
# define LORAWAN_SENDING_INTERVAL 300 // [s] This value should not be too low. See https://www.thethingsnetwork.org/docs/lorawan/duty-cycle.html#maximum-duty-cycle
// WARNING: If AMPEL_LORAWAN is enabled, you need to modify the 3 following constants!
// This EUI must be in little-endian format, so least-significant-byte first.
// When copying an EUI from ttnctl output, this means to reverse the bytes.
# define LORAWAN_DEVICE_EUI {0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11}
// This should also be in little endian format, see above.
// For TheThingsNetwork issued EUIs the last bytes should be 0xD5, 0xB3, 0x70.
# define LORAWAN_APPLICATION_EUI {0x00, 0x00, 0x00, 0x00, 0x00, 0xD5, 0xB3, 0x70}
// This key should be in big endian format (or, since it is not really a
// number but a block of memory, endianness does not really apply). In
// practice, a key taken from ttnctl can be copied as-is.
# define LORAWAN_APPLICATION_KEY {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }
/**
* NTP
*/
......
#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]
......@@ -45,9 +47,7 @@ namespace csv_writer {
}
}
}
#endif
#if defined(ESP32)
#elif defined(ESP32)
/**
* SPECIFIC FUNCTIONS FOR SPIFFS
*/
......@@ -83,7 +83,6 @@ namespace csv_writer {
const String filename = "/" + SENSOR_ID + ".csv";
int getAvailableSpace() {
//TODO : Check if too low?
return getTotalSpace() - getUsedSpace();
}
......@@ -135,32 +134,36 @@ namespace csv_writer {
return csv_file;
}
void logIfTimeHasCome(const String &timeStamp, int16_t co2, float temp, float hum) {
void log(const String &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);
if (csv_file) {
size_t written_bytes = csv_file.print(csv_line);
csv_file.close();
if (written_bytes == 0) {
Serial.println(F("Nothing written. Disk full?"));
} else {
Serial.println(F("Wrote file content:"));
Serial.print(csv_line);
last_successful_write = ntp::getLocalTime();
}
updateFsInfo();
delay(50);
} else {
//NOTE: Can it ever happen that outfile is false?
Serial.println(F("Problem on create file!"));
}
led_effects::onBoardLEDOff();
}
void logIfTimeHasCome(const String &timeStamp, const int16_t &co2, const float &temperature, const float &humidity) {
unsigned long now = seconds();
//TODO: Write average since last CSV write?
if (now - last_written_at > config::csv_interval) {
last_written_at = now;
LedEffects::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, temp, hum);
if (csv_file) {
size_t written_bytes = csv_file.print(csv_line);
csv_file.close();
if (written_bytes == 0) {
Serial.println(F("Nothing written. Disk full?"));
} else {
Serial.println(F("Wrote file content:"));
Serial.print(csv_line);
last_successful_write = ntp::getLocalTime();
}
updateFsInfo();
delay(50);
} else {
//NOTE: Can it ever happen that outfile is false?
Serial.println(F("Problem on create file!"));
}
LedEffects::onBoardLEDOff();
log(timeStamp, co2, temperature, humidity);
}
}
}
......@@ -11,15 +11,17 @@
# error Board should be either ESP8266 or ESP832
#endif
#include "led_effects.h"
#include "config.h"
#include "util.h"
#include "led_effects.h"
namespace config {
extern uint16_t csv_interval; // [s]
extern uint16_t csv_interval; // [s]
}
namespace csv_writer {
extern String last_successful_write;
void initialize();
void logIfTimeHasCome(const String &timeStamp, int16_t co2, float temp, float hum);
void logIfTimeHasCome(const String &timeStamp, const int16_t &co2, const float &temperature, const float &humidity);
int getAvailableSpace();
extern const String filename;
}
......
......@@ -11,34 +11,29 @@ namespace config {
/*****************************************************************
* Configuration (calculated from above values) *
*****************************************************************/
namespace config //TODO: Use a class instead. NightMode could then be another state.
namespace config //NOTE: Use a class instead? NightMode could then be another state.
{
const float average_brightness = 0.5 * (config::max_brightness + config::min_brightness);
const float brightness_amplitude = 0.5 * (config::max_brightness - config::min_brightness);
const uint8_t brightness_amplitude = config::max_brightness - config::min_brightness;
bool night_mode = false;
}
// Adafruit NeoPixel (Arduino library for controlling single-wire-based LED pixels and strip)
// https://github.com/adafruit/Adafruit_NeoPixel
// Documentation : http://adafruit.github.io/Adafruit_NeoPixel/html/class_adafruit___neo_pixel.html
// NeoPixels on GPIO05, aka D1 on ESP8266 or 5 on ESP32.
#if defined(ESP8266)
// NeoPixels on GPIO05, aka D1 on ESP8266.
const int NEOPIXELS_PIN = 5;
#elif defined(ESP32)
// NeoPixels on GPIO23 on ESP32. To avoid conflict with LoRa_SCK on TTGO.
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]
// 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[NUMPIXELS] = { 21845, 19114, 16383, 13653, 10922, 8191, 5461, 2730, 0, 0, 0, 0 }; // [hue angle]
const uint16_t LED_HUES[NUMPIXELS] = { 21845U, 19114U, 16383U, 13653U, 10922U, 8191U, 5461U, 2730U, 0, 0, 0, 0 }; // [hue angle]
Adafruit_NeoPixel pixels(NUMPIXELS, NEOPIXELS_PIN, NEO_GRB + NEO_KHZ800);
namespace counter {
uint16_t wheel_offset = 0;
uint16_t kitt_offset = 0;
uint16_t breathing_offset = 0;
} // namespace counter
namespace LedEffects {
namespace led_effects {
//On-board LED on D4, aka GPIO02
const int ONBOARD_LED_PIN = 2;
......@@ -47,25 +42,39 @@ namespace LedEffects {
}
void onBoardLEDOff() {
//NOTE: OFF is LOW on ESP32 and HIGH on ESP8266 :-/
#ifdef ESP8266
digitalWrite(ONBOARD_LED_PIN, HIGH);
#else
digitalWrite(ONBOARD_LED_PIN, LOW);
#endif
}
void onBoardLEDOn() {
#ifdef ESP8266
digitalWrite(ONBOARD_LED_PIN, LOW);
#else
digitalWrite(ONBOARD_LED_PIN, HIGH);
#endif
}
void LEDsOff() {
pixels.clear();
pixels.show();
onBoardLEDOff();
}
void setupRing() {
pixels.begin();
pixels.setBrightness(config::max_brightness);
pixels.clear();
LEDsOff();
}
void toggleNightMode() {
config::night_mode = !config::night_mode;
if (config::night_mode) {
Serial.println(F("NIGHT MODE!"));
pixels.clear();
pixels.show();
LEDsOff();
} else {
Serial.println(F("DAY MODE!"));
}
......@@ -77,13 +86,14 @@ namespace LedEffects {
if (config::night_mode) {
return;
}
static uint16_t kitt_offset = 0;
pixels.clear();
for (int j = config::kitt_tail; j >= 0; j--) {
int ledNumber = abs((counter::kitt_offset - j + NUMPIXELS) % (2 * NUMPIXELS) - NUMPIXELS) % NUMPIXELS; // Triangular function
int ledNumber = abs((kitt_offset - j + NUMPIXELS) % (2 * NUMPIXELS) - NUMPIXELS) % NUMPIXELS; // Triangular function
pixels.setPixelColor(ledNumber, color * pixels.gamma8(255 - j * 76) / 255);
}
pixels.show();
counter::kitt_offset += 1;
kitt_offset++;
}
// Start K.I.T.T. led effect. Red color as default.
......@@ -129,16 +139,17 @@ namespace LedEffects {
pixels.show();
}
void showRainbowWheel(int duration_s, uint16_t hue_increment) {
void showRainbowWheel(int duration_ms, uint16_t hue_increment) {
if (config::night_mode) {
return;
}
unsigned long t0 = seconds();
static uint16_t wheel_offset = 0;
unsigned long t0 = millis();
pixels.setBrightness(config::max_brightness);
while (seconds() < t0 + duration_s) {
while (millis() < t0 + duration_ms) {
for (int i = 0; i < NUMPIXELS; i++) {
pixels.setPixelColor(i, pixels.ColorHSV(i * 65535 / NUMPIXELS + counter::wheel_offset));
counter::wheel_offset += hue_increment;
pixels.setPixelColor(i, pixels.ColorHSV(i * 65535 / NUMPIXELS + wheel_offset));
wheel_offset += hue_increment;
}
pixels.show();
delay(10);
......@@ -163,14 +174,14 @@ namespace LedEffects {
void breathe(int16_t co2) {
if (!config::night_mode) {
//TODO: use integer sine
pixels.setBrightness(
static_cast<int>(config::average_brightness
+ cos(counter::breathing_offset * 0.1) * config::brightness_amplitude));
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();
counter::breathing_offset += 1;
breathing_offset += 3; // breathing speed. +3 looks like slow human breathing.
}
delay(co2 > 1600 ? 50 : 100); // faster breathing for higher CO2 values
delay(co2 > 1600 ? 50 : 100); // faster breathing for higher CO2 values
}
/**
......@@ -179,7 +190,7 @@ namespace LedEffects {
*/
int countdownToZero() {
if (config::night_mode) {
Serial.println("Night mode. Not doing anything.");
Serial.println(F("Night mode. Not doing anything."));
delay(1000); // Wait for a while, to avoid coming back to this function too many times when button is pressed.
return 1;
}
......
#ifndef LED_EFFECTS_H_INCLUDED
#define LED_EFFECTS_H_INCLUDED
#include <Arduino.h>
#include "util.h"
#include "config.h"
// Adafruit NeoPixel (Arduino library for controlling single-wire-based LED pixels and strip)
// https://github.com/adafruit/Adafruit_NeoPixel
// Documentation : http://adafruit.github.io/Adafruit_NeoPixel/html/class_adafruit___neo_pixel.html
#include "src/lib/Adafruit_NeoPixel/Adafruit_NeoPixel.h"
namespace color {
......@@ -13,11 +16,12 @@ namespace color {
const uint32_t magenta = 0xFF00FF;
}
namespace LedEffects {
namespace led_effects {
void setupOnBoardLED();
void onBoardLEDOff();
void onBoardLEDOn();
void toggleNightMode();
void LEDsOff();
void setupRing();
void redAlert();
......@@ -25,7 +29,7 @@ namespace LedEffects {
int countdownToZero();
void showWaitingLED(uint32_t color);
void showKITTWheel(uint32_t color, uint16_t duration_s = 2);
void showRainbowWheel(int duration_s = 1, uint16_t hue_increment = 50);
void showRainbowWheel(int duration_ms = 1000, uint16_t hue_increment = 50);
void displayCO2color(uint16_t co2);
}
#endif
#include "lorawan.h"
#if defined(AMPEL_LORAWAN) && defined(ESP32)
namespace config {
// Values should be defined in config.h
uint16_t lorawan_sending_interval = LORAWAN_SENDING_INTERVAL; // [s]
static const u1_t PROGMEM APPEUI[8] = LORAWAN_APPLICATION_EUI;
static const u1_t PROGMEM DEVEUI[8] = LORAWAN_DEVICE_EUI;
static const u1_t PROGMEM APPKEY[16] = LORAWAN_APPLICATION_KEY;
}
// Payloads will be automatically sent via MQTT by TheThingsNetwork, and can be seen with:
// mosquitto_sub -h eu.thethings.network -t '+/devices/+/up' -u 'APPLICATION-NAME' -P 'ttn-account-v2.4xxxxxxxx-xxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxx' -v
// or encrypted:
// mosquitto_sub -h eu.thethings.network -t '+/devices/+/up' -u 'APPLICATION-NAME' -P 'ttn-account-v2.4xxxxxxxx-xxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxx' -v --cafile mqtt-ca.pem -p 8883
// ->
// co2ampel-test/devices/esp3a7c94/up {"app_id":"co2ampel-test","dev_id":"esp3a7c94","hardware_serial":"00xxxxxxxx","port":1,"counter":5,"payload_raw":"TJd7","payload_fields":{"co2":760,"rh":61.5,"temp":20.2},"metadata":{"time":"2020-12-23T23:00:51.44020438Z","frequency":867.5,"modulation":"LORA","data_rate":"SF7BW125","airtime":51456000,"coding_rate":"4/5","gateways":[{"gtw_id":"eui-xxxxxxxxxxxxxxxxxx","timestamp":1765406908,"time":"2020-12-23T23:00:51.402519Z","channel":5,"rssi":-64,"snr":7.5,"rf_chain":0,"latitude":22.7,"longitude":114.24,"altitude":450}]}}
// More info : https://www.thethingsnetwork.org/docs/applications/mqtt/quick-start.html
void os_getArtEui(u1_t *buf) {
memcpy_P(buf, config::APPEUI, 8);
}
void os_getDevEui(u1_t *buf) {
memcpy_P(buf, config::DEVEUI, 8);
}
void os_getDevKey(u1_t *buf) {
memcpy_P(buf, config::APPKEY, 16);
}
namespace lorawan {
bool waiting_for_confirmation = false;
bool connected = false;
String last_transmission = "";
void initialize() {
Serial.println(F("Starting LoRaWAN. Frequency plan : " LMIC_FREQUENCY_PLAN " MHz."));
// More info about pin mapping : https://github.com/mcci-catena/arduino-lmic#pin-mapping
// Has been tested successfully with ESP32 TTGO LoRa32 V1, and might work with other ESP32+LoRa boards.
const lmic_pinmap *pPinMap = Arduino_LMIC::GetPinmap_ThisBoard();
// LMIC init.
os_init_ex(pPinMap);
// Reset the MAC state. Session and pending data transfers will be discarded.
LMIC_reset();
// Join, but don't send anything yet.
LMIC_startJoining();
}
// 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.
void process() {
os_runloop_once();
}
void printHex2(unsigned v) {
v &= 0xff;
if (v < 16)
Serial.print('0');
Serial.print(v, HEX);
}
void onEvent(ev_t ev) {
Serial.print("LoRa - ");
Serial.print(ntp::getLocalTime());
Serial.print(" - ");
switch (ev) {
case EV_JOINING:
Serial.println(F("EV_JOINING"));
break;
case EV_JOINED:
waiting_for_confirmation = false;
connected = true;
led_effects::onBoardLEDOff();
Serial.println(F("EV_JOINED"));
{
u4_t netid = 0;
devaddr_t devaddr = 0;
u1_t nwkKey[16];
u1_t artKey[16];
LMIC_getSessionKeys(&netid, &devaddr, nwkKey, artKey);
Serial.print(F("netid: "));
Serial.println(netid, DEC);
Serial.print(F("devaddr: "));
Serial.println(devaddr, HEX);
Serial.print(F("AppSKey: "));
for (size_t i = 0; i < sizeof(artKey); ++i) {
if (i != 0)
Serial.print("-");
printHex2(artKey[i]);
}
Serial.println("");
Serial.print("NwkSKey: ");
for (size_t i = 0; i < sizeof(nwkKey); ++i) {
if (i != 0)
Serial.print("-");
printHex2(nwkKey[i]);
}
Serial.println();
}
Serial.println(F("Other services may resume, and will not be frozen anymore."));
// Disable link check validation (automatically enabled during join)
LMIC_setLinkCheckMode(0);
break;
case EV_JOIN_FAILED:
Serial.println(F("EV_JOIN_FAILED"));
break;
case EV_REJOIN_FAILED:
Serial.println(F("EV_REJOIN_FAILED"));
break;
case EV_TXCOMPLETE:
last_transmission = ntp::getLocalTime();
Serial.println(F("EV_TXCOMPLETE"));
break;
case EV_TXSTART:
waiting_for_confirmation = !connected;
Serial.println(F("EV_TXSTART"));
break;
case EV_TXCANCELED:
waiting_for_confirmation = false;
led_effects::onBoardLEDOff();
Serial.println(F("EV_TXCANCELED"));
break;
case EV_JOIN_TXCOMPLETE:
waiting_for_confirmation = false;
led_effects::onBoardLEDOff();
Serial.println(F("EV_JOIN_TXCOMPLETE: no JoinAccept."));
Serial.println(F("Other services may resume."));
break;
default:
Serial.print(F("LoRa event: "));
Serial.println((unsigned) ev);
break;
}
if (waiting_for_confirmation) {
led_effects::onBoardLEDOn();
Serial.println(F("LoRa - waiting for OTAA confirmation. Freezing every other service!"));
}
}
void preparePayload(int16_t co2, float temperature, float humidity) {
// Check if there is not a current TX/RX job running
if (LMIC.opmode & OP_TXRXPEND) {
Serial.println(F("OP_TXRXPEND, not sending"));
} else {
uint8_t buff[3];
// Mapping CO2 from 0ppm to 5100ppm to [0, 255], with 20ppm increments.
buff[0] = (util::min(util::max(co2, 0), 5100) + 10) / 20;
// Mapping temperatures from [-10°C, 41°C] to [0, 255], with 0.2°C increment
buff[1] = static_cast<uint8_t>((util::min(util::max(temperature, -10), 41) + 10.1f) * 5);
// Mapping humidity from [0%, 100%] to [0, 200], with 0.5°C increment (0.4°C would also be possible)
buff[2] = static_cast<uint8_t>(util::min(util::max(humidity, 0) + 0.25f, 100) * 2);
Serial.print(F("LoRa - Payload : '"));
printHex2(buff[0]);
Serial.print(" ");
printHex2(buff[1]);
Serial.print(" ");
printHex2(buff[2]);
Serial.print(F("', "));
Serial.print(buff[0] * 20);
Serial.print(F(" ppm, "));
Serial.print(buff[1] * 0.2 - 10);
Serial.print(F(" °C, "));
Serial.print(buff[2] * 0.5);
Serial.println(F(" %."));
// Prepare upstream data transmission at the next possible time.
LMIC_setTxData2(1, buff, sizeof(buff), 0);
//NOTE: To decode in TheThingsNetwork:
//function Decoder(bytes, port) {
// return {
// co2: bytes[0] * 20,
// temp: bytes[1] / 5.0 - 10,
// rh: bytes[2] / 2.0
// };
//}
}
}
void preparePayloadIfTimeHasCome(const int16_t &co2, const float &temperature, const float &humidity) {
static unsigned long last_sent_at = 0;
unsigned long now = seconds();
if (connected && (now > last_sent_at + config::lorawan_sending_interval)) {
last_sent_at = now;
preparePayload(co2, temperature, humidity);
}
}
}
void onEvent(ev_t ev) {
lorawan::onEvent(ev);
}
#endif
#ifndef AMPEL_LORAWAN_H_
#define AMPEL_LORAWAN_H_
#include "config.h"
#if defined(AMPEL_LORAWAN) && defined(ESP32)
#include <Arduino.h>
// Requires "MCCI LoRaWAN LMIC library", which will be automatically used with PlatformIO but should be added in "Arduino IDE".
// Tested successfully with v3.2.0 and connected to a thethingsnetwork.org app.
#include <lmic.h>
#include <hal/hal.h>
#include <arduino_lmic_hal_boards.h>
#include <SPI.h>
#include "led_effects.h"
#include "util.h"
namespace config {
extern uint16_t lorawan_sending_interval; // [s]
}
#if defined(CFG_eu868)
# define LMIC_FREQUENCY_PLAN "Europe 868"
#elif defined(CFG_us915)
# define LMIC_FREQUENCY_PLAN "US 915"
#elif defined(CFG_au915)
# define LMIC_FREQUENCY_PLAN "Australia 915"
#elif defined(CFG_as923)
# define LMIC_FREQUENCY_PLAN "Asia 923"
#elif defined(CFG_kr920)
# define LMIC_FREQUENCY_PLAN "Korea 920"
#elif defined(CFG_in866)
# define LMIC_FREQUENCY_PLAN "India 866"
#else
# error "Region should be specified"
#endif
namespace lorawan {
extern bool waiting_for_confirmation;
extern bool connected;
extern String last_transmission;
void initialize();
void process();
void preparePayloadIfTimeHasCome(const int16_t &co2, const float &temp, const float &hum);
}
#endif
#endif
......@@ -2,7 +2,7 @@
namespace config {
// Values should be defined in config.h
uint16_t sending_interval = SENDING_INTERVAL; // [s]
uint16_t 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;
......@@ -22,6 +22,7 @@ PubSubClient mqttClient(espClient);
namespace mqtt {
unsigned long last_sent_at = 0;
unsigned long last_failed_at = 0;
bool connected = false;
String publish_topic;
const char *json_sensor_format;
......@@ -39,19 +40,19 @@ namespace mqtt {
void publish(const String &timestamp, int16_t co2, float temperature, float humidity) {
if (WiFi.status() == WL_CONNECTED && mqttClient.connected()) {
LedEffects::onBoardLEDOn();
led_effects::onBoardLEDOn();
Serial.print(F("Publishing MQTT message ... "));
char payload[75]; // Should be enough for json...
snprintf(payload, sizeof(payload), json_sensor_format, timestamp.c_str(), co2, temperature, humidity);
// Topic is the same as clientID. e.g. 'CO2sensors/ESP3d03da'
if (mqttClient.publish(publish_topic.c_str(), payload)) {
Serial.println("OK");
Serial.println(F("OK"));
last_successful_publish = ntp::getLocalTime();
} else {
Serial.println("Failed.");
Serial.println(F("Failed."));
}
LedEffects::onBoardLEDOff();
led_effects::onBoardLEDOff();
}
}
......@@ -64,7 +65,7 @@ namespace mqtt {
Serial.println("s.");
sensor::scd30.setMeasurementInterval(messageString.toInt());
config::measurement_timestep = messageString.toInt();
LedEffects::showKITTWheel(color::green, 1);
led_effects::showKITTWheel(color::green, 1);
}
}
......@@ -74,19 +75,21 @@ namespace mqtt {
Serial.print(F("Setting Sending Interval to : "));
Serial.print(config::sending_interval);
Serial.println("s.");
LedEffects::showKITTWheel(color::green, 1);
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.");
LedEffects::showKITTWheel(color::green, 1);
led_effects::showKITTWheel(color::green, 1);
}
#endif
void calibrateSensor(String messageString) {
void calibrateSensorToSpecificPPM(String messageString) {
messageString.replace("calibrate ", "");
long int calibrationLevel = messageString.toInt();
if (calibrationLevel >= 400 && calibrationLevel <= 2000) {
......@@ -106,7 +109,7 @@ namespace mqtt {
}
void sendInfoAboutLocalNetwork() {
char info_topic[60]; // Should be enough for "CO2sensors/ESPd05cc9/info"
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...
......@@ -128,7 +131,7 @@ namespace mqtt {
if (length == 0) {
return;
}
LedEffects::onBoardLEDOn();
led_effects::onBoardLEDOn();
Serial.print(F("Message arrived on topic: "));
Serial.print(sub_topic);
Serial.print(F(". Message: '"));
......@@ -143,32 +146,34 @@ namespace mqtt {
setCO2forDebugging(messageString);
} else if (messageString.startsWith("timer ")) {
setTimer(messageString);
} else if (messageString == "calibrate") {
sensor::startCalibrationProcess();
} else if (messageString.startsWith("calibrate ")) {
calibrateSensor(messageString);
// config::atmospheric_co2_concentration
calibrateSensorToSpecificPPM(messageString);
} else if (messageString.startsWith("mqtt ")) {
setMQTTinterval(messageString);
} else if (messageString.startsWith("csv ")) {
setCSVinterval(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();
LedEffects::showKITTWheel(color::blue, 2);
led_effects::showKITTWheel(color::blue, 2);
#endif
} else if (messageString == "night_mode") {
LedEffects::toggleNightMode();
led_effects::toggleNightMode();
} else if (messageString == "local_ip") {
sendInfoAboutLocalNetwork();
} else if (messageString == "reset") {
FS_LIB.end();
ESP.restart();
ESP.restart(); // softer than ESP.reset()
} else {
LedEffects::showKITTWheel(color::red, 1);
led_effects::showKITTWheel(color::red, 1);
Serial.println(F("Message not supported. Doing nothing."));
}
delay(50);
LedEffects::onBoardLEDOff();
led_effects::onBoardLEDOff();
}
void reconnect() {
......@@ -182,15 +187,16 @@ namespace mqtt {
}
Serial.print(F("Attempting MQTT connection..."));
LedEffects::onBoardLEDOn();
led_effects::onBoardLEDOn();
// Wait for connection, at most 15s (default)
mqttClient.connect(publish_topic.c_str(), config::mqtt_user, config::mqtt_password);
LedEffects::onBoardLEDOff();
led_effects::onBoardLEDOff();
connected = mqttClient.connected();
if (mqttClient.connected()) {
//TODO: Send local IP?
if (connected) {
if (config::allow_mqtt_commands) {
char control_topic[60]; // Should be enough for "CO2sensors/ESPd05cc9/control"
char control_topic[60]; // Should be enough for "CO2sensors/ESPd03cc5/control"
snprintf(control_topic, sizeof(control_topic), "%s/control", publish_topic.c_str());
mqttClient.subscribe(control_topic);
mqttClient.setCallback(controlSensorCallback);
......@@ -207,7 +213,7 @@ namespace mqtt {
}
}
void publishIfTimeHasCome(const String &timeStamp, int16_t co2, float temp, float hum) {
void publishIfTimeHasCome(const String &timeStamp, const int16_t &co2, const float &temp, const float &hum) {
// Send message via MQTT according to sending interval
unsigned long now = seconds();
//TODO: Send average since last MQTT message?
......
......@@ -4,17 +4,20 @@
#include <Arduino.h>
#include "config.h"
#include "led_effects.h"
#include "csv_writer.h"
#ifdef AMPEL_CSV
# include "csv_writer.h"
#endif
#include "co2_sensor.h"
#include "src/lib/PubSubClient/src/PubSubClient.h"
#include "wifi_util.h"
namespace config {
extern uint16_t sending_interval; // [s]
extern uint16_t sending_interval; // [s]
}
namespace mqtt {
extern String last_successful_publish;
extern bool connected;
void initialize(String &topic);
void keepConnection();
void publishIfTimeHasCome(const String &timeStamp, int16_t co2, float temp, float hum);
void publishIfTimeHasCome(const String &timeStamp, const int16_t &co2, const float &temp, const float &hum);
}
#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