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 _items; + const unsigned _itemCount; + const unsigned _startLine; + const unsigned _height; + unsigned _selectionIndex{}; + unsigned _viewPosition{}; + + void drawItems(const bool selectedItemOnly) + { + auto itemIndex{ _viewPosition }; + auto line{ _startLine }; + + while ( + itemIndex < _itemCount + && line < (_startLine + _height) + && line <= (GraphicsType::Lines - 1) + ) { + if (selectedItemOnly && itemIndex != _selectionIndex) { + ++line; + ++itemIndex; + continue; + } + + if (itemIndex == _selectionIndex) { + _graphics.drawBitmap(0, line, Resources::Assets::ArrowRightIcon); + } else { + _graphics.fillArea(0, line, sizeof(Resources::Assets::ArrowRightIcon), 1, Graphics::Color::Black); + } + + // Title text + auto x = _graphics.drawText( + sizeof(Resources::Assets::ArrowRightIcon) + 2, + line, + _items[itemIndex].text, + Resources::Fonts::Oled + ); + + if (x < (GraphicsType::Width - 1 - sizeof(Resources::Assets::EmptyPositionIndicator))) { + _graphics.fillArea( + x, + line, + GraphicsType::Width - 1 - x - sizeof(Resources::Assets::EmptyPositionIndicator), + 1, + Graphics::Color::Black + ); + } + + // Value text + if (_items[itemIndex].value) { + _graphics.drawText( + GraphicsType::Width - sizeof(Resources::Assets::FullPositionIndicator) - 3, + line, + _items[itemIndex].value, + Resources::Fonts::Oled, + Graphics::Alignment::Right + ); + } + + uint8_t position = _selectionIndex == 0 ? 0 : ((_selectionIndex + 1) * (_height - 1) / _itemCount); + + for (uint8_t i = 0; i <= (_height - 1); ++i) { + if (i == position) { + _graphics.drawBitmap( + GraphicsType::Width - sizeof(Resources::Assets::FullPositionIndicator) - 1, + i + _startLine, + Resources::Assets::FullPositionIndicator + ); + } else { + _graphics.drawBitmap( + GraphicsType::Width - sizeof(Resources::Assets::EmptyPositionIndicator) - 1, + i + _startLine, + Resources::Assets::EmptyPositionIndicator + ); + } + } + + ++line; + ++itemIndex; + } + } +}; + +template +using Menu = MenuImpl; + +#if 0 +class Menu +{ +public: + template + Menu(GraphicsType& graphics, const unsigned startLine, const unsigned height, Item&&... items) + : _menu{ + MenuImpl{ + graphics, + startLine, + height, + std::forward(items)... + } + } + { + static_assert((std::same_as && ...)); + } + + void reset() + { + std::visit( + [](MenuType& m) { + m.reset(); + }, + _menu + ); + } + + void update() + { + std::visit( + [](MenuType& m) { + m.update(); + }, + _menu + ); + } + + void updateSelectedItem() + { + std::visit( + [](MenuType& m) { + m.updateSelectedItem(); + }, + _menu + ); + } + + void step(const StepDirection direction) + { + std::visit( + [direction](MenuType& menu) { + menu.step(direction); + }, + _menu + ); + } + + [[nodiscard]] int currentIndex() const + { + return std::visit( + [](const MenuType& menu) { + return menu.currentIndex(); + }, + _menu + ); + } + +private: + struct Detail + { + template + struct VariantGenerator; + + template + struct VariantGenerator> { + using Variant = std::variant...>; + }; + + template + struct GenerateVariantForMaxItemCount { + using Type = typename VariantGenerator< + GraphicsType, + std::make_index_sequence + >::Variant; + }; + }; + + using MenuVariant = Detail::GenerateVariantForMaxItemCount< + Graphics, + MaxMenuItemCount + >::Type; + + MenuVariant _menu; +}; +#endif + +} \ No newline at end of file diff --git a/src/ui/MenuScreen.cpp b/src/ui/MenuScreen.cpp deleted file mode 100644 index b2f345c..0000000 --- a/src/ui/MenuScreen.cpp +++ /dev/null @@ -1,520 +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-08 -*/ - -#include "DrawHelper.h" -#include "Extras.h" -#include "Graphics.h" -#include "Keypad.h" -#include "MenuScreen.h" -#include "Settings.h" -#include "TextInput.h" -#include "WifiScreen.h" - -#include "display/Display.h" -#include "display/Text.h" - -#include - -MenuScreen::MenuScreen(Settings& settings) - : Screen("Menu") - , _settings(settings) -{ -} - -void MenuScreen::activate() -{ - _page = Page::First; - revertSettings(); - draw(); -} - -void MenuScreen::update() -{ -} - -Screen::Action MenuScreen::keyPress(Keypad::Keys keys) -{ - if (_page != Page::WIFI_PASSWORD && _page != Page::WiFi) { - // 1: increment current value - // 2: decrement current value - // 3: save and exit - // 4: cancel (revert settings) - // 5: navigate to previous page - // 6: navigate to next page - - if (keys & Keypad::Keys::Plus) { - adjustValue(1); - } else if (keys & Keypad::Keys::Minus) { - adjustValue(-1); - } else if (keys & Keypad::Keys::Menu) { - applySettings(); - return Action::NavigateBack; - } else if (keys & Keypad::Keys::Boost) { - revertSettings(); - return Action::NavigateBack; - } else if (keys & Keypad::Keys::Left) { - previousPage(); - } else if (keys & Keypad::Keys::Right) { - nextPage(); - } - } else if (_page == Page::WIFI_PASSWORD) { - ti_key_event_t keyEvent; - bool validKey = true; - - if (keys & Keypad::Keys::Plus) { - keyEvent = TI_KE_UP; - } else if (keys & Keypad::Keys::Minus) { - keyEvent = TI_KE_DOWN; - } else if (keys & Keypad::Keys::Menu) { - keyEvent = TI_KE_SELECT; - } else if (keys & Keypad::Keys::Left) { - keyEvent = TI_KE_LEFT; - } else if (keys & Keypad::Keys::Right) { - keyEvent = TI_KE_RIGHT; - } else { - validKey = false; - } - - if (validKey) { - const ti_key_event_result_t res = text_input_key_event(keyEvent); - - if (res != TI_KE_NO_ACTION) { - _page = Page::WiFi; - draw(); - } - } - } else if (_page == Page::WiFi) { - wifi_screen_key_event(keys); - } - - return Action::NoAction; -} - -void MenuScreen::draw() -{ - Display::clear(); - - switch (_page) { - case Page::HeatCtlMode: - drawPageHeatCtlMode(); - break; - - case Page::DaytimeTemp: - drawPageDaytimeTemp(); - break; - - case Page::NightTimeTemp: - drawPageNightTimeTemp(); - break; - - case Page::TempOvershoot: - drawPageTempOvershoot(); - break; - - case Page::TempUndershoot: - drawPageTempUndershoot(); - break; - - case Page::BoostInterval: - drawPageBoostIntval(); - break; - - case Page::CustomTempTimeout: - drawPageCustomTempTimeout(); - break; - - case Page::DisplayBrightness: - drawPageDisplayBrightness(); - break; - - case Page::DisplayTimeout: - drawPageDisplayTimeout(); - break; - - case Page::TempCorrection: - drawPageTempCorrection(); - break; - - case Page::Reboot: - drawPageReboot(); - break; - - case Page::WiFi: - drawPageWifi(); - break; - - case Page::WIFI_PASSWORD: - drawPageWifiPassword(); - break; - - default: - break; - } - - if (_page != Page::WiFi) { - // "<-" previous page indicator - if (_page > Page::First) { - Text::draw("<-", 7, 0, 0, false); - } - - // "->" next page indicator - if (static_cast(_page) < static_cast(Page::Last)) { - Text::draw("->", 7, 115, 0, false); - } - } -} - -void MenuScreen::drawPageHeatCtlMode() -{ - drawPageTitle("MODE"); - updatePageHeatCtlMode(); -} - -void MenuScreen::drawPageDaytimeTemp() -{ - drawPageTitle("DAYTIME T."); - updatePageDaytimeTemp(); -} - -void MenuScreen::drawPageNightTimeTemp() -{ - drawPageTitle("NIGHTTIME T."); - updatePageNightTimeTemp(); -} - -void MenuScreen::drawPageTempOvershoot() -{ - drawPageTitle("T. OVERSHOOT"); - updatePageTempOvershoot(); -} - -void MenuScreen::drawPageTempUndershoot() -{ - drawPageTitle("T. UNDERSHOOT"); - updatePageTempUndershoot(); -} - -void MenuScreen::drawPageBoostIntval() -{ - drawPageTitle("BOOST INT. (MIN)"); - updatePageBoostIntval(); -} - -void MenuScreen::drawPageCustomTempTimeout() -{ - drawPageTitle("CUS. TMP. TOUT. (MIN)"); - updatePageCustomTempTimeout(); -} - -void MenuScreen::drawPageDisplayBrightness() -{ - drawPageTitle("DISP. BRIGHT."); - updatePageDisplayBrightness(); -} - -void MenuScreen::drawPageDisplayTimeout() -{ - drawPageTitle("DISP. TIMEOUT"); - updatePageDisplayTimeout(); -} - -void MenuScreen::drawPageTempCorrection() -{ - drawPageTitle("TEMP. CORR."); - updatePageTempCorrection(); -} - -void MenuScreen::drawPageReboot() -{ - drawPageTitle("REBOOT"); - _rebootCounter = 3; - updatePageReboot(); -} - -void MenuScreen::drawPageWifi() -{ - wifi_screen_init(); -} - -void MenuScreen::drawPageWifiPassword() -{ - text_input_init(_wifiPsw, sizeof(_wifiPsw), "WiFi password:"); -} - -void MenuScreen::updatePageHeatCtlMode() -{ - // FIXME: proper mode name must be shown - - // char num[4] = { 0 }; - // sprintf(num, "%2d", _newSettings.HeatingController.Mode); - - Display::fillArea(0, 3, 128, 3, 0); - - // switch (static_cast(_newSettings.HeatingController.Mode)) { - // case HeatingController::Mode::Normal: - // graphics_draw_multipage_bitmap(graphics_calendar_icon_20x3p, 20, 3, 20, 2); - // Text::draw("NORMAL", 3, 50, 0, false); - // Text::draw("(SCHEDULE)", 4, 50, 0, false); - // break; - - // case HeatingController::Mode::Off: - // graphics_draw_multipage_bitmap(graphics_off_icon_20x3p, 20, 3, 20, 2); - // Text::draw("OFF", 3, 50, 0, false); - // break; - - // case HeatingController::Mode::Boost: - // break; - // } -} - -void MenuScreen::updatePageDaytimeTemp() -{ - // draw_temperature_value(20, - // _newSettings.HeatingController.DaytimeTemp / 10, - // _newSettings.HeatingController.DaytimeTemp % 10); -} - -void MenuScreen::updatePageNightTimeTemp() -{ - // draw_temperature_value(20, - // _newSettings.HeatingController.NightTimeTemp / 10, - // _newSettings.HeatingController.NightTimeTemp % 10); -} - -void MenuScreen::updatePageTempOvershoot() -{ - // draw_temperature_value(20, - // _newSettings.HeatingController.Overshoot / 10, - // _newSettings.HeatingController.Overshoot % 10); -} - -void MenuScreen::updatePageTempUndershoot() -{ - // draw_temperature_value(20, - // _newSettings.HeatingController.Undershoot / 10, - // _newSettings.HeatingController.Undershoot % 10); -} - -void MenuScreen::updatePageBoostIntval() -{ - // char num[4] = { 0 }; - // sprintf(num, "%2d", _newSettings.HeatingController.BoostIntervalMins); - // Text::draw7Seg(num, 2, 20); -} - -void MenuScreen::updatePageCustomTempTimeout() -{ - // char num[6] = { 0 }; - // sprintf(num, "%4u", _newSettings.HeatingController.CustomTempTimeoutMins); - // Text::draw7Seg(num, 2, 20); -} - -void MenuScreen::updatePageTempCorrection() -{ - // draw_temperature_value(20, - // _newSettings.HeatingController.TempCorrection / 10, - // _newSettings.HeatingController.TempCorrection% 10); -} - -void MenuScreen::updatePageDisplayBrightness() -{ - // char num[4] = { 0 }; - // sprintf(num, "%3d", _newSettings.Display.Brightness); - // Text::draw7Seg(num, 2, 20); - - // Display::setContrast(_newSettings.Display.Brightness); -} - -void MenuScreen::updatePageDisplayTimeout() -{ - // char num[4] = { 0 }; - // sprintf(num, "%3d", _newSettings.Display.TimeoutSecs); - // Text::draw7Seg(num, 2, 20); -} - -void MenuScreen::updatePageReboot() -{ - Text::draw("Press the (+) button", 2, 5, 0, 0); - - char s[] = "0 time(s) to reboot."; - s[0] = '0' + _rebootCounter; - Text::draw(s, 3, 5, 0, 0); -} - -void MenuScreen::updatePageWifi() -{ - // Text::draw("CONNECTED: YES", 2, 0, 0, false); - // Text::draw("NETWORK:", 4, 0, 0, false); - // Text::draw("", 5, 0, 0, false); - - wifi_screen_update(); -} - -void MenuScreen::drawPageTitle(const char* text) -{ - Text::draw(text, 0, 0, 0, false); -} - -void MenuScreen::nextPage() -{ - if (static_cast(_page) < static_cast(Page::Last)) { - _page = static_cast(static_cast(_page) + 1); - draw(); - } -} - -void MenuScreen::previousPage() -{ - if (_page > Page::First) { - _page = static_cast(static_cast(_page) - 1); - draw(); - } -} - -void MenuScreen::applySettings() -{ - _settings.data = _newSettings; - - auto rebootAfterSave = false; - - _settings.save(); - - if (rebootAfterSave) { - ESP.restart(); - } -} - -void MenuScreen::revertSettings() -{ - _newSettings = _settings.data; - - Display::setContrast(_settings.data.display.Brightness); -} - -void MenuScreen::adjustValue(int8_t amount) -{ - switch (_page) { - case Page::HeatCtlMode: - // if (_newSettings.HeatingController.Mode == 0 && amount > 0) { - // _newSettings.HeatingController.Mode = 2; - // } else if (_newSettings.HeatingController.Mode == 2 && amount < 0) { - // _newSettings.HeatingController.Mode = 0; - // } - updatePageHeatCtlMode(); - break; - - case Page::DaytimeTemp: - // _newSettings.HeatingController.DaytimeTemp = Extras::adjustValueWithRollOver( - // _newSettings.HeatingController.DaytimeTemp, - // amount, - // Limits::HeatingController::DaytimeTempMin, - // Limits::HeatingController::DaytimeTempMax - // ); - updatePageDaytimeTemp(); - break; - - case Page::NightTimeTemp: - // _newSettings.HeatingController.NightTimeTemp = Extras::adjustValueWithRollOver( - // _newSettings.HeatingController.NightTimeTemp, - // amount, - // Limits::HeatingController::NightTimeTempMin, - // Limits::HeatingController::NightTimeTempMax - // ); - updatePageNightTimeTemp(); - break; - - case Page::TempOvershoot: - // _newSettings.HeatingController.Overshoot = Extras::adjustValueWithRollOver( - // _newSettings.HeatingController.Overshoot, - // amount, - // Limits::HeatingController::TempOvershootMin, - // Limits::HeatingController::TempOvershootMax - // ); - updatePageTempOvershoot(); - break; - - case Page::TempUndershoot: - // _newSettings.HeatingController.Undershoot = Extras::adjustValueWithRollOver( - // _newSettings.HeatingController.Undershoot, - // amount, - // Limits::HeatingController::TempUndershootMin, - // Limits::HeatingController::TempUndershootMax - // ); - updatePageTempUndershoot(); - break; - - case Page::BoostInterval: - // _newSettings.HeatingController.BoostIntervalMins = Extras::adjustValueWithRollOver( - // _newSettings.HeatingController.BoostIntervalMins, - // amount, - // Limits::HeatingController::BoostIntervalMin, - // Limits::HeatingController::BoostIntervalMax - // ); - updatePageBoostIntval(); - break; - - case Page::CustomTempTimeout: - // _newSettings.HeatingController.CustomTempTimeoutMins = Extras::adjustValueWithRollOver( - // _newSettings.HeatingController.CustomTempTimeoutMins, - // amount, - // Limits::HeatingController::CustomTempTimeoutMin, - // Limits::HeatingController::CustomTempTimeoutMax - // ); - updatePageCustomTempTimeout(); - break; - - case Page::DisplayBrightness: - // _newSettings.Display.Brightness += amount; - updatePageDisplayBrightness(); - break; - - case Page::DisplayTimeout: - // _newSettings.Display.TimeoutSecs += amount; - updatePageDisplayTimeout(); - break; - - case Page::TempCorrection: - // _newSettings.HeatingController.TempCorrection = Extras::adjustValueWithRollOver( - // _newSettings.HeatingController.TempCorrection, - // amount, - // Limits::HeatingController::TempCorrectionMin, - // Limits::HeatingController::TempCorrectionMax - // ); - updatePageTempCorrection(); - break; - - case Page::Reboot: - if (amount > 0 && --_rebootCounter == 0) { - _log.warning_P(PSTR("initiating manual reboot")); - ESP.restart(); - } - updatePageReboot(); - break; - - // TODO dummy implementation - case Page::WiFi: - _page = Page::WIFI_PASSWORD; - drawPageWifiPassword(); - break; - - default: - break; - } -} \ No newline at end of file diff --git a/src/ui/MenuScreen.h b/src/ui/MenuScreen.h deleted file mode 100644 index 355e1ea..0000000 --- a/src/ui/MenuScreen.h +++ /dev/null @@ -1,104 +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-08 -*/ - -#pragma once - -#include "Keypad.h" -#include "Screen.h" -#include "Settings.h" - -#include - -#include - -class MenuScreen : public Screen -{ -public: - MenuScreen(Settings& settings); - - void activate() override; - void update() override; - Action keyPress(Keypad::Keys keys) override; - -private: - Logger _log{ "MenuScreen" }; - Settings& _settings; - Settings::Data _newSettings; - char _wifiPsw[64] = { 0 }; - - uint8_t _rebootCounter = 3; - - enum class Page - { - First = 0, - - HeatCtlMode = First, - DaytimeTemp, - NightTimeTemp, - TempOvershoot, - TempUndershoot, - BoostInterval, - CustomTempTimeout, - DisplayBrightness, - DisplayTimeout, - TempCorrection, - Reboot, - - // These pages cannot be accessed by normal navigation - WiFi, - WIFI_PASSWORD, - - Last = Reboot - } _page = Page::First; - - void draw(); - - void drawPageHeatCtlMode(); - void drawPageDaytimeTemp(); - void drawPageNightTimeTemp(); - void drawPageTempOvershoot(); - void drawPageTempUndershoot(); - void drawPageBoostIntval(); - void drawPageCustomTempTimeout(); - void drawPageDisplayBrightness(); - void drawPageDisplayTimeout(); - void drawPageTempCorrection(); - void drawPageReboot(); - void drawPageWifi(); - void drawPageWifiPassword(); - void updatePageHeatCtlMode(); - void updatePageDaytimeTemp(); - void updatePageNightTimeTemp(); - void updatePageTempOvershoot(); - void updatePageTempUndershoot(); - void updatePageBoostIntval(); - void updatePageCustomTempTimeout(); - void updatePageTempCorrection(); - void updatePageDisplayBrightness(); - void updatePageDisplayTimeout(); - void updatePageReboot(); - void updatePageWifi(); - void drawPageTitle(const char* text); - void nextPage(); - void previousPage(); - void applySettings(); - void revertSettings(); - void adjustValue(int8_t amount); -}; diff --git a/src/ui/Model.h b/src/ui/Model.h new file mode 100644 index 0000000..5292666 --- /dev/null +++ b/src/ui/Model.h @@ -0,0 +1,55 @@ +#pragma once + +#include "Config.h" +#include "Settings.h" + +#include +#include +#include + +namespace UI +{ + struct Model { + Settings& settings; + + struct Navigation { + int selectedZoneIndex{}; + } navigation; + + struct Clock { + uint8_t hours{}; + uint8_t minutes{}; + uint8_t dayOfWeek{}; + } clock; + + struct Zone { + int zoneNumber{}; + int currentTemperature{}; + std::optional targetTemperature{}; + + enum class Status { + Off, + Idle, + Heating, + Holiday, + Boost, + WindowOpen, + WindowLockout + } status{ Status::Idle }; + }; + + std::array zones; + + bool heating{}; + bool masterEnable{}; + bool energyOptimizerEnabled{}; + + int16_t internalTemperature{}; + + bool wifiConnected{}; + bool mqttConnected{}; + + VersionNumber firmwareVersion{}; + VersionNumber baseVersion{}; + }; +} \ No newline at end of file diff --git a/src/ui/Resources.cpp b/src/ui/Resources.cpp new file mode 100644 index 0000000..051c2cc --- /dev/null +++ b/src/ui/Resources.cpp @@ -0,0 +1,1542 @@ +#include "Resources.h" + +namespace UI::Resources::Fonts +{ + const decltype(Oled) Oled = { + .glyphs = { + // [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 + } + }, + .placeholder = { + 0b01111111, + 0b01010101, + 0b01001001, + 0b01010101, + 0b01111111 + } + }; + + const decltype(SevenSegment) SevenSegment = { + .glyphs = { + { + // 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 + } + } + }, + .negativeSignGlyph = { + { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + }, + { + 0b00000000, + 0b00001000, + 0b00011100, + 0b00011100, + 0b00011100, + 0b00011100, + 0b00011100, + 0b00011100, + 0b00011100, + 0b00011100, + 0b00001000, + 0b00000000, + }, + { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + } + } + }; +} + +namespace UI::Resources::Assets +{ + const uint8_t ArrowRightIcon[7] = { + 0b00001000, + 0b00001000, + 0b00001000, + 0b01111111, + 0b00111110, + 0b00011100, + 0b00001000 + }; + + const uint8_t EmptyPositionIndicator[3] = { + 0b10101010, + 0b01010101, + 0b10101010 + }; + + const uint8_t FullPositionIndicator[3] = { + 0b11111111, + 0b11111111, + 0b11111111 + }; + + const decltype(FlameIcon) FlameIcon = { + // 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 decltype(StandbyIcon) StandbyIcon = { + // 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 decltype(CalendarIcon) CalendarIcon = { + // 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 + }; + + const decltype(SevenSegmentLargeC) SevenSegmentLargeC = { + { + // 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 + } + }; +} diff --git a/src/ui/Resources.h b/src/ui/Resources.h new file mode 100644 index 0000000..bcd1ef7 --- /dev/null +++ b/src/ui/Resources.h @@ -0,0 +1,55 @@ +#pragma once + +#include +#include + +namespace UI::Resources::Fonts +{ + template + struct Font + { + static_assert(CharCount > 0 && CharCount < 256); + static_assert(CharWidth > 0 && CharWidth < 129); + static constexpr auto charCount{ CharCount }; + static constexpr auto charWidth{ CharWidth }; + const uint8_t glyphs[CharCount][CharWidth]{}; + const uint8_t placeholder[CharWidth]{}; + }; + + template + struct LargeNumberFont + { + static_assert(CharWidth > 0 && CharWidth < 129); + static_assert(CharPages > 0 && CharPages < 9); + static constexpr auto charWidth{ CharWidth }; + static constexpr auto charPages{ CharPages }; + const uint8_t glyphs[10][CharPages][CharWidth]; + const uint8_t negativeSignGlyph[CharPages][CharWidth]; + }; + + extern const Font<95, 5> Oled; + extern const LargeNumberFont<12, 3> SevenSegment; +} + +namespace UI::Resources::Assets +{ + template + struct MultiPageBitmap + { + static_assert(Pages > 0 && Pages < 8); + static_assert(Width > 0 && Width < 129); + static constexpr auto width{ Width }; + static constexpr auto pages{ Pages }; + using Bitmap = std::array; + Bitmap bitmap{{}}; + }; + + extern const uint8_t ArrowRightIcon[7]; + extern const uint8_t EmptyPositionIndicator[3]; + extern const uint8_t FullPositionIndicator[3]; + + extern const MultiPageBitmap<20, 3> FlameIcon; + extern const MultiPageBitmap<20, 3> StandbyIcon; + extern const MultiPageBitmap<20, 3> CalendarIcon; + extern const MultiPageBitmap<12, 3> SevenSegmentLargeC; +} \ No newline at end of file diff --git a/src/ui/SchedulingScreen.cpp b/src/ui/SchedulingScreen.cpp deleted file mode 100644 index 75409cd..0000000 --- a/src/ui/SchedulingScreen.cpp +++ /dev/null @@ -1,198 +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 "Keypad.h" -#include "SchedulingScreen.h" -#include "SystemClock.h" -#include "main.h" - -#include "display/Display.h" -#include "display/Text.h" - -#include - -SchedulingScreen::SchedulingScreen(Settings& settings, const ISystemClock& systemClock) - : Screen("Scheduling") - , _settings(settings) - , _systemClock(systemClock) -{ - memset(_daysData, 0, sizeof(_daysData)); -} - -void SchedulingScreen::activate() -{ - const auto localTime = _systemClock.localTime(); - struct tm* t = gmtime(&localTime); - _day = t->tm_wday; - _intvalIdx = 0; - // TODO - // memcpy(_daysData, _settings.data.Scheduler.DayData, sizeof(Settings::SchedulerDayData) * 7); - draw(); -} - -void SchedulingScreen::update() -{ -} - -Screen::Action SchedulingScreen::keyPress(Keypad::Keys keys) -{ - // 1: advance 15 minutes (long: go back 15 minutes) - // 2: advance 1 day (long: go back 1 day) - // 3: save and exit - // 4: cancel - // 5: set nighttime mode + advance 15 minutes - // 6: set daytime mode + advance 15 minutes - - if (keys & Keypad::Keys::Plus) { - setModeAndAdvance(true); - // if (keys & KEY_LONG_PRESS) - // prev_interval(); - // else - // next_interval(); - } else if (keys & Keypad::Keys::Minus) { - setModeAndAdvance(false); - // if (keys & KEY_LONG_PRESS) - // prev_day(); - // else - // next_day(); - } else if (keys & Keypad::Keys::Menu) { - if (++_menuPressCnt == 2) { - _menuPressCnt = 0; - if (!(keys & Keypad::Keys::LongPress)) { - applyChanges(); - } - return Action::NavigateBack; - } - } else if (keys & Keypad::Keys::Boost) { - nextDay(); - // return UI_RESULT_SWITCH_MAIN_SCREEN; - } else if (keys & Keypad::Keys::Left) { - prevInterval(); - // set_mode_and_advance(false); - } else if (keys & Keypad::Keys::Right) { - nextInterval(); - // set_mode_and_advance(true); - } - - return Action::NoAction; -} - -void SchedulingScreen::draw() -{ - Display::clear(); - - drawDayName(); - drawIntervalDisplay(); - drawIntervalIndicator(); - updateScheduleBar(); -} - -void SchedulingScreen::drawDayName() -{ - draw_weekday(0, _day); -} - -void SchedulingScreen::drawIntervalDisplay() -{ - uint8_t hours = _intvalIdx >> 1; - uint8_t mins = (_intvalIdx & 1) * 30; - - char s[8] = { 0 }; - sprintf(s, "%02u %02u", hours, mins); - - Text::draw7Seg(s, 2, 29); -} - -void SchedulingScreen::drawIntervalIndicator() -{ - draw_schedule_indicator(_intvalIdx); -} - -void SchedulingScreen::updateScheduleBar() -{ - draw_schedule_bar(_daysData[_day]); -} - -void SchedulingScreen::setModeAndAdvance(bool daytime) -{ - uint8_t bit_idx = _intvalIdx & 0b111; - uint8_t byte_idx = _intvalIdx >> 3; - uint8_t mask = 1 << bit_idx; - - if (daytime) - _daysData[_day][byte_idx] |= mask; - else - _daysData[_day][byte_idx] &= ~mask; - - ++_intvalIdx; - if (_intvalIdx > 47) - _intvalIdx = 0; - - drawIntervalIndicator(); - drawIntervalDisplay(); - updateScheduleBar(); -} - -void SchedulingScreen::nextInterval() -{ - if (_intvalIdx < 47) { - ++_intvalIdx; - drawIntervalDisplay(); - drawIntervalIndicator(); - } -} - -void SchedulingScreen::prevInterval() -{ - if (_intvalIdx > 0) { - --_intvalIdx; - drawIntervalDisplay(); - drawIntervalIndicator(); - } -} - -void SchedulingScreen::nextDay() -{ - ++_day; - if (_day > 6) - _day = 0; - - _intvalIdx = 0; - draw(); -} - -void SchedulingScreen::prevDay() -{ - --_day; - if (_day > 6) - _day = 6; - - _intvalIdx = 0; - draw(); -} - -void SchedulingScreen::applyChanges() -{ - // TODO - // memcpy(_settings.data.Scheduler.DayData, _daysData, sizeof(Settings::SchedulerDayData) * 7); - // _settings.save(); -} \ No newline at end of file diff --git a/src/ui/SchedulingScreen.h b/src/ui/SchedulingScreen.h deleted file mode 100644 index 990d17a..0000000 --- a/src/ui/SchedulingScreen.h +++ /dev/null @@ -1,62 +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 -*/ - -#pragma once - -#include "Keypad.h" -#include "Screen.h" -#include "Settings.h" - -#include - -#include - -class ISystemClock; - -class SchedulingScreen : public Screen -{ -public: - SchedulingScreen(Settings& settings, const ISystemClock& systemClock); - - void activate() override; - void update() override; - Action keyPress(Keypad::Keys keys) override; - -private: - Settings& _settings; - const ISystemClock& _systemClock; - - uint8_t _day = 0; - uint8_t _intvalIdx = 0; - HeatingZoneController::Schedule _daysData[7]; - uint8_t _menuPressCnt = 0; - - void draw(); - void drawDayName(); - void drawIntervalDisplay(); - void drawIntervalIndicator(); - void updateScheduleBar(); - void setModeAndAdvance(bool daytime); - void nextInterval(); - void prevInterval(); - void nextDay(); - void prevDay(); - void applyChanges(); -}; diff --git a/src/ui/Screen.h b/src/ui/Screen.h index 95f7af4..1c38ff7 100644 --- a/src/ui/Screen.h +++ b/src/ui/Screen.h @@ -1,46 +1,62 @@ #pragma once -#include - +#include "Graphics.h" #include "Keypad.h" +#include "ScreenID.h" -class Screen -{ -public: - explicit Screen(const char* name) - : _name(name) - {} - - const char* name() const - { - return _name; - } +#include +#include +#include - const char* nextScreen() const - { - return _nextScreen; - } +namespace UI +{ + struct Model; - enum class Action + class Screen { - NoAction, - NavigateBack, - NavigateForward + public: + explicit Screen(const int id, Model& model, Graphics& graphics) + : _id{ id } + , _model{ model } + , _graphics{ graphics } + {} + + [[nodiscard]] int id() const + { + return _id; + } + + struct NoAction {}; + + struct Navigate { + int id{ ScreenID::Invalid }; + }; + + using Result = std::variant; + + protected: + Model& model() const + { + return _model; + } + + Graphics& graphics() const + { + return _graphics; + } + + private: + int _id{ ScreenID::Invalid }; + Model& _model; + Graphics& _graphics; }; - Action navigateForward(const char* name) - { - _nextScreen = name; - return Action::NavigateForward; - } - - virtual ~Screen() = default; - - virtual void activate() = 0; - virtual void update() = 0; - virtual Action keyPress(Keypad::Keys keys) = 0; - -private: - const char* const _name; - const char* _nextScreen = nullptr; -}; \ No newline at end of file + template + concept IsScreen = requires(T s, int id, Keypad::Keys keys) { + std::derived_from; + { s.id() } -> std::same_as; + { s.handleKeyPress(keys) } -> std::same_as; + { s.activate() } -> std::same_as; + { s.update() } -> std::same_as; + }; +} \ No newline at end of file diff --git a/src/ui/ScreenID.h b/src/ui/ScreenID.h new file mode 100644 index 0000000..8ffe703 --- /dev/null +++ b/src/ui/ScreenID.h @@ -0,0 +1,17 @@ +#pragma once + +namespace UI::ScreenID { + constexpr int Invalid{ -1 }; + + constexpr int Main{ 100 }; + + constexpr int MainMenu{ 200 }; + constexpr int ZoneSettingsMenu{ 210 }; + constexpr int GeneralSettings{ 220 }; + constexpr int DisplaySettings{ 230 }; + constexpr int DateTimeSettings{ 240 }; + constexpr int DebuggingSettingsScreen{ 250 }; + + constexpr int ZoneSettings{ 300 }; + constexpr int ZoneSchedule{ 310 }; +} \ No newline at end of file diff --git a/src/ui/Screens/DateTimeSettingsScreen.cpp b/src/ui/Screens/DateTimeSettingsScreen.cpp new file mode 100644 index 0000000..7b54c20 --- /dev/null +++ b/src/ui/Screens/DateTimeSettingsScreen.cpp @@ -0,0 +1,57 @@ +#include "DateTimeSettingsScreen.h" + +using namespace UI; +using namespace std::string_view_literals; + +DateTimeSettingsScreen::DateTimeSettingsScreen(Model& model, Graphics& graphics) + : Screen{ ScreenID::DateTimeSettings, model, graphics } + , _menu{ + graphics, + 1, + 7, + MenuItem{ "" } + } +{} + +void DateTimeSettingsScreen::activate() +{ + graphics().drawText(0, 0, "Date/Time Settings"sv, Resources::Fonts::Oled); + + _menu.reset(); + _menu.update(); +} + +void DateTimeSettingsScreen::update() +{ +} + +Screen::Result DateTimeSettingsScreen::handleKeyPress(const Keypad::Keys keys) +{ + using Keys = Keypad::Keys; + + if (keys & Keys::Left) { + _menu.step(UI::StepDirection::Up); + _menu.update(); + } else if (keys & Keys::Right) { + _menu.step(UI::StepDirection::Down); + _menu.update(); + } else if (keys & Keys::Menu) { + if (!(keys & Keys::LongPress)) { + return Navigate{ .id = ScreenID::MainMenu }; + } + } else if (keys & Keys::Boost) { + return selectMenuItem(); + } + + return Result{}; +} + +Screen::Result DateTimeSettingsScreen::selectMenuItem() const +{ + switch (_menu.currentIndex()) { + default: + break; + } + + return Result{}; +} \ No newline at end of file diff --git a/src/ui/Screens/DateTimeSettingsScreen.h b/src/ui/Screens/DateTimeSettingsScreen.h new file mode 100644 index 0000000..7c01196 --- /dev/null +++ b/src/ui/Screens/DateTimeSettingsScreen.h @@ -0,0 +1,23 @@ +#pragma once + +#include "../Menu.h" +#include "../Screen.h" +#include "../ScreenID.h" + +namespace UI +{ + class DateTimeSettingsScreen : public Screen + { + public: + explicit DateTimeSettingsScreen(Model& model, Graphics& graphics); + + void activate(); + void update(); + [[nodiscard]] Result handleKeyPress(Keypad::Keys keys); + + private: + Menu<1> _menu; + + [[nodiscard]] Result selectMenuItem() const; + }; +} \ No newline at end of file diff --git a/src/ui/Screens/DebuggingSettingsScreen.cpp b/src/ui/Screens/DebuggingSettingsScreen.cpp new file mode 100644 index 0000000..cb094ab --- /dev/null +++ b/src/ui/Screens/DebuggingSettingsScreen.cpp @@ -0,0 +1,135 @@ +#include "DebuggingSettingsScreen.h" +#include "Utilities.h" + +#include "../Model.h" + +using namespace UI; +using namespace std::string_view_literals; + +namespace +{ + char _logLevelValueLabel[6]{}; +} + +DebuggingSettingsScreen::DebuggingSettingsScreen(Model& model, Graphics& graphics) + : Screen{ ScreenID::DebuggingSettingsScreen, model, graphics } + , _menu{ + graphics, + 1, + 7, + MenuItem{ "Log Level", _logLevelValueLabel }, + MenuItem{ "[Reboot]" }, + MenuItem{ + "Firmware Ver.", + [&] { + static char s[12]{}; + const auto [major, minor, patch] = model.firmwareVersion.parts(); + snprintf(s, sizeof(s), "%u.%u.%u", major, minor, patch); + return s; + }() + }, + MenuItem{ + "IoT Base Ver.", + [&] { + static char s[12]{}; + const auto [major, minor, patch] = model.baseVersion.parts(); + snprintf(s, sizeof(s), "%u.%u.%u", major, minor, patch); + return s; + }() + } + } +{} + +void DebuggingSettingsScreen::activate() +{ + graphics().drawText(0, 0, "Debugging Settings"sv, Resources::Fonts::Oled); + + updateValueLabels(); + _menu.reset(); + _menu.update(); +} + +void DebuggingSettingsScreen::update() +{ +} + +Screen::Result DebuggingSettingsScreen::handleKeyPress(const Keypad::Keys keys) +{ + using Keys = Keypad::Keys; + + if (keys & Keys::Left) { + _menu.step(UI::StepDirection::Up); + _menu.update(); + } else if (keys & Keys::Right) { + _menu.step(UI::StepDirection::Down); + _menu.update(); + } else if (keys & Keys::Menu) { + if (!(keys & Keys::LongPress)) { + return Navigate{ .id = ScreenID::MainMenu }; + } + } else if (keys & Keys::Boost) { + return selectMenuItem(); + } else if (keys & Keys::Plus) { + stepSelectedSetting(StepDirection::Up); + } else if (keys & Keys::Minus) { + stepSelectedSetting(StepDirection::Down); + } + + return Result{}; +} + +Screen::Result DebuggingSettingsScreen::selectMenuItem() const +{ + switch (_menu.currentIndex()) { + case 1: + system_restart(); + break; + + default: + break; + } + + return Result{}; +} + +void DebuggingSettingsScreen::stepSelectedSetting(const StepDirection direction) +{ + switch (_menu.currentIndex()) { + case 0: + model().settings.system.maximumLogLevel = stepValue( + model().settings.system.maximumLogLevel, + direction, + static_cast(Log::Severity::Error), + static_cast(Log::Severity::Debug) + ); + break; + + default: + return; + } + + updateValueLabels(); + _menu.updateSelectedItem(); +} + +void DebuggingSettingsScreen::updateValueLabels() +{ + snprintf( + _logLevelValueLabel, + sizeof(_logLevelValueLabel), + "%s", + [&] { + switch (model().settings.system.maximumLogLevel) { + case 0: + return "Error"; + case 1: + return "Warn."; + case 2: + return "Info"; + case 3: + return "Debug"; + } + return "Unkn."; + }() + ); +} diff --git a/src/ui/Screens/DebuggingSettingsScreen.h b/src/ui/Screens/DebuggingSettingsScreen.h new file mode 100644 index 0000000..17a4823 --- /dev/null +++ b/src/ui/Screens/DebuggingSettingsScreen.h @@ -0,0 +1,26 @@ +#pragma once + +#include "../Menu.h" +#include "../Screen.h" +#include "../ScreenID.h" + +namespace UI +{ + class DebuggingSettingsScreen : public Screen + { + public: + explicit DebuggingSettingsScreen(Model& model, Graphics& graphics); + + void activate(); + void update(); + [[nodiscard]] Result handleKeyPress(Keypad::Keys keys); + + private: + Menu<4> _menu; + + [[nodiscard]] Result selectMenuItem() const; + void stepSelectedSetting(StepDirection direction); + + void updateValueLabels(); + }; +} \ No newline at end of file diff --git a/src/ui/Screens/DisplaySettingsScreen.cpp b/src/ui/Screens/DisplaySettingsScreen.cpp new file mode 100644 index 0000000..7812740 --- /dev/null +++ b/src/ui/Screens/DisplaySettingsScreen.cpp @@ -0,0 +1,113 @@ +#include "DisplaySettingsScreen.h" + +#include "../Model.h" + +using namespace UI; +using namespace std::string_view_literals; + +namespace +{ + // FIXME There's a potential bug in extensa GCC, and because of that, it can't take the proper address of a char array until something is written into it + char _brightnessValueLabel[4]{}; + char _timeoutValueLabel[6]{}; +} + +DisplaySettingsScreen::DisplaySettingsScreen(Model& model, Graphics& graphics) + : Screen{ ScreenID::DisplaySettings, model, graphics } + , _menu{ + graphics, + 1, + 7, + MenuItem{ "Brightness", _brightnessValueLabel }, + MenuItem{ "Timeout", _timeoutValueLabel } + } +{} + +void DisplaySettingsScreen::activate() +{ + graphics().drawText(0, 0, "Display Settings"sv, Resources::Fonts::Oled); + + updateValueLabels(); + _menu.reset(); + _menu.update(); +} + +void DisplaySettingsScreen::update() +{ +} + +Screen::Result DisplaySettingsScreen::handleKeyPress(const Keypad::Keys keys) +{ + using Keys = Keypad::Keys; + + if (keys & Keys::Left) { + _menu.step(UI::StepDirection::Up); + _menu.update(); + } else if (keys & Keys::Right) { + _menu.step(UI::StepDirection::Down); + _menu.update(); + } else if (keys & Keys::Menu) { + if (!(keys & Keys::LongPress)) { + return Navigate{ .id = ScreenID::MainMenu }; + } + } else if (keys & Keys::Boost) { + return selectMenuItem(); + } else if (keys & Keys::Plus) { + stepSelectedSetting(StepDirection::Up); + } else if (keys & Keys::Minus) { + stepSelectedSetting(StepDirection::Down); + } + + return Result{}; +} + +Screen::Result DisplaySettingsScreen::selectMenuItem() const +{ + switch (_menu.currentIndex()) { + default: + break; + } + + return Result{}; +} + +void DisplaySettingsScreen::stepSelectedSetting(const StepDirection direction) +{ + auto& settings = model().settings.system.display; + + switch (_menu.currentIndex()) { + case 0: + settings.brightness += direction == StepDirection::Up ? 1 : -1; + Display::setContrast(settings.brightness); + break; + + case 1: + settings.timeoutSecs += direction == StepDirection::Up ? 1 : -1; + break; + + default: + return; + } + + updateValueLabels(); + _menu.updateSelectedItem(); +} + +void DisplaySettingsScreen::updateValueLabels() +{ + auto& settings = model().settings.system.display; + + snprintf( + _brightnessValueLabel, + sizeof(_brightnessValueLabel), + "%u", + settings.brightness + ); + + snprintf( + _timeoutValueLabel, + sizeof(_timeoutValueLabel), + "%u s", + settings.timeoutSecs + ); +} diff --git a/src/ui/Screens/DisplaySettingsScreen.h b/src/ui/Screens/DisplaySettingsScreen.h new file mode 100644 index 0000000..4f2358a --- /dev/null +++ b/src/ui/Screens/DisplaySettingsScreen.h @@ -0,0 +1,26 @@ +#pragma once + +#include "../Menu.h" +#include "../Screen.h" +#include "../ScreenID.h" + +namespace UI +{ + class DisplaySettingsScreen : public Screen + { + public: + explicit DisplaySettingsScreen(Model& model, Graphics& graphics); + + void activate(); + void update(); + [[nodiscard]] Result handleKeyPress(Keypad::Keys keys); + + private: + Menu<2> _menu; + + [[nodiscard]] Result selectMenuItem() const; + void stepSelectedSetting(StepDirection direction); + + void updateValueLabels(); + }; +} \ No newline at end of file diff --git a/src/ui/Screens/GeneralSettingsScreen.cpp b/src/ui/Screens/GeneralSettingsScreen.cpp new file mode 100644 index 0000000..4e221a5 --- /dev/null +++ b/src/ui/Screens/GeneralSettingsScreen.cpp @@ -0,0 +1,107 @@ +#include "GeneralSettingsScreen.h" + +#include "../Model.h" + +using namespace UI; +using namespace std::string_view_literals; + +namespace +{ + char _masterEnableValueLabel[4]{}; + char _energySaverValueLabel[4]{}; +} + +GeneralSettingsScreen::GeneralSettingsScreen(Model& model, Graphics& graphics) + : Screen{ ScreenID::GeneralSettings, model, graphics } + , _menu{ + graphics, + 1, + 7, + MenuItem{ "Master Enable", _masterEnableValueLabel }, + MenuItem{ "Energy Optim.", _energySaverValueLabel } + } +{} + +void GeneralSettingsScreen::activate() +{ + graphics().drawText(0, 0, "General Settings"sv, Resources::Fonts::Oled); + + updateValueLabels(); + _menu.reset(); + _menu.update(); +} + +void GeneralSettingsScreen::update() +{ +} + +Screen::Result GeneralSettingsScreen::handleKeyPress(const Keypad::Keys keys) +{ + using Keys = Keypad::Keys; + + if (keys & Keys::Left) { + _menu.step(UI::StepDirection::Up); + _menu.update(); + } else if (keys & Keys::Right) { + _menu.step(UI::StepDirection::Down); + _menu.update(); + } else if (keys & Keys::Menu) { + if (!(keys & Keys::LongPress)) { + return Navigate{ .id = ScreenID::MainMenu }; + } + } else if (keys & Keys::Boost) { + return selectMenuItem(); + } else if (keys & Keys::Plus) { + stepSelectedSetting(StepDirection::Up); + } else if (keys & Keys::Minus) { + stepSelectedSetting(StepDirection::Down); + } + + return Result{}; +} + +Screen::Result GeneralSettingsScreen::selectMenuItem() const +{ + switch (_menu.currentIndex()) { + default: + break; + } + + return Result{}; +} + +void GeneralSettingsScreen::stepSelectedSetting(const StepDirection direction) +{ + switch (_menu.currentIndex()) { + case 0: + model().settings.system.masterEnable ^= true; + break; + + case 1: + model().settings.system.energyOptimizerEnabled ^= true; + break; + + default: + return; + } + + updateValueLabels(); + _menu.updateSelectedItem(); +} + +void GeneralSettingsScreen::updateValueLabels() +{ + snprintf( + _masterEnableValueLabel, + sizeof(_masterEnableValueLabel), + "%s", + model().settings.system.masterEnable ? "On" : "Off" + ); + + snprintf( + _energySaverValueLabel, + sizeof(_energySaverValueLabel), + "%s", + model().settings.system.energyOptimizerEnabled ? "On" : "Off" + ); +} diff --git a/src/ui/Screens/GeneralSettingsScreen.h b/src/ui/Screens/GeneralSettingsScreen.h new file mode 100644 index 0000000..f881d81 --- /dev/null +++ b/src/ui/Screens/GeneralSettingsScreen.h @@ -0,0 +1,26 @@ +#pragma once + +#include "../Menu.h" +#include "../Screen.h" +#include "../ScreenID.h" + +namespace UI +{ + class GeneralSettingsScreen : public Screen + { + public: + explicit GeneralSettingsScreen(Model& model, Graphics& graphics); + + void activate(); + void update(); + [[nodiscard]] Result handleKeyPress(Keypad::Keys keys); + + private: + Menu<2> _menu; + + [[nodiscard]] Result selectMenuItem() const; + void stepSelectedSetting(StepDirection direction); + + void updateValueLabels(); + }; +} \ No newline at end of file diff --git a/src/ui/Screens/MainMenuScreen.cpp b/src/ui/Screens/MainMenuScreen.cpp new file mode 100644 index 0000000..8aea58f --- /dev/null +++ b/src/ui/Screens/MainMenuScreen.cpp @@ -0,0 +1,100 @@ +#include "MainMenuScreen.h" +#include "Utilities.h" + +#include "../Model.h" + +#include + +using namespace UI; +using namespace std::string_view_literals; + +namespace +{ +} + +MainMenuScreen::MainMenuScreen(Model& model, Graphics& graphics) + : Screen{ ScreenID::MainMenu, model, graphics } + , _menu{ + graphics, + 1, + 7, + MenuItem{ "[Zone Settings]" }, + MenuItem{ "[General Settings]" }, + MenuItem{ "[Display Settings]" }, + MenuItem{ "[Date/Time Sett.]" }, + MenuItem{ "[Debugging Sett.]" }, + } +{} + +void MainMenuScreen::activate() +{ + graphics().drawText(0, 0, "Main Menu"sv, Resources::Fonts::Oled); + + updateValueLabels(); + _menu.reset(); + _menu.update(); +} + +void MainMenuScreen::update() +{ +} + +Screen::Result MainMenuScreen::handleKeyPress(const Keypad::Keys keys) +{ + using Keys = Keypad::Keys; + + if (keys & Keys::Left) { + _menu.step(UI::StepDirection::Up); + _menu.update(); + } else if (keys & Keys::Right) { + _menu.step(UI::StepDirection::Down); + _menu.update(); + } else if (keys & Keys::Menu) { + if (!(keys & Keys::LongPress)) { + return Navigate{ .id = ScreenID::Main }; + } + } else if (keys & Keys::Boost) { + return selectMenuItem(); + } else if (keys & Keys::Plus) { + stepSelectedSetting(StepDirection::Up); + } else if (keys & Keys::Minus) { + stepSelectedSetting(StepDirection::Down); + } + + return Result{}; +} + +Screen::Result MainMenuScreen::selectMenuItem() const +{ + switch (_menu.currentIndex()) { + case 0: + return Navigate{ .id = ScreenID::ZoneSettingsMenu }; + case 1: + return Navigate{ .id = ScreenID::GeneralSettings }; + case 2: + return Navigate{ .id = ScreenID::DisplaySettings }; + case 3: + return Navigate{ .id = ScreenID::DateTimeSettings }; + case 4: + return Navigate{ .id = ScreenID::DebuggingSettingsScreen }; + default: + break; + } + + return Result{}; +} + +void MainMenuScreen::stepSelectedSetting(const StepDirection direction) +{ + switch (_menu.currentIndex()) { + default: + return; + } + + updateValueLabels(); + _menu.updateSelectedItem(); +} + +void MainMenuScreen::updateValueLabels() +{ +} \ No newline at end of file diff --git a/src/ui/Screens/MainMenuScreen.h b/src/ui/Screens/MainMenuScreen.h new file mode 100644 index 0000000..dac522c --- /dev/null +++ b/src/ui/Screens/MainMenuScreen.h @@ -0,0 +1,26 @@ +#pragma once + +#include "../Menu.h" +#include "../Screen.h" +#include "../ScreenID.h" + +namespace UI +{ + class MainMenuScreen : public Screen + { + public: + explicit MainMenuScreen(Model& model, Graphics& graphics); + + void activate(); + void update(); + [[nodiscard]] Result handleKeyPress(Keypad::Keys keys); + + private: + Menu<5> _menu; + + [[nodiscard]] Result selectMenuItem() const; + void stepSelectedSetting(StepDirection direction); + + void updateValueLabels(); + }; +} \ No newline at end of file diff --git a/src/ui/Screens/MainScreen.cpp b/src/ui/Screens/MainScreen.cpp new file mode 100644 index 0000000..bbea00f --- /dev/null +++ b/src/ui/Screens/MainScreen.cpp @@ -0,0 +1,258 @@ +#include "MainScreen.h" + +#include "../Menu.h" + +#include +#include + +using namespace std::string_view_literals; + +using namespace UI; + +namespace Positions +{ + struct Position + { + unsigned x{}; + unsigned line{}; + }; + + constexpr Position Clock{ 0, 0 }; + constexpr Position ClockDayOfWeek{ 31, 0 }; + constexpr Position InternalTemperature{ 53, 0 }; + constexpr Position HeatingState{ 81, 0 }; + constexpr Position ConnectionState{ 112, 0 }; + + namespace Zone + { + constexpr auto BaseLine{ 2 }; + constexpr std::array Columns{ 0, 64 }; + + constexpr auto Name{ 0 }; + constexpr auto Temperature{ 16 }; + constexpr auto SLabel{ 4 }; + constexpr auto Icon{ 40 }; + } +} + +MainScreen::MainScreen(Model& model, Graphics& graphics) + : Screen{ ScreenID::Main, model, graphics } +{} + +void MainScreen::activate() +{ + // testMenu.update(); + update(); +} + +void MainScreen::update() +{ + // testMenu.update(); + drawClock(); + drawInternalTemperature(); + drawHeatingState(); + drawConnectionStatus(); + drawZoneStatuses(); +} + +Screen::Result MainScreen::handleKeyPress(const Keypad::Keys keys) +{ + using Keys = Keypad::Keys; + + // if (keys & Keys::Plus) { + // testMenu.step(UI::StepDirection::Up); + // testMenu.update(); + // } else if (keys & Keys::Minus) { + // testMenu.step(UI::StepDirection::Down); + // testMenu.update(); + // } + + if (keys & Keys::Menu) { + // Avoid entering the menu while exiting + // from another screen with long press + if (!(keys & Keys::LongPress)) { + return Navigate{ .id = ScreenID::MainMenu }; + } + } + + return NoAction{}; +} + +void MainScreen::drawClock() const +{ + char clock[10]{}; + snprintf(clock, sizeof(clock), "%02d:%02d", model().clock.hours, model().clock.minutes); + + graphics().drawText( + Positions::Clock.x, + Positions::Clock.line, + clock, + Resources::Fonts::Oled + ); + + graphics().drawVerticalSeparator(Positions::ClockDayOfWeek.x, Positions::ClockDayOfWeek.line); + + graphics().drawShortWeekday( + Positions::ClockDayOfWeek.x + 3, + Positions::ClockDayOfWeek.line, + model().clock.dayOfWeek + ); +} + +void MainScreen::drawInternalTemperature() const +{ + char text[9]{}; + snprintf( + text, + sizeof(text), + "%2d.%d", + model().internalTemperature / 100, + model().internalTemperature % 100 / 10 + ); + + graphics().drawVerticalSeparator(Positions::InternalTemperature.x, Positions::InternalTemperature.line); + + graphics().drawText( + Positions::InternalTemperature.x + 3, + Positions::InternalTemperature.line, + text, + Resources::Fonts::Oled + ); +} + +void MainScreen::drawHeatingState() const +{ + const auto text{ + model().masterEnable + ? (model().heating ? "Heat"sv : "Idle"sv) + : "Off "sv + }; + + graphics().drawVerticalSeparator(Positions::HeatingState.x, Positions::HeatingState.line); + + graphics().drawText( + Positions::HeatingState.x + 3, + Positions::HeatingState.line, + text, + Resources::Fonts::Oled + ); +} + +void MainScreen::drawZoneStatus( + const unsigned line, + const unsigned column, + const Model::Zone& zoneModel +) const +{ + if (line > 7 || column > Positions::Zone::Columns.size()) { + return; + } + + const auto left{ Positions::Zone::Columns[column] }; + + char buf[14]{}; + + const auto formatTemperatureIntoBuf = [&](const int temperature) { + snprintf( + buf, + sizeof(buf), + "%2d.%d", + temperature / 10, + temperature % 10 + ); + }; + + snprintf(buf, sizeof(buf), "%02d", zoneModel.zoneNumber); + graphics().drawText( + left + Positions::Zone::Name, + line, + buf, + Resources::Fonts::Oled + ); + + formatTemperatureIntoBuf(zoneModel.currentTemperature); + graphics().drawText( + left + Positions::Zone::Temperature, + line, + buf, + Resources::Fonts::Oled + ); + + graphics().drawText( + left + Positions::Zone::SLabel, + line + 1, + "S:", + Resources::Fonts::Oled + ); + + if (zoneModel.targetTemperature.has_value()) { + formatTemperatureIntoBuf(*zoneModel.targetTemperature); + } + graphics().drawText( + left + Positions::Zone::Temperature, + line + 1, + zoneModel.targetTemperature.has_value() ? buf : "--.-", + Resources::Fonts::Oled + ); + + // TODO create bitmaps (2-page) of status icons + + // FIXME until the bitmaps are ready, show the state as text + graphics().drawText( + left + Positions::Zone::Icon + 2, + line, + [&] { + switch (zoneModel.status) { + case Model::Zone::Status::Off: + return "OFF"sv; + case Model::Zone::Status::Idle: + return "IDL"sv; + case Model::Zone::Status::Heating: + return "HEA"sv; + case Model::Zone::Status::Holiday: + return "HOL"sv; + case Model::Zone::Status::Boost: + return "BST"sv; + case Model::Zone::Status::WindowOpen: + return "WND"sv; + case Model::Zone::Status::WindowLockout: + return "WLO"sv; + } + return "UNK"sv; + }(), + Resources::Fonts::Oled + ); +} + +void MainScreen::drawZoneStatuses() +{ + unsigned column{}; + unsigned line{ Positions::Zone::BaseLine }; + + for (const auto& zone : model().zones) { + drawZoneStatus(line, column, zone); + + line += 2; + if (line >= graphics().Lines) { + ++column; + line = Positions::Zone::BaseLine; + } + } +} + +void MainScreen::drawConnectionStatus() +{ + graphics().drawVerticalSeparator(Positions::ConnectionState.x, Positions::ConnectionState.line); + + char s[3]{}; + + s[0] = model().wifiConnected ? 'W' : '-'; + s[1] = model().mqttConnected ? 'M' : '-'; + + graphics().drawText( + Positions::ConnectionState.x + 3, + Positions::ConnectionState.line, + s, + Resources::Fonts::Oled + ); +} \ No newline at end of file diff --git a/src/ui/Screens/MainScreen.h b/src/ui/Screens/MainScreen.h new file mode 100644 index 0000000..77a2d46 --- /dev/null +++ b/src/ui/Screens/MainScreen.h @@ -0,0 +1,34 @@ +#pragma once + +#include "Config.h" + +#include "../Model.h" +#include "../Screen.h" +#include "../ScreenID.h" + +#include + +namespace UI +{ + class MainScreen : public Screen + { + public: + MainScreen(Model& model, Graphics& graphics); + + void activate(); + void update(); + [[nodiscard]] Result handleKeyPress(Keypad::Keys keys); + + private: + void drawClock() const; + void drawInternalTemperature() const; + void drawHeatingState() const; + void drawZoneStatus( + unsigned line, + unsigned column, + const Model::Zone& zoneModel + ) const; + void drawZoneStatuses(); + void drawConnectionStatus(); + }; +} \ No newline at end of file diff --git a/src/ui/Screens/Utilities.h b/src/ui/Screens/Utilities.h new file mode 100644 index 0000000..1f9e3e5 --- /dev/null +++ b/src/ui/Screens/Utilities.h @@ -0,0 +1,57 @@ +#pragma once + +#include "../Types.h" + +#include +#include + +namespace UI +{ + template + void formatTemperatureValue(CharArray& s, const Value value) + { + snprintf( + s, + sizeof(s), + "%d.%d C", + value / 10, + value % 10 + ); + } + + template + void formatValueWithSuffix(CharArray& s, const Value value, const char suffix) + { + snprintf( + s, + sizeof(s), + "%d %c", + value, + suffix + ); + } + + template + ValueType stepValue( + const ValueType value, + const StepDirection direction, + const ValueType min = std::numeric_limits::min(), + const ValueType max = std::numeric_limits::max(), + const ValueType step = 1 + ) + { + if (direction == StepDirection::Up) { + if (value <= (max - step)) { + return value + step; + } else { + return min; + } + } else { + if (value >= (min + step)) { + return value - step; + } else { + return max; + } + } + } +} \ No newline at end of file diff --git a/src/ui/Screens/ZoneScheduleScreen.cpp b/src/ui/Screens/ZoneScheduleScreen.cpp new file mode 100644 index 0000000..cad2445 --- /dev/null +++ b/src/ui/Screens/ZoneScheduleScreen.cpp @@ -0,0 +1,125 @@ +#include "ZoneScheduleScreen.h" + +#include "../Graphics.h" +#include "../Model.h" + +#include + +using namespace UI; +using namespace std::string_view_literals; + +ZoneScheduleScreen::ZoneScheduleScreen(Model& model, Graphics& graphics) + : Screen{ ScreenID::ZoneSchedule, model, graphics } +{} + +void ZoneScheduleScreen::activate() +{ + auto x = graphics().drawText(0, 0, "Schedule: "sv, Resources::Fonts::Oled); + graphics().drawText( + x, + 0, + std::to_string(model().zones[model().navigation.selectedZoneIndex].zoneNumber), + Resources::Fonts::Oled + ); + + drawScheduleBar(); + drawSchedulePositionIndicator(); + drawWeekday(); +} + +void ZoneScheduleScreen::update() +{ +} + +Screen::Result ZoneScheduleScreen::handleKeyPress(const Keypad::Keys keys) +{ + using Keys = Keypad::Keys; + + if (keys & Keys::Plus) { + setScheduleBit(true); + drawScheduleBar(); + stepSchedulePosition(StepDirection::Up); + } else if (keys & Keys::Minus) { + setScheduleBit(false); + drawScheduleBar(); + stepSchedulePosition(StepDirection::Up); + } else if (keys & Keys::Menu) { + if (!(keys & Keys::LongPress)) { + return Navigate{ .id = ScreenID::ZoneSettings }; + } + } else if (keys & Keys::Boost) { + if (++_scheduleDay > 6) { + _scheduleDay = 0; + } + _scheduleBitIndex = 0; + drawWeekday(); + drawScheduleBar(); + drawSchedulePositionIndicator(); + } else if (keys & Keys::Left) { + stepSchedulePosition(StepDirection::Down); + } else if (keys & Keys::Right) { + stepSchedulePosition(StepDirection::Up); + } + + return Result{}; +} + +void ZoneScheduleScreen::drawScheduleBar() +{ + graphics().drawScheduleBar( + model().settings.heating.zones[model().navigation.selectedZoneIndex].schedule, + _scheduleDay * 6 + ); +} + +void ZoneScheduleScreen::drawSchedulePositionIndicator() +{ + graphics().drawScheduleBarPositionIndicator(_scheduleBitIndex); + + auto mins = (_scheduleBitIndex & 1) * 30; + auto hours = _scheduleBitIndex >> 1; + + char buf[14]{}; + snprintf(buf, sizeof(buf), "%2u:%02u", hours, mins); + + const auto x = graphics().drawText(0, 3, "Time: "sv, Resources::Fonts::Oled); + graphics().drawText(x, 3, buf, Resources::Fonts::Oled); +} + +void ZoneScheduleScreen::drawWeekday() +{ + const auto x = graphics().drawText(0, 2, "Day: "sv, Resources::Fonts::Oled); + graphics().drawShortWeekday(x, 2, _scheduleDay); +} + +void ZoneScheduleScreen::stepSchedulePosition(const StepDirection direction) +{ + switch (direction) { + case StepDirection::Down: + if (_scheduleBitIndex > 0) { + --_scheduleBitIndex; + drawSchedulePositionIndicator(); + } + break; + + case StepDirection::Up: + if (_scheduleBitIndex < 47) { + ++_scheduleBitIndex; + drawSchedulePositionIndicator(); + } + break; + } +} + +void ZoneScheduleScreen::setScheduleBit(const bool on) +{ + auto& schedule = model().settings.heating.zones[model().navigation.selectedZoneIndex].schedule; + const auto byteOffset = _scheduleDay * 6u + _scheduleBitIndex / 8u; + const auto bitMask = 1 << (7 - _scheduleBitIndex % 8); // MSB-first + + if (on) { + schedule[byteOffset] |= bitMask; + } else { + schedule[byteOffset] &= ~bitMask; + } +} \ No newline at end of file diff --git a/src/ui/Screens/ZoneScheduleScreen.h b/src/ui/Screens/ZoneScheduleScreen.h new file mode 100644 index 0000000..796a98c --- /dev/null +++ b/src/ui/Screens/ZoneScheduleScreen.h @@ -0,0 +1,29 @@ +#pragma once + +#include "../Screen.h" +#include "../ScreenID.h" +#include "../Types.h" + +namespace UI +{ + class ZoneScheduleScreen : public Screen + { + public: + explicit ZoneScheduleScreen(Model& model, Graphics& graphics); + + void activate(); + void update(); + [[nodiscard]] Result handleKeyPress(Keypad::Keys keys); + + private: + unsigned _scheduleBitIndex{}; + unsigned _scheduleDay{}; + + void drawScheduleBar(); + void drawSchedulePositionIndicator(); + void drawWeekday(); + + void stepSchedulePosition(StepDirection direction); + void setScheduleBit(bool on); + }; +} \ No newline at end of file diff --git a/src/ui/Screens/ZoneSettingsMenuScreen.cpp b/src/ui/Screens/ZoneSettingsMenuScreen.cpp new file mode 100644 index 0000000..81df683 --- /dev/null +++ b/src/ui/Screens/ZoneSettingsMenuScreen.cpp @@ -0,0 +1,56 @@ +#include "ZoneSettingsMenuScreen.h" + +#include "../Model.h" + +using namespace UI; +using namespace std::string_view_literals; + +ZoneSettingsMenuScreen::ZoneSettingsMenuScreen(Model& model, Graphics& graphics) + : Screen{ ScreenID::ZoneSettingsMenu, model, graphics } + , _menu{ + graphics, + 1, + 7, + // FIXME use dynamic zone names to match HeatingZone objects + MenuItem{ "[Zone 0]" }, + MenuItem{ "[Zone 1]" }, + MenuItem{ "[Zone 2]" }, + MenuItem{ "[Zone 3]" }, + MenuItem{ "[Zone 10]" }, + MenuItem{ "[Zone 11]" } + } +{} + +void ZoneSettingsMenuScreen::activate() +{ + graphics().drawText(0, 0, "Zone Settings", Resources::Fonts::Oled); + + _menu.reset(); + _menu.update(); +} + +void ZoneSettingsMenuScreen::update() +{ +} + +Screen::Result ZoneSettingsMenuScreen::handleKeyPress(const Keypad::Keys keys) +{ + using Keys = Keypad::Keys; + + if (keys & Keys::Left) { + _menu.step(UI::StepDirection::Up); + _menu.update(); + } else if (keys & Keys::Right) { + _menu.step(UI::StepDirection::Down); + _menu.update(); + } else if (keys & Keys::Menu) { + if (!(keys & Keys::LongPress)) { + return Navigate{ .id = ScreenID::MainMenu }; + } + } else if (keys & Keys::Boost) { + model().navigation.selectedZoneIndex = _menu.currentIndex(); + return Navigate{ .id = ScreenID::ZoneSettings }; + } + + return Result{}; +} diff --git a/src/ui/Screens/ZoneSettingsMenuScreen.h b/src/ui/Screens/ZoneSettingsMenuScreen.h new file mode 100644 index 0000000..3f1daf9 --- /dev/null +++ b/src/ui/Screens/ZoneSettingsMenuScreen.h @@ -0,0 +1,21 @@ +#pragma once + +#include "../Menu.h" +#include "../Screen.h" +#include "../ScreenID.h" + +namespace UI +{ + class ZoneSettingsMenuScreen : public Screen + { + public: + ZoneSettingsMenuScreen(Model& model, Graphics& graphics); + + void activate(); + void update(); + [[nodiscard]] Result handleKeyPress(Keypad::Keys keys); + + private: + Menu<6> _menu; + }; +} \ No newline at end of file diff --git a/src/ui/Screens/ZoneSettingsScreen.cpp b/src/ui/Screens/ZoneSettingsScreen.cpp new file mode 100644 index 0000000..7031bb5 --- /dev/null +++ b/src/ui/Screens/ZoneSettingsScreen.cpp @@ -0,0 +1,254 @@ +#include "ZoneSettingsScreen.h" +#include "Utilities.h" + +#include "../Model.h" + +#include + +using namespace UI; +using namespace std::string_view_literals; + +namespace +{ + char _modeValueLabel[8]{}; + char _highTargetValueLabel[16]{}; + char _lowTargetValueLabel[16]{}; + char _holidayTargetValueLabel[16]{}; + char _boostInitialDurationValueLabel[11]{}; + char _boostExtensionDurationValueLabel[11]{}; + char _overrideTimeoutValueLabel[11]{}; + char _heatingStartDelayValueLabel[11]{}; + char _windowLockoutDurationValueLabel[11]{}; + char _heatingOvershootValueLabel[16]{}; + char _heatingUndershootValueLabel[16]{}; +} + +ZoneSettingsScreen::ZoneSettingsScreen(Model& model, Graphics& graphics) + : Screen{ ScreenID::ZoneSettings, model, graphics } + , _menu{ + graphics, + 1, + 7, + MenuItem{ "[Schedule]" }, + MenuItem{ "Mode", _modeValueLabel }, + MenuItem{ "High Target", _highTargetValueLabel }, + MenuItem{ "Low Target", _lowTargetValueLabel }, + MenuItem{ "Holiday Tgt.", _holidayTargetValueLabel }, + MenuItem{ "Bst.Init.Dur.", _boostInitialDurationValueLabel }, + MenuItem{ "Bst.Ext.Dur.", _boostExtensionDurationValueLabel }, + MenuItem{ "Ovrrd. T.out.", _overrideTimeoutValueLabel }, + MenuItem{ "Heat.Strt.Dly.", _heatingStartDelayValueLabel }, + MenuItem{ "Wnd.Lckout.Dur.", _windowLockoutDurationValueLabel }, + MenuItem{ "Oversht.Tmp.", _heatingOvershootValueLabel }, + MenuItem{ "Undersht.Tmp.", _heatingUndershootValueLabel }, + } +{} + +void ZoneSettingsScreen::activate() +{ + auto x = graphics().drawText(0, 0, "Zone Settings: "sv, Resources::Fonts::Oled); + graphics().drawText( + x, + 0, + std::to_string(model().zones[model().navigation.selectedZoneIndex].zoneNumber), + Resources::Fonts::Oled + ); + + updateValueLabels(); + _menu.reset(); + _menu.update(); +} + +void ZoneSettingsScreen::update() +{ +} + +Screen::Result ZoneSettingsScreen::handleKeyPress(const Keypad::Keys keys) +{ + using Keys = Keypad::Keys; + + if (keys & Keys::Left) { + _menu.step(UI::StepDirection::Up); + _menu.update(); + } else if (keys & Keys::Right) { + _menu.step(UI::StepDirection::Down); + _menu.update(); + } else if (keys & Keys::Menu) { + if (!(keys & Keys::LongPress)) { + return Navigate{ .id = ScreenID::ZoneSettingsMenu }; + } + } else if (keys & Keys::Boost) { + return selectMenuItem(); + } else if (keys & Keys::Plus) { + stepSelectedSetting(StepDirection::Up); + } else if (keys & Keys::Minus) { + stepSelectedSetting(StepDirection::Down); + } + + return Result{}; +} + +Screen::Result ZoneSettingsScreen::selectMenuItem() const +{ + switch (_menu.currentIndex()) { + case 0: + return Navigate{ .id = ScreenID::ZoneSchedule }; + default: + break; + } + + return Result{}; +} + +void ZoneSettingsScreen::stepSelectedSetting(const StepDirection direction) +{ + auto& zone = model().settings.heating.zones[model().navigation.selectedZoneIndex]; + + switch (_menu.currentIndex()) { + case 1: { + zone.state.mode = static_cast( + stepValue( + static_cast(zone.state.mode), + direction, + static_cast(HeatingZoneController::Mode::Off), + static_cast(HeatingZoneController::Mode::Holiday) + ) + ); + break; + } + + case 2: + zone.state.highTargetTemperature = stepValue( + zone.state.highTargetTemperature, + direction, + 100, + 300 + ); + break; + + case 3: + zone.state.lowTargetTemperature = stepValue( + zone.state.lowTargetTemperature, + direction, + 100, + 300 + ); + break; + + case 4: + zone.config.holidayModeTemperature = stepValue( + zone.config.holidayModeTemperature, + direction, + 100, + 300 + ); + break; + + case 5: + zone.config.boostInitialDurationSeconds = stepValue( + zone.config.boostInitialDurationSeconds, + direction, + 5 * 60u, + 60 * 60u, + 60u + ); + break; + + case 6: + zone.config.boostExtensionDurationSeconds = stepValue( + zone.config.boostExtensionDurationSeconds, + direction, + 5 * 60u, + 60 * 60u, + 60u + ); + break; + + case 7: + zone.config.overrideTimeoutSeconds = stepValue( + zone.config.overrideTimeoutSeconds, + direction, + 30 * 60u, + 180 * 60u, + 10 * 60u + ); + break; + + case 8: + zone.config.heatingStartDelaySeconds = stepValue( + zone.config.heatingStartDelaySeconds, + direction, + 0 * 60u, + 60 * 60u, + 60u + ); + break; + + case 9: + zone.config.openWindowLockoutDurationSeconds = stepValue( + zone.config.openWindowLockoutDurationSeconds, + direction, + 0 * 60u, + 60 * 60u, + 60u + ); + break; + + case 10: + zone.config.heatingOvershoot = stepValue( + zone.config.heatingOvershoot, + direction, + 1, + 10 + ); + break; + + case 11: + zone.config.heatingUndershoot = stepValue( + zone.config.heatingUndershoot, + direction, + 1, + 10 + ); + break; + + default: + return; + } + + updateValueLabels(); + _menu.updateSelectedItem(); +} + +void ZoneSettingsScreen::updateValueLabels() +{ + const auto& zone = model().settings.heating.zones[model().navigation.selectedZoneIndex]; + + snprintf( + _modeValueLabel, + sizeof(_modeValueLabel), + "%s", + [&] { + switch (zone.state.mode) { + case HeatingZoneController::Mode::Off: + return "Off"; + case HeatingZoneController::Mode::Auto: + return "Auto"; + case HeatingZoneController::Mode::Holiday: + return "Holiday"; + } + return "Unknown"; + }() + ); + + formatTemperatureValue(_highTargetValueLabel, zone.state.highTargetTemperature); + formatTemperatureValue(_lowTargetValueLabel, zone.state.lowTargetTemperature); + formatTemperatureValue(_holidayTargetValueLabel, zone.config.holidayModeTemperature); + formatValueWithSuffix(_boostInitialDurationValueLabel, zone.config.boostInitialDurationSeconds / 60u, 'm'); + formatValueWithSuffix(_boostExtensionDurationValueLabel, zone.config.boostExtensionDurationSeconds / 60u, 'm'); + formatValueWithSuffix(_overrideTimeoutValueLabel, zone.config.overrideTimeoutSeconds / 60u, 'm'); + formatValueWithSuffix(_heatingStartDelayValueLabel, zone.config.heatingStartDelaySeconds / 60u, 'm'); + formatValueWithSuffix(_windowLockoutDurationValueLabel, zone.config.openWindowLockoutDurationSeconds / 60u, 'm'); + formatTemperatureValue(_heatingOvershootValueLabel, zone.config.heatingOvershoot); + formatTemperatureValue(_heatingUndershootValueLabel, zone.config.heatingUndershoot); +} diff --git a/src/ui/Screens/ZoneSettingsScreen.h b/src/ui/Screens/ZoneSettingsScreen.h new file mode 100644 index 0000000..7f7a9c0 --- /dev/null +++ b/src/ui/Screens/ZoneSettingsScreen.h @@ -0,0 +1,26 @@ +#pragma once + +#include "../Menu.h" +#include "../Screen.h" +#include "../ScreenID.h" + +namespace UI +{ + class ZoneSettingsScreen : public Screen + { + public: + explicit ZoneSettingsScreen(Model& model, Graphics& graphics); + + void activate(); + void update(); + [[nodiscard]] Result handleKeyPress(Keypad::Keys keys); + + private: + Menu<12> _menu; + + [[nodiscard]] Result selectMenuItem() const; + void stepSelectedSetting(StepDirection direction); + + void updateValueLabels(); + }; +} \ No newline at end of file diff --git a/src/ui/TextInput.cpp b/src/ui/TextInput.cpp deleted file mode 100644 index 5ab61f7..0000000 --- a/src/ui/TextInput.cpp +++ /dev/null @@ -1,579 +0,0 @@ -#include "Config.h" -#include "TextInput.h" - -#include "display/Display.h" -#include "display/Text.h" - -#include -#include -#include -#include - -// #define DEBUG - -#define TI_ROW_OFFSET (1) -#define TI_ROW_LEFT_OFFSET (1) -#define TI_ROW_HEIGHT (8) -#define TI_CHAR_WIDTH (5) -#define TI_SPACE_WIDTH (1) -#define TI_INPUT_FIELD_WIDTH (21) - -#define TI_KEY_MTX_ROWS (5) -#define TI_KEY_MTX_COLS (21) - -typedef enum { - SS_OFF, - SS_ON, - SS_LOCKED -} shift_state_t; - -static struct { - const char* title; - - struct { - uint8_t buf; - uint8_t row; - uint8_t col; - uint8_t cursor; - } pos; - - char* buf; - uint8_t maxlen; - uint8_t txt_offset; - - shift_state_t s_shift; -} ctx; - -enum { - K_NONE, - K_SHIFT, - K_BCKSPC, -// K_LEFT, -// K_RIGHT, - K_OK, - K_CANCEL -}; - -// Repeating key-codes are handled as one large key -static const char key_mtx[TI_KEY_MTX_ROWS][TI_KEY_MTX_COLS] = { - { '1' , '2' , '3' , '4', '5', '6', '7', '8', '9', '0', '-', '_', '=' , '+', '!', '@' , '#' , '$' , '%' , '^' , '&' }, - { K_NONE , 'q' , 'w' , 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '[', ']' , '{', '}', '*' , '(' , ')' , '~' , '`' , K_NONE }, - { K_SHIFT, K_SHIFT, 'a' , 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', '\'', ':', '"', '\\' , '|' , K_BCKSPC, K_BCKSPC, K_BCKSPC, K_BCKSPC }, - { K_SHIFT, K_SHIFT, K_NONE, 'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '?', '<' , '>', '/' , K_NONE , K_BCKSPC, K_BCKSPC, K_BCKSPC, K_BCKSPC }, - { K_OK , K_OK , ' ' , ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ' , ' ', ' ', K_CANCEL, K_CANCEL, K_CANCEL, K_CANCEL, K_CANCEL, K_CANCEL } -}; - -static const uint8_t key_shift[2][11] = { - { - 0b00000000, - 0b10000000, - 0b11000000, - 0b10100000, - 0b10010000, - 0b00001000, - 0b10010000, - 0b10100000, - 0b11000000, - 0b10000000, - 0b00000000 - }, - { - 0b00000000, - 0b00000000, - 0b00000000, - 0b00000000, - 0b00011111, - 0b00010000, - 0b00011111, - 0b00000000, - 0b00000000, - 0b00000000, - 0b00000000 - } -}; - -static const uint8_t key_bckspc[2][21] = { - { - 0b10000000, - 0b01000000, - 0b00100000, - 0b00010000, - 0b00001000, - 0b00000100, - 0b00000010, - 0b00000010, - 0b00000010, - 0b00000010, - 0b00100010, - 0b01000010, - 0b10000010, - 0b01000010, - 0b00100010, - 0b00000010, - 0b00000010, - 0b00000010, - 0b00000010, - 0b00000010, - 0b11111100 - }, - { - 0b00000000, - 0b00000001, - 0b00000010, - 0b00000100, - 0b00001000, - 0b00010000, - 0b00100000, - 0b00100000, - 0b00100000, - 0b00100000, - 0b00100010, - 0b00100001, - 0b00100000, - 0b00100001, - 0b00100010, - 0b00100000, - 0b00100000, - 0b00100000, - 0b00100000, - 0b00100000, - 0b00011111 - } -}; - -static const uint8_t key_left[] = { - 0b00001000, - 0b00011100, - 0b00101010, - 0b00001000, - 0b00001000 -}; - -static const uint8_t key_right[] = { - 0b00001000, - 0b00001000, - 0b00101010, - 0b00011100, - 0b00001000 -}; - -static const uint8_t key_space[] = { - 0b01100000, - 0b01000000, - 0b01000000, - 0b01000000, - 0b01100000 -}; - -typedef enum { - FC_LEFT, - FC_RIGHT -} find_col_dir_t; - -typedef enum { - SR_UP, - SR_DOWN -} sel_row_dir_t; - -static void draw(); -static void draw_input_field(); -static void draw_header(); -static void draw_ctrl_keys(int ch); -static void draw_background( - const uint8_t line, - const uint8_t start_col, - const uint8_t width, - const uint8_t pattern -); -static int find_col(find_col_dir_t dir); -static void select_row(sel_row_dir_t dir); -static ti_key_event_result_t select_key(); - -static void on_shift_pressed(); -static bool is_shifted(); -static void on_bckspc_pressed(); - -void text_input_init(char *buf, int buflen, const char* title) -{ - printf("text_input::text_input_init: initializing context...\n"); - - memset(&ctx, 0, sizeof(ctx)); - - ctx.title = title; - - ctx.buf = buf; - ctx.maxlen = buflen - 1; - - printf("text_input::text_input_init: clearing text buffer...\n"); - memset(buf, 0, buflen); - - printf("text_input::text_input_init: clearing the display...\n"); - Display::clear(); - - printf("text_input::text_input_init: drawing UI elements...\n"); - draw_header(); - draw(); -} - -ti_key_event_result_t text_input_key_event(ti_key_event_t event) -{ - switch (event) { - case TI_KE_RIGHT: - ctx.pos.col = find_col(FC_RIGHT); - draw(); - break; - - case TI_KE_LEFT: - ctx.pos.col = find_col(FC_LEFT); - draw(); - break; - - case TI_KE_DOWN: - select_row(SR_DOWN); - draw(); - break; - - case TI_KE_UP: - select_row(SR_UP); - draw(); - break; - - case TI_KE_SELECT: { - const ti_key_event_result_t result = select_key(); - if (result != TI_KE_NO_ACTION) - return result; - draw(); - } - } - - return TI_KE_NO_ACTION; -} - -static void draw() -{ -#ifdef DEBUG - printf("text_input::draw: key_mtx size: %ux%u\n", TI_KEY_MTX_COLS, TI_KEY_MTX_ROWS); -#endif - - for (uint8_t row = 0; row < TI_KEY_MTX_ROWS; ++row) { - for (uint8_t col = 0; col < TI_KEY_MTX_COLS; ++col) { - const uint8_t x = col * (TI_CHAR_WIDTH + TI_SPACE_WIDTH) + TI_ROW_LEFT_OFFSET; - const uint8_t y = row + TI_ROW_OFFSET + 2; /* +2 for the header */ - const char c = key_mtx[row][col]; - const bool highlight = ctx.pos.col == col && ctx.pos.row == row; - -#ifdef DEBUG - printf("text_input::draw(): col=%d, row=%d, x=%d, y=%d\n", col, row, x, y); -#endif - - if (c > ' ') { -#ifdef DEBUG - printf("text_input::draw(): col=%d, row=%d, x=%d, y=%d, c='%c'\n", col, row, x, y, c); -#endif - Text::draw( - is_shifted() ? toupper(c) : c, - y, - x, - 0, - highlight - ); - } - } - } - - const char key = key_mtx[ctx.pos.row][ctx.pos.col]; - draw_ctrl_keys(key); - - draw_input_field(); - -#ifdef DEBUG - printf("text_input::draw finished\n"); -#endif -} - -static void draw_input_field() -{ -#ifdef DEBUG - printf("text_input::draw_input_field: text offset: %d\n", ctx.txt_offset); -#endif - - const uint8_t last_col = Text::draw(ctx.buf + ctx.txt_offset, TI_ROW_OFFSET, 1, 0, false); - - // Fill the remaining space - if (last_col < 128) { - draw_background(TI_ROW_OFFSET, last_col, 128 - last_col, 0); - } -} - -static void draw_header() -{ - printf("text_input::draw_header: drawing title\n"); - - Text::draw(ctx.title ? ctx.title : "Input text:", 0, 1, 0, false); - - printf("text_input::draw_header: drawing separator\n"); - - Display::setLine(TI_ROW_OFFSET + 1); - Display::setColumn(0); - - const uint8_t pattern = 0b00000010; - - for (uint8_t i = 0; i < 128; ++i) { - Display::sendData(&pattern, 1, 0, false); - } -} - -static void draw_ctrl_keys(int key) -{ - // Shift - Display::setLine(TI_ROW_OFFSET + 4); - Display::setColumn(1); - Display::sendData( - key_shift[0], - sizeof(key_shift[0]) / sizeof(key_shift[0][0]), - 0, - key == K_SHIFT - ); - Display::setLine(TI_ROW_OFFSET + 5); - Display::setColumn(1); - Display::sendData( - key_shift[1], - sizeof(key_shift[0]) / sizeof(key_shift[0][0]), - 0, - key == K_SHIFT - ); - - // Backspace - Display::setLine(TI_ROW_OFFSET + 4); - Display::setColumn(105); - Display::sendData( - key_bckspc[0], - sizeof(key_bckspc[0]) / sizeof(key_bckspc[0][0]), - 0, - key == K_BCKSPC - ); - Display::setLine(TI_ROW_OFFSET + 5); - Display::setColumn(105); - Display::sendData( - key_bckspc[1], - sizeof(key_bckspc[0]) / sizeof(key_bckspc[0][0]), - 0, - key == K_BCKSPC - ); - - // OK - Text::draw("OK", TI_ROW_OFFSET + 6, 1, 0, key == K_OK); - - // CANCEL - Text::draw("CANCEL", TI_ROW_OFFSET + 6, 92, 0, key == K_CANCEL); - - // Space - draw_background(TI_ROW_OFFSET + 6, 13, 36, key == ' ' ? 0xff : 0); - Display::sendData(key_space, sizeof(key_space) / sizeof(key_space[0]), 0, key == ' '); - draw_background(TI_ROW_OFFSET + 6, 54, 37, key == ' ' ? 0xff : 0); -} - -static void draw_background( - const uint8_t line, - const uint8_t start_col, - const uint8_t width, - const uint8_t pattern -) -{ - Display::setLine(line); - Display::setColumn(start_col); - for (uint8_t i = 0; i < width; ++i) { - Display::sendData(&pattern, 1, 0, false); - } -} - -static int find_col(find_col_dir_t dir) -{ -#ifdef DEBUG - printf("text_input::find_col: dir=%s\n", dir == FC_LEFT ? "LEFT" : "RIGHT"); -#endif - - if (dir == FC_LEFT && ctx.pos.col <= 0) - return 0; - - if (dir == FC_RIGHT && ctx.pos.col >= TI_KEY_MTX_COLS - 1) - return TI_KEY_MTX_COLS - 1; - - char curr_key = key_mtx[ctx.pos.row][ctx.pos.col]; - int next = ctx.pos.col + (dir == FC_RIGHT ? 1 : -1); - -#ifdef DEBUG - printf("text_input::find_col: next candidate: %d\n", next); -#endif - - if (key_mtx[ctx.pos.row][next] != K_NONE && key_mtx[ctx.pos.row][next] != curr_key) - return next; - -#ifdef DEBUG - printf("text_input::find_col: trying to find a new candidate item\n"); -#endif - - while ( - (key_mtx[ctx.pos.row][next] == K_NONE || key_mtx[ctx.pos.row][next] == curr_key) - && next >= 0 - && next <= TI_KEY_MTX_COLS - 1 - ) { - next += dir == FC_RIGHT ? 1 : -1; - } - -#ifdef DEBUG - printf("text_input::find_col: new candidate: %d\n", next); -#endif - - if (next >= 0 && next <= TI_KEY_MTX_COLS - 1) - return next; - -#ifdef DEBUG - printf("text_input::find_col: returning with original col\n"); -#endif - - return ctx.pos.col; -} - -static void select_row(sel_row_dir_t dir) -{ - if ( - (dir == SR_UP && ctx.pos.row <= 0) - || (dir == SR_DOWN && ctx.pos.row >= TI_KEY_MTX_ROWS - 1) - ) { - return; - } - - ctx.pos.row += dir == SR_DOWN ? 1 : -1; - - if (key_mtx[ctx.pos.row][ctx.pos.col] == K_NONE) { - int next = find_col(FC_RIGHT); - if (next == ctx.pos.col) { - next = find_col(FC_LEFT); - if (next == ctx.pos.col) { - ctx.pos.row += dir == SR_DOWN ? -1 : 1; - return; - } - } - - ctx.pos.col = next; - } -} - -static ti_key_event_result_t select_key() -{ - const char key = key_mtx[ctx.pos.row][ctx.pos.col]; - -#ifdef DEBUG - printf("text_input::select_key: %d\n", key); -#endif - - if (key >= ' ') { -#ifdef DEBUG - printf("text_input::select_key: selecting printable '%c'\n", key); -#endif - - if (ctx.pos.buf >= ctx.maxlen - 1) { -#ifdef DEBUG - printf("select_key: text buffer is full\n"); -#endif - return TI_KE_NO_ACTION; - } - - ctx.buf[ctx.pos.buf] = is_shifted() ? toupper(key) : key; - ++ctx.pos.buf; - - const int cur_pos = ctx.pos.buf - ctx.txt_offset; -#ifdef DEBUG - printf("text_input::select_key: cur_pos=%d, bufidx=%d, txt_offset=%d\n", - cur_pos, ctx.pos.buf, ctx.txt_offset); -#endif - - if (cur_pos > TI_INPUT_FIELD_WIDTH) - ctx.txt_offset += cur_pos - TI_INPUT_FIELD_WIDTH; - else if (cur_pos < 0) - ctx.txt_offset += cur_pos; - -#ifdef DEBUG - printf("text_input::select_key: new buffer index: %d\n", ctx.pos.buf); -#endif - - // Turn of shift if it's not locked - if (ctx.s_shift == SS_ON) - ctx.s_shift = SS_OFF; - - draw(); - } else { - switch (key) { - case K_SHIFT: - on_shift_pressed(); - break; - case K_BCKSPC: - on_bckspc_pressed(); - break; - case K_OK: - Display::clear(); - return TI_KE_ACCEPT; - case K_CANCEL: - Display::clear(); - return TI_KE_CANCEL; - } - } - - return TI_KE_NO_ACTION; -} - -static void on_shift_pressed() -{ -#ifdef DEBUG - printf("text_input::on_shift_pressed: "); -#endif - - switch (ctx.s_shift) { - case SS_OFF: -#ifdef DEBUG - printf("OFF->ON\n"); -#endif - ctx.s_shift = SS_ON; - break; - case SS_ON: -#ifdef DEBUG - printf("ON->LOCKED\n"); -#endif - ctx.s_shift = SS_LOCKED; - break; - case SS_LOCKED: -#ifdef DEBUG - printf("LOCKED->OFF\n"); -#endif - ctx.s_shift = SS_OFF; - break; - default: -#ifdef DEBUG - printf("UNKNOWN\n"); -#endif - break; - } -} - -static bool is_shifted() -{ - return ctx.s_shift == SS_ON || ctx.s_shift == SS_LOCKED; -} - -static void on_bckspc_pressed() -{ - if (ctx.pos.buf > 0) { - --ctx.pos.buf; - ctx.buf[ctx.pos.buf] = 0; - - const int cur_pos = ctx.pos.buf - ctx.txt_offset; - if (cur_pos < 0) - ctx.txt_offset += cur_pos; - -#ifdef DEBUG - printf("text_input::on_bckspc_pressed: pos.buf=%d, cur_pos=%d, txt_offset=%d\n", - ctx.pos.buf, cur_pos, ctx.txt_offset); -#endif - } -} \ No newline at end of file diff --git a/src/ui/TextInput.h b/src/ui/TextInput.h deleted file mode 100644 index 5e91ced..0000000 --- a/src/ui/TextInput.h +++ /dev/null @@ -1,18 +0,0 @@ -#pragma once - -typedef enum { - TI_KE_UP, - TI_KE_DOWN, - TI_KE_LEFT, - TI_KE_RIGHT, - TI_KE_SELECT -} ti_key_event_t; - -typedef enum { - TI_KE_NO_ACTION, - TI_KE_ACCEPT, - TI_KE_CANCEL -} ti_key_event_result_t; - -void text_input_init(char* buf, int maxlen, const char *title); -ti_key_event_result_t text_input_key_event(ti_key_event_t event); diff --git a/src/ui/Types.h b/src/ui/Types.h new file mode 100644 index 0000000..685acca --- /dev/null +++ b/src/ui/Types.h @@ -0,0 +1,12 @@ +#pragma once + +namespace UI +{ + +enum class StepDirection +{ + Up, + Down +}; + +} \ No newline at end of file diff --git a/src/ui/UIController.cpp b/src/ui/UIController.cpp new file mode 100644 index 0000000..b54286b --- /dev/null +++ b/src/ui/UIController.cpp @@ -0,0 +1,223 @@ +/* + 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 "UIController.h" +#include "Settings.h" +#include "SystemClock.h" +#include "TemperatureSensor.h" +#include "main.h" + +#include "display/Display.h" + +#include + +#include +#include +#include +#include +#include + +// #define ENABLE_DEBUG + +using namespace UI; + +UIController::UIController( + CoreApplication& application, + Settings& settings, + Model& model +) + : _app{ application } + , _settings{ settings } + , _screens{ RegisteredScreens::constructScreens(model, _graphics) } + , _clockController{ model.clock, _app.systemClock() } +{ + _log.info_P(PSTR("initializing Display, brightness: %d"), _settings.system.display.brightness); + Display::init(); + Display::setContrast(_settings.system.display.brightness); + + _lastKeyPressTime = _app.systemClock().utcTime(); + + if (loadScreen(ScreenID::Main)) { + invokeActivate(*_currentScreen); + } +} + +void UIController::task(const uint32_t deltaMillis) +{ + _clockController.task(deltaMillis); + + const auto pressedKeys = _keypad.scan(); + handleKeyPress(pressedKeys); + + _lastUpdateMillis += deltaMillis; + + if (_lastUpdateMillis >= 500) { + _lastUpdateMillis = 0; + update(); + } +} + +void UIController::update() +{ + // _log.debug("update"); + + if (_currentScreen) { + invokeUpdate(*_currentScreen); + } else { + _log.warning_P(PSTR("update: current screen is null")); + } + + updateActiveState(); +} + +void UIController::handleKeyPress(const Keypad::Keys keys) +{ + if (keys == Keypad::Keys::None) { + return; + } + + _lastKeyPressTime = _app.systemClock().utcTime(); + + // _log.info_P(PSTR("keys=%xh, _lastKeyPressTime=%ld"), keys, _lastKeyPressTime); + + // If the display is sleeping, use this keypress to wake it up, + // but don't interact with the UI while it's invisible. + if (!Display::isPoweredOn()) { + _log.debug_P(PSTR("display is off, ignoring key press")); + return; + } + + const auto screenChanged = std::visit( + [this](const Result& result) -> bool { + if constexpr (std::is_same_v) { + return loadScreen(result.id); + } + + return false; + }, + invokeHandleKeyPress(*_currentScreen, keys) + ); + + if (screenChanged) { + _log.debug("handleKeyPress::screenChanged"); + Display::clear(); + invokeActivate(*_currentScreen); + } +} + +void UIController::updateActiveState() +{ + if (isActive()) { + if (!Display::isPoweredOn()) { + _log.debug_P(PSTR("powering on the display, brightness: %d"), _settings.system.display.brightness); + Display::powerOn(); + Display::setContrast(_settings.system.display.brightness); + } + } else { + if (Display::isPoweredOn()) { + _log.debug_P(PSTR("powering off the display")); + Display::powerOff(); + } + } +} + +bool UIController::isActive() const +{ + if (_settings.system.display.timeoutSecs == 0) { + return true; + } + + return (_app.systemClock().utcTime() - _lastKeyPressTime) < static_cast(_settings.system.display.timeoutSecs); +} + +bool UIController::loadScreen(const int id) +{ + if (_currentScreen) { + const auto currentScreenId{ + std::visit( + [](const ScreenType& s) { + static_assert(IsScreen); + return s.id(); + }, + *_currentScreen + ) + }; + + if (currentScreenId == id) { + return false; + } + } + + for (auto& screen : _screens) { + if (id == getId(screen)) { + _currentScreen = &screen; + return true; + } + } + + return false; +} + +int UIController::getId(const RegisteredScreens::Screen& screen) +{ + return std::visit( + [](const ScreenType& s) { + static_assert(IsScreen); + return s.id(); + }, + screen + ); +} + +void UIController::invokeActivate(RegisteredScreens::Screen& screen) +{ + std::visit( + [](ScreenType& s) { + static_assert(IsScreen); + s.activate(); + }, + screen + ); +} + +void UIController::invokeUpdate(RegisteredScreens::Screen& screen) +{ + std::visit( + [](ScreenType& s) { + static_assert(IsScreen); + s.update(); + }, + screen + ); +} + +Screen::Result UIController::invokeHandleKeyPress( + RegisteredScreens::Screen& screen, + const Keypad::Keys keys +) +{ + return std::visit( + [&](ScreenType& s) { + static_assert(IsScreen); + return s.handleKeyPress(keys); + }, + screen + ); +} diff --git a/src/ui/UIController.h b/src/ui/UIController.h new file mode 100644 index 0000000..9b1c9dc --- /dev/null +++ b/src/ui/UIController.h @@ -0,0 +1,129 @@ +/* + 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 +*/ + +#pragma once + +#include "Keypad.h" +#include "Logger.h" +#include "Model.h" +#include "Screen.h" + +#include "Controllers/ClockController.h" + +#include "Screens/DateTimeSettingsScreen.h" +#include "Screens/DebuggingSettingsScreen.h" +#include "Screens/DisplaySettingsScreen.h" +#include "Screens/GeneralSettingsScreen.h" +#include "Screens/MainScreen.h" +#include "Screens/MainMenuScreen.h" +#include "Screens/ZoneScheduleScreen.h" +#include "Screens/ZoneSettingsScreen.h" +#include "Screens/ZoneSettingsMenuScreen.h" + +#include +#include +#include + +class CoreApplication; +class HeatingController; +class ISystemClock; +class Settings; +class TemperatureSensor; + +namespace UI +{ + namespace Detail + { + template + struct ScreenHelper + { + static constexpr auto TypeCount = sizeof...(ScreenTypes); + using Screen = std::variant; + using Container = std::array; + + static Container constructScreens(Model& model, Graphics& graphics) + { + return Container{ + { + ScreenTypes{ model, graphics }... + } + }; + } + }; + } + + // Register screens here + using RegisteredScreens = Detail::ScreenHelper< + MainScreen, + MainMenuScreen, + ZoneSettingsMenuScreen, + GeneralSettingsScreen, + DisplaySettingsScreen, + DateTimeSettingsScreen, + DebuggingSettingsScreen, + ZoneSettingsScreen, + ZoneScheduleScreen + >; + + class UIController + { + public: + UIController( + CoreApplication& application, + Settings& settings, + Model& modelconst + ); + + void task(uint32_t deltaMillis); + + void update(); + void handleKeyPress(Keypad::Keys keys); + + private: + CoreApplication& _app; + Settings& _settings; + Keypad _keypad; + Logger _log{ "UIController" }; + std::time_t _lastKeyPressTime = 0; + + uint32_t _lastUpdateMillis{}; + + Graphics _graphics; + + RegisteredScreens::Container _screens; + RegisteredScreens::Screen* _currentScreen{}; + + Controllers::ClockController _clockController; + + void updateActiveState(); + bool isActive() const; + + [[nodiscard]] bool loadScreen(int id); + + [[nodiscard]] static int getId(const RegisteredScreens::Screen& screen); + static void invokeActivate(RegisteredScreens::Screen& screen); + static void invokeUpdate(RegisteredScreens::Screen& screen); + [[nodiscard]] static Screen::Result invokeHandleKeyPress( + RegisteredScreens::Screen& screen, + Keypad::Keys keys + ); + }; +} + diff --git a/src/ui/Ui.cpp b/src/ui/Ui.cpp deleted file mode 100644 index 1114f68..0000000 --- a/src/ui/Ui.cpp +++ /dev/null @@ -1,188 +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 "Ui.h" -#include "Settings.h" -#include "SystemClock.h" -#include "TemperatureSensor.h" -#include "main.h" - -#include "display/Display.h" - -#include "MainScreen.h" -#include "MenuScreen.h" -#include "SchedulingScreen.h" - -#include -#include -#include -#include - -// #define ENABLE_DEBUG - -// TODO rename settings_ -Ui::Ui( - Settings& settings, - const ISystemClock& systemClock, - Keypad& keypad, - HeatingController& heatingController, - const TemperatureSensor& temperatureSensor -) - : _settings(settings) - , _systemClock(systemClock) - , _keypad(keypad) - // , _heatingController(heatingController) - , _temperatureSensor(temperatureSensor) -{ - _log.info_P(PSTR("initializing Display, brightness: %d"), _settings.data.display.Brightness); - Display::init(); - Display::setContrast(_settings.data.display.Brightness); - - auto mainScreen = std::unique_ptr(new MainScreen(_settings, _systemClock, _temperatureSensor)); - _mainScreen = mainScreen.get(); - _currentScreen = _mainScreen; - mainScreen->activate(); - _screens.push_back(std::move(mainScreen)); - - _screens.emplace_back(new MenuScreen(_settings)); - _screens.emplace_back(new SchedulingScreen(_settings, _systemClock)); - - _lastKeyPressTime = _systemClock.utcTime(); -} - -void Ui::task() -{ - const auto pressedKeys = _keypad.scan(); - handleKeyPress(pressedKeys); -} - -void Ui::update() -{ - if (_currentScreen) { - _currentScreen->update(); - } else { - _log.warning_P(PSTR("update: current screen is null")); - } - - updateActiveState(); -} - -void Ui::handleKeyPress(const Keypad::Keys keys) -{ - if (keys == Keypad::Keys::None) - return; - - _lastKeyPressTime = _systemClock.utcTime(); - - _log.info_P(PSTR("keys=%xh, _lastKeyPressTime=%ld"), keys, _lastKeyPressTime); - - // If the display is sleeping, use this keypress to wake it up, - // but don't interact with the UI while it's invisible. - if (!Display::isPoweredOn()) { - _log.info_P(PSTR("display is off, ignoring key press")); - return; - } - - const auto action = _currentScreen->keyPress(keys); - bool screenChanged = true; - - switch (action) { - case Screen::Action::NoAction: - screenChanged = false; - break; - - case Screen::Action::NavigateBack: - navigateBackward(); - break; - - case Screen::Action::NavigateForward: - navigateForward(_currentScreen->nextScreen()); - break; - } - - if (screenChanged) { - Display::clear(); - _currentScreen->activate(); - } -} - -void Ui::updateActiveState() -{ - if (isActive()) { - if (!Display::isPoweredOn()) { - _log.debug_P(PSTR("powering on the display, brightness: %d"), _settings.data.display.Brightness); - Display::powerOn(); - Display::setContrast(_settings.data.display.Brightness); - } - } else { - if (Display::isPoweredOn()) { - _log.debug_P(PSTR("powering off the display")); - Display::powerOff(); - } - } -} - -bool Ui::isActive() const -{ - if (_settings.data.display.TimeoutSecs == 0) { - return true; - } - - return (_systemClock.utcTime() - _lastKeyPressTime) < static_cast(_settings.data.display.TimeoutSecs); -} - -void Ui::navigateForward(const char* name) -{ - if (!name) { - _log.warning_P(PSTR("navigating forward, screen name is null, going to main screen")); - _currentScreen = _mainScreen; - return; - } - - _log.debug_P(PSTR("navigating forward, next screen: %s"), name); - - const auto it = std::find_if(std::begin(_screens), std::end(_screens), [name](const std::unique_ptr& scr) { - return strcmp(scr->name(), name) == 0; - }); - - if (it == std::end(_screens)) { - _log.warning_P(PSTR("screen not found: %s, going to main screen")); - _currentScreen = _mainScreen; - return; - } - - _currentScreen = it->get(); -} - -void Ui::navigateBackward() -{ - _log.debug_P(PSTR("navigating back")); - - if (_screenStack.empty()) { - _log.debug_P(PSTR("screen stack is empty, navigating to main screen")); - _currentScreen = _mainScreen; - return; - } - - _currentScreen = _screenStack.top(); - _screenStack.pop(); - - _log.debug_P(PSTR("current screen: %s"), _currentScreen->name()); -} diff --git a/src/ui/Ui.h b/src/ui/Ui.h deleted file mode 100644 index eb8c0fb..0000000 --- a/src/ui/Ui.h +++ /dev/null @@ -1,75 +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 -*/ - -#pragma once - -#include "Keypad.h" -#include "Logger.h" - -#include "Screen.h" -#include "MainScreen.h" -#include "MenuScreen.h" -#include "SchedulingScreen.h" - -#include -#include -#include - -class ISystemClock; -class Settings; -class TemperatureSensor; - -class Ui -{ -public: - Ui( - Settings& settings, - const ISystemClock& systemClock, - Keypad& keypad, - HeatingController& heatingController, - const TemperatureSensor& temperatureSensor - ); - - void task(); - - void update(); - void handleKeyPress(Keypad::Keys keys); - -private: - Settings& _settings; - const ISystemClock& _systemClock; - Keypad& _keypad; - // HeatingController& _heatingController; - const TemperatureSensor& _temperatureSensor; - Logger _log{ "Ui" }; - std::time_t _lastKeyPressTime = 0; - - std::stack _screenStack; - std::vector> _screens; - - Screen* _currentScreen = nullptr; - Screen* _mainScreen = nullptr; - - void updateActiveState(); - bool isActive() const; - - void navigateForward(const char* name); - void navigateBackward(); -}; diff --git a/src/ui/WifiScreen.cpp b/src/ui/WifiScreen.cpp deleted file mode 100644 index f7784ae..0000000 --- a/src/ui/WifiScreen.cpp +++ /dev/null @@ -1,393 +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-08 -*/ - -#include "Keypad.h" -#include "TextInput.h" -#include "WifiScreen.h" - -#include "display/Display.h" -#include "display/Text.h" - -#include -#include - -/* - Main screen: - - WiFi Connection - Connected to: - - IP: 000.000.000.000 - - > Disconnect - Scan - Leave - - - Scan screen (scanning): - - WiFi Networks: - - Scanning... - - > Abort - - - Scan screen (found networks): - - WiFi Networks: - > SSID 1 - SSID 2 - SSID 3 - SSID 4 - SSID 5 - Back - - - -*/ - -typedef enum { - SCR_MAIN, - SCR_SCAN, - SCR_PASSWORD -} screen_t; - -typedef enum { - SCN_IDLE, - SCN_SCANNING, - SCN_FINISHED -} scan_state_t; - -static struct state_s { - int ap_cnt; - char ap_psk[64]; - char ap_ssid[32]; - int list_pos; - int list_offs; - screen_t screen; - scan_state_t scan; -} state; - -static struct cb_s { - wifi_scan_cb scan; - wifi_read_ssid_cb read_ssid; - wifi_is_open_cb is_open; -} callbacks; - -static void draw(); -static void update(); -static void select_item(); -static void init_scan(); - -void wifi_screen_set_scan_cb(wifi_scan_cb cb) -{ - callbacks.scan = cb; -} - -void wifi_screen_set_read_ssid_cb(wifi_read_ssid_cb cb) -{ - callbacks.read_ssid = cb; -} - -void wifi_screen_set_is_open_cb(wifi_is_open_cb cb) -{ - callbacks.is_open = cb; -} - -void wifi_screen_init() -{ - printf("wifi_screen::init\n"); - - memset(&state, 0, sizeof(struct state_s)); - - draw(); -} - -void wifi_screen_update() -{ - printf("wifi_screen::update\n"); -} - -void wifi_screen_leave() -{ - printf("wifi_screen::leave\n"); -} - -void wifi_screen_key_event(Keypad::Keys keys) -{ - printf("wifi_screen::key_event: keys=0x%x\n", static_cast(keys)); - - bool update_needed = false; - - switch (state.screen) { - case SCR_MAIN: - // + -> Down - if (keys & Keypad::Keys::Plus) { - if (++state.list_pos >= 3) { - state.list_pos = 0; - } - update_needed = true; - // - -> Up - } else if (keys & Keypad::Keys::Minus) { - if (state.list_pos == 0) { - state.list_pos = 2; - } else { - --state.list_pos; - } - update_needed = true; - // Right -> Select - } else if (keys & Keypad::Keys::Right) { - select_item(); - } - break; - - case SCR_SCAN: - // + -> Down - if (keys & Keypad::Keys::Plus) { - if (state.list_pos < state.ap_cnt) { - ++state.list_pos; - if (state.list_pos - state.list_offs > 6) { - ++state.list_offs; - } - } - update_needed = true; - // - -> Up - } else if (keys & Keypad::Keys::Minus) { - if (state.list_pos > 0) { - --state.list_pos; - if (state.list_pos - state.list_offs < 0) { - --state.list_offs; - } - } - update_needed = true; - } else if (keys & Keypad::Keys::Right) { - select_item(); - } - - case SCR_PASSWORD: { - ti_key_event_t key_event; - bool valid_key = true; - - if (keys & Keypad::Keys::Plus) { - key_event = TI_KE_UP; - } else if (keys & Keypad::Keys::Minus) { - key_event = TI_KE_DOWN; - } else if (keys & Keypad::Keys::Menu) { - key_event = TI_KE_SELECT; - } else if (keys & Keypad::Keys::Left) { - key_event = TI_KE_LEFT; - } else if (keys & Keypad::Keys::Right) { - key_event = TI_KE_RIGHT; - } else { - valid_key = false; - } - - if (valid_key) { - const ti_key_event_result_t res = text_input_key_event(key_event); - - switch (res) { - case TI_KE_ACCEPT: - printf("wifi_screen: password entered: %s\n", state.ap_psk); - state.screen = SCR_MAIN; - draw(); - break; - - case TI_KE_CANCEL: - printf("wifi_screen: password input canceled\n"); - state.screen = SCR_SCAN; - draw(); - break; - - default: - break; - } - } - break; - } - } - - if (update_needed) { - update(); - } -} - -static void draw() -{ - printf("wifi_screen::draw: screen=%d\n", state.screen); - - Display::clear(); - - switch (state.screen) { - case SCR_MAIN: - Text::draw("WiFi Connection", 0, 0, 0, false); - Text::draw("Connected to:", 1, 0, 0, false); - Text::draw("IP:", 3, 0, 0, false); - Text::draw("Disconnect", 5, 10, 0, false); - Text::draw("Scan", 6, 10, 0, false); - Text::draw("Leave", 7, 10, 0, false); - break; - - case SCR_SCAN: - Text::draw("WiFi Networks", 0, 0, 0, false); - switch (state.scan) { - case SCN_IDLE: - Text::draw("Idle", 2, 10, 0, false); - break; - - case SCN_SCANNING: - Text::draw("Scanning...", 2, 10, 0, false); - break; - - case SCN_FINISHED: - break; - } - break; - - case SCR_PASSWORD: - text_input_init(state.ap_psk, sizeof(state.ap_psk), "WiFi Password:"); - break; - } - - update(); -} - -static void update() -{ - printf("wifi_screen::update: screen=%d\n", state.screen); - - switch (state.screen) { - case SCR_MAIN: - for (int i = 0; i < 3; ++i) { - Text::draw(i == state.list_pos ? ">" : " ", i + 5, 0, 0, false); - } - break; - - case SCR_SCAN: - switch (state.scan) { - case SCN_FINISHED: - for (int i = state.list_offs, line = 1; i < state.ap_cnt + 1 && i - state.list_offs < 7; ++i, ++line) { - printf("wifi_screen::update: drawing AP list item, i=%d, list_pos=%d, list_offs=%d, line=%d\n", - i, state.list_pos, state.list_offs, line - ); - - // Clear the row - Display::setColumn(10); - Display::setLine(line); - for (int i = 0; i < 118; ++i) { - static const uint8_t pattern = 0; - Display::sendData(&pattern, 1, 0, false); - } - - if (i == 0) { - Text::draw("<< Back", line, 10, 0, false); - } else { - if (callbacks.read_ssid) { - char buf[32] = { 0 }; - callbacks.read_ssid(i - 1, buf); - Text::draw(buf, line, 10, 0, false); - } - } - Text::draw(i == state.list_pos ? ">" : " ", line, 0, 0, false); - } - break; - - case SCN_IDLE: - case SCN_SCANNING: - break; - } - break; - - case SCR_PASSWORD: - break; - } -} - -static void select_item() -{ - printf("wifi_screen::select_item: %d\n", state.list_pos); - - switch (state.screen) { - case SCR_MAIN: - if (state.list_pos == 0) { - // Disconnect - } else if (state.list_pos == 1) { - // Scan - state.screen = SCR_SCAN; - draw(); - init_scan(); - } else if (state.list_pos == 2) { - // Leave - } - break; - - case SCR_SCAN: - if (state.list_pos == 0) { - // Back - state.screen = SCR_MAIN; - draw(); - } else { - // Select SSID - if (callbacks.read_ssid) { - callbacks.read_ssid(state.list_pos - 1, state.ap_ssid); - printf("wifi_screen::select_item: selected SSID: %s\n", state.ap_ssid); - } - - if (callbacks.is_open) { - if (!callbacks.is_open(state.list_pos - 1)) { - printf("wifi_screen::select_item: selected AP requires password\n"); - state.screen = SCR_PASSWORD; - text_input_init(state.ap_psk, sizeof(state.ap_psk), "Enter password:"); - } else { - state.screen = SCR_MAIN; - printf("wifi_screen::select_item: selected AP is open\n"); - draw(); - } - } - } - break; - - case SCR_PASSWORD: - break; - } -} - -static void init_scan() -{ - printf("wifi_screen::init_scan\n"); - - state.scan = SCN_IDLE; - state.ap_cnt = 0; - state.list_offs = 0; - - update(); - - if (callbacks.scan) { - state.scan = SCN_SCANNING; - update(); - printf("wifi_screen::init_scan: scanning\n"); - state.ap_cnt = callbacks.scan(); - printf("wifi_screen::init_scan: finished, count=%d\n", state.ap_cnt); - state.scan = SCN_FINISHED; - update(); - } else { - printf("wifi_screen::init_scan: scan callback is null\n"); - } -} \ No newline at end of file diff --git a/src/ui/WifiScreen.h b/src/ui/WifiScreen.h deleted file mode 100644 index c744f8d..0000000 --- a/src/ui/WifiScreen.h +++ /dev/null @@ -1,38 +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-08 -*/ - -#pragma once - -#include "Keypad.h" - -#include - -typedef int8_t (* wifi_scan_cb)(); -typedef void (* wifi_read_ssid_cb)(int8_t index, char* ssid); -typedef bool (* wifi_is_open_cb)(int8_t index); - -void wifi_screen_set_scan_cb(wifi_scan_cb cb); -void wifi_screen_set_read_ssid_cb(wifi_read_ssid_cb cb); -void wifi_screen_set_is_open_cb(wifi_is_open_cb cb); - -void wifi_screen_init(); -void wifi_screen_update(); -void wifi_screen_leave(); -void wifi_screen_key_event(Keypad::Keys keys); \ No newline at end of file diff --git a/test/test_FurnaceController/test_main.cpp b/test/test_FurnaceController/test_main.cpp index 2c65f87..a5f6f72 100644 --- a/test/test_FurnaceController/test_main.cpp +++ b/test/test_FurnaceController/test_main.cpp @@ -21,7 +21,7 @@ namespace TestUtils { HeatingZoneController::Schedule schedule{{}}; for (auto& byte : schedule) { - byte = 0b01010101; + byte = 0b10101010; // MSB-firts } return schedule; } @@ -30,10 +30,60 @@ namespace TestUtils { HeatingZoneController::Schedule schedule{{}}; for (auto& byte : schedule) { - byte = 0b10101010; + byte = 0b01010101; // MSB-first } return schedule; } + + void setScheduleFor( + HeatingZoneController::Schedule& schedule, + unsigned weekday, + unsigned hour, + const unsigned minute + ) + { + weekday = std::clamp(weekday, 0u, 6u); + hour = std::clamp(hour, 0u, 23u); + const auto segmentIndex = weekday * 48 + hour * 2 + minute / 30; + const auto byteIndex = segmentIndex / 8; + const auto bitIndex = segmentIndex - byteIndex * 8; + + std::cout + << "hour=" << hour + << ", minute=" << minute + << ", segment=" << segmentIndex + << ", byte=" << byteIndex + << ", bit=" << bitIndex + << '\n'; + + schedule[byteIndex] |= 1 << (7 - bitIndex); + } + + void printSchedule(const HeatingZoneController::Schedule& schedule) + { + std::cout << "Hour: 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23\n"; + + 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 = schedule[dayOffset + byteIdx]; + for (auto bitIdx = 0; bitIdx < 8; ++bitIdx) { + bits[byteIdx * 8 + bitIdx] = (b & (1 << (7 - bitIdx)) ? '1' : '0'); + } + } + + std::cout << "Schedule[day=" << day << "]: "; + for (auto i = 0u; i < sizeof(bits); ++i) { + if (i > 0 && i % 2 == 0) { + std::cout << ' '; + } + std::cout << bits[i]; + } + std::cout << '\n'; + } + } } std::ostream& operator<<(std::ostream& str, const HeatingZoneController::Mode mode) @@ -844,6 +894,277 @@ TEST(HeatingZoneController, TargetTemperatureForAlternatingSchedule2) } } +TEST(HeatingZoneController, TargetTemperatureForSpecificSchedule) +{ + HeatingZoneController::Configuration config{}; + HeatingZoneController::Schedule schedule{{}}; + HeatingZoneController controller{ config, schedule }; + + for (auto dayOfWeek = 0u; dayOfWeek < 7; ++dayOfWeek) { + for (auto hour = 6; hour <= 18; ++hour) { + TestUtils::setScheduleFor(schedule, dayOfWeek, hour, 0); + if (hour < 18) { + TestUtils::setScheduleFor(schedule, dayOfWeek, hour, 30); + } + } + } + + TestUtils::printSchedule(schedule); + + controller.setMode(HeatingZoneController::Mode::Auto); + controller.setHighTargetTemperature(230); + controller.setLowTargetTemperature(210); + + controller.inputTemperature(220); + + for (auto dayOfWeek = 0; dayOfWeek < 7; ++dayOfWeek) { + for (auto hour = 0; hour < 24; ++hour) { + for (auto minute = 0; minute < 60; ++minute) { + controller.updateDateTime(dayOfWeek, hour, minute); + + bool expectFailed{ false }; + + if ((hour >= 6 && hour < 18) || (hour == 18 && minute < 30)) { + EXPECT_EQ(controller.targetTemperature(), 230); + expectFailed = controller.targetTemperature() != 230; + + EXPECT_TRUE(controller.callingForHeating()); + } else { + EXPECT_EQ(controller.targetTemperature(), 210); + expectFailed = controller.targetTemperature() != 210; + + EXPECT_FALSE(controller.callingForHeating()); + } + + if (expectFailed) { + std::cout + << "dayOfWeek=" << dayOfWeek + << ",hour=" << hour + << ",minute=" << minute + << '\n'; + + return; + } + } + } + } +} + +TEST(HeatingZoneController, TargetTemperatureForFirstScheduleSegment) +{ + HeatingZoneController::Configuration config{}; + HeatingZoneController::Schedule schedule{{}}; + HeatingZoneController controller{ config, schedule }; + + TestUtils::setScheduleFor(schedule, 0, 0, 0); + TestUtils::printSchedule(schedule); + + controller.setMode(HeatingZoneController::Mode::Auto); + controller.setHighTargetTemperature(230); + controller.setLowTargetTemperature(210); + + controller.inputTemperature(220); + + controller.updateDateTime(0, 0, 0); + EXPECT_EQ(controller.targetTemperature(), 230); + EXPECT_TRUE(controller.callingForHeating()); + + controller.updateDateTime(0, 0, 30); + EXPECT_EQ(controller.targetTemperature(), 210); + EXPECT_FALSE(controller.callingForHeating()); +} + +TEST(HeatingZoneController, TargetTemperatureForLastScheduleSegment) +{ + HeatingZoneController::Configuration config{}; + HeatingZoneController::Schedule schedule{{}}; + HeatingZoneController controller{ config, schedule }; + + TestUtils::setScheduleFor(schedule, 6, 23, 30); + TestUtils::printSchedule(schedule); + + controller.setMode(HeatingZoneController::Mode::Auto); + controller.setHighTargetTemperature(230); + controller.setLowTargetTemperature(210); + + controller.inputTemperature(220); + + controller.updateDateTime(6, 23, 0); + EXPECT_EQ(controller.targetTemperature(), 210); + EXPECT_FALSE(controller.callingForHeating()); + + controller.updateDateTime(6, 23, 30); + EXPECT_EQ(controller.targetTemperature(), 230); + EXPECT_TRUE(controller.callingForHeating()); +} + +TEST(HeatingZoneController, FailSafeLowTargetTemperature) +{ + HeatingZoneController::Configuration config{ + .heatingOvershoot = 5, + .heatingUndershoot = 5 + }; + HeatingZoneController::Schedule schedule{{}}; + HeatingZoneController controller{ config, schedule }; + + controller.setMode(HeatingZoneController::Mode::Auto); + + controller.setLowTargetTemperature(1); + + controller.inputTemperature(100); + EXPECT_FALSE(controller.callingForHeating()); + controller.inputTemperature(100 - config.heatingUndershoot); + EXPECT_TRUE(controller.callingForHeating()); +} + +TEST(HeatingZoneController, FailSafeHighTargetTemperature) +{ + HeatingZoneController::Configuration config{ + .heatingOvershoot = 5, + .heatingUndershoot = 5 + }; + HeatingZoneController::Schedule schedule{{}}; + HeatingZoneController controller{ config, schedule }; + + controller.setMode(HeatingZoneController::Mode::Auto); + + controller.setLowTargetTemperature(400); + + // Start heating to be able to test overshooting + controller.inputTemperature(200); + EXPECT_TRUE(controller.callingForHeating()); + + controller.inputTemperature(300); + EXPECT_TRUE(controller.callingForHeating()); + controller.inputTemperature(300 + config.heatingOvershoot); + EXPECT_FALSE(controller.callingForHeating()); +} + +TEST(HeatingZoneController, MinimumOvershoot) +{ + HeatingZoneController::Configuration config{ + .heatingOvershoot = 0, + .heatingUndershoot = 0 + }; + HeatingZoneController::Schedule schedule{{}}; + HeatingZoneController controller{ config, schedule }; + + controller.setMode(HeatingZoneController::Mode::Auto); + + controller.setLowTargetTemperature(230); + + controller.inputTemperature(220); + EXPECT_TRUE(controller.callingForHeating()); + + controller.inputTemperature(230); + EXPECT_TRUE(controller.callingForHeating()); + + controller.inputTemperature(231); + EXPECT_FALSE(controller.callingForHeating()); +} + +TEST(HeatingZoneController, MaximumOvershoot) +{ + HeatingZoneController::Configuration config{ + .heatingOvershoot = 20, + .heatingUndershoot = 0 + }; + HeatingZoneController::Schedule schedule{{}}; + HeatingZoneController controller{ config, schedule }; + + controller.setMode(HeatingZoneController::Mode::Auto); + + controller.setLowTargetTemperature(230); + + controller.inputTemperature(220); + EXPECT_TRUE(controller.callingForHeating()); + + controller.inputTemperature(230); + EXPECT_TRUE(controller.callingForHeating()); + + controller.inputTemperature(240); + EXPECT_FALSE(controller.callingForHeating()); +} + +TEST(HeatingZoneController, MinimumUndershoot) +{ + HeatingZoneController::Configuration config{ + .heatingOvershoot = 0, + .heatingUndershoot = 0 + }; + HeatingZoneController::Schedule schedule{{}}; + HeatingZoneController controller{ config, schedule }; + + controller.setMode(HeatingZoneController::Mode::Auto); + + controller.setLowTargetTemperature(230); + + controller.inputTemperature(240); + EXPECT_FALSE(controller.callingForHeating()); + + controller.inputTemperature(230); + EXPECT_FALSE(controller.callingForHeating()); + + controller.inputTemperature(229); + EXPECT_TRUE(controller.callingForHeating()); +} + +TEST(HeatingZoneController, MaximumUndershoot) +{ + HeatingZoneController::Configuration config{ + .heatingOvershoot = 0, + .heatingUndershoot = 20 + }; + HeatingZoneController::Schedule schedule{{}}; + HeatingZoneController controller{ config, schedule }; + + controller.setMode(HeatingZoneController::Mode::Auto); + + controller.setLowTargetTemperature(230); + + controller.inputTemperature(240); + EXPECT_FALSE(controller.callingForHeating()); + + controller.inputTemperature(230); + EXPECT_FALSE(controller.callingForHeating()); + + controller.inputTemperature(220); + EXPECT_TRUE(controller.callingForHeating()); +} + +#if 0 +// Oscillation test to support zero over/undershoot +TEST(HeatingZoneController, EnergyOptimizerWithOvershootSetToZero) +{ + HeatingZoneController::Configuration config{ + .heatingOvershoot = 0, + .heatingUndershoot = 5 + }; + + HeatingZoneController controller{ config, HeatingZoneController::Schedule{} }; + + controller.setMode(HeatingZoneController::Mode::Auto); + controller.setLowTargetTemperature(230); + controller.handleFurnaceHeatingChanged(true); + + controller.inputTemperature(229); + EXPECT_TRUE(controller.callingForHeating()); + + controller.handleFurnaceHeatingChanged(false); + + controller.inputTemperature(229); + EXPECT_TRUE(controller.callingForHeating()); + + controller.inputTemperature(230); + EXPECT_FALSE(controller.callingForHeating()); + + controller.handleFurnaceHeatingChanged(true); + + controller.inputTemperature(230); + EXPECT_FALSE(controller.callingForHeating()); +} +#endif + #pragma endregion #pragma region Tests for all active modes @@ -1025,6 +1346,9 @@ TEST_P(ActiveModeTest, HeatingStartsAfterOpenWindowLockoutDisengagedAndTemperatu controller.setHighTargetTemperature(230); controller.setLowTargetTemperature(210); + constexpr auto LockoutDurationSeconds = 120 * 1000; + config.openWindowLockoutDurationSeconds = LockoutDurationSeconds; + controller.setWindowOpened(true); controller.inputTemperature(100); EXPECT_FALSE(controller.callingForHeating()); @@ -1034,9 +1358,9 @@ TEST_P(ActiveModeTest, HeatingStartsAfterOpenWindowLockoutDisengagedAndTemperatu controller.setWindowOpened(false); controller.inputTemperature(100); EXPECT_FALSE(controller.callingForHeating()); - EXPECT_EQ(controller.openWindowLockoutRemainingMs(), 10 * 60 * 1000); + EXPECT_EQ(controller.openWindowLockoutRemainingMs(), LockoutDurationSeconds * 1000); - controller.task(10 * 60 * 1000); + controller.task(LockoutDurationSeconds * 1000); EXPECT_TRUE(controller.callingForHeating()); } @@ -1045,6 +1369,9 @@ TEST_P(ActiveModeTest, HeatingDoesntStartAfterOpenWindowLockoutDisengagedAndTemp controller.setHighTargetTemperature(230); controller.setLowTargetTemperature(210); + constexpr auto LockoutDurationSeconds = 120 * 1000; + config.openWindowLockoutDurationSeconds = LockoutDurationSeconds; + controller.setWindowOpened(true); controller.inputTemperature(100); EXPECT_FALSE(controller.callingForHeating()); @@ -1054,7 +1381,7 @@ TEST_P(ActiveModeTest, HeatingDoesntStartAfterOpenWindowLockoutDisengagedAndTemp controller.setWindowOpened(false); controller.inputTemperature(100); EXPECT_FALSE(controller.callingForHeating()); - EXPECT_EQ(controller.openWindowLockoutRemainingMs(), 10 * 60 * 1000); + EXPECT_EQ(controller.openWindowLockoutRemainingMs(), LockoutDurationSeconds * 1000); controller.inputTemperature(240); controller.task(10 * 60 * 1000);