Commit ce87c556 authored by Eric Duminil's avatar Eric Duminil
Browse files

Merge. Ready for release

parents 1815167d b8a71725
......@@ -10,3 +10,8 @@ before_script:
job:
stage: build
script: "platformio run"
artifacts:
paths:
- .pio/build/esp32/firmware.bin
- .pio/build/esp8266/firmware.bin
expire_in: never
......@@ -16,6 +16,7 @@ The *CO<sub>2</sub> Ampel* can:
* Send data over [MQTT](https://en.wikipedia.org/wiki/MQTT).
* Send data over [LoRaWAN](https://en.wikipedia.org/wiki/LoRa#LoRaWAN).
* Display measurements and configuration on a small website.
* Allow configuration of every parameter via web-interface.
* Log data to a [CSV](https://en.wikipedia.org/wiki/Comma-separated_values) file, directly on the ESP flash memory.
* Accept many interactive commands.
......@@ -36,7 +37,8 @@ or
## Installation
* If `config.h` does not exist, copy it from `config.public.h`
* Modify `config.h`, e.g. for measurement time-steps, WiFi access, MQTT, NTP and web-server.
* You can modify `config.h`, e.g. for measurement time-steps, WiFi access, MQTT, NTP and web-server.
* Every parameter can be modified later, via the web-interface.
### PlatformIO
......@@ -68,15 +70,44 @@ make upload board=esp32 && make monitor # For ESP32
* *Upload*
* *Tools > Serial Monitor*
## Usage and configuration
* Start the *Ampel*
* If you didn't define wifi SSID and Passwords in config.h
* The *Ampel* should blink turquoise, to indicate *Access Point* mode.
* Connect with a laptop/smartphone to ESPxxxxxx WiFi.
* Open a browser, it should bring you to the *Ampel* web-page.
* The *Ampel* web-page will be shown. You can click on "Configuration".
* You'll have to set an *Ampel* password with at least 8 characters.
* You can set WiFi SSID + Password if you want. It will then try to connect if you disconnect from the *Access Point*.
* If you don't specify SSID + Password, the *Ampel* will simply stay in *Access Point* mode.
* The *Ampel* password will be required if a connection fails, and the *Access Point* starts again.
* The *Ampel* password will be needed (with user 'admin') to connect to the config web-page once the *Ampel* is connected to WiFi.
* Once you defined Wifi SSID, Ampel password and WiFi password:
* the *Ampel* will try to connect, displaying a rainbow wheel.
* if the connection is successful, LEDs will be green.
* if the connection fails, LEDs will be red, and the *Ampel* will return back to *Access Point* mode.
* It might be necessary to reset the *Ampel* after changing important parameters.
* You can rename the *Ampel*. This name will be used instead of ESPxxxxxx for CSV files and the mDNS address. You'll get a new CSV file after renaming, which can be convenient, e.g. to indicate at which location the measurements were made.
* If you forgot the password, you can send `reset_config` as command, and `reset` the ampel.
* If you disabled WiFi, you can enable it again with `wifi 1`.
* It's also possible to specify WiFi SSID and password in the console, with `ssid wifi_ssid_here` and `pwd wifi_password_here` commands.
* You can enable CSV by clicking on "+CSV", or disable it by clicking on "Disable CSV".
* The same applies to MQTT and LoRaWAN.
* If you disable a set, the parameters are still saved, and will appear again once the service is enabled.
## Available commands
In Arduino IDE *Serial Monitor* or PlatformIO *Monitor*, type `help` + <kbd>Enter</kbd> in order to list the available commands:
* `ap 0/1` (Disables/enables access point).
* `auto_calibrate 0/1` (Disables/enables autocalibration).
* `calibrate` (Starts calibration process).
* `calibrate 600` (Starts calibration process, to given ppm).
* `calibrate` (Starts calibration process).
* `calibrate! 600` (Calibrates right now, to given ppm).
* `co2 1500` (Sets CO<sub>2</sub> level, for debugging purposes).
* `co2 1500` (Sets CO<sub>2</sub> level, for debugging).
* `color 0xFF0015` (Shows color, specified as RGB, for debugging).
* `csv 60` (Sets CSV writing interval, in s).
* `format_filesystem` (Deletes the whole filesystem).
......@@ -85,12 +116,17 @@ In Arduino IDE *Serial Monitor* or PlatformIO *Monitor*, type `help` + <kbd>Ente
* `local_ip` (Displays local IP and current SSID).
* `lora 300` (Sets LoRaWAN sending interval, in s).
* `mqtt 60` (Sets MQTT sending interval, in s).
* `pwd abc` (Sets WiFi password to 'abc').
* `reset` (Restarts the ESP).
* `reset_config` (Resets the complete IotWeb config).
* `reset_scd` (Resets SCD30).
* `save_config` (Saves the config to EEPROM).
* `send_local_ip` (Sends local IP and SSID via MQTT. Can be useful to find sensor).
* `set_time 1618829570` (Sets time to the given UNIX time).
* `show_csv` (Displays the complete CSV file on Serial).
* `ssid name` (Sets SSID to 'name').
* `timer 30` (Sets measurement interval, in s).
* `wifi 0/1` (Turns Wifi on/off).
* `wifi_scan` (Scans available WiFi networks).
The commands can be sent via the Serial interface, from the webpage or via MQTT.
......
......@@ -3,34 +3,17 @@
/*****************************************************************
* Libraries *
*****************************************************************/
#include "config.h"
#ifndef MEASUREMENT_TIMESTEP
# error Missing config.h file. Please copy config.public.h to config.h.
#endif
#ifdef AMPEL_CSV
# include "csv_writer.h"
#endif
//NOTE: Too many headers. Move them to include/ folder?
#include "web_config.h" // Needed for offline config too.
#ifdef AMPEL_WIFI
# include "wifi_util.h"
# ifdef AMPEL_MQTT
# include "mqtt.h"
# endif
# 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
#include "csv_writer.h"
#ifdef AMPEL_LORAWAN
# include "lorawan.h"
#endif
#include "wifi_util.h"
#include "mqtt.h"
#include "web_server.h"
#include "lorawan.h"
#include "util.h"
#include "ntp.h"
......@@ -38,4 +21,12 @@
#include "co2_sensor.h"
#include "led_effects.h"
void wifiConnecting();
void wifiConnected();
void wifiFailed();
void apModeStarts();
void keepServicesAlive();
void checkFlashButton();
#endif
......@@ -63,13 +63,22 @@ void setup() {
led_effects::setupOnBoardLED();
led_effects::onBoardLEDOff();
Serial.begin(BAUDS);
Serial.begin(config::bauds);
web_config::initialize();
web_config::setWifiConnectingCallback(wifiConnecting);
web_config::setWifiConnectionCallback(wifiConnected);
web_config::setWifiFailCallback(wifiFailed);
web_config::setApModeCallback(apModeStarts);
pinMode(0, INPUT); // Flash button (used for forced calibration)
Serial.println();
Serial.print(F("Sensor ID: "));
Serial.println(ampel.sensorId);
Serial.print(F("Name : "));
Serial.println(config::ampel_name());
Serial.print(F("MAC : "));
Serial.println(ampel.macAddress);
Serial.print(F("Board : "));
......@@ -81,56 +90,36 @@ void setup() {
sensor::initialize();
#ifdef AMPEL_CSV
csv_writer::initialize(ampel.sensorId);
#endif
#ifdef AMPEL_WIFI
wifi::connect(ampel.sensorId);
if (wifi::connected()) {
# ifdef AMPEL_HTTP
web_server::initialize();
# endif
csv_writer::initialize(config::ampel_name());
ntp::initialize();
ntp::initialize();
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 {
Serial.println(F("Error setting up MDNS responder!"));
}
# ifdef AMPEL_MQTT
mqtt::initialize(ampel.sensorId);
# endif
if (config::is_wifi_on) {
wifi::defineCommands();
web_server::definePages();
wifi::tryConnection();
}
#endif
#if defined(AMPEL_LORAWAN) && defined(ESP32)
lorawan::initialize();
#if defined(ESP32)
if (config::is_lorawan_active()) {
lorawan::initialize();
}
#endif
}
/*****************************************************************
* Helper functions *
*****************************************************************/
void keepServicesAlive();
void checkFlashButton();
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.
lorawan::process();
if (lorawan::waiting_for_confirmation) {
// If node is waiting for join confirmation from Gateway, nothing else should run.
return;
#if defined(ESP32)
if (config::is_lorawan_active()) {
//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.
......@@ -142,19 +131,21 @@ void loop() {
// Short press for night mode, Long press for calibration.
checkFlashButton();
checkSerialInput();
sensor_console::checkSerialInput();
if (sensor::processData()) {
#ifdef AMPEL_CSV
csv_writer::logIfTimeHasCome(sensor::timestamp, sensor::co2, sensor::temperature, sensor::humidity);
#endif
if (config::is_csv_active()) {
csv_writer::logIfTimeHasCome(sensor::timestamp, sensor::co2, sensor::temperature, sensor::humidity);
}
#if defined(AMPEL_WIFI) && defined(AMPEL_MQTT)
mqtt::publishIfTimeHasCome(sensor::timestamp, sensor::co2, sensor::temperature, sensor::humidity);
#endif
if (config::is_wifi_on && config::is_mqtt_active()) {
mqtt::publishIfTimeHasCome(sensor::timestamp, sensor::co2, sensor::temperature, sensor::humidity);
}
#if defined(AMPEL_LORAWAN) && defined(ESP32)
lorawan::preparePayloadIfTimeHasCome(sensor::co2, sensor::temperature, sensor::humidity);
#if defined(ESP32)
if (config::is_lorawan_active()) {
lorawan::preparePayloadIfTimeHasCome(sensor::co2, sensor::temperature, sensor::humidity);
}
#endif
}
......@@ -167,12 +158,59 @@ void loop() {
}
}
void checkSerialInput() {
while (Serial.available() > 0) {
sensor_console::processSerialInput(Serial.read());
/*****************************************************************
* Callbacks *
*****************************************************************/
void wifiConnecting() {
Serial.print(F("WiFi - Trying to connect to "));
Serial.print(config::selected_ssid());
Serial.print(F(" (max "));
Serial.print(config::wifi_timeout);
Serial.println(F("s)."));
led_effects::showRainbowWheel();
}
void wifiConnected() {
Serial.println();
Serial.print(F("WiFi - Connected to "));
Serial.print(WiFi.SSID());
Serial.print(F(", IP address: "));
IPAddress address = WiFi.localIP();
snprintf(wifi::local_ip, sizeof(wifi::local_ip), "%d.%d.%d.%d", address[0], address[1], address[2], address[3]);
Serial.println(wifi::local_ip);
led_effects::showKITTWheel(color::green);
ntp::connect();
if (config::is_mqtt_active()) {
mqtt::initialize(ampel.sensorId);
}
Serial.print(F("You can access this sensor via http://"));
Serial.print(config::ampel_name());
Serial.print(F(".local (might be unstable) or http://"));
Serial.println(WiFi.localIP());
}
void wifiFailed() {
// Ampel will go back to Access Point mode for AP_TIMEOUT seconds, and try connection again after
Serial.println();
Serial.print(F("WiFi - Could not connect to "));
Serial.println(config::selected_ssid());
led_effects::showKITTWheel(color::red);
}
void apModeStarts() {
Serial.print(F("WiFi - Starting Access Point mode ("));
Serial.print(config::ampel_name());
Serial.println(F(")."));
led_effects::alert(color::turquoise);
}
/*****************************************************************
* Helper functions *
*****************************************************************/
/**
* Checks if flash button has been pressed:
* If not, do nothing.
......@@ -186,6 +224,7 @@ void checkFlashButton() {
if (digitalRead(0)) {
Serial.println(F("Flash has been pressed for a short time. Should toggle night mode."));
led_effects::toggleNightMode();
//NOTE: Start Access Point instead?
} else {
Serial.println(F("Flash has been pressed for a long time. Keep it pressed for calibration."));
if (led_effects::countdownToZero()) {
......@@ -199,20 +238,13 @@ void checkFlashButton() {
}
void keepServicesAlive() {
#ifdef AMPEL_WIFI
if (wifi::connected()) {
# 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
ntp::update(); // NTP client has its own timer. It will connect to NTP server every 60s.
# ifdef AMPEL_HTTP
web_server::update();
# endif
# ifdef AMPEL_MQTT
mqtt::keepConnection(); // MQTT client has its own timer. It will keep alive every 15s.
# endif
if (config::is_wifi_on) {
web_config::update();
if (wifi::connected()) {
ntp::update(); // NTP client has its own timer. It will connect to NTP server every 60s.
if (config::is_mqtt_active()) {
mqtt::keepConnection(); // MQTT client has its own timer. It will keep alive every 15s.
}
}
}
#endif
}
#include "co2_sensor.h"
#include "config.h"
#include "web_config.h"
#include "ntp.h"
#include "led_effects.h"
#include "sensor_console.h"
......@@ -11,25 +11,12 @@
#include "src/lib/SparkFun_SCD30_Arduino_Library/src/SparkFun_SCD30_Arduino_Library.h" // From: http://librarymanager/All#SparkFun_SCD30
namespace config {
// 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]
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.
const uint16_t co2_alert_threshold = 2000; // [ppm] Display a flashing led ring, if concentration exceeds this value
#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]
#else
const float temperature_offset = -3.0; // [K] Temperature measured by sensor is usually at least 3K too high.
#endif
bool auto_calibrate_sensor = AUTO_CALIBRATE_SENSOR; // [true / false]
const bool debug_sensor_states = false; // If true, log state transitions over serial console
}
......@@ -65,6 +52,10 @@ namespace sensor {
state current_state = BOOTUP;
void switchState(state);
void setCO2forDebugging(int32_t fakeCo2);
void calibrateSensorToSpecificPPM(int32_t calibrationLevel);
void calibrateSensorRightNow(int32_t calibrationLevel);
void setAutoCalibration(int32_t autoCalibration);
void initialize() {
#if defined(ESP8266)
......@@ -95,14 +86,17 @@ namespace sensor {
// for acclimatization. Resetting it after startup seems to fix this behaviour.
scd30.reset();
//NOTE: It seems that the sensor needs some time for getting/setting temperature offset.
delay(500);
Serial.print(F("Setting temperature offset to -"));
Serial.print(abs(config::temperature_offset));
Serial.println(F(" K."));
scd30.setTemperatureOffset(abs(config::temperature_offset)); // setTemperatureOffset only accepts positive numbers, but shifts the temperature down.
delay(100);
delay(500);
Serial.print(F("Temperature offset is : -"));
Serial.print(scd30.getTemperatureOffset());
//NOTE: Even once the temperature offset is saved, the sensor still needs some time (~10 minutes?) to apply it.
Serial.print(F("Temperature offset is : "));
Serial.print(getTemperatureOffset());
Serial.println(F(" K"));
Serial.print(F("Auto-calibration is "));
......@@ -116,7 +110,7 @@ namespace sensor {
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("co2", setCO2forDebugging, F("1500 (Sets co2 level, for debugging)"));
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,
......@@ -243,7 +237,7 @@ namespace sensor {
delay(100);
} else {
// Display a flashing led ring, if concentration exceeds a specific value
led_effects::redAlert();
led_effects::alert(color::red);
}
}
......@@ -294,6 +288,10 @@ namespace sensor {
return freshData && (current_state == READY || current_state == NEEDS_CALIBRATION);
}
float getTemperatureOffset() {
return -abs(scd30.getTemperatureOffset());
}
/*****************************************************************
* Callbacks for sensor commands *
*****************************************************************/
......
......@@ -3,13 +3,6 @@
#include <stdint.h> // For uint16_t
namespace config {
extern uint16_t measurement_timestep; // [s] Value between 2 and 1800 (range for SCD30 sensor)
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 uint16_t co2;
extern float temperature;
......@@ -19,12 +12,8 @@ namespace sensor {
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);
void resetSCD();
float getTemperatureOffset();
}
#endif
#ifndef CONFIG_H_INCLUDED
# define CONFIG_H_INCLUDED
// This file is a config template, and can be copied to config.h. Please don't save any important password in this template.
/*** _ _
* / \ _ __ ___ _ __ ___| |
* / _ \ | '_ ` _ \| '_ \ / _ \ |
* / ___ \| | | | | | |_) | __/ |
* /_/ __\_\_| |_| |_| .__/_\___|_|
* / ___|___ _ __|_/ _(_) __ _
* | | / _ \| '_ \| |_| |/ _` |
* | |__| (_) | | | | _| | (_| |
* \____\___/|_| |_|_| |_|\__, |
* |___/
***/
// This file is a config template, and can be copied to config.h.
// Please don't save any important password in this template.
// IMPORTANT: Parameters defined in config.h are only default values, and are applied if:
// * the ampel is flashed for the first time
// * or 'reset_config' command is called
// * or AMPEL_CONFIG_VERSION has been changed.
// Once those default values have been applied, uploading the firmware with a modified config.h will not update the ampel configuration!
// Every parameter can be modified and saved later via the web-config.
// Some of those parameters can also be modified via commands in the Serial monitor :
// e.g. 'wifi 0' to turn WiFi off, or 'csv 60' to log data in csv every minute.
/***
* AMPEL
*/
// You can rename the Ampel if you want.
// This name will be used for CSV files and the mDNS address.
// You'll get a new CSV file after renaming, which can be convenient, e.g. after moving
// the ampel to another room.
// If left empty, the name will be ESPxxxxxx, where xxxxxx represent the last half of the MAC address.
# define AMPEL_NAME ""
// This password will be used for Access Point (without username), and for web-server available at http://local_ip with user 'admin', without quotes.
// If left empty, the password will have to be set during the first configuration, via access point.
// In order to be set successfully, it should have at least 8 characters.
# define AMPEL_PASSWORD ""
// AMPEL_CONFIG_VERSION should be defined, and have exactly 3 characters.
// If you modify this string, every parameter saved on the Ampel will be replaced by the ones in config.h.
// AMPEL_CONFIG_VERSION should also be updated if the configuration structure is modified.
// The structure of the Ampel configuration has been modified 11 times, so it's called "a11" for now.
# define AMPEL_CONFIG_VERSION "a11"
/**
* 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")
// Define the default for corresponding services. They can be enabled/disabled later in the web-config.
# define AMPEL_WIFI true // Should ESP connect to WiFi? Web configuration will not be available when set to false. Use "wifi 1" command to set to true.
# define AMPEL_MQTT true // Should data be sent over MQTT? (AMPEL_WIFI should be enabled too)
# define AMPEL_CSV true // Should data be logged as CSV, on the ESP flash memory?
# define AMPEL_LORAWAN false // Should data be sent over LoRaWAN? (Requires ESP32 + LoRa modem, and "MCCI LoRaWAN LMIC library")
/**
* WIFI
*/
# define WIFI_SSID "MY_SSID"
# define WIFI_PASSWORD "P4SSW0RD"
// SSID and PASSWORD need to be defined, but can be empty.
# define WIFI_SSID ""
# define WIFI_PASSWORD ""
// How long should the Ampel try to connect to WIFI_SSID?
# define WIFI_TIMEOUT 30 // [s]
// If the Ampel cannot connect to WIFI_SSID, it will start an Access Point for ACCESS_POINT_TIMEOUT seconds.
// If someone connects to this Access Point, the Ampel will stay in this mode until everybody logs out.
// If nobody connects to the Access Point before ACCESS_POINT_TIMEOUT seconds, the Ampel will try to connect WIFI_SSID again.
# define ACCESS_POINT_TIMEOUT 60 // [s]
/**
* Sensor
......@@ -61,22 +107,14 @@
// LED brightness, which can vary between min and max brightness ("LED breathing")
// 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
// How many LEDs in the ring? 12 and 16 are currently supported. If undefined, 12 is used as default.
// NOTE: LEDs require a decent amount of electricity, so the default is chosen to be relativitely low.
# define MAX_BRIGHTNESS 80
// MIN_BRIGHTNESS, must be defined, and should be between 0 and MAX_BRIGHTNESS
// If MIN_BRIGHTNESS is set to MAX_BRIGHTNESS, breathing is disabled.
# define MIN_BRIGHTNESS 40
// How many LEDs in the ring? 12 and 16 are currently supported.
# define LED_COUNT 12
/**
* WEB SERVER
* available at http://local_ip, with user HTTP_USER and password HTTP_PASSWORD
*/
// Define empty strings in order to disable authentication, or remove the constants altogether.
# define HTTP_USER "co2ampel"
# define HTTP_PASSWORD "my_password"
/**
* MQTT
*/
......@@ -107,48 +145,49 @@
// How often should measurements be sent to MQTT server?
// 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_SENDING_INTERVAL 300 // [s]
# define MQTT_SERVER "test.mosquitto.org" // MQTT server URL or IP address
# define MQTT_PORT 8883
# define MQTT_ENCRYPTED true // Set to false for unencrypted MQTT (e.g. with port 1883). If undefined, MQTT_ENCRYPTED will be set to true.
# define MQTT_ENCRYPTED true // Set to false for unencrypted MQTT (e.g. with port 1883).
# define MQTT_USER ""
# define MQTT_PASSWORD ""
# define MQTT_TOPIC_PREFIX "CO2sensors/" // ESPxxxxxx will be added to the prefix, so complete topic will be "CO2sensors/ESPxxxxxx". The prefix should probably end with '/'
/**
* LoRaWAN
*/
// 1) Requires "MCCI LoRaWAN LMIC library", which will be automatically used with PlatformIO but should be added in "Arduino IDE".
// 2) If you need to, region and transceiver type can be specified in lorawan.cpp. Default is "Europe 868"
// 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/
// e.g. on https://www.thethingsindustries.com/docs/integrations/
// with "Europe 863-870 MHz (SF9 for RX2 - recommended)", "MAC v1.0.3"
// 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 }
// WARNING: If AMPEL_LORAWAN is true, you need to modify the 3 following constants
// They are written as hexadecimal strings, and will be parsed in the correct order.
// This EUI must be in big-endian format, so most-significant-byte first.
// You can copy the string from TheThingsNetwork as-is, without reversing the bytes.
// For TheThingsNetwork issued EUIs the string should start with "70B3D5..."
# define LORAWAN_DEVICE_EUI "70B3D57ED004CB17"
// This should also be in big-endian format, and can be copied as is from TheThingsNetwork.
# define LORAWAN_APPLICATION_EUI "0102030405060708"
// This should also be in big-endian format, and can be copied as is from TheThingsNetwork.
# define LORAWAN_APPLICATION_KEY "9D06308E20B974919DA6404E063BE01D"
/**
* NTP
*/
# define NTP_SERVER "pool.ntp.org"
# define UTC_OFFSET_IN_SECONDS 7200 // [s] 3600 for UTC+1, 7200 for UTC+1 and daylight saving time
/**
* Others
*/
# define BAUDS 115200 // Transmission rate
# define UTC_OFFSET 1 // [h] +1 for Paris/Berlin, -5 for NYC
# define DAYLIGHT_SAVING_TIME false // true in summer, false in winter
#endif
#include "csv_writer.h"
#include "config.h"
#include "web_config.h"
#include "ntp.h"
#include "led_effects.h"
#include "sensor_console.h"
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;
char last_successful_write[23];
......@@ -83,14 +79,14 @@ namespace csv_writer {
}
#endif
char filename[15]; // "/ESPxxxxxx.csv\0"
char filename[20]; // e.g. "/ESPxxxxxx.csv\0"
int getAvailableSpace() {
return getTotalSpace() - getUsedSpace();
}
void initialize(const char *sensorId) {
snprintf(filename, sizeof(filename), "/%s.csv", sensorId);
void initialize(const char *basename) {
snprintf(filename, sizeof(filename), "/%.14s.csv", basename);
Serial.println();
Serial.print(F("Initializing FS..."));
......@@ -184,6 +180,7 @@ namespace csv_writer {
}
void showCSVContent() {
//TODO: Now that ampel_name can be set, should show the content of every csv
Serial.print(F("### "));
Serial.print(filename);
Serial.println(F(" ###"));
......
......@@ -10,13 +10,11 @@
#else
# error Board should be either ESP8266 or ESP832
#endif
//NOTE: LittleFS will be available for Arduino esp32 core v2
namespace config {
extern uint16_t csv_interval; // [s]
}
namespace csv_writer {
extern char last_successful_write[];
void initialize(const char *sensorId);
void initialize(const char *basename);
void logIfTimeHasCome(const char *timestamp, const int16_t &co2, const float &temperature, const float &humidity);
int getAvailableSpace();
extern char filename[];
......
#include "led_effects.h"
#include "config.h"
#include "web_config.h"
#include "sensor_console.h"
// Adafruit NeoPixel (Arduino library for controlling single-wire-based LED pixels and strip)
......@@ -12,37 +12,14 @@
* Configuration *
*****************************************************************/
namespace config {
const uint8_t max_brightness = MAX_BRIGHTNESS;
#if defined(MIN_BRIGHTNESS)
const uint8_t min_brightness = MIN_BRIGHTNESS;
#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.
bool display_led = true; // Will be set to false during "night mode".
#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]
uint16_t co2_ticks[16 + 1] = { 0, 500, 600, 700, 800, 900, 1000 }; // rest will be filled later
// For a given LED, which color should be displayed? First LED will be pure green (hue angle 120°),
// LEDs >= 1600ppm 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, 500, 600, 700, 800, 900, 1000, 1100, 1200,
1300, 1400, 1500, 1600, 1700, 1800, 2000, 2200 }; // [ppm]
const uint16_t led_hues[led_count] = {21845U, 19859U, 17873U, 15887U, 13901U, 11915U, 9929U, 7943U,
5957U, 3971U, 1985U, 0, 0, 0, 0, 0}; // [hue angle]
#else
# error "Only 12 and 16 LEDs rings are currently supported."
#endif
uint16_t led_hues[16];
}
#if defined(ESP8266)
......@@ -53,7 +30,8 @@ const int NEOPIXELS_PIN = 5;
const int NEOPIXELS_PIN = 23;
#endif
Adafruit_NeoPixel pixels(config::led_count, NEOPIXELS_PIN, NEO_GRB + NEO_KHZ800);
// config::led_count is not yet known, will be set later.
Adafruit_NeoPixel pixels(0, NEOPIXELS_PIN, NEO_GRB + NEO_KHZ800);
namespace led_effects {
//On-board LED on D4, aka GPIO02
......@@ -94,6 +72,64 @@ namespace led_effects {
}
void setupRing() {
Serial.print(F("Ring : "));
Serial.print(config::led_count);
Serial.println(F(" LEDs."));
pixels.updateLength(config::led_count);
if (config::led_count == 12) {
config::co2_ticks[7] = 1200;
config::co2_ticks[8] = 1400;
config::co2_ticks[9] = 1600;
config::co2_ticks[10] = 1800;
config::co2_ticks[11] = 2000;
config::co2_ticks[12] = 2200;
config::led_hues[0] = 21845U;
config::led_hues[1] = 19114U;
config::led_hues[2] = 16383U;
config::led_hues[3] = 13653U;
config::led_hues[4] = 10922U;
config::led_hues[5] = 8191U;
config::led_hues[6] = 5461U;
config::led_hues[7] = 2730U;
config::led_hues[8] = 0;
config::led_hues[9] = 0;
config::led_hues[10] = 0;
config::led_hues[11] = 0;
} else if (config::led_count == 16) {
config::co2_ticks[7] = 1100;
config::co2_ticks[8] = 1200;
config::co2_ticks[9] = 1300;
config::co2_ticks[10] = 1400;
config::co2_ticks[11] = 1500;
config::co2_ticks[12] = 1600;
config::co2_ticks[13] = 1700;
config::co2_ticks[14] = 1800;
config::co2_ticks[15] = 2000;
config::co2_ticks[16] = 2200;
config::led_hues[0] = 21845U;
config::led_hues[1] = 19859U;
config::led_hues[2] = 17873U;
config::led_hues[3] = 15887U;
config::led_hues[4] = 13901U;
config::led_hues[5] = 11915U;
config::led_hues[6] = 9929U;
config::led_hues[7] = 7943U;
config::led_hues[8] = 5957U;
config::led_hues[9] = 3971U;
config::led_hues[10] = 1985U;
config::led_hues[11] = 0;
config::led_hues[12] = 0;
config::led_hues[13] = 0;
config::led_hues[14] = 0;
config::led_hues[15] = 0;
} else {
// "Only 12 and 16 LEDs rings are currently supported."
config::display_led = false;
}
pixels.begin();
pixels.setBrightness(config::max_brightness);
LEDsOff();
......@@ -106,6 +142,7 @@ namespace led_effects {
}
void turnLEDsOnOff(int32_t display_led) {
//TODO: Could use strategy pattern with 2 different Effects classes.
config::display_led = display_led;
if (config::display_led) {
Serial.println(F("LEDs are on!"));
......@@ -165,7 +202,8 @@ namespace led_effects {
*/
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;
uint8_t brightness_amplitude = config::max_brightness - config::min_brightness;
uint16_t brightness = config::min_brightness + pixels.sine8(breathing_offset) * 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.
......@@ -184,7 +222,7 @@ namespace led_effects {
pixels.setPixelColor(ledId, pixels.ColorHSV(config::led_hues[ledId], 255, brightness));
}
pixels.show();
if (config::brightness_amplitude > 0) {
if (config::max_brightness > config::min_brightness) {
breathe(co2);
}
}
......@@ -207,7 +245,7 @@ namespace led_effects {
}
}
void redAlert() {
void alert(uint32_t color) {
if (!config::display_led) {
onBoardLEDOn();
delay(500);
......@@ -218,7 +256,7 @@ namespace led_effects {
for (int i = 0; i < 10; i++) {
pixels.setBrightness(static_cast<int>(config::max_brightness * (1 - i * 0.1)));
delay(50);
pixels.fill(color::red);
pixels.fill(color);
pixels.show();
}
}
......
......@@ -9,6 +9,7 @@ namespace color {
const uint32_t blue = 0x0000FF;
const uint32_t black = 0x000000;
const uint32_t magenta = 0xFF00FF;
const uint32_t turquoise = 0x1CFF68;
}
namespace led_effects {
......@@ -20,7 +21,7 @@ namespace led_effects {
void LEDsOff();
void setupRing();
void redAlert();
void alert(uint32_t color);
bool countdownToZero();
void showWaitingLED(uint32_t color);
void showKITTWheel(uint32_t color, uint16_t duration_s = 2);
......
#include "lorawan.h"
#if defined(AMPEL_LORAWAN) && defined(ESP32)
#if defined(ESP32)
#include "web_config.h"
#include "led_effects.h"
#include "sensor_console.h"
#include "util.h"
#include "ntp.h"
/*** Define region and transceiver type, and ignore lmic_project_config.h from lmic library ***/
// Those values are probably okay if you're in Europe.
#define ARDUINO_LMIC_PROJECT_CONFIG_H_SUPPRESS
#define CFG_eu868 1
#define CFG_sx1276_radio 1
/****************************************************************************************/
// 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>
......@@ -37,12 +31,6 @@ namespace config {
#else
# error "Region should be specified"
#endif
// 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:
......@@ -53,18 +41,6 @@ namespace config {
// 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;
......@@ -104,9 +80,9 @@ namespace lorawan {
void onEvent(ev_t ev) {
char current_time[23];
ntp::getLocalTime(current_time);
Serial.print("LoRa - ");
Serial.print(F("LoRa - "));
Serial.print(current_time);
Serial.print(" - ");
Serial.print(F(" - "));
switch (ev) {
case EV_JOINING:
Serial.println(F("EV_JOINING"));
......@@ -122,6 +98,7 @@ namespace lorawan {
u1_t nwkKey[16];
u1_t artKey[16];
LMIC_getSessionKeys(&netid, &devaddr, nwkKey, artKey);
//NOTE: Saving session to EEPROM seems like a good idea at first, but unfortunately: too much info is needed, and a counter would need to be save every single time data is sent.
Serial.print(F(" netid: "));
Serial.println(netid, DEC);
Serial.print(F(" devaddr: "));
......@@ -210,14 +187,19 @@ namespace lorawan {
// 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
// };
//}
// function decodeUplink(input) {
// return {
// data: {
// co2: input.bytes[0] * 20,
// temp: input.bytes[1] / 5.0 - 10,
// rh: input.bytes[2] / 2.0
// },
// warnings: [],
// errors: []
// };
// }
}
}
......@@ -245,4 +227,53 @@ namespace lorawan {
void onEvent(ev_t ev) {
lorawan::onEvent(ev);
}
// 'A' -> 10, 'F' -> 15, 'f' -> 15, 'z' -> -1
int8_t hexCharToInt(char c) {
int8_t v = -1;
if ((c >= '0') && (c <= '9')) {
v = (c - '0');
} else if ((c >= 'A') && (c <= 'F')) {
v = (c - 'A' + 10);
} else if ((c >= 'a') && (c <= 'f')) {
v = (c - 'a' + 10);
}
return v;
}
/**
* Parses hex string and saves the corresponding bytes in buf.
* msb is true for most-significant-byte, false for least-significant-byte.
*
* "112233" will be loaded into {0x11, 0x22, 0x33} in MSB, {0x33, 0x22, 0x11} in LSB.
*/
void hexStringToByteArray(uint8_t *buf, const char *hex, uint max_n, bool msb) {
int n = util::min(strlen(hex) / 2, max_n);
for (int i = 0; i < n; i++) {
int j;
if (msb) {
j = i;
} else {
j = n - 1 - i;
}
uint8_t r = hexCharToInt(hex[j * 2]) * 16 + hexCharToInt(hex[j * 2 + 1]);
buf[i] = r;
}
}
// Load config into LMIC byte arrays.
void os_getArtEui(u1_t *buf) {
hexStringToByteArray(buf, config::lorawan_app_eui, 8, false);
}
void os_getDevEui(u1_t *buf) {
hexStringToByteArray(buf, config::lorawan_device_eui, 8, false);
}
void os_getDevKey(u1_t *buf) {
hexStringToByteArray(buf, config::lorawan_app_key, 16, true);
}
#endif
#ifndef AMPEL_LORAWAN_H_
#define AMPEL_LORAWAN_H_
#include "config.h"
# if defined(AMPEL_LORAWAN) && defined(ESP32)
# if defined(ESP32)
#include <stdint.h> // For uint32_t & uint16_t
namespace config {
extern uint16_t lorawan_sending_interval; // [s]
extern const char *lorawan_frequency_plan; // e.g. "Europe 868"
}
......
#include "mqtt.h"
#include "config.h"
#include "web_config.h"
#include "led_effects.h"
#include "sensor_console.h"
#include "wifi_util.h"
......@@ -14,46 +14,44 @@
#endif
namespace config {
// Values should be defined in config.h
uint16_t mqtt_sending_interval = MQTT_SENDING_INTERVAL; // [s]
// Values should be defined in config.h or over webconfig
//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;
const uint16_t mqtt_port = MQTT_PORT;
const char *mqtt_user = MQTT_USER;
const char *mqtt_password = MQTT_PASSWORD;
const bool allow_mqtt_commands = ALLOW_MQTT_COMMANDS;
const unsigned long wait_after_fail = 900; // [s] Wait 15 minutes after an MQTT connection fail, before trying again.
}
#if MQTT_ENCRYPTED
# if defined(ESP32)
# include <WiFiClientSecure.h>
# endif
WiFiClientSecure espClient;
#else
WiFiClient espClient;
#if defined(ESP32)
# include <WiFiClientSecure.h>
#endif
PubSubClient mqttClient(espClient);
WiFiClient *espClient;
PubSubClient mqttClient;
namespace mqtt {
unsigned long last_sent_at = 0;
unsigned long last_failed_at = 0;
bool connected = false;
char publish_topic[21]; // e.g. "CO2sensors/ESPxxxxxx\0"
char publish_topic[42]; // "MQTT_TOPIC_PREFIX/ESPxxxxxx\0", e.g. "CO2sensors/ESPxxxxxx\0"
const char *json_sensor_format;
char last_successful_publish[23] = "";
void initialize(const char *sensorId) {
json_sensor_format = PSTR("{\"time\":\"%s\", \"co2\":%d, \"temp\":%.1f, \"rh\":%.1f}");
snprintf(publish_topic, sizeof(publish_topic), "CO2sensors/%s", sensorId);
#if MQTT_ENCRYPTED
// The sensor doesn't check the fingerprint of the MQTT broker, because otherwise this fingerprint should be updated
// on the sensor every 3 months. The connection can still be encrypted, though:
espClient.setInsecure(); // If not available for ESP32, please update Arduino IDE / PlatformIO
#endif
snprintf(publish_topic, sizeof(publish_topic), "%s%s", config::mqtt_topic_prefix, sensorId);
if (config::mqtt_encryption) {
// The sensor doesn't check the fingerprint of the MQTT broker, because otherwise this fingerprint should be updated
// on the sensor every 3 months. The connection can still be encrypted, though:
WiFiClientSecure *secureClient = new WiFiClientSecure();
secureClient->setInsecure();
espClient = secureClient;
} else {
espClient = new WiFiClient();
}
mqttClient.setClient(*espClient);
mqttClient.setServer(config::mqtt_server, config::mqtt_port);
sensor_console::defineIntCommand("mqtt", setMQTTinterval, F("60 (Sets MQTT sending interval, in s)"));
......@@ -62,13 +60,15 @@ namespace mqtt {
}
void publish(const char *timestamp, int16_t co2, float temperature, float humidity) {
if (WiFi.status() == WL_CONNECTED && mqttClient.connected()) {
if (wifi::connected() && mqttClient.connected()) {
led_effects::onBoardLEDOn();
Serial.print(F("MQTT - Publishing message ... "));
Serial.print(F("MQTT - Publishing message to '"));
Serial.print(publish_topic);
Serial.print(F("' ... "));
char payload[75]; // Should be enough for json...
snprintf(payload, sizeof(payload), json_sensor_format, timestamp, co2, temperature, humidity);
// Topic is the same as clientID. e.g. 'CO2sensors/ESP3d03da'
// Topic is 'MQTT_TOPIC_PREFIX/ESP123456'
if (mqttClient.publish(publish_topic, payload)) {
Serial.println(F("OK"));
ntp::getLocalTime(last_successful_publish);
......@@ -108,17 +108,20 @@ namespace mqtt {
// It failed less than wait_after_fail ago. Not even trying.
return;
}
if (WiFi.status() != WL_CONNECTED) { //NOTE: Sadly, WiFi.status is sometimes WL_CONNECTED even though it's really not
if (!wifi::connected()) { //NOTE: Sadly, WiFi.status is sometimes WL_CONNECTED even though it's really not
// No WIFI
return;
}
Serial.print(F("MQTT - Attempting connection to "));
Serial.print(MQTT_SERVER);
Serial.print(MQTT_ENCRYPTED ? F(" (Encrypted") : F(" (Unencrypted"));
Serial.print(config::mqtt_server);
Serial.print(config::mqtt_encryption ? F(" (Encrypted") : F(" (Unencrypted"));
Serial.print(F(", port "));
Serial.print(MQTT_PORT);
Serial.print(F(") ..."));
Serial.print(config::mqtt_port);
Serial.print(F(") "));
Serial.print(F("User:'"));
Serial.print(config::mqtt_user);
Serial.print(F("' ..."));
led_effects::onBoardLEDOn();
// Wait for connection, at most 15s (default)
......@@ -129,7 +132,7 @@ namespace mqtt {
if (connected) {
if (config::allow_mqtt_commands) {
char control_topic[60]; // Should be enough for "CO2sensors/ESPd03cc5/control"
char control_topic[50]; // Should be enough for "MQTT_TOPIC_PREFIX/ESPd03cc5/control\0"
snprintf(control_topic, sizeof(control_topic), "%s/control", publish_topic);
mqttClient.subscribe(control_topic);
mqttClient.setCallback(controlSensorCallback);
......@@ -137,10 +140,15 @@ namespace mqtt {
Serial.println(F(" Connected."));
last_failed_at = 0;
} else {
// As defined in PubSubClient, between -4 and 5
const __FlashStringHelper *mqtt_statuses[] = { F("Connection timeout"), F("Connection lost"), F(
"Connection failed"), F("Disconnected"), F("Connected"), F("Bad protocol"), F("Bad client ID"), F(
"Unavailable"), F("Bad credentials"), F("Unauthorized") };
last_failed_at = seconds();
Serial.print(F(" Failed! Error code="));
Serial.print(mqtt_statuses[mqttClient.state() + 4]);
Serial.print("! (Code=");
Serial.print(mqttClient.state());
Serial.print(F(". Will try again in "));
Serial.print(F("). Will try again in "));
Serial.print(config::wait_after_fail);
Serial.println("s.");
}
......@@ -170,7 +178,7 @@ namespace mqtt {
config::mqtt_sending_interval = sending_interval;
Serial.print(F("Setting MQTT sending interval to : "));
Serial.print(config::mqtt_sending_interval);
Serial.println("s.");
Serial.println(F("s."));
led_effects::showKITTWheel(color::green, 1);
}
......@@ -178,12 +186,12 @@ namespace mqtt {
// 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"
char info_topic[50]; // Should be enough for "MQTT_TOPIC_PREFIX/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);
snprintf(payload, sizeof(payload), json_info_format, wifi::local_ip, config::selected_ssid());
mqttClient.publish(info_topic, payload);
}
......
......@@ -3,15 +3,6 @@
#include <stdint.h> // For uint32_t & uint16_t
#include "config.h"
#if !defined(MQTT_ENCRYPTED)
# define MQTT_ENCRYPTED true // Old config files might not define it, and encryption was on by default.
#endif
namespace config {
extern uint16_t mqtt_sending_interval; // [s]
}
namespace mqtt {
extern char last_successful_publish[];
extern bool connected;
......
#include "ntp.h"
#include "sensor_console.h"
#include "config.h"
#include "web_config.h"
#include <WiFiUdp.h> // required for NTP
#include "src/lib/NTPClient/NTPClient.h" // NTP
namespace config {
const char *ntp_server = NTP_SERVER;
const long utc_offset_in_seconds = UTC_OFFSET_IN_SECONDS; // UTC+1
}
//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);
NTPClient timeClient(ntpUDP);
bool connected_at_least_once = false;
void setLocalTime(int32_t unix_seconds);
// Should be defined, even offline
void initialize() {
timeClient.begin();
timeClient.setTimeOffset((config::time_zone + config::daylight_saving_time) * 3600);
sensor_console::defineIntCommand("set_time", ntp::setLocalTime, F("1618829570 (Sets time to the given UNIX time)"));
}
void connect(){
timeClient.setPoolServerName(config::ntp_server);
timeClient.setUpdateInterval(60000UL);
Serial.print("NTP - Trying to connect to : ");
Serial.println(config::ntp_server);
timeClient.begin();
}
void update() {
connected_at_least_once |= timeClient.update();
}
......
......@@ -2,7 +2,9 @@
#define AMPEL_TIME_H_INCLUDED
namespace ntp {
extern bool connected_at_least_once;
void initialize();
void connect();
void update();
void getLocalTime(char *timestamp);
}
......
#include "sensor_console.h"
namespace sensor_console {
const uint8_t MAX_COMMANDS = 20;
const uint8_t MAX_COMMAND_SIZE = 30;
const uint8_t MAX_COMMANDS = 26;
const uint8_t MAX_COMMAND_SIZE = 40;
uint8_t commands_count = 0;
......@@ -149,6 +149,12 @@ namespace sensor_console {
}
}
void checkSerialInput() {
while (Serial.available() > 0) {
sensor_console::processSerialInput(Serial.read());
}
}
/*
* Tries to find the corresponding callback for a given command. Name and parameter type should fit.
*/
......
......@@ -12,7 +12,7 @@ namespace sensor_console {
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 uint8_t in_byte);
void checkSerialInput();
void execute(const char *command_line);
}
......
set(COMPONENT_SRCS
src/IotWebConf.cpp
src/IotWebConfMultipleWifi.cpp
src/IotWebConfOptionalGroup.cpp
src/IotWebConfParameter.cpp
src/IotWebConfESP32HTTPUpdateServer.cpp
)
set(COMPONENT_ADD_INCLUDEDIRS
src/
)
list(APPEND COMPONENT_REQUIRES "arduino")
register_component()
#ADD_DEFINITIONS(-DESP32)
list(APPEND DEFINITIONS "ESP32")
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