Code formatting and structuring the project.

This commit is contained in:
Maurice Makaay 2020-07-09 21:09:46 +02:00
parent ea8723493c
commit e66b8ccdd5
34 changed files with 1205 additions and 988 deletions

Binary file not shown.

BIN
Schematics.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

242
src/Data/DataController.cpp Normal file
View File

@ -0,0 +1,242 @@
#include "Data/DataController.h"
#include "Data/Measurements.h"
// ----------------------------------------------------------------------
// Constructor
// ----------------------------------------------------------------------
DataController *DataController::_instance = nullptr;
/**
* Fetch the DoughData singleton.
*/
DataController *DataController::Instance()
{
if (DataController::_instance == nullptr)
{
DataController::_instance = new DataController();
}
return DataController::_instance;
}
DataController::DataController() : _temperatureMeasurements(TEMPERATURE_AVG_LOOKBACK),
_humidityMeasurements(HUMIDITY_AVG_LOOKBACK),
_distanceMeasurements(DISTANCE_AVG_LOOKBACK) {}
// ----------------------------------------------------------------------
// Setup
// ----------------------------------------------------------------------
void DataController::setup()
{
_containerHeight = 0.00;
_containerHeightSet = false;
DoughMQTT *mqtt = DoughMQTT::Instance();
mqtt->onConnect(DataController::handleMqttConnect);
mqtt->onMessage(DataController::handleMqttMessage);
}
void DataController::handleMqttConnect(DoughMQTT *mqtt)
{
mqtt->subscribe("container_height");
}
void DataController::handleMqttMessage(String &key, String &payload)
{
if (key.equals("container_height"))
{
DataController::Instance()->setContainerHeight(payload.toInt());
}
else
{
DoughUI::Instance()->log("DATA", "sS", "ERROR - Unhandled MQTT message, key = ", key);
}
}
/**
* Check if configuration has been taken care of. Some configuration is
* required before measurements can be processed.
*/
bool DataController::isConfigured()
{
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)
{
DoughUI::Instance()->log("DATA", "sisis",
"ERROR - Container height ", height,
"mm is less than the minimum measuring distance of ",
HCSR04_MIN_MM, "mm");
return;
}
if (height >= HCSR04_MAX_MM)
{
DoughUI::Instance()->log("DATA", "sisis",
"ERROR - Container height ", height,
"mm is more than the maximum measuring distance of ",
HCSR04_MAX_MM, "mm");
return;
}
DoughUI::Instance()->log("DATA", "sis", "Set container height to ", height, "mm");
_containerHeight = height;
_containerHeightSet = true;
}
// ----------------------------------------------------------------------
// Loop
// ----------------------------------------------------------------------
void DataController::loop()
{
if (isConfigured())
{
_sample();
_publish();
}
}
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)
{
DoughUI *ui = DoughUI::Instance();
_lastSample = now;
DoughSensors *sensors = DoughSensors::Instance();
// 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.add(sensors->readTemperature());
_sampleType = SAMPLE_HUMIDITY;
break;
case SAMPLE_HUMIDITY:
_humidityMeasurements.add(sensors->readHumidity());
_sampleType = SAMPLE_DISTANCE;
break;
case SAMPLE_DISTANCE:
_distanceMeasurements.add(sensors->readDistance());
break;
}
ui->resume();
_sampleCounter++;
if (_sampleCounter == SAMPLE_CYCLE_LENGTH)
{
_sampleCounter = 0;
_sampleType = SAMPLE_TEMPERATURE;
}
}
}
void DataController::_publish()
{
static unsigned long lastSample = 0;
if (lastSample == 0 || millis() - lastSample > PUBLISH_INTERVAL)
{
lastSample = millis();
DoughUI *ui = DoughUI::Instance();
DoughMQTT *mqtt = DoughMQTT::Instance();
ui->log("DATA", "s", "Publish temperature");
auto m = _temperatureMeasurements.getLast();
if (m->ok)
{
mqtt->publish("temperature", m->value);
}
else
{
mqtt->publish("temperature", "null");
}
ui->log("DATA", "s", "Publish temperature average");
m = _temperatureMeasurements.getAverage();
if (m->ok)
{
mqtt->publish("temperature/average", m->value);
}
else
{
mqtt->publish("temperature/average", "null");
}
ui->log("DATA", "s", "Publish humidity");
m = _humidityMeasurements.getLast();
if (m->ok)
{
mqtt->publish("humidity", m->value);
}
else
{
mqtt->publish("humidity", "null");
}
m = _humidityMeasurements.getAverage();
if (m->ok)
{
mqtt->publish("humidity/average", m->value);
}
else
{
mqtt->publish("humidity/average", "null");
}
m = _distanceMeasurements.getLast();
if (m->ok)
{
mqtt->publish("distance", m->value);
}
else
{
mqtt->publish("distance", "null");
}
m = _distanceMeasurements.getAverage();
if (m->ok)
{
mqtt->publish("distance/average", m->value);
}
else
{
mqtt->publish("distance/average", "null");
}
ui->led1.dip()->fast();
}
}

71
src/Data/DataController.h Normal file
View File

@ -0,0 +1,71 @@
#ifndef DOUGH_DATA_DATACONTROLLER_H
#define DOUGH_DATA_DATACONTROLLER_H
// These definitions describes what measurements are performed in sequence.
// One measurement is done every SAMPLE_INTERVAL microseconds.
// We always start with a temperature measurement, then a humidity measurement,
// and finally a number of distance measurements.
// The SAMPLE_CYCLE_LENGTH defines the total number of samples in this sequence.
#define SAMPLE_INTERVAL 1000
#define SAMPLE_CYCLE_LENGTH 30 // 1 temperature + 1 humidity + 28 distance samples
// Two different values are published per sensor: a recent value and an average
// value. These definition define the number of measurements to include in the
// average computation.
#define TEMPERATURE_AVG_LOOKBACK 10 // making this a 5 minute average
#define HUMIDITY_AVG_LOOKBACK 10 // making this a 5 minute average
#define DISTANCE_AVG_LOOKBACK 28 * 2 * 5 // making this a 5 minute average
// The minimal interval at which to publish measurements to the MQTT broker.
// When significant changes occur in the measurements, then these will be published
// to the MQTT broker at all times, independent from this interval.
#define PUBLISH_INTERVAL 4000
#include <Arduino.h>
#include "Data/Measurements.h"
#include "Sensors/DoughSensors.h"
#include "Network/DoughWiFi.h"
#include "Network/DoughMQTT.h"
#include "UI/DoughUI.h"
typedef enum
{
SAMPLE_TEMPERATURE,
SAMPLE_HUMIDITY,
SAMPLE_DISTANCE
} DoughSampleType;
/**
* The DoughData class is responsible for holding the device configuration,
* collecting measurements from sensors, gathering the 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(DoughMQTT *mqtt);
static void handleMqttMessage(String &key, String &value);
private:
DataController();
static DataController *_instance;
DoughSensors *_sensors;
unsigned long _lastSample = 0;
DoughSampleType _sampleType = SAMPLE_TEMPERATURE;
int _sampleCounter = 0;
int _containerHeight;
bool _containerHeightSet;
void _sample();
void _publish();
Measurements _temperatureMeasurements;
Measurements _humidityMeasurements;
Measurements _distanceMeasurements;
};
#endif

19
src/Data/Measurement.cpp Normal file
View File

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

18
src/Data/Measurement.h Normal file
View File

@ -0,0 +1,18 @@
#ifndef DOUGH_DATA_MEASUREMENT_H
#define DOUGH_DATA_MEASUREMENT_H
/**
* The DoughDataMeasurement class represents a single measurement.
*/
class Measurement
{
public:
Measurement();
Measurement(bool ok, int value);
int value = 0;
bool ok = false;
static Measurement *Failed();
static Measurement *Ok(int value);
};
#endif

