Initial import.

This commit is contained in:
Maurice Makaay 2020-07-08 01:16:00 +02:00
commit ea8723493c
27 changed files with 1832 additions and 0 deletions

13
.gitignore vendored Normal file
View File

@ -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

7
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,7 @@
{
// See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
"recommendations": [
"platformio.platformio-ide"
]
}

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) <year> <copyright holders>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice (including the next
paragraph) shall be included in all copies or substantial portions of the
Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

3
README.md Normal file
View File

@ -0,0 +1,3 @@
# arduino-doughboy
Firmware for my Doughboy project, used to monitor my sourdough starter and dough proofing.

BIN
Schematics.fzz Normal file

Binary file not shown.

9
platformio.ini Normal file
View File

@ -0,0 +1,9 @@
[env:nano_33_iot]
platform = atmelsam
board = nano_33_iot
framework = arduino
lib_deps =
WiFiNINA
DHT sensor library
MQTT

17
src/.vscode/c_cpp_properties.json vendored Normal file
View File

@ -0,0 +1,17 @@
{
"configurations": [
{
"name": "Win32",
"includePath": [
"${workspaceFolder}/**"
],
"defines": [
"_DEBUG",
"UNICODE",
"_UNICODE"
],
"intelliSenseMode": "msvc-x64"
}
],
"version": 4
}

156
src/DoughBoy.cpp Normal file
View File

@ -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");
}

35
src/DoughBoy.h Normal file
View File

@ -0,0 +1,35 @@
#ifndef DOUGHBOY_H
#define DOUGHBOY_H
#include <Arduino.h>
#include "DoughNetwork.h"
#include "DoughMQTT.h"
#include "DoughSensors.h"
#include "DoughData.h"
#include "DoughButton.h"
#include "DoughUI.h"
#include "config.h"
typedef enum {
CONNECTING_WIFI,
CONNECTING_MQTT,
CONNECTED
} DoughBoyConnectionState;
typedef enum {
CONFIGURING,
MEASURING,
PAUSED,
CALIBRATING
} DoughBoyState;
bool setupNetworkConnection();
void handleMqttMessage(String &topic, String &payload);
void handleOnoffButtonPress();
void handleSetupButtonPress();
void setStateToConfiguring();
void setStateToMeasuring();
void setStateToPaused();
void setStateToCalibrating();
#endif

125
src/DoughButton.cpp Normal file
View File

@ -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;
}
}

42
src/DoughButton.h Normal file
View File

@ -0,0 +1,42 @@
#ifndef DOUGH_BUTTON_H
#define DOUGH_BUTTON_H
#define BUTTON_DEBOUNCE_DELAY 50
#define BUTTON_LONGPRESS_DELAY 1000
#include <Arduino.h>
#include "config.h"
typedef enum {
UP,
DOWN,
DOWN_LONG,
UP_AFTER_LONG,
UP_AFTER_SHORT,
READY_FOR_NEXT_PRESS
} DoughButtonState;
typedef void (*DoughButtonHandler)();
class DoughButton {
public:
DoughButton(int pin);
void setup();
void loop();
void onInterrupt(DoughButtonHandler isr);
void onPress(DoughButtonHandler handler);
void onShortPress(DoughButtonHandler handler);
void onLongPress(DoughButtonHandler handler);
void clearEvents();
void handleButtonState();
private:
int _pin;
DoughButtonHandler _pressHandler = nullptr;
DoughButtonHandler _shortPressHandler = nullptr;
DoughButtonHandler _longPressHandler = nullptr;
bool _debounceState = false;
unsigned long _debounceTimer = 0;
DoughButtonState _state = UP;
};
#endif

274
src/DoughData.cpp Normal file
View File

@ -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();
}
}

98
src/DoughData.h Normal file
View File

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

142
src/DoughLED.cpp Normal file
View File

@ -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;
}

53
src/DoughLED.h Normal file
View File

