Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 134 additions & 0 deletions code/espurna/ac_switch.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*

AC FREQUENCY DETECTION SWITCH MODULE

*/

#include "ac_switch.h"
#include "espurna.h"

namespace espurna {

AcSwitch::AcSwitch(uint8_t pin, unsigned long timeout_ms, unsigned long min_pulses, int frequency) :
_pin(pin),
_timeout_ms(timeout_ms),
_min_pulses(min_pulses)
{
if (frequency == 50) {
// 10000us period for 50Hz with CHANGE interrupt. Allow 3.5% tolerance.
_min_filter_period = 9650;
_max_filter_period = 10350;
} else if (frequency == 60) {
// ~8333us period for 60Hz. Allow 3.5% tolerance.
_min_filter_period = 8041;
_max_filter_period = 8625;
}

// INPUT_PULLUP is usually safer if the optocoupler pulls to ground.
// If the schematic is different, INPUT might be required.
pinMode(_pin, INPUT_PULLUP);
attachInterrupt(_pin, [this]() { this->_isr(); }, CHANGE);
Comment thread
mcspr marked this conversation as resolved.
Outdated
}

AcSwitch::~AcSwitch() {
detachInterrupt(_pin);
}

bool AcSwitch::loop() {
const bool debug_enabled = getSetting("btnAcDbg", false);
unsigned long now = millis();
if (now - _last_check < _timeout_ms) {
return false;
}

noInterrupts();
unsigned long count = _pulses;
_pulses = 0;
interrupts();

// Debug output every 5 seconds
if (debug_enabled && (now - _last_debug > 5000)) {
noInterrupts();
unsigned long d_count = _debug_pulse_count;
unsigned long d_valid = _debug_valid_pulse_count;
unsigned long d_min = _min_period;
unsigned long d_max = _max_period;
unsigned long d_sum = _sum_period;
unsigned long d_p_count = _period_count;

// Reset stats
_debug_pulse_count = 0;
_debug_valid_pulse_count = 0;
_min_period = -1UL;
_max_period = 0;
_sum_period = 0;
_period_count = 0;
interrupts();

unsigned long avg = d_p_count ? (d_sum / d_p_count) : 0;
DEBUG_MSG_P(PSTR("[AC] Pin %u: pulses=%lu valid=%lu period(us) min=%lu max=%lu avg=%lu\n"),
_pin, d_count, d_valid, (d_min == -1UL ? 0 : d_min), d_max, avg);

_last_debug = now;
}

bool changed = false;
bool new_state = (count >= _min_pulses);

if (new_state == _candidate_state) {
if (_latch < 10) _latch++;
} else {
_candidate_state = new_state;
_latch = 0;
}

if (_latch >= 1 && _candidate_state != _state) {
if (debug_enabled) {
DEBUG_MSG_P(PSTR("[AC] Pin %u: State changed %s -> %s (count: %lu, min: %lu)\n"),
_pin, _state ? "ON" : "OFF", _candidate_state ? "ON" : "OFF", count, _min_pulses);
}
_state = _candidate_state;
changed = true;
}

_last_check = now;
return changed;
}

bool AcSwitch::state() const {
return _state;
}

void IRAM_ATTR AcSwitch::_isr() {
unsigned long now = micros();
unsigned long delta = now - _last_isr_time;

// Filter out noise (pulses shorter than 5ms)
if (_last_isr_time != 0 && delta < 5000) {
return;
}

if (_last_isr_time != 0) {
if (delta < _min_period) _min_period = delta;
if (delta > _max_period) _max_period = delta;
_sum_period += delta;
_period_count++;
}
_last_isr_time = now;
++_debug_pulse_count;

// If frequency filter is disabled, count all non-noise pulses
if (_min_filter_period == 0) {
++_pulses;
++_debug_valid_pulse_count;
return;
}

// Count only pulses that are within the expected frequency range
if ((delta >= _min_filter_period) && (delta <= _max_filter_period)) {
++_pulses;
++_debug_valid_pulse_count;
}
}

} // namespace espurna
52 changes: 52 additions & 0 deletions code/espurna/ac_switch.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*

AC FREQUENCY DETECTION SWITCH MODULE

*/

#pragma once

#include <Arduino.h>
#include <FunctionalInterrupt.h>

