MycilaJSY 13.0.0
Arduino / ESP32 library for the JSY1031, JSY-MK-163, JSY-MK-193, JSY-MK-194, JSY-MK-227, JSY-MK-229, JSY-MK-333 families single-phase and three-phase AC bidirectional meters from Shenzhen Jiansiyan Technologies Co, Ltd.
Loading...
Searching...
No Matches
MycilaDimmer.h
1// SPDX-License-Identifier: MIT
2/*
3 * Copyright (C) 2023-2025 Mathieu Carbou
4 */
5#pragma once
6
7#define MYCILA_DIMMER_VERSION "2.2.4"
8#define MYCILA_DIMMER_VERSION_MAJOR 2
9#define MYCILA_DIMMER_VERSION_MINOR 2
10#define MYCILA_DIMMER_VERSION_REVISION 4
11
12#ifdef MYCILA_JSON_SUPPORT
13 #include <ArduinoJson.h>
14#endif
15
16#include <assert.h>
17#include <esp32-hal-gpio.h>
18
19#include <cstdio>
20
21namespace Mycila {
22 class Dimmer {
23 public:
24 typedef struct {
25 // Output voltage (dimmed)
26 float voltage = 0.0f;
27 float current = 0.0f;
28 float power = 0.0f;
29 float apparentPower = 0.0f;
30 float powerFactor = NAN;
31 float thdi = NAN;
32 } Metrics;
33
34 public:
35 virtual ~Dimmer() {};
36
37 virtual void begin() { _enabled = true; };
38 virtual void end() { _enabled = false; };
39 virtual const char* type() const { return "virtual"; }
40
42 // DIMMER CONFIG //
44
50 void setDutyCycleLimit(float limit) {
51 _dutyCycleLimit = _contrain(limit, 0, 1);
52 if (_dutyCycle > _dutyCycleLimit)
53 setDutyCycle(_dutyCycleLimit);
54 }
55
62 void setDutyCycleMin(float min) {
63 _dutyCycleMin = _contrain(min, 0, _dutyCycleMax);
64 setDutyCycle(_dutyCycle);
65 }
66
73 void setDutyCycleMax(float max) {
74 _dutyCycleMax = _contrain(max, _dutyCycleMin, 1);
75 setDutyCycle(_dutyCycle);
76 }
77
81 float getDutyCycleLimit() const { return _dutyCycleLimit; }
82
86 float getDutyCycleMin() const { return _dutyCycleMin; }
87
91 float getDutyCycleMax() const { return _dutyCycleMax; }
92
94 // POWER LUT //
96
109 void enablePowerLUT(bool enable, uint16_t semiPeriod = 0) {
110 if (!enable) {
111 _powerLUTEnabled = false;
112 return;
113 }
114 // Enabling the LUT
115 if (semiPeriod > 0) {
116 // A semi-period is provided, use it
117 _semiPeriod = semiPeriod;
118 } else {
119 // semiPeriod == 0, use already set semi-period, must be >0
120 if (_semiPeriod == 0) {
121 ESP_LOGE("Dimmer", "enablePowerLUT: semiPeriod must be provided or must be already set when enabling power LUT");
122 }
123 assert(_semiPeriod > 0);
124 }
125 _powerLUTEnabled = true;
126 }
127
131 bool isPowerLUTEnabled() const { return _powerLUTEnabled; }
132
136 uint16_t getPowerLUTSemiPeriod() const { return _powerLUTEnabled ? _semiPeriod : 0; }
137
139 // DIMMER STATES //
141
145 bool isEnabled() const { return _enabled; }
146
150 bool isOnline() const { return _enabled && _online; }
151
156 void setOnline(bool online) {
157 _online = online;
158 if (!_online) {
159 _dutyCycleFire = 0.0f;
160 if (_enabled)
161 _apply();
162 } else {
163 setDutyCycle(_dutyCycle);
164 }
165 }
166
168 // DIMMER CONTROL //
170
174 void on() { setDutyCycle(1); }
175
179 void off() { setDutyCycle(0); }
180
184 bool isOff() const { return !isOn(); }
185
189 bool isOn() const { return isOnline() && _dutyCycle; }
190
194 bool isOnAtFullPower() const { return _dutyCycle >= _dutyCycleMax; }
195
201 bool setDutyCycle(float dutyCycle) {
202 // Apply limit and save the wanted duty cycle.
203 // It will only be applied when dimmer will be on.
204 _dutyCycle = _contrain(dutyCycle, 0, _dutyCycleLimit);
205
206 const float mapped = getDutyCycleMapped();
207
208 if (_powerLUTEnabled) {
209 if (mapped == 0) {
210 _dutyCycleFire = 0.0f;
211 } else if (mapped == 1) {
212 _dutyCycleFire = 1.0f;
213 } else {
214 _dutyCycleFire = 1.0f - static_cast<float>(_lookupFiringDelay(mapped, _semiPeriod)) / static_cast<float>(_semiPeriod);
215 }
216 } else {
217 _dutyCycleFire = mapped;
218 }
219
220 return isOnline() && _apply();
221 }
222
224 // DUTY CYCLE //
226
230 float getDutyCycle() const { return _dutyCycle; }
231
235 float getDutyCycleMapped() const { return _dutyCycleMin + _dutyCycle * (_dutyCycleMax - _dutyCycleMin); }
236
245 float getDutyCycleFire() const { return isOnline() ? _dutyCycleFire : 0.0f; }
246
248 // METRICS //
250
251 // Calculate harmonics based on dimmer firing angle for resistive loads
252 // array[0] = H1 (fundamental), array[1] = H3, array[2] = H5, array[3] = H7, etc.
253 // Only odd harmonics are calculated (even harmonics are negligible for symmetric dimmers)
254 // Returns true if harmonics were calculated, false if dimmer is not active
255 bool calculateHarmonics(float* array, size_t n) const {
256 if (array == nullptr || n == 0)
257 return false;
258
259 // Check if dimmer is active and routing
260 if (!isOnline() || _dutyCycleFire <= 0.0f) {
261 for (size_t i = 0; i < n; i++) {
262 array[i] = 0.0f; // No power, no harmonics
263 }
264 return true;
265 }
266
267 if (_dutyCycleFire >= 1.0f) {
268 array[0] = 100.0f; // H1 (fundamental) = 100% reference
269 for (size_t i = 1; i < n; i++) {
270 array[i] = 0.0f; // No harmonics at full power
271 }
272 return true;
273 }
274
275 // Initialize all values to NAN
276 for (size_t i = 0; i < n; i++) {
277 array[i] = NAN;
278 }
279
280 return _calculateHarmonics(array, n);
281 }
282
283 virtual bool calculateMetrics(Metrics& metrics, float gridVoltage, float loadResistance) const { return false; }
284
285#ifdef MYCILA_JSON_SUPPORT
291 virtual void toJson(const JsonObject& root) const;
292#endif
293
294 protected:
295 bool _enabled = false;
296 bool _online = false;
297
298 float _dutyCycle = 0.0f;
299 float _dutyCycleFire = 0.0f;
300 float _dutyCycleLimit = 1.0f;
301 float _dutyCycleMin = 0.0f;
302 float _dutyCycleMax = 1.0f;
303
304 bool _powerLUTEnabled = false;
305 uint16_t _semiPeriod = 0;
306
307 virtual bool _apply() { return _enabled; }
308 virtual bool _calculateHarmonics(float* array, size_t n) const {
309 for (size_t i = 0; i < n; i++) {
310 array[i] = 0.0f; // No harmonics for virtual dimmer
311 }
312 return true;
313 }
314
316 // STATIC HELPERS //
318
319 static uint16_t _lookupFiringDelay(float dutyCycle, uint16_t semiPeriod);
320
321 static inline float _contrain(float amt, float low, float high) {
322 return (amt < low) ? low : ((amt > high) ? high : amt);
323 }
324
325 static bool _calculatePhaseControlHarmonics(float dutyCycleFire, float* array, size_t n) {
326 // getDutyCycleFire() returns the conduction angle normalized (0-1)
327 // Convert to firing angle: α = π × (1 - conduction)
328 // At 50% power: α ≈ 90° (π/2), which gives maximum harmonics
329 const float firingAngle = M_PI * (1.0f - dutyCycleFire);
330
331 // Calculate RMS of fundamental component (reference)
332 // Formula from Thierry Lequeu: I1_rms = (1/π) × √[2(π - α + ½sin(2α))]
333 const float sin_2a = sinf(2.0f * firingAngle);
334 const float i1_rms = sqrtf((2.0f / M_PI) * (M_PI - firingAngle + 0.5f * sin_2a));
335
336 if (i1_rms <= 0.001f)
337 return false;
338
339 array[0] = 100.0f; // H1 (fundamental) = 100% reference
340
341 // Pre-compute scale factor for efficiency
342 const float scale_factor = (2.0f / M_PI) * 0.70710678f * 100.0f / i1_rms;
343
344 // Calculate odd harmonics (H3, H5, H7, ...)
345 // Formula for phase-controlled resistive loads (IEEE standard):
346 // Hn = (2/π√2) × |cos((n-1)α)/(n-1) - cos((n+1)α)/(n+1)| / I1_rms × 100%
347 // This gives the correct harmonic magnitudes relative to the fundamental
348 for (size_t i = 1; i < n; i++) {
349 const float n_f = static_cast<float>(2 * i + 1); // 3, 5, 7, 9, ...
350 const float n_minus_1 = n_f - 1.0f;
351 const float n_plus_1 = n_f + 1.0f;
352
353 // Compute Fourier coefficient
354 const float coeff = cosf(n_minus_1 * firingAngle) / n_minus_1 -
355 cosf(n_plus_1 * firingAngle) / n_plus_1;
356
357 // Convert to percentage of fundamental
358 array[i] = fabsf(coeff) * scale_factor;
359 }
360
361 return true;
362 }
363
364 static bool _calculatePhaseControlMetrics(Metrics& metrics, float dutyCycleFire, float gridVoltage, float loadResistance) {
365 if (loadResistance > 0 && gridVoltage > 0) {
366 if (dutyCycleFire > 0) {
367 const float nominalPower = gridVoltage * gridVoltage / loadResistance;
368 if (dutyCycleFire >= 1.0f) {
369 // full power
370 metrics.powerFactor = 1.0f;
371 metrics.thdi = 0.0f;
372 metrics.power = nominalPower;
373 metrics.voltage = gridVoltage;
374 metrics.current = gridVoltage / loadResistance;
375 metrics.apparentPower = nominalPower;
376 return true;
377 } else {
378 // partial power
379 metrics.powerFactor = std::sqrt(dutyCycleFire);
380 metrics.thdi = 100.0f * std::sqrt(1 / dutyCycleFire - 1);
381 metrics.power = dutyCycleFire * nominalPower;
382 metrics.voltage = metrics.powerFactor * gridVoltage;
383 metrics.current = metrics.voltage / loadResistance;
384 metrics.apparentPower = gridVoltage * metrics.current;
385 return true;
386 }
387 } else {
388 // no power
389 metrics.voltage = 0.0f;
390 metrics.current = 0.0f;
391 metrics.power = 0.0f;
392 metrics.apparentPower = 0.0f;
393 metrics.powerFactor = NAN;
394 metrics.thdi = NAN;
395 return true;
396 }
397 } else {
398 return false;
399 }
400 }
401 };
402} // namespace Mycila
403
404#include "MycilaDimmerDFRobot.h"
405#include "MycilaDimmerPWM.h"
406#include "MycilaDimmerThyristor.h"
void off()
Turn off the dimmer.
float getDutyCycleFire() const
Get the real firing duty cycle (conduction duty cycle) applied to the dimmer in the range [0,...
bool isOnline() const
Returns true if the dimmer is marked online.
uint16_t getPowerLUTSemiPeriod() const
Get the semi-period in us used for the power LUT calculations. If LUT is disabled,...
float getDutyCycleMax() const
Get the remapped "1" of the dimmer duty cycle.
bool isOn() const
Check if the dimmer is on.
bool isEnabled() const
Check if the dimmer is enabled (if it was able to initialize correctly).
void setDutyCycleMin(float min)
Duty remapping (equivalent to Shelly Dimmer remapping feature). Useful to calibrate the dimmer when u...
float getDutyCycle() const
Get the power duty cycle configured for the dimmer by teh user.
void setOnline(bool online)
Set the online status of the dimmer.
void on()
Turn on the dimmer at full power.
bool isPowerLUTEnabled() const
Check if the power LUT is enabled.
float getDutyCycleLimit() const
Get the power duty cycle limit of the dimmer.
float getDutyCycleMapped() const
Get the remapped power duty cycle from the currently user set duty cycle.
bool setDutyCycle(float dutyCycle)
Set the power duty.
bool isOff() const
Check if the dimmer is off.
bool isOnAtFullPower() const
Check if the dimmer is on at full power.
void enablePowerLUT(bool enable, uint16_t semiPeriod=0)
Enable or disable the use of power LUT for dimmer curve The power LUT provides a non-linear dimming c...
float getDutyCycleMin() const
Get the remapped "0" of the dimmer duty cycle.
void setDutyCycleMax(float max)
Duty remapping (equivalent to Shelly Dimmer remapping feature). Useful to calibrate the dimmer when u...
void setDutyCycleLimit(float limit)
Set the power duty cycle limit of the dimmer. The duty cycle will be clamped to this limit.