Browse Source

fan: experiment with relay provider

early init plus simple state switch w/ api
pull/2575/head
Maxim Prokhorov 1 year ago
parent
commit
7362068717
5 changed files with 321 additions and 159 deletions
  1. +3
    -0
      code/espurna/config/hardware.h
  2. +1
    -0
      code/espurna/config/types.h
  3. +9
    -0
      code/espurna/fan.h
  4. +297
    -157
      code/espurna/ifan.cpp
  5. +11
    -2
      code/espurna/relay.cpp

+ 3
- 0
code/espurna/config/hardware.h View File

@ -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


+ 1
- 0
code/espurna/config/types.h View File

@ -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


+ 9
- 0
code/espurna/fan.h View File

@ -8,6 +8,9 @@ Copyright (C) 2021 by Maxim Prokhorov <prokhorov dot max at outlook dot com>
#pragma once
#include <cstddef>
#include <memory>
enum class FanSpeed {
Off,
Low,
@ -15,6 +18,12 @@ enum class FanSpeed {
High
};
class RelayProviderBase;
std::unique_ptr<RelayProviderBase> fanMakeRelayProvider(size_t);
bool fanStatus();
void fanStatus(bool);
void fanSpeed(FanSpeed);
FanSpeed fanSpeed();


+ 297
- 157
code/espurna/ifan.cpp View File

@ -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<int8_t, Gpios>;
namespace settings {
namespace keys {
using Pin = std::pair<int, BasePinPtr>;
using StatePins = std::array<Pin, Gpios>;
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<int8_t, Gpios>;
using Pins = std::array<Pin, Gpios>;
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<bool>(_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<void(FanSpeed)>;
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<FanProvider>(
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<FanRelayProvider>();
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() {


+ 11
- 2
code/espurna/relay.cpp View File

@ -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<Enumeration<RelayProvider>, 6> RelayProviderOptions PROGMEM {
static constexpr std::array<Enumeration<RelayProvider>, 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;
}


Loading…
Cancel
Save