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,176 +1,177 @@
#include "Data/DataController.h"
// ----------------------------------------------------------------------
// Constructor
// ----------------------------------------------------------------------
DataController *DataController::_instance = nullptr;
DataController *DataController::Instance()
namespace Dough
{
if (DataController::_instance == nullptr)
// ----------------------------------------------------------------------
// Constructor
// ----------------------------------------------------------------------
DataController *DataController::_instance = nullptr;
DataController *DataController::Instance()
{
DataController::_instance = new DataController();
}
return DataController::_instance;
}
DataController::DataController() : _temperatureMeasurements(
"temperature",
TemperatureSensor::Instance(),
TEMPERATURE_AVG_LOOKBACK,
TEMPERATURE_SIGNIFICANT_CHANGE,
PUBLISH_INTERVAL),
_humidityMeasurements(
"humidity",
HumiditySensor::Instance(),
HUMIDITY_AVG_LOOKBACK,
HUMIDITY_SIGNIFICANT_CHANGE,
PUBLISH_INTERVAL),
_distanceMeasurements(
"distance",
DistanceSensor::Instance(),
DISTANCE_AVG_LOOKBACK,
DISTANCE_SIGNIFICANT_CHANGE,
PUBLISH_INTERVAL),
_logger("DATA")
{
_ui = DoughUI::Instance();
_mqtt = Dough::MQTT::Instance();
}
// ----------------------------------------------------------------------
// Setup
// ----------------------------------------------------------------------
void DataController::setup()
{
_containerHeight = 0.00;
_containerHeightSet = false;
Dough::MQTT *mqtt = Dough::MQTT::Instance();
mqtt->onConnect(DataController::handleMqttConnect);
mqtt->onMessage(DataController::handleMqttMessage);
_temperatureMeasurements.setup();
_humidityMeasurements.setup();
_distanceMeasurements.setup();
}
void DataController::handleMqttConnect(Dough::MQTT *mqtt)
{
mqtt->subscribe("container_height");
}
void DataController::handleMqttMessage(String &key, String &payload)
{
if (key.equals("container_height"))
{
DataController::Instance()->setContainerHeight(payload.toInt());
}
else
{
DataController::Instance()->_logger.log("sS", "ERROR - Unhandled MQTT message, key = ", key);
}
}
bool DataController::isConfigured()
{
return _containerHeightSet;
}
/**
* 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
* the starter or dough by subtracting the distance measurement from it.
*/
void DataController::setContainerHeight(int height)
{
_containerHeightSet = false;
if (height <= HCSR04_MIN_MM)
{
_logger.log("sisis", "ERROR - Container height ", height,
"mm is less than the minimum measuring distance of ",
HCSR04_MIN_MM, "mm");
return;
}
if (height >= HCSR04_MAX_MM)
{
_logger.log("sisis", "ERROR - Container height ", height,
"mm is more than the maximum measuring distance of ",
HCSR04_MAX_MM, "mm");
return;
}
_logger.log("sis", "Set container height to ", height, "mm");
_containerHeight = height;
_containerHeightSet = true;
}
// ----------------------------------------------------------------------
// Loop
// ----------------------------------------------------------------------
void DataController::loop()
{
if (isConfigured())
{
_sample();
}
}
void DataController::clearHistory()
{
_temperatureMeasurements.clearHistory();
_humidityMeasurements.clearHistory();
_distanceMeasurements.clearHistory();
_sampleType = SAMPLE_TEMPERATURE;
_sampleCounter = 0;
}
void DataController::_sample()
{
auto now = millis();
auto delta = now - _lastSample;
auto tick = _lastSample == 0 || delta >= SAMPLE_INTERVAL;
if (tick)
{
_lastSample = now;
// Quickly dip the LED to indicate that a measurement is started.
// This is done synchroneously, because we suspend the timer interrupts
// in the upcoming code.
_ui->led3.off();
delay(50);
_ui->led3.on();
// Suspend the UI timer interrupts, to not let these interfere
// with the sensor measurements.
_ui->suspend();
// Take a sample.
switch (_sampleType)
if (DataController::_instance == nullptr)
{
case SAMPLE_TEMPERATURE:
_temperatureMeasurements.process();
_sampleType = SAMPLE_HUMIDITY;
break;
case SAMPLE_HUMIDITY:
_humidityMeasurements.process();
_sampleType = SAMPLE_DISTANCE;
break;
case SAMPLE_DISTANCE:
_distanceMeasurements.process();
break;
DataController::_instance = new DataController();
}
return DataController::_instance;
}
_ui->resume();
DataController::DataController() : _temperatureMeasurements(
"temperature",
TemperatureSensor::Instance(),
TEMPERATURE_AVG_LOOKBACK,
TEMPERATURE_SIGNIFICANT_CHANGE,
PUBLISH_INTERVAL),
_humidityMeasurements(
"humidity",
HumiditySensor::Instance(),
HUMIDITY_AVG_LOOKBACK,
HUMIDITY_SIGNIFICANT_CHANGE,
PUBLISH_INTERVAL),
_distanceMeasurements(
"distance",
DistanceSensor::Instance(),
DISTANCE_AVG_LOOKBACK,
DISTANCE_SIGNIFICANT_CHANGE,
PUBLISH_INTERVAL),
_logger("DATA")
{
_ui = UI::Instance();
_mqtt = MQTT::Instance();
}
_sampleCounter++;
if (_sampleCounter == SAMPLE_CYCLE_LENGTH)
// ----------------------------------------------------------------------
// Setup
// ----------------------------------------------------------------------
void DataController::setup()
{
_containerHeight = 0.00;
_containerHeightSet = false;
MQTT *mqtt = MQTT::Instance();
mqtt->onConnect(DataController::handleMqttConnect);
mqtt->onMessage(DataController::handleMqttMessage);
_temperatureMeasurements.setup();
_humidityMeasurements.setup();
_distanceMeasurements.setup();
}
void DataController::handleMqttConnect(MQTT *mqtt)
{
mqtt->subscribe("container_height");
}
void DataController::handleMqttMessage(String &key, String &payload)
{
if (key.equals("container_height"))
{
_sampleCounter = 0;
_sampleType = SAMPLE_TEMPERATURE;
DataController::Instance()->setContainerHeight(payload.toInt());
}
else
{
DataController::Instance()->_logger.log("sS", "ERROR - Unhandled MQTT message, key = ", key);
}
}
}
bool DataController::isConfigured()
{
return _containerHeightSet;
}
// 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
// the starter or dough by subtracting the distance measurement from it.
void DataController::setContainerHeight(int height)
{
_containerHeightSet = false;
if (height <= HCSR04_MIN_MM)
{
_logger.log("sisis", "ERROR - Container height ", height,
"mm is less than the minimum measuring distance of ",
HCSR04_MIN_MM, "mm");
return;
}
if (height >= HCSR04_MAX_MM)
{
_logger.log("sisis", "ERROR - Container height ", height,
"mm is more than the maximum measuring distance of ",
HCSR04_MAX_MM, "mm");
return;
}
_logger.log("sis", "Set container height to ", height, "mm");
_containerHeight = height;
_containerHeightSet = true;
}
// ----------------------------------------------------------------------
// Loop
// ----------------------------------------------------------------------
void DataController::loop()
{
if (isConfigured())
{
_sample();
}
}
void DataController::clearHistory()
{
_temperatureMeasurements.clearHistory();
_humidityMeasurements.clearHistory();
_distanceMeasurements.clearHistory();
_sampleType = SAMPLE_TEMPERATURE;
_sampleCounter = 0;
}
void DataController::_sample()
{
auto now = millis();
auto delta = now - _lastSample;
auto tick = _lastSample == 0 || delta >= SAMPLE_INTERVAL;
if (tick)
{
_lastSample = now;
// Quickly dip the LED to indicate that a measurement is started.
// This is done synchroneously, because we suspend the timer interrupts
// in the upcoming code.
_ui->led3.off();
delay(50);
_ui->led3.on();
// Suspend the UI timer interrupts, to not let these interfere
// with the sensor measurements.
_ui->suspend();
// Take a sample.
switch (_sampleType)
{
case SAMPLE_TEMPERATURE:
_temperatureMeasurements.process();
_sampleType = SAMPLE_HUMIDITY;
break;
case SAMPLE_HUMIDITY:
_humidityMeasurements.process();
_sampleType = SAMPLE_DISTANCE;
break;
case SAMPLE_DISTANCE:
_distanceMeasurements.process();
break;
}
_ui->resume();
_sampleCounter++;
if (_sampleCounter == SAMPLE_CYCLE_LENGTH)
{
_sampleCounter = 0;
_sampleType = SAMPLE_TEMPERATURE;
}
}
}
} // namespace Dough

