Commit 58daf119 authored by Eric Duminil's avatar Eric Duminil
Browse files

Merge branch 'feature/sensor_console' into develop

parents 77831ea4 8dfc9f69
......@@ -33,11 +33,12 @@
#endif
#include "util.h"
#include "sensor_console.h"
#include "co2_sensor.h"
#include "led_effects.h"
void keepServicesAlive();
void checkFlashButton();
void checkSerialInput();
#endif
......@@ -56,7 +56,6 @@
* and define your credentials and parameters in 'config.h'.
*/
/*****************************************************************
* PreInit *
*****************************************************************/
......@@ -68,7 +67,6 @@ void preinit() {
#endif
}
/*****************************************************************
* Setup *
*****************************************************************/
......@@ -147,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);
......@@ -166,7 +166,13 @@ void loop() {
max_loop_duration = duration;
Serial.print(F("Debug - Max loop duration : "));
Serial.print(max_loop_duration);
Serial.println(" ms.");
Serial.println(F(" ms."));
}
}
void checkSerialInput() {
while (Serial.available() > 0) {
sensor_console::processSerialInput(Serial.read());
}
}
......
......@@ -5,6 +5,8 @@ namespace config {
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]
int8_t max_deviation_during_calibration = 30; // [ppm]
int8_t enough_stable_measurements = 60;
#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.
......@@ -17,7 +19,7 @@ namespace config {
namespace sensor {
SCD30 scd30;
int16_t co2 = 0;
uint16_t co2 = 0;
float temperature = 0;
float humidity = 0;
String timestamp = "";
......@@ -68,13 +70,25 @@ 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("calibrate", calibrateSensorToSpecificPPM,
" 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)");
}
//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(F("Measurement time offset : "));
Serial.print(now - previous_measurement_at - config::measurement_timestep * 1000);
Serial.println(" ms.");
previous_measurement_at = now;
......@@ -82,7 +96,8 @@ namespace sensor {
void countStableMeasurements() {
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);
......@@ -177,7 +192,7 @@ namespace sensor {
}
if (should_calibrate) {
if (stable_measurements == 60) {
if (stable_measurements == config::enough_stable_measurements) {
calibrateAndRestart();
}
led_effects::showWaitingLED(waiting_color);
......@@ -187,4 +202,39 @@ namespace sensor {
displayCO2OnLedRing();
return freshData;
}
/*****************************************************************
* Callbacks for sensor commands *
*****************************************************************/
void setCO2forDebugging(int32_t fakeCo2) {
Serial.print(F("DEBUG. Setting CO2 to "));
co2 = fakeCo2;
Serial.println(co2);
}
void setTimer(int32_t timestep) {
if (timestep >= 2 && timestep <= 1800) {
Serial.print(F("Setting Measurement Interval to : "));
Serial.print(timestep);
Serial.println("s.");
sensor::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(" ppm.");
sensor::startCalibrationProcess();
}
}
void calibrateSensorRightNow(int32_t calibrationLevel) {
stable_measurements = config::enough_stable_measurements;
calibrateSensorToSpecificPPM(calibrationLevel);
}
}
......@@ -7,6 +7,7 @@
#include "config.h"
#include "led_effects.h"
#include "util.h"
#include "sensor_console.h"
#include <Wire.h>
namespace config {
......@@ -18,7 +19,7 @@ namespace config {
namespace sensor {
extern SCD30 scd30;
extern int16_t co2;
extern uint16_t co2;
extern float temperature;
extern float humidity;
extern String timestamp;
......@@ -26,5 +27,10 @@ namespace sensor {
void initialize();
bool processData();
void startCalibrationProcess();
void setCO2forDebugging(int32_t fakeCo2);
void setTimer(int32_t timestep);
void calibrateSensorToSpecificPPM(int32_t calibrationLevel);
void calibrateSensorRightNow(int32_t calibrationLevel);
}
#endif
#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]
......@@ -113,9 +111,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, " 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)");
}
File openOrCreate() {
......@@ -161,4 +163,35 @@ namespace csv_writer {
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,6 +14,7 @@
#include "config.h"
#include "util.h"
#include "led_effects.h"
#include "sensor_console.h"
namespace config {
extern uint16_t csv_interval; // [s]
......@@ -24,6 +25,10 @@ namespace csv_writer {
void logIfTimeHasCome(const String &timeStamp, const int16_t &co2, const float &temperature, const float &humidity);
int getAvailableSpace();
extern const String filename;
void setCSVinterval(int32_t csv_interval);
void showCSVContent();
void formatFilesystem();
}
#endif
......@@ -47,11 +47,12 @@ 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)");
}
// 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();
}
......@@ -189,6 +190,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 {
......@@ -43,6 +43,8 @@ namespace lorawan {
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;
......@@ -35,6 +35,10 @@ namespace mqtt {
#endif
// 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::defineCommand("local_ip", sendInfoAboutLocalNetwork,
" (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) {
......@@ -55,70 +59,6 @@ namespace mqtt {
}
}
//TODO: Move all those setters to a separate class, which could be used by Serial/MQTT/WebServer
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
*
......@@ -133,48 +73,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("'.");
//TODO: Move this logic to a separate class, which could be used by Serial/MQTT/WebServer
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::runCommand(command);
led_effects::onBoardLEDOff();
}
......@@ -218,7 +123,7 @@ namespace mqtt {
void publishIfTimeHasCome(const String &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);
}
......@@ -232,4 +137,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.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);
}
}
......@@ -4,14 +4,11 @@
#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;
......@@ -19,5 +16,8 @@ namespace mqtt {
void initialize(String &topic);
void keepConnection();
void publishIfTimeHasCome(const String &timeStamp, const int16_t &co2, const float &temp, const float &hum);
void setMQTTinterval(int32_t sending_interval);
void sendInfoAboutLocalNetwork();
}
#endif
#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;
struct Command {
const char *name;
union {
void (*intFunction)(int32_t);
void (*voidFunction)(void);
};
const char *doc;
bool has_parameter;
};
Command commands[MAX_COMMANDS];
//NOTE: Probably possible to DRY (with templates?)
void defineCommand(const char *name, void (*function)(void), const char *doc) {
if (commands_count < MAX_COMMANDS) {
commands[commands_count].name = name;
commands[commands_count].voidFunction = function;
commands[commands_count].doc = doc;
commands[commands_count].has_parameter = false;
commands_count++;
} else {
Serial.println(F("Too many commands have been defined."));
}
}
void defineIntCommand(const char *name, void (*function)(int32_t), const char *doc) {
if (commands_count < MAX_COMMANDS) {
commands[commands_count].name = name;
commands[commands_count].intFunction = function;
commands[commands_count].doc = doc;
commands[commands_count].has_parameter = true;
commands_count++;
} else {
Serial.println(F("Too many commands have been defined."));
}
}
/*
* Tries to split a string command (e.g. 'mqtt 60' or 'show_csv') into a function_name and an argument.
* Returns 0 if both are found, 1 if there is a problem and 2 if no argument is found.
*/
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) {
// Empty string
return 1;
}
strlcpy(function_name, part1, MAX_COMMAND_SIZE);
arg = strtok(NULL, " ");
uint8_t code = 0;
if (arg) {
char *end;
argument = strtol(arg, &end, 10);
if (*end) {
// Second argument isn't a number
code = 2;
}
} else {
// No argument
code = 2;
}
return code;
}
/*
* 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;
runCommand(input_line);
input_pos = 0;