@ -0,0 +1,53 @@
#ifndef DOUGH_LED_H
#define DOUGH_LED_H
// Delay times for blinking, flashing and dipping.
#define LED_TRANSITION_TIME_SLOW 400
#define LED_TRANSITION_TIME_DEFAULT 250
#define LED_TRANSITION_TIME_FAST 100
#include <Arduino.h>
#include "config.h"
typedef enum {
ON,
OFF,
BLINK_ON,
BLINK_OFF,
FLASH,
DIP,
PULSE
} DoughLEDState;
class DoughLED {
public:
DoughLED(int pin);
void setup();
void loop();
void on();
void off();
DoughLED* blink();
DoughLED* blink(int onStep, int ofSteps);
DoughLED* flash();
DoughLED* dip();
void pulse();
void slow();
void fast();
bool isOn();
bool isOff();
private:
int _pin;
int _pinState = LOW;
DoughLEDState _state = OFF;
void _setPin(int high_or_low);
unsigned long _timer;
unsigned int _time;
int _blinkOnStep;
int _blinkOfSteps;
int _blinkStep;
int _brightness;
int _pulseStep;
};
#endif

110
src/DoughMQTT.cpp Normal file
View File

@ -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);
}

39
src/DoughMQTT.h Normal file
View File

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

80
src/DoughNetwork.cpp Normal file
View File

@ -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;
}

26
src/DoughNetwork.h Normal file
View File

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

77
src/DoughSensors.cpp Normal file
View File

@ -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]"));
}

32
src/DoughSensors.h Normal file
View File

@ -0,0 +1,32 @@
#ifndef DOUGH_SENSORS_H
#define DOUGH_SENSORS_H
#include <DHT.h>
#include "HCSR04.h"
#include "DoughUI.h"
#include "config.h"
class DoughSensors {
public:
static DoughSensors* Instance();
void setup();
void readAll();
void readTemperature();
int temperature = 0;
bool temperatureOk = false;
void readHumidity();
int humidity = 0;
bool humidityOk = false;
void readDistance();
int distance = 0;
bool distanceOk = false;
private:
DoughSensors();
static DoughSensors* _instance;
DoughUI *_ui;
DHT* _dht;
HCSR04* _hcsr04;
};
#endif

198
src/DoughUI.cpp Normal file
View File