View File

@ -28,54 +28,55 @@
#define PUBLISH_INTERVAL 300
#include <Arduino.h>
#include "Data/Measurements.h"
#include "Data/SensorController.h"
#include "Sensors/TemperatureSensor.h"
#include "Sensors/HumiditySensor.h"
#include "Sensors/DistanceSensor.h"
#include "Network/DoughWiFi.h"
#include "Network/WiFi.h"
#include "Network/MQTT.h"
#include "UI/DoughUI.h"
#include "UI/DoughLogger.h"
#include "UI/UI.h"
#include "UI/Logger.h"
typedef enum
namespace Dough
{
SAMPLE_TEMPERATURE,
SAMPLE_HUMIDITY,
SAMPLE_DISTANCE
} DoughSampleType;
typedef enum
{
SAMPLE_TEMPERATURE,
SAMPLE_HUMIDITY,
SAMPLE_DISTANCE
} DoughSampleType;
/**
* This class is responsible for handling all things "data".
* It holds the device configuration, collects measurements from sensors,
* gathers statistics on these data, and publishing results to the MQTT broker.
*/
class DataController
{
public:
static DataController *Instance();
void setup();
void loop();
void clearHistory();
void setContainerHeight(int height);
bool isConfigured();
static void handleMqttConnect(Dough::MQTT *mqtt);
static void handleMqttMessage(String &key, String &value);
// This class is responsible for handling all things "data".
// It holds the device configuration, collects measurements from sensors,
// gathers statistics on these data, and publishing results to the MQTT broker.
class DataController
{
public:
static DataController *Instance();
void setup();
void loop();
void clearHistory();
void setContainerHeight(int height);
bool isConfigured();
static void handleMqttConnect(MQTT *mqtt);
static void handleMqttMessage(String &key, String &value);
private:
DataController();
static DataController *_instance;
DoughUI *_ui;
Dough::MQTT *_mqtt;
Measurements _temperatureMeasurements;
Measurements _humidityMeasurements;
Measurements _distanceMeasurements;
DoughLogger _logger;
unsigned long _lastSample = 0;
DoughSampleType _sampleType = SAMPLE_TEMPERATURE;
int _sampleCounter = 0;
int _containerHeight;
bool _containerHeightSet;
void _sample();
};
private:
DataController();
static DataController *_instance;
UI *_ui;
MQTT *_mqtt;
SensorController _temperatureMeasurements;
SensorController _humidityMeasurements;
SensorController _distanceMeasurements;
Logger _logger;
unsigned long _lastSample = 0;
DoughSampleType _sampleType = SAMPLE_TEMPERATURE;
int _sampleCounter = 0;
int _containerHeight;
bool _containerHeightSet;
void _sample();
};
} // namespace Dough
#endif
#endif

