-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Feature: Add Triac Phase Control Plugin (_P184_Triac.ino) #5426
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: mega
Are you sure you want to change the base?
Feature: Add Triac Phase Control Plugin (_P184_Triac.ino) #5426
Conversation
src/_P184_Triac.ino
Outdated
| #ifdef USES_P184 | ||
| #define PLUGIN_184 | ||
| #define PLUGIN_ID_184 184 // plugin id | ||
| #define PLUGIN_NAME_184 "Triac" // "Plugin Name" is what will be dislpayed in the selection list |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The name convention is to use a category, a dash (-) and the plugin name, so this could be Output - Triac.
(The Output category seems most fitting for this plugin)
src/_P184_Triac.ino
Outdated
| P184_data_struct P184_data; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Defining this instance globally prevents having multiple concurrent instances of this plugin. Please have a look at how most other plugins have this done, f.e. P140 (a simple plugin example 😃)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And/or have a look at the P003 Pulse Counter plugin, as you need to act on GPIO interrupts. This is a special case where you need to also register a pointer to go along with the callback function to store the runtime data.
src/_P184_Triac.ino
Outdated
| dev.PullUpOption = false; // Allow to set internal pull-up resistors. | ||
| dev.InverseLogicOption = false; // Allow to invert the boolean state (e.g. a switch) | ||
| dev.FormulaOption = false; // Allow to enter a formula to convert values during read. (not possible with Custom enabled) | ||
| dev.Custom = false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All Device flags are explicitly initialized to false, so no need to have cpu cycles spent on setting them again. (also a few below)
src/_P184_Triac.ino
Outdated
| case PLUGIN_WEBFORM_SHOW_CONFIG: | ||
| { | ||
| // Called to show non default pin assignment or addresses like for plugins using serial or 1-Wire | ||
| // string += serialHelper_getSerialTypeLabel(event); | ||
| success = true; | ||
| break; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused / inactive cases can be removed.
src/_P184_Triac.ino
Outdated
| case PLUGIN_GET_DEVICEVALUECOUNT: | ||
| { | ||
| // This is only called when dev.OutputDataType is not Output_Data_type_t::Default | ||
| // The position in the config parameters used in this example is PCONFIG(P184_OUTPUT_TYPE_INDEX) | ||
| // Must match the one used in case PLUGIN_GET_DEVICEVTYPE (best to use a define for it) | ||
| // see P026_Sysinfo.ino for more examples. | ||
| event->Par1 = 2; //getValueCountFromSensorType(static_cast<Sensor_VType>(PCONFIG(P184_OUTPUT_TYPE_INDEX))); | ||
| success = true; | ||
| break; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This case is useful when dynamically changing the number of device values, that's not used in this plugin, so this code can be removed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To be honest. I can't understand how to use this field.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For this plugin, the marked code can be removed, as you're not changing the number of Values based on some configuration setting.
src/_P184_Triac.ino
Outdated
|
|
||
| P184_data.trigger_value = P184_TRIGGER_CONFIG(); | ||
| // Recalculate power value based on the new trigger value from the form | ||
| for (int i = 0; i <= 100; ++i) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Magic number used here, better would be to use:
constexpr uint8_t power_to_trigger_lut_size = NR_ELEMENTS(power_to_trigger_lut);
(just below the power_to_trigger_lut array, and the 101 size in that array definition can also be removed. The size is determined at compile-time, this way)
and here, use:
for (uint8_t i = 0; i < power_to_trigger_lut_size; ++i) {
src/_P184_Triac.ino
Outdated
| for (int i = 0; i <= 100; ++i) { | ||
| if (pgm_read_byte(&power_to_trigger_lut[i]) <= P184_data.trigger_value) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto.
src/_P184_Triac.ino
Outdated
| String valueStr = parseString(string, 3); | ||
| long value = valueStr.toInt(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Value is already available in event->Par2, no need to re-do a string to int conversion.
src/_P184_Triac.ino
Outdated
| // case PLUGIN_ONCE_A_SECOND: | ||
| // { | ||
| // // code to be executed once a second. Tasks which do not require fast response can be added here | ||
|
|
||
| // success = true; | ||
| // } | ||
|
|
||
| // case PLUGIN_TEN_PER_SECOND: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Commented code can be removed
|
|
||
| #define USES_P184 | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will enable the plugin in all builds, unconditionally. That's not correct, it should probably go in the Collection H build (that will be available soon, for now put it in Collection G), the Energy collection and the MAX build definition.
src/_P184_Triac.ino
Outdated
| // int8_t P184_TRIGGER_PIN(); | ||
| // uint8_t p184_trigger; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
More commented code.
src/_P184_Triac.ino
Outdated
| void IRAM_ATTR P184_zero_crossing() { | ||
| // This is only necessary for debounce | ||
| // detachInterrupt(P184_data.zero_crossing_pin); | ||
| if (P184_data.trigger_pin != -1 && P184_data.p184_timer != NULL) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For an example of how instance-independent interrupt handlers can be implemented, please have a look at plugin P008. By moving this code to the plugin_struct sources, you will also avoid compilation warnings about the iram section being ignored...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moving it, make sense use attachInterruptArg?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, if you need to keep some kind of state as static variable, then it makes sense to have a pointer attached to the interrupt, so you can access those variables without creating some elaborate structures to keep track of states per task.
See P003 and P098 for some examples on how to use this attachInterruptArg to implement for use with multiple instances of the same plugin.
src/_P184_Triac.ino
Outdated
| P184_data.trigger_value = P184_TRIGGER_CONFIG(); | ||
| // Recalculate power value based on the new trigger value from the form |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The plugin is stopped before the PLUGIN_WEBFORM_SAVE function is called, so no need to update settings here, that should (only) be done in PLUGIN_INIT.
src/_P184_Triac.ino
Outdated
| { | ||
| pinMode(P184_data.trigger_pin, OUTPUT); | ||
| digitalWrite(P184_data.trigger_pin, LOW); | ||
| P184_data.p184_timer = timerBegin(1000000); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Magic number?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lol
https://espressif-docs.readthedocs-hosted.com/projects/arduino-esp32/en/latest/api/timer.html#timerbegin
I set to 1MHz. Thus I can use the int values in microseconds.
src/_P184_Triac.ino
Outdated
| String log = strformat(F("P184 CMD : Trigger %d%% . Power %d%%"), P184_data.trigger_value, P184_data.power_value); | ||
| addLogMove(LOG_LEVEL_INFO, log); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be a single statement, no need to instantiate a new String, only to pass it on.
Then you can also use addLog instead of addLogMove.
|
@thalesmaoa Thanks for this plugin! I've added a few comments 😅 but that's all positive 👍 There's no reference to hardware or schematics, but that can be addressed in the documentation 😉 |
|
In your example you use this command:
|
src/_P184_Triac.ino
Outdated
| // detachInterrupt(P184_data.zero_crossing_pin); | ||
| if (P184_data.trigger_pin != -1 && P184_data.p184_timer != NULL) { | ||
| if (P184_data.trigger_value == 0) { | ||
| digitalWrite(P184_data.trigger_pin, HIGH); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please look at the code for DIRECT_pinWrite_ISR as that's way faster than digitalWrite.
And with "way faster" I mean about a factor 100x - 1000x on ESP32-xx
|
Looking at the code and the description, it seems like you turn the Triac 'on' with some delay after the zero-crossing and not on immediately and 'off' after some delay. I think this will cause way more EMI noise and will introduce way higher peak currents compared to the other way around. I would expect 'starting' at zero-crossing and turning 'off' after some delay will be way better for your device. |
|
This could probably be implemented by adding a setting to use either the current way or starting at zero-crossing (IIRC this was a 'thing' many years ago, when household lighting dimmers became popular) |
src/_P184_Triac.ino
Outdated
| uint8_t power_value = 0; | ||
| uint8_t dead_zone = 0; | ||
| uint32_t freq_timing_val = P184_60HZ_HALF_WAVE_TIME_US_ONE_PERCENT; | ||
| uint64_t time_us = 0; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No idea why this should be a 64 bit int.
Even if the time was in nano-seconds, it would perfectly fit in an uint32_t as it is a sub-second timer.
The reason why it is better to use a 32bit value is because the 64-bit values are dealt with in software and thus are better not to be used in interrupt-handler callback functions.
For sure not for callbacks as frequent as these.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No idea why this should be a 64 bit int.
Me neither.
https://espressif-docs.readthedocs-hosted.com/projects/arduino-esp32/en/latest/api/timer.html#timeralarm
Also, I've tried using 32bit and I got some strange behavior.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe because it isn't declared volatile (or std::atomic<uint32_t> for modern compilers as used for ESP32)
src/_P184_Triac.ino
Outdated
|
|
||
| void IRAM_ATTR P184_timer_handler() { | ||
| if (P184_data.trigger_pin != -1) { | ||
| if (P184_data.trigger_value != 100) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Normally it would be good to have these checks for trigger pin and value, but here you can also make sure not to start the timer at all if these values are unusable.
This makes the callback functions even smaller.
Also these callback functions will be linked to the iram by the compiler and that's a really limited resource. So better make those as small as possible or else other unrelated builds may fail due to insufficient iRAM size.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. You are right. Despite of my best efforts, I was having panicked when deleting the plugin.
Not sure if hardware timer and interrupt takes more time than other to execute. My workaround was to add the check for the pin.
Also, the check for the trigger value is due to grid frequency variations. If frequency is a bit lower, it still triggers. Thus, I need to force it not to.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps you explicitly need to disable the pending timer when deleting the task/plugin?
src/_P184_Triac.ino
Outdated
| void IRAM_ATTR P184_zero_crossing() { | ||
| // This is only necessary for debounce | ||
| // detachInterrupt(P184_data.zero_crossing_pin); | ||
| if (P184_data.trigger_pin != -1 && P184_data.p184_timer != NULL) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same for this callback function. You don't need to check for those trigger pin and timer pointers, as you should not even attach to an interrupt when those conditions are not met.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here. Panicked when deleting the plugin. Not always, but one in four.
yes! From my previous experience, some devices need some dead time after zero-cross. This can also come due to inductive loads.
It really doesn't matter. A triac only needs a trigger. It will only block again when current reaches zero.
Not sure if I follow.
Can't do for a triac.
Inductive loads will affect next trigger point. It only turn off when current reaches zero. |
That's what I meant... if you would cut off the current through an inductive load, it will generate a high (opposite) voltage as it tries to keep the magnetic field it had. However with resistive loads, there is a clear difference as the resistance of the load often is temperature dependent. A capacitive load, like those cheap LED bulbs (or nearly any switching power supply without power factor correction) also may draw way too much when turned on at the peak voltage, as a discharged capacitor acts like a short circuit. |
Remove global struct Remove comments and unnecessary code Add fixed lut size REG write for fast pin write Make interrupt static
|
Hi @tonhuisman and @TD-er . Tried to fill up all the requests. |
| [env:normal_ESP32_IRExt_4M316k_LittleFS_ETH_P184] | ||
| extends = env:normal_ESP32_IRExt_4M316k_LittleFS_ETH | ||
| board = esp32_4M | ||
| build_flags = ${env:normal_ESP32_IRExt_4M316k_LittleFS_ETH.build_flags} | ||
| -DUSES_P184 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We've dropped the _LittleFS_ETH part from all env names, so when rebasing with mega, this env might cause issues, but these are easy to fix 😃
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ehh a few remarks....
Looks like you may be using mains voltage on the headers at the bottom of that screenshot?
Those pins are way too close to the low voltage parts of the PCB.
The area under the dimmer circuit should also be isolated, so rather not have a copper pour on the top layer of the PCB, under the intended dimmer circuit.
If you would like to shield it, then you could add copper pour on the bottom side of your own PCB.
What happens if you scratch the green solder mask with some sharp pieces of the dimmer circuit? Then you expose the GND of the ESP to mains voltage.
The heat sink of the dimmer circuit is also way too close to the Wemos pins and is probably not isolated from mains.
Also you seem to be using a Wemos ESP32 form factor, which then has the WiFi antenna right above the copper pour area and thus will likely have a very sub-optimal WiFi performance, unless you will be using some external WiFi antenna.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi @TD-er , I really appreciate your feedback. Thanks!
Looks like you may be using mains voltage on the headers at the bottom of that screenshot?
Yes. That is correct.
Those pins are way too close to the low voltage parts of the PCB.
I performed some calculations based on IEC 60664. For 500V, the minimum suggested clearance is 1.5mm, and I’ve allowed for 1.9mm. However, you’ve brought up a good point: I should have included an isolation slot (milling) in that area, which I missed.
The area under the dimmer circuit should also be isolated, so rather not have a copper pour on the top layer of the PCB, under the intended dimmer circuit.
I understand. I've decided to use a 3D spacer between them for now. The EMI is significant in Triac-based PCBs, and my intention was for the ground plane to act as a shield.
Also you seem to be using a Wemos ESP32 form factor, which then has the WiFi antenna right above the copper pour area and thus will likely have a very sub-optimal WiFi performance, unless you will be using some external WiFi antenna.
Do you have any specific tips for improving Wi-Fi performance in this layout?
I have already sent this batch to production, but I will definitely incorporate all of your suggestions into version 2.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In open air the rule of thumb is 1 mm per 100V.
So for mains voltage you should keep 3 mm.
An encapsulated PCB trace (covered in solder mask) does have better insulation, so that figure of 1.5mm might be correct.... however... ;)
You're switching some load which can also be inductive. This means the voltages can be much much higher. Just looking at the EMI is also a good indicator there might be some inductive behavior.
N.B. you also have a trace quite close to the other pad of the screw terminal. I doubt that's at a minimal distance of 1.5 mm of the square pad of J1.
Regarding the antenna.
What you can do for testing, or at least proving the effect of the ground plane on the antenna, is using one or more stacked pin headers to lift the Wemos from the board.
If your Wemos board does have an IPEX connector, you may want to consider connecting an external antenna to it. Make sure the IPEX connector is actually wired as quite often there is a 0 Ohm resistor which needs to be moved (or 90 degree rotated) to either connect the PCB trace antenna or the IPEX connector.
If you have some PCB material left which doesn't have an area with copper on it (at least on one side), you can place it between the triac circuit board and the Wemos. Make sure no copper is on the side facing the triac board.
1.6 mm PCB material can isolate upto either 16 or 60 kV (not sure about the number), so it may prevent sparking between the triac board and the Wemos.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh and if you let the boards assemble at for example JLCPCB, you can also use the ESP32 modules they have.
I typically use the 16M module with an UFL connector, so I can use the "MAX" builds and can use whatever antenna I like and place it wherever I like.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh and if you let the boards assemble at for example JLCPCB, you can also use the ESP32 modules they have. I typically use the 16M module with an UFL connector, so I can use the "MAX" builds and can use whatever antenna I like and place it wherever I like.
Do mean like this:
https://jlcpcb.com/partdetail/EspressifSystems-ESP32_WROOM_32UEN4/C701344
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, but then the 16M version so you don't have to worry about build sizes
https://jlcpcb.com/partdetail/736354-ESP32_WROOM_32UEN16/C701346
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just add a few capacitors close to the module and 2 transistors for toggling GPIO-0 and reset so you can use those Wemos USB to UART boards for flashing. (CH340 chip, micro USB, 6 pin header)
Just make sure not to use the Vcc pin of those boards as the voltage regulator is a bit underdimensioned and by default wired to just forward the 5V which you need to scratch away, etc...
So just add a row of 6 pins in the correct order accessible on the side to allow you to use a programmer clamp with pogo pins to program the board.
II would just add an 1117 as linear voltage regulator as they are no-nonsense and stable. Not the most power-efficient, but they just work.
Check the datasheet to see if you need to pull-up or -down some GPIO pins (e.g. GPIO-0) and double check this: https://espeasy.readthedocs.io/en/latest/Reference/GPIO.html#best-pins-to-use-on-esp32
Make sure to have a good ground from the pads below the module to the GND plane on the other side.



This PR introduces a new plugin, _P184_Triac.ino, designed for AC phase control (dimming).
This plugin uses a zero-crossing detection (ZCD) signal to perform its function. It monitors a specific GPIO for this signal, which is typically provided by an external ZCD circuit (e.g., one using an H11AA1 or similar optocoupler).
Based on the timing of the zero-cross signal, the plugin manages the precise firing of a Triac on a separate output GPIO.
This functionality is the fundamental building block for AC phase control, enabling applications such as:
Control Methods
The plugin can be controlled in two different ways via commands, making it flexible for integration into control loops.
triac,trigger,<value>valueis a percentage from 0 to 100.0= 100% Power (Triac triggers immediately at the start of the wave).100= 0% Power (Triac never triggers).50= Triggers at the halfway point of the cycle.triac,power,[value]valueis a percentage from 0 to 100.0= 0% Power.100= 100% Power.Commands and Usage
The plugin can be easily controlled via ESPEasy rules or other controllers (like MQTT or HTTP).
Assuming the plugin's Task Name is
triac:This command-based interface allows the plugin to be easily coupled to any external or internal control loop (e.g., a PID controller, a web UI slider, or an MQTT topic).