|
|
@ -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() { |
|
|
|