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
|
||||
.vscode
|
||||
*.ino.cpp
|
||||
config.h
|
||||
config.hftstuttgart.h
|
||||
.project
|
||||
.cproject
|
||||
.settings
|
||||
|
|
|
@ -2,29 +2,11 @@ image: python:3.6
|
|||
|
||||
stages:
|
||||
- build
|
||||
- release
|
||||
|
||||
before_script:
|
||||
- "pip install -U platformio"
|
||||
- "cp ampel-firmware/config.public.h ampel-firmware/config.h"
|
||||
|
||||
esp8266:
|
||||
job:
|
||||
stage: build
|
||||
script:
|
||||
- "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
|
||||
|
||||
script: "platformio run"
|
||||
|
|
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* 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.
|
||||
|
||||
|
@ -12,20 +12,20 @@ The *CO<sub>2</sub> Ampel* can:
|
|||
|
||||
* Display CO2 concentration on LED ring.
|
||||
* Allow calibration.
|
||||
* Get current time over NTP
|
||||
* Send data over MQTT.
|
||||
* Send data over LoRaWAN.
|
||||
* Get current time over [NTP](https://en.wikipedia.org/wiki/Network_Time_Protocol)
|
||||
* Send data over [MQTT](https://en.wikipedia.org/wiki/MQTT).
|
||||
* Send data over [LoRaWAN](https://en.wikipedia.org/wiki/LoRa#LoRaWAN).
|
||||
* Display measurements and configuration on a small website.
|
||||
* 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
|
||||
|
||||
* [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"
|
||||
* [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.
|
||||
Or our Wiki [MakerLab Wiki CO2 Ampel](https://wiki.makerlab-murnau.de/books/co2-ampel).
|
||||
See the [documentation](https://transfer.hft-stuttgart.de/gitlab/co2ampel/ampel-documentation) for more info.
|
||||
|
||||
## Software Requirements
|
||||
|
||||
|
@ -68,27 +68,49 @@ make upload board=esp32 && make monitor # For ESP32
|
|||
* *Upload*
|
||||
* *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
|
||||
|
||||
* Eric Duminil
|
||||
* Robert Otto
|
||||
* Myriam Guedey
|
||||
* Tobias Gabriel Erhart
|
||||
* Jonas Stave
|
||||
|
||||
Hochschule für Technik Stuttgart
|
||||
|
||||
## Modifications by
|
||||
|
||||
* Jens Noack
|
||||
|
||||
MakerLab Murnau e.V.
|
||||
* Eric Duminil (HfT Stuttgart)
|
||||
* Robert Otto (HfT Stuttgart)
|
||||
* Myriam Guedey (HfT Stuttgart)
|
||||
* Tobias Gabriel Erhart (HfT Stuttgart)
|
||||
* Jonas Stave (HfT Stuttgart)
|
||||
* Michael Käppler
|
||||
|
||||
## Contributing
|
||||
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
|
||||
|
||||
* Merge requests are welcome, and should be based on the `develop` branch.
|
||||
* The `develop` branch gets merged into the `master` once it has been sufficiently tested.
|
||||
* For major changes, please open an issue first to discuss what you would like to change.
|
||||
|
||||
## License
|
||||
|
||||
Copyright © 2020, [HfT Stuttgart](https://www.hft-stuttgart.de/)
|
||||
|
||||
Copyright © 2021, [HfT Stuttgart](https://www.hft-stuttgart.de/)
|
||||
[GPLv3](https://choosealicense.com/licenses/gpl-3.0/)
|
||||
|
|
|
@ -20,6 +20,12 @@
|
|||
# ifdef AMPEL_HTTP
|
||||
# include "web_server.h"
|
||||
# endif
|
||||
# if defined(ESP8266)
|
||||
//allows sensor to be seen as SENSOR_ID.local, from the local network. For example : espd03cc5.local
|
||||
# include <ESP8266mDNS.h>
|
||||
# elif defined(ESP32)
|
||||
# include <ESPmDNS.h>
|
||||
# endif
|
||||
#endif
|
||||
|
||||
#ifdef AMPEL_LORAWAN
|
||||
|
@ -27,17 +33,8 @@
|
|||
#endif
|
||||
|
||||
#include "util.h"
|
||||
#include "sensor_console.h"
|
||||
#include "co2_sensor.h"
|
||||
#include "led_effects.h"
|
||||
|
||||
#if defined(ESP8266)
|
||||
//allows sensor to be seen as SENSOR_ID.local, from the local network. For example : espd03cc5.local
|
||||
# include <ESP8266mDNS.h>
|
||||
#elif defined(ESP32)
|
||||
# include <ESPmDNS.h>
|
||||
#endif
|
||||
|
||||
void keepServicesAlive();
|
||||
void checkFlashButton();
|
||||
|
||||
#endif
|
||||
|
|
|
@ -43,6 +43,7 @@
|
|||
* Myriam Guedey
|
||||
* Tobias Gabriel Erhart
|
||||
* Jonas Stave
|
||||
* Michael Käppler
|
||||
*/
|
||||
|
||||
/*****************************************************************
|
||||
|
@ -61,22 +62,31 @@
|
|||
void setup() {
|
||||
led_effects::setupOnBoardLED();
|
||||
led_effects::onBoardLEDOff();
|
||||
|
||||
|
||||
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::showRainbowWheel(5000);
|
||||
sensor::initialize();
|
||||
|
||||
Serial.print(F("Sensor ID: "));
|
||||
Serial.println(SENSOR_ID);
|
||||
Serial.print(F("Board : "));
|
||||
Serial.println(BOARD);
|
||||
#ifdef AMPEL_CSV
|
||||
csv_writer::initialize(ampel.sensorId);
|
||||
#endif
|
||||
|
||||
#ifdef AMPEL_WIFI
|
||||
WiFiConnect(SENSOR_ID);
|
||||
wifi::connect(ampel.sensorId);
|
||||
|
||||
Serial.print(F("WiFi - Status: "));
|
||||
Serial.println(WiFi.status());
|
||||
|
@ -88,7 +98,7 @@ void setup() {
|
|||
|
||||
ntp::initialize();
|
||||
|
||||
if (MDNS.begin(SENSOR_ID.c_str())) { // Start the mDNS responder for SENSOR_ID.local
|
||||
if (MDNS.begin(ampel.sensorId)) { // Start the mDNS responder for SENSOR_ID.local
|
||||
MDNS.addService("http", "tcp", 80);
|
||||
Serial.println(F("mDNS responder started"));
|
||||
} else {
|
||||
|
@ -96,24 +106,26 @@ void setup() {
|
|||
}
|
||||
|
||||
# ifdef AMPEL_MQTT
|
||||
mqtt::initialize("CO2sensors/" + SENSOR_ID);
|
||||
mqtt::initialize(ampel.sensorId);
|
||||
# endif
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef AMPEL_CSV
|
||||
csv_writer::initialize();
|
||||
#endif
|
||||
|
||||
#if defined(AMPEL_LORAWAN) && defined(ESP32)
|
||||
lorawan::initialize();
|
||||
#endif
|
||||
}
|
||||
|
||||
/*****************************************************************
|
||||
* Helper functions *
|
||||
*****************************************************************/
|
||||
void keepServicesAlive();
|
||||
void checkFlashButton();
|
||||
void checkSerialInput();
|
||||
|
||||
/*****************************************************************
|
||||
* Main loop *
|
||||
*****************************************************************/
|
||||
|
||||
void loop() {
|
||||
#if defined(AMPEL_LORAWAN) && defined(ESP32)
|
||||
//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.
|
||||
checkFlashButton();
|
||||
|
||||
checkSerialInput();
|
||||
|
||||
if (sensor::processData()) {
|
||||
#ifdef AMPEL_CSV
|
||||
csv_writer::logIfTimeHasCome(sensor::timestamp, sensor::co2, sensor::temperature, sensor::humidity);
|
||||
|
@ -148,11 +162,17 @@ void loop() {
|
|||
}
|
||||
|
||||
uint32_t duration = millis() - t0;
|
||||
if (duration > max_loop_duration) {
|
||||
max_loop_duration = duration;
|
||||
if (duration > ampel.max_loop_duration) {
|
||||
ampel.max_loop_duration = duration;
|
||||
Serial.print(F("Debug - Max loop duration : "));
|
||||
Serial.print(max_loop_duration);
|
||||
Serial.println(" ms.");
|
||||
Serial.print(ampel.max_loop_duration);
|
||||
Serial.println(F(" ms."));
|
||||
}
|
||||
}
|
||||
|
||||
void checkSerialInput() {
|
||||
while (Serial.available() > 0) {
|
||||
sensor_console::processSerialInput(Serial.read());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -163,16 +183,18 @@ void loop() {
|
|||
* If long press, start calibration process.
|
||||
*/
|
||||
void checkFlashButton() {
|
||||
if (!digitalRead(buttonPin)) { // Button has been pressed
|
||||
if (!digitalRead(0)) { // Button has been pressed
|
||||
led_effects::onBoardLEDOn();
|
||||
delay(300);
|
||||
if (digitalRead(buttonPin)) {
|
||||
if (digitalRead(0)) {
|
||||
Serial.println(F("Flash has been pressed for a short time. Should toggle night mode."));
|
||||
led_effects::toggleNightMode();
|
||||
} else {
|
||||
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();
|
||||
led_effects::showKITTWheel(color::red, 2);
|
||||
}
|
||||
}
|
||||
led_effects::onBoardLEDOff();
|
||||
|
|
|
@ -1,10 +1,17 @@
|
|||
#include "co2_sensor.h"
|
||||
|
||||
namespace config {
|
||||
// Values should be defined in config.h
|
||||
uint16_t measurement_timestep = MEASUREMENT_TIMESTEP; // [s] Value between 2 and 1800 (range for SCD30 sensor)
|
||||
// UPPERCASE values should be defined in config.h
|
||||
uint16_t measurement_timestep = MEASUREMENT_TIMESTEP; // [s] Value between 2 and 1800 (range for SCD30 sensor).
|
||||
const uint16_t altitude_above_sea_level = ALTITUDE_ABOVE_SEA_LEVEL; // [m]
|
||||
uint16_t co2_calibration_level = ATMOSPHERIC_CO2_CONCENTRATION; // [ppm]
|
||||
const uint16_t measurement_timestep_bootup = 5; // [s] Measurement timestep during acclimatization.
|
||||
const uint8_t max_deviation_during_bootup = 20; // [%]
|
||||
const int8_t max_deviation_during_calibration = 30; // [ppm]
|
||||
const int16_t timestep_during_calibration = 10; // [s] WARNING: Measurements can be unreliable for timesteps shorter than 10s.
|
||||
const int8_t stable_measurements_before_calibration = 120 / timestep_during_calibration; // [-] Stable measurements during at least 2 minutes.
|
||||
const uint16_t co2_alert_threshold = 2000; // [ppm] Display a flashing led ring, if concentration exceeds this value
|
||||
|
||||
#ifdef TEMPERATURE_OFFSET
|
||||
// Residual heat from CO2 sensor seems to be high enough to change the temperature reading. How much should it be offset?
|
||||
// NOTE: Sign isn't relevant. The returned temperature will always be shifted down.
|
||||
|
@ -12,23 +19,46 @@ namespace config {
|
|||
#else
|
||||
const float temperature_offset = -3.0; // [K] Temperature measured by sensor is usually at least 3K too high.
|
||||
#endif
|
||||
const bool auto_calibrate_sensor = AUTO_CALIBRATE_SENSOR; // [true / false]
|
||||
bool auto_calibrate_sensor = AUTO_CALIBRATE_SENSOR; // [true / false]
|
||||
const bool debug_sensor_states = false; // If true, log state transitions over serial console
|
||||
}
|
||||
|
||||
namespace sensor {
|
||||
SCD30 scd30;
|
||||
int16_t co2 = 0;
|
||||
uint16_t co2 = 0;
|
||||
float temperature = 0;
|
||||
float humidity = 0;
|
||||
String timestamp = "";
|
||||
char timestamp[23];
|
||||
int16_t stable_measurements = 0;
|
||||
uint32_t waiting_color = color::blue;
|
||||
bool should_calibrate = false;
|
||||
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() {
|
||||
#if defined(ESP8266)
|
||||
Wire.begin(12, 14); // ESP8266 - SDA: D6, SCL: D5;
|
||||
Wire.begin(12, 14); // ESP8266 - D6, D5;
|
||||
#endif
|
||||
#if defined(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)
|
||||
*/
|
||||
#endif
|
||||
Serial.println();
|
||||
scd30.enableDebugging(); // Prints firmware version in the console.
|
||||
|
||||
// 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);
|
||||
}
|
||||
if (!scd30.begin(config::auto_calibrate_sensor)) {
|
||||
Serial.println(F("ERROR - CO2 sensor not detected. Please check wiring!"));
|
||||
led_effects::showKITTWheel(color::red, 30);
|
||||
ESP.restart();
|
||||
}
|
||||
|
||||
// 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);
|
||||
Serial.println(" s.");
|
||||
scd30.setMeasurementInterval(config::measurement_timestep); // [s]
|
||||
// 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(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.
|
||||
delay(100);
|
||||
|
||||
Serial.print(F("Temperature offset is : -"));
|
||||
Serial.print(scd30.getTemperatureOffset());
|
||||
Serial.println(" K");
|
||||
Serial.println(F(" K"));
|
||||
|
||||
Serial.print(F("Auto-calibration is "));
|
||||
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?
|
||||
void checkTimerDeviation() {
|
||||
static int32_t previous_measurement_at = 0;
|
||||
int32_t now = millis();
|
||||
Serial.print("Measurement time offset : ");
|
||||
Serial.print(now - previous_measurement_at - config::measurement_timestep * 1000);
|
||||
Serial.println(" ms.");
|
||||
previous_measurement_at = now;
|
||||
bool hasSensorSettled() {
|
||||
static uint16_t last_co2 = 0;
|
||||
uint16_t delta;
|
||||
delta = abs(co2 - last_co2);
|
||||
last_co2 = co2;
|
||||
// We assume the sensor has acclimated to the environment if measurements
|
||||
// change less than a specified percentage of the current value.
|
||||
return (co2 > 0 && delta < ((uint32_t) co2 * config::max_deviation_during_bootup / 100));
|
||||
}
|
||||
|
||||
void countStableMeasurements() {
|
||||
bool enoughStableMeasurements() {
|
||||
static int16_t previous_co2 = 0;
|
||||
if (co2 > (previous_co2 - 30) && co2 < (previous_co2 + 30)) {
|
||||
if (co2 > (previous_co2 - config::max_deviation_during_calibration)
|
||||
&& co2 < (previous_co2 + config::max_deviation_during_calibration)) {
|
||||
stable_measurements++;
|
||||
Serial.print(F("Number of stable measurements : "));
|
||||
Serial.println(stable_measurements);
|
||||
waiting_color = color::green;
|
||||
Serial.print(stable_measurements);
|
||||
Serial.print(F(" / "));
|
||||
Serial.println(config::stable_measurements_before_calibration);
|
||||
switchState(PREPARE_CALIBRATION_STABLE);
|
||||
} else {
|
||||
stable_measurements = 0;
|
||||
waiting_color = color::red;
|
||||
switchState(PREPARE_CALIBRATION_UNSTABLE);
|
||||
}
|
||||
previous_co2 = co2;
|
||||
return (stable_measurements == config::stable_measurements_before_calibration);
|
||||
}
|
||||
|
||||
void startCalibrationProcess() {
|
||||
/** From the sensor documentation:
|
||||
* For best results, the sensor has to be run in a stable environment in continuous mode at
|
||||
* a measurement rate of 2s for at least two minutes before applying the FRC command and sending the reference value.
|
||||
* Before applying FRC, SCD30 needs to be operated for 2 minutes with the desired measurement period in continuous mode.
|
||||
*/
|
||||
Serial.println(F("Setting SCD30 timestep to 2s, prior to calibration."));
|
||||
scd30.setMeasurementInterval(MEASUREMENT_TIMESTEP); // [s] The change will only take effect after next measurement.
|
||||
Serial.print(F("Setting SCD30 timestep to "));
|
||||
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("It could take a very long time."));
|
||||
should_calibrate = true;
|
||||
time_calaibration_started = millis();
|
||||
switchState(PREPARE_CALIBRATION_UNSTABLE);
|
||||
}
|
||||
|
||||
void calibrateAndRestart() {
|
||||
void calibrate() {
|
||||
Serial.print(F("Calibrating SCD30 now..."));
|
||||
scd30.setAltitudeCompensation(config::altitude_above_sea_level);
|
||||
scd30.setForcedRecalibrationFactor(config::co2_calibration_level);
|
||||
Serial.println(F(" Done!"));
|
||||
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() {
|
||||
|
@ -127,29 +179,86 @@ namespace sensor {
|
|||
Serial.println(humidity, 1);
|
||||
}
|
||||
|
||||
void displayCO2OnLedRing() {
|
||||
int16_t co2_int = co2;
|
||||
if (co2_int < CALIBRATE_LEVEL) {
|
||||
// Sensor should be calibrated.
|
||||
led_effects::showWaitingLED(color::magenta);
|
||||
void switchState(state new_state) {
|
||||
if (new_state == current_state) {
|
||||
return;
|
||||
}
|
||||
if(co2_int < 400) {
|
||||
co2_int = 400;
|
||||
if (config::debug_sensor_states) {
|
||||
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).
|
||||
* 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) {
|
||||
led_effects::displayCO2color(co2_int);
|
||||
led_effects::breathe(co2_int);
|
||||
if (co2 < config::co2_alert_threshold) {
|
||||
led_effects::displayCO2color(co2);
|
||||
delay(100);
|
||||
} else {
|
||||
// >= 2000: entire ring blinks red
|
||||
// Display a flashing led ring, if concentration exceeds a specific value
|
||||
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.
|
||||
* 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();
|
||||
|
||||
if (freshData) {
|
||||
// checkTimerDeviation();
|
||||
timestamp = ntp::getLocalTime();
|
||||
ntp::getLocalTime(timestamp);
|
||||
co2 = scd30.getCO2();
|
||||
temperature = scd30.getTemperature();
|
||||
humidity = scd30.getHumidity();
|
||||
}
|
||||
|
||||
//NOTE: Data is available, but it's sometimes erroneous: the sensor outputs zero ppm but non-zero temperature and non-zero humidity.
|
||||
if (co2 <= 0) {
|
||||
// No measurement yet. Waiting.
|
||||
led_effects::showWaitingLED(color::blue);
|
||||
return false;
|
||||
}
|
||||
switchStateForCurrentPPM();
|
||||
|
||||
/**
|
||||
* Fresh data. Log it and send it if needed.
|
||||
*/
|
||||
if (freshData) {
|
||||
if (should_calibrate) {
|
||||
if(millis() - time_calaibration_started > 60000)
|
||||
{
|
||||
countStableMeasurements();
|
||||
}
|
||||
}
|
||||
// Log every time fresh data is available.
|
||||
logToSerial();
|
||||
}
|
||||
|
||||
if (should_calibrate) {
|
||||
if (stable_measurements == 60) {
|
||||
calibrateAndRestart();
|
||||
}
|
||||
led_effects::showWaitingLED(waiting_color);
|
||||
return false;
|
||||
}
|
||||
showState();
|
||||
|
||||
displayCO2OnLedRing();
|
||||
return freshData;
|
||||
// Report data for further processing only if the data is reliable
|
||||
// (state 'READY') or manual calibration is necessary (state 'NEEDS_CALIBRATION').
|
||||
return freshData && (current_state == READY || current_state == NEEDS_CALIBRATION);
|
||||
}
|
||||
|
||||
/*****************************************************************
|
||||
* 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 "led_effects.h"
|
||||
#include "util.h"
|
||||
#include "sensor_console.h"
|
||||
#include <Wire.h>
|
||||
|
||||
namespace config {
|
||||
extern uint16_t measurement_timestep; // [s] Value between 2 and 1800 (range for SCD30 sensor)
|
||||
extern const bool auto_calibrate_sensor; // [true / false]
|
||||
extern bool auto_calibrate_sensor; // [true / false]
|
||||
extern uint16_t co2_calibration_level; // [ppm]
|
||||
extern const float temperature_offset; // [K] Sign isn't relevant.
|
||||
}
|
||||
|
||||
namespace sensor {
|
||||
extern SCD30 scd30;
|
||||
extern int16_t co2;
|
||||
extern uint16_t co2;
|
||||
extern float temperature;
|
||||
extern float humidity;
|
||||
extern String timestamp;
|
||||
extern char timestamp[];
|
||||
|
||||
void initialize();
|
||||
bool processData();
|
||||
void startCalibrationProcess();
|
||||
|
||||
void setCO2forDebugging(int32_t fakeCo2);
|
||||
void setTimer(int32_t timestep);
|
||||
void calibrateSensorToSpecificPPM(int32_t calibrationLevel);
|
||||
void calibrateSensorRightNow(int32_t calibrationLevel);
|
||||
void setAutoCalibration(int32_t autoCalibration);
|
||||
void resetSCD();
|
||||
}
|
||||
#endif
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
#ifndef CONFIG_H_INCLUDED
|
||||
# define CONFIG_H_INCLUDED
|
||||
|
||||
#define CONFIG_H_INCLUDED
|
||||
|
||||
#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.
|
||||
|
@ -14,18 +13,18 @@
|
|||
*/
|
||||
|
||||
// Comment or remove those lines if you want to disable the corresponding services
|
||||
// # define AMPEL_WIFI // Should ESP connect to WiFi? It allows the Ampel to get time from an NTP server.
|
||||
// # define AMPEL_HTTP // Should HTTP web server be started? (AMPEL_WIFI should be enabled too)
|
||||
//# define AMPEL_WIFI // Should ESP connect to WiFi? It allows the Ampel to get time from an NTP server.
|
||||
//# define AMPEL_HTTP // Should HTTP web server be started? (AMPEL_WIFI should be enabled too)
|
||||
//# define AMPEL_MQTT // Should data be sent over MQTT? (AMPEL_WIFI should be enabled too)
|
||||
// # define AMPEL_CSV // Should data be logged as CSV, on the ESP flash memory?
|
||||
//# define AMPEL_CSV // Should data be logged as CSV, on the ESP flash memory?
|
||||
// # define AMPEL_LORAWAN // Should data be sent over LoRaWAN? (Requires ESP32 + LoRa modem, and "MCCI LoRaWAN LMIC library")
|
||||
|
||||
/**
|
||||
* WIFI
|
||||
*/
|
||||
|
||||
# define WIFI_SSID ""
|
||||
# define WIFI_PASSWORD ""
|
||||
# define WIFI_SSID "MY_SSID"
|
||||
# define WIFI_PASSWORD "P4SSW0RD"
|
||||
# define WIFI_TIMEOUT 30 // [s]
|
||||
|
||||
/**
|
||||
|
@ -33,8 +32,10 @@
|
|||
*/
|
||||
|
||||
// How often should measurement be performed, and displayed?
|
||||
//NOTE: SCD30 timer does not seem to be very precise. Variations may occur.
|
||||
# define MEASUREMENT_TIMESTEP 2 // [s] Value between 2 and 1800 (range for SCD30 sensor)
|
||||
//WARNING: On some sensors, measurements become very unreliable when timestep is set to 2s.
|
||||
//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 ?
|
||||
// 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?
|
||||
// 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
|
||||
// Used for CO2 calibration
|
||||
// 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.
|
||||
// Used for CO2 calibration
|
||||
// here : measured concentration in Stuttgart
|
||||
# define ATMOSPHERIC_CO2_CONCENTRATION 430 // [ppm]
|
||||
# define ATMOSPHERIC_CO2_CONCENTRATION 425 // [ppm]
|
||||
|
||||
// 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.
|
||||
# define AUTO_CALIBRATE_SENSOR false // [true / false]
|
||||
# define AUTO_CALIBRATE_SENSOR true // [true / false]
|
||||
|
||||
/**
|
||||
* LEDs
|
||||
*/
|
||||
|
||||
// LED brightness, which can vary between min and max brightness ("LED breathing")
|
||||
// max_brightness should be between 0 and 255.
|
||||
// min_brightness should be between 0 and max_brightness
|
||||
// MAX_BRIGHTNESS must be defined, and should be between 0 and 255.
|
||||
# define MAX_BRIGHTNESS 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
|
||||
// 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
|
||||
|
@ -76,8 +80,8 @@
|
|||
*/
|
||||
|
||||
// Define empty strings in order to disable authentication, or remove the constants altogether.
|
||||
# define HTTP_USER ""
|
||||
# define HTTP_PASSWORD ""
|
||||
# define HTTP_USER "co2ampel"
|
||||
# define HTTP_PASSWORD "my_password"
|
||||
|
||||
/**
|
||||
* MQTT
|
||||
|
@ -106,11 +110,11 @@
|
|||
*/
|
||||
# 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
|
||||
// Set to 0 if you want to send values after each measurement
|
||||
// # define MQTT_SENDING_INTERVAL MEASUREMENT_TIMESTEP * 5 // [s]
|
||||
# define MQTT_SENDING_INTERVAL 60 // [s]
|
||||
# define MQTT_SENDING_INTERVAL MEASUREMENT_TIMESTEP * 5 // [s]
|
||||
//# define MQTT_SENDING_INTERVAL 60 // [s]
|
||||
# define MQTT_SERVER "test.mosquitto.org" // MQTT server URL or IP address
|
||||
# define MQTT_PORT 8883
|
||||
# define MQTT_USER ""
|
||||
|
@ -149,7 +153,7 @@
|
|||
*/
|
||||
|
||||
# define NTP_SERVER "pool.ntp.org"
|
||||
# define UTC_OFFSET_IN_SECONDS 3600 // [s] 3600 for UTC+1
|
||||
# define UTC_OFFSET_IN_SECONDS 7200 // [s] 3600 for UTC+1, 7200 for UTC+1 and daylight saving time
|
||||
|
||||
/**
|
||||
* Others
|
|
@ -1,14 +1,12 @@
|
|||
#include "csv_writer.h"
|
||||
|
||||
//TODO: Allow CSV download via USB Serial, when requested (e.g. via a Python script)
|
||||
|
||||
namespace config {
|
||||
// Values should be defined in config.h
|
||||
uint16_t csv_interval = CSV_INTERVAL; // [s]
|
||||
}
|
||||
namespace csv_writer {
|
||||
unsigned long last_written_at = 0;
|
||||
String last_successful_write = "";
|
||||
char last_successful_write[23];
|
||||
|
||||
#if defined(ESP8266)
|
||||
/**
|
||||
|
@ -80,13 +78,16 @@ namespace csv_writer {
|
|||
}
|
||||
#endif
|
||||
|
||||
const String filename = "/" + SENSOR_ID + ".csv";
|
||||
char filename[15]; // "/ESPxxxxxx.csv\0"
|
||||
|
||||
int getAvailableSpace() {
|
||||
return getTotalSpace() - getUsedSpace();
|
||||
}
|
||||
|
||||
void initialize() {
|
||||
void initialize(const char *sensorId) {
|
||||
snprintf(filename, sizeof(filename), "/%s.csv", sensorId);
|
||||
|
||||
Serial.println();
|
||||
Serial.print(F("Initializing FS..."));
|
||||
if (mountFS()) {
|
||||
Serial.println(F("done."));
|
||||
|
@ -113,9 +114,13 @@ namespace csv_writer {
|
|||
Serial.println();
|
||||
|
||||
// Open dir folder
|
||||
Serial.println("Filesystem content:");
|
||||
Serial.println(F("Filesystem content:"));
|
||||
showFilesystemContent();
|
||||
Serial.println();
|
||||
|
||||
sensor_console::defineIntCommand("csv", setCSVinterval, F("60 (Sets CSV writing interval, in s)"));
|
||||
sensor_console::defineCommand("format_filesystem", formatFilesystem, F("(Deletes the whole filesystem)"));
|
||||
sensor_console::defineCommand("show_csv", showCSVContent, F("(Displays the complete CSV file on Serial)"));
|
||||
}
|
||||
|
||||
File openOrCreate() {
|
||||
|
@ -130,11 +135,11 @@ namespace csv_writer {
|
|||
return csv_file;
|
||||
}
|
||||
|
||||
void log(const String &timeStamp, const int16_t &co2, const float &temperature, const float &humidity) {
|
||||
void log(const char *timestamp, const int16_t &co2, const float &temperature, const float &humidity) {
|
||||
led_effects::onBoardLEDOn();
|
||||
File csv_file = openOrCreate();
|
||||
char csv_line[42];
|
||||
snprintf(csv_line, sizeof(csv_line), "%s;%d;%.1f;%.1f\r\n", timeStamp.c_str(), co2, temperature, humidity);
|
||||
snprintf(csv_line, sizeof(csv_line), "%s;%d;%.1f;%.1f\r\n", timestamp, co2, temperature, humidity);
|
||||
if (csv_file) {
|
||||
size_t written_bytes = csv_file.print(csv_line);
|
||||
csv_file.close();
|
||||
|
@ -143,7 +148,7 @@ namespace csv_writer {
|
|||
} else {
|
||||
Serial.print(F("CSV - Wrote : "));
|
||||
Serial.print(csv_line);
|
||||
last_successful_write = ntp::getLocalTime();
|
||||
ntp::getLocalTime(last_successful_write);
|
||||
}
|
||||
updateFsInfo();
|
||||
delay(50);
|
||||
|
@ -154,11 +159,42 @@ namespace csv_writer {
|
|||
led_effects::onBoardLEDOff();
|
||||
}
|
||||
|
||||
void logIfTimeHasCome(const String &timeStamp, const int16_t &co2, const float &temperature, const float &humidity) {
|
||||
void logIfTimeHasCome(const char *timeStamp, const int16_t &co2, const float &temperature, const float &humidity) {
|
||||
unsigned long now = seconds();
|
||||
if (now - last_written_at > config::csv_interval) {
|
||||
last_written_at = now;
|
||||
log(timeStamp, co2, temperature, humidity);
|
||||
}
|
||||
}
|
||||
|
||||
/*****************************************************************
|
||||
* Callbacks for sensor commands *
|
||||
*****************************************************************/
|
||||
void setCSVinterval(int32_t csv_interval) {
|
||||
config::csv_interval = csv_interval;
|
||||
Serial.print(F("Setting CSV Interval to : "));
|
||||
Serial.print(config::csv_interval);
|
||||
Serial.println("s.");
|
||||
led_effects::showKITTWheel(color::green, 1);
|
||||
}
|
||||
|
||||
void showCSVContent() {
|
||||
Serial.print(F("### "));
|
||||
Serial.print(filename);
|
||||
Serial.println(F(" ###"));
|
||||
File csv_file;
|
||||
if (FS_LIB.exists(filename)) {
|
||||
csv_file = FS_LIB.open(filename, "r");
|
||||
while (csv_file.available()) {
|
||||
Serial.write(csv_file.read());
|
||||
}
|
||||
csv_file.close();
|
||||
}
|
||||
Serial.println(F("######################"));
|
||||
}
|
||||
|
||||
void formatFilesystem() {
|
||||
FS_LIB.format();
|
||||
led_effects::showKITTWheel(color::blue, 2);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,16 +14,21 @@
|
|||
#include "config.h"
|
||||
#include "util.h"
|
||||
#include "led_effects.h"
|
||||
#include "sensor_console.h"
|
||||
|
||||
namespace config {
|
||||
extern uint16_t csv_interval; // [s]
|
||||
}
|
||||
namespace csv_writer {
|
||||
extern String last_successful_write;
|
||||
void initialize();
|
||||
void logIfTimeHasCome(const String &timeStamp, const int16_t &co2, const float &temperature, const float &humidity);
|
||||
extern char last_successful_write[];
|
||||
void initialize(const char *sensorId);
|
||||
void logIfTimeHasCome(const char *timestamp, const int16_t &co2, const float &temperature, const float &humidity);
|
||||
int getAvailableSpace();
|
||||
extern const String filename;
|
||||
extern char filename[];
|
||||
|
||||
void setCSVinterval(int32_t csv_interval);
|
||||
void showCSVContent();
|
||||
void formatFilesystem();
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
|
@ -4,17 +4,36 @@
|
|||
*****************************************************************/
|
||||
namespace config {
|
||||
const uint8_t max_brightness = MAX_BRIGHTNESS;
|
||||
#if defined(MIN_BRIGHTNESS)
|
||||
const uint8_t min_brightness = MIN_BRIGHTNESS;
|
||||
const int kitt_tail = 3; // How many dimmer LEDs follow in K.I.T.T. wheel
|
||||
}
|
||||
|
||||
/*****************************************************************
|
||||
* Configuration (calculated from above values) *
|
||||
*****************************************************************/
|
||||
namespace config //NOTE: Use a class instead? NightMode could then be another state.
|
||||
{
|
||||
#else
|
||||
const uint8_t min_brightness = MAX_BRIGHTNESS;
|
||||
#endif
|
||||
const uint8_t brightness_amplitude = config::max_brightness - config::min_brightness;
|
||||
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)
|
||||
|
@ -25,20 +44,7 @@ const int NEOPIXELS_PIN = 5;
|
|||
const int NEOPIXELS_PIN = 23;
|
||||
#endif
|
||||
|
||||
const int NUMPIXELS = 12;
|
||||
//NOTE: One value has been prepended, to make calculations easier and avoid out of bounds index.
|
||||
const uint16_t CO2_TICKS[NUMPIXELS + 1] = { 0, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000, 2200 }; // [ppm]
|
||||
// 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);
|
||||
Adafruit_NeoPixel pixels(config::led_count, NEOPIXELS_PIN, NEO_GRB + NEO_KHZ800);
|
||||
|
||||
namespace led_effects {
|
||||
//On-board LED on D4, aka GPIO02
|
||||
|
@ -71,10 +77,19 @@ namespace led_effects {
|
|||
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() {
|
||||
pixels.begin();
|
||||
pixels.setBrightness(config::max_brightness);
|
||||
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() {
|
||||
|
@ -89,14 +104,15 @@ namespace led_effects {
|
|||
|
||||
//NOTE: basically one iteration of KITT wheel
|
||||
void showWaitingLED(uint32_t color) {
|
||||
using namespace config;
|
||||
delay(80);
|
||||
if (config::night_mode) {
|
||||
if (night_mode) {
|
||||
return;
|
||||
}
|
||||
static uint16_t kitt_offset = 0;
|
||||
pixels.clear();
|
||||
for (int j = config::kitt_tail; j >= 0; j--) {
|
||||
int ledNumber = abs((kitt_offset - j + NUMPIXELS) % (2 * NUMPIXELS) - NUMPIXELS) % NUMPIXELS; // Triangular function
|
||||
for (int j = kitt_tail; j >= 0; j--) {
|
||||
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.show();
|
||||
|
@ -108,7 +124,7 @@ namespace led_effects {
|
|||
// Takes approximately 1s for each direction.
|
||||
void showKITTWheel(uint32_t color, uint16_t duration_s) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
*/
|
||||
uint8_t getLedBrightness(uint16_t co2, int ledId) {
|
||||
if (co2 >= CO2_TICKS[ledId + 1]) {
|
||||
if (co2 >= config::co2_ticks[ledId + 1]) {
|
||||
return 255;
|
||||
} 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.
|
||||
return 27; // Brightness isn't linear, so 27 / 255 looks much brighter than 10%
|
||||
} 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.
|
||||
*/
|
||||
|
@ -139,24 +166,28 @@ namespace led_effects {
|
|||
return;
|
||||
}
|
||||
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);
|
||||
pixels.setPixelColor(ledId, pixels.ColorHSV(LED_HUES[ledId], 255, brightness));
|
||||
pixels.setPixelColor(ledId, pixels.ColorHSV(config::led_hues[ledId], 255, brightness));
|
||||
}
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
static uint16_t wheel_offset = 0;
|
||||
static uint16_t sine_offset = 0;
|
||||
unsigned long t0 = millis();
|
||||
pixels.setBrightness(config::max_brightness);
|
||||
while (millis() - t0 < duration_ms) {
|
||||
for (int i = 0; i < NUMPIXELS; i++) {
|
||||
pixels.setPixelColor(i, pixels.ColorHSV(i * 65535 / NUMPIXELS + wheel_offset));
|
||||
wheel_offset += hue_increment;
|
||||
for (int i = 0; i < config::led_count; i++) {
|
||||
pixels.setPixelColor(i, pixels.ColorHSV(i * 65535 / config::led_count + wheel_offset));
|
||||
wheel_offset += (pixels.sine8(sine_offset++ / 50) - 127) / 2;
|
||||
}
|
||||
pixels.show();
|
||||
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.
|
||||
* Can be used for calibration, e.g. when countdown is 0. Does not work in night mode.
|
||||
* Displays a complete blue circle, and starts removing LEDs one by one.
|
||||
* 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) {
|
||||
Serial.println(F("Night mode. Not doing anything."));
|
||||
delay(1000); // Wait for a while, to avoid coming back to this function too many times when button is pressed.
|
||||
return 1;
|
||||
return false;
|
||||
}
|
||||
pixels.fill(color::blue);
|
||||
pixels.show();
|
||||
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.show();
|
||||
Serial.println(countdown);
|
||||
delay(500);
|
||||
}
|
||||
return countdown;
|
||||
return countdown < 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
#define LED_EFFECTS_H_INCLUDED
|
||||
#include <Arduino.h>
|
||||
#include "config.h"
|
||||
#include "sensor_console.h"
|
||||
|
||||
// Adafruit NeoPixel (Arduino library for controlling single-wire-based LED pixels and strip)
|
||||
// https://github.com/adafruit/Adafruit_NeoPixel
|
||||
|
@ -25,11 +26,10 @@ namespace led_effects {
|
|||
|
||||
void setupRing();
|
||||
void redAlert();
|
||||
void breathe(int16_t co2);
|
||||
int countdownToZero();
|
||||
bool countdownToZero();
|
||||
void showWaitingLED(uint32_t color);
|
||||
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);
|
||||
}
|
||||
#endif
|
||||
|
|
|
@ -33,7 +33,7 @@ void os_getDevKey(u1_t *buf) {
|
|||
namespace lorawan {
|
||||
bool waiting_for_confirmation = false;
|
||||
bool connected = false;
|
||||
String last_transmission = "";
|
||||
char last_transmission[23] = "";
|
||||
|
||||
void initialize() {
|
||||
Serial.println(F("Starting LoRaWAN. Frequency plan : " LMIC_FREQUENCY_PLAN " MHz."));
|
||||
|
@ -47,11 +47,12 @@ namespace lorawan {
|
|||
LMIC_reset();
|
||||
// Join, but don't send anything yet.
|
||||
LMIC_startJoining();
|
||||
sensor_console::defineIntCommand("lora", setLoRaInterval, F("300 (Sets LoRaWAN sending interval, in s)"));
|
||||
}
|
||||
|
||||
// Checks if OTAA is connected, or if payload should be sent.
|
||||
// NOTE: while a transaction is in process (i.e. until the TXcomplete event has been received, no blocking code (e.g. delay loops etc.) are allowed, otherwise the LMIC/OS code might miss the event.
|
||||
// If this rule is not followed, a typical symptom is that the first send is ok and all following ones end with the TX not complete failure.
|
||||
// If this rule is not followed, a typical symptom is that the first send is ok and all following ones end with the 'TX not complete' failure.
|
||||
void process() {
|
||||
os_runloop_once();
|
||||
}
|
||||
|
@ -64,8 +65,10 @@ namespace lorawan {
|
|||
}
|
||||
|
||||
void onEvent(ev_t ev) {
|
||||
char current_time[23];
|
||||
ntp::getLocalTime(current_time);
|
||||
Serial.print("LoRa - ");
|
||||
Serial.print(ntp::getLocalTime());
|
||||
Serial.print(current_time);
|
||||
Serial.print(" - ");
|
||||
switch (ev) {
|
||||
case EV_JOINING:
|
||||
|
@ -93,7 +96,7 @@ namespace lorawan {
|
|||
printHex2(artKey[i]);
|
||||
}
|
||||
Serial.println();
|
||||
Serial.print(" NwkSKey: ");
|
||||
Serial.print(F(" NwkSKey: "));
|
||||
for (size_t i = 0; i < sizeof(nwkKey); ++i) {
|
||||
if (i != 0)
|
||||
Serial.print("-");
|
||||
|
@ -112,7 +115,7 @@ namespace lorawan {
|
|||
Serial.println(F("EV_REJOIN_FAILED"));
|
||||
break;
|
||||
case EV_TXCOMPLETE:
|
||||
last_transmission = ntp::getLocalTime();
|
||||
ntp::getLocalTime(last_transmission);
|
||||
Serial.println(F("EV_TXCOMPLETE"));
|
||||
break;
|
||||
case EV_TXSTART:
|
||||
|
@ -189,6 +192,17 @@ namespace lorawan {
|
|||
preparePayload(co2, temperature, humidity);
|
||||
}
|
||||
}
|
||||
|
||||
/*****************************************************************
|
||||
* Callbacks for sensor commands *
|
||||
*****************************************************************/
|
||||
void setLoRaInterval(int32_t sending_interval) {
|
||||
config::lorawan_sending_interval = sending_interval;
|
||||
Serial.print(F("Setting LoRa sending interval to : "));
|
||||
Serial.print(config::lorawan_sending_interval);
|
||||
Serial.println("s.");
|
||||
led_effects::showKITTWheel(color::green, 1);
|
||||
}
|
||||
}
|
||||
|
||||
void onEvent(ev_t ev) {
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
#include <SPI.h>
|
||||
|
||||
#include "led_effects.h"
|
||||
|
||||
#include "sensor_console.h"
|
||||
#include "util.h"
|
||||
|
||||
namespace config {
|
||||
|
@ -39,10 +39,12 @@ namespace config {
|
|||
namespace lorawan {
|
||||
extern bool waiting_for_confirmation;
|
||||
extern bool connected;
|
||||
extern String last_transmission;
|
||||
extern char last_transmission[];
|
||||
void initialize();
|
||||
void process();
|
||||
void preparePayloadIfTimeHasCome(const int16_t &co2, const float &temp, const float &hum);
|
||||
|
||||
void setLoRaInterval(int32_t sending_interval);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace config {
|
||||
// Values should be defined in config.h
|
||||
uint16_t sending_interval = MQTT_SENDING_INTERVAL; // [s]
|
||||
uint16_t mqtt_sending_interval = MQTT_SENDING_INTERVAL; // [s]
|
||||
//INFO: Listen to every CO2 sensor which is connected to the server:
|
||||
// mosquitto_sub -h MQTT_SERVER -t 'CO2sensors/#' -p 443 --capath /etc/ssl/certs/ -u "MQTT_USER" -P "MQTT_PASSWORD" -v
|
||||
const char *mqtt_server = MQTT_SERVER;
|
||||
|
@ -23,31 +23,34 @@ namespace mqtt {
|
|||
unsigned long last_failed_at = 0;
|
||||
bool connected = false;
|
||||
|
||||
String publish_topic;
|
||||
char publish_topic[21]; // e.g. "CO2sensors/ESPxxxxxx\0"
|
||||
const char *json_sensor_format;
|
||||
String last_successful_publish = "";
|
||||
char last_successful_publish[23] = "";
|
||||
|
||||
void initialize(String &topic) {
|
||||
void initialize(const char *sensorId) {
|
||||
json_sensor_format = PSTR("{\"time\":\"%s\", \"co2\":%d, \"temp\":%.1f, \"rh\":%.1f}");
|
||||
publish_topic = topic;
|
||||
#if defined(ESP8266)
|
||||
espClient.setInsecure(); // Sorry, we don't want to flash the sensors every 3 months.
|
||||
#endif
|
||||
// mqttClient.setSocketTimeout(config::mqtt_timeout); //NOTE: somehow doesn't seem to have any effect on connect()
|
||||
snprintf(publish_topic, sizeof(publish_topic), "CO2sensors/%s", sensorId);
|
||||
// The sensor doesn't check the fingerprint of the MQTT broker, because otherwise this fingerprint should be updated
|
||||
// on the sensor every 3 months. The connection can still be encrypted, though:
|
||||
espClient.setInsecure(); // If not available for ESP32, please update Arduino IDE / PlatformIO
|
||||
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()) {
|
||||
led_effects::onBoardLEDOn();
|
||||
Serial.print(F("MQTT - Publishing message ... "));
|
||||
|
||||
char payload[75]; // Should be enough for json...
|
||||
snprintf(payload, sizeof(payload), json_sensor_format, timestamp.c_str(), co2, temperature, humidity);
|
||||
snprintf(payload, sizeof(payload), json_sensor_format, timestamp, co2, temperature, humidity);
|
||||
// Topic is the same as clientID. e.g. 'CO2sensors/ESP3d03da'
|
||||
if (mqttClient.publish(publish_topic.c_str(), payload)) {
|
||||
if (mqttClient.publish(publish_topic, payload)) {
|
||||
Serial.println(F("OK"));
|
||||
last_successful_publish = ntp::getLocalTime();
|
||||
ntp::getLocalTime(last_successful_publish);
|
||||
} else {
|
||||
Serial.println(F("Failed."));
|
||||
}
|
||||
|
@ -55,69 +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
|
||||
*
|
||||
|
@ -132,46 +72,13 @@ namespace mqtt {
|
|||
}
|
||||
led_effects::onBoardLEDOn();
|
||||
Serial.print(F("Message arrived on topic: "));
|
||||
Serial.print(sub_topic);
|
||||
Serial.print(F(". Message: '"));
|
||||
String messageString;
|
||||
Serial.println(sub_topic);
|
||||
char command[length + 1];
|
||||
for (unsigned int i = 0; i < length; i++) {
|
||||
Serial.print((char) message[i]);
|
||||
messageString += (char) message[i];
|
||||
command[i] = message[i];
|
||||
}
|
||||
Serial.println("'.");
|
||||
|
||||
if (messageString.startsWith("co2 ")) {
|
||||
setCO2forDebugging(messageString);
|
||||
} else if (messageString.startsWith("timer ")) {
|
||||
setTimer(messageString);
|
||||
} else if (messageString == "calibrate") {
|
||||
sensor::startCalibrationProcess();
|
||||
} else if (messageString.startsWith("calibrate ")) {
|
||||
calibrateSensorToSpecificPPM(messageString);
|
||||
} else if (messageString.startsWith("mqtt ")) {
|
||||
setMQTTinterval(messageString);
|
||||
} else if (messageString == "publish") {
|
||||
Serial.println(F("Forcing MQTT publish now."));
|
||||
publish(sensor::timestamp, sensor::co2, sensor::temperature, sensor::humidity);
|
||||
#ifdef AMPEL_CSV
|
||||
} else if (messageString.startsWith("csv ")) {
|
||||
setCSVinterval(messageString);
|
||||
} else if (messageString == "format_filesystem") {
|
||||
FS_LIB.format();
|
||||
led_effects::showKITTWheel(color::blue, 2);
|
||||
#endif
|
||||
} else if (messageString == "night_mode") {
|
||||
led_effects::toggleNightMode();
|
||||
} else if (messageString == "local_ip") {
|
||||
sendInfoAboutLocalNetwork();
|
||||
} else if (messageString == "reset") {
|
||||
ESP.restart(); // softer than ESP.reset()
|
||||
} else {
|
||||
led_effects::showKITTWheel(color::red, 1);
|
||||
Serial.println(F("Message not supported. Doing nothing."));
|
||||
}
|
||||
delay(50);
|
||||
command[length] = 0;
|
||||
sensor_console::execute(command);
|
||||
led_effects::onBoardLEDOff();
|
||||
}
|
||||
|
||||
|
@ -188,7 +95,7 @@ namespace mqtt {
|
|||
|
||||
led_effects::onBoardLEDOn();
|
||||
// Wait for connection, at most 15s (default)
|
||||
mqttClient.connect(publish_topic.c_str(), config::mqtt_user, config::mqtt_password);
|
||||
mqttClient.connect(publish_topic, config::mqtt_user, config::mqtt_password);
|
||||
led_effects::onBoardLEDOff();
|
||||
|
||||
connected = mqttClient.connected();
|
||||
|
@ -196,7 +103,7 @@ namespace mqtt {
|
|||
if (connected) {
|
||||
if (config::allow_mqtt_commands) {
|
||||
char control_topic[60]; // Should be enough for "CO2sensors/ESPd03cc5/control"
|
||||
snprintf(control_topic, sizeof(control_topic), "%s/control", publish_topic.c_str());
|
||||
snprintf(control_topic, sizeof(control_topic), "%s/control", publish_topic);
|
||||
mqttClient.subscribe(control_topic);
|
||||
mqttClient.setCallback(controlSensorCallback);
|
||||
}
|
||||
|
@ -212,12 +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
|
||||
unsigned long now = seconds();
|
||||
if (now - last_sent_at > config::sending_interval) {
|
||||
if (now - last_sent_at > config::mqtt_sending_interval) {
|
||||
last_sent_at = now;
|
||||
publish(timeStamp, co2, temp, hum);
|
||||
publish(timestamp, co2, temp, hum);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -229,4 +136,28 @@ namespace mqtt {
|
|||
mqttClient.loop();
|
||||
}
|
||||
|
||||
/*****************************************************************
|
||||
* Callbacks for sensor commands *
|
||||
*****************************************************************/
|
||||
void setMQTTinterval(int32_t sending_interval) {
|
||||
config::mqtt_sending_interval = sending_interval;
|
||||
Serial.print(F("Setting MQTT sending interval to : "));
|
||||
Serial.print(config::mqtt_sending_interval);
|
||||
Serial.println("s.");
|
||||
led_effects::showKITTWheel(color::green, 1);
|
||||
}
|
||||
|
||||
// It can be hard to find the local IP of a sensor if it isn't connected to Serial port, and if mDNS is disabled.
|
||||
// If the sensor can be reach by MQTT, it can answer with info about local_ip and ssid.
|
||||
// The sensor will send the info to "CO2sensors/ESP123456/info".
|
||||
void sendInfoAboutLocalNetwork() {
|
||||
char info_topic[60]; // Should be enough for "CO2sensors/ESP123456/info"
|
||||
snprintf(info_topic, sizeof(info_topic), "%s/info", publish_topic);
|
||||
|
||||
char payload[75]; // Should be enough for info json...
|
||||
const char *json_info_format = PSTR("{\"local_ip\":\"%s\", \"ssid\":\"%s\"}");
|
||||
snprintf(payload, sizeof(payload), json_info_format, wifi::local_ip, WIFI_SSID);
|
||||
|
||||
mqttClient.publish(info_topic, payload);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,20 +4,22 @@
|
|||
#include <Arduino.h>
|
||||
#include "config.h"
|
||||
#include "led_effects.h"
|
||||
#ifdef AMPEL_CSV
|
||||
# include "csv_writer.h"
|
||||
#endif
|
||||
#include "co2_sensor.h"
|
||||
#include "sensor_console.h"
|
||||
#include "src/lib/PubSubClient/src/PubSubClient.h"
|
||||
#include "wifi_util.h"
|
||||
|
||||
namespace config {
|
||||
extern uint16_t sending_interval; // [s]
|
||||
extern uint16_t mqtt_sending_interval; // [s]
|
||||
}
|
||||
|
||||
namespace mqtt {
|
||||
extern String last_successful_publish;
|
||||
extern char last_successful_publish[];
|
||||
extern bool connected;
|
||||
void initialize(String &topic);
|
||||
void initialize(const char *sensorId);
|
||||
void keepConnection();
|
||||
void publishIfTimeHasCome(const String &timeStamp, const int16_t &co2, const float &temp, const float &hum);
|
||||
void publishIfTimeHasCome(const char *timestamp, const int16_t &co2, const float &temp, const float &hum);
|
||||
|
||||
void setMQTTinterval(int32_t sending_interval);
|
||||
void sendInfoAboutLocalNetwork();
|
||||
}
|
||||
#endif
|
||||
|
|
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);
|
||||
}
|
||||
|
||||
String NTPClient::getFormattedTime(unsigned long secs) {
|
||||
void NTPClient::getFormattedTime(char *formatted_time, unsigned long secs) {
|
||||
unsigned long rawTime = secs ? secs : this->getEpochTime();
|
||||
unsigned int hours = (rawTime % 86400L) / 3600;
|
||||
unsigned int minutes = (rawTime % 3600) / 60;
|
||||
unsigned int seconds = rawTime % 60;
|
||||
|
||||
char formatted_time[9];
|
||||
snprintf(formatted_time, sizeof(formatted_time), "%02d:%02d:%02d", hours, minutes, seconds);
|
||||
return String(formatted_time);
|
||||
snprintf(formatted_time, 9, "%02d:%02d:%02d", hours, minutes, seconds);
|
||||
}
|
||||
|
||||
// Based on https://github.com/PaulStoffregen/Time/blob/master/Time.cpp
|
||||
String NTPClient::getFormattedDate(unsigned long secs) {
|
||||
void NTPClient::getFormattedDate(char *formatted_date, unsigned long secs) {
|
||||
unsigned long rawTime = (secs ? secs : this->getEpochTime()) / 86400L; // in days
|
||||
unsigned long days = 0, year = 1970;
|
||||
uint8_t month;
|
||||
|
@ -187,11 +185,9 @@ String NTPClient::getFormattedDate(unsigned long secs) {
|
|||
month++; // jan is month 1
|
||||
rawTime++; // first day is day 1
|
||||
|
||||
char formatted_date[23];
|
||||
snprintf(formatted_date, sizeof(formatted_date), "%4lu-%02d-%02lu %s%+03d",
|
||||
year, month, rawTime, this->getFormattedTime(secs).c_str(), this->_timeOffset / 3600);
|
||||
|
||||
return String(formatted_date);
|
||||
char formatted_time[9];
|
||||
this->getFormattedTime(formatted_time, secs);
|
||||
snprintf(formatted_date, 23, "%4lu-%02d-%02lu %s%+03d", year, month, rawTime, formatted_time, this->_timeOffset / 3600);
|
||||
}
|
||||
|
||||
void NTPClient::end() {
|
||||
|
|
|
@ -80,7 +80,7 @@ class NTPClient {
|
|||
/**
|
||||
* @return secs argument (or 0 for current time) formatted like `hh:mm:ss`
|
||||
*/
|
||||
String getFormattedTime(unsigned long secs = 0);
|
||||
void getFormattedTime(char *formatted_time, unsigned long secs = 0);
|
||||
|
||||
/**
|
||||
* @return time in seconds since Jan. 1, 1970
|
||||
|
@ -91,7 +91,7 @@ class NTPClient {
|
|||
* @return secs argument (or 0 for current date) formatted to ISO 8601
|
||||
* like `2004-02-12T15:19:21+00:00`
|
||||
*/
|
||||
String getFormattedDate(unsigned long secs = 0);
|
||||
void getFormattedDate(char *formatted_date, unsigned long secs = 0);
|
||||
|
||||
/**
|
||||
* Stops the underlying UDP client
|
||||
|
|
|
@ -34,7 +34,7 @@ Code
|
|||
|
||||
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
|
||||
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)
|
||||
* [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)
|
||||
* [yamamaya](https://github.com/yamamaya) for the [3ms delay](https://github.com/sparkfun/SparkFun_SCD30_Arduino_Library/pull/24)
|
||||
|
||||
Repository Contents
|
||||
-------------------
|
||||
|
@ -43,8 +44,6 @@ License Information
|
|||
|
||||
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.
|
||||
|
||||
Distributed as-is; no warranty is given.
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
name=SparkFun SCD30 Arduino Library
|
||||
version=1.0.11
|
||||
version=1.0.13
|
||||
author=SparkFun Electronics
|
||||
maintainer=SparkFun Electronics <sparkfun.com>
|
||||
sentence=Library for the Sensirion SCD30 CO2 Sensor
|
||||
|
|
|
@ -266,6 +266,8 @@ bool SCD30::readMeasurement()
|
|||
if (_i2cPort->endTransmission() != 0)
|
||||
return (0); //Sensor did not ACK
|
||||
|
||||
delay(3);
|
||||
|
||||
const uint8_t receivedBytes = _i2cPort->requestFrom((uint8_t)SCD30_ADDRESS, (uint8_t)18);
|
||||
bool error = false;
|
||||
if (_i2cPort->available())
|
||||
|
@ -358,6 +360,8 @@ bool SCD30::getSettingValue(uint16_t registerAddress, uint16_t *val)
|
|||
if (_i2cPort->endTransmission() != 0)
|
||||
return (false); //Sensor did not ACK
|
||||
|
||||
delay(3);
|
||||
|
||||
_i2cPort->requestFrom((uint8_t)SCD30_ADDRESS, (uint8_t)3); // Request data and CRC
|
||||
if (_i2cPort->available())
|
||||
{
|
||||
|
@ -389,6 +393,8 @@ uint16_t SCD30::readRegister(uint16_t registerAddress)
|
|||
if (_i2cPort->endTransmission() != 0)
|
||||
return (0); //Sensor did not ACK
|
||||
|
||||
delay(3);
|
||||
|
||||
_i2cPort->requestFrom((uint8_t)SCD30_ADDRESS, (uint8_t)2);
|
||||
if (_i2cPort->available())
|
||||
{
|
||||
|
|
|
@ -57,9 +57,10 @@
|
|||
#define COMMAND_STOP_MEAS 0x0104
|
||||
#define COMMAND_READ_FW_VER 0xD100
|
||||
|
||||
typedef union {
|
||||
byte array[4];
|
||||
float value;
|
||||
typedef union
|
||||
{
|
||||
byte array[4];
|
||||
float value;
|
||||
} ByteToFl; // paulvha
|
||||
|
||||
class SCD30
|
||||
|
@ -69,9 +70,9 @@ public:
|
|||
|
||||
bool begin(bool autoCalibrate) { return begin(Wire, autoCalibrate); }
|
||||
#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
|
||||
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
|
||||
|
||||
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
|
||||
bool getSettingValue(uint16_t registerAddress, uint16_t *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 getTemperatureOffset(uint16_t *val) {return(getSettingValue(COMMAND_SET_TEMPERATURE_OFFSET, 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 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 getTemperatureOffset(uint16_t *val) { return (getSettingValue(COMMAND_SET_TEMPERATURE_OFFSET, 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)); }
|
||||
|
||||
uint16_t getCO2(void);
|
||||
float getHumidity(void);
|
||||
|
@ -115,12 +116,11 @@ public:
|
|||
uint8_t computeCRC8(uint8_t data[], uint8_t len);
|
||||
|
||||
private:
|
||||
|
||||
//Variables
|
||||
#ifdef USE_TEENSY3_I2C_LIB
|
||||
i2c_t3 *_i2cPort; //The generic connection to user's chosen I2C hardware
|
||||
#else
|
||||
TwoWire *_i2cPort; //The generic connection to user's chosen I2C hardware
|
||||
TwoWire *_i2cPort; //The generic connection to user's chosen I2C hardware
|
||||
#endif
|
||||
//Global main datums
|
||||
float co2 = 0;
|
||||
|
@ -136,6 +136,5 @@ private:
|
|||
//Debug
|
||||
Stream *_debugPort; //The stream to send debug messages to if enabled. Usually Serial.
|
||||
boolean _printDebug = false; //Flag to print debugging variables
|
||||
|
||||
};
|
||||
#endif
|
||||
|
|
|
@ -5,41 +5,96 @@ namespace config {
|
|||
const long utc_offset_in_seconds = UTC_OFFSET_IN_SECONDS; // UTC+1
|
||||
}
|
||||
|
||||
// Get last 3 bytes of ESP MAC (worldwide unique)
|
||||
String macToID() {
|
||||
uint8_t mac[6];
|
||||
WiFi.macAddress(mac);
|
||||
String result;
|
||||
for (int i = 3; i < 6; i++) {
|
||||
if (mac[i] < 16)
|
||||
result += '0';
|
||||
result += String(mac[i], HEX);
|
||||
}
|
||||
result.toLowerCase();
|
||||
return result;
|
||||
#if defined(ESP8266)
|
||||
const char *current_board = "ESP8266";
|
||||
# if !defined(AMPEL_WIFI)
|
||||
void preinit() {
|
||||
// WiFi would be initialized otherwise (on ESP8266), even if unused.
|
||||
// see https://github.com/esp8266/Arduino/issues/2111#issuecomment-224251391
|
||||
ESP8266WiFiClass::preinitWiFiOff();
|
||||
}
|
||||
# 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
|
||||
// ifdefs could be used to define functions specific to ESP32, e.g. with configTime
|
||||
namespace ntp {
|
||||
WiFiUDP ntpUDP;
|
||||
NTPClient timeClient(ntpUDP, config::ntp_server, config::utc_offset_in_seconds, 60000UL);
|
||||
bool connected_at_least_once = false;
|
||||
|
||||
void initialize() {
|
||||
timeClient.begin();
|
||||
}
|
||||
|
||||
void update() {
|
||||
timeClient.update();
|
||||
connected_at_least_once |= timeClient.update();
|
||||
}
|
||||
|
||||
String getLocalTime() {
|
||||
return timeClient.getFormattedDate();
|
||||
void getLocalTime(char *timestamp) {
|
||||
timeClient.getFormattedDate(timestamp);
|
||||
}
|
||||
|
||||
void setLocalTime(int32_t unix_seconds) {
|
||||
char time[23];
|
||||
timeClient.getFormattedDate(time);
|
||||
Serial.print(F("Current time : "));
|
||||
Serial.println(time);
|
||||
if (connected_at_least_once) {
|
||||
Serial.println(F("NTP update already happened. Not changing anything."));
|
||||
return;
|
||||
}
|
||||
Serial.print(F("Setting UNIX time to : "));
|
||||
Serial.println(unix_seconds);
|
||||
timeClient.setEpochTime(unix_seconds - seconds());
|
||||
timeClient.getFormattedDate(time);
|
||||
Serial.print(F("Current time : "));
|
||||
Serial.println(time);
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
// See more https://cpp4arduino.com/2020/02/07/how-to-format-strings-without-the-string-class.html
|
||||
const String SENSOR_ID = "ESP" + macToID();
|
||||
char sensorId[10]; // e.g "ESPxxxxxx\0"
|
||||
char macAddress[18]; // e.g "XX:XX:XX:XX:XX:XX\0"
|
||||
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
|
||||
#include <Arduino.h>
|
||||
#include "config.h"
|
||||
#include "sensor_console.h"
|
||||
|
||||
#include <WiFiUdp.h> // required for NTP
|
||||
#include "src/lib/NTPClient-master/NTPClient.h" // NTP
|
||||
|
||||
#if defined(ESP8266)
|
||||
# define BOARD "ESP8266"
|
||||
# include <ESP8266WiFi.h> // required to get MAC address
|
||||
# define get_free_heap_size() system_get_free_heap_size()
|
||||
# define esp_get_max_free_block_size() ESP.getMaxFreeBlockSize()
|
||||
# define esp_get_heap_fragmentation() ESP.getHeapFragmentation()
|
||||
#elif defined(ESP32)
|
||||
# define BOARD "ESP32"
|
||||
# include <WiFi.h> // required to get MAC address
|
||||
# define get_free_heap_size() esp_get_free_heap_size()
|
||||
#else
|
||||
# define BOARD "Unknown"
|
||||
# define esp_get_max_free_block_size() ESP.getMaxAllocHeap() //largest block of heap that can be allocated.
|
||||
# define esp_get_heap_fragmentation() "?" // apparently not available for ESP32
|
||||
#endif
|
||||
|
||||
namespace ntp {
|
||||
void initialize();
|
||||
void update();
|
||||
String getLocalTime();
|
||||
void getLocalTime(char *timestamp);
|
||||
}
|
||||
|
||||
namespace util {
|
||||
|
@ -35,10 +34,21 @@ namespace util {
|
|||
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.
|
||||
#define seconds() (millis() / 1000UL)
|
||||
extern uint32_t max_loop_duration;
|
||||
const extern String SENSOR_ID;
|
||||
|
||||
#endif
|
||||
|
|
|
@ -22,6 +22,7 @@ namespace web_server {
|
|||
const char *script_template;
|
||||
void handleWebServerRoot();
|
||||
void handlePageNotFound();
|
||||
void handleWebServerCommand();
|
||||
|
||||
#ifdef AMPEL_CSV
|
||||
void handleDeleteCSV();
|
||||
|
@ -58,13 +59,13 @@ namespace web_server {
|
|||
"</head>\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"
|
||||
"<li class='pure-menu-item'><a href='#table' class='pure-menu-link'>Info</a></li>\n"
|
||||
#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='#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
|
||||
"<li class='pure-menu-item' id='led'>⬤</li>\n" // LED
|
||||
"</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.
|
||||
"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"
|
||||
"</script>\n");
|
||||
|
||||
body_template =
|
||||
PSTR("<div class='pure-g'>\n"
|
||||
"<div class='pure-u-1' id='graph'></div>\n" // Graph placeholder
|
||||
"</script>\n"
|
||||
"<div class='pure-g'>\n"
|
||||
"<div class='pure-u-1' id='graph'></div>\n"// Graph placeholder
|
||||
"</div>\n"
|
||||
"<div class='pure-g'>\n"
|
||||
//Sensor table
|
||||
"<table id='table' class='pure-table-striped pure-u-1 pure-u-md-1-2'>\n"
|
||||
"<tr><th colspan='2'>%s</th></tr>\n"
|
||||
"<table id='table' class='pure-table-striped pure-u-1 pure-u-md-1-2'>\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>Temperature</td><td>%.1f℃</td></tr>\n"
|
||||
"<tr><td>Humidity</td><td>%.1f%%</td></tr>\n"
|
||||
|
@ -108,14 +108,19 @@ namespace web_server {
|
|||
#endif
|
||||
"<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>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 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>Largest heap block</td><td>%6d bytes</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>Ampel firmware</td><td>%s</td></tr>\n"
|
||||
"<tr><td>Uptime</td><td>%2d d %4d h %02d min %02d s</td></tr>\n"
|
||||
"</table>\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
|
||||
"<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'/>"
|
||||
|
@ -130,7 +135,7 @@ namespace web_server {
|
|||
#ifdef AMPEL_CSV
|
||||
"<script>\n"
|
||||
"document.body.style.cursor = 'default';\n"
|
||||
"fetch('./%s',{credentials:'include'})\n"
|
||||
"fetch('%s',{credentials:'include'})\n"
|
||||
".then(response=>response.text())\n"
|
||||
".then(csvText=>csvToTable(csvText))\n"
|
||||
".then(htmlTable=>addLogTableToPage(htmlTable))\n"
|
||||
|
@ -173,15 +178,16 @@ namespace web_server {
|
|||
|
||||
// Web-server
|
||||
http.on("/", handleWebServerRoot);
|
||||
http.on("/command", handleWebServerCommand);
|
||||
#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);
|
||||
#endif
|
||||
http.onNotFound(handlePageNotFound);
|
||||
http.begin();
|
||||
|
||||
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.println(WiFi.localIP());
|
||||
}
|
||||
|
@ -207,44 +213,49 @@ namespace web_server {
|
|||
|
||||
//NOTE: Splitting in multiple parts in order to use less RAM
|
||||
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, SENSOR_ID.c_str(),
|
||||
WiFi.localIP().toString().c_str()
|
||||
snprintf_P(content, sizeof(content), header_template, sensor::co2, ampel.sensorId, wifi::local_ip
|
||||
#ifdef AMPEL_CSV
|
||||
, csv_writer::filename.c_str()
|
||||
, csv_writer::filename
|
||||
#endif
|
||||
);
|
||||
|
||||
// Serial.print(F("INFO - Header size : "));
|
||||
// Serial.print(strlen(content));
|
||||
http.setContentLength(CONTENT_LENGTH_UNKNOWN);
|
||||
http.send_P(200, PSTR("text/html"), content);
|
||||
|
||||
// Body
|
||||
snprintf_P(content, sizeof(content), body_template, SENSOR_ID.c_str(), sensor::co2, sensor::temperature,
|
||||
sensor::humidity, sensor::timestamp.c_str(), config::measurement_timestep,
|
||||
snprintf_P(content, sizeof(content), body_template, ampel.sensorId, sensor::co2, sensor::temperature,
|
||||
sensor::humidity, sensor::timestamp, config::measurement_timestep,
|
||||
#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
|
||||
#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
|
||||
#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,
|
||||
#endif
|
||||
config::temperature_offset, SENSOR_ID.c_str(), SENSOR_ID.c_str(), WiFi.localIP().toString().c_str(),
|
||||
WiFi.localIP().toString().c_str(), get_free_heap_size(), max_loop_duration, BOARD, dd, hh, mm, ss);
|
||||
config::temperature_offset, config::auto_calibrate_sensor ? "Yes" : "No", ampel.sensorId, ampel.sensorId,
|
||||
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);
|
||||
|
||||
// Script
|
||||
snprintf_P(content, sizeof(content), script_template
|
||||
#ifdef AMPEL_CSV
|
||||
, csv_writer::filename.c_str(), SENSOR_ID.c_str()
|
||||
, csv_writer::filename, ampel.sensorId
|
||||
#endif
|
||||
);
|
||||
|
||||
// Serial.print(F(" - Script size : "));
|
||||
// Serial.println(strlen(content));
|
||||
http.sendContent(content);
|
||||
}
|
||||
|
||||
|
@ -255,7 +266,9 @@ namespace web_server {
|
|||
}
|
||||
if (FS_LIB.exists(csv_writer::filename)) {
|
||||
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"));
|
||||
csv_file.close();
|
||||
} else {
|
||||
|
@ -267,14 +280,23 @@ namespace web_server {
|
|||
if (!shouldBeAllowed()) {
|
||||
return http.requestAuthentication(DIGEST_AUTH);
|
||||
}
|
||||
Serial.print("Removing CSV file...");
|
||||
Serial.print(F("Removing CSV file..."));
|
||||
FS_LIB.remove(csv_writer::filename);
|
||||
Serial.println(" Done!");
|
||||
Serial.println(F(" Done!"));
|
||||
http.sendHeader("Location", "/");
|
||||
http.send(303);
|
||||
}
|
||||
#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() {
|
||||
http.send(404, F("text/plain"), F("404: Not found"));
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
#ifndef WEB_SERVER_H_
|
||||
#define WEB_SERVER_H_
|
||||
|
||||
#if defined(ESP8266)
|
||||
# include <ESP8266WebServer.h>
|
||||
#elif defined(ESP32)
|
||||
|
@ -8,7 +9,9 @@
|
|||
|
||||
#include "config.h"
|
||||
#include "util.h"
|
||||
#include "wifi_util.h"
|
||||
#include "co2_sensor.h"
|
||||
#include "sensor_console.h"
|
||||
#ifdef AMPEL_CSV
|
||||
# include "csv_writer.h"
|
||||
#endif
|
||||
|
|
|
@ -12,34 +12,69 @@ namespace config {
|
|||
#endif
|
||||
}
|
||||
|
||||
// Initialize Wi-Fi
|
||||
void WiFiConnect(const String &hostname) {
|
||||
//NOTE: WiFi Multi could allow multiple SSID and passwords.
|
||||
WiFi.persistent(false); // Don't write user & password to Flash.
|
||||
WiFi.mode(WIFI_STA); // Set ESP to be a WiFi-client only
|
||||
namespace wifi {
|
||||
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.
|
||||
WiFi.persistent(false); // Don't write user & password to Flash.
|
||||
WiFi.mode(WIFI_STA); // Set ESP to be a WiFi-client only
|
||||
#if defined(ESP8266)
|
||||
WiFi.hostname(hostname);
|
||||
#elif defined(ESP32)
|
||||
WiFi.setHostname(hostname.c_str());
|
||||
WiFi.setHostname(hostname);
|
||||
#endif
|
||||
|
||||
Serial.print(F("WiFi - Connecting to "));
|
||||
Serial.println(config::wifi_ssid);
|
||||
WiFi.begin(config::wifi_ssid, config::wifi_password);
|
||||
Serial.print(F("WiFi - Connecting to "));
|
||||
Serial.println(config::wifi_ssid);
|
||||
WiFi.begin(config::wifi_ssid, config::wifi_password);
|
||||
|
||||
// Wait for connection, at most wifi_timeout seconds
|
||||
for (int i = 0; i <= config::wifi_timeout && (WiFi.status() != WL_CONNECTED); i++) {
|
||||
led_effects::showRainbowWheel();
|
||||
Serial.print(".");
|
||||
}
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
led_effects::showKITTWheel(color::green);
|
||||
Serial.println();
|
||||
Serial.print(F("WiFi - Connected! IP address: "));
|
||||
Serial.println(WiFi.localIP());
|
||||
} else {
|
||||
//TODO: Allow sensor to work as an Access Point, in order to define SSID & password?
|
||||
led_effects::showKITTWheel(color::red);
|
||||
Serial.println(F("Connection to WiFi failed"));
|
||||
// Wait for connection, at most wifi_timeout seconds
|
||||
for (int i = 0; i <= config::wifi_timeout && (WiFi.status() != WL_CONNECTED); i++) {
|
||||
led_effects::showRainbowWheel();
|
||||
Serial.print(".");
|
||||
}
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
led_effects::showKITTWheel(color::green);
|
||||
Serial.println();
|
||||
Serial.print(F("WiFi - Connected! IP address: "));
|
||||
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 {
|
||||
//TODO: Allow sensor to work as an Access Point, in order to define SSID & password?
|
||||
led_effects::showKITTWheel(color::red);
|
||||
Serial.println(F("Connection to WiFi failed"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,9 @@
|
|||
#include "util.h"
|
||||
#include "led_effects.h"
|
||||
|
||||
void WiFiConnect(const String &hostname);
|
||||
namespace wifi {
|
||||
extern char local_ip[];
|
||||
void connect(const char *hostname);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
Loading…
Reference in a new issue