Completed moving all code into its own Dough namespace, to prevent naming clashes with stuff like WiFi and DHT11.

This commit is contained in:
Maurice Makaay 2020-07-12 22:41:23 +02:00
parent 3f8eb8b2b8
commit a31840c479
42 changed files with 1766 additions and 1750 deletions

View File

@ -1,21 +1,23 @@
#include "Data/DataController.h" #include "Data/DataController.h"
// ---------------------------------------------------------------------- namespace Dough
// Constructor
// ----------------------------------------------------------------------
DataController *DataController::_instance = nullptr;
DataController *DataController::Instance()
{ {
// ----------------------------------------------------------------------
// Constructor
// ----------------------------------------------------------------------
DataController *DataController::_instance = nullptr;
DataController *DataController::Instance()
{
if (DataController::_instance == nullptr) if (DataController::_instance == nullptr)
{ {
DataController::_instance = new DataController(); DataController::_instance = new DataController();
} }
return DataController::_instance; return DataController::_instance;
} }
DataController::DataController() : _temperatureMeasurements( DataController::DataController() : _temperatureMeasurements(
"temperature", "temperature",
TemperatureSensor::Instance(), TemperatureSensor::Instance(),
TEMPERATURE_AVG_LOOKBACK, TEMPERATURE_AVG_LOOKBACK,
@ -34,36 +36,36 @@ DataController::DataController() : _temperatureMeasurements(
DISTANCE_SIGNIFICANT_CHANGE, DISTANCE_SIGNIFICANT_CHANGE,
PUBLISH_INTERVAL), PUBLISH_INTERVAL),
_logger("DATA") _logger("DATA")
{ {
_ui = DoughUI::Instance(); _ui = UI::Instance();
_mqtt = Dough::MQTT::Instance(); _mqtt = MQTT::Instance();
} }
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
// Setup // Setup
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
void DataController::setup() void DataController::setup()
{ {
_containerHeight = 0.00; _containerHeight = 0.00;
_containerHeightSet = false; _containerHeightSet = false;
Dough::MQTT *mqtt = Dough::MQTT::Instance(); MQTT *mqtt = MQTT::Instance();
mqtt->onConnect(DataController::handleMqttConnect); mqtt->onConnect(DataController::handleMqttConnect);
mqtt->onMessage(DataController::handleMqttMessage); mqtt->onMessage(DataController::handleMqttMessage);
_temperatureMeasurements.setup(); _temperatureMeasurements.setup();
_humidityMeasurements.setup(); _humidityMeasurements.setup();
_distanceMeasurements.setup(); _distanceMeasurements.setup();
} }
void DataController::handleMqttConnect(Dough::MQTT *mqtt) void DataController::handleMqttConnect(MQTT *mqtt)
{ {
mqtt->subscribe("container_height"); mqtt->subscribe("container_height");
} }
void DataController::handleMqttMessage(String &key, String &payload) void DataController::handleMqttMessage(String &key, String &payload)
{ {
if (key.equals("container_height")) if (key.equals("container_height"))
{ {
DataController::Instance()->setContainerHeight(payload.toInt()); DataController::Instance()->setContainerHeight(payload.toInt());
@ -72,20 +74,18 @@ void DataController::handleMqttMessage(String &key, String &payload)
{ {
DataController::Instance()->_logger.log("sS", "ERROR - Unhandled MQTT message, key = ", key); DataController::Instance()->_logger.log("sS", "ERROR - Unhandled MQTT message, key = ", key);
} }
} }
bool DataController::isConfigured() bool DataController::isConfigured()
{ {
return _containerHeightSet; return _containerHeightSet;
} }
/** // Set the container height in mm. This is the distance between the sensor
* Set the container height in mm. This is the distance between the sensor // and the bottom of the container. It is used to determine the height of
* and the bottom of the container. It is used to determine the height of // the starter or dough by subtracting the distance measurement from it.
* the starter or dough by subtracting the distance measurement from it. void DataController::setContainerHeight(int height)
*/ {
void DataController::setContainerHeight(int height)
{
_containerHeightSet = false; _containerHeightSet = false;
if (height <= HCSR04_MIN_MM) if (height <= HCSR04_MIN_MM)
{ {
@ -104,31 +104,31 @@ void DataController::setContainerHeight(int height)
_logger.log("sis", "Set container height to ", height, "mm"); _logger.log("sis", "Set container height to ", height, "mm");
_containerHeight = height; _containerHeight = height;
_containerHeightSet = true; _containerHeightSet = true;
} }
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
// Loop // Loop
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
void DataController::loop() void DataController::loop()
{ {
if (isConfigured()) if (isConfigured())
{ {
_sample(); _sample();
} }
} }
void DataController::clearHistory() void DataController::clearHistory()
{ {
_temperatureMeasurements.clearHistory(); _temperatureMeasurements.clearHistory();
_humidityMeasurements.clearHistory(); _humidityMeasurements.clearHistory();
_distanceMeasurements.clearHistory(); _distanceMeasurements.clearHistory();
_sampleType = SAMPLE_TEMPERATURE; _sampleType = SAMPLE_TEMPERATURE;
_sampleCounter = 0; _sampleCounter = 0;
} }
void DataController::_sample() void DataController::_sample()
{ {
auto now = millis(); auto now = millis();
auto delta = now - _lastSample; auto delta = now - _lastSample;
auto tick = _lastSample == 0 || delta >= SAMPLE_INTERVAL; auto tick = _lastSample == 0 || delta >= SAMPLE_INTERVAL;
@ -173,4 +173,5 @@ void DataController::_sample()
_sampleType = SAMPLE_TEMPERATURE; _sampleType = SAMPLE_TEMPERATURE;
} }
} }
} }
} // namespace Dough

View File

@ -28,54 +28,55 @@
#define PUBLISH_INTERVAL 300 #define PUBLISH_INTERVAL 300
#include <Arduino.h> #include <Arduino.h>
#include "Data/Measurements.h" #include "Data/SensorController.h"
#include "Sensors/TemperatureSensor.h" #include "Sensors/TemperatureSensor.h"
#include "Sensors/HumiditySensor.h" #include "Sensors/HumiditySensor.h"
#include "Sensors/DistanceSensor.h" #include "Sensors/DistanceSensor.h"
#include "Network/DoughWiFi.h" #include "Network/WiFi.h"
#include "Network/MQTT.h" #include "Network/MQTT.h"
#include "UI/DoughUI.h" #include "UI/UI.h"
#include "UI/DoughLogger.h" #include "UI/Logger.h"
typedef enum namespace Dough
{ {
typedef enum
{
SAMPLE_TEMPERATURE, SAMPLE_TEMPERATURE,
SAMPLE_HUMIDITY, SAMPLE_HUMIDITY,
SAMPLE_DISTANCE SAMPLE_DISTANCE
} DoughSampleType; } DoughSampleType;
/** // This class is responsible for handling all things "data".
* This class is responsible for handling all things "data". // It holds the device configuration, collects measurements from sensors,
* It holds the device configuration, collects measurements from sensors, // gathers statistics on these data, 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(Dough::MQTT *mqtt); static void handleMqttConnect(MQTT *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;
DoughUI *_ui; UI *_ui;
Dough::MQTT *_mqtt; MQTT *_mqtt;
Measurements _temperatureMeasurements; SensorController _temperatureMeasurements;
Measurements _humidityMeasurements; SensorController _humidityMeasurements;
Measurements _distanceMeasurements; SensorController _distanceMeasurements;
DoughLogger _logger; Logger _logger;
unsigned long _lastSample = 0; unsigned long _lastSample = 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();
}; };
} // namespace Dough
#endif #endif

View File

@ -1,29 +1,32 @@
#include "Data/Measurement.h" #include "Data/Measurement.h"
Measurement::Measurement() { } namespace Dough
Measurement Measurement::Failed()
{ {
Measurement::Measurement() {}
Measurement Measurement::Failed()
{
Measurement m; Measurement m;
return m; return m;
} }
Measurement Measurement::Value(int value) Measurement Measurement::Value(int value)
{ {
Measurement m; Measurement m;
m.ok = true; m.ok = true;
m.value = value; m.value = value;
return m; return m;
} }
void Measurement::clear() void Measurement::clear()
{ {
ok = false; ok = false;
value = 0; value = 0;
} }
void Measurement::copyTo(Measurement* target) void Measurement::copyTo(Measurement *target)
{ {
target->ok = ok; target->ok = ok;
target->value = value; target->value = value;
} }
} // namespace Dough

View File

@ -1,13 +1,13 @@
#ifndef DOUGH_DATA_MEASUREMENT_H #ifndef DOUGH_DATA_MEASUREMENT_H
#define DOUGH_DATA_MEASUREMENT_H #define DOUGH_DATA_MEASUREMENT_H
/** namespace Dough
* This class represents a single measurement, which can be either a
* successful (bearing a measurement value) or a failed one.
*/
class Measurement
{ {
public: // This class represents a single measurement, which can be either a
// successful (bearing a measurement value) or a failed one.
class Measurement
{
public:
Measurement(); Measurement();
int value = 0; int value = 0;
bool ok = false; bool ok = false;
@ -15,6 +15,7 @@ public:
static Measurement Value(int value); static Measurement Value(int value);
void clear(); void clear();
void copyTo(Measurement *target); void copyTo(Measurement *target);
}; };
} // namespace Dough
#endif #endif

View File

@ -1,162 +0,0 @@
#include "Data/Measurements.h"
#include "UI/DoughUI.h"
Measurements::Measurements(
const char *mqttKey,
SensorBase *sensor,
unsigned int storageSize,
unsigned int significantChange,
unsigned int minimumPublishTime)
{
_mqttKey = mqttKey;
_sensor = sensor;
_storageSize = storageSize;
_significantChange = significantChange;
_minimumPublishTime = minimumPublishTime;
_mqtt = Dough::MQTT::Instance();
}
void Measurements::setup()
{
// Format the key to use for publishing the average (i.e. "<mqttKey>/average").
auto lenAverageKey = strlen(_mqttKey) + 9; // +9 for the "/average\0" suffix
_mqttAverageKey = new char[lenAverageKey];
snprintf(_mqttAverageKey, lenAverageKey, "%s/average", _mqttKey);
// Initialize the storage for holding the measurements.
_storage = new Measurement *[_storageSize];
for (unsigned int i = 0; i < _storageSize; i++)
{
_storage[i] = new Measurement;
}
clearHistory();
}
void Measurements::process()
{
auto m = _sensor->read();
_store(m);
if (_mustPublish())
{
_publish();
}
}
bool Measurements::_mustPublish()
{
Measurement lastMeasurement = getLast();
// When the measurement failed, then there's no need to publish it.
if (lastMeasurement.ok == false)
{
return false;
}
// When no data was published before, then this is a great time to do so.
if (_lastPublished.ok == false)
{
return true;
}
// If the value did not change, only publish when the minimum publishing
// time has passed.
if (_lastPublished.value == lastMeasurement.value)
{
auto now = millis();
auto delta = now - _lastPublishedAt;
return _lastPublishedAt == 0 || delta >= (_minimumPublishTime * 1000);
}
// When there is a significant change in the sensor value, then publish.
if (abs(_lastPublished.value - lastMeasurement.value) >= _significantChange)
{
return true;
}
auto average = getAverage();
// When there is a significant change in the average value, then publish.
if (average.ok && abs(_lastPublishedAverage.value - average.value) >= _significantChange)
{
return true;
}
// When the value changed less than the significant change, but it reached
// the current average value, then publish it, since we might have reached
// a stable value.
if (average.ok && average.value == lastMeasurement.value)
{
return true;
}
// Well, we're out of options. No reason to publish the data right now.
return false;
}
void Measurements::_publish()
{
auto average = getAverage();
auto last = getLast();
_mqtt->publish(_mqttKey, last);
_mqtt->publish(_mqttAverageKey, average);
_lastPublishedAt = millis();
average.copyTo(&_lastPublishedAverage);
last.copyTo(&_lastPublished);
}
void Measurements::_store(Measurement measurement)
{
measurement.copyTo(_storage[_next()]);
if (measurement.ok)
{
_averageCount++;
_averageSum += measurement.value;
}
}
unsigned int Measurements::_next()
{
_index++;
// Wrap around at the end of the circular buffer.
if (_index == _storageSize)
{
_index = 0;
}
// If the new position contains an ok value, update the running totals.
if (_storage[_index]->ok)
{
_averageSum -= _storage[_index]->value;
_averageCount--;
}
_storage[_index]->clear();
return _index;
}
Measurement Measurements::getLast()
{
return *_storage[_index];
}
Measurement Measurements::getAverage()
{
return _averageCount > 0
? Measurement::Value(round(_averageSum / _averageCount))
: Measurement::Failed();
}
void Measurements::clearHistory()
{
_averageCount = 0;
_averageSum = 0;
for (unsigned int i = 0; i < _storageSize; i++)
{
_storage[i]->clear();
}
}

View File

@ -1,66 +0,0 @@
#ifndef DOUGH_DATA_MEASUREMENTS_H
#define DOUGH_DATA_MEASUREMENTS_H
#include <Arduino.h>
#include "Sensors/SensorBase.h"
#include "Data/Measurement.h"
#include "Network/MQTT.h"
/**
* This class is used to store measurements for a sensor and to keep
* track of running totals for handling average computations.
* It also provides functionality to decide when to publish measurements
* to MQTT (after significant changes occur or when the last publish
* was too long ago).
*/
class Measurements
{
public:
/**
* Create a new Measurements object.
*
* @param sensor
* The sensor to read, implements SensorBase.
* @param storageSize
* Number of measurements to keep track of for computing an average.
* @param significantChange
* Number that describes how much a measurement value needs to change,
* before it is considered significant and must be published to MQTT.
* @param minimumPublishTime
* The number of seconds after which to forcibly publish measurements
* to MQTT, even when no significant changes to measurements were seen.
*/
Measurements(
const char *mqttKey,
SensorBase *sensor,
unsigned int storageSize,
unsigned int significantChange,
unsigned int minimumPublishTime);
void setup();
void process();
Measurement getLast();
Measurement getAverage();
void clearHistory();
private:
Dough::MQTT *_mqtt;
const char *_mqttKey;
char *_mqttAverageKey;
SensorBase *_sensor;
Measurement **_storage;
unsigned int _storageSize;
unsigned int _significantChange;
unsigned int _minimumPublishTime;
int _averageSum = 0;
unsigned int _averageCount = 0;
unsigned int _index = 0;
unsigned long _lastPublishedAt = 0;
Measurement _lastPublished;
Measurement _lastPublishedAverage;
bool _mustPublish();
void _publish();
void _store(Measurement measurement);
unsigned int _next();
};
#endif

View File

@ -0,0 +1,165 @@
#include "Data/SensorController.h"
#include "UI/UI.h"
namespace Dough
{
SensorController::SensorController(
const char *mqttKey,
SensorBase *sensor,
unsigned int storageSize,
unsigned int significantChange,
unsigned int minimumPublishTime)
{
_mqttKey = mqttKey;
_sensor = sensor;
_storageSize = storageSize;
_significantChange = significantChange;
_minimumPublishTime = minimumPublishTime;
_mqtt = MQTT::Instance();
}
void SensorController::setup()
{
// Format the key to use for publishing the average (i.e. "<mqttKey>/average").
auto lenAverageKey = strlen(_mqttKey) + 9; // +9 for the "/average\0" suffix
_mqttAverageKey = new char[lenAverageKey];
snprintf(_mqttAverageKey, lenAverageKey, "%s/average", _mqttKey);
// Initialize the storage for holding the measurements.
_storage = new Measurement *[_storageSize];
for (unsigned int i = 0; i < _storageSize; i++)
{
_storage[i] = new Measurement;
}
clearHistory();
}
void SensorController::process()
{
auto m = _sensor->read();
_store(m);
if (_mustPublish())
{
_publish();
}
}
bool SensorController::_mustPublish()
{
Measurement lastMeasurement = getLast();
// When the measurement failed, then there's no need to publish it.
if (lastMeasurement.ok == false)
{
return false;
}
// When no data was published before, then this is a great time to do so.
if (_lastPublished.ok == false)
{
return true;
}
// If the value did not change, only publish when the minimum publishing
// time has passed.
if (_lastPublished.value == lastMeasurement.value)
{
auto now = millis();
auto delta = now - _lastPublishedAt;
return _lastPublishedAt == 0 || delta >= (_minimumPublishTime * 1000);
}
// When there is a significant change in the sensor value, then publish.
if (abs(_lastPublished.value - lastMeasurement.value) >= _significantChange)
{
return true;
}
auto average = getAverage();
// When there is a significant change in the average value, then publish.
if (average.ok && abs(_lastPublishedAverage.value - average.value) >= _significantChange)
{
return true;
}
// When the value changed less than the significant change, but it reached
// the current average value, then publish it, since we might have reached
// a stable value.
if (average.ok && average.value == lastMeasurement.value)
{
return true;
}
// Well, we're out of options. No reason to publish the data right now.
return false;
}
void SensorController::_publish()
{
auto average = getAverage();
auto last = getLast();
_mqtt->publish(_mqttKey, last);
_mqtt->publish(_mqttAverageKey, average);
_lastPublishedAt = millis();
average.copyTo(&_lastPublishedAverage);
last.copyTo(&_lastPublished);
}
void SensorController::_store(Measurement measurement)
{
measurement.copyTo(_storage[_next()]);
if (measurement.ok)
{
_averageCount++;
_averageSum += measurement.value;
}
}
unsigned int SensorController::_next()
{
_index++;
// Wrap around at the end of the circular buffer.
if (_index == _storageSize)
{
_index = 0;
}
// If the new position contains an ok value, update the running totals.
if (_storage[_index]->ok)
{
_averageSum -= _storage[_index]->value;
_averageCount--;
}
_storage[_index]->clear();
return _index;
}
Measurement SensorController::getLast()
{
return *_storage[_index];
}
Measurement SensorController::getAverage()
{
return _averageCount > 0
? Measurement::Value(round(_averageSum / _averageCount))
: Measurement::Failed();
}
void SensorController::clearHistory()
{
_averageCount = 0;
_averageSum = 0;
for (unsigned int i = 0; i < _storageSize; i++)
{
_storage[i]->clear();
}
}
} // namespace Dough

View File

@ -0,0 +1,65 @@
#ifndef DOUGH_DATA_MEASUREMENTS_H
#define DOUGH_DATA_MEASUREMENTS_H
#include <Arduino.h>
#include "Sensors/SensorBase.h"
#include "Data/Measurement.h"
#include "Network/MQTT.h"
namespace Dough
{
// This class is used to store measurements for a sensor and to keep
// track of running totals for handling average computations.
// It also provides functionality to decide when to publish measurements
// to MQTT (after significant changes occur or when the last publish
// was too long ago).
class SensorController
{
public:
// Create a new Measurements object.
//
// @param sensor
// The sensor to read, implements SensorBase.
// @param storageSize
// Number of measurements to keep track of for computing an average.
// @param significantChange
// Number that describes how much a measurement value needs to change,
// before it is considered significant and must be published to MQTT.
// @param minimumPublishTime
// The number of seconds after which to forcibly publish measurements
// to MQTT, even when no significant changes to measurements were seen.
SensorController(
const char *mqttKey,
SensorBase *sensor,
unsigned int storageSize,
unsigned int significantChange,
unsigned int minimumPublishTime);
void setup();
void process();
Measurement getLast();
Measurement getAverage();
void clearHistory();
private:
MQTT *_mqtt;
const char *_mqttKey;
char *_mqttAverageKey;
SensorBase *_sensor;
Measurement **_storage;
unsigned int _storageSize;
unsigned int _significantChange;
unsigned int _minimumPublishTime;
int _averageSum = 0;
unsigned int _averageCount = 0;
unsigned int _index = 0;
unsigned long _lastPublishedAt = 0;
Measurement _lastPublished;
Measurement _lastPublishedAverage;
bool _mustPublish();
void _publish();
void _store(Measurement measurement);
unsigned int _next();
};
} // namespace Dough
#endif

View File

@ -1,87 +0,0 @@
#include "Network/DoughWiFi.h"
// ----------------------------------------------------------------------
// Constructor
// ----------------------------------------------------------------------
DoughWiFi *DoughWiFi::_instance = nullptr;
DoughWiFi *DoughWiFi::Instance()
{
if (DoughWiFi::_instance == nullptr)
{
DoughWiFi::_instance = new DoughWiFi();
}
return DoughWiFi::_instance;
}
DoughWiFi::DoughWiFi() : _logger("WIFI") {}
// ----------------------------------------------------------------------
// Setup
// ----------------------------------------------------------------------
void DoughWiFi::_setMacAddress()
{
byte mac[6];
WiFi.macAddress(mac);
snprintf(
_macAddress, sizeof(_macAddress) / sizeof(_macAddress[0]),
"%x:%x:%x:%x:%x:%x", mac[5], mac[4], mac[3], mac[2], mac[1], mac[0]);
}
void DoughWiFi::setup()
{
_setMacAddress();
_logger.log("ss", "MAC address = ", getMacAddress());
}
// ----------------------------------------------------------------------
// Loop
// ----------------------------------------------------------------------
bool DoughWiFi::isConnected()
{
return WiFi.status() == WL_CONNECTED;
}
bool DoughWiFi::connect()
{
int status = WiFi.status();
// Check if a device with a WiFi shield is used.
if (status == WL_NO_SHIELD)
{
_logger.log("s", "ERROR - Device has no WiFi shield");
delay(5000);
return false;
}
// Check if the WiFi network is already up.
if (status == WL_CONNECTED)
{
return true;
}
// Setup the connection to the WiFi network.
_logger.log("ss", "WiFi network = ", WIFI_SSID);
status = WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
// Check if the connection attempt was successful.
if (status == WL_CONNECTED)
{
_logger.log("sa", "IP-Address = ", WiFi.localIP());
_logger.log("sis", "Signal strength = ", WiFi.RSSI(), " dBm");
return true;
}
else
{
_logger.log("sis", "ERROR - WiFi connection failed (reason: ", WiFi.reasonCode(), ")");
return false;
}
}
char *DoughWiFi::getMacAddress()
{
return _macAddress;
}

View File

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

View File

@ -1,4 +1,4 @@
#include "MQTT.h" #include "Network/MQTT.h"
namespace Dough namespace Dough
{ {
@ -26,7 +26,7 @@ namespace Dough
void MQTT::setup() void MQTT::setup()
{ {
DoughWiFi *network = DoughWiFi::Instance(); WiFi *network = WiFi::Instance();
#ifdef MQTT_DEVICE_ID #ifdef MQTT_DEVICE_ID
_mqttDeviceId = MQTT_DEVICE_ID; _mqttDeviceId = MQTT_DEVICE_ID;

View File

@ -3,17 +3,15 @@
#include <MQTT.h> #include <MQTT.h>
#include <MQTTClient.h> #include <MQTTClient.h>
#include "Network/DoughWiFi.h" #include "Network/WiFi.h"
#include "Data/Measurement.h" #include "Data/Measurement.h"
#include "UI/DoughLogger.h" #include "UI/Logger.h"
#include "config.h" #include "config.h"
namespace Dough namespace Dough
{ {
/** // This class encapsulates the connection to the MQTT broker.
* This class encapsulates the connection to the MQTT broker. // MQTT is used to publish measurements and to store configuration data.
* MQTT is used to publish measurements and to store configuration data.
*/
class MQTT; class MQTT;
typedef void (*MQTTConnectHandler)(MQTT *mqtt); typedef void (*MQTTConnectHandler)(MQTT *mqtt);
@ -38,7 +36,7 @@ namespace Dough
MQTT(); MQTT();
static MQTT *_instance; static MQTT *_instance;
MQTTClient _mqttClient; MQTTClient _mqttClient;
DoughLogger _logger; Logger _logger;
MQTTConnectHandler _onConnect = nullptr; MQTTConnectHandler _onConnect = nullptr;
MQTTClientCallbackSimple _onMessage = nullptr; MQTTClientCallbackSimple _onMessage = nullptr;
static void handleMessage(String &topic, String &payload); static void handleMessage(String &topic, String &payload);

90
src/Network/WiFi.cpp Normal file
View File

@ -0,0 +1,90 @@
#include "Network/WiFi.h"
namespace Dough
{
// ----------------------------------------------------------------------
// Constructor
// ----------------------------------------------------------------------
WiFi *WiFi::_instance = nullptr;
WiFi *WiFi::Instance()
{
if (WiFi::_instance == nullptr)
{
WiFi::_instance = new WiFi();
}
return WiFi::_instance;
}
WiFi::WiFi() : _logger("WIFI") {}
// ----------------------------------------------------------------------
// Setup
// ----------------------------------------------------------------------
void WiFi::_setMacAddress()
{
byte mac[6];
::WiFi.macAddress(mac);
snprintf(
_macAddress, sizeof(_macAddress) / sizeof(_macAddress[0]),
"%x:%x:%x:%x:%x:%x", mac[5], mac[4], mac[3], mac[2], mac[1], mac[0]);
}
void WiFi::setup()
{
_setMacAddress();
_logger.log("ss", "MAC address = ", getMacAddress());
}
// ----------------------------------------------------------------------
// Loop
// ----------------------------------------------------------------------
bool WiFi::isConnected()
{
return ::WiFi.status() == WL_CONNECTED;
}
bool WiFi::connect()
{
int status = ::WiFi.status();
// Check if a device with a WiFi shield is used.
if (status == WL_NO_SHIELD)
{
_logger.log("s", "ERROR - Device has no WiFi shield");
delay(5000);
return false;
}
// Check if the WiFi network is already up.
if (status == WL_CONNECTED)
{
return true;
}
// Setup the connection to the WiFi network.
_logger.log("ss", "WiFi network = ", WIFI_SSID);
status = ::WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
// Check if the connection attempt was successful.
if (status == WL_CONNECTED)
{
_logger.log("sa", "IP-Address = ", ::WiFi.localIP());
_logger.log("sis", "Signal strength = ", ::WiFi.RSSI(), " dBm");
return true;
}
else
{
_logger.log("sis", "ERROR - WiFi connection failed (reason: ", ::WiFi.reasonCode(), ")");
return false;
}
}
char *WiFi::getMacAddress()
{
return _macAddress;
}
} // namespace Dough

31
src/Network/WiFi.h Normal file
View File

@ -0,0 +1,31 @@
#ifndef DOUGH_NETWORK_H
#define DOUGH_NETWORK_H
#include <WiFiNINA.h>
#include "UI/Logger.h"
#include "config.h"
namespace Dough
{
// This class encapsulates the connection to the WiFi network.
class WiFi
{
public:
static WiFi *Instance();
char *getMacAddress();
void setup();
void loop();
bool isConnected();
bool connect();
WiFiClient client;
private:
WiFi();
static WiFi *_instance;
void _setMacAddress();
char _macAddress[18]; // max MAC address length + 1
Logger _logger;
};
} // namespace Dough
#endif

View File

@ -1,50 +1,52 @@
#include "DistanceSensor.h" #include "DistanceSensor.h"
// ---------------------------------------------------------------------- namespace Dough
// Constructor
// ----------------------------------------------------------------------
DistanceSensor *DistanceSensor::_instance = nullptr;
DistanceSensor *DistanceSensor::Instance()
{ {
// ----------------------------------------------------------------------
// Constructor
// ----------------------------------------------------------------------
DistanceSensor *DistanceSensor::_instance = nullptr;
DistanceSensor *DistanceSensor::Instance()
{
if (DistanceSensor::_instance == nullptr) if (DistanceSensor::_instance == nullptr)
{ {
DistanceSensor::_instance = new DistanceSensor(); DistanceSensor::_instance = new DistanceSensor();
} }
return DistanceSensor::_instance; return DistanceSensor::_instance;
} }
DistanceSensor::DistanceSensor() : _logger("DISTANCE") DistanceSensor::DistanceSensor() : _logger("DISTANCE")
{ {
_hcsr04 = new SensorHCSR04(HCSR04_TRIG_PIN, HCSR04_ECHO_PIN); _hcsr04 = new SensorHCSR04(HCSR04_TRIG_PIN, HCSR04_ECHO_PIN);
} }
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
// setup // setup
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
void DistanceSensor::setup() void DistanceSensor::setup()
{ {
_hcsr04->setup(); _hcsr04->setup();
} }
void DistanceSensor::setTemperature(int temperature) void DistanceSensor::setTemperature(int temperature)
{ {
_hcsr04->setTemperature(temperature); _hcsr04->setTemperature(temperature);
} }
void DistanceSensor::setHumidity(int humidity) void DistanceSensor::setHumidity(int humidity)
{ {
_hcsr04->setHumidity(humidity); _hcsr04->setHumidity(humidity);
} }
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
// loop // loop
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
Measurement DistanceSensor::read() Measurement DistanceSensor::read()
{ {
int d = _hcsr04->readDistance(); int d = _hcsr04->readDistance();
if (d == -1) if (d == -1)
{ {
@ -56,4 +58,5 @@ Measurement DistanceSensor::read()
_logger.log("sis", "Distance = ", d, "mm"); _logger.log("sis", "Distance = ", d, "mm");
return Measurement::Value(d); return Measurement::Value(d);
} }
} }
} // namespace Dough

View File

@ -3,27 +3,28 @@
#include "Sensors/SensorBase.h" #include "Sensors/SensorBase.h"
#include "Sensors/LowLevel/SensorHCSR04.h" #include "Sensors/LowLevel/SensorHCSR04.h"
#include "UI/DoughLogger.h" #include "UI/Logger.h"
#include "Data/Measurement.h" #include "Data/Measurement.h"
#include "config.h" #include "config.h"
/** namespace Dough
* This class provides access to the distance sensor in the device.
*/
class DistanceSensor : public SensorBase
{ {
public: // This class provides access to the distance sensor in the device.
class DistanceSensor : public SensorBase
{
public:
static DistanceSensor *Instance(); static DistanceSensor *Instance();
virtual void setup(); virtual void setup();
virtual Measurement read(); virtual Measurement read();
void setTemperature(int temperature); void setTemperature(int temperature);
void setHumidity(int humidity); void setHumidity(int humidity);
private: private:
DistanceSensor(); DistanceSensor();
static DistanceSensor *_instance; static DistanceSensor *_instance;
DoughLogger _logger; Logger _logger;
SensorHCSR04 *_hcsr04; SensorHCSR04 *_hcsr04;
}; };
} // namespace Dough
#endif #endif

View File

@ -1,37 +1,39 @@
#include "HumiditySensor.h" #include "HumiditySensor.h"
// ---------------------------------------------------------------------- namespace Dough
// Constructor
// ----------------------------------------------------------------------
HumiditySensor *HumiditySensor::_instance = nullptr;
HumiditySensor *HumiditySensor::Instance()
{ {
// ----------------------------------------------------------------------
// Constructor
// ----------------------------------------------------------------------
HumiditySensor *HumiditySensor::_instance = nullptr;
HumiditySensor *HumiditySensor::Instance()
{
if (HumiditySensor::_instance == nullptr) if (HumiditySensor::_instance == nullptr)
{ {
HumiditySensor::_instance = new HumiditySensor(); HumiditySensor::_instance = new HumiditySensor();
} }
return HumiditySensor::_instance; return HumiditySensor::_instance;
} }
HumiditySensor::HumiditySensor() : _logger("HUMIDITY") {} HumiditySensor::HumiditySensor() : _logger("HUMIDITY") {}
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
// setup // setup
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
void HumiditySensor::setup() void HumiditySensor::setup()
{ {
SensorDHT11::Instance()->begin(); SensorDHT11::Instance()->begin();
} }
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
// loop // loop
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
Measurement HumiditySensor::read() Measurement HumiditySensor::read()
{ {
float t = SensorDHT11::Instance()->readHumidity(); float t = SensorDHT11::Instance()->readHumidity();
if (t == -1) if (t == -1)
{ {
@ -44,4 +46,5 @@ Measurement HumiditySensor::read()
DistanceSensor::Instance()->setHumidity(int(t)); DistanceSensor::Instance()->setHumidity(int(t));
return Measurement::Value(int(t)); return Measurement::Value(int(t));
} }
} }
} // namespace Dough

View File

@ -3,25 +3,26 @@
#include "Sensors/SensorBase.h" #include "Sensors/SensorBase.h"
#include "Sensors/LowLevel/SensorDHT11.h" #include "Sensors/LowLevel/SensorDHT11.h"
#include "UI/DoughLogger.h" #include "UI/Logger.h"
#include "Data/Measurement.h" #include "Data/Measurement.h"
#include "Sensors/DistanceSensor.h" #include "Sensors/DistanceSensor.h"
#include "config.h" #include "config.h"
/** namespace Dough
* This class provides access to the humidity sensor in the device.
*/
class HumiditySensor : public SensorBase
{ {
public: // This class provides access to the humidity sensor in the device.
class HumiditySensor : public SensorBase
{
public:
static HumiditySensor *Instance(); static HumiditySensor *Instance();
virtual void setup(); virtual void setup();
virtual Measurement read(); virtual Measurement read();
private: private:
HumiditySensor(); HumiditySensor();
static HumiditySensor *_instance; static HumiditySensor *_instance;
DoughLogger _logger; Logger _logger;
}; };
} // namespace Dough
#endif #endif

View File

@ -1,44 +1,47 @@
#include "Sensors/LowLevel/SensorDHT11.h" #include "Sensors/LowLevel/SensorDHT11.h"
// ---------------------------------------------------------------------- namespace Dough
// Constructor
// ----------------------------------------------------------------------
SensorDHT11 *SensorDHT11::_instance = nullptr;
SensorDHT11 *SensorDHT11::Instance()
{ {
// ----------------------------------------------------------------------
// Constructor
// ----------------------------------------------------------------------
SensorDHT11 *SensorDHT11::_instance = nullptr;
SensorDHT11 *SensorDHT11::Instance()
{
if (SensorDHT11::_instance == nullptr) if (SensorDHT11::_instance == nullptr)
{ {
SensorDHT11::_instance = new SensorDHT11(); SensorDHT11::_instance = new SensorDHT11();
} }
return SensorDHT11::_instance; return SensorDHT11::_instance;
} }
SensorDHT11::SensorDHT11() SensorDHT11::SensorDHT11()
{ {
_dht = new DHT(DHT11_DATA_PIN, DHT11); _dht = new DHT(DHT11_DATA_PIN, DHT11);
} }
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
// setup // setup
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
void SensorDHT11::begin() void SensorDHT11::begin()
{ {
_dht->begin(); _dht->begin();
} }
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
// loop // loop
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
float SensorDHT11::readHumidity() float SensorDHT11::readHumidity()
{ {
return _dht->readHumidity(); return _dht->readHumidity();
} }
float SensorDHT11::readTemperature() float SensorDHT11::readTemperature()
{ {
return _dht->readTemperature(); return _dht->readTemperature();
} }
} // namespace Dough

View File

@ -4,21 +4,22 @@
#include <DHT.h> #include <DHT.h>
#include "config.h" #include "config.h"
/** namespace Dough
* This class provides access to the DHT11 sensor in the device.
*/
class SensorDHT11
{ {
public: // This class provides access to the DHT11 sensor in the device.
class SensorDHT11
{
public:
static SensorDHT11 *Instance(); static SensorDHT11 *Instance();
void begin(); void begin();
float readTemperature(); float readTemperature();
float readHumidity(); float readHumidity();
private: private:
SensorDHT11(); SensorDHT11();
static SensorDHT11 *_instance; static SensorDHT11 *_instance;
DHT *_dht; DHT *_dht;
}; };
} // namespace Dough
#endif #endif

View File

@ -1,42 +1,42 @@
#include "Sensors/LowLevel/SensorHCSR04.h" #include "Sensors/LowLevel/SensorHCSR04.h"
SensorHCSR04::SensorHCSR04(int triggerPin, int echoPin) : _logger("HCSR04") namespace Dough
{ {
SensorHCSR04::SensorHCSR04(int triggerPin, int echoPin) : _logger("HCSR04")
{
_triggerPin = triggerPin; _triggerPin = triggerPin;
_echoPin = echoPin; _echoPin = echoPin;
_temperature = HCSR04_INIT_TEMPERATURE; _temperature = HCSR04_INIT_TEMPERATURE;
_humidity = HCSR04_INIT_HUMIDITY; _humidity = HCSR04_INIT_HUMIDITY;
#ifndef HCSR04_DEBUG #ifndef HCSR04_DEBUG
_logger.suspend(); _logger.suspend();
#endif #endif
} }
void SensorHCSR04::setup() void SensorHCSR04::setup()
{ {
_logger.log("sisi", "Setup output pin ", _triggerPin, " and input pin ", _echoPin); _logger.log("sisi", "Setup output pin ", _triggerPin, " and input pin ", _echoPin);
pinMode(_triggerPin, OUTPUT); pinMode(_triggerPin, OUTPUT);
pinMode(_echoPin, INPUT); pinMode(_echoPin, INPUT);
} }
void SensorHCSR04::setTemperature(int temperature) void SensorHCSR04::setTemperature(int temperature)
{ {
_logger.log("sis", "Set temperature to ", temperature, "°C"); _logger.log("sis", "Set temperature to ", temperature, "°C");
_temperature = temperature; _temperature = temperature;
} }
void SensorHCSR04::setHumidity(int humidity) void SensorHCSR04::setHumidity(int humidity)
{ {
_logger.log("sis", "Set humidity to ", humidity, "%"); _logger.log("sis", "Set humidity to ", humidity, "%");
_humidity = humidity; _humidity = humidity;
} }
/** // Get a distance reading.
* Get a distance reading. // When reading the distance fails, -1 is returned.
* When reading the distance fails, -1 is returned. // Otherwise the distance in mm.
* Otherwise the distance in mm. int SensorHCSR04::readDistance()
*/ {
int SensorHCSR04::readDistance()
{
_setSpeedOfSound(); _setSpeedOfSound();
_setEchoTimeout(); _setEchoTimeout();
_takeSamples(); _takeSamples();
@ -46,30 +46,28 @@ int SensorHCSR04::readDistance()
return _computeAverage(); return _computeAverage();
} }
return -1; return -1;
} }
/** // Sets the speed of sound in mm/Ms, depending on the temperature
* Sets the speed of sound in mm/Ms, depending on the temperature // and relative humidity. I derived this formula from a YouTube
* and relative humidity. I derived this formula from a YouTube // video about the HC-SR04: https://youtu.be/6F1B_N6LuKw?t=1548
* video about the HC-SR04: https://youtu.be/6F1B_N6LuKw?t=1548 void SensorHCSR04::_setSpeedOfSound()
*/ {
void SensorHCSR04::_setSpeedOfSound()
{
_speedOfSound = _speedOfSound =
0.3314 + 0.3314 +
(0.000606 * _temperature) + (0.000606 * _temperature) +
(0.0000124 * _humidity); (0.0000124 * _humidity);
_logger.log("sfs", "Speed of sound = ", _speedOfSound, "mm/Ms"); _logger.log("sfs", "Speed of sound = ", _speedOfSound, "mm/Ms");
} }
void SensorHCSR04::_setEchoTimeout() void SensorHCSR04::_setEchoTimeout()
{ {
_echoTimeout = HCSR04_MAX_MM * 2 / _speedOfSound; _echoTimeout = HCSR04_MAX_MM * 2 / _speedOfSound;
_logger.log("sfs", "Echo timeout = ", _echoTimeout, "Ms"); _logger.log("sfs", "Echo timeout = ", _echoTimeout, "Ms");
} }
void SensorHCSR04::_takeSamples() void SensorHCSR04::_takeSamples()
{ {
_successfulSamples = 0; _successfulSamples = 0;
for (int i = 0; i < HCSR04_SAMPLES_TAKE; i++) for (int i = 0; i < HCSR04_SAMPLES_TAKE; i++)
{ {
@ -87,15 +85,15 @@ void SensorHCSR04::_takeSamples()
_successfulSamples++; _successfulSamples++;
} }
} }
} }
bool SensorHCSR04::_haveEnoughSamples() bool SensorHCSR04::_haveEnoughSamples()
{ {
return _successfulSamples >= HCSR04_SAMPLES_USE; return _successfulSamples >= HCSR04_SAMPLES_USE;
} }
int SensorHCSR04::_takeSample() int SensorHCSR04::_takeSample()
{ {
// Send 10μs trigger to ask sensor for a measurement. // Send 10μs trigger to ask sensor for a measurement.
digitalWrite(HCSR04_TRIG_PIN, LOW); digitalWrite(HCSR04_TRIG_PIN, LOW);
delayMicroseconds(2); delayMicroseconds(2);
@ -117,10 +115,10 @@ int SensorHCSR04::_takeSample()
{ {
return distance; return distance;
} }
} }
void SensorHCSR04::_sortSamples() void SensorHCSR04::_sortSamples()
{ {
int holder, x, y; int holder, x, y;
for (x = 0; x < _successfulSamples; x++) for (x = 0; x < _successfulSamples; x++)
{ {
@ -134,16 +132,14 @@ void SensorHCSR04::_sortSamples()
} }
} }
} }
} }
/** // Compute the average of the samples. To get rid of measuring extremes,
* Compute the average of the samples. To get rid of measuring extremes, // only a subset of measurements from the middle are used.
* only a subset of measurements from the middle are used. // When not enough samples were collected in the previous steps, then
* When not enough samples were collected in the previous steps, then // NAN is returned.
* NAN is returned. int SensorHCSR04::_computeAverage()
*/ {
int SensorHCSR04::_computeAverage()
{
float sum = 0; float sum = 0;
int offset = (_successfulSamples - HCSR04_SAMPLES_USE) / 2; int offset = (_successfulSamples - HCSR04_SAMPLES_USE) / 2;
for (int i = 0; i < HCSR04_SAMPLES_USE; i++) for (int i = 0; i < HCSR04_SAMPLES_USE; i++)
@ -152,4 +148,5 @@ int SensorHCSR04::_computeAverage()
} }
return round(sum / HCSR04_SAMPLES_USE); return round(sum / HCSR04_SAMPLES_USE);
} }
} // namespace Dough