namespace espurna {

class AcSwitch {
public:
// pin: GPIO pin
// timeout_ms: time window for pulse counting (default 100 ms)
// min_pulses: minimum number of pulses for ON state (default 5)
// frequency: target AC frequency (50 or 60). 0 to disable frequency filter.
AcSwitch(uint8_t pin, unsigned long timeout_ms = 100, unsigned long min_pulses = 5, int frequency = 0);
~AcSwitch();

bool loop();
bool state() const;

private:
void IRAM_ATTR _isr();

uint8_t _pin;
unsigned long _timeout_ms;
unsigned long _min_pulses;
uint16_t _min_filter_period { 0 };
uint16_t _max_filter_period { 0 };

volatile unsigned long _pulses{0};
unsigned long _last_check{0};
bool _state{false};
bool _candidate_state{false};
unsigned int _latch{0};

// Statistics for debugging
unsigned long _last_debug{0};
volatile unsigned long _last_isr_time{0};
volatile unsigned long _min_period{-1UL};
volatile unsigned long _max_period{0};
volatile unsigned long _sum_period{0};
volatile unsigned long _period_count{0};
volatile unsigned long _debug_pulse_count{0};
volatile unsigned long _debug_valid_pulse_count{0};
};

} // namespace espurna
79 changes: 78 additions & 1 deletion code/espurna/button.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Copyright (C) 2019-2021 by Maxim Prokhorov <prokhorov dot max at outlook dot com
#if BUTTON_SUPPORT

#include "button.h"
#include "ac_switch.h"
#include "compat.h"
#include "fan.h"
#include "gpio.h"
Expand All @@ -37,6 +38,10 @@ Copyright (C) 2019-2021 by Maxim Prokhorov <prokhorov dot max at outlook dot com

// -----------------------------------------------------------------------------

#ifndef BUTTON_AC_FREQ
#define BUTTON_AC_FREQ 0
#endif

static constexpr auto ButtonsPresetMax [[gnu::unused]] = size_t(8);