67
src/Data/Measurements.cpp Normal file
View File

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

28
src/Data/Measurements.h Normal file
View File

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

View File

@ -1,42 +0,0 @@
#ifndef DOUGH_BUTTON_H
#define DOUGH_BUTTON_H
#define BUTTON_DEBOUNCE_DELAY 50
#define BUTTON_LONGPRESS_DELAY 1000
#include <Arduino.h>
#include "config.h"
typedef enum {
UP,
DOWN,
DOWN_LONG,
UP_AFTER_LONG,
UP_AFTER_SHORT,
READY_FOR_NEXT_PRESS
} DoughButtonState;
typedef void (*DoughButtonHandler)();
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,274 +0,0 @@
#include "DoughData.h"
// ----------------------------------------------------------------------
// Constructor
// ----------------------------------------------------------------------
DoughData* DoughData::_instance = nullptr;
/**
* Fetch the DoughData singleton.
*/
DoughData* DoughData::Instance() {
if (DoughData::_instance == nullptr) {
DoughData::_instance = new DoughData();
}
return DoughData::_instance;
}
DoughData::DoughData() : _temperatureMeasurements(TEMPERATURE_AVG_LOOKBACK),
_humidityMeasurements(HUMIDITY_AVG_LOOKBACK),
_distanceMeasurements(DISTANCE_AVG_LOOKBACK) {}
// ----------------------------------------------------------------------
// Measurements storage
// ----------------------------------------------------------------------
DoughDataMeasurements::DoughDataMeasurements(int avgLookback) {
_storageSize = avgLookback;
_storage = new DoughDataMeasurement[avgLookback];
for (int i = 0; i < avgLookback; i++) {
_storage[i] = DoughDataMeasurement();
}
}
void DoughDataMeasurements::registerValue(int value) {
auto measurement = _next();
_averageCount++;
_averageSum += value;
measurement->ok = true;
measurement->value = value;
}
void DoughDataMeasurements::registerFailed() {
auto measurement = _next();
measurement->ok = false;
measurement->value = 0;
}
DoughDataMeasurement* DoughDataMeasurements::_next() {
_index++;
if (_index == _storageSize) {
_index = 0;
}
if (_storage[_index].ok) {
_averageSum -= _storage[_index].value;
_averageCount--;
}
return &(_storage[_index]);
}
DoughDataMeasurement DoughDataMeasurements::getLast() {
return _storage[_index];
}
DoughDataMeasurement DoughDataMeasurements::getAverage() {
DoughDataMeasurement result;
if (_averageCount > 0) {
result.ok = true;
result.value = round(_averageSum / _averageCount);
}
return result;
}
void DoughDataMeasurements::clearHistory() {
_averageCount = 0;
_averageSum = 0;
for (unsigned int i = 0; i < _storageSize; i++) {
_storage[i].ok = false;
_storage[i].value = 0;
}
}
// ----------------------------------------------------------------------
// Setup
// ----------------------------------------------------------------------
void DoughData::setup() {
_containerHeight = 0.00;
_containerHeightSet = false;
DoughMQTT *mqtt = DoughMQTT::Instance();
mqtt->onConnect(DoughData::handleMqttConnect);
mqtt->onMessage(DoughData::handleMqttMessage);
}
void DoughData::handleMqttConnect(DoughMQTT* mqtt) {
mqtt->subscribe("container_height");
}
void DoughData::handleMqttMessage(String &key, String &payload) {
if (key.equals("container_height")) {
DoughData::Instance()->setContainerHeight(payload.toInt());
} else {
DoughUI::Instance()->log("DATA", "sS", "ERROR - Unhandled MQTT message, key = ", key);
}
}
/**
* Check if configuration has been taken care of. Some configuration is
* required before measurements can be processed.
*/
bool DoughData::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 DoughData::setContainerHeight(int height) {
_containerHeightSet = false;
if (height <= HCSR04_MIN_MM) {
DoughUI::Instance()->log("DATA", "sisis",
"ERROR - Container height ", height,
"mm is less than the minimum measuring distance of ",
HCSR04_MIN_MM, "mm");
return;
}
if (height >= HCSR04_MAX_MM) {
DoughUI::Instance()->log("DATA", "sisis",
"ERROR - Container height ", height,
"mm is more than the maximum measuring distance of ",
HCSR04_MAX_MM, "mm");
return;
}
DoughUI::Instance()->log("DATA", "sis", "Set container height to ", height, "mm");
_containerHeight = height;
_containerHeightSet = true;
}
// ----------------------------------------------------------------------
// Loop
// ----------------------------------------------------------------------
void DoughData::loop() {
if (isConfigured()) {
_sample();
_publish();
}
}
void DoughData::clearHistory() {
_temperatureMeasurements.clearHistory();
_humidityMeasurements.clearHistory();
_distanceMeasurements.clearHistory();
_sampleType = SAMPLE_TEMPERATURE;
_sampleCounter = 0;
}
void DoughData::_sample() {
auto now = millis();
auto delta = now - _lastSample;
auto tick = _lastSample == 0 || delta >= SAMPLE_INTERVAL;
if (tick) {
_lastSample = now;
DoughUI* ui = DoughUI::Instance();
DoughSensors* sensors = DoughSensors::Instance();
// 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:
sensors->readTemperature();
if (sensors->temperatureOk) {
_temperatureMeasurements.registerValue(sensors->temperature);
} else {
_temperatureMeasurements.registerFailed();
}
_sampleType = SAMPLE_HUMIDITY;
break;
case SAMPLE_HUMIDITY:
sensors->readHumidity();
if (sensors->humidityOk) {
_humidityMeasurements.registerValue(sensors->humidity);
} else {
_humidityMeasurements.registerFailed();
}
_sampleType = SAMPLE_DISTANCE;
break;
case SAMPLE_DISTANCE:
sensors->readDistance();
if (sensors->distanceOk) {
_distanceMeasurements.registerValue(sensors->distance);
} else {
_distanceMeasurements.registerFailed();
}
break;
}
ui->resume();
_sampleCounter++;
if (_sampleCounter == SAMPLE_CYCLE_LENGTH) {
_sampleCounter = 0;
_sampleType = SAMPLE_TEMPERATURE;
}
}
}
void DoughData::_publish() {
static unsigned long lastSample = 0;
if (lastSample == 0 || millis() - lastSample > PUBLISH_INTERVAL) {
lastSample = millis();
DoughUI* ui = DoughUI::Instance();
DoughMQTT* mqtt = DoughMQTT::Instance();
auto m = _temperatureMeasurements.getLast();
if (m.ok) {
mqtt->publish("temperature", m.value);
} else {
mqtt->publish("temperature", "null");
}
m = _temperatureMeasurements.getAverage();
if (m.ok) {
mqtt->publish("temperature/average", m.value);
} else {
mqtt->publish("temperature/average", "null");
}
m = _humidityMeasurements.getLast();
if (m.ok) {
mqtt->publish("humidity", m.value);
} else {
mqtt->publish("humidity", "null");
}
m = _humidityMeasurements.getAverage();
if (m.ok) {
mqtt->publish("humidity/average", m.value);
} else {
mqtt->publish("humidity/average", "null");
}
m = _distanceMeasurements.getLast();
if (m.ok) {
mqtt->publish("distance", m.value);
} else {
mqtt->publish("distance", "null");
}
m = _distanceMeasurements.getAverage();
if (m.ok) {
mqtt->publish("distance/average", m.value);
} else {
mqtt->publish("distance/average", "null");
}
ui->led1.dip()->fast();
}
}