View File

@ -29,23 +29,23 @@
#undef HCSR04_DEBUG #undef HCSR04_DEBUG
#include <Arduino.h> #include <Arduino.h>
#include "UI/DoughLogger.h" #include "UI/Logger.h"
#include "config.h" #include "config.h"
/** namespace Dough
* This class is used to get a distance reading from an HCSR04 sensor.
*/
class SensorHCSR04
{ {
public: // This class is used to get a distance reading from an HCSR04 sensor.
class SensorHCSR04
{
public:
SensorHCSR04(int triggerPin, int echoPin); SensorHCSR04(int triggerPin, int echoPin);
void setup(); void setup();
void setTemperature(int temperature); void setTemperature(int temperature);
void setHumidity(int humidity); void setHumidity(int humidity);
int readDistance(); int readDistance();
private: private:
DoughLogger _logger; Logger _logger;
int _triggerPin; int _triggerPin;
int _echoPin; int _echoPin;
int _humidity; int _humidity;
@ -61,6 +61,7 @@ private:
int _successfulSamples; int _successfulSamples;
void _sortSamples(); void _sortSamples();
int _computeAverage(); int _computeAverage();
}; };
} // namespace Dough
#endif #endif

View File

@ -3,14 +3,15 @@
#include "Data/Measurement.h" #include "Data/Measurement.h"
/** namespace Dough
* This interface is implemented by all sensors.
*/
class SensorBase
{ {
public: // This interface is implemented by all sensors.
class SensorBase
{
public:
virtual void setup(); virtual void setup();
virtual Measurement read(); virtual Measurement read();
}; };
}
#endif #endif

