Initial commit: HermitCrab project structure

This commit is contained in:
Heuideog Yi @ PC RnD1 2026-04-03 20:40:52 +09:00
commit 3276c9527d
45 changed files with 9642 additions and 0 deletions

BIN
.gitignore vendored Normal file

Binary file not shown.

BIN
.vs/HermitCrab/v16/.suo Normal file

Binary file not shown.

Binary file not shown.

40
.vscode/c_cpp_properties.json vendored Normal file
View File

@ -0,0 +1,40 @@
{
"configurations": [
{
"name": "Arduino",
"includePath": [
"${workspaceFolder}/**",
"C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/hardware/esp32/3.0.5/libraries/**",
"C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/hardware/esp32/3.0.5/cores/esp32/**",
"C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/tools/esp32-arduino-libs/idf-release_v5.1-33fbade6/esp32/include/**",
"C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/tools/esp32-arduino-libs/idf-release_v5.1-33fbade6/esp32/include/freertos/FreeRTOS-Kernel/include/**",
"C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/tools/esp32-arduino-libs/idf-release_v5.1-33fbade6/esp32/include/freertos/FreeRTOS-Kernel/portable/xtensa/include/**",
"C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/tools/esp32-arduino-libs/idf-release_v5.1-33fbade6/esp32/include/freertos/esp_additions/include/**",
"C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/tools/esp32-arduino-libs/idf-release_v5.1-33fbade6/esp32/include/freertos/esp_additions/arch/xtensa/include/**",
"C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/tools/esp32-arduino-libs/idf-release_v5.1-33fbade6/esp32/qio_qspi/include/**",
"C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/hardware/esp32/3.0.5/variants/esp32/**",
"C:/Users/hxyi/AppData/Local/Arduino15/packages/esp8266/hardware/esp8266/3.1.2/cores/esp8266/**",
"C:/Users/hxyi/AppData/Local/Arduino15/packages/esp8266/hardware/esp8266/3.1.2/libraries/**",
"C:/Users/hxyi/AppData/Local/Arduino15/packages/esp8266/hardware/esp8266/3.1.2/libraries/ESP8266WiFi/src/**",
"C:/Users/hxyi/AppData/Local/Arduino15/packages/esp8266/hardware/esp8266/3.1.2/tools/SDK/include/**",
"D:/Projects/libraries/ESP-TuyaBLE/src/**",
"D:/Projects/libraries/NimBLE-Arduino/src/**",
"D:/Projects/libraries/NimBLE-Arduino/src/nimble/nimble/host/include/host/**"
],
"defines": [
"_DEBUG",
"UNICODE",
"_UNICODE",
"ESP32"
],
"windowsSdkVersion": "10.0.19041.0",
"compilerPath": "C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/tools/esp-x32/2302/bin/xtensa-esp32-elf-gcc.exe",
"cStandard": "c11",
"cppStandard": "c++17",
"intelliSenseMode": "gcc-x64"
}
],
"version": 4
}

69
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,69 @@
{
"files.associations": {
"\"*.ino\"": "\"cpp\"",
"array": "cpp",
"atomic": "cpp",
"bit": "cpp",
"*.tcc": "cpp",
"cctype": "cpp",
"chrono": "cpp",
"clocale": "cpp",
"cmath": "cpp",
"compare": "cpp",
"concepts": "cpp",
"cstdarg": "cpp",
"cstddef": "cpp",
"cstdint": "cpp",
"cstdio": "cpp",
"cstdlib": "cpp",
"cstring": "cpp",
"ctime": "cpp",
"cwchar": "cpp",
"cwctype": "cpp",
"deque": "cpp",
"list": "cpp",
"map": "cpp",
"set": "cpp",
"string": "cpp",
"unordered_map": "cpp",
"unordered_set": "cpp",
"vector": "cpp",
"exception": "cpp",
"algorithm": "cpp",
"functional": "cpp",
"iterator": "cpp",
"memory": "cpp",
"memory_resource": "cpp",
"netfwd": "cpp",
"numeric": "cpp",
"optional": "cpp",
"random": "cpp",
"ratio": "cpp",
"string_view": "cpp",
"system_error": "cpp",
"tuple": "cpp",
"type_traits": "cpp",
"utility": "cpp",
"initializer_list": "cpp",
"iomanip": "cpp",
"iosfwd": "cpp",
"iostream": "cpp",
"istream": "cpp",
"limits": "cpp",
"new": "cpp",
"numbers": "cpp",
"ostream": "cpp",
"span": "cpp",
"sstream": "cpp",
"stdexcept": "cpp",
"streambuf": "cpp",
"cinttypes": "cpp",
"typeinfo": "cpp",
"variant": "cpp"
},
"workbench.colorCustomizations": {
"editor.background": "#0f0f0f" // You can change this hex value to any darker shade you prefer
}
}

207
AHT2x.cpp Normal file
View File

@ -0,0 +1,207 @@
#include "AHT2x.h"
#define AHT_STATUS_BUSY 0x01
#define AHT_STATUS_CALIBRATED 0x10
#define AHT_CMD_INIT 0xBE
#define AHT_CMD_TRIGGER 0xAC
#define AHT_CMD_RESET 0xBA
#define AHT_CRC_POLYNOMIAL 0x31
#define AHT_CRC_MSB 0x80
#define AHT_CRC_INIT 0xFF
#ifndef DPRINTF
#define DPRINTF(...)
#endif
AHT2x aht25(Wire);
AHT2x aht10_0x39(Wire);
const uint8_t cmd_init[3] = { 0xBE, 0x08, 0x00 };
AHT2x::AHT2x(TwoWire &i2c) : _i2c(i2c) {};
bool AHT2x::setup(uint8_t address, bool crc) {
m_nAddress = address;
_active_crc = crc;
m_nErrorCount = 0;
m_nTemp = -9999;
m_nRH = -9999;
bRequested = false;
m_bSensor = false;
m_bScan = false;
tickRequested = millis();
delay(40);
if (scan()) {
while (!isCalibrated()) {
calibrate();
}
requestMeasurement(tickRequested);
delay(82);
humid[3] = -9999;
temp[3] = -9999;
if (readSensor(tickRequested + 82)) {
for (int i = 0; i < 3; i++) {
humid[i] = humid[3];
temp[i] = temp[3];
}
}
return true;
}
return false;
}
bool AHT2x::scan() {
//DPRINTF("AHTx0 - Scanning AHTx0 Device at %X\n", m_nAddress);
int i;
for (i = 0; i < 5; i++) {
Wire.beginTransmission(m_nAddress);
if (Wire.endTransmission() == 0) {
DPRINTF("AHTx0 - FOUND I2C Device at %X\n", m_nAddress);
bool ret = false;
//sendCommand(m_nAddress, 0xBE, 0x08, 0x00)) { // AHT20 init command
uint8_t cmd[3] = { 0xBE, 0x08, 0x00 };
_i2c.beginTransmission(m_nAddress);
_i2c.write((uint8_t *)cmd_init, 3);
if (_i2c.endTransmission() == 0) {
m_bSensor = true;
DPRINTF("AHT2x - Found AHT2x at %X\n", m_nAddress);
break;
}
}
delay(2);
}
if (i == 5) {
DPRINTF("AHTx0 - I2C Transmission Error at %X\n", m_nAddress);
}
return m_bSensor;
}
bool AHT2x::isReady() {
if (status() & AHT_STATUS_BUSY) {
return false;
}
return measure();
}
bool AHT2x::isCalibrated() {
return status() & AHT_STATUS_CALIBRATED;
}
void AHT2x::reset() {
uint8_t cmd = AHT_CMD_RESET;
_i2c.beginTransmission(m_nAddress);
_i2c.write(cmd);
_i2c.endTransmission();
delay(20);
while (!isCalibrated()) {
calibrate();
}
}
uint8_t AHT2x::status() {
_i2c.requestFrom(m_nAddress, (uint8_t)6);
for (int i = 0; i < 6; i++) {
_buf[i] = _i2c.read();
}
if (!_active_crc || (crc8() == _buf[6])) {
return _buf[0];
}
return AHT_STATUS_BUSY;
}
void AHT2x::calibrate() {
uint8_t cmd[] = {AHT_CMD_INIT, 0x08, 0x00};
_i2c.beginTransmission(m_nAddress);
_i2c.write(cmd, 3);
_i2c.endTransmission();
delay(10);
}
void AHT2x::requestMeasurement(unsigned long tick) {
uint8_t cmd[] = {AHT_CMD_TRIGGER, 0x33, 0x00};
_i2c.beginTransmission(m_nAddress);
_i2c.write(cmd, 3);
_i2c.endTransmission();
tickRequested = tick;
bRequested = true;
}
bool AHT2x::readSensor(unsigned long tick) {
if (m_bScan) {
// Re-Scan sensor for any hardware change or failure
scan();
m_bScan = false;
}
bool ret = false;
if (bRequested) {
if (tick - tickRequested > 80) {
_i2c.requestFrom(m_nAddress, (uint8_t)6);
ret = true;
for (int i = 0; i < 6; i++) {
if (Wire.available()) {
_buf[i] = _i2c.read();
} else {
ret = false; // If data is unavailable, return false
break;
}
}
if (ret && (!_active_crc || (crc8() == _buf[6]))) {
humid[0] = humid[1];
humid[1] = humid[2];
humid[2] = humid[3];
uint32_t hum32 = (_buf[1] << 12) | (_buf[2] << 4) | (_buf[3] >> 4);
humid[3] = (int16_t) roundf(hum32 * 10000.0f / 0x100000);
if (m_nRH > 2000 && m_nRH <= 10000) {
if (humid[3] - m_nRH < -1500 || humid[3] - m_nRH> 1500)
humid[3] = m_nRH;
}
m_nRH = (int16_t)((humid[0] + humid[1] + humid[2] + humid[3]) / 4);
temp[0] = temp[1];
temp[1] = temp[2];
temp[2] = temp[3];
uint32_t temp32 = ((_buf[3] & 0xF) << 16) | (_buf[4] << 8) | _buf[5];
temp[3] = (int16_t) roundf(temp32 * 20000.0f / 0x100000 - 5000);
if (m_nTemp > 2000 && m_nTemp <= 3000) {
if (temp[3] - m_nTemp < -1000 || temp[3] - m_nTemp> 1000)
temp[3] = m_nTemp;
}
m_nTemp = (int16_t)((temp[0] + temp[1] + temp[2] + temp[3]) / 4);
m_nErrorCount = 0;
ret = true;
} else {
if (++m_nErrorCount > 4) {
m_nTemp = -9999;
m_nRH = -9999;
m_nErrorCount = 0;
}
}
} else {
// tick - tickRequested <= 80
// do nothing;
return false;
}
}
requestMeasurement(tick);
return ret;
}
uint8_t AHT2x::crc8() {
uint8_t crc = AHT_CRC_INIT;
for (int i = 0; i < 6; i++) {
crc ^= _buf[i];
for (int j = 0; j < 8; j++) {
if (crc & AHT_CRC_MSB) {
crc = (crc << 1) ^ AHT_CRC_POLYNOMIAL;
} else {
crc <<= 1;
}
}
}
return crc;
}

43
AHT2x.h Normal file
View File

@ -0,0 +1,43 @@
#ifndef __AHT2x_H
#define __AHT2x_H
#include <Wire.h>
#define AHT_I2C_ADDR 0x38
class AHT2x {
public:
AHT2x(TwoWire &i2c);
bool setup(uint8_t address = AHT_I2C_ADDR, bool crc = false);
bool scan();
bool isReady();
bool isCalibrated();
void reset();
bool readSensor(unsigned long tick);
inline int16_t getHumidity() const { return m_nRH; }
inline int16_t getTemperature() const { return m_nTemp; }
inline void setScanFlag(bool bScan) { m_bScan = bScan; }
inline bool sensor() { return m_bSensor; }
private:
TwoWire &_i2c;
uint8_t m_nAddress;
bool _active_crc;
uint8_t _buf[7];
int16_t m_nRH, m_nTemp;
int16_t humid[4], temp[4];
uint8_t status();
void calibrate();
bool measure();
uint8_t crc8();
void requestMeasurement(unsigned long tick);
bool bRequested;
bool m_bSensor;
bool m_bScan;
unsigned long tickRequested;
uint8_t m_nErrorCount;
};
extern class AHT2x aht25, aht10_0x39;
#endif

433
BLEScan.cpp Normal file
View File

@ -0,0 +1,433 @@
#include <NimBLEDevice.h>
#include <BLEScan.h>
#include <WiFi.h>
#include "Config.h"
#include "HermitCrab.h"
#include "ConnectWiFi.h"
CBLEScan ble;
std::string serviceData;
char BLE_SSID[32];
char BLE_PW[32];
#define TAG_BLE "BLE_SCAN"
#define ADVERTISE_INTERVAL_MS 5000 // Expected advertisement interval
#define SCAN_SHORT_MS 300 // Short scan time
#define SCAN_LONG_MS 12000 // Longer scan if missed
// Xiaomi - The remote service and characteristic UUIDs
static BLEUUID serviceUUID("ebe0ccb0-7a0a-4b0c-8a1a-6ff2997da3a6");
static BLEUUID charUUID("ebe0ccc1-7a0a-4b0c-8a1a-6ff2997da3a6");
//#define TUYA_BLE_NAME "THB1-xxxxxx" // sensor name
//#define SENSOR_BLE_NAME
//#define SENSOR_BLE_MAC "38:1F:8D:77:73:34" // Tuya sensor MAC address
#define WIFI_BLE_NAME "HC_" // Name of PC/Android BLE advertiser
// UUIDs for environmental sensing and characteristics
#define ENV_SENSING_UUID "181A" // Environmental Sensing UUID
#define TEMP_UUID "2A6E" // Temperature UUID
#define HUMIDITY_UUID "2A6F" // Humidity UUID
// Tuya sensor advertisement data structure (in network order)
typedef struct __attribute__((packed)) _bthome_t {
//uint8_t flag[3]; // Advertise type flags
uint8_t info; // = 0x40 BtHomeID_Info (not encrypted)
uint8_t p_id; // = BtHomeID_PacketId
uint8_t pid; // PacketId (measurement count)
uint8_t b_id; // = BtHomeID_battery
uint8_t battery_level; // 0..100 %
uint8_t t_id; // = BtHomeID_temperature
int16_t temp; // x 0.001 degree
uint8_t h_id; // = BtHomeID_humidity
uint16_t humid; // x 0.01 %
uint8_t v_id; // = BtHomeID_voltage
uint16_t battery_mv; // x 0.001 V
} bthome_t;
// Callback when a device is found during scan
class HCScanCallbacks : public NimBLEScanCallbacks {
/**
* @brief Called when a new device is discovered, before the scan result is received (if applicable).
* @param [in] advertisedDevice The device which was discovered.
*/
void onDiscovered(const NimBLEAdvertisedDevice* advertisedDevice) override {
static uint8_t flag = 0;
static char ssid[32];
static char pw[32];
if (isWiFiConnected()) return;
// Retrieve manufacturer data (SSID or password)
const std::string& manufacturerData = advertisedDevice->getManufacturerData();
// Check for specific manufacturer data signatures (SIGNATURE1 for SSID, SIGNATURE2 for password)
uint16_t manufacturerID = * (uint16_t *) &manufacturerData[0];
if (manufacturerID == SIGNATURE2 - 1) { // For SSID (SIGNATURE1)
strncpy(ssid, (char *) &manufacturerData[2], std::min(sizeof(ssid) - 1, manufacturerData.length() - 1));
ssid[sizeof(ssid) - 1] = '\0'; // Ensure null termination
flag |= 1;
//DPRINTF("BLE Scan - SSID(\"%s\")\n", (char *) &manufacturerData[2]);
} else if (manufacturerID == SIGNATURE2) { // For password (SIGNATURE2)
strncpy(pw, (char *)&manufacturerData[2], std::min(sizeof(pw) - 1, manufacturerData.length() - 1));
pw[sizeof(pw) - 1] = '\0'; // Ensure null termination
flag |= 2;
//DPRINTF("BLE Scan - PW(\"%s\")\n", (char *) &manufacturerData[2]);
} else {
flag = 0;
}
if (flag == 3) {
strncpy(BLE_SSID, ssid, sizeof(BLE_SSID) - 1);
BLE_SSID[sizeof(BLE_SSID) - 1] = 0;
strncpy(BLE_PW, pw, sizeof(BLE_PW) - 1);
BLE_PW[sizeof(BLE_PW) - 1] = 0;
flag = 0;
}
};
/**
* @brief Called when a new scan result is complete, including scan response data (if applicable).
* @param [in] advertisedDevice The device for which the complete result is available.
*/
void onResult(const NimBLEAdvertisedDevice* advertisedDevice) override {
uint16_t len;
bthome_t *pTuyaData = (bthome_t *) advertisedDevice->getServiceData(&len);
// Tuya
if (len == sizeof(bthome_t) && pTuyaData->info == 0x40) {
if (config.nTemp1SensorType == TEMP_SENSOR_TYPE::BLE_TUYA) {
// Look for the custom Tuya sensor data structure
ble.setData(pTuyaData->temp, pTuyaData->humid, pTuyaData->battery_level);
} else if (config.nTemp2SensorType == TEMP_SENSOR_TYPE::BLE_TUYA) {
// Look for the custom Tuya sensor data structure
ble.setData2(pTuyaData->temp, pTuyaData->humid, pTuyaData->battery_level);
}
} else
// InkBird
if (config.nTemp1SensorType == TEMP_SENSOR_TYPE::BLE_INKBIRD ||
config.nTemp2SensorType == TEMP_SENSOR_TYPE::BLE_INKBIRD) {
// Process Inkbird Sensors
std::string manufacturerData = advertisedDevice->getManufacturerData();
if (manufacturerData.length() >= 7) {
const uint8_t* data = reinterpret_cast<const uint8_t*>(manufacturerData.data());
// Inkbird data format: [unknown][unknown][temp_LSB][temp_MSB][humidity][voltage_LSB][voltage_MSB]
int16_t temp = (data[2] | (data[3] << 8)); // Little-endian, scale by 0.01
uint16_t humi = data[4]; // Humidity byte (0-100%)
uint16_t batteryLevel = map(data[5] | (data[6] << 8), 2300, 3200, 0, 100); // Convert voltage to battery %
if (config.nTemp1SensorType == TEMP_SENSOR_TYPE::BLE_INKBIRD) {
ble.setData(temp, humi, batteryLevel);
} else if (config.nTemp2SensorType == TEMP_SENSOR_TYPE::BLE_INKBIRD) {
ble.setData2(temp, humi, batteryLevel);
}
}
}
};
/**
* @brief Called when a scan operation ends.
* @param [in] scanResults The results of the scan that ended.
* @param [in] reason The reason code for why the scan ended.
*/
void onScanEnd(const NimBLEScanResults& scanResults, int reason) override {};
} hcScanCallbacks;
// Connection based Notification from Xiaomi devices
static void notifyCallback(
BLERemoteCharacteristic* pBLERemoteCharacteristic,
uint8_t* pData,
size_t length,
bool isNotify)
{
uint16_t voltage;
if (config.nTemp1SensorType == TEMP_SENSOR_TYPE::BLE_XIAOMI_MIJIA) {
voltage = pData[3] | (pData[4] << 8); // little endian
ble.setData(pData[0] | (pData[1] << 8), pData[2] * 100, map(voltage, 2300, 3200, 0, 100));
} else if (config.nTemp2SensorType == TEMP_SENSOR_TYPE::BLE_XIAOMI_MIJIA) {
voltage = pData[3] | (pData[4] << 8); // little endian
ble.setData2(pData[0] | (pData[1] << 8), pData[2] * 100, map(voltage, 2300, 3200, 0, 100));
}
}
// Function to setup BLE
void CBLEScan::setupConnect(uint64_t addr, uint64_t addr2) {
m_nAddr = addr;
m_nAddr2 = addr2;
m_nLatestBLEAdvertise =
m_nLatestBLEAdvertise2 = millis();
m_nLastReceive =
m_nLastReceive2 = m_nLatestBLEAdvertise;
m_bDataReceived =
m_bDataReceived2 = false;
NimBLEDevice::init("");
status.nFlags &= ~FLAG_BLE_BATT;
status.nFlags &= ~FLAG_BLE_NODATA;
status.nFlags &= ~FLAG_BLE_LOST;
m_nTemp = -9999;
m_nHumid = -9999;
m_nBatteryLevel = 0;
m_nTemp2 = -9999;
m_nHumid2 = -9999;
m_nBatteryLevel2 = 0;
m_bConnected = false;
m_bConnectionInProgress = false;
pClient = nullptr;
if ( config.nTemp1SensorType == TEMP_SENSOR_TYPE::BLE_XIAOMI_MIJIA) {
connect(NimBLEAddress(m_nAddr, BLE_ADDR_PUBLIC), false);
} else if ( config.nTemp2SensorType == TEMP_SENSOR_TYPE::BLE_XIAOMI_MIJIA) {
connect(NimBLEAddress(m_nAddr2, BLE_ADDR_PUBLIC), false);
}
}
void CBLEScan::setupScan() {
NimBLEAddress cTargetAddress;
if (config.nBLEScanInterval == 143 || config.nBLEScanInterval == 77 ||
config.nBLEScanInterval == 139 || config.nBLEScanInterval == 91)
m_nInterval = config.nBLEScanInterval;
else
m_nInterval = 143;
serviceData.reserve(256);
NimBLEScan* pBLEScan = NimBLEDevice::getScan();
pBLEScan->setScanCallbacks((NimBLEScanCallbacks *) &hcScanCallbacks, true);
pBLEScan->setActiveScan(false);
pBLEScan->setDuplicateFilter(false);
pBLEScan->setMaxResults(0);
m_bContinuousScan = true;
m_bScanStarted = false;
if (config.nTemp1SensorType == TEMP_SENSOR_TYPE::BLE_TUYA ||
config.nTemp1SensorType == TEMP_SENSOR_TYPE::BLE_INKBIRD) {
cTargetAddress = NimBLEAddress(m_nAddr, BLE_ADDR_PUBLIC);
pBLEScan->setTargetAddress(cTargetAddress);
startScan();
} else if (config.nTemp2SensorType == TEMP_SENSOR_TYPE::BLE_TUYA ||
config.nTemp2SensorType == TEMP_SENSOR_TYPE::BLE_INKBIRD) {
cTargetAddress = NimBLEAddress(m_nAddr2, BLE_ADDR_PUBLIC);
pBLEScan->setTargetAddress(cTargetAddress);
startScan();
} else if (!isWiFiConnected()) {
startScan();
}
}
void CBLEScan::startScan() {
DPRINTF("Starting BLE scan... %d(%d)\n", m_nInterval, 5000 % m_nInterval);
NimBLEScan* pBLEScan = NimBLEDevice::getScan();
pBLEScan->setInterval(m_nInterval); // Interval between scan windows in ms
pBLEScan->setWindow(m_nInterval - 3); // Length of time the scanner listens in ms
if (m_bContinuousScan) {
// 85(15), 77(5), 82(2), 61(2) 122(2), 139(4), 91(5), 143(5), 135(-5)
pBLEScan->start(0, false, false); // Continuous scan
} else {
pBLEScan->start(12000, false, true); // Continuous scan
}
m_bScanStarted = true;
}
bool CBLEScan::connect(NimBLEAddress pAddress, bool bAsync) {
DPRINTLN("BLE Connect: Connecting...");
if (pClient == nullptr) {
pClient = NimBLEDevice::createClient();
pClient->setConnectTimeout(30000);
}
m_bConnectionInProgress = false;
bool ret = pClient->connect(pAddress, true, bAsync, true);
if (bAsync) {
m_bConnectionInProgress = true;
m_bConnected = false;
} else {
m_bConnected = ret;
}
if (!bAsync && m_bConnected) {
NimBLERemoteService* pRemoteService = pClient->getService(serviceUUID);
if (!pRemoteService) {
DPRINTLN("BLE Connect: Service not found");
return false;
}
pRemoteCharacteristic = pRemoteService->getCharacteristic(charUUID);
if (!pRemoteCharacteristic) {
DPRINTLN("BLE Connect: Characteristic not found");
return false;
}
pRemoteCharacteristic->subscribe(true, notifyCallback);
DPRINTF("BLE Connect: Connected to %s\n", pAddress.toString().c_str());
}
return m_bConnected;
}
void CBLEScan::loop(unsigned long clock) {
unsigned long gap;
if (config.nTemp1SensorType == TEMP_SENSOR_TYPE::BLE_XIAOMI_MIJIA ||
config.nTemp2SensorType == TEMP_SENSOR_TYPE::BLE_XIAOMI_MIJIA) {
if (!m_bConnected) {
if (pClient->isConnected()) {
m_bConnectionInProgress = false;
NimBLERemoteService* pRemoteService = pClient->getService(serviceUUID);
if (!pRemoteService) {
DPRINTLN("BLE Connect: Service not found");
return;
}
pRemoteCharacteristic = pRemoteService->getCharacteristic(charUUID);
if (!pRemoteCharacteristic) {
DPRINTLN("BLE Connect: Characteristic not found");
return;
}
pRemoteCharacteristic->subscribe(true, notifyCallback);
DPRINTLN("BLE Connect: Connected within the Loop");
m_bConnected = true;
} else if (!m_bConnectionInProgress) {
if ( config.nTemp1SensorType == TEMP_SENSOR_TYPE::BLE_XIAOMI_MIJIA) {
connect(NimBLEAddress(m_nAddr, BLE_ADDR_PUBLIC), true);
} else if ( config.nTemp2SensorType == TEMP_SENSOR_TYPE::BLE_XIAOMI_MIJIA) {
connect(NimBLEAddress(m_nAddr2, BLE_ADDR_PUBLIC), true);
}
}
}
} else {
if (m_bConnected) {
pClient->disconnect();
m_bConnected = false;
m_bConnectionInProgress = false;
}
}
if (config.nTemp1SensorType == TEMP_SENSOR_TYPE::BLE_TUYA ||
config.nTemp1SensorType == TEMP_SENSOR_TYPE::BLE_INKBIRD) {
if (m_bScanStarted) {
gap = clock - m_nLatestBLEAdvertise;
if (gap < 6050) {
status.nFlags &= ~FLAG_BLE_NODATA;
status.nFlags &= ~FLAG_BLE_LOST;
}
else if (gap < 8150) {
status.nFlags |= FLAG_BLE_NODATA;
status.nFlags &= ~FLAG_BLE_LOST;
if (config.bBLETest) m_nHumid = 0;
}
else if (gap > 60100 && gap < 63200) {
//DPRINTF("GAP Reached... %d", gap);
status.nFlags |= (FLAG_BLE_NODATA | FLAG_BLE_LOST);
m_nTemp = -9999;
m_nHumid = -9999;
m_nBatteryLevel = 0;
}
} else {
startScan();
}
} else if (config.nTemp2SensorType == TEMP_SENSOR_TYPE::BLE_TUYA ||
config.nTemp2SensorType == TEMP_SENSOR_TYPE::BLE_INKBIRD) {
if (m_bScanStarted) {
gap = clock - m_nLatestBLEAdvertise2;
if (gap > 6050 && gap < 8150) {
if (config.bBLETest) m_nHumid2 = 0;
}
else if (gap > 60100 && gap < 63200) {
m_nTemp2 = -9999;
m_nHumid2 = -9999;
m_nBatteryLevel2 = 0;
}
} else {
startScan();
}
}
#ifdef BLE_DEBUG
{
static int col = 0;
static int hit = 0;
static int miss = 0;
static int hit_total = 0;
static int miss_total = 0;
if (m_bDataReceived) {
gap = m_nLatestBLEAdvertise - m_nLastReceive;
int cnt = (gap + 2500)/5000 - 1;
for (int i = 0; i < cnt; i++)
{
Serial.print(cnt);
miss++;
if (++col >=60) {
hit_total += hit;
miss_total += miss;
DPRINTF(" Hit: %2d, Miss: %2d Total(Hit: %3d, Miss: %3d, Ratio %.1f%%) Mem: %d\n",
hit, miss, hit_total, miss_total, (float)hit_total * 100.0f / (hit_total + miss_total),
ESP.getFreeHeap());
col = 0;
hit = 0;
miss = 0;
}
}
Serial.print('.');
hit++;
if (++col >=60) {
hit_total += hit;
miss_total += miss;
DPRINTF(" Hit: %d, Miss: %d Total(Hit: %d, Miss: %d, Ratio %.1f%%) Mem: %d\n",
hit, miss, hit_total, miss_total, (float)hit_total * 100.0f / (hit_total + miss_total),
ESP.getFreeHeap());
col = 0;
hit = 0;
miss = 0;
}
m_bDataReceived = false;
status.nFlags &= ~FLAG_BLE_NODATA;
}
/*
// Interval Scan
// {
// NimBLEScan* pScan = NimBLEDevice::getScan();
// if (g_bBLEDataReceived) {
// Serial.printf("BLE(%d) - Temp: %.2f°C Hum: %.2f%% Batt: %d Mem: %d (Stop)\n",
// tickMillis - lastReceived,
// g_nBLETemp / 100.0f,
// g_nBLEHumid / 100.0f,
// g_nBLEBatt,
// ESP.getFreeHeap() );
// status.nFlags &= ~FLAG_BLE_NODATA;
// g_bBLEDataReceived = false;
// if (pScan->isScanning()) {
// pScan->stop();
// pScan->clearResults();
// }
// lastErrorMessage = tickMillis;
// lastReceived = lastTickMillis;
// } else {
// unsigned long gap = tickMillis - g_lastBLEAdvertise;
// if (!pScan->isScanning()) {
// if (gap > 4910 && gap < 4960) {
// pScan->start(200, false, true);
// Serial.printf("Starting Scan for 200ms at %d\n", tickMillis - g_lastBLEAdvertise);
// } else if (gap > 9500) {
// pScan->start(5200, false, true);
// Serial.printf("Starting Scan for 5200ms at %d\n", tickMillis - g_lastBLEAdvertise);
// }
// }
// if (gap > 10050 && tickMillis - lastErrorMessage > 10050) {
// Serial.printf("No BLE Data for % seconds\n", tickMillis - lastErrorMessage);
// status.nFlags |= FLAG_BLE_NODATA;
// lastErrorMessage = tickMillis;
// }
// }
// }
*/
}
#endif
}