@ -0,0 +1,198 @@
#include "DoughUI.h"
DoughUI* DoughUI::_instance = nullptr;
/**
* Fetch the DoughUI singleton.
*/
DoughUI* DoughUI::Instance() {
if (DoughUI::_instance == nullptr) {
DoughUI::_instance = new DoughUI();
}
return DoughUI::_instance;
}
DoughUI::DoughUI() : onoffButton(ONOFF_BUTTON_PIN),
setupButton(SETUP_BUTTON_PIN),
ledBuiltin(LED_BUILTIN),
led1(LED1_PIN),
led2(LED2_PIN),
led3(LED3_PIN) {}
/**
* Called from the main setup() function of the sketch.
*/
void DoughUI::setup() {
// Setup the serial port, used for logging.
Serial.begin(LOG_BAUDRATE);
#ifdef LOG_WAIT_SERIAL
while (!Serial) {
// wait for serial port to connect. Needed for native USB.
}
#endif
// Setup the buttons.
onoffButton.setup();
onoffButton.onInterrupt(DoughUI::onoffButtonISR);
setupButton.setup();
setupButton.onInterrupt(DoughUI::setupButtonISR);
// Setup the LEDs.
ledBuiltin.setup();
led1.setup();
led2.setup();
led3.setup();
// Setup a timer interrupt that is used to update the
// user interface (a.k.a. "LEDs") in parallel to other activities.
// This allows for example to have a flashing LED, during the
// wifi connection setup.
_setupTimerInterrupt();
// Notify the user that we're on a roll!
flash_all_leds();
}
void DoughUI::onoffButtonISR() {
DoughUI::Instance()->onoffButton.handleButtonState();
}
void DoughUI::setupButtonISR() {
DoughUI::Instance()->setupButton.handleButtonState();
}
/**
* Log a message to the serial interface.
*/
void DoughUI::log(const char *category, const char *fmt, ...) {
char buf[12];
snprintf(buf, sizeof(buf)/sizeof(buf[0]), "%8s | ", category);
Serial.print(buf);
va_list args;
va_start(args, fmt);
while (*fmt != '\0') {
if (*fmt == 'i') {
int i = va_arg(args, int);
Serial.print(i);
}
else if (*fmt == 'f') {
float f = va_arg(args, double);
Serial.print(f);
}
else if (*fmt == 'a') {
IPAddress a = va_arg(args, IPAddress);
Serial.print(a);
}
else if (*fmt == 's') {
const char* s = va_arg(args, const char*);
Serial.print(s);
}
else if (*fmt == 'S') {
String S = va_arg(args, String);
Serial.print(S);
}
else {
Serial.print("<log(): invalid format char '");
Serial.print(*fmt);
Serial.print("'>");
}
fmt++;
}
va_end(args);
Serial.println("");
}
/**
* Setup a timer interrupt for updating the GUI. Unfortunately, the standard
* libraries that I can find for this, are not equipped to work for the
* Arduino Nano 33 IOT architecture. Luckily, documentation and various
* helpful threads on the internet helped me piece the following code together.
*/
void DoughUI::_setupTimerInterrupt() {
REG_GCLK_GENDIV = GCLK_GENDIV_DIV(200) | // Use divider (32kHz/200 = 160Hz)
GCLK_GENDIV_ID(4); // for Generic Clock GCLK4
while (GCLK->STATUS.bit.SYNCBUSY); // Wait for synchronization
REG_GCLK_GENCTRL = GCLK_GENCTRL_IDC | // Set the duty cycle to 50/50 HIGH/LOW
GCLK_GENCTRL_GENEN | // and enable the clock
GCLK_GENCTRL_SRC_OSC32K | // using the 32kHz clock source as input
GCLK_GENCTRL_ID(4); // for Generic Clock GCLK4
while (GCLK->STATUS.bit.SYNCBUSY); // Wait for synchronization
REG_GCLK_CLKCTRL = GCLK_CLKCTRL_CLKEN | // Enable timer
GCLK_CLKCTRL_GEN_GCLK4 | // using Generic Clock GCLK4 as input
GCLK_CLKCTRL_ID_TC4_TC5; // and feed its output to TC4 and TC5
while (GCLK->STATUS.bit.SYNCBUSY); // Wait for synchronization
REG_TC4_CTRLA |= TC_CTRLA_PRESCALER_DIV8 | // Use prescaler (160Hz / 8 = 20Hz)
TC_CTRLA_WAVEGEN_MFRQ | // Use match frequency (MFRQ) mode
TC_CTRLA_MODE_COUNT8 | // Set the timer to 8-bit mode
TC_CTRLA_ENABLE; // Enable TC4
REG_TC4_INTENSET = TC_INTENSET_OVF; // Enable TC4 overflow (OVF) interrupts
REG_TC4_COUNT8_CC0 = 1; // Set the CC0 as the TOP value for MFRQ (1 => 50ms per pulse)
while (TC4->COUNT8.STATUS.bit.SYNCBUSY); // Wait for synchronization
// Enable interrupts for TC4 in the Nested Vector InterruptController (NVIC),
NVIC_SetPriority(TC4_IRQn, 0); // Set NVIC priority for TC4 to 0 (highest)
NVIC_EnableIRQ(TC4_IRQn); // Enable TC4 interrupts
}
void DoughUI::resume() {
NVIC_EnableIRQ(TC4_IRQn); // Enable TC4 interrupts
}
void DoughUI::suspend() {
NVIC_DisableIRQ(TC4_IRQn); // Disable TC4 interrupts
}
/**
* This callback is called when the TC4 timer hits an overflow interrupt.
*/
void TC4_Handler() {
DoughUI::Instance()->updatedLEDs();
REG_TC4_INTFLAG = TC_INTFLAG_OVF; // Clear the OVF interrupt flag.
}
/**
* Fire pending button events.
*/
void DoughUI::processButtonEvents() {
onoffButton.loop();
setupButton.loop();
}
/**
* Clear pending button events.
*/
void DoughUI::clearButtonEvents() {
onoffButton.clearEvents();
setupButton.clearEvents();
}
/**
* Update the state of all the LEDs in the system.
* This method is called both sync by methods in this class and async by
* the timer interrupt code from above. The timer interrupt based invocatino
* makes it possible to do LED updates, while the device is busy doing
* something else.
*/
void DoughUI::updatedLEDs() {
ledBuiltin.loop();
led1.loop();
led2.loop();
led3.loop();
}
/**
* Flash all LEDs, one at a time.
*/
void DoughUI::flash_all_leds() {
ledBuiltin.on(); delay(100);
ledBuiltin.off(); led1.on(); delay(100);
led1.off(); led2.on(); delay(100);
led2.off(); led3.on(); delay(100);
led3.off();
}

