pulse_counter: Restore original pulse counter

This commit is contained in:
brisk 2024-07-04 22:39:09 +09:30
parent 2b51fe7f95
commit 7e22e7f813
5 changed files with 457 additions and 0 deletions

View file

@ -0,0 +1,24 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/automation.h"
#include "esphome/components/pulse_counter/pulse_counter_sensor.h"
namespace esphome {
namespace pulse_counter {
template<typename... Ts> class SetTotalPulsesAction : public Action<Ts...> {
public:
SetTotalPulsesAction(PulseCounterSensor *pulse_counter) : pulse_counter_(pulse_counter) {}
TEMPLATABLE_VALUE(uint32_t, total_pulses)
void play(Ts... x) override { this->pulse_counter_->set_total_pulses(this->total_pulses_.value(x...)); }
protected:
PulseCounterSensor *pulse_counter_;
};
} // namespace pulse_counter
} // namespace esphome

View file

@ -0,0 +1,187 @@
#include "pulse_counter_sensor.h"
#include "esphome/core/log.h"
namespace esphome {
namespace pulse_counter {
static const char *const TAG = "pulse_counter";
const char *const EDGE_MODE_TO_STRING[] = {"DISABLE", "INCREMENT", "DECREMENT"};
#ifdef HAS_PCNT
PulseCounterStorageBase *get_storage(bool hw_pcnt) {
return (hw_pcnt ? (PulseCounterStorageBase *) (new HwPulseCounterStorage)
: (PulseCounterStorageBase *) (new BasicPulseCounterStorage));
}
#else
PulseCounterStorageBase *get_storage(bool) { return new BasicPulseCounterStorage; }
#endif
void IRAM_ATTR BasicPulseCounterStorage::gpio_intr(BasicPulseCounterStorage *arg) {
const uint32_t now = micros();
const bool discard = now - arg->last_pulse < arg->filter_us;
arg->last_pulse = now;
if (discard)
return;
PulseCounterCountMode mode = arg->isr_pin.digital_read() ? arg->rising_edge_mode : arg->falling_edge_mode;
switch (mode) {
case PULSE_COUNTER_DISABLE:
break;
case PULSE_COUNTER_INCREMENT:
arg->counter++;
break;
case PULSE_COUNTER_DECREMENT:
arg->counter--;
break;
}
}
bool BasicPulseCounterStorage::pulse_counter_setup(InternalGPIOPin *pin) {
this->pin = pin;
this->pin->setup();
this->isr_pin = this->pin->to_isr();
this->pin->attach_interrupt(BasicPulseCounterStorage::gpio_intr, this, gpio::INTERRUPT_ANY_EDGE);
return true;
}
pulse_counter_t BasicPulseCounterStorage::read_raw_value() {
pulse_counter_t counter = this->counter;
pulse_counter_t ret = counter - this->last_value;
this->last_value = counter;
return ret;
}
#ifdef HAS_PCNT
bool HwPulseCounterStorage::pulse_counter_setup(InternalGPIOPin *pin) {
static pcnt_unit_t next_pcnt_unit = PCNT_UNIT_0;
this->pin = pin;
this->pin->setup();
this->pcnt_unit = next_pcnt_unit;
next_pcnt_unit = pcnt_unit_t(int(next_pcnt_unit) + 1);
ESP_LOGCONFIG(TAG, " PCNT Unit Number: %u", this->pcnt_unit);
pcnt_count_mode_t rising = PCNT_COUNT_DIS, falling = PCNT_COUNT_DIS;
switch (this->rising_edge_mode) {
case PULSE_COUNTER_DISABLE:
rising = PCNT_COUNT_DIS;
break;
case PULSE_COUNTER_INCREMENT:
rising = PCNT_COUNT_INC;
break;
case PULSE_COUNTER_DECREMENT:
rising = PCNT_COUNT_DEC;
break;
}
switch (this->falling_edge_mode) {
case PULSE_COUNTER_DISABLE:
falling = PCNT_COUNT_DIS;
break;
case PULSE_COUNTER_INCREMENT:
falling = PCNT_COUNT_INC;
break;
case PULSE_COUNTER_DECREMENT:
falling = PCNT_COUNT_DEC;
break;
}
pcnt_config_t pcnt_config = {
.pulse_gpio_num = this->pin->get_pin(),
.ctrl_gpio_num = PCNT_PIN_NOT_USED,
.lctrl_mode = PCNT_MODE_KEEP,
.hctrl_mode = PCNT_MODE_KEEP,
.pos_mode = rising,
.neg_mode = falling,
.counter_h_lim = 0,
.counter_l_lim = 0,
.unit = this->pcnt_unit,
.channel = PCNT_CHANNEL_0,
};
esp_err_t error = pcnt_unit_config(&pcnt_config);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Configuring Pulse Counter failed: %s", esp_err_to_name(error));
return false;
}
if (this->filter_us != 0) {
uint16_t filter_val = std::min(static_cast<unsigned int>(this->filter_us * 80u), 1023u);
ESP_LOGCONFIG(TAG, " Filter Value: %" PRIu32 "us (val=%u)", this->filter_us, filter_val);
error = pcnt_set_filter_value(this->pcnt_unit, filter_val);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Setting filter value failed: %s", esp_err_to_name(error));
return false;
}
error = pcnt_filter_enable(this->pcnt_unit);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Enabling filter failed: %s", esp_err_to_name(error));
return false;
}
}
error = pcnt_counter_pause(this->pcnt_unit);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Pausing pulse counter failed: %s", esp_err_to_name(error));
return false;
}
error = pcnt_counter_clear(this->pcnt_unit);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Clearing pulse counter failed: %s", esp_err_to_name(error));
return false;
}
error = pcnt_counter_resume(this->pcnt_unit);
if (error != ESP_OK) {
ESP_LOGE(TAG, "Resuming pulse counter failed: %s", esp_err_to_name(error));
return false;
}
return true;
}
pulse_counter_t HwPulseCounterStorage::read_raw_value() {
pulse_counter_t counter;
pcnt_get_counter_value(this->pcnt_unit, &counter);
pulse_counter_t ret = counter - this->last_value;
this->last_value = counter;
return ret;
}
#endif
void PulseCounterSensor::setup() {
ESP_LOGCONFIG(TAG, "Setting up pulse counter '%s'...", this->name_.c_str());
if (!this->storage_.pulse_counter_setup(this->pin_)) {
this->mark_failed();
return;
}
}
void PulseCounterSensor::set_total_pulses(uint32_t pulses) {
this->current_total_ = pulses;
this->total_sensor_->publish_state(pulses);
}
void PulseCounterSensor::dump_config() {
LOG_SENSOR("", "Pulse Counter", this);
LOG_PIN(" Pin: ", this->pin_);
ESP_LOGCONFIG(TAG, " Rising Edge: %s", EDGE_MODE_TO_STRING[this->storage_.rising_edge_mode]);
ESP_LOGCONFIG(TAG, " Falling Edge: %s", EDGE_MODE_TO_STRING[this->storage_.falling_edge_mode]);
ESP_LOGCONFIG(TAG, " Filtering pulses shorter than %" PRIu32 " µs", this->storage_.filter_us);
LOG_UPDATE_INTERVAL(this);
}
void PulseCounterSensor::update() {
pulse_counter_t raw = this->storage_.read_raw_value();
uint32_t now = millis();
if (this->last_time_ != 0) {
uint32_t interval = now - this->last_time_;
float value = (60000.0f * raw) / float(interval); // per minute
ESP_LOGD(TAG, "'%s': Retrieved counter: %0.2f pulses/min", this->get_name().c_str(), value);
this->publish_state(value);
}
if (this->total_sensor_ != nullptr) {
current_total_ += raw;
ESP_LOGD(TAG, "'%s': Total : %" PRIu32 " pulses", this->get_name().c_str(), current_total_);
this->total_sensor_->publish_state(current_total_);
}
this->last_time_ = now;
}
} // namespace pulse_counter
} // namespace esphome

