commit ea8723493c370794eb9195b431e7d998f972991c Author: Maurice Makaay Date: Wed Jul 8 01:16:00 2020 +0200 Initial import. 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 0000000..164e260 Binary files /dev/null and b/Schematics.fzz differ 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