Further cleanup. Most noticably is moving all physical LED logic into the Dough::UI class, providing app-state-dependent logical functions for letting the LEDs represent a given application state.

This commit is contained in:
Maurice Makaay 2020-07-15 02:48:44 +02:00
parent 52b33de7aa
commit fd1363dd45
9 changed files with 196 additions and 147 deletions

View File

@ -9,6 +9,7 @@ namespace Dough
} }
App::App() : config(), App::App() : config(),
ui(),
wifi(), wifi(),
mqtt( mqtt(
&wifi, &wifi,
@ -19,26 +20,30 @@ namespace Dough
"temperature", "temperature",
TemperatureSensor::Instance(), TemperatureSensor::Instance(),
TEMPERATURE_AVERAGE_STORAGE, TEMPERATURE_AVERAGE_STORAGE,
TEMPERATURE_MEASURE_INTERVAL, TEMPERATURE_MEASURE_INTERVAL, sensorOnMeasureCallback,
MINIMUM_PUBLISH_INTERVAL), MINIMUM_PUBLISH_INTERVAL, sensorOnPublishCallback),
humiditySensor( humiditySensor(
&mqtt, &mqtt,
"humidity", "humidity",
HumiditySensor::Instance(), HumiditySensor::Instance(),
HUMIDITY_AVERAGE_STORAGE, HUMIDITY_AVERAGE_STORAGE,
HUMIDITY_MEASURE_INTERVAL, HUMIDITY_MEASURE_INTERVAL, sensorOnMeasureCallback,
MINIMUM_PUBLISH_INTERVAL), MINIMUM_PUBLISH_INTERVAL, sensorOnPublishCallback),
distanceSensor( distanceSensor(
&mqtt, &mqtt,
"distance", "distance",
DistanceSensor::Instance(), DistanceSensor::Instance(),
DISTANCE_AVERAGE_STORAGE, DISTANCE_AVERAGE_STORAGE,
DISTANCE_MEASURE_INTERVAL, DISTANCE_MEASURE_INTERVAL, sensorOnMeasureCallback,
MINIMUM_PUBLISH_INTERVAL), MINIMUM_PUBLISH_INTERVAL, sensorOnPublishCallback),
_logger("APP") {} _logger("APP") {}
void App::setup() void App::setup()
{ {
ui.setup();
ui.onoffButton.onInterrupt(::onoffButtonInterruptCallback);
ui.setupButton.onInterrupt(::setupButtonInterruptCallback);
wifi.setup(); wifi.setup();
mqtt.setup(); mqtt.setup();
temperatureSensor.setup(); temperatureSensor.setup();
@ -50,9 +55,14 @@ namespace Dough
{ {
if (config.isOk()) if (config.isOk())
{ {
// Get measurements from the sensors. Suspend the user interface
// interrupts in the meanwhile, to not disturb the timing-sensitive
// sensor readings.
ui.suspend();
temperatureSensor.loop(); temperatureSensor.loop();
humiditySensor.loop(); humiditySensor.loop();
distanceSensor.loop(); distanceSensor.loop();
ui.resume();
} }
} }
@ -89,4 +99,33 @@ void mqttOnMessageCallback(String &topic, String &payload)
{ {
callbackLogger.log("ss", "ERROR - Unhandled MQTT message, topic = ", topic.c_str()); callbackLogger.log("ss", "ERROR - Unhandled MQTT message, topic = ", topic.c_str());
} }
}
void onoffButtonInterruptCallback()
{
Dough::App::Instance()->ui.onoffButton.handleButtonState();
}
void setupButtonInterruptCallback()
{
Dough::App::Instance()->ui.setupButton.handleButtonState();
}
void sensorOnMeasureCallback()
{
Dough::App::Instance()->ui.notifySensorActivity();
}
void sensorOnPublishCallback()
{
Dough::App::Instance()->ui.notifyNetworkActivity();
}
// This callback is called when the TC4 timer from the UI code hits an overflow
// interrupt. It is defined outside the Dough namespace, because TC4_Handler is
// a hard-coded root namespace function name.
void TC4_Handler()
{
Dough::App::Instance()->ui.updateLEDs();
REG_TC4_INTFLAG = TC_INTFLAG_OVF; // Clear the OVF interrupt flag.
} }

