// -----------------------------------------------------------------------------
|
|
// Abstract Energy Monitor Sensor (other EMON sensors extend this class)
|
|
// Copyright (C) 2017-2019 by Xose Pérez <xose dot perez at gmail dot com>
|
|
//
|
|
// Modified to be an extended version of the BaseEmonSensor and have more reusable code
|
|
// Copyright (C) 2020-2021 by Maxim Prokhorov <prokhorov dot max at outlook dot com>
|
|
// -----------------------------------------------------------------------------
|
|
|
|
#pragma once
|
|
|
|
#include "BaseEmonSensor.h"
|
|
|
|
#include "../libs/fs_math.h"
|
|
|
|
class BaseAnalogEmonSensor : public BaseEmonSensor {
|
|
public:
|
|
static const BaseSensor::ClassKind Kind;
|
|
BaseSensor::ClassKind kind() const override {
|
|
return Kind;
|
|
}
|
|
|
|
using TimeSource = espurna::time::CoreClock;
|
|
static constexpr auto MaxTime = TimeSource::duration { EMON_MAX_TIME };
|
|
|
|
static constexpr double IRef { EMON_CURRENT_RATIO };
|
|
|
|
// TODO: mask common magnitudes (...voltage), when there are multiple channels?
|
|
static constexpr Magnitude Magnitudes[] {
|
|
MAGNITUDE_CURRENT,
|
|
MAGNITUDE_VOLTAGE,
|
|
MAGNITUDE_POWER_APPARENT,
|
|
MAGNITUDE_ENERGY
|
|
};
|
|
|
|
BaseAnalogEmonSensor() :
|
|
BaseEmonSensor(Magnitudes)
|
|
{}
|
|
|
|
unsigned char count() const override {
|
|
return std::size(Magnitudes);
|
|
}
|
|
|
|
virtual unsigned int analogRead() = 0;
|
|
|
|
virtual void setVoltage(double) = 0;
|
|
virtual double getVoltage() const = 0;
|
|
|
|
virtual void setReferenceVoltage(double) = 0;
|
|
virtual double getReferenceVoltage() const = 0;
|
|
|
|
virtual void setPivot(double) = 0;
|
|
virtual double getPivot() const = 0;
|
|
|
|
virtual void updateCurrent(double) = 0;
|
|
virtual double getCurrent() const = 0;
|
|
|
|
double defaultVoltage() const {
|
|
return EMON_MAINS_VOLTAGE;
|
|
}
|
|
|
|
double defaultReferenceVoltage() const {
|
|
return EMON_REFERENCE_VOLTAGE;
|
|
}
|
|
|
|
void setSamplesMax(size_t samples) {
|
|
_samples = samples;
|
|
_samples_max = samples;
|
|
_dirty = true;
|
|
}
|
|
|
|
void setResolution(size_t resolution) {
|
|
_resolution = resolution;
|
|
_adc_counts = 1 << _resolution;
|
|
setPivot(_adc_counts >> 1);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------
|
|
// Sensor API
|
|
// ---------------------------------------------------------------------
|
|
|
|
void begin() override {
|
|
updateCurrent(0.0);
|
|
setPivot(_adc_counts >> 1); // aka divide by 2
|
|
calculateFactors();
|
|
|
|
_ready = true;
|
|
_dirty = false;
|
|
|
|
#if SENSOR_DEBUG
|
|
DEBUG_MSG_P(PSTR("[EMON] Reference (mV): %ld\n"),
|
|
std::lround(1000.0 * getReferenceVoltage()));
|
|
DEBUG_MSG_P(PSTR("[EMON] ADC counts: %lu\n"), _adc_counts);
|
|
DEBUG_MSG_P(PSTR("[EMON] Channel current ratio (mA/V): %ld\n"),
|
|
std::lround(1000.0 * getRatio(0)));
|
|
DEBUG_MSG_P(PSTR("[EMON] Channel current factor (mA/bit): %ld\n"),
|
|
std::lround(1000.0 * _current_factor));
|
|
DEBUG_MSG_P(PSTR("[EMON] Channel multiplier: %u\n"), _multiplier);
|
|
#endif
|
|
}
|
|
|
|
void pre() override {
|
|
updateCurrent(sampleCurrent());
|
|
|
|
const auto now = TimeSource::now();
|
|
if (!_initial) {
|
|
using namespace espurna::sensor;
|
|
const auto elapsed = std::chrono::duration_cast<espurna::duration::Seconds>(now - _last_reading);
|
|
_energy[0] += WattSeconds(Watts{getCurrent() * getVoltage()}, elapsed);
|
|
}
|
|
|
|
_initial = false;
|
|
_last_reading = now;
|
|
_error = SENSOR_ERROR_OK;
|
|
}
|
|
|
|
unsigned char type(unsigned char index) const override {
|
|
if (index < std::size(Magnitudes)) {
|
|
return Magnitudes[index].type;
|
|
}
|
|
|
|
return MAGNITUDE_NONE;
|
|
}
|
|
|
|
double value(unsigned char index) override {
|
|
switch (index) {
|
|
case 0:
|
|
return getCurrent();
|
|
case 1:
|
|
return getVoltage();
|
|
case 2:
|
|
return getCurrent() * getVoltage();
|
|
case 3:
|
|
return _energy[0].asDouble();
|
|
}
|
|
|
|
return 0.0;
|
|
}
|
|
|
|
double sampleCurrent() {
|
|
int max = 0;
|
|
int min = _adc_counts;
|
|
double sum = 0;
|
|
|
|
auto pivot = getPivot();
|
|
|
|
const auto time_span = TimeSource::now();
|
|
for (size_t i = 0; i < _samples; i++) {
|
|
int sample;
|
|
double filtered;
|
|
|
|
sample = this->analogRead();
|
|
if (sample > max) max = sample;
|
|
if (sample < min) min = sample;
|
|
|
|
// Digital low pass filter extracts the VDC offset
|
|
pivot = (pivot + (sample - pivot) / EMON_FILTER_SPEED);
|
|
filtered = sample - pivot;
|
|
|
|
// Root-mean-square method
|
|
sum += (filtered * filtered);
|
|
}
|
|
|
|
const auto elapsed = TimeSource::now() - time_span;
|
|
|
|
// Quick fix
|
|
if (pivot < min || max < pivot) {
|
|
pivot = (max + min) / 2.0;
|
|
}
|
|
|
|
setPivot(pivot);
|
|
|
|
// Calculate current
|
|
double rms = _samples > 0 ? fs_sqrt(sum / _samples) : 0;
|
|
double current = _current_factor * rms;
|
|
|
|
current = (double) (int(current * _multiplier) - 1) / _multiplier;
|
|
if (current < 0) {
|
|
current = 0;
|
|
}
|
|
|
|
#if SENSOR_DEBUG
|
|
DEBUG_MSG_P(PSTR("[EMON] Total samples: %d\n"), _samples);
|
|
DEBUG_MSG_P(PSTR("[EMON] Total time (ms): %u\n"), elapsed.count());
|
|
DEBUG_MSG_P(PSTR("[EMON] Sample frequency (Hz): %d\n"), int(1000 * _samples / elapsed.count()));
|
|
DEBUG_MSG_P(PSTR("[EMON] Max value: %d\n"), max);
|
|
DEBUG_MSG_P(PSTR("[EMON] Min value: %d\n"), min);
|
|
DEBUG_MSG_P(PSTR("[EMON] Midpoint value: %d\n"), int(getPivot()));
|
|
DEBUG_MSG_P(PSTR("[EMON] RMS value: %d\n"), int(rms));
|
|
DEBUG_MSG_P(PSTR("[EMON] Current (mA): %d\n"), int(1000 * current));
|
|
#endif
|
|
|
|
if ((elapsed > MaxTime)
|
|
|| ((elapsed < MaxTime) && (_samples < _samples_max)))
|
|
{
|
|
_samples = (_samples * MaxTime.count()) / elapsed.count();
|
|
}
|
|
|
|
return current;
|
|
}
|
|
|
|
void calculateFactors() {
|
|
_current_factor = getRatio(0) * getReferenceVoltage() / _adc_counts;
|
|
unsigned int s = 1;
|
|
unsigned int i = 1;
|
|
unsigned int m = 1;
|
|
unsigned int multiplier = 1;
|
|
while (m * _current_factor < 1) {
|
|
multiplier = m;
|
|
i = (i == 1) ? 2 : (i == 2) ? 5 : 1;
|
|
if (i == 1) s *= 10;
|
|
m = s * i;
|
|
}
|
|
_multiplier = multiplier;
|
|
}
|
|
|
|
private:
|
|
TimeSource::time_point _last_reading;
|
|
bool _initial { true };
|
|
|
|
double _current_factor { 1.0 }; // Calculated, reads (RMS) to current
|
|
unsigned int _multiplier { 1 }; // Calculated, error
|
|
|
|
size_t _samples_max { EMON_MAX_SAMPLES }; // Number of samples, will be adjusted at runtime
|
|
size_t _samples { _samples_max }; // based on the maximum value
|
|
|
|
size_t _resolution { EMON_ANALOG_RESOLUTION }; // ADC resolution (in bits)
|
|
size_t _adc_counts { static_cast<size_t>(1) << _resolution }; // Max count
|
|
};
|
|
|
|
#if __cplusplus < 201703L
|
|
constexpr BaseSensor::Magnitude BaseAnalogEmonSensor::Magnitudes[];
|
|
#endif
|
|
|
|
// Provide EMON API helper where we don't care about specifics of how the values are stored
|
|
|
|
class SimpleAnalogEmonSensor : public BaseAnalogEmonSensor {
|
|
public:
|
|
SimpleAnalogEmonSensor() = default;
|
|
SimpleAnalogEmonSensor(const SimpleAnalogEmonSensor&) = default;
|
|
SimpleAnalogEmonSensor(SimpleAnalogEmonSensor&&) = default;
|
|
|
|
// ---------------------------------------------------------------------
|
|
// EMON API
|
|
// ---------------------------------------------------------------------
|
|
|
|
void setVoltage(double voltage) override {
|
|
_voltage = voltage;
|
|
_dirty = true;
|
|
}
|
|
|
|
double getVoltage() const override {
|
|
return _voltage;
|
|
}
|
|
|
|
void setReferenceVoltage(double voltage) override {
|
|
_reference_voltage = voltage;
|
|
_dirty = true;
|
|
}
|
|
|
|
double getReferenceVoltage() const override {
|
|
return _reference_voltage;
|
|
}
|
|
|
|
double defaultRatio(unsigned char index) const override {
|
|
if (index == 0) {
|
|
return IRef;
|
|
}
|
|
|
|
return BaseEmonSensor::defaultRatio(index);
|
|
}
|
|
|
|
double getRatio(unsigned char index) const override {
|
|
if (index == 0) {
|
|
return _current_ratio;
|
|
}
|
|
|
|
return BaseEmonSensor::getRatio(index);
|
|
}
|
|
|
|
void setRatio(unsigned char index, double ratio) override {
|
|
if ((index == 0) && (ratio > 0.0)) {
|
|
_current_ratio = ratio;
|
|
calculateFactors();
|
|
_dirty = true;
|
|
}
|
|
}
|
|
|
|
void resetRatios() override {
|
|
setRatio(0, defaultRatio(0));
|
|
calculateFactors();
|
|
}
|
|
|
|
void setPivot(double pivot) override {
|
|
_pivot = pivot;
|
|
_dirty = true;
|
|
}
|
|
|
|
double getPivot() const override {
|
|
return _pivot;
|
|
}
|
|
|
|
void updateCurrent(double current) override {
|
|
_current = current;
|
|
}
|
|
|
|
double getCurrent() const override {
|
|
return _current;
|
|
}
|
|
|
|
private:
|
|
double _voltage { 0.0 };
|
|
double _reference_voltage { 0.0 };
|
|
double _pivot { 0.0 };
|
|
double _current { 0.0 };
|
|
};
|
|
|
|
const BaseSensor::ClassKind BaseAnalogEmonSensor::Kind;
|