View File

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

View File

@ -1,20 +1,21 @@
#ifndef DOUGH_DATA_MEASUREMENT_H
#define DOUGH_DATA_MEASUREMENT_H
/**
* This class represents a single measurement, which can be either a
* successful (bearing a measurement value) or a failed one.
*/
class Measurement
namespace Dough
{
public:
Measurement();
int value = 0;
bool ok = false;
static Measurement Failed();
static Measurement Value(int value);
void clear();
void copyTo(Measurement *target);
};
// This class represents a single measurement, which can be either a
// successful (bearing a measurement value) or a failed one.
class Measurement
{
public:
Measurement();
int value = 0;
bool ok = false;
static Measurement Failed();
static Measurement Value(int value);
void clear();
void copyTo(Measurement *target);
};
} // namespace Dough
#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
{
@ -26,7 +26,7 @@ namespace Dough
void MQTT::setup()
{
DoughWiFi *network = DoughWiFi::Instance();
WiFi *network = WiFi::Instance();
#ifdef MQTT_DEVICE_ID
_mqttDeviceId = MQTT_DEVICE_ID;

View File

@ -3,17 +3,15 @@
#include <MQTT.h>
#include <MQTTClient.h>
#include "Network/DoughWiFi.h"
#include "Network/WiFi.h"
#include "Data/Measurement.h"
#include "UI/DoughLogger.h"
#include "UI/Logger.h"
#include "config.h"
namespace Dough
{
/**
* This class encapsulates the connection to the MQTT broker.
* MQTT is used to publish measurements and to store configuration data.
*/
// This class encapsulates the connection to the MQTT broker.
// MQTT is used to publish measurements and to store configuration data.
class MQTT;
typedef void (*MQTTConnectHandler)(MQTT *mqtt);
@ -38,7 +36,7 @@ namespace Dough
MQTT();
static MQTT *_instance;
MQTTClient _mqttClient;
DoughLogger _logger;
Logger _logger;
MQTTConnectHandler _onConnect = nullptr;
MQTTClientCallbackSimple _onMessage = nullptr;
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,59 +1,62 @@
#include "DistanceSensor.h"
// ----------------------------------------------------------------------
// Constructor
// ----------------------------------------------------------------------
DistanceSensor *DistanceSensor::_instance = nullptr;
DistanceSensor *DistanceSensor::Instance()
namespace Dough
{
if (DistanceSensor::_instance == nullptr)
// ----------------------------------------------------------------------
// Constructor
// ----------------------------------------------------------------------
DistanceSensor *DistanceSensor::_instance = nullptr;
DistanceSensor *DistanceSensor::Instance()
{
DistanceSensor::_instance = new DistanceSensor();
if (DistanceSensor::_instance == nullptr)
{
DistanceSensor::_instance = new DistanceSensor();
}
return DistanceSensor::_instance;
}
return DistanceSensor::_instance;
}
DistanceSensor::DistanceSensor() : _logger("DISTANCE")
{
_hcsr04 = new SensorHCSR04(HCSR04_TRIG_PIN, HCSR04_ECHO_PIN);
}
// ----------------------------------------------------------------------
// setup
// ----------------------------------------------------------------------
void DistanceSensor::setup()
{
_hcsr04->setup();
}
void DistanceSensor::setTemperature(int temperature)
{
_hcsr04->setTemperature(temperature);
}
void DistanceSensor::setHumidity(int humidity)
{
_hcsr04->setHumidity(humidity);
}
// ----------------------------------------------------------------------
// loop
// ----------------------------------------------------------------------
Measurement DistanceSensor::read()
{
int d = _hcsr04->readDistance();
if (d == -1)
DistanceSensor::DistanceSensor() : _logger("DISTANCE")
{
_logger.log("s", "ERROR - Distance measurement failed");
return Measurement::Failed();
_hcsr04 = new SensorHCSR04(HCSR04_TRIG_PIN, HCSR04_ECHO_PIN);
}
else
// ----------------------------------------------------------------------
// setup
// ----------------------------------------------------------------------
void DistanceSensor::setup()
{
_logger.log("sis", "Distance = ", d, "mm");
return Measurement::Value(d);
_hcsr04->setup();
}
}
void DistanceSensor::setTemperature(int temperature)
{
_hcsr04->setTemperature(temperature);
}
void DistanceSensor::setHumidity(int humidity)
{
_hcsr04->setHumidity(humidity);
}
// ----------------------------------------------------------------------
// loop
// ----------------------------------------------------------------------
Measurement DistanceSensor::read()
{
int d = _hcsr04->readDistance();
if (d == -1)
{
_logger.log("s", "ERROR - Distance measurement failed");
return Measurement::Failed();
}
else
{
_logger.log("sis", "Distance = ", d, "mm");
return Measurement::Value(d);
}
}
} // namespace Dough

View File

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

View File

@ -1,47 +1,50 @@
#include "HumiditySensor.h"
// ----------------------------------------------------------------------
// Constructor
// ----------------------------------------------------------------------
HumiditySensor *HumiditySensor::_instance = nullptr;
HumiditySensor *HumiditySensor::Instance()
namespace Dough
{
if (HumiditySensor::_instance == nullptr)
// ----------------------------------------------------------------------
// Constructor
// ----------------------------------------------------------------------
HumiditySensor *HumiditySensor::_instance = nullptr;
HumiditySensor *HumiditySensor::Instance()
{
HumiditySensor::_instance = new HumiditySensor();
if (HumiditySensor::_instance == nullptr)
{
HumiditySensor::_instance = new HumiditySensor();
}
return HumiditySensor::_instance;
}
return HumiditySensor::_instance;
}
HumiditySensor::HumiditySensor() : _logger("HUMIDITY") {}
HumiditySensor::HumiditySensor() : _logger("HUMIDITY") {}
// ----------------------------------------------------------------------
// setup
// ----------------------------------------------------------------------
// ----------------------------------------------------------------------
// setup
// ----------------------------------------------------------------------
void HumiditySensor::setup()
{
SensorDHT11::Instance()->begin();
}
// ----------------------------------------------------------------------
// loop
// ----------------------------------------------------------------------
Measurement HumiditySensor::read()
{
float t = SensorDHT11::Instance()->readHumidity();
if (t == -1)
void HumiditySensor::setup()
{
_logger.log("s", "ERROR - Humidity measurement failed");
return Measurement::Failed();
SensorDHT11::Instance()->begin();
}
else
// ----------------------------------------------------------------------
// loop
// ----------------------------------------------------------------------
Measurement HumiditySensor::read()
{
_logger.log("sis", "Humidity = ", int(t), "%");
DistanceSensor::Instance()->setHumidity(int(t));
return Measurement::Value(int(t));
float t = SensorDHT11::Instance()->readHumidity();
if (t == -1)
{
_logger.log("s", "ERROR - Humidity measurement failed");
return Measurement::Failed();
}
else
{
_logger.log("sis", "Humidity = ", int(t), "%");
DistanceSensor::Instance()->setHumidity(int(t));
return Measurement::Value(int(t));
}
}
}
} // namespace Dough

View File

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

View File

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

View File

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

View File

@ -1,155 +1,152 @@
#include "Sensors/LowLevel/SensorHCSR04.h"
SensorHCSR04::SensorHCSR04(int triggerPin, int echoPin) : _logger("HCSR04")
namespace Dough
{
_triggerPin = triggerPin;
_echoPin = echoPin;
_temperature = HCSR04_INIT_TEMPERATURE;
_humidity = HCSR04_INIT_HUMIDITY;
#ifndef HCSR04_DEBUG
_logger.suspend();
#endif
}
void SensorHCSR04::setup()
{
_logger.log("sisi", "Setup output pin ", _triggerPin, " and input pin ", _echoPin);
pinMode(_triggerPin, OUTPUT);
pinMode(_echoPin, INPUT);
}
void SensorHCSR04::setTemperature(int temperature)
{
_logger.log("sis", "Set temperature to ", temperature, "°C");
_temperature = temperature;
}
void SensorHCSR04::setHumidity(int humidity)
{
_logger.log("sis", "Set humidity to ", humidity, "%");
_humidity = humidity;
}
/**
* Get a distance reading.
* When reading the distance fails, -1 is returned.
* Otherwise the distance in mm.
*/
int SensorHCSR04::readDistance()
{
_setSpeedOfSound();
_setEchoTimeout();
_takeSamples();
if (_haveEnoughSamples())
SensorHCSR04::SensorHCSR04(int triggerPin, int echoPin) : _logger("HCSR04")
{
_sortSamples();
return _computeAverage();
_triggerPin = triggerPin;
_echoPin = echoPin;
_temperature = HCSR04_INIT_TEMPERATURE;
_humidity = HCSR04_INIT_HUMIDITY;
#ifndef HCSR04_DEBUG
_logger.suspend();
#endif
}
return -1;
}
/**
* Sets the speed of sound in mm/Ms, depending on the temperature
* and relative humidity. I derived this formula from a YouTube
* video about the HC-SR04: https://youtu.be/6F1B_N6LuKw?t=1548
*/
void SensorHCSR04::_setSpeedOfSound()
{
_speedOfSound =
0.3314 +
(0.000606 * _temperature) +
(0.0000124 * _humidity);
_logger.log("sfs", "Speed of sound = ", _speedOfSound, "mm/Ms");
}
void SensorHCSR04::_setEchoTimeout()
{
_echoTimeout = HCSR04_MAX_MM * 2 / _speedOfSound;
_logger.log("sfs", "Echo timeout = ", _echoTimeout, "Ms");
}
void SensorHCSR04::_takeSamples()
{
_successfulSamples = 0;
for (int i = 0; i < HCSR04_SAMPLES_TAKE; i++)
void SensorHCSR04::setup()
{
// Because I notice some repeating patterns in timings when doing
// a tight loop here, I add some random waits to get a better spread
// of sample values.
if (i > 0)
{
delay(HCSR04_SAMPLE_WAIT + random(HCSR04_SAMPLE_WAIT_SPREAD));
}
int distance = _takeSample();
if (distance != -1)
{
_samples[i] = distance;
_successfulSamples++;
}
_logger.log("sisi", "Setup output pin ", _triggerPin, " and input pin ", _echoPin);
pinMode(_triggerPin, OUTPUT);
pinMode(_echoPin, INPUT);
}
}
bool SensorHCSR04::_haveEnoughSamples()
{
return _successfulSamples >= HCSR04_SAMPLES_USE;
}
int SensorHCSR04::_takeSample()
{
// Send 10μs trigger to ask sensor for a measurement.
digitalWrite(HCSR04_TRIG_PIN, LOW);
delayMicroseconds(2);
digitalWrite(HCSR04_TRIG_PIN, HIGH);
delayMicroseconds(10);
digitalWrite(HCSR04_TRIG_PIN, LOW);
// Measure the length of echo signal.
unsigned long durationMicroSec = pulseIn(HCSR04_ECHO_PIN, HIGH, _echoTimeout);
// Compute the distance, based on the echo signal length.
double distance = durationMicroSec / 2.0 * _speedOfSound;
_logger.log("sfs", "Sample result = ", distance, "mm");
if (distance < HCSR04_MIN_MM || distance >= HCSR04_MAX_MM)
void SensorHCSR04::setTemperature(int temperature)
{
_logger.log("sis", "Set temperature to ", temperature, "°C");
_temperature = temperature;
}
void SensorHCSR04::setHumidity(int humidity)
{
_logger.log("sis", "Set humidity to ", humidity, "%");
_humidity = humidity;
}
// Get a distance reading.
// When reading the distance fails, -1 is returned.
// Otherwise the distance in mm.
int SensorHCSR04::readDistance()
{
_setSpeedOfSound();
_setEchoTimeout();
_takeSamples();
if (_haveEnoughSamples())
{
_sortSamples();
return _computeAverage();
}
return -1;
}
else
{
return distance;
}
}
void SensorHCSR04::_sortSamples()
{
int holder, x, y;
for (x = 0; x < _successfulSamples; x++)
// Sets the speed of sound in mm/Ms, depending on the temperature
// and relative humidity. I derived this formula from a YouTube
// video about the HC-SR04: https://youtu.be/6F1B_N6LuKw?t=1548
void SensorHCSR04::_setSpeedOfSound()
{
for (y = 0; y < _successfulSamples - 1; y++)
_speedOfSound =
0.3314 +
(0.000606 * _temperature) +
(0.0000124 * _humidity);
_logger.log("sfs", "Speed of sound = ", _speedOfSound, "mm/Ms");
}
void SensorHCSR04::_setEchoTimeout()
{
_echoTimeout = HCSR04_MAX_MM * 2 / _speedOfSound;
_logger.log("sfs", "Echo timeout = ", _echoTimeout, "Ms");
}
void SensorHCSR04::_takeSamples()
{
_successfulSamples = 0;
for (int i = 0; i < HCSR04_SAMPLES_TAKE; i++)
{
if (_samples[y] > _samples[y + 1])
// Because I notice some repeating patterns in timings when doing
// a tight loop here, I add some random waits to get a better spread
// of sample values.
if (i > 0)
{
holder = _samples[y + 1];
_samples[y + 1] = _samples[y];
_samples[y] = holder;
delay(HCSR04_SAMPLE_WAIT + random(HCSR04_SAMPLE_WAIT_SPREAD));
}
int distance = _takeSample();
if (distance != -1)
{
_samples[i] = distance;
_successfulSamples++;
}
}
}
}
/**
* Compute the average of the samples. To get rid of measuring extremes,
* only a subset of measurements from the middle are used.
* When not enough samples were collected in the previous steps, then
* NAN is returned.
*/
int SensorHCSR04::_computeAverage()
{
float sum = 0;
int offset = (_successfulSamples - HCSR04_SAMPLES_USE) / 2;
for (int i = 0; i < HCSR04_SAMPLES_USE; i++)
bool SensorHCSR04::_haveEnoughSamples()
{
sum += _samples[i + offset];
return _successfulSamples >= HCSR04_SAMPLES_USE;
}
return round(sum / HCSR04_SAMPLES_USE);
}
int SensorHCSR04::_takeSample()
{
// Send 10μs trigger to ask sensor for a measurement.
digitalWrite(HCSR04_TRIG_PIN, LOW);
delayMicroseconds(2);
digitalWrite(HCSR04_TRIG_PIN, HIGH);
delayMicroseconds(10);
digitalWrite(HCSR04_TRIG_PIN, LOW);
// Measure the length of echo signal.
unsigned long durationMicroSec = pulseIn(HCSR04_ECHO_PIN, HIGH, _echoTimeout);
// Compute the distance, based on the echo signal length.
double distance = durationMicroSec / 2.0 * _speedOfSound;
_logger.log("sfs", "Sample result = ", distance, "mm");
if (distance < HCSR04_MIN_MM || distance >= HCSR04_MAX_MM)
{
return -1;
}
else
{
return distance;
}
}
void SensorHCSR04::_sortSamples()
{
int holder, x, y;
for (x = 0; x < _successfulSamples; x++)
{
for (y = 0; y < _successfulSamples - 1; y++)
{
if (_samples[y] > _samples[y + 1])
{
holder = _samples[y + 1];
_samples[y + 1] = _samples[y];
_samples[y] = holder;
}
}
}
}
// Compute the average of the samples. To get rid of measuring extremes,
// only a subset of measurements from the middle are used.
// When not enough samples were collected in the previous steps, then
// NAN is returned.
int SensorHCSR04::_computeAverage()
{
float sum = 0;
int offset = (_successfulSamples - HCSR04_SAMPLES_USE) / 2;
for (int i = 0; i < HCSR04_SAMPLES_USE; i++)
{
sum += _samples[i + offset];
}
return round(sum / HCSR04_SAMPLES_USE);
}
} // namespace Dough