View File

@ -1,37 +1,39 @@
#include "TemperatureSensor.h" #include "TemperatureSensor.h"
// ---------------------------------------------------------------------- namespace Dough
// Constructor
// ----------------------------------------------------------------------
TemperatureSensor *TemperatureSensor::_instance = nullptr;
TemperatureSensor *TemperatureSensor::Instance()
{ {
// ----------------------------------------------------------------------
// Constructor
// ----------------------------------------------------------------------
TemperatureSensor *TemperatureSensor::_instance = nullptr;
TemperatureSensor *TemperatureSensor::Instance()
{
if (TemperatureSensor::_instance == nullptr) if (TemperatureSensor::_instance == nullptr)
{ {
TemperatureSensor::_instance = new TemperatureSensor(); TemperatureSensor::_instance = new TemperatureSensor();
} }
return TemperatureSensor::_instance; return TemperatureSensor::_instance;
} }
TemperatureSensor::TemperatureSensor() : _logger("TEMPERATURE") {} TemperatureSensor::TemperatureSensor() : _logger("TEMPERATURE") {}
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
// setup // setup
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
void TemperatureSensor::setup() void TemperatureSensor::setup()
{ {
SensorDHT11::Instance()->begin(); SensorDHT11::Instance()->begin();
} }
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
// loop // loop
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
Measurement TemperatureSensor::read() Measurement TemperatureSensor::read()
{ {
float t = SensorDHT11::Instance()->readTemperature(); float t = SensorDHT11::Instance()->readTemperature();
if (isnan(t)) if (isnan(t))
{ {
@ -44,4 +46,5 @@ Measurement TemperatureSensor::read()
DistanceSensor::Instance()->setTemperature(int(t)); DistanceSensor::Instance()->setTemperature(int(t));
return Measurement::Value(int(t)); return Measurement::Value(int(t));
} }
} }
} // namespace Dough

