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.3.0"
8#define MYCILA_DIMMER_VERSION_MAJOR 2
9#define MYCILA_DIMMER_VERSION_MINOR 2
10#define MYCILA_DIMMER_VERSION_REVISION 7
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 bool begin() {
38 _enabled = true;
39 return true;
40 }
41 virtual void end() { _enabled = false; }
42 virtual const char* type() const { return "virtual"; }
43
45 // DIMMER CONFIG //
47
53 void setDutyCycleLimit(float limit) {
54 _dutyCycleLimit = _contrain(limit, 0, 1);
55 if (_dutyCycle > _dutyCycleLimit)
56 setDutyCycle(_dutyCycleLimit);
57 }
58
65 void setDutyCycleMin(float min) {
66 _dutyCycleMin = _contrain(min, 0, _dutyCycleMax);
67 setDutyCycle(_dutyCycle);
68 }
69
76 void setDutyCycleMax(float max) {
77 _dutyCycleMax = _contrain(max, _dutyCycleMin, 1);
78 setDutyCycle(_dutyCycle);
79 }
80
84 float getDutyCycleLimit() const { return _dutyCycleLimit; }
85
89 float getDutyCycleMin() const { return _dutyCycleMin; }
90
94 float getDutyCycleMax() const { return _dutyCycleMax; }
95
97 // POWER LUT //
99
106 void enablePowerLUT(bool enable) { _powerLUTEnabled = enable; }
107
111 bool isPowerLUTEnabled() const { return _powerLUTEnabled; }
112
114 // SEMi-PERIOD //
116
120 static uint16_t getSemiPeriod() { return _semiPeriod; }
121
127 static void setSemiPeriod(uint16_t semiPeriod) { _semiPeriod = semiPeriod; }
128
130 // DIMMER STATES //
132
136 bool isEnabled() const { return _enabled; }
137
142 bool isOnline() const { return _enabled && _online && (!_powerLUTEnabled || _semiPeriod > 0); }
143
148 void setOnline(bool online) {
149 _online = online;
150 if (!_online) {
151 _dutyCycleFire = 0.0f;
152 if (_enabled)
153 _apply();
154 } else {
155 setDutyCycle(_dutyCycle);
156 }
157 }
158
160 // DIMMER CONTROL //
162
166 void on() { setDutyCycle(1); }
167
171 void off() { setDutyCycle(0); }
172
176 bool isOff() const { return !isOn(); }
177
181 bool isOn() const { return isOnline() && _dutyCycle; }
182
186 bool isOnAtFullPower() const { return _dutyCycle >= _dutyCycleMax; }
187
193 bool setDutyCycle(float dutyCycle) {
194 // Apply limit and save the wanted duty cycle.
195 // It will only be applied when dimmer will be on.
196 _dutyCycle = _contrain(dutyCycle, 0, _dutyCycleLimit);
197
198 const float mapped = getDutyCycleMapped();
199
200 if (_powerLUTEnabled) {
201 if (mapped == 0) {
202 _dutyCycleFire = 0.0f;
203 } else if (mapped == 1) {
204 _dutyCycleFire = 1.0f;
205 } else {
206 _dutyCycleFire = _semiPeriod > 0 ? (1.0f - static_cast<float>(_lookupFiringDelay(mapped, _semiPeriod)) / static_cast<float>(_semiPeriod)) : mapped;
207 }
208 } else {
209 _dutyCycleFire = mapped;
210 }
211
212 return isOnline() && _apply();
213 }
214
216 // DUTY CYCLE //
218
222 float getDutyCycle() const { return _dutyCycle; }
223
227 float getDutyCycleMapped() const { return _dutyCycleMin + _dutyCycle * (_dutyCycleMax - _dutyCycleMin); }
228
237 float getDutyCycleFire() const { return isOnline() ? _dutyCycleFire : 0.0f; }
238
240 // METRICS //
242
243 // Calculate harmonics based on dimmer firing angle for resistive loads
244 // array[0] = H1 (fundamental), array[1] = H3, array[2] = H5, array[3] = H7, etc.
245 // Only odd harmonics are calculated (even harmonics are negligible for symmetric dimmers)
246 // Returns true if harmonics were calculated, false if dimmer is not active
247 bool calculateHarmonics(float* array, size_t n) const {
248 if (array == nullptr || n == 0)
249 return false;
250
251 // Check if dimmer is active and routing
252 if (!isOnline() || _dutyCycleFire <= 0.0f) {
253 for (size_t i = 0; i < n; i++) {
254 array[i] = 0.0f; // No power, no harmonics
255 }
256 return true;
257 }
258
259 if (_dutyCycleFire >= 1.0f) {
260 array[0] = 100.0f; // H1 (fundamental) = 100% reference
261 for (size_t i = 1; i < n; i++) {
262 array[i] = 0.0f; // No harmonics at full power
263 }
264 return true;
265 }
266
267 // Initialize all values to NAN
268 for (size_t i = 0; i < n; i++) {
269 array[i] = NAN;
270 }
271
272 return _calculateHarmonics(array, n);
273 }
274
275 virtual bool calculateMetrics(Metrics& metrics, float gridVoltage, float loadResistance) const { return false; }
276
277#ifdef MYCILA_JSON_SUPPORT
283 virtual void toJson(const JsonObject& root) const;
284#endif
285
286 protected:
287 bool _enabled = false;
288 bool _online = false;
289
290 float _dutyCycle = 0.0f;
291 float _dutyCycleFire = 0.0f;
292 float _dutyCycleLimit = 1.0f;
293 float _dutyCycleMin = 0.0f;
294 float _dutyCycleMax = 1.0f;
295
296 bool _powerLUTEnabled = false;
297
298 static uint16_t _semiPeriod;
299
300 virtual bool _apply() { return _enabled; }
301 virtual bool _calculateHarmonics(float* array, size_t n) const {
302 for (size_t i = 0; i < n; i++) {
303 array[i] = 0.0f; // No harmonics for virtual dimmer
304 }
305 return true;
306 }
307
309 // STATIC HELPERS //
311
312 static uint16_t _lookupFiringDelay(float dutyCycle, uint16_t semiPeriod);
313
314 static inline float _contrain(float amt, float low, float high) {
315 return (amt < low) ? low : ((amt > high) ? high : amt);
316 }
317
318 static bool _calculatePhaseControlHarmonics(float dutyCycleFire, float* array, size_t n) {
319 // getDutyCycleFire() returns the conduction angle normalized (0-1)
320 // Convert to firing angle: α = π × (1 - conduction)
321 // At 50% power: α ≈ 90° (π/2), which gives maximum harmonics
322 const float firingAngle = M_PI * (1.0f - dutyCycleFire);
323
324 // Calculate RMS of fundamental component (reference)
325 // Formula from Thierry Lequeu: I1_rms = (1/π) × √[2(π - α + ½sin(2α))]
326 const float sin_2a = sinf(2.0f * firingAngle);
327 const float i1_rms = sqrtf((2.0f / M_PI) * (M_PI - firingAngle + 0.5f * sin_2a));
328
329 if (i1_rms <= 0.001f)
330 return false;
331
332 array[0] = 100.0f; // H1 (fundamental) = 100% reference
333
334 // Pre-compute scale factor for efficiency
335 const float scale_factor = (2.0f / M_PI) * 0.70710678f * 100.0f / i1_rms;
336
337 // Calculate odd harmonics (H3, H5, H7, ...)
338 // Formula for phase-controlled resistive loads (IEEE standard):
339 // Hn = (2/π√2) × |cos((n-1)α)/(n-1) - cos((n+1)α)/(n+1)| / I1_rms × 100%
340 // This gives the correct harmonic magnitudes relative to the fundamental
341 for (size_t i = 1; i < n; i++) {
342 const float n_f = static_cast<float>(2 * i + 1); // 3, 5, 7, 9, ...
343 const float n_minus_1 = n_f - 1.0f;
344 const float n_plus_1 = n_f + 1.0f;
345
346 // Compute Fourier coefficient
347 const float coeff = cosf(n_minus_1 * firingAngle) / n_minus_1 -
348 cosf(n_plus_1 * firingAngle) / n_plus_1;
349
350 // Convert to percentage of fundamental
351 array[i] = fabsf(coeff) * scale_factor;
352 }
353
354 return true;
355 }
356
357 static bool _calculatePhaseControlMetrics(Metrics& metrics, float dutyCycleFire, float gridVoltage, float loadResistance) {
358 if (loadResistance > 0 && gridVoltage > 0) {
359 if (dutyCycleFire > 0) {
360 const float nominalPower = gridVoltage * gridVoltage / loadResistance;
361 if (dutyCycleFire >= 1.0f) {
362 // full power
363 metrics.powerFactor = 1.0f;
364 metrics.thdi = 0.0f;
365 metrics.power = nominalPower;
366 metrics.voltage = gridVoltage;
367 metrics.current = gridVoltage / loadResistance;
368 metrics.apparentPower = nominalPower;
369 return true;
370 } else {
371 // partial power
372 metrics.powerFactor = std::sqrt(dutyCycleFire);
373 metrics.thdi = 100.0f * std::sqrt(1 / dutyCycleFire - 1);
374 metrics.power = dutyCycleFire * nominalPower;
375 metrics.voltage = metrics.powerFactor * gridVoltage;
376 metrics.current = metrics.voltage / loadResistance;
377 metrics.apparentPower = gridVoltage * metrics.current;
378 return true;
379 }
380 } else {
381 // no power
382 metrics.voltage = 0.0f;
383 metrics.current = 0.0f;
384 metrics.power = 0.0f;
385 metrics.apparentPower = 0.0f;
386 metrics.powerFactor = NAN;
387 metrics.thdi = NAN;
388 return true;
389 }
390 } else {
391 return false;
392 }
393 }
394 };
395} // namespace Mycila
396
397#include "MycilaDimmerCycleStealing.h"
398#include "MycilaDimmerDFRobot.h"
399#include "MycilaDimmerPWM.h"
400#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,...
static void setSemiPeriod(uint16_t semiPeriod)
Set the semi-period of the grid frequency in us for this dimmer. This is mandatory when using power L...
bool isOnline() const
Returns true if the dimmer is online.
float getDutyCycleMax() const
Get the remapped "1" of the dimmer duty cycle.
bool isOn() const
Check if the dimmer is on.
static uint16_t getSemiPeriod()
Get the semi-period in us used for the power LUT calculations. If LUT is disabled,...
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.
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 enablePowerLUT(bool enable)
Enable or disable the use of power LUT for this dimmer The power LUT provides a non-linear dimming cu...
void setDutyCycleLimit(float limit)
Set the power duty cycle limit of the dimmer. The duty cycle will be clamped to this limit.