View File

@ -29,38 +29,39 @@
#undef HCSR04_DEBUG
#include <Arduino.h>
#include "UI/DoughLogger.h"
#include "UI/Logger.h"
#include "config.h"
/**
* This class is used to get a distance reading from an HCSR04 sensor.
*/
class SensorHCSR04
namespace Dough
{
public:
SensorHCSR04(int triggerPin, int echoPin);
void setup();
void setTemperature(int temperature);
void setHumidity(int humidity);
int readDistance();
// This class is used to get a distance reading from an HCSR04 sensor.
class SensorHCSR04
{
public:
SensorHCSR04(int triggerPin, int echoPin);
void setup();
void setTemperature(int temperature);
void setHumidity(int humidity);
int readDistance();
private:
DoughLogger _logger;
int _triggerPin;
int _echoPin;
int _humidity;
int _temperature;
void _setSpeedOfSound();
float _speedOfSound;
void _setEchoTimeout();
int _echoTimeout;
float _samples[HCSR04_SAMPLES_TAKE];
void _takeSamples();
bool _haveEnoughSamples();
int _takeSample();
int _successfulSamples;
void _sortSamples();
int _computeAverage();
};
private:
Logger _logger;
int _triggerPin;
int _echoPin;
int _humidity;
int _temperature;
void _setSpeedOfSound();
float _speedOfSound;
void _setEchoTimeout();
int _echoTimeout;
float _samples[HCSR04_SAMPLES_TAKE];
void _takeSamples();
bool _haveEnoughSamples();
int _takeSample();
int _successfulSamples;
void _sortSamples();
int _computeAverage();
};
} // namespace Dough
#endif
#endif

