File MycilaDimmer.h¶
File List > src > MycilaDimmer.h
Go to the documentation of this file
// SPDX-License-Identifier: MIT
/*
* Copyright (C) Mathieu Carbou
*/
#pragma once
#define MYCILA_DIMMER_VERSION "3.0.0"
#define MYCILA_DIMMER_VERSION_MAJOR 3
#define MYCILA_DIMMER_VERSION_MINOR 0
#define MYCILA_DIMMER_VERSION_REVISION 0
#ifdef MYCILA_JSON_SUPPORT
#include <ArduinoJson.h>
#endif
#include <assert.h>
#include <esp32-hal-gpio.h>
#include <cmath>
#include <cstdio>
namespace Mycila {
class Dimmer {
public:
typedef struct {
// Output voltage (dimmed)
float voltage = 0.0f;
float current = 0.0f;
float power = 0.0f;
float apparentPower = 0.0f;
float powerFactor = NAN;
float thdi = NAN;
} Metrics;
public:
virtual ~Dimmer() { end(); };
virtual bool begin() {
_enabled = true;
return true;
}
virtual void end() { _enabled = false; }
virtual const char* type() const { return "virtual"; }
// DIMMER CONFIG //
void setDutyCycleLimit(float limit) {
_dutyCycleLimit = _contrain(limit, 0, 1);
if (_dutyCycle > _dutyCycleLimit)
setDutyCycle(_dutyCycleLimit);
}
void setDutyCycleMin(float min) {
_dutyCycleMin = _contrain(min, 0, _dutyCycleMax);
setDutyCycle(_dutyCycle);
}
void setDutyCycleMax(float max) {
_dutyCycleMax = _contrain(max, _dutyCycleMin, 1);
setDutyCycle(_dutyCycle);
}
float getDutyCycleLimit() const { return _dutyCycleLimit; }
float getDutyCycleMin() const { return _dutyCycleMin; }
float getDutyCycleMax() const { return _dutyCycleMax; }
// SEMi-PERIOD //
static uint16_t getSemiPeriod() { return _semiPeriod; }
static void setSemiPeriod(uint16_t semiPeriod) { _semiPeriod = semiPeriod; }
// DIMMER STATES //
bool isEnabled() const { return _enabled; }
virtual bool isOnline() const { return _enabled && _online; }
void setOnline(bool online) {
_online = online;
if (!_online) {
_dutyCycleFire = 0.0f;
if (_enabled)
_apply();
} else {
setDutyCycle(_dutyCycle);
}
}
// DIMMER CONTROL //
void on() { setDutyCycle(1); }
void off() { setDutyCycle(0); }
bool isOff() const { return !isOn(); }
bool isOn() const { return isOnline() && _dutyCycle; }
bool isOnAtFullPower() const { return _dutyCycle >= _dutyCycleMax; }
virtual bool setDutyCycle(float dutyCycle) {
// Apply limit and save the wanted duty cycle.
// It will only be applied when dimmer will be on.
_dutyCycle = _contrain(dutyCycle, 0, _dutyCycleLimit);
_dutyCycleFire = getDutyCycleMapped();
return isOnline() && _apply();
}
// DUTY CYCLE //
float getDutyCycle() const { return _dutyCycle; }
float getDutyCycleMapped() const { return _dutyCycleMin + _dutyCycle * (_dutyCycleMax - _dutyCycleMin); }
float getDutyCycleFire() const { return isOnline() ? _dutyCycleFire : 0.0f; }
// METRICS //
virtual float getPowerRatio() const {
// For a linear dimmer, the power ratio is directly proportional to the duty cycle
return getDutyCycleFire();
}
// Calculate harmonics based on dimmer firing angle for resistive loads
// array[0] = H1 (fundamental), array[1] = H3, array[2] = H5, array[3] = H7, etc.
// Only odd harmonics are calculated (even harmonics are negligible for symmetric dimmers)
// Returns true if harmonics were calculated, false if dimmer is not active
bool calculateHarmonics(float* array, size_t n) const {
if (array == nullptr || n == 0)
return false;
float duty = getDutyCycleFire();
// Check if dimmer is active and routing
if (duty <= 0.0f) {
for (size_t i = 0; i < n; i++) {
array[i] = 0.0f; // No power, no harmonics
}
return true;
}
if (duty >= 1.0f) {
array[0] = 100.0f; // H1 (fundamental) = 100% reference
for (size_t i = 1; i < n; i++) {
array[i] = 0.0f; // No harmonics at full power
}
return true;
}
// Initialize all values to NAN
for (size_t i = 0; i < n; i++) {
array[i] = NAN;
}
return _calculateDimmerHarmonics(array, n);
}
bool calculateMetrics(Metrics& metrics, float gridVoltage, float loadResistance) const {
if (!_enabled || loadResistance <= 0 || gridVoltage <= 0) {
return false;
}
const float powerRatio = getPowerRatio();
if (powerRatio <= 0) {
// no power
metrics.apparentPower = 0.0f;
metrics.current = 0.0f;
metrics.power = 0.0f;
metrics.powerFactor = NAN;
metrics.thdi = NAN;
metrics.voltage = 0.0f;
return true;
}
if (powerRatio >= 1) {
// full power
const float nominalPower = gridVoltage * gridVoltage / loadResistance;
metrics.apparentPower = nominalPower;
metrics.current = gridVoltage / loadResistance;
metrics.power = nominalPower;
metrics.powerFactor = 1.0f;
metrics.thdi = 0.0f;
metrics.voltage = gridVoltage;
return true;
}
const float nominalPower = gridVoltage * gridVoltage / loadResistance;
metrics.power = powerRatio * nominalPower;
metrics.powerFactor = std::sqrt(powerRatio);
metrics.voltage = metrics.powerFactor * gridVoltage;
metrics.current = metrics.voltage / loadResistance;
metrics.apparentPower = gridVoltage * metrics.current;
// THDi calculation for resistive load:
// PF = 1 / sqrt(1 + THDi^2) => THDi = sqrt(1/PF^2 - 1)
metrics.thdi = 100.0f * std::sqrt(1.0f / (metrics.powerFactor * metrics.powerFactor) - 1.0f);
return true;
}
#ifdef MYCILA_JSON_SUPPORT
virtual void toJson(const JsonObject& root) const {
static const char* H_LEVELS[] = {"H1", "H3", "H5", "H7", "H9", "H11", "H13", "H15", "H17", "H19", "H21"};
root["type"] = type();
root["enabled"] = isEnabled();
root["online"] = isOnline();
root["state"] = isOn() ? "on" : "off";
root["semi_period"] = getSemiPeriod();
root["duty_cycle"] = getDutyCycle();
root["duty_cycle_mapped"] = getDutyCycleMapped();
root["duty_cycle_fire"] = getDutyCycleFire();
root["duty_cycle_limit"] = getDutyCycleLimit();
root["duty_cycle_min"] = getDutyCycleMin();
root["duty_cycle_max"] = getDutyCycleMax();
JsonObject harmonics = root["harmonics"].to<JsonObject>();
float* output = new float[11]; // H1 to H21
if (calculateHarmonics(output, 11)) {
for (size_t i = 0; i < 11; i++) {
if (!std::isnan(output[i])) {
harmonics[H_LEVELS[i]] = output[i];
}
}
}
}
#endif
protected:
bool _enabled = false;
bool _online = false;
float _dutyCycle = 0.0f;
float _dutyCycleFire = 0.0f;
float _dutyCycleLimit = 1.0f;
float _dutyCycleMin = 0.0f;
float _dutyCycleMax = 1.0f;
inline static uint16_t _semiPeriod = 0;
virtual bool _apply() { return _enabled; }
virtual bool _calculateDimmerHarmonics(float* array, size_t n) const {
for (size_t i = 0; i < n; i++) {
array[i] = 0.0f; // No harmonics for default dimmer
}
return true;
}
// STATIC HELPERS //
static inline float _contrain(float amt, float low, float high) {
return (amt < low) ? low : ((amt > high) ? high : amt);
}
};
} // namespace Mycila