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.3"
8#define MYCILA_DIMMER_VERSION_MAJOR 2
9#define MYCILA_DIMMER_VERSION_MINOR 2
10#define MYCILA_DIMMER_VERSION_REVISION 1
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 root["type"] = type();
293 root["enabled"] = _enabled;
294 root["online"] = _online;
295 root["state"] = isOn() ? "on" : "off";
296 root["duty_cycle"] = _dutyCycle;
297 root["duty_cycle_mapped"] = getDutyCycleMapped();
298 root["duty_cycle_fire"] = _dutyCycleFire;
299 root["duty_cycle_limit"] = _dutyCycleLimit;
300 root["duty_cycle_min"] = _dutyCycleMin;
301 root["duty_cycle_max"] = _dutyCycleMax;
302 root["power_lut"] = _powerLUTEnabled;
303 root["power_lut_semi_period"] = _semiPeriod;
304 JsonObject harmonics = root["harmonics"].to<JsonObject>();
305 float output[11]; // H1 to H21
306 if (calculateHarmonics(output, 11)) {
307 for (size_t i = 0; i < 11; i++) {
308 if (!std::isnan(output[i])) {
309 char key[8];
310 snprintf(key, sizeof(key), "H%d", static_cast<int>(2 * i + 1));
311 harmonics[key] = output[i];
312 }
313 }
314 }
315 }
316#endif
317
318 protected:
319 bool _enabled = false;
320 bool _online = false;
321
322 float _dutyCycle = 0.0f;
323 float _dutyCycleFire = 0.0f;
324 float _dutyCycleLimit = 1.0f;
325 float _dutyCycleMin = 0.0f;
326 float _dutyCycleMax = 1.0f;
327
328 bool _powerLUTEnabled = false;
329 uint16_t _semiPeriod = 0;
330
331 virtual bool _apply() { return _enabled; }
332 virtual bool _calculateHarmonics(float* array, size_t n) const {
333 for (size_t i = 0; i < n; i++) {
334 array[i] = 0.0f; // No harmonics for virtual dimmer
335 }
336 return true;
337 }
338
340 // STATIC HELPERS //
342
343 static uint16_t _lookupFiringDelay(float dutyCycle, uint16_t semiPeriod);
344
345 static inline float _contrain(float amt, float low, float high) {
346 return (amt < low) ? low : ((amt > high) ? high : amt);
347 }
348
349 static bool _calculatePhaseControlHarmonics(float dutyCycleFire, float* array, size_t n) {
350 // getDutyCycleFire() returns the conduction angle normalized (0-1)
351 // Convert to firing angle: α = π × (1 - conduction)
352 // At 50% power: α ≈ 90° (π/2), which gives maximum harmonics
353 const float firingAngle = M_PI * (1.0f - dutyCycleFire);
354
355 // Calculate RMS of fundamental component (reference)
356 // Formula from Thierry Lequeu: I1_rms = (1/π) × √[2(π - α + ½sin(2α))]
357 const float sin_2a = sinf(2.0f * firingAngle);
358 const float i1_rms = sqrtf((2.0f / M_PI) * (M_PI - firingAngle + 0.5f * sin_2a));
359
360 if (i1_rms <= 0.001f)
361 return false;
362
363 array[0] = 100.0f; // H1 (fundamental) = 100% reference
364
365 // Pre-compute scale factor for efficiency
366 const float scale_factor = (2.0f / M_PI) * 0.70710678f * 100.0f / i1_rms;
367
368 // Calculate odd harmonics (H3, H5, H7, ...)
369 // Formula for phase-controlled resistive loads (IEEE standard):
370 // Hn = (2/π√2) × |cos((n-1)α)/(n-1) - cos((n+1)α)/(n+1)| / I1_rms × 100%
371 // This gives the correct harmonic magnitudes relative to the fundamental
372 for (size_t i = 1; i < n; i++) {
373 const float n_f = static_cast<float>(2 * i + 1); // 3, 5, 7, 9, ...
374 const float n_minus_1 = n_f - 1.0f;
375 const float n_plus_1 = n_f + 1.0f;
376
377 // Compute Fourier coefficient
378 const float coeff = cosf(n_minus_1 * firingAngle) / n_minus_1 -
379 cosf(n_plus_1 * firingAngle) / n_plus_1;
380
381 // Convert to percentage of fundamental
382 array[i] = fabsf(coeff) * scale_factor;
383 }
384
385 return true;
386 }
387
388 static bool _calculatePhaseControlMetrics(Metrics& metrics, float dutyCycleFire, float gridVoltage, float loadResistance) {
389 if (loadResistance > 0 && gridVoltage > 0) {
390 if (dutyCycleFire > 0) {
391 const float nominalPower = gridVoltage * gridVoltage / loadResistance;
392 if (dutyCycleFire >= 1.0f) {
393 // full power
394 metrics.powerFactor = 1.0f;
395 metrics.thdi = 0.0f;
396 metrics.power = nominalPower;
397 metrics.voltage = gridVoltage;
398 metrics.current = gridVoltage / loadResistance;
399 metrics.apparentPower = nominalPower;
400 return true;
401 } else {
402 // partial power
403 metrics.powerFactor = std::sqrt(dutyCycleFire);
404 metrics.thdi = 100.0f * std::sqrt(1 / dutyCycleFire - 1);
405 metrics.power = dutyCycleFire * nominalPower;
406 metrics.voltage = metrics.powerFactor * gridVoltage;
407 metrics.current = metrics.voltage / loadResistance;
408 metrics.apparentPower = gridVoltage * metrics.current;
409 return true;
410 }
411 } else {
412 // no power
413 metrics.voltage = 0.0f;
414 metrics.current = 0.0f;
415 metrics.power = 0.0f;
416 metrics.apparentPower = 0.0f;
417 metrics.powerFactor = NAN;
418 metrics.thdi = NAN;
419 return true;
420 }
421 } else {
422 return false;
423 }
424 }
425 };
426} // namespace Mycila
427
428#include "MycilaDimmerDFRobot.h"
429#include "MycilaDimmerPWM.h"
430#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.