View File

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

View File

@ -1,47 +1,50 @@
#include "TemperatureSensor.h"
// ----------------------------------------------------------------------
// Constructor
// ----------------------------------------------------------------------
TemperatureSensor *TemperatureSensor::_instance = nullptr;
TemperatureSensor *TemperatureSensor::Instance()
namespace Dough
{
if (TemperatureSensor::_instance == nullptr)
// ----------------------------------------------------------------------
// Constructor
// ----------------------------------------------------------------------
TemperatureSensor *TemperatureSensor::_instance = nullptr;
TemperatureSensor *TemperatureSensor::Instance()
{
TemperatureSensor::_instance = new TemperatureSensor();
if (TemperatureSensor::_instance == nullptr)
{
TemperatureSensor::_instance = new TemperatureSensor();
}
return TemperatureSensor::_instance;
}
return TemperatureSensor::_instance;
}
TemperatureSensor::TemperatureSensor() : _logger("TEMPERATURE") {}
TemperatureSensor::TemperatureSensor() : _logger("TEMPERATURE") {}
// ----------------------------------------------------------------------
// setup
// ----------------------------------------------------------------------
// ----------------------------------------------------------------------
// setup
// ----------------------------------------------------------------------
void TemperatureSensor::setup()
{
SensorDHT11::Instance()->begin();
}
// ----------------------------------------------------------------------
// loop
// ----------------------------------------------------------------------
Measurement TemperatureSensor::read()
{
float t = SensorDHT11::Instance()->readTemperature();
if (isnan(t))
void TemperatureSensor::setup()
{
_logger.log("s", "ERROR - Temperature measurement failed");
return Measurement::Failed();
SensorDHT11::Instance()->begin();
}
else
// ----------------------------------------------------------------------
// loop
// ----------------------------------------------------------------------
Measurement TemperatureSensor::read()
{
_logger.log("sis", "Temperature = ", int(t), "°C");
DistanceSensor::Instance()->setTemperature(int(t));
return Measurement::Value(int(t));
float t = SensorDHT11::Instance()->readTemperature();
if (isnan(t))
{
_logger.log("s", "ERROR - Temperature measurement failed");
return Measurement::Failed();
}
else
{
_logger.log("sis", "Temperature = ", int(t), "°C");
DistanceSensor::Instance()->setTemperature(int(t));
return Measurement::Value(int(t));
}
}
}
} // namespace Dough