46
src/DoughUI.h Normal file
View File

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

124
src/HCSR04.cpp Normal file
View File

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

58
src/HCSR04.h Normal file
View File

@ -0,0 +1,58 @@
#ifndef HCSR04_H
#define HCSR04_H
// The minimum and maximum distance that can be measured in mm.
// This is based on the specifications of the HCSR04 sensor.
#define HCSR04_MIN_MM 40
#define HCSR04_MAX_MM 4000
// Some parameters that are used to get more stable reading from the sensor.
// To get a better reading:
// - multiple samples are taken
// - between each sample, a random wait is added (because I saw repeating
// patterns when reading from a tight loop)
// - only a subset from the samples is used to compute the average distance
// (the high and low extremes are ignored)
#define HCSR04_SAMPLES_TAKE 20
#define HCSR04_SAMPLES_USE 8
#define HCSR04_SAMPLE_WAIT 30
#define HCSR04_SAMPLE_WAIT_SPREAD 12
// Default values for temperature and humidity, which have an effect
// on the speed of sound. At runtime, the temperature and humidity
// can be modified by using their respective setter functions
// setTemperature() and setHumidity().
#define HCSR04_INIT_TEMPERATURE 19.000
#define HCSR04_INIT_HUMIDITY 50.000
#include <Arduino.h>
#include "DoughUI.h"
#include "config.h"
class HCSR04 {
public:
HCSR04(int triggerPin, int echoPin);
void begin();
void setTemperature(int temperature);
void setHumidity(int humidity);
int readDistance();
private:
int _triggerPin;
int _echoPin;
int _humidity;
int _temperature;
void _setSpeedOfSound();
float _speedOfSound;
void _setEchoTimeout();
int _echoTimeout;
float _samples[HCSR04_SAMPLES_TAKE];
void _takeSamples();
bool _haveEnoughSamples();
int _takeSample();
int _successfulSamples;
void _sortSamples();
int _computeAverage();
};
#endif

24
src/config.h Normal file
View File

@ -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"

View File

@ -0,0 +1,23 @@
// WPA2 WiFi connection configuration.
#define WIFI_SSID "<SSID network name>"
#define WIFI_PASSWORD "<network password>"
// MQTT broker configuration.
#define MQTT_BROKER "<IP or hostname>"
#define MQTT_PORT 1883
#define MQTT_USERNAME "<mqtt username>"
#define MQTT_PASSWORD "<mqtt password>"
// The prefix to use for the MQTT publishing topic.
#define MQTT_TOPIC_PREFIX "sensors/doughboy"
// Define this one to not use the WiFi MAC address as the device ID
// in the publish topics (sensors/doughboy/<MQTT_DEVICE_ID>/...)
//#define MQTT_DEVICE_ID "1"
// Define this one to wait for USB serial to come up.
// This can be useful during development, when you want all
// serial messages to appear in the serial monitor.
// Without this, some of the initial serial messages might
// be missing from the output.
//#define LOG_WAIT_SERIAL