Extracted the logging functionality into a separate logger, making it easier to log info per module. This also drags less of the UI functionality into low level modules, which seems a lot cleaner to me.

This commit is contained in:
Maurice Makaay 2020-07-11 02:13:36 +02:00
parent e66b8ccdd5
commit 10f8fc2290
20 changed files with 306 additions and 288 deletions

View File

@ -1,5 +1,4 @@
#include "Data/DataController.h" #include "Data/DataController.h"
#include "Data/Measurements.h"
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
// Constructor // Constructor
@ -21,7 +20,13 @@ DataController *DataController::Instance()
DataController::DataController() : _temperatureMeasurements(TEMPERATURE_AVG_LOOKBACK), DataController::DataController() : _temperatureMeasurements(TEMPERATURE_AVG_LOOKBACK),
_humidityMeasurements(HUMIDITY_AVG_LOOKBACK), _humidityMeasurements(HUMIDITY_AVG_LOOKBACK),
_distanceMeasurements(DISTANCE_AVG_LOOKBACK) {} _distanceMeasurements(DISTANCE_AVG_LOOKBACK),
_logger("DATA")
{
_ui = DoughUI::Instance();
_sensors = DoughSensors::Instance();
_mqtt = DoughMQTT::Instance();
}
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
// Setup // Setup
@ -50,14 +55,10 @@ void DataController::handleMqttMessage(String &key, String &payload)
} }
else else
{ {
DoughUI::Instance()->log("DATA", "sS", "ERROR - Unhandled MQTT message, key = ", key); DataController::Instance()->_logger.log("sS", "ERROR - Unhandled MQTT message, key = ", key);
} }
} }
/**
* Check if configuration has been taken care of. Some configuration is
* required before measurements can be processed.
*/
bool DataController::isConfigured() bool DataController::isConfigured()
{ {
return _containerHeightSet; return _containerHeightSet;
@ -73,21 +74,19 @@ void DataController::setContainerHeight(int height)
_containerHeightSet = false; _containerHeightSet = false;
if (height <= HCSR04_MIN_MM) if (height <= HCSR04_MIN_MM)
{ {
DoughUI::Instance()->log("DATA", "sisis", _logger.log("sisis", "ERROR - Container height ", height,
"ERROR - Container height ", height, "mm is less than the minimum measuring distance of ",
"mm is less than the minimum measuring distance of ", HCSR04_MIN_MM, "mm");
HCSR04_MIN_MM, "mm");
return; return;
} }
if (height >= HCSR04_MAX_MM) if (height >= HCSR04_MAX_MM)
{ {
DoughUI::Instance()->log("DATA", "sisis", _logger.log("sisis", "ERROR - Container height ", height,
"ERROR - Container height ", height, "mm is more than the maximum measuring distance of ",
"mm is more than the maximum measuring distance of ", HCSR04_MAX_MM, "mm");
HCSR04_MAX_MM, "mm");
return; return;
} }
DoughUI::Instance()->log("DATA", "sis", "Set container height to ", height, "mm"); _logger.log("sis", "Set container height to ", height, "mm");
_containerHeight = height; _containerHeight = height;
_containerHeightSet = true; _containerHeightSet = true;
} }
@ -122,38 +121,36 @@ void DataController::_sample()
if (tick) if (tick)
{ {
DoughUI *ui = DoughUI::Instance();
_lastSample = now; _lastSample = now;
DoughSensors *sensors = DoughSensors::Instance();
// Quickly dip the LED to indicate that a measurement is started. // Quickly dip the LED to indicate that a measurement is started.
// This is done synchroneously, because we suspend the timer interrupts // This is done synchroneously, because we suspend the timer interrupts
// in the upcoming code. // in the upcoming code.
ui->led3.off(); _ui->led3.off();
delay(50); delay(50);
ui->led3.on(); _ui->led3.on();
// Suspend the UI timer interrupts, to not let these interfere // Suspend the UI timer interrupts, to not let these interfere
// with the sensor measurements. // with the sensor measurements.
ui->suspend(); _ui->suspend();
// Take a sample. // Take a sample.
switch (_sampleType) switch (_sampleType)
{ {
case SAMPLE_TEMPERATURE: case SAMPLE_TEMPERATURE:
_temperatureMeasurements.add(sensors->readTemperature()); _temperatureMeasurements.add(_sensors->readTemperature());
_sampleType = SAMPLE_HUMIDITY; _sampleType = SAMPLE_HUMIDITY;
break; break;
case SAMPLE_HUMIDITY: case SAMPLE_HUMIDITY:
_humidityMeasurements.add(sensors->readHumidity()); _humidityMeasurements.add(_sensors->readHumidity());
_sampleType = SAMPLE_DISTANCE; _sampleType = SAMPLE_DISTANCE;
break; break;
case SAMPLE_DISTANCE: case SAMPLE_DISTANCE:
_distanceMeasurements.add(sensors->readDistance()); _distanceMeasurements.add(_sensors->readDistance());
break; break;
} }
ui->resume(); _ui->resume();
_sampleCounter++; _sampleCounter++;
if (_sampleCounter == SAMPLE_CYCLE_LENGTH) if (_sampleCounter == SAMPLE_CYCLE_LENGTH)
@ -166,77 +163,17 @@ void DataController::_sample()
void DataController::_publish() void DataController::_publish()
{ {
static unsigned long lastSample = 0; if (_lastPublish == 0 || millis() - _lastPublish > PUBLISH_INTERVAL)
if (lastSample == 0 || millis() - lastSample > PUBLISH_INTERVAL)
{ {
lastSample = millis(); _lastPublish = millis();
DoughUI *ui = DoughUI::Instance(); _mqtt->publish("temperature", _temperatureMeasurements.getLast());
DoughMQTT *mqtt = DoughMQTT::Instance(); _mqtt->publish("temperature/average", _temperatureMeasurements.getAverage());
_mqtt->publish("humidity", _humidityMeasurements.getLast());
_mqtt->publish("humidity/average", _humidityMeasurements.getAverage());
_mqtt->publish("distance", _distanceMeasurements.getLast());
_mqtt->publish("distance/average", _distanceMeasurements.getAverage());
ui->log("DATA", "s", "Publish temperature"); _ui->led1.dip()->fast();
auto m = _temperatureMeasurements.getLast();
if (m->ok)
{
mqtt->publish("temperature", m->value);
}
else
{
mqtt->publish("temperature", "null");
}
ui->log("DATA", "s", "Publish temperature average");
m = _temperatureMeasurements.getAverage();
if (m->ok)
{
mqtt->publish("temperature/average", m->value);
}
else
{
mqtt->publish("temperature/average", "null");
}
ui->log("DATA", "s", "Publish humidity");
m = _humidityMeasurements.getLast();
if (m->ok)
{
mqtt->publish("humidity", m->value);
}
else
{
mqtt->publish("humidity", "null");
}
m = _humidityMeasurements.getAverage();
if (m->ok)
{
mqtt->publish("humidity/average", m->value);
}
else
{
mqtt->publish("humidity/average", "null");
}
m = _distanceMeasurements.getLast();
if (m->ok)
{
mqtt->publish("distance", m->value);
}
else
{
mqtt->publish("distance", "null");
}
m = _distanceMeasurements.getAverage();
if (m->ok)
{
mqtt->publish("distance/average", m->value);
}
else
{
mqtt->publish("distance/average", "null");
}
ui->led1.dip()->fast();
} }
} }

View File

@ -27,6 +27,7 @@
#include "Network/DoughWiFi.h" #include "Network/DoughWiFi.h"
#include "Network/DoughMQTT.h" #include "Network/DoughMQTT.h"
#include "UI/DoughUI.h" #include "UI/DoughUI.h"
#include "UI/DoughLogger.h"
typedef enum typedef enum
{ {
@ -36,36 +37,40 @@ typedef enum
} DoughSampleType; } DoughSampleType;
/** /**
* The DoughData class is responsible for holding the device configuration, * This class is responsible for handling all things "data".
* collecting measurements from sensors, gathering the statistics on these data, * It holds the device configuration, collects measurements from sensors,
* and publishing results to the MQTT broker. * gathers statistics on these data, and publishing results to the MQTT broker.
*/ */
class DataController class DataController
{ {
public: public:
static DataController *Instance(); static DataController* Instance();
void setup(); void setup();
void loop(); void loop();
void clearHistory(); void clearHistory();
void setContainerHeight(int height); void setContainerHeight(int height);
bool isConfigured(); bool isConfigured();
static void handleMqttConnect(DoughMQTT *mqtt); static void handleMqttConnect(DoughMQTT* mqtt);
static void handleMqttMessage(String &key, String &value); static void handleMqttMessage(String &key, String &value);
private: private:
DataController(); DataController();
static DataController *_instance; static DataController* _instance;
DoughSensors *_sensors; Measurements _temperatureMeasurements;
Measurements _humidityMeasurements;
Measurements _distanceMeasurements;
DoughLogger _logger;
DoughUI* _ui;
DoughSensors* _sensors;
DoughMQTT* _mqtt;
unsigned long _lastSample = 0; unsigned long _lastSample = 0;
unsigned long _lastPublish = 0;
DoughSampleType _sampleType = SAMPLE_TEMPERATURE; DoughSampleType _sampleType = SAMPLE_TEMPERATURE;
int _sampleCounter = 0; int _sampleCounter = 0;
int _containerHeight; int _containerHeight;
bool _containerHeightSet; bool _containerHeightSet;
void _sample(); void _sample();
void _publish(); void _publish();
Measurements _temperatureMeasurements;
Measurements _humidityMeasurements;
Measurements _distanceMeasurements;
}; };
#endif #endif

View File

@ -1,19 +1,17 @@
#include "Data/Measurement.h" #include "Data/Measurement.h"
Measurement::Measurement() {} Measurement::Measurement() { }
Measurement::Measurement(bool ok, int value) Measurement Measurement::Failed()
{ {
this->ok = ok; Measurement m;
this->value = value; return m;
} }
Measurement *Measurement::Failed() Measurement Measurement::Value(int value)
{ {
return new Measurement(false, 0); Measurement m;
} m.ok = true;
m.value = value;
Measurement *Measurement::Ok(int value) return m;
{
return new Measurement(true, value);
} }

View File

@ -2,17 +2,17 @@
#define DOUGH_DATA_MEASUREMENT_H #define DOUGH_DATA_MEASUREMENT_H
/** /**
* The DoughDataMeasurement class represents a single measurement. * This class represents a single measurement, which can be either a
* successful (bearing a measurement value) or a failed one.
*/ */
class Measurement class Measurement
{ {
public: public:
Measurement(); Measurement();
Measurement(bool ok, int value);
int value = 0; int value = 0;
bool ok = false; bool ok = false;
static Measurement *Failed(); static Measurement Failed();
static Measurement *Ok(int value); static Measurement Value(int value);
}; };
#endif #endif

View File

@ -1,26 +1,27 @@
#include "Data/DataController.h" #include "Data/Measurements.h"
#include "Data/Measurement.h" #include "UI/DoughUI.h"
typedef Measurement *MeasurementPtr;
Measurements::Measurements(unsigned int avgLookback) Measurements::Measurements(unsigned int avgLookback)
{ {
_storageSize = avgLookback; _storageSize = avgLookback;
_storage = new MeasurementPtr[avgLookback];
for (unsigned int i = 0; i < avgLookback; i++) _storage = new Measurement*[avgLookback];
{ for (unsigned int i = 0; i < avgLookback; i++) {
_storage[i] = nullptr; _storage[i] = new Measurement;
} }
clearHistory();
} }
void Measurements::add(Measurement *measurement) void Measurements::add(Measurement measurement)
{ {
auto index = _next(); unsigned int index = _next();
_storage[index] = measurement; _storage[index]->ok = measurement.ok;
if (measurement->ok) _storage[index]->value = measurement.value;
if (measurement.ok)
{ {
_averageCount++; _averageCount++;
_averageSum += measurement->value; _averageSum += measurement.value;
} }
} }
@ -31,26 +32,28 @@ unsigned int Measurements::_next()
{ {
_index = 0; _index = 0;
} }
if (_storage[_index] != nullptr && _storage[_index]->ok) if (_storage[_index]->ok)
{ {
_averageSum -= _storage[_index]->value; _averageSum -= _storage[_index]->value;
_averageCount--; _averageCount--;
} }
_storage[_index]->ok = false;
_storage[_index]->value = 0;
return _index; return _index;
} }
Measurement *Measurements::getLast() Measurement Measurements::getLast()
{ {
return _storage[_index]; return *_storage[_index];
} }
Measurement *Measurements::getAverage() Measurement Measurements::getAverage()
{ {
auto result = new Measurement(); Measurement result;
if (_averageCount > 0) if (_averageCount > 0)
{ {
result->ok = true; result.ok = true;
result->value = round(_averageSum / _averageCount); result.value = round(_averageSum / _averageCount);
} }
return result; return result;
} }

