#include "HermitCrab.h" #include "Config.h" #include "AHT2x.h" #include "NTC_10K.h" #include "ZCD.h" #include "History.h" #include "TimeManager.h" #include "ConnectWiFi.h" #include "WiFiHost.h" #include "OTA.h" #include "UI.h" #include "BLEScan.h" #if defined(ESP32) #include "esp_wifi.h" #endif #define TAG_MAIN "Main" STATUS_TYPE status; // Time volatile unsigned short g_nYear, g_nMonth, g_nDay, g_nHour, g_nMinute, g_nSecond; // Environment bool bShowSensor = false; time_t now; struct tm timeinfo; void readSensors(); void controlAC1(short temp, unsigned long tick); void controlAC2(short temp, unsigned long tick); void controlMist(short humid, unsigned long tick); void controlFan(short temp, unsigned long tick); void controlMotor(short hour, short min, unsigned long tick); void controlLight(short hour, short min, unsigned long tick); void controlFanDuty(); void controlMotorDuty(); void controlLightDuty(); // ================================================================================== // // Arduino Loop - controls // // ================================================================================== MY_IRAM_ATTR void loop() { static unsigned long lastTickSecond = 0; static uint8_t lastSecond = -1; unsigned long tickMillis = millis(); unsigned long tickSecond = tickMillis / 1000; // Un-Conditional Loop { //ESP_LOGI(TAG_MAIN,"Checking WiFi2"); checkWiFi(tickMillis); //ESP_LOGI(TAG_MAIN,"Host Loop"); host.Loop(tickMillis); // UI Button Check ui.loopButton(tickMillis); } // Every Second if (tickSecond != lastTickSecond) { // Time and ZCD setZCD(); setTime(); // Temperature and Humidity readSensors(); //ble.loop(tickMillis); // Fan, Motor, Light Duties controlFanDuty(); controlMotorDuty(); controlLightDuty(); // Add to History - every minutes if (g_nSecond == 0 && lastSecond != g_nSecond) { history.add(status); } lastSecond = g_nSecond; // Every 10 Second switch(tickSecond % 10) { case 1: // Every 5 second - xx:xx-x7 if (bShowSensor) { ESP_LOGI(TAG_MAIN, "%s\n", printStatus(tickSecond, true)); } break; case 2: // AC1 controlAC1(status.nTemp1, tickSecond); break; case 3: // AC2 controlAC2(status.nTemp1, tickSecond); break; case 4: // Mist controlMist(status.nHumid1, tickSecond); break; case 5: // Fan Control controlFan(status.nTemp1, tickSecond); break; case 6: // Motor Control controlMotor(g_nHour, g_nMinute, tickSecond); break; case 7: // Light Control controlLight(g_nHour, g_nMinute, tickSecond); break; default: break; } lastTickSecond = tickSecond; } yield(); } // ================================================================================== // End of Main Loop // ================================================================================== void readSensors() { switch (config.nTemp1SensorType) { case TEMP_SENSOR_TYPE::AHT20: case TEMP_SENSOR_TYPE::AHT2x: status.nTemp1 = (aht25.getTemperature() + 5) / 10 + config.nTemp1Offset; status.nHumid1 = (aht25.getHumidity() + 5) / 10 + config.nHumid1Offset; break; case TEMP_SENSOR_TYPE::AHT10_0x39: status.nTemp1 = (aht10_0x39.getTemperature() + 5) / 10 + config.nTemp1Offset; status.nHumid1 = (aht10_0x39.getHumidity() + 5) / 10 + config.nHumid1Offset; break; case TEMP_SENSOR_TYPE::NTC: status.nTemp1 = ntc.getTemp() + config.nTemp1Offset; status.nHumid1 = 0; break; case TEMP_SENSOR_TYPE::BLE_TUYA: case TEMP_SENSOR_TYPE::BLE_XIAOMI_MIJIA: status.nTemp1 = (ble.getTemp() + 5) / 10 + config.nTemp1Offset; status.nHumid1 = (ble.getHumid() + 5) / 10 + config.nHumid1Offset; if (ble.getBatteyLevel() > 30) status.nFlags &= ~FLAG_BLE_BATT; else status.nFlags |= FLAG_BLE_BATT; break; default: status.nTemp1 = 0; status.nHumid1 = 0; break; } switch (config.nTemp2SensorType) { case TEMP_SENSOR_TYPE::AHT20: case TEMP_SENSOR_TYPE::AHT2x: status.nTemp2 = (aht25.getTemperature() + 5) / 10 + config.nTemp2Offset; status.nHumid2 = (aht25.getHumidity() + 5) / 10 + config.nHumid2Offset; break; case TEMP_SENSOR_TYPE::AHT10_0x39: status.nTemp2 = (aht10_0x39.getTemperature() + 5) / 10 + config.nTemp2Offset; status.nHumid2 = (aht10_0x39.getHumidity() + 5) / 10 + config.nHumid2Offset; break; case TEMP_SENSOR_TYPE::NTC: status.nTemp2 = ntc.getTemp() + config.nTemp2Offset; status.nHumid2 = 0; break; case TEMP_SENSOR_TYPE::BLE_TUYA: case TEMP_SENSOR_TYPE::BLE_XIAOMI_MIJIA: status.nTemp2 = (ble.getTemp2() + 5) / 10 + config.nTemp2Offset; status.nHumid2 = (ble.getHumid2() + 5) / 10 + config.nHumid2Offset; if (ble.getBatteyLevel2() > 30) status.nFlags &= ~FLAG_BLE_BATT; else status.nFlags |= FLAG_BLE_BATT; break; default: status.nTemp2 = 0; status.nHumid2 = 0; break; } status.nTemp3 = ntc.getTemp() + config.nTemp3Offset; } // ================================================================================== // Device Control Functions // ================================================================================== MY_IRAM_ATTR void controlAC1(short temp, unsigned long tick) { uint16_t max = config.ac1.dutyMax * 10; // Check Safety if (temp >= config.nTempSafety) { setHeater1Duty(0); status.nHeater1Duty = 0; return; } // Manual Control if (status.nFlags & FLAG_MANUAL_HEATER1) { if (status.nHeater1Duty > max) status.nHeater1Duty = max; setHeater1Duty(status.nHeater1Duty); return; } // Day & Night bool bNight = isNight(g_nHour, g_nMinute); if (!config.ac1.bDay && !bNight || !config.ac1.bNight && bNight) { setHeater1Duty(0); status.nHeater1Duty = 0; return; } // No Sensor if ((status.nTemp1 - config.nTemp1Offset) == 0 && (status.nHumid1 - config.nHumid1Offset) == 0) { setHeater1Duty(0); status.nHeater1Duty = 0; return; } uint16_t setPoint; int16_t duty; //DPRINTF("AC1 Start: %d°C set: %d duty: %d\n", temp, setPoint, status.nHeater1Duty); switch(config.ac1.nControlType) { case CONTROL_TEMP_HEAT_PID: { setPoint = config.bNightControl && isNight(g_nHour, g_nMinute) ? config.nTempTargetNight: config.nTempTarget; duty = history.calculateDutyForTemp1(setPoint, temp, status.nHeater1Duty); //DPRINTF("AC1: %d°C set: %d duty: %d --> %d\n", temp, setPoint, status.nHeater1Duty, duty); status.nHeater1Duty = duty; if (duty > 0) duty = map(duty, 1, 10000, config.ac1.dutyMin * 10, config.ac1.dutyMax * 10); setHeater1Duty(duty); } break; case CONTROL_TEMP_COOL_PID: case CONTROL_HUMIDITY_INC_PID: case CONTROL_HUMIDITY_DEC_PID: break; default: duty = controlDevice(&config.ac1, 0) * 10; status.nHeater1Duty = duty; if (duty > 0) duty = map(duty, 1, 10000, config.ac1.dutyMin * 10, config.ac1.dutyMax * 10); setHeater1Duty(duty); break; } //DPRINTF("AC1 End: %d°C set: %d duty: %d\n", temp, setPoint, status.nHeater1Duty); } MY_IRAM_ATTR void controlAC2(short temp, unsigned long tick) { uint16_t max = config.ac2.dutyMax * 10; // Check Safety if (temp >= config.nTempSafety) { setHeater2Duty(0); status.nHeater2Duty = 0; //DPRINTLN("Safety Temp"); return; } // Manual Control if (status.nFlags & FLAG_MANUAL_HEATER2) { if (status.nHeater2Duty > max) status.nHeater2Duty = max; setHeater2Duty(status.nHeater2Duty); return; } // Day & Night bool bNight = isNight(g_nHour, g_nMinute); if (!config.ac2.bDay && !bNight || !config.ac2.bNight && bNight) { setHeater2Duty(0); status.nHeater2Duty = 0; //DPRINTLN("Day/Night Reject"); return; } // No Sensor if ((status.nTemp1 - config.nTemp1Offset) == 0 && (status.nHumid1 - config.nHumid1Offset) == 0) { setHeater2Duty(0); status.nHeater2Duty = 0; //DPRINTLN("No Sensor"); return; } uint16_t setPoint; int16_t duty; switch(config.ac2.nControlType) { case CONTROL_TEMP_HEAT_PID: { setPoint = config.bNightControl && isNight(g_nHour, g_nMinute) ? config.nTempTargetNight: config.nTempTarget; duty = history.calculateDutyForTemp1(setPoint, temp, status.nHeater2Duty); //DPRINTF("AC2: %d°C set: %d duty: %d --> %d\n", temp, setPoint, status.nHeater2Duty, duty); status.nHeater2Duty = duty; if (duty > 0) duty = map(duty, 1, 10000, config.ac1.dutyMin * 10, config.ac1.dutyMax * 10); setHeater2Duty(duty); } break; case CONTROL_TEMP_COOL_PID: case CONTROL_HUMIDITY_INC_PID: case CONTROL_HUMIDITY_DEC_PID: break; default: status.nHeater2Duty = controlDevice(&config.ac2, 1) * 10; if (status.nHeater2Duty > 0) duty = map(status.nHeater2Duty, 1, 10000, config.ac2.dutyMin * 10, config.ac2.dutyMax * 10); setHeater2Duty(duty); break; } } MY_IRAM_ATTR void controlMist(short humid, unsigned long tick) { uint16_t duty = 0; bool bNight = isNight(g_nHour, g_nMinute); // Manual Control if (status.nFlags & FLAG_MANUAL_MIST) { duty = status.nMistDuty; } else // Day & Night if (!config.mist.bDay && !bNight || !config.mist.bNight && bNight) { duty = 0; } else { if (status.nTemp1 != 0 && status.nHumid1 != 0) { switch(config.mist.nControlType) { case CONTROL_HUMIDITY_INC_PID: { uint16_t setPoint; setPoint = config.bNightControl && isNight(g_nHour, g_nMinute) ? config.nHumidTargetNight: config.nHumidTarget; duty = history.calculateMistDuty(setPoint, humid, status.nMistDuty); //DPRINTF("Mist: %d%% set: %d duty: %d --> %d\n", humid, setPoint, status.nMistDuty, duty); } break; case CONTROL_TEMP_HEAT_PID: case CONTROL_TEMP_COOL_PID: case CONTROL_HUMIDITY_DEC_PID: duty = status.nMistDuty = 0; break; default: duty = controlDevice(&config.mist, 2) * 10; break; } } else { duty = 0; } } status.nMistDuty = duty; // Control Duty if (duty > PWM_OFF) duty = map(duty, 1, 10000, config.mist.dutyMin, config.mist.dutyMax); ledcWrite(PIN_MIST, duty); } MY_IRAM_ATTR void controlFan(short temp, unsigned long tick) { if (status.nFlags & FLAG_MANUAL_FAN ) return; status.nFanDuty = controlDevice(&config.fan, 3); } MY_IRAM_ATTR void controlMotor(short hour, short min, unsigned long tick) { if (status.nFlags & FLAG_MANUAL_MOTOR ) return; status.nMotorDuty = controlDevice(&config.motor, 4); } MY_IRAM_ATTR void controlLight(short hour, short min, unsigned long tick) { if (status.nFlags & FLAG_MANUAL_LIGHT ) return; status.nLightTargetDuty = controlDevice(&config.light, 5); } MY_IRAM_ATTR uint16_t controlDevice(DEVICE_PARAM_TYPE *pDevice, int idx) { uint16_t duty; switch(idx) { case 0: duty = status.nHeater1Duty / 10; break; // AC1 case 1: duty = status.nHeater2Duty / 10; break; // AC2 case 2: duty = status.nMistDuty / 10; break; // Mist case 3: duty = status.nFanDuty; break; // Fan case 4: duty = status.nMotorDuty; break; // Motor case 5: duty = status.nLightDuty; break; // Light default: duty = 0; break; } bool bNight = isNight(g_nHour, g_nMinute); if (!pDevice->bDay && !bNight || !pDevice->bNight && bNight) return 0; switch (pDevice->nControlType) { case CONTROL_TEMP_HEAT: if (status.nTemp1 < pDevice->tempLow) { duty = pDevice->dutyOn; } else if (status.nTemp1 > pDevice->tempHigh) { duty = pDevice->dutyOff; } break; case CONTROL_TEMP_COOL: if (status.nTemp1 > pDevice->tempHigh) duty = pDevice->dutyOn; else if (status.nTemp1 < pDevice->tempLow) duty = pDevice->dutyOff; break; case CONTROL_HUMIDITY_DEC: if (status.nHumid1 > pDevice->humidHigh) duty = pDevice->dutyOn; else if (status.nHumid1 < pDevice->humidLow) duty = pDevice->dutyOff; break; case CONTROL_HUMIDITY_INC: if (status.nHumid1 < pDevice->humidLow) duty = pDevice->dutyOn; else if (status.nHumid1 > pDevice->humidHigh) duty = pDevice->dutyOff; break; case CONTROL_DAY_NIGHT: { duty = isNight(g_nHour, g_nMinute) ? pDevice->dutyNight : pDevice->dutyDay; } break; case CONTROL_TIME: { uint16_t time = g_nHour * 60 + g_nMinute; if (pDevice->timeBegin < pDevice->timeEnd) { if (time >= pDevice->timeBegin && time < pDevice->timeEnd) duty = pDevice->dutyOn; else duty = pDevice->dutyOff; } else { if (time >= pDevice->timeEnd && time < pDevice->timeBegin) duty = pDevice->dutyOff; else duty = pDevice->dutyOn; } } break; case CONTROL_PERIOD: { static unsigned long periodStartTime[6] = {0}; // Tracks start of the current period (in seconds) static unsigned long totalElapsedSeconds[6] = {0}; // Tracks elapsed time within the current period static unsigned long lastCurrentTime[6] = {0xFFFFFFFF}; unsigned long time = g_nHour * 3600UL + g_nMinute * 60UL + g_nSecond; unsigned long period = pDevice->periodPeriod * 60; if (lastCurrentTime[idx] == 0xFFFFFFFF) { lastCurrentTime[idx] = time; } if (time < lastCurrentTime[idx]) { // Handle day rollover (23:59:59 -> 00:00:00) totalElapsedSeconds[idx] += (24UL * 3600) + time - lastCurrentTime[idx]; // Normal time increment } else { totalElapsedSeconds[idx] += time - lastCurrentTime[idx]; // Normal time increment } lastCurrentTime[idx] = time; // Reset totalElapsedSeconds when it exceeds the period if (totalElapsedSeconds[idx] >= periodStartTime[idx] + period) { totalElapsedSeconds[idx] -= period; // Wrap around to start a new period periodStartTime[idx] = totalElapsedSeconds[idx]; // Reset the start time duty = pDevice->dutyOn; // Start of the new period } // Turn off LED if the onDuration has passed within the current period if (totalElapsedSeconds[idx] >= periodStartTime[idx] + pDevice->periodOn) { duty = pDevice->dutyOff; // Start of the new period } else { duty = pDevice->dutyOn; } } break; default: duty = 0; } return duty; } MY_IRAM_ATTR void controlFanDuty() { static uint16_t nFanDuty = 0; if (nFanDuty != status.nFanDuty) { if (nFanDuty == 0) { // Start-up nFanDuty = status.nFanDuty < config.fan.dutyStart ? config.fan.dutyStart : status.nFanDuty; } else { nFanDuty = status.nFanDuty; } int duty = 0; if (nFanDuty > 0) { duty = map(nFanDuty, 1, 1000, config.fan.dutyMin, config.fan.dutyMax); } ledcWrite(PIN_FAN, duty); } } MY_IRAM_ATTR void controlMotorDuty() { static uint16_t nMotorDuty = 0; if (nMotorDuty != status.nMotorDuty) { // Start-up if (nMotorDuty == 0) { nMotorDuty = status.nMotorDuty < config.motor.dutyStart ? config.motor.dutyStart : status.nMotorDuty; } else { nMotorDuty = status.nMotorDuty; } int duty = 0; if (nMotorDuty > 0) { duty = map(nMotorDuty, 1, 1000, config.motor.dutyMin, config.motor.dutyMax); } ledcWrite(PIN_MOTOR, duty); } } MY_IRAM_ATTR void controlLightDuty() { if (status.nLightDuty != status.nLightTargetDuty) { int16_t step = status.nLightDuty < status.nLightTargetDuty ? 1 : -1; status.nLightDuty += step; int16_t duty = map(status.nLightDuty, 0, 1000, config.light.dutyMin, config.light.dutyMax); ledcWrite(PIN_LIGHT, duty); } } MY_IRAM_ATTR bool isNight(unsigned char currentHour, unsigned char currentMin) { // Check if the current time is within the night range if (config.nNightStartHour < config.nNightEndHour || (config.nNightStartHour == config.nNightEndHour && config.nNightStartMin < config.nNightEndMin)) { // Case 1: Night starts and ends on the same day (e.g., 22:00 to 06:00) if ((currentHour > config.nNightStartHour || (currentHour == config.nNightStartHour && currentMin >= config.nNightStartMin)) && (currentHour < config.nNightEndHour || (currentHour == config.nNightEndHour && currentMin <= config.nNightEndMin))) { return true; // It's night time } } else { // Case 2: Night crosses midnight (e.g., 22:00 to 06:00) if ((currentHour > config.nNightStartHour || (currentHour == config.nNightStartHour && currentMin >= config.nNightStartMin)) || (currentHour < config.nNightEndHour || (currentHour == config.nNightEndHour && currentMin <= config.nNightEndMin))) { return true; // It's night time } } return false; // It's day time } // ====================================================================== // // Utilities // // ====================================================================== MY_IRAM_ATTR char *printStatus(unsigned long tick, bool bLong) { static char szStatus[256] = { 0 }; //if (config.bSendStatusSerial) { // Build Status String char strHeat1[32], strHeat2[32], strMist[32], strLight1[32], strLight2[32]; strHeat1[0] = 0; strHeat2[0] = 0; strMist[0] = 0; strLight1[0] = 0; strLight2[0] = 0; sprintf(strHeat1, "T(%2d.%d/%2d.%d°C H%2d.%d%%)", status.nTemp1 / 10, status.nTemp1 % 10, config.nTempTarget / 10, config.nTempTarget % 10, status.nHeater1Duty / 100, status.nHeater1Duty % 100 ); sprintf(strMist, "H(%2d.%d/%2d.%d%% M%2d.%d%%)", status.nHumid1 / 10, status.nHumid1 % 10, config.nHumidTarget / 10, config.nHumidTarget % 10, status.nMistDuty / 10, status.nMistDuty % 10); sprintf(strLight1, "L(%2d.%d%%)", status.nLightDuty / 10, status.nLightDuty % 10); sprintf(szStatus, "%s %s %s %s H(Kp %.2f, Kd %.2f) M(Kp %.2f, %.2f)", printTime(bLong), strHeat1, strMist, strLight1, history.getKpTemperature(), history.getKdTemperature(), history.getKpHumidity(), history.getKdHumidity()); // Send out to clients // ESP_LOGI(TAG_MAIN,"%s\n", szStatus); } return szStatus; } MY_IRAM_ATTR char *printTime(bool bLong) { static char szBuff[48]; // Get current time from the system clock time_t now; time(&now); // Get current system time in seconds struct tm *timeinfo = localtime(&now); if (bLong) { // Calculate uptime in seconds /* unsigned long uptime = now - timeManager.getFirstNTPTime(); // Calculate days, hours, minutes, seconds for uptime int days = uptime / 86400; // Seconds in a day uptime %= 86400; int hours = uptime / 3600; // Seconds in an hour uptime %= 3600; int minutes = uptime / 60; // Seconds in a minute int seconds = uptime % 60; // Format the current time char szDays[8], szHours[8]; if (days > 0) sprintf(szDays, "%d ", days); else szDays[0] = 0; if (hours > 0) sprintf(szHours, "%d:", hours); else szHours[0] = 0; */ sprintf(szBuff, "%04d-%02d-%02d %02d:%02d:%02d", timeinfo->tm_year + 1900, // Year timeinfo->tm_mon + 1, // Month (0-11) timeinfo->tm_mday, // Day of month timeinfo->tm_hour, // Hour timeinfo->tm_min, // Minute timeinfo->tm_sec // Second ); } else { sprintf(szBuff, "%02d:%02d:%02d", timeinfo->tm_hour, // Hour timeinfo->tm_min, // Minute timeinfo->tm_sec); } return szBuff; } inline void setTime() { // Get time from System Clock - UTC time(&now); // Get current system time in seconds (still in local time settings) status.now = (uint32_t) now; // Store UTC time for external communication // UTC time_r(&now, &timeinfo); //gmtime_r(&now, &timeinfo); //ESP_LOGI(TAG_MAIN,"Time - UTC: %2d:%02d:%02d ", timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec, g_nSecond); // local time now += config.m_nTimeOffset; gmtime_r(&now, &timeinfo); //ESP_LOGI(TAG_MAIN,"Local: %4d-%02d-%02d %2d:%02d:%02d (UTC%c%d)\n", // g_nYear, g_nMonth, g_nDay, // timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec, // config.m_nTimeOffset >= 0 ? '+' : '-', (uint16_t)(config.m_nTimeOffset / 3600)); g_nSecond = static_cast(timeinfo.tm_sec); g_nMinute = static_cast(timeinfo.tm_min); g_nHour = static_cast(timeinfo.tm_hour); g_nDay = static_cast(timeinfo.tm_mday); g_nMonth = static_cast(timeinfo.tm_mon) + 1; g_nYear = timeinfo.tm_year + 1900; } inline void setZCD() { // ZCD status.zcdAC = zcdACCount; zcdACCount = 0; status.zcdLoad = zcdLoadCount; zcdLoadCount = 0; if (status.zcdAC < 118 || status.zcdAC > 122) { status.nFlags |= FLAG_ZCD_AC; } else { status.nFlags &= ~FLAG_ZCD_AC; } if (status.zcdLoad < 118 || status.zcdLoad > 122) { status.nFlags |= FLAG_ZCD_LOAD; } else { status.nFlags &= ~FLAG_ZCD_LOAD; } }