View file

@ -0,0 +1,90 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/components/sensor/sensor.h"
#include <cinttypes>
#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3)
#include <driver/pcnt.h>
#define HAS_PCNT
#endif
namespace esphome {
namespace pulse_counter {
enum PulseCounterCountMode {
PULSE_COUNTER_DISABLE = 0,
PULSE_COUNTER_INCREMENT,
PULSE_COUNTER_DECREMENT,
};
#ifdef HAS_PCNT
using pulse_counter_t = int16_t;
#else
using pulse_counter_t = int32_t;
#endif
struct PulseCounterStorageBase {
virtual bool pulse_counter_setup(InternalGPIOPin *pin) = 0;
virtual pulse_counter_t read_raw_value() = 0;
InternalGPIOPin *pin;
PulseCounterCountMode rising_edge_mode{PULSE_COUNTER_INCREMENT};
PulseCounterCountMode falling_edge_mode{PULSE_COUNTER_DISABLE};
uint32_t filter_us{0};
pulse_counter_t last_value{0};
};
struct BasicPulseCounterStorage : public PulseCounterStorageBase {
static void gpio_intr(BasicPulseCounterStorage *arg);
bool pulse_counter_setup(InternalGPIOPin *pin) override;
pulse_counter_t read_raw_value() override;
volatile pulse_counter_t counter{0};
volatile uint32_t last_pulse{0};
ISRInternalGPIOPin isr_pin;
};
#ifdef HAS_PCNT
struct HwPulseCounterStorage : public PulseCounterStorageBase {
bool pulse_counter_setup(InternalGPIOPin *pin) override;
pulse_counter_t read_raw_value() override;
pcnt_unit_t pcnt_unit;
};
#endif
PulseCounterStorageBase *get_storage(bool hw_pcnt = false);
class PulseCounterSensor : public sensor::Sensor, public PollingComponent {
public:
explicit PulseCounterSensor(bool hw_pcnt = false) : storage_(*get_storage(hw_pcnt)) {}
void set_pin(InternalGPIOPin *pin) { pin_ = pin; }
void set_rising_edge_mode(PulseCounterCountMode mode) { storage_.rising_edge_mode = mode; }
void set_falling_edge_mode(PulseCounterCountMode mode) { storage_.falling_edge_mode = mode; }
void set_filter_us(uint32_t filter) { storage_.filter_us = filter; }
void set_total_sensor(sensor::Sensor *total_sensor) { total_sensor_ = total_sensor; }
void set_total_pulses(uint32_t pulses);
/// Unit of measurement is "pulses/min".
void setup() override;
void update() override;
float get_setup_priority() const override { return setup_priority::DATA; }
void dump_config() override;
protected:
InternalGPIOPin *pin_;
PulseCounterStorageBase &storage_;
uint32_t last_time_{0};
uint32_t current_total_{0};
sensor::Sensor *total_sensor_{nullptr};
};
} // namespace pulse_counter
} // namespace esphome

