ZCD using gpio based Interrupt

This commit is contained in:
Heuideog Yi @ PC RnD1 2026-04-18 13:09:48 +09:00
parent 1dd537b5d0
commit ef05f6f997
7 changed files with 19 additions and 689 deletions

View File

@ -1,154 +0,0 @@
#include "HermitCrab.h"
#include "Config.h"
#include "History.h"
#include "zcd.h"
const char *strDeviceType[] = {
"None",
"Test",
"ESP8266",
"ON_OFF",
"ZCD",
"CAM",
"Beta",
"Beta_BLE",
"End"
};
MY_IRAM_ATTR void checkSerial(unsigned long tick)
{
static char buffer[256];
static short idx = 0;
static unsigned long val;
while (Serial.available() > 0) {
if (idx > 254) {
idx = 0;
ESP_LOGI(TAG,"SrialHost: Buffer OverFlow");
}
buffer[idx] = Serial.read();
if (buffer[idx] == '\n') {
if (idx >= 1) {
buffer[idx] = 0;
switch(buffer[0])
{
case 'T': // Temp Target
if (idx > 1) {
val = atoi(&buffer[1]);
if (val >= 25 && val <= 35) {
config.nTempTarget = val * 10;
} else if (val >= 250 && val <= 350) {
config.nTempTarget = val;
}
}
Serial.printf("%s SerialSet: Temp Target %d.%d°C Duty(%.2f%%)\n",
printStatus(tick, false), config.nTempTarget / 10, config.nTempTarget % 10,
status.nHeater1Duty / 100.0f);
break;
case 't': // Temp Target Night
if (idx > 1) {
val = atoi(&buffer[1]);
if (val >= 25 && val <= 35) {
config.nTempTargetNight = val * 10;
} else if (val >= 250 && val <= 350) {
config.nTempTargetNight = val;
}
}
Serial.printf("%s SerialSet: Temp Target Night %d.%d°C Duty(%.2f%%)\n",
printStatus(tick, false),
config.nTempTargetNight / 10, config.nTempTargetNight % 10,
status.nHeater1Duty/ 100.0f);
break;
case 'H': // Humidity Target
case 'h':
if (idx > 1) {
val = atoi(&buffer[1]);
if (val >= 20 && val <= 95) {
config.nHumidTarget = val * 10;
} else if (val >= 200 && val <= 950) {
config.nHumidTarget = val;
}
}
Serial.printf("%s SerialSet: Humid Target(%d.%d%%) Mist %s (Duty: %d)\n",
printStatus(tick),
config.nHumidTarget / 10, config.nHumidTarget % 10,
status.nMistDuty > 0 ? "ON" : "OFF",
status.nMistDuty);
break;
case 'M': // MistOn time
case 'm': // MistDelay time
if (idx > 1) {
val = atoi(&buffer[1]);
if (val >= 0 && val <= 1023) {
status.nMistDuty = val;
}
}
Serial.printf("%s SerialSet: Mist %s (Duty: %d)\n",
printStatus(tick), status.nMistDuty > 0 ? "ON" : "OFF", status.nMistDuty);
break;
case 'l': //Light1
case 'L':
if (idx > 2) {
val = atoi(&buffer[1]);
if (val >= PWM_OFF && val <= PWM_FULL) {
status.nLightTargetDuty = val;
}
}
Serial.printf("%s SerialSet: Light1 %s (%d --> %d)\n",
printStatus(tick), status.nLightDuty > 0 ? "ON" : "OFF",
status.nLightDuty, status.nLightTargetDuty);
break;
case 'd': // Display Sensor
if (idx < 2) {
bShowSensor = !bShowSensor;
} else {
val = atoi(&buffer[1]);
bShowSensor = val == 0 ? false : true;
}
Serial.printf("%s SerialSet: DisplaySensor %s\n", printStatus(tick), bShowSensor ? "On" : "Off");
break;
case 'p': // Print
case 'P':
break;
case 's': // Save Config
case 'S':
history.savePID();
config.save();
Serial.printf("%s Config Saved\n", printStatus(tick));
break;
case 'Y': // Device Type
case 'y':
if (idx > 1) {
val = atoi(&buffer[1]);
char *sz = NULL;
if (val > TYPE_NONE && val < TYPE_DEVICE_END) {
config.m_nDeviceType = val;
Serial.printf("%s SeriaSet: DeviceType %s\n", printStatus(tick), strDeviceType[val]);
} else {
Serial.printf("%s SerialSet: Invalid DeviceType\n");
}
}
break;
case 'Z':
case 'z':
if (idx > 1) {
val = atoi(&buffer[1]);
if (val >= 0 && val <= 10000) {
//ESP_LOGI(TAG,"%s SerialSet: Set Heater Duty %.1f%%\n", printStatus(tick), dutyPercent);
status.nHeater1Duty = val;
setHeater1Duty(status.nHeater1Duty);
}
}
break;
}
}
// Clear Buffer
idx = 0;
} else {
idx++;
}
}
}