View File

@ -1,98 +0,0 @@
#ifndef DOUGH_DATA_H
#define DOUGH_DATA_H
// These definitions describes what measurements are performed in sequence.
// One measurement is done every SAMPLE_INTERVAL microseconds.
// We always start with a temperature measurement, then a humidity measurement,
// and finally a number of distance measurements.
// The SAMPLE_CYCLE_LENGTH defines the total number of samples in this sequence.
#define SAMPLE_INTERVAL 1000
#define SAMPLE_CYCLE_LENGTH 30 // 1 temperature + 1 humidity + 28 distance samples
// Two different values are published per sensor: a recent value and an average
// value. These definition define the number of measurements to include in the
// average computation.
#define TEMPERATURE_AVG_LOOKBACK 10 // making this a 5 minute average
#define HUMIDITY_AVG_LOOKBACK 10 // making this a 5 minute average
#define DISTANCE_AVG_LOOKBACK 28 * 2 * 5 // making this a 5 minute average
// The minimal interval at which to publish measurements to the MQTT broker.
// When significant changes occur in the measurements, then these will be published
// to the MQTT broker at all times, independent from this interval.
#define PUBLISH_INTERVAL 4000
#include <Arduino.h>
#include "DoughSensors.h"
#include "DoughNetwork.h"
#include "DoughMQTT.h"
#include "DoughUI.h"
typedef enum {
SAMPLE_TEMPERATURE,
SAMPLE_HUMIDITY,
SAMPLE_DISTANCE
} DoughSampleType;
/**
* The DoughDataMeasurement struct represents a single measurement.
*/
struct DoughDataMeasurement {
public:
int value = 0;
bool ok = false;
};
/**
* The DoughDataMeasurements class is used to store measurements for a sensor
* and to keep track of running totals for handling average computations.
*/
class DoughDataMeasurements {
public:
DoughDataMeasurements(int avgLookback);
void registerValue(int value);
void registerFailed();
DoughDataMeasurement getLast();
DoughDataMeasurement getAverage();
void clearHistory();
private:
DoughDataMeasurement* _storage;
unsigned int _storageSize;
int _averageSum = 0;
unsigned int _averageCount = 0;
unsigned int _index = 0;
DoughDataMeasurement* _next();
};
/**
* The DoughData class is responsible for holding the device configuration,
* collecting measurements from sensors, gathering the statistics on these data,
* and publishing results to the MQTT broker.
*/
class DoughData {
public:
static DoughData* Instance();
void setup();
void loop();
void clearHistory();
void setContainerHeight(int height);
bool isConfigured();
static void handleMqttConnect(DoughMQTT *mqtt);
static void handleMqttMessage(String &key, String &value);
private:
DoughData();
static DoughData* _instance;
DoughSensors * _sensors;
unsigned long _lastSample = 0;
DoughSampleType _sampleType = SAMPLE_TEMPERATURE;
int _sampleCounter = 0;
int _containerHeight;
bool _containerHeightSet;
void _sample();
void _publish();
DoughDataMeasurements _temperatureMeasurements;
DoughDataMeasurements _humidityMeasurements;
DoughDataMeasurements _distanceMeasurements;
};
#endif

View File

@ -1,53 +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>
#include "config.h"
typedef enum {
ON,
OFF,
BLINK_ON,
BLINK_OFF,
FLASH,
DIP,
PULSE
} DoughLEDState;
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,39 +0,0 @@
#ifndef DOUGH_MQTT_H
#define DOUGH_MQTT_H
#include <MQTT.h>
#include <MQTTClient.h>
#include "DoughNetwork.h"
#include "DoughUI.h"
#include "config.h"
class DoughMQTT;
typedef void (*DoughMQTTConnectHandler)(DoughMQTT* mqtt);
typedef void (*DoughMQTTMessageHandler)(String &key, String &value);
class DoughMQTT {
public:
static DoughMQTT* Instance();
void setup();
void onConnect(DoughMQTTConnectHandler callback);
void onMessage(DoughMQTTMessageHandler callback);
bool isConnected();
bool connect();
void subscribe(const char* key);
void procesIncomingsMessages();
void publish(const char* key, const char* payload);
void publish(const char* key, int payload);
private:
DoughMQTT();
static DoughMQTT* _instance;
MQTTClient _mqttClient;
DoughUI* _ui;
DoughMQTTConnectHandler _onConnect = nullptr;
MQTTClientCallbackSimple _onMessage = nullptr;
static void handleMessage(String &topic, String &payload);
char *_mqttDeviceId;
};
#endif

View File