View file

@ -0,0 +1,156 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import automation, pins
from esphome.components import sensor
from esphome.const import (
CONF_COUNT_MODE,
CONF_FALLING_EDGE,
CONF_ID,
CONF_INTERNAL_FILTER,
CONF_PIN,
CONF_RISING_EDGE,
CONF_NUMBER,
CONF_TOTAL,
CONF_VALUE,
ICON_PULSE,
STATE_CLASS_MEASUREMENT,
STATE_CLASS_TOTAL_INCREASING,
UNIT_PULSES_PER_MINUTE,
UNIT_PULSES,
)
from esphome.core import CORE
CONF_USE_PCNT = "use_pcnt"
pulse_counter_ns = cg.esphome_ns.namespace("pulse_counter")
PulseCounterCountMode = pulse_counter_ns.enum("PulseCounterCountMode")
COUNT_MODES = {
"DISABLE": PulseCounterCountMode.PULSE_COUNTER_DISABLE,
"INCREMENT": PulseCounterCountMode.PULSE_COUNTER_INCREMENT,
"DECREMENT": PulseCounterCountMode.PULSE_COUNTER_DECREMENT,
}
COUNT_MODE_SCHEMA = cv.enum(COUNT_MODES, upper=True)
PulseCounterSensor = pulse_counter_ns.class_(
"PulseCounterSensor", sensor.Sensor, cg.PollingComponent
)
SetTotalPulsesAction = pulse_counter_ns.class_(
"SetTotalPulsesAction", automation.Action
)
def validate_internal_filter(value):
use_pcnt = value.get(CONF_USE_PCNT)
if CORE.is_esp8266 and use_pcnt:
raise cv.Invalid(
"Using hardware PCNT is only available on ESP32",
[CONF_USE_PCNT],
)
if CORE.is_esp32 and use_pcnt:
if value.get(CONF_INTERNAL_FILTER).total_microseconds > 13:
raise cv.Invalid(
"Maximum internal filter value when using ESP32 hardware PCNT is 13us",
[CONF_INTERNAL_FILTER],
)
return value
def validate_pulse_counter_pin(value):
value = pins.internal_gpio_input_pin_schema(value)
if CORE.is_esp8266 and value[CONF_NUMBER] >= 16:
raise cv.Invalid(
"Pins GPIO16 and GPIO17 cannot be used as pulse counters on ESP8266."
)
return value
def validate_count_mode(value):
rising_edge = value[CONF_RISING_EDGE]
falling_edge = value[CONF_FALLING_EDGE]
if rising_edge == "DISABLE" and falling_edge == "DISABLE":
raise cv.Invalid(
"Can't set both count modes to DISABLE! This means no counting occurs at "
"all!"
)
return value
CONFIG_SCHEMA = cv.All(
sensor.sensor_schema(
PulseCounterSensor,
unit_of_measurement=UNIT_PULSES_PER_MINUTE,
icon=ICON_PULSE,
accuracy_decimals=2,
state_class=STATE_CLASS_MEASUREMENT,
)
.extend(
{
cv.Required(CONF_PIN): validate_pulse_counter_pin,
cv.Optional(
CONF_COUNT_MODE,
default={
CONF_RISING_EDGE: "INCREMENT",
CONF_FALLING_EDGE: "DISABLE",
},
): cv.All(
cv.Schema(
{
cv.Required(CONF_RISING_EDGE): COUNT_MODE_SCHEMA,
cv.Required(CONF_FALLING_EDGE): COUNT_MODE_SCHEMA,
}
),
validate_count_mode,
),
cv.SplitDefault(CONF_USE_PCNT, esp32=True): cv.boolean,
cv.Optional(
CONF_INTERNAL_FILTER, default="13us"
): cv.positive_time_period_microseconds,
cv.Optional(CONF_TOTAL): sensor.sensor_schema(
unit_of_measurement=UNIT_PULSES,
icon=ICON_PULSE,
accuracy_decimals=0,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
},
)
.extend(cv.polling_component_schema("60s")),
validate_internal_filter,
)
async def to_code(config):
var = await sensor.new_sensor(config, config.get(CONF_USE_PCNT))
await cg.register_component(var, config)
pin = await cg.gpio_pin_expression(config[CONF_PIN])
cg.add(var.set_pin(pin))
count = config[CONF_COUNT_MODE]
cg.add(var.set_rising_edge_mode(count[CONF_RISING_EDGE]))
cg.add(var.set_falling_edge_mode(count[CONF_FALLING_EDGE]))
cg.add(var.set_filter_us(config[CONF_INTERNAL_FILTER]))
if CONF_TOTAL in config:
sens = await sensor.new_sensor(config[CONF_TOTAL])
cg.add(var.set_total_sensor(sens))
@automation.register_action(
"pulse_counter.set_total_pulses",
SetTotalPulsesAction,
cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(PulseCounterSensor),
cv.Required(CONF_VALUE): cv.templatable(cv.uint32_t),
}
),
)
async def set_total_action_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_VALUE], args, int)
cg.add(var.set_total_pulses(template_))
return var