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.0"
8#define MYCILA_DIMMER_VERSION_MAJOR 2
9#define MYCILA_DIMMER_VERSION_MINOR 1
10#define MYCILA_DIMMER_VERSION_REVISION 0
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 readHarmonics(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())
248 return false;
249
250 if (_dutyCycleFire <= 0.0f) {
251 for (size_t i = 0; i < n; i++) {
252 array[i] = 0.0f; // No power, no harmonics
253 }
254 return true;
255 }
256
257 if (_dutyCycleFire >= 1.0f) {
258 array[0] = 100.0f; // H1 (fundamental) = 100% reference
259 for (size_t i = 1; i < n; i++) {
260 array[i] = 0.0f; // No harmonics at full power
261 }
262 return true;
263 }
264
265 // Initialize all values to NAN
266 for (size_t i = 0; i < n; i++) {
267 array[i] = NAN;
268 }
269
270 // getDutyCycleFire() returns the conduction angle normalized (0-1)
271 // Convert to firing angle: α = π × (1 - conduction)
272 const float firingAngle = M_PI * (1.0f - _dutyCycleFire);
273 const float sin_2a = sinf(2.0f * firingAngle);
274
275 // RMS of fundamental component: I1_rms = (1/π) × √[2(π - α + ½sin(2α))]
276 const float i1_rms = sqrtf((2.0f / M_PI) * (M_PI - firingAngle + 0.5f * sin_2a));
277
278 if (i1_rms <= 0.001f)
279 return false;
280
281 array[0] = 100.0f; // H1 (fundamental) = 100% reference
282
283 // Pre-compute constant values
284 static constexpr float inv_pi_2 = 2.0f / M_PI;
285 static constexpr float inv_sqrt2 = 0.70710678f; // 1/√2
286 const float scale_factor = inv_pi_2 * inv_sqrt2 * 100.0f / i1_rms;
287
288 // Calculate odd harmonics (H3, H5, H7, ...)
289 // Formula: Hn% = (2/π√2) × |cos((n-1)α)/(n-1) - cos((n+1)α)/(n+1)| / I1_rms × 100
290 for (size_t i = 1; i < n; i++) {
291 const float n_f = static_cast<float>(2 * i + 1); // 3, 5, 7, 9, ...
292 const float n_minus_1 = n_f - 1.0f;
293 const float n_plus_1 = n_f + 1.0f;
294
295 // Compute Fourier coefficient
296 const float coeff = cosf(n_minus_1 * firingAngle) / n_minus_1 -
297 cosf(n_plus_1 * firingAngle) / n_plus_1;
298
299 // Convert to percentage of fundamental
300 array[i] = fabsf(coeff) * scale_factor;
301 }
302
303 return true;
304 }
305
306#ifdef MYCILA_JSON_SUPPORT
312 virtual void toJson(const JsonObject& root) const {
313 root["type"] = type();
314 root["enabled"] = _enabled;
315 root["online"] = _online;
316 root["state"] = isOn() ? "on" : "off";
317 root["duty_cycle"] = _dutyCycle;
318 root["duty_cycle_mapped"] = getDutyCycleMapped();
319 root["duty_cycle_fire"] = _dutyCycleFire;
320 root["duty_cycle_limit"] = _dutyCycleLimit;
321 root["duty_cycle_min"] = _dutyCycleMin;
322 root["duty_cycle_max"] = _dutyCycleMax;
323 root["power_lut"] = _powerLUTEnabled;
324 root["power_lut_semi_period"] = _semiPeriod;
325 }
326#endif
327
328 protected:
329 bool _enabled = false;
330 bool _online = false;
331
332 float _dutyCycle = 0.0f;
333 float _dutyCycleFire = 0.0f;
334 float _dutyCycleLimit = 1.0f;
335 float _dutyCycleMin = 0.0f;
336 float _dutyCycleMax = 1.0f;
337
338 bool _powerLUTEnabled = false;
339 uint16_t _semiPeriod = 0;
340
341 static uint16_t _lookupFiringDelay(float dutyCycle, uint16_t semiPeriod);
342
343 virtual bool _apply() = 0;
344
345 static inline float _contrain(float amt, float low, float high) {
346 return (amt < low) ? low : ((amt > high) ? high : amt);
347 }
348 };
349
350 class VirtualDimmer : public Dimmer {
351 public:
352 virtual ~VirtualDimmer() { end(); }
353
354 virtual void begin() { _enabled = true; }
355 virtual void end() { _enabled = false; }
356 virtual const char* type() const { return "virtual"; }
357
358 protected:
359 virtual bool _apply() { return true; }
360 };
361} // namespace Mycila
362
363#include "MycilaDimmerDFRobot.h"
364#include "MycilaDimmerPWM.h"
365#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.