View File

@ -3,25 +3,26 @@
#include "Sensors/SensorBase.h" #include "Sensors/SensorBase.h"
#include "Sensors/LowLevel/SensorDHT11.h" #include "Sensors/LowLevel/SensorDHT11.h"
#include "UI/DoughLogger.h" #include "UI/Logger.h"
#include "Data/Measurement.h" #include "Data/Measurement.h"
#include "Sensors/DistanceSensor.h" #include "Sensors/DistanceSensor.h"
#include "config.h" #include "config.h"
/** namespace Dough
* This class provides access to the temperature sensor in the device.
*/
class TemperatureSensor : public SensorBase
{ {
public: // This class provides access to the temperature sensor in the device.
class TemperatureSensor : public SensorBase
{
public:
static TemperatureSensor *Instance(); static TemperatureSensor *Instance();
virtual void setup(); virtual void setup();
virtual Measurement read(); virtual Measurement read();
private: private:
TemperatureSensor(); TemperatureSensor();
static TemperatureSensor *_instance; static TemperatureSensor *_instance;
DoughLogger _logger; Logger _logger;
}; };
} // namespace Dough
#endif #endif

146
src/UI/Button.cpp Normal file
View File

@ -0,0 +1,146 @@
#include "Button.h"
namespace Dough
{
// Constructor for a button instance.
// As a necessary evil, because of the way attachinterrupt() works in
// Arduino, construction needs a bit of extra work to get the button
// working. An interrupt service routine (ISR) function must be created
// and linked to the button to get the interrupts working. Pattern:
//
// // Construct the button instance.
// Button myButton(MYBUTTON_PIN);
//
// // A function for handling interrupts.
// void myButtonISR() {
// myButton.handleButtonState();
// }
//
// // Linking the function ot button interrupts.
// myButton.onInterrupt(myButtonISR);
Button::Button(int pin)
{
_pin = pin;
}
void Button::setup()
{
pinMode(_pin, INPUT_PULLUP);
}
// Assign an interrupt service routine (ISR) for handling button
// interrupts. The provided isr should relay interrupts to the
// handleButtonState() method of this class (see constructor docs).
void Button::onInterrupt(ButtonHandler isr)
{
attachInterrupt(digitalPinToInterrupt(_pin), isr, CHANGE);
}
// Assign an event handler for short and long button presses.
// When specific handlers for long and/or short presses are
// configured as well, those have precedence over this one.
void Button::onPress(ButtonHandler handler)
{
_pressHandler = handler;
}
// Assign an event handler for long button presses.
void Button::onLongPress(ButtonHandler handler)
{
_longPressHandler = handler;
}
// Assign an event handler for short button presses.
void Button::onShortPress(ButtonHandler handler)
{
_shortPressHandler = handler;
}
void Button::loop()
{
handleButtonState();
if (_state == UP_AFTER_SHORT)
{
if (_shortPressHandler != nullptr)
{
_shortPressHandler();
}
else if (_pressHandler != nullptr)
{
_pressHandler();
}
_state = READY_FOR_NEXT_PRESS;
}
else if (_state == DOWN_LONG || _state == UP_AFTER_LONG)
{
if (_longPressHandler != nullptr)
{
_longPressHandler();
}
else if (_pressHandler != nullptr)
{
_pressHandler();
}
_state = READY_FOR_NEXT_PRESS;
}
else if (_state == DOWN && _shortPressHandler == nullptr && _longPressHandler == nullptr)
{
if (_pressHandler != nullptr)
{
_pressHandler();
}
_state = READY_FOR_NEXT_PRESS;
}
}
void Button::clearEvents()
{
_state = READY_FOR_NEXT_PRESS;
}
void Button::handleButtonState()
{
bool buttonIsDown = digitalRead(_pin) == 0;
bool buttonIsUp = !buttonIsDown;
// When the button state has changed since the last time, then
// start the debounce timer.
if (buttonIsDown != _debounceState)
{
_debounceTimer = millis();
_debounceState = buttonIsDown;
}
unsigned long interval = (millis() - _debounceTimer);
// Only when the last state change has been stable for longer than the
// configured debounce delay, then we accept the current state as
// a stabilized button state.
if (interval < BUTTON_DEBOUNCE_DELAY)
{
return;
}
// Handle button state changes.
if (_state == READY_FOR_NEXT_PRESS && buttonIsUp)
{
_state = UP;
}
else if (_state == UP && buttonIsDown)
{
_state = DOWN;
}
else if (_state == DOWN && buttonIsDown && interval > BUTTON_LONGPRESS_DELAY)
{
_state = DOWN_LONG;
}
else if (_state == DOWN && buttonIsUp)
{
_state = UP_AFTER_SHORT;
}
else if (_state == DOWN_LONG && buttonIsUp)
{
_state = UP_AFTER_LONG;
}
}
} // namespace Dough