72
BLEScan.h Normal file
View File

@ -0,0 +1,72 @@
#ifndef __BLE_SCAN_H
#define __BLE_SCAN_H
class NimBLEClient;
class NimBLERemoteCharacteristic;
class NimBLEAddress;
class CBLEScan {
public:
void setupConnect(uint64_t addr, uint64_t addr2);
void setupScan();
void loop(unsigned long clock);
void startScan();
bool connect(NimBLEAddress addr, bool bAsync);
inline int16_t getTemp() { return m_nTemp; };
inline int16_t getHumid() { return m_nHumid; };
inline uint8_t getBatteyLevel() { return m_nBatteryLevel; };
inline int16_t getTemp2() { return m_nTemp2; };
inline int16_t getHumid2() { return m_nHumid2; };
inline uint8_t getBatteyLevel2() { return m_nBatteryLevel2; };
inline void setData(int16_t temp, uint16_t humid, uint8_t bLevel) {
m_nTemp = temp;
m_nHumid = humid;
m_nBatteryLevel = bLevel;
m_bDataReceived = true;
m_nLastReceive = m_nLatestBLEAdvertise;
m_nLatestBLEAdvertise = millis();
}
inline void setData2(int16_t temp, uint16_t humid, uint8_t bLevel) {
m_nTemp2 = temp;
m_nHumid2 = humid;
m_nBatteryLevel2 = bLevel;
m_bDataReceived2 = true;
m_nLastReceive2 = m_nLatestBLEAdvertise2;
m_nLatestBLEAdvertise2 = millis();
}
inline void setScanType(bool bContinuous) { m_bContinuousScan = bContinuous; };
private:
bool m_bContinuousScan;
bool m_bScanStarted;
bool m_bConnected;
bool m_bConnectionInProgress;
int16_t m_nInterval;
uint64_t m_nAddr, m_nAddr2;
int16_t m_nTemp;
int16_t m_nHumid;
uint8_t m_nBatteryLevel;
int16_t m_nTemp2;
int16_t m_nHumid2;
uint8_t m_nBatteryLevel2;
unsigned long m_nLatestBLEAdvertise;
unsigned long m_nLastReceive;
bool m_bDataReceived;
unsigned long m_nLatestBLEAdvertise2;
unsigned long m_nLastReceive2;
bool m_bDataReceived2;
NimBLEClient *pClient;
NimBLERemoteCharacteristic *pRemoteCharacteristic;
};
extern CBLEScan ble;
#endif

1346
BearSSL.h Normal file

File diff suppressed because it is too large Load Diff

154
CommSerial.cpp Normal file
View File

@ -0,0 +1,154 @@
#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++;
}
}
}

158
Config.cpp Normal file
View File

@ -0,0 +1,158 @@
#include "HermitCrab.h"
#include "AHT2x.h"
#include "Config.h"
#include "TimeManager.h"
#include <Preferences.h>
#ifndef SIGNATURE1
#define SIGNATURE1 ((uint16_t) 0xC8AB)
#endif
#ifndef SIGNATURE2
#define SIGNATURE2 ((uint16_t) 0x4E81)
#endif
Preferences preferences;
CONFIG_TYPE config;
// Function to initialize default values for the config structure
void CONFIG_TYPE::init() {
m_nSignature1 = SIGNATURE1;
m_nSignature2 = SIGNATURE2;
// Block 1 - Control and Environment
bSmartControl = false;
bNightControl = false;
bControlTemperature = true;
bControlHumidity = true;
bEnableIO = true;
bAC2_OnOff = false;
nNightStartHour = 18;
nNightStartMin = 0;
nNightEndHour = 6;
nNightEndMin = 0;
// Block 2 - Sensor and Temperature/Humidity Settings
nTempTarget = 260;
nTempTargetNight = 260;
nTemp1Offset = 0;
nTemp2Offset = 0;
nTemp3Offset = 0;
nHumidTarget = 880;
nHumidTargetNight = 880;
nHumid1Offset = 0;
nHumid2Offset = 0;
nTemp1SensorType = TEMP_SENSOR_TYPE::BLE_TUYA;
nTemp2SensorType = TEMP_SENSOR_TYPE::AHT20;
Kp_Temp1 = 3.0f; // Load Kp for Temperature control
Kd_Temp1 = 750.0f; // Load Kd for Temperature control
LR_Temp1 = 0.05f; // Load learning rate for Temperature
Kp_Humidity = 2.0f; // Load Kp for Humidity control
Kd_Humidity = 150.0f; // Load Kd for Humidity control
LR_Humidity = 0.08f; // Load learning rate for Humidity
// Block 3 - AC1 and AC2
// Day/Night Min/Max/Start On/Off THigh/Low Time B/E Period Hum H/L
config.ac1 = {CONTROL_TEMP_HEAT_PID, 0, 1,1, 0, 1000, 0, 250,0, 920,880, 0,24*60-1, 60, 5, 920,880 };
config.ac2 = {CONTROL_TEMP_HEAT, 0, 0,0, 0, 1000, 0, 250,0, 920,880, 0,25*60-1, 60, 5, 920,880 };
// Block 4 = Mist and Fan
// Day/Night Min/Max/Start On/Off THigh/Low Time B/E Period Hum H/L
config.mist = {CONTROL_HUMIDITY_INC_PID, 0, 1,1, 0, 1000, 0, 250,0, 920,880, 0,25*60-1, 60, 5, 920,880 };
config.fan = {CONTROL_TEMP_COOL, 0, 1,1, 0, 1000, 0, 250,0, 920,880, 0,25*60-1, 60, 5, 920,880 };
// Block 5 - Motor and Light
// Day/Night Min/Max/Start On/Off THigh/Low Time B/E Period Hum H/L
config.motor = {CONTROL_PERIOD, 0, 1,1, 0, 1000, 0, 250,0, 920,880, 0,25*60-1, 60, 5, 920,880 };
config.light = {CONTROL_DAY_NIGHT, 0, 1,1, 0, 1000, 0, 250,0, 920,880, 0,25*60-1, 60, 5, 920,880 };
// Block 6 - Environment and Operations
bSendStatusSerial = false;
bConfigSaved = false;
bStatusSaved = false;
//
// Reserved
//
// TBD
for (int i = 0; i < 17; i = i + 8) {
m_nChipId |= ((ESP.getEfuseMac() >> (40 - i)) & 0xff) << i;
}
m_nPublicPort = (uint16_t)(m_nChipId & 0xFFFF);
m_nDeviceType = THIS_DEVICE_TYPE;
//
// WiFi Client Only
//
m_nDisplayTime = 1800;
m_nDisplayTempHigh = 40;
m_nDisplayTempLow = 20;
m_nTempHigh = 20;
m_fShowRealTime = 0x000F;
// Names
strcpy(m_sDeviceName, "Beta X");
strcpy(m_sMake, "VisionSoft");
strcpy(m_sModel, "HermitCrab");
strncpy(m_sVersion, HC__VERSION, 11);
// WiFi - SSID and Password
strcpy(ssid, "RECALL");
strcpy(pw, "BBBB9999");
ESP_LOGI(TAG,"Config Initialized");
}
// Function to load the configuration block from preferences
bool CONFIG_TYPE::load() {
preferences.begin("HermitCrab", false); // Open preferences in read-write mode
// Check if config has been saved previously
size_t len = preferences.getBytesLength("config_data");
if (len != sizeof(config)) {
ESP_LOGI(TAG,"\nPreferences - First Time");
// First time, initialize default config
ESP_LOGI(TAG, "Config Size Error: %d out of %d\n", len, sizeof(config));
init();
save(); // Save defaults
preferences.end();
return true; // Indicates that it was initialized
}
// Load the structure as a block of data
preferences.getBytes("config_data", &config, sizeof(config));
if (m_nSignature1 != SIGNATURE1 ||
m_nSignature2 != SIGNATURE2) {
ESP_LOGI(TAG, "Config Load: Signature Mismatch %X %X - %X %X\n",
m_nSignature1, m_nSignature2,
SIGNATURE1, SIGNATURE2);
}
preferences.end();
ESP_LOGI(TAG,"Config Loaded");
bConfigSaved = false;
bStatusSaved = false;
for (int i = 0; i < 17; i = i + 8) {
m_nChipId |= ((ESP.getEfuseMac() >> (40 - i)) & 0xff) << i;
}
strncpy(m_sVersion, HC__VERSION, 11);
m_sVersion[11] = 0;
m_nDeviceType = THIS_DEVICE_TYPE;
return true; // Config loaded successfully
}
// Function to save the entire configuration block to preferences
void CONFIG_TYPE::save() {
// Update before save
config.statusSave = status;
config.bStatusSaved = true;
// Save the entire config structure as one block
preferences.begin("HermitCrab", false); // Open preferences in read-write mode
preferences.putBytes("config_data", &config, sizeof(config));
preferences.end(); // Close preferences
bConfigSaved = true;
ESP_LOGI(TAG,"Config Saved");
}

197
Config.h Normal file
View File

@ -0,0 +1,197 @@
# ifndef __CONFIG_H
# define __CONFIG_H
#include <Preferences.h>
#include "HermitCrab.h"
extern const char *strDeviceType[];
enum ENUM_DEVICE_TYPE {
TYPE_NONE = 0,
TYPE_TEST,
TYPE_ESP8266,
TYPE_ON_OFF,
TYPE_ZCD,
TYPE_CAM,
TYPE_BETA,
TYPE_BETA_BLE,
TYPE_DEVICE_END,
TYPE_CLIENT = 1000,
TYPE_CLIENT_WIN,
TYPE_CLIENT_ANDROID
};
enum HEATER_TYPE {
TYPE_BULB,
TYPE_IR,
TYPE_CERAMIC,
TYPE_ZCD3,
TYPE_ZCD4,
TYPE_ZCD5,
TYPE_ZCD6,
TYPE_ZCD_CONTROL,
TYPE_FLUORESCENT = 16,
TYPE_LED,
TYPE_UAV,
TYPE_HEATER_END
};
enum DEVICE_CONTROL_TYPE {
CONTROL_NONE,
CONTROL_TEMP_HEAT,
CONTROL_TEMP_COOL,
CONTROL_HUMIDITY_INC,
CONTROL_HUMIDITY_DEC,
CONTROL_DAY_NIGHT,
CONTROL_TIME,
CONTROL_PERIOD,
CONTROL_TEMP_HEAT_PID,
CONTROL_TEMP_COOL_PID,
CONTROL_HUMIDITY_INC_PID,
CONTROL_HUMIDITY_DEC_PID
};
// Enumeration for the sensor
enum TEMP_SENSOR_TYPE {
SENSOR_NONE = 0,
AHT20, // AHT20 on 0x38 - default
AHT2x, // AHT25 on 0x38 - default
AHT10_0x39, // AHT10 on 0x39 - altanative
NTC,
BLE_TUYA,
BLE_XIAOMI_MIJIA,
BLE_INKBIRD
};
#pragma pack(push) /* push current alignment to stack */
#pragma pack(1) /* set alignment to 1 byte boundary */
typedef struct CONFIG_STRUCT {
uint16_t m_nSignature1;
// Block 1 - Control and EnvironMent
// Offset 2
bool bSmartControl;
bool bNightControl;
bool bControlTemperature;
bool bControlHumidity;
bool bEnableIO;
bool bCheckAC;
bool bAC2_OnOff;
bool bdummy;
uint8_t nNightStartHour, nNightStartMin, nNightEndHour, nNightEndMin;
float Kp_Temp2; // Load Kp for Temperature control
float Kd_Temp2; // Load Kd for Temperature control
float LR_Temp2; // Load learning rate for Temperature
float Kp_Temp3; // Load Kp for Temperature control
float Kd_Temp3; // Load Kd for Temperature control
float LR_Temp3; // Load learning rate for Temperature
union {
uint64_t nBLESensorAddr2;
uint8_t nBLESensorAddrBytes2[8];
};
char bExtra[64 - 8 * sizeof(bool) - 4 * sizeof(uint8_t) - 6 * sizeof(float) - sizeof(uint64_t)];
// Block 2 - Sensor and TargetTemperature and Himidity
// Offset 64 + 2
int16_t nTempTarget, nTempTargetNight; // Target Temperature
int16_t nTemp1Offset, nTemp2Offset, nTemp3Offset;
uint16_t nHumidTarget, nHumidTargetNight;
int16_t nHumid1Offset, nHumid2Offset;
uint8_t nTemp1SensorType; // TempSensor Type enum
uint8_t nTemp2SensorType; // TempSensor Type enum
float Kp_Temp1; // Load Kp for Temperature control
float Kd_Temp1; // Load Kd for Temperature control
float LR_Temp1; // Load learning rate for Temperature
float Kp_Humidity; // Load Kp for humidity control
float Kd_Humidity; // Load Kd for humidity control
float LR_Humidity; // Load learning rate for humidity
int16_t nTempSafety;
union {
uint64_t nBLESensorAddr;
uint8_t nBLESensorAddrBytes[8];
};
uint8_t bNTCNegativePolarity;
uint8_t nBLEScanInterval;
uint8_t bBLETest;
char nTempExtra[64 - 10 * sizeof(int16_t) - 6 * sizeof(float) - 5 * sizeof(uint8_t) - sizeof(uint64_t)];
// Block 3 - AC1 and AC2
// Offset 128 + 2
DEVICE_PARAM_TYPE ac1;
DEVICE_PARAM_TYPE ac2;
// Block 4 - Mist and Fan
// Offset 192 + 2
DEVICE_PARAM_TYPE mist;
DEVICE_PARAM_TYPE fan;
// Block 5 - Motor and Light
// Offset 256 + 2
DEVICE_PARAM_TYPE motor;
DEVICE_PARAM_TYPE light;
// Block 6 - Environment and Operations
// Offset 320 + 2
bool bSendStatusSerial;
bool bConfigSaved;
bool bStatusSaved;
char nEnvExtra[32 - 3 * sizeof(bool)];
// Offset 352 + 2
STATUS_TYPE statusSave;
// Block 7 - ID and Client Display
// Offset 384 + 2
uint32_t m_nChipId;
uint16_t m_nDeviceType;
uint16_t m_nPublicPort;
uint16_t m_nExtraTBD[4];
// WiFi Client Only
uint16_t m_nDisplayTime;
int16_t m_nDisplayTempHigh;
int16_t m_nDisplayTempLow;
int16_t m_nTempHigh;
uint16_t m_fShowRealTime;
uint16_t m_fShowHistory;
uint32_t m_nEpochTime;
uint32_t m_nTimeOffset;
uint8_t m_bFahrenheit;
char nLongExtra[64 - 3 * sizeof(uint32_t) - 12 * sizeof(uint16_t)- 1 * sizeof(uint8_t)];
// Block 8 and 9 - WiFi AP ssid and pw
// Offset 448 + 2 & 512 + 2
char ssid[64], pw[64];
// Block 10
// Offset 576 + 2
char m_sDeviceName[32];
char m_sMake[32];
// Block 11
// Offset 640 + 2
char m_sModel[32];
char m_sVersion[12];
char nModelExtra[64 - 44];
// Offset 704 + 2
uint16_t m_nSignature2;
#ifdef ESP32
// ConfigStruct Size: 708
public:
void init();
bool load();
void save();
//bool saveToServer();
#endif
} CONFIG_TYPE;
#pragma pack(pop) // Restore previous alignment setting
extern class Preferences preferences;
extern CONFIG_TYPE config;
extern char BLE_SSID[32];
extern char BLE_PW[32];
#endif

99
ConnectWiFi.cpp Normal file
View File

@ -0,0 +1,99 @@
#include "ConnectWiFi.h"
#include "Config.h"
extern bool g_bWiFiSetupExecuted;
extern bool g_bWiFiHasBeenConnected;
void setupPostWiFi(bool bBoot = false);
void WiFiEvent(WiFiEvent_t event) {
switch (event) {
case IP_EVENT_STA_GOT_IP:
DPRINTF("WiFi connected, IP: %s\n", WiFi.localIP().toString().c_str());
g_bWiFiHasBeenConnected = true;
ledcWrite(PIN_LED_WIFI, PWM_FULL * 9 / 10); // LED_OFF
if (!g_bWiFiSetupExecuted) setupPostWiFi(false);
break;
case WIFI_EVENT_STA_DISCONNECTED:
DPRINTLN("WiFi disconnected.");
ledcWrite(PIN_LED_WIFI, PWM_OFF); // LED_ON
break;
default:
break;
}
}
// Function to compare current Wi-Fi credentials with the ones in config
void checkAndUpdateWiFiCredentials() {
// Check if the SSID or PW are different from the current Wi-Fi credentials
if (strncmp(BLE_SSID, config.ssid, sizeof(BLE_SSID)) ||
strncmp(BLE_PW, config.pw, sizeof(BLE_PW))) {
DPRINTF("BLE Credentials SSID(%s) PW(%S)\n", BLE_SSID, BLE_PW);
DPRINTF("Cfg Credentials SSID(%s) PW(%S)\n", config.ssid, config.pw);
DPRINTLN("Wi-Fi credentials changed! Saving new credentials...");
strncpy(config.ssid, BLE_SSID, sizeof(BLE_SSID));
strncpy(config.pw, BLE_PW, sizeof(BLE_PW));
// Save the new credentials
config.save();
// Restart ESP32 to apply the new credentials
ESP.restart();
}
}
IRAM_ATTR void checkWiFi(unsigned long tickMillis) {
static unsigned long lastAttempt = 0;
static bool bConnecting = false;
wl_status_t status = WiFi.status();
// Connected
if (status == WL_CONNECTED) {
bConnecting = false;
g_bWiFiHasBeenConnected = true;
return; // Already connected, no need to proceed further
}
checkAndUpdateWiFiCredentials();
// Connecting
if (bConnecting) {
if (tickMillis - lastAttempt < 60000) {
// give 30 seconds for connection try
return;
}
// Connection Failure
DPRINTF("Connection timed out! Status: %d\n", status);
//WiFi.disconnect(false, true);
bConnecting = false;
g_bWiFiHasBeenConnected = false;
}
// Not Connected 1 - Reconnect
if (status == WL_DISCONNECTED && g_bWiFiHasBeenConnected) {
DPRINTLN("Attempting WiFi reconnection...");
WiFi.reconnect();
ledcWrite(PIN_LED_WIFI, PWM_FULL * 4 / 5); // Light Blink
lastAttempt = tickMillis;
bConnecting = true;
}
// Not Conneccted 2 - Connect
if (tickMillis - lastAttempt > 300000) { // Retry every 5 minutes
DPRINTF("Loop: Connecting to WiFi: SSID: '%s', PW: '%s'\n", config.ssid, config.pw);
WiFi.disconnect(); // Stop Wi-Fi connection attempt
// delay(50);
// WiFi.mode(WIFI_OFF);
// delay(100);
// WiFi.mode(WIFI_STA);
// delay(50);
wl_status_t ret = WiFi.begin(config.ssid, config.pw);
DPRINTF(" WiFi.Begin(%s,%s) returned %d\n", config.ssid, config.pw, ret);
lastAttempt = tickMillis;
ledcWrite(PIN_LED_WIFI, PWM_FULL * 4 / 5); // Light Blink
lastAttempt = tickMillis;
bConnecting = true;
}
}

13
ConnectWiFi.h Normal file
View File

@ -0,0 +1,13 @@
#ifndef __CONNECT_WIFI_H
#define __CONNECT_WIFI_H
#ifdef ESP8266
#include <ESP8266WiFi.h>
#else
#include <WiFi.h>
#endif
void checkWiFi(unsigned long tick);
void WiFiEvent(WiFiEvent_t event);
inline bool isWiFiConnected() { return WiFi.status() == WL_CONNECTED; };
#endif

291
HCUpdate.h Normal file
View File

