HCesp/HermitCrab.ino
2026-04-17 11:34:49 +09:00

686 lines
23 KiB
C++

#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"
#include "hal/timer_ll.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;
volatile uint32_t g_millis = 0l;
extern timg_dev_t *tg1;
// 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;
g_millis = (uint32_t)timer_ll_get_counter_value(tg1, 1);
unsigned long tickMillis = g_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<char>(timeinfo.tm_sec);
g_nMinute = static_cast<char>(timeinfo.tm_min);
g_nHour = static_cast<char>(timeinfo.tm_hour);
g_nDay = static_cast<char>(timeinfo.tm_mday);
g_nMonth = static_cast<char>(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;
}
}