54
src/UI/Button.h Normal file
View File

@ -0,0 +1,54 @@
#ifndef DOUGH_BUTTON_H
#define DOUGH_BUTTON_H
#define BUTTON_DEBOUNCE_DELAY 50
#define BUTTON_LONGPRESS_DELAY 1000
#include <Arduino.h>
namespace Dough
{
typedef enum
{
UP,
DOWN,
DOWN_LONG,
UP_AFTER_LONG,
UP_AFTER_SHORT,
READY_FOR_NEXT_PRESS
} ButtonState;
typedef void (*ButtonHandler)();
// 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 Button
{
public:
Button(int pin);
void setup();
void loop();
void onInterrupt(ButtonHandler isr);
void onPress(ButtonHandler handler);
void onShortPress(ButtonHandler handler);
void onLongPress(ButtonHandler handler);
void clearEvents();
void handleButtonState();
private:
int _pin;
ButtonHandler _pressHandler = nullptr;
ButtonHandler _shortPressHandler = nullptr;
ButtonHandler _longPressHandler = nullptr;
bool _debounceState = false;
unsigned long _debounceTimer = 0;
ButtonState _state = UP;
};
} // namespace Dough
#endif

View File

@ -1,153 +0,0 @@
#include "DoughButton.h"
/**
* Constructor for a button instance.
* As a necessary evil, because of the way attachinterrupt() works in
* Arduino, construction needs a bit of extra work to get the button
* working. An interrupt service routine (ISR) function must be created
* and linked to the button to get the interrupts working. Pattern:
*
* // Construct the button instance.
* DoughButton myButton(MYBUTTON_PIN);
*
* // A function for handling interrupts.
* void myButtonISR() {
* myButton.handleButtonState();
* }
*
* // Linking the function ot button interrupts.
* myButton.onInterrupt(myButtonISR);
*/
DoughButton::DoughButton(int pin)
{
_pin = pin;
}
void DoughButton::setup()
{
pinMode(_pin, INPUT_PULLUP);
}
/**
* Assign an interrupt service routine (ISR) for handling button
* interrupts. The provided isr should relay interrupts to the
* handleButtonState() method of this class (see constructor docs).
*/
void DoughButton::onInterrupt(DoughButtonHandler isr)
{
attachInterrupt(digitalPinToInterrupt(_pin), isr, CHANGE);
}
/**
* Assign an event handler for short and long button presses.
* When specific handlers for long and/or short presses are
* configured as well, those have precedence over this one.
*/
void DoughButton::onPress(DoughButtonHandler handler)
{
_pressHandler = handler;
}
/**
* Assign an event handler for long button presses.
*/
void DoughButton::onLongPress(DoughButtonHandler handler)
{
_longPressHandler = handler;
}
/**
* Assign an event handler for short button presses.
*/
void DoughButton::onShortPress(DoughButtonHandler handler)
{
_shortPressHandler = handler;
}
void DoughButton::loop()
{
handleButtonState();
if (_state == UP_AFTER_SHORT)
{
if (_shortPressHandler != nullptr)
{
_shortPressHandler();
}
else if (_pressHandler != nullptr)
{
_pressHandler();
}
_state = READY_FOR_NEXT_PRESS;
}
else if (_state == DOWN_LONG || _state == UP_AFTER_LONG)
{
if (_longPressHandler != nullptr)
{
_longPressHandler();
}
else if (_pressHandler != nullptr)
{
_pressHandler();
}
_state = READY_FOR_NEXT_PRESS;
}
else if (_state == DOWN && _shortPressHandler == nullptr && _longPressHandler == nullptr)
{
if (_pressHandler != nullptr)
{
_pressHandler();
}
_state = READY_FOR_NEXT_PRESS;
}
}
void DoughButton::clearEvents()
{
_state = READY_FOR_NEXT_PRESS;
}
void DoughButton::handleButtonState()
{
bool buttonIsDown = digitalRead(_pin) == 0;
bool buttonIsUp = !buttonIsDown;
// When the button state has changed since the last time, then
// start the debounce timer.
if (buttonIsDown != _debounceState)
{
_debounceTimer = millis();
_debounceState = buttonIsDown;
}
unsigned long interval = (millis() - _debounceTimer);
// Only when the last state change has been stable for longer than the
// configured debounce delay, then we accept the current state as
// a stabilized button state.
if (interval < BUTTON_DEBOUNCE_DELAY)
{
return;
}
// Handle button state changes.
if (_state == READY_FOR_NEXT_PRESS && buttonIsUp)
{
_state = UP;
}
else if (_state == UP && buttonIsDown)
{
_state = DOWN;
}
else if (_state == DOWN && buttonIsDown && interval > BUTTON_LONGPRESS_DELAY)
{
_state = DOWN_LONG;
}
else if (_state == DOWN && buttonIsUp)
{
_state = UP_AFTER_SHORT;
}
else if (_state == DOWN_LONG && buttonIsUp)
{
_state = UP_AFTER_LONG;
}
}

