From 7362068717baa680adf24ee4e038897134a107e2 Mon Sep 17 00:00:00 2001 From: Maxim Prokhorov Date: Wed, 11 Jan 2023 04:20:54 +0300 Subject: [PATCH] fan: experiment with relay provider early init plus simple state switch w/ api --- code/espurna/config/hardware.h | 3 + code/espurna/config/types.h | 1 + code/espurna/fan.h | 9 + code/espurna/ifan.cpp | 454 +++++++++++++++++++++------------ code/espurna/relay.cpp | 13 +- 5 files changed, 321 insertions(+), 159 deletions(-) diff --git a/code/espurna/config/hardware.h b/code/espurna/config/hardware.h index a601ef79..590b6f47 100644 --- a/code/espurna/config/hardware.h +++ b/code/espurna/config/hardware.h @@ -1099,6 +1099,9 @@ #define BUTTON4_PIN 14 #define BUTTON4_CLICK BUTTON_ACTION_FAN_HIGH + // Relay + #define RELAY1_PROVIDER RELAY_PROVIDER_FAN + // LEDs #define LED1_PIN 13 #define LED1_PIN_INVERSE 1 diff --git a/code/espurna/config/types.h b/code/espurna/config/types.h index 521d4c9b..c7692c5e 100644 --- a/code/espurna/config/types.h +++ b/code/espurna/config/types.h @@ -107,6 +107,7 @@ #define RELAY_PROVIDER_DUAL RelayProvider::Dual #define RELAY_PROVIDER_STM RelayProvider::Stm #define RELAY_PROVIDER_LIGHT_STATE RelayProvider::LightState +#define RELAY_PROVIDER_FAN RelayProvider::Fan #define RFB_PROVIDER_RCSWITCH 0 #define RFB_PROVIDER_EFM8BB1 1 diff --git a/code/espurna/fan.h b/code/espurna/fan.h index b7ec01c1..5af8a85b 100644 --- a/code/espurna/fan.h +++ b/code/espurna/fan.h @@ -8,6 +8,9 @@ Copyright (C) 2021 by Maxim Prokhorov #pragma once +#include +#include + enum class FanSpeed { Off, Low, @@ -15,6 +18,12 @@ enum class FanSpeed { High }; +class RelayProviderBase; +std::unique_ptr fanMakeRelayProvider(size_t); + +bool fanStatus(); +void fanStatus(bool); + void fanSpeed(FanSpeed); FanSpeed fanSpeed(); diff --git a/code/espurna/ifan.cpp b/code/espurna/ifan.cpp index c3df4c66..f796c721 100644 --- a/code/espurna/ifan.cpp +++ b/code/espurna/ifan.cpp @@ -48,7 +48,7 @@ namespace internal { template <> FanSpeed convert(const String& value) { - return convert(options::FanSpeedOptions, value, FanSpeed::Off); + return convert(options::FanSpeedOptions, value, FanSpeed::Medium); } String serialize(FanSpeed speed) { @@ -69,152 +69,302 @@ String speedToPayload(FanSpeed speed) { return espurna::settings::internal::serialize(speed); } -static constexpr auto DefaultSaveDelay = duration::Seconds{ 10 }; +namespace build { -// We expect to write a specific 'mask' via GPIO LOW & HIGH to set the speed -// Sync up with the relay and write it on ON / OFF status events +static constexpr auto ControlPin = uint8_t{ 12 }; + +static constexpr auto SaveDelay = duration::Seconds{ 10 }; +static constexpr auto Speed = FanSpeed::Medium; -constexpr size_t Gpios { 3ul }; +} // namespace build -using State = std::array; +namespace settings { +namespace keys { -using Pin = std::pair; -using StatePins = std::array; +PROGMEM_STRING(Save, "fanSave"); +PROGMEM_STRING(Speed, "fanSpeed"); -// XXX: while these are hard-coded, we don't really benefit from having these in the hardware cfg +} // namespace keys -StatePins statePins() { - return { - {{5, nullptr}, - {4, nullptr}, - {15, nullptr}} - }; +duration::Seconds save() { + return getSetting(keys::Save, build::SaveDelay); } -constexpr int controlPin() { - return 12; +FanSpeed speed() { + return getSetting(keys::Speed, build::Speed); } -struct Config { - duration::Seconds save; - FanSpeed speed; -}; +} // namespace settings -Config readSettings() { - return Config{ - .save = getSetting("fanSave", DefaultSaveDelay), - .speed = getSetting("fanSpeed", FanSpeed::Medium)}; -} +// We expect to write a specific 'mask' via GPIO LOW & HIGH to set the speed +// Sync up with the relay and write it on ON / OFF status events -StatePins state_pins; -Config config { - .save = DefaultSaveDelay, - .speed = FanSpeed::Medium, +struct Pin { + unsigned char init; + BasePinPtr handle; }; -void configure() { - config = readSettings(); -} +struct StatePins { + static constexpr size_t Gpios { 3ul }; + using State = std::array; + using Pins = std::array; -void report(FanSpeed speed [[gnu::unused]]) { -#if MQTT_SUPPORT - mqttSend(MQTT_TOPIC_SPEED, speedToPayload(speed).c_str()); -#endif -} + StatePins(const StatePins&) = delete; -void save(FanSpeed speed) { - static timer::SystemTimer ticker; - config.speed = speed; - ticker.once(config.save, []() { - const auto value = speedToPayload(config.speed); - setSetting("fanSpeed", value); - DEBUG_MSG_P(PSTR("[IFAN] Saved speed setting \"%s\"\n"), value.c_str()); - }); -} - -void cleanupPins(StatePins& pins) { - for (auto& pin : pins) { - if (!pin.second) continue; - gpioUnlock(pin.second->pin()); - pin.second.reset(nullptr); + StatePins() = default; + ~StatePins() { + reset(); } -} -StatePins setupStatePins() { - StatePins pins = statePins(); + StatePins(StatePins&&) = default; - for (auto& pair : pins) { - auto ptr = gpioRegister(pair.first); - if (!ptr) { - DEBUG_MSG_P(PSTR("[IFAN] Could not set up GPIO%d\n"), pair.first); - cleanupPins(pins); - return pins; - } - ptr->pinMode(OUTPUT); - pair.second = std::move(ptr); + bool init(); + + bool initialized() const { + return _initialized; } - return pins; -} + void reset(); + + State state(FanSpeed); + State update(FanSpeed); + + State state() const { + return _state; + } + + String mask(); + +private: + // XXX: while these are hard-coded, we don't really benefit from having these in the hardware cfg + bool _initialized { false }; + Pins _pins{ + Pin{5, nullptr}, + Pin{4, nullptr}, + Pin{15, nullptr} + }; -State stateFromSpeed(FanSpeed speed) { + State _state {LOW, LOW, LOW}; +}; + +StatePins::State StatePins::state(FanSpeed speed) { switch (speed) { case FanSpeed::Low: - return {HIGH, LOW, LOW}; + _state = {HIGH, LOW, LOW}; + break; case FanSpeed::Medium: - return {HIGH, HIGH, LOW}; + _state = {HIGH, HIGH, LOW}; + break; case FanSpeed::High: - return {HIGH, LOW, HIGH}; + _state = {HIGH, LOW, HIGH}; + break; case FanSpeed::Off: + _state = {LOW, LOW, LOW}; break; } - return {LOW, LOW, LOW}; + return _state; } -const char* maskFromSpeed(FanSpeed speed) { - switch (speed) { - case FanSpeed::Low: - return "0b100"; - case FanSpeed::Medium: - return "0b110"; - case FanSpeed::High: - return "0b101"; - case FanSpeed::Off: - return "0b000"; +String StatePins::mask() { + String out("0b000"); + for (size_t index = 2; index != out.length(); ++index) { + out[index] = (_state[index - 2] == HIGH) ? '1' : '0'; } - return ""; + return out; } -// Note that we use API speed endpoint strictly for the setting -// (which also allows to pre-set the speed without turning the relay ON) +void StatePins::reset() { + for (auto& pin : _pins) { + if (pin.handle) { + gpioUnlock(pin.handle->pin()); + pin.handle.reset(nullptr); + } + } +} + +bool StatePins::init() { + if (_initialized) { + return true; + } + + for (auto& pair : _pins) { + pair.handle = gpioRegister(pair.init); + if (!pair.handle) { + DEBUG_MSG_P(PSTR("[IFAN] Could not set up GPIO%hhu\n"), pair.init); + reset(); + return false; + } + + pair.handle->pinMode(OUTPUT); + } + + _initialized = true; + return true; +} + +StatePins::State StatePins::update(FanSpeed speed) { + const auto out = state(speed); + + for (size_t index = 0; index < _pins.size(); ++index) { + auto& handle = _pins[index].handle; + if (!handle) { + continue; + } + + handle->digitalWrite(_state[index]); + } + + return out; +} + +struct ControlPin { + ~ControlPin() { + reset(); + } + + explicit operator bool() const { + return static_cast(_pin); + } + + ControlPin& operator=(uint8_t pin) { + reset(); + + _pin = gpioRegister(pin); + if (_pin) { + _pin->pinMode(OUTPUT); + } + + return *this; + } + + ControlPin& operator=(BasePinPtr pin) { + reset(); + _pin = std::move(pin); + return *this; + } + + void reset() { + if (_pin) { + gpioUnlock(_pin->pin()); + _pin.reset(nullptr); + } + } + + BasePin* operator->() { + return _pin.get(); + } + + BasePin* operator->() const { + return _pin.get(); + } -using FanSpeedUpdate = std::function; +private: + BasePinPtr _pin; +}; -FanSpeedUpdate onFanSpeedUpdate = [](FanSpeed) { +struct Config { + duration::Seconds save; + FanSpeed speed; }; -void updateSpeed(Config& config, FanSpeed speed) { - switch (speed) { - case FanSpeed::Low: - case FanSpeed::Medium: - case FanSpeed::High: - save(speed); - report(speed); - onFanSpeedUpdate(speed); - break; - case FanSpeed::Off: - break; +namespace internal { + +timer::SystemTimer config_timer; +Config config; + +size_t relay_id { RelaysMax }; +ControlPin control_pin; +FanSpeed speed { FanSpeed::Off }; + +StatePins state_pins; + +} // namespace internal + +bool currentStatus() { + return internal::speed != FanSpeed::Off; +} + +void currentStatus(bool status) { + internal::speed = status + ? internal::config.speed + : FanSpeed::Off; +} + +FanSpeed currentSpeed() { + return internal::speed; +} + +String speedToPayload() { + return speedToPayload(currentSpeed()); +} + +void save(FanSpeed speed) { + internal::config.speed = speed; + + if (FanSpeed::Off != speed) { + internal::config_timer.once( + internal::config.save, + [speed]() { + const auto value = speedToPayload(speed); + setSetting(settings::keys::Speed, value); + DEBUG_MSG_P(PSTR("[IFAN] Saved speed \"%s\"\n"), value.c_str()); + }); } } -void updateSpeed(FanSpeed speed) { - updateSpeed(config, speed); +void report(FanSpeed speed [[gnu::unused]]) { +#if MQTT_SUPPORT + mqttSend(MQTT_TOPIC_SPEED, speedToPayload(speed).c_str()); +#endif +} + +void pin_update(FanSpeed speed) { + const bool status = FanSpeed::Off != speed; + + relayStatus(internal::relay_id, status); + internal::control_pin->digitalWrite(status ? HIGH : LOW); + + internal::state_pins.update(speed); } -void updateSpeedFromPayload(StringView payload) { - updateSpeed(payloadToSpeed(payload.toString())); +void pin_update() { + pin_update(internal::speed); +} + +FanSpeed update(FanSpeed value) { + const auto last = internal::speed; + if (value != last) { + save(value); + report(value); + } + + internal::speed = value; + pin_update(value); + + return value; +} + +FanSpeed update(bool status) { + currentStatus(status); + return update(internal::speed); +} + +void configure() { + const auto updated = Config{ + .save = settings::save(), + .speed = settings::speed()}; + + internal::config = updated; + pin_update(); +} + +// Note that we use API speed endpoint strictly for the setting +// (which also allows to pre-set the speed without turning the relay ON) + +FanSpeed updateSpeedFromPayload(StringView payload) { + return update(payloadToSpeed(payload.toString())); } #if MQTT_SUPPORT @@ -239,47 +389,18 @@ void onMqttEvent(unsigned int type, StringView topic, StringView payload) { #endif // MQTT_SUPPORT -class FanProvider : public RelayProviderBase { +class FanRelayProvider : public RelayProviderBase { public: - FanProvider(BasePinPtr&& pin, const Config& config, const StatePins& pins, FanSpeedUpdate& callback) : - _pin(std::move(pin)), - _config(config), - _pins(pins) - { - callback = [this](FanSpeed speed) { - change(speed); - }; - _pin->pinMode(OUTPUT); - } - - const char* id() const override { - return "fan"; - } - - void change(FanSpeed speed) { - _pin->digitalWrite((FanSpeed::Off != speed) ? HIGH : LOW); - - auto state = stateFromSpeed(speed); - DEBUG_MSG_P(PSTR("[IFAN] State mask: %s\n"), maskFromSpeed(speed)); - - for (size_t index = 0; index < _pins.size(); ++index) { - auto& pin = _pins[index].second; - if (!pin) { - continue; - } - - pin->digitalWrite(state[index]); - } + espurna::StringView id() const override { + return STRING_VIEW("fan"); } void change(bool status) override { - change(status ? _config.speed : FanSpeed::Off); + ifan02::update(status); } private: BasePinPtr _pin; - const Config& _config; - const StatePins& _pins; }; #if TERMINAL_SUPPORT @@ -288,15 +409,16 @@ namespace terminal { PROGMEM_STRING(Speed, "SPEED"); void speed(::terminal::CommandContext&& ctx) { + auto value = ifan02::currentSpeed(); if (ctx.argv.size() == 2) { - updateSpeedFromPayload(ctx.argv[1]); + value = updateSpeedFromPayload(ctx.argv[1]); } ctx.output.printf_P(PSTR("%s %s\n"), - (config.speed != FanSpeed::Off) + (value != FanSpeed::Off) ? PSTR("speed") : PSTR("fan is"), - speedToPayload(config.speed).c_str()); + speedToPayload(value).c_str()); terminalOK(ctx); } @@ -311,27 +433,20 @@ void setup() { } // namespace terminal #endif -void setup() { - state_pins = setupStatePins(); - if (!state_pins.size()) { - return; +bool setup() { + if (internal::control_pin && internal::state_pins.initialized()) { + return true; + } + + internal::control_pin = build::ControlPin; + if (!internal::state_pins.init()) { + internal::control_pin.reset(); + return false; } configure(); espurnaRegisterReload(configure); - auto relay_pin = gpioRegister(controlPin()); - if (relay_pin) { - auto provider = std::make_unique( - std::move(relay_pin), config, state_pins, onFanSpeedUpdate); - - const auto result = relayAdd(std::move(provider)); - if (result) { - DEBUG_MSG_P(PSTR("[IFAN] Could not add relay provider for GPIO%d\n"), controlPin()); - gpioUnlock(controlPin()); - } - } - #if MQTT_SUPPORT mqttRegister(onMqttEvent); #endif @@ -339,7 +454,7 @@ void setup() { #if API_SUPPORT apiRegister(F(MQTT_TOPIC_SPEED), [](ApiRequest& request) { - request.send(speedToPayload(config.speed)); + request.send(speedToPayload()); return true; }, [](ApiRequest& request) { @@ -353,18 +468,43 @@ void setup() { terminal::setup(); #endif + return true; +} + +RelayProviderBasePtr make_relay_provider(size_t index) { + RelayProviderBasePtr out; + + if (setup()) { + out = std::make_unique(); + internal::relay_id = index; + } + + return out; } + } // namespace } // namespace ifan02 } // namespace espurna +RelayProviderBasePtr fanMakeRelayProvider(size_t index) { + return espurna::ifan02::make_relay_provider(index); +} + +void fanStatus(bool value) { + espurna::ifan02::currentStatus(value); +} + +bool fanStatus() { + return espurna::ifan02::currentStatus(); +} + FanSpeed fanSpeed() { - return espurna::ifan02::config.speed; + return espurna::ifan02::currentSpeed(); } void fanSpeed(FanSpeed speed) { - espurna::ifan02::updateSpeed(FanSpeed::Low); + espurna::ifan02::update(speed); } void fanSetup() { diff --git a/code/espurna/relay.cpp b/code/espurna/relay.cpp index ce4030f6..07ccc529 100644 --- a/code/espurna/relay.cpp +++ b/code/espurna/relay.cpp @@ -66,6 +66,7 @@ enum class RelayProvider { Dual, Stm, LightState, + Fan, }; enum class RelaySync { @@ -643,14 +644,16 @@ PROGMEM_STRING(RelayProviderGpio, "gpio"); PROGMEM_STRING(RelayProviderDual, "dual"); PROGMEM_STRING(RelayProviderStm, "stm"); PROGMEM_STRING(RelayProviderLightState, "light-state"); +PROGMEM_STRING(RelayProviderFan, "fan"); -static constexpr std::array, 6> RelayProviderOptions PROGMEM { +static constexpr std::array, 7> RelayProviderOptions PROGMEM { {{RelayProvider::None, RelayProviderNone}, {RelayProvider::Dummy, RelayProviderDummy}, {RelayProvider::Gpio, RelayProviderGpio}, {RelayProvider::Dual, RelayProviderDual}, {RelayProvider::Stm, RelayProviderStm}, - {RelayProvider::LightState, RelayProviderLightState}} + {RelayProvider::LightState, RelayProviderLightState}, + {RelayProvider::Fan, RelayProviderFan}} }; PROGMEM_STRING(RelayTypeNormal, "normal"); @@ -2932,6 +2935,12 @@ RelayProviderBasePtr _relaySetupProvider(size_t index) { #endif break; + case RelayProvider::Fan: +#if FAN_SUPPORT + result = fanMakeRelayProvider(index); +#endif + break; + case RelayProvider::None: break; }