@ -0,0 +1,291 @@
/*
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#if defined(ESP32)
#ifndef ESP32UPDATER_H
#define ESP32UPDATER_H
#include <Arduino.h>
#include <MD5Builder.h>
#include <functional>
#include <WiFiClientSecure.h>
#include "esp_partition.h"
#define UPDATE_ERROR_OK (0)
#define UPDATE_ERROR_WRITE (1)
#define UPDATE_ERROR_ERASE (2)
#define UPDATE_ERROR_READ (3)
#define UPDATE_ERROR_SPACE (4)
#define UPDATE_ERROR_SIZE (5)
#define UPDATE_ERROR_STREAM (6)
#define UPDATE_ERROR_MD5 (7)
#define UPDATE_ERROR_MAGIC_BYTE (8)
#define UPDATE_ERROR_ACTIVATE (9)
#define UPDATE_ERROR_NO_PARTITION (10)
#define UPDATE_ERROR_BAD_ARGUMENT (11)
#define UPDATE_ERROR_ABORT (12)
#define UPDATE_ERROR_DECRYPT (13)
#define UPDATE_SIZE_UNKNOWN 0xFFFFFFFF
#define U_FLASH 0
#define U_SPIFFS 100
#define U_AUTH 200
#define ENCRYPTED_BLOCK_SIZE 16
#define ENCRYPTED_TWEAK_BLOCK_SIZE 32
#define ENCRYPTED_KEY_SIZE 32
#define U_AES_DECRYPT_NONE 0
#define U_AES_DECRYPT_AUTO 1
#define U_AES_DECRYPT_ON 2
#define U_AES_DECRYPT_MODE_MASK 3
#define U_AES_IMAGE_DECRYPTING_BIT 4
#define SPI_SECTORS_PER_BLOCK 16 // usually large erase block is 32k/64k
#define SPI_FLASH_BLOCK_SIZE (SPI_SECTORS_PER_BLOCK * SPI_FLASH_SEC_SIZE)
enum HTTPUpdateResult {
HTTP_UPDATE_FAILED,
HTTP_UPDATE_NO_UPDATES,
HTTP_UPDATE_OK
};
class UpdateClass {
public:
typedef std::function<void(size_t, size_t)> THandlerFunction_Progress;
UpdateClass();
UpdateClass(int httpClientTimeout);
int update(WiFiClientSecure& client, String &url, uint16_t port, String& uri,
String &currentVersion, short nDeviceType, bool bForceUpdate);
int handleUpdate(HTTPClient& http, const String& currentVersion, short nDeviceType);
int http_downloadUpdate(HTTPClient &httpClient);
/*
This callback will be called when Update is receiving data
*/
UpdateClass &onProgress(THandlerFunction_Progress fn);
/*
Call this to check the space needed for the update
Will return false if there is not enough space
*/
bool begin(size_t size = UPDATE_SIZE_UNKNOWN, int command = U_FLASH, int ledPin = -1, uint8_t ledOn = LOW, const char *label = NULL);
/*
Setup decryption configuration
Crypt Key is 32bytes(256bits) block of data, use the same key as used to encrypt image file
Crypt Address, use the same value as used to encrypt image file
Crypt Config, use the same value as used to encrypt image file
Crypt Mode, used to select if image files should be decrypted or not
*/
bool setupCrypt(const uint8_t *cryptKey = 0, size_t cryptAddress = 0, uint8_t cryptConfig = 0xf, int cryptMode = U_AES_DECRYPT_AUTO);
/*
Writes a buffer to the flash and increments the address
Returns the amount written
*/
size_t write(uint8_t *data, size_t len);
/*
Writes the remaining bytes from the Stream to the flash
Uses readBytes() and sets UPDATE_ERROR_STREAM on timeout
Returns the bytes written
Should be equal to the remaining bytes when called
Usable for slow streams like Serial
*/
size_t writeStream(Stream &data);
/*
If all bytes are written
this call will write the config to eboot
and return true
If there is already an update running but is not finished and !evenIfRemaining
or there is an error
this will clear everything and return false
the last error is available through getError()
evenIfRemaining is helpful when you update without knowing the final size first
*/
bool end(bool evenIfRemaining = false);
/*
sets AES256 key(32 bytes) used for decrypting image file
*/
bool setCryptKey(const uint8_t *cryptKey);
/*
sets crypt mode used on image files
*/
bool setCryptMode(const int cryptMode);
/*
sets address used for decrypting image file
*/
void setCryptAddress(const size_t cryptAddress) {
_cryptAddress = cryptAddress & 0x00fffff0;
}
/*
sets crypt config used for decrypting image file
*/
void setCryptConfig(const uint8_t cryptConfig) {
_cryptCfg = cryptConfig & 0x0f;
}
/*
Aborts the running update
*/
void abort();
/*
Prints the last error to an output stream
*/
void printError(Print &out);
const char *errorString();
/*
sets the expected MD5 for the firmware (hexString)
*/
bool setMD5(const char *expected_md5);
/*
returns the MD5 String of the successfully ended firmware
*/
String md5String(void) {
return _md5.toString();
}
/*
populated the result with the md5 bytes of the successfully ended firmware
*/
void md5(uint8_t *result) {
return _md5.getBytes(result);
}
//Helpers
uint8_t getError() {
return _error;
}
void clearError() {
_error = UPDATE_ERROR_OK;
}
bool hasError() {
return _error != UPDATE_ERROR_OK;
}
bool isRunning() {
return _size > 0;
}
bool isFinished() {
return _progress == _size;
}
size_t size() {
return _size;
}
size_t progress() {
return _progress;
}
size_t remaining() {
return _size - _progress;
}
/*
Template to write from objects that expose
available() and read(uint8_t*, size_t) methods
faster than the writeStream method
writes only what is available
*/
template<typename T> size_t write(T &data) {
size_t written = 0;
if (hasError() || !isRunning()) {
return 0;
}
size_t available = data.available();
while (available) {
if (_bufferLen + available > remaining()) {
available = remaining() - _bufferLen;
}
if (_bufferLen + available > 4096) {
size_t toBuff = 4096 - _bufferLen;
data.read(_buffer + _bufferLen, toBuff);
_bufferLen += toBuff;
if (!_writeBuffer()) {
return written;
}
written += toBuff;
} else {
data.read(_buffer + _bufferLen, available);
_bufferLen += available;
written += available;
if (_bufferLen == remaining()) {
if (!_writeBuffer()) {
return written;
}
}
}
if (remaining() == 0) {
return written;
}
available = data.available();
}
return written;
}
/*
check if there is a firmware on the other OTA partition that you can bootinto
*/
bool canRollBack();
/*
set the other OTA partition as bootable (reboot to enable)
*/
bool rollBack();
private:
void _reset();
void _abort(uint8_t err);
void _cryptKeyTweak(size_t cryptAddress, uint8_t *tweaked_key);
bool _decryptBuffer();
bool _writeBuffer();
bool _verifyHeader(uint8_t data);
bool _verifyEnd();
bool _enablePartition(const esp_partition_t *partition);
bool _chkDataInBlock(const uint8_t *data, size_t len) const; // check if block contains any data or is empty
int _httpClientTimeout;
followRedirects_t _followRedirects = HTTPC_DISABLE_FOLLOW_REDIRECTS;
uint8_t _error;
uint8_t *_cryptKey;
uint8_t *_cryptBuffer;
uint8_t *_buffer;
uint8_t *_skipBuffer;
size_t _bufferLen;
size_t _size;
THandlerFunction_Progress _progress_callback;
uint32_t _progress;
uint32_t _paroffset;
uint32_t _command;
const esp_partition_t *_partition;
String _target_md5;
MD5Builder _md5;
int _ledPin;
uint8_t _ledOn;
uint8_t _cryptMode;
size_t _cryptAddress;
uint8_t _cryptCfg;
};
#if !defined(NO_GLOBAL_INSTANCES) && !defined(NO_GLOBAL_UPDATE)
extern UpdateClass Update;
#endif
#endif // defined(ESP32UPDATER_H)
#endif // defined(ESP32)

787
HCUpdater.cpp Normal file
View File

@ -0,0 +1,787 @@
#if defined(ESP32)
/*
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "HermitCrab.h"
#include "Arduino.h"
#include "spi_flash_mmap.h"
#include "esp_ota_ops.h"
#include "esp_image_format.h"
#include "mbedtls/aes.h"
#include <WiFi.h>
#include <WiFiClient.h>
#include <HTTPClient.h>
#include "HCUpdate.h"
#define TAG_FW "FW Upate"
static const char *_err2str(uint8_t _error) {
if (_error == UPDATE_ERROR_OK) {
return ("No Error");
} else if (_error == UPDATE_ERROR_WRITE) {
return ("Flash Write Failed");
} else if (_error == UPDATE_ERROR_ERASE) {
return ("Flash Erase Failed");
} else if (_error == UPDATE_ERROR_READ) {
return ("Flash Read Failed");
} else if (_error == UPDATE_ERROR_SPACE) {
return ("Not Enough Space");
} else if (_error == UPDATE_ERROR_SIZE) {
return ("Bad Size Given");
} else if (_error == UPDATE_ERROR_STREAM) {
return ("Stream Read Timeout");
} else if (_error == UPDATE_ERROR_MD5) {
return ("MD5 Check Failed");
} else if (_error == UPDATE_ERROR_MAGIC_BYTE) {
return ("Wrong Magic Byte");
} else if (_error == UPDATE_ERROR_ACTIVATE) {
return ("Could Not Activate The Firmware");
} else if (_error == UPDATE_ERROR_NO_PARTITION) {
return ("Partition Could Not be Found");
} else if (_error == UPDATE_ERROR_BAD_ARGUMENT) {
return ("Bad Argument");
} else if (_error == UPDATE_ERROR_ABORT) {
return ("Aborted");
} else if (_error == UPDATE_ERROR_DECRYPT) {
return ("Decryption error");
}
return ("UNKNOWN");
}
int UpdateClass::update(WiFiClientSecure& client, String& host, uint16_t port, String&uri, String& currentVersion, short nDeviceType, bool bForceUpdate)
{
HTTPClient http;
if (!http.begin(client, host, port, uri)) {
ESP_LOGI(TAG_FW,"OTA - httpClient begin failed\n");
return HTTP_UPDATE_FAILED;
}
int size = handleUpdate(http, currentVersion, nDeviceType);
ESP_LOGI(TAG_FW,"OTA - size(%d) Server Version: %s Mode: %s\n", size, http.header("version").c_str(),
http.header("update") && http.header("update").toInt() == 1 ? "Download" : "Check Only" );
//is there an image to download
if (size >= 0) {
if (!http.header("update") || http.header("update").toInt() == 0) {
ESP_LOGI(TAG_FW,"OTA - No Firmware available");
} else if (!http.header("version") || strcmp(http.header("version").c_str(), HC__VERSION) <= 0) {
ESP_LOGI(TAG_FW,"OTA - Firmware is upto Date");
} else {
//image avaliabe to download & update
if (!bForceUpdate) {
ESP_LOGI(TAG_FW,"OTA - Found V%s Firmware\n", http.header("version").c_str());
} else {
ESP_LOGI(TAG_FW,"OTA - Downloading & Installing V%s Firmware\n", http.header("version").c_str());
}
if (bForceUpdate) {
if (http_downloadUpdate(http) == 0) {
http.end(); //end connection
ESP_LOGI(TAG_FW,"OTA - Firmware Update Successful, rebooting");
ESP.restart();
}
}
}
}
http.end(); //end connection
return 0;
}
int UpdateClass::handleUpdate(HTTPClient& http, const String& currentVersion, short nDeviceType)
{
HTTPUpdateResult ret = HTTP_UPDATE_FAILED;
// use HTTP/1.0 for update since the update handler not support any transfer Encoding
http.useHTTP10(true);
http.setTimeout(_httpClientTimeout);
http.setFollowRedirects(_followRedirects);
http.setUserAgent("ESP32-http-Update");
http.addHeader("Cache-Control", "no-cache");
http.addHeader("X-ESP32-DEVICE-TYPE", String(nDeviceType));
http.addHeader("X-ESP32-VERSION", HC__VERSION);
http.addHeader("X-ESP32-STA-MAC", WiFi.macAddress());
unsigned long nChipId = 0;
for (int i = 0; i < 17; i = i + 8) {
nChipId |= ((ESP.getEfuseMac() >> (40 - i)) & 0xff) << i;
}
http.addHeader(F("X-ESP32-Chip-ID"), String(nChipId));
http.addHeader(F("x-ESP32-free-space"), String(ESP.getFreeSketchSpace()));
http.addHeader(F("x-ESP32-sketch-size"), String(ESP.getSketchSize()));
http.addHeader(F("x-ESP32-sketch-md5"), String(ESP.getSketchMD5()));
//http.addHeader(F("x-ESP32-chip-size"), String(ESP.getFlashChipRealSize()));
http.addHeader(F("x-ESP32-sdk-version"), ESP.getSdkVersion());
//set headers to look for to get returned values in servers http response to our http request
const char *headerkeys[] = {"update", "version"}; //server returns update 0=no update found, 1=update found, version=version of update found
size_t headerkeyssize = sizeof(headerkeys) / sizeof(char *);
http.collectHeaders(headerkeys, headerkeyssize);
int code = http.GET();
int len = http.getSize();
if (code == HTTP_CODE_OK) {
return (len > 0 ? len : 0); //return 0 or length of image to download
} else if (code < 0) {
ESP_LOGI(TAG_FW,"Error: %s\n", http.errorToString(code).c_str());
ESP_LOGI(TAG_FW, "%s", http.getString());
return code; //error code should be minus between -1 to -11
} else {
ESP_LOGI(TAG_FW,"Error: HTTP Server response code %i\n", code);
ESP_LOGI(TAG_FW, "%s", http.getString());
return -code; //return code should be minus between -100 to -511
}
return ret;
}
int UpdateClass::http_downloadUpdate(HTTPClient &httpClient) {
WiFiClient *stream = httpClient.getStreamPtr();
int written = 0;
// Check content length and begin update
int fileSize = httpClient.getSize();
if (fileSize <= 0) {
ESP_LOGI(TAG_FW,"Invalid content length");
return 1;
}
if (!Update.begin(fileSize, U_FLASH)) {
ESP_LOGI(TAG_FW,"Update begin failed!");
return 1;
}
// Download and write the update
while (httpClient.connected() && written < fileSize) {
size_t availableBytes = stream->available();
if (availableBytes) {
uint8_t buffer[128]; // Buffer to hold incoming data
int bytesRead = stream->readBytes(buffer, min(availableBytes, sizeof(buffer)));
int bytesWritten = Update.write(buffer, bytesRead);
if (bytesWritten != bytesRead) {
ESP_LOGI(TAG_FW,"Update write failed!");
Update.end();
return 1;
}
written += bytesWritten;
}
}
// Finalize update
if (Update.end()) {
if (Update.isFinished()) {
ESP_LOGI(TAG_FW,"Update successful!");
return 0;
} else {
ESP_LOGI(TAG_FW,"Update not finished!");
return 1;
}
} else {
ESP_LOGI(TAG_FW,"Update end failed!");
return 1;
}
}
static bool _partitionIsBootable(const esp_partition_t *partition) {
uint8_t buf[ENCRYPTED_BLOCK_SIZE];
if (!partition) {
return false;
}
if (!ESP.partitionRead(partition, 0, (uint32_t *)buf, ENCRYPTED_BLOCK_SIZE)) {
return false;
}
if (buf[0] != ESP_IMAGE_HEADER_MAGIC) {
return false;
}
return true;
}
bool UpdateClass::_enablePartition(const esp_partition_t *partition) {
if (!partition) {
return false;
}
return ESP.partitionWrite(partition, 0, (uint32_t *)_skipBuffer, ENCRYPTED_BLOCK_SIZE);
}
UpdateClass::UpdateClass()
: _error(0)
, _cryptKey(0)
, _cryptBuffer(0)
, _buffer(0)
, _skipBuffer(0)
, _bufferLen(0)
, _size(0)
, _progress_callback(NULL)
, _progress(0)
, _paroffset(0)
, _command(U_FLASH)
, _partition(NULL)
, _cryptMode(U_AES_DECRYPT_AUTO)
, _cryptAddress(0)
, _cryptCfg(0xf)
, _httpClientTimeout(8000)
{}
UpdateClass::UpdateClass(int httpClientTimeout)
: _error(0)
, _cryptKey(0)
, _cryptBuffer(0)
, _buffer(0)
, _skipBuffer(0)
, _bufferLen(0)
, _size(0)
, _progress_callback(NULL)
, _progress(0)
, _paroffset(0)
, _command(U_FLASH)
, _partition(NULL)
, _cryptMode(U_AES_DECRYPT_AUTO)
, _cryptAddress(0)
, _cryptCfg(0xf)
, _httpClientTimeout(httpClientTimeout)
{}
UpdateClass &UpdateClass::onProgress(THandlerFunction_Progress fn) {
_progress_callback = fn;
return *this;
}
void UpdateClass::_reset() {
if (_buffer) {
delete[] _buffer;
}
if (_skipBuffer) {
delete[] _skipBuffer;
}
_cryptBuffer = nullptr;
_buffer = nullptr;
_skipBuffer = nullptr;
_bufferLen = 0;
_progress = 0;
_size = 0;
_command = U_FLASH;
if (_ledPin != -1) {
digitalWrite(_ledPin, !_ledOn); // off
}
}
bool UpdateClass::canRollBack() {
if (_buffer) { //Update is running
return false;
}
const esp_partition_t *partition = esp_ota_get_next_update_partition(NULL);
return _partitionIsBootable(partition);
}
bool UpdateClass::rollBack() {
if (_buffer) { //Update is running
return false;
}
const esp_partition_t *partition = esp_ota_get_next_update_partition(NULL);
return _partitionIsBootable(partition) && !esp_ota_set_boot_partition(partition);
}
bool UpdateClass::begin(size_t size, int command, int ledPin, uint8_t ledOn, const char *label) {
if (_size > 0) {
log_w("already running");
return false;
}
_ledPin = ledPin;
_ledOn = !!ledOn; // 0(LOW) or 1(HIGH)
_reset();
_error = 0;
_target_md5 = emptyString;
_md5 = MD5Builder();
if (size == 0) {
_error = UPDATE_ERROR_SIZE;
return false;
}
if (command == U_FLASH) {
_partition = esp_ota_get_next_update_partition(NULL);
if (!_partition) {
_error = UPDATE_ERROR_NO_PARTITION;
return false;
}
log_d("OTA Partition: %s", _partition->label);
} else if (command == U_SPIFFS) {
_partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_SPIFFS, label);
_paroffset = 0;
if (!_partition) {
_partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_FAT, NULL);
_paroffset = 0x1000; //Offset for ffat, assuming size is already corrected
if (!_partition) {
_error = UPDATE_ERROR_NO_PARTITION;
return false;
}
}
} else {
_error = UPDATE_ERROR_BAD_ARGUMENT;
log_e("bad command %u", command);
return false;
}
if (size == UPDATE_SIZE_UNKNOWN) {
size = _partition->size;
} else if (size > _partition->size) {
_error = UPDATE_ERROR_SIZE;
log_e("too large %u > %u", size, _partition->size);
return false;
}
//initialize
_buffer = new (std::nothrow) uint8_t[SPI_FLASH_SEC_SIZE];
if (!_buffer) {
log_e("_buffer allocation failed");
return false;
}
_size = size;
_command = command;
_md5.begin();
return true;
}
bool UpdateClass::setupCrypt(const uint8_t *cryptKey, size_t cryptAddress, uint8_t cryptConfig, int cryptMode) {
if (setCryptKey(cryptKey)) {
if (setCryptMode(cryptMode)) {
setCryptAddress(cryptAddress);
setCryptConfig(cryptConfig);
return true;
}
}
return false;
}
bool UpdateClass::setCryptKey(const uint8_t *cryptKey) {
if (!cryptKey) {
if (_cryptKey) {
delete[] _cryptKey;
_cryptKey = 0;
log_d("AES key unset");
}
return false; //key cleared, no key to decrypt with
}
//initialize
if (!_cryptKey) {
_cryptKey = new (std::nothrow) uint8_t[ENCRYPTED_KEY_SIZE];
}
if (!_cryptKey) {
log_e("new failed");
return false;
}
memcpy(_cryptKey, cryptKey, ENCRYPTED_KEY_SIZE);
return true;
}
bool UpdateClass::setCryptMode(const int cryptMode) {
if (cryptMode >= U_AES_DECRYPT_NONE && cryptMode <= U_AES_DECRYPT_ON) {
_cryptMode = cryptMode;
} else {
log_e("bad crypt mode argument %i", cryptMode);
return false;
}
return true;
}
void UpdateClass::_abort(uint8_t err) {
_reset();
_error = err;
}
void UpdateClass::abort() {
_abort(UPDATE_ERROR_ABORT);
}
void UpdateClass::_cryptKeyTweak(size_t cryptAddress, uint8_t *tweaked_key) {
memcpy(tweaked_key, _cryptKey, ENCRYPTED_KEY_SIZE);
if (_cryptCfg == 0) {
return; //no tweaking needed, use crypt key as-is
}
const uint8_t pattern[] = {23, 23, 23, 14, 23, 23, 23, 12, 23, 23, 23, 10, 23, 23, 23, 8};
int pattern_idx = 0;
int key_idx = 0;
int bit_len = 0;
uint32_t tweak = 0;
cryptAddress &= 0x00ffffe0; //bit 23-5
cryptAddress <<= 8; //bit23 shifted to bit31(MSB)
while (pattern_idx < sizeof(pattern)) {
tweak = cryptAddress << (23 - pattern[pattern_idx]); //bit shift for small patterns
// alternative to: tweak = rotl32(tweak,8 - bit_len);
tweak = (tweak << (8 - bit_len)) | (tweak >> (24 + bit_len)); //rotate to line up with end of previous tweak bits
bit_len += pattern[pattern_idx++] - 4; //add number of bits in next pattern(23-4 = 19bits = 23bit to 5bit)
while (bit_len > 7) {
tweaked_key[key_idx++] ^= tweak; //XOR byte
// alternative to: tweak = rotl32(tweak, 8);
tweak = (tweak << 8) | (tweak >> 24); //compiler should optimize to use rotate(fast)
bit_len -= 8;
}
tweaked_key[key_idx] ^= tweak; //XOR remaining bits, will XOR zeros if no remaining bits
}
if (_cryptCfg == 0xf) {
return; //return with fully tweaked key
}
//some of tweaked key bits need to be restore back to crypt key bits
const uint8_t cfg_bits[] = {67, 65, 63, 61};
key_idx = 0;
pattern_idx = 0;
while (key_idx < ENCRYPTED_KEY_SIZE) {
bit_len += cfg_bits[pattern_idx];
if ((_cryptCfg & (1 << pattern_idx)) == 0) { //restore crypt key bits
while (bit_len > 0) {
if (bit_len > 7 || ((_cryptCfg & (2 << pattern_idx)) == 0)) { //restore a crypt key byte
tweaked_key[key_idx] = _cryptKey[key_idx];
} else { //MSBits restore crypt key bits, LSBits keep as tweaked bits
tweaked_key[key_idx] &= (0xff >> bit_len);
tweaked_key[key_idx] |= (_cryptKey[key_idx] & (~(0xff >> bit_len)));
}
key_idx++;
bit_len -= 8;
}
} else { //keep tweaked key bits
while (bit_len > 0) {
if (bit_len < 8 && ((_cryptCfg & (2 << pattern_idx)) == 0)) { //MSBits keep as tweaked bits, LSBits restore crypt key bits
tweaked_key[key_idx] &= (~(0xff >> bit_len));
tweaked_key[key_idx] |= (_cryptKey[key_idx] & (0xff >> bit_len));
}
key_idx++;
bit_len -= 8;
}
}
pattern_idx++;
}
}
bool UpdateClass::_decryptBuffer() {
if (!_cryptKey) {
log_w("AES key not set");
return false;
}
if (_bufferLen % ENCRYPTED_BLOCK_SIZE != 0) {
log_e("buffer size error");
return false;
}
if (!_cryptBuffer) {
_cryptBuffer = new (std::nothrow) uint8_t[ENCRYPTED_BLOCK_SIZE];
}
if (!_cryptBuffer) {
log_e("new failed");
return false;
}
uint8_t tweaked_key[ENCRYPTED_KEY_SIZE]; //tweaked crypt key
int done = 0;
/*
Mbedtls functions will be replaced with esp_aes functions when hardware acceleration is available
To Do:
Replace mbedtls for the cases where there's no hardware acceleration
*/
mbedtls_aes_context ctx; //initialize AES
mbedtls_aes_init(&ctx);
while ((_bufferLen - done) >= ENCRYPTED_BLOCK_SIZE) {
for (int i = 0; i < ENCRYPTED_BLOCK_SIZE; i++) {
_cryptBuffer[(ENCRYPTED_BLOCK_SIZE - 1) - i] = _buffer[i + done]; //reverse order 16 bytes to decrypt
}
if (((_cryptAddress + _progress + done) % ENCRYPTED_TWEAK_BLOCK_SIZE) == 0 || done == 0) {
_cryptKeyTweak(_cryptAddress + _progress + done, tweaked_key); //update tweaked crypt key
if (mbedtls_aes_setkey_enc(&ctx, tweaked_key, 256)) {
return false;
}
if (mbedtls_aes_setkey_dec(&ctx, tweaked_key, 256)) {
return false;
}
}
if (mbedtls_aes_crypt_ecb(&ctx, MBEDTLS_AES_ENCRYPT, _cryptBuffer, _cryptBuffer)) { //use MBEDTLS_AES_ENCRYPT to decrypt flash code
return false;
}
for (int i = 0; i < ENCRYPTED_BLOCK_SIZE; i++) {
_buffer[i + done] = _cryptBuffer[(ENCRYPTED_BLOCK_SIZE - 1) - i]; //reverse order 16 bytes from decrypt
}
done += ENCRYPTED_BLOCK_SIZE;
}
return true;
}
bool UpdateClass::_writeBuffer() {
//first bytes of loading image, check to see if loading image needs decrypting
if (!_progress) {
_cryptMode &= U_AES_DECRYPT_MODE_MASK;
if ((_cryptMode == U_AES_DECRYPT_ON) || ((_command == U_FLASH) && (_cryptMode & U_AES_DECRYPT_AUTO) && (_buffer[0] != ESP_IMAGE_HEADER_MAGIC))) {
_cryptMode |= U_AES_IMAGE_DECRYPTING_BIT; //set to decrypt the loading image
log_d("Decrypting OTA Image");
}
}
//check if data in buffer needs decrypting
if (_cryptMode & U_AES_IMAGE_DECRYPTING_BIT) {
if (!_decryptBuffer()) {
_abort(UPDATE_ERROR_DECRYPT);
return false;
}
}
//first bytes of new firmware
uint8_t skip = 0;
if (!_progress && _command == U_FLASH) {
//check magic
if (_buffer[0] != ESP_IMAGE_HEADER_MAGIC) {
_abort(UPDATE_ERROR_MAGIC_BYTE);
return false;
}
//Stash the first 16 bytes of data and set the offset so they are
//not written at this point so that partially written firmware
//will not be bootable
skip = ENCRYPTED_BLOCK_SIZE;
_skipBuffer = new (std::nothrow) uint8_t[skip];
if (!_skipBuffer) {
log_e("_skipBuffer allocation failed");
return false;
}
memcpy(_skipBuffer, _buffer, skip);
}
if (!_progress && _progress_callback) {
_progress_callback(0, _size);
}
size_t offset = _partition->address + _progress;
bool block_erase =
(_size - _progress >= SPI_FLASH_BLOCK_SIZE) && (offset % SPI_FLASH_BLOCK_SIZE == 0); // if it's the block boundary, than erase the whole block from here
bool part_head_sectors =
_partition->address % SPI_FLASH_BLOCK_SIZE
&& offset < (_partition->address / SPI_FLASH_BLOCK_SIZE + 1) * SPI_FLASH_BLOCK_SIZE; // sector belong to unaligned partition heading block
bool part_tail_sectors =
offset >= (_partition->address + _size) / SPI_FLASH_BLOCK_SIZE * SPI_FLASH_BLOCK_SIZE; // sector belong to unaligned partition tailing block
if (block_erase || part_head_sectors || part_tail_sectors) {
if (!ESP.partitionEraseRange(_partition, _progress, block_erase ? SPI_FLASH_BLOCK_SIZE : SPI_FLASH_SEC_SIZE)) {
_abort(UPDATE_ERROR_ERASE);
return false;
}
}
// try to skip empty blocks on unencrypted partitions
if ((_partition->encrypted || _chkDataInBlock(_buffer + skip / sizeof(uint32_t), _bufferLen - skip))
&& !ESP.partitionWrite(_partition, _progress + skip, (uint32_t *)_buffer + skip / sizeof(uint32_t), _bufferLen - skip)) {
_abort(UPDATE_ERROR_WRITE);
return false;
}
//restore magic or md5 will fail
if (!_progress && _command == U_FLASH) {
_buffer[0] = ESP_IMAGE_HEADER_MAGIC;
}
_md5.add(_buffer, _bufferLen);
_progress += _bufferLen;
_bufferLen = 0;
if (_progress_callback) {
_progress_callback(_progress, _size);
}
return true;
}
bool UpdateClass::_verifyHeader(uint8_t data) {
if (_command == U_FLASH) {
if (data != ESP_IMAGE_HEADER_MAGIC) {
_abort(UPDATE_ERROR_MAGIC_BYTE);
return false;
}
return true;
} else if (_command == U_SPIFFS) {
return true;
}
return false;
}
bool UpdateClass::_verifyEnd() {
if (_command == U_FLASH) {
if (!_enablePartition(_partition) || !_partitionIsBootable(_partition)) {
_abort(UPDATE_ERROR_READ);
return false;
}
if (esp_ota_set_boot_partition(_partition)) {
_abort(UPDATE_ERROR_ACTIVATE);
return false;
}
_reset();
return true;
} else if (_command == U_SPIFFS) {
_reset();
return true;
}
return false;
}
bool UpdateClass::setMD5(const char *expected_md5) {
if (strlen(expected_md5) != 32) {
return false;
}
_target_md5 = expected_md5;
_target_md5.toLowerCase();
return true;
}
bool UpdateClass::end(bool evenIfRemaining) {
if (hasError() || _size == 0) {
return false;
}
if (!isFinished() && !evenIfRemaining) {
log_e("premature end: res:%u, pos:%u/%u\n", getError(), progress(), _size);
_abort(UPDATE_ERROR_ABORT);
return false;
}
if (evenIfRemaining) {
if (_bufferLen > 0) {
_writeBuffer();
}
_size = progress();
}
_md5.calculate();
if (_target_md5.length()) {
if (_target_md5 != _md5.toString()) {
_abort(UPDATE_ERROR_MD5);
return false;
}
}
return _verifyEnd();
}
size_t UpdateClass::write(uint8_t *data, size_t len) {
if (hasError() || !isRunning()) {
return 0;
}
if (len > remaining()) {
_abort(UPDATE_ERROR_SPACE);
return 0;
}
size_t left = len;
while ((_bufferLen + left) > SPI_FLASH_SEC_SIZE) {
size_t toBuff = SPI_FLASH_SEC_SIZE - _bufferLen;
memcpy(_buffer + _bufferLen, data + (len - left), toBuff);
_bufferLen += toBuff;
if (!_writeBuffer()) {
return len - left;
}
left -= toBuff;
}
memcpy(_buffer + _bufferLen, data + (len - left), left);
_bufferLen += left;
if (_bufferLen == remaining()) {
if (!_writeBuffer()) {
return len - left;
}
}
return len;
}
size_t UpdateClass::writeStream(Stream &data) {
size_t written = 0;
size_t toRead = 0;
int timeout_failures = 0;
if (hasError() || !isRunning()) {
return 0;
}
if (_command == U_FLASH && !_cryptMode) {
if (!_verifyHeader(data.peek())) {
_reset();
return 0;
}
}
if (_ledPin != -1) {
pinMode(_ledPin, OUTPUT);
}
while (remaining()) {
if (_ledPin != -1) {
digitalWrite(_ledPin, _ledOn); // Switch LED on
}
size_t bytesToRead = SPI_FLASH_SEC_SIZE - _bufferLen;
if (bytesToRead > remaining()) {
bytesToRead = remaining();
}
/*
Init read&timeout counters and try to read, if read failed, increase counter,
wait 100ms and try to read again. If counter > 300 (30 sec), give up/abort
*/
toRead = 0;
timeout_failures = 0;
while (!toRead) {
toRead = data.readBytes(_buffer + _bufferLen, bytesToRead);
if (toRead == 0) {
timeout_failures++;
if (timeout_failures >= 300) {
_abort(UPDATE_ERROR_STREAM);
return written;
}
delay(100);
}
}
if (_ledPin != -1) {
digitalWrite(_ledPin, !_ledOn); // Switch LED off
}
_bufferLen += toRead;
if ((_bufferLen == remaining() || _bufferLen == SPI_FLASH_SEC_SIZE) && !_writeBuffer()) {
return written;
}
written += toRead;
#if CONFIG_FREERTOS_UNICORE
delay(1); // Fix solo WDT
#endif
}
return written;
}
void UpdateClass::printError(Print &out) {
out.println(_err2str(_error));
}
const char *UpdateClass::errorString() {
return _err2str(_error);
}
bool UpdateClass::_chkDataInBlock(const uint8_t *data, size_t len) const {
// check 32-bit aligned blocks only
if (!len || len % sizeof(uint32_t)) {
return true;
}
size_t dwl = len / sizeof(uint32_t);
do {
if (*(uint32_t *)data ^ 0xffffffff) { // for SPI NOR flash empty blocks are all one's, i.e. filled with 0xff byte
return true;
}
data += sizeof(uint32_t);
} while (--dwl);
return false;
}
#if !defined(NO_GLOBAL_INSTANCES) && !defined(NO_GLOBAL_UPDATE)
UpdateClass Update;
#endif
#endif // defined(ESP32)