@ -1,26 +0,0 @@
#ifndef DOUGH_NETWORK_H
#define DOUGH_NETWORK_H
#include <WiFiNINA.h>
#include "DoughUI.h"
#include "config.h"
class DoughNetwork {
public:
static DoughNetwork* Instance();
char *getMacAddress();
void setup();
void loop();
bool isConnected();
bool connect();
WiFiClient client;
private:
DoughNetwork();
static DoughNetwork* _instance;
void _setMacAddress();
char _macAddress[18]; // max MAC address length + 1
DoughUI* _ui;
};
#endif

View File

@ -1,198 +0,0 @@
#include "DoughUI.h"
DoughUI* DoughUI::_instance = nullptr;
/**
* Fetch the DoughUI singleton.
*/
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) {}
/**
* Called from the main setup() function of the sketch.
*/
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();
}
/**
* Log a message to the serial interface.
*/
void DoughUI::log(const char *category, const char *fmt, ...) {
char buf[12];
snprintf(buf, sizeof(buf)/sizeof(buf[0]), "%8s | ", category);
Serial.print(buf);
va_list args;
va_start(args, fmt);
while (*fmt != '\0') {
if (*fmt == 'i') {
int i = va_arg(args, int);
Serial.print(i);
}
else if (*fmt == 'f') {
float f = va_arg(args, double);
Serial.print(f);
}
else if (*fmt == 'a') {
IPAddress a = va_arg(args, IPAddress);
Serial.print(a);
}
else if (*fmt == 's') {
const char* s = va_arg(args, const char*);
Serial.print(s);
}
else if (*fmt == 'S') {
String S = va_arg(args, String);
Serial.print(S);
}
else {
Serial.print("<log(): invalid format char '");
Serial.print(*fmt);
Serial.print("'>");
}
fmt++;
}
va_end(args);
Serial.println("");
}
/**
* Setup a timer interrupt for updating the GUI. Unfortunately, the standard
* 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
}
void DoughUI::resume() {
NVIC_EnableIRQ(TC4_IRQn); // Enable TC4 interrupts
}
void DoughUI::suspend() {
NVIC_DisableIRQ(TC4_IRQn); // Disable TC4 interrupts
}
/**
* This callback is called when the TC4 timer hits an overflow interrupt.
*/
void TC4_Handler() {
DoughUI::Instance()->updatedLEDs();
REG_TC4_INTFLAG = TC_INTFLAG_OVF; // Clear the OVF interrupt flag.
}
/**
* 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 invocatino
* 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();
}

View File

@ -1,46 +0,0 @@
#ifndef DOUGH_UI_H
#define DOUGH_UI_H
#define LOG_BAUDRATE 9600
// Define this one to wait for USB serial to come up.
// This can be useful during development, when you want all
// serial messages to appear in the serial monitor.
// Without this, some of the initial serial messages might
// be missing from the output.
#undef LOG_WAIT_SERIAL
#include <Arduino.h>
#include <WiFiNINA.h>
#include <stdarg.h>
#include "DoughButton.h"
#include "DoughLED.h"
#include "config.h"
class DoughUI {
public:
static DoughUI* Instance();
void setup();
static void onoffButtonISR();
static void setupButtonISR();
DoughButton onoffButton;
DoughButton setupButton;
DoughLED ledBuiltin;
DoughLED led1;
DoughLED led2;
DoughLED led3;
void processButtonEvents();
void clearButtonEvents();
void updatedLEDs();
void flash_all_leds();
void resume();
void suspend();
void log(const char *category, const char *fmt, ...);
private:
DoughUI();
void _setupTimerInterrupt();
static DoughUI* _instance;
};
#endif

View File

@ -4,19 +4,22 @@
// Constructor
// ----------------------------------------------------------------------
DoughMQTT* DoughMQTT::_instance = nullptr;
DoughMQTT *DoughMQTT::_instance = nullptr;
/**
* Fetch the DoughMQTT singleton.
*/
DoughMQTT* DoughMQTT::Instance() {
if (DoughMQTT::_instance == nullptr) {
DoughMQTT *DoughMQTT::Instance()
{
if (DoughMQTT::_instance == nullptr)
{
DoughMQTT::_instance = new DoughMQTT();
}
return DoughMQTT::_instance;
}
DoughMQTT::DoughMQTT() {
DoughMQTT::DoughMQTT()
{
_ui = DoughUI::Instance();
}
@ -24,24 +27,27 @@ DoughMQTT::DoughMQTT() {
// Setup
// ----------------------------------------------------------------------
void DoughMQTT::setup() {
DoughNetwork* network = DoughNetwork::Instance();
void DoughMQTT::setup()
{
DoughWiFi *network = DoughWiFi::Instance();
#ifdef MQTT_DEVICE_ID
#ifdef MQTT_DEVICE_ID
_mqttDeviceId = MQTT_DEVICE_ID;
#else
#else
_mqttDeviceId = network->getMacAddress();
#endif
#endif
_ui->log("MQTT", "ss", "Device ID = ", _mqttDeviceId);
_mqttClient.begin(MQTT_BROKER, MQTT_PORT, network->client);
_mqttClient.begin(MQTT_BROKER, MQTT_PORT, network->client);
}
void DoughMQTT::onConnect(DoughMQTTConnectHandler callback) {
void DoughMQTT::onConnect(DoughMQTTConnectHandler callback)
{
_onConnect = callback;
}
void DoughMQTT::onMessage(MQTTClientCallbackSimple callback) {
void DoughMQTT::onMessage(MQTTClientCallbackSimple callback)
{
_onMessage = callback;
}
@ -49,62 +55,73 @@ void DoughMQTT::onMessage(MQTTClientCallbackSimple callback) {
// Loop
// ----------------------------------------------------------------------
bool DoughMQTT::isConnected() {
bool DoughMQTT::isConnected()
{
return _mqttClient.connected();
}
bool DoughMQTT::connect() {
_ui->log("MQTT" , "sssi", "Broker = ", MQTT_BROKER, ":", MQTT_PORT);
bool DoughMQTT::connect()
{
_ui->log("MQTT", "sssi", "Broker = ", MQTT_BROKER, ":", MQTT_PORT);
_mqttClient.connect(_mqttDeviceId, MQTT_USERNAME, MQTT_PASSWORD);
// Check if the connection to the broker was successful.
if (!_mqttClient.connected()) {
if (!_mqttClient.connected())
{
_ui->log("MQTT", "s", "ERROR - Connection to broker failed");
return false;
}
_mqttClient.onMessage(DoughMQTT::handleMessage);
if (_onConnect != nullptr) {
if (_onConnect != nullptr)
{
_onConnect(this);
}
return true;
}
void DoughMQTT::procesIncomingsMessages() {
void DoughMQTT::procesIncomingsMessages()
{
_mqttClient.loop();
}
void DoughMQTT::handleMessage(String &topic, String &payload) {
DoughUI::Instance()->log("MQTT", "sSsS", "<<< ", topic, " = ", payload);
void DoughMQTT::handleMessage(String &topic, String &payload)
{
DoughUI::Instance()->log("MQTT", "sSsS", "<<< ", topic, " = ", payload);
DoughMQTT *mqtt = DoughMQTT::Instance();
if (mqtt->_onMessage != nullptr) {
if (mqtt->_onMessage != nullptr)
{
int pos = topic.lastIndexOf('/');
if (pos != -1) {
topic.remove(0, pos+1);
if (pos != -1)
{
topic.remove(0, pos + 1);
mqtt->_onMessage(topic, payload);
}
}
}
void DoughMQTT::subscribe(const char* key) {
void DoughMQTT::subscribe(const char *key)
{
char topic[200];
snprintf(topic, sizeof(topic)/sizeof(topic[0]), "%s/%s/%s", MQTT_TOPIC_PREFIX, _mqttDeviceId, key);
snprintf(topic, sizeof(topic) / sizeof(topic[0]), "%s/%s/%s", MQTT_TOPIC_PREFIX, _mqttDeviceId, key);
DoughUI::Instance()->log("MQTT", "ss", "Subscribe to ", topic);
_mqttClient.subscribe(topic);
}
void DoughMQTT::publish(const char* key, const char* payload) {
void DoughMQTT::publish(const char *key, const char *payload)
{
char topic[200];
snprintf(topic, sizeof(topic)/sizeof(topic[0]), "%s/%s/%s", MQTT_TOPIC_PREFIX, _mqttDeviceId, key);
snprintf(topic, sizeof(topic) / sizeof(topic[0]), "%s/%s/%s", MQTT_TOPIC_PREFIX, _mqttDeviceId, key);
DoughUI::Instance()->log("MQTT", "ssss", ">>> ", topic, " = ", payload);
_mqttClient.publish(topic, payload);
}
void DoughMQTT::publish(const char* key, int payload) {
void DoughMQTT::publish(const char *key, int payload)
{
char buf[16];
snprintf(buf, 16, "%d", payload);
publish(key, buf);
}
}

40
src/Network/DoughMQTT.h Normal file
View File

@ -0,0 +1,40 @@
#ifndef DOUGH_MQTT_H
#define DOUGH_MQTT_H
#include <MQTT.h>
#include <MQTTClient.h>
#include "Network/DoughWiFi.h"
#include "UI/DoughUI.h"
#include "config.h"
class DoughMQTT;
typedef void (*DoughMQTTConnectHandler)(DoughMQTT *mqtt);
typedef void (*DoughMQTTMessageHandler)(String &key, String &value);
class DoughMQTT
{
public:
static DoughMQTT *Instance();
void setup();
void onConnect(DoughMQTTConnectHandler callback);
void onMessage(DoughMQTTMessageHandler callback);
bool isConnected();
bool connect();
void subscribe(const char *key);
void procesIncomingsMessages();
void publish(const char *key, const char *payload);
void publish(const char *key, int payload);
private:
DoughMQTT();
static DoughMQTT *_instance;
MQTTClient _mqttClient;
DoughUI *_ui;
DoughMQTTConnectHandler _onConnect = nullptr;
MQTTClientCallbackSimple _onMessage = nullptr;
static void handleMessage(String &topic, String &payload);
char *_mqttDeviceId;
};
#endif

View File

@ -1,22 +1,25 @@
#include "DoughNetwork.h"
#include "Network/DoughWiFi.h"
// ----------------------------------------------------------------------
// Constructor
// ----------------------------------------------------------------------
DoughNetwork* DoughNetwork::_instance = nullptr;
DoughWiFi *DoughWiFi::_instance = nullptr;
/**
* Fetch the DoughNetwork singleton.
* Fetch the DoughWiFi singleton.
*/
DoughNetwork* DoughNetwork::Instance() {
if (DoughNetwork::_instance == nullptr) {
DoughNetwork::_instance = new DoughNetwork();
DoughWiFi *DoughWiFi::Instance()
{
if (DoughWiFi::_instance == nullptr)
{
DoughWiFi::_instance = new DoughWiFi();
}
return DoughNetwork::_instance;
return DoughWiFi::_instance;
}
DoughNetwork::DoughNetwork() {
DoughWiFi::DoughWiFi()
{
_ui = DoughUI::Instance();
}
@ -24,57 +27,67 @@ DoughNetwork::DoughNetwork() {
// Setup
// ----------------------------------------------------------------------
void DoughNetwork::_setMacAddress() {
void DoughWiFi::_setMacAddress()
{
byte mac[6];
WiFi.macAddress(mac);
snprintf(
_macAddress, sizeof(_macAddress)/sizeof(_macAddress[0]),
_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 DoughNetwork::setup() {
void DoughWiFi::setup()
{
_setMacAddress();
DoughUI::Instance()->log("NETWORK", "ss", "MAC address = ", getMacAddress());
DoughUI::Instance()->log("NETWORK", "ss", "MAC address = ", getMacAddress());
}
// ----------------------------------------------------------------------
// Loop
// ----------------------------------------------------------------------
bool DoughNetwork::isConnected() {
bool DoughWiFi::isConnected()
{
return WiFi.status() == WL_CONNECTED;
}
bool DoughNetwork::connect() {
bool DoughWiFi::connect()
{
int status = WiFi.status();
// Check if a device with a WiFi shield is used.
if (status == WL_NO_SHIELD) {
if (status == WL_NO_SHIELD)
{
_ui->log("NETWORK", "s", "ERROR - Device has no WiFi shield");
delay(5000);
return false;
}
// Check if the WiFi network is already up.
if (status == WL_CONNECTED) {
if (status == WL_CONNECTED)
{
return true;
}
// Setup the connection to the WiFi network.
_ui->log("NETWORK", "ss", "WiFi network = ", WIFI_SSID);
status = WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
// Check if the connection attempt was successful.
if (status == WL_CONNECTED) {
if (status == WL_CONNECTED)
{
_ui->log("NETWORK", "sa", "IP-Address = ", WiFi.localIP());
_ui->log("NETWORK", "sis", "Signal strength = ", WiFi.RSSI(), " dBm");
_ui->log("NETWORK", "sis", "Signal strength = ", WiFi.RSSI(), " dBm");
return true;
} else {
}
else
{
_ui->log("NETWORK", "sis", "ERROR - WiFi connection failed (reason: ", WiFi.reasonCode(), ")");
return false;
}
}
char* DoughNetwork::getMacAddress() {
char *DoughWiFi::getMacAddress()
{
return _macAddress;
}

27
src/Network/DoughWiFi.h Normal file
View File

@ -0,0 +1,27 @@
#ifndef DOUGH_NETWORK_H
#define DOUGH_NETWORK_H
#include <WiFiNINA.h>
#include "UI/DoughUI.h"
#include "config.h"
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
DoughUI *_ui;
};
#endif

View File

@ -20,9 +20,6 @@ DoughSensors::DoughSensors() {
_ui = DoughUI::Instance();
_dht = new DHT(DHT11_DATA_PIN, DHT11);
_hcsr04 = new HCSR04(HCSR04_TRIG_PIN, HCSR04_ECHO_PIN);
temperature = 0;
humidity = 0;
distance = 0;
}
// ----------------------------------------------------------------------
@ -38,40 +35,38 @@ void DoughSensors::setup() {
// loop
// ----------------------------------------------------------------------
void DoughSensors::readTemperature() {
Measurement* DoughSensors::readTemperature() {
float t = _dht->readTemperature();
if (isnan(t)) {
_ui->log("SENSORS", "s", "ERROR - Temperature measurement failed");
temperatureOk = false;
return Measurement::Failed();
} else {
temperature = int(t);
temperatureOk = true;
_hcsr04->setTemperature(t);
_hcsr04->setTemperature(int(t));
auto m = new Measurement(true, int(t));
_ui->log("SENSORS", "sisi", "Temperature = ", int(t), "°C ", m->value);
return m;
}
_ui->log("SENSORS", "siss", "Temperature = ", temperature, "°C ", (temperatureOk ? "[OK]" : "[ERR]"));
}
void DoughSensors::readHumidity() {
Measurement* DoughSensors::readHumidity() {
int h = _dht->readHumidity();
if (h == 0) {
_ui->log("SENSORS", "s", "ERROR - Humidity measurement failed");
humidityOk = false;
return Measurement::Failed();
} else {
humidity = h;
humidityOk = true;
_hcsr04->setHumidity(h);
_ui->log("SENSORS", "sis", "Humidity = ", h, "%");
return Measurement::Ok(h);
}
_ui->log("SENSORS", "siss", "Humidity = ", humidity, "% ", (humidityOk ? "[OK]" : "[ERR]"));
}
void DoughSensors::readDistance() {
Measurement* DoughSensors::readDistance() {
int d = _hcsr04->readDistance();
if (d == -1) {
_ui->log("SENSORS", "s", "ERROR - Distance measurement failed");
distanceOk = false;
return Measurement::Failed();
} else {
distanceOk = true;
distance = d;
}
_ui->log("SENSORS", "siss", "Distance = ", distance, "mm ", (distanceOk? "[OK]" : "[ERR]"));
_ui->log("SENSORS", "sis", "Distance = ", d, "mm");
return Measurement::Ok(d);
}
}

View File

@ -2,24 +2,18 @@
#define DOUGH_SENSORS_H
#include <DHT.h>
#include "HCSR04.h"
#include "DoughUI.h"
#include "Sensors/HCSR04.h"
#include "UI/DoughUI.h"
#include "Data/Measurement.h"
#include "config.h"
class DoughSensors {
public:
static DoughSensors* Instance();
void setup();
void readAll();
void readTemperature();
int temperature = 0;
bool temperatureOk = false;
void readHumidity();
int humidity = 0;
bool humidityOk = false;
void readDistance();
int distance = 0;
bool distanceOk = false;
Measurement* readTemperature();
Measurement* readHumidity();
Measurement* readDistance();
private:
DoughSensors();

View File

@ -1,4 +1,4 @@
#include "HCSR04.h"
#include "Sensors/HCSR04.h"
HCSR04::HCSR04(int triggerPin, int echoPin) {
_triggerPin = triggerPin;
@ -33,7 +33,6 @@ int HCSR04::readDistance() {
_sortSamples();
return _computeAverage();
}
DoughUI::Instance()->log("HCSR04", "s", "ERROR - Not enough samples for reading distance, returning NAN");
return -1;
}

View File

@ -26,7 +26,6 @@
#define HCSR04_INIT_HUMIDITY 50.000
#include <Arduino.h>
#include "DoughUI.h"
#include "config.h"
class HCSR04 {

View File

@ -18,11 +18,13 @@
* // Linking the function ot button interrupts.
* myButton.onInterrupt(myButtonISR);
*/
DoughButton::DoughButton(int pin) {
DoughButton::DoughButton(int pin)
{
_pin = pin;
}
void DoughButton::setup() {
void DoughButton::setup()
{
pinMode(_pin, INPUT_PULLUP);
}
@ -31,7 +33,8 @@ void DoughButton::setup() {
* interrupts. The provided isr should relay interrupts to the
* handleButtonState() method of this class (see constructor docs).
*/
void DoughButton::onInterrupt(DoughButtonHandler isr) {
void DoughButton::onInterrupt(DoughButtonHandler isr)
{
attachInterrupt(digitalPinToInterrupt(_pin), isr, CHANGE);
}
@ -40,86 +43,111 @@ void DoughButton::onInterrupt(DoughButtonHandler isr) {
* When specific handlers for long and/or short presses are
* configured as well, those have precedence over this one.
*/
void DoughButton::onPress(DoughButtonHandler handler) {
void DoughButton::onPress(DoughButtonHandler handler)
{
_pressHandler = handler;
}
/**
* Assign an event handler for long button presses.
*/
void DoughButton::onLongPress(DoughButtonHandler handler) {
void DoughButton::onLongPress(DoughButtonHandler handler)
{
_longPressHandler = handler;
}
/**
* Assign an event handler for short button presses.
*/
void DoughButton::onShortPress(DoughButtonHandler handler) {
void DoughButton::onShortPress(DoughButtonHandler handler)
{
_shortPressHandler = handler;
}
void DoughButton::loop() {
void DoughButton::loop()
{
handleButtonState();
if (_state == UP_AFTER_SHORT) {
if (_shortPressHandler != nullptr) {
if (_state == UP_AFTER_SHORT)
{
if (_shortPressHandler != nullptr)
{
_shortPressHandler();
}
else if (_pressHandler != nullptr) {
else if (_pressHandler != nullptr)
{
_pressHandler();
}
_state = READY_FOR_NEXT_PRESS;
}
else if (_state == DOWN_LONG || _state == UP_AFTER_LONG) {
if (_longPressHandler != nullptr) {
else if (_state == DOWN_LONG || _state == UP_AFTER_LONG)
{
if (_longPressHandler != nullptr)
{
_longPressHandler();
}
else if (_pressHandler != nullptr) {
else if (_pressHandler != nullptr)
{
_pressHandler();
}
_state = READY_FOR_NEXT_PRESS;
}
else if (_state == DOWN && _shortPressHandler == nullptr && _longPressHandler == nullptr) {
if (_pressHandler != nullptr) {
else if (_state == DOWN && _shortPressHandler == nullptr && _longPressHandler == nullptr)
{
if (_pressHandler != nullptr)
{
_pressHandler();
}
}
_state = READY_FOR_NEXT_PRESS;
}
}
void DoughButton::clearEvents() {
void DoughButton::clearEvents()
{
_state = READY_FOR_NEXT_PRESS;
}
void DoughButton::handleButtonState() {
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) {
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) {
if (interval < BUTTON_DEBOUNCE_DELAY)
{
return;
}
// Handle button state changes.
if (_state == READY_FOR_NEXT_PRESS && buttonIsUp) {
if (_state == READY_FOR_NEXT_PRESS && buttonIsUp)
{
_state = UP;
} else if (_state == UP && buttonIsDown) {
}
else if (_state == UP && buttonIsDown)
{
_state = DOWN;
} else if (_state == DOWN && buttonIsDown && interval > BUTTON_LONGPRESS_DELAY) {
}
else if (_state == DOWN && buttonIsDown && interval > BUTTON_LONGPRESS_DELAY)
{
_state = DOWN_LONG;
} else if (_state == DOWN && buttonIsUp) {
}
else if (_state == DOWN && buttonIsUp)
{
_state = UP_AFTER_SHORT;
} else if (_state == DOWN_LONG && buttonIsUp) {
}
else if (_state == DOWN_LONG && buttonIsUp)
{
_state = UP_AFTER_LONG;
}
}

44
src/UI/DoughButton.h Normal file
View File

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

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

@ -0,0 +1,54 @@
#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;
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

231
src/UI/DoughUI.cpp Normal file
View File

@ -0,0 +1,231 @@
#include "DoughUI.h"
DoughUI *DoughUI::_instance = nullptr;
/**
* Fetch the DoughUI singleton.
*/
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) {}
/**
* Called from the main setup() function of the sketch.
*/
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();
}
/**
* Log a message to the serial interface.
*/
void DoughUI::log(const char *category, const char *fmt, ...)
{
char buf[12];
snprintf(buf, sizeof(buf) / sizeof(buf[0]), "%8s | ", category);
Serial.print(buf);
va_list args;
va_start(args, fmt);
while (*fmt != '\0')
{
if (*fmt == 'i')
{
int i = va_arg(args, int);
Serial.print(i);
}
else if (*fmt == 'f')
{
float f = va_arg(args, double);
Serial.print(f);
}
else if (*fmt == 'a')
{
IPAddress a = va_arg(args, IPAddress);
Serial.print(a);
}
else if (*fmt == 's')
{
const char *s = va_arg(args, const char *);
Serial.print(s);
}
else if (*fmt == 'S')
{
String S = va_arg(args, String);
Serial.print(S);
}
else
{
Serial.print("<log(): invalid format char '");
Serial.print(*fmt);
Serial.print("'>");
}
fmt++;
}
va_end(args);
Serial.println("");
}
/**
* Setup a timer interrupt for updating the GUI. Unfortunately, the standard
* 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
}
void DoughUI::resume()
{
NVIC_EnableIRQ(TC4_IRQn); // Enable TC4 interrupts
}
void DoughUI::suspend()
{
NVIC_DisableIRQ(TC4_IRQn); // Disable TC4 interrupts
}
/**
* This callback is called when the TC4 timer hits an overflow interrupt.
*/
void TC4_Handler()
{
DoughUI::Instance()->updatedLEDs();
REG_TC4_INTFLAG = TC_INTFLAG_OVF; // Clear the OVF interrupt flag.
}
/**
* 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 invocatino
* 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();
}

47
src/UI/DoughUI.h Normal file
View File

@ -0,0 +1,47 @@
#ifndef DOUGH_UI_H
#define DOUGH_UI_H
#define LOG_BAUDRATE 9600
// Define this one to wait for USB serial to come up.
// This can be useful during development, when you want all
// serial messages to appear in the serial monitor.
// Without this, some of the initial serial messages might
// be missing from the output.
#undef LOG_WAIT_SERIAL
#include <Arduino.h>
#include <WiFiNINA.h>
#include <stdarg.h>
#include "UI/DoughButton.h"
#include "UI/DoughLED.h"
#include "config.h"
class DoughUI
{
public:
static DoughUI *Instance();
void setup();
static void onoffButtonISR();
static void setupButtonISR();
DoughButton onoffButton;
DoughButton setupButton;
DoughLED ledBuiltin;
DoughLED led1;
DoughLED led2;
DoughLED led3;
void processButtonEvents();
void clearButtonEvents();
void updatedLEDs();
void flash_all_leds();
void resume();
void suspend();
void log(const char *category, const char *fmt, ...);
private:
DoughUI();
void _setupTimerInterrupt();
static DoughUI *_instance;
};
#endif

View File

@ -1,20 +1,20 @@
// The digital pin to which the DATA pin of the DHT11
// temperature/humidity sensor is connected.
#define DHT11_DATA_PIN 10
#define DHT11_DATA_PIN 10
// The digital pins to which the TRIG and ECHO pins of
// the HCSR04 distance sensor are connected.
#define HCSR04_TRIG_PIN 4
#define HCSR04_ECHO_PIN 5
#define HCSR04_TRIG_PIN 4
#define HCSR04_ECHO_PIN 5
// The digital pins to which the three LEDs are connected.
#define LED1_PIN 8
#define LED2_PIN 7
#define LED3_PIN 6
#define LED1_PIN 8
#define LED2_PIN 7
#define LED3_PIN 6
// The digital pins to which the push buttons are connected.
#define ONOFF_BUTTON_PIN 2
#define SETUP_BUTTON_PIN 3
// The digital pins to which the push buttons are connected.
#define ONOFF_BUTTON_PIN 2
#define SETUP_BUTTON_PIN 3
// The network configuration and possibly overrides for the above
// definitions are stored in a separate header file, which is

View File

@ -1,12 +1,12 @@
// WPA2 WiFi connection configuration.
#define WIFI_SSID "<SSID network name>"
#define WIFI_PASSWORD "<network password>"
#define WIFI_SSID "<SSID network name>"
#define WIFI_PASSWORD "<network password>"
// MQTT broker configuration.
#define MQTT_BROKER "<IP or hostname>"
#define MQTT_PORT 1883
#define MQTT_USERNAME "<mqtt username>"
#define MQTT_PASSWORD "<mqtt password>"
#define MQTT_BROKER "<IP or hostname>"
#define MQTT_PORT 1883
#define MQTT_USERNAME "<mqtt username>"
#define MQTT_PASSWORD "<mqtt password>"
// The prefix to use for the MQTT publishing topic.
#define MQTT_TOPIC_PREFIX "sensors/doughboy"

View File

@ -1,4 +1,4 @@
#include "DoughBoy.h"
#include "main.h"
// TOOD: implement the calibration logic
// TODO: use different timings for temperature, humidity and distance measurements. Temp/Humidity together takes about 500ms, which slows down stuff.
@ -8,11 +8,12 @@
DoughBoyState state = CONFIGURING;
void setup() {
void setup()
{
DoughSensors::Instance()->setup();
DoughNetwork::Instance()->setup();
DoughWiFi::Instance()->setup();
DoughMQTT::Instance()->setup();
DoughData::Instance()->setup();
DataController::Instance()->setup();
auto ui = DoughUI::Instance();
ui->setup();
ui->onoffButton.onPress(handleOnoffButtonPress);
@ -20,34 +21,41 @@ void setup() {
ui->log("MAIN", "s", "Initialization completed, starting device");
}
void loop() {
void loop()
{
auto ui = DoughUI::Instance();
auto data = DoughData::Instance();
auto data = DataController::Instance();
auto mqtt = DoughMQTT::Instance();
ui->processButtonEvents();
if (!setupNetworkConnection()) {
if (!setupNetworkConnection())
{
return;
}
mqtt->procesIncomingsMessages();
if (state == CONFIGURING && data->isConfigured()) {
if (state == CONFIGURING && data->isConfigured())
{
setStateToMeasuring();
}
else if (state == MEASURING && !data->isConfigured()) {
else if (state == MEASURING && !data->isConfigured())
{
setStateToConfiguring();
}
else if (state == MEASURING) {
DoughData::Instance()->loop();
else if (state == MEASURING)
{
DataController::Instance()->loop();
}
else if (state == CALIBRATING) {
else if (state == CALIBRATING)
{
delay(3000);
setStateToPaused();
}
else if (state == PAUSED) {
DoughData::Instance()->clearHistory();
else if (state == PAUSED)
{
DataController::Instance()->clearHistory();
}
}
@ -56,18 +64,23 @@ void loop() {
* If not, then try to setup the connection.
* Returns true if the connection was established, false otherwise.
*/
bool setupNetworkConnection() {
bool setupNetworkConnection()
{
static auto connectionState = CONNECTING_WIFI;
auto ui = DoughUI::Instance();
auto network = DoughNetwork::Instance();
auto network = DoughWiFi::Instance();
auto mqtt = DoughMQTT::Instance();
if (!network->isConnected()) {
if (connectionState == CONNECTED) {
if (!network->isConnected())
{
if (connectionState == CONNECTED)
{
ui->log("MAIN", "s", "ERROR - Connection to WiFi network lost! Reconnecting ...");
} else {
ui->log("MAIN", "s", "Connecting to the WiFi network ...");
}
else
{
ui->log("MAIN", "s", "Connecting to the WiFi network ...");
}
connectionState = CONNECTING_WIFI;
ui->led1.blink()->slow();
@ -75,20 +88,26 @@ bool setupNetworkConnection() {
ui->led3.off();
network->connect();
}
if (network->isConnected() && !mqtt->isConnected()) {
if (connectionState == CONNECTED) {
if (network->isConnected() && !mqtt->isConnected())
{
if (connectionState == CONNECTED)
{
ui->log("MAIN", "s", "ERROR - Connection to the MQTT broker lost! Reconnecting ...");
} else {
ui->log("MAIN", "s", "Connecting to the MQTT broker ...");
}
else
{
ui->log("MAIN", "s", "Connecting to the MQTT broker ...");
}
connectionState = CONNECTING_MQTT;
ui->led1.blink()->fast();
ui->led2.off();
ui->led3.off();
mqtt->connect();
}
if (network->isConnected() && mqtt->isConnected()) {
if (connectionState != CONNECTED) {
}
if (network->isConnected() && mqtt->isConnected())
{
if (connectionState != CONNECTED)
{
ui->log("MAIN", "s", "Connection to MQTT broker established");
ui->led1.on();
ui->led2.off();
@ -102,20 +121,25 @@ bool setupNetworkConnection() {
return connectionState == CONNECTED;
}
void handleOnoffButtonPress() {
if (state == MEASURING) {
void handleOnoffButtonPress()
{
if (state == MEASURING)
{
setStateToPaused();
}
else if (state == PAUSED) {
else if (state == PAUSED)
{
setStateToMeasuring();
}
}
void handleSetupButtonPress() {
void handleSetupButtonPress()
{
setStateToCalibrating();
}
void setStateToConfiguring() {
void setStateToConfiguring()
{
auto ui = DoughUI::Instance();
ui->log("MAIN", "s", "Waiting for configuration ...");
state = CONFIGURING;
@ -125,7 +149,8 @@ void setStateToConfiguring() {
DoughMQTT::Instance()->publish("state", "configuring");
}
void setStateToMeasuring() {
void setStateToMeasuring()
{
auto ui = DoughUI::Instance();
ui->log("MAIN", "s", "Starting measurements");
state = MEASURING;
@ -135,22 +160,24 @@ void setStateToMeasuring() {
DoughMQTT::Instance()->publish("state", "measuring");
}
void setStateToPaused() {
auto ui = DoughUI::Instance();
void setStateToPaused()
{
auto ui = DoughUI::Instance();
ui->log("MAIN", "s", "Pausing measurements");
state = PAUSED;
ui->led1.on();
ui->led2.on();
ui->led3.pulse();
DoughMQTT::Instance()->publish("state", "paused");
DoughMQTT::Instance()->publish("state", "paused");
}
void setStateToCalibrating() {
void setStateToCalibrating()
{
auto ui = DoughUI::Instance();
ui->log("MAIN", "s", "Requested device calibration");
state = CALIBRATING;
ui->led1.on();
ui->led2.blink()->slow();
ui->led3.off();
DoughMQTT::Instance()->publish("state", "calibrating");
DoughMQTT::Instance()->publish("state", "calibrating");
}

View File

@ -2,21 +2,23 @@
#define DOUGHBOY_H
#include <Arduino.h>
#include "DoughNetwork.h"
#include "DoughMQTT.h"
#include "DoughSensors.h"
#include "DoughData.h"
#include "DoughButton.h"
#include "DoughUI.h"
#include "Network/DoughWiFi.h"
#include "Network/DoughMQTT.h"
#include "Sensors/DoughSensors.h"
#include "Data/DataController.h"
#include "UI/DoughButton.h"
#include "UI/DoughUI.h"
#include "config.h"
typedef enum {
typedef enum
{
CONNECTING_WIFI,
CONNECTING_MQTT,
CONNECTED
} DoughBoyConnectionState;
typedef enum {
typedef enum
{
CONFIGURING,
MEASURING,
PAUSED,