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.1.1"
8#define MYCILA_DIMMER_VERSION_MAJOR 2
9#define MYCILA_DIMMER_VERSION_MINOR 1
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
19namespace Mycila {
20 class Dimmer {
21 public:
22 virtual ~Dimmer() {};
23
24 virtual void begin() = 0;
25 virtual void end() = 0;
26 virtual const char* type() const = 0;
27
29 // DIMMER CONFIG //
31
37 void setDutyCycleLimit(float limit) {
38 _dutyCycleLimit = _contrain(limit, 0, 1);
39 if (_dutyCycle > _dutyCycleLimit)
40 setDutyCycle(_dutyCycleLimit);
41 }
42
49 void setDutyCycleMin(float min) {
50 _dutyCycleMin = _contrain(min, 0, _dutyCycleMax);
51 setDutyCycle(_dutyCycle);
52 }
53
60 void setDutyCycleMax(float max) {
61 _dutyCycleMax = _contrain(max, _dutyCycleMin, 1);
62 setDutyCycle(_dutyCycle);
63 }
64
68 float getDutyCycleLimit() const { return _dutyCycleLimit; }
69
73 float getDutyCycleMin() const { return _dutyCycleMin; }
74
78 float getDutyCycleMax() const { return _dutyCycleMax; }
79
81 // POWER LUT //
83
96 void enablePowerLUT(bool enable, uint16_t semiPeriod = 0) {
97 if (!enable) {
98 _powerLUTEnabled = false;
99 return;
100 }
101 // Enabling the LUT
102 if (semiPeriod > 0) {
103 // A semi-period is provided, use it
104 _semiPeriod = semiPeriod;
105 } else {
106 // semiPeriod == 0, use already set semi-period, must be >0
107 if (_semiPeriod == 0) {
108 ESP_LOGE("MycilaDimmer", "enablePowerLUT: semiPeriod must be provided or must be already set when enabling power LUT");
109 }
110 assert(_semiPeriod > 0);
111 }
112 _powerLUTEnabled = true;
113 }
114
118 bool isPowerLUTEnabled() const { return _powerLUTEnabled; }
119
123 uint16_t getPowerLUTSemiPeriod() const { return _powerLUTEnabled ? _semiPeriod : 0; }
124
126 // DIMMER STATES //
128
132 bool isEnabled() const { return _enabled; }
133
137 bool isOnline() const { return _enabled && _online; }
138
143 void setOnline(bool online) {
144 _online = online;
145 if (!_online) {
146 _dutyCycleFire = 0.0f;
147 if (_enabled)
148 _apply();
149 } else {
150 setDutyCycle(_dutyCycle);
151 }
152 }
153
155 // DIMMER CONTROL //
157
161 void on() { setDutyCycle(1); }
162
166 void off() { setDutyCycle(0); }
167
171 bool isOff() const { return !isOn(); }
172
176 bool isOn() const { return isOnline() && _dutyCycle; }
177
181 bool isOnAtFullPower() const { return _dutyCycle >= _dutyCycleMax; }
182
188 bool setDutyCycle(float dutyCycle) {
189 // Apply limit and save the wanted duty cycle.
190 // It will only be applied when dimmer will be on.
191 _dutyCycle = _contrain(dutyCycle, 0, _dutyCycleLimit);
192
193 const float mapped = getDutyCycleMapped();
194
195 if (_powerLUTEnabled) {
196 if (mapped == 0) {
197 _dutyCycleFire = 0.0f;
198 } else if (mapped == 1) {
199 _dutyCycleFire = 1.0f;
200 } else {
201 _dutyCycleFire = 1.0f - static_cast<float>(_lookupFiringDelay(mapped, _semiPeriod)) / static_cast<float>(_semiPeriod);
202 }
203 } else {
204 _dutyCycleFire = mapped;
205 }
206
207 return isOnline() && _apply();
208 }
209
211 // DUTY CYCLE //
213
217 float getDutyCycle() const { return _dutyCycle; }
218
222 float getDutyCycleMapped() const { return _dutyCycleMin + _dutyCycle * (_dutyCycleMax - _dutyCycleMin); }
223
232 float getDutyCycleFire() const { return isOnline() ? _dutyCycleFire : 0.0f; }
233
235 // HARMONICS //
237
238 // Calculate harmonics based on dimmer firing angle for resistive loads
239 // array[0] = H1 (fundamental), array[1] = H3, array[2] = H5, array[3] = H7, etc.
240 // Only odd harmonics are calculated (even harmonics are negligible for symmetric dimmers)
241 // Returns true if harmonics were calculated, false if dimmer is not active
242 bool calculateHarmonics(float* array, size_t n) const {
243 if (array == nullptr || n == 0)
244 return false;
245
246 // Check if dimmer is active and routing
247 if (!isOnline() || _dutyCycleFire <= 0.0f) {
248 for (size_t i = 0; i < n; i++) {
249 array[i] = 0.0f; // No power, no harmonics
250 }
251 return true;
252 }
253
254 if (_dutyCycleFire >= 1.0f) {
255 array[0] = 100.0f; // H1 (fundamental) = 100% reference
256 for (size_t i = 1; i < n; i++) {
257 array[i] = 0.0f; // No harmonics at full power
258 }
259 return true;
260 }
261
262 // Initialize all values to NAN
263 for (size_t i = 0; i < n; i++) {
264 array[i] = NAN;
265 }
266
267 return _calculateHarmonics(array, n);
268 }
269
270#ifdef MYCILA_JSON_SUPPORT
276 virtual void toJson(const JsonObject& root) const {
277 root["type"] = type();
278 root["enabled"] = _enabled;
279 root["online"] = _online;
280 root["state"] = isOn() ? "on" : "off";
281 root["duty_cycle"] = _dutyCycle;
282 root["duty_cycle_mapped"] = getDutyCycleMapped();
283 root["duty_cycle_fire"] = _dutyCycleFire;
284 root["duty_cycle_limit"] = _dutyCycleLimit;
285 root["duty_cycle_min"] = _dutyCycleMin;
286 root["duty_cycle_max"] = _dutyCycleMax;
287 root["power_lut"] = _powerLUTEnabled;
288 root["power_lut_semi_period"] = _semiPeriod;
289 JsonObject harmonics = root["harmonics"].to<JsonObject>();
290 float output[11]; // H1 to H21
291 if (calculateHarmonics(output, 11)) {
292 for (size_t i = 0; i < 11; i++) {
293 if (!std::isnan(output[i])) {
294 char key[8];
295 snprintf(key, sizeof(key), "H%d", static_cast<int>(2 * i + 1));
296 harmonics[key] = output[i];
297 }
298 }
299 }
300 }
301#endif
302
303 protected:
304 bool _enabled = false;
305 bool _online = false;
306
307 float _dutyCycle = 0.0f;
308 float _dutyCycleFire = 0.0f;
309 float _dutyCycleLimit = 1.0f;
310 float _dutyCycleMin = 0.0f;
311 float _dutyCycleMax = 1.0f;
312
313 bool _powerLUTEnabled = false;
314 uint16_t _semiPeriod = 0;
315
316 virtual bool _apply() = 0;
317 virtual bool _calculateHarmonics(float* array, size_t n) const = 0;
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
365 class VirtualDimmer : public Dimmer {
366 public:
367 virtual ~VirtualDimmer() { end(); }
368
369 virtual void begin() { _enabled = true; }
370 virtual void end() { _enabled = false; }
371 virtual const char* type() const { return "virtual"; }
372
373 protected:
374 virtual bool _apply() { return true; }
375 virtual bool _calculateHarmonics(float* array, size_t n) const {
376 for (size_t i = 0; i < n; i++) {
377 array[i] = 0.0f; // No harmonics for virtual dimmer
378 }
379 return true;
380 }
381 };
382} // namespace Mycila
383
384#include "MycilaDimmerDFRobot.h"
385#include "MycilaDimmerPWM.h"
386#include "MycilaDimmerThyristor.h"
void off()
Turn off the dimmer.
float getDutyCycleFire() const
Get the real firing duty cycle applied to the dimmer in the range [0, 1].
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.