From ea8723493c370794eb9195b431e7d998f972991c Mon Sep 17 00:00:00 2001 From: Maurice Makaay Date: Wed, 8 Jul 2020 01:16:00 +0200 Subject: [PATCH] Initial import. --- .gitignore | 13 ++ .vscode/extensions.json | 7 + LICENSE | 21 +++ README.md | 3 + Schematics.fzz | Bin 0 -> 30476 bytes platformio.ini | 9 + src/.vscode/c_cpp_properties.json | 17 ++ src/DoughBoy.cpp | 156 +++++++++++++++++ src/DoughBoy.h | 35 ++++ src/DoughButton.cpp | 125 ++++++++++++++ src/DoughButton.h | 42 +++++ src/DoughData.cpp | 274 ++++++++++++++++++++++++++++++ src/DoughData.h | 98 +++++++++++ src/DoughLED.cpp | 142 ++++++++++++++++ src/DoughLED.h | 53 ++++++ src/DoughMQTT.cpp | 110 ++++++++++++ src/DoughMQTT.h | 39 +++++ src/DoughNetwork.cpp | 80 +++++++++ src/DoughNetwork.h | 26 +++ src/DoughSensors.cpp | 77 +++++++++ src/DoughSensors.h | 32 ++++ src/DoughUI.cpp | 198 +++++++++++++++++++++ src/DoughUI.h | 46 +++++ src/HCSR04.cpp | 124 ++++++++++++++ src/HCSR04.h | 58 +++++++ src/config.h | 24 +++ src/config_local.example.h | 23 +++ 27 files changed, 1832 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/extensions.json create mode 100644 LICENSE create mode 100644 README.md create mode 100644 Schematics.fzz create mode 100644 platformio.ini create mode 100644 src/.vscode/c_cpp_properties.json create mode 100644 src/DoughBoy.cpp create mode 100644 src/DoughBoy.h create mode 100644 src/DoughButton.cpp create mode 100644 src/DoughButton.h create mode 100644 src/DoughData.cpp create mode 100644 src/DoughData.h create mode 100644 src/DoughLED.cpp create mode 100644 src/DoughLED.h create mode 100644 src/DoughMQTT.cpp create mode 100644 src/DoughMQTT.h create mode 100644 src/DoughNetwork.cpp create mode 100644 src/DoughNetwork.h create mode 100644 src/DoughSensors.cpp create mode 100644 src/DoughSensors.h create mode 100644 src/DoughUI.cpp create mode 100644 src/DoughUI.h create mode 100644 src/HCSR04.cpp create mode 100644 src/HCSR04.h create mode 100644 src/config.h create mode 100644 src/config_local.example.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..39add63 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..e80666b --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": [ + "platformio.platformio-ide" + ] +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d449d3e --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) + +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b30a704 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# arduino-doughboy + +Firmware for my Doughboy project, used to monitor my sourdough starter and dough proofing. diff --git a/Schematics.fzz b/Schematics.fzz new file mode 100644 index 0000000000000000000000000000000000000000..164e260da27c90c7a805e40eb06dd2eca4de447f GIT binary patch literal 30476 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 literal 0 HcmV?d00001 diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..5dd0522 --- /dev/null +++ b/platformio.ini @@ -0,0 +1,9 @@ +[env:nano_33_iot] +platform = atmelsam +board = nano_33_iot +framework = arduino + +lib_deps = + WiFiNINA + DHT sensor library + MQTT diff --git a/src/.vscode/c_cpp_properties.json b/src/.vscode/c_cpp_properties.json new file mode 100644 index 0000000..1df4412 --- /dev/null +++ b/src/.vscode/c_cpp_properties.json @@ -0,0 +1,17 @@ +{ + "configurations": [ + { + "name": "Win32", + "includePath": [ + "${workspaceFolder}/**" + ], + "defines": [ + "_DEBUG", + "UNICODE", + "_UNICODE" + ], + "intelliSenseMode": "msvc-x64" + } + ], + "version": 4 +} \ No newline at end of file diff --git a/src/DoughBoy.cpp b/src/DoughBoy.cpp new file mode 100644 index 0000000..b2f6add --- /dev/null +++ b/src/DoughBoy.cpp @@ -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"); +} diff --git a/src/DoughBoy.h b/src/DoughBoy.h new file mode 100644 index 0000000..373acc6 --- /dev/null +++ b/src/DoughBoy.h @@ -0,0 +1,35 @@ +#ifndef DOUGHBOY_H +#define DOUGHBOY_H + +#include +#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 diff --git a/src/DoughButton.cpp b/src/DoughButton.cpp new file mode 100644 index 0000000..70e33dc --- /dev/null +++ b/src/DoughButton.cpp @@ -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; + } +} diff --git a/src/DoughButton.h b/src/DoughButton.h new file mode 100644 index 0000000..fa28adc --- /dev/null +++ b/src/DoughButton.h @@ -0,0 +1,42 @@ +#ifndef DOUGH_BUTTON_H +#define DOUGH_BUTTON_H + +#define BUTTON_DEBOUNCE_DELAY 50 +#define BUTTON_LONGPRESS_DELAY 1000 + +#include +#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 diff --git a/src/DoughData.cpp b/src/DoughData.cpp new file mode 100644 index 0000000..0fb7c97 --- /dev/null +++ b/src/DoughData.cpp @@ -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(); + } +} diff --git a/src/DoughData.h b/src/DoughData.h new file mode 100644 index 0000000..039643e --- /dev/null +++ b/src/DoughData.h @@ -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 +#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 diff --git a/src/DoughLED.cpp b/src/DoughLED.cpp new file mode 100644 index 0000000..3a848c9 --- /dev/null +++ b/src/DoughLED.cpp @@ -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; +} diff --git a/src/DoughLED.h b/src/DoughLED.h new file mode 100644 index 0000000..acb11d5 --- /dev/null +++ b/src/DoughLED.h @@ -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 +#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 diff --git a/src/DoughMQTT.cpp b/src/DoughMQTT.cpp new file mode 100644 index 0000000..2603b11 --- /dev/null +++ b/src/DoughMQTT.cpp @@ -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); +} diff --git a/src/DoughMQTT.h b/src/DoughMQTT.h new file mode 100644 index 0000000..9b85d5e --- /dev/null +++ b/src/DoughMQTT.h @@ -0,0 +1,39 @@ +#ifndef DOUGH_MQTT_H +#define DOUGH_MQTT_H + +#include +#include +#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 diff --git a/src/DoughNetwork.cpp b/src/DoughNetwork.cpp new file mode 100644 index 0000000..f702083 --- /dev/null +++ b/src/DoughNetwork.cpp @@ -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; +} diff --git a/src/DoughNetwork.h b/src/DoughNetwork.h new file mode 100644 index 0000000..621167d --- /dev/null +++ b/src/DoughNetwork.h @@ -0,0 +1,26 @@ +#ifndef DOUGH_NETWORK_H +#define DOUGH_NETWORK_H + +#include +#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 diff --git a/src/DoughSensors.cpp b/src/DoughSensors.cpp new file mode 100644 index 0000000..db3a5fd --- /dev/null +++ b/src/DoughSensors.cpp @@ -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]")); +} diff --git a/src/DoughSensors.h b/src/DoughSensors.h new file mode 100644 index 0000000..dd28676 --- /dev/null +++ b/src/DoughSensors.h @@ -0,0 +1,32 @@ +#ifndef DOUGH_SENSORS_H +#define DOUGH_SENSORS_H + +#include +#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 diff --git a/src/DoughUI.cpp b/src/DoughUI.cpp new file mode 100644 index 0000000..dac7eba --- /dev/null +++ b/src/DoughUI.cpp @@ -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(""); + } + 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(); +} diff --git a/src/DoughUI.h b/src/DoughUI.h new file mode 100644 index 0000000..c08be2c --- /dev/null +++ b/src/DoughUI.h @@ -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 +#include +#include +#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 diff --git a/src/HCSR04.cpp b/src/HCSR04.cpp new file mode 100644 index 0000000..b31e3e3 --- /dev/null +++ b/src/HCSR04.cpp @@ -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 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 +#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 diff --git a/src/config.h b/src/config.h new file mode 100644 index 0000000..fc42e26 --- /dev/null +++ b/src/config.h @@ -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" diff --git a/src/config_local.example.h b/src/config_local.example.h new file mode 100644 index 0000000..3ef8998 --- /dev/null +++ b/src/config_local.example.h @@ -0,0 +1,23 @@ +// WPA2 WiFi connection configuration. +#define WIFI_SSID "" +#define WIFI_PASSWORD "" + +// MQTT broker configuration. +#define MQTT_BROKER "" +#define MQTT_PORT 1883 +#define MQTT_USERNAME "" +#define 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//...) +//#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 \ No newline at end of file