683
HCesp.ino Normal file
View File

@ -0,0 +1,683 @@
#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<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;
}
}

243
HermitCrab.h Normal file
View File

@ -0,0 +1,243 @@
#ifndef __HERMIT_CRAB_H
#define __HERMIT_CRAB_H
#include <Arduino.h>
#ifndef ESP32
#define ESP32
#endif
#define USE_BLE
#ifdef USE_BLE
#define THIS_DEVICE_TYPE ENUM_DEVICE_TYPE::TYPE_BETA_BLE
#define MY_IRAM_ATTR
#else
#define THIS_DEVICE_TYPE ENUM_DEVICE_TYPE::TYPE_BETA
#define MY_IRAM_ATTR IRAM_ATTR
#endif
// Define DEBUG flag
#ifndef DEBUG
#define DEBUG 1 // Set to 0 to disable debug output
#endif
#undef DEBUG
//#define BLE_DEBUG
#ifdef BLE_DEBUG
#ifndef DEBUG
#define DEBUG 1
#endif
#endif
// Debug print macros
#if defined(DEBUG)
#define DPRINT(...) Serial.print(__VA_ARGS__)
#define DPRINTLN(...) Serial.println(__VA_ARGS__)
#define DPRINTF(...) Serial.printf(__VA_ARGS__)
#else
#define DPRINT(...) // Do nothing
#define DPRINTLN(...) // Do nothing
#define DPRINTF(...) // Do nothing
#endif
#ifndef SIGNATURE1
#define SIGNATURE1 ((uint16_t) 0xC8AB)
#endif
#ifndef SIGNATURE2
#define SIGNATURE2 ((uint16_t) 0x4E81)
#endif
#define KALVIN (273.125f)
#define LIGHT_ON HIGH
#define LIGHT_OFF LOW
#define HUMID_ON HIGH
#define HUMID_OFF LOW
#define HEATER_ON HIGH
#define HEATER_OFF LOW
#define LED_ON LOW
#define LED_OFF HIGH
#define SEGMENT_ON LOW
#define SEGMENT_OFF HIGH
// =======================
// PIN Definitions
//
// LEFT
#define PIN_NTC 39 // Input Only
#define PIN_ZCD_LOAD 34 // Input Only
#define PIN_ZCD_AC 35 // Input Only
#define PIN_HEATER1 32
#define PIN_HEATER2 33
#define PIN_LIGHT 25
#define PIN_MOTOR 26
#define PIN_FAN 27
#define PIN_MIST 14 // Outputs PWM signal at boot
#define PIN_NONE_1 12 // boot fails if pulled high, strapping pin
// BOTTOM
#define PIN_NONE_2 13
#define PIN_NONE_6 15 // Outputs PWM signal at Boot, strapping pin
#define PIN_NONE_5 2
// RIGHT
#define PIN_LED_WIFI 0 // pulled up outputs PWM signal at boot, must be LOW to enter flashing mode
#define PIN_NONE_4 4
#define PIN_LED_HEATER2 16
#define PIN_LED_HEATER1 17
#define PIN_SW_DOWN 5
#define PIN_SW_UP 18
#define PIN_SW_SET 19
#define PIN_SDA 21
#define PIN_RX 3
#define PIN_TX 1
#define PIN_SCL 22
#define PIN_NONE_3 23
// End of PIN Definition
// =======================
#define PWM_RESOLUTION 10 // 10-bit resolution
#define PWM_MOTOR_RESOLUTION 8
#define PWM_AP_CHANNEL 9
#define PWM_SC_CHANNEL 10
#define PWM_AP_FREQ 1
#define PWM_SC_FREQ 2
#define PWM_MIST_CHANNEL 8 // MIST
#define PWM_MIST_FREQ 1 // MIST
#define PWM_HEATER1_CHANNEL 2
#define PWM_HEATER2_CHANNEL 3
#define PWM_LIGHT_CHANNEL 4
#define PWM_MOTOR_CHANNEL 5
#define PWM_FAN_CHANNEL 6
#define PWM_1KHZ_FREQ 1000 // 1 kHz frequency
#define PWM_25KHZ_FREQ 25000 // 25KHz
#define PWM_FULL 1023
#define PWM_OFF 0
#define TAG "HC"
enum EVENT_TYPE {
// Temperature
// Humidity
// Light
// Fan
// Connection
WiFi_Conneted,
WiFi_Disconnected,
BT_Connected,
BT_Disconnected
};
#define FLAG_MANUAL_HEATER1 (0x0001)
#define FLAG_MANUAL_HEATER2 (0x0002)
#define FLAG_MANUAL_MIST (0x0004)
#define FLAG_MANUAL_FAN (0x0008)
#define FLAG_MANUAL_MOTOR (0x0010)
#define FLAG_MANUAL_LIGHT (0x0020)
#define FLAG_ZCD_AC (0x0040)
#define FLAG_ZCD_LOAD (0x0080)
#define FLAG_ZCD (0x0100)
#define FLAG_UPNP (0x0200)
#define FLAG_BLE_BATT (0x0400)
#define FLAG_BLE_NODATA (0x0800)
#define FLAG_BLE_LOST (0x1000)
#pragma pack(push) /* push current alignment to stack */
#pragma pack(1) /* set alignment to 1 byte boundary */
typedef struct DEVICE_PARAM_STRUCT {
uint8_t nControlType;
uint8_t x;
uint8_t bDay, bNight;
//4
uint16_t dutyMin, dutyMax, dutyStart;
//6 + 4 = 10
union {
uint16_t dutyDay;
uint16_t dutyOn;
uint16_t dutyDayOrOn;
};
union {
uint16_t dutyNight;
uint16_t dutyOff;
uint16_t dutyNightOrOff;
};
//4 + 10 = 14
int16_t tempHigh, tempLow;
uint16_t timeBegin, timeEnd;
uint16_t periodPeriod, periodOn;
uint16_t humidHigh, humidLow;
//16 + 14 = 30
uint8_t extra[32 - 30];
} DEVICE_PARAM_TYPE;
typedef struct STATUS_STRUCT {
// Sensor
int16_t nTemp1;
int16_t nTemp2;
int16_t nTemp3;
int16_t nHumid1;
int16_t nHumid2;
// 10
// Control
uint16_t nHeater1Duty; // (0..10000)
uint16_t nHeater2Duty; // (0..10000)
uint16_t nMistDuty; //
uint16_t nFanDuty; //
uint16_t nMotorDuty;
uint16_t nLightDuty;
uint16_t nLightTargetDuty;
// 24
// Current Time
uint32_t now;
// 28
uint16_t nFlags;
// 30
// AC status
uint8_t zcdAC;
uint8_t zcdLoad;
// 32
} STATUS_TYPE;
#pragma pack(pop) /* restore original alignment from stack */
// =======================================================
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);
void core0Task(void *pvParameters);
// =======================================================
// Status
extern STATUS_TYPE status;
// Time
extern volatile unsigned short g_nYear, g_nMonth, g_nDay, g_nHour, g_nMinute, g_nSecond;
// Environment
extern bool bShowSensor;
extern const char *COMPANY_NAME;
extern const char *SERVICE_NAME;
extern const char *HC__VERSION;
#endif

72
HermitCrab.vcxproj Normal file
View File

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32">
<Configuration>Debug</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|Win32">
<Configuration>Release</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>16.0</VCProjectVersion>
<ProjectGuid>{9EFFA72D-8A81-44D2-B7B4-37C53EBAD29F}</ProjectGuid>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v142</PlatformToolset>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v142</PlatformToolset>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v142</PlatformToolset>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v142</PlatformToolset>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup />
<ItemDefinitionGroup>
</ItemDefinitionGroup>
<ItemGroup>
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>

4
HermitCrab.vcxproj.user Normal file
View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup />
</Project>

214
History.cpp Normal file
View File

@ -0,0 +1,214 @@
#include "History.h"
#include "Config.h"
extern class Preferences preferences;
CHistory history;
CHistory::CHistory()
: head(0), tail(0), count(0), lastTemp(0), lastHumid(0)
{
Kd_Humidity = 3.6; // Load Kd for humidity
Kd_Humidity = 1.8;
LR_Humidity = 0.25;
Kp_Temp1 = 3.6;
Kd_Temp1 = 1.8;
LR_Temp1 = 0.25;
head = 0;
tail = 0;
count = 0;
lastTemp = 0;
lastHumid = 0;
for (int i = 0; i < RING_SIZE; i++)
ring[i] = {{0}};
}
void CHistory::init(int16_t _lastTemp, int16_t _lastHumid) {
lastTemp = _lastTemp;
lastHumid = _lastHumid;
}
void CHistory::loadPID() {
Kp_Humidity = config.Kp_Humidity;
Kd_Humidity = config.Kd_Humidity;
LR_Humidity = config.LR_Humidity;
Kp_Temp1 = config.Kp_Temp1;
Kd_Temp1 = config.Kd_Temp1;
LR_Temp1 = config.LR_Temp1;
Kp_Temp2 = config.Kp_Temp2;
Kd_Temp2 = config.Kd_Temp2;
LR_Temp2 = config.LR_Temp2;
Kp_Temp3 = config.Kp_Temp3;
Kd_Temp3 = config.Kd_Temp3;
LR_Temp3 = config.LR_Temp3;
}
void CHistory::savePID() {
config.Kp_Humidity = Kp_Humidity;
config.Kd_Humidity = Kd_Humidity;
config.LR_Humidity = LR_Humidity;
config.Kp_Temp1 = Kp_Temp1;
config.Kd_Temp1 = Kd_Temp1;
config.LR_Temp1 = LR_Temp1;
config.Kp_Temp2 = Kp_Temp2;
config.Kd_Temp2 = Kd_Temp2;
config.LR_Temp2 = LR_Temp2;
config.Kp_Temp3 = Kp_Temp3;
config.Kd_Temp3 = Kd_Temp3;
config.LR_Temp3 = LR_Temp3;
}
// Method to add status data to the history buffer
MY_IRAM_ATTR short CHistory::add(STATUS_TYPE &status) {
ring[head] = status;
if (++head >= RING_SIZE) head = 0; // Mask with 0xFF for wrap-around
if (count < RING_SIZE) {
count++;
} else {
if (++tail > RING_SIZE) tail = 0; // Advance tail if buffer is full
}
return count;
}
// Main PD Control method for heater duty calculation
MY_IRAM_ATTR int16_t CHistory::calculateDutyForTemp1(int16_t setpoint, int16_t curTemp, int16_t lastDuty) {
// Calculate Proportional term
float currentError = setpoint - curTemp;
float P = Kp_Temp1 * currentError;
// Calculate Derivative term based on temperature slope
float timeInterval = 10.0; // Assuming a 5-second interval between updates; adjust as needed
float slope = (curTemp - lastTemp) / timeInterval;
float D = Kd_Temp1 * (-slope); // Negative sign to counteract the rising trend
// Limit magnitute of change
int16_t diff = roundf(P + D);
if (diff > 500) diff = 500;
else if (diff < -500) diff = -500;
// Calculate new heater duty cycle
int16_t newDuty = lastDuty + diff;
// Constrain the duty cycle within the range of 0 to 10000
if (newDuty < 0) newDuty = 0;
else if (newDuty > 10000) newDuty = 10000;
// Adjust Kp and Kd using gradient descent based on the current and previous errors
adjustGainsUsingGradientDescent(Kp_Temp1, Kd_Temp1, currentError, setpoint, lastTemp, 100.0f);
if (Kp_Temp1 < 0.5f * config.Kp_Temp1) Kp_Temp1 = 0.5f * config.Kp_Temp1;
else if (Kp_Temp1 > 2.0f * config.Kp_Temp1) Kp_Temp1 = 2.0f * config.Kp_Temp1;
if (Kd_Temp1 < 0.5f * config.Kd_Temp1) Kd_Temp1 = 0.5f * config.Kd_Temp1;
else if (Kd_Temp1 > 2.0f * config.Kd_Temp1) Kd_Temp1 = 2.0f * config.Kd_Temp1;
lastTemp = curTemp;
return newDuty;
}
MY_IRAM_ATTR int16_t CHistory::calculateDutyForTemp2(int16_t setpoint, int16_t curTemp, int16_t lastDuty) {
// Calculate Proportional term
float currentError = setpoint - curTemp;
float P = Kp_Temp2 * currentError;
// Calculate Derivative term based on temperature slope
float timeInterval = 10.0; // Assuming a 5-second interval between updates; adjust as needed
float slope = (curTemp - lastTemp) / timeInterval;
float D = Kd_Temp2 * (-slope); // Negative sign to counteract the rising trend
// Limit magnitute of change
int16_t diff = roundf(P + D);
if (diff > 500) diff = 500;
else if (diff < -500) diff = -500;
// Calculate new heater duty cycle
int16_t newDuty = lastDuty + diff;
// Constrain the duty cycle within the range of 0 to 10000
if (newDuty < 0) newDuty = 0;
else if (newDuty > 10000) newDuty = 10000;
// Adjust Kp and Kd using gradient descent based on the current and previous errors
adjustGainsUsingGradientDescent(Kp_Temp2, Kd_Temp2, currentError, setpoint, lastTemp, 100.0f);
if (Kp_Temp2 < 0.5f * config.Kp_Temp2) Kp_Temp2 = 0.5f * config.Kp_Temp2;
else if (Kp_Temp2 > 2.0f * config.Kp_Temp2) Kp_Temp2 = 2.0f * config.Kp_Temp2;
if (Kd_Temp2 < 0.5f * config.Kd_Temp2) Kd_Temp2 = 0.5f * config.Kd_Temp2;
else if (Kd_Temp2 > 2.0f * config.Kd_Temp2) Kd_Temp2 = 2.0f * config.Kd_Temp2;
lastTemp = curTemp;
return newDuty;
}
MY_IRAM_ATTR int16_t CHistory::calculateMistDuty(uint16_t setpoint, uint16_t curHumid, int16_t lastDuty) {
// Calculate Proportional (P) term based on the error (difference between setpoint and current humidity)
float currentError = setpoint - curHumid;
float P = Kp_Humidity * currentError;
// Calculate Derivative (D) term based on humidity slope
float timeInterval = 10.0; // Assuming a 10-second interval between updates; adjust as needed
float slope = (curHumid - lastHumid) / timeInterval;
float D = Kd_Humidity * (-slope); // Negative sign to counteract the rising or falling trend
// Limit magnitute of change
int16_t diff = roundf(P + D);
if (diff > 500) diff = 500;
else if (diff < -500) diff = -500;
// Calculate the new mist duty cycle
int16_t newDuty = lastDuty + diff;
// Constrain the duty cycle within the allowable range (0 to 511 for 50% duty cycle)
if (newDuty < 0) newDuty = 0;
else if (newDuty > 10000) newDuty = 10000;
// Adjust Kp and Kd using gradient descent based on current and previous errors
adjustGainsUsingGradientDescent(Kp_Humidity, Kd_Humidity, currentError, setpoint, lastHumid, 300.0f);
if (Kp_Humidity < 0.5f * config.Kp_Humidity) Kp_Humidity = 0.5f * config.Kp_Humidity;
else if (Kp_Humidity > 2.0f * config.Kp_Humidity) Kp_Humidity = 2.0f * config.Kp_Humidity;
if (Kd_Humidity < 0.5f * config.Kd_Humidity) Kd_Humidity = 0.5f * config.Kd_Humidity;
else if (Kd_Humidity > 2.0f * config.Kd_Humidity) Kd_Humidity = 2.0f * config.Kd_Humidity;
lastHumid = curHumid;
return newDuty;
}
// Method to adjust Kp and Kd using gradient descent based on current and previous errors
MY_IRAM_ATTR void CHistory::adjustGainsUsingGradientDescent(float &Kp, float &Kd, float currentError, uint16_t setpoint, float prevTemperature, float MAX_EXPECTED_ERROR) {
// Normalize the error (assuming a max expected error for normalization)
float normalizedError = currentError / MAX_EXPECTED_ERROR;
// Calculate change in temperature as the slope
float temperatureSlope = (setpoint - prevTemperature);
// Define limits for adjusting Kp and Kd
const float proportionalLimitFactorKp = 0.25f;
const float proportionalLimitFactorKd = 0.25f;
// Scale the gradients based on normalized error and temperature slope
float limitedProportionalGradient = proportionalLimitFactorKp * Kp;
float limitedDerivativeGradient = proportionalLimitFactorKd * Kd;
// Update Kp and Kd using their respective gradients, ensuring adjustments are within limits
Kp -= LR_Temp1 * fmin(fabs(normalizedError), limitedProportionalGradient) * (normalizedError < 0 ? 1 : -1);
Kd -= LR_Temp1 * fmin(fabs(temperatureSlope), limitedDerivativeGradient) * (temperatureSlope < 0 ? 1 : -1);
// Constrain Kp and Kd to avoid going below minimum threshold values
const float MIN_KP = 0.01f;
const float MIN_KD = 0.01f;
Kp = max(Kp, MIN_KP);
Kd = max(Kd, MIN_KD);
}