View File

@ -3,25 +3,26 @@
#include "Sensors/SensorBase.h"
#include "Sensors/LowLevel/SensorDHT11.h"
#include "UI/DoughLogger.h"
#include "UI/Logger.h"
#include "Data/Measurement.h"
#include "Sensors/DistanceSensor.h"
#include "config.h"
/**
* This class provides access to the temperature sensor in the device.
*/
class TemperatureSensor : public SensorBase
namespace Dough
{
public:
static TemperatureSensor *Instance();
virtual void setup();
virtual Measurement read();
// This class provides access to the temperature sensor in the device.
class TemperatureSensor : public SensorBase
{
public:
static TemperatureSensor *Instance();
virtual void setup();
virtual Measurement read();
private:
TemperatureSensor();
static TemperatureSensor *_instance;
DoughLogger _logger;
};
private:
TemperatureSensor();
static TemperatureSensor *_instance;
Logger _logger;
};
} // namespace Dough
#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 <stdarg.h>
#include "UI/DoughButton.h"
#include "UI/DoughLED.h"
#include "UI/Button.h"
#include "UI/LED.h"
#include "config.h"
/**
* This class groups all user interface functionality: serial logging,
* LEDs and buttons.
*/
class DoughUI
namespace Dough
{
// This class groups all user interface functionality: serial logging,
// LEDs and buttons.
class UI
{
public:
static DoughUI *Instance();
static UI *Instance();
void setup();
static void onoffButtonISR();
static void setupButtonISR();
DoughButton onoffButton;
DoughButton setupButton;
DoughLED ledBuiltin;
DoughLED led1;
DoughLED led2;
DoughLED led3;
Button onoffButton;
Button setupButton;
LED ledBuiltin;
LED led1;
LED led2;
LED led3;
void processButtonEvents();
void clearButtonEvents();
void updatedLEDs();
@ -40,10 +40,11 @@ public:
void suspend();
private:
DoughUI();
UI();
void _setupTimerInterrupt();
static DoughUI *_instance;
static UI *_instance;
void _flash_all_leds();
};
}
#endif
#endif

View File

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

View File

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