View File

@ -1,23 +1,24 @@
#ifndef DOUGH_DATA_MEASUREMENTS_H #ifndef DOUGH_DATA_MEASUREMENTS_H
#define DOUGH_DATA_MEASUREMENTS_H #define DOUGH_DATA_MEASUREMENTS_H
#include <Arduino.h>
#include "Data/Measurement.h" #include "Data/Measurement.h"
/** /**
* The DoughDataMeasurements class is used to store measurements for a sensor * This class is used to store measurements for a sensor and to keep
* and to keep track of running totals for handling average computations. * track of running totals for handling average computations.
*/ */
class Measurements class Measurements
{ {
public: public:
Measurements(unsigned int avgLookback); Measurements(unsigned int avgLookback);
void add(Measurement *measurement); void add(Measurement measurement);
Measurement *getLast(); Measurement getLast();
Measurement *getAverage(); Measurement getAverage();
void clearHistory(); void clearHistory();
private: private:
Measurement **_storage; Measurement** _storage;
unsigned int _storageSize; unsigned int _storageSize;
int _averageSum = 0; int _averageSum = 0;
unsigned int _averageCount = 0; unsigned int _averageCount = 0;

View File

@ -4,12 +4,12 @@
// Constructor // Constructor
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
DoughMQTT *DoughMQTT::_instance = nullptr; DoughMQTT* DoughMQTT::_instance = nullptr;
/** /**
* Fetch the DoughMQTT singleton. * Fetch the DoughMQTT singleton.
*/ */
DoughMQTT *DoughMQTT::Instance() DoughMQTT* DoughMQTT::Instance()
{ {
if (DoughMQTT::_instance == nullptr) if (DoughMQTT::_instance == nullptr)
{ {
@ -18,10 +18,7 @@ DoughMQTT *DoughMQTT::Instance()
return DoughMQTT::_instance; return DoughMQTT::_instance;
} }
DoughMQTT::DoughMQTT() DoughMQTT::DoughMQTT() : _logger("MQTT") { }
{
_ui = DoughUI::Instance();
}
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
// Setup // Setup
@ -29,14 +26,14 @@ DoughMQTT::DoughMQTT()
void DoughMQTT::setup() void DoughMQTT::setup()
{ {
DoughWiFi *network = DoughWiFi::Instance(); DoughWiFi* network = DoughWiFi::Instance();
#ifdef MQTT_DEVICE_ID #ifdef MQTT_DEVICE_ID
_mqttDeviceId = MQTT_DEVICE_ID; _mqttDeviceId = MQTT_DEVICE_ID;
#else #else
_mqttDeviceId = network->getMacAddress(); _mqttDeviceId = network->getMacAddress();
#endif #endif
_ui->log("MQTT", "ss", "Device ID = ", _mqttDeviceId); _logger.log("ss", "Device ID = ", _mqttDeviceId);
_mqttClient.begin(MQTT_BROKER, MQTT_PORT, network->client); _mqttClient.begin(MQTT_BROKER, MQTT_PORT, network->client);
} }
@ -62,13 +59,13 @@ bool DoughMQTT::isConnected()
bool DoughMQTT::connect() bool DoughMQTT::connect()
{ {
_ui->log("MQTT", "sssi", "Broker = ", MQTT_BROKER, ":", MQTT_PORT); _logger.log("sssi", "Broker = ", MQTT_BROKER, ":", MQTT_PORT);
_mqttClient.connect(_mqttDeviceId, MQTT_USERNAME, MQTT_PASSWORD); _mqttClient.connect(_mqttDeviceId, MQTT_USERNAME, MQTT_PASSWORD);
// Check if the connection to the broker was successful. // Check if the connection to the broker was successful.
if (!_mqttClient.connected()) if (!_mqttClient.connected())
{ {
_ui->log("MQTT", "s", "ERROR - Connection to broker failed"); _logger.log("s", "ERROR - Connection to broker failed");
return false; return false;
} }
@ -89,9 +86,9 @@ void DoughMQTT::procesIncomingsMessages()
void DoughMQTT::handleMessage(String &topic, String &payload) void DoughMQTT::handleMessage(String &topic, String &payload)
{ {
DoughUI::Instance()->log("MQTT", "sSsS", "<<< ", topic, " = ", payload); DoughMQTT::Instance()->_logger.log("sSsS", "<<< ", topic, " = ", payload);
DoughMQTT *mqtt = DoughMQTT::Instance(); DoughMQTT* mqtt = DoughMQTT::Instance();
if (mqtt->_onMessage != nullptr) if (mqtt->_onMessage != nullptr)
{ {
int pos = topic.lastIndexOf('/'); int pos = topic.lastIndexOf('/');
@ -103,19 +100,19 @@ void DoughMQTT::handleMessage(String &topic, String &payload)
} }
} }
void DoughMQTT::subscribe(const char *key) void DoughMQTT::subscribe(const char* key)
{ {
char topic[200]; char topic[200];
snprintf(topic, sizeof(topic) / sizeof(topic[0]), "%s/%s/%s", MQTT_TOPIC_PREFIX, _mqttDeviceId, key); snprintf(topic, sizeof(topic) / sizeof(topic[0]), "%s/%s/%s", MQTT_TOPIC_PREFIX, _mqttDeviceId, key);
DoughUI::Instance()->log("MQTT", "ss", "Subscribe to ", topic); _logger.log("ss", "Subscribe to ", topic);
_mqttClient.subscribe(topic); _mqttClient.subscribe(topic);
} }
void DoughMQTT::publish(const char *key, const char *payload) void DoughMQTT::publish(const char* key, const char* payload)
{ {
char topic[200]; char topic[200];
snprintf(topic, sizeof(topic) / sizeof(topic[0]), "%s/%s/%s", MQTT_TOPIC_PREFIX, _mqttDeviceId, key); snprintf(topic, sizeof(topic) / sizeof(topic[0]), "%s/%s/%s", MQTT_TOPIC_PREFIX, _mqttDeviceId, key);
DoughUI::Instance()->log("MQTT", "ssss", ">>> ", topic, " = ", payload); _logger.log("ssss", ">>> ", topic, " = ", payload);
_mqttClient.publish(topic, payload); _mqttClient.publish(topic, payload);
} }
@ -124,4 +121,12 @@ void DoughMQTT::publish(const char *key, int payload)
char buf[16]; char buf[16];
snprintf(buf, 16, "%d", payload); snprintf(buf, 16, "%d", payload);
publish(key, buf); publish(key, buf);
}
void DoughMQTT::publish(const char *key, Measurement measurement) {
if (measurement.ok) {
publish(key, measurement.value);
} else {
publish(key, "null");
}
} }

View File

@ -4,37 +4,43 @@
#include <MQTT.h> #include <MQTT.h>
#include <MQTTClient.h> #include <MQTTClient.h>
#include "Network/DoughWiFi.h" #include "Network/DoughWiFi.h"
#include "UI/DoughUI.h" #include "Data/Measurement.h"
#include "UI/DoughLogger.h"
#include "config.h" #include "config.h"
/**
* This class encapsulates the connection to the MQTT broker.
* MQTT is used to publish measurements and to store configuration data.
*/
class DoughMQTT; class DoughMQTT;
typedef void (*DoughMQTTConnectHandler)(DoughMQTT *mqtt); typedef void (*DoughMQTTConnectHandler)(DoughMQTT* mqtt);
typedef void (*DoughMQTTMessageHandler)(String &key, String &value); typedef void (*DoughMQTTMessageHandler)(String &key, String &value);
class DoughMQTT class DoughMQTT
{ {
public: public:
static DoughMQTT *Instance(); static DoughMQTT* Instance();
void setup(); void setup();
void onConnect(DoughMQTTConnectHandler callback); void onConnect(DoughMQTTConnectHandler callback);
void onMessage(DoughMQTTMessageHandler callback); void onMessage(DoughMQTTMessageHandler callback);
bool isConnected(); bool isConnected();
bool connect(); bool connect();
void subscribe(const char *key); void subscribe(const char* key);
void procesIncomingsMessages(); void procesIncomingsMessages();
void publish(const char *key, const char *payload); void publish(const char* key, const char* payload);
void publish(const char *key, int payload); void publish(const char* key, int payload);
void publish(const char* key, Measurement measurement);
private: private:
DoughMQTT(); DoughMQTT();
static DoughMQTT *_instance; static DoughMQTT* _instance;
MQTTClient _mqttClient; MQTTClient _mqttClient;
DoughUI *_ui; DoughLogger _logger;
DoughMQTTConnectHandler _onConnect = nullptr; DoughMQTTConnectHandler _onConnect = nullptr;
MQTTClientCallbackSimple _onMessage = nullptr; MQTTClientCallbackSimple _onMessage = nullptr;
static void handleMessage(String &topic, String &payload); static void handleMessage(String &topic, String &payload);
char *_mqttDeviceId; char* _mqttDeviceId;
}; };
#endif #endif

View File

@ -18,10 +18,7 @@ DoughWiFi *DoughWiFi::Instance()
return DoughWiFi::_instance; return DoughWiFi::_instance;
} }
DoughWiFi::DoughWiFi() DoughWiFi::DoughWiFi() : _logger("WIFI") {}
{
_ui = DoughUI::Instance();
}
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
// Setup // Setup
@ -39,7 +36,7 @@ void DoughWiFi::_setMacAddress()
void DoughWiFi::setup() void DoughWiFi::setup()
{ {
_setMacAddress(); _setMacAddress();
DoughUI::Instance()->log("NETWORK", "ss", "MAC address = ", getMacAddress()); _logger.log("ss", "MAC address = ", getMacAddress());
} }
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
@ -58,7 +55,7 @@ bool DoughWiFi::connect()
// Check if a device with a WiFi shield is used. // Check if a device with a WiFi shield is used.
if (status == WL_NO_SHIELD) if (status == WL_NO_SHIELD)
{ {
_ui->log("NETWORK", "s", "ERROR - Device has no WiFi shield"); _logger.log("s", "ERROR - Device has no WiFi shield");
delay(5000); delay(5000);
return false; return false;
} }
@ -70,19 +67,19 @@ bool DoughWiFi::connect()
} }
// Setup the connection to the WiFi network. // Setup the connection to the WiFi network.
_ui->log("NETWORK", "ss", "WiFi network = ", WIFI_SSID); _logger.log("ss", "WiFi network = ", WIFI_SSID);
status = WiFi.begin(WIFI_SSID, WIFI_PASSWORD); status = WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
// Check if the connection attempt was successful. // Check if the connection attempt was successful.
if (status == WL_CONNECTED) if (status == WL_CONNECTED)
{ {
_ui->log("NETWORK", "sa", "IP-Address = ", WiFi.localIP()); _logger.log("sa", "IP-Address = ", WiFi.localIP());
_ui->log("NETWORK", "sis", "Signal strength = ", WiFi.RSSI(), " dBm"); _logger.log("sis", "Signal strength = ", WiFi.RSSI(), " dBm");
return true; return true;
} }
else else
{ {
_ui->log("NETWORK", "sis", "ERROR - WiFi connection failed (reason: ", WiFi.reasonCode(), ")"); _logger.log("sis", "ERROR - WiFi connection failed (reason: ", WiFi.reasonCode(), ")");
return false; return false;
} }
} }