69
History.h Normal file
View File

@ -0,0 +1,69 @@
#ifndef __HISTORY_H
#define __HISTORY_H
#include "HermitCrab.h" // Assuming CURRENT_STATUS structure is defined elsewhere
#define RING_SIZE (256 * 7)
//#define RING_MASK (RING_SIZE - 1)
class CHistory {
private:
int16_t head;
int16_t tail;
int16_t count;
STATUS_TYPE ring[RING_SIZE]; // Ring buffer
float Kp_Humidity;
float Kd_Humidity;
float LR_Humidity;
float Kp_Temp1;
float Kd_Temp1;
float LR_Temp1;
float Kp_Temp2;
float Kd_Temp2;
float LR_Temp2;
float Kp_Temp3;
float Kd_Temp3;
float LR_Temp3;
int16_t lastTemp, lastHumid;
public:
CHistory();
void loadPID();
void savePID();
void init(int16_t lastTemp, int16_t lastHumid);
inline float getKpTemperature() { return Kp_Temp1;}
inline float getKdTemperature() { return Kd_Temp1;}
inline float getLRTemperature() { return LR_Temp1;}
inline float getKpHumidity() { return Kp_Humidity;}
inline float getKdHumidity() { return Kd_Humidity;}
inline float getLRHumidity() { return LR_Humidity;}
inline float getKpHeater1() { return Kp_Temp1; }
inline float getKdHeater1() { return Kd_Temp1; }
inline float getKpMist() { return Kp_Humidity; }
inline float getKdMist() { return Kd_Humidity; }
int16_t add(STATUS_TYPE &status);
int16_t calculateDutyForTemp1(int16_t setPoint, int16_t curTemp, int16_t lastDuty);
int16_t calculateDutyForTemp2(int16_t setPoint, int16_t curTemp, int16_t lastDuty);
int16_t calculateMistDuty(uint16_t setPpoint, uint16_t curHumid, int16_t lastDuty);
inline int16_t getRingCount() { return count; };
inline int16_t getRingSize() { return RING_SIZE; }
inline uint8_t *getRingData1() { return (uint8_t *) &ring[tail]; }
inline uint8_t *getRingData2() { return (uint8_t *) &ring[0]; }
inline int16_t getRingHead() { return head; }
inline int16_t getRingTail() { return tail; }
private:
void adjustGainsUsingGradientDescent(float &Kp, float &Kd, float currentError, uint16_t setpoint, float prevTemperature, float MAX_EXPECTED_ERROR);
};
extern CHistory history;
#endif // CHISTORY_H

52
LED0.cpp Normal file
View File

@ -0,0 +1,52 @@
#include "LED0.h"
CLED0 led0;
void CLED0::setup(uint8_t _pin, uint16_t _freq, uint16_t _channel) {
freq = _freq;
channel = _channel;
pin = _pin;
bPWMMode = true;
bAC = false;
bLoad = false;
duty = 0;
ledcAttachChannel(pin, _freq, PWM_RESOLUTION, channel);
setDuty(duty);
};
MY_IRAM_ATTR void CLED0::setFreq(uint16_t _freq) {
if (freq != _freq) {
if (_freq == 0) {
ledcDetach(pin);
pinMode(pin, OUTPUT);
digitalWrite(pin, LED_OFF);
bPWMMode = false;
}
else {
ledcAttachChannel(pin, _freq, PWM_RESOLUTION, channel);
bPWMMode = true;
}
freq = _freq;
}
}
MY_IRAM_ATTR void CLED0::setDuty() {
uint16_t _duty;
if (bAC) _duty = LED0_DUTY_AC;
else if (bLoad) _duty = LED0_DUTY_LOAD;
else _duty = duty;
ledcWrite(PIN_LED_WIFI, PWM_FULL * (100 - _duty) / 100); // Light Blink
}
MY_IRAM_ATTR void CLED0::setDuty(uint16_t _duty) {
if (duty != _duty) {
if (bPWMMode)
ledcWrite(PIN_LED_WIFI, PWM_FULL * (100 - _duty) / 100); // Light Blink
else
digitalWrite(pin, _duty ? LED_ON : LED_OFF);
duty = _duty;
}
};

38
LED0.h Normal file
View File

@ -0,0 +1,38 @@
#ifndef __LED0_H
#define __LED0_H
#ifndef __HERMIT_CRAB_H
#include "HermitCrab.h"
#endif
#define LED0_DUTY_BOOT 20
#define LED0_DUTY_CONNECTED 0
#define LED0_DUTY_CONNECTING 5
#define LED0_DUTY_SMART_CONFIG 100
#define LED0_DUTY_CLIENT 1
#define LED0_DUTY_AC 90
#define LED0_DUTY_LOAD 80
class CLED0 {
public:
void setup(uint8_t _pin, uint16_t _freq, uint16_t _channel);
void loop();
void setFreq(uint16_t _freq);
void setDuty(uint16_t _duty);
void setDuty();
inline void setAC() { bAC = true; };
inline void clearAC() { bAC = false; };
inline void setLoad() { bLoad = true; };
inline void clearLoad() { bLoad = false; };
private:
uint16_t channel;
uint16_t freq;
uint16_t duty;
uint8_t pin;
bool bPWMMode;
bool bAC, bLoad;
};
extern CLED0 led0;
#endif

127
NTC_10K.cpp Normal file
View File

@ -0,0 +1,127 @@
#include "Arduino.h"
#include "HermitCrab.h"
#include "NTC_10K.h"
#include "Config.h"
const float resistance[] = {
3360850.37, // -40°C
1973470.32, // -35°C
1179560.43, // -30°C
718858.73, // -25°C
445267.47, // -20°C
281046.96, // -15°C
180321.36, // -10°C
117081.11, // -5°C
77147.90, // 0°C
51471.97, // 5°C
34838.43, // 10°C
23847.63, // 15°C
16594.38, // 20°C
12307.39, // 21°C
11739.87, // 22°C
11203.64, // 23°C
10696.86, // 24°C
10217.84, // 25°C
9527.52, // 26°C
9076.66, // 27°C
8656.02, // 28°C
8263.81, // 29°C
7897.84, // 30°C
5868.86, // 35°C
4383.72, // 40°C
3315.12, // 45°C
2527.73, // 50°C
1942.15, // 55°C
1505.11, // 60°C
1174.71, // 65°C
926.23, // 70°C
735.99, // 75°C
588.91, // 80°C
474.43, // 85°C
384.48, // 90°C
313.88, // 95°C
258.87, // 100°C
215.64, // 105°C
181.10, // 110°C
153.01, // 115°C
129.77, // 120°C
110.27, // 125°C
94.03, // 130°C
80.13, // 135°C
68.18, // 140°C
57.99, // 145°C
49.30 // 150°C
};
const int16_t temp_C[] = {
-40, -35, -30, -25, -20, -15, -10, -5, 0, 5, 10, 15,
20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100,
105, 110, 115, 120, 125, 130, 135, 140, 145, 150 };
NTC_10K ntc;
void NTC_10K::setup(bool bNegativePolarity) {
m_bNegativePolarity = bNegativePolarity;
_vRef = 3.3f;
_RESO = 4095;
for (int i = 0; i < 16; i++) temps[i] = 0;
temp_idx = 0;
temp_sum = 0;
pinMode(PIN_NTC, INPUT); // Set PIN_NTC as input
analogReadResolution(12); // Set ADC resolution to 12 bits (0-4095)
analogSetAttenuation(ADC_11db); // Set attenuation for full-scale 3.3V
}
void NTC_10K::readSensor() {
float Vin;
int16_t temp;
static int16_t lastTemp = 0;
int adcValue = analogRead(PIN_NTC); // Read ADC value from PIN_NTC
// Calculate the input voltage from the ADC reading
Vin = (float)adcValue * _vRef / _RESO;
// Calculate the resistance of the thermistor
// Calculate the resistance of the thermistor (adjusted for inverted ADC behavior)
float r;
if (m_bNegativePolarity) {
// NTC is connected to Negative
r = (Vin / (_vRef - Vin)) * rRef;
} else {
// NTC is connected to Positive
r = ((_vRef - Vin) / Vin) * rRef;
}
// Find the index of the resistance in the table where r is between resistance[i-1] and resistance[i]
int i = 0;
while (i < sizeof(resistance) / sizeof(resistance[0]) - 1 && resistance[i] > r) {
i++;
}
// If r is out of range, return the closest extreme temperature
if (i == 0 || i == sizeof(resistance) / sizeof(resistance[0]) - 1) {
if (lastTemp != 0) temp = lastTemp;
else temp = 0;
}
else {
// Interpolate between resistance[i-1] and resistance[i]
float m = (temp_C[i] - temp_C[i - 1]) / (resistance[i] - resistance[i - 1]); // Slope
float b = temp_C[i - 1] - (m * resistance[i - 1]); // Intercept
temp = (int16_t) roundf((m * r + b) * 10.0f);
}
// Return the temperature as an integer scaled by 10 (e.g., 25.3°C => 253)
temp_sum -= temps[temp_idx];
temps[temp_idx++] = temp;
temp_idx &= NTC_MASK;
temp_sum += temp;
lastTemp = temp;
m_nTemp = temp_sum / NTC_COUNT;
}

32
NTC_10K.h Normal file
View File

@ -0,0 +1,32 @@
#ifndef __NTC_H
#define __NTC_H
#ifndef rRef
#define rRef 10000
#endif
#define NTC_COUNT 32
#define NTC_MASK (NTC_COUNT - 1)
class NTC_10K {
private:
//lookup table
//float resistance[64];
//int16_t temp_C[64];
int16_t temps[NTC_COUNT];
int16_t temp_sum;
int16_t temp_idx;
float _vRef;
int _RESO;
bool m_bNegativePolarity;
int16_t m_nTemp;
public:
void setup(bool bNegativePolarity);
void readSensor();
inline int16_t getTemp() { return m_nTemp; };
};
extern NTC_10K ntc;
#endif

134
OTA.cpp Normal file
View File

@ -0,0 +1,134 @@
#define NO_GLOBAL_UPDATE
#include <string.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <HTTPClient.h>
#include <esp_task_wdt.h>
#include "HCUpdate.h"
#include "HermitCrab.h"
#include "Config.h"
#include "ConnectWiFi.h"
#define TAG_OTA "OTA"
// ==============================================================
//
// OTA
//
// ==============================================================
const char *HC__VERSION = "20250405001";
#define UPDATE_PORT ((uint16_t) 443)
String url = "visionsoft.kr";
String uri = "/hc/hc_firmware_update.php";
const char *HTTPUPDATE_USERAGRENT = "ESP32-http-Update";
const char *COMPANY_NAME = "VisionSoft";
const char *SERVICE_NAME = "HermitCrab";
const char* rootCACertificate = \
"-----BEGIN CERTIFICATE-----\n" \
"MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw\n" \
"TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh\n" \
"cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4\n" \
"WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu\n" \
"ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY\n" \
"MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc\n" \
"h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+\n" \
"0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U\n" \
"A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW\n" \
"T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH\n" \
"B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC\n" \
"B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv\n" \
"KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn\n" \
"OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn\n" \
"jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw\n" \
"qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI\n" \
"rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV\n" \
"HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq\n" \
"hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL\n" \
"ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ\n" \
"3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK\n" \
"NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5\n" \
"ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur\n" \
"TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC\n" \
"jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc\n" \
"oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq\n" \
"4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA\n" \
"mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d\n" \
"emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=\n" \
"-----END CERTIFICATE-----\n";
String getSketchSHA256(); // Function to retrieve current sketch hash
// Callback function for OTA progress
void onOTAProgress(int current, int total) {
ESP_LOGD(TAG_OTA,"OTA -- Progress: %d%%\n", (current * 100) / total);
}
//==========================================================================
String urlEncode(const String &url, const char *safeChars = "-_.~") {
String encoded = "";
char temp[4];
for (int i = 0; i < url.length(); i++) {
temp[0] = url.charAt(i);
if (temp[0] == 32) { //space
encoded.concat('+');
} else if ((temp[0] >= 48 && temp[0] <= 57) /*0-9*/
|| (temp[0] >= 65 && temp[0] <= 90) /*A-Z*/
|| (temp[0] >= 97 && temp[0] <= 122) /*a-z*/
|| (strchr(safeChars, temp[0]) != NULL) /* "=&-_.~" */
) {
encoded.concat(temp[0]);
} else { //character needs encoding
snprintf(temp, 4, "%%%02X", temp[0]);
encoded.concat(temp);
}
}
return encoded;
}
//==========================================================================
bool addQuery(String *query, const String name, const String value) {
if (name.length() && value.length()) {
if (query->length() < 3) {
*query = "?";
} else {
query->concat('&');
}
query->concat(urlEncode(name));
query->concat('=');
query->concat(urlEncode(value));
return true;
}
return false;
}
//==========================================================================
bool checkOTA(bool bForceUpdate)
{
// Set callbacks
//Update.onStart(onOTAStart);
//Update.onEnd(onOTAEnd);
String query = "";
addQuery(&query, "cmd", (bForceUpdate ? "download" : "check")); //action command
uri.concat(query);
String version = String(HC__VERSION);
ESP_LOGI(TAG_OTA,"OTA - URL: %s\n", url.c_str());
ESP_LOGI(TAG_OTA,"OTA - URI: %s\n", uri.c_str());
ESP_LOGI(TAG_OTA,"OTA - Ver: %s\n", HC__VERSION);
WiFiClientSecure client;
client.setCACert(rootCACertificate);
UpdateClass hcUpdate(5000);
hcUpdate.onProgress(onOTAProgress);
//int update(WiFiClient& client, String &url, uint16_t port, String& uri,
// String &currentVersion, short nDeviceType, bool bForceUpdate);
int result = hcUpdate.update(client, url, UPDATE_PORT, uri, version, (short)config.m_nDeviceType, true);
return false;
}

10
OTA.h Normal file
View File

@ -0,0 +1,10 @@
#ifndef __OTA_H
#define __OTA_H
extern const char *COMPANY_NAME;
extern const char *SERVICE_NAME;
extern const char *HC__VERSION;
extern const char* rootCACertificate;
bool checkOTA(bool bForceUpdate);
#endif

BIN
README.md Normal file

Binary file not shown.

225
SSD1306.cpp Normal file
View File

@ -0,0 +1,225 @@
#include "HermitCrab.h"
#include "SSD1306.h"
//#define DISPLAY_WIDTH SCREEN_WIDTH
//#define DISPLAY_HEIGHT SCREEN_HEIGHT
#define WIRE_WRITE Wire.write
#define SETWIRECLOCK Wire.setClock(400000UL) ///< Set before I2C transfer
#define RESWIRECLOCK Wire.setClock(100000UL) ///< Restore after I2C xfer
#define TRANSACTION_START SETWIRECLOCK
#define TRANSACTION_END RESWIRECLOCK
#define WIRE_MAX I2C_BUFFER_LENGTH
// Constructor for SSD1306 class
//SSD1306::SSD1306(Adafruit_SSD1306 &display)
SSD1306::SSD1306()
: Adafruit_GFX(SCREEN_WIDTH, SCREEN_HEIGHT)
, i2caddr(DISPLAY_I2C_ADDRESS)
{}
void SSD1306::dim(bool dim) {
// the range of contrast to too small to be really useful
// it is useful to dim the display
TRANSACTION_START;
ssd1306_command1(SSD1306_SETCONTRAST);
ssd1306_command1(dim ? 0 : contrast);
TRANSACTION_END;
}
void SSD1306::setContrast(uint8_t con) {
// the range of contrast to too small to be really useful
// it is useful to dim the display
TRANSACTION_START;
ssd1306_command1(SSD1306_SETCONTRAST);
ssd1306_command1(100 * con / contrast);
TRANSACTION_END;
}
bool SSD1306::begin(uint8_t vcs, uint8_t addr) {
vccstate = vcs;
i2caddr = addr;
TRANSACTION_START;
// Init sequence
static const uint8_t init1[] = {SSD1306_DISPLAYOFF, // 0xAE
SSD1306_SETDISPLAYCLOCKDIV, // 0xD5
0x80, // the suggested ratio 0x80
SSD1306_SETMULTIPLEX,
SCREEN_HEIGHT - 1}; // 0xA8
ssd1306_commandList(init1, sizeof(init1));
static const uint8_t init2[] = {SSD1306_SETDISPLAYOFFSET, // 0xD3
0x0, // no offset
SSD1306_SETSTARTLINE | 0x0, // line #0
SSD1306_CHARGEPUMP}; // 0x8D
ssd1306_commandList(init2, sizeof(init2));
ssd1306_command1((vccstate == SSD1306_EXTERNALVCC) ? 0x10 : 0x14);
static const uint8_t init3[] = {SSD1306_MEMORYMODE, // 0x20
0x00, // 0x0 act like ks0108
SSD1306_SEGREMAP | 0x1,
SSD1306_COMSCANDEC};
ssd1306_commandList(init3, sizeof(init3));
uint8_t comPins = 0x02;
contrast = 0x8F;
if ((WIDTH == 128) && (HEIGHT == 32)) {
comPins = 0x02;
contrast = 0x8F;
} else if ((WIDTH == 128) && (HEIGHT == 64)) {
comPins = 0x12;
contrast = (vccstate == SSD1306_EXTERNALVCC) ? 0x9F : 0xCF;
} else if ((WIDTH == 96) && (HEIGHT == 16)) {
comPins = 0x2; // ada x12
contrast = (vccstate == SSD1306_EXTERNALVCC) ? 0x10 : 0xAF;
} else {
// Other screen varieties -- TBD
}
ssd1306_command1(SSD1306_SETCOMPINS);
ssd1306_command1(comPins);
ssd1306_command1(SSD1306_SETCONTRAST);
ssd1306_command1(contrast);
ssd1306_command1(SSD1306_SETPRECHARGE); // 0xd9
ssd1306_command1((vccstate == SSD1306_EXTERNALVCC) ? 0x22 : 0xF1);
static const uint8_t init5[] = {
SSD1306_SETVCOMDETECT, // 0xDB
0x40,
SSD1306_DISPLAYALLON_RESUME, // 0xA4
SSD1306_NORMALDISPLAY, // 0xA6
SSD1306_DEACTIVATE_SCROLL,
SSD1306_DISPLAYON}; // Main screen turn on
ssd1306_commandList(init5, sizeof(init5));
TRANSACTION_END;
clearDisplayBuffer();
return true;
}
MY_IRAM_ATTR void SSD1306::updateScreen() {
TRANSACTION_START;
uint8_t dlist1[6];
dlist1[0] = SSD1306_PAGEADDR;
dlist1[1] = 0;
dlist1[2] = (height() / 8) - 1;
dlist1[3] = SSD1306_COLUMNADDR;
dlist1[4] = 0;
dlist1[5] = width() - 1;
ssd1306_commandList(dlist1, sizeof(dlist1));
uint16_t count = height() * width() / 8;
uint8_t *ptr = getBuffer();
Wire.beginTransmission(i2caddr);
WIRE_WRITE((uint8_t)0x40);
uint16_t bytesOut = 1;
// Loop through each page in the range
while (count--) {
if (bytesOut >= WIRE_MAX) {
Wire.endTransmission();
Wire.beginTransmission(i2caddr);
WIRE_WRITE((uint8_t)0x40);
bytesOut = 1;
}
WIRE_WRITE(*ptr++);
bytesOut++;
}
Wire.endTransmission(); // End transmission for the page
TRANSACTION_END;
}
MY_IRAM_ATTR void SSD1306::updateRegion(uint8_t pageStart, uint8_t pageEnd, uint8_t colStart = 0, uint8_t colEnd = 127) {
TRANSACTION_START;
uint8_t dlist1[6];
dlist1[0] = SSD1306_PAGEADDR;
dlist1[1] = pageStart;
dlist1[2] = pageEnd;
dlist1[3] = SSD1306_COLUMNADDR;
dlist1[4] = colStart;
dlist1[5] = colEnd;
ssd1306_commandList(dlist1, sizeof(dlist1));
uint16_t count = (colEnd - colStart + 1) * (pageEnd - pageStart + 1);
uint8_t *ptr = getBuffer() + pageStart * SCREEN_WIDTH + colStart;
uint16_t cols = colEnd - colStart + 1;
uint16_t offset = SCREEN_WIDTH - cols;
Wire.beginTransmission(i2caddr);
WIRE_WRITE((uint8_t)0x40);
uint16_t bytesOut = 1;
// Loop through each page in the range
for (uint8_t page = pageStart; page <= pageEnd; page++) {
// Send column data for the current page
for (uint8_t col = 0; col < cols; col++) {
if (bytesOut >= WIRE_MAX) {
Wire.endTransmission();
Wire.beginTransmission(i2caddr);
WIRE_WRITE((uint8_t)0x40);
bytesOut = 1;
}
WIRE_WRITE(*ptr++);
bytesOut++;
}
ptr += offset;
}
Wire.endTransmission(); // End transmission for the page
TRANSACTION_END;
}
MY_IRAM_ATTR void SSD1306::ssd1306_hscroll(uint8_t dir, uint8_t pageStart, uint8_t pageEnd, uint8_t offset, uint8_t interval) {
ssd1306_command1(dir ? SSD1306_RIGHT_HORIZONTAL_SCROLL : SSD1306_LEFT_HORIZONTAL_SCROLL);
ssd1306_command1(0x00); // Dummy
ssd1306_command1(pageStart);
ssd1306_command1(interval);
ssd1306_command1(pageEnd);
ssd1306_command1(offset);
ssd1306_command1(SSD1306_ACTIVATE_SCROLL);
}
MY_IRAM_ATTR void SSD1306::drawPixel(int16_t x, int16_t y, uint16_t color) {
if ((x >= 0) && (x < width()) && (y >= 0) && (y < height())) {
switch (color) {
case SSD1306_WHITE:
buffer[x + (y / 8) * WIDTH] |= (1 << (y & 7));
break;
case SSD1306_BLACK:
buffer[x + (y / 8) * WIDTH] &= ~(1 << (y & 7));
break;
case SSD1306_INVERSE:
buffer[x + (y / 8) * WIDTH] ^= (1 << (y & 7));
break;
}
}
}
MY_IRAM_ATTR void SSD1306::ssd1306_commandList(const uint8_t *c, uint8_t n) {
Wire.beginTransmission(i2caddr);
WIRE_WRITE((uint8_t)0x00); // Co = 0, D/C = 0
uint16_t bytesOut = 1;
while (n--) {
if (bytesOut >= WIRE_MAX) {
Wire.endTransmission();
Wire.beginTransmission(i2caddr);
WIRE_WRITE((uint8_t)0x00); // Co = 0, D/C = 0
bytesOut = 1;
}
WIRE_WRITE(*c++);
bytesOut++;
}
Wire.endTransmission();
}
MY_IRAM_ATTR void SSD1306::ssd1306_command1(uint8_t c) {
Wire.beginTransmission(i2caddr);
WIRE_WRITE((uint8_t)0x00); // Co = 0, D/C = 0
WIRE_WRITE(c);
Wire.endTransmission();
}

86
SSD1306.h Normal file
View File

