Initial import.
This commit is contained in:
commit
ea8723493c
|
@ -0,0 +1,13 @@
|
||||||
|
.pio
|
||||||
|
.vscode/.browse.c_cpp.db*
|
||||||
|
.vscode/c_cpp_properties.json
|
||||||
|
.vscode/launch.json
|
||||||
|
.vscode/ipch
|
||||||
|
|
||||||
|
# I don't use the lib directory, so no need to have it in the repo.
|
||||||
|
# It will be autocreated by PlatformIO, therefore I ignore it here.
|
||||||
|
lib
|
||||||
|
|
||||||
|
# This file contains local configuration information required to
|
||||||
|
# connect to the WiFi network and MQTT broker
|
||||||
|
src/config_local.h
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
// See http://go.microsoft.com/fwlink/?LinkId=827846
|
||||||
|
// for the documentation about the extensions.json format
|
||||||
|
"recommendations": [
|
||||||
|
"platformio.platformio-ide"
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) <year> <copyright holders>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is furnished
|
||||||
|
to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice (including the next
|
||||||
|
paragraph) shall be included in all copies or substantial portions of the
|
||||||
|
Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
|
||||||
|
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
|
||||||
|
OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@ -0,0 +1,3 @@
|
||||||
|
# arduino-doughboy
|
||||||
|
|
||||||
|
Firmware for my Doughboy project, used to monitor my sourdough starter and dough proofing.
|
Binary file not shown.
|
@ -0,0 +1,9 @@
|
||||||
|
[env:nano_33_iot]
|
||||||
|
platform = atmelsam
|
||||||
|
board = nano_33_iot
|
||||||
|
framework = arduino
|
||||||
|
|
||||||
|
lib_deps =
|
||||||
|
WiFiNINA
|
||||||
|
DHT sensor library
|
||||||
|
MQTT
|
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Win32",
|
||||||
|
"includePath": [
|
||||||
|
"${workspaceFolder}/**"
|
||||||
|
],
|
||||||
|
"defines": [
|
||||||
|
"_DEBUG",
|
||||||
|
"UNICODE",
|
||||||
|
"_UNICODE"
|
||||||
|
],
|
||||||
|
"intelliSenseMode": "msvc-x64"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version": 4
|
||||||
|
}
|
|
@ -0,0 +1,156 @@
|
||||||
|
#include "DoughBoy.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.
|
||||||
|
// TODO: make the measuring more loop-y, giving back control to the main loop more often for better UI responsiveness
|
||||||
|
// TODO: see what more stuff can be moved to the UI code. Maybe state to UI state translation ought to be there as well
|
||||||
|
// TODO: use longer term averages for data
|
||||||
|
|
||||||
|
DoughBoyState state = CONFIGURING;
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
DoughSensors::Instance()->setup();
|
||||||
|
DoughNetwork::Instance()->setup();
|
||||||
|
DoughMQTT::Instance()->setup();
|
||||||
|
DoughData::Instance()->setup();
|
||||||
|
auto ui = DoughUI::Instance();
|
||||||
|
ui->setup();
|
||||||
|
ui->onoffButton.onPress(handleOnoffButtonPress);
|
||||||
|
ui->setupButton.onPress(handleSetupButtonPress);
|
||||||
|
ui->log("MAIN", "s", "Initialization completed, starting device");
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
auto ui = DoughUI::Instance();
|
||||||
|
auto data = DoughData::Instance();
|
||||||
|
auto mqtt = DoughMQTT::Instance();
|
||||||
|
|
||||||
|
ui->processButtonEvents();
|
||||||
|
|
||||||
|
if (!setupNetworkConnection()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mqtt->procesIncomingsMessages();
|
||||||
|
|
||||||
|
if (state == CONFIGURING && data->isConfigured()) {
|
||||||
|
setStateToMeasuring();
|
||||||
|
}
|
||||||
|
else if (state == MEASURING && !data->isConfigured()) {
|
||||||
|
setStateToConfiguring();
|
||||||
|
}
|
||||||
|
else if (state == MEASURING) {
|
||||||
|
DoughData::Instance()->loop();
|
||||||
|
}
|
||||||
|
else if (state == CALIBRATING) {
|
||||||
|
delay(3000);
|
||||||
|
setStateToPaused();
|
||||||
|
}
|
||||||
|
else if (state == PAUSED) {
|
||||||
|
DoughData::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.
|
||||||
|
*/
|
||||||
|
bool setupNetworkConnection() {
|
||||||
|
static auto connectionState = CONNECTING_WIFI;
|
||||||
|
|
||||||
|
auto ui = DoughUI::Instance();
|
||||||
|
auto network = DoughNetwork::Instance();
|
||||||
|
auto mqtt = DoughMQTT::Instance();
|
||||||
|
|
||||||
|
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 ...");
|
||||||
|
}
|
||||||
|
connectionState = CONNECTING_WIFI;
|
||||||
|
ui->led1.blink()->slow();
|
||||||
|
ui->led2.off();
|
||||||
|
ui->led3.off();
|
||||||
|
network->connect();
|
||||||
|
}
|
||||||
|
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 ...");
|
||||||
|
}
|
||||||
|
connectionState = CONNECTING_MQTT;
|
||||||
|
ui->led1.blink()->fast();
|
||||||
|
ui->led2.off();
|
||||||
|
ui->led3.off();
|
||||||
|
mqtt->connect();
|
||||||
|
}
|
||||||
|
if (network->isConnected() && mqtt->isConnected()) {
|
||||||
|
if (connectionState != CONNECTED) {
|
||||||
|
ui->log("MAIN", "s", "Connection to MQTT broker established");
|
||||||
|
ui->led1.on();
|
||||||
|
ui->led2.off();
|
||||||
|
ui->led3.off();
|
||||||
|
ui->clearButtonEvents();
|
||||||
|
connectionState = CONNECTED;
|
||||||
|
setStateToConfiguring();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return connectionState == CONNECTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleOnoffButtonPress() {
|
||||||
|
if (state == MEASURING) {
|
||||||
|
setStateToPaused();
|
||||||
|
}
|
||||||
|
else if (state == PAUSED) {
|
||||||
|
setStateToMeasuring();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleSetupButtonPress() {
|
||||||
|
setStateToCalibrating();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setStateToConfiguring() {
|
||||||
|
auto ui = DoughUI::Instance();
|
||||||
|
ui->log("MAIN", "s", "Waiting for configuration ...");
|
||||||
|
state = CONFIGURING;
|
||||||
|
ui->led1.on();
|
||||||
|
ui->led2.blink()->fast();
|
||||||
|
ui->led3.off();
|
||||||
|
DoughMQTT::Instance()->publish("state", "configuring");
|
||||||
|
}
|
||||||
|
|
||||||
|
void setStateToMeasuring() {
|
||||||
|
auto ui = DoughUI::Instance();
|
||||||
|
ui->log("MAIN", "s", "Starting measurements");
|
||||||
|
state = MEASURING;
|
||||||
|
ui->led1.on();
|
||||||
|
ui->led2.on();
|
||||||
|
ui->led3.on();
|
||||||
|
DoughMQTT::Instance()->publish("state", "measuring");
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
#ifndef DOUGHBOY_H
|
||||||
|
#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 "config.h"
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
CONNECTING_WIFI,
|
||||||
|
CONNECTING_MQTT,
|
||||||
|
CONNECTED
|
||||||
|
} DoughBoyConnectionState;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
CONFIGURING,
|
||||||
|
MEASURING,
|
||||||
|
PAUSED,
|
||||||
|
CALIBRATING
|
||||||
|
} DoughBoyState;
|
||||||
|
|
||||||
|
bool setupNetworkConnection();
|
||||||
|
void handleMqttMessage(String &topic, String &payload);
|
||||||
|
void handleOnoffButtonPress();
|
||||||
|
void handleSetupButtonPress();
|
||||||
|
void setStateToConfiguring();
|
||||||
|
void setStateToMeasuring();
|
||||||
|
void setStateToPaused();
|
||||||
|
void setStateToCalibrating();
|
||||||
|
|
||||||
|
#endif
|
|
@ -0,0 +1,125 @@
|
||||||
|
#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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
#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
|
|
@ -0,0 +1,274 @@
|
||||||
|
#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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,98 @@
|
||||||
|
#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
|
|
@ -0,0 +1,142 @@
|
||||||
|
#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;
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
#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
|
|
@ -0,0 +1,110 @@
|
||||||
|
#include "DoughMQTT.h"
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
// Constructor
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
DoughMQTT* DoughMQTT::_instance = nullptr;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the DoughMQTT singleton.
|
||||||
|
*/
|
||||||
|
DoughMQTT* DoughMQTT::Instance() {
|
||||||
|
if (DoughMQTT::_instance == nullptr) {
|
||||||
|
DoughMQTT::_instance = new DoughMQTT();
|
||||||
|
}
|
||||||
|
return DoughMQTT::_instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
DoughMQTT::DoughMQTT() {
|
||||||
|
_ui = DoughUI::Instance();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
// Setup
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
void DoughMQTT::setup() {
|
||||||
|
DoughNetwork* network = DoughNetwork::Instance();
|
||||||
|
|
||||||
|
#ifdef MQTT_DEVICE_ID
|
||||||
|
_mqttDeviceId = MQTT_DEVICE_ID;
|
||||||
|
#else
|
||||||
|
_mqttDeviceId = network->getMacAddress();
|
||||||
|
#endif
|
||||||
|
_ui->log("MQTT", "ss", "Device ID = ", _mqttDeviceId);
|
||||||
|
|
||||||
|
_mqttClient.begin(MQTT_BROKER, MQTT_PORT, network->client);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DoughMQTT::onConnect(DoughMQTTConnectHandler callback) {
|
||||||
|
_onConnect = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DoughMQTT::onMessage(MQTTClientCallbackSimple callback) {
|
||||||
|
_onMessage = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
// Loop
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
bool DoughMQTT::isConnected() {
|
||||||
|
return _mqttClient.connected();
|
||||||
|
}
|
||||||
|
|
||||||
|
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()) {
|
||||||
|
_ui->log("MQTT", "s", "ERROR - Connection to broker failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_mqttClient.onMessage(DoughMQTT::handleMessage);
|
||||||
|
|
||||||
|
if (_onConnect != nullptr) {
|
||||||
|
_onConnect(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DoughMQTT::procesIncomingsMessages() {
|
||||||
|
_mqttClient.loop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DoughMQTT::handleMessage(String &topic, String &payload) {
|
||||||
|
DoughUI::Instance()->log("MQTT", "sSsS", "<<< ", topic, " = ", payload);
|
||||||
|
|
||||||
|
DoughMQTT *mqtt = DoughMQTT::Instance();
|
||||||
|
if (mqtt->_onMessage != nullptr) {
|
||||||
|
int pos = topic.lastIndexOf('/');
|
||||||
|
if (pos != -1) {
|
||||||
|
topic.remove(0, pos+1);
|
||||||
|
mqtt->_onMessage(topic, payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DoughMQTT::subscribe(const char* key) {
|
||||||
|
char topic[200];
|
||||||
|
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) {
|
||||||
|
char topic[200];
|
||||||
|
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) {
|
||||||
|
char buf[16];
|
||||||
|
snprintf(buf, 16, "%d", payload);
|
||||||
|
publish(key, buf);
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
#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
|
|
@ -0,0 +1,80 @@
|
||||||
|
#include "DoughNetwork.h"
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
// Constructor
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
DoughNetwork* DoughNetwork::_instance = nullptr;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the DoughNetwork singleton.
|
||||||
|
*/
|
||||||
|
DoughNetwork* DoughNetwork::Instance() {
|
||||||
|
if (DoughNetwork::_instance == nullptr) {
|
||||||
|
DoughNetwork::_instance = new DoughNetwork();
|
||||||
|
}
|
||||||
|
return DoughNetwork::_instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
DoughNetwork::DoughNetwork() {
|
||||||
|
_ui = DoughUI::Instance();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
// Setup
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
void DoughNetwork::_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 DoughNetwork::setup() {
|
||||||
|
_setMacAddress();
|
||||||
|
DoughUI::Instance()->log("NETWORK", "ss", "MAC address = ", getMacAddress());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
// Loop
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
bool DoughNetwork::isConnected() {
|
||||||
|
return WiFi.status() == WL_CONNECTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DoughNetwork::connect() {
|
||||||
|
int status = WiFi.status();
|
||||||
|
|
||||||
|
// Check if a device with a WiFi shield is used.
|
||||||
|
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) {
|
||||||
|
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) {
|
||||||
|
_ui->log("NETWORK", "sa", "IP-Address = ", WiFi.localIP());
|
||||||
|
_ui->log("NETWORK", "sis", "Signal strength = ", WiFi.RSSI(), " dBm");
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
_ui->log("NETWORK", "sis", "ERROR - WiFi connection failed (reason: ", WiFi.reasonCode(), ")");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
char* DoughNetwork::getMacAddress() {
|
||||||
|
return _macAddress;
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
#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
|
|
@ -0,0 +1,77 @@
|
||||||
|
#include "DoughSensors.h"
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
// Constructor
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
DoughSensors* DoughSensors::_instance = nullptr;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the DoughSensors singleton.
|
||||||
|
*/
|
||||||
|
DoughSensors* DoughSensors::Instance() {
|
||||||
|
if (DoughSensors::_instance == nullptr) {
|
||||||
|
DoughSensors::_instance = new DoughSensors();
|
||||||
|
}
|
||||||
|
return DoughSensors::_instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
// setup
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
void DoughSensors::setup() {
|
||||||
|
_dht->begin();
|
||||||
|
_hcsr04->begin();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
// loop
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
void DoughSensors::readTemperature() {
|
||||||
|
float t = _dht->readTemperature();
|
||||||
|
if (isnan(t)) {
|
||||||
|
_ui->log("SENSORS", "s", "ERROR - Temperature measurement failed");
|
||||||
|
temperatureOk = false;
|
||||||
|
} else {
|
||||||
|
temperature = int(t);
|
||||||
|
temperatureOk = true;
|
||||||
|
_hcsr04->setTemperature(t);
|
||||||
|
}
|
||||||
|
_ui->log("SENSORS", "siss", "Temperature = ", temperature, "°C ", (temperatureOk ? "[OK]" : "[ERR]"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void DoughSensors::readHumidity() {
|
||||||
|
int h = _dht->readHumidity();
|
||||||
|
if (h == 0) {
|
||||||
|
_ui->log("SENSORS", "s", "ERROR - Humidity measurement failed");
|
||||||
|
humidityOk = false;
|
||||||
|
} else {
|
||||||
|
humidity = h;
|
||||||
|
humidityOk = true;
|
||||||
|
_hcsr04->setHumidity(h);
|
||||||
|
}
|
||||||
|
_ui->log("SENSORS", "siss", "Humidity = ", humidity, "% ", (humidityOk ? "[OK]" : "[ERR]"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void DoughSensors::readDistance() {
|
||||||
|
int d = _hcsr04->readDistance();
|
||||||
|
if (d == -1) {
|
||||||
|
_ui->log("SENSORS", "s", "ERROR - Distance measurement failed");
|
||||||
|
distanceOk = false;
|
||||||
|
} else {
|
||||||
|
distanceOk = true;
|
||||||
|
distance = d;
|
||||||
|
}
|
||||||
|
_ui->log("SENSORS", "siss", "Distance = ", distance, "mm ", (distanceOk? "[OK]" : "[ERR]"));
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
#ifndef DOUGH_SENSORS_H
|
||||||
|
#define DOUGH_SENSORS_H
|
||||||
|
|
||||||
|
#include <DHT.h>
|
||||||
|
#include "HCSR04.h"
|
||||||
|
#include "DoughUI.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;
|
||||||
|
|
||||||
|
private:
|
||||||
|
DoughSensors();
|
||||||
|
static DoughSensors* _instance;
|
||||||
|
DoughUI *_ui;
|
||||||
|
DHT* _dht;
|
||||||
|
HCSR04* _hcsr04;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
|
@ -0,0 +1,198 @@
|
||||||
|
#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();
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
#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
|
|
@ -0,0 +1,124 @@
|
||||||
|
#include "HCSR04.h"
|
||||||
|
|
||||||
|
HCSR04::HCSR04(int triggerPin, int echoPin) {
|
||||||
|
_triggerPin = triggerPin;
|
||||||
|
_echoPin = echoPin;
|
||||||
|
_temperature = HCSR04_INIT_TEMPERATURE;
|
||||||
|
_humidity = HCSR04_INIT_HUMIDITY;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HCSR04::begin() {
|
||||||
|
pinMode(_triggerPin, OUTPUT);
|
||||||
|
pinMode(_echoPin, INPUT);
|
||||||
|
}
|
||||||
|
|
||||||
|
void HCSR04::setTemperature(int temperature) {
|
||||||
|
_temperature = temperature;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HCSR04::setHumidity(int humidity) {
|
||||||
|
_humidity = humidity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a distance reading.
|
||||||
|
* When reading the distance fails, -1 is returned.
|
||||||
|
* Otherwise the distance in mm.
|
||||||
|
*/
|
||||||
|
int HCSR04::readDistance() {
|
||||||
|
_setSpeedOfSound();
|
||||||
|
_setEchoTimeout();
|
||||||
|
_takeSamples();
|
||||||
|
if (_haveEnoughSamples()) {
|
||||||
|
_sortSamples();
|
||||||
|
return _computeAverage();
|
||||||
|
}
|
||||||
|
DoughUI::Instance()->log("HCSR04", "s", "ERROR - Not enough samples for reading distance, returning NAN");
|
||||||
|
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 HCSR04::_setSpeedOfSound() {
|
||||||
|
_speedOfSound =
|
||||||
|
0.3314 +
|
||||||
|
(0.000606 * _temperature) +
|
||||||
|
(0.0000124 * _humidity);
|
||||||
|
}
|
||||||
|
|
||||||
|
void HCSR04::_setEchoTimeout() {
|
||||||
|
_echoTimeout = HCSR04_MAX_MM * 2 / _speedOfSound;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HCSR04::_takeSamples() {
|
||||||
|
_successfulSamples = 0;
|
||||||
|
for (int i = 0; i<HCSR04_SAMPLES_TAKE; i++) {
|
||||||
|
// 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++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HCSR04::_haveEnoughSamples() {
|
||||||
|
return _successfulSamples >= HCSR04_SAMPLES_USE;
|
||||||
|
}
|
||||||
|
|
||||||
|
int HCSR04::_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;
|
||||||
|
if (distance < HCSR04_MIN_MM || distance >= HCSR04_MAX_MM) {
|
||||||
|
return -1;
|
||||||
|
} else {
|
||||||
|
return distance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HCSR04::_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 HCSR04::_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);
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
#ifndef HCSR04_H
|
||||||
|
#define HCSR04_H
|
||||||
|
|
||||||
|
// The minimum and maximum distance that can be measured in mm.
|
||||||
|
// This is based on the specifications of the HCSR04 sensor.
|
||||||
|
#define HCSR04_MIN_MM 40
|
||||||
|
#define HCSR04_MAX_MM 4000
|
||||||
|
|
||||||
|
// Some parameters that are used to get more stable reading from the sensor.
|
||||||
|
// To get a better reading:
|
||||||
|
// - multiple samples are taken
|
||||||
|
// - between each sample, a random wait is added (because I saw repeating
|
||||||
|
// patterns when reading from a tight loop)
|
||||||
|
// - only a subset from the samples is used to compute the average distance
|
||||||
|
// (the high and low extremes are ignored)
|
||||||
|
#define HCSR04_SAMPLES_TAKE 20
|
||||||
|
#define HCSR04_SAMPLES_USE 8
|
||||||
|
#define HCSR04_SAMPLE_WAIT 30
|
||||||
|
#define HCSR04_SAMPLE_WAIT_SPREAD 12
|
||||||
|
|
||||||
|
// Default values for temperature and humidity, which have an effect
|
||||||
|
// on the speed of sound. At runtime, the temperature and humidity
|
||||||
|
// can be modified by using their respective setter functions
|
||||||
|
// setTemperature() and setHumidity().
|
||||||
|
#define HCSR04_INIT_TEMPERATURE 19.000
|
||||||
|
#define HCSR04_INIT_HUMIDITY 50.000
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include "DoughUI.h"
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
class HCSR04 {
|
||||||
|
public:
|
||||||
|
HCSR04(int triggerPin, int echoPin);
|
||||||
|
void begin();
|
||||||
|
void setTemperature(int temperature);
|
||||||
|
void setHumidity(int humidity);
|
||||||
|
int readDistance();
|
||||||
|
|
||||||
|
private:
|
||||||
|
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();
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
|
@ -0,0 +1,24 @@
|
||||||
|
// The digital pin to which the DATA pin of the DHT11
|
||||||
|
// temperature/humidity sensor is connected.
|
||||||
|
#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
|
||||||
|
|
||||||
|
// The digital pins to which the three LEDs are connected.
|
||||||
|
#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 network configuration and possibly overrides for the above
|
||||||
|
// definitions are stored in a separate header file, which is
|
||||||
|
// not stored in the repository. Before compiling this code,
|
||||||
|
// rename or copy the file config_local.example.h to config_local.h
|
||||||
|
// and update the settings in that file.
|
||||||
|
#include "config_local.h"
|
|
@ -0,0 +1,23 @@
|
||||||
|
// WPA2 WiFi connection configuration.
|
||||||
|
#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>"
|
||||||
|
|
||||||
|
// The prefix to use for the MQTT publishing topic.
|
||||||
|
#define MQTT_TOPIC_PREFIX "sensors/doughboy"
|
||||||
|
|
||||||
|
// Define this one to not use the WiFi MAC address as the device ID
|
||||||
|
// in the publish topics (sensors/doughboy/<MQTT_DEVICE_ID>/...)
|
||||||
|
//#define MQTT_DEVICE_ID "1"
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
//#define LOG_WAIT_SERIAL
|
Loading…
Reference in New Issue