View File

@ -2,14 +2,17 @@
#define DOUGH_NETWORK_H #define DOUGH_NETWORK_H
#include <WiFiNINA.h> #include <WiFiNINA.h>
#include "UI/DoughUI.h" #include "UI/DoughLogger.h"
#include "config.h" #include "config.h"
/**
* This class encapsulates the connection to the WiFi network.
*/
class DoughWiFi class DoughWiFi
{ {
public: public:
static DoughWiFi *Instance(); static DoughWiFi* Instance();
char *getMacAddress(); char* getMacAddress();
void setup(); void setup();
void loop(); void loop();
bool isConnected(); bool isConnected();
@ -18,10 +21,10 @@ public:
private: private:
DoughWiFi(); DoughWiFi();
static DoughWiFi *_instance; static DoughWiFi* _instance;
void _setMacAddress(); void _setMacAddress();
char _macAddress[18]; // max MAC address length + 1 char _macAddress[18]; // max MAC address length + 1
DoughUI *_ui; DoughLogger _logger;
}; };
#endif #endif

View File

@ -16,8 +16,7 @@ DoughSensors* DoughSensors::Instance() {
return DoughSensors::_instance; return DoughSensors::_instance;
} }
DoughSensors::DoughSensors() { DoughSensors::DoughSensors() : _logger("SENSORS") {
_ui = DoughUI::Instance();
_dht = new DHT(DHT11_DATA_PIN, DHT11); _dht = new DHT(DHT11_DATA_PIN, DHT11);
_hcsr04 = new HCSR04(HCSR04_TRIG_PIN, HCSR04_ECHO_PIN); _hcsr04 = new HCSR04(HCSR04_TRIG_PIN, HCSR04_ECHO_PIN);
} }
@ -35,38 +34,38 @@ void DoughSensors::setup() {
// loop // loop
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
Measurement* DoughSensors::readTemperature() { Measurement DoughSensors::readTemperature() {
float t = _dht->readTemperature(); float t = _dht->readTemperature();
if (isnan(t)) { if (isnan(t)) {
_ui->log("SENSORS", "s", "ERROR - Temperature measurement failed"); _logger.log("s", "ERROR - Temperature measurement failed");
return Measurement::Failed(); return Measurement::Failed();
} else { } else {
_logger.log("sis", "Temperature = ", int(t), "°C ");
_hcsr04->setTemperature(int(t)); _hcsr04->setTemperature(int(t));
auto m = new Measurement(true, int(t)); auto m = Measurement::Value(int(t));
_ui->log("SENSORS", "sisi", "Temperature = ", int(t), "°C ", m->value);
return m; return m;
} }
} }
Measurement* DoughSensors::readHumidity() { Measurement DoughSensors::readHumidity() {
int h = _dht->readHumidity(); int h = _dht->readHumidity();
if (h == 0) { if (h == 0) {
_ui->log("SENSORS", "s", "ERROR - Humidity measurement failed"); _logger.log("s", "ERROR - Humidity measurement failed");
return Measurement::Failed(); return Measurement::Failed();
} else { } else {
_logger.log("sis", "Humidity = ", h, "%");
_hcsr04->setHumidity(h); _hcsr04->setHumidity(h);
_ui->log("SENSORS", "sis", "Humidity = ", h, "%"); return Measurement::Value(h);
return Measurement::Ok(h);
} }
} }
Measurement* DoughSensors::readDistance() { Measurement DoughSensors::readDistance() {
int d = _hcsr04->readDistance(); int d = _hcsr04->readDistance();
if (d == -1) { if (d == -1) {
_ui->log("SENSORS", "s", "ERROR - Distance measurement failed"); _logger.log("s", "ERROR - Distance measurement failed");
return Measurement::Failed(); return Measurement::Failed();
} else { } else {
_ui->log("SENSORS", "sis", "Distance = ", d, "mm"); _logger.log("sis", "Distance = ", d, "mm");
return Measurement::Ok(d); return Measurement::Value(d);
} }
} }

View File

@ -3,22 +3,25 @@
#include <DHT.h> #include <DHT.h>
#include "Sensors/HCSR04.h" #include "Sensors/HCSR04.h"
#include "UI/DoughUI.h" #include "UI/DoughLogger.h"
#include "Data/Measurement.h" #include "Data/Measurement.h"
#include "config.h" #include "config.h"
/**
* This class provides access to the sensors in the device.
*/
class DoughSensors { class DoughSensors {
public: public:
static DoughSensors* Instance(); static DoughSensors* Instance();
void setup(); void setup();
Measurement* readTemperature(); Measurement readTemperature();
Measurement* readHumidity(); Measurement readHumidity();
Measurement* readDistance(); Measurement readDistance();
private: private:
DoughSensors(); DoughSensors();
static DoughSensors* _instance; static DoughSensors* _instance;
DoughUI *_ui; DoughLogger _logger;
DHT* _dht; DHT* _dht;
HCSR04* _hcsr04; HCSR04* _hcsr04;
}; };

View File

@ -28,6 +28,9 @@
#include <Arduino.h> #include <Arduino.h>
#include "config.h" #include "config.h"
/**
* This class is used to get a distance reading from an HCSR04 sensor.
*/
class HCSR04 { class HCSR04 {
public: public:
HCSR04(int triggerPin, int echoPin); HCSR04(int triggerPin, int echoPin);

View File

@ -18,6 +18,15 @@ typedef enum
typedef void (*DoughButtonHandler)(); typedef void (*DoughButtonHandler)();
/**
* This class provides a simple interface for handling button presses.
* Only a few events are supported:
*
* - short press: a button up event, will not trigger after long press
* - long press: when a button is held down for a while
* - press: this is a button down event, which will only trigger if
* short press and long press events aren't used
*/
class DoughButton class DoughButton
{ {
public: public:

View File

@ -19,6 +19,10 @@ typedef enum
PULSE PULSE
} DoughLEDState; } DoughLEDState;
/**
* This class provides a set of basic LED lighting patterns, which can
* be used in an async way.
*/
class DoughLED class DoughLED
{ {
public: public:

55
src/UI/DoughLogger.cpp Normal file
View File

@ -0,0 +1,55 @@
#include "DoughLogger.h"
DoughLogger::DoughLogger(const char *section)
{
_section = section;
}
void DoughLogger::log(const char* fmt, ...)
{
char buf[LOGGER_PREFIX_BUFLEN];
snprintf(buf, sizeof(buf) / sizeof(buf[0]), LOGGER_PREFIX_FORMAT, _section);
Serial.print(buf);
va_list args;
va_start(args, fmt);
while (*fmt != '\0')
{
if (*fmt == 'i')
{
int i = va_arg(args, int);
Serial.print(i);
}
else if (*fmt == 'f')
{
float f = va_arg(args, double);
Serial.print(f);
}
else if (*fmt == 'a')
{
IPAddress a = va_arg(args, IPAddress);
Serial.print(a);
}
else if (*fmt == 's')
{
const char *s = va_arg(args, const char *);
Serial.print(s);
}
else if (*fmt == 'S')
{
String S = va_arg(args, String);
Serial.print(S);
}
else
{
Serial.print("<log(): invalid format char '");
Serial.print(*fmt);
Serial.print("'>");
}
fmt++;
}
va_end(args);
Serial.println("");
}

25
src/UI/DoughLogger.h Normal file
View File

@ -0,0 +1,25 @@
#ifndef DOUGH_LOGGER_H
#define DOUGH_LOGGER_H
#define LOGGER_PREFIX_BUFLEN 16
#define LOGGER_PREFIX_FORMAT "%10s | "
#include <Arduino.h>
#include <stdarg.h>
#include <WiFiNINA.h>
/**
* This class provides an interface for logging data in a structured
* manner to the serial console.
*/
class DoughLogger
{
public:
DoughLogger(const char *section);
void log(const char *fmt, ...);
private:
const char* _section;
};
#endif

View File

@ -1,10 +1,11 @@
#include "DoughUI.h" #include "DoughUI.h"
// ----------------------------------------------------------------------
// Constructor
// ----------------------------------------------------------------------
DoughUI *DoughUI::_instance = nullptr; DoughUI *DoughUI::_instance = nullptr;
/**
* Fetch the DoughUI singleton.
*/
DoughUI *DoughUI::Instance() DoughUI *DoughUI::Instance()
{ {
if (DoughUI::_instance == nullptr) if (DoughUI::_instance == nullptr)
@ -21,9 +22,10 @@ DoughUI::DoughUI() : onoffButton(ONOFF_BUTTON_PIN),
led2(LED2_PIN), led2(LED2_PIN),
led3(LED3_PIN) {} led3(LED3_PIN) {}
/** // ----------------------------------------------------------------------
* Called from the main setup() function of the sketch. // Setup
*/ // ----------------------------------------------------------------------
void DoughUI::setup() void DoughUI::setup()
{ {
// Setup the serial port, used for logging. // Setup the serial port, used for logging.
@ -54,7 +56,7 @@ void DoughUI::setup()
_setupTimerInterrupt(); _setupTimerInterrupt();
// Notify the user that we're on a roll! // Notify the user that we're on a roll!
flash_all_leds(); _flash_all_leds();
} }
void DoughUI::onoffButtonISR() void DoughUI::onoffButtonISR()
@ -67,58 +69,6 @@ void DoughUI::setupButtonISR()
DoughUI::Instance()->setupButton.handleButtonState(); DoughUI::Instance()->setupButton.handleButtonState();
} }
/**
* Log a message to the serial interface.
*/
void DoughUI::log(const char *category, const char *fmt, ...)
{
char buf[12];
snprintf(buf, sizeof(buf) / sizeof(buf[0]), "%8s | ", category);
Serial.print(buf);
va_list args;
va_start(args, fmt);
while (*fmt != '\0')
{
if (*fmt == 'i')
{
int i = va_arg(args, int);
Serial.print(i);
}
else if (*fmt == 'f')
{
float f = va_arg(args, double);
Serial.print(f);
}
else if (*fmt == 'a')
{
IPAddress a = va_arg(args, IPAddress);
Serial.print(a);
}
else if (*fmt == 's')
{
const char *s = va_arg(args, const char *);
Serial.print(s);
}
else if (*fmt == 'S')
{
String S = va_arg(args, String);
Serial.print(S);
}
else
{
Serial.print("<log(): invalid format char '");
Serial.print(*fmt);
Serial.print("'>");
}
fmt++;
}
va_end(args);
Serial.println("");
}
/** /**
* Setup a timer interrupt for updating the GUI. Unfortunately, the standard * Setup a timer interrupt for updating the GUI. Unfortunately, the standard
* libraries that I can find for this, are not equipped to work for the * libraries that I can find for this, are not equipped to work for the
@ -159,15 +109,9 @@ void DoughUI::_setupTimerInterrupt()
NVIC_EnableIRQ(TC4_IRQn); // Enable TC4 interrupts NVIC_EnableIRQ(TC4_IRQn); // Enable TC4 interrupts
} }
void DoughUI::resume() // ----------------------------------------------------------------------
{ // Loop
NVIC_EnableIRQ(TC4_IRQn); // Enable TC4 interrupts // ----------------------------------------------------------------------
}
void DoughUI::suspend()
{
NVIC_DisableIRQ(TC4_IRQn); // Disable TC4 interrupts
}
/** /**
* This callback is called when the TC4 timer hits an overflow interrupt. * This callback is called when the TC4 timer hits an overflow interrupt.
@ -178,6 +122,24 @@ void TC4_Handler()
REG_TC4_INTFLAG = TC_INTFLAG_OVF; // Clear the OVF interrupt flag. REG_TC4_INTFLAG = TC_INTFLAG_OVF; // Clear the OVF interrupt flag.
} }
/**
* Disables the TC4 interrupts, suspending timed async updates to
* the user interface.
*/
void DoughUI::suspend()
{
NVIC_DisableIRQ(TC4_IRQn);
}
/**
* Enables the TC4 interrupts, resuming timed async updates to the
* user interface.
*/
void DoughUI::resume()
{
NVIC_EnableIRQ(TC4_IRQn);
}
/** /**
* Fire pending button events. * Fire pending button events.
*/ */
@ -199,7 +161,7 @@ void DoughUI::clearButtonEvents()
/** /**
* Update the state of all the LEDs in the system. * Update the state of all the LEDs in the system.
* This method is called both sync by methods in this class and async by * This method is called both sync by methods in this class and async by
* the timer interrupt code from above. The timer interrupt based invocatino * the timer interrupt code from above. The timer interrupt based invocation
* makes it possible to do LED updates, while the device is busy doing * makes it possible to do LED updates, while the device is busy doing
* something else. * something else.
*/ */
@ -214,7 +176,7 @@ void DoughUI::updatedLEDs()
/** /**
* Flash all LEDs, one at a time. * Flash all LEDs, one at a time.
*/ */
void DoughUI::flash_all_leds() void DoughUI::_flash_all_leds()
{ {
ledBuiltin.on(); ledBuiltin.on();
delay(100); delay(100);

View File

@ -11,12 +11,15 @@
#undef LOG_WAIT_SERIAL #undef LOG_WAIT_SERIAL
#include <Arduino.h> #include <Arduino.h>
#include <WiFiNINA.h>
#include <stdarg.h> #include <stdarg.h>
#include "UI/DoughButton.h" #include "UI/DoughButton.h"
#include "UI/DoughLED.h" #include "UI/DoughLED.h"
#include "config.h" #include "config.h"
/**
* This class groups all user interface functionality: serial logging,
* LEDs and buttons.
*/
class DoughUI class DoughUI
{ {
public: public:
@ -33,15 +36,14 @@ public:
void processButtonEvents(); void processButtonEvents();
void clearButtonEvents(); void clearButtonEvents();
void updatedLEDs(); void updatedLEDs();
void flash_all_leds();
void resume(); void resume();
void suspend(); void suspend();
void log(const char *category, const char *fmt, ...);
private: private:
DoughUI(); DoughUI();
void _setupTimerInterrupt(); void _setupTimerInterrupt();
static DoughUI *_instance; static DoughUI *_instance;
void _flash_all_leds();
}; };
#endif #endif

View File

@ -7,6 +7,7 @@
// TODO: use longer term averages for data // TODO: use longer term averages for data
DoughBoyState state = CONFIGURING; DoughBoyState state = CONFIGURING;
auto logger = DoughLogger("MAIN");
void setup() void setup()
{ {
@ -18,7 +19,7 @@ void setup()
ui->setup(); ui->setup();
ui->onoffButton.onPress(handleOnoffButtonPress); ui->onoffButton.onPress(handleOnoffButtonPress);
ui->setupButton.onPress(handleSetupButtonPress); ui->setupButton.onPress(handleSetupButtonPress);
ui->log("MAIN", "s", "Initialization completed, starting device"); logger.log("s", "Initialization completed, starting device");
} }
void loop() void loop()
@ -76,11 +77,11 @@ bool setupNetworkConnection()
{ {
if (connectionState == CONNECTED) if (connectionState == CONNECTED)
{ {
ui->log("MAIN", "s", "ERROR - Connection to WiFi network lost! Reconnecting ..."); logger.log("s", "ERROR - Connection to WiFi network lost! Reconnecting ...");
} }
else else
{ {
ui->log("MAIN", "s", "Connecting to the WiFi network ..."); logger.log("s", "Connecting to the WiFi network ...");
} }
connectionState = CONNECTING_WIFI; connectionState = CONNECTING_WIFI;
ui->led1.blink()->slow(); ui->led1.blink()->slow();
@ -92,11 +93,11 @@ bool setupNetworkConnection()
{ {
if (connectionState == CONNECTED) if (connectionState == CONNECTED)
{ {
ui->log("MAIN", "s", "ERROR - Connection to the MQTT broker lost! Reconnecting ..."); logger.log("s", "ERROR - Connection to the MQTT broker lost! Reconnecting ...");
} }
else else
{ {
ui->log("MAIN", "s", "Connecting to the MQTT broker ..."); logger.log("s", "Connecting to the MQTT broker ...");
} }
connectionState = CONNECTING_MQTT; connectionState = CONNECTING_MQTT;
ui->led1.blink()->fast(); ui->led1.blink()->fast();
@ -108,7 +109,7 @@ bool setupNetworkConnection()
{ {
if (connectionState != CONNECTED) if (connectionState != CONNECTED)
{ {
ui->log("MAIN", "s", "Connection to MQTT broker established"); logger.log("s", "Connection to MQTT broker established");
ui->led1.on(); ui->led1.on();
ui->led2.off(); ui->led2.off();
ui->led3.off(); ui->led3.off();
@ -141,7 +142,7 @@ void handleSetupButtonPress()
void setStateToConfiguring() void setStateToConfiguring()
{ {
auto ui = DoughUI::Instance(); auto ui = DoughUI::Instance();
ui->log("MAIN", "s", "Waiting for configuration ..."); logger.log("s", "Waiting for configuration ...");
state = CONFIGURING; state = CONFIGURING;
ui->led1.on(); ui->led1.on();
ui->led2.blink()->fast(); ui->led2.blink()->fast();
@ -152,7 +153,7 @@ void setStateToConfiguring()
void setStateToMeasuring() void setStateToMeasuring()
{ {
auto ui = DoughUI::Instance(); auto ui = DoughUI::Instance();
ui->log("MAIN", "s", "Starting measurements"); logger.log("s", "Starting measurements");
state = MEASURING; state = MEASURING;
ui->led1.on(); ui->led1.on();
ui->led2.on(); ui->led2.on();
@ -163,7 +164,7 @@ void setStateToMeasuring()
void setStateToPaused() void setStateToPaused()
{ {
auto ui = DoughUI::Instance(); auto ui = DoughUI::Instance();
ui->log("MAIN", "s", "Pausing measurements"); logger.log("s", "Pausing measurements");
state = PAUSED; state = PAUSED;
ui->led1.on(); ui->led1.on();
ui->led2.on(); ui->led2.on();
@ -174,7 +175,7 @@ void setStateToPaused()
void setStateToCalibrating() void setStateToCalibrating()
{ {
auto ui = DoughUI::Instance(); auto ui = DoughUI::Instance();
ui->log("MAIN", "s", "Requested device calibration"); logger.log("s", "Requested device calibration");
state = CALIBRATING; state = CALIBRATING;
ui->led1.on(); ui->led1.on();
ui->led2.blink()->slow(); ui->led2.blink()->slow();