View File

@ -16,6 +16,7 @@ namespace Dough
public: public:
static App *Instance(); static App *Instance();
Configuration config; Configuration config;
UI ui;
WiFi wifi; WiFi wifi;
MQTT mqtt; MQTT mqtt;
SensorController temperatureSensor; SensorController temperatureSensor;
@ -33,7 +34,11 @@ namespace Dough
} }
// Callback functions that need to live in the global namespace. // Callback functions that need to live in the global namespace.
void mqttOnConnectCallback(Dough::MQTT* mqtt); void mqttOnConnectCallback(Dough::MQTT *mqtt);
void mqttOnMessageCallback(String &topic, String &payload); void mqttOnMessageCallback(String &topic, String &payload);
void onoffButtonInterruptCallback();
void setupButtonInterruptCallback();
void sensorOnMeasureCallback();
void sensorOnPublishCallback();
#endif #endif

View File

@ -2,10 +2,7 @@
namespace Dough namespace Dough
{ {
Configuration::Configuration() : _logger("CONFIG") Configuration::Configuration() : _logger("CONFIG") {}
{
// _mqtt = MQTT::Instance();
}
void Configuration::setup() void Configuration::setup()
{ {

View File

@ -2,8 +2,6 @@
#define DOUGH_CONFIG_H #define DOUGH_CONFIG_H
#include <Arduino.h> #include <Arduino.h>
//#include "App/App.h"
#include "Network/MQTT.h"
#include "UI/Logger.h" #include "UI/Logger.h"
#include "Sensors/LowLevel/SensorHCSR04.h" #include "Sensors/LowLevel/SensorHCSR04.h"
@ -18,8 +16,6 @@ namespace Dough
public: public:
Configuration(); Configuration();
void setup(); void setup();
static void handleMqttConnect(MQTT *mqtt);
static void handleMqttMessage(String &key, String &value);
void setContainerHeight(int height); void setContainerHeight(int height);
unsigned int getContainerHeight(); unsigned int getContainerHeight();
void setTemperatureOffset(int offset); void setTemperatureOffset(int offset);
@ -27,7 +23,6 @@ namespace Dough
bool isOk(); bool isOk();
private: private:
MQTT *_mqtt;
Logger _logger; Logger _logger;
unsigned int _containerHeight; unsigned int _containerHeight;
bool _containerHeightSet; bool _containerHeightSet;

View File

@ -9,15 +9,18 @@ namespace Dough
SensorBase *sensor, SensorBase *sensor,
unsigned int storageSize, unsigned int storageSize,
unsigned int minimumMeasureTime, unsigned int minimumMeasureTime,
unsigned int minimumPublishTime) SensorControllerCallback onMeasure,
unsigned int minimumPublishTime,
SensorControllerCallback onPublish)
{ {
_mqtt = mqtt; _mqtt = mqtt;
_mqttKey = mqttKey; _mqttKey = mqttKey;
_sensor = sensor; _sensor = sensor;
_storageSize = storageSize; _storageSize = storageSize;
_minimumMeasureTime = minimumMeasureTime; _minimumMeasureTime = minimumMeasureTime;
_onMeasure = onMeasure;
_minimumPublishTime = minimumPublishTime; _minimumPublishTime = minimumPublishTime;
_ui = UI::Instance(); _onPublish = onPublish;
} }
void SensorController::setup() void SensorController::setup()
@ -42,10 +45,12 @@ namespace Dough
{ {
if (_mustMeasure()) if (_mustMeasure())
{ {
_onMeasure();
_measure(); _measure();
} }
if (_mustPublish()) if (_mustPublish())
{ {
_onPublish();
_publish(); _publish();
} }
} }
@ -69,19 +74,7 @@ namespace Dough
{ {
_lastMeasuredAt = millis(); _lastMeasuredAt = millis();
// Quickly dip the LED to indicate that a measurement has started.
// This is done synchroneously, because we suspend the timer interrupts
// in the upcoming code.
_ui->led3.off();
delay(50);
_ui->led3.on();
// Read a measurement from the sensor. Suspend the user interface
// interrupts in the meanwhile, to not disturb the timing-sensitive
// sensor readings.
_ui->suspend();
_store(_sensor->read()); _store(_sensor->read());
_ui->resume();
} }
bool SensorController::_mustPublish() bool SensorController::_mustPublish()
@ -139,14 +132,6 @@ namespace Dough
void SensorController::_publish() void SensorController::_publish()
{ {
// Quickly dip the LED to indicate that a publish has started.
// This is done synchroneously, because the upcoming code is too
// fast normally to register a LED going off and on again during
// the operation.
_ui->led1.off();
delay(50);
_ui->led1.on();
auto average = getAverage(); auto average = getAverage();
auto last = getLast(); auto last = getLast();

View File

@ -9,6 +9,8 @@
namespace Dough namespace Dough
{ {
typedef void (*SensorControllerCallback)();
// This class is used to store measurements for a sensor and to keep // This class is used to store measurements for a sensor and to keep
// track of running totals for handling average computations. // track of running totals for handling average computations.
// It also provides functionality to decide when to read a measurement // It also provides functionality to decide when to read a measurement
@ -32,16 +34,24 @@ namespace Dough
// @param minimumMeasureTime // @param minimumMeasureTime
// The number of seconds after which to read the next measurement // The number of seconds after which to read the next measurement
// from the sensor. // from the sensor.
// @param onMeasure
// A callback function that is called right before a measurement
// is taken.
// @param minimumPublishTime // @param minimumPublishTime
// The number of seconds after which to forcibly publish measurements // The number of seconds after which to forcibly publish measurements
// to MQTT, even when no significant changes to measurements were seen. // to MQTT, even when no significant changes to measurements were seen.
// @param onPublish
// A callback function that is called right before a measurement
// is published.
SensorController( SensorController(
MQTT *mqtt, MQTT *mqtt,
const char *mqttKey, const char *mqttKey,
SensorBase *sensor, SensorBase *sensor,
unsigned int storageSize, unsigned int storageSize,
unsigned int minimumMeasureTime, unsigned int minimumMeasureTime,
unsigned int minimumPublishTime); SensorControllerCallback onMeasure,
unsigned int minimumPublishTime,
SensorControllerCallback onPublish);
void setup(); void setup();
void loop(); void loop();
Measurement getLast(); Measurement getLast();
@ -50,7 +60,6 @@ namespace Dough
private: private:
MQTT *_mqtt; MQTT *_mqtt;
UI *_ui;
const char *_mqttKey; const char *_mqttKey;
char *_mqttAverageKey; char *_mqttAverageKey;
SensorBase *_sensor; SensorBase *_sensor;
@ -60,10 +69,12 @@ namespace Dough
unsigned int _averageCount = 0; unsigned int _averageCount = 0;
unsigned int _index = 0; unsigned int _index = 0;
bool _mustMeasure(); bool _mustMeasure();
SensorControllerCallback _onMeasure;
void _measure(); void _measure();
unsigned int _minimumMeasureTime; unsigned int _minimumMeasureTime;
unsigned long _lastMeasuredAt = 0; unsigned long _lastMeasuredAt = 0;
bool _mustPublish(); bool _mustPublish();
SensorControllerCallback _onPublish ;
void _publish(); void _publish();
unsigned int _minimumPublishTime; unsigned int _minimumPublishTime;
unsigned long _lastPublishedAt = 0; unsigned long _lastPublishedAt = 0;

View File

@ -2,17 +2,6 @@
namespace Dough namespace Dough
{ {
UI *UI::_instance = nullptr;
UI *UI::Instance()
{
if (UI::_instance == nullptr)
{
UI::_instance = new UI();
}
return UI::_instance;
}
UI::UI() : onoffButton(ONOFF_BUTTON_PIN), UI::UI() : onoffButton(ONOFF_BUTTON_PIN),
setupButton(SETUP_BUTTON_PIN), setupButton(SETUP_BUTTON_PIN),
ledBuiltin(LED_BUILTIN), ledBuiltin(LED_BUILTIN),
@ -24,9 +13,7 @@ namespace Dough
{ {
// Setup the buttons. // Setup the buttons.
onoffButton.setup(); onoffButton.setup();
onoffButton.onInterrupt(UI::onoffButtonISR);
setupButton.setup(); setupButton.setup();
setupButton.onInterrupt(UI::setupButtonISR);
// Setup the LEDs. // Setup the LEDs.
ledBuiltin.setup(); ledBuiltin.setup();
@ -47,16 +34,6 @@ namespace Dough
resume(); resume();
} }
void UI::onoffButtonISR()
{
UI::Instance()->onoffButton.handleButtonState();
}
void UI::setupButtonISR()
{
UI::Instance()->setupButton.handleButtonState();
}
// Setup a timer interrupt for updating the GUI. Unfortunately, the standard // 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 // libraries that I can find for this, are not equipped to work for the
// Arduino Nano 33 IOT architecture. Luckily, documentation and various // Arduino Nano 33 IOT architecture. Luckily, documentation and various
@ -91,13 +68,6 @@ namespace Dough
; // Wait for synchronization ; // Wait for synchronization
} }
// Disables the TC4 interrupts, suspending timed async updates to
// the user interface.
void UI::suspend()
{
NVIC_DisableIRQ(TC4_IRQn);
}
// Enables the TC4 interrupts in the Nested Vector InterruptController (NVIC), // Enables the TC4 interrupts in the Nested Vector InterruptController (NVIC),
// starting timed async updates for the user interface. // starting timed async updates for the user interface.
void UI::resume() void UI::resume()
@ -106,6 +76,13 @@ namespace Dough
NVIC_EnableIRQ(TC4_IRQn); // Enable TC4 interrupts NVIC_EnableIRQ(TC4_IRQn); // Enable TC4 interrupts
} }
// Disables the TC4 interrupts, suspending timed async updates to
// the user interface.
void UI::suspend()
{
NVIC_DisableIRQ(TC4_IRQn);
}
// Fire pending button events. // Fire pending button events.
void UI::processButtonEvents() void UI::processButtonEvents()
{ {
@ -125,7 +102,7 @@ namespace Dough
// the timer interrupt code from above. The timer interrupt based invocation // the timer interrupt code from above. The timer interrupt based invocation
// makes it possible to do LED updates, while the device is busy doing // makes it possible to do LED updates, while the device is busy doing
// something else. // something else.
void UI::updatedLEDs() void UI::updateLEDs()
{ {
ledBuiltin.loop(); ledBuiltin.loop();
led1.loop(); led1.loop();
@ -133,7 +110,65 @@ namespace Dough
led3.loop(); led3.loop();
} }
// Flash all LEDs, one at a time. void UI::notifyConnectingToWifi()
{
led1.blink()->slow();
led2.off();
led3.off();
}
void UI::notifyConnectingToMQTT()
{
led1.blink()->fast();
led2.off();
led3.off();
}
void UI::notifyWaitingForConfiguration()
{
led1.on();
led2.blink()->slow();
led3.off();
}
void UI::notifyCalibrating()
{
led1.on();
led2.blink()->fast();
led3.off();
}
void UI::notifyMeasurementsActive()
{
led1.on();
led2.on();
led3.on();
}
void UI::notifyMeasurementsPaused()
{
led1.on();
led2.on();
led3.pulse();
}
void UI::notifySensorActivity()
{
led3.off();
delay(50);
led3.on();
}
void UI::notifyNetworkActivity()
{
led1.off();
delay(50);
led1.on();
}
// Flash all LEDs, one at a time in a synchroneous manner, making
// this work when the UI timer interrupt is inactive. This is used
// as a "Hey, I'm awake!" signal from the device after booting up.
void UI::_flash_all_leds() void UI::_flash_all_leds()
{ {
ledBuiltin.on(); ledBuiltin.on();
@ -149,13 +184,4 @@ namespace Dough
delay(100); delay(100);
led3.off(); led3.off();
} }
} // namespace Dough } // namespace Dough
// This callback is called when the TC4 timer hits an overflow interrupt.
// Defined outside the Dough namespace, because TC4_Handler is a hard-coded
// root namespace function name.
void TC4_Handler()
{
Dough::UI::Instance()->updatedLEDs();
REG_TC4_INTFLAG = TC_INTFLAG_OVF; // Clear the OVF interrupt flag.
}

View File

@ -9,33 +9,40 @@
namespace Dough namespace Dough
{ {
// This class groups all user interface functionality: serial logging, // This class groups all user interface functionality: serial logging,
// LEDs and buttons. // LEDs and buttons.
class UI class UI
{ {
public: public:
static UI *Instance(); UI();
void setup(); void setup();
static void onoffButtonISR(); static void onoffButtonISR();
static void setupButtonISR(); static void setupButtonISR();
Button onoffButton; Button onoffButton;
Button setupButton; Button setupButton;
LED ledBuiltin; LED ledBuiltin;
LED led1; LED led1;
LED led2; LED led2;
LED led3; LED led3;
void processButtonEvents(); void processButtonEvents();
void clearButtonEvents(); void clearButtonEvents();
void updatedLEDs(); void updateLEDs();
void resume(); void resume();
void suspend(); void suspend();
void notifyConnectingToWifi();
void notifyConnectingToMQTT();
void notifyWaitingForConfiguration();
void notifyCalibrating();
void notifyMeasurementsActive();
void notifyMeasurementsPaused();
void notifySensorActivity();
void notifyNetworkActivity();
private: private:
UI(); void _setupTimerInterrupt();
void _setupTimerInterrupt(); static UI *_instance;
static UI *_instance; void _flash_all_leds();
void _flash_all_leds(); };
}; } // namespace Dough
}
#endif #endif

View File

@ -1,6 +1,7 @@
#include "main.h" #include "main.h"
// TOOD: implement the calibration logic // TOOD: implement the calibration logic
// TODO: don't make sensors instances
// 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: see what more stuff can be moved to the UI code. Maybe state to UI state translation ought to be there as well
DoughBoyState state = CONFIGURING; DoughBoyState state = CONFIGURING;
@ -10,20 +11,18 @@ void setup()
{ {
Dough::Logger::setup(); Dough::Logger::setup();
logger.log("s", "Initializing device"); logger.log("s", "Initializing device");
Dough::App::Instance()->setup(); auto app = Dough::App::Instance();
auto ui = Dough::UI::Instance(); app->setup();
ui->setup(); app->ui.onoffButton.onPress(handleOnoffButtonPress);
ui->onoffButton.onPress(handleOnoffButtonPress); app->ui.setupButton.onPress(handleSetupButtonPress);
ui->setupButton.onPress(handleSetupButtonPress);
logger.log("s", "Initialization completed, starting device"); logger.log("s", "Initialization completed, starting device");
} }
void loop() void loop()
{ {
auto app = Dough::App::Instance(); auto app = Dough::App::Instance();
auto ui = Dough::UI::Instance();
ui->processButtonEvents(); app->ui.processButtonEvents();
if (!setupNetworkConnection()) if (!setupNetworkConnection())
{ {
@ -66,7 +65,6 @@ bool setupNetworkConnection()
static auto connectionState = CONNECTING_WIFI; static auto connectionState = CONNECTING_WIFI;
auto app = Dough::App::Instance(); auto app = Dough::App::Instance();
auto ui = Dough::UI::Instance();
if (!app->wifi.isConnected()) if (!app->wifi.isConnected())
{ {
@ -79,9 +77,7 @@ bool setupNetworkConnection()
logger.log("s", "Connecting to the WiFi network ..."); logger.log("s", "Connecting to the WiFi network ...");
} }
connectionState = CONNECTING_WIFI; connectionState = CONNECTING_WIFI;
ui->led1.blink()->slow(); app->ui.notifyConnectingToWifi();
ui->led2.off();
ui->led3.off();
app->wifi.connect(); app->wifi.connect();
} }
if (app->wifi.isConnected() && !app->mqtt.isConnected()) if (app->wifi.isConnected() && !app->mqtt.isConnected())
@ -95,9 +91,7 @@ bool setupNetworkConnection()
logger.log("s", "Connecting to the MQTT broker ..."); logger.log("s", "Connecting to the MQTT broker ...");
} }
connectionState = CONNECTING_MQTT; connectionState = CONNECTING_MQTT;
ui->led1.blink()->fast(); app->ui.notifyConnectingToMQTT();
ui->led2.off();
ui->led3.off();
app->mqtt.connect(); app->mqtt.connect();
delay(1000); // purely costmetic, to make the faster LED blinking noticable delay(1000); // purely costmetic, to make the faster LED blinking noticable
} }
@ -106,10 +100,8 @@ bool setupNetworkConnection()
if (connectionState != CONNECTED) if (connectionState != CONNECTED)
{ {
logger.log("s", "Connection to MQTT broker established"); logger.log("s", "Connection to MQTT broker established");
ui->led1.on(); app->ui.notifyConnected();
ui->led2.off(); app->ui.clearButtonEvents();
ui->led3.off();
ui->clearButtonEvents();
connectionState = CONNECTED; connectionState = CONNECTED;
setStateToConfiguring(); setStateToConfiguring();
} }
@ -137,44 +129,36 @@ void handleSetupButtonPress()
void setStateToConfiguring() void setStateToConfiguring()
{ {
auto ui = Dough::UI::Instance(); auto app = Dough::App::Instance();
logger.log("s", "Waiting for configuration ..."); logger.log("s", "Waiting for configuration ...");
state = CONFIGURING; state = CONFIGURING;
ui->led1.on(); app->ui.notifyWaitingForConfiguration();
ui->led2.blink()->fast(); app->mqtt.publish("state", "configuring");
ui->led3.off();
Dough::App::Instance()->mqtt.publish("state", "configuring");
} }
void setStateToMeasuring() void setStateToMeasuring()
{ {
auto ui = Dough::UI::Instance(); auto app = Dough::App::Instance();
logger.log("s", "Starting measurements"); logger.log("s", "Starting measurements");
state = MEASURING; state = MEASURING;
ui->led1.on(); app->ui.notifyMeasurementsActive();
ui->led2.on(); app->mqtt.publish("state", "measuring");
ui->led3.on();
Dough::App::Instance()->mqtt.publish("state", "measuring");
} }
void setStateToPaused() void setStateToPaused()
{ {
auto ui = Dough::UI::Instance(); auto app = Dough::App::Instance();
logger.log("s", "Pausing measurements"); logger.log("s", "Pausing measurements");
state = PAUSED; state = PAUSED;
ui->led1.on(); app->ui.notifyMeasurementsPaused();
ui->led2.on(); app->mqtt.publish("state", "paused");
ui->led3.pulse();
Dough::App::Instance()->mqtt.publish("state", "paused");
} }
void setStateToCalibrating() void setStateToCalibrating()
{ {
auto ui = Dough::UI::Instance(); auto app = Dough::App::Instance();
logger.log("s", "Requested device calibration"); logger.log("s", "Requested device calibration");
state = CALIBRATING; state = CALIBRATING;
ui->led1.on(); app->ui.notifyCalibrating();
ui->led2.blink()->slow(); app->mqtt.publish("state", "calibrating");
ui->led3.off();
Dough::App::Instance()->mqtt.publish("state", "calibrating");
} }