View File

@ -1,53 +0,0 @@
#ifndef DOUGH_BUTTON_H
#define DOUGH_BUTTON_H
#define BUTTON_DEBOUNCE_DELAY 50
#define BUTTON_LONGPRESS_DELAY 1000
#include <Arduino.h>
typedef enum
{
UP,
DOWN,
DOWN_LONG,
UP_AFTER_LONG,
UP_AFTER_SHORT,
READY_FOR_NEXT_PRESS
} DoughButtonState;
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
{
public:
DoughButton(int pin);
void setup();
void loop();
void onInterrupt(DoughButtonHandler isr);
void onPress(DoughButtonHandler handler);
void onShortPress(DoughButtonHandler handler);
void onLongPress(DoughButtonHandler handler);
void clearEvents();
void handleButtonState();
private:
int _pin;
DoughButtonHandler _pressHandler = nullptr;
DoughButtonHandler _shortPressHandler = nullptr;
DoughButtonHandler _longPressHandler = nullptr;
bool _debounceState = false;
unsigned long _debounceTimer = 0;
DoughButtonState _state = UP;
};
#endif

View File

@ -1,173 +0,0 @@
#include "DoughLED.h"
DoughLED::DoughLED(int pin)
{
_pin = pin;
}
void DoughLED::setup()
{
pinMode(_pin, OUTPUT);
_state = OFF;
_setPin(LOW);
}
void DoughLED::loop()
{
unsigned long now = millis();
bool tick = (now - _timer) > _time;
if (_state == FLASH)
{
if (tick)
{
_setPin(LOW);
_state = OFF;
}
}
else if (_state == DIP)
{
if (tick)
{
_setPin(HIGH);
_state = ON;
}
}
else if (_state == BLINK_ON)
{
if (_blinkStep == _blinkOnStep)
{
_setPin(HIGH);
}
if (tick)
{
_setPin(LOW);
_state = BLINK_OFF;
_timer = now;
}
}
else if (_state == BLINK_OFF)
{
if (tick)
{
_state = BLINK_ON;
_timer = now;
_blinkStep++;
if (_blinkStep > _blinkOfSteps)
{
_blinkStep = 1;
}
}
}
else if (_state == PULSE)
{
if (tick)
{
_timer = now;
_time = 1;
_brightness += _pulseStep;
if (_brightness <= 0)
{
_time = 200;
_brightness = 0;
_pulseStep = -_pulseStep;
}
else if (_brightness >= 100)
{
_brightness = 100;
_pulseStep = -_pulseStep;
}
}
analogWrite(_pin, _brightness);
}
else if (_state == OFF)
{
_setPin(LOW);
}
else if (_state == ON)
{
_setPin(HIGH);
}
}
void DoughLED::_setPin(int high_or_low)
{
_pinState = high_or_low;
analogWrite(_pin, _pinState == LOW ? 0 : 255);
}
void DoughLED::on()
{
_state = ON;
loop();
}
void DoughLED::off()
{
_state = OFF;
loop();
}
DoughLED *DoughLED::flash()
{
_setPin(HIGH);
_state = FLASH;
_timer = millis();
_time = LED_TRANSITION_TIME_DEFAULT;
loop();
return this;
}
DoughLED *DoughLED::blink()
{
return blink(1, 1);
}
DoughLED *DoughLED::dip()
{
_setPin(LOW);
_state = DIP;
_timer = millis();
_time = LED_TRANSITION_TIME_DEFAULT;
loop();
return this;
}
DoughLED *DoughLED::blink(int onStep, int ofSteps)
{
_blinkOnStep = onStep;
_blinkOfSteps = ofSteps;
_blinkStep = 1;
_state = BLINK_ON;
_time = LED_TRANSITION_TIME_DEFAULT;
loop();
return this;
}
void DoughLED::pulse()
{
_state = PULSE;
_brightness = 0;
_pulseStep = +8;
_time = 1;
}
void DoughLED::slow()
{
_time = LED_TRANSITION_TIME_SLOW;
}
void DoughLED::fast()
{
_time = LED_TRANSITION_TIME_FAST;
}
bool DoughLED::isOn()
{
return _pinState == HIGH;
}
bool DoughLED::isOff()
{
return _pinState == LOW;
}

View File

@ -1,58 +0,0 @@
#ifndef DOUGH_LED_H
#define DOUGH_LED_H
// Delay times for blinking, flashing and dipping.
#define LED_TRANSITION_TIME_SLOW 400
#define LED_TRANSITION_TIME_DEFAULT 250
#define LED_TRANSITION_TIME_FAST 100
#include <Arduino.h>
typedef enum
{
ON,
OFF,
BLINK_ON,
BLINK_OFF,
FLASH,
DIP,
PULSE
} DoughLEDState;
/**
* This class provides a set of basic LED lighting patterns, which can
* be used in an async way.
*/
class DoughLED
{
public:
DoughLED(int pin);
void setup();
void loop();
void on();
void off();
DoughLED *blink();
DoughLED *blink(int onStep, int ofSteps);
DoughLED *flash();
DoughLED *dip();
void pulse();
void slow();
void fast();
bool isOn();
bool isOff();
private:
int _pin;
int _pinState = LOW;
DoughLEDState _state = OFF;
void _setPin(int high_or_low);
unsigned long _timer;
unsigned int _time;
int _blinkOnStep;
int _blinkOfSteps;
int _blinkStep;
int _brightness;
int _pulseStep;
};
#endif

View File

@ -1,70 +0,0 @@
#include "DoughLogger.h"
DoughLogger::DoughLogger(const char *section)
{
_section = section;
}
void DoughLogger::suspend()
{
_suspended = true;
}
void DoughLogger::resume()
{
_suspended = false;
}
void DoughLogger::log(const char *fmt, ...)
{
if (_suspended)
{
return;
}
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("");
}

View File

@ -1,28 +0,0 @@
#ifndef DOUGH_LOGGER_H
#define DOUGH_LOGGER_H
#define LOGGER_PREFIX_BUFLEN 16
#define LOGGER_PREFIX_FORMAT "%11s | "
#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, ...);
void suspend();
void resume();
private:
const char *_section;
bool _suspended = false;
};
#endif

View File