enum class ButtonProvider {
Expand All @@ -62,6 +67,7 @@ struct ButtonEventDelays {
unsigned long repeat;
unsigned long lngclick;
unsigned long lnglngclick;
unsigned long min_pulses;
};

namespace espurna {
Expand All @@ -76,11 +82,14 @@ struct Button {
Button(ButtonActions&& actions, ButtonEventDelays&& delays);
Button(BasePinPtr&& pin, const debounce_event::types::Config& config,
ButtonActions&& actions, ButtonEventDelays&& delays);
Button(BasePinPtr&& pin, const debounce_event::types::Config& config,
ButtonActions&& actions, ButtonEventDelays&& delays, bool is_freq);

bool state();
ButtonEvent loop();

ButtonEventEmitterPtr event_emitter;
std::unique_ptr<AcSwitch> ac_switch;

ButtonActions actions;
ButtonEventDelays event_delays;
Expand Down Expand Up @@ -114,6 +123,11 @@ PROGMEM_STRING(Relay, "btnRelay");
PROGMEM_STRING(MqttSendAll, "btnMqttSendAll");
PROGMEM_STRING(MqttRetain, "btnMqttRetain");

PROGMEM_STRING(Frequency, "btnFreq");
PROGMEM_STRING(MinPulses, "btnMinPulses");
PROGMEM_STRING(AcFreq, "btnAcFreq");
PROGMEM_STRING(AcDebug, "btnAcDbg");

[[gnu::unused]] PROGMEM_STRING(AnalogLevel, "btnLevel");

[[gnu::unused]] PROGMEM_STRING(TerminalCommand, "btnTermCmd");
Expand Down Expand Up @@ -329,6 +343,7 @@ constexpr int DefaultHigh { 1 << 3 };
constexpr int DefaultBoot { 1 << 4 };
constexpr int SetPullup { 1 << 5 };
constexpr int SetPulldown { 1 << 6 };
constexpr int Frequency { 1 << 7 };

} // namespace ButtonMask

Expand Down Expand Up @@ -558,6 +573,10 @@ constexpr unsigned long longLongClickDelay(size_t index) {
);
}

constexpr int acFreq() {
return BUTTON_AC_FREQ;
}

constexpr bool mqttSendAllEvents() {
return (1 == BUTTON_MQTT_SEND_ALL_EVENTS);
}
Expand Down Expand Up @@ -711,6 +730,22 @@ unsigned long longLongClickDelay(size_t index) {
return internal::indexedThenGlobal(keys::LongLongClickDelay, index, build::longLongClickDelay(index));
}

bool isFrequency(size_t index) {
return getSetting({keys::Frequency, index}, (build::internal::configBitmask(index) & build::internal::ButtonMask::Frequency));
}

unsigned long minPulses(size_t index) {
return internal::indexedThenGlobal(keys::MinPulses, index, 5ul);
}

int acFreq() {
return getSetting(keys::AcFreq, build::acFreq());
}

bool acDebug() {
return getSetting(keys::AcDebug, false);
}

[[gnu::unused]]
unsigned long repeatDelay() {
return getSetting(keys::RepeatDelay, build::repeatDelay());
Expand Down Expand Up @@ -771,6 +806,16 @@ ID_VALUE(longLongClick, settings::longLongClick)
ID_VALUE(debounceDelay, settings::debounceDelay)
ID_VALUE(longClickDelay, settings::longClickDelay)
ID_VALUE(longLongClickDelay, settings::longLongClickDelay)
ID_VALUE(isFrequency, settings::isFrequency)
ID_VALUE(minPulses, settings::minPulses)

String acFreq(size_t) {
return espurna::settings::internal::serialize(settings::acFreq());
}

String acDebug(size_t) {
return espurna::settings::internal::serialize(settings::acDebug());
}

#if RELAY_SUPPORT
ID_VALUE(relay, settings::relay)
Expand Down Expand Up @@ -802,6 +847,10 @@ static constexpr espurna::settings::query::IndexedSetting IndexedSettings[] PROG
{keys::DebounceDelay, internal::debounceDelay},
{keys::LongClickDelay, internal::longClickDelay},
{keys::LongLongClickDelay, internal::longLongClickDelay},
{keys::Frequency, internal::isFrequency},
{keys::MinPulses, internal::minPulses},
{keys::AcFreq, internal::acFreq},
{keys::AcDebug, internal::acDebug},
#if RELAY_SUPPORT
{keys::Relay, internal::relay},
#endif
Expand Down Expand Up @@ -902,6 +951,17 @@ Button::Button(BasePinPtr&& pin, const debounce_event::types::Config& config, Bu
event_delays(std::move(delays_))
{}

Button::Button(BasePinPtr&& pin, const debounce_event::types::Config& config, ButtonActions&& actions_, ButtonEventDelays&& delays_, bool is_freq) :
actions(std::move(actions_)),
event_delays(std::move(delays_))
{
if (is_freq && pin) {
ac_switch = std::make_unique<AcSwitch>(pin->pin(), delays_.debounce, delays_.min_pulses, settings::acFreq());
} else {
event_emitter = std::make_unique<debounce_event::EventEmitter>(std::move(pin), config, delays_.debounce, delays_.repeat);
}
}

constexpr ButtonEvent map_released(uint8_t count, unsigned long length, unsigned long lngclick_delay, unsigned long lnglngclick_delay) {
return (
(0 == count) ? ButtonEvent::Released :
Expand Down Expand Up @@ -933,6 +993,12 @@ ButtonEvent Button::loop() {
}
}

if (ac_switch) {
if (ac_switch->loop()) {
return ac_switch->state() ? ButtonEvent::Pressed : ButtonEvent::Released;
}
}

Comment thread
mcspr marked this conversation as resolved.
Outdated
return ButtonEvent::None;
}

Expand Down Expand Up @@ -1520,7 +1586,8 @@ ButtonEventDelays _buttonDelays(size_t index) {
.debounce = espurna::button::settings::debounceDelay(index),
.repeat = espurna::button::settings::repeatDelay(index),
.lngclick = espurna::button::settings::longClickDelay(index),
.lnglngclick = espurna::button::settings::longLongClickDelay(index)};
.lnglngclick = espurna::button::settings::longLongClickDelay(index),
.min_pulses = espurna::button::settings::minPulses(index)};
}

template <typename T>
Expand All @@ -1529,6 +1596,16 @@ void _buttonAdd(T&& button) {
}

espurna::button::Button _buttonWithPin(size_t index, BasePinPtr&& pin) {
if (espurna::button::settings::isFrequency(index)) {
return espurna::button::Button(
std::move(pin),
espurna::button::runtime_config(index),
_buttonActions(index),
_buttonDelays(index),
true
);
}

return espurna::button::Button(
std::move(pin),
espurna::button::runtime_config(index),
Expand Down
1 change: 1 addition & 0 deletions code/espurna/config/types.h
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
#define BUTTON_DEFAULT_BOOT ButtonMask::DefaultBoot
#define BUTTON_SET_PULLUP ButtonMask::SetPullup
#define BUTTON_SET_PULLDOWN ButtonMask::SetPulldown
#define BUTTON_FREQUENCY ButtonMask::Frequency

// configure where do we get the button events
#define BUTTON_PROVIDER_NONE ButtonProvider::None
Expand Down
Loading