Commit 8efb0a30 authored by Eric Duminil's avatar Eric Duminil
Browse files

Merge branch 'experimental/iotwebconfig' into develop

Current state seems to be stable enough for develop
parents 34496cfe 6ea33a0e
Pipeline #5893 passed with stage
in 2 minutes and 22 seconds
...@@ -6,6 +6,34 @@ It measures the current CO<sub>2</sub> concentration (in ppm), and displays it o ...@@ -6,6 +6,34 @@ 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. The room should be ventilated as soon as one LED turns red.
## Ampel web-conf
Ampel-firmware + [IotWebConf](https://github.com/prampec/IotWebConf).
This is beta software, and bugs are expected! There is not much free RAM left on ESP8266, so the ESP might crash randomly. :-/
* You probably need to update `config.h`. There's a new template in `config.public.h`.
* Every parameter can be set in the web-configuration, so you don't have to modify `config.h` once it's been updated.
* Flash
* Start the *Ampel*
* It 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 usual 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 web-page once the *Ampel* is connected to WiFi.
* 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.
## Features ## Features
The *CO<sub>2</sub> Ampel* can: The *CO<sub>2</sub> Ampel* can:
...@@ -72,11 +100,12 @@ make upload board=esp32 && make monitor # For ESP32 ...@@ -72,11 +100,12 @@ make upload board=esp32 && make monitor # For ESP32
In Arduino IDE *Serial Monitor* or PlatformIO *Monitor*, type `help` + <kbd>Enter</kbd> in order to list the 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). * `auto_calibrate 0/1` (Disables/enables autocalibration).
* `calibrate` (Starts calibration process).
* `calibrate 600` (Starts calibration process, to given ppm). * `calibrate 600` (Starts calibration process, to given ppm).
* `calibrate` (Starts calibration process).
* `calibrate! 600` (Calibrates right now, to given ppm). * `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). * `color 0xFF0015` (Shows color, specified as RGB, for debugging).
* `csv 60` (Sets CSV writing interval, in s). * `csv 60` (Sets CSV writing interval, in s).
* `format_filesystem` (Deletes the whole filesystem). * `format_filesystem` (Deletes the whole filesystem).
...@@ -85,12 +114,17 @@ In Arduino IDE *Serial Monitor* or PlatformIO *Monitor*, type `help` + <kbd>Ente ...@@ -85,12 +114,17 @@ In Arduino IDE *Serial Monitor* or PlatformIO *Monitor*, type `help` + <kbd>Ente
* `local_ip` (Displays local IP and current SSID). * `local_ip` (Displays local IP and current SSID).
* `lora 300` (Sets LoRaWAN sending interval, in s). * `lora 300` (Sets LoRaWAN sending interval, in s).
* `mqtt 60` (Sets MQTT sending interval, in s). * `mqtt 60` (Sets MQTT sending interval, in s).
* `pwd abc` (Sets WiFi password to 'abc').
* `reset` (Restarts the ESP). * `reset` (Restarts the ESP).
* `reset_config` (Resets the complete IotWeb config).
* `reset_scd` (Resets SCD30). * `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). * `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). * `set_time 1618829570` (Sets time to the given UNIX time).
* `show_csv` (Displays the complete CSV file on Serial). * `show_csv` (Displays the complete CSV file on Serial).
* `ssid name` (Sets SSID to 'name').
* `timer 30` (Sets measurement interval, in s). * `timer 30` (Sets measurement interval, in s).
* `wifi 0/1` (Turns Wifi on/off).
* `wifi_scan` (Scans available WiFi networks). * `wifi_scan` (Scans available WiFi networks).
The commands can be sent via the Serial interface, from the webpage or via MQTT. The commands can be sent via the Serial interface, from the webpage or via MQTT.
......
...@@ -3,34 +3,17 @@ ...@@ -3,34 +3,17 @@
/***************************************************************** /*****************************************************************
* Libraries * * Libraries *
*****************************************************************/ *****************************************************************/
#include "config.h"
#ifndef MEASUREMENT_TIMESTEP
# error Missing config.h file. Please copy config.public.h to config.h.
#endif
#ifdef AMPEL_CSV //NOTE: Too many headers. Move them to include/ folder?
# include "csv_writer.h" #include "web_config.h" // Needed for offline config too.
#endif
#ifdef AMPEL_WIFI #include "csv_writer.h"
# 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
#ifdef AMPEL_LORAWAN #include "wifi_util.h"
# include "lorawan.h" #include "mqtt.h"
#endif #include "web_server.h"
#include "lorawan.h"
#include "util.h" #include "util.h"
#include "ntp.h" #include "ntp.h"
...@@ -38,4 +21,9 @@ ...@@ -38,4 +21,9 @@
#include "co2_sensor.h" #include "co2_sensor.h"
#include "led_effects.h" #include "led_effects.h"
void wifiConnected();
void wifiFailed();
void keepServicesAlive();
void checkFlashButton();
#endif #endif
...@@ -63,13 +63,21 @@ void setup() { ...@@ -63,13 +63,21 @@ void setup() {
led_effects::setupOnBoardLED(); led_effects::setupOnBoardLED();
led_effects::onBoardLEDOff(); led_effects::onBoardLEDOff();
Serial.begin(BAUDS); Serial.begin(config::bauds);
web_config::initialize();
web_config::setWifiConnectionCallback(wifiConnected);
web_config::setWifiFailCallback(wifiFailed);
pinMode(0, INPUT); // Flash button (used for forced calibration) pinMode(0, INPUT); // Flash button (used for forced calibration)
Serial.println(); Serial.println();
Serial.print(F("Sensor ID: ")); Serial.print(F("Sensor ID: "));
Serial.println(ampel.sensorId); Serial.println(ampel.sensorId);
Serial.print(F("Name : "));
Serial.println(config::ampel_name());
Serial.print(F("MAC : ")); Serial.print(F("MAC : "));
Serial.println(ampel.macAddress); Serial.println(ampel.macAddress);
Serial.print(F("Board : ")); Serial.print(F("Board : "));
...@@ -81,56 +89,36 @@ void setup() { ...@@ -81,56 +89,36 @@ void setup() {
sensor::initialize(); sensor::initialize();
#ifdef AMPEL_CSV csv_writer::initialize(config::ampel_name());
csv_writer::initialize(ampel.sensorId);
#endif
#ifdef AMPEL_WIFI
wifi::connect(ampel.sensorId);
if (wifi::connected()) { ntp::initialize();
# ifdef AMPEL_HTTP
web_server::initialize();
# endif
ntp::initialize(); if (config::is_wifi_on) {
wifi::defineCommands();
if (MDNS.begin(ampel.sensorId)) { // Start the mDNS responder for SENSOR_ID.local web_server::definePages();
MDNS.addService("http", "tcp", 80); wifi::tryConnection();
Serial.println(F("mDNS responder started"));
} else {
Serial.println(F("Error setting up MDNS responder!"));
}
# ifdef AMPEL_MQTT
mqtt::initialize(ampel.sensorId);
# endif
} }
#endif
#if defined(AMPEL_LORAWAN) && defined(ESP32) #if defined(ESP32)
lorawan::initialize(); if (config::is_lorawan_active()) {
lorawan::initialize();
}
#endif #endif
} }
/*****************************************************************
* Helper functions *
*****************************************************************/
void keepServicesAlive();
void checkFlashButton();
void checkSerialInput();
/***************************************************************** /*****************************************************************
* Main loop * * Main loop *
*****************************************************************/ *****************************************************************/
void loop() { void loop() {
#if defined(AMPEL_LORAWAN) && defined(ESP32) #if defined(ESP32)
//LMIC Library seems to be very sensitive to timing issues, so run it first. if (config::is_lorawan_active()) {
lorawan::process(); //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. if (lorawan::waiting_for_confirmation) {
return; // If node is waiting for join confirmation from Gateway, nothing else should run.
return;
}
} }
#endif #endif
//NOTE: Loop should never take more than 1000ms. Split in smaller methods and logic if needed. //NOTE: Loop should never take more than 1000ms. Split in smaller methods and logic if needed.
...@@ -142,19 +130,21 @@ void loop() { ...@@ -142,19 +130,21 @@ void loop() {
// Short press for night mode, Long press for calibration. // Short press for night mode, Long press for calibration.
checkFlashButton(); checkFlashButton();
checkSerialInput(); sensor_console::checkSerialInput();
if (sensor::processData()) { if (sensor::processData()) {
#ifdef AMPEL_CSV if (config::is_csv_active()) {
csv_writer::logIfTimeHasCome(sensor::timestamp, sensor::co2, sensor::temperature, sensor::humidity); csv_writer::logIfTimeHasCome(sensor::timestamp, sensor::co2, sensor::temperature, sensor::humidity);
#endif }
#if defined(AMPEL_WIFI) && defined(AMPEL_MQTT) if (config::is_wifi_on && config::is_mqtt_active()) {
mqtt::publishIfTimeHasCome(sensor::timestamp, sensor::co2, sensor::temperature, sensor::humidity); mqtt::publishIfTimeHasCome(sensor::timestamp, sensor::co2, sensor::temperature, sensor::humidity);
#endif }
#if defined(AMPEL_LORAWAN) && defined(ESP32) #if defined(ESP32)
lorawan::preparePayloadIfTimeHasCome(sensor::co2, sensor::temperature, sensor::humidity); if (config::is_lorawan_active()) {
lorawan::preparePayloadIfTimeHasCome(sensor::co2, sensor::temperature, sensor::humidity);
}
#endif #endif
} }
...@@ -167,12 +157,39 @@ void loop() { ...@@ -167,12 +157,39 @@ void loop() {
} }
} }
void checkSerialInput() { /*****************************************************************
while (Serial.available() > 0) { * Callbacks *
sensor_console::processSerialInput(Serial.read()); *****************************************************************/
void wifiConnected() {
led_effects::showKITTWheel(color::green);
Serial.println();
Serial.print(F("WiFi - Connected! 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);
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() {
Serial.print(F("WiFi - Could not connect to "));
Serial.println(config::selected_ssid());
led_effects::showKITTWheel(color::red);
}
/*****************************************************************
* Helper functions *
*****************************************************************/
/** /**
* Checks if flash button has been pressed: * Checks if flash button has been pressed:
* If not, do nothing. * If not, do nothing.
...@@ -186,6 +203,7 @@ void checkFlashButton() { ...@@ -186,6 +203,7 @@ void checkFlashButton() {
if (digitalRead(0)) { if (digitalRead(0)) {
Serial.println(F("Flash has been pressed for a short time. Should toggle night mode.")); Serial.println(F("Flash has been pressed for a short time. Should toggle night mode."));
led_effects::toggleNightMode(); led_effects::toggleNightMode();
//NOTE: Start Access Point instead?
} else { } else {
Serial.println(F("Flash has been pressed for a long time. Keep it pressed for calibration.")); Serial.println(F("Flash has been pressed for a long time. Keep it pressed for calibration."));
if (led_effects::countdownToZero()) { if (led_effects::countdownToZero()) {
...@@ -199,20 +217,13 @@ void checkFlashButton() { ...@@ -199,20 +217,13 @@ void checkFlashButton() {
} }
void keepServicesAlive() { void keepServicesAlive() {
#ifdef AMPEL_WIFI if (config::is_wifi_on) {
if (wifi::connected()) { web_config::update();
# if defined(ESP8266) if (wifi::connected()) {
//NOTE: Sadly, there seems to be a bug in the current MDNS implementation. ntp::update(); // NTP client has its own timer. It will connect to NTP server every 60s.
// It stops working after 2 minutes. And forcing a restart leads to a memory leak. if (config::is_mqtt_active()) {
MDNS.update(); mqtt::keepConnection(); // MQTT client has its own timer. It will keep alive every 15s.
# 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
} }
#endif
} }
#include "co2_sensor.h" #include "co2_sensor.h"
#include "config.h" #include "web_config.h"
#include "ntp.h" #include "ntp.h"
#include "led_effects.h" #include "led_effects.h"
#include "sensor_console.h" #include "sensor_console.h"
...@@ -11,25 +11,12 @@ ...@@ -11,25 +11,12 @@
#include "src/lib/SparkFun_SCD30_Arduino_Library/src/SparkFun_SCD30_Arduino_Library.h" // From: http://librarymanager/All#SparkFun_SCD30 #include "src/lib/SparkFun_SCD30_Arduino_Library/src/SparkFun_SCD30_Arduino_Library.h" // From: http://librarymanager/All#SparkFun_SCD30
namespace config { 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 uint16_t measurement_timestep_bootup = 5; // [s] Measurement timestep during acclimatization.
const uint8_t max_deviation_during_bootup = 20; // [%] const uint8_t max_deviation_during_bootup = 20; // [%]
const int8_t max_deviation_during_calibration = 30; // [ppm] 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 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 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 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 const bool debug_sensor_states = false; // If true, log state transitions over serial console
} }
...@@ -116,7 +103,7 @@ namespace sensor { ...@@ -116,7 +103,7 @@ namespace sensor {
Serial.println(F(" s during acclimatization.")); Serial.println(F(" s during acclimatization."));
scd30.setMeasurementInterval(config::measurement_timestep_bootup); // [s] 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::defineIntCommand("timer", setTimer, F("30 (Sets measurement interval, in s)"));
sensor_console::defineCommand("calibrate", startCalibrationProcess, F("(Starts calibration process)")); sensor_console::defineCommand("calibrate", startCalibrationProcess, F("(Starts calibration process)"));
sensor_console::defineIntCommand("calibrate", calibrateSensorToSpecificPPM, sensor_console::defineIntCommand("calibrate", calibrateSensorToSpecificPPM,
...@@ -243,7 +230,7 @@ namespace sensor { ...@@ -243,7 +230,7 @@ namespace sensor {
delay(100); delay(100);
} else { } else {
// Display a flashing led ring, if concentration exceeds a specific value // Display a flashing led ring, if concentration exceeds a specific value
led_effects::redAlert(); led_effects::alert(color::red);
} }
} }
......
...@@ -3,13 +3,6 @@ ...@@ -3,13 +3,6 @@
#include <stdint.h> // For uint16_t #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 { namespace sensor {
extern uint16_t co2; extern uint16_t co2;
extern float temperature; extern float temperature;
......
#ifndef CONFIG_H_INCLUDED #ifndef CONFIG_H_INCLUDED
# define 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.
// NOTE: 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 ""
/** /**
* SERVICES * SERVICES
*/ */
// Comment or remove those lines if you want to disable the corresponding services // Define the default for corresponding services. They can be enabled/disabled later in the web-config.
# define AMPEL_WIFI // Should ESP connect to WiFi? It allows the Ampel to get time from an NTP server. # define AMPEL_WIFI true // 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 true // Should data be sent over MQTT? (AMPEL_WIFI should be enabled too)
# define AMPEL_MQTT // 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_CSV // 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")
// # define AMPEL_LORAWAN // Should data be sent over LoRaWAN? (Requires ESP32 + LoRa modem, and "MCCI LoRaWAN LMIC library")
/** /**
* WIFI * WIFI
*/ */
# define WIFI_SSID "MY_SSID" // SSID and PASSWORD need to be defined, but can be empty.
# define WIFI_PASSWORD "P4SSW0RD" # define WIFI_SSID ""
# define WIFI_PASSWORD ""
# define WIFI_TIMEOUT 30 // [s] # define WIFI_TIMEOUT 30 // [s]
/** /**
...@@ -69,13 +93,14 @@ ...@@ -69,13 +93,14 @@
# define LED_COUNT 12 # define LED_COUNT 12
/** /**
* WEB SERVER * AMPEL PASSWORD
* available at http://local_ip, with user HTTP_USER and password HTTP_PASSWORD * will be used for Access Point (without username), and for web-server available at http://local_ip
* with user 'admin', without quotes.
*/ */
// Define empty strings in order to disable authentication, or remove the constants altogether. // If left empty, the password will be set during the first configuration, via access point.
# define HTTP_USER "co2ampel" // In order to be set successfully, it should have at least 8 characters.
# define HTTP_PASSWORD "my_password" # define AMPEL_PASSWORD ""
/** /**
* MQTT * MQTT
...@@ -119,36 +144,35 @@ ...@@ -119,36 +144,35 @@
*/ */
// 1) Requires "MCCI LoRaWAN LMIC library", which will be automatically used with PlatformIO but should be added in "Arduino IDE". // 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 // 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, // 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/
// 5) The corresponding keys should be defined in LORAWAN_DEVICE_EUI, LORAWAN_APPLICATION_EUI and LORAWAN_APPLICATION_KEY // 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? // 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 # 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! // WARNING: If AMPEL_LORAWAN is true, you need to modify the 3 following constants
// This EUI must be in little-endian format, so least-significant-byte first. // They are written as hexadecimal strings, and will be parsed in the correct order.
// 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 EUI must be in big-endian format, so most-significant-byte first.
// This should also be in little endian format, see above. // You can copy the string from TheThingsNetwork as-is, without reversing the bytes.
// For TheThingsNetwork issued EUIs the last bytes should be 0xD5, 0xB3, 0x70. // For TheThingsNetwork issued EUIs the string should start with "70B3D5..."
# define LORAWAN_APPLICATION_EUI {0x00, 0x00, 0x00, 0x00, 0x00, 0xD5, 0xB3, 0x70} # define LORAWAN_DEVICE_EUI "70B3D57ED004CB17"
// This key should be in big endian format (or, since it is not really a // This should also be in big-endian format, and can be copied as is from TheThingsNetwork.
// number but a block of memory, endianness does not really apply). In # define LORAWAN_APPLICATION_EUI "0102030405060708"
// practice, a key taken from ttnctl can be copied as-is. // This should also be in big-endian format, and can be copied as is from TheThingsNetwork.
# define LORAWAN_APPLICATION_KEY {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 } # define LORAWAN_APPLICATION_KEY "9D06308E20B974919DA6404E063BE01D"
/** /**
* NTP * NTP
*/ */
# define NTP_SERVER "pool.ntp.org" # 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 # define UTC_OFFSET 1 // [h] +1 for Paris/Berlin, -5 for NYC
# define DAYLIGHT_SAVING_TIME false // true in summer, false in winter
/**
* Others
*/
# define BAUDS 115200 // Transmission rate
#endif #endif
#include "csv_writer.h" #include "csv_writer.h"
#include "config.h" #include "web_config.h"
#include "ntp.h" #include "ntp.h"
#include "led_effects.h" #include "led_effects.h"
#include "sensor_console.h" #include "sensor_console.h"
namespace config {
// Values should be defined in config.h
uint16_t csv_interval = CSV_INTERVAL; // [s]
}
namespace csv_writer { namespace csv_writer {
unsigned long last_written_at = 0; unsigned long last_written_at = 0;
char last_successful_write[23]; char last_successful_write[23];
...@@ -83,14 +79,14 @@ namespace csv_writer { ...@@ -83,14 +79,14 @@ namespace csv_writer {
} }
#endif #endif
char filename[15]; // "/ESPxxxxxx.csv\0" char filename[20]; // e.g. "/ESPxxxxxx.csv\0"
int getAvailableSpace() { int getAvailableSpace() {
return getTotalSpace() - getUsedSpace(); return getTotalSpace() - getUsedSpace();
} }
void initialize(const char *sensorId) { void initialize(const char *basename) {
snprintf(filename, sizeof(filename), "/%s.csv", sensorId); snprintf(filename, sizeof(filename), "/%.14s.csv", basename);
Serial.println(); Serial.println();
Serial.print(F("Initializing FS...")); Serial.print(F("Initializing FS..."));
...@@ -184,6 +180,7 @@ namespace csv_writer { ...@@ -184,6 +180,7 @@ namespace csv_writer {
} }
void showCSVContent() { void showCSVContent() {
//TODO: Now that ampel_name can be set, should show the content of every csv
Serial.print(F("### ")); Serial.print(F("### "));
Serial.print(filename); Serial.print(filename);
Serial.println(F(" ###")); Serial.println(F(" ###"));
......
...@@ -10,13 +10,11 @@ ...@@ -10,13 +10,11 @@
#else #else
# error Board should be either ESP8266 or ESP832 # error Board should be either ESP8266 or ESP832
#endif #endif
//NOTE: LittleFS will be available for Arduino esp32 core v2
namespace config {
extern uint16_t csv_interval; // [s]
}
namespace csv_writer { namespace csv_writer {
extern char last_successful_write[]; 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); void logIfTimeHasCome(const char *timestamp, const int16_t &co2, const float &temperature, const float &humidity);
int getAvailableSpace(); int getAvailableSpace();
extern char filename[]; extern char filename[];
......
#include "led_effects.h" #include "led_effects.h"
#include "config.h" #include "web_config.h"
#include "sensor_console.h" #include "sensor_console.h"
// Adafruit NeoPixel (Arduino library for controlling single-wire-based LED pixels and strip) // Adafruit NeoPixel (Arduino library for controlling single-wire-based LED pixels and strip)
...@@ -12,37 +12,14 @@ ...@@ -12,37 +12,14 @@
* Configuration * * Configuration *
*****************************************************************/ *****************************************************************/
namespace config { 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 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. 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". 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. //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°), // 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. // 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] uint16_t led_hues[16];
#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
} }
#if defined(ESP8266) #if defined(ESP8266)
...@@ -53,7 +30,8 @@ const int NEOPIXELS_PIN = 5; ...@@ -53,7 +30,8 @@ const int NEOPIXELS_PIN = 5;
const int NEOPIXELS_PIN = 23; const int NEOPIXELS_PIN = 23;
#endif #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 { namespace led_effects {
//On-board LED on D4, aka GPIO02 //On-board LED on D4, aka GPIO02
...@@ -94,6 +72,64 @@ namespace led_effects { ...@@ -94,6 +72,64 @@ namespace led_effects {
} }
void setupRing() { 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.begin();
pixels.setBrightness(config::max_brightness); pixels.setBrightness(config::max_brightness);
LEDsOff(); LEDsOff();
...@@ -165,7 +201,8 @@ namespace led_effects { ...@@ -165,7 +201,8 @@ namespace led_effects {
*/ */
void breathe(int16_t co2) { void breathe(int16_t co2) {
static uint8_t breathing_offset = 0; 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.setBrightness(brightness);
pixels.show(); pixels.show();
breathing_offset += co2 > config::poor_air_quality_ppm ? 6 : 3; // breathing speed. +3 looks like slow human breathing. breathing_offset += co2 > config::poor_air_quality_ppm ? 6 : 3; // breathing speed. +3 looks like slow human breathing.
...@@ -184,7 +221,7 @@ namespace led_effects { ...@@ -184,7 +221,7 @@ namespace led_effects {
pixels.setPixelColor(ledId, pixels.ColorHSV(config::led_hues[ledId], 255, brightness)); pixels.setPixelColor(ledId, pixels.ColorHSV(config::led_hues[ledId], 255, brightness));
} }
pixels.show(); pixels.show();
if (config::brightness_amplitude > 0) { if (config::max_brightness > config::min_brightness) {
breathe(co2); breathe(co2);
} }
} }
...@@ -207,7 +244,7 @@ namespace led_effects { ...@@ -207,7 +244,7 @@ namespace led_effects {
} }
} }
void redAlert() { void alert(uint32_t color) {
if (!config::display_led) { if (!config::display_led) {
onBoardLEDOn(); onBoardLEDOn();
delay(500); delay(500);
...@@ -218,7 +255,7 @@ namespace led_effects { ...@@ -218,7 +255,7 @@ namespace led_effects {
for (int i = 0; i < 10; i++) { for (int i = 0; i < 10; i++) {
pixels.setBrightness(static_cast<int>(config::max_brightness * (1 - i * 0.1))); pixels.setBrightness(static_cast<int>(config::max_brightness * (1 - i * 0.1)));
delay(50); delay(50);
pixels.fill(color::red); pixels.fill(color);
pixels.show(); pixels.show();
} }
} }
......
...@@ -20,7 +20,7 @@ namespace led_effects { ...@@ -20,7 +20,7 @@ namespace led_effects {
void LEDsOff(); void LEDsOff();
void setupRing(); void setupRing();
void redAlert(); void alert(uint32_t color);
bool countdownToZero(); bool countdownToZero();
void showWaitingLED(uint32_t color); void showWaitingLED(uint32_t color);
void showKITTWheel(uint32_t color, uint16_t duration_s = 2); void showKITTWheel(uint32_t color, uint16_t duration_s = 2);
......
#include "lorawan.h" #include "lorawan.h"
#if defined(AMPEL_LORAWAN) && defined(ESP32) #if defined(ESP32)
#include "web_config.h"
#include "led_effects.h" #include "led_effects.h"
#include "sensor_console.h" #include "sensor_console.h"
#include "util.h" #include "util.h"
#include "ntp.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" // 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. // Tested successfully with v3.2.0 and connected to a thethingsnetwork.org app.
#include <lmic.h> #include <lmic.h>
...@@ -37,12 +31,6 @@ namespace config { ...@@ -37,12 +31,6 @@ namespace config {
#else #else
# error "Region should be specified" # error "Region should be specified"
#endif #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: // Payloads will be automatically sent via MQTT by TheThingsNetwork, and can be seen with:
...@@ -53,18 +41,6 @@ namespace config { ...@@ -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}]}} // 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 // 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 { namespace lorawan {
bool waiting_for_confirmation = false; bool waiting_for_confirmation = false;
bool connected = false; bool connected = false;
...@@ -104,9 +80,9 @@ namespace lorawan { ...@@ -104,9 +80,9 @@ namespace lorawan {
void onEvent(ev_t ev) { void onEvent(ev_t ev) {
char current_time[23]; char current_time[23];
ntp::getLocalTime(current_time); ntp::getLocalTime(current_time);
Serial.print("LoRa - "); Serial.print(F("LoRa - "));
Serial.print(current_time); Serial.print(current_time);
Serial.print(" - "); Serial.print(F(" - "));
switch (ev) { switch (ev) {
case EV_JOINING: case EV_JOINING:
Serial.println(F("EV_JOINING")); Serial.println(F("EV_JOINING"));
...@@ -122,6 +98,7 @@ namespace lorawan { ...@@ -122,6 +98,7 @@ namespace lorawan {
u1_t nwkKey[16]; u1_t nwkKey[16];
u1_t artKey[16]; u1_t artKey[16];
LMIC_getSessionKeys(&netid, &devaddr, nwkKey, artKey); 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.print(F(" netid: "));
Serial.println(netid, DEC); Serial.println(netid, DEC);
Serial.print(F(" devaddr: ")); Serial.print(F(" devaddr: "));
...@@ -210,14 +187,19 @@ namespace lorawan { ...@@ -210,14 +187,19 @@ namespace lorawan {
// Prepare upstream data transmission at the next possible time. // Prepare upstream data transmission at the next possible time.
LMIC_setTxData2(1, buff, sizeof(buff), 0); LMIC_setTxData2(1, buff, sizeof(buff), 0);
//NOTE: To decode in TheThingsNetwork: //NOTE: To decode in TheThingsNetwork:
//function Decoder(bytes, port) { // function decodeUplink(input) {
// return { // return {
// co2: bytes[0] * 20, // data: {
// temp: bytes[1] / 5.0 - 10, // co2: input.bytes[0] * 20,
// rh: bytes[2] / 2.0 // temp: input.bytes[1] / 5.0 - 10,
// }; // rh: input.bytes[2] / 2.0
//} // },
// warnings: [],
// errors: []
// };
// }
} }
} }
...@@ -245,4 +227,53 @@ namespace lorawan { ...@@ -245,4 +227,53 @@ namespace lorawan {
void onEvent(ev_t ev) { void onEvent(ev_t ev) {
lorawan::onEvent(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 #endif
#ifndef AMPEL_LORAWAN_H_ #ifndef AMPEL_LORAWAN_H_
#define AMPEL_LORAWAN_H_ #define AMPEL_LORAWAN_H_
#include "config.h" # if defined(ESP32)
# if defined(AMPEL_LORAWAN) && defined(ESP32)
#include <stdint.h> // For uint32_t & uint16_t #include <stdint.h> // For uint32_t & uint16_t
namespace config { namespace config {
extern uint16_t lorawan_sending_interval; // [s]
extern const char *lorawan_frequency_plan; // e.g. "Europe 868" extern const char *lorawan_frequency_plan; // e.g. "Europe 868"
} }
......
#include "mqtt.h" #include "mqtt.h"
#include "config.h" #include "web_config.h"
#include "led_effects.h" #include "led_effects.h"
#include "sensor_console.h" #include "sensor_console.h"
#include "wifi_util.h" #include "wifi_util.h"
...@@ -14,28 +14,19 @@ ...@@ -14,28 +14,19 @@
#endif #endif
namespace config { namespace config {
// Values should be defined in config.h // Values should be defined in config.h or over webconfig
uint16_t mqtt_sending_interval = MQTT_SENDING_INTERVAL; // [s]
//INFO: Listen to every CO2 sensor which is connected to the server: //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 // 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. 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)
# if defined(ESP32) # include <WiFiClientSecure.h>
# include <WiFiClientSecure.h>
# endif
WiFiClientSecure espClient;
#else
WiFiClient espClient;
#endif #endif
PubSubClient mqttClient(espClient); WiFiClient *espClient;
PubSubClient mqttClient;
namespace mqtt { namespace mqtt {
unsigned long last_sent_at = 0; unsigned long last_sent_at = 0;
...@@ -49,11 +40,18 @@ namespace mqtt { ...@@ -49,11 +40,18 @@ namespace mqtt {
void initialize(const char *sensorId) { void initialize(const char *sensorId) {
json_sensor_format = PSTR("{\"time\":\"%s\", \"co2\":%d, \"temp\":%.1f, \"rh\":%.1f}"); json_sensor_format = PSTR("{\"time\":\"%s\", \"co2\":%d, \"temp\":%.1f, \"rh\":%.1f}");
snprintf(publish_topic, sizeof(publish_topic), "CO2sensors/%s", sensorId); 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 if (config::mqtt_encryption) {
// on the sensor every 3 months. The connection can still be encrypted, though: // The sensor doesn't check the fingerprint of the MQTT broker, because otherwise this fingerprint should be updated
espClient.setInsecure(); // If not available for ESP32, please update Arduino IDE / PlatformIO // on the sensor every 3 months. The connection can still be encrypted, though:
#endif WiFiClientSecure *secureClient = new WiFiClientSecure();
secureClient->setInsecure();
espClient = secureClient;
} else {
espClient = new WiFiClient();
}
mqttClient.setClient(*espClient);
mqttClient.setServer(config::mqtt_server, config::mqtt_port); mqttClient.setServer(config::mqtt_server, config::mqtt_port);
sensor_console::defineIntCommand("mqtt", setMQTTinterval, F("60 (Sets MQTT sending interval, in s)")); sensor_console::defineIntCommand("mqtt", setMQTTinterval, F("60 (Sets MQTT sending interval, in s)"));
...@@ -62,7 +60,7 @@ namespace mqtt { ...@@ -62,7 +60,7 @@ namespace mqtt {
} }
void publish(const char *timestamp, int16_t co2, float temperature, float humidity) { void publish(const char *timestamp, int16_t co2, float temperature, float humidity) {
if (WiFi.status() == WL_CONNECTED && mqttClient.connected()) { if (wifi::connected() && mqttClient.connected()) {
led_effects::onBoardLEDOn(); led_effects::onBoardLEDOn();
Serial.print(F("MQTT - Publishing message ... ")); Serial.print(F("MQTT - Publishing message ... "));
...@@ -108,17 +106,20 @@ namespace mqtt { ...@@ -108,17 +106,20 @@ namespace mqtt {
// It failed less than wait_after_fail ago. Not even trying. // It failed less than wait_after_fail ago. Not even trying.
return; 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 // No WIFI
return; return;
} }
Serial.print(F("MQTT - Attempting connection to ")); Serial.print(F("MQTT - Attempting connection to "));
Serial.print(MQTT_SERVER); Serial.print(config::mqtt_server);
Serial.print(MQTT_ENCRYPTED ? F(" (Encrypted") : F(" (Unencrypted")); Serial.print(config::mqtt_encryption ? F(" (Encrypted") : F(" (Unencrypted"));
Serial.print(F(", port ")); Serial.print(F(", port "));
Serial.print(MQTT_PORT); Serial.print(config::mqtt_port);
Serial.print(F(") ...")); Serial.print(F(") "));
Serial.print(F("User:'"));
Serial.print(config::mqtt_user);
Serial.print(F("' ..."));
led_effects::onBoardLEDOn(); led_effects::onBoardLEDOn();
// Wait for connection, at most 15s (default) // Wait for connection, at most 15s (default)
...@@ -137,10 +138,15 @@ namespace mqtt { ...@@ -137,10 +138,15 @@ namespace mqtt {
Serial.println(F(" Connected.")); Serial.println(F(" Connected."));
last_failed_at = 0; last_failed_at = 0;
} else { } 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(); 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(mqttClient.state());
Serial.print(F(". Will try again in ")); Serial.print(F("). Will try again in "));
Serial.print(config::wait_after_fail); Serial.print(config::wait_after_fail);
Serial.println("s."); Serial.println("s.");
} }
...@@ -170,7 +176,7 @@ namespace mqtt { ...@@ -170,7 +176,7 @@ namespace mqtt {
config::mqtt_sending_interval = sending_interval; config::mqtt_sending_interval = sending_interval;
Serial.print(F("Setting MQTT sending interval to : ")); Serial.print(F("Setting MQTT sending interval to : "));
Serial.print(config::mqtt_sending_interval); Serial.print(config::mqtt_sending_interval);
Serial.println("s."); Serial.println(F("s."));
led_effects::showKITTWheel(color::green, 1); led_effects::showKITTWheel(color::green, 1);
} }
...@@ -183,7 +189,7 @@ namespace mqtt { ...@@ -183,7 +189,7 @@ namespace mqtt {
char payload[75]; // Should be enough for info json... char payload[75]; // Should be enough for info json...
const char *json_info_format = PSTR("{\"local_ip\":\"%s\", \"ssid\":\"%s\"}"); 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); mqttClient.publish(info_topic, payload);
} }
......
...@@ -3,15 +3,6 @@ ...@@ -3,15 +3,6 @@
#include <stdint.h> // For uint32_t & uint16_t #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 { namespace mqtt {
extern char last_successful_publish[]; extern char last_successful_publish[];
extern bool connected; extern bool connected;
......
#include "ntp.h" #include "ntp.h"
#include "sensor_console.h" #include "sensor_console.h"
#include "config.h" #include "web_config.h"
#include <WiFiUdp.h> // required for NTP #include <WiFiUdp.h> // required for NTP
#include "src/lib/NTPClient/NTPClient.h" // 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 //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 // ifdefs could be used to define functions specific to ESP32, e.g. with configTime
namespace ntp { namespace ntp {
WiFiUDP ntpUDP; WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, config::ntp_server, config::utc_offset_in_seconds, 60000UL); NTPClient timeClient(ntpUDP);
bool connected_at_least_once = false; bool connected_at_least_once = false;
void setLocalTime(int32_t unix_seconds); void setLocalTime(int32_t unix_seconds);
// Should be defined, even offline
void initialize() { 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)")); 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() { void update() {
connected_at_least_once |= timeClient.update(); connected_at_least_once |= timeClient.update();
} }
......
...@@ -2,7 +2,9 @@ ...@@ -2,7 +2,9 @@
#define AMPEL_TIME_H_INCLUDED #define AMPEL_TIME_H_INCLUDED
namespace ntp { namespace ntp {
extern bool connected_at_least_once;
void initialize(); void initialize();
void connect();
void update(); void update();
void getLocalTime(char *timestamp); void getLocalTime(char *timestamp);
} }
......
#include "sensor_console.h" #include "sensor_console.h"
namespace sensor_console { namespace sensor_console {
const uint8_t MAX_COMMANDS = 20; const uint8_t MAX_COMMANDS = 26;
const uint8_t MAX_COMMAND_SIZE = 30; //TODO: Check if it's really needed? Is it including parameter???
const uint8_t MAX_COMMAND_SIZE = 20; // Should be enough for "set_time 1618829570\n"
uint8_t commands_count = 0; uint8_t commands_count = 0;
...@@ -149,6 +150,12 @@ namespace sensor_console { ...@@ -149,6 +150,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. * Tries to find the corresponding callback for a given command. Name and parameter type should fit.
*/ */
......
...@@ -12,7 +12,7 @@ namespace sensor_console { ...@@ -12,7 +12,7 @@ namespace sensor_console {
void defineIntCommand(const char *name, void (*function)(int32_t), const __FlashStringHelper *doc_fstring); void defineIntCommand(const char *name, void (*function)(int32_t), const __FlashStringHelper *doc_fstring);
void defineStringCommand(const char *name, void (*function)(char*), const __FlashStringHelper *doc_fstring); void 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); 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")
{
"folders": [
{
"path": "."
},
{
"path": "../IotWebConf-examples/IotWebConf01Minimal"
},
{
"path": "../IotWebConf-examples/IotWebConf02StatusAndReset"
},
{
"path": "../IotWebConf-examples/IotWebConf03CustomParameters"
},
{
"path": "../IotWebConf-examples/IotWebConf03TypedParameters"
},
{
"path": "../IotWebConf-examples/IotWebConf04UpdateServer"
},
{
"path": "../IotWebConf-examples/IotWebConf05Callbacks"
},
{
"path": "../IotWebConf-examples/IotWebConf06MqttApp"
},
{
"path": "../IotWebConf-examples/IotWebConf07MqttRelay"
},
{
"path": "../IotWebConf-examples/IotWebConf08WebRelay"
},
{
"path": "../IotWebConf-examples/IotWebConf09CustomConnection"
},
{
"path": "../IotWebConf-examples/IotWebConf10CustomHtml"
},
{
"path": "../IotWebConf-examples/IotWebConf11AdvancedRuntime"
},
{
"path": "../IotWebConf-examples/IotWebConf12CustomParameterType"
},
{
"path": "../IotWebConf-examples/IotWebConf13OptionalGroup"
},
{
"path": "../IotWebConf-examples/IotWebConf14GroupChain"
},
{
"path": "../IotWebConf-examples/IotWebConf15MultipleWifi"
},
{
"path": "../IotWebConf-examples/IotWebConf16OffLineMode"
},
{
"path": "../IotWebConf-examples/IotWebConf17JsonConfig"
}
],
"settings": {
"workbench.tree.indent": 16
}
}
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