@ -0,0 +1,86 @@
#ifndef SSD1306_H
#define SSD1306_H
#include <Adafruit_GFX.h>
#include <Wire.h>
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define DISPLAY_I2C_ADDRESS_32 (0x3C)
#define DISPLAY_I2C_ADDRESS_64 (0x3D)
#define DISPLAY_I2C_ADDRESS DISPLAY_I2C_ADDRESS_32
#define SSD1306_BLACK 0 ///< Draw 'off' pixels
#define SSD1306_WHITE 1 ///< Draw 'on' pixels
#define SSD1306_INVERSE 2 ///< Invert pixels
#define SSD1306_MEMORYMODE 0x20 ///< See datasheet
#define SSD1306_COLUMNADDR 0x21 ///< See datasheet
#define SSD1306_PAGEADDR 0x22 ///< See datasheet
#define SSD1306_SETCONTRAST 0x81 ///< See datasheet
#define SSD1306_CHARGEPUMP 0x8D ///< See datasheet
#define SSD1306_SEGREMAP 0xA0 ///< See datasheet
#define SSD1306_DISPLAYALLON_RESUME 0xA4 ///< See datasheet
#define SSD1306_DISPLAYALLON 0xA5 ///< Not currently used
#define SSD1306_NORMALDISPLAY 0xA6 ///< See datasheet
#define SSD1306_INVERTDISPLAY 0xA7 ///< See datasheet
#define SSD1306_SETMULTIPLEX 0xA8 ///< See datasheet
#define SSD1306_DISPLAYOFF 0xAE ///< See datasheet
#define SSD1306_DISPLAYON 0xAF ///< See datasheet
#define SSD1306_COMSCANINC 0xC0 ///< Not currently used
#define SSD1306_COMSCANDEC 0xC8 ///< See datasheet
#define SSD1306_SETDISPLAYOFFSET 0xD3 ///< See datasheet
#define SSD1306_SETDISPLAYCLOCKDIV 0xD5 ///< See datasheet
#define SSD1306_SETPRECHARGE 0xD9 ///< See datasheet
#define SSD1306_SETCOMPINS 0xDA ///< See datasheet
#define SSD1306_SETVCOMDETECT 0xDB ///< See datasheet
#define SSD1306_SETLOWCOLUMN 0x00 ///< Not currently used
#define SSD1306_SETHIGHCOLUMN 0x10 ///< Not currently used
#define SSD1306_SETSTARTLINE 0x40 ///< See datasheet
#define SSD1306_EXTERNALVCC 0x01 ///< External display voltage source
#define SSD1306_SWITCHCAPVCC 0x02 ///< Gen. display voltage from 3.3V
#define SSD1306_RIGHT_HORIZONTAL_SCROLL 0x26 ///< Init rt scroll
#define SSD1306_LEFT_HORIZONTAL_SCROLL 0x27 ///< Init left scroll
#define SSD1306_VERTICAL_AND_RIGHT_HORIZONTAL_SCROLL 0x29 ///< Init diag scroll
#define SSD1306_VERTICAL_AND_LEFT_HORIZONTAL_SCROLL 0x2A ///< Init diag scroll
#define SSD1306_DEACTIVATE_SCROLL 0x2E ///< Stop scroll
#define SSD1306_ACTIVATE_SCROLL 0x2F ///< Start scroll
#define SSD1306_SET_VERTICAL_SCROLL_AREA 0xA3 ///< Set scroll range
#define SSD1306_LCDWIDTH SCREEN_WIDTH ///< DEPRECATED: width w/SSD1306_128_64 defined
#define SSD1306_LCDHEIGHT SCREEN_HEIGHT ///< DEPRECATED: height w/SSD1306_128_64 defined
class SSD1306 : public Adafruit_GFX {
public:
// Constructor
//CUI(Adafruit_SSD1306 &display);
SSD1306();
void dim(bool dim);
void setContrast(uint8_t contrast);
protected:
bool bOK;
uint8_t buffer[SCREEN_WIDTH * SCREEN_HEIGHT / 8];
int8_t i2caddr; ///< I2C address initialized when begin method is called.
uint8_t contrast; ///< normal contrast setting for this device
bool begin(uint8_t switchvcc = SSD1306_SWITCHCAPVCC, uint8_t i2caddr = DISPLAY_I2C_ADDRESS);
inline uint8_t *getBuffer(void) { return buffer; };
inline void clearDisplayBuffer(void) { memset(buffer, 0, WIDTH * ((HEIGHT + 7) / 8)); };
void updateScreen();
void updateRegion(uint8_t pageStart, uint8_t pageEnd, uint8_t colStart , uint8_t colEnd);
void ssd1306_hscroll(uint8_t dir, uint8_t pageStart, uint8_t pageEnd, uint8_t offset, uint8_t interval);
private:
int8_t vccstate; ///< VCC selection, set by begin method.
uint32_t wireClk; ///< Wire speed for SSD1306 transfers
uint32_t restoreClk; ///< Wire speed following SSD1306 transfers
virtual void drawPixel(int16_t x, int16_t y, uint16_t color);
void ssd1306_commandList(const uint8_t *c, uint8_t n);
void ssd1306_command1(uint8_t c);
};
#endif

302
Setup.cpp Normal file
View File

@ -0,0 +1,302 @@
#include "HermitCrab.h"
#include "Config.h"
#include "AHT2x.h"
#include "NTC_10K.h"
#include "ZCD.h"
#include "History.h"
#include "ConnectWiFi.h"
#include "WiFiHost.h"
#include "TimeManager.h"
#include "OTA.h"
#include "UI.h"
#include "LED0.h"
#include "BLEScan.h"
#include <esp_wifi.h>
#include "esp_coexist.h"
#define TAG_SETUP "TAG_SETUP"
// Task handle
TaskHandle_t TaskHandle_0;
bool g_bWiFiSetupExecuted = false;
bool g_bWiFiHasBeenConnected = false;
extern STATUS_TYPE status;
extern CHistory history;
void setupConfig();
void setupStatus();
void restoreStatus();
void setupWiFi();
void setupPostWiFi(bool bBoot);
void setupPins();
void setupSensor();
void setupZCD();
void setup_BLE();
void scanI2C();
void setup() {
// put your setup code here, to run once:
#ifdef DEBUG
Serial.begin(115200);
#endif
//esp_log_level_set("*", ESP_LOG_INFO); // Global log level
//esp_log_level_set("BLE_POLL", ESP_LOG_INFO); // Module-specific level
//esp_coex_preference_set(ESP_COEX_PREFER_BT);
DPRINTLN(" **********************");
DPRINTF(" SETUP - Start - ver. %s type: %d\n", HC__VERSION, THIS_DEVICE_TYPE);
DPRINTLN(" **********************");
g_bWiFiHasBeenConnected = false;
g_bWiFiHasBeenConnected = false;
g_nYear = 2024;
g_nMonth = 10;
g_nDay = 15;
g_nHour = 0;
g_nMinute = 0;
g_nSecond = 0;
bShowSensor = false;
led0.setup(PIN_LED_WIFI, PWM_AP_FREQ, PWM_AP_CHANNEL);
led0.setDuty(20);
setupConfig();
setupStatus();
setupPins();
scanI2C();
setupSensor();
ui.setup();
ui.message(0, "WiFi...");
setupWiFi();
if (aht25.sensor() || aht10_0x39.sensor()) {
ui.message(4, "Sensor... OK!");
} else {
ui.message(4, "Sensor... None!");
}
ui.message(5, "ZCD...");
setupZCD();
ui.message(5, "ZCD... OK!");
ui.message(6, "Setup OK!");
//if (!isWiFiConnected) delay(3000);
ble.setupConnect(config.nBLESensorAddr, config.nBLESensorAddr2);
// Restore Status
restoreStatus();
// Create a task pinned to core 0
xTaskCreatePinnedToCore(
core0Task, // Function to run as a task
"Task0", // Task name
10240, // Stack size in words
NULL, // Task input parameter
0, // Priority of the task
&TaskHandle_0, // Task handle
0 // Core 0
);
DPRINTLN("Setup Completed\n========================\n");
}
// ======================================================================
//
// Setup
//
// ======================================================================
void setupConfig() {
config.load();
history.loadPID();
config.m_nDeviceType = THIS_DEVICE_TYPE;
if (config.m_nPublicPort == 3939)
config.m_nPublicPort = (uint16_t)(config.m_nChipId & 0xFFFF);
}
void setupWiFi() {
// Set WiFiEvent
WiFi.onEvent(WiFiEvent);
strncpy(BLE_SSID, config.ssid, sizeof(BLE_SSID));
strncpy(BLE_PW, config.pw, sizeof(BLE_PW));
// Connect WiFi for OTA
if (config.ssid[0] && config.pw[0]) {
#if defined(ESP32)
esp_wifi_set_max_tx_power(84);
#elif defined(ESP8266)
WiFi.setOutputPower(20.5f);
pinMode(16,OUTPUT);
digitalWrite(16, LOW);
int c = 0;
#endif
DPRINTF("BOOT: Connecting to WiFi: SSID: '%s', PW: '%s'\n", config.ssid, config.pw);
WiFi.mode(WIFI_STA);
WiFi.begin(config.ssid, config.pw);
unsigned long beginTime = millis();
while (WiFi.status() != WL_CONNECTED) {
delay(250);
DPRINT('.');
if (millis() - beginTime > 30000)
break;
}
DPRINTLN();
if (WiFi.status() == WL_CONNECTED) {
ledcWrite(PIN_LED_WIFI, PWM_FULL); // LED_OFF
ui.message(0, "WiFi...OK!");
DPRINTLN("WiFi - Connected at SETUP");
DPRINTF("WiFi - SSID(%s) PW(%s) IP(%s)\n", config.ssid, config.pw, WiFi.localIP().toString().c_str());
g_bWiFiHasBeenConnected = true;
setupPostWiFi(true);
} else {
DPRINTLN("WiFi - ** NOT ** Connected at SETUP.");
//WiFi.disconnect(false, true, 500);
}
}
}
void setupPostWiFi(bool bBoot = false) {
if (WiFi.status() == WL_CONNECTED) {
// Time
if (bBoot) ui.message(1, "Time...");
timeManager.begin();
vTaskDelay((bBoot ? 500 : 250)/portTICK_PERIOD_MS);
timeManager.checkNTPResponse();
if (bBoot) {
if (timeManager.hasNTPUpdate()) {
ui.message(1, "Time...OK!");
// OTA
DPRINTLN("Setup - TimeManager.begin()");
DPRINTLN("\n===============================\n");
DPRINTLN(" Trying OTA");
ui.message(2, "Update check...");
checkOTA(true);
ui.message(2, "Update check...OK!");
DPRINTLN(" OTA Process completed!");
DPRINTLN("===============================\n");
} else {
ui.message(2, "Update check SKIPPED!");
timeManager.sendNTPRequest();
}
}
// Host
if (bBoot) ui.message(3, "Server...");
host.Setup();
DPRINTLN("Setup - host.Setup()");
if (bBoot) ui.message(3, "Server...OK!");
g_bWiFiSetupExecuted = true;
}
}
void setupPins() {
pinMode(PIN_HEATER1, OUTPUT);
pinMode(PIN_HEATER2, OUTPUT);
digitalWrite(PIN_HEATER1, HEATER_OFF);
digitalWrite(PIN_HEATER2, HEATER_OFF);
//ledcAttachChannel(PIN_LED_WIFI, PWM_AP_FREQ, PWM_RESOLUTION, PWM_AP_CHANNEL);
//ledcWrite(PIN_LED_WIFI, PWM_FULL * 4 / 5); // Light Blink
ledcAttachChannel(PIN_MIST, PWM_MIST_FREQ, PWM_RESOLUTION, PWM_MIST_CHANNEL);
ledcWrite(PIN_MIST, PWM_OFF);
ledcAttachChannel(PIN_LED_HEATER1, PWM_1KHZ_FREQ, PWM_RESOLUTION, PWM_HEATER1_CHANNEL);
ledcAttachChannel(PIN_LED_HEATER2, PWM_1KHZ_FREQ, PWM_RESOLUTION, PWM_HEATER2_CHANNEL);
ledcAttachChannel(PIN_LIGHT, PWM_1KHZ_FREQ, PWM_RESOLUTION, PWM_LIGHT_CHANNEL);
ledcAttachChannel(PIN_MOTOR, PWM_25KHZ_FREQ, PWM_RESOLUTION, PWM_MOTOR_CHANNEL);
ledcAttachChannel(PIN_FAN, PWM_25KHZ_FREQ, PWM_RESOLUTION, PWM_FAN_CHANNEL);
ledcWrite(PIN_LED_HEATER1, PWM_FULL - PWM_OFF);
ledcWrite(PIN_LED_HEATER2, PWM_FULL - PWM_OFF);
ledcWrite(PIN_LIGHT, PWM_OFF);
ledcWrite(PIN_MOTOR, PWM_OFF);
ledcWrite(PIN_FAN, PWM_OFF);
}
void setupStatus() {
if (config.bStatusSaved) {
status = config.statusSave;
config.bStatusSaved = false;
}
// init sensor and counter
status.nTemp1 = 0;
status.nTemp2 = 0;
status.nTemp3 = 0;
status.nHumid1 = 0;
status.nHumid2 = 0;
status.zcdAC = 0;
status.zcdLoad = 0;
status.nFlags = 0x00;
}
void restoreStatus() {
if (isWiFiConnected()) {
if (timeManager.hasNTPUpdate()) {
time_t now;
time(&now);
uint32_t gap = (uint32_t)now - config.statusSave.now;
DPRINTF("Reboot in %.1f seconds\n", gap / 1000.0f);
if (gap < 60000 && config.bStatusSaved) {
status = config.statusSave;
status.nFlags |= (uint16_t)(config.statusSave.nFlags & 0xFF);
status.nLightDuty = 0;
config.bStatusSaved = false;
DPRINTLN(" Status Restored!");
}
}
}
}
void scanI2C() {
Wire.begin();
DPRINTLN("I2C - Scanning...");
for (byte addr = 1; addr < 127; addr++) {
Wire.beginTransmission(addr);
if (Wire.endTransmission() == 0) {
DPRINTF("I2C - Found device at address: 0x%02X\n", addr);
}
}
DPRINTLN(" Scanning Done.");
}
void setupSensor() {
// AHTx0
if (aht25.setup()) {
delay(82);
aht25.readSensor(millis());
DPRINTF("AHTx0 initialized successfully at 0x38. Temp: %.2f, Humid: %.2f%%\n",
aht25.getTemperature() / 100.0f, aht25.getHumidity() / 100.0f);
}
delay(10);
if (aht10_0x39.setup(0x39)) { //begin(PIN_SCL, PIN_SDA, AHT10_ADDRESS_0X39)) {
delay(82);
if (aht10_0x39.readSensor(millis())) {
//aht10_0x39.initBuffer();
}
DPRINTF("AHTx0 initialized successfully at 0x39. Temp: %.2f, Humid: %.2f%%\n",
aht10_0x39.getTemperature() / 100.0f , aht10_0x39.getHumidity() / 100.0f );
}
delay(10);
history.init(status.nTemp1, status.nHumid1);
if (!aht25.sensor() && !aht10_0x39.sensor()) {
DPRINTF("AHTx0 initialization failed. SCL:%d SDA:%d\n", PIN_SCL, PIN_SDA );
}
// Temp3 - NTC
ntc.setup(config.bNTCNegativePolarity);
status.nTemp3 = ntc.getTemp();
}

128
Task0.ino Normal file
View File

@ -0,0 +1,128 @@
#include "HermitCrab.h"
#include "Config.h"
#include "ConnectWiFi.h"
#include "UI.h"
#include <esp_task_wdt.h>
#include <BLEScan.h>
#include "TimeManager.h"
#include "WiFiHost.h"
#include "NTC_10K.h"
#include "AHT2x.h"
#define TAG_TASK0 "Task0"
extern bool g_bWiFiSetupExecuted;
void setup_BLE();
// ==================================================================================
//
// Core0 Loop - Connection and communication
//
// ==================================================================================
MY_IRAM_ATTR void core0Task(void *pvParameters) {
ESP_LOGI(TAG_TASK0,"Core 0 Task Started");
wl_status_t lastWiFiStatus = WL_DISCONNECTED;
unsigned long tickMillis = millis();
unsigned long tickSecond;
unsigned long lastTick = tickMillis / 1000;
unsigned long lastTickMillis = tickMillis;
unsigned long lastSensorUpdate1 = tickMillis;
unsigned long lastSensorUpdate2 = tickMillis + 2500;
uint16_t tick1000, lastTick1000 = tickMillis % 1000;;
uint16_t tick250, lastTick250 = tickMillis % 250;
uint8_t slot;
uint8_t lastSlot = tickMillis / 50;
esp_task_wdt_add(NULL); // NULL for the current task
ui.start();
ble.setupScan();
while (true) {
esp_task_wdt_reset();
tickMillis = millis();
tick250 = tickMillis % 250;
tick1000 = tickMillis % 1000;
tickSecond = tickMillis / 1000;
slot = tick1000 / 50;
//===============================================================================
// Loop top
// Once in a second loop
if (tick1000 != lastTick1000) {
if (slot != lastSlot) {
switch (slot) {
case 1:
case 6:
case 11:
case 16: // UI Display
ui.updateDisplayTop(tickSecond);
break;
case 2:
case 7:
case 12:
case 17: // NTC Temp Sensor
ntc.readSensor();
break;
case 3: // NTP - Time
if (isWiFiConnected()) {
if (timeManager.getTime(tickMillis)) {
ESP_LOGI(TAG_TASK0,"NTP time loaded: %s", printTime());
}
}
break;
case 4:
case 9:
case 14:
case 19:
ntc.readSensor();
break;
case 5: // Heartbeat
if (isWiFiConnected()) {
host.SendHeartBeat(tickMillis);
}
break;
case 8: // BLE
ble.loop(tickMillis);
break;
case 13: // ATH2x - 0x38
aht25.readSensor(tickMillis);
break;
case 15: // ATH0x - 0x39
aht10_0x39.readSensor(tickMillis);
break;
case 18: // UI Bottom
ui.updateDisplayBottom(tickSecond);
break;
default: // 0 10
break;
}
lastSlot = slot;
}
lastTick1000 = tick1000;
lastTick250 = tick250;
} // end of - if (tick1000 != lastTick1000)
// =====================================================
// Unconditional Loop
host.MonitorUDP();
// Loop end
//==========================================================================
lastTickMillis = tickMillis;
esp_task_wdt_reset();
} // end of - While(True)
ESP_LOGI(TAG_TASK0,"Core 0 Task Exit");
vTaskDelete(NULL);
}
// ==================================================================================
// End of Core0 Loop
// ==================================================================================

92
TimeManager.cpp Normal file
View File

@ -0,0 +1,92 @@
#include "HermitCrab.h"
#include "TimeManager.h"
#include "ConnectWiFi.h"
#include <sys/time.h>
#include "Config.h"
#define TAG_TIME "Time"
TimeManager timeManager;
void TimeManager::begin() {
setenv("TZ", "UTC", 1); // Set time zone to Asia/Seoul (UTC+9)
tzset(); // Apply the timezone setting
// Initialize UDP only if WiFi is connected
if (isWiFiConnected() && !udpInitialized) {
udpInitialized = udp.begin(localPort);
}
// Send an initial NTP request on startup
sendNTPRequest();
lastNTPRequestMillis = millis();
}
void TimeManager::Stop() {
if (udpInitialized) {
udp.stop();
}
}
bool TimeManager::getTime(unsigned long tickMillis) {
// Check WiFi Connection
if (!isWiFiConnected()) {
//ESP_LOGI(TAG_TIME,"TimeManager - getTime called while not connected");
if (udpInitialized) udpInitialized = false;
return false;
}
// Check if 30 minutes have passed since last NTP request
if (udpInitialized &&
((tickMillis - lastNTPRequestMillis >= NTP_REQUEST_INTERVAL) ||
!bHasNTPTime && (tickMillis - lastNTPRequestMillis >= NTP_FAILED_INTERNAL))) {
sendNTPRequest();
lastNTPRequestMillis = tickMillis;
}
// Check if there is an NTP response, and if so, update the system clock
return checkNTPResponse();
}
MY_IRAM_ATTR void TimeManager::sendNTPRequest() {
byte ntpPacket[48] = {0};
ntpPacket[0] = 0b11100011; // LI, Version, Mode
ntpPacket[1] = 0; // Stratum, or type of clock
ntpPacket[2] = 6; // Polling Interval
ntpPacket[3] = 0xEC; // Peer Clock Precision
udp.beginPacket(ntpServer, 123); // NTP requests are sent to port 123
udp.write(ntpPacket, 48);
udp.endPacket();
}
MY_IRAM_ATTR bool TimeManager::checkNTPResponse() {
if (udp.parsePacket() == 0) {
// No response yet
return false;
}
byte ntpBuffer[48];
udp.read(ntpBuffer, 48);
// Extract and set the time if response is valid
unsigned long epochTime = (unsigned long)ntpBuffer[40] << 24 |
(unsigned long)ntpBuffer[41] << 16 |
(unsigned long)ntpBuffer[42] << 8 |
(unsigned long)ntpBuffer[43];
epochTime -= 2208988800UL; // Convert from NTP to Unix time
// epochTime += (9 * 3600); // GMT+9
if (!bHasNTPTime) {
bHasNTPTime = true;
firstNTPTime = epochTime;
config.m_nEpochTime = epochTime;
}
setSystemClock(epochTime);
// ESP_LOGI(TAG_TIME,printTime());
return true;
}
void TimeManager::setSystemClock(unsigned long epochTime) {
struct timeval now;
now.tv_sec = epochTime;
now.tv_usec = 0;
settimeofday(&now, NULL);
}

33
TimeManager.h Normal file
View File

@ -0,0 +1,33 @@
#ifndef TIMEMANAGER_H
#define TIMEMANAGER_H
#include <WiFiUdp.h>
class TimeManager {
public:
void begin();
void Stop();
bool getTime(unsigned long tick); // Called every second to handle requests and updates
inline bool hasNTPUpdate() { return bHasNTPTime; };
inline unsigned long getFirstNTPTime() { return firstNTPTime; }
bool checkNTPResponse();
void sendNTPRequest();
private:
WiFiUDP udp;
const char* ntpServer = "pool.ntp.org";
const unsigned int localPort = 2390;
bool udpInitialized = false;
bool bHasNTPTime = false;
unsigned long firstNTPTime = 0;
unsigned long lastNTPRequestMillis = 0;
const unsigned long NTP_REQUEST_INTERVAL = 30 * 60 * 1000; // 30 minutes
const unsigned long NTP_FAILED_INTERNAL = 1 * 60 * 1000; // 1 minute
void setSystemClock(unsigned long epochTime);
};
extern TimeManager timeManager;
#endif // TIMEMANAGER_H

1152
UI.cpp Normal file

File diff suppressed because it is too large Load Diff

103
UI.h Normal file
View File

@ -0,0 +1,103 @@
#ifndef CUI_H
#define CUI_H
#include "SSD1306.h"
#define WIDTH SCREEN_WIDTH
#define HEIGHT SCREEN_HEIGHT
#define DISPLAY_PAGE_COUNT (SCREEN_HEIGHT / 8)
enum ITEM_MODE {
MODE_NORMAL,
MODE_SET,
MODE_CONFIG,
};
enum MESSAGE_MODE {
MODE_NONE,
MODE_NO_WIFI,
MODE_AC,
MODE_LOAD
};
typedef struct box_struct {
int16_t x, y;
uint16_t w, h;
} BOX_TYPE;
class CUI : public SSD1306 {
public:
// Constructor
//CUI(Adafruit_SSD1306 &display);
CUI();
void setup();
void start();
void updateDisplay(unsigned long tick);
void updateDisplayTop(unsigned long tick);
void updateDisplayBottom(unsigned long tick);
void message(uint8_t lineNo, char *szMessage);
void progress(uint8_t lineNo, uint8_t progress);
void loopButton(unsigned long tickMillis);
inline void setItemMode(uint16_t mode) { m_nItemMode = mode; };
// Shared variables with ISR
volatile bool bButtonSetChanged;
volatile bool bButtonUpChanged;
volatile bool bButtonDownChanged;
volatile unsigned long buttonSetChangeTime; // Time when button was pressed
volatile unsigned long buttonUpChangeTime; // Time when button was pressed
volatile unsigned long buttonDownChangeTime; // Time when button was pressed
private:
uint16_t m_nMessageMode, lastMessageMode;
uint16_t m_nMainMode, lastMainMode; // Temp, Humid, Clock
uint16_t m_nItemMode, lastItemMode; // Normal, Set, Config
uint16_t m_nItem, lastItem; // Temp, Hum, Heat ...
int16_t m_nValue, lastValue;
uint8_t *m_pUnit, *lastUnit;
uint16_t m_nD0, lastD0;
uint16_t m_nD1, lastD1;
uint16_t m_nD2, lastD2;
uint16_t m_nD3, lastD3;
uint8_t *m_pDUnit, *lastpDUnit;
BOX_TYPE boxMode, boxTitle, boxValue, boxUnit, boxD0, boxD1, boxD2, boxDot, boxD3, boxDUnit;
bool bDot;
bool bButtonChanged;
// Helper function to update digits
bool displayTop();
bool displayBottom();
//bool displayTemperature();
//bool displayHumidity();
//void displayClock() {};
void getBoundaries();
// Buttons
unsigned long buttonSetDownTime;
unsigned long buttonUpDownTime;
unsigned long buttonDownDownTime;
unsigned int buttonSetDownDuration;
unsigned int buttonUpDownDuration;
unsigned int buttonDownDownDuration;
bool bButtonSetUp; // Flag for menu button
bool bButtonSetDown; // Flag for menu button
bool bButtonUpUp; // Flag for up button
bool bButtonUpDown; // Flag for down button
bool bButtonDownUp; // Flag for up button
bool bButtonDownDown; // Flag for down button
void initButtonState();
void checkButtonStates(unsigned long tickMillis);
};
#endif
extern CUI ui;

491
UPnPClient.cpp Normal file
View File

@ -0,0 +1,491 @@
#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;
};

37
UPnPClient.h Normal file
View File

@ -0,0 +1,37 @@
#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();
};

885
WiFiHost.cpp Normal file
View File