@ -1,193 +0,0 @@
#include "DoughUI.h"
// ----------------------------------------------------------------------
// Constructor
// ----------------------------------------------------------------------
DoughUI *DoughUI::_instance = nullptr;
DoughUI *DoughUI::Instance()
{
if (DoughUI::_instance == nullptr)
{
DoughUI::_instance = new DoughUI();
}
return DoughUI::_instance;
}
DoughUI::DoughUI() : onoffButton(ONOFF_BUTTON_PIN),
setupButton(SETUP_BUTTON_PIN),
ledBuiltin(LED_BUILTIN),
led1(LED1_PIN),
led2(LED2_PIN),
led3(LED3_PIN) {}
// ----------------------------------------------------------------------
// Setup
// ----------------------------------------------------------------------
void DoughUI::setup()
{
// Setup the serial port, used for logging.
Serial.begin(LOG_BAUDRATE);
#ifdef LOG_WAIT_SERIAL
while (!Serial)
{
// wait for serial port to connect. Needed for native USB.
}
#endif
// Setup the buttons.
onoffButton.setup();
onoffButton.onInterrupt(DoughUI::onoffButtonISR);
setupButton.setup();
setupButton.onInterrupt(DoughUI::setupButtonISR);
// Setup the LEDs.
ledBuiltin.setup();
led1.setup();
led2.setup();
led3.setup();
// Setup a timer interrupt that is used to update the
// user interface (a.k.a. "LEDs") in parallel to other activities.
// This allows for example to have a flashing LED, during the
// wifi connection setup.
_setupTimerInterrupt();
// Notify the user that we're on a roll!
_flash_all_leds();
}
void DoughUI::onoffButtonISR()
{
DoughUI::Instance()->onoffButton.handleButtonState();
}
void DoughUI::setupButtonISR()
{
DoughUI::Instance()->setupButton.handleButtonState();
}
/**
* 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
* Arduino Nano 33 IOT architecture. Luckily, documentation and various
* helpful threads on the internet helped me piece the following code together.
*/
void DoughUI::_setupTimerInterrupt()
{
REG_GCLK_GENDIV = GCLK_GENDIV_DIV(200) | // Use divider (32kHz/200 = 160Hz)
GCLK_GENDIV_ID(4); // for Generic Clock GCLK4
while (GCLK->STATUS.bit.SYNCBUSY)
; // Wait for synchronization
REG_GCLK_GENCTRL = GCLK_GENCTRL_IDC | // Set the duty cycle to 50/50 HIGH/LOW
GCLK_GENCTRL_GENEN | // and enable the clock
GCLK_GENCTRL_SRC_OSC32K | // using the 32kHz clock source as input
GCLK_GENCTRL_ID(4); // for Generic Clock GCLK4
while (GCLK->STATUS.bit.SYNCBUSY)
; // Wait for synchronization
REG_GCLK_CLKCTRL = GCLK_CLKCTRL_CLKEN | // Enable timer
GCLK_CLKCTRL_GEN_GCLK4 | // using Generic Clock GCLK4 as input
GCLK_CLKCTRL_ID_TC4_TC5; // and feed its output to TC4 and TC5
while (GCLK->STATUS.bit.SYNCBUSY)
; // Wait for synchronization
REG_TC4_CTRLA |= TC_CTRLA_PRESCALER_DIV8 | // Use prescaler (160Hz / 8 = 20Hz)
TC_CTRLA_WAVEGEN_MFRQ | // Use match frequency (MFRQ) mode
TC_CTRLA_MODE_COUNT8 | // Set the timer to 8-bit mode
TC_CTRLA_ENABLE; // Enable TC4
REG_TC4_INTENSET = TC_INTENSET_OVF; // Enable TC4 overflow (OVF) interrupts
REG_TC4_COUNT8_CC0 = 1; // Set the CC0 as the TOP value for MFRQ (1 => 50ms per pulse)
while (TC4->COUNT8.STATUS.bit.SYNCBUSY)
; // Wait for synchronization
// Enable interrupts for TC4 in the Nested Vector InterruptController (NVIC),
NVIC_SetPriority(TC4_IRQn, 0); // Set NVIC priority for TC4 to 0 (highest)
NVIC_EnableIRQ(TC4_IRQn); // Enable TC4 interrupts
}
// ----------------------------------------------------------------------
// Loop
// ----------------------------------------------------------------------
/**
* This callback is called when the TC4 timer hits an overflow interrupt.
*/
void TC4_Handler()
{
DoughUI::Instance()->updatedLEDs();
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.
*/
void DoughUI::processButtonEvents()
{
onoffButton.loop();
setupButton.loop();
}
/**
* Clear pending button events.
*/
void DoughUI::clearButtonEvents()
{
onoffButton.clearEvents();
setupButton.clearEvents();
}
/**
* Update the state of all the LEDs in the system.
* This method is called both sync by methods in this class and async by
* the timer interrupt code from above. The timer interrupt based invocation
* makes it possible to do LED updates, while the device is busy doing
* something else.
*/
void DoughUI::updatedLEDs()
{
ledBuiltin.loop();
led1.loop();
led2.loop();
led3.loop();
}
/**
* Flash all LEDs, one at a time.
*/
void DoughUI::_flash_all_leds()
{
ledBuiltin.on();
delay(100);
ledBuiltin.off();
led1.on();
delay(100);
led1.off();
led2.on();
delay(100);
led2.off();
led3.on();
delay(100);
led3.off();
}

176
src/UI/LED.cpp Normal file
View File

@ -0,0 +1,176 @@
#include "LED.h"
namespace Dough
{
LED::LED(int pin)
{
_pin = pin;
}
void LED::setup()
{
pinMode(_pin, OUTPUT);
_state = OFF;
_setPin(LOW);
}
void LED::loop()
{
unsigned long now = millis();
bool tick = (now - _timer) > _time;
if (_state == FLASH)
{
if (tick)
{
_setPin(LOW);
_state = OFF;
}
}
else if (_state == DIP)
{
if (tick)
{
_setPin(HIGH);
_state = ON;
}
}
else if (_state == BLINK_ON)
{
if (_blinkStep == _blinkOnStep)
{
_setPin(HIGH);
}
if (tick)
{
_setPin(LOW);
_state = BLINK_OFF;
_timer = now;
}
}
else if (_state == BLINK_OFF)
{
if (tick)
{
_state = BLINK_ON;
_timer = now;
_blinkStep++;
if (_blinkStep > _blinkOfSteps)
{
_blinkStep = 1;
}
}
}
else if (_state == PULSE)
{
if (tick)
{
_timer = now;
_time = 1;
_brightness += _pulseStep;
if (_brightness <= 0)
{
_time = 200;
_brightness = 0;
_pulseStep = -_pulseStep;
}
else if (_brightness >= 100)
{
_brightness = 100;
_pulseStep = -_pulseStep;
}
}
analogWrite(_pin, _brightness);
}
else if (_state == OFF)
{
_setPin(LOW);
}
else if (_state == ON)
{
_setPin(HIGH);
}
}
void LED::_setPin(int high_or_low)
{
_pinState = high_or_low;
analogWrite(_pin, _pinState == LOW ? 0 : 255);
}
void LED::on()
{
_state = ON;
loop();
}
void LED::off()
{
_state = OFF;
loop();
}
LED *LED::flash()
{
_setPin(HIGH);
_state = FLASH;
_timer = millis();
_time = LED_TRANSITION_TIME_DEFAULT;
loop();
return this;
}
LED *LED::blink()
{
return blink(1, 1);
}
LED *LED::dip()
{
_setPin(LOW);
_state = DIP;
_timer = millis();
_time = LED_TRANSITION_TIME_DEFAULT;
loop();
return this;
}
LED *LED::blink(int onStep, int ofSteps)
{
_blinkOnStep = onStep;
_blinkOfSteps = ofSteps;
_blinkStep = 1;
_state = BLINK_ON;
_time = LED_TRANSITION_TIME_DEFAULT;
loop();
return this;
}
void LED::pulse()
{
_state = PULSE;
_brightness = 0;
_pulseStep = +8;
_time = 1;
}
void LED::slow()
{
_time = LED_TRANSITION_TIME_SLOW;
}
void LED::fast()
{
_time = LED_TRANSITION_TIME_FAST;
}
bool LED::isOn()
{
return _pinState == HIGH;
}
bool LED::isOff()
{
return _pinState == LOW;
}
} // namespace Dough

59
src/UI/LED.h Normal file
View File

@ -0,0 +1,59 @@
#ifndef DOUGH_LED_H
#define DOUGH_LED_H
// Delay times for blinking, flashing and dipping.
#define LED_TRANSITION_TIME_SLOW 400
#define LED_TRANSITION_TIME_DEFAULT 250
#define LED_TRANSITION_TIME_FAST 100
#include <Arduino.h>
namespace Dough
{
typedef enum
{
ON,
OFF,
BLINK_ON,
BLINK_OFF,
FLASH,
DIP,
PULSE
} LEDState;
// This class provides a set of basic LED lighting patterns, which can
// be used in an async way.
class LED
{
public:
LED(int pin);
void setup();
void loop();
void on();
void off();
LED *blink();
LED *blink(int onStep, int ofSteps);
LED *flash();
LED *dip();
void pulse();
void slow();
void fast();
bool isOn();
bool isOff();
private:
int _pin;
int _pinState = LOW;
LEDState _state = OFF;
void _setPin(int high_or_low);
unsigned long _timer;
unsigned int _time;
int _blinkOnStep;
int _blinkOfSteps;
int _blinkStep;
int _brightness;
int _pulseStep;
};
} // namespace Dough
#endif

73
src/UI/Logger.cpp Normal file
View File

@ -0,0 +1,73 @@
#include "Logger.h"
namespace Dough
{
Logger::Logger(const char *section)
{
_section = section;
}
void Logger::suspend()
{
_suspended = true;
}
void Logger::resume()
{
_suspended = false;
}
void Logger::log(const char *fmt, ...)
{
if (_suspended)
{
return;
}
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("");
}
} // namespace Dough

29
src/UI/Logger.h Normal file
View File

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

182
src/UI/UI.cpp Normal file
View File

@ -0,0 +1,182 @@
#include "UI.h"
namespace Dough
{
// ----------------------------------------------------------------------
// Constructor
// ----------------------------------------------------------------------
UI *UI::_instance = nullptr;
UI *UI::Instance()
{
if (UI::_instance == nullptr)
{
UI::_instance = new UI();
}
return UI::_instance;
}
UI::UI() : onoffButton(ONOFF_BUTTON_PIN),
setupButton(SETUP_BUTTON_PIN),
ledBuiltin(LED_BUILTIN),
led1(LED1_PIN),
led2(LED2_PIN),
led3(LED3_PIN) {}
// ----------------------------------------------------------------------
// Setup
// ----------------------------------------------------------------------
void UI::setup()
{
// Setup the serial port, used for logging.
Serial.begin(LOG_BAUDRATE);
#ifdef LOG_WAIT_SERIAL
while (!Serial)
{
// wait for serial port to connect. Needed for native USB.
}
#endif
// Setup the buttons.
onoffButton.setup();
onoffButton.onInterrupt(UI::onoffButtonISR);
setupButton.setup();
setupButton.onInterrupt(UI::setupButtonISR);
// Setup the LEDs.
ledBuiltin.setup();
led1.setup();
led2.setup();
led3.setup();
// Setup a timer interrupt that is used to update the
// user interface (a.k.a. "LEDs") in parallel to other activities.
// This allows for example to have a flashing LED, during the
// wifi connection setup.
_setupTimerInterrupt();
// Notify the user that we're on a roll!
_flash_all_leds();
// Enable the timer interrupt handling.
resume();
}
void UI::onoffButtonISR()
{
UI::Instance()->onoffButton.handleButtonState();
}
void UI::setupButtonISR()
{
UI::Instance()->setupButton.handleButtonState();
}
// 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
// Arduino Nano 33 IOT architecture. Luckily, documentation and various
// helpful threads on the internet helped me piece the following code together.
void UI::_setupTimerInterrupt()
{
REG_GCLK_GENDIV = GCLK_GENDIV_DIV(200) | // Use divider (32kHz/200 = 160Hz)
GCLK_GENDIV_ID(4); // for Generic Clock GCLK4
while (GCLK->STATUS.bit.SYNCBUSY)
; // Wait for synchronization
REG_GCLK_GENCTRL = GCLK_GENCTRL_IDC | // Set the duty cycle to 50/50 HIGH/LOW
GCLK_GENCTRL_GENEN | // and enable the clock
GCLK_GENCTRL_SRC_OSC32K | // using the 32kHz clock source as input
GCLK_GENCTRL_ID(4); // for Generic Clock GCLK4
while (GCLK->STATUS.bit.SYNCBUSY)
; // Wait for synchronization
REG_GCLK_CLKCTRL = GCLK_CLKCTRL_CLKEN | // Enable timer
GCLK_CLKCTRL_GEN_GCLK4 | // using Generic Clock GCLK4 as input
GCLK_CLKCTRL_ID_TC4_TC5; // and feed its output to TC4 and TC5
while (GCLK->STATUS.bit.SYNCBUSY)
; // Wait for synchronization
REG_TC4_CTRLA |= TC_CTRLA_PRESCALER_DIV8 | // Use prescaler (160Hz / 8 = 20Hz)
TC_CTRLA_WAVEGEN_MFRQ | // Use match frequency (MFRQ) mode
TC_CTRLA_MODE_COUNT8 | // Set the timer to 8-bit mode
TC_CTRLA_ENABLE; // Enable TC4
REG_TC4_INTENSET = TC_INTENSET_OVF; // Enable TC4 overflow (OVF) interrupts
REG_TC4_COUNT8_CC0 = 1; // Set the CC0 as the TOP value for MFRQ (1 => 50ms per pulse)
while (TC4->COUNT8.STATUS.bit.SYNCBUSY); // Wait for synchronization
}
// ----------------------------------------------------------------------
// Loop
// ----------------------------------------------------------------------
// Disables the TC4 interrupts, suspending timed async updates to
// the user interface.
void UI::suspend()
{
NVIC_DisableIRQ(TC4_IRQn);
}
// Enables the TC4 interrupts in the Nested Vector InterruptController (NVIC),
// starting timed async updates for the user interface.
void UI::resume()
{
NVIC_SetPriority(TC4_IRQn, 0); // Set NVIC priority for TC4 to 0 (highest)
NVIC_EnableIRQ(TC4_IRQn); // Enable TC4 interrupts
}
// Fire pending button events.
void UI::processButtonEvents()
{
onoffButton.loop();
setupButton.loop();
}
// Clear pending button events.
void UI::clearButtonEvents()
{
onoffButton.clearEvents();
setupButton.clearEvents();
}
// Update the state of all the LEDs in the system.
// This method is called both sync by methods in this class and async by
// the timer interrupt code from above. The timer interrupt based invocation
// makes it possible to do LED updates, while the device is busy doing
// something else.
void UI::updatedLEDs()
{
ledBuiltin.loop();
led1.loop();
led2.loop();
led3.loop();
}
// Flash all LEDs, one at a time.
void UI::_flash_all_leds()
{
ledBuiltin.on();
delay(100);
ledBuiltin.off();
led1.on();
delay(100);
led1.off();
led2.on();
delay(100);
led2.off();
led3.on();
delay(100);
led3.off();
}
} // namespace Dough
// This callback is called when the TC4 timer hits an overflow interrupt.
// Defined outside the Dough namespace, because TC4_Handler is a hard-coded
// root namespace function name.
void TC4_Handler()
{
Dough::UI::Instance()->updatedLEDs();
REG_TC4_INTFLAG = TC_INTFLAG_OVF; // Clear the OVF interrupt flag.
}

View File

@ -12,27 +12,27 @@
#include <Arduino.h> #include <Arduino.h>
#include <stdarg.h> #include <stdarg.h>
#include "UI/DoughButton.h" #include "UI/Button.h"
#include "UI/DoughLED.h" #include "UI/LED.h"
#include "config.h" #include "config.h"
/** namespace Dough
* This class groups all user interface functionality: serial logging, {
* LEDs and buttons. // This class groups all user interface functionality: serial logging,
*/ // LEDs and buttons.
class DoughUI class UI
{ {
public: public:
static DoughUI *Instance(); static UI *Instance();
void setup(); void setup();
static void onoffButtonISR(); static void onoffButtonISR();
static void setupButtonISR(); static void setupButtonISR();
DoughButton onoffButton; Button onoffButton;
DoughButton setupButton; Button setupButton;
DoughLED ledBuiltin; LED ledBuiltin;
DoughLED led1; LED led1;
DoughLED led2; LED led2;
DoughLED led3; LED led3;
void processButtonEvents(); void processButtonEvents();
void clearButtonEvents(); void clearButtonEvents();
void updatedLEDs(); void updatedLEDs();
@ -40,10 +40,11 @@ public:
void suspend(); void suspend();
private: private:
DoughUI(); UI();
void _setupTimerInterrupt(); void _setupTimerInterrupt();
static DoughUI *_instance; static UI *_instance;
void _flash_all_leds(); void _flash_all_leds();
}; };
}
#endif #endif