View File

@ -20,7 +20,7 @@
#ifndef DEBUG
#define DEBUG 1 // Set to 0 to disable debug output
#endif
//#undef DEBUG
#undef DEBUG
//#define BLE_DEBUG
@ -219,7 +219,6 @@ typedef struct STATUS_STRUCT {
char *printStatus(unsigned long tick, bool bLong = false);
char *printTime(bool bLong = false);
void checkSerial(unsigned long tick);
void checkWiFi(unsigned long tick);
void checkWiFiHost(unsigned long tick);

View File

@ -17,7 +17,7 @@
// OTA
//
// ==============================================================
const char *HC__VERSION = "2026041814";
const char *HC__VERSION = "20260418016";
#define UPDATE_PORT ((uint16_t) 443)
const char *url = "visionsoft.kr";
const char *uri = "/sc/pages/firmware_download.php";

View File

@ -1,491 +0,0 @@
#include <HTTPClient.h>
#include "HermitCrab.h"
#include "UPnpClient.h"
#include "Config.h"
#include "WiFiHost.h"
#define TAG_UPNP "UPnP"
bool CUpnpClient::registerUPnP(uint32_t *pip, uint16_t *pport) {
routerIP = *pip; // Gateway IP address
publicPort = *pport; // host.m_nPublicPort = config.m_nPublicPort
publicIP = 0UL;
bool bSuccess = false;
ESP_LOGI(TAG_UPNP,"UPnP %s(%D)\n", routerIP.toString().c_str(), publicPort);
// Step 1 - Discover UPnP service.
if (discoverUPnP()) {
// Step 2 - Check if mapping already exists
if (!(bSuccess = requestPortMappingEntry())) {
// Step 3 - Request new mapping
bSuccess = requestPortForwarding();
}
} else {
ESP_LOGI(TAG_UPNP," UPnP discovery failed.");
}
if (bSuccess) {
// Extract external IP
requestExternalIP();
ESP_LOGI(TAG_UPNP," Public IP(Port): %s(%d)\n", publicIP.toString().c_str(), publicPort);
// Extract external port assigned by UPnP
// requestExternalPort();
// ESP_LOGI(TAG_UPNP,"Assigned External Port: %d\n", publicPort);
*pport = publicPort; // External server port
*pip = publicIP; // External IP address
return true;
} else {
ESP_LOGI(TAG_UPNP," UPnP PortForwarding failed.");
}
return bSuccess;
}
bool CUpnpClient::discoverUPnP() {
//ESP_LOGI(TAG_UPNP,"\n\n** Sending SSDP discovery request...");
WiFiUDP udp;
bool ret = false;
// SSDP M-SEARCH Request
const char *ssdpRequest =
"M-SEARCH * HTTP/1.1\r\n"
"HOST: 239.255.255.250:1900\r\n"
"MAN: \"ssdp:discover\"\r\n"
"MX: 3\r\n"
"ST: urn:schemas-upnp-org:device:InternetGatewayDevice:1\r\n"
"\r\n";
udp.beginMulticast(SSDP_MULTICAST_IP, SSDP_PORT);
udp.beginPacket(SSDP_MULTICAST_IP, SSDP_PORT);
udp.write((const uint8_t *)ssdpRequest, strlen(ssdpRequest)); // Fix length
udp.endPacket();
unsigned long startTime = millis();
while (millis() - startTime < SEARCH_TIMEOUT) {
int packetSize = udp.parsePacket();
if (packetSize > 0) {
udp.read(buffer, sizeof(buffer) - 1);
buffer[packetSize] = '\0';
//ESP_LOGI(TAG_UPNP,"SSDP response received:");
//ESP_LOGI(TAG_UPNP,buffer);
// Check if the response contains "ST: urn:schemas-upnp-org:device:InternetGatewayDevice:1"
char *stField = strstr(buffer, "ST: urn:schemas-upnp-org:device:InternetGatewayDevice:1");
if (stField) {
// Extract router's UPnP service URL
char *location = strstr(buffer, "LOCATION: ");
if (location) {
location += 9; // Move past "LOCATION: "
while (*location == ' ') location++; // skip blanks
char *end = strchr(location, '\r');
if (end) {
*end = '\0';
//ESP_LOGI(TAG_UPNP,"Router UPnP URL: %s\n", location);
routerLocation = String(location);
// Extract IP and Port from the Location URL
int firstColonPos = routerLocation.indexOf(':'); // Find the first colon (for the protocol)
int secondColonPos = routerLocation.indexOf(':', firstColonPos + 1); // Find the second colon (IP:PORT)
if (secondColonPos != -1) {
routerIPString = routerLocation.substring(routerLocation.indexOf("://") + 3, secondColonPos); // Extract IP
routerPort = routerLocation.substring(secondColonPos + 1, routerLocation.indexOf('/', secondColonPos)).toInt(); // Extract Port
}
routerIP.fromString(routerIPString);
//ESP_LOGI(TAG_UPNP,"Router UPnP IP(Port): %s(%d)\n", routerIP.toString().c_str(), routerPort);
String xml = fetchUPnPDescription(routerLocation);
if (!xml.isEmpty()) {
// ESP_LOGI(TAG_UPNP,xml);
// ESP_LOGI(TAG_UPNP,"\n--- End Of XML ----\n");
// Parse the XML and get the controlURL
// String parseXML(const String &xml);
controlURL = parseXML(xml);
if (!controlURL.isEmpty()) {
ret = true;
break;
}
}
}
}
}
}
}
udp.stop();
if (!ret) {
DPRINTLN("No SSDP response from UPnP device.");
}
return ret;
}
String CUpnpClient::fetchUPnPDescription(const String &location) {
HTTPClient http;
WiFiClient client;
//ESP_LOGI(TAG_UPNP,"Fetching UPnP XML from: \"%s\"\n", location.c_str());
http.begin(client, location);
int httpCode = http.GET();
if (httpCode > 0) {
//ESP_LOGI(TAG_UPNP,"HTTP Response Code: %d\n", httpCode);
if (httpCode == HTTP_CODE_OK) {
String xmlContent = http.getString();
http.end();
return xmlContent;
}
}
else {
//ESP_LOGI(TAG_UPNP,"HTTP Request failed, error: %s\n", http.errorToString(httpCode).c_str());
}
http.end();
return "";
}
String CUpnpClient::parseXML(const String &xml) {
// Locate the WANIPConnection service
int servicePos = xml.indexOf("<serviceType>urn:schemas-upnp-org:service:WANIPConnection:1</serviceType>");
if (servicePos == -1) {
ESP_LOGI(TAG_UPNP,"WANIPConnection service not found in XML.");
return "";
}
// Find the start of the controlURL tag within the <service> block
int controlStart = xml.indexOf("<controlURL>", servicePos);
if (controlStart == -1) {
ESP_LOGI(TAG_UPNP,"controlURL not found in service block.");
return "";
}
controlStart += 12; // Move past "<controlURL>"
// Find the closing tag
int controlEnd = xml.indexOf("</controlURL>", controlStart);
if (controlEnd == -1) {
ESP_LOGI(TAG_UPNP,"Malformed XML: Missing </controlURL>.");
return "";
}
// Extract and clean the controlURL
String controlURL = xml.substring(controlStart, controlEnd);
controlURL.trim(); // Remove any spaces or newlines
//ESP_LOGI(TAG_UPNP,"Extracted controlURL: %s\n", controlURL.c_str());
return controlURL;
}
int CUpnpClient::sendSoapRequest(const char *request, char *response, size_t responseSize) {
WiFiClient client;
if (!client.connect(routerIP, routerPort)) { // UPnP uses port 1900 for SOAP requests
ESP_LOGI(TAG_UPNP,"Failed to connect to router: %s\n", routerIP.toString().c_str());
return -1;
}
client.print("POST /control?WANIPConn1 HTTP/1.1\r\n");
client.print("Host: ");
client.print(routerIP);
client.print("\r\n");
client.print("Content-Type: text/xml; charset=\"utf-8\"\r\n");
client.print("SOAPAction: \"urn:schemas-upnp-org:service:WANIPConnection:1#GetSpecificPortMappingEntry\"\r\n");
client.print("Content-Length: ");
client.print(strlen(request));
client.print("\r\n\r\n");
client.print(request);
unsigned long startMillis = millis();
while (!client.available() && millis() - startMillis < 5000) {
delay(10); // Wait for response
}
int index = 0;
while (client.available() && index < responseSize - 1) {
response[index++] = client.read();
}
response[index] = '\0';
client.stop();
return index;
}
bool CUpnpClient::requestPortMappingEntry() {
ESP_LOGI(TAG_UPNP,"** Requesting UPnP Port Mapping Entry...");
char soapRequest[512];
char mappingName[32];
sprintf(mappingName, "HC_%04X", publicPort);
snprintf(soapRequest, sizeof(soapRequest),
"<?xml version=\"1.0\"?>"
"<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" "
"s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
"<s:Body>"
"<u:GetSpecificPortMappingEntry xmlns:u=\"urn:schemas-upnp-org:service:WANIPConnection:1\">"
"<NewRemoteHost></NewRemoteHost>"
"<NewExternalPort>%d</NewExternalPort>"
"<NewProtocol>TCP</NewProtocol>"
"<NewInternalClient>%s</NewInternalClient>"
"<NewPortMappingDescription>%s</NewPortMappingDescription>"
"</u:GetSpecificPortMappingEntry>"
"</s:Body>"
"</s:Envelope>",
publicPort,
WiFi.localIP().toString().c_str(),
mappingName);
char response[1024];
int responseLen = sendSoapRequest(soapRequest, response, sizeof(response));
if (responseLen > 0) {
if (strstr(response, mappingName)) {
//ESP_LOGI(TAG_UPNP," Port mapping already exists.");
return true;
}
}
ESP_LOGI(TAG_UPNP," Port mapping not found.");
return false;
}
bool CUpnpClient::requestPortForwarding() {
// Sending port-forwarding request to the router's external IP or gateway
ESP_LOGI(TAG_UPNP,"** Requesting UPnP Port Forwarding...");
if (controlURL.isEmpty()) {
ESP_LOGI(TAG_UPNP,"Error: controlURL is empty.");
return false;
}
// Ensure controlURL is correctly formatted (handle absolute/relative cases)
String postURL = controlURL;
if (postURL.startsWith("http://") || postURL.startsWith("https://")) {
int pathStart = postURL.indexOf('/', 8); // Skip "http://"
if (pathStart != -1) {
postURL = postURL.substring(pathStart); // Extract path only
ESP_LOGI(TAG_UPNP,"PostURL: \"%s\"\n", postURL.c_str());
} else {
ESP_LOGI(TAG_UPNP,"Invalid controlURL format.");
return false;
}
}
// XML body
char xmlBody[800];
snprintf(xmlBody, sizeof(xmlBody),
"<?xml version=\"1.0\"?>"
"<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
"<s:Body><u:AddPortMapping xmlns:u=\"urn:schemas-upnp-org:service:WANIPConnection:1\">"
"<NewRemoteHost></NewRemoteHost>"
"<NewExternalPort>%d</NewExternalPort>"
"<NewProtocol>TCP</NewProtocol>"
"<NewInternalPort>%d</NewInternalPort>"
"<NewInternalClient>%s</NewInternalClient>"
"<NewEnabled>1</NewEnabled>"
"<NewPortMappingDescription>HC_%04X</NewPortMappingDescription>"
"<NewLeaseDuration>0</NewLeaseDuration>"
"</u:AddPortMapping></s:Body></s:Envelope>",
publicPort, publicPort,
WiFi.localIP().toString().c_str(),
publicPort);
//ESP_LOGI(TAG_UPNP,"XML Body: ");
//ESP_LOGI(TAG_UPNP,xmlBody);
// Calculate the correct content length
int contentLength = strlen(xmlBody);
snprintf(buffer, sizeof(buffer),
"POST %s HTTP/1.1\r\n"
"Host: %s:%d\r\n"
"Content-Type: text/xml; charset=\"utf-8\"\r\n"
"SOAPAction: \"urn:schemas-upnp-org:service:WANIPConnection:1#AddPortMapping\"\r\n"
"Content-Length: %d\r\n"
"Connection: Close\r\n"
"\r\n"
"%s", // Append XML body after headers
controlURL.c_str(),
routerIP.toString().c_str(), routerPort,
contentLength,
xmlBody);
//ESP_LOGI(TAG_UPNP,"==== HTTP Request ===\n\n%s\n\n--- End Of HTTP ---\n", buffer);
// Connect and send
WiFiClient client;
if (!client.connect(routerIP, routerPort)) {
ESP_LOGI(TAG_UPNP,"Failed to connect to the router. %s:%d\n", routerIP.toString().c_str(), routerPort);
return false;
}
client.print(buffer);
// Read response
String response;
uint32_t timeout = millis() + 5000;
while (client.available() == 0) {
if (millis() > timeout) {
ESP_LOGI(TAG_UPNP,"Router did not respond to port mapping request.");
client.stop();
return false;
}
}
while (client.available()) {
response += client.readString();
}
client.stop();
return true;
};
bool CUpnpClient::requestExternalIP() {
//ESP_LOGI(TAG_UPNP,"\n\nRequesting External IP via UPnP...");
// XML Request Body
char xmlBody[300];
snprintf(xmlBody, sizeof(xmlBody),
"<?xml version=\"1.0\"?>"
"<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
"<s:Body>"
"<u:GetExternalIPAddress xmlns:u=\"urn:schemas-upnp-org:service:WANIPConnection:1\"/>"
"</s:Body>"
"</s:Envelope>");
int contentLength = strlen(xmlBody);
// HTTP Request
snprintf(buffer, sizeof(buffer),
"POST %s HTTP/1.1\r\n"
"Host: %s:%d\r\n"
"Content-Type: text/xml; charset=\"utf-8\"\r\n"
"SOAPAction: \"urn:schemas-upnp-org:service:WANIPConnection:1#GetExternalIPAddress\"\r\n"
"Content-Length: %d\r\n"
"Connection: Close\r\n"
"\r\n"
"%s",
controlURL.c_str(),
routerIP.toString().c_str(), routerPort,
contentLength,
xmlBody);
// Connect and Send
WiFiClient client;
if (!client.connect(routerIP, routerPort)) {
DPRINTLN("Failed to connect to router for External IP request.");
return false;
}
client.print(buffer);
// Read Response
String response;
uint32_t timeout = millis() + 5000;
while (client.available() == 0) {
if (millis() > timeout) {
DPRINTLN("Router did not respond to External IP request.");
client.stop();
return false;
}
}
while (client.available()) {
response += client.readString();
}
client.stop();
//ESP_LOGI(TAG_UPNP,"External IP Response:");
//ESP_LOGI(TAG_UPNP,response);
// Extract External IP from XML
char *sz = (char *)(response.c_str());
char *extIP = strstr(sz, "<NewExternalIPAddress>");
if (extIP) {
extIP += 22;
char *end = strchr(extIP, '<');
if (end) *end = '\0';
publicIP = IPAddress(extIP);
DPRINTF("UPnP - External IP: %s\n", extIP);
return true;
}
DPRINTLN("Failed to extract External IP.");
return false;
}
bool CUpnpClient::requestExternalPort() {
//ESP_LOGI(TAG_UPNP,"\n\nRequesting External Port via UPnP...");
// XML Request Body for getting External Port
char xmlBody[300];
snprintf(xmlBody, sizeof(xmlBody),
"<?xml version=\"1.0\"?>"
"<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
"<s:Body>"
"<u:GetExternalPort xmlns:u=\"urn:schemas-upnp-org:service:WANIPConnection:1\"/>"
"</s:Body>"
"</s:Envelope>");
int contentLength = strlen(xmlBody);
// HTTP Request to get External Port
snprintf(buffer, sizeof(buffer),
"POST %s HTTP/1.1\r\n"
"Host: %s:%d\r\n"
"Content-Type: text/xml; charset=\"utf-8\"\r\n"
"SOAPAction: \"urn:schemas-upnp-org:service:WANIPConnection:1#GetExternalPort\"\r\n"
"Content-Length: %d\r\n"
"Connection: Close\r\n"
"\r\n"
"%s",
controlURL.c_str(),
routerIP.toString().c_str(), routerPort,
contentLength,
xmlBody);
// Connect and Send
WiFiClient client;
if (!client.connect(routerIP, routerPort)) {
ESP_LOGI(TAG_UPNP,"Failed to connect to router for External Port request.");
return false;
}
client.print(buffer);
// Read Response
String response;
uint32_t timeout = millis() + 5000;
while (client.available() == 0) {
if (millis() > timeout) {
ESP_LOGI(TAG_UPNP,"Router did not respond to External Port request.");
client.stop();
return false;
}
}
while (client.available()) {
response += client.readString();
}
client.stop();
//ESP_LOGI(TAG_UPNP,"External Port Response:");
//ESP_LOGI(TAG_UPNP,response);
// Extract External Port from XML
char *sz = (char *)(response.c_str());
char *extPort = strstr(sz, "<NewExternalPort>");
if (extPort) {
extPort += 17;
char *end = strchr(extPort, '<');
if (end) *end = '\0';
publicPort = atoi(extPort);
ESP_LOGI(TAG_UPNP,"External Port: %d\n", publicPort);
return true;
}
ESP_LOGI(TAG_UPNP,"Failed to extract External Port.");
return false;
};

View File

@ -1,37 +0,0 @@
#include <WiFi.h>
#include <WiFiUdp.h>
#define SSDP_MULTICAST_IP IPAddress("239.255.255.250")
#define SSDP_PORT 1900
#define UPNP_HTTP_PORT 5000 // Most routers use 5000, but it can vary
#define SEARCH_TIMEOUT 5000 // 2 seconds timeout
class CUpnpClient {
private:
IPAddress routerIP;
String routerIPString;
uint16_t routerPort;
IPAddress publicIP;
uint16_t publicPort;
uint32_t lastDiscoveryTime;
String routerLocation;
String controlURL;
char buffer[1024]; // Stack-based buffer for responses
public:
CUpnpClient() : publicPort(0), lastDiscoveryTime(0) {}
bool registerUPnP(uint32_t *pip, uint16_t *pport);
private:
String fetchUPnPDescription(const String &location);
String parseXML(const String &xml);
int sendSoapRequest(const char *request, char *response, size_t responseSize);
bool discoverUPnP();
bool requestPortMappingEntry();
bool requestPortForwarding();
bool requestExternalIP();
bool requestExternalPort();
};

View File

@ -149,7 +149,7 @@ int ESPUpdateClass::update(WiFiClient& client,
{
HTTPClient http;
if (!http.begin(client, host, port, uri)) {
Serial.println("OTA - httpClient begin failed");
DPRINTLN("OTA - httpClient begin failed");
return -1;
}

19
zcd.cpp
View File

@ -10,6 +10,7 @@
#include "hal/timer_types.h"
#include "driver/timer.h" // Needed for timer_isr_register
#include "rom/ets_sys.h"
#include "driver/gpio.h"
timg_dev_t *tg0 = &TIMERG0;
@ -307,10 +308,22 @@ void setupZCD() {
//attachInterrupt(PIN_ZCD_LOAD, zcdLoadISR, CHANGE); // Attach zero-cross detection ISR
// Attach Load ISR with higher priority
esp_intr_alloc(ETS_GPIO_INTR_SOURCE, ESP_INTR_FLAG_LEVEL3, zcdLoadISR, NULL, NULL);
// Set the edges
gpio_config_t io_conf = {};
io_conf.intr_type = GPIO_INTR_ANYEDGE;
io_conf.pin_bit_mask = (1ULL << PIN_ZCD_AC) | (1ULL << PIN_ZCD_LOAD);
io_conf.mode = GPIO_MODE_INPUT;
gpio_config(&io_conf);
// Attach AC ISR with normal/lower priority
esp_intr_alloc(ETS_GPIO_INTR_SOURCE, ESP_INTR_FLAG_LEVEL1, zcdACISR, NULL, NULL);
// 2. Set the GLOBAL Priority for all GPIOs
// We use LEVEL3 so that BOTH ZCD pulses can preempt lower tasks.
// This replaces the attachInterrupt() default (Level 1).
gpio_install_isr_service(ESP_INTR_FLAG_IRAM | ESP_INTR_FLAG_LEVEL3);
// 3. Register the independent handlers
// The service handles the "Which pin was it?" logic automatically.
gpio_isr_handler_add((gpio_num_t)PIN_ZCD_LOAD, zcdLoadISR, NULL);
gpio_isr_handler_add((gpio_num_t)PIN_ZCD_AC, zcdACISR, NULL);
}
void setACLoadStatus(uint32_t tNow) {