diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..2445e50
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,59 @@
+[*]
+cpp_indent_braces=false
+cpp_indent_multi_line_relative_to=innermost_parenthesis
+cpp_indent_within_parentheses=indent
+cpp_indent_preserve_within_parentheses=false
+cpp_indent_case_labels=false
+cpp_indent_case_contents=true
+cpp_indent_case_contents_when_block=false
+cpp_indent_lambda_braces_when_parameter=false
+cpp_indent_goto_labels=one_left
+cpp_indent_preprocessor=leftmost_column
+cpp_indent_access_specifiers=false
+cpp_indent_namespace_contents=true
+cpp_indent_preserve_comments=false
+cpp_new_line_before_open_brace_namespace=new_line
+cpp_new_line_before_open_brace_type=new_line
+cpp_new_line_before_open_brace_function=new_line
+cpp_new_line_before_open_brace_block=same_line
+cpp_new_line_before_open_brace_lambda=same_line
+cpp_new_line_scope_braces_on_separate_lines=false
+cpp_new_line_close_brace_same_line_empty_type=false
+cpp_new_line_close_brace_same_line_empty_function=false
+cpp_new_line_before_catch=same_line
+cpp_new_line_before_else=false
+cpp_new_line_before_while_in_do_while=false
+cpp_space_before_function_open_parenthesis=remove
+cpp_space_within_parameter_list_parentheses=false
+cpp_space_between_empty_parameter_list_parentheses=false
+cpp_space_after_keywords_in_control_flow_statements=true
+cpp_space_within_control_flow_statement_parentheses=false
+cpp_space_before_lambda_open_parenthesis=false
+cpp_space_within_cast_parentheses=false
+cpp_space_after_cast_close_parenthesis=false
+cpp_space_within_expression_parentheses=false
+cpp_space_before_block_open_brace=true
+cpp_space_between_empty_braces=false
+cpp_space_before_initializer_list_open_brace=false
+cpp_space_within_initializer_list_braces=true
+cpp_space_preserve_in_initializer_list=true
+cpp_space_before_open_square_bracket=false
+cpp_space_within_square_brackets=false
+cpp_space_before_empty_square_brackets=false
+cpp_space_between_empty_square_brackets=false
+cpp_space_group_square_brackets=true
+cpp_space_within_lambda_brackets=false
+cpp_space_between_empty_lambda_brackets=false
+cpp_space_before_comma=false
+cpp_space_after_comma=true
+cpp_space_remove_around_member_operators=true
+cpp_space_before_inheritance_colon=true
+cpp_space_before_constructor_colon=true
+cpp_space_remove_before_semicolon=true
+cpp_space_after_semicolon=false
+cpp_space_remove_around_unary_operator=true
+cpp_space_around_binary_operator=insert
+cpp_space_around_assignment_operator=insert
+cpp_space_pointer_reference_alignment=left
+cpp_space_around_ternary_operator=insert
+cpp_wrap_preserve_blocks=one_liners
diff --git a/doc/FurnaceControllerScreen.drawio b/doc/FurnaceControllerScreen.drawio
new file mode 100644
index 0000000..bdfc22d
--- /dev/null
+++ b/doc/FurnaceControllerScreen.drawio
@@ -0,0 +1,280 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/lib/FurnaceController/HeatingZoneController.cpp b/lib/FurnaceController/HeatingZoneController.cpp
index bc18aeb..8c5f27d 100644
--- a/lib/FurnaceController/HeatingZoneController.cpp
+++ b/lib/FurnaceController/HeatingZoneController.cpp
@@ -1,21 +1,24 @@
#include "HeatingZoneController.h"
+#include
+
namespace
{
constexpr HeatingZoneController::DeciDegrees FailSafeLowTarget{ 100 };
constexpr HeatingZoneController::DeciDegrees FailSafeHighTarget{ 300 };
- constexpr uint32_t OpenWindowLockoutDurationMs{ 10 * 60 * 1000 };
+
+ constexpr HeatingZoneController::DeciDegrees MinTargetTempHysteresis{ 1 };
+ constexpr HeatingZoneController::DeciDegrees MaxTargetTempHysteresis{ 10 };
}
HeatingZoneController::HeatingZoneController(
- Configuration& config,
- Schedule& schedule
+ const Configuration& config,
+ const Schedule& schedule
)
: _config{ config }
, _schedule{ schedule }
, _lastInputTemperature{ FailSafeHighTarget }
-{
-}
+{}
void HeatingZoneController::updateDateTime(
const int dayOfWeek,
@@ -31,7 +34,7 @@ void HeatingZoneController::updateDateTime(
_scheduleDataDay = dayOfWeek;
_scheduleDataByte = intervalIndex >> 3;
- _scheduleDataMask = 1 << (intervalIndex & 0b111);
+ _scheduleDataMask = 1 << (7 - (intervalIndex & 0b111));
}
void HeatingZoneController::setMode(const Mode mode)
@@ -79,6 +82,11 @@ void HeatingZoneController::inputTemperature(const DeciDegrees value)
_lastInputTemperature = value;
}
+HeatingZoneController::DeciDegrees HeatingZoneController::lastInputTemperature() const
+{
+ return _lastInputTemperature;
+}
+
void HeatingZoneController::setHighTargetTemperature(const DeciDegrees value)
{
_highTargetTemperature = value;
@@ -147,7 +155,7 @@ std::optional HeatingZoneController::targetT
void HeatingZoneController::setWindowOpened(const bool open)
{
if (!open && _windowOpen) {
- _openWindowLockoutRemainingMs = OpenWindowLockoutDurationMs;
+ _openWindowLockoutRemainingMs = _config.openWindowLockoutDurationSeconds * 1000;
} else if (open) {
_openWindowLockoutRemainingMs = 0;
}
@@ -187,13 +195,21 @@ bool HeatingZoneController::callingForHeating()
calculatedTargetTemperature = t.value();
}
- if (_callForHeatingByTemperature) {
- const auto target = calculatedTargetTemperature + _config.heatingOvershoot;
+ calculatedTargetTemperature = std::clamp(
+ calculatedTargetTemperature,
+ FailSafeLowTarget,
+ FailSafeHighTarget
+ );
- if (
- _lastInputTemperature >= target
- || _lastInputTemperature >= FailSafeHighTarget
- ) {
+ if (_callForHeatingByTemperature) {
+ const auto target = calculatedTargetTemperature
+ + std::clamp(
+ _config.heatingOvershoot,
+ MinTargetTempHysteresis,
+ MaxTargetTempHysteresis
+ );
+
+ if (_lastInputTemperature >= target) {
_callForHeatingByTemperature = false;
}
} else {
@@ -204,12 +220,14 @@ bool HeatingZoneController::callingForHeating()
const auto target =
_furnaceHeating
? calculatedTargetTemperature
- : calculatedTargetTemperature - _config.heatingUndershoot;
-
- if (
- _lastInputTemperature <= target
- || _lastInputTemperature <= FailSafeLowTarget
- ) {
+ : calculatedTargetTemperature -
+ std::clamp(
+ _config.heatingUndershoot,
+ MinTargetTempHysteresis,
+ MaxTargetTempHysteresis
+ );
+
+ if (_lastInputTemperature <= target) {
_callForHeatingByTemperature = true;
// Start the delay timer
diff --git a/lib/FurnaceController/HeatingZoneController.h b/lib/FurnaceController/HeatingZoneController.h
index 01c7226..3aa3601 100644
--- a/lib/FurnaceController/HeatingZoneController.h
+++ b/lib/FurnaceController/HeatingZoneController.h
@@ -14,15 +14,33 @@ class HeatingZoneController
/**
* @brief Bit mask of high target temperature (30-minute slots) for 7 days.
+ * This structure is directly written into the settings memory.
+ * Be cautious when modifying this structure to avoid breaking data layout
+ * in existing devices. New fields should be added to the end of this structure.
+ * When data should be copied over from an existing field into a new one,
+ * or a new field should be initialized with a specific value,
+ * be sure to do a settings data version check and do the migration with the help
+ * of that.
*/
using Schedule = std::array;
+ /**
+ * @brief Essential controller configuration data.
+ * This structure is directly written into the settings memory.
+ * Be cautious when modifying this structure to avoid breaking data layout
+ * in existing devices. New fields should be added to the end of this structure.
+ * When data should be copied over from an existing field into a new one,
+ * or a new field should be initialized with a specific value,
+ * be sure to do a settings data version check and do the migration with the help
+ * of that.
+ */
struct Configuration
{
uint32_t overrideTimeoutSeconds{ 120 * 60 };
uint32_t boostInitialDurationSeconds{ 30 * 60 };
uint32_t boostExtensionDurationSeconds{ 15 * 60 };
uint32_t heatingStartDelaySeconds{ 0 };
+ uint32_t openWindowLockoutDurationSeconds{ 600 };
DeciDegrees heatingOvershoot{ 5 };
DeciDegrees heatingUndershoot{ 5 };
DeciDegrees holidayModeTemperature{ 180 };
@@ -35,16 +53,28 @@ class HeatingZoneController
Holiday
};
+ /**
+ * @brief Controller state data.
+ * This structure is directly written into the settings memory.
+ * Be cautious when modifying this structure to avoid breaking data layout
+ * in existing devices. New fields should be added to the end of this structure.
+ * When data should be copied over from an existing field into a new one,
+ * or a new field should be initialized with a specific value,
+ * be sure to do a settings data version check and do the migration with the help
+ * of that.
+ */
struct State
{
Mode mode{ Mode::Off };
DeciDegrees highTargetTemperature{ 220 };
DeciDegrees lowTargetTemperature{ 220 };
+
+ [[nodiscard]] bool operator<=>(const State&) const = default;
};
explicit HeatingZoneController(
- Configuration& config,
- Schedule& schedule
+ const Configuration& config,
+ const Schedule& schedule
);
void updateDateTime(int dayOfWeek, int hour, int minute);
@@ -64,6 +94,8 @@ class HeatingZoneController
*/
void inputTemperature(DeciDegrees value);
+ [[nodiscard]] DeciDegrees lastInputTemperature() const;
+
void setHighTargetTemperature(DeciDegrees value);
[[nodiscard]] DeciDegrees highTargetTemperature() const;
@@ -144,8 +176,8 @@ class HeatingZoneController
[[nodiscard]] bool startDelayActive() const;
private:
- Configuration& _config;
- Schedule& _schedule;
+ const Configuration& _config;
+ const Schedule& _schedule;
bool _stateChanged{ false };
diff --git a/lib/esp-iot-base b/lib/esp-iot-base
index 58e8358..8502638 160000
--- a/lib/esp-iot-base
+++ b/lib/esp-iot-base
@@ -1 +1 @@
-Subproject commit 58e83587970eb6200b8aba827bc4949ef6e044ce
+Subproject commit 8502638e765cb27e4997ebc078c711016cb9c2fa
diff --git a/platformio.ini b/platformio.ini
index 1c4004c..e133fd2 100644
--- a/platformio.ini
+++ b/platformio.ini
@@ -11,21 +11,55 @@
[platformio]
extra_configs = lib/esp-iot-base/platformio.ini
-[env:native]
-platform = native
-test_framework = googletest
-build_flags = -std=c++20
+[common]
+build_flags =
+ -std=c++2a
+ -fconcepts
+ ; ${iot.extra_release_flags}
+ ; -Wall
+ -DIOT_ENABLE_PERSISTENCE
+ -DIOT_PERSISTENCE_EERAM_47L16
+ -DIOT_ENABLE_SYSTEM_CLOCK
+ -DIOT_SYSTEM_CLOCK_HW_RTC
+ -DIOT_ENABLE_MQTT
+ -DMQTT_MAX_PACKET_SIZE=2048
+ ; -DDEBUG_ESP_PORT=Serial
+ ; -DDEBUG_ESP_SSL
+ ; -DDEBUG_ESP_HTTP_SERVER
+ ; -DDEBUG_ESP_WIFI
+ ; -DTEST_BUILD
+ ; -DSIMPLE_I2C_DEBUG
+
+build_unflags = -std=c++11 -std=gnu++17
-[env:esp12e]
+[env:esp_local_debug]
platform = espressif8266@4.2.1
board = esp12e
framework = arduino
upload_speed = 460800
monitor_speed = 74880
monitor_filters = esp8266_exception_decoder, default
-test_framework = googletest
+upload_resetmethod = nodemcu
+
+build_flags =
+ ${iot.build_flags}
+ ${iot.release_flags}
+ ${common.build_flags}
+ -DTEST_BUILD
+
+lib_deps =
+ ${iot.lib_deps}
-#upload_resetmethod = nodemcu
+build_unflags =
+ ${common.build_unflags}
+
+[env:esp_deploy]
+platform = espressif8266@4.2.1
+board = esp12e
+framework = arduino
+upload_speed = 460800
+monitor_speed = 74880
+monitor_filters = esp8266_exception_decoder, default
upload_protocol = espota
upload_port = furnace.iot.home
upload_flags = --auth="${sysenv.PIO_ESP_THERMOSTAT_AUTH}"
@@ -33,20 +67,16 @@ upload_flags = --auth="${sysenv.PIO_ESP_THERMOSTAT_AUTH}"
build_flags =
${iot.build_flags}
${iot.release_flags}
- ; ${iot.extra_release_flags}
- ; -Wall
- -DIOT_ENABLE_PERSISTENCE
- -DIOT_PERSISTENCE_EERAM_47L16
- -DIOT_ENABLE_SYSTEM_CLOCK
- -DIOT_SYSTEM_CLOCK_HW_RTC
- -DIOT_ENABLE_MQTT
- -DIOT_ENABLE_MQTT_EXTRA_LARGE_BUFFER
- ; -DDEBUG_ESP_PORT=Serial
- ; -DDEBUG_ESP_SSL
- ; -DDEBUG_ESP_HTTP_SERVER
- ; -DDEBUG_ESP_WIFI
- ; -DTEST_BUILD
- ; -DSIMPLE_I2C_DEBUG
+ ${common.build_flags}
lib_deps =
- ${iot.lib_deps}
\ No newline at end of file
+ ${iot.lib_deps}
+
+build_unflags =
+ ${common.build_unflags}
+
+[env:unit_test]
+platform = native
+test_framework = googletest
+build_flags = ${common.build_flags}
+build_unflags = ${common.build_unflags}
\ No newline at end of file
diff --git a/src/Config.h b/src/Config.h
index 4b9a081..066c653 100644
--- a/src/Config.h
+++ b/src/Config.h
@@ -18,13 +18,9 @@
Created on 2017-01-04
*/
-#ifndef CONFIG_H
-#define CONFIG_H
+#include
-#define CONFIG_HEATCTL_SETTINGS_BASE_ADDR 0x00
-#define CONFIG_SCHEDULER_SETTINGS_BASE_ADDR 0x10
+#include "PrivateConfig.h"
#define CONFIG_USE_OLED_SH1106
-#endif /* CONFIG_H */
-
diff --git a/src/FurnaceController.cpp b/src/FurnaceController.cpp
index 401d3b1..4754762 100644
--- a/src/FurnaceController.cpp
+++ b/src/FurnaceController.cpp
@@ -2,6 +2,9 @@
#include "Extras.h"
#include "HomeAssistant.h"
+#include "TemperatureSensor.h"
+
+#include "ui/Model.h"
#include
@@ -30,18 +33,58 @@ namespace Devices::CallingForHeatingSensor
auto stateTopic() { return PSTR("/calling_for_heating"); }
}
-FurnaceController::FurnaceController(const ApplicationConfig& appConfig)
- : _appConfig{ appConfig }
- , _app{ _appConfig }
- , _settings{ _app.settings().registerSetting(32) }
- , _zones{
- HeatingZone{ 0, _app },
- HeatingZone{ 1, _app },
- HeatingZone{ 2, _app },
- HeatingZone{ 3, _app },
- HeatingZone{ 10, _app },
- HeatingZone{ 11, _app }
+namespace
+{
+ namespace Detail
+ {
+ template
+ [[nodiscard]] constexpr std::array createZones(
+ const auto zoneIds,
+ CoreApplication& app,
+ std::array& settings,
+ std::index_sequence
+ )
+ {
+ return std::array{
+ HeatingZone{
+ std::get(zoneIds),
+ app,
+ HeatingZone::SettingDependencies{
+ .state = settings[Indices].state,
+ .configuration = settings[Indices].config,
+ .schedule = settings[Indices].schedule
+ }
+ }...
+ };
+ }
}
+
+ template
+ [[nodiscard]] constexpr auto createZones(CoreApplication& app, Settings& settings)
+ {
+ static_assert(sizeof...(ZoneIds) == Config::ZoneCount, "Zone count mismatch");
+
+ return Detail::createZones(
+ std::make_tuple(ZoneIds...),
+ app,
+ settings.heating.zones,
+ std::make_index_sequence()
+ );
+ }
+}
+
+FurnaceController::FurnaceController(
+ CoreApplication& application,
+ const ApplicationConfig& appConfig,
+ Settings& settings,
+ UI::Model& uiModel
+)
+ : _appConfig{ appConfig }
+ , _app{ application }
+ , _settings{ settings }
+ , _uiModel{ uiModel }
+ , _zones{ createZones<0, 1, 2, 3, 10, 11>(_app, _settings) }
+ , _temperatureSensor{ _settings }
, _topicPrefix{
HomeAssistant::makeUniqueId()
}
@@ -63,22 +106,21 @@ FurnaceController::FurnaceController(const ApplicationConfig& appConfig)
_app.mqttClient()
}
{
- if (!_settings.load()) {
- _log.warning_P("failed to load settings, restoring defaults");
- }
+ static_assert(MQTT_MAX_PACKET_SIZE >= 2048, "MQTT packet size too low");
+
+ _log.debug_P(PSTR("stack memory usage: %u B"), sizeof(FurnaceController));
setupRelayOutput();
setupMqttComponentConfigs();
setupMqttChangeHandlers();
updateMqtt();
+
+ _uiModel.firmwareVersion = _appConfig.firmwareVersion;
+ _uiModel.baseVersion = _appConfig.applicationVersion;
}
-void FurnaceController::task()
+void FurnaceController::task(const uint32_t deltaMillis)
{
- const uint32_t currentMillis = millis();
- const uint32_t deltaMillis = currentMillis - _lastTaskMillis;
- _lastTaskMillis = currentMillis;
-
_app.task();
_mqttUpdateTimer += deltaMillis;
@@ -89,10 +131,22 @@ void FurnaceController::task()
bool callingForHeating{ false };
+ _clockUpdateTimer += deltaMillis;
+ if (_clockUpdateTimer >= 1000) {
+ _clockUpdateTimer = 0;
+
+ const time_t localTime{ _app.systemClock().localTime() };
+ const auto* t{ gmtime(&localTime) };
+
+ for (auto& zone : _zones) {
+ zone.controller().updateDateTime(t->tm_wday, t->tm_hour, t->tm_min);
+ }
+ }
+
for (auto& zone : _zones) {
zone.task(deltaMillis);
- if (_settings.value().masterEnable) {
+ if (_settings.system.masterEnable) {
if (zone.callingForHeating()) {
callingForHeating = true;
}
@@ -101,13 +155,17 @@ void FurnaceController::task()
_callingForHeatingState = callingForHeating ? 1 : 0;
- if (_settings.value().energyOptimizerEnabled) {
+ if (_settings.system.energyOptimizerEnabled) {
for (auto& zone : _zones) {
zone.handleFurnaceHeatingChanged(callingForHeating);
}
}
setRelayOutputActive(callingForHeating);
+
+ _temperatureSensor.task();
+
+ updateUiModel();
}
void FurnaceController::setupRelayOutput() const
@@ -214,22 +272,68 @@ void FurnaceController::setupMqttChangeHandlers()
_masterSwitch.setChangedHandler(
[this](const auto value) {
_log.debug_P(PSTR("masterSwitch=%d"), value);
- _settings.value().masterEnable = value != 0;
- _settings.save();
+ _settings.system.masterEnable = value != 0;
}
);
_energyOptimizerSwitch.setChangedHandler(
[this](const auto value) {
_log.debug_P(PSTR("energyOptimizerEnabled=%d"), value);
- _settings.value().energyOptimizerEnabled = value != 0;
- _settings.save();
+ _settings.system.energyOptimizerEnabled = value != 0;
}
);
}
void FurnaceController::updateMqtt()
{
- _masterSwitch = _settings.value().masterEnable;
- _energyOptimizerSwitch = _settings.value().energyOptimizerEnabled;
+ _masterSwitch = _settings.system.masterEnable;
+ _energyOptimizerSwitch = _settings.system.energyOptimizerEnabled;
}
+
+void FurnaceController::updateUiModel()
+{
+ for (auto i = 0u; i < Config::ZoneCount; ++i) {
+ auto& zone = _zones[i];
+ auto& zoneModel = _uiModel.zones[i];
+
+ // TODO only for debugging
+ // zone.controller().inputTemperature(_temperatureSensor.read() / 10);
+
+ zoneModel.targetTemperature = zone.controller().targetTemperature();
+ zoneModel.currentTemperature = zone.controller().lastInputTemperature();
+ zoneModel.zoneNumber = zone.index();
+ zoneModel.status = [&] {
+ using Status = UI::Model::Zone::Status;
+ if (zone.controller().boostActive()) {
+ return Status::Boost;
+ }
+ if (zone.callingForHeating()) {
+ return Status::Heating;
+ }
+ switch (zone.controller().mode()) {
+ case HeatingZoneController::Mode::Off:
+ return Status::Off;
+ case HeatingZoneController::Mode::Auto:
+ break;
+ case HeatingZoneController::Mode::Holiday:
+ return Status::Holiday;
+ }
+ if (zone.controller().windowOpened()) {
+ return Status::WindowOpen;
+ }
+ if (zone.controller().openWindowLockoutActive()) {
+ return Status::WindowLockout;
+ }
+ return Status::Idle;
+ }();
+ }
+
+ _uiModel.heating = static_cast(_callingForHeatingState) == 1;
+ _uiModel.internalTemperature = _temperatureSensor.read();
+
+ _uiModel.wifiConnected = _app.isWifiConnected();
+ _uiModel.mqttConnected = _app.mqttClient().isConnected();
+
+ _uiModel.masterEnable = _settings.system.masterEnable;
+ _uiModel.energyOptimizerEnabled = _settings.system.energyOptimizerEnabled;
+}
\ No newline at end of file
diff --git a/src/FurnaceController.h b/src/FurnaceController.h
index b414656..6b3660a 100644
--- a/src/FurnaceController.h
+++ b/src/FurnaceController.h
@@ -1,10 +1,12 @@
#pragma once
+#include "Config.h"
#include "HeatingZone.h"
+#include "Settings.h"
+#include "TemperatureSensor.h"
#include
#include
-#include
#include
@@ -13,31 +15,33 @@
class ApplicationConfig;
+namespace UI
+{
+ struct Model;
+}
+
class FurnaceController
{
public:
- static constexpr auto ZoneCount = 6;
-
- explicit FurnaceController(const ApplicationConfig& appConfig);
+ explicit FurnaceController(
+ CoreApplication& application,
+ const ApplicationConfig& appConfig,
+ Settings& settings,
+ UI::Model& uiModel
+ );
- void task();
+ void task(uint32_t deltaMillis);
private:
const ApplicationConfig& _appConfig;
- CoreApplication _app;
-
- struct Settings
- {
- bool masterEnable{ false };
- bool energyOptimizerEnabled{ true };
- };
-
- Setting _settings;
-
+ CoreApplication& _app;
+ Settings& _settings;
+ UI::Model& _uiModel;
Logger _log{ "FurnaceController" };
- std::array _zones;
- uint32_t _lastTaskMillis{};
+ std::array _zones;
+ TemperatureSensor _temperatureSensor;
uint32_t _mqttUpdateTimer{};
+ uint32_t _clockUpdateTimer{};
bool _relayOutputActive{ false };
std::string _topicPrefix;
@@ -51,4 +55,6 @@ class FurnaceController
void setupMqttComponentConfigs();
void setupMqttChangeHandlers();
void updateMqtt();
+
+ void updateUiModel();
};
\ No newline at end of file
diff --git a/src/HeatingZone.cpp b/src/HeatingZone.cpp
index d50976d..9026b08 100644
--- a/src/HeatingZone.cpp
+++ b/src/HeatingZone.cpp
@@ -1,8 +1,8 @@
#include "HeatingZone.h"
+#include "Config.h"
#include "Extras.h"
#include "HomeAssistant.h"
-#include "PrivateConfig.h"
#include
@@ -90,13 +90,14 @@ namespace Topics::BoostActive
HeatingZone::HeatingZone(
const unsigned index,
- CoreApplication& app
+ CoreApplication& app,
+ const SettingDependencies& settingDependencies
)
: _index{ index }
, _app{ app }
, _log{ appendIndex(Extras::fromPstr(PSTR("HeatingZone")), _index) }
- , _stateSetting{ _app.settings().registerSetting() }
- , _controller{ _controllerConfig, _controllerSchedule }
+ , _state{ settingDependencies.state }
+ , _controller{ settingDependencies.configuration, settingDependencies.schedule }
, _topicPrefix{
HA::makeUniqueId()
+ appendIndex(Extras::fromPstr(PSTR("/zone")), _index)
@@ -134,26 +135,15 @@ HeatingZone::HeatingZone(
setupMqttComponentConfigs();
setupMqttChangeHandlers();
- if (_stateSetting.load()) {
- _log.info_P(PSTR("controller state loaded"));
+ _controller.loadState(_state);
+ _lastState = _state;
- _log.debug_P(
- PSTR("mode=%u, high=%d, low=%d"),
- _stateSetting.value().mode,
- _stateSetting.value().highTargetTemperature,
- _stateSetting.value().lowTargetTemperature
- );
-
- _controller.loadState(_stateSetting.value());
- } else {
- _log.warning_P(PSTR("failed to load controller state, resetting to default"));
-
- _controller.loadState(HeatingZoneController::State{});
-
- if (!_stateSetting.save()) {
- _log.warning_P(PSTR("failed to save the default state"));
- }
- }
+ _log.debug_P(
+ PSTR("mode=%u, high=%d, low=%d"),
+ _state.mode,
+ _state.highTargetTemperature,
+ _state.lowTargetTemperature
+ );
updateMqtt();
}
@@ -170,12 +160,14 @@ void HeatingZone::task(const uint32_t systemClockDeltaMs)
if (_controller.stateChanged()) {
_log.debug_P(PSTR("controller state changed, saving"));
+ _state = _controller.saveState();
+ _lastState = _state;
+ }
- _stateSetting.value() = _controller.saveState();
-
- if (!_stateSetting.save()) {
- _log.warning_P(PSTR("failed to save controller state"));
- }
+ if (_state != _lastState) {
+ _log.debug_P(PSTR("controller state changed externally, applying"));
+ _controller.loadState(_state);
+ _lastState = _state;
}
}
@@ -184,17 +176,6 @@ bool HeatingZone::callingForHeating()
return _controller.callingForHeating();
}
-void HeatingZone::loadDefaultSettings()
-{
- _log.debug_P(PSTR("%s"), __func__);
-
- _controllerConfig = {};
- _controllerSchedule = {};
- // _controllerState = HeatingZoneController::State{};
-
- // _controller.loadState(_controllerState);
-}
-
void HeatingZone::handleFurnaceHeatingChanged(const bool heating)
{
_controller.handleFurnaceHeatingChanged(heating);
diff --git a/src/HeatingZone.h b/src/HeatingZone.h
index b14bc69..59184e3 100644
--- a/src/HeatingZone.h
+++ b/src/HeatingZone.h
@@ -2,7 +2,6 @@
#include "network/MQTT/MqttVariable.h"
-#include
#include
#include
@@ -11,26 +10,40 @@ class CoreApplication;
class HeatingZone
{
public:
+ struct SettingDependencies {
+ HeatingZoneController::State& state;
+ HeatingZoneController::Configuration& configuration;
+ HeatingZoneController::Schedule& schedule;
+ };
+
explicit HeatingZone(
unsigned index,
- CoreApplication& app
+ CoreApplication& app,
+ const SettingDependencies& settingDependencies
);
void task(uint32_t systemClockDeltaMs);
[[nodiscard]] bool callingForHeating();
- void loadDefaultSettings();
-
void handleFurnaceHeatingChanged(bool heating);
+ [[nodiscard]] HeatingZoneController& controller()
+ {
+ return _controller;
+ }
+
+ [[nodiscard]] unsigned index() const
+ {
+ return _index;
+ }
+
private:
unsigned _index{};
CoreApplication& _app;
Logger _log;
- HeatingZoneController::Configuration _controllerConfig;
- HeatingZoneController::Schedule _controllerSchedule;
- Setting _stateSetting;
+ HeatingZoneController::State& _state;
+ HeatingZoneController::State _lastState;
HeatingZoneController _controller;
const std::string _topicPrefix;
diff --git a/src/Settings.cpp b/src/Settings.cpp
index fcf9e08..e6c3b23 100644
--- a/src/Settings.cpp
+++ b/src/Settings.cpp
@@ -21,8 +21,125 @@
#include "Settings.h"
#include
+#include
#include
+namespace Layout
+{
+ constexpr auto BaseAddress{ ISettingsHandler::ReservedAreaSize };
+
+ namespace Reserved
+ {
+ constexpr auto BaseAddress{ Layout::BaseAddress };
+ constexpr auto AddressStep{ 16u };
+ }
+
+ namespace System
+ {
+ constexpr auto BaseAddress{ Reserved::BaseAddress + Reserved::AddressStep };
+ constexpr auto AddressStep{ 32u };
+
+ static_assert(BaseAddress >= Layout::BaseAddress);
+ static_assert(AddressStep > sizeof(Settings::System));
+ }
+
+ namespace Heating
+ {
+ constexpr auto BaseAddress{ System::BaseAddress + System::AddressStep };
+ constexpr auto AddressStep{ 256u };
+ constexpr auto NextBaseAddress{ BaseAddress + AddressStep * Config::ZoneCount };
+
+ static_assert(BaseAddress >= System::BaseAddress + System::AddressStep);
+ static_assert(AddressStep > sizeof(Settings::Heating::ZoneControllerSettings));
+
+ namespace Configuration
+ {
+ constexpr auto AddressStep{ 64u };
+ static_assert(AddressStep > sizeof(HeatingZoneController::Configuration));
+ }
+
+ namespace Schedule
+ {
+ constexpr auto AddressStep{ 64u };
+ static_assert(AddressStep > sizeof(HeatingZoneController::Schedule));
+ }
+
+ namespace State
+ {
+ constexpr auto AddressStep{ 64u };
+ static_assert(AddressStep > sizeof(HeatingZoneController::State));
+ }
+
+ static_assert(
+ AddressStep > (
+ Configuration::AddressStep
+ + Schedule::AddressStep
+ + State::AddressStep
+ )
+ );
+ }
+}
+
+/**
+ * @brief Versions can be used determine which version of the settings data structure can be found in the EEPROM
+ * Use the provided function to create a valid version number.
+ */
+namespace Versioning
+{
+ constexpr uint32_t Magic = 0b10100011;
+
+ constexpr uint32_t makeVersion(uint8_t major, uint8_t minor, uint8_t patch)
+ {
+ return Magic
+ | (static_cast(major) << 24)
+ | (static_cast(minor) << 16)
+ | (static_cast(patch) << 8);
+ }
+
+ [[nodiscard]] constexpr bool isValidVersion(const uint32_t version)
+ {
+ return (version & 0xFF) == Magic;
+ }
+
+ [[nodiscard]] constexpr auto getVersionParts(const uint32_t version)
+ {
+ return std::tuple(
+ version >> 24,
+ version >> 16,
+ version >> 8
+ );
+ }
+
+ constexpr auto CurrentVersion = makeVersion(1, 0, 0);
+}
+
+namespace
+{
+ template
+ [[nodiscard]] bool registerSetting(
+ ISettingsHandler& handler,
+ T& setting,
+ const std::size_t address,
+ Logger& log,
+ const char* name
+ )
+ {
+ auto ok = handler.registerSetting(setting, address);
+
+ log.log_P(
+ ok ? Log::Severity::Debug : Log::Severity::Error,
+ PSTR("setting registration '%s': %s ref=%p, address=%u, size=%u"),
+ name,
+ ok ? "succeeded" : "FAILED",
+ &setting,
+ address,
+ sizeof(T)
+ );
+
+ return ok;
+ }
+}
+
Settings::Settings(ISettingsHandler& handler)
: _handler(handler)
{
@@ -31,9 +148,19 @@ Settings::Settings(ISettingsHandler& handler)
loadDefaults();
});
- _handler.registerSetting(data);
+ if (!registerSetting(_handler, _settingsDataVersion, Layout::Reserved::BaseAddress, _log, "Reserved")) {
+ abort();
+ }
+
+ if (!registerSetting(_handler, system, Layout::System::BaseAddress, _log, "System")) {
+ abort();
+ }
+
+ registerHeatingSettings();
load();
+
+ checkVersion();
}
bool Settings::load()
@@ -54,14 +181,13 @@ bool Settings::load()
bool Settings::save()
{
- if (!check()) {
- _log.warning_P(PSTR("settings corrected before saving"));
- }
+ // if (!check()) {
+ // _log.warning_P(PSTR("settings corrected before saving"));
+ // }
dumpData();
const auto ok = _handler.save() != ISettingsHandler::SaveResult::Error;
-
_log.info_P(PSTR("saving settings: ok=%d"), ok);
return ok;
@@ -71,11 +197,56 @@ void Settings::loadDefaults()
{
_log.info_P(PSTR("loading defaults"));
- data = {};
+ _settingsDataVersion = Versioning::CurrentVersion;
- if (!check()) {
- _log.warning_P(PSTR("loaded defaults corrected"));
+ system = System{};
+ heating = Heating{};
+
+ const auto ok = _handler.save(true) != ISettingsHandler::SaveResult::Error;
+ _log.info_P(PSTR("saving default settings: ok=%d"), ok);
+
+ // if (!check()) {
+ // _log.warning_P(PSTR("loaded defaults corrected"));
+ // }
+}
+
+void Settings::checkVersion()
+{
+ // How to add migration code for new versions:
+ // 1. Add the previous `CurrentVersion` as a separate constant into the `Versioning` namespace
+ // 2. Add the previous version constant to `KnownVersions`
+ // 3. Set `CurrentVersion` to the actual version
+ // 4. Add the version-dependent migration logic to the end of this function
+
+ static constexpr std::array KnownVersions{
+ Versioning::CurrentVersion
+ };
+
+ if (!Versioning::isValidVersion(_settingsDataVersion)) {
+ _log.warning_P(PSTR("settings data version is invalid, loading defaults"));
+ loadDefaults();
+ return;
+ }
+
+ const auto [major, minor, patch] = Versioning::getVersionParts(_settingsDataVersion);
+ _log.info_P(PSTR("settings data version: %u.%u.%u"), major, minor, patch);
+
+ if (
+ !std::ranges::any_of(
+ KnownVersions,
+ [&](const auto version) {
+ return version == _settingsDataVersion;
+ }
+ )
+ ) {
+ _log.warning_P(PSTR("settings data version is unknown, loading defaults"));
+ loadDefaults();
+ return;
}
+
+ _log.info_P(PSTR("settings data version OK"));
+
+ // Add version-dependent checks here
}
bool Settings::check()
@@ -136,8 +307,8 @@ bool Settings::check()
// // for backlight level thus we cannot decide if it's corrupted or not.
// // At last, save the corrected values.
// if (modified) {
- // data.Display.Brightness = DefaultSettings::Display::Brightness;
- // data.Display.TimeoutSecs = DefaultSettings::Display::TimeoutSecs;
+ // data.Display.brightness = DefaultSettings::Display::Brightness;
+ // data.Display.timeoutSecs = DefaultSettings::Display::timeoutSecs;
// }
return !modified;
@@ -145,39 +316,124 @@ bool Settings::check()
void Settings::dumpData() const
{
- // _log.debug("Display{ Brightness=%u, TimeoutSecs=%u }",
- // data.Display.Brightness,
- // data.Display.TimeoutSecs
- // );
-
- // _log.debug("HeatingController{ Mode=%u, DaytimeTemp=%d, NightTimeTemp=%d, TargetTemp=%d, TargetTempSetTimestamp=%ld, Overshoot=%u, Undershoot=%u, TempCorrection=%d, BoostIntervalMins=%u, CustomTempTimeputMins=%u }",
- // data.HeatingController.Mode,
- // data.HeatingController.DaytimeTemp,
- // data.HeatingController.NightTimeTemp,
- // data.HeatingController.TargetTemp,
- // data.HeatingController.TargetTempSetTimestamp,
- // data.HeatingController.Overshoot,
- // data.HeatingController.Undershoot,
- // data.HeatingController.TempCorrection,
- // data.HeatingController.BoostIntervalMins,
- // data.HeatingController.CustomTempTimeoutMins
- // );
-
- // std::stringstream schDays;
- // for (auto i = 0; i < 7; ++i) {
- // schDays << std::to_string(i) << "=";
- // schDays << std::hex << std::setw(2) << std::setfill('0');
- // for (auto j = 0; j < 6; ++j) {
- // schDays << static_cast(data.Scheduler.DayData[i][j]);
- // }
- // schDays << std::resetiosflags << 'h';
- // if (i < 6) {
- // schDays << ", ";
- // }
- // }
+ _log.info_P(
+ PSTR("Reserved{ version=0x%08lX }"),
+ _settingsDataVersion
+ );
+
+ _log.info_P(
+ PSTR("System{ maximumLogLevel=%u, masterEnable=%u, energyOptimizerEnabled=%u }"),
+ system.maximumLogLevel,
+ system.masterEnable,
+ system.energyOptimizerEnabled
+ );
+
+ _log.info_P(
+ PSTR("System.Display{ brightness=%u, timeoutSecs=%u }"),
+ system.display.brightness,
+ system.display.timeoutSecs
+ );
+
+ auto i = 0u;
+ for (const auto& zone : heating.zones) {
+ _log.info(
+ "System.Heating.Zones[%u].Configuration{ overrideTimeoutSeconds=%u, boostInitialDurationSeconds=%u, boostExtensionDurationSeconds=%u, heatingStartDelaySeconds=%u, heatingOvershoot=%u, heatingUndershoot=%u, holidayModeTemperature=%u, openWindowLockoutDurationSeconds=%u }",
+ i,
+ zone.config.overrideTimeoutSeconds,
+ zone.config.boostInitialDurationSeconds,
+ zone.config.boostExtensionDurationSeconds,
+ zone.config.heatingStartDelaySeconds,
+ zone.config.heatingOvershoot,
+ zone.config.heatingUndershoot,
+ zone.config.holidayModeTemperature,
+ zone.config.openWindowLockoutDurationSeconds
+ );
+
+ _log.info(
+ "System.Heating.Zones[%u].State{ mode=%u, highTargetTemperature=%u, lowTargetTemperature=%u }",
+ i,
+ zone.state.mode,
+ zone.state.highTargetTemperature,
+ zone.state.lowTargetTemperature
+ );
+
+ static_assert(
+ std::is_same_v>,
+ "Dumping code must be adjusted to the Schedule type"
+ );
+
+ for (auto day = 0u; day < 7; ++day) {
+ char bits[49]{}; // 48 + \0
+
+ const auto dayOffset = day * 6u;
+ for (auto byteIdx = 0u; byteIdx < 6u; ++byteIdx) {
+ const auto b = zone.schedule[dayOffset + byteIdx];
+ for (auto bitIdx = 0; bitIdx < 8; ++bitIdx) {
+ bits[byteIdx * 8 + bitIdx] = (b & (1 << (7 - bitIdx)) ? '1' : '0');
+ }
+ }
+
+ _log.info(
+ "System.Heating.Zones[%u].Schedule[%u]{ %s }",
+ i,
+ day,
+ bits
+ );
+ }
+
+ ++i;
+ }
+}
- // _log.debug("Scheduler{ Enabled=%u, Days=[ %s ] }",
- // data.Scheduler.Enabled,
- // schDays.str().c_str()
- // );
+void Settings::registerHeatingSettings()
+{
+ auto nextBaseAddress{ Layout::Heating::BaseAddress };
+
+ auto i = 0;
+ for (auto& s : heating.zones) {
+ _log.debug_P(
+ PSTR("registering heating settings, index=%d, baseAddress=%u"),
+ i++,
+ nextBaseAddress
+ );
+
+ if (
+ !registerSetting(
+ _handler,
+ s.config,
+ nextBaseAddress,
+ _log,
+ "ZoneControllerSettings::Configuration"
+ )
+ ) {
+ abort();
+ }
+ if (
+ !registerSetting(
+ _handler,
+ s.schedule,
+ nextBaseAddress + Layout::Heating::Configuration::AddressStep,
+ _log,
+ "ZoneControllerSettings::Schedule"
+ )
+ ) {
+ abort();
+ }
+
+ if (
+ !registerSetting(
+ _handler,
+ s.state,
+ nextBaseAddress
+ + Layout::Heating::Configuration::AddressStep
+ + Layout::Heating::Schedule::AddressStep,
+ _log,
+ "ZoneControllerSettings::State"
+ )
+ ) {
+ abort();
+ }
+
+ nextBaseAddress += Layout::Heating::AddressStep;
+ }
}
\ No newline at end of file
diff --git a/src/Settings.h b/src/Settings.h
index 310035f..6173b61 100644
--- a/src/Settings.h
+++ b/src/Settings.h
@@ -20,6 +20,8 @@
#pragma once
+#include "Config.h"
+
#include
#include
#include
@@ -61,7 +63,7 @@ namespace DefaultSettings
{
namespace Display
{
- constexpr auto Brightness = 20;
+ constexpr auto Brightness = 10;
constexpr auto TimeoutSecs = 15;
}
}
@@ -69,36 +71,46 @@ namespace DefaultSettings
class Settings
{
public:
- static constexpr uint8_t DataVersion = 1;
-
explicit Settings(ISettingsHandler& handler);
- DECLARE_SETTINGS_STRUCT(DisplaySettings)
- {
- uint8_t Brightness = DefaultSettings::Display::Brightness;
- uint8_t TimeoutSecs = DefaultSettings::Display::TimeoutSecs;
- };
+ /*
+ * Be cautious when modifying these structures to avoid breaking data layout
+ * in existing devices. New fields should be added to the end of these structures.
+ * When data should be copied over from an existing field into a new one,
+ * or a new field should be initialized with a specific value,
+ * be sure to do a settings data version check and do the migration with the help
+ * of that.
+ */
- DECLARE_SETTINGS_STRUCT(HeatingZoneSettings)
+ struct Heating
{
- HeatingZoneController::Configuration config{};
- HeatingZoneController::Schedule schedule{};
- HeatingZoneController::State state{};
- };
+ struct ZoneControllerSettings
+ {
+ HeatingZoneController::Configuration config{};
+ HeatingZoneController::Schedule schedule{};
+ HeatingZoneController::State state{};
+ };
- DECLARE_SETTINGS_STRUCT(SystemSettings)
- {
- HeatingZoneController::DeciDegrees internalSensorOffset{ 0 };
- };
+ std::array zones{{}};
+ } heating;
- DECLARE_SETTINGS_STRUCT(Data)
+ DECLARE_SETTINGS_STRUCT(System)
{
- DisplaySettings display;
- SystemSettings system;
- std::array heatingZones;
- };
+ DECLARE_SETTINGS_STRUCT(Display)
+ {
+ uint8_t brightness = DefaultSettings::Display::Brightness;
+ uint8_t timeoutSecs = DefaultSettings::Display::TimeoutSecs;
+ };
+
+ uint8_t maximumLogLevel{ static_cast(Log::Severity::Info) };
- Data data;
+ bool masterEnable{ false };
+ bool energyOptimizerEnabled{ true };
+
+ HeatingZoneController::DeciDegrees internalSensorOffset{ 0 };
+
+ Display display;
+ } system;
bool load();
bool save();
@@ -109,7 +121,12 @@ class Settings
Logger _log{ "Settings" };
ISettingsHandler& _handler;
+ uint32_t _settingsDataVersion{};
+
+ void checkVersion();
bool check();
void dumpData() const;
+
+ void registerHeatingSettings();
};
diff --git a/src/TemperatureSensor.cpp b/src/TemperatureSensor.cpp
index 07726e6..f5d7117 100644
--- a/src/TemperatureSensor.cpp
+++ b/src/TemperatureSensor.cpp
@@ -39,7 +39,7 @@ void TemperatureSensor::task()
int16_t TemperatureSensor::read() const
{
auto t = Peripherals::Sensors::MainTemperature::lastReading()
- + _settings.data.system.internalSensorOffset * 10;
+ + _settings.system.internalSensorOffset * 10;
return std::min(9999, std::max(-9999, t));
}
\ No newline at end of file
diff --git a/src/display/Text.cpp b/src/display/Text.cpp
deleted file mode 100644
index cb32db9..0000000
--- a/src/display/Text.cpp
+++ /dev/null
@@ -1,1426 +0,0 @@
-/*
- This file is part of esp-thermostat.
-
- esp-thermostat is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- esp-thermostat is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with esp-thermostat. If not, see .
-
- Author: Tamas Karpati
- Created on 2020-01-22
-*/
-
-#include "Display.h"
-#include "Text.h"
-
-#include
-
-constexpr auto DefaultCharWidth = 5;
-constexpr auto DefaultSpaceWidth = 1;
-
-static const uint8_t DefaultCharset[][DefaultCharWidth] = {
- // [space]
- {
- 0, 0, 0, 0, 0
- },
- // !
- {
- 0b00000000,
- 0b00000000,
- 0b01001111,
- 0b00000000,
- 0b00000000
- },
- // "
- {
- 0b00000000,
- 0b00000111,
- 0b00000000,
- 0b00000111,
- 0b00000000
- },
- // #
- {
- 0b00010100,
- 0b01111111,
- 0b00010100,
- 0b01111111,
- 0b00010100
- },
- // $
- {
- 0b00100100,
- 0b00101010,
- 0b01111111,
- 0b00101010,
- 0b00010010
- },
- // %
- {
- 0b00100011,
- 0b00010011,
- 0b00001000,
- 0b01100100,
- 0b01100010
- },
- // &
- {
- 0b00110110,
- 0b01001001,
- 0b01010101,
- 0b00100010,
- 0b01010000
- },
- // '
- {
- 0b00000000,
- 0b00000101,
- 0b00000011,
- 0b00000000,
- 0b00000000
- },
- // (
- {
- 0b00000000,
- 0b00011100,
- 0b00100010,
- 0b01000001,
- 0b00000000
- },
- // )
- {
- 0b00000000,
- 0b01000001,
- 0b00100010,
- 0b00011100,
- 0b00000000
- },
- // *
- {
- 0b00010100,
- 0b00001000,
- 0b00111110,
- 0b00001000,
- 0b00010100
- },
- // +
- {
- 0b00001000,
- 0b00001000,
- 0b00111110,
- 0b00001000,
- 0b00001000,
- },
- // ,
- {
- 0b00000000,
- 0b01010000,
- 0b00110000,
- 0b00000000,
- 0b00000000
- },
- // -
- {
- 0b00001000,
- 0b00001000,
- 0b00001000,
- 0b00001000,
- 0b00001000
- },
- // .
- {
- 0b00000000,
- 0b01100000,
- 0b01100000,
- 0b00000000,
- 0b00000000
- },
- // /
- {
- 0b00100000,
- 0b00010000,
- 0b00001000,
- 0b00000100,
- 0b00000010
- },
- // 0
- {
- 0b00111110,
- 0b01010001,
- 0b01001001,
- 0b01000101,
- 0b00111110
- },
- // 1
- {
- 0b00000000,
- 0b01000010,
- 0b01111111,
- 0b01000000,
- 0b00000000
- },
- // 2
- {
- 0b01000010,
- 0b01100001,
- 0b01010001,
- 0b01001001,
- 0b01000110
- },
- // 3
- {
- 0b00100001,
- 0b01000001,
- 0b01000101,
- 0b01001011,
- 0b00110001
- },
- // 4
- {
- 0b00011000,
- 0b00010100,
- 0b00010010,
- 0b01111111,
- 0b00010000
- },
- // 5
- {
- 0b00100111,
- 0b01000101,
- 0b01000101,
- 0b01000101,
- 0b00111001
- },
- // 6
- {
- 0b00111100,
- 0b01001010,
- 0b01001001,
- 0b01001001,
- 0b00110000
- },
- // 7
- {
- 0b00000001,
- 0b01110001,
- 0b00001001,
- 0b00000101,
- 0b00000011
- },
- // 8
- {
- 0b00110110,
- 0b01001001,
- 0b01001001,
- 0b01001001,
- 0b00110110
- },
- // 9
- {
- 0b00000110,
- 0b01001001,
- 0b01001001,
- 0b00101001,
- 0b00011110
- },
- // :
- {
- 0b00000000,
- 0b00110110,
- 0b00110110,
- 0b00000000,
- 0b00000000
- },
- // ;
- {
- 0b00000000,
- 0b01010110,
- 0b00110110,
- 0b00000000,
- 0b00000000
- },
- // <
- {
- 0b00001000,
- 0b00010100,
- 0b00100010,
- 0b01000001,
- 0b00000000
- },
- // =
- {
- 0b00010100,
- 0b00010100,
- 0b00010100,
- 0b00010100,
- 0b00010100
- },
- // >
- {
- 0b00000000,
- 0b01000001,
- 0b00100010,
- 0b00010100,
- 0b00001000
- },
- // ?
- {
- 0b00000010,
- 0b00000001,
- 0b01010001,
- 0b00001001,
- 0b00000110
- },
- // @
- {
- 0b00110010,
- 0b01001001,
- 0b01111001,
- 0b01000001,
- 0b00111110
- },
- // A
- {
- 0b01111110,
- 0b00010001,
- 0b00010001,
- 0b00010001,
- 0b01111110
- },
- // B
- {
- 0b01111111,
- 0b01001001,
- 0b01001001,
- 0b01001001,
- 0b00110110
- },
- // C
- {
- 0b00111110,
- 0b01000001,
- 0b01000001,
- 0b01000001,
- 0b00100010
- },
- // D
- {
- 0b01111111,
- 0b01000001,
- 0b01000001,
- 0b00100010,
- 0b00011100
- },
- // E
- {
- 0b01111111,
- 0b01001001,
- 0b01001001,
- 0b01001001,
- 0b01000001
- },
- // F
- {
- 0b01111111,
- 0b00001001,
- 0b00001001,
- 0b00001001,
- 0b00000001
- },
- // G
- {
- 0b00111110,
- 0b01000001,
- 0b01001001,
- 0b01001001,
- 0b01111010
- },
- // H
- {
- 0b01111111,
- 0b00001000,
- 0b00001000,
- 0b00001000,
- 0b01111111
- },
- // I
- {
- 0b00000000,
- 0b01000001,
- 0b01111111,
- 0b01000001,
- 0b00000000
- },
- // J
- {
- 0b00100000,
- 0b01000000,
- 0b01000001,
- 0b00111111,
- 0b00000001
- },
- // K
- {
- 0b01111111,
- 0b00001000,
- 0b00010100,
- 0b00100010,
- 0b01000001
- },
- // L
- {
- 0b01111111,
- 0b01000000,
- 0b01000000,
- 0b01000000,
- 0b01000000
- },
- // M
- {
- 0b01111111,
- 0b00000010,
- 0b00001100,
- 0b00000010,
- 0b01111111
- },
- // N
- {
- 0b01111111,
- 0b00000100,
- 0b00001000,
- 0b00010000,
- 0b01111111
- },
- // O
- {
- 0b00111110,
- 0b01000001,
- 0b01000001,
- 0b01000001,
- 0b00111110
- },
- // P
- {
- 0b01111111,
- 0b00001001,
- 0b00001001,
- 0b00001001,
- 0b00000110
- },
- // Q
- {
- 0b00111110,
- 0b01000001,
- 0b01010001,
- 0b00100001,
- 0b01011110
- },
- // R
- {
- 0b01111111,
- 0b00001001,
- 0b00011001,
- 0b00101001,
- 0b01000110
- },
- // S
- {
- 0b01000110,
- 0b01001001,
- 0b01001001,
- 0b01001001,
- 0b00110001
- },
- // T
- {
- 0b00000001,
- 0b00000001,
- 0b01111111,
- 0b00000001,
- 0b00000001
- },
- // U
- {
- 0b00111111,
- 0b01000000,
- 0b01000000,
- 0b01000000,
- 0b00111111
- },
- // V
- {
- 0b00011111,
- 0b00100000,
- 0b01000000,
- 0b00100000,
- 0b00011111
- },
- // W
- {
- 0b00111111,
- 0b01000000,
- 0b00111000,
- 0b01000000,
- 0b00111111
- },
- // X
- {
- 0b01100011,
- 0b00010100,
- 0b00001000,
- 0b00010100,
- 0b01100011
- },
- // Y
- {
- 0b00000111,
- 0b00001000,
- 0b01110000,
- 0b00001000,
- 0b00000111
- },
- // Z
- {
- 0b01100001,
- 0b01010001,
- 0b01001001,
- 0b01000101,
- 0b01000011
- },
- // [
- {
- 0b00000000,
- 0b01111111,
- 0b01000001,
- 0b01000001,
- 0b00000000
- },
- // backslash
- {
- 0b00000010,
- 0b00000100,
- 0b00001000,
- 0b00010000,
- 0b00100000
- },
- // ]
- {
- 0b00000000,
- 0b01000001,
- 0b01000001,
- 0b01111111,
- 0b00000000
- },
- // ^
- {
- 0b00000100,
- 0b00000010,
- 0b00000001,
- 0b00000010,
- 0b00000100
- },
- // _
- {
- 0b01000000,
- 0b01000000,
- 0b01000000,
- 0b01000000,
- 0b01000000
- },
- // `
- {
- 0b00000000,
- 0b00000001,
- 0b00000010,
- 0b00000100,
- 0b00000000
- },
- // a
- {
- 0b00100000,
- 0b01010100,
- 0b01010100,
- 0b01010100,
- 0b01111000
- },
- // b
- {
- 0b01111111,
- 0b01001000,
- 0b01000100,
- 0b01000100,
- 0b00111000
- },
- // c
- {
- 0b00111000,
- 0b01000100,
- 0b01000100,
- 0b01000100,
- 0b00100000
- },
- // d
- {
- 0b00111000,
- 0b01000100,
- 0b01000100,
- 0b01001000,
- 0b01111111
- },
- // e
- {
- 0b00111000,
- 0b01010100,
- 0b01010100,
- 0b01010100,
- 0b00011000
- },
- // f
- {
- 0b00001000,
- 0b01111110,
- 0b00001001,
- 0b00000001,
- 0b00000010
- },
- // g
- {
- 0b00001100,
- 0b01010010,
- 0b01010010,
- 0b01010010,
- 0b00111110
- },
- // h
- {
- 0b01111111,
- 0b00001000,
- 0b00000100,
- 0b00000100,
- 0b01111000
- },
- // i
- {
- 0b00000000,
- 0b01000100,
- 0b01111101,
- 0b01000000,
- 0b00000000
- },
- // j
- {
- 0b00100000,
- 0b01000000,
- 0b01000000,
- 0b00111101,
- 0b00000000
- },
- // k
- {
- 0b01111111,
- 0b00010000,
- 0b00101000,
- 0b01000100,
- 0b00000000
- },
- // l
- {
- 0b00000000,
- 0b01000001,
- 0b01111111,
- 0b01000000,
- 0b00000000
- },
- // m
- {
- 0b01111100,
- 0b00000100,
- 0b00011000,
- 0b00000100,
- 0b01111000
- },
- // n
- {
- 0b01111100,
- 0b00001000,
- 0b00000100,
- 0b00000100,
- 0b01111000
- },
- // o
- {
- 0b00111000,
- 0b01000100,
- 0b01000100,
- 0b01000100,
- 0b00111000
- },
- // p
- {
- 0b01111100,
- 0b00010100,
- 0b00010100,
- 0b00010100,
- 0b00001000
- },
- // q
- {
- 0b00001000,
- 0b00010100,
- 0b00010100,
- 0b00011000,
- 0b01111100
- },
- // r
- {
- 0b01111100,
- 0b00001000,
- 0b00000100,
- 0b00000100,
- 0b00001000
- },
- // s
- {
- 0b01001000,
- 0b01010100,
- 0b01010100,
- 0b01010100,
- 0b00100000
- },
- // t
- {
- 0b00000100,
- 0b00111111,
- 0b01000100,
- 0b01000000,
- 0b00100000
- },
- // u
- {
- 0b00111100,
- 0b01000000,
- 0b01000000,
- 0b00100000,
- 0b01111100
- },
- // v
- {
- 0b00011100,
- 0b00100000,
- 0b01000000,
- 0b00100000,
- 0b00011100
- },
- // w
- {
- 0b00111100,
- 0b01000000,
- 0b00110000,
- 0b01000000,
- 0b00111100
- },
- // x
- {
- 0b01000100,
- 0b00101000,
- 0b00010000,
- 0b00101000,
- 0b01000100
- },
- // y
- {
- 0b00001100,
- 0b01010000,
- 0b01010000,
- 0b01010000,
- 0b00111100
- },
- // z
- {
- 0b01000100,
- 0b01100100,
- 0b01010100,
- 0b01001100,
- 0b01000100
- },
- // {
- {
- 0b00000000,
- 0b00001000,
- 0b00110110,
- 0b01000001,
- 0b00000000
- },
- // |
- {
- 0b00000000,
- 0b00000000,
- 0b01111111,
- 0b00000000,
- 0b00000000
- },
- // }
- {
- 0b00000000,
- 0b01000001,
- 0b00110110,
- 0b00001000,
- 0b00000000
- },
- // ~
- {
- 0b00001000,
- 0b00000100,
- 0b00001000,
- 0b00010000,
- 0b00001000
- },
-};
-
-static const uint8_t DefaultCharPlaceholder[DefaultCharWidth] = {
- 0b01111111,
- 0b01010101,
- 0b01001001,
- 0b01010101,
- 0b01111111
-};
-
-#define SevenSegCharWidth 12
-#define SevenSegCharLines 3
-static const uint8_t SevenSegCharset[11][SevenSegCharLines][SevenSegCharWidth] = {
- {
- // 0, Page 0
- {
- 0b11111110,
- 0b11111101,
- 0b11111011,
- 0b00000111,
- 0b00000111,
- 0b00000111,
- 0b00000111,
- 0b00000111,
- 0b00000111,
- 0b11111011,
- 0b11111101,
- 0b11111110
- },
- // 0, Page 1
- {
- 0b11111111,
- 0b11110111,
- 0b11100011,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b11100011,
- 0b11110111,
- 0b11111111
- },
- // 0, Page 2
- {
- 0b00111111,
- 0b01011111,
- 0b01101111,
- 0b01110000,
- 0b01110000,
- 0b01110000,
- 0b01110000,
- 0b01110000,
- 0b01110000,
- 0b01101111,
- 0b01011111,
- 0b00111111
- }
- },
- {
- // 1, Page 0
- {
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b11111000,
- 0b11111100,
- 0b11111110
- },
- // 1, Page 1
- {
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b11100011,
- 0b11110111,
- 0b11111111
- },
- // 1, Page 2
- {
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00001111,
- 0b00011111,
- 0b00111111
- }
- },
- {
- // 2, Page 0
- {
- 0b00000000,
- 0b00000001,
- 0b00000011,
- 0b00000111,
- 0b00000111,
- 0b00000111,
- 0b00000111,
- 0b00000111,
- 0b00000111,
- 0b11111011,
- 0b11111101,
- 0b11111110
- },
- // 2, Page 1
- {
- 0b11111000,
- 0b11110100,
- 0b11101100,
- 0b00011100,
- 0b00011100,
- 0b00011100,
- 0b00011100,
- 0b00011100,
- 0b00011100,
- 0b00011011,
- 0b00010111,
- 0b00001111
- },
- // 2, Page 2
- {
- 0b00111111,
- 0b01011111,
- 0b01101111,
- 0b01110000,
- 0b01110000,
- 0b01110000,
- 0b01110000,
- 0b01110000,
- 0b01110000,
- 0b01100000,
- 0b01000000,
- 0b00000000
- }
- },
- {
- // 3, Page 0
- {
- 0b00000000,
- 0b00000001,
- 0b00000011,
- 0b00000111,
- 0b00000111,
- 0b00000111,
- 0b00000111,
- 0b00000111,
- 0b00000111,
- 0b11111011,
- 0b11111101,
- 0b11111110
- },
- // 3, Page 1
- {
- 0b00000000,
- 0b00000000,
- 0b00001000,
- 0b00011100,
- 0b00011100,
- 0b00011100,
- 0b00011100,
- 0b00011100,
- 0b00011100,
- 0b11101011,
- 0b11110111,
- 0b11111111
- },
- // 3, Page 2
- {
- 0b00000000,
- 0b01000000,
- 0b01100000,
- 0b01110000,
- 0b01110000,
- 0b01110000,
- 0b01110000,
- 0b01110000,
- 0b01110000,
- 0b01101111,
- 0b01011111,
- 0b00111111
- }
- },
- {
- // 4, Page 0
- {
- 0b11111110,
- 0b11111100,
- 0b11111000,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b11111000,
- 0b11111100,
- 0b11111110
- },
- // 4, Page 1
- {
- 0b00001111,
- 0b00010111,
- 0b00011011,
- 0b00011100,
- 0b00011100,
- 0b00011100,
- 0b00011100,
- 0b00011100,
- 0b00011100,
- 0b11101011,
- 0b11110111,
- 0b11111111
- },
- // 4, Page 2
- {
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00001111,
- 0b00011111,
- 0b00111111
- }
- },
- {
- // 5, Page 0
- {
- 0b11111110,
- 0b11111101,
- 0b11111011,
- 0b00000111,
- 0b00000111,
- 0b00000111,
- 0b00000111,
- 0b00000111,
- 0b00000111,
- 0b00000011,
- 0b00000001,
- 0b00000000
- },
- // 5, Page 1
- {
- 0b00001111,
- 0b00010111,
- 0b00011011,
- 0b00011100,
- 0b00011100,
- 0b00011100,
- 0b00011100,
- 0b00011100,
- 0b00011100,
- 0b11101100,
- 0b11110100,
- 0b11111000
- },
- // 5, Page 2
- {
- 0b00000000,
- 0b01000000,
- 0b01100000,
- 0b01110000,
- 0b01110000,
- 0b01110000,
- 0b01110000,
- 0b01110000,
- 0b01110000,
- 0b01101111,
- 0b01011111,
- 0b00111111
- }
- },
- {
- // 6, Page 0
- {
- 0b11111110,
- 0b11111101,
- 0b11111011,
- 0b00000111,
- 0b00000111,
- 0b00000111,
- 0b00000111,
- 0b00000111,
- 0b00000111,
- 0b00000011,
- 0b00000001,
- 0b00000000
- },
- // 6, Page 1
- {
- 0b11111111,
- 0b11110111,
- 0b11101011,
- 0b00011100,
- 0b00011100,
- 0b00011100,
- 0b00011100,
- 0b00011100,
- 0b00011100,
- 0b11101100,
- 0b11110100,
- 0b11111000
- },
- // 6, Page 2
- {
- 0b00111111,
- 0b01011111,
- 0b01101111,
- 0b01110000,
- 0b01110000,
- 0b01110000,
- 0b01110000,
- 0b01110000,
- 0b01110000,
- 0b01101111,
- 0b01011111,
- 0b00111111
- }
- },
- {
- // 7, Page 0
- {
- 0b00000000,
- 0b00000001,
- 0b00000011,
- 0b00000111,
- 0b00000111,
- 0b00000111,
- 0b00000111,
- 0b00000111,
- 0b00000111,
- 0b11111011,
- 0b11111101,
- 0b11111110
- },
- // 7, Page 1
- {
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b11100011,
- 0b11110111,
- 0b11111111
- },
- // 7, Page 2
- {
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00001111,
- 0b00011111,
- 0b00111111
- }
- },
- {
- // 8, Page 0
- {
- 0b11111110,
- 0b11111101,
- 0b11111011,
- 0b00000111,
- 0b00000111,
- 0b00000111,
- 0b00000111,
- 0b00000111,
- 0b00000111,
- 0b11111011,
- 0b11111101,
- 0b11111110
- },
- // 8, Page 1
- {
- 0b11111111,
- 0b11110111,
- 0b11101011,
- 0b00011100,
- 0b00011100,
- 0b00011100,
- 0b00011100,
- 0b00011100,
- 0b00011100,
- 0b11101011,
- 0b11110111,
- 0b11111111
- },
- // 8, Page 2
- {
- 0b00111111,
- 0b01011111,
- 0b01101111,
- 0b01110000,
- 0b01110000,
- 0b01110000,
- 0b01110000,
- 0b01110000,
- 0b01110000,
- 0b01101111,
- 0b01011111,
- 0b00111111
- }
- },
- {
- // 9, Page 0
- {
- 0b11111110,
- 0b11111101,
- 0b11111011,
- 0b00000111,
- 0b00000111,
- 0b00000111,
- 0b00000111,
- 0b00000111,
- 0b00000111,
- 0b11111011,
- 0b11111101,
- 0b11111110
- },
- // 9, Page 1
- {
- 0b00001111,
- 0b00010111,
- 0b00011011,
- 0b00011100,
- 0b00011100,
- 0b00011100,
- 0b00011100,
- 0b00011100,
- 0b00011100,
- 0b11101011,
- 0b11110111,
- 0b11111111
- },
- // 9, Page 2
- {
- 0b00000000,
- 0b01000000,
- 0b01100000,
- 0b01110000,
- 0b01110000,
- 0b01110000,
- 0b01110000,
- 0b01110000,
- 0b01110000,
- 0b01101111,
- 0b01011111,
- 0b00111111
- }
- },
- {
- // C, Page 0
- {
- 0b11111110,
- 0b11111101,
- 0b11111011,
- 0b00000111,
- 0b00000111,
- 0b00000111,
- 0b00000111,
- 0b00000111,
- 0b00000111,
- 0b00000011,
- 0b00000001,
- 0b00000000
- },
- // C, Page 1
- {
- 0b11111111,
- 0b11110111,
- 0b11100011,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00000000
- },
- // C, Page 2
- {
- 0b00111111,
- 0b01011111,
- 0b01101111,
- 0b01110000,
- 0b01110000,
- 0b01110000,
- 0b01110000,
- 0b01110000,
- 0b01110000,
- 0b01100000,
- 0b01000000,
- 0b00000000
- }
- }
-};
-
-
-void drawChar(const char c, const uint8_t yOffset, const bool invert)
-{
- const uint8_t* charData;
-
- // If character is not supported, draw placeholder
- if (c >= sizeof(DefaultCharset)) {
- charData = DefaultCharPlaceholder;
- }
- else {
- // Get data for the next character
- charData = DefaultCharset[c - 32];
- }
-
- Display::sendData(charData, DefaultCharWidth, yOffset, invert);
-}
-
-void Text::draw(const char c, const uint8_t line, const uint8_t x, const uint8_t yOffset, const bool invert)
-{
- Display::setLine(line);
- Display::setColumn(x);
- drawChar(c, yOffset, invert);
-}
-
-uint8_t Text::draw(const char* s, const uint8_t line, uint8_t x, const uint8_t yOffset, const bool invert)
-{
- const auto length = strlen(s);
-
- Display::setLine(line);
-
- for (uint8_t i = 0; i < length; ++i) {
- Display::setColumn(x);
-
- x += DefaultCharWidth + 1;
-
- drawChar(s[i], yOffset, invert);
-
- // Stop if the next character won't fit
- if (x > Display::Driver::Width - 1)
- return x;
-
- // Fill the background between letters
- if (i < length - 1) {
- const uint8_t pattern[DefaultSpaceWidth] = { 0 };
- for (uint8_t j = 0; j < DefaultSpaceWidth; ++j) {
- Display::sendData(pattern, DefaultSpaceWidth, yOffset, invert);
- }
- }
- }
-
- return x;
-}
-
-void Text::draw7Seg(const char* number, const uint8_t line, uint8_t x)
-{
- const auto length = strlen(number);
-
- if (length == 0 || line > (7 - SevenSegCharLines) || x > 127)
- return;
-
- for (uint8_t i = 0; i < length; ++i) {
- // Stop if the next character won't fit
- if (x + SevenSegCharWidth + 1 > Display::Driver::Width - 1)
- return;
-
- const auto c = number[i];
-
- // Space character only increments the left offset
- if (c != ' ') {
- if (c == '-') {
- Display::setLine(line + 1);
- Display::setColumn(x);
-
- // Cost-efficient dash symbol
- const uint8_t charData = 0b00011100;
- for (uint8_t j = 2; j < SevenSegCharWidth - 2; ++j)
- Display::sendData(charData);
- }
- else {
- // Draw the pages of the character
- for (uint8_t page = 0; page < SevenSegCharLines; ++page) {
- Display::setLine(line + page);
- Display::setColumn(x);
-
- const uint8_t* charData = nullptr;
-
- if (c >= '0' && c <= '9')
- charData = SevenSegCharset[c - '0'][page];
- else if (c == 'C' || c == 'c')
- charData = SevenSegCharset[9 + 1][page];
-
- Display::sendData(charData, SevenSegCharWidth);
- }
- }
- }
- else {
- for (uint8_t page = 0; page < SevenSegCharLines; ++page) {
- Display::setLine(line + page);
- Display::setColumn(x);
-
- uint8_t charData[SevenSegCharWidth] = { 0 };
-
- Display::sendData(charData, SevenSegCharWidth);
- }
- }
-
- x += SevenSegCharWidth + 2;
- }
-}
diff --git a/src/display/Text.h b/src/display/Text.h
deleted file mode 100644
index cdb7be2..0000000
--- a/src/display/Text.h
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- This file is part of esp-thermostat.
-
- esp-thermostat is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- esp-thermostat is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with esp-thermostat. If not, see .
-
- Author: Tamas Karpati
- Created on 2020-01-22
-*/
-
-#pragma once
-
-#include
-
-namespace Text
-{
- void draw(char c, uint8_t line, uint8_t x, uint8_t yOffset, bool invert);
- uint8_t draw(const char* s, uint8_t line, uint8_t x, uint8_t yOffset, bool invert);
- void draw7Seg(const char* number, uint8_t line, uint8_t x);
-}
\ No newline at end of file
diff --git a/src/main.cpp b/src/main.cpp
index 23ce828..b3c21a5 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -1,16 +1,25 @@
#include "main.h"
+#include "Config.h"
#include "FurnaceController.h"
#include "Peripherals.h"
-#include "PrivateConfig.h"
+#include "Settings.h"
+
+#include "ui/Model.h"
+#include "ui/UIController.h"
#include
+#include
#include
namespace
{
ApplicationConfig appConfig;
- std::unique_ptr controller;
+ CoreApplication* coreApplication{};
+ Settings* settings{};
+ UI::Model* uiModel{};
+ FurnaceController* furnaceController{};
+ UI::UIController* uiController{};
}
void initializeTempSensor()
@@ -25,7 +34,7 @@ void setup()
{
initializeTempSensor();
- appConfig.firmwareVersion = VersionNumber{ 1, 6, 0 };
+ appConfig.firmwareVersion = VersionNumber{ 1, 7, 8 };
#ifndef TEST_BUILD
appConfig.logging.syslog.enabled = true;
@@ -52,12 +61,47 @@ void setup()
appConfig.wifi.password = Config::WiFi::Password;
appConfig.wifi.ssid = Config::WiFi::SSID;
+ appConfig.ntp.server = Config::Ntp::Server;
+
appConfig.hostName = Config::HostName;
- controller = std::make_unique(appConfig);
+ coreApplication = [] {
+ static CoreApplication application{ appConfig };
+ return &application;
+ }();
+
+ settings = [] {
+ static Settings s{ coreApplication->settings() };
+ return &s;
+ }();
+
+ appConfig.logging.maximumLevel = static_cast(settings->system.maximumLogLevel);
+
+ uiModel = [] {
+ static UI::Model model{ .settings = *settings };
+ return &model;
+ }();
+
+ furnaceController = [] {
+ static FurnaceController controller{ *coreApplication, appConfig, *settings, *uiModel };
+ return &controller;
+ }();
+
+ uiController = [] {
+ static UI::UIController controller{ *coreApplication, *settings, *uiModel };
+ return &controller;
+ }();
}
void loop()
{
- controller->task();
+ static uint32_t lastTaskMillis{};
+
+ const uint32_t currentMillis{ millis() };
+ const uint32_t deltaMillis{ currentMillis - lastTaskMillis };
+ lastTaskMillis = currentMillis;
+
+ coreApplication->task();
+ furnaceController->task(deltaMillis);
+ uiController->task(deltaMillis);
}
diff --git a/src/ui/Controllers/ClockController.h b/src/ui/Controllers/ClockController.h
new file mode 100644
index 0000000..092a3d6
--- /dev/null
+++ b/src/ui/Controllers/ClockController.h
@@ -0,0 +1,47 @@
+#pragma once
+
+#include "../Model.h"
+
+#include
+
+#include
+
+namespace UI::Controllers
+{
+ class ClockController
+ {
+ public:
+ static constexpr auto UpdateIntervalMillis{ 500 };
+
+ explicit ClockController(
+ Model::Clock& model,
+ const ISystemClock& systemClock
+ )
+ : _model{ model }
+ , _systemClock{ systemClock }
+ {}
+
+ void task(const uint32_t deltaMillis)
+ {
+ _lastUpdateMillis += deltaMillis;
+
+ if (_lastUpdateMillis < UpdateIntervalMillis) {
+ return;
+ }
+
+ _lastUpdateMillis = 0;
+
+ const time_t localTime{ _systemClock.localTime() };
+ const auto* t{ gmtime(&localTime) };
+
+ _model.dayOfWeek = t->tm_wday;
+ _model.hours = t->tm_hour;
+ _model.minutes = t->tm_min;
+ }
+
+ private:
+ Model::Clock& _model;
+ const ISystemClock& _systemClock;
+ uint32_t _lastUpdateMillis{};
+ };
+}
\ No newline at end of file
diff --git a/src/ui/DrawHelper.cpp b/src/ui/DrawHelper.cpp
deleted file mode 100644
index b35e3ec..0000000
--- a/src/ui/DrawHelper.cpp
+++ /dev/null
@@ -1,201 +0,0 @@
-/*
- This file is part of esp-thermostat.
-
- esp-thermostat is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- esp-thermostat is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with esp-thermostat. If not, see .
-
- Author: Tamas Karpati
- Created on 2017-01-07
-*/
-
-#include "DrawHelper.h"
-#include "Graphics.h"
-#include "Extras.h"
-
-#include "display/Display.h"
-#include "display/Text.h"
-
-#include
-
-void draw_weekday(uint8_t x, uint8_t wday)
-{
- if (wday > 6)
- return;
-
- static const char days[7][4] = {
- "SUN",
- "MON",
- "TUE",
- "WED",
- "THU",
- "FRI",
- "SAT"
- };
-
- Text::draw(days[wday], 0, x, 0, false);
-}
-
-void draw_mode_indicator(mode_indicator_t indicator)
-{
- switch (indicator) {
- case DH_MODE_HEATING:
- graphics_draw_multipage_bitmap(graphics_flame_icon_20x3p, 20, 3, 92, 2);
- break;
-
- case DH_MODE_OFF:
- graphics_draw_multipage_bitmap(graphics_off_icon_20x3p, 20, 3, 92, 2);
- break;
-
- default:
- Display::fillArea(92, 2, 20, 3, 0);
- break;
- }
-}
-
-void draw_schedule_bar(const HeatingZoneController::Schedule& sday)
-{
- static const uint8_t long_tick = 0b11110000;
- static const uint8_t short_tick = 0b01110000;
- static const uint8_t bar_indicator = 0b00010111;
- static const uint8_t bar_no_indicator = 0b00010000;
-
- Display::setLine(6);
- Display::setColumn(3);
-
- uint8_t tick_counter = 0;
- uint8_t long_tick_counter = 0;
- uint8_t schedule_byte_idx = 0;
- uint8_t schedule_bit_idx = 255; // Will overflow in the first round
- uint8_t indicator_counter = 0;
- uint8_t schedule_bit = 0;
-
- for (uint8_t x = 0; x < 121; ++x) {
- uint8_t bitmap;
-
- if (tick_counter == 0) {
- // Draw ticks
- if (long_tick_counter == 0)
- bitmap = long_tick;
- else
- bitmap = short_tick;
- } else {
- // Draw rest of the bar with or without the indicators
- if (indicator_counter < 2 && schedule_bit)
- bitmap = bar_indicator;
- else
- bitmap = bar_no_indicator;
- }
-
- Display::sendData(bitmap);
-
- if (++tick_counter == 5) {
- tick_counter = 0;
- if (++long_tick_counter == 6)
- long_tick_counter = 0;
- }
-
- ++indicator_counter;
- if (tick_counter == 1 || tick_counter == 3) {
- indicator_counter = 0;
- if (++schedule_bit_idx == 8) {
- ++schedule_byte_idx;
- schedule_bit_idx = 0;
- }
-
- schedule_bit = (sday[schedule_byte_idx] >> schedule_bit_idx) & 1;
- }
- }
-
- Text::draw("0", 7, 1, 1, false);
- Text::draw("6", 7, 31, 1, false);
- Text::draw("12", 7, 58, 1, false);
- Text::draw("18", 7, 88, 1, false);
- Text::draw("24", 7, 115, 1, false);
-}
-
-void draw_schedule_indicator(uint8_t sch_intval_idx)
-{
- static const uint8_t indicator_bitmap[] = {
- 0b00010000,
- 0b00100000,
- 0b01111100,
- 0b00100000,
- 0b00010000
- };
-
- uint8_t x = 2; // initial offset from left
- x += sch_intval_idx << 1; // for every "tick"
- x += sch_intval_idx >> 1; // for every padding between "ticks"
-
- /*
- 0: v
- 1: . v
- 2: . . v
- 3: . . . v
- 4: . . . . v
- 5: . . . . . v
- |||| |||| ||||
- 01234567890123
-
- 0 -> 0
- 1 -> 2
- 2 -> 5
- 3 -> 7
- 4 -> 10
- 5 -> 12
- */
-
- Display::fillArea(0, 5, 128, 1, 0);
-
- graphics_draw_bitmap(indicator_bitmap, sizeof(indicator_bitmap), x, 5);
-}
-
-void draw_temperature_value(uint8_t x, int8_t int_part, int8_t frac_part)
-{
- if (int_part < 0 || frac_part < 0)
- Text::draw7Seg("-", 2, x - 13);
- else
- Text::draw7Seg(" ", 2, x - 13);
-
- // Draw integral part of the value
- char s[4] = { 0 };
- sprintf(s, "%02d", int_part >= 0 ? int_part : -int_part);
- Text::draw7Seg(s, 2, x);
-
- // Draw the decimal point
- static const uint8_t dp_bitmap[] = { 0b01100000, 0b01100000 };
- Display::setLine(4);
- Display::setColumn(x + 28);
- Display::sendData(dp_bitmap, sizeof(dp_bitmap), 0, false);
-
- // Draw fractional part of the value
- sprintf(s, "%d", frac_part >= 0 ? frac_part : -frac_part);
- Text::draw7Seg(s, 2, x + 32);
-
- // Draw the degree symbol
- static const uint8_t ds_bitmap[6] = {
- 0b00011110,
- 0b00101101,
- 0b00110011,
- 0b00110011,
- 0b00101101,
- 0b00011110
- };
-
- Display::setLine(2);
- Display::setColumn(x + 48);
- Display::sendData(ds_bitmap, sizeof(ds_bitmap), 1, false);
-
- // Draw temperature unit
- Text::draw7Seg("C ", 2, x + 56);
-}
\ No newline at end of file
diff --git a/src/ui/DrawHelper.h b/src/ui/DrawHelper.h
deleted file mode 100644
index 195a20b..0000000
--- a/src/ui/DrawHelper.h
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- This file is part of esp-thermostat.
-
- esp-thermostat is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- esp-thermostat is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with esp-thermostat. If not, see .
-
- Author: Tamas Karpati
- Created on 2017-01-07
-*/
-
-#ifndef DRAW_HELPER_H
-#define DRAW_HELPER_H
-
-#include
-
-#include
-#include
-
-#include "Settings.h"
-
-typedef enum {
- DH_NO_INDICATOR,
- DH_MODE_HEATING,
- DH_MODE_OFF
-} mode_indicator_t;
-
-void draw_weekday(uint8_t x, uint8_t wday);
-void draw_mode_indicator(mode_indicator_t indicator);
-void draw_schedule_bar(const HeatingZoneController::Schedule& sday);
-void draw_schedule_indicator(uint8_t sch_intval_idx);
-void draw_temperature_value(uint8_t x, int8_t int_part, int8_t frac_part);
-
-#endif /* DRAW_HELPER_H */
-
diff --git a/src/ui/Graphics.cpp b/src/ui/Graphics.cpp
index 136354e..d7b493a 100644
--- a/src/ui/Graphics.cpp
+++ b/src/ui/Graphics.cpp
@@ -19,240 +19,241 @@
*/
#include "Graphics.h"
-#include "display/Display.h"
+#include "Resources.h"
-void graphics_draw_bitmap(
- const uint8_t* bitmap,
- uint8_t width,
- uint8_t x,
- uint8_t line)
+#include
+#include
+#include
+
+using namespace UI;
+
+OLEDGraphics::OLEDGraphics()
{
- if (line > Display::Lines || width == 0 || x + width >= Display::Width)
+ DisplayImpl::init();
+ DisplayImpl::powerOn();
+ DisplayImpl::setContrast(0);
+}
+
+void OLEDGraphics::drawBitmap(
+ const unsigned x,
+ const unsigned line,
+ const std::span bitmap
+)
+{
+ if (line > DisplayImpl::Lines || bitmap.size() == 0 || x + bitmap.size() >= DisplayImpl::Width)
return;
- Display::setLine(line);
- Display::setColumn(x);
- Display::sendData(bitmap, width, 0, false);
+ DisplayImpl::setLine(line);
+ DisplayImpl::setColumn(x);
+ DisplayImpl::sendData(bitmap.data(), bitmap.size());
}
-void graphics_draw_multipage_bitmap(
- const uint8_t* mp_bitmap,
- uint8_t width,
- uint8_t lineCount,
- uint8_t x,
- uint8_t startLine)
+void OLEDGraphics::drawBitmap(
+ const unsigned x,
+ const unsigned startLine,
+ const std::span bitmap,
+ const unsigned width,
+ const unsigned pageCount
+)
{
- if (startLine + lineCount > Display::Lines)
+ if (startLine + pageCount > DisplayImpl::Lines)
return;
- const uint8_t* bitmap = mp_bitmap;
+ auto offset{ 0 };
+ for (uint8_t line = startLine; line < startLine + pageCount; ++line) {
+ drawBitmap(x, line, bitmap.subspan(offset, width));
+ offset += width;
+ }
+}
+
+void OLEDGraphics::fillArea(
+ const unsigned x,
+ const unsigned line,
+ const unsigned width,
+ const unsigned pages,
+ const Color color
+)
+{
+ const auto pattern{ static_cast(color == Color::White ? 0xFFu : 0u) };
+ DisplayImpl::fillArea(x, line, width, pages, pattern);
+}
+
+void OLEDGraphics::drawScheduleBar(
+ const std::span& scheduleBits,
+ const unsigned byteOffset
+)
+{
+ static constexpr uint8_t longTick = 0b11110000;
+ static constexpr uint8_t shortTick = 0b01110000;
+ static constexpr uint8_t setIndicator = 0b00010111;
+ static constexpr uint8_t clearedIndicator = 0b00010000;
+
+ DisplayImpl::setLine(6);
+ DisplayImpl::setColumn(3);
- for (uint8_t line = startLine; line < startLine + lineCount; ++line) {
- graphics_draw_bitmap(bitmap, width, x, line);
- bitmap += width;
+ uint8_t tickCounter = 0;
+ uint8_t longTickCounter = 0;
+ uint8_t scheduleByteIdx = byteOffset;
+ uint8_t scheduleBitIdx = 255; // Will overflow in the first round
+ uint8_t indicatorCounter = 0;
+ uint8_t scheduleBitValue = 0;
+
+ for (uint8_t x = 0; x < 121; ++x) {
+ uint8_t bitmap;
+
+ if (tickCounter == 0) {
+ // Draw ticks
+ if (longTickCounter == 0)
+ bitmap = longTick;
+ else
+ bitmap = shortTick;
+ } else {
+ // Draw rest of the bar with or without the indicators
+ if (indicatorCounter < 2 && scheduleBitValue)
+ bitmap = setIndicator;
+ else
+ bitmap = clearedIndicator;
+ }
+
+ DisplayImpl::sendData(bitmap);
+
+ if (++tickCounter == 5) {
+ tickCounter = 0;
+ if (++longTickCounter == 6)
+ longTickCounter = 0;
+ }
+
+ ++indicatorCounter;
+ if (tickCounter == 1 || tickCounter == 3) {
+ indicatorCounter = 0;
+ if (++scheduleBitIdx == 8) {
+ ++scheduleByteIdx;
+ scheduleBitIdx = 0;
+ }
+
+ scheduleBitValue = (scheduleBits[scheduleByteIdx] >> (7 - scheduleBitIdx)) & 1; // MSB-first
+ }
+ }
+
+ using namespace std::string_view_literals;
+ using Label = std::tuple;
+
+ static constexpr auto labels = {
+ Label{ "0"sv, 1 },
+ Label{ "6"sv, 31 },
+ Label{ "12"sv, 58 },
+ Label{ "18"sv, 88 },
+ Label{ "24"sv, 115 }
+ };
+
+ for (const auto& [text, x] : labels) {
+ drawText(x, 7, text, Resources::Fonts::Oled);
}
}
-const uint8_t graphics_flame_icon_20x3p[20 * 3] = {
- // page 0
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b11100000,
- 0b11111000,
- 0b00011100,
- 0b00001110,
- 0b11111111,
- 0b11110000,
- 0b00000000,
- 0b00000000,
- 0b10000000,
- 0b11000000,
- 0b11100000,
- 0b11100000,
- 0b00000000,
- 0b00000000,
-
- // page 1
- 0b11110000,
- 0b11111100,
- 0b00001110,
- 0b00111100,
- 0b01110000,
- 0b01101110,
- 0b11111111,
- 0b00000001,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00000011,
- 0b00000110,
- 0b00001100,
- 0b00011111,
- 0b00000011,
- 0b00000000,
- 0b00001111,
- 0b11111111,
- 0b11110000,
-
- // page 2
- 0b00000000,
- 0b00000111,
- 0b00001111,
- 0b00011100,
- 0b00111000,
- 0b01110000,
- 0b01100000,
- 0b11000000,
- 0b11000000,
- 0b11000000,
- 0b11000000,
- 0b11000000,
- 0b11000000,
- 0b01100000,
- 0b01110000,
- 0b00111000,
- 0b00011100,
- 0b00001111,
- 0b00000111,
- 0b00000000
-};
-
-const uint8_t graphics_off_icon_20x3p[20 * 3] = {
- // page 0
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b10000000,
- 0b11000000,
- 0b11100000,
- 0b01100000,
- 0b00000000,
- 0b00000000,
- 0b11111110,
- 0b11111110,
- 0b00000000,
- 0b00000000,
- 0b01100000,
- 0b11100000,
- 0b11000000,
- 0b10000000,
- 0b00000000,
- 0b00000000,
- 0b00000000,
-
- // page 1
- 0b11111000,
- 0b11111110,
- 0b00000111,
- 0b00000011,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00000111,
- 0b00000111,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00000000,
- 0b00000011,
- 0b00000111,
- 0b11111110,
- 0b11111000,
-
- // page 2
- 0b00000001,
- 0b00000111,
- 0b00001110,
- 0b00011100,
- 0b00110000,
- 0b01110000,
- 0b01100000,
- 0b11000000,
- 0b11000000,
- 0b11000000,
- 0b11000000,
- 0b11000000,
- 0b11000000,
- 0b01100000,
- 0b01110000,
- 0b00110000,
- 0b00011100,
- 0b00001110,
- 0b00000111,
- 0b00000001
-};
-
-const uint8_t graphics_calendar_icon_20x3p[20 * 3] = {
- // page 0
- 0b11000000,
- 0b11100000,
- 0b01100000,
- 0b01100000,
- 0b11111000,
- 0b11111100,
- 0b11111000,
- 0b01100000,
- 0b01100000,
- 0b01100000,
- 0b01100000,
- 0b01100000,
- 0b01100000,
- 0b11111000,
- 0b11111100,
- 0b11111000,
- 0b01100000,
- 0b01100000,
- 0b11100000,
- 0b11000000,
-
- // page 1
- 0b11111111,
- 0b11111111,
- 0b00000000,
- 0b01100000,
- 0b01100000,
- 0b00000001,
- 0b00000000,
- 0b01101100,
- 0b01101100,
- 0b00000000,
- 0b00000000,
- 0b01101100,
- 0b01101100,
- 0b00000000,
- 0b00000001,
- 0b01101100,
- 0b01101100,
- 0b00000000,
- 0b11111111,
- 0b11111111,
-
- // page 2
- 0b00001111,
- 0b00011111,
- 0b00011000,
- 0b00011011,
- 0b00011011,
- 0b00011000,
- 0b00011000,
- 0b00011011,
- 0b00011011,
- 0b00011000,
- 0b00011000,
- 0b00011000,
- 0b00011000,
- 0b00011000,
- 0b00011000,
- 0b00011000,
- 0b00011000,
- 0b00011000,
- 0b00011111,
- 0b00001111
-};
\ No newline at end of file
+void OLEDGraphics::drawScheduleBarPositionIndicator(const uint8_t scheduleBitIndex)
+{
+ static constexpr uint8_t indicatorBitmap[] = {
+ 0b00010000,
+ 0b00100000,
+ 0b01111100,
+ 0b00100000,
+ 0b00010000
+ };
+
+ uint8_t x = 2; // initial offset from left
+ x += scheduleBitIndex << 1; // for every "tick"
+ x += scheduleBitIndex >> 1; // for every padding between "ticks"
+
+ /*
+ 0: v
+ 1: . v
+ 2: . . v
+ 3: . . . v
+ 4: . . . . v
+ 5: . . . . . v
+ |||| |||| ||||
+ 01234567890123
+
+ 0 -> 0
+ 1 -> 2
+ 2 -> 5
+ 3 -> 7
+ 4 -> 10
+ 5 -> 12
+ */
+
+ Display::fillArea(0, 5, 128, 1, 0);
+
+ drawBitmap(x, 5, indicatorBitmap);
+}
+
+void Graphics::drawShortWeekday(const unsigned x, const unsigned line, const unsigned weekday)
+{
+ if (weekday > 6) {
+ return;
+ }
+
+ using namespace std::string_view_literals;
+
+ static constexpr std::array days{
+ "Sun"sv,
+ "Mon"sv,
+ "Tue"sv,
+ "Wed"sv,
+ "Thu"sv,
+ "Fri"sv,
+ "Sat"sv
+ };
+
+ drawText(x, line, days[weekday], Resources::Fonts::Oled);
+}
+
+void Graphics::drawVerticalSeparator(const unsigned x, const unsigned line)
+{
+ DisplayImpl::setColumn(x);
+ DisplayImpl::setLine(line);
+ DisplayImpl::sendData(0x55);
+}
+
+/*
+void draw_temperature_value(uint8_t x, int8_t int_part, int8_t frac_part)
+{
+ if (int_part < 0 || frac_part < 0)
+ Text::draw7Seg("-", 2, x - 13);
+ else
+ Text::draw7Seg(" ", 2, x - 13);
+
+ // Draw integral part of the value
+ char s[4] = { 0 };
+ sprintf(s, "%02d", int_part >= 0 ? int_part : -int_part);
+ Text::draw7Seg(s, 2, x);
+
+ // Draw the decimal point
+ static const uint8_t dp_bitmap[] = { 0b01100000, 0b01100000 };
+ Display::setLine(4);
+ Display::setColumn(x + 28);
+ Display::sendData(dp_bitmap, sizeof(dp_bitmap), 0, false);
+
+ // Draw fractional part of the value
+ sprintf(s, "%d", frac_part >= 0 ? frac_part : -frac_part);
+ Text::draw7Seg(s, 2, x + 32);
+
+ // Draw the degree symbol
+ static const uint8_t ds_bitmap[6] = {
+ 0b00011110,
+ 0b00101101,
+ 0b00110011,
+ 0b00110011,
+ 0b00101101,
+ 0b00011110
+ };
+
+ Display::setLine(2);
+ Display::setColumn(x + 48);
+ Display::sendData(ds_bitmap, sizeof(ds_bitmap), 1, false);
+
+ // Draw temperature unit
+ Text::draw7Seg("C ", 2, x + 56);
+}
+*/
\ No newline at end of file
diff --git a/src/ui/Graphics.h b/src/ui/Graphics.h
index 0d54f07..893b104 100644
--- a/src/ui/Graphics.h
+++ b/src/ui/Graphics.h
@@ -18,27 +18,181 @@
Created on 2017-01-02
*/
-#ifndef GRAPHICS_H
-#define GRAPHICS_H
+#pragma once
-#include
+#include "Resources.h"
-extern const uint8_t graphics_flame_icon_20x3p[];
-extern const uint8_t graphics_off_icon_20x3p[];
-extern const uint8_t graphics_calendar_icon_20x3p[];
+#include "display/Display.h"
-void graphics_draw_bitmap(
- const uint8_t* bitmap,
- uint8_t width,
- uint8_t x,
- uint8_t line);
+#include
+#include
+#include
-void graphics_draw_multipage_bitmap(
- const uint8_t* mp_bitmap,
- uint8_t width,
- uint8_t page_count,
- uint8_t x,
- uint8_t start_page);
+namespace UI
+{
-#endif /* GRAPHICS_H */
+class GraphicsBase
+{
+public:
+ enum class Color
+ {
+ Black,
+ White
+ };
+ enum class Alignment
+ {
+ Left,
+ Right
+ };
+};
+
+class OLEDGraphics : public GraphicsBase
+{
+public:
+ using DisplayImpl = Display;
+
+ static constexpr auto Width{ 128 };
+ static constexpr auto Height{ 64 };
+ static constexpr auto Lines{ 8 };
+
+ OLEDGraphics();
+
+ void drawBitmap(unsigned x, unsigned line, std::span bitmap);
+ void drawBitmap(unsigned x, unsigned line, std::span bitmap, unsigned width, unsigned pageCount);
+ void fillArea(unsigned x, unsigned line, unsigned width, unsigned pages, Color color);
+
+ void drawScheduleBar(const std::span& scheduleBits, unsigned byteOffset);
+ void drawScheduleBarPositionIndicator(uint8_t scheduleBitIndex);
+
+ void drawShortWeekday(unsigned x, unsigned line, unsigned weekday);
+
+ void drawVerticalSeparator(unsigned x, unsigned line);
+
+ template
+ void drawBitmap(unsigned x, unsigned line, const Resources::Assets::MultiPageBitmap& bitmap)
+ {
+ drawBitmap(x, line, bitmap.bitmap, bitmap.width, bitmap.pages);
+ }
+
+ template
+ void drawChar(
+ const char c,
+ const Font& font,
+ const unsigned yOffset = 0,
+ const bool inverted = false
+ )
+ {
+ const auto glyphIndex{ static_cast(c - 32) };
+ const auto* charData{ font.placeholder };
+
+ if (glyphIndex < font.charCount) {
+ charData = font.glyphs[glyphIndex];
+ }
+
+ DisplayImpl::sendData(charData, font.charWidth, yOffset, inverted);
+ }
+
+ template
+ unsigned drawText(
+ unsigned x,
+ const unsigned line,
+ const std::string_view& text,
+ const Font& font,
+ const Alignment alignment = Alignment::Left,
+ const unsigned yOffset = 0,
+ const bool inverted = false
+ )
+ {
+ static constexpr auto CharacterSpacing{ 1 };
+ static constexpr std::array BackgroundPattern{{}};
+
+ DisplayImpl::setLine(line);
+
+ if (alignment == Alignment::Right && !text.empty()) {
+ const auto textPixelWidth = text.length() * (font.charWidth + CharacterSpacing) - CharacterSpacing;
+ if (textPixelWidth > x) {
+ return x;
+ }
+ x -= textPixelWidth;
+ }
+
+ for (uint8_t i = 0; i < text.length(); ++i) {
+ DisplayImpl::setColumn(x);
+
+ x += font.charWidth + CharacterSpacing;
+
+ drawChar(text[i], font, yOffset, inverted);
+
+ // Fill the background between letters
+ DisplayImpl::sendData(BackgroundPattern.data(), BackgroundPattern.size(), yOffset, inverted);
+
+ // Stop if the next character won't fit
+ if (x > DisplayImpl::Driver::Width - 1) {
+ return x;
+ }
+ }
+
+ return x;
+ }
+
+ template
+ unsigned drawLargeNumber(
+ unsigned x,
+ const unsigned line,
+ int number,
+ const LargeNumberFont& font,
+ const bool inverted = false
+ )
+ {
+ static constexpr auto CharacterSpacing{ 1 };
+ static constexpr std::array BackgroundPattern{};
+
+ const bool negative{ number < 0 };
+
+ // By storing the individual digits, calculating character positions
+ // is much easier for left-aligned drawing
+ std::array digits{{}};
+
+ for (int i = static_cast(digits.size()) - 1; i >= 0; --i) {
+ digits[i] = std::abs(number % 10);
+ number /= 10;
+ }
+
+ if (negative) {
+ for (auto page = 0u; page < font.charPages; ++page) {
+ DisplayImpl::setColumn(x);
+ DisplayImpl::setLine(page + line);
+ DisplayImpl::sendData(font.negativeSignGlyph[page], font.charWidth, inverted);
+ DisplayImpl::sendData(BackgroundPattern.data(), BackgroundPattern.size(), inverted);
+ }
+
+ x += font.charWidth + CharacterSpacing;
+ }
+
+ bool skipZeros{ true };
+ for (const auto digit : digits) {
+
+ if (digit == 0 && skipZeros) {
+ continue;
+ }
+
+ skipZeros = false;
+
+ for (auto page = 0u; page < font.charPages; ++page) {
+ DisplayImpl::setColumn(x);
+ DisplayImpl::setLine(page + line);
+ DisplayImpl::sendData(font.glyphs[digit][page], font.charWidth, inverted);
+ DisplayImpl::sendData(BackgroundPattern.data(), BackgroundPattern.size(), inverted);
+ }
+
+ x += font.charWidth + CharacterSpacing;
+ }
+
+ return x;
+ }
+};
+
+using Graphics = OLEDGraphics;
+
+}
diff --git a/src/ui/MainScreen.cpp b/src/ui/MainScreen.cpp
deleted file mode 100644
index c27d0aa..0000000
--- a/src/ui/MainScreen.cpp
+++ /dev/null
@@ -1,196 +0,0 @@
-/*
- This file is part of esp-thermostat.
-
- esp-thermostat is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- esp-thermostat is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with esp-thermostat. If not, see .
-
- Author: Tamas Karpati
- Created on 2017-01-02
-*/
-
-#include "DrawHelper.h"
-#include "Extras.h"
-#include "Graphics.h"
-#include "Keypad.h"
-#include "MainScreen.h"
-#include "Settings.h"
-#include "SystemClock.h"
-#include "TemperatureSensor.h"
-
-#include "display/Text.h"
-
-#include
-#include
-#include
-
-MainScreen::MainScreen(
- Settings& settings,
- const ISystemClock& clock,
- // HeatingController& heatingController,
- const TemperatureSensor& temperatureSensor
-)
- : Screen("Main")
- , _settings(settings)
- , _clock(clock)
- // , _heatingController(heatingController)
- , _temperatureSensor(temperatureSensor)
-{}
-
-void MainScreen::activate()
-{
- _indicator = 0;
- _boostIndicator = 0;
- draw();
-}
-
-void MainScreen::update()
-{
- drawTemperatureDisplay();
- drawClock();
- updateModeIndicator();
- drawTargetTempBoostIndicator();
- updateScheduleBar();
-}
-
-Screen::Action MainScreen::keyPress(Keypad::Keys keys)
-{
- // 1: increase temperature (long: repeat)
- // 2: decrease temperature (long: repeat)
- // 3: menu
- // 4: boost start, extend x minutes (long: stop)
- // 5: daytime manual -> back to automatic
- // 6: nighttime manual -> back to automatic
-
- if (keys & Keypad::Keys::Plus) {
- // _heatingController.incTargetTemp();
- } else if (keys & Keypad::Keys::Minus) {
- // _heatingController.decTargetTemp();
- } else if (keys & Keypad::Keys::Menu) {
- // Avoid entering the menu while exiting
- // from another screen with long press
- if (!(keys & Keypad::Keys::LongPress)) {
- return navigateForward("Menu");
- }
- } else if (keys & Keypad::Keys::Boost) {
- if (keys & Keypad::Keys::LongPress) {
- // if (_heatingController.isBoostActive()) {
- // _heatingController.deactivateBoost();
- // }
- } else {
- // if (!_heatingController.isBoostActive())
- // _heatingController.activateBoost();
- // else
- // _heatingController.extendBoost();
- }
- // } else if (keys & KEY_LEFT) {
- // heatctl_deactivate_boost();
- } else if (keys & Keypad::Keys::Right) {
- return navigateForward("Scheduling");
- }
-
- update();
-
- return Action::NoAction;
-}
-
-void MainScreen::draw()
-{
- _lastScheduleIndex = 255;
-
- updateScheduleBar();
- update();
- draw_mode_indicator(static_cast(_indicator));
-}
-
-void MainScreen::drawClock()
-{
- const auto localTime = _clock.localTime();
- const struct tm* t = gmtime(&localTime);
-
- char time_fmt[10] = { 6 };
- sprintf(time_fmt, "%02d:%02d", t->tm_hour, t->tm_min);
-
- Text::draw(time_fmt, 0, 0, 0, false);
- draw_weekday(33, t->tm_wday);
-}
-
-void MainScreen::drawTargetTempBoostIndicator()
-{
- char s[15] = "";
-
- // if (!_heatingController.isBoostActive()) {
- // uint16_t temp = _heatingController.targetTemp();
- // sprintf(s, " %2d.%d C", temp / 10, temp % 10);
- // } else {
- // time_t secs = _heatingController.boostRemaining();
- // uint16_t minutes = secs / 60;
- // secs -= minutes * 60;
-
- // sprintf(s, " BST %3u:%02lld", minutes, secs);
- // }
-
- Text::draw(s, 0, 60, 0, false);
-}
-
-void MainScreen::updateScheduleBar()
-{
- const auto localTime = _clock.localTime();
- const struct tm* t = gmtime(&localTime);
-
- // draw_schedule_bar(_settings.data.Scheduler.DayData[t->tm_wday]);
-
- uint8_t idx = calculate_schedule_intval_idx(t->tm_hour, t->tm_min);
-
- if (idx != _lastScheduleIndex) {
- _lastScheduleIndex = idx;
- draw_schedule_indicator(idx);
- }
-}
-
-void MainScreen::updateModeIndicator()
-{
- // switch (_heatingController.mode())
- // {
- // case HeatingController::Mode::Boost:
- // case HeatingController::Mode::Normal:
- // if (_heatingController.isActive()) {
- // if (_indicator != DH_MODE_HEATING)
- // _indicator = DH_MODE_HEATING;
- // else
- // return;
- // } else {
- // if (_indicator != DH_NO_INDICATOR)
- // _indicator = DH_NO_INDICATOR;
- // else
- // return;
- // }
- // break;
-
- // case HeatingController::Mode::Off:
- // if (_indicator != DH_MODE_OFF) {
- // _indicator = DH_MODE_OFF;
- // break;
- // } else {
- // return;
- // }
- // }
-
- draw_mode_indicator(static_cast(_indicator));
-}
-
-void MainScreen::drawTemperatureDisplay()
-{
- // const auto reading = _heatingController.currentTemp() * 10;
- // draw_temperature_value(10, reading / 100,
- // (reading % 100) / 10);
-}
diff --git a/src/ui/MainScreen.h b/src/ui/MainScreen.h
deleted file mode 100644
index c95ebe0..0000000
--- a/src/ui/MainScreen.h
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- This file is part of esp-thermostat.
-
- esp-thermostat is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- esp-thermostat is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with esp-thermostat. If not, see .
-
- Author: Tamas Karpati
- Created on 2017-01-02
-*/
-
-#pragma once
-
-#include "Keypad.h"
-#include "Logger.h"
-#include "Screen.h"
-
-#include
-
-class ISystemClock;
-class Settings;
-class HeatingController;
-class TemperatureSensor;
-
-class MainScreen : public Screen
-{
-public:
- MainScreen(
- Settings& settings,
- const ISystemClock& clock,
- // HeatingController& heatingController,
- const TemperatureSensor& temperatureSensor
- );
-
- void activate() override;
- void update() override;
- Action keyPress(Keypad::Keys keys) override;
-
-private:
- Settings& _settings;
- const ISystemClock& _clock;
- // HeatingController& _heatingController;
- const TemperatureSensor& _temperatureSensor;
- Logger _log{ "MainScreen" };
- uint8_t _indicator = 0;
- bool _boostIndicator = false;
- uint8_t _lastScheduleIndex = 0;
-
- void draw();
- void drawClock();
- void drawTargetTempBoostIndicator();
- void updateScheduleBar();
- void updateModeIndicator();
- void drawTemperatureDisplay();
-};
diff --git a/src/ui/Menu.h b/src/ui/Menu.h
new file mode 100644
index 0000000..da0941c
--- /dev/null
+++ b/src/ui/Menu.h
@@ -0,0 +1,281 @@
+#pragma once
+
+#include "Graphics.h"
+#include "Resources.h"
+#include "Types.h"
+
+#include
+#include
+#include
+
+namespace UI
+{
+
+namespace
+{
+ constexpr auto MaxMenuItemCount{ 12 };
+}
+
+struct MenuItem
+{
+ std::string_view text;
+ const char* value{};
+};
+
+template
+class MenuImpl
+{
+public:
+ template
+ explicit MenuImpl(
+ GraphicsType& graphics,
+ const unsigned startLine,
+ const unsigned height,
+ Items&&... items
+ )
+ : _graphics{ graphics }
+ , _items{ std::forward(items)... }
+ , _itemCount{ sizeof...(items) }
+ , _startLine{ startLine }
+ , _height{ height }
+ {
+ static_assert(sizeof...(Items) <= Capacity, "Increase capacity");
+ }
+
+ void reset()
+ {
+ _selectionIndex = 0;
+ _viewPosition = 0;
+ }
+
+ void update()
+ {
+ drawItems(false);
+ }
+
+ void updateSelectedItem()
+ {
+ drawItems(true);
+ }
+
+ void step(const StepDirection direction)
+ {
+ switch (direction) {
+ case StepDirection::Up:
+ if (_selectionIndex == 0) {
+ _selectionIndex = _itemCount - 1;
+ if (_itemCount > _height) {
+ _viewPosition = std::max(_itemCount - _height, 0u);
+ }
+ } else {
+ --_selectionIndex;
+ if (_selectionIndex < _viewPosition) {
+ _viewPosition = _selectionIndex;
+ }
+ }
+
+ break;
+
+ case StepDirection::Down:
+ ++_selectionIndex;
+ if (_selectionIndex == _itemCount) {
+ _selectionIndex = 0;
+ _viewPosition = 0;
+ } else if ((_selectionIndex - _viewPosition) >= _height) {
+ ++_viewPosition;
+ }
+
+ break;
+ }
+ }
+
+ [[nodiscard]] int currentIndex() const
+ {
+ return _selectionIndex;
+ }
+
+private:
+ GraphicsType& _graphics;
+ std::array