Commit 36bd5ec7 authored by Eric Duminil's avatar Eric Duminil
Browse files

Merge branch 'feature/sensor_console' into develop

parents 58daf119 d2b43f65
Pipeline #2943 passed with stage
in 1 minute and 33 seconds
......@@ -83,12 +83,12 @@ void setup() {
sensor::initialize();
Serial.print(F("Sensor ID: "));
Serial.println(SENSOR_ID);
Serial.println(ampel.sensorId);
Serial.print(F("Board : "));
Serial.println(BOARD);
Serial.println(ampel.board);
#ifdef AMPEL_WIFI
WiFiConnect(SENSOR_ID);
WiFiConnect(ampel.sensorId);
Serial.print(F("WiFi - Status: "));
Serial.println(WiFi.status());
......@@ -100,7 +100,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.c_str())) { // Start the mDNS responder for SENSOR_ID.local
MDNS.addService("http", "tcp", 80);
Serial.println(F("mDNS responder started"));
} else {
......@@ -108,7 +108,7 @@ void setup() {
}
# ifdef AMPEL_MQTT
mqtt::initialize("CO2sensors/" + SENSOR_ID);
mqtt::initialize("CO2sensors/" + ampel.sensorId);
# endif
}
#endif
......@@ -162,10 +162,10 @@ 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.print(ampel.max_loop_duration);
Serial.println(F(" ms."));
}
}
......
......@@ -14,7 +14,7 @@ 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]
}
namespace sensor {
......@@ -22,7 +22,7 @@ namespace sensor {
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;
......@@ -71,17 +71,15 @@ namespace sensor {
Serial.print(F("Auto-calibration is "));
Serial.println(config::auto_calibrate_sensor ? "ON." : "OFF.");
sensor_console::defineIntCommand("co2", setCO2forDebugging, " 1500 (Sets co2 level, for debugging purposes)");
sensor_console::defineIntCommand("timer", setTimer, " 30 (Sets measurement interval, in s)");
sensor_console::defineCommand("calibrate", startCalibrationProcess, " (Starts calibration process)");
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,
" 600 (Starts calibration process, to given ppm)");
F(" 600 (Starts calibration process, to given ppm)"));
sensor_console::defineIntCommand("calibrate!", calibrateSensorRightNow,
" 600 (Calibrates right now, to given ppm)");
sensor_console::defineCommand("reset", []() {
ESP.restart();
}, " (Restarts the sensor)");
sensor_console::defineCommand("night_mode", led_effects::toggleNightMode, " (Toggles night mode on/off)");
F(" 600 (Calibrates right now, to given ppm)"));
sensor_console::defineIntCommand("auto_calibrate", setAutoCalibration,
F(" 0/1 (Disables/enables autocalibration)"));
}
//NOTE: should timer deviation be used to adjust measurement_timestep?
......@@ -168,7 +166,7 @@ namespace sensor {
if (freshData) {
// checkTimerDeviation();
timestamp = ntp::getLocalTime();
ntp::getLocalTime(timestamp);
co2 = scd30.getCO2();
temperature = scd30.getTemperature();
humidity = scd30.getHumidity();
......@@ -212,6 +210,13 @@ namespace sensor {
Serial.println(co2);
}
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 : "));
......
......@@ -12,7 +12,7 @@
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.
}
......@@ -22,7 +22,7 @@ namespace sensor {
extern uint16_t co2;
extern float temperature;
extern float humidity;
extern String timestamp;
extern char timestamp[23];
void initialize();
bool processData();
......@@ -32,5 +32,6 @@ namespace sensor {
void setTimer(int32_t timestep);
void calibrateSensorToSpecificPPM(int32_t calibrationLevel);
void calibrateSensorRightNow(int32_t calibrationLevel);
void setAutoCalibration(int32_t autoCalibration);
}
#endif
......@@ -144,7 +144,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
......
......@@ -6,7 +6,7 @@ namespace config {
}
namespace csv_writer {
unsigned long last_written_at = 0;
String last_successful_write = "";
char last_successful_write[23];
#if defined(ESP8266)
/**
......@@ -78,7 +78,7 @@ namespace csv_writer {
}
#endif
const String filename = "/" + SENSOR_ID + ".csv";
const String filename = "/" + ampel.sensorId + ".csv";
int getAvailableSpace() {
return getTotalSpace() - getUsedSpace();
......@@ -115,9 +115,9 @@ namespace csv_writer {
showFilesystemContent();
Serial.println();
sensor_console::defineIntCommand("csv", setCSVinterval, " 60 (Sets CSV writing interval, in s)");
sensor_console::defineCommand("format_filesystem", formatFilesystem, " (Deletes the whole filesystem)");
sensor_console::defineCommand("show_csv", showCSVContent, " (Displays the complete CSV file on Serial)");
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() {
......@@ -132,11 +132,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();
......@@ -145,7 +145,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);
......@@ -156,7 +156,7 @@ 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;
......
......@@ -20,9 +20,9 @@ namespace config {
extern uint16_t csv_interval; // [s]
}
namespace csv_writer {
extern String last_successful_write;
extern char last_successful_write[23];
void initialize();
void logIfTimeHasCome(const String &timeStamp, const int16_t &co2, const float &temperature, const float &humidity);
void logIfTimeHasCome(const char* timestamp, const int16_t &co2, const float &temperature, const float &humidity);
int getAvailableSpace();
extern const String filename;
......
......@@ -74,6 +74,7 @@ namespace led_effects {
pixels.begin();
pixels.setBrightness(config::max_brightness);
LEDsOff();
sensor_console::defineCommand("night_mode", toggleNightMode, F(" (Toggles night mode on/off)"));
}
void toggleNightMode() {
......
......@@ -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
......
......@@ -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,7 +47,7 @@ namespace lorawan {
LMIC_reset();
// Join, but don't send anything yet.
LMIC_startJoining();
sensor_console::defineIntCommand("lora", setLoRaInterval, " 300 (Sets LoRaWAN sending interval, in s)");
sensor_console::defineIntCommand("lora", setLoRaInterval, F(" 300 (Sets LoRaWAN sending interval, in s)"));
}
// Checks if OTAA is connected, or if payload should be sent.
......@@ -65,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:
......@@ -113,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:
......
......@@ -39,7 +39,7 @@ 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);
......
......@@ -25,7 +25,7 @@ namespace mqtt {
String publish_topic;
const char *json_sensor_format;
String last_successful_publish = "";
char last_successful_publish[23] = "";
void initialize(String &topic) {
json_sensor_format = PSTR("{\"time\":\"%s\", \"co2\":%d, \"temp\":%.1f, \"rh\":%.1f}");
......@@ -36,22 +36,22 @@ namespace mqtt {
// mqttClient.setSocketTimeout(config::mqtt_timeout); //NOTE: somehow doesn't seem to have any effect on connect()
mqttClient.setServer(config::mqtt_server, config::mqtt_port);
sensor_console::defineIntCommand("mqtt", setMQTTinterval, " 60 (Sets MQTT sending interval, in s)");
sensor_console::defineIntCommand("mqtt", setMQTTinterval, F(" 60 (Sets MQTT sending interval, in s)"));
sensor_console::defineCommand("local_ip", sendInfoAboutLocalNetwork,
" (Sends local IP and SSID via MQTT. Can be useful to find sensor)");
F(" (Sends local IP and SSID via MQTT. Can be useful to find sensor)"));
}
void publish(const String &timestamp, int16_t co2, float temperature, float humidity) {
void publish(const char *timestamp, int16_t co2, float temperature, float humidity) {
if (WiFi.status() == WL_CONNECTED && mqttClient.connected()) {
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)) {
Serial.println(F("OK"));
last_successful_publish = ntp::getLocalTime();
ntp::getLocalTime(last_successful_publish);
} else {
Serial.println(F("Failed."));
}
......@@ -120,12 +120,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::mqtt_sending_interval) {
last_sent_at = now;
publish(timeStamp, co2, temp, hum);
publish(timestamp, co2, temp, hum);
}
}
......
......@@ -11,11 +11,11 @@ namespace config {
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 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();
......
......@@ -19,7 +19,8 @@ namespace sensor_console {
Command commands[MAX_COMMANDS];
//NOTE: Probably possible to DRY (with templates?)
void defineCommand(const char *name, void (*function)(void), const char *doc) {
void defineCommand(const char *name, void (*function)(void), const __FlashStringHelper *doc_fstring) {
const char *doc = (const char PROGMEM*) doc_fstring;
if (commands_count < MAX_COMMANDS) {
commands[commands_count].name = name;
commands[commands_count].voidFunction = function;
......@@ -31,7 +32,8 @@ namespace sensor_console {
}
}
void defineIntCommand(const char *name, void (*function)(int32_t), const char *doc) {
void defineIntCommand(const char *name, void (*function)(int32_t), const __FlashStringHelper *doc_fstring) {
const char *doc = (const char PROGMEM*) doc_fstring;
if (commands_count < MAX_COMMANDS) {
commands[commands_count].name = name;
commands[commands_count].intFunction = function;
......@@ -50,13 +52,11 @@ namespace sensor_console {
uint8_t parseCommand(const char *command, char *function_name, int32_t &argument) {
char split_command[MAX_COMMAND_SIZE];
strlcpy(split_command, command, MAX_COMMAND_SIZE);
Serial.print(F("Received : '"));
Serial.print(command);
Serial.println("'");
char *arg;
char *part1;
part1 = strtok(split_command, " ");
if (!part1) {
Serial.println(F("Received empty command"));
// Empty string
return 1;
}
......@@ -126,7 +126,6 @@ namespace sensor_console {
Serial.print(commands[i].doc);
Serial.println(".");
}
led_effects::showKITTWheel(color::red, 1);
}
/*
......@@ -143,18 +142,20 @@ namespace sensor_console {
Serial.print(F("Calling : "));
Serial.print(function_name);
if (has_argument) {
Serial.print("(");
Serial.print(F("("));
Serial.print(argument);
Serial.println(")");
Serial.println(F(")"));
commands[i].intFunction(argument);
} else {
Serial.println("()");
Serial.println(F("()"));
commands[i].voidFunction();
}
return;
}
}
Serial.println(F("Message not supported. Available commands :"));
Serial.print(F("'"));
Serial.print(command);
Serial.println(F("' not supported. Available commands :"));
listAvailableCommands();
}
}
#ifndef SENSOR_CONSOLE_H_INCLUDED
#define SENSOR_CONSOLE_H_INCLUDED
#include <Arduino.h>
#include "led_effects.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, ...)
......@@ -9,6 +10,8 @@
namespace sensor_console {
void processSerialInput(const byte in_byte);
void runCommand(const char *command);
void defineIntCommand(const char *command, void (*function)(int32_t), const char *doc);
void defineCommand(const char *command, void (*function)(void), const char *doc);
void defineIntCommand(const char *command, void (*function)(int32_t), const __FlashStringHelper *ifsh);
void defineCommand(const char *command, void (*function)(void), const __FlashStringHelper *ifsh);
}
#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
......
......@@ -5,6 +5,14 @@ namespace config {
const long utc_offset_in_seconds = UTC_OFFSET_IN_SECONDS; // UTC+1
}
#if defined(ESP8266)
const char *current_board = "ESP8266";
#elif defined(ESP32)
const char *current_board = "ESP32";
#else
const char *current_board = "UNKNOWN";
#endif
// Get last 3 bytes of ESP MAC (worldwide unique)
String macToID() {
uint8_t mac[6];
......@@ -24,22 +32,51 @@ String macToID() {
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();
}
void getLocalTime(char *timestamp) {
timeClient.getFormattedDate(timestamp);
}
String getLocalTime() {
return timeClient.getFormattedDate();
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(get_free_heap_size());
Serial.println(F(" bytes."));
}
Ampel::Ampel() :
board(current_board), sensorId("ESP" + macToID()), 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 sensor)"));
}
//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();
Ampel ampel;
......@@ -2,26 +2,23 @@
#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()
#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"
#endif
namespace ntp {
void initialize();
void update();
String getLocalTime();
void getLocalTime(char *timestamp);