@ -0,0 +1,885 @@
#include <lwip/sockets.h>
#undef INADDR_NONE
#include <lwip/netdb.h>
#include <WiFiServer.h>
#include <HTTPClient.h>
#include <WiFiClientSecure.h>
#include <esp_task_wdt.h>
#include <netinet/in.h> // For struct in_addr
#include <esp_task_wdt.h>
#include <esp_system.h>
#include "OTA.h"
#include "HermitCrab.h"
#include "Config.h"
#include "ConnectWiFi.h"
#include "TimeManager.h"
#include "History.h"
#include "zcd.h"
#include "AHT2x.h"
#include "WiFiHost.h"
#include "UPnPClient.h"
#define TAG_WIFI_HOST "WiFi Host"
#define TCP_PACKET_SIZE_MAX 1460
//WiFiServer wifiServer(SERVER_PORT);
//WiFiClient wifiClient;
CWiFiHost host;
CONFIG_TYPE configCopy;
void CWiFiHost::Setup()
{
// UDP Packet
packetUDP = {
.sig1 = SIGNATURE1,
.m_nSize = sizeof(UDP_PACKET),
.m_nMessage = UDP_MESSAGE::MESSAGE_HEARTBEAT,
.m_nDeviceType = config.m_nDeviceType,
.m_nResetReason = RESET_REASON_CHIP_POWER_ON,
.m_nChipID = config.m_nChipId,
.m_nDeviceID = 0,
//.m_nVersion = (uint32_t)atoll(&HC__VERSION[2]),
.dwIPAddress = 0,
.m_nPort = UDP_PORT,
//.m_MACAddress = "",
//.m_sDeviceName = "",
.status = {0},
.sig2 = SIGNATURE2 };
packetUDP.m_nVersion = (uint32_t)atoll(&HC__VERSION[2]);
strncpy((char *)packetUDP.m_MACAddress, WiFi.macAddress().c_str(), 17);
packetUDP.m_MACAddress[17] = 0;
strcpy(packetUDP.m_sDeviceName, config.m_sDeviceName);
// TCP Packet
hostPacket = {
.sig1 = SIGNATURE1,
.len = sizeof(TCP_PACKET),
.cmd = ENUM_COMMAND::CMD_HELLO,
.op = OPERATION_MODE::MODE_WAITING,
.time = 0,
.status = {0},
.sig2 = SIGNATURE2 };
clientPacket = hostPacket;
//if (m_nPublicPort == 0)
{
if (config.m_nPublicPort == 0)
config.m_nPublicPort = (uint16_t)(config.m_nChipId & 0xFFFF);
m_nPublicPort = config.m_nPublicPort;
}
wifiServer = WiFiServer(SERVER_PORT, 1);
wifiExternal = WiFiServer(m_nPublicPort, 1);
//wifiStatus = WIFI_NOT_CONNECTED;
m_nDataSend_sent = 0;
m_nDataReceive_size = 0;
m_pDataSend_data = nullptr;
m_nDataReceive_received = 0;
m_nDataReceive_size = 0;
m_pDataReceive_data = nullptr;
externalServerIP = IPAddress((uint32_t) 0UL);
//sockfd = -1;
//connectStartTime = 0;
//isConnecting = false;
// Operation
m_nMode = MODE_WAITING;
m_bHelloSent = false;
m_bSendHistoryPending = false;
m_nLastReceivedTime =
m_nLastHeartBeatSentTime =
m_nLastUDPBroadcastTime = millis();
m_bClientConnected = false;
m_dwPublicIP = 0;
wifiClient.stop();
if (isWiFiConnected())
{
// UPnP Client
{
uint32_t ip = WiFi.gatewayIP();
uint16_t port = m_nPublicPort;
CUpnpClient upnp;
if (upnp.registerUPnP(&ip, &port)) {
status.nFlags |= FLAG_UPNP;
} else {
status.nFlags &= ~FLAG_UPNP;
}
if (ip != 0)
m_dwPublicIP = ip;
if (port != m_nPublicPort) {
config.m_nPublicPort = port;
config.save();
m_nPublicPort = port;
}
}
// Server
wifiServer.begin(SERVER_PORT, 1);
wifiExternal.begin(m_nPublicPort, 1);
m_nLastReceivedTime = millis();
m_bClientConnected = false;
// UDP
packetUDP.dwIPAddress = WiFi.localIP();
udpLocal.begin(UDP_PORT);
udpExternal.begin(UDP_EXTERNAL_PORT);
if (m_cExternalServerIPAddress == IPAddress((uint32_t) 0l)) {
if (WiFi.hostByName("visionsoft.kr", m_cExternalServerIPAddress)) {
DPRINTF("WiFi - ExternalServer IP resolved as \"%s\"\n", m_cExternalServerIPAddress.toString().c_str());
} else {
m_cExternalServerIPAddress = IPAddress((uint32_t) 0);
DPRINTF("WiFi - ExternalServer IP NOT resolved\n");
}
}
}
}
MY_IRAM_ATTR void CWiFiHost::Stop() {
CloseConnection();
// Stop server
wifiServer.stop();
wifiExternal.stop();
m_bClientConnected = false;
// Stop Client
if (wifiClient)
wifiClient.stop();
// Stop UDP
udpLocal.stop(); // or udpLocal.end(); depending on your preference
udpExternal.stop(); // or udpExternal.end(); depending on your preference
}
MY_IRAM_ATTR void CWiFiHost::CloseConnection()
{
if (wifiClient && wifiClient.connected()) wifiClient.stop();
m_bClientConnected = false;
m_nMode = MODE_WAITING;
}
IRAM_ATTR void CWiFiHost::Loop(unsigned long clock)
{
static unsigned long lastReceivedTime = 0;
if (!isWiFiConnected()) return;
switch (m_nMode) {
case MODE_WAITING: // Expecting connection from clients
if (m_bClientConnected)
{
ESP_LOGI(TAG_WIFI_HOST,"Host: dropping connection for not connected");
if (wifiClient && wifiClient.connected()) {
ESP_LOGI(TAG_WIFI_HOST,"Host: stopping wifi client");
wifiClient.stop();
}
m_bClientConnected = false;
}
// Accept from internal XOR external port
wifiClient = wifiServer.accept();
if (!wifiClient || !wifiClient.connected()) {
wifiClient = wifiExternal.accept();
}
if (wifiClient && wifiClient.connected())
{
ESP_LOGI(TAG_WIFI_HOST,"Host: Connection Accepted");
wifiClient.setNoDelay(true);
m_nLastReceivedTime = clock;
m_bClientConnected = true;
m_bHelloSent = false;
m_nMode = MODE_PACKET;
// LED
ledcWrite(PIN_LED_WIFI, PWM_FULL - 10); // Amost ON
}
break;
case MODE_PACKET:
// Client Connected
if (m_bClientConnected && wifiClient.connected())
{
CheckClient(clock);
if (m_bClientConnected) {
if (clock - m_nLastReceivedTime > 60000)
{
ESP_LOGI(TAG_WIFI_HOST,"Host: dropping connection for no HB in 60 seconds");
wifiClient.stop();
m_bClientConnected = false;
}
// Send HeartBeat
if (clock - m_nLastHeartBeatSentTime >= 1000) {
SendHeartBeat();
m_nLastHeartBeatSentTime = clock;
}
}
}
if (!m_bClientConnected || !wifiClient.connected()) {
m_nMode = MODE_WAITING;
// LED
ledcWrite(PIN_LED_WIFI, PWM_FULL - 2); // Almost Off
}
break;
case MODE_SEND:
if (SendData(clock)) {
if (m_bSendHistoryPending) {
// Mark pending to send the second part: from the start to head-1
//SendData(history.getRingData2() /* &ring[0] */, sizeof(STATUS_TYPE) * head);
//m_bSendHistoryPending = true;
//m_nPendingHistoryCount = history.getHead();
m_bSendHistoryPending = false;
if (m_nPendingHistoryCount > 0) {
ESP_LOGI(TAG_WIFI_HOST,"WfFi Host - SendData - 2nd part of History (%d)\n", m_nPendingHistoryCount);
SendData(history.getRingData2(), sizeof(STATUS_TYPE) * m_nPendingHistoryCount);
m_nPendingHistoryCount = 0;
}
} else {
m_nMode = MODE_PACKET;
}
}
break;
case MODE_RECV:
if (ReceiveData(clock)) m_nMode = MODE_PACKET;
break;
}
}
MY_IRAM_ATTR void CWiFiHost::SendHeartBeat(unsigned long clock) {
if (!isWiFiConnected()) {
//ESP_LOGI(TAG_WIFI_HOST,"WiFiHost - SendHeartBeat() called while not connected!");
return;
}
// Send Heartbeats to external server and to local devices
if (clock - m_nLastUDPBroadcastTime > 1000) {
static int count = 55;
// UDP Heartbeat
if (++count >= 60) {
// External Heartbeat
UDP_CONFIG_TYPE pktConfig;
pktConfig.udp = packetUDP;
pktConfig.udp.m_nPort = m_nPublicPort;
pktConfig.udp.dwIPAddress = m_dwPublicIP;
pktConfig.udp.status = status;
if (((uint32_t)m_cExternalServerIPAddress) != (uint32_t)0l)
udpExternal.beginPacket(m_cExternalServerIPAddress, (uint16_t)UDP_EXTERNAL_PORT);
else
udpExternal.beginPacket("visionsoft.kr", (uint16_t)UDP_EXTERNAL_PORT);
if (config.bConfigSaved) {
// Config is save locally. Save it on cloud
config.bConfigSaved = false;
pktConfig.udp.m_nMessage = MESSAGE_CONFIG_SAVE;
pktConfig.con = config;
udpExternal.write((uint8_t*)&pktConfig, sizeof(pktConfig));
ESP_LOGI(TAG_WIFI_HOST,"HeartBeat Packet *Config Save* sent out to external Server");
} else {
// Send only UDP packet
udpExternal.write((uint8_t*)&(pktConfig.udp), sizeof(UDP_PACKET));
}
udpExternal.endPacket();
count = 0;
} else if (!m_bClientConnected) {
// Local AP broadcast
packetUDP.m_nMessage = UDP_MESSAGE::MESSAGE_HEARTBEAT;
packetUDP.dwIPAddress = WiFi.localIP();
packetUDP.m_nPort = SERVER_PORT;
strcpy(packetUDP.m_sCompanyName, COMPANY_NAME);
strcpy(packetUDP.m_sService, SERVICE_NAME);
udpLocal.beginPacket((IPAddress)0xFFFFFFFF, (uint16_t)UDP_PORT);
udpLocal.write((uint8_t*)&packetUDP, sizeof(UDP_PACKET));
udpLocal.endPacket();
}
m_nLastUDPBroadcastTime = clock;
}
}
MY_IRAM_ATTR void CWiFiHost::MonitorUDP() {
// Listen for UDP External port
IPAddress ip;
int size = udpExternal.parsePacket();
if (size >= sizeof(packetUDP)) {
if ((udpExternal.read((char *) &packetUDP, sizeof(packetUDP)) == sizeof(packetUDP)) &&
packetUDP.sig1 == SIGNATURE1 &&
packetUDP.sig2 == SIGNATURE2 &&
packetUDP.m_nChipID == config.m_nChipId) {
switch (packetUDP.m_nMessage) {
case MESSAGE_IP: // Extenal IP and Port set
// if (packetUDP.m_nChipID == config.m_nChipId) {
// m_dwPublicIP = packetUDP.dwIPAddress;
// m_nPublicPort = packetUDP.m_nPort;
// ip = IPAddress(m_dwPublicIP);
// ESP_LOGI(TAG_WIFI_HOST,"External IP(%s) and Port(%d)\n", ip.toString().c_str(), m_nPublicPort);
// } else {
// ESP_LOGI(TAG_WIFI_HOST,"External Server Response for other device Received\n");
// }
break;
case MESSAGE_CONFIG_SEND:
if (size == sizeof(UDP_PACKET) + sizeof(CONFIG_STRUCT)) {
if (udpExternal.read((char*) &configCopy, sizeof(configCopy))) {
ESP_LOGI(TAG_WIFI_HOST,"CONFIG received from the external DB server\n");
}
} else {
char *buffer[256];
while((size = udpExternal.parsePacket()) > 0) {
// Discard any leftover data
udpExternal.read((char*) &buffer, sizeof(buffer));
}
}
break;
case MESSAGE_QUERY_IP:
// if (m_nMode == MODE_WAITING) {
// // Get sender's IP and port
// externalServerIP = udpExternal.remoteIP();
// uint16_t senderPort = udpExternal.remotePort();
// ESP_LOGI(TAG_WIFI_HOST,"External Server - MESSAGE_QUERY_IP from %s:%d\n", externalServerIP.toString().c_str(), senderPort);
// m_nMode = MODE_EXTERNAL_SERVER;
// }
break;
case MESSAGE_RESET:
Restart();
break;
default:
break;
}
}
}
}
IRAM_ATTR void CWiFiHost::CheckClient(unsigned long clock)
{
bool bLED = false;
static TCP_PACKET cpkt;
int available = wifiClient.available();
if (available >= sizeof(TCP_PACKET)) {
uint8_t* pC = (uint8_t*)&cpkt;
int count = wifiClient.readBytes(pC, sizeof(TCP_PACKET));
if (count == sizeof(TCP_PACKET)) {
// Now we have a full packet size data
if (cpkt.sig1 == SIGNATURE1 &&
cpkt.sig2 == SIGNATURE2 &&
cpkt.len == sizeof(TCP_PACKET)) {
// Process the completed PACKET
ProcessPacket(cpkt);
m_nLastReceivedTime = clock;
return;
}
// Invalid Packet - remove the first byte off from the buff
ESP_LOGI(TAG_WIFI_HOST,"Invalid Packet %s%s size: %d/%d\n",
cpkt.sig1 == SIGNATURE1 ? "SIG1 OK" : "",
cpkt.sig2 == SIGNATURE2 ? "SIG2 OK" : "",
cpkt.len, sizeof(TCP_PACKET));
// Shift the buffer data off by 1 byte for the next cycle
//buffIndexC = sizeof(TCP_PACKET) - 1;
//memcpy(pC, &pC[1], buffIndexC);
while (m_bClientConnected &&
wifiClient.connected() &&
wifiClient.readBytes((uint8_t *) &cpkt, sizeof(cpkt)) > 0) {
};
}
}
}
IRAM_ATTR void CWiFiHost::ProcessPacket(TCP_PACKET& pkt)
{
switch (pkt.cmd)
{
// System
case CMD_HEARTBEAT:
m_nLastReceivedTime = millis();
//ESP_LOGI(TAG_WIFI_HOST,"H");
break;
case CMD_HELLO:
case CMD_HELLO_DEBUG:
{
// Send the Config Data to PC
// Send Packet Back
ESP_LOGI(TAG_WIFI_HOST,"WiFi - HELLO received");
pkt.u16[0] = SIGNATURE1;
pkt.n16[1] = sizeof(CONFIG_TYPE);
pkt.u16[2] = SIGNATURE2;
config.m_nChipId;
SendPacket(pkt);
// Send Data Packets
ESP_LOGI(TAG_WIFI_HOST,"WiFi - Config Send pending...");
int sent = SendData((uint8_t*)&config, sizeof(CONFIG_TYPE));
m_bHelloSent = true;
}
break;
case CMD_DROP_CONNECTION:
wifiClient.stop();
m_bClientConnected = false;
m_nMode = MODE_WAITING;
ESP_LOGI(TAG_WIFI_HOST,"WiFI - Client requets to DROP CONNECTION");
break;
case CMD_RESET_REASON:
pkt.n16[0] = esp_reset_reason();
SendPacket(pkt);
break;
case CMD_SAVE_RESTART:
if (pkt.u16[0] == SIGNATURE1 &&
pkt.u16[1] == SIGNATURE2) {
yield();
Restart();
}
break;
case CMD_RESET_RESTART:
if (pkt.u16[0] == SIGNATURE1 &&
pkt.u16[1] == SIGNATURE2) {
pkt.cmd = CMD_DROP_CONNECTION;
SendPacket(pkt);
yield();
Restart();
}
break;
case CMD_SEND_HISTORY:
if (pkt.u16[0] == SIGNATURE1 && pkt.u16[1] == SIGNATURE2) {
int16_t count = history.getRingCount();
if (count > 0 ) {
// inform client the count of history
pkt.op = count;
SendPacket(pkt);
int16_t size = history.getRingSize();
if (count < size) {
ESP_LOGI(TAG_WIFI_HOST,"WfFi Host - SendData - Whold part of History (%d)\n", count);
SendData(history.getRingData1()/* &ring[tail] */, sizeof(STATUS_TYPE) * count);
} else {
int count1st = size - history.getRingTail();
ESP_LOGI(TAG_WIFI_HOST,"WfFi Host - SendData - 1st part of History Total(%d), 1st(%d) H(%d) T(%d)\n",
count, count1st, history.getRingHead(), history.getRingTail());
SendData(history.getRingData1(), sizeof(STATUS_TYPE) * count1st),
// Mark pending to send the second part: from the start to head-1
//SendData(history.getRingData2() /* &ring[0] */, sizeof(STATUS_TYPE) * head);
m_bSendHistoryPending = true;
m_nPendingHistoryCount = history.getRingHead();
}
}
}
break;
case CMD_PAUSE:
break;
case CMD_RESUME:
break;
case CMD_RESET_SENSOR:
aht25.setScanFlag(true);
aht10_0x39.setScanFlag(true);
break;
// Config
case CMD_INIT_CONFIG:
if (pkt.u16[0] == SIGNATURE1 &&
pkt.u16[1] == sizeof(CONFIG_STRUCT) &&
pkt.u16[2] == SIGNATURE2) {
config.init();
config.save();
}
break;
case CMD_LOAD_CONFIG:
if (pkt.u16[0] == SIGNATURE1 &&
pkt.u16[1] == sizeof(CONFIG_STRUCT) &&
pkt.u16[2] == SIGNATURE2) {
config.load();
history.loadPID();
}
break;
case CMD_SAVE_CONFIG:
if (pkt.u16[0] == SIGNATURE1 &&
pkt.u16[1] == sizeof(CONFIG_STRUCT) &&
pkt.u16[2] == SIGNATURE2) {
config.save();
}
break;
case CMD_RECV_CONFIG:
// Receive Confif Data from PC
if (pkt.u16[0] == SIGNATURE1 &&
pkt.u16[1] == sizeof(CONFIG_STRUCT) &&
pkt.u16[2] == SIGNATURE2) {
ReceiveData((uint8_t *)&configCopy, sizeof(CONFIG_TYPE));
m_bReceiveConfigPending = true;
ESP_LOGI(TAG_WIFI_HOST,"WiFi - Receive Config initiated...");
}
break;
case CMD_SEND_CONFIG:
if (pkt.u16[0] == SIGNATURE1 &&
pkt.u16[1] == sizeof(CONFIG_STRUCT) &&
pkt.u16[2] == SIGNATURE2) {
SendPacket(pkt);
SendData((const uint8_t *)&config, (unsigned int) sizeof(config));
ESP_LOGI(TAG_WIFI_HOST,"WiFi - Send Config initiated...");
}
break;
case CMD_SEND_CONFIG_SERVER:
case CMD_LOAD_CONFIG_SERVER:
break;
// PID
case CMD_INIT_PID_PARAM:
history.savePID();
break;
case CMD_LOAD_PID_PARAM:
history.loadPID();
break;
case CMD_SAVE_PID_PARAM:
history.savePID();
break;
case CMD_SET_PID:
config.Kp_Temp1 = pkt.f[0];
config.Kd_Temp1 = pkt.f[1];
config.LR_Temp1 = pkt.f[2];
config.Kp_Humidity = pkt.f[3];
config.Kd_Humidity = pkt.f[4];
config.LR_Humidity = pkt.f[5];
history.loadPID();
break;
case CMD_GET_PID:
pkt.f[0] = config.Kp_Temp1;
pkt.f[1] = config.Kd_Temp1;
pkt.f[2] = config.LR_Temp1;
pkt.f[3] = config.Kp_Humidity;
pkt.f[4] = config.Kd_Humidity;
pkt.f[5] = config.LR_Humidity;
SendPacket(pkt);
break;
// Control
case CMD_SET_CONTROL:
config.bSmartControl = pkt.by[0] ? true : false;
config.bNightControl = pkt.by[1] ? true : false;
config.bControlTemperature = pkt.by[2] ? true : false;
config.bControlHumidity = pkt.by[3] ? true : false;
break;
case CMD_GET_CONTROL:
pkt.by[0] = config.bSmartControl ? 0xFF : 0;
pkt.by[1] = config.bNightControl ? 0xFF : 0;
pkt.by[2] = config.bControlTemperature ? 0xFF : 0;
pkt.by[3] = config.bControlHumidity ? 0xFF : 0;
SendPacket(pkt);
break;
// Operation
case CMD_SET_TEMP_TARGET:
config.nTempTarget = pkt.u16[0];
ESP_LOGI(TAG_WIFI_HOST,"WiFi - TempTarget changed to: %d\n", pkt.u16[0]);
break;
case CMD_SET_TEMP_TARGET_NIGHT:
config.nTempTargetNight = pkt.u16[0];
ESP_LOGI(TAG_WIFI_HOST,"WiFi - TempTargetNight changed to: %d\n", pkt.u16[0]);
break;
case CMD_SET_HUMID_TARGET:
config.nHumidTarget = pkt.u16[0];
ESP_LOGI(TAG_WIFI_HOST,"WiFi - HumidTarget changed to: %d\n", pkt.u16[0]);
break;
case CMD_SET_SENSOR_OFFSET:
config.nTemp1Offset = pkt.n16[0];
config.nHumid1Offset = pkt.n16[1];
if (pkt.n16[2] > 1) {
config.nTemp2Offset = pkt.n16[2];
config.nHumid2Offset = pkt.n16[3];
if (pkt.n16[2] > 2)
config.nTemp3Offset = pkt.n16[4];
}
ESP_LOGI(TAG_WIFI_HOST,"WiFi - SensorOffset changed to: %d, %d\n", pkt.n16[0], pkt.n16[1]);
break;
case CMD_SET_AC1_PARAM:
config.ac1 = pkt.device;
break;
case CMD_SET_AC2_PARAM:
config.ac2 = pkt.device;
break;
case CMD_SET_MIST_PARAM:
config.mist = pkt.device;
break;
case CMD_SET_FAN_PARAM:
config.fan = pkt.device;
break;
case CMD_SET_MOTOR_PARAM:
config.motor = pkt.device;
break;
case CMD_SET_LIGHT_PARAM:
config.light = pkt.device;
break;
// Status
case CMD_SET_HEATER1_DUTY:
status.nHeater1Duty = pkt.u16[0];
if (pkt.u16[1])
status.nFlags |= FLAG_MANUAL_HEATER1;
else
status.nFlags &= ~FLAG_MANUAL_HEATER1;
if (status.nHeater1Duty == 0) {
setHeater1Duty(0);
}
break;
case CMD_SET_HEATER2_DUTY:
status.nHeater2Duty = pkt.u16[0];
if (pkt.u16[1])
status.nFlags |= FLAG_MANUAL_HEATER2;
else
status.nFlags &= ~FLAG_MANUAL_HEATER2;
if (status.nHeater2Duty == 0) {
setHeater2Duty(0);
}
break;
case CMD_SET_MIST_DUTY:
status.nMistDuty = pkt.u16[0];
if (pkt.u16[1])
status.nFlags |= FLAG_MANUAL_MIST;
else
status.nFlags &= ~FLAG_MANUAL_MIST;
break;
case CMD_SET_FAN_DUTY:
status.nFanDuty = pkt.u16[0];
if (pkt.u16[1])
status.nFlags |= FLAG_MANUAL_FAN;
else
status.nFlags &= ~FLAG_MANUAL_FAN;
break;
case CMD_SET_MOTOR_DUTY:
status.nMotorDuty = pkt.u16[0];
if (pkt.u16[1])
status.nFlags |= FLAG_MANUAL_MOTOR;
else
status.nFlags &= ~FLAG_MANUAL_MOTOR;
break;
case CMD_SET_LIGHT_DUTY:
status.nLightTargetDuty = pkt.u16[0];
if (pkt.u16[1])
status.nFlags |= FLAG_MANUAL_LIGHT;
else
status.nFlags &= ~FLAG_MANUAL_LIGHT;
break;
// Manual Operation
case CMD_SET_MANUAL_HEATER1:
if (pkt.u16[0])
status.nFlags |= FLAG_MANUAL_HEATER1;
else
status.nFlags &= ~FLAG_MANUAL_HEATER1;
break;
case CMD_SET_MANUAL_HEATER2:
if (pkt.u16[0])
status.nFlags |= FLAG_MANUAL_HEATER2;
else
status.nFlags &= ~FLAG_MANUAL_HEATER2;
break;
case CMD_SET_MANUAL_MIST:
if (pkt.u16[0])
status.nFlags |= FLAG_MANUAL_MIST;
else
status.nFlags &= ~FLAG_MANUAL_MIST;
break;
case CMD_SET_MANUAL_FAN:
if (pkt.u16[0])
status.nFlags |= FLAG_MANUAL_FAN;
else
status.nFlags &= ~FLAG_MANUAL_FAN;
break;
case CMD_SET_MANUAL_LIGHT:
if (pkt.u16[0])
status.nFlags |= FLAG_MANUAL_LIGHT;
else
status.nFlags &= ~FLAG_MANUAL_LIGHT;
break;
// Time
case CMD_SET_TIME_NIGHT:
config.nNightStartHour = pkt.u16[0];
config.nNightStartMin = pkt.u16[1];
config.nNightEndHour = pkt.u16[2];
config.nNightEndMin = pkt.u16[3];
break;
case CMD_SET_WIFI_CLIENT_DISPLAY:
config.m_nDisplayTempHigh = pkt.n16[0];
config.m_nDisplayTempLow = pkt.n16[1];
config.m_nDisplayTime = pkt.n16[2];
config.m_fShowRealTime = pkt.n16[3];
config.m_fShowHistory = pkt.n16[4];
ESP_LOGI(TAG_WIFI_HOST,"WiFi - Client Display Settings changed.");
break;
default:
ESP_LOGI(TAG_WIFI_HOST,"WiFi - Packet Received Type: %d\n", pkt.cmd);
break;
}
}
IRAM_ATTR int CWiFiHost::SendPacket(TCP_PACKET& pkt)
{
size_t sent = 0;
if (m_bClientConnected && wifiClient && wifiClient.connected())
{
sent = wifiClient.write((char*)&pkt, sizeof(TCP_PACKET));
}
return sent;
}
IRAM_ATTR size_t CWiFiHost::SendData(const uint8_t* data, size_t size) {
if (data != nullptr) {
m_nMode = MODE_SEND;
m_pDataSend_data = (char *) data;
m_nDataSend_size = size;
m_nDataSend_sent = 0;
ESP_LOGI(TAG_WIFI_HOST,"WfFi Host - SendData(size: %d)\n", size);
return size;
}
return 0;
}
IRAM_ATTR bool CWiFiHost::SendData(unsigned long clock)
{
if (m_nDataSend_sent < m_nDataSend_size)
{
bool connected = wifiClient.connected();
if (m_bClientConnected && wifiClient && connected) {
size_t count = m_nDataSend_size - m_nDataSend_sent;
if (count > TCP_PACKET_SIZE_MAX) count = TCP_PACKET_SIZE_MAX;
//size_t avail = wifiClient.availableForWrite();
//if (avail < count) {
// ESP_LOGI(TAG_WIFI_HOST,"WiFiHost - SendData() avail(%d) is less than count(%d)\n", avail, count);
// //count = avail;
//}
if (count > 0) {
int16_t sentCount = wifiClient.write(&m_pDataSend_data[m_nDataSend_sent], count);
if (sentCount != count) {
ESP_LOGI(TAG_WIFI_HOST,"WiFiHost - SendData() sent(%d) is not count(%d)\n", sentCount, count);
if (sentCount <= 0)
yield();
}
m_nDataSend_sent += sentCount;
ESP_LOGI(TAG_WIFI_HOST,"WiFiHost - SendData() size(%d) sent(%d) total_sent(%d)\n", m_nDataSend_size, count, m_nDataSend_sent);
}
//else {
// ESP_LOGI(TAG_WIFI_HOST,"WiFiHost - SendData() avail is 0");
// yield();
//}
} else {
m_nMode = MODE_WAITING;
m_pDataSend_data = nullptr;
m_nDataSend_size = 0;
m_nDataSend_sent = 0;
ESP_LOGI(TAG_WIFI_HOST," SendData: Connection lost - reset sendData()!");
return true;
}
}
if (m_nDataSend_sent == m_nDataSend_size) {
ESP_LOGI(TAG_WIFI_HOST, " SendDdata: DataSend completed!");
return true;
}
return false;
}
IRAM_ATTR size_t CWiFiHost::ReceiveData(uint8_t* data, size_t size)
{
m_nMode = MODE_RECV;
m_pDataReceive_data = (char *) data;
m_nDataReceive_size = size;
m_nDataReceive_received = 0;
#ifdef ESP8266
digitalWrite(PIN_EXTRA, LED_ON);
#endif
return size;
}
IRAM_ATTR bool CWiFiHost::ReceiveData(unsigned long clock)
{
// Receive Data
size_t nSize = 0;
if (m_nDataReceive_received < m_nDataReceive_size)
{
if (m_bClientConnected && wifiClient && wifiClient.connected()) {
int16_t count = m_nDataReceive_size - m_nDataReceive_received;
if (count > TCP_PACKET_SIZE_MAX) count = TCP_PACKET_SIZE_MAX;
nSize = wifiClient.readBytes(&m_pDataReceive_data[m_nDataReceive_received], count);
if (nSize > 0) {
m_nLastReceivedTime = clock;
m_nDataReceive_received += nSize;
}
} else {
m_nMode = MODE_WAITING;
m_pDataReceive_data = nullptr;
m_nDataReceive_size = 0;
m_nDataReceive_received = 0;
ESP_LOGI(TAG_WIFI_HOST," SendData: Connection lost - reset receiveData()!");
return true;
}
}
// Check for any pending action
if (m_nDataReceive_received == m_nDataReceive_size) {
if (m_bReceiveConfigPending && m_pDataReceive_data == (char *)&configCopy) {
config = configCopy;
history.loadPID();
ESP_LOGI(TAG_WIFI_HOST,"WiFi - Config Received");
m_bReceiveConfigPending = false;
}
ESP_LOGI(TAG_WIFI_HOST," ReceiveData: Data Receive Completed!");
return true;
}
if (nSize <= 0 && clock - m_nLastReceivedTime > 5000) {
ESP_LOGI(TAG_WIFI_HOST," ReceiveData: TimeOut Abort!");
return true;
}
return false;
}
IRAM_ATTR void CWiFiHost::SendHeartBeat() {
if (m_bHelloSent) {
hostPacket.cmd = CMD_HEARTBEAT;
time_t now = time(NULL); // Get current time in seconds
status.now = (uint32_t) time(NULL);
//status.uptime = now - timeManager.getFirstNTPTime();
hostPacket.status = status;
SendPacket(hostPacket);
}
}
MY_IRAM_ATTR void CWiFiHost::Restart() {
if (isWiFiConnected()) {
if (m_bClientConnected && m_bClientConnected && wifiClient && wifiClient.connected()) {
hostPacket.cmd = CMD_DROP_CONNECTION;
SendPacket(hostPacket);
}
}
vTaskDelay(500/portTICK_PERIOD_MS);
// stop all network sockets
Stop(); // Stop Sockets
vTaskDelay(50/portTICK_PERIOD_MS);
// Turn Off
setHeater1Duty(0);
setHeater2Duty(0);
ledcWrite(PIN_MIST, 0);
ledcWrite(PIN_FAN, 0);
ledcWrite(PIN_MOTOR, 0);
ledcWrite(PIN_LIGHT, 0);
vTaskDelay(50/portTICK_PERIOD_MS);
config.statusSave = status;
config.save();
vTaskDelay(50/portTICK_PERIOD_MS);
ESP.restart();
}

