new version 0_2_2 from stuttgart
This commit is contained in:
parent
0514952897
commit
c1b71e9829
32 changed files with 1049 additions and 501 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -3,8 +3,6 @@
|
||||||
.pio/build
|
.pio/build
|
||||||
.vscode
|
.vscode
|
||||||
*.ino.cpp
|
*.ino.cpp
|
||||||
config.h
|
|
||||||
config.hftstuttgart.h
|
|
||||||
.project
|
.project
|
||||||
.cproject
|
.cproject
|
||||||
.settings
|
.settings
|
||||||
|
|
|
@ -2,29 +2,11 @@ image: python:3.6
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- build
|
- build
|
||||||
- release
|
|
||||||
|
|
||||||
before_script:
|
before_script:
|
||||||
- "pip install -U platformio"
|
- "pip install -U platformio"
|
||||||
- "cp ampel-firmware/config.public.h ampel-firmware/config.h"
|
- "cp ampel-firmware/config.public.h ampel-firmware/config.h"
|
||||||
|
|
||||||
esp8266:
|
job:
|
||||||
stage: build
|
stage: build
|
||||||
script:
|
script: "platformio run"
|
||||||
- "platformio run --environment esp8266"
|
|
||||||
- cp .pio/build/esp8266/firmware.bin esp8266.bin
|
|
||||||
artifacts:
|
|
||||||
paths:
|
|
||||||
- esp8266.bin
|
|
||||||
expire_in: 1 week
|
|
||||||
|
|
||||||
release:
|
|
||||||
image: inetprocess/gitlab-release
|
|
||||||
stage: release
|
|
||||||
only:
|
|
||||||
- tags
|
|
||||||
dependencies:
|
|
||||||
- esp8266
|
|
||||||
script:
|
|
||||||
- gitlab-release --message '' ./*.bin
|
|
||||||
|
|
||||||
|
|
25
CHANGELOG.md
Normal file
25
CHANGELOG.md
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
# Change Log
|
||||||
|
|
||||||
|
## [v0.2.2](https://transfer.hft-stuttgart.de/gitlab/co2ampel/ampel-firmware/-/releases/v0.2.2) 2021/10/01
|
||||||
|
|
||||||
|
* MAC address is now shown at startup and via HTTP
|
||||||
|
|
||||||
|
## [v0.2.1](https://transfer.hft-stuttgart.de/gitlab/co2ampel/ampel-firmware/-/releases/v0.2.1) 2021/06/06
|
||||||
|
|
||||||
|
* BUGFIX: Sensor would keep on calibrating after successful calibration.
|
||||||
|
|
||||||
|
## [v0.2.0](https://transfer.hft-stuttgart.de/gitlab/co2ampel/ampel-firmware/-/releases/v0.2.0) 2021/06/06
|
||||||
|
|
||||||
|
* BUGFIX: Calibration was not applied correctly (Thanks Michael Käppler for bug finding & fixing!)
|
||||||
|
* MQTT works again for ESP32 (board needs to be updated in Arduino IDE/PlaftormIO)
|
||||||
|
|
||||||
|
## [v0.1.0](https://transfer.hft-stuttgart.de/gitlab/co2ampel/ampel-firmware/-/releases/v0.1.0) 2021/05/19
|
||||||
|
|
||||||
|
* More stable calibration, with 10s timestep
|
||||||
|
* Allow commands from Serial/MQTT/Webserver
|
||||||
|
* Many bugfixes
|
||||||
|
|
||||||
|
## [v0.0.9](https://transfer.hft-stuttgart.de/gitlab/co2ampel/ampel-firmware/-/releases/v0.0.9) 2020/12/15
|
||||||
|
|
||||||
|
* First CO2 Ampel release.
|
||||||
|
* Basic functionality
|
70
README.md
70
README.md
|
@ -1,6 +1,6 @@
|
||||||
# CO<sub>2</sub> Ampel
|
# CO<sub>2</sub> Ampel
|
||||||
|
|
||||||
*CO<sub>2</sub> Ampel* is an open-source project, written in C++ for ESP8266 or ESP32.
|
*CO<sub>2</sub> Ampel* is an open-source project, written in C++ for [ESP8266](https://en.wikipedia.org/wiki/ESP8266) or [ESP32](https://en.wikipedia.org/wiki/ESP32).
|
||||||
|
|
||||||
It measures the current CO<sub>2</sub> concentration (in ppm), and displays it on an LED ring.
|
It measures the current CO<sub>2</sub> concentration (in ppm), and displays it on an LED ring.
|
||||||
|
|
||||||
|
@ -12,20 +12,20 @@ The *CO<sub>2</sub> Ampel* can:
|
||||||
|
|
||||||
* Display CO2 concentration on LED ring.
|
* Display CO2 concentration on LED ring.
|
||||||
* Allow calibration.
|
* Allow calibration.
|
||||||
* Get current time over NTP
|
* Get current time over [NTP](https://en.wikipedia.org/wiki/Network_Time_Protocol)
|
||||||
* Send data over MQTT.
|
* Send data over [MQTT](https://en.wikipedia.org/wiki/MQTT).
|
||||||
* Send data over LoRaWAN.
|
* Send data over [LoRaWAN](https://en.wikipedia.org/wiki/LoRa#LoRaWAN).
|
||||||
* Display measurements and configuration on a small website.
|
* Display measurements and configuration on a small website.
|
||||||
* Log data to a CSV file, directly on the ESP flash memory.
|
* Log data to a [CSV](https://en.wikipedia.org/wiki/Comma-separated_values) file, directly on the ESP flash memory.
|
||||||
|
* Accept many interactive commands.
|
||||||
|
|
||||||
## Hardware Requirements
|
## Hardware Requirements
|
||||||
|
|
||||||
* [ESP8266](https://en.wikipedia.org/wiki/ESP8266) or [ESP32](https://en.wikipedia.org/wiki/ESP32) microcontroller (this project has been tested with *ESP8266 ESP-12 WIFI* and *TTGO ESP32 SX1276 LoRa*)
|
* [ESP8266](https://en.wikipedia.org/wiki/ESP8266) or [ESP32](https://en.wikipedia.org/wiki/ESP32) microcontroller (this project has been tested with *ESP8266 ESP-12 WIFI* and *TTGO ESP32 SX1276 LoRa*)
|
||||||
* [Sensirion SCD30](https://www.sensirion.com/en/environmental-sensors/carbon-dioxide-sensors/carbon-dioxide-sensors-co2/) "Sensor Module for HVAC and Indoor Air Quality Applications"
|
* [Sensirion SCD30](https://www.sensirion.com/en/environmental-sensors/carbon-dioxide-sensors/carbon-dioxide-sensors-co2/) "Sensor Module for HVAC and Indoor Air Quality Applications"
|
||||||
* [NeoPixel Ring - 12](https://www.adafruit.com/product/1643)
|
* [NeoPixel Ring - 12](https://www.adafruit.com/product/1643), or [NeoPixel Ring - 16](https://www.adafruit.com/product/1463) (experimental)
|
||||||
|
|
||||||
See the [original documentation](https://transfer.hft-stuttgart.de/gitlab/co2ampel/ampel-documentation) for more info.
|
See the [documentation](https://transfer.hft-stuttgart.de/gitlab/co2ampel/ampel-documentation) for more info.
|
||||||
Or our Wiki [MakerLab Wiki CO2 Ampel](https://wiki.makerlab-murnau.de/books/co2-ampel).
|
|
||||||
|
|
||||||
## Software Requirements
|
## Software Requirements
|
||||||
|
|
||||||
|
@ -68,27 +68,49 @@ make upload board=esp32 && make monitor # For ESP32
|
||||||
* *Upload*
|
* *Upload*
|
||||||
* *Tools > Serial Monitor*
|
* *Tools > Serial Monitor*
|
||||||
|
|
||||||
|
## Available commands
|
||||||
|
|
||||||
|
In Arduino IDE *Serial Monitor* or PlatformIO *Monitor*, type `help` + <kbd>Enter</kbd> in order to list the available commands:
|
||||||
|
|
||||||
|
* `auto_calibrate 0/1` (Disables/enables autocalibration).
|
||||||
|
* `calibrate` (Starts calibration process).
|
||||||
|
* `calibrate 600` (Starts calibration process, to given ppm).
|
||||||
|
* `calibrate! 600` (Calibrates right now, to given ppm).
|
||||||
|
* `co2 1500` (Sets co2 level, for debugging purposes).
|
||||||
|
* `color 0xFF0015` (Shows color, specified as RGB, for debugging).
|
||||||
|
* `csv 60` (Sets CSV writing interval, in s).
|
||||||
|
* `format_filesystem` (Deletes the whole filesystem).
|
||||||
|
* `free` (Displays available heap space).
|
||||||
|
* `local_ip` (Displays local IP and current SSID).
|
||||||
|
* `lora 300` (Sets LoRaWAN sending interval, in s).
|
||||||
|
* `mqtt 60` (Sets MQTT sending interval, in s).
|
||||||
|
* `night_mode` (Toggles night mode on/off).
|
||||||
|
* `reset` (Restarts the ESP).
|
||||||
|
* `reset_scd` (Resets SCD30).
|
||||||
|
* `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).
|
||||||
|
* `timer 30` (Sets measurement interval, in s).
|
||||||
|
* `wifi_scan` (Scans available WiFi networks).
|
||||||
|
|
||||||
|
The commands can be sent via the Serial interface, from the webpage or via MQTT.
|
||||||
|
|
||||||
## Authors
|
## Authors
|
||||||
|
|
||||||
* Eric Duminil
|
* Eric Duminil (HfT Stuttgart)
|
||||||
* Robert Otto
|
* Robert Otto (HfT Stuttgart)
|
||||||
* Myriam Guedey
|
* Myriam Guedey (HfT Stuttgart)
|
||||||
* Tobias Gabriel Erhart
|
* Tobias Gabriel Erhart (HfT Stuttgart)
|
||||||
* Jonas Stave
|
* Jonas Stave (HfT Stuttgart)
|
||||||
|
* Michael Käppler
|
||||||
Hochschule für Technik Stuttgart
|
|
||||||
|
|
||||||
## Modifications by
|
|
||||||
|
|
||||||
* Jens Noack
|
|
||||||
|
|
||||||
MakerLab Murnau e.V.
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
|
|
||||||
|
* Merge requests are welcome, and should be based on the `develop` branch.
|
||||||
|
* The `develop` branch gets merged into the `master` once it has been sufficiently tested.
|
||||||
|
* For major changes, please open an issue first to discuss what you would like to change.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Copyright © 2020, [HfT Stuttgart](https://www.hft-stuttgart.de/)
|
Copyright © 2021, [HfT Stuttgart](https://www.hft-stuttgart.de/)
|
||||||
|
|
||||||
[GPLv3](https://choosealicense.com/licenses/gpl-3.0/)
|
[GPLv3](https://choosealicense.com/licenses/gpl-3.0/)
|
||||||
|
|
|
@ -20,6 +20,12 @@
|
||||||
# ifdef AMPEL_HTTP
|
# ifdef AMPEL_HTTP
|
||||||
# include "web_server.h"
|
# include "web_server.h"
|
||||||
# endif
|
# 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
|
#endif
|
||||||
|
|
||||||
#ifdef AMPEL_LORAWAN
|
#ifdef AMPEL_LORAWAN
|
||||||
|
@ -27,17 +33,8 @@
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include "util.h"
|
#include "util.h"
|
||||||
|
#include "sensor_console.h"
|
||||||
#include "co2_sensor.h"
|
#include "co2_sensor.h"
|
||||||
#include "led_effects.h"
|
#include "led_effects.h"
|
||||||
|
|
||||||
#if defined(ESP8266)
|
|
||||||
//allows sensor to be seen as SENSOR_ID.local, from the local network. For example : espd03cc5.local
|
|
||||||
# include <ESP8266mDNS.h>
|
|
||||||
#elif defined(ESP32)
|
|
||||||
# include <ESPmDNS.h>
|
|
||||||
#endif
|
|
||||||
|
|
||||||
void keepServicesAlive();
|
|
||||||
void checkFlashButton();
|
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -43,6 +43,7 @@
|
||||||
* Myriam Guedey
|
* Myriam Guedey
|
||||||
* Tobias Gabriel Erhart
|
* Tobias Gabriel Erhart
|
||||||
* Jonas Stave
|
* Jonas Stave
|
||||||
|
* Michael Käppler
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/*****************************************************************
|
/*****************************************************************
|
||||||
|
@ -64,19 +65,28 @@ void setup() {
|
||||||
|
|
||||||
Serial.begin(BAUDS);
|
Serial.begin(BAUDS);
|
||||||
|
|
||||||
pinMode(0, INPUT); // Flash button (used for forced calibration)
|
pinMode(buttonPin, INPUT); // Flash button (used for forced calibration)
|
||||||
|
|
||||||
|
Serial.println();
|
||||||
|
Serial.print(F("Sensor ID: "));
|
||||||
|
Serial.println(ampel.sensorId);
|
||||||
|
Serial.print(F("MAC : "));
|
||||||
|
Serial.println(ampel.macAddress);
|
||||||
|
Serial.print(F("Board : "));
|
||||||
|
Serial.println(ampel.board);
|
||||||
|
Serial.print(F("Firmware : "));
|
||||||
|
Serial.println(ampel.version);
|
||||||
|
|
||||||
led_effects::setupRing();
|
led_effects::setupRing();
|
||||||
|
led_effects::showRainbowWheel(5000);
|
||||||
sensor::initialize();
|
sensor::initialize();
|
||||||
|
|
||||||
Serial.print(F("Sensor ID: "));
|
#ifdef AMPEL_CSV
|
||||||
Serial.println(SENSOR_ID);
|
csv_writer::initialize(ampel.sensorId);
|
||||||
Serial.print(F("Board : "));
|
#endif
|
||||||
Serial.println(BOARD);
|
|
||||||
|
|
||||||
#ifdef AMPEL_WIFI
|
#ifdef AMPEL_WIFI
|
||||||
WiFiConnect(SENSOR_ID);
|
wifi::connect(ampel.sensorId);
|
||||||
|
|
||||||
Serial.print(F("WiFi - Status: "));
|
Serial.print(F("WiFi - Status: "));
|
||||||
Serial.println(WiFi.status());
|
Serial.println(WiFi.status());
|
||||||
|
@ -88,7 +98,7 @@ void setup() {
|
||||||
|
|
||||||
ntp::initialize();
|
ntp::initialize();
|
||||||
|
|
||||||
if (MDNS.begin(SENSOR_ID.c_str())) { // Start the mDNS responder for SENSOR_ID.local
|
if (MDNS.begin(ampel.sensorId)) { // Start the mDNS responder for SENSOR_ID.local
|
||||||
MDNS.addService("http", "tcp", 80);
|
MDNS.addService("http", "tcp", 80);
|
||||||
Serial.println(F("mDNS responder started"));
|
Serial.println(F("mDNS responder started"));
|
||||||
} else {
|
} else {
|
||||||
|
@ -96,24 +106,26 @@ void setup() {
|
||||||
}
|
}
|
||||||
|
|
||||||
# ifdef AMPEL_MQTT
|
# ifdef AMPEL_MQTT
|
||||||
mqtt::initialize("CO2sensors/" + SENSOR_ID);
|
mqtt::initialize(ampel.sensorId);
|
||||||
# endif
|
# endif
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef AMPEL_CSV
|
|
||||||
csv_writer::initialize();
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if defined(AMPEL_LORAWAN) && defined(ESP32)
|
#if defined(AMPEL_LORAWAN) && defined(ESP32)
|
||||||
lorawan::initialize();
|
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(AMPEL_LORAWAN) && defined(ESP32)
|
||||||
//LMIC Library seems to be very sensitive to timing issues, so run it first.
|
//LMIC Library seems to be very sensitive to timing issues, so run it first.
|
||||||
|
@ -133,6 +145,8 @@ void loop() {
|
||||||
// Short press for night mode, Long press for calibration.
|
// Short press for night mode, Long press for calibration.
|
||||||
checkFlashButton();
|
checkFlashButton();
|
||||||
|
|
||||||
|
checkSerialInput();
|
||||||
|
|
||||||
if (sensor::processData()) {
|
if (sensor::processData()) {
|
||||||
#ifdef AMPEL_CSV
|
#ifdef AMPEL_CSV
|
||||||
csv_writer::logIfTimeHasCome(sensor::timestamp, sensor::co2, sensor::temperature, sensor::humidity);
|
csv_writer::logIfTimeHasCome(sensor::timestamp, sensor::co2, sensor::temperature, sensor::humidity);
|
||||||
|
@ -148,11 +162,17 @@ void loop() {
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t duration = millis() - t0;
|
uint32_t duration = millis() - t0;
|
||||||
if (duration > max_loop_duration) {
|
if (duration > ampel.max_loop_duration) {
|
||||||
max_loop_duration = duration;
|
ampel.max_loop_duration = duration;
|
||||||
Serial.print(F("Debug - Max loop duration : "));
|
Serial.print(F("Debug - Max loop duration : "));
|
||||||
Serial.print(max_loop_duration);
|
Serial.print(ampel.max_loop_duration);
|
||||||
Serial.println(" ms.");
|
Serial.println(F(" ms."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void checkSerialInput() {
|
||||||
|
while (Serial.available() > 0) {
|
||||||
|
sensor_console::processSerialInput(Serial.read());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -163,16 +183,18 @@ void loop() {
|
||||||
* If long press, start calibration process.
|
* If long press, start calibration process.
|
||||||
*/
|
*/
|
||||||
void checkFlashButton() {
|
void checkFlashButton() {
|
||||||
if (!digitalRead(buttonPin)) { // Button has been pressed
|
if (!digitalRead(0)) { // Button has been pressed
|
||||||
led_effects::onBoardLEDOn();
|
led_effects::onBoardLEDOn();
|
||||||
delay(300);
|
delay(300);
|
||||||
if (digitalRead(buttonPin)) {
|
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();
|
||||||
} 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() < 0) {
|
if (led_effects::countdownToZero()) {
|
||||||
|
Serial.println(F("You can now release the button."));
|
||||||
sensor::startCalibrationProcess();
|
sensor::startCalibrationProcess();
|
||||||
|
led_effects::showKITTWheel(color::red, 2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
led_effects::onBoardLEDOff();
|
led_effects::onBoardLEDOff();
|
||||||
|
|
|
@ -1,10 +1,17 @@
|
||||||
#include "co2_sensor.h"
|
#include "co2_sensor.h"
|
||||||
|
|
||||||
namespace config {
|
namespace config {
|
||||||
// Values should be defined in config.h
|
// UPPERCASE values should be defined in config.h
|
||||||
uint16_t measurement_timestep = MEASUREMENT_TIMESTEP; // [s] Value between 2 and 1800 (range for SCD30 sensor)
|
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]
|
const uint16_t altitude_above_sea_level = ALTITUDE_ABOVE_SEA_LEVEL; // [m]
|
||||||
uint16_t co2_calibration_level = ATMOSPHERIC_CO2_CONCENTRATION; // [ppm]
|
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
|
#ifdef TEMPERATURE_OFFSET
|
||||||
// Residual heat from CO2 sensor seems to be high enough to change the temperature reading. How much should it be 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.
|
// NOTE: Sign isn't relevant. The returned temperature will always be shifted down.
|
||||||
|
@ -12,23 +19,46 @@ namespace config {
|
||||||
#else
|
#else
|
||||||
const float temperature_offset = -3.0; // [K] Temperature measured by sensor is usually at least 3K too high.
|
const float temperature_offset = -3.0; // [K] Temperature measured by sensor is usually at least 3K too high.
|
||||||
#endif
|
#endif
|
||||||
const bool auto_calibrate_sensor = AUTO_CALIBRATE_SENSOR; // [true / false]
|
bool auto_calibrate_sensor = AUTO_CALIBRATE_SENSOR; // [true / false]
|
||||||
|
const bool debug_sensor_states = false; // If true, log state transitions over serial console
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace sensor {
|
namespace sensor {
|
||||||
SCD30 scd30;
|
SCD30 scd30;
|
||||||
int16_t co2 = 0;
|
uint16_t co2 = 0;
|
||||||
float temperature = 0;
|
float temperature = 0;
|
||||||
float humidity = 0;
|
float humidity = 0;
|
||||||
String timestamp = "";
|
char timestamp[23];
|
||||||
int16_t stable_measurements = 0;
|
int16_t stable_measurements = 0;
|
||||||
uint32_t waiting_color = color::blue;
|
|
||||||
bool should_calibrate = false;
|
/**
|
||||||
unsigned long time_calaibration_started = millis();
|
* Define sensor states
|
||||||
|
* BOOTUP -> initial state, until first >0 ppm values are returned
|
||||||
|
* READY -> sensor does output valid information (> 0 ppm) and no other condition takes place
|
||||||
|
* NEEDS_CALIBRATION -> sensor measurements are too low (< 250 ppm)
|
||||||
|
* PREPARE_CALIBRATION_UNSTABLE -> forced calibration was initiated, last measurements were too far apart
|
||||||
|
* PREPARE_CALIBRATION_STABLE -> forced calibration was initiated, last measurements were close to each others
|
||||||
|
*/
|
||||||
|
enum state {
|
||||||
|
BOOTUP,
|
||||||
|
READY,
|
||||||
|
NEEDS_CALIBRATION,
|
||||||
|
PREPARE_CALIBRATION_UNSTABLE,
|
||||||
|
PREPARE_CALIBRATION_STABLE
|
||||||
|
};
|
||||||
|
const char *state_names[] = {
|
||||||
|
"BOOTUP",
|
||||||
|
"READY",
|
||||||
|
"NEEDS_CALIBRATION",
|
||||||
|
"PREPARE_CALIBRATION_UNSTABLE",
|
||||||
|
"PREPARE_CALIBRATION_STABLE" };
|
||||||
|
|
||||||
|
state current_state = BOOTUP;
|
||||||
|
void switchState(state);
|
||||||
|
|
||||||
void initialize() {
|
void initialize() {
|
||||||
#if defined(ESP8266)
|
#if defined(ESP8266)
|
||||||
Wire.begin(12, 14); // ESP8266 - SDA: D6, SCL: D5;
|
Wire.begin(12, 14); // ESP8266 - D6, D5;
|
||||||
#endif
|
#endif
|
||||||
#if defined(ESP32)
|
#if defined(ESP32)
|
||||||
Wire.begin(21, 22); // ESP32
|
Wire.begin(21, 22); // ESP32
|
||||||
|
@ -40,81 +70,103 @@ namespace sensor {
|
||||||
* SDA --- SDA (GPIO21) //NOTE: GPIO1 would be more convenient (right next to GPO3)
|
* SDA --- SDA (GPIO21) //NOTE: GPIO1 would be more convenient (right next to GPO3)
|
||||||
*/
|
*/
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// CO2
|
|
||||||
if (scd30.begin(config::auto_calibrate_sensor) == false) {
|
|
||||||
Serial.println("Air sensor not detected. Please check wiring. Freezing...");
|
|
||||||
while (1) {
|
|
||||||
led_effects::showWaitingLED(color::red);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SCD30 has its own timer.
|
|
||||||
//NOTE: The timer seems to be inaccurate, though, possibly depending on voltage. Should it be offset?
|
|
||||||
Serial.println();
|
Serial.println();
|
||||||
Serial.print(F("Setting SCD30 timestep to "));
|
scd30.enableDebugging(); // Prints firmware version in the console.
|
||||||
Serial.print(config::measurement_timestep);
|
|
||||||
Serial.println(" s.");
|
if (!scd30.begin(config::auto_calibrate_sensor)) {
|
||||||
scd30.setMeasurementInterval(config::measurement_timestep); // [s]
|
Serial.println(F("ERROR - CO2 sensor not detected. Please check wiring!"));
|
||||||
|
led_effects::showKITTWheel(color::red, 30);
|
||||||
|
ESP.restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Changes of the SCD30's measurement timestep do not come into effect
|
||||||
|
// before the next measurement takes place. That means that after a hard reset
|
||||||
|
// of the ESP the SCD30 sometimes needs a long time until switching back to 2 s
|
||||||
|
// for acclimatization. Resetting it after startup seems to fix this behaviour.
|
||||||
|
scd30.reset();
|
||||||
|
|
||||||
Serial.print(F("Setting temperature offset to -"));
|
Serial.print(F("Setting temperature offset to -"));
|
||||||
Serial.print(abs(config::temperature_offset));
|
Serial.print(abs(config::temperature_offset));
|
||||||
Serial.println(" K.");
|
Serial.println(F(" K."));
|
||||||
scd30.setTemperatureOffset(abs(config::temperature_offset)); // setTemperatureOffset only accepts positive numbers, but shifts the temperature down.
|
scd30.setTemperatureOffset(abs(config::temperature_offset)); // setTemperatureOffset only accepts positive numbers, but shifts the temperature down.
|
||||||
delay(100);
|
delay(100);
|
||||||
|
|
||||||
Serial.print(F("Temperature offset is : -"));
|
Serial.print(F("Temperature offset is : -"));
|
||||||
Serial.print(scd30.getTemperatureOffset());
|
Serial.print(scd30.getTemperatureOffset());
|
||||||
Serial.println(" K");
|
Serial.println(F(" K"));
|
||||||
|
|
||||||
Serial.print(F("Auto-calibration is "));
|
Serial.print(F("Auto-calibration is "));
|
||||||
Serial.println(config::auto_calibrate_sensor ? "ON." : "OFF.");
|
Serial.println(config::auto_calibrate_sensor ? "ON." : "OFF.");
|
||||||
|
|
||||||
|
// SCD30 has its own timer.
|
||||||
|
//NOTE: The timer seems to be inaccurate, though, possibly depending on voltage. Should it be offset?
|
||||||
|
Serial.println();
|
||||||
|
Serial.print(F("Setting SCD30 timestep to "));
|
||||||
|
Serial.print(config::measurement_timestep_bootup);
|
||||||
|
Serial.println(F(" s during acclimatization."));
|
||||||
|
scd30.setMeasurementInterval(config::measurement_timestep_bootup); // [s]
|
||||||
|
|
||||||
|
sensor_console::defineIntCommand("co2", setCO2forDebugging, F("1500 (Sets co2 level, for debugging purposes)"));
|
||||||
|
sensor_console::defineIntCommand("timer", setTimer, F("30 (Sets measurement interval, in s)"));
|
||||||
|
sensor_console::defineCommand("calibrate", startCalibrationProcess, F("(Starts calibration process)"));
|
||||||
|
sensor_console::defineIntCommand("calibrate", calibrateSensorToSpecificPPM,
|
||||||
|
F("600 (Starts calibration process, to given ppm)"));
|
||||||
|
sensor_console::defineIntCommand("calibrate!", calibrateSensorRightNow,
|
||||||
|
F("600 (Calibrates right now, to given ppm)"));
|
||||||
|
sensor_console::defineIntCommand("auto_calibrate", setAutoCalibration, F("0/1 (Disables/enables autocalibration)"));
|
||||||
|
sensor_console::defineCommand("reset_scd", resetSCD, F("(Resets SCD30)"));
|
||||||
}
|
}
|
||||||
|
|
||||||
//NOTE: should timer deviation be used to adjust measurement_timestep?
|
bool hasSensorSettled() {
|
||||||
void checkTimerDeviation() {
|
static uint16_t last_co2 = 0;
|
||||||
static int32_t previous_measurement_at = 0;
|
uint16_t delta;
|
||||||
int32_t now = millis();
|
delta = abs(co2 - last_co2);
|
||||||
Serial.print("Measurement time offset : ");
|
last_co2 = co2;
|
||||||
Serial.print(now - previous_measurement_at - config::measurement_timestep * 1000);
|
// We assume the sensor has acclimated to the environment if measurements
|
||||||
Serial.println(" ms.");
|
// change less than a specified percentage of the current value.
|
||||||
previous_measurement_at = now;
|
return (co2 > 0 && delta < ((uint32_t) co2 * config::max_deviation_during_bootup / 100));
|
||||||
}
|
}
|
||||||
|
|
||||||
void countStableMeasurements() {
|
bool enoughStableMeasurements() {
|
||||||
static int16_t previous_co2 = 0;
|
static int16_t previous_co2 = 0;
|
||||||
if (co2 > (previous_co2 - 30) && co2 < (previous_co2 + 30)) {
|
if (co2 > (previous_co2 - config::max_deviation_during_calibration)
|
||||||
|
&& co2 < (previous_co2 + config::max_deviation_during_calibration)) {
|
||||||
stable_measurements++;
|
stable_measurements++;
|
||||||
Serial.print(F("Number of stable measurements : "));
|
Serial.print(F("Number of stable measurements : "));
|
||||||
Serial.println(stable_measurements);
|
Serial.print(stable_measurements);
|
||||||
waiting_color = color::green;
|
Serial.print(F(" / "));
|
||||||
|
Serial.println(config::stable_measurements_before_calibration);
|
||||||
|
switchState(PREPARE_CALIBRATION_STABLE);
|
||||||
} else {
|
} else {
|
||||||
stable_measurements = 0;
|
stable_measurements = 0;
|
||||||
waiting_color = color::red;
|
switchState(PREPARE_CALIBRATION_UNSTABLE);
|
||||||
}
|
}
|
||||||
previous_co2 = co2;
|
previous_co2 = co2;
|
||||||
|
return (stable_measurements == config::stable_measurements_before_calibration);
|
||||||
}
|
}
|
||||||
|
|
||||||
void startCalibrationProcess() {
|
void startCalibrationProcess() {
|
||||||
/** From the sensor documentation:
|
/** From the sensor documentation:
|
||||||
* For best results, the sensor has to be run in a stable environment in continuous mode at
|
* Before applying FRC, SCD30 needs to be operated for 2 minutes with the desired measurement period in continuous mode.
|
||||||
* a measurement rate of 2s for at least two minutes before applying the FRC command and sending the reference value.
|
|
||||||
*/
|
*/
|
||||||
Serial.println(F("Setting SCD30 timestep to 2s, prior to calibration."));
|
Serial.print(F("Setting SCD30 timestep to "));
|
||||||
scd30.setMeasurementInterval(MEASUREMENT_TIMESTEP); // [s] The change will only take effect after next measurement.
|
Serial.print(config::timestep_during_calibration);
|
||||||
|
Serial.println(F("s, prior to calibration."));
|
||||||
|
scd30.setMeasurementInterval(config::timestep_during_calibration); // [s] The change will only take effect after next measurement.
|
||||||
Serial.println(F("Waiting until the measurements are stable for at least 2 minutes."));
|
Serial.println(F("Waiting until the measurements are stable for at least 2 minutes."));
|
||||||
Serial.println(F("It could take a very long time."));
|
Serial.println(F("It could take a very long time."));
|
||||||
should_calibrate = true;
|
switchState(PREPARE_CALIBRATION_UNSTABLE);
|
||||||
time_calaibration_started = millis();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void calibrateAndRestart() {
|
void calibrate() {
|
||||||
Serial.print(F("Calibrating SCD30 now..."));
|
Serial.print(F("Calibrating SCD30 now..."));
|
||||||
scd30.setAltitudeCompensation(config::altitude_above_sea_level);
|
scd30.setAltitudeCompensation(config::altitude_above_sea_level);
|
||||||
scd30.setForcedRecalibrationFactor(config::co2_calibration_level);
|
scd30.setForcedRecalibrationFactor(config::co2_calibration_level);
|
||||||
Serial.println(F(" Done!"));
|
Serial.println(F(" Done!"));
|
||||||
Serial.println(F("Sensor calibrated."));
|
Serial.println(F("Sensor calibrated."));
|
||||||
ESP.restart(); // softer than ESP.reset
|
switchState(BOOTUP); // In order to stop the calibration and select the desired timestep.
|
||||||
|
//WARNING: Do not reset the ampel or the SCD30!
|
||||||
|
//At least one measurement needs to happen in order for the calibration to be correctly applied.
|
||||||
}
|
}
|
||||||
|
|
||||||
void logToSerial() {
|
void logToSerial() {
|
||||||
|
@ -127,29 +179,86 @@ namespace sensor {
|
||||||
Serial.println(humidity, 1);
|
Serial.println(humidity, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
void displayCO2OnLedRing() {
|
void switchState(state new_state) {
|
||||||
int16_t co2_int = co2;
|
if (new_state == current_state) {
|
||||||
if (co2_int < CALIBRATE_LEVEL) {
|
|
||||||
// Sensor should be calibrated.
|
|
||||||
led_effects::showWaitingLED(color::magenta);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if(co2_int < 400) {
|
if (config::debug_sensor_states) {
|
||||||
co2_int = 400;
|
Serial.print(F("Changing sensor state: "));
|
||||||
|
Serial.print(state_names[current_state]);
|
||||||
|
Serial.print(F(" -> "));
|
||||||
|
Serial.println(state_names[new_state]);
|
||||||
}
|
}
|
||||||
|
current_state = new_state;
|
||||||
|
}
|
||||||
|
|
||||||
|
void switchStateForCurrentPPM() {
|
||||||
|
if (current_state == BOOTUP) {
|
||||||
|
if (!hasSensorSettled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switchState(READY);
|
||||||
|
Serial.println(F("Sensor acclimatization finished."));
|
||||||
|
Serial.print(F("Setting SCD30 timestep to "));
|
||||||
|
Serial.print(config::measurement_timestep);
|
||||||
|
Serial.println(F(" s."));
|
||||||
|
if (config::measurement_timestep < 10) {
|
||||||
|
Serial.println(F("WARNING: Timesteps shorter than 10s can lead to unreliable measurements!"));
|
||||||
|
}
|
||||||
|
scd30.setMeasurementInterval(config::measurement_timestep); // [s]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for pre-calibration states first, because we do not want to
|
||||||
|
// leave them before calibration is done.
|
||||||
|
if ((current_state == PREPARE_CALIBRATION_UNSTABLE) || (current_state == PREPARE_CALIBRATION_STABLE)) {
|
||||||
|
if (enoughStableMeasurements()) {
|
||||||
|
calibrate();
|
||||||
|
}
|
||||||
|
} else if (co2 < 250) {
|
||||||
|
// Sensor should be calibrated.
|
||||||
|
switchState(NEEDS_CALIBRATION);
|
||||||
|
} else {
|
||||||
|
switchState(READY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void displayCO2OnLedRing() {
|
||||||
/**
|
/**
|
||||||
* Display data, even if it's "old" (with breathing).
|
* Display data, even if it's "old" (with breathing).
|
||||||
* Those effects include a short delay.
|
* A short delay is required in order to let background tasks run on the ESP8266.
|
||||||
|
* see https://github.com/esp8266/Arduino/issues/3241#issuecomment-301290392
|
||||||
*/
|
*/
|
||||||
if (co2_int < 2000) {
|
if (co2 < config::co2_alert_threshold) {
|
||||||
led_effects::displayCO2color(co2_int);
|
led_effects::displayCO2color(co2);
|
||||||
led_effects::breathe(co2_int);
|
delay(100);
|
||||||
} else {
|
} else {
|
||||||
// >= 2000: entire ring blinks red
|
// Display a flashing led ring, if concentration exceeds a specific value
|
||||||
led_effects::redAlert();
|
led_effects::redAlert();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void showState() {
|
||||||
|
switch (current_state) {
|
||||||
|
case BOOTUP:
|
||||||
|
led_effects::showWaitingLED(color::blue);
|
||||||
|
break;
|
||||||
|
case READY:
|
||||||
|
displayCO2OnLedRing();
|
||||||
|
break;
|
||||||
|
case NEEDS_CALIBRATION:
|
||||||
|
led_effects::showWaitingLED(color::magenta);
|
||||||
|
break;
|
||||||
|
case PREPARE_CALIBRATION_UNSTABLE:
|
||||||
|
led_effects::showWaitingLED(color::red);
|
||||||
|
break;
|
||||||
|
case PREPARE_CALIBRATION_STABLE:
|
||||||
|
led_effects::showWaitingLED(color::green);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Serial.println(F("Encountered unknown sensor state")); // This should not happen.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Gets fresh data if available, checks calibration status, displays CO2 levels.
|
/** Gets fresh data if available, checks calibration status, displays CO2 levels.
|
||||||
* Returns true if fresh data is available, for further processing (e.g. MQTT, CSV or LoRa)
|
* Returns true if fresh data is available, for further processing (e.g. MQTT, CSV or LoRa)
|
||||||
*/
|
*/
|
||||||
|
@ -157,42 +266,75 @@ namespace sensor {
|
||||||
bool freshData = scd30.dataAvailable();
|
bool freshData = scd30.dataAvailable();
|
||||||
|
|
||||||
if (freshData) {
|
if (freshData) {
|
||||||
// checkTimerDeviation();
|
ntp::getLocalTime(timestamp);
|
||||||
timestamp = ntp::getLocalTime();
|
|
||||||
co2 = scd30.getCO2();
|
co2 = scd30.getCO2();
|
||||||
temperature = scd30.getTemperature();
|
temperature = scd30.getTemperature();
|
||||||
humidity = scd30.getHumidity();
|
humidity = scd30.getHumidity();
|
||||||
}
|
|
||||||
|
|
||||||
//NOTE: Data is available, but it's sometimes erroneous: the sensor outputs zero ppm but non-zero temperature and non-zero humidity.
|
switchStateForCurrentPPM();
|
||||||
if (co2 <= 0) {
|
|
||||||
// No measurement yet. Waiting.
|
|
||||||
led_effects::showWaitingLED(color::blue);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
// Log every time fresh data is available.
|
||||||
* Fresh data. Log it and send it if needed.
|
|
||||||
*/
|
|
||||||
if (freshData) {
|
|
||||||
if (should_calibrate) {
|
|
||||||
if(millis() - time_calaibration_started > 60000)
|
|
||||||
{
|
|
||||||
countStableMeasurements();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
logToSerial();
|
logToSerial();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (should_calibrate) {
|
showState();
|
||||||
if (stable_measurements == 60) {
|
|
||||||
calibrateAndRestart();
|
// Report data for further processing only if the data is reliable
|
||||||
}
|
// (state 'READY') or manual calibration is necessary (state 'NEEDS_CALIBRATION').
|
||||||
led_effects::showWaitingLED(waiting_color);
|
return freshData && (current_state == READY || current_state == NEEDS_CALIBRATION);
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
displayCO2OnLedRing();
|
/*****************************************************************
|
||||||
return freshData;
|
* Callbacks for sensor commands *
|
||||||
|
*****************************************************************/
|
||||||
|
void setCO2forDebugging(int32_t fakeCo2) {
|
||||||
|
Serial.print(F("DEBUG. Setting CO2 to "));
|
||||||
|
co2 = fakeCo2;
|
||||||
|
Serial.println(co2);
|
||||||
|
switchStateForCurrentPPM();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setAutoCalibration(int32_t autoCalibration) {
|
||||||
|
config::auto_calibrate_sensor = autoCalibration;
|
||||||
|
scd30.setAutoSelfCalibration(autoCalibration);
|
||||||
|
Serial.print(F("Setting auto-calibration to : "));
|
||||||
|
Serial.println(autoCalibration ? F("On.") : F("Off."));
|
||||||
|
}
|
||||||
|
|
||||||
|
void setTimer(int32_t timestep) {
|
||||||
|
if (timestep >= 2 && timestep <= 1800) {
|
||||||
|
Serial.print(F("Setting Measurement Interval to : "));
|
||||||
|
Serial.print(timestep);
|
||||||
|
Serial.println(F("s (change will only be applied after next measurement)."));
|
||||||
|
scd30.setMeasurementInterval(timestep);
|
||||||
|
config::measurement_timestep = timestep;
|
||||||
|
led_effects::showKITTWheel(color::green, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void calibrateSensorToSpecificPPM(int32_t calibrationLevel) {
|
||||||
|
if (calibrationLevel >= 400 && calibrationLevel <= 2000) {
|
||||||
|
Serial.print(F("Force calibration, at "));
|
||||||
|
config::co2_calibration_level = calibrationLevel;
|
||||||
|
Serial.print(config::co2_calibration_level);
|
||||||
|
Serial.println(F(" ppm."));
|
||||||
|
startCalibrationProcess();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void calibrateSensorRightNow(int32_t calibrationLevel) {
|
||||||
|
if (calibrationLevel >= 400 && calibrationLevel <= 2000) {
|
||||||
|
Serial.print(F("Force calibration, right now, at "));
|
||||||
|
config::co2_calibration_level = calibrationLevel;
|
||||||
|
Serial.print(config::co2_calibration_level);
|
||||||
|
Serial.println(F(" ppm."));
|
||||||
|
calibrate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void resetSCD() {
|
||||||
|
Serial.print(F("Resetting SCD30..."));
|
||||||
|
scd30.reset();
|
||||||
|
Serial.println(F("done."));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,24 +7,32 @@
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "led_effects.h"
|
#include "led_effects.h"
|
||||||
#include "util.h"
|
#include "util.h"
|
||||||
|
#include "sensor_console.h"
|
||||||
#include <Wire.h>
|
#include <Wire.h>
|
||||||
|
|
||||||
namespace config {
|
namespace config {
|
||||||
extern uint16_t measurement_timestep; // [s] Value between 2 and 1800 (range for SCD30 sensor)
|
extern uint16_t measurement_timestep; // [s] Value between 2 and 1800 (range for SCD30 sensor)
|
||||||
extern const bool auto_calibrate_sensor; // [true / false]
|
extern bool auto_calibrate_sensor; // [true / false]
|
||||||
extern uint16_t co2_calibration_level; // [ppm]
|
extern uint16_t co2_calibration_level; // [ppm]
|
||||||
extern const float temperature_offset; // [K] Sign isn't relevant.
|
extern const float temperature_offset; // [K] Sign isn't relevant.
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace sensor {
|
namespace sensor {
|
||||||
extern SCD30 scd30;
|
extern SCD30 scd30;
|
||||||
extern int16_t co2;
|
extern uint16_t co2;
|
||||||
extern float temperature;
|
extern float temperature;
|
||||||
extern float humidity;
|
extern float humidity;
|
||||||
extern String timestamp;
|
extern char timestamp[];
|
||||||
|
|
||||||
void initialize();
|
void initialize();
|
||||||
bool processData();
|
bool processData();
|
||||||
void startCalibrationProcess();
|
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();
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
#ifndef CONFIG_H_INCLUDED
|
#ifndef CONFIG_H_INCLUDED
|
||||||
# define CONFIG_H_INCLUDED
|
#define CONFIG_H_INCLUDED
|
||||||
|
|
||||||
|
|
||||||
#define buttonPin 0
|
#define buttonPin 0
|
||||||
// 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.
|
||||||
|
@ -14,18 +13,18 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Comment or remove those lines if you want to disable the corresponding 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_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_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_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_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 AMPEL_LORAWAN // Should data be sent over LoRaWAN? (Requires ESP32 + LoRa modem, and "MCCI LoRaWAN LMIC library")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* WIFI
|
* WIFI
|
||||||
*/
|
*/
|
||||||
|
|
||||||
# define WIFI_SSID ""
|
# define WIFI_SSID "MY_SSID"
|
||||||
# define WIFI_PASSWORD ""
|
# define WIFI_PASSWORD "P4SSW0RD"
|
||||||
# define WIFI_TIMEOUT 30 // [s]
|
# define WIFI_TIMEOUT 30 // [s]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -33,8 +32,10 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// How often should measurement be performed, and displayed?
|
// How often should measurement be performed, and displayed?
|
||||||
//NOTE: SCD30 timer does not seem to be very precise. Variations may occur.
|
//WARNING: On some sensors, measurements become very unreliable when timestep is set to 2s.
|
||||||
# define MEASUREMENT_TIMESTEP 2 // [s] Value between 2 and 1800 (range for SCD30 sensor)
|
//NOTE: 10s or longer should be fine in order to get reliable results.
|
||||||
|
//NOTE: SCD30 timer does not seem to be very precise. Time variations may occur.
|
||||||
|
# define MEASUREMENT_TIMESTEP 5 // [s] Value between 2 and 1800 (range for SCD30 sensor)
|
||||||
|
|
||||||
// How often should measurements be appended to CSV ?
|
// How often should measurements be appended to CSV ?
|
||||||
// Probably a good idea to use a multiple of MEASUREMENT_TIMESTEP, so that averages can be calculated
|
// Probably a good idea to use a multiple of MEASUREMENT_TIMESTEP, so that averages can be calculated
|
||||||
|
@ -44,31 +45,34 @@
|
||||||
|
|
||||||
// Residual heat from CO2 sensor seems to be high enough to change the temperature reading. How much should it be 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.
|
// NOTE: Sign isn't relevant. The returned temperature will always be shifted down.
|
||||||
# define TEMPERATURE_OFFSET 0 // [K]
|
# define TEMPERATURE_OFFSET -3 // [K]
|
||||||
|
|
||||||
// Altitude above sea level
|
// Altitude above sea level
|
||||||
// Used for CO2 calibration
|
// Used for CO2 calibration
|
||||||
// here: Stuttgart, Schellingstr. 24. (Source: Google Earth)
|
// here: Stuttgart, Schellingstr. 24. (Source: Google Earth)
|
||||||
# define ALTITUDE_ABOVE_SEA_LEVEL 660 // [m]
|
# define ALTITUDE_ABOVE_SEA_LEVEL 600 // [m]
|
||||||
|
|
||||||
// The reference CO2 concentration has to be within the range 400 ppm ≤ cref(CO2) ≤ 2000 ppm.
|
// The reference CO2 concentration has to be within the range 400 ppm ≤ cref(CO2) ≤ 2000 ppm.
|
||||||
// Used for CO2 calibration
|
// Used for CO2 calibration
|
||||||
// here : measured concentration in Stuttgart
|
// here : measured concentration in Stuttgart
|
||||||
# define ATMOSPHERIC_CO2_CONCENTRATION 430 // [ppm]
|
# define ATMOSPHERIC_CO2_CONCENTRATION 425 // [ppm]
|
||||||
|
|
||||||
// Should the sensor try to calibrate itself?
|
// Should the sensor try to calibrate itself?
|
||||||
// Sensirion recommends 7 days of continuous readings with at least 1 hour a day of 'fresh air' for self-calibration to complete.
|
// Sensirion recommends 7 days of continuous readings with at least 1 hour a day of 'fresh air' for self-calibration to complete.
|
||||||
# define AUTO_CALIBRATE_SENSOR false // [true / false]
|
# define AUTO_CALIBRATE_SENSOR true // [true / false]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* LEDs
|
* LEDs
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// LED brightness, which can vary between min and max brightness ("LED breathing")
|
// LED brightness, which can vary between min and max brightness ("LED breathing")
|
||||||
// max_brightness should be between 0 and 255.
|
// MAX_BRIGHTNESS must be defined, and should be between 0 and 255.
|
||||||
// min_brightness should be between 0 and max_brightness
|
|
||||||
# define MAX_BRIGHTNESS 128
|
# define MAX_BRIGHTNESS 128
|
||||||
|
// 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
|
# define MIN_BRIGHTNESS 60
|
||||||
|
// How many LEDs in the ring? 12 and 16 are currently supported. If undefined, 12 is used as default.
|
||||||
|
# define LED_COUNT 12
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* WEB SERVER
|
* WEB SERVER
|
||||||
|
@ -76,8 +80,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Define empty strings in order to disable authentication, or remove the constants altogether.
|
// Define empty strings in order to disable authentication, or remove the constants altogether.
|
||||||
# define HTTP_USER ""
|
# define HTTP_USER "co2ampel"
|
||||||
# define HTTP_PASSWORD ""
|
# define HTTP_PASSWORD "my_password"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MQTT
|
* MQTT
|
||||||
|
@ -106,11 +110,11 @@
|
||||||
*/
|
*/
|
||||||
# define ALLOW_MQTT_COMMANDS false
|
# define ALLOW_MQTT_COMMANDS false
|
||||||
|
|
||||||
// How often measurements should be sent to MQTT server?
|
// How often should measurements be sent to MQTT server?
|
||||||
// Probably a good idea to use a multiple of MEASUREMENT_TIMESTEP, so that averages can be calculated
|
// Probably a good idea to use a multiple of MEASUREMENT_TIMESTEP, so that averages can be calculated
|
||||||
// Set to 0 if you want to send values after each measurement
|
// Set to 0 if you want to send values after each measurement
|
||||||
// # define MQTT_SENDING_INTERVAL MEASUREMENT_TIMESTEP * 5 // [s]
|
# define MQTT_SENDING_INTERVAL MEASUREMENT_TIMESTEP * 5 // [s]
|
||||||
# define MQTT_SENDING_INTERVAL 60 // [s]
|
//# define MQTT_SENDING_INTERVAL 60 // [s]
|
||||||
# define MQTT_SERVER "test.mosquitto.org" // MQTT server URL or IP address
|
# define MQTT_SERVER "test.mosquitto.org" // MQTT server URL or IP address
|
||||||
# define MQTT_PORT 8883
|
# define MQTT_PORT 8883
|
||||||
# define MQTT_USER ""
|
# define MQTT_USER ""
|
||||||
|
@ -149,7 +153,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
# define NTP_SERVER "pool.ntp.org"
|
# define NTP_SERVER "pool.ntp.org"
|
||||||
# define UTC_OFFSET_IN_SECONDS 3600 // [s] 3600 for UTC+1
|
# define UTC_OFFSET_IN_SECONDS 7200 // [s] 3600 for UTC+1, 7200 for UTC+1 and daylight saving time
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Others
|
* Others
|
|
@ -1,14 +1,12 @@
|
||||||
#include "csv_writer.h"
|
#include "csv_writer.h"
|
||||||
|
|
||||||
//TODO: Allow CSV download via USB Serial, when requested (e.g. via a Python script)
|
|
||||||
|
|
||||||
namespace config {
|
namespace config {
|
||||||
// Values should be defined in config.h
|
// Values should be defined in config.h
|
||||||
uint16_t csv_interval = CSV_INTERVAL; // [s]
|
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;
|
||||||
String last_successful_write = "";
|
char last_successful_write[23];
|
||||||
|
|
||||||
#if defined(ESP8266)
|
#if defined(ESP8266)
|
||||||
/**
|
/**
|
||||||
|
@ -80,13 +78,16 @@ namespace csv_writer {
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
const String filename = "/" + SENSOR_ID + ".csv";
|
char filename[15]; // "/ESPxxxxxx.csv\0"
|
||||||
|
|
||||||
int getAvailableSpace() {
|
int getAvailableSpace() {
|
||||||
return getTotalSpace() - getUsedSpace();
|
return getTotalSpace() - getUsedSpace();
|
||||||
}
|
}
|
||||||
|
|
||||||
void initialize() {
|
void initialize(const char *sensorId) {
|
||||||
|
snprintf(filename, sizeof(filename), "/%s.csv", sensorId);
|
||||||
|
|
||||||
|
Serial.println();
|
||||||
Serial.print(F("Initializing FS..."));
|
Serial.print(F("Initializing FS..."));
|
||||||
if (mountFS()) {
|
if (mountFS()) {
|
||||||
Serial.println(F("done."));
|
Serial.println(F("done."));
|
||||||
|
@ -113,9 +114,13 @@ namespace csv_writer {
|
||||||
Serial.println();
|
Serial.println();
|
||||||
|
|
||||||
// Open dir folder
|
// Open dir folder
|
||||||
Serial.println("Filesystem content:");
|
Serial.println(F("Filesystem content:"));
|
||||||
showFilesystemContent();
|
showFilesystemContent();
|
||||||
Serial.println();
|
Serial.println();
|
||||||
|
|
||||||
|
sensor_console::defineIntCommand("csv", setCSVinterval, F("60 (Sets CSV writing interval, in s)"));
|
||||||
|
sensor_console::defineCommand("format_filesystem", formatFilesystem, F("(Deletes the whole filesystem)"));
|
||||||
|
sensor_console::defineCommand("show_csv", showCSVContent, F("(Displays the complete CSV file on Serial)"));
|
||||||
}
|
}
|
||||||
|
|
||||||
File openOrCreate() {
|
File openOrCreate() {
|
||||||
|
@ -130,11 +135,11 @@ namespace csv_writer {
|
||||||
return csv_file;
|
return csv_file;
|
||||||
}
|
}
|
||||||
|
|
||||||
void log(const String &timeStamp, const int16_t &co2, const float &temperature, const float &humidity) {
|
void log(const char *timestamp, const int16_t &co2, const float &temperature, const float &humidity) {
|
||||||
led_effects::onBoardLEDOn();
|
led_effects::onBoardLEDOn();
|
||||||
File csv_file = openOrCreate();
|
File csv_file = openOrCreate();
|
||||||
char csv_line[42];
|
char csv_line[42];
|
||||||
snprintf(csv_line, sizeof(csv_line), "%s;%d;%.1f;%.1f\r\n", timeStamp.c_str(), co2, temperature, humidity);
|
snprintf(csv_line, sizeof(csv_line), "%s;%d;%.1f;%.1f\r\n", timestamp, co2, temperature, humidity);
|
||||||
if (csv_file) {
|
if (csv_file) {
|
||||||
size_t written_bytes = csv_file.print(csv_line);
|
size_t written_bytes = csv_file.print(csv_line);
|
||||||
csv_file.close();
|
csv_file.close();
|
||||||
|
@ -143,7 +148,7 @@ namespace csv_writer {
|
||||||
} else {
|
} else {
|
||||||
Serial.print(F("CSV - Wrote : "));
|
Serial.print(F("CSV - Wrote : "));
|
||||||
Serial.print(csv_line);
|
Serial.print(csv_line);
|
||||||
last_successful_write = ntp::getLocalTime();
|
ntp::getLocalTime(last_successful_write);
|
||||||
}
|
}
|
||||||
updateFsInfo();
|
updateFsInfo();
|
||||||
delay(50);
|
delay(50);
|
||||||
|
@ -154,11 +159,42 @@ namespace csv_writer {
|
||||||
led_effects::onBoardLEDOff();
|
led_effects::onBoardLEDOff();
|
||||||
}
|
}
|
||||||
|
|
||||||
void logIfTimeHasCome(const String &timeStamp, const int16_t &co2, const float &temperature, const float &humidity) {
|
void logIfTimeHasCome(const char *timeStamp, const int16_t &co2, const float &temperature, const float &humidity) {
|
||||||
unsigned long now = seconds();
|
unsigned long now = seconds();
|
||||||
if (now - last_written_at > config::csv_interval) {
|
if (now - last_written_at > config::csv_interval) {
|
||||||
last_written_at = now;
|
last_written_at = now;
|
||||||
log(timeStamp, co2, temperature, humidity);
|
log(timeStamp, co2, temperature, humidity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*****************************************************************
|
||||||
|
* Callbacks for sensor commands *
|
||||||
|
*****************************************************************/
|
||||||
|
void setCSVinterval(int32_t csv_interval) {
|
||||||
|
config::csv_interval = csv_interval;
|
||||||
|
Serial.print(F("Setting CSV Interval to : "));
|
||||||
|
Serial.print(config::csv_interval);
|
||||||
|
Serial.println("s.");
|
||||||
|
led_effects::showKITTWheel(color::green, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void showCSVContent() {
|
||||||
|
Serial.print(F("### "));
|
||||||
|
Serial.print(filename);
|
||||||
|
Serial.println(F(" ###"));
|
||||||
|
File csv_file;
|
||||||
|
if (FS_LIB.exists(filename)) {
|
||||||
|
csv_file = FS_LIB.open(filename, "r");
|
||||||
|
while (csv_file.available()) {
|
||||||
|
Serial.write(csv_file.read());
|
||||||
|
}
|
||||||
|
csv_file.close();
|
||||||
|
}
|
||||||
|
Serial.println(F("######################"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void formatFilesystem() {
|
||||||
|
FS_LIB.format();
|
||||||
|
led_effects::showKITTWheel(color::blue, 2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,16 +14,21 @@
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "util.h"
|
#include "util.h"
|
||||||
#include "led_effects.h"
|
#include "led_effects.h"
|
||||||
|
#include "sensor_console.h"
|
||||||
|
|
||||||
namespace config {
|
namespace config {
|
||||||
extern uint16_t csv_interval; // [s]
|
extern uint16_t csv_interval; // [s]
|
||||||
}
|
}
|
||||||
namespace csv_writer {
|
namespace csv_writer {
|
||||||
extern String last_successful_write;
|
extern char last_successful_write[];
|
||||||
void initialize();
|
void initialize(const char *sensorId);
|
||||||
void logIfTimeHasCome(const String &timeStamp, const int16_t &co2, const float &temperature, const float &humidity);
|
void logIfTimeHasCome(const char *timestamp, const int16_t &co2, const float &temperature, const float &humidity);
|
||||||
int getAvailableSpace();
|
int getAvailableSpace();
|
||||||
extern const String filename;
|
extern char filename[];
|
||||||
|
|
||||||
|
void setCSVinterval(int32_t csv_interval);
|
||||||
|
void showCSVContent();
|
||||||
|
void formatFilesystem();
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -4,17 +4,36 @@
|
||||||
*****************************************************************/
|
*****************************************************************/
|
||||||
namespace config {
|
namespace config {
|
||||||
const uint8_t max_brightness = MAX_BRIGHTNESS;
|
const uint8_t max_brightness = MAX_BRIGHTNESS;
|
||||||
|
#if defined(MIN_BRIGHTNESS)
|
||||||
const uint8_t min_brightness = MIN_BRIGHTNESS;
|
const uint8_t min_brightness = MIN_BRIGHTNESS;
|
||||||
const int kitt_tail = 3; // How many dimmer LEDs follow in K.I.T.T. wheel
|
#else
|
||||||
}
|
const uint8_t min_brightness = MAX_BRIGHTNESS;
|
||||||
|
#endif
|
||||||
/*****************************************************************
|
|
||||||
* Configuration (calculated from above values) *
|
|
||||||
*****************************************************************/
|
|
||||||
namespace config //NOTE: Use a class instead? NightMode could then be another state.
|
|
||||||
{
|
|
||||||
const uint8_t brightness_amplitude = config::max_brightness - config::min_brightness;
|
const uint8_t brightness_amplitude = config::max_brightness - config::min_brightness;
|
||||||
bool night_mode = false;
|
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 night_mode = false; //NOTE: Use a class instead? NightMode could then be another state.
|
||||||
|
|
||||||
|
#if !defined(LED_COUNT)
|
||||||
|
# define LED_COUNT 12
|
||||||
|
#endif
|
||||||
|
|
||||||
|
const uint16_t led_count = LED_COUNT;
|
||||||
|
|
||||||
|
#if LED_COUNT == 12
|
||||||
|
//NOTE: One value has been prepended, to make calculations easier and avoid out of bounds index.
|
||||||
|
const uint16_t co2_ticks[led_count + 1] = { 0, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000, 2200 }; // [ppm]
|
||||||
|
// For a given LED, which color should be displayed? First LED will be pure green (hue angle 120°),
|
||||||
|
// 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
|
||||||
}
|
}
|
||||||
|
|
||||||
#if defined(ESP8266)
|
#if defined(ESP8266)
|
||||||
|
@ -25,20 +44,7 @@ const int NEOPIXELS_PIN = 5;
|
||||||
const int NEOPIXELS_PIN = 23;
|
const int NEOPIXELS_PIN = 23;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
const int NUMPIXELS = 12;
|
Adafruit_NeoPixel pixels(config::led_count, NEOPIXELS_PIN, NEO_GRB + NEO_KHZ800);
|
||||||
//NOTE: One value has been prepended, to make calculations easier and avoid out of bounds index.
|
|
||||||
const uint16_t CO2_TICKS[NUMPIXELS + 1] = { 0, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000, 2200 }; // [ppm]
|
|
||||||
// const uint16_t CO2_TICKS[NUMPIXELS + 1] = { 0, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400, 1500, 1600, 1800, 2000, 2200 }; // [ppm]
|
|
||||||
// For a given LED, which color should be displayed? First LED will be pure green (hue angle 120°),
|
|
||||||
// last 4 LEDs will be pure red (hue angle 0°), LEDs in-between will be yellowish.
|
|
||||||
// For reference, this python code can be used to generate the array
|
|
||||||
// NUMPIXELS = 12
|
|
||||||
// RED_LEDS = 4
|
|
||||||
// hues = [ (2**16-1) // 3 * max(NUMPIXELS - RED_LEDS - i, 0) // (NUMPIXELS - RED_LEDS) for i in range(NUMPIXELS) ]
|
|
||||||
// '{' + ', '.join([str(hue) + ('U' if hue else '') for hue in hues]) + '}; // [hue angle]'
|
|
||||||
const uint16_t LED_HUES[NUMPIXELS] = { 21845U, 19114U, 16383U, 13653U, 10922U, 8191U, 5461U, 2730U, 0, 0, 0, 0 }; // [hue angle]
|
|
||||||
// const uint16_t LED_HUES[NUMPIXELS] = { 21845U, 20024U, 18204U, 16383U, 14563U, 12742U, 10922U, 9102U, 7281U, 5461U, 3640U, 1820U, 0, 0, 0, 0 }; // [hue angle]
|
|
||||||
Adafruit_NeoPixel pixels(NUMPIXELS, NEOPIXELS_PIN, NEO_GRB + NEO_KHZ800);
|
|
||||||
|
|
||||||
namespace led_effects {
|
namespace led_effects {
|
||||||
//On-board LED on D4, aka GPIO02
|
//On-board LED on D4, aka GPIO02
|
||||||
|
@ -71,10 +77,19 @@ namespace led_effects {
|
||||||
onBoardLEDOff();
|
onBoardLEDOff();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void showColor(int32_t color) {
|
||||||
|
config::night_mode = true; // In order to avoid overwriting the desired color next time CO2 is displayed
|
||||||
|
pixels.setBrightness(255);
|
||||||
|
pixels.fill(color);
|
||||||
|
pixels.show();
|
||||||
|
}
|
||||||
|
|
||||||
void setupRing() {
|
void setupRing() {
|
||||||
pixels.begin();
|
pixels.begin();
|
||||||
pixels.setBrightness(config::max_brightness);
|
pixels.setBrightness(config::max_brightness);
|
||||||
LEDsOff();
|
LEDsOff();
|
||||||
|
sensor_console::defineCommand("night_mode", toggleNightMode, F("(Toggles night mode on/off)"));
|
||||||
|
sensor_console::defineIntCommand("color", showColor, F("0xFF0015 (Shows color, specified as RGB, for debugging)"));
|
||||||
}
|
}
|
||||||
|
|
||||||
void toggleNightMode() {
|
void toggleNightMode() {
|
||||||
|
@ -89,14 +104,15 @@ namespace led_effects {
|
||||||
|
|
||||||
//NOTE: basically one iteration of KITT wheel
|
//NOTE: basically one iteration of KITT wheel
|
||||||
void showWaitingLED(uint32_t color) {
|
void showWaitingLED(uint32_t color) {
|
||||||
|
using namespace config;
|
||||||
delay(80);
|
delay(80);
|
||||||
if (config::night_mode) {
|
if (night_mode) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
static uint16_t kitt_offset = 0;
|
static uint16_t kitt_offset = 0;
|
||||||
pixels.clear();
|
pixels.clear();
|
||||||
for (int j = config::kitt_tail; j >= 0; j--) {
|
for (int j = kitt_tail; j >= 0; j--) {
|
||||||
int ledNumber = abs((kitt_offset - j + NUMPIXELS) % (2 * NUMPIXELS) - NUMPIXELS) % NUMPIXELS; // Triangular function
|
int ledNumber = abs((kitt_offset - j + led_count) % (2 * led_count) - led_count) % led_count; // Triangular function
|
||||||
pixels.setPixelColor(ledNumber, color * pixels.gamma8(255 - j * 76) / 255);
|
pixels.setPixelColor(ledNumber, color * pixels.gamma8(255 - j * 76) / 255);
|
||||||
}
|
}
|
||||||
pixels.show();
|
pixels.show();
|
||||||
|
@ -108,7 +124,7 @@ namespace led_effects {
|
||||||
// Takes approximately 1s for each direction.
|
// Takes approximately 1s for each direction.
|
||||||
void showKITTWheel(uint32_t color, uint16_t duration_s) {
|
void showKITTWheel(uint32_t color, uint16_t duration_s) {
|
||||||
pixels.setBrightness(config::max_brightness);
|
pixels.setBrightness(config::max_brightness);
|
||||||
for (int i = 0; i < duration_s * NUMPIXELS; ++i) {
|
for (int i = 0; i < duration_s * config::led_count; ++i) {
|
||||||
showWaitingLED(color);
|
showWaitingLED(color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -118,10 +134,10 @@ namespace led_effects {
|
||||||
* For example, for 1500ppm, every LED between 0 and 7 (500 -> 1400ppm) should be on, LED at 8 (1600ppm) should be half-on.
|
* For example, for 1500ppm, every LED between 0 and 7 (500 -> 1400ppm) should be on, LED at 8 (1600ppm) should be half-on.
|
||||||
*/
|
*/
|
||||||
uint8_t getLedBrightness(uint16_t co2, int ledId) {
|
uint8_t getLedBrightness(uint16_t co2, int ledId) {
|
||||||
if (co2 >= CO2_TICKS[ledId + 1]) {
|
if (co2 >= config::co2_ticks[ledId + 1]) {
|
||||||
return 255;
|
return 255;
|
||||||
} else {
|
} else {
|
||||||
if (2 * co2 >= CO2_TICKS[ledId] + CO2_TICKS[ledId + 1]) {
|
if (2 * co2 >= config::co2_ticks[ledId] + config::co2_ticks[ledId + 1]) {
|
||||||
// Show partial LED if co2 more than halfway between ticks.
|
// Show partial LED if co2 more than halfway between ticks.
|
||||||
return 27; // Brightness isn't linear, so 27 / 255 looks much brighter than 10%
|
return 27; // Brightness isn't linear, so 27 / 255 looks much brighter than 10%
|
||||||
} else {
|
} else {
|
||||||
|
@ -131,6 +147,17 @@ namespace led_effects {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If enabled, slowly varies the brightness between MAX_BRIGHTNESS & MIN_BRIGHTNESS.
|
||||||
|
*/
|
||||||
|
void breathe(int16_t co2) {
|
||||||
|
static uint8_t breathing_offset = 0;
|
||||||
|
uint16_t brightness = config::min_brightness + pixels.sine8(breathing_offset) * config::brightness_amplitude / 255;
|
||||||
|
pixels.setBrightness(brightness);
|
||||||
|
pixels.show();
|
||||||
|
breathing_offset += co2 > config::poor_air_quality_ppm ? 6 : 3; // breathing speed. +3 looks like slow human breathing.
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fills the whole ring with green, yellow, orange or black, depending on co2 input and CO2_TICKS.
|
* Fills the whole ring with green, yellow, orange or black, depending on co2 input and CO2_TICKS.
|
||||||
*/
|
*/
|
||||||
|
@ -139,24 +166,28 @@ namespace led_effects {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
pixels.setBrightness(config::max_brightness);
|
pixels.setBrightness(config::max_brightness);
|
||||||
for (int ledId = 0; ledId < NUMPIXELS; ++ledId) {
|
for (int ledId = 0; ledId < config::led_count; ++ledId) {
|
||||||
uint8_t brightness = getLedBrightness(co2, ledId);
|
uint8_t brightness = getLedBrightness(co2, ledId);
|
||||||
pixels.setPixelColor(ledId, pixels.ColorHSV(LED_HUES[ledId], 255, brightness));
|
pixels.setPixelColor(ledId, pixels.ColorHSV(config::led_hues[ledId], 255, brightness));
|
||||||
}
|
}
|
||||||
pixels.show();
|
pixels.show();
|
||||||
|
if (config::brightness_amplitude > 0) {
|
||||||
|
breathe(co2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void showRainbowWheel(uint16_t duration_ms, uint16_t hue_increment) {
|
void showRainbowWheel(uint16_t duration_ms) {
|
||||||
if (config::night_mode) {
|
if (config::night_mode) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
static uint16_t wheel_offset = 0;
|
static uint16_t wheel_offset = 0;
|
||||||
|
static uint16_t sine_offset = 0;
|
||||||
unsigned long t0 = millis();
|
unsigned long t0 = millis();
|
||||||
pixels.setBrightness(config::max_brightness);
|
pixels.setBrightness(config::max_brightness);
|
||||||
while (millis() - t0 < duration_ms) {
|
while (millis() - t0 < duration_ms) {
|
||||||
for (int i = 0; i < NUMPIXELS; i++) {
|
for (int i = 0; i < config::led_count; i++) {
|
||||||
pixels.setPixelColor(i, pixels.ColorHSV(i * 65535 / NUMPIXELS + wheel_offset));
|
pixels.setPixelColor(i, pixels.ColorHSV(i * 65535 / config::led_count + wheel_offset));
|
||||||
wheel_offset += hue_increment;
|
wheel_offset += (pixels.sine8(sine_offset++ / 50) - 127) / 2;
|
||||||
}
|
}
|
||||||
pixels.show();
|
pixels.show();
|
||||||
delay(10);
|
delay(10);
|
||||||
|
@ -179,37 +210,28 @@ namespace led_effects {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void breathe(int16_t co2) {
|
|
||||||
if (!config::night_mode) {
|
|
||||||
static uint16_t breathing_offset = 0;
|
|
||||||
uint16_t brightness = config::min_brightness
|
|
||||||
+ pixels.sine8(breathing_offset) * config::brightness_amplitude / 255;
|
|
||||||
pixels.setBrightness(brightness);
|
|
||||||
pixels.show();
|
|
||||||
breathing_offset += 3; // breathing speed. +3 looks like slow human breathing.
|
|
||||||
}
|
|
||||||
delay(co2 > 1600 ? 50 : 100); // faster breathing for higher CO2 values
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays a complete blue circle, and starts removing LEDs one by one. Returns the number of remaining LEDs.
|
* Displays a complete blue circle, and starts removing LEDs one by one.
|
||||||
* Can be used for calibration, e.g. when countdown is 0. Does not work in night mode.
|
* Does nothing in night mode and returns false then. Returns true if
|
||||||
|
* the countdown has finished. Can be used for calibration, e.g. when countdown is 0.
|
||||||
|
* NOTE: This function is blocking and returns only after the button has
|
||||||
|
* been released or after every LED has been turned off.
|
||||||
*/
|
*/
|
||||||
int countdownToZero() {
|
bool countdownToZero() {
|
||||||
if (config::night_mode) {
|
if (config::night_mode) {
|
||||||
Serial.println(F("Night mode. Not doing anything."));
|
Serial.println(F("Night mode. Not doing anything."));
|
||||||
delay(1000); // Wait for a while, to avoid coming back to this function too many times when button is pressed.
|
delay(1000); // Wait for a while, to avoid coming back to this function too many times when button is pressed.
|
||||||
return 1;
|
return false;
|
||||||
}
|
}
|
||||||
pixels.fill(color::blue);
|
pixels.fill(color::blue);
|
||||||
pixels.show();
|
pixels.show();
|
||||||
int countdown;
|
int countdown;
|
||||||
for (countdown = NUMPIXELS; countdown >= 0 && !digitalRead(0); countdown--) {
|
for (countdown = config::led_count; countdown >= 0 && !digitalRead(0); countdown--) {
|
||||||
pixels.setPixelColor(countdown, color::black);
|
pixels.setPixelColor(countdown, color::black);
|
||||||
pixels.show();
|
pixels.show();
|
||||||
Serial.println(countdown);
|
Serial.println(countdown);
|
||||||
delay(500);
|
delay(500);
|
||||||
}
|
}
|
||||||
return countdown;
|
return countdown < 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
#define LED_EFFECTS_H_INCLUDED
|
#define LED_EFFECTS_H_INCLUDED
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include "config.h"
|
#include "config.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)
|
||||||
// https://github.com/adafruit/Adafruit_NeoPixel
|
// https://github.com/adafruit/Adafruit_NeoPixel
|
||||||
|
@ -25,11 +26,10 @@ namespace led_effects {
|
||||||
|
|
||||||
void setupRing();
|
void setupRing();
|
||||||
void redAlert();
|
void redAlert();
|
||||||
void breathe(int16_t co2);
|
bool countdownToZero();
|
||||||
int 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);
|
||||||
void showRainbowWheel(uint16_t duration_ms = 1000, uint16_t hue_increment = 50);
|
void showRainbowWheel(uint16_t duration_ms = 1000);
|
||||||
void displayCO2color(uint16_t co2);
|
void displayCO2color(uint16_t co2);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -33,7 +33,7 @@ void os_getDevKey(u1_t *buf) {
|
||||||
namespace lorawan {
|
namespace lorawan {
|
||||||
bool waiting_for_confirmation = false;
|
bool waiting_for_confirmation = false;
|
||||||
bool connected = false;
|
bool connected = false;
|
||||||
String last_transmission = "";
|
char last_transmission[23] = "";
|
||||||
|
|
||||||
void initialize() {
|
void initialize() {
|
||||||
Serial.println(F("Starting LoRaWAN. Frequency plan : " LMIC_FREQUENCY_PLAN " MHz."));
|
Serial.println(F("Starting LoRaWAN. Frequency plan : " LMIC_FREQUENCY_PLAN " MHz."));
|
||||||
|
@ -47,11 +47,12 @@ namespace lorawan {
|
||||||
LMIC_reset();
|
LMIC_reset();
|
||||||
// Join, but don't send anything yet.
|
// Join, but don't send anything yet.
|
||||||
LMIC_startJoining();
|
LMIC_startJoining();
|
||||||
|
sensor_console::defineIntCommand("lora", setLoRaInterval, F("300 (Sets LoRaWAN sending interval, in s)"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checks if OTAA is connected, or if payload should be sent.
|
// Checks if OTAA is connected, or if payload should be sent.
|
||||||
// NOTE: while a transaction is in process (i.e. until the TXcomplete event has been received, no blocking code (e.g. delay loops etc.) are allowed, otherwise the LMIC/OS code might miss the event.
|
// NOTE: while a transaction is in process (i.e. until the TXcomplete event has been received, no blocking code (e.g. delay loops etc.) are allowed, otherwise the LMIC/OS code might miss the event.
|
||||||
// If this rule is not followed, a typical symptom is that the first send is ok and all following ones end with the TX not complete failure.
|
// If this rule is not followed, a typical symptom is that the first send is ok and all following ones end with the 'TX not complete' failure.
|
||||||
void process() {
|
void process() {
|
||||||
os_runloop_once();
|
os_runloop_once();
|
||||||
}
|
}
|
||||||
|
@ -64,8 +65,10 @@ namespace lorawan {
|
||||||
}
|
}
|
||||||
|
|
||||||
void onEvent(ev_t ev) {
|
void onEvent(ev_t ev) {
|
||||||
|
char current_time[23];
|
||||||
|
ntp::getLocalTime(current_time);
|
||||||
Serial.print("LoRa - ");
|
Serial.print("LoRa - ");
|
||||||
Serial.print(ntp::getLocalTime());
|
Serial.print(current_time);
|
||||||
Serial.print(" - ");
|
Serial.print(" - ");
|
||||||
switch (ev) {
|
switch (ev) {
|
||||||
case EV_JOINING:
|
case EV_JOINING:
|
||||||
|
@ -93,7 +96,7 @@ namespace lorawan {
|
||||||
printHex2(artKey[i]);
|
printHex2(artKey[i]);
|
||||||
}
|
}
|
||||||
Serial.println();
|
Serial.println();
|
||||||
Serial.print(" NwkSKey: ");
|
Serial.print(F(" NwkSKey: "));
|
||||||
for (size_t i = 0; i < sizeof(nwkKey); ++i) {
|
for (size_t i = 0; i < sizeof(nwkKey); ++i) {
|
||||||
if (i != 0)
|
if (i != 0)
|
||||||
Serial.print("-");
|
Serial.print("-");
|
||||||
|
@ -112,7 +115,7 @@ namespace lorawan {
|
||||||
Serial.println(F("EV_REJOIN_FAILED"));
|
Serial.println(F("EV_REJOIN_FAILED"));
|
||||||
break;
|
break;
|
||||||
case EV_TXCOMPLETE:
|
case EV_TXCOMPLETE:
|
||||||
last_transmission = ntp::getLocalTime();
|
ntp::getLocalTime(last_transmission);
|
||||||
Serial.println(F("EV_TXCOMPLETE"));
|
Serial.println(F("EV_TXCOMPLETE"));
|
||||||
break;
|
break;
|
||||||
case EV_TXSTART:
|
case EV_TXSTART:
|
||||||
|
@ -189,6 +192,17 @@ namespace lorawan {
|
||||||
preparePayload(co2, temperature, humidity);
|
preparePayload(co2, temperature, humidity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*****************************************************************
|
||||||
|
* Callbacks for sensor commands *
|
||||||
|
*****************************************************************/
|
||||||
|
void setLoRaInterval(int32_t sending_interval) {
|
||||||
|
config::lorawan_sending_interval = sending_interval;
|
||||||
|
Serial.print(F("Setting LoRa sending interval to : "));
|
||||||
|
Serial.print(config::lorawan_sending_interval);
|
||||||
|
Serial.println("s.");
|
||||||
|
led_effects::showKITTWheel(color::green, 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void onEvent(ev_t ev) {
|
void onEvent(ev_t ev) {
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
#include <SPI.h>
|
#include <SPI.h>
|
||||||
|
|
||||||
#include "led_effects.h"
|
#include "led_effects.h"
|
||||||
|
#include "sensor_console.h"
|
||||||
#include "util.h"
|
#include "util.h"
|
||||||
|
|
||||||
namespace config {
|
namespace config {
|
||||||
|
@ -39,10 +39,12 @@ namespace config {
|
||||||
namespace lorawan {
|
namespace lorawan {
|
||||||
extern bool waiting_for_confirmation;
|
extern bool waiting_for_confirmation;
|
||||||
extern bool connected;
|
extern bool connected;
|
||||||
extern String last_transmission;
|
extern char last_transmission[];
|
||||||
void initialize();
|
void initialize();
|
||||||
void process();
|
void process();
|
||||||
void preparePayloadIfTimeHasCome(const int16_t &co2, const float &temp, const float &hum);
|
void preparePayloadIfTimeHasCome(const int16_t &co2, const float &temp, const float &hum);
|
||||||
|
|
||||||
|
void setLoRaInterval(int32_t sending_interval);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
namespace config {
|
namespace config {
|
||||||
// Values should be defined in config.h
|
// Values should be defined in config.h
|
||||||
uint16_t sending_interval = MQTT_SENDING_INTERVAL; // [s]
|
uint16_t mqtt_sending_interval = MQTT_SENDING_INTERVAL; // [s]
|
||||||
//INFO: Listen to every CO2 sensor which is connected to the server:
|
//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 char *mqtt_server = MQTT_SERVER;
|
||||||
|
@ -23,31 +23,34 @@ namespace mqtt {
|
||||||
unsigned long last_failed_at = 0;
|
unsigned long last_failed_at = 0;
|
||||||
bool connected = false;
|
bool connected = false;
|
||||||
|
|
||||||
String publish_topic;
|
char publish_topic[21]; // e.g. "CO2sensors/ESPxxxxxx\0"
|
||||||
const char *json_sensor_format;
|
const char *json_sensor_format;
|
||||||
String last_successful_publish = "";
|
char last_successful_publish[23] = "";
|
||||||
|
|
||||||
void initialize(String &topic) {
|
void initialize(const char *sensorId) {
|
||||||
json_sensor_format = PSTR("{\"time\":\"%s\", \"co2\":%d, \"temp\":%.1f, \"rh\":%.1f}");
|
json_sensor_format = PSTR("{\"time\":\"%s\", \"co2\":%d, \"temp\":%.1f, \"rh\":%.1f}");
|
||||||
publish_topic = topic;
|
snprintf(publish_topic, sizeof(publish_topic), "CO2sensors/%s", sensorId);
|
||||||
#if defined(ESP8266)
|
// The sensor doesn't check the fingerprint of the MQTT broker, because otherwise this fingerprint should be updated
|
||||||
espClient.setInsecure(); // Sorry, we don't want to flash the sensors every 3 months.
|
// on the sensor every 3 months. The connection can still be encrypted, though:
|
||||||
#endif
|
espClient.setInsecure(); // If not available for ESP32, please update Arduino IDE / PlatformIO
|
||||||
// mqttClient.setSocketTimeout(config::mqtt_timeout); //NOTE: somehow doesn't seem to have any effect on connect()
|
|
||||||
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::defineCommand("send_local_ip", sendInfoAboutLocalNetwork,
|
||||||
|
F("(Sends local IP and SSID via MQTT. Can be useful to find sensor)"));
|
||||||
}
|
}
|
||||||
|
|
||||||
void publish(const String ×tamp, 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.status() == WL_CONNECTED && mqttClient.connected()) {
|
||||||
led_effects::onBoardLEDOn();
|
led_effects::onBoardLEDOn();
|
||||||
Serial.print(F("MQTT - Publishing message ... "));
|
Serial.print(F("MQTT - Publishing message ... "));
|
||||||
|
|
||||||
char payload[75]; // Should be enough for json...
|
char payload[75]; // Should be enough for json...
|
||||||
snprintf(payload, sizeof(payload), json_sensor_format, timestamp.c_str(), co2, temperature, humidity);
|
snprintf(payload, sizeof(payload), json_sensor_format, timestamp, co2, temperature, humidity);
|
||||||
// Topic is the same as clientID. e.g. 'CO2sensors/ESP3d03da'
|
// Topic is the same as clientID. e.g. 'CO2sensors/ESP3d03da'
|
||||||
if (mqttClient.publish(publish_topic.c_str(), payload)) {
|
if (mqttClient.publish(publish_topic, payload)) {
|
||||||
Serial.println(F("OK"));
|
Serial.println(F("OK"));
|
||||||
last_successful_publish = ntp::getLocalTime();
|
ntp::getLocalTime(last_successful_publish);
|
||||||
} else {
|
} else {
|
||||||
Serial.println(F("Failed."));
|
Serial.println(F("Failed."));
|
||||||
}
|
}
|
||||||
|
@ -55,69 +58,6 @@ namespace mqtt {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void setTimer(String messageString) {
|
|
||||||
messageString.replace("timer ", "");
|
|
||||||
int timestep = messageString.toInt();
|
|
||||||
if (timestep >= 2 && timestep <= 1800) {
|
|
||||||
Serial.print(F("Setting Measurement Interval to : "));
|
|
||||||
Serial.print(timestep);
|
|
||||||
Serial.println("s.");
|
|
||||||
sensor::scd30.setMeasurementInterval(messageString.toInt());
|
|
||||||
config::measurement_timestep = messageString.toInt();
|
|
||||||
led_effects::showKITTWheel(color::green, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void setMQTTinterval(String messageString) {
|
|
||||||
messageString.replace("mqtt ", "");
|
|
||||||
config::sending_interval = messageString.toInt();
|
|
||||||
Serial.print(F("Setting Sending Interval to : "));
|
|
||||||
Serial.print(config::sending_interval);
|
|
||||||
Serial.println("s.");
|
|
||||||
led_effects::showKITTWheel(color::green, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef AMPEL_CSV
|
|
||||||
void setCSVinterval(String messageString) {
|
|
||||||
messageString.replace("csv ", "");
|
|
||||||
config::csv_interval = messageString.toInt();
|
|
||||||
Serial.print(F("Setting CSV Interval to : "));
|
|
||||||
Serial.print(config::csv_interval);
|
|
||||||
Serial.println("s.");
|
|
||||||
led_effects::showKITTWheel(color::green, 1);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
void calibrateSensorToSpecificPPM(String messageString) {
|
|
||||||
messageString.replace("calibrate ", "");
|
|
||||||
long int calibrationLevel = messageString.toInt();
|
|
||||||
if (calibrationLevel >= 400 && calibrationLevel <= 2000) {
|
|
||||||
Serial.print(F("Force calibration, at "));
|
|
||||||
config::co2_calibration_level = messageString.toInt();
|
|
||||||
Serial.print(config::co2_calibration_level);
|
|
||||||
Serial.println(" ppm.");
|
|
||||||
sensor::startCalibrationProcess();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void setCO2forDebugging(String messageString) {
|
|
||||||
Serial.print(F("DEBUG. Setting CO2 to "));
|
|
||||||
messageString.replace("co2 ", "");
|
|
||||||
sensor::co2 = messageString.toInt();
|
|
||||||
Serial.println(sensor::co2);
|
|
||||||
}
|
|
||||||
|
|
||||||
void sendInfoAboutLocalNetwork() {
|
|
||||||
char info_topic[60]; // Should be enough for "CO2sensors/ESPd03cc5/info"
|
|
||||||
snprintf(info_topic, sizeof(info_topic), "%s/info", publish_topic.c_str());
|
|
||||||
|
|
||||||
char payload[75]; // Should be enough for info json...
|
|
||||||
const char *json_info_format = PSTR("{\"local_ip\":\"%s\", \"ssid\":\"%s\"}");
|
|
||||||
snprintf(payload, sizeof(payload), json_info_format, WiFi.localIP().toString().c_str(), WiFi.SSID().c_str());
|
|
||||||
|
|
||||||
mqttClient.publish(info_topic, payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allows sensor to be controlled by commands over MQTT
|
* Allows sensor to be controlled by commands over MQTT
|
||||||
*
|
*
|
||||||
|
@ -132,46 +72,13 @@ namespace mqtt {
|
||||||
}
|
}
|
||||||
led_effects::onBoardLEDOn();
|
led_effects::onBoardLEDOn();
|
||||||
Serial.print(F("Message arrived on topic: "));
|
Serial.print(F("Message arrived on topic: "));
|
||||||
Serial.print(sub_topic);
|
Serial.println(sub_topic);
|
||||||
Serial.print(F(". Message: '"));
|
char command[length + 1];
|
||||||
String messageString;
|
|
||||||
for (unsigned int i = 0; i < length; i++) {
|
for (unsigned int i = 0; i < length; i++) {
|
||||||
Serial.print((char) message[i]);
|
command[i] = message[i];
|
||||||
messageString += (char) message[i];
|
|
||||||
}
|
}
|
||||||
Serial.println("'.");
|
command[length] = 0;
|
||||||
|
sensor_console::execute(command);
|
||||||
if (messageString.startsWith("co2 ")) {
|
|
||||||
setCO2forDebugging(messageString);
|
|
||||||
} else if (messageString.startsWith("timer ")) {
|
|
||||||
setTimer(messageString);
|
|
||||||
} else if (messageString == "calibrate") {
|
|
||||||
sensor::startCalibrationProcess();
|
|
||||||
} else if (messageString.startsWith("calibrate ")) {
|
|
||||||
calibrateSensorToSpecificPPM(messageString);
|
|
||||||
} else if (messageString.startsWith("mqtt ")) {
|
|
||||||
setMQTTinterval(messageString);
|
|
||||||
} else if (messageString == "publish") {
|
|
||||||
Serial.println(F("Forcing MQTT publish now."));
|
|
||||||
publish(sensor::timestamp, sensor::co2, sensor::temperature, sensor::humidity);
|
|
||||||
#ifdef AMPEL_CSV
|
|
||||||
} else if (messageString.startsWith("csv ")) {
|
|
||||||
setCSVinterval(messageString);
|
|
||||||
} else if (messageString == "format_filesystem") {
|
|
||||||
FS_LIB.format();
|
|
||||||
led_effects::showKITTWheel(color::blue, 2);
|
|
||||||
#endif
|
|
||||||
} else if (messageString == "night_mode") {
|
|
||||||
led_effects::toggleNightMode();
|
|
||||||
} else if (messageString == "local_ip") {
|
|
||||||
sendInfoAboutLocalNetwork();
|
|
||||||
} else if (messageString == "reset") {
|
|
||||||
ESP.restart(); // softer than ESP.reset()
|
|
||||||
} else {
|
|
||||||
led_effects::showKITTWheel(color::red, 1);
|
|
||||||
Serial.println(F("Message not supported. Doing nothing."));
|
|
||||||
}
|
|
||||||
delay(50);
|
|
||||||
led_effects::onBoardLEDOff();
|
led_effects::onBoardLEDOff();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -188,7 +95,7 @@ namespace mqtt {
|
||||||
|
|
||||||
led_effects::onBoardLEDOn();
|
led_effects::onBoardLEDOn();
|
||||||
// Wait for connection, at most 15s (default)
|
// Wait for connection, at most 15s (default)
|
||||||
mqttClient.connect(publish_topic.c_str(), config::mqtt_user, config::mqtt_password);
|
mqttClient.connect(publish_topic, config::mqtt_user, config::mqtt_password);
|
||||||
led_effects::onBoardLEDOff();
|
led_effects::onBoardLEDOff();
|
||||||
|
|
||||||
connected = mqttClient.connected();
|
connected = mqttClient.connected();
|
||||||
|
@ -196,7 +103,7 @@ namespace mqtt {
|
||||||
if (connected) {
|
if (connected) {
|
||||||
if (config::allow_mqtt_commands) {
|
if (config::allow_mqtt_commands) {
|
||||||
char control_topic[60]; // Should be enough for "CO2sensors/ESPd03cc5/control"
|
char control_topic[60]; // Should be enough for "CO2sensors/ESPd03cc5/control"
|
||||||
snprintf(control_topic, sizeof(control_topic), "%s/control", publish_topic.c_str());
|
snprintf(control_topic, sizeof(control_topic), "%s/control", publish_topic);
|
||||||
mqttClient.subscribe(control_topic);
|
mqttClient.subscribe(control_topic);
|
||||||
mqttClient.setCallback(controlSensorCallback);
|
mqttClient.setCallback(controlSensorCallback);
|
||||||
}
|
}
|
||||||
|
@ -212,12 +119,12 @@ namespace mqtt {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void publishIfTimeHasCome(const String &timeStamp, const int16_t &co2, const float &temp, const float &hum) {
|
void publishIfTimeHasCome(const char *timestamp, const int16_t &co2, const float &temp, const float &hum) {
|
||||||
// Send message via MQTT according to sending interval
|
// Send message via MQTT according to sending interval
|
||||||
unsigned long now = seconds();
|
unsigned long now = seconds();
|
||||||
if (now - last_sent_at > config::sending_interval) {
|
if (now - last_sent_at > config::mqtt_sending_interval) {
|
||||||
last_sent_at = now;
|
last_sent_at = now;
|
||||||
publish(timeStamp, co2, temp, hum);
|
publish(timestamp, co2, temp, hum);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -229,4 +136,28 @@ namespace mqtt {
|
||||||
mqttClient.loop();
|
mqttClient.loop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*****************************************************************
|
||||||
|
* Callbacks for sensor commands *
|
||||||
|
*****************************************************************/
|
||||||
|
void setMQTTinterval(int32_t sending_interval) {
|
||||||
|
config::mqtt_sending_interval = sending_interval;
|
||||||
|
Serial.print(F("Setting MQTT sending interval to : "));
|
||||||
|
Serial.print(config::mqtt_sending_interval);
|
||||||
|
Serial.println("s.");
|
||||||
|
led_effects::showKITTWheel(color::green, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// It can be hard to find the local IP of a sensor if it isn't connected to Serial port, and if mDNS is disabled.
|
||||||
|
// If the sensor can be reach by MQTT, it can answer with info about local_ip and ssid.
|
||||||
|
// The sensor will send the info to "CO2sensors/ESP123456/info".
|
||||||
|
void sendInfoAboutLocalNetwork() {
|
||||||
|
char info_topic[60]; // Should be enough for "CO2sensors/ESP123456/info"
|
||||||
|
snprintf(info_topic, sizeof(info_topic), "%s/info", publish_topic);
|
||||||
|
|
||||||
|
char payload[75]; // Should be enough for info json...
|
||||||
|
const char *json_info_format = PSTR("{\"local_ip\":\"%s\", \"ssid\":\"%s\"}");
|
||||||
|
snprintf(payload, sizeof(payload), json_info_format, wifi::local_ip, WIFI_SSID);
|
||||||
|
|
||||||
|
mqttClient.publish(info_topic, payload);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,20 +4,22 @@
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "led_effects.h"
|
#include "led_effects.h"
|
||||||
#ifdef AMPEL_CSV
|
#include "sensor_console.h"
|
||||||
# include "csv_writer.h"
|
|
||||||
#endif
|
|
||||||
#include "co2_sensor.h"
|
|
||||||
#include "src/lib/PubSubClient/src/PubSubClient.h"
|
#include "src/lib/PubSubClient/src/PubSubClient.h"
|
||||||
#include "wifi_util.h"
|
#include "wifi_util.h"
|
||||||
|
|
||||||
namespace config {
|
namespace config {
|
||||||
extern uint16_t sending_interval; // [s]
|
extern uint16_t mqtt_sending_interval; // [s]
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace mqtt {
|
namespace mqtt {
|
||||||
extern String last_successful_publish;
|
extern char last_successful_publish[];
|
||||||
extern bool connected;
|
extern bool connected;
|
||||||
void initialize(String &topic);
|
void initialize(const char *sensorId);
|
||||||
void keepConnection();
|
void keepConnection();
|
||||||
void publishIfTimeHasCome(const String &timeStamp, const int16_t &co2, const float &temp, const float &hum);
|
void publishIfTimeHasCome(const char *timestamp, const int16_t &co2, const float &temp, const float &hum);
|
||||||
|
|
||||||
|
void setMQTTinterval(int32_t sending_interval);
|
||||||
|
void sendInfoAboutLocalNetwork();
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
188
ampel-firmware/sensor_console.cpp
Normal file
188
ampel-firmware/sensor_console.cpp
Normal file
|
@ -0,0 +1,188 @@
|
||||||
|
#include "sensor_console.h"
|
||||||
|
|
||||||
|
namespace sensor_console {
|
||||||
|
const uint8_t MAX_COMMANDS = 20;
|
||||||
|
const uint8_t MAX_COMMAND_SIZE = 30;
|
||||||
|
|
||||||
|
uint8_t commands_count = 0;
|
||||||
|
|
||||||
|
enum input_type {
|
||||||
|
NONE,
|
||||||
|
INT32,
|
||||||
|
STRING
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Command {
|
||||||
|
const char *name;
|
||||||
|
union {
|
||||||
|
void (*voidFunction)();
|
||||||
|
void (*intFunction)(int32_t);
|
||||||
|
void (*strFunction)(char*);
|
||||||
|
};
|
||||||
|
const char *doc;
|
||||||
|
input_type parameter_type;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct CommandLine {
|
||||||
|
char function_name[MAX_COMMAND_SIZE];
|
||||||
|
input_type argument_type;
|
||||||
|
int32_t int_argument;
|
||||||
|
char str_argument[MAX_COMMAND_SIZE];
|
||||||
|
};
|
||||||
|
|
||||||
|
Command commands[MAX_COMMANDS];
|
||||||
|
|
||||||
|
bool addCommand(const char *name, const __FlashStringHelper *doc_fstring) {
|
||||||
|
if (commands_count < MAX_COMMANDS) {
|
||||||
|
commands[commands_count].name = name;
|
||||||
|
commands[commands_count].doc = (const char*) doc_fstring;
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
Serial.println(F("Too many commands have been defined."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void defineCommand(const char *name, void (*function)(), const __FlashStringHelper *doc_fstring) {
|
||||||
|
if (addCommand(name, doc_fstring)) {
|
||||||
|
commands[commands_count].voidFunction = function;
|
||||||
|
commands[commands_count++].parameter_type = NONE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void defineIntCommand(const char *name, void (*function)(int32_t), const __FlashStringHelper *doc_fstring) {
|
||||||
|
if (addCommand(name, doc_fstring)) {
|
||||||
|
commands[commands_count].intFunction = function;
|
||||||
|
commands[commands_count++].parameter_type = INT32;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void defineStringCommand(const char *name, void (*function)(char*), const __FlashStringHelper *doc_fstring) {
|
||||||
|
if (addCommand(name, doc_fstring)) {
|
||||||
|
commands[commands_count].strFunction = function;
|
||||||
|
commands[commands_count++].parameter_type = STRING;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Tries to split a string command (e.g. 'mqtt 60' or 'show_csv') into
|
||||||
|
* a CommandLine struct (function_name, argument_type and argument)
|
||||||
|
*/
|
||||||
|
void parseCommand(const char *command, CommandLine &command_line) {
|
||||||
|
if (strlen(command) == 0) {
|
||||||
|
Serial.println(F("Received empty command"));
|
||||||
|
command_line.argument_type = NONE;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
char *first_space;
|
||||||
|
first_space = strchr(command, ' ');
|
||||||
|
|
||||||
|
if (first_space == NULL) {
|
||||||
|
command_line.argument_type = NONE;
|
||||||
|
strlcpy(command_line.function_name, command, MAX_COMMAND_SIZE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
strlcpy(command_line.function_name, command, first_space - command + 1);
|
||||||
|
strlcpy(command_line.str_argument, first_space + 1, MAX_COMMAND_SIZE - (first_space - command) - 1);
|
||||||
|
|
||||||
|
char *end;
|
||||||
|
command_line.int_argument = strtol(command_line.str_argument, &end, 0); // Accepts 123 or 0xFF00FF
|
||||||
|
|
||||||
|
if (*end) {
|
||||||
|
command_line.argument_type = STRING;
|
||||||
|
} else {
|
||||||
|
command_line.argument_type = INT32;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int compareCommandNames(const void *s1, const void *s2) {
|
||||||
|
struct Command *c1 = (struct Command*) s1;
|
||||||
|
struct Command *c2 = (struct Command*) s2;
|
||||||
|
return strcmp(c1->name, c2->name);
|
||||||
|
}
|
||||||
|
|
||||||
|
void listAvailableCommands() {
|
||||||
|
qsort(commands, commands_count, sizeof(commands[0]), compareCommandNames);
|
||||||
|
for (uint8_t i = 0; i < commands_count; i++) {
|
||||||
|
Serial.print(F(" "));
|
||||||
|
Serial.print(commands[i].name);
|
||||||
|
Serial.print(F(" "));
|
||||||
|
Serial.print(commands[i].doc);
|
||||||
|
Serial.println(F("."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Saves bytes from Serial.read() until enter is pressed, and tries to run the corresponding command.
|
||||||
|
* http://www.gammon.com.au/serial
|
||||||
|
*/
|
||||||
|
void processSerialInput(const byte input_byte) {
|
||||||
|
static char input_line[MAX_COMMAND_SIZE];
|
||||||
|
static unsigned int input_pos = 0;
|
||||||
|
switch (input_byte) {
|
||||||
|
case '\n': // end of text
|
||||||
|
Serial.println();
|
||||||
|
input_line[input_pos] = 0;
|
||||||
|
execute(input_line);
|
||||||
|
input_pos = 0;
|
||||||
|
break;
|
||||||
|
case '\r': // discard carriage return
|
||||||
|
break;
|
||||||
|
case '\b': // backspace
|
||||||
|
if (input_pos > 0) {
|
||||||
|
input_pos--;
|
||||||
|
Serial.print(F("\b \b"));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (input_pos == 0) {
|
||||||
|
Serial.print(F("> "));
|
||||||
|
}
|
||||||
|
// keep adding if not full ... allow for terminating null byte
|
||||||
|
if (input_pos < (MAX_COMMAND_SIZE - 1)) {
|
||||||
|
input_line[input_pos++] = input_byte;
|
||||||
|
Serial.print((char) input_byte);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Tries to find the corresponding callback for a given command. Name and parameter type should fit.
|
||||||
|
*/
|
||||||
|
void execute(const char *command_str) {
|
||||||
|
CommandLine input;
|
||||||
|
parseCommand(command_str, input);
|
||||||
|
for (uint8_t i = 0; i < commands_count; i++) {
|
||||||
|
if (!strcmp(input.function_name, commands[i].name) && input.argument_type == commands[i].parameter_type) {
|
||||||
|
Serial.print(F("Calling : "));
|
||||||
|
Serial.print(input.function_name);
|
||||||
|
switch (input.argument_type) {
|
||||||
|
case NONE:
|
||||||
|
Serial.println(F("()"));
|
||||||
|
commands[i].voidFunction();
|
||||||
|
return;
|
||||||
|
case INT32:
|
||||||
|
Serial.print(F("("));
|
||||||
|
Serial.print(input.int_argument);
|
||||||
|
Serial.println(F(")"));
|
||||||
|
commands[i].intFunction(input.int_argument);
|
||||||
|
return;
|
||||||
|
case STRING:
|
||||||
|
Serial.print(F("('"));
|
||||||
|
Serial.print(input.str_argument);
|
||||||
|
Serial.println(F("')"));
|
||||||
|
commands[i].strFunction(input.str_argument);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Serial.print(F("'"));
|
||||||
|
Serial.print(command_str);
|
||||||
|
Serial.println(F("' not supported. Available commands :"));
|
||||||
|
listAvailableCommands();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
20
ampel-firmware/sensor_console.h
Normal file
20
ampel-firmware/sensor_console.h
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
#ifndef SENSOR_CONSOLE_H_INCLUDED
|
||||||
|
#define SENSOR_CONSOLE_H_INCLUDED
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
/** Other scripts can use this namespace, in order to define commands, via callbacks.
|
||||||
|
* Those callbacks can then be used to send commands to the sensor (reset, calibrate, night mode, ...)
|
||||||
|
* The callbacks can either have no parameter, or one int32_t parameter.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace sensor_console {
|
||||||
|
void defineCommand(const char *name, void (*function)(), const __FlashStringHelper *doc_fstring);
|
||||||
|
void defineIntCommand(const char *name, void (*function)(int32_t), const __FlashStringHelper *doc_fstring);
|
||||||
|
void defineStringCommand(const char *name, void (*function)(char*), const __FlashStringHelper *doc_fstring);
|
||||||
|
|
||||||
|
void processSerialInput(const byte in_byte);
|
||||||
|
|
||||||
|
void execute(const char *command_line);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
|
@ -152,19 +152,17 @@ int NTPClient::getSeconds() {
|
||||||
return (this->getEpochTime() % 60);
|
return (this->getEpochTime() % 60);
|
||||||
}
|
}
|
||||||
|
|
||||||
String NTPClient::getFormattedTime(unsigned long secs) {
|
void NTPClient::getFormattedTime(char *formatted_time, unsigned long secs) {
|
||||||
unsigned long rawTime = secs ? secs : this->getEpochTime();
|
unsigned long rawTime = secs ? secs : this->getEpochTime();
|
||||||
unsigned int hours = (rawTime % 86400L) / 3600;
|
unsigned int hours = (rawTime % 86400L) / 3600;
|
||||||
unsigned int minutes = (rawTime % 3600) / 60;
|
unsigned int minutes = (rawTime % 3600) / 60;
|
||||||
unsigned int seconds = rawTime % 60;
|
unsigned int seconds = rawTime % 60;
|
||||||
|
|
||||||
char formatted_time[9];
|
snprintf(formatted_time, 9, "%02d:%02d:%02d", hours, minutes, seconds);
|
||||||
snprintf(formatted_time, sizeof(formatted_time), "%02d:%02d:%02d", hours, minutes, seconds);
|
|
||||||
return String(formatted_time);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Based on https://github.com/PaulStoffregen/Time/blob/master/Time.cpp
|
// Based on https://github.com/PaulStoffregen/Time/blob/master/Time.cpp
|
||||||
String NTPClient::getFormattedDate(unsigned long secs) {
|
void NTPClient::getFormattedDate(char *formatted_date, unsigned long secs) {
|
||||||
unsigned long rawTime = (secs ? secs : this->getEpochTime()) / 86400L; // in days
|
unsigned long rawTime = (secs ? secs : this->getEpochTime()) / 86400L; // in days
|
||||||
unsigned long days = 0, year = 1970;
|
unsigned long days = 0, year = 1970;
|
||||||
uint8_t month;
|
uint8_t month;
|
||||||
|
@ -187,11 +185,9 @@ String NTPClient::getFormattedDate(unsigned long secs) {
|
||||||
month++; // jan is month 1
|
month++; // jan is month 1
|
||||||
rawTime++; // first day is day 1
|
rawTime++; // first day is day 1
|
||||||
|
|
||||||
char formatted_date[23];
|
char formatted_time[9];
|
||||||
snprintf(formatted_date, sizeof(formatted_date), "%4lu-%02d-%02lu %s%+03d",
|
this->getFormattedTime(formatted_time, secs);
|
||||||
year, month, rawTime, this->getFormattedTime(secs).c_str(), this->_timeOffset / 3600);
|
snprintf(formatted_date, 23, "%4lu-%02d-%02lu %s%+03d", year, month, rawTime, formatted_time, this->_timeOffset / 3600);
|
||||||
|
|
||||||
return String(formatted_date);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void NTPClient::end() {
|
void NTPClient::end() {
|
||||||
|
|
|
@ -80,7 +80,7 @@ class NTPClient {
|
||||||
/**
|
/**
|
||||||
* @return secs argument (or 0 for current time) formatted like `hh:mm:ss`
|
* @return secs argument (or 0 for current time) formatted like `hh:mm:ss`
|
||||||
*/
|
*/
|
||||||
String getFormattedTime(unsigned long secs = 0);
|
void getFormattedTime(char *formatted_time, unsigned long secs = 0);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return time in seconds since Jan. 1, 1970
|
* @return time in seconds since Jan. 1, 1970
|
||||||
|
@ -91,7 +91,7 @@ class NTPClient {
|
||||||
* @return secs argument (or 0 for current date) formatted to ISO 8601
|
* @return secs argument (or 0 for current date) formatted to ISO 8601
|
||||||
* like `2004-02-12T15:19:21+00:00`
|
* like `2004-02-12T15:19:21+00:00`
|
||||||
*/
|
*/
|
||||||
String getFormattedDate(unsigned long secs = 0);
|
void getFormattedDate(char *formatted_date, unsigned long secs = 0);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stops the underlying UDP client
|
* Stops the underlying UDP client
|
||||||
|
|
|
@ -34,7 +34,7 @@ Code
|
||||||
|
|
||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2016 SparkFun Electronics
|
Copyright (c) 2020 SparkFun Electronics
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|
|
@ -24,6 +24,7 @@ Thanks to!
|
||||||
* [awatterott](https://github.com/awatterott) for adding [getAltitudeCompensation()](https://github.com/sparkfun/SparkFun_SCD30_Arduino_Library/pull/18)
|
* [awatterott](https://github.com/awatterott) for adding [getAltitudeCompensation()](https://github.com/sparkfun/SparkFun_SCD30_Arduino_Library/pull/18)
|
||||||
* [jogi-k](https://github.com/jogi-k) for adding [teensy i2clib](https://github.com/sparkfun/SparkFun_SCD30_Arduino_Library/pull/19) support
|
* [jogi-k](https://github.com/jogi-k) for adding [teensy i2clib](https://github.com/sparkfun/SparkFun_SCD30_Arduino_Library/pull/19) support
|
||||||
* [paulvha](https://github.com/paulvha) for the suggestions and corrections in [his version of the library](https://github.com/paulvha/scd30)
|
* [paulvha](https://github.com/paulvha) for the suggestions and corrections in [his version of the library](https://github.com/paulvha/scd30)
|
||||||
|
* [yamamaya](https://github.com/yamamaya) for the [3ms delay](https://github.com/sparkfun/SparkFun_SCD30_Arduino_Library/pull/24)
|
||||||
|
|
||||||
Repository Contents
|
Repository Contents
|
||||||
-------------------
|
-------------------
|
||||||
|
@ -43,8 +44,6 @@ License Information
|
||||||
|
|
||||||
This product is _**open source**_!
|
This product is _**open source**_!
|
||||||
|
|
||||||
Various bits of the code have different licenses applied. Anything SparkFun wrote is beerware; if you see me (or any other SparkFun employee) at the local, and you've found our code helpful, please buy us a round!
|
|
||||||
|
|
||||||
Please use, reuse, and modify these files as you see fit. Please maintain attribution to SparkFun Electronics and release anything derivative under the same license.
|
Please use, reuse, and modify these files as you see fit. Please maintain attribution to SparkFun Electronics and release anything derivative under the same license.
|
||||||
|
|
||||||
Distributed as-is; no warranty is given.
|
Distributed as-is; no warranty is given.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
name=SparkFun SCD30 Arduino Library
|
name=SparkFun SCD30 Arduino Library
|
||||||
version=1.0.11
|
version=1.0.13
|
||||||
author=SparkFun Electronics
|
author=SparkFun Electronics
|
||||||
maintainer=SparkFun Electronics <sparkfun.com>
|
maintainer=SparkFun Electronics <sparkfun.com>
|
||||||
sentence=Library for the Sensirion SCD30 CO2 Sensor
|
sentence=Library for the Sensirion SCD30 CO2 Sensor
|
||||||
|
|
|
@ -266,6 +266,8 @@ bool SCD30::readMeasurement()
|
||||||
if (_i2cPort->endTransmission() != 0)
|
if (_i2cPort->endTransmission() != 0)
|
||||||
return (0); //Sensor did not ACK
|
return (0); //Sensor did not ACK
|
||||||
|
|
||||||
|
delay(3);
|
||||||
|
|
||||||
const uint8_t receivedBytes = _i2cPort->requestFrom((uint8_t)SCD30_ADDRESS, (uint8_t)18);
|
const uint8_t receivedBytes = _i2cPort->requestFrom((uint8_t)SCD30_ADDRESS, (uint8_t)18);
|
||||||
bool error = false;
|
bool error = false;
|
||||||
if (_i2cPort->available())
|
if (_i2cPort->available())
|
||||||
|
@ -358,6 +360,8 @@ bool SCD30::getSettingValue(uint16_t registerAddress, uint16_t *val)
|
||||||
if (_i2cPort->endTransmission() != 0)
|
if (_i2cPort->endTransmission() != 0)
|
||||||
return (false); //Sensor did not ACK
|
return (false); //Sensor did not ACK
|
||||||
|
|
||||||
|
delay(3);
|
||||||
|
|
||||||
_i2cPort->requestFrom((uint8_t)SCD30_ADDRESS, (uint8_t)3); // Request data and CRC
|
_i2cPort->requestFrom((uint8_t)SCD30_ADDRESS, (uint8_t)3); // Request data and CRC
|
||||||
if (_i2cPort->available())
|
if (_i2cPort->available())
|
||||||
{
|
{
|
||||||
|
@ -389,6 +393,8 @@ uint16_t SCD30::readRegister(uint16_t registerAddress)
|
||||||
if (_i2cPort->endTransmission() != 0)
|
if (_i2cPort->endTransmission() != 0)
|
||||||
return (0); //Sensor did not ACK
|
return (0); //Sensor did not ACK
|
||||||
|
|
||||||
|
delay(3);
|
||||||
|
|
||||||
_i2cPort->requestFrom((uint8_t)SCD30_ADDRESS, (uint8_t)2);
|
_i2cPort->requestFrom((uint8_t)SCD30_ADDRESS, (uint8_t)2);
|
||||||
if (_i2cPort->available())
|
if (_i2cPort->available())
|
||||||
{
|
{
|
||||||
|
|
|
@ -57,7 +57,8 @@
|
||||||
#define COMMAND_STOP_MEAS 0x0104
|
#define COMMAND_STOP_MEAS 0x0104
|
||||||
#define COMMAND_READ_FW_VER 0xD100
|
#define COMMAND_READ_FW_VER 0xD100
|
||||||
|
|
||||||
typedef union {
|
typedef union
|
||||||
|
{
|
||||||
byte array[4];
|
byte array[4];
|
||||||
float value;
|
float value;
|
||||||
} ByteToFl; // paulvha
|
} ByteToFl; // paulvha
|
||||||
|
@ -69,9 +70,9 @@ public:
|
||||||
|
|
||||||
bool begin(bool autoCalibrate) { return begin(Wire, autoCalibrate); }
|
bool begin(bool autoCalibrate) { return begin(Wire, autoCalibrate); }
|
||||||
#ifdef USE_TEENSY3_I2C_LIB
|
#ifdef USE_TEENSY3_I2C_LIB
|
||||||
bool begin(i2c_t3 &wirePort = Wire, bool autoCalibrate=true, bool measBegin=true); //By default use Wire port
|
bool begin(i2c_t3 &wirePort = Wire, bool autoCalibrate = false, bool measBegin = true); //By default use Wire port
|
||||||
#else
|
#else
|
||||||
bool begin(TwoWire &wirePort = Wire, bool autoCalibrate=true, bool measBegin=true); //By default use Wire port
|
bool begin(TwoWire &wirePort = Wire, bool autoCalibrate = false, bool measBegin = true); //By default use Wire port
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
void enableDebugging(Stream &debugPort = Serial); //Turn on debug printing. If user doesn't specify then Serial will be used.
|
void enableDebugging(Stream &debugPort = Serial); //Turn on debug printing. If user doesn't specify then Serial will be used.
|
||||||
|
@ -82,11 +83,11 @@ public:
|
||||||
|
|
||||||
// based on paulvha
|
// based on paulvha
|
||||||
bool getSettingValue(uint16_t registerAddress, uint16_t *val);
|
bool getSettingValue(uint16_t registerAddress, uint16_t *val);
|
||||||
bool getForcedRecalibration(uint16_t *val) {return(getSettingValue(COMMAND_SET_FORCED_RECALIBRATION_FACTOR, val));}
|
bool getForcedRecalibration(uint16_t *val) { return (getSettingValue(COMMAND_SET_FORCED_RECALIBRATION_FACTOR, val)); }
|
||||||
bool getMeasurementInterval(uint16_t *val) {return(getSettingValue(COMMAND_SET_MEASUREMENT_INTERVAL, val));}
|
bool getMeasurementInterval(uint16_t *val) { return (getSettingValue(COMMAND_SET_MEASUREMENT_INTERVAL, val)); }
|
||||||
bool getTemperatureOffset(uint16_t *val) {return(getSettingValue(COMMAND_SET_TEMPERATURE_OFFSET, val));}
|
bool getTemperatureOffset(uint16_t *val) { return (getSettingValue(COMMAND_SET_TEMPERATURE_OFFSET, val)); }
|
||||||
bool getAltitudeCompensation(uint16_t *val) {return(getSettingValue(COMMAND_SET_ALTITUDE_COMPENSATION, val));}
|
bool getAltitudeCompensation(uint16_t *val) { return (getSettingValue(COMMAND_SET_ALTITUDE_COMPENSATION, val)); }
|
||||||
bool getFirmwareVersion(uint16_t *val) {return(getSettingValue(COMMAND_READ_FW_VER, val));}
|
bool getFirmwareVersion(uint16_t *val) { return (getSettingValue(COMMAND_READ_FW_VER, val)); }
|
||||||
|
|
||||||
uint16_t getCO2(void);
|
uint16_t getCO2(void);
|
||||||
float getHumidity(void);
|
float getHumidity(void);
|
||||||
|
@ -115,7 +116,6 @@ public:
|
||||||
uint8_t computeCRC8(uint8_t data[], uint8_t len);
|
uint8_t computeCRC8(uint8_t data[], uint8_t len);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
||||||
//Variables
|
//Variables
|
||||||
#ifdef USE_TEENSY3_I2C_LIB
|
#ifdef USE_TEENSY3_I2C_LIB
|
||||||
i2c_t3 *_i2cPort; //The generic connection to user's chosen I2C hardware
|
i2c_t3 *_i2cPort; //The generic connection to user's chosen I2C hardware
|
||||||
|
@ -136,6 +136,5 @@ private:
|
||||||
//Debug
|
//Debug
|
||||||
Stream *_debugPort; //The stream to send debug messages to if enabled. Usually Serial.
|
Stream *_debugPort; //The stream to send debug messages to if enabled. Usually Serial.
|
||||||
boolean _printDebug = false; //Flag to print debugging variables
|
boolean _printDebug = false; //Flag to print debugging variables
|
||||||
|
|
||||||
};
|
};
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -5,41 +5,96 @@ namespace config {
|
||||||
const long utc_offset_in_seconds = UTC_OFFSET_IN_SECONDS; // UTC+1
|
const long utc_offset_in_seconds = UTC_OFFSET_IN_SECONDS; // UTC+1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get last 3 bytes of ESP MAC (worldwide unique)
|
#if defined(ESP8266)
|
||||||
String macToID() {
|
const char *current_board = "ESP8266";
|
||||||
uint8_t mac[6];
|
# if !defined(AMPEL_WIFI)
|
||||||
WiFi.macAddress(mac);
|
void preinit() {
|
||||||
String result;
|
// WiFi would be initialized otherwise (on ESP8266), even if unused.
|
||||||
for (int i = 3; i < 6; i++) {
|
// see https://github.com/esp8266/Arduino/issues/2111#issuecomment-224251391
|
||||||
if (mac[i] < 16)
|
ESP8266WiFiClass::preinitWiFiOff();
|
||||||
result += '0';
|
|
||||||
result += String(mac[i], HEX);
|
|
||||||
}
|
|
||||||
result.toLowerCase();
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
# endif
|
||||||
|
#elif defined(ESP32)
|
||||||
|
const char *current_board = "ESP32";
|
||||||
|
#else
|
||||||
|
const char *current_board = "UNKNOWN";
|
||||||
|
#endif
|
||||||
|
|
||||||
//NOTE: ESP32 sometimes couldn't access the NTP server, and every loop would take +1000ms
|
//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, config::ntp_server, config::utc_offset_in_seconds, 60000UL);
|
||||||
|
bool connected_at_least_once = false;
|
||||||
|
|
||||||
void initialize() {
|
void initialize() {
|
||||||
timeClient.begin();
|
timeClient.begin();
|
||||||
}
|
}
|
||||||
|
|
||||||
void update() {
|
void update() {
|
||||||
timeClient.update();
|
connected_at_least_once |= timeClient.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
String getLocalTime() {
|
void getLocalTime(char *timestamp) {
|
||||||
return timeClient.getFormattedDate();
|
timeClient.getFormattedDate(timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setLocalTime(int32_t unix_seconds) {
|
||||||
|
char time[23];
|
||||||
|
timeClient.getFormattedDate(time);
|
||||||
|
Serial.print(F("Current time : "));
|
||||||
|
Serial.println(time);
|
||||||
|
if (connected_at_least_once) {
|
||||||
|
Serial.println(F("NTP update already happened. Not changing anything."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Serial.print(F("Setting UNIX time to : "));
|
||||||
|
Serial.println(unix_seconds);
|
||||||
|
timeClient.setEpochTime(unix_seconds - seconds());
|
||||||
|
timeClient.getFormattedDate(time);
|
||||||
|
Serial.print(F("Current time : "));
|
||||||
|
Serial.println(time);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t max_loop_duration = 0;
|
void Ampel::showFreeSpace() {
|
||||||
|
Serial.print(F("Free heap space : "));
|
||||||
|
Serial.print(ESP.getFreeHeap());
|
||||||
|
Serial.println(F(" bytes."));
|
||||||
|
Serial.print(F("Max free block size : "));
|
||||||
|
Serial.print(esp_get_max_free_block_size());
|
||||||
|
Serial.println(F(" bytes."));
|
||||||
|
Serial.print(F("Heap fragmentation : "));
|
||||||
|
Serial.print(esp_get_heap_fragmentation());
|
||||||
|
Serial.println(F(" %"));
|
||||||
|
}
|
||||||
|
|
||||||
//FIXME: Remove every instance of Strings, to avoid heap fragmentation problems. (Start: "Free heap space : 17104 bytes")
|
char sensorId[10]; // e.g "ESPxxxxxx\0"
|
||||||
// See more https://cpp4arduino.com/2020/02/07/how-to-format-strings-without-the-string-class.html
|
char macAddress[18]; // e.g "XX:XX:XX:XX:XX:XX\0"
|
||||||
const String SENSOR_ID = "ESP" + macToID();
|
uint8_t mac[6];
|
||||||
|
|
||||||
|
char* getMacString() {
|
||||||
|
WiFi.macAddress(mac);
|
||||||
|
// Get all 6 bytes of ESP MAC
|
||||||
|
snprintf(macAddress, sizeof(macAddress), "%02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4],
|
||||||
|
mac[5]);
|
||||||
|
return macAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
char* getSensorId() {
|
||||||
|
WiFi.macAddress(mac);
|
||||||
|
// Get last 3 bytes of ESP MAC (worldwide unique)
|
||||||
|
snprintf(sensorId, sizeof(sensorId), "ESP%02x%02x%02x", mac[3], mac[4], mac[5]);
|
||||||
|
return sensorId;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ampel::Ampel() :
|
||||||
|
board(current_board), sensorId(getSensorId()), macAddress(getMacString()), max_loop_duration(0) {
|
||||||
|
sensor_console::defineIntCommand("set_time", ntp::setLocalTime, F("1618829570 (Sets time to the given UNIX time)"));
|
||||||
|
sensor_console::defineCommand("free", Ampel::showFreeSpace, F("(Displays available heap space)"));
|
||||||
|
sensor_console::defineCommand("reset", []() {
|
||||||
|
ESP.restart();
|
||||||
|
}, F("(Restarts the ESP)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ampel ampel;
|
||||||
|
|
|
@ -2,26 +2,25 @@
|
||||||
#define AMPEL_UTIL_H_INCLUDED
|
#define AMPEL_UTIL_H_INCLUDED
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
#include "sensor_console.h"
|
||||||
|
|
||||||
#include <WiFiUdp.h> // required for NTP
|
#include <WiFiUdp.h> // required for NTP
|
||||||
#include "src/lib/NTPClient-master/NTPClient.h" // NTP
|
#include "src/lib/NTPClient-master/NTPClient.h" // NTP
|
||||||
|
|
||||||
#if defined(ESP8266)
|
#if defined(ESP8266)
|
||||||
# define BOARD "ESP8266"
|
|
||||||
# include <ESP8266WiFi.h> // required to get MAC address
|
# include <ESP8266WiFi.h> // required to get MAC address
|
||||||
# define get_free_heap_size() system_get_free_heap_size()
|
# define esp_get_max_free_block_size() ESP.getMaxFreeBlockSize()
|
||||||
|
# define esp_get_heap_fragmentation() ESP.getHeapFragmentation()
|
||||||
#elif defined(ESP32)
|
#elif defined(ESP32)
|
||||||
# define BOARD "ESP32"
|
|
||||||
# include <WiFi.h> // required to get MAC address
|
# include <WiFi.h> // required to get MAC address
|
||||||
# define get_free_heap_size() esp_get_free_heap_size()
|
# define esp_get_max_free_block_size() ESP.getMaxAllocHeap() //largest block of heap that can be allocated.
|
||||||
#else
|
# define esp_get_heap_fragmentation() "?" // apparently not available for ESP32
|
||||||
# define BOARD "Unknown"
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace ntp {
|
namespace ntp {
|
||||||
void initialize();
|
void initialize();
|
||||||
void update();
|
void update();
|
||||||
String getLocalTime();
|
void getLocalTime(char *timestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace util {
|
namespace util {
|
||||||
|
@ -35,10 +34,21 @@ namespace util {
|
||||||
return b > a ? b : a;
|
return b > a ? b : a;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
class Ampel {
|
||||||
|
private:
|
||||||
|
static void showFreeSpace();
|
||||||
|
public:
|
||||||
|
const char *version = "v0.2.3-DEV"; // Update manually after significant changes.
|
||||||
|
const char *board;
|
||||||
|
const char *sensorId;
|
||||||
|
const char *macAddress;
|
||||||
|
uint32_t max_loop_duration;
|
||||||
|
Ampel();
|
||||||
|
};
|
||||||
|
|
||||||
|
extern Ampel ampel;
|
||||||
|
|
||||||
//NOTE: Only use seconds() for duration comparison, not timestamps comparison. Otherwise, problems happen when millis roll over.
|
//NOTE: Only use seconds() for duration comparison, not timestamps comparison. Otherwise, problems happen when millis roll over.
|
||||||
#define seconds() (millis() / 1000UL)
|
#define seconds() (millis() / 1000UL)
|
||||||
extern uint32_t max_loop_duration;
|
|
||||||
const extern String SENSOR_ID;
|
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -22,6 +22,7 @@ namespace web_server {
|
||||||
const char *script_template;
|
const char *script_template;
|
||||||
void handleWebServerRoot();
|
void handleWebServerRoot();
|
||||||
void handlePageNotFound();
|
void handlePageNotFound();
|
||||||
|
void handleWebServerCommand();
|
||||||
|
|
||||||
#ifdef AMPEL_CSV
|
#ifdef AMPEL_CSV
|
||||||
void handleDeleteCSV();
|
void handleDeleteCSV();
|
||||||
|
@ -58,13 +59,13 @@ namespace web_server {
|
||||||
"</head>\n"
|
"</head>\n"
|
||||||
"<body>\n"
|
"<body>\n"
|
||||||
|
|
||||||
"<div class='pure-g'><div class='pure-u-1'><div class='pure-menu'><p class='pure-menu-heading'>MakerLab Murnau e.V. CO<sub>2</sub> Ampel</p></div></div>\n"
|
"<div class='pure-g'><div class='pure-u-1'><div class='pure-menu'><p class='pure-menu-heading'>HfT-Stuttgart CO<sub>2</sub> Ampel</p></div></div>\n"
|
||||||
"<div class='pure-u-1'><ul class='pure-menu pure-menu-horizontal pure-menu-list'>\n"
|
"<div class='pure-u-1'><ul class='pure-menu pure-menu-horizontal pure-menu-list'>\n"
|
||||||
"<li class='pure-menu-item'><a href='#table' class='pure-menu-link'>Info</a></li>\n"
|
"<li class='pure-menu-item'><a href='#table' class='pure-menu-link'>Info</a></li>\n"
|
||||||
#ifdef AMPEL_CSV
|
#ifdef AMPEL_CSV
|
||||||
"<li class='pure-menu-item'><a href='#graph' class='pure-menu-link'>Graph</a></li>\n"
|
"<li class='pure-menu-item'><a href='#graph' class='pure-menu-link'>Graph</a></li>\n"
|
||||||
"<li class='pure-menu-item'><a href='#log' class='pure-menu-link'>Log</a></li>\n"
|
"<li class='pure-menu-item'><a href='#log' class='pure-menu-link'>Log</a></li>\n"
|
||||||
"<li class='pure-menu-item'><a href='./%s' class='pure-menu-link'>Download CSV</a></li>\n"
|
"<li class='pure-menu-item'><a href='%s' class='pure-menu-link'>Download CSV</a></li>\n"
|
||||||
#endif
|
#endif
|
||||||
"<li class='pure-menu-item' id='led'>⬤</li>\n" // LED
|
"<li class='pure-menu-item' id='led'>⬤</li>\n" // LED
|
||||||
"</ul></div></div>\n"
|
"</ul></div></div>\n"
|
||||||
|
@ -72,16 +73,15 @@ namespace web_server {
|
||||||
// Show a colored dot on the webpage, with a similar color than on LED Ring.
|
// Show a colored dot on the webpage, with a similar color than on LED Ring.
|
||||||
"hue=(1-(Math.min(Math.max(parseInt(document.title),500),1600)-500)/1100)*120;\n"
|
"hue=(1-(Math.min(Math.max(parseInt(document.title),500),1600)-500)/1100)*120;\n"
|
||||||
"document.getElementById('led').style.color=['hsl(',hue,',100%%,50%%)'].join('');\n"
|
"document.getElementById('led').style.color=['hsl(',hue,',100%%,50%%)'].join('');\n"
|
||||||
"</script>\n");
|
"</script>\n"
|
||||||
|
"<div class='pure-g'>\n"
|
||||||
body_template =
|
"<div class='pure-u-1' id='graph'></div>\n"// Graph placeholder
|
||||||
PSTR("<div class='pure-g'>\n"
|
|
||||||
"<div class='pure-u-1' id='graph'></div>\n" // Graph placeholder
|
|
||||||
"</div>\n"
|
"</div>\n"
|
||||||
"<div class='pure-g'>\n"
|
"<div class='pure-g'>\n"
|
||||||
//Sensor table
|
"<table id='table' class='pure-table-striped pure-u-1 pure-u-md-1-2'>\n");
|
||||||
"<table id='table' class='pure-table-striped pure-u-1 pure-u-md-1-2'>\n"
|
|
||||||
"<tr><th colspan='2'>%s</th></tr>\n"
|
body_template =
|
||||||
|
PSTR("<tr><th colspan='2'>%s</th></tr>\n"
|
||||||
"<tr><td>CO<sub>2</sub> concentration</td><td>%5d ppm</td></tr>\n"
|
"<tr><td>CO<sub>2</sub> concentration</td><td>%5d ppm</td></tr>\n"
|
||||||
"<tr><td>Temperature</td><td>%.1f℃</td></tr>\n"
|
"<tr><td>Temperature</td><td>%.1f℃</td></tr>\n"
|
||||||
"<tr><td>Humidity</td><td>%.1f%%</td></tr>\n"
|
"<tr><td>Humidity</td><td>%.1f%%</td></tr>\n"
|
||||||
|
@ -108,14 +108,19 @@ namespace web_server {
|
||||||
#endif
|
#endif
|
||||||
"<tr><th colspan='2'>Sensor</th></tr>\n"
|
"<tr><th colspan='2'>Sensor</th></tr>\n"
|
||||||
"<tr><td>Temperature offset</td><td>%.1fK</td></tr>\n" //TODO: Read it from sensor?
|
"<tr><td>Temperature offset</td><td>%.1fK</td></tr>\n" //TODO: Read it from sensor?
|
||||||
|
"<tr><td>Auto-calibration?</td><td>%s</td></tr>\n"
|
||||||
"<tr><td>Local address</td><td><a href='http://%s.local/'>%s.local</a></td></tr>\n"
|
"<tr><td>Local address</td><td><a href='http://%s.local/'>%s.local</a></td></tr>\n"
|
||||||
"<tr><td>Local IP</td><td><a href='http://%s'>%s</a></td></tr>\n"
|
"<tr><td>Local IP</td><td><a href='http://%s'>%s</a></td></tr>\n"
|
||||||
|
"<tr><td>MAC</td><td>%s</td></tr>\n"
|
||||||
"<tr><td>Free heap space</td><td>%6d bytes</td></tr>\n"
|
"<tr><td>Free heap space</td><td>%6d bytes</td></tr>\n"
|
||||||
|
"<tr><td>Largest heap block</td><td>%6d bytes</td></tr>\n"
|
||||||
"<tr><td>Max loop duration</td><td>%5d ms</td></tr>\n"
|
"<tr><td>Max loop duration</td><td>%5d ms</td></tr>\n"
|
||||||
"<tr><td>Board</td><td>%s</td></tr>\n"
|
"<tr><td>Board</td><td>%s</td></tr>\n"
|
||||||
|
"<tr><td>Ampel firmware</td><td>%s</td></tr>\n"
|
||||||
"<tr><td>Uptime</td><td>%2d d %4d h %02d min %02d s</td></tr>\n"
|
"<tr><td>Uptime</td><td>%2d d %4d h %02d min %02d s</td></tr>\n"
|
||||||
"</table>\n"
|
"</table>\n"
|
||||||
"<div id='log' class='pure-u-1 pure-u-md-1-2'></div>\n"
|
"<div id='log' class='pure-u-1 pure-u-md-1-2'></div>\n"
|
||||||
|
"<form action='/command'><input type='text' id='send' name='send'><input type='submit' value='Send'></form>\n"
|
||||||
#ifdef AMPEL_CSV
|
#ifdef AMPEL_CSV
|
||||||
"<form action='/delete_csv' method='POST' onsubmit=\"return confirm('Are you really sure you want to delete all data?') && (document.body.style.cursor = 'wait');\">"
|
"<form action='/delete_csv' method='POST' onsubmit=\"return confirm('Are you really sure you want to delete all data?') && (document.body.style.cursor = 'wait');\">"
|
||||||
"<input type='submit' value='Delete CSV'/>"
|
"<input type='submit' value='Delete CSV'/>"
|
||||||
|
@ -130,7 +135,7 @@ namespace web_server {
|
||||||
#ifdef AMPEL_CSV
|
#ifdef AMPEL_CSV
|
||||||
"<script>\n"
|
"<script>\n"
|
||||||
"document.body.style.cursor = 'default';\n"
|
"document.body.style.cursor = 'default';\n"
|
||||||
"fetch('./%s',{credentials:'include'})\n"
|
"fetch('%s',{credentials:'include'})\n"
|
||||||
".then(response=>response.text())\n"
|
".then(response=>response.text())\n"
|
||||||
".then(csvText=>csvToTable(csvText))\n"
|
".then(csvText=>csvToTable(csvText))\n"
|
||||||
".then(htmlTable=>addLogTableToPage(htmlTable))\n"
|
".then(htmlTable=>addLogTableToPage(htmlTable))\n"
|
||||||
|
@ -173,15 +178,16 @@ namespace web_server {
|
||||||
|
|
||||||
// Web-server
|
// Web-server
|
||||||
http.on("/", handleWebServerRoot);
|
http.on("/", handleWebServerRoot);
|
||||||
|
http.on("/command", handleWebServerCommand);
|
||||||
#ifdef AMPEL_CSV
|
#ifdef AMPEL_CSV
|
||||||
http.on("/" + csv_writer::filename, handleWebServerCSV);
|
http.on(csv_writer::filename, handleWebServerCSV); //NOTE: csv_writer should have been initialized first.
|
||||||
http.on("/delete_csv", HTTP_POST, handleDeleteCSV);
|
http.on("/delete_csv", HTTP_POST, handleDeleteCSV);
|
||||||
#endif
|
#endif
|
||||||
http.onNotFound(handlePageNotFound);
|
http.onNotFound(handlePageNotFound);
|
||||||
http.begin();
|
http.begin();
|
||||||
|
|
||||||
Serial.print(F("You can access this sensor via http://"));
|
Serial.print(F("You can access this sensor via http://"));
|
||||||
Serial.print(SENSOR_ID);
|
Serial.print(ampel.sensorId);
|
||||||
Serial.print(F(".local (might be unstable) or http://"));
|
Serial.print(F(".local (might be unstable) or http://"));
|
||||||
Serial.println(WiFi.localIP());
|
Serial.println(WiFi.localIP());
|
||||||
}
|
}
|
||||||
|
@ -207,44 +213,49 @@ namespace web_server {
|
||||||
|
|
||||||
//NOTE: Splitting in multiple parts in order to use less RAM
|
//NOTE: Splitting in multiple parts in order to use less RAM
|
||||||
char content[2000]; // Update if needed
|
char content[2000]; // Update if needed
|
||||||
// Header size : 1611 - Body size : 1800 - Script size : 1920
|
// INFO - Header size : 1767 - Body size : 1991 - Script size : 1909
|
||||||
|
|
||||||
// Header
|
snprintf_P(content, sizeof(content), header_template, sensor::co2, ampel.sensorId, wifi::local_ip
|
||||||
snprintf_P(content, sizeof(content), header_template, sensor::co2, SENSOR_ID.c_str(),
|
|
||||||
WiFi.localIP().toString().c_str()
|
|
||||||
#ifdef AMPEL_CSV
|
#ifdef AMPEL_CSV
|
||||||
, csv_writer::filename.c_str()
|
, csv_writer::filename
|
||||||
#endif
|
#endif
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Serial.print(F("INFO - Header size : "));
|
||||||
|
// Serial.print(strlen(content));
|
||||||
http.setContentLength(CONTENT_LENGTH_UNKNOWN);
|
http.setContentLength(CONTENT_LENGTH_UNKNOWN);
|
||||||
http.send_P(200, PSTR("text/html"), content);
|
http.send_P(200, PSTR("text/html"), content);
|
||||||
|
|
||||||
// Body
|
// Body
|
||||||
snprintf_P(content, sizeof(content), body_template, SENSOR_ID.c_str(), sensor::co2, sensor::temperature,
|
snprintf_P(content, sizeof(content), body_template, ampel.sensorId, sensor::co2, sensor::temperature,
|
||||||
sensor::humidity, sensor::timestamp.c_str(), config::measurement_timestep,
|
sensor::humidity, sensor::timestamp, config::measurement_timestep,
|
||||||
#ifdef AMPEL_CSV
|
#ifdef AMPEL_CSV
|
||||||
csv_writer::last_successful_write.c_str(), config::csv_interval, csv_writer::getAvailableSpace() / 1024,
|
csv_writer::last_successful_write, config::csv_interval, csv_writer::getAvailableSpace() / 1024,
|
||||||
#endif
|
#endif
|
||||||
#ifdef AMPEL_MQTT
|
#ifdef AMPEL_MQTT
|
||||||
mqtt::connected ? "Yes" : "No", mqtt::last_successful_publish.c_str(), config::sending_interval,
|
mqtt::connected ? "Yes" : "No", mqtt::last_successful_publish, config::mqtt_sending_interval,
|
||||||
#endif
|
#endif
|
||||||
#if defined(AMPEL_LORAWAN) && defined(ESP32)
|
#if defined(AMPEL_LORAWAN) && defined(ESP32)
|
||||||
lorawan::connected ? "Yes" : "No", LMIC_FREQUENCY_PLAN, lorawan::last_transmission.c_str(),
|
lorawan::connected ? "Yes" : "No", LMIC_FREQUENCY_PLAN, lorawan::last_transmission,
|
||||||
config::lorawan_sending_interval,
|
config::lorawan_sending_interval,
|
||||||
#endif
|
#endif
|
||||||
config::temperature_offset, SENSOR_ID.c_str(), SENSOR_ID.c_str(), WiFi.localIP().toString().c_str(),
|
config::temperature_offset, config::auto_calibrate_sensor ? "Yes" : "No", ampel.sensorId, ampel.sensorId,
|
||||||
WiFi.localIP().toString().c_str(), get_free_heap_size(), max_loop_duration, BOARD, dd, hh, mm, ss);
|
wifi::local_ip, wifi::local_ip, ampel.macAddress, ESP.getFreeHeap(), esp_get_max_free_block_size(),
|
||||||
|
ampel.max_loop_duration, ampel.board, ampel.version, dd, hh, mm, ss);
|
||||||
|
|
||||||
|
// Serial.print(F(" - Body size : "));
|
||||||
|
// Serial.print(strlen(content));
|
||||||
http.sendContent(content);
|
http.sendContent(content);
|
||||||
|
|
||||||
// Script
|
// Script
|
||||||
snprintf_P(content, sizeof(content), script_template
|
snprintf_P(content, sizeof(content), script_template
|
||||||
#ifdef AMPEL_CSV
|
#ifdef AMPEL_CSV
|
||||||
, csv_writer::filename.c_str(), SENSOR_ID.c_str()
|
, csv_writer::filename, ampel.sensorId
|
||||||
#endif
|
#endif
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Serial.print(F(" - Script size : "));
|
||||||
|
// Serial.println(strlen(content));
|
||||||
http.sendContent(content);
|
http.sendContent(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -255,7 +266,9 @@ namespace web_server {
|
||||||
}
|
}
|
||||||
if (FS_LIB.exists(csv_writer::filename)) {
|
if (FS_LIB.exists(csv_writer::filename)) {
|
||||||
fs::File csv_file = FS_LIB.open(csv_writer::filename, "r");
|
fs::File csv_file = FS_LIB.open(csv_writer::filename, "r");
|
||||||
http.sendHeader("Content-Length", String(csv_file.size()));
|
char csv_size[10];
|
||||||
|
snprintf(csv_size, sizeof(csv_size), "%d", csv_file.size());
|
||||||
|
http.sendHeader("Content-Length", csv_size);
|
||||||
http.streamFile(csv_file, F("text/csv"));
|
http.streamFile(csv_file, F("text/csv"));
|
||||||
csv_file.close();
|
csv_file.close();
|
||||||
} else {
|
} else {
|
||||||
|
@ -267,14 +280,23 @@ namespace web_server {
|
||||||
if (!shouldBeAllowed()) {
|
if (!shouldBeAllowed()) {
|
||||||
return http.requestAuthentication(DIGEST_AUTH);
|
return http.requestAuthentication(DIGEST_AUTH);
|
||||||
}
|
}
|
||||||
Serial.print("Removing CSV file...");
|
Serial.print(F("Removing CSV file..."));
|
||||||
FS_LIB.remove(csv_writer::filename);
|
FS_LIB.remove(csv_writer::filename);
|
||||||
Serial.println(" Done!");
|
Serial.println(F(" Done!"));
|
||||||
http.sendHeader("Location", "/");
|
http.sendHeader("Location", "/");
|
||||||
http.send(303);
|
http.send(303);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
void handleWebServerCommand() {
|
||||||
|
if (!shouldBeAllowed()) {
|
||||||
|
return http.requestAuthentication(DIGEST_AUTH);
|
||||||
|
}
|
||||||
|
http.sendHeader("Location", "/");
|
||||||
|
http.send(303);
|
||||||
|
sensor_console::execute(http.arg("send").c_str());
|
||||||
|
}
|
||||||
|
|
||||||
void handlePageNotFound() {
|
void handlePageNotFound() {
|
||||||
http.send(404, F("text/plain"), F("404: Not found"));
|
http.send(404, F("text/plain"), F("404: Not found"));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
#ifndef WEB_SERVER_H_
|
#ifndef WEB_SERVER_H_
|
||||||
#define WEB_SERVER_H_
|
#define WEB_SERVER_H_
|
||||||
|
|
||||||
#if defined(ESP8266)
|
#if defined(ESP8266)
|
||||||
# include <ESP8266WebServer.h>
|
# include <ESP8266WebServer.h>
|
||||||
#elif defined(ESP32)
|
#elif defined(ESP32)
|
||||||
|
@ -8,7 +9,9 @@
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "util.h"
|
#include "util.h"
|
||||||
|
#include "wifi_util.h"
|
||||||
#include "co2_sensor.h"
|
#include "co2_sensor.h"
|
||||||
|
#include "sensor_console.h"
|
||||||
#ifdef AMPEL_CSV
|
#ifdef AMPEL_CSV
|
||||||
# include "csv_writer.h"
|
# include "csv_writer.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -12,15 +12,47 @@ namespace config {
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize Wi-Fi
|
namespace wifi {
|
||||||
void WiFiConnect(const String &hostname) {
|
char local_ip[16]; // "255.255.255.255\0"
|
||||||
|
|
||||||
|
void scanNetworks() {
|
||||||
|
Serial.println();
|
||||||
|
Serial.println(F("WiFi - Scanning..."));
|
||||||
|
bool async = false;
|
||||||
|
bool showHidden = true;
|
||||||
|
int n = WiFi.scanNetworks(async, showHidden);
|
||||||
|
for (int i = 0; i < n; ++i) {
|
||||||
|
Serial.print(F(" * '"));
|
||||||
|
Serial.print(WiFi.SSID(i));
|
||||||
|
Serial.print(F("' ("));
|
||||||
|
int16_t quality = 2 * (100 + WiFi.RSSI(i));
|
||||||
|
Serial.print(util::min(util::max(quality, 0), 100));
|
||||||
|
Serial.println(F(" %)"));
|
||||||
|
}
|
||||||
|
Serial.println(F("Done!"));
|
||||||
|
Serial.println();
|
||||||
|
}
|
||||||
|
|
||||||
|
void showLocalIp() {
|
||||||
|
Serial.print(F("WiFi - Local IP : "));
|
||||||
|
Serial.println(wifi::local_ip);
|
||||||
|
Serial.print(F("WiFi - SSID : "));
|
||||||
|
Serial.println(WIFI_SSID);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize Wi-Fi
|
||||||
|
void connect(const char *hostname) {
|
||||||
|
|
||||||
|
sensor_console::defineCommand("wifi_scan", scanNetworks, F("(Scans available WiFi networks)"));
|
||||||
|
sensor_console::defineCommand("local_ip", showLocalIp, F("(Displays local IP and current SSID)"));
|
||||||
|
|
||||||
//NOTE: WiFi Multi could allow multiple SSID and passwords.
|
//NOTE: WiFi Multi could allow multiple SSID and passwords.
|
||||||
WiFi.persistent(false); // Don't write user & password to Flash.
|
WiFi.persistent(false); // Don't write user & password to Flash.
|
||||||
WiFi.mode(WIFI_STA); // Set ESP to be a WiFi-client only
|
WiFi.mode(WIFI_STA); // Set ESP to be a WiFi-client only
|
||||||
#if defined(ESP8266)
|
#if defined(ESP8266)
|
||||||
WiFi.hostname(hostname);
|
WiFi.hostname(hostname);
|
||||||
#elif defined(ESP32)
|
#elif defined(ESP32)
|
||||||
WiFi.setHostname(hostname.c_str());
|
WiFi.setHostname(hostname);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
Serial.print(F("WiFi - Connecting to "));
|
Serial.print(F("WiFi - Connecting to "));
|
||||||
|
@ -36,10 +68,13 @@ void WiFiConnect(const String &hostname) {
|
||||||
led_effects::showKITTWheel(color::green);
|
led_effects::showKITTWheel(color::green);
|
||||||
Serial.println();
|
Serial.println();
|
||||||
Serial.print(F("WiFi - Connected! IP address: "));
|
Serial.print(F("WiFi - Connected! IP address: "));
|
||||||
Serial.println(WiFi.localIP());
|
IPAddress address = WiFi.localIP();
|
||||||
|
snprintf(local_ip, sizeof(local_ip), "%d.%d.%d.%d", address[0], address[1], address[2], address[3]);
|
||||||
|
Serial.println(local_ip);
|
||||||
} else {
|
} else {
|
||||||
//TODO: Allow sensor to work as an Access Point, in order to define SSID & password?
|
//TODO: Allow sensor to work as an Access Point, in order to define SSID & password?
|
||||||
led_effects::showKITTWheel(color::red);
|
led_effects::showKITTWheel(color::red);
|
||||||
Serial.println(F("Connection to WiFi failed"));
|
Serial.println(F("Connection to WiFi failed"));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,9 @@
|
||||||
#include "util.h"
|
#include "util.h"
|
||||||
#include "led_effects.h"
|
#include "led_effects.h"
|
||||||
|
|
||||||
void WiFiConnect(const String &hostname);
|
namespace wifi {
|
||||||
|
extern char local_ip[];
|
||||||
|
void connect(const char *hostname);
|
||||||
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
Loading…
Reference in a new issue