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(),
ui(),
wifi(),
mqtt(
&wifi,
@ -19,26 +20,30 @@ namespace Dough
"temperature",
TemperatureSensor::Instance(),
TEMPERATURE_AVERAGE_STORAGE,
TEMPERATURE_MEASURE_INTERVAL,
MINIMUM_PUBLISH_INTERVAL),
TEMPERATURE_MEASURE_INTERVAL, sensorOnMeasureCallback,
MINIMUM_PUBLISH_INTERVAL, sensorOnPublishCallback),
humiditySensor(
&mqtt,
"humidity",
HumiditySensor::Instance(),
HUMIDITY_AVERAGE_STORAGE,
HUMIDITY_MEASURE_INTERVAL,
MINIMUM_PUBLISH_INTERVAL),
HUMIDITY_MEASURE_INTERVAL, sensorOnMeasureCallback,
MINIMUM_PUBLISH_INTERVAL, sensorOnPublishCallback),
distanceSensor(
&mqtt,
"distance",
DistanceSensor::Instance(),
DISTANCE_AVERAGE_STORAGE,
DISTANCE_MEASURE_INTERVAL,
MINIMUM_PUBLISH_INTERVAL),
DISTANCE_MEASURE_INTERVAL, sensorOnMeasureCallback,
MINIMUM_PUBLISH_INTERVAL, sensorOnPublishCallback),
_logger("APP") {}
void App::setup()
{
ui.setup();
ui.onoffButton.onInterrupt(::onoffButtonInterruptCallback);
ui.setupButton.onInterrupt(::setupButtonInterruptCallback);
wifi.setup();
mqtt.setup();
temperatureSensor.setup();
@ -50,9 +55,14 @@ namespace Dough
{
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();
humiditySensor.loop();
distanceSensor.loop();
ui.resume();
}
}
@ -90,3 +100,32 @@ void mqttOnMessageCallback(String &topic, String &payload)
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:
static App *Instance();
Configuration config;
UI ui;
WiFi wifi;
MQTT mqtt;
SensorController temperatureSensor;
@ -35,5 +36,9 @@ namespace Dough
// Callback functions that need to live in the global namespace.
void mqttOnConnectCallback(Dough::MQTT *mqtt);
void mqttOnMessageCallback(String &topic, String &payload);
void onoffButtonInterruptCallback();
void setupButtonInterruptCallback();
void sensorOnMeasureCallback();
void sensorOnPublishCallback();
#endif

View File

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

View File

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

View File

@ -9,15 +9,18 @@ namespace Dough
SensorBase *sensor,
unsigned int storageSize,
unsigned int minimumMeasureTime,
unsigned int minimumPublishTime)
SensorControllerCallback onMeasure,
unsigned int minimumPublishTime,
SensorControllerCallback onPublish)
{
_mqtt = mqtt;
_mqttKey = mqttKey;
_sensor = sensor;
_storageSize = storageSize;
_minimumMeasureTime = minimumMeasureTime;
_onMeasure = onMeasure;
_minimumPublishTime = minimumPublishTime;
_ui = UI::Instance();
_onPublish = onPublish;
}
void SensorController::setup()
@ -42,10 +45,12 @@ namespace Dough
{
if (_mustMeasure())
{
_onMeasure();
_measure();
}
if (_mustPublish())
{
_onPublish();
_publish();
}
}
@ -69,19 +74,7 @@ namespace Dough
{
_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());
_ui->resume();
}
bool SensorController::_mustPublish()
@ -139,14 +132,6 @@ namespace Dough
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 last = getLast();

View File

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

View File

@ -2,17 +2,6 @@
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),
setupButton(SETUP_BUTTON_PIN),
ledBuiltin(LED_BUILTIN),
@ -24,9 +13,7 @@ namespace Dough
{
// Setup the buttons.
onoffButton.setup();
onoffButton.onInterrupt(UI::onoffButtonISR);
setupButton.setup();
setupButton.onInterrupt(UI::setupButtonISR);
// Setup the LEDs.
ledBuiltin.setup();
@ -47,16 +34,6 @@ namespace Dough
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
// libraries that I can find for this, are not equipped to work for the
// Arduino Nano 33 IOT architecture. Luckily, documentation and various
@ -91,13 +68,6 @@ namespace Dough
; // 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),
// starting timed async updates for the user interface.
void UI::resume()
@ -106,6 +76,13 @@ namespace Dough
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.
void UI::processButtonEvents()
{
@ -125,7 +102,7 @@ namespace Dough
// the timer interrupt code from above. The timer interrupt based invocation
// makes it possible to do LED updates, while the device is busy doing
// something else.
void UI::updatedLEDs()
void UI::updateLEDs()
{
ledBuiltin.loop();
led1.loop();
@ -133,7 +110,65 @@ namespace Dough
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()
{
ledBuiltin.on();
@ -150,12 +185,3 @@ namespace Dough
led3.off();
}
} // 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

@ -14,7 +14,7 @@ namespace Dough
class UI
{
public:
static UI *Instance();
UI();
void setup();
static void onoffButtonISR();
static void setupButtonISR();
@ -26,16 +26,23 @@ public:
LED led3;
void processButtonEvents();
void clearButtonEvents();
void updatedLEDs();
void updateLEDs();
void resume();
void suspend();
void notifyConnectingToWifi();
void notifyConnectingToMQTT();
void notifyWaitingForConfiguration();
void notifyCalibrating();
void notifyMeasurementsActive();
void notifyMeasurementsPaused();
void notifySensorActivity();
void notifyNetworkActivity();
private:
UI();
void _setupTimerInterrupt();
static UI *_instance;
void _flash_all_leds();
};
}
} // namespace Dough
#endif

View File

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