284
WiFiHost.h Normal file
View File

@ -0,0 +1,284 @@
#ifndef __WIFI_HOST_H
#define __WIFI_HOST_H
#include "HermitCrab.h"
#ifdef ESP8266
#include <ESP8266WiFi.h>
#else
#include <WiFi.h>
#endif
#include <WiFiUdp.h>
#define UDP_PORT 4949
#define SERVER_PORT 3939
#define UDP_EXTERNAL_PORT (UDP_PORT + 1)
#define TCP_EXTERNAL_PORT (SERVER_PORT + 1)
#define CONNECT_TIMEOUT_MS 5000
enum UDP_MESSAGE {
MESSAGE_HEARTBEAT,
MESSAGE_IP,
MESSAGE_NOTFOUND,
MESSAGE_CONFIG_SAVE,
MESSAGE_CONFIG_SEND,
MESSAGE_AP,
MESSAGE_QUERY_IP,
MESSAGE_UPDATE_STATUS,
MESSAGE_UPDATE_CONFIG,
MESSAGE_COUNT,
MESSAGE_RESET = UDP_EXTERNAL_PORT,
};
enum OPCODE_TYPE {
OP_RECV_ONLY,
OP_RECV_SAVE,
};
enum OPERATION_MODE {
MODE_WAITING,
MODE_PACKET,
MODE_SEND,
MODE_RECV,
MODE_EXTERNAL_SERVER
};
enum ENUM_COMMAND
{
// Command
CMD_HEARTBEAT = 1, // 1 HeartBeat - prevents disconnect from the host
// Connection
CMD_HELLO = 101, // 0 Hello
CMD_DROP_CONNECTION,
CMD_RESET_REASON,
// Device Command
CMD_SAVE_RESTART = 201,
CMD_RESET_RESTART,
CMD_SEND_HISTORY,
CMD_PAUSE,
CMD_RESUME,
CMD_RESET_SENSOR,
CMD_RESET_DISPLAY,
// OTA Update
CMD_UPDATE_CHECK = 301,
CMD_UPDATE_AVAILABLE,
CMD_UPDATE_FORCED,
// Config
CMD_INIT_CONFIG = 1201,
CMD_LOAD_CONFIG,
CMD_SAVE_CONFIG,
CMD_SEND_CONFIG,
CMD_RECV_CONFIG,
CMD_SEND_CONFIG_SERVER,
CMD_LOAD_CONFIG_SERVER,
// PID
CMD_INIT_PID_PARAM = 1301,
CMD_LOAD_PID_PARAM,
CMD_SAVE_PID_PARAM,
CMD_SET_PID,
CMD_GET_PID,
// Control
CMD_SET_CONTROL = 1401,
CMD_GET_CONTROL,
// Operation
CMD_SET_TEMP_TARGET = 1501,
CMD_SET_TEMP_TARGET_NIGHT,
CMD_SET_HUMID_TARGET,
CMD_SET_SENSOR_OFFSET,
CMD_SET_AC1_PARAM,
CMD_SET_AC2_PARAM,
CMD_SET_MIST_PARAM,
CMD_SET_FAN_PARAM,
CMD_SET_MOTOR_PARAM,
CMD_SET_LIGHT_PARAM,
// Status
CMD_SET_HEATER1_DUTY = 2101,
CMD_SET_HEATER2_DUTY,
CMD_SET_MIST_DUTY,
CMD_SET_FAN_DUTY,
CMD_SET_FAN_DUTY_AUTO,
CMD_SET_MOTOR_DUTY,
CMD_SET_MOTOR_DUTY_AUTO,
CMD_SET_LIGHT_DUTY,
// Manual Operations
CMD_SET_MANUAL_HEATER1 = 2201,
CMD_SET_MANUAL_HEATER2,
CMD_SET_MANUAL_MIST,
CMD_SET_MANUAL_FAN,
CMD_SET_MANUAL_MOTOR,
CMD_SET_MANUAL_LIGHT,
// Time
CMD_SET_TIME_HEATER1 = 2301,
CMD_SET_TIME_HEATER2,
CMD_SET_TIME_MIST,
CMD_SET_TIME_FAN,
CMD_SET_TIME_MOTOR,
CMD_SET_TIME_LIGHT,
CMD_SET_TIME_NIGHT,
// UI
CMD_SET_WIFI_CLIENT_DISPLAY = 2401, // Graph TempHigh, TempLow, TimeScale etc.
CMD_SET_OLED_CONTRAST,
CMD_SET_OLED_DISPLAY_CHECKAC,
// Debugging
CMD_HELLO_DEBUG = 9101,
CMD_SHOW_CODE,
COMPUTE_TIME_MAX,
CMD_INVALID = 9999
};
#pragma pack(push) /* push current alignment to stack */
#pragma pack(1) /* set alignment to 1 byte boundary */
// TCP Packet
typedef struct TCP_PACKET_STRUCT
{
uint16_t sig1;
uint16_t len;
uint16_t cmd;
uint16_t op; // 8
uint32_t time; // 4 : 12
union {
char ch[40];
uint8_t by[40];
int16_t n16[20];
uint16_t u16[20];
int32_t n32[10];
uint32_t u32[10];
float f[10];
STATUS_TYPE status;
DEVICE_PARAM_TYPE device;
}; //40 : 52
uint16_t sig2; // 2 : 54
} TCP_PACKET;
// UDP Packet
typedef struct UDP_PACKET_STRUCT
{
uint16_t sig1;
uint16_t m_nSize;
uint16_t m_nMessage;
uint16_t m_nDeviceType;
uint16_t m_nResetReason;
// 10
uint32_t m_nChipID;
uint32_t m_nDeviceID;
// 18
union {
uint32_t m_nVersion;
uint32_t clock;
};
// 22
union {
uint32_t dwIPAddress;
uint8_t byIPAddress[4];
};
// 26
uint16_t m_nPort;
// 28
uint8_t m_MACAddress[20];
// 48
char m_sDeviceName[32];
// 80
union {
char m_sService[32];
STATUS_TYPE status; //
};
// 112
char m_sCompanyName[32];
// 144
uint16_t sig2;
// Size: 146
} UDP_PACKET;
typedef struct {
UDP_PACKET udp;
CONFIG_TYPE con;
} UDP_CONFIG_TYPE;
#pragma pack(pop) /* restore original alignment from stack */
class CWiFiHost
{
public:
void Setup();
void Stop();
void Loop(unsigned long clock);
inline bool isConnected() { return m_bClientConnected; }
void CloseConnection();
void SendHeartBeat(unsigned long clock);
void MonitorUDP();
inline void setPublicIPPort(uint32_t ip, uint16_t port) { m_dwPublicIP = ip; m_nPublicPort = port; }
private:
//void setupUDP();
//void loopUDP(unsigned long clock);
//bool checkOTA(bool bForceUpdate);
void CheckClient(unsigned long clock);
void ProcessPacket(TCP_PACKET& pkt);
void SendHeartBeat();
int SendConfig();
//void FillPacket();
private:
int SendPacket(TCP_PACKET& pkt);
size_t SendData(const uint8_t* data, size_t size);
bool SendData(unsigned long clockMills);
size_t ReceiveData(uint8_t* data, size_t size);
bool ReceiveData(unsigned long clockMills);
void Restart();
// Data Send/Receive Mode
char *m_pDataSend_data;
size_t m_nDataSend_size;
size_t m_nDataSend_sent;
char *m_pDataReceive_data;
size_t m_nDataReceive_size;
size_t m_nDataReceive_received;
bool m_bReceiveConfigPending;
bool m_bSendHistoryPending;
int16_t m_nPendingHistoryCount;
// SocketConnection
IPAddress externalServerIP;
//int sockfd = -1;
//unsigned long connectStartTime = 0;
//bool isConnecting = false;
//bool initiateConnection(unsigned long clock);
//bool checkConnection(unsigned long clock);
// Operation
uint8_t m_nMode;
bool m_bHelloSent;
unsigned long m_nLastReceivedTime;
unsigned long m_nLastHeartBeatSentTime;
unsigned long m_nLastUDPBroadcastTime;
volatile bool m_bClientConnected;
uint32_t m_dwPublicIP;
uint16_t m_nPublicPort;
IPAddress m_cExternalServerIPAddress;
WiFiUDP udpLocal, udpExternal;
WiFiServer wifiServer, wifiExternal;
WiFiClient wifiClient;
UDP_PACKET packetUDP;
TCP_PACKET hostPacket;
TCP_PACKET clientPacket;
};
extern CWiFiHost host;
#endif

39
c_cpp_properties.json Normal file
View File

@ -0,0 +1,39 @@
{
"configurations": [
{
"name": "Arduino",
"includePath": [
"${workspaceFolder}/**",
"C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/hardware/esp32/3.0.5/libraries/**",
"C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/hardware/esp32/3.0.5/cores/esp32/**",
"C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/tools/esp32-arduino-libs/idf-release_v5.1-33fbade6/esp32/include/**",
"C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/tools/esp32-arduino-libs/idf-release_v5.1-33fbade6/esp32/include/freertos/FreeRTOS-Kernel/include/**",
"C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/tools/esp32-arduino-libs/idf-release_v5.1-33fbade6/esp32/include/freertos/FreeRTOS-Kernel/portable/xtensa/include/**",
"C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/tools/esp32-arduino-libs/idf-release_v5.1-33fbade6/esp32/include/freertos/esp_additions/include/**",
"C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/tools/esp32-arduino-libs/idf-release_v5.1-33fbade6/esp32/include/freertos/esp_additions/arch/xtensa/include/**",
"C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/tools/esp32-arduino-libs/idf-release_v5.1-33fbade6/esp32/qio_qspi/include/**",
"C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/hardware/esp32/3.0.5/variants/esp32/**",
"C:/Users/hxyi/AppData/Local/Arduino15/packages/esp8266/hardware/esp8266/3.1.2/cores/esp8266/**",
"C:/Users/hxyi/AppData/Local/Arduino15/packages/esp8266/hardware/esp8266/3.1.2/libraries/**",
"C:/Users/hxyi/AppData/Local/Arduino15/packages/esp8266/hardware/esp8266/3.1.2/libraries/ESP8266WiFi/src/**",
"C:/Users/hxyi/AppData/Local/Arduino15/packages/esp8266/hardware/esp8266/3.1.2/tools/SDK/include/**",
"D:/Projects/libraries/ESP-TuyaBLE/src/**",
"D:/Projects/libraries/NimBLE-Arduino/src/**"
],
"defines": [
"_DEBUG",
"UNICODE",
"_UNICODE",
"ESP32"
],
"windowsSdkVersion": "10.0.19041.0",
"compilerPath": "C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/tools/esp-x32/2302/bin/xtensa-esp32-elf-gcc.exe",
"cStandard": "c11",
"cppStandard": "c++17",
"intelliSenseMode": "gcc-x64"
}
],
"version": 4
}

185
zcd.cpp Normal file
View File

@ -0,0 +1,185 @@
#include "HermitCrab.h"
#include "Config.h"
#include "zcd.h"
#include <math.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <driver/gptimer.h>
#define TAG_ZCD "ZCD"
// Constants
#define EFFECTIVE_POWER 0.86
#define LEADING_TIME_RATIO 0.06
// ESP32 Clock Constants
const uint32_t AC_CYCLE_TIME_CLOCKS = 8333; // Half cycle of 60Hz AC in clock cycles
const uint32_t EFFECTIVE_HALF_CYCLE = EFFECTIVE_POWER * AC_CYCLE_TIME_CLOCKS; // Effective half cycle in clock cycles
const uint32_t LEADING_PULSE_COUNT = EFFECTIVE_HALF_CYCLE * LEADING_TIME_RATIO; // Leading pulse count
const uint32_t MAX_PULSE_COUNT = LEADING_PULSE_COUNT + EFFECTIVE_HALF_CYCLE; // Maximum valid pulse count
volatile uint32_t dutyHeater1; // Calculated timerHeater1 count for TRIAC firing
volatile uint32_t dutyHeater2; // Calculated timerZCD count for TRIAC firing
volatile uint8_t zcdACCount;
volatile uint8_t zcdLoadCount;
hw_timer_t *timerHeater1;
hw_timer_t *timerHeater2;
short getHeater1Duty() {
if (dutyHeater1 == 0) return 0;
if (dutyHeater1 >= LEADING_PULSE_COUNT + EFFECTIVE_HALF_CYCLE) return 10000;
return round(10000.0f * (dutyHeater1 - LEADING_PULSE_COUNT) / EFFECTIVE_HALF_CYCLE);
}
short getHeater2Duty() {
if (dutyHeater2 == 0) return 0;
if (dutyHeater2 >= LEADING_PULSE_COUNT + EFFECTIVE_HALF_CYCLE) return 10000;
return round(10000.0f * (dutyHeater1 - LEADING_PULSE_COUNT) / EFFECTIVE_HALF_CYCLE);
}
// Function to set the duty based on percentage (0 to 10000)
IRAM_ATTR void setHeater1Duty(short duty) {
if (duty <= 0) {
dutyHeater1 = 0; // If 0% duty, no pulse (turn off TRIAC)
} else if (duty >= 10000) {
// 100% duty corresponds to the leading pulse + full effective half cycle
dutyHeater1 = LEADING_PULSE_COUNT;
} else {
// Map duty to power ratio (0 to 1)
float powerRatio = (float) duty / 10000.0f;
// Calculate the angle in radians using the inverse cosine directly
float angleRadians = acosf(1.0f - 2.0f * powerRatio);
// Convert angle to time delay (in clock cycles)
// Normalized angle (0 to PI) maps to half-cycle (0 to EFFECTIVE_HALF_CYCLE)
uint32_t pulseCount = (angleRadians / M_PI) * EFFECTIVE_HALF_CYCLE;
dutyHeater1 = LEADING_PULSE_COUNT + EFFECTIVE_HALF_CYCLE - pulseCount;
}
uint32_t nDuty = duty * PWM_FULL / 10000;
ledcWrite(PIN_LED_HEATER1, PWM_FULL - nDuty);
ESP_LOGD(TAG_ZCD,"Set Duty: %.2f%%, Timer Count: %u clock cycles", duty, dutyHeater1);
}
// Function to set the duty based on percentage (0 to 10000)
IRAM_ATTR void setHeater2Duty(short duty) {
if (config.bAC2_OnOff) {
if (duty >= 10000) {
digitalWrite(PIN_HEATER2, HEATER_ON);
duty = 10000;
} else {
digitalWrite(PIN_HEATER2, HEATER_OFF);
duty = 0;
}
dutyHeater2 = 0;
} else {
if (duty <= 0) {
dutyHeater2 = 0; // If 0% duty, no pulse (turn off TRIAC)
} else if (duty >= 10000) {
// 100% duty corresponds to the leading pulse + full effective half cycle
dutyHeater2 = LEADING_PULSE_COUNT;
} else {
// Map duty to power ratio (0 to 1)
float powerRatio = (float) duty / 10000.0f;
// Calculate the angle in radians using the inverse cosine directly
float angleRadians = acosf(1.0f - 2.0f * powerRatio);
// Convert angle to time delay (in clock cycles)
// Normalized angle (0 to PI) maps to half-cycle (0 to EFFECTIVE_HALF_CYCLE)
uint32_t pulseCount = (angleRadians / M_PI) * EFFECTIVE_HALF_CYCLE;
dutyHeater2 = LEADING_PULSE_COUNT + EFFECTIVE_HALF_CYCLE - pulseCount;
}
}
uint32_t nDuty = duty * PWM_FULL / 10000;
ledcWrite(PIN_LED_HEATER2, PWM_FULL - nDuty);
ESP_LOGD(TAG_ZCD,"Set Duty: %.2f%%, Timer Count: %u clock cycles\n", duty, dutyHeater1);
}
void ARDUINO_ISR_ATTR onTimer1() {
digitalWrite(PIN_HEATER1, HIGH); // Fire TRIAC
delayMicroseconds(10); // Short pulse to trigger TRIAC
digitalWrite(PIN_HEATER1, LOW); // Turn off TRIAC trigger
}
void ARDUINO_ISR_ATTR onTimer2() {
digitalWrite(PIN_HEATER2, HIGH); // Fire TRIAC
delayMicroseconds(10); // Short pulse to trigger TRIAC
digitalWrite(PIN_HEATER2, LOW); // Turn off TRIAC trigger
}
// Zero-Cross Detection Interrupt Service Routine
void ARDUINO_ISR_ATTR zcdACISR() {
uint32_t clock = micros();
static uint32_t lastClock = 0l;
if (clock - lastClock < 8000)
return;
lastClock = clock;
zcdACCount++;
// Heater 1
if (dutyHeater1 == MAX_PULSE_COUNT) {
onTimer1();
}
else if (dutyHeater1 >= LEADING_PULSE_COUNT && dutyHeater1 < MAX_PULSE_COUNT) {
// Stop the timer, configure new alarm, then explicitly start
timerStop(timerHeater1); // Stop any existing timer action
timerWrite(timerHeater1, 0); // Reset counter to 0
timerAlarm(timerHeater1, dutyHeater1, false, 0); // Set alarm with updated duty
timerStart(timerHeater1); // Start the timer explicitly
}
// Heater 2
if (dutyHeater2 == MAX_PULSE_COUNT) {
onTimer2();
}
else if (dutyHeater2 >= LEADING_PULSE_COUNT && dutyHeater2 < MAX_PULSE_COUNT) {
// Stop the timer, configure new alarm, then explicitly start
timerStop(timerHeater2); // Stop any existing timer action
timerWrite(timerHeater2, 0); // Reset counter to 0
timerAlarm(timerHeater2, dutyHeater2, false, 0); // Set alarm with updated duty
timerStart(timerHeater2); // Start the timer explicitly
}
}
void ARDUINO_ISR_ATTR zcdLoadISR() {
zcdLoadCount++;
}
void setupZCD() {
pinMode(PIN_ZCD_AC, INPUT);
pinMode(PIN_ZCD_LOAD, INPUT);
pinMode(PIN_HEATER1, OUTPUT);
pinMode(PIN_HEATER2, OUTPUT);
dutyHeater1 = 0; // Calculated timerHeater1 count for TRIAC firing
dutyHeater2 = 0; // Calculated timerZCD count for TRIAC firing
zcdACCount = 0;
zcdLoadCount = 0;
timerHeater1 = NULL;
attachInterrupt(PIN_ZCD_AC, zcdACISR, CHANGE); // Attach zero-cross detection ISR
attachInterrupt(PIN_ZCD_LOAD, zcdLoadISR, CHANGE); // Attach zero-cross detection ISR
// Initialize and configure the timer
if ((timerHeater1 = timerBegin(1000000)) != NULL) {
timerAttachInterrupt(timerHeater1, &onTimer1); // Attach TRIAC firing routine
timerStop(timerHeater1); // Ensure timer is stopped initially
timerStart(timerHeater1); // Explicitly start the timer after setup
}
if ((timerHeater2 = timerBegin(1000000)) != NULL) {
timerAttachInterrupt(timerHeater2, &onTimer2); // Attach TRIAC firing routine
timerStop(timerHeater2); // Ensure timer is stopped initially
timerStart(timerHeater2); // Explicitly start the timer after setup
}
}

13
zcd.h Normal file
View File

@ -0,0 +1,13 @@
#ifndef __ZCD_H
#define __ZCD_H
extern volatile uint8_t zcdACCount, zcdLoadCount;
// ZCD
void setupZCD();
void setHeater1Duty(short duty);
void setHeater2Duty(short duty);
short getHeater1Duty();
#if defined(ESP8266)
void setMistDuty(short duty);
#endif
#endif