View File

@ -8,17 +8,18 @@
// TODO: make significantChange part of sensor class? // TODO: make significantChange part of sensor class?
DoughBoyState state = CONFIGURING; DoughBoyState state = CONFIGURING;
auto logger = DoughLogger("MAIN"); auto logger = Dough::Logger("MAIN");
void setup() void setup()
{ {
TemperatureSensor::Instance()->setup(); logger.log("s", "Initializing device");
HumiditySensor::Instance()->setup(); Dough::TemperatureSensor::Instance()->setup();
DistanceSensor::Instance()->setup(); Dough::HumiditySensor::Instance()->setup();
DoughWiFi::Instance()->setup(); Dough::DistanceSensor::Instance()->setup();
Dough::WiFi::Instance()->setup();
Dough::MQTT::Instance()->setup(); Dough::MQTT::Instance()->setup();
DataController::Instance()->setup(); Dough::DataController::Instance()->setup();
auto ui = DoughUI::Instance(); auto ui = Dough::UI::Instance();
ui->setup(); ui->setup();
ui->onoffButton.onPress(handleOnoffButtonPress); ui->onoffButton.onPress(handleOnoffButtonPress);
ui->setupButton.onPress(handleSetupButtonPress); ui->setupButton.onPress(handleSetupButtonPress);
@ -27,8 +28,8 @@ void setup()
void loop() void loop()
{ {
auto ui = DoughUI::Instance(); auto ui = Dough::UI::Instance();
auto data = DataController::Instance(); auto data = Dough::DataController::Instance();
auto mqtt = Dough::MQTT::Instance(); auto mqtt = Dough::MQTT::Instance();
ui->processButtonEvents(); ui->processButtonEvents();
@ -50,7 +51,7 @@ void loop()
} }
else if (state == MEASURING) else if (state == MEASURING)
{ {
DataController::Instance()->loop(); Dough::DataController::Instance()->loop();
} }
else if (state == CALIBRATING) else if (state == CALIBRATING)
{ {
@ -59,21 +60,19 @@ void loop()
} }
else if (state == PAUSED) else if (state == PAUSED)
{ {
DataController::Instance()->clearHistory(); Dough::DataController::Instance()->clearHistory();
} }
} }
/** // Check if the device is connected to the WiFi network and the MQTT broker.
* Check if the device is connected to the WiFi network and the MQTT broker. // If not, then try to setup the connection.
* If not, then try to setup the connection. // Returns true if the connection was established, false otherwise.
* Returns true if the connection was established, false otherwise.
*/
bool setupNetworkConnection() bool setupNetworkConnection()
{ {
static auto connectionState = CONNECTING_WIFI; static auto connectionState = CONNECTING_WIFI;
auto ui = DoughUI::Instance(); auto ui = Dough::UI::Instance();
auto network = DoughWiFi::Instance(); auto network = Dough::WiFi::Instance();
auto mqtt = Dough::MQTT::Instance(); auto mqtt = Dough::MQTT::Instance();
if (!network->isConnected()) if (!network->isConnected())
@ -144,7 +143,7 @@ void handleSetupButtonPress()
void setStateToConfiguring() void setStateToConfiguring()
{ {
auto ui = DoughUI::Instance(); auto ui = Dough::UI::Instance();
logger.log("s", "Waiting for configuration ..."); logger.log("s", "Waiting for configuration ...");
state = CONFIGURING; state = CONFIGURING;
ui->led1.on(); ui->led1.on();
@ -155,7 +154,7 @@ void setStateToConfiguring()
void setStateToMeasuring() void setStateToMeasuring()
{ {
auto ui = DoughUI::Instance(); auto ui = Dough::UI::Instance();
logger.log("s", "Starting measurements"); logger.log("s", "Starting measurements");
state = MEASURING; state = MEASURING;
ui->led1.on(); ui->led1.on();
@ -166,7 +165,7 @@ void setStateToMeasuring()
void setStateToPaused() void setStateToPaused()
{ {
auto ui = DoughUI::Instance(); auto ui = Dough::UI::Instance();
logger.log("s", "Pausing measurements"); logger.log("s", "Pausing measurements");
state = PAUSED; state = PAUSED;
ui->led1.on(); ui->led1.on();
@ -177,7 +176,7 @@ void setStateToPaused()
void setStateToCalibrating() void setStateToCalibrating()
{ {
auto ui = DoughUI::Instance(); auto ui = Dough::UI::Instance();
logger.log("s", "Requested device calibration"); logger.log("s", "Requested device calibration");
state = CALIBRATING; state = CALIBRATING;
ui->led1.on(); ui->led1.on();

View File

@ -5,11 +5,11 @@
#include "Sensors/TemperatureSensor.h" #include "Sensors/TemperatureSensor.h"
#include "Sensors/HumiditySensor.h" #include "Sensors/HumiditySensor.h"
#include "Sensors/DistanceSensor.h" #include "Sensors/DistanceSensor.h"
#include "Network/DoughWiFi.h" #include "Network/WiFi.h"
#include "Network/MQTT.h" #include "Network/MQTT.h"
#include "Data/DataController.h" #include "Data/DataController.h"
#include "UI/DoughButton.h" #include "UI/Button.h"
#include "UI/DoughUI.h" #include "UI/UI.h"
#include "config.h" #include "config.h"
typedef enum typedef enum