commit 3276c9527d8c5c165a3925e963a901c01febcd03 Author: RnD1 Date: Fri Apr 3 20:40:52 2026 +0900 Initial commit: HermitCrab project structure diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..35bfb1a Binary files /dev/null and b/.gitignore differ diff --git a/.vs/HermitCrab/v16/.suo b/.vs/HermitCrab/v16/.suo new file mode 100644 index 0000000..455ebe4 Binary files /dev/null and b/.vs/HermitCrab/v16/.suo differ diff --git a/.vs/HermitCrab/v16/Browse.VC.db b/.vs/HermitCrab/v16/Browse.VC.db new file mode 100644 index 0000000..1526dc8 Binary files /dev/null and b/.vs/HermitCrab/v16/Browse.VC.db differ diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json new file mode 100644 index 0000000..2dab81a --- /dev/null +++ b/.vscode/c_cpp_properties.json @@ -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 +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9de3be8 --- /dev/null +++ b/.vscode/settings.json @@ -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 + } + +} \ No newline at end of file diff --git a/AHT2x.cpp b/AHT2x.cpp new file mode 100644 index 0000000..b964ef6 --- /dev/null +++ b/AHT2x.cpp @@ -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; +} diff --git a/AHT2x.h b/AHT2x.h new file mode 100644 index 0000000..e35cde2 --- /dev/null +++ b/AHT2x.h @@ -0,0 +1,43 @@ +#ifndef __AHT2x_H +#define __AHT2x_H +#include + +#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 \ No newline at end of file diff --git a/BLEScan.cpp b/BLEScan.cpp new file mode 100644 index 0000000..f710691 --- /dev/null +++ b/BLEScan.cpp @@ -0,0 +1,433 @@ +#include +#include +#include +#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(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 +} diff --git a/BLEScan.h b/BLEScan.h new file mode 100644 index 0000000..5526be5 --- /dev/null +++ b/BLEScan.h @@ -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 \ No newline at end of file diff --git a/BearSSL.h b/BearSSL.h new file mode 100644 index 0000000..8e7a31f --- /dev/null +++ b/BearSSL.h @@ -0,0 +1,1346 @@ +/* + * Copyright (c) 2016 Thomas Pornin + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#ifndef BR_BEARSSL_HASH_H__ +#define BR_BEARSSL_HASH_H__ + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** \file bearssl_hash.h + * + * # Hash Functions + * + * This file documents the API for hash functions. + * + * + * ## Procedural API + * + * For each implemented hash function, of name "`xxx`", the following + * elements are defined: + * + * - `br_xxx_vtable` + * + * An externally defined instance of `br_hash_class`. + * + * - `br_xxx_SIZE` + * + * A macro that evaluates to the output size (in bytes) of the + * hash function. + * + * - `br_xxx_ID` + * + * A macro that evaluates to a symbolic identifier for the hash + * function. Such identifiers are used with HMAC and signature + * algorithm implementations. + * + * NOTE: for the "standard" hash functions defined in [the TLS + * standard](https://tools.ietf.org/html/rfc5246#section-7.4.1.4.1), + * the symbolic identifiers match the constants used in TLS, i.e. + * 1 to 6 for MD5, SHA-1, SHA-224, SHA-256, SHA-384 and SHA-512, + * respectively. + * + * - `br_xxx_context` + * + * Context for an ongoing computation. It is allocated by the + * caller, and a pointer to it is passed to all functions. A + * context contains no interior pointer, so it can be moved around + * and cloned (with a simple `memcpy()` or equivalent) in order to + * capture the function state at some point. Computations that use + * distinct context structures are independent of each other. The + * first field of `br_xxx_context` is always a pointer to the + * `br_xxx_vtable` structure; `br_xxx_init()` sets that pointer. + * + * - `br_xxx_init(br_xxx_context *ctx)` + * + * Initialise the provided context. Previous contents of the structure + * are ignored. This calls resets the context to the start of a new + * hash computation; it also sets the first field of the context + * structure (called `vtable`) to a pointer to the statically + * allocated constant `br_xxx_vtable` structure. + * + * - `br_xxx_update(br_xxx_context *ctx, const void *data, size_t len)` + * + * Add some more bytes to the hash computation represented by the + * provided context. + * + * - `br_xxx_out(const br_xxx_context *ctx, void *out)` + * + * Complete the hash computation and write the result in the provided + * buffer. The output buffer MUST be large enough to accommodate the + * result. The context is NOT modified by this operation, so this + * function can be used to get a "partial hash" while still keeping + * the possibility of adding more bytes to the input. + * + * - `br_xxx_state(const br_xxx_context *ctx, void *out)` + * + * Get a copy of the "current state" for the computation so far. For + * MD functions (MD5, SHA-1, SHA-2 family), this is the running state + * resulting from the processing of the last complete input block. + * Returned value is the current input length (in bytes). + * + * - `br_xxx_set_state(br_xxx_context *ctx, const void *stb, uint64_t count)` + * + * Set the internal state to the provided values. The 'stb' and + * 'count' values shall match that which was obtained from + * `br_xxx_state()`. This restores the hash state only if the state + * values were at an appropriate block boundary. This does NOT set + * the `vtable` pointer in the context. + * + * Context structures can be discarded without any explicit deallocation. + * Hash function implementations are purely software and don't reserve + * any resources outside of the context structure itself. + * + * + * ## Object-Oriented API + * + * For each hash function that follows the procedural API described + * above, an object-oriented API is also provided. In that API, function + * pointers from the vtable (`br_xxx_vtable`) are used. The vtable + * incarnates object-oriented programming. An introduction on the OOP + * concept used here can be read on the BearSSL Web site:
+ *    [https://www.bearssl.org/oop.html](https://www.bearssl.org/oop.html) + * + * The vtable offers functions called `init()`, `update()`, `out()`, + * `set()` and `set_state()`, which are in fact the functions from + * the procedural API. That vtable also contains two informative fields: + * + * - `context_size` + * + * The size of the context structure (`br_xxx_context`), in bytes. + * This can be used by generic implementations to perform dynamic + * context allocation. + * + * - `desc` + * + * A "descriptor" field that encodes some information on the hash + * function: symbolic identifier, output size, state size, + * internal block size, details on the padding. + * + * Users of this object-oriented API (in particular generic HMAC + * implementations) may make the following assumptions: + * + * - Hash output size is no more than 64 bytes. + * - Hash internal state size is no more than 64 bytes. + * - Internal block size is a power of two, no less than 16 and no more + * than 256. + * + * + * ## Implemented Hash Functions + * + * Implemented hash functions are: + * + * | Function | Name | Output length | State length | + * | :-------- | :------ | :-----------: | :----------: | + * | MD5 | md5 | 16 | 16 | + * | SHA-1 | sha1 | 20 | 20 | + * | SHA-224 | sha224 | 28 | 32 | + * | SHA-256 | sha256 | 32 | 32 | + * | SHA-384 | sha384 | 48 | 64 | + * | SHA-512 | sha512 | 64 | 64 | + * | MD5+SHA-1 | md5sha1 | 36 | 36 | + * + * (MD5+SHA-1 is the concatenation of MD5 and SHA-1 computed over the + * same input; in the implementation, the internal data buffer is + * shared, thus making it more memory-efficient than separate MD5 and + * SHA-1. It can be useful in implementing SSL 3.0, TLS 1.0 and TLS + * 1.1.) + * + * + * ## Multi-Hasher + * + * An aggregate hasher is provided, that can compute several standard + * hash functions in parallel. It uses `br_multihash_context` and a + * procedural API. It is configured with the implementations (the vtables) + * that it should use; it will then compute all these hash functions in + * parallel, on the same input. It is meant to be used in cases when the + * hash of an object will be used, but the exact hash function is not + * known yet (typically, streamed processing on X.509 certificates). + * + * Only the standard hash functions (MD5, SHA-1, SHA-224, SHA-256, SHA-384 + * and SHA-512) are supported by the multi-hasher. + * + * + * ## GHASH + * + * GHASH is not a generic hash function; it is a _universal_ hash function, + * which, as the name does not say, means that it CANNOT be used in most + * places where a hash function is needed. GHASH is used within the GCM + * encryption mode, to provide the checked integrity functionality. + * + * A GHASH implementation is basically a function that uses the type defined + * in this file under the name `br_ghash`: + * + * typedef void (*br_ghash)(void *y, const void *h, const void *data, size_t len); + * + * The `y` pointer refers to a 16-byte value which is used as input, and + * receives the output of the GHASH invocation. `h` is a 16-byte secret + * value (that serves as key). `data` and `len` define the input data. + * + * Three GHASH implementations are provided, all constant-time, based on + * the use of integer multiplications with appropriate masking to cancel + * carry propagation. + */ + +/** + * \brief Class type for hash function implementations. + * + * A `br_hash_class` instance references the methods implementing a hash + * function. Constant instances of this structure are defined for each + * implemented hash function. Such instances are also called "vtables". + * + * Vtables are used to support object-oriented programming, as + * described on [the BearSSL Web site](https://www.bearssl.org/oop.html). + */ +typedef struct br_hash_class_ br_hash_class; +struct br_hash_class_ { + /** + * \brief Size (in bytes) of the context structure appropriate for + * computing this hash function. + */ + size_t context_size; + + /** + * \brief Descriptor word that contains information about the hash + * function. + * + * For each word `xxx` described below, use `BR_HASHDESC_xxx_OFF` + * and `BR_HASHDESC_xxx_MASK` to access the specific value, as + * follows: + * + * (hf->desc >> BR_HASHDESC_xxx_OFF) & BR_HASHDESC_xxx_MASK + * + * The defined elements are: + * + * - `ID`: the symbolic identifier for the function, as defined + * in [TLS](https://tools.ietf.org/html/rfc5246#section-7.4.1.4.1) + * (MD5 = 1, SHA-1 = 2,...). + * + * - `OUT`: hash output size, in bytes. + * + * - `STATE`: internal running state size, in bytes. + * + * - `LBLEN`: base-2 logarithm for the internal block size, as + * defined for HMAC processing (this is 6 for MD5, SHA-1, SHA-224 + * and SHA-256, since these functions use 64-byte blocks; for + * SHA-384 and SHA-512, this is 7, corresponding to their + * 128-byte blocks). + * + * The descriptor may contain a few other flags. + */ + uint32_t desc; + + /** + * \brief Initialisation method. + * + * This method takes as parameter a pointer to a context area, + * that it initialises. The first field of the context is set + * to this vtable; other elements are initialised for a new hash + * computation. + * + * \param ctx pointer to (the first field of) the context. + */ + void (*init)(const br_hash_class **ctx); + + /** + * \brief Data injection method. + * + * The `len` bytes starting at address `data` are injected into + * the running hash computation incarnated by the specified + * context. The context is updated accordingly. It is allowed + * to have `len == 0`, in which case `data` is ignored (and could + * be `NULL`), and nothing happens. + * on the input data. + * + * \param ctx pointer to (the first field of) the context. + * \param data pointer to the first data byte to inject. + * \param len number of bytes to inject. + */ + void (*update)(const br_hash_class **ctx, const void *data, size_t len); + + /** + * \brief Produce hash output. + * + * The hash output corresponding to all data bytes injected in the + * context since the last `init()` call is computed, and written + * in the buffer pointed to by `dst`. The hash output size depends + * on the implemented hash function (e.g. 16 bytes for MD5). + * The context is _not_ modified by this call, so further bytes + * may be afterwards injected to continue the current computation. + * + * \param ctx pointer to (the first field of) the context. + * \param dst destination buffer for the hash output. + */ + void (*out)(const br_hash_class *const *ctx, void *dst); + + /** + * \brief Get running state. + * + * This method saves the current running state into the `dst` + * buffer. What constitutes the "running state" depends on the + * hash function; for Merkle-Damgård hash functions (like + * MD5 or SHA-1), this is the output obtained after processing + * each block. The number of bytes injected so far is returned. + * The context is not modified by this call. + * + * \param ctx pointer to (the first field of) the context. + * \param dst destination buffer for the state. + * \return the injected total byte length. + */ + uint64_t (*state)(const br_hash_class *const *ctx, void *dst); + + /** + * \brief Set running state. + * + * This methods replaces the running state for the function. + * + * \param ctx pointer to (the first field of) the context. + * \param stb source buffer for the state. + * \param count injected total byte length. + */ + void (*set_state)(const br_hash_class **ctx, + const void *stb, uint64_t count); +}; + +#ifndef BR_DOXYGEN_IGNORE +#define BR_HASHDESC_ID(id) ((uint32_t)(id) << BR_HASHDESC_ID_OFF) +#define BR_HASHDESC_ID_OFF 0 +#define BR_HASHDESC_ID_MASK 0xFF + +#define BR_HASHDESC_OUT(size) ((uint32_t)(size) << BR_HASHDESC_OUT_OFF) +#define BR_HASHDESC_OUT_OFF 8 +#define BR_HASHDESC_OUT_MASK 0x7F + +#define BR_HASHDESC_STATE(size) ((uint32_t)(size) << BR_HASHDESC_STATE_OFF) +#define BR_HASHDESC_STATE_OFF 15 +#define BR_HASHDESC_STATE_MASK 0xFF + +#define BR_HASHDESC_LBLEN(ls) ((uint32_t)(ls) << BR_HASHDESC_LBLEN_OFF) +#define BR_HASHDESC_LBLEN_OFF 23 +#define BR_HASHDESC_LBLEN_MASK 0x0F + +#define BR_HASHDESC_MD_PADDING ((uint32_t)1 << 28) +#define BR_HASHDESC_MD_PADDING_128 ((uint32_t)1 << 29) +#define BR_HASHDESC_MD_PADDING_BE ((uint32_t)1 << 30) +#endif + +/* + * Specific hash functions. + * + * Rules for contexts: + * -- No interior pointer. + * -- No pointer to external dynamically allocated resources. + * -- First field is called 'vtable' and is a pointer to a + * const-qualified br_hash_class instance (pointer is set by init()). + * -- SHA-224 and SHA-256 contexts are identical. + * -- SHA-384 and SHA-512 contexts are identical. + * + * Thus, contexts can be moved and cloned to capture the hash function + * current state; and there is no need for any explicit "release" function. + */ + +/** + * \brief Symbolic identifier for MD5. + */ +#define br_md5_ID 1 + +/** + * \brief MD5 output size (in bytes). + */ +#define br_md5_SIZE 16 + +/** + * \brief Constant vtable for MD5. + */ +extern const br_hash_class br_md5_vtable; + +/** + * \brief MD5 context. + * + * First field is a pointer to the vtable; it is set by the initialisation + * function. Other fields are not supposed to be accessed by user code. + */ +typedef struct { + /** + * \brief Pointer to vtable for this context. + */ + const br_hash_class *vtable; +#ifndef BR_DOXYGEN_IGNORE + unsigned char buf[64]; + uint64_t count; + uint32_t val[4]; +#endif +} br_md5_context; + +/** + * \brief MD5 context initialisation. + * + * This function initialises or resets a context for a new MD5 + * computation. It also sets the vtable pointer. + * + * \param ctx pointer to the context structure. + */ +void br_md5_init(br_md5_context *ctx); + +/** + * \brief Inject some data bytes in a running MD5 computation. + * + * The provided context is updated with some data bytes. If the number + * of bytes (`len`) is zero, then the data pointer (`data`) is ignored + * and may be `NULL`, and this function does nothing. + * + * \param ctx pointer to the context structure. + * \param data pointer to the injected data. + * \param len injected data length (in bytes). + */ +void br_md5_update(br_md5_context *ctx, const void *data, size_t len); + +/** + * \brief Compute MD5 output. + * + * The MD5 output for the concatenation of all bytes injected in the + * provided context since the last initialisation or reset call, is + * computed and written in the buffer pointed to by `out`. The context + * itself is not modified, so extra bytes may be injected afterwards + * to continue that computation. + * + * \param ctx pointer to the context structure. + * \param out destination buffer for the hash output. + */ +void br_md5_out(const br_md5_context *ctx, void *out); + +/** + * \brief Save MD5 running state. + * + * The running state for MD5 (output of the last internal block + * processing) is written in the buffer pointed to by `out`. The + * number of bytes injected since the last initialisation or reset + * call is returned. The context is not modified. + * + * \param ctx pointer to the context structure. + * \param out destination buffer for the running state. + * \return the injected total byte length. + */ +uint64_t br_md5_state(const br_md5_context *ctx, void *out); + +/** + * \brief Restore MD5 running state. + * + * The running state for MD5 is set to the provided values. + * + * \param ctx pointer to the context structure. + * \param stb source buffer for the running state. + * \param count the injected total byte length. + */ +void br_md5_set_state(br_md5_context *ctx, const void *stb, uint64_t count); + +/** + * \brief Symbolic identifier for SHA-1. + */ +#define br_sha1_ID 2 + +/** + * \brief SHA-1 output size (in bytes). + */ +#define br_sha1_SIZE 20 + +/** + * \brief Constant vtable for SHA-1. + */ +extern const br_hash_class br_sha1_vtable; + +/** + * \brief SHA-1 context. + * + * First field is a pointer to the vtable; it is set by the initialisation + * function. Other fields are not supposed to be accessed by user code. + */ +typedef struct { + /** + * \brief Pointer to vtable for this context. + */ + const br_hash_class *vtable; +#ifndef BR_DOXYGEN_IGNORE + unsigned char buf[64]; + uint64_t count; + uint32_t val[5]; +#endif +} br_sha1_context; + +/** + * \brief SHA-1 context initialisation. + * + * This function initialises or resets a context for a new SHA-1 + * computation. It also sets the vtable pointer. + * + * \param ctx pointer to the context structure. + */ +void br_sha1_init(br_sha1_context *ctx); + +/** + * \brief Inject some data bytes in a running SHA-1 computation. + * + * The provided context is updated with some data bytes. If the number + * of bytes (`len`) is zero, then the data pointer (`data`) is ignored + * and may be `NULL`, and this function does nothing. + * + * \param ctx pointer to the context structure. + * \param data pointer to the injected data. + * \param len injected data length (in bytes). + */ +void br_sha1_update(br_sha1_context *ctx, const void *data, size_t len); + +/** + * \brief Compute SHA-1 output. + * + * The SHA-1 output for the concatenation of all bytes injected in the + * provided context since the last initialisation or reset call, is + * computed and written in the buffer pointed to by `out`. The context + * itself is not modified, so extra bytes may be injected afterwards + * to continue that computation. + * + * \param ctx pointer to the context structure. + * \param out destination buffer for the hash output. + */ +void br_sha1_out(const br_sha1_context *ctx, void *out); + +/** + * \brief Save SHA-1 running state. + * + * The running state for SHA-1 (output of the last internal block + * processing) is written in the buffer pointed to by `out`. The + * number of bytes injected since the last initialisation or reset + * call is returned. The context is not modified. + * + * \param ctx pointer to the context structure. + * \param out destination buffer for the running state. + * \return the injected total byte length. + */ +uint64_t br_sha1_state(const br_sha1_context *ctx, void *out); + +/** + * \brief Restore SHA-1 running state. + * + * The running state for SHA-1 is set to the provided values. + * + * \param ctx pointer to the context structure. + * \param stb source buffer for the running state. + * \param count the injected total byte length. + */ +void br_sha1_set_state(br_sha1_context *ctx, const void *stb, uint64_t count); + +/** + * \brief Symbolic identifier for SHA-224. + */ +#define br_sha224_ID 3 + +/** + * \brief SHA-224 output size (in bytes). + */ +#define br_sha224_SIZE 28 + +/** + * \brief Constant vtable for SHA-224. + */ +extern const br_hash_class br_sha224_vtable; + +/** + * \brief SHA-224 context. + * + * First field is a pointer to the vtable; it is set by the initialisation + * function. Other fields are not supposed to be accessed by user code. + */ +typedef struct { + /** + * \brief Pointer to vtable for this context. + */ + const br_hash_class *vtable; +#ifndef BR_DOXYGEN_IGNORE + unsigned char buf[64]; + uint64_t count; + uint32_t val[8]; +#endif +} br_sha224_context; + +/** + * \brief SHA-224 context initialisation. + * + * This function initialises or resets a context for a new SHA-224 + * computation. It also sets the vtable pointer. + * + * \param ctx pointer to the context structure. + */ +void br_sha224_init(br_sha224_context *ctx); + +/** + * \brief Inject some data bytes in a running SHA-224 computation. + * + * The provided context is updated with some data bytes. If the number + * of bytes (`len`) is zero, then the data pointer (`data`) is ignored + * and may be `NULL`, and this function does nothing. + * + * \param ctx pointer to the context structure. + * \param data pointer to the injected data. + * \param len injected data length (in bytes). + */ +void br_sha224_update(br_sha224_context *ctx, const void *data, size_t len); + +/** + * \brief Compute SHA-224 output. + * + * The SHA-224 output for the concatenation of all bytes injected in the + * provided context since the last initialisation or reset call, is + * computed and written in the buffer pointed to by `out`. The context + * itself is not modified, so extra bytes may be injected afterwards + * to continue that computation. + * + * \param ctx pointer to the context structure. + * \param out destination buffer for the hash output. + */ +void br_sha224_out(const br_sha224_context *ctx, void *out); + +/** + * \brief Save SHA-224 running state. + * + * The running state for SHA-224 (output of the last internal block + * processing) is written in the buffer pointed to by `out`. The + * number of bytes injected since the last initialisation or reset + * call is returned. The context is not modified. + * + * \param ctx pointer to the context structure. + * \param out destination buffer for the running state. + * \return the injected total byte length. + */ +uint64_t br_sha224_state(const br_sha224_context *ctx, void *out); + +/** + * \brief Restore SHA-224 running state. + * + * The running state for SHA-224 is set to the provided values. + * + * \param ctx pointer to the context structure. + * \param stb source buffer for the running state. + * \param count the injected total byte length. + */ +void br_sha224_set_state(br_sha224_context *ctx, + const void *stb, uint64_t count); + +/** + * \brief Symbolic identifier for SHA-256. + */ +#define br_sha256_ID 4 + +/** + * \brief SHA-256 output size (in bytes). + */ +#define br_sha256_SIZE 32 + +/** + * \brief Constant vtable for SHA-256. + */ +extern const br_hash_class br_sha256_vtable; + +#ifdef BR_DOXYGEN_IGNORE +/** + * \brief SHA-256 context. + * + * First field is a pointer to the vtable; it is set by the initialisation + * function. Other fields are not supposed to be accessed by user code. + */ +typedef struct { + /** + * \brief Pointer to vtable for this context. + */ + const br_hash_class *vtable; +} br_sha256_context; +#else +typedef br_sha224_context br_sha256_context; +#endif + +/** + * \brief SHA-256 context initialisation. + * + * This function initialises or resets a context for a new SHA-256 + * computation. It also sets the vtable pointer. + * + * \param ctx pointer to the context structure. + */ +void br_sha256_init(br_sha256_context *ctx); + +#ifdef BR_DOXYGEN_IGNORE +/** + * \brief Inject some data bytes in a running SHA-256 computation. + * + * The provided context is updated with some data bytes. If the number + * of bytes (`len`) is zero, then the data pointer (`data`) is ignored + * and may be `NULL`, and this function does nothing. + * + * \param ctx pointer to the context structure. + * \param data pointer to the injected data. + * \param len injected data length (in bytes). + */ +void br_sha256_update(br_sha256_context *ctx, const void *data, size_t len); +#else +#define br_sha256_update br_sha224_update +#endif + +/** + * \brief Compute SHA-256 output. + * + * The SHA-256 output for the concatenation of all bytes injected in the + * provided context since the last initialisation or reset call, is + * computed and written in the buffer pointed to by `out`. The context + * itself is not modified, so extra bytes may be injected afterwards + * to continue that computation. + * + * \param ctx pointer to the context structure. + * \param out destination buffer for the hash output. + */ +void br_sha256_out(const br_sha256_context *ctx, void *out); + +#ifdef BR_DOXYGEN_IGNORE +/** + * \brief Save SHA-256 running state. + * + * The running state for SHA-256 (output of the last internal block + * processing) is written in the buffer pointed to by `out`. The + * number of bytes injected since the last initialisation or reset + * call is returned. The context is not modified. + * + * \param ctx pointer to the context structure. + * \param out destination buffer for the running state. + * \return the injected total byte length. + */ +uint64_t br_sha256_state(const br_sha256_context *ctx, void *out); +#else +#define br_sha256_state br_sha224_state +#endif + +#ifdef BR_DOXYGEN_IGNORE +/** + * \brief Restore SHA-256 running state. + * + * The running state for SHA-256 is set to the provided values. + * + * \param ctx pointer to the context structure. + * \param stb source buffer for the running state. + * \param count the injected total byte length. + */ +void br_sha256_set_state(br_sha256_context *ctx, + const void *stb, uint64_t count); +#else +#define br_sha256_set_state br_sha224_set_state +#endif + +/** + * \brief Symbolic identifier for SHA-384. + */ +#define br_sha384_ID 5 + +/** + * \brief SHA-384 output size (in bytes). + */ +#define br_sha384_SIZE 48 + +/** + * \brief Constant vtable for SHA-384. + */ +extern const br_hash_class br_sha384_vtable; + +/** + * \brief SHA-384 context. + * + * First field is a pointer to the vtable; it is set by the initialisation + * function. Other fields are not supposed to be accessed by user code. + */ +typedef struct { + /** + * \brief Pointer to vtable for this context. + */ + const br_hash_class *vtable; +#ifndef BR_DOXYGEN_IGNORE + unsigned char buf[128]; + uint64_t count; + uint64_t val[8]; +#endif +} br_sha384_context; + +/** + * \brief SHA-384 context initialisation. + * + * This function initialises or resets a context for a new SHA-384 + * computation. It also sets the vtable pointer. + * + * \param ctx pointer to the context structure. + */ +void br_sha384_init(br_sha384_context *ctx); + +/** + * \brief Inject some data bytes in a running SHA-384 computation. + * + * The provided context is updated with some data bytes. If the number + * of bytes (`len`) is zero, then the data pointer (`data`) is ignored + * and may be `NULL`, and this function does nothing. + * + * \param ctx pointer to the context structure. + * \param data pointer to the injected data. + * \param len injected data length (in bytes). + */ +void br_sha384_update(br_sha384_context *ctx, const void *data, size_t len); + +/** + * \brief Compute SHA-384 output. + * + * The SHA-384 output for the concatenation of all bytes injected in the + * provided context since the last initialisation or reset call, is + * computed and written in the buffer pointed to by `out`. The context + * itself is not modified, so extra bytes may be injected afterwards + * to continue that computation. + * + * \param ctx pointer to the context structure. + * \param out destination buffer for the hash output. + */ +void br_sha384_out(const br_sha384_context *ctx, void *out); + +/** + * \brief Save SHA-384 running state. + * + * The running state for SHA-384 (output of the last internal block + * processing) is written in the buffer pointed to by `out`. The + * number of bytes injected since the last initialisation or reset + * call is returned. The context is not modified. + * + * \param ctx pointer to the context structure. + * \param out destination buffer for the running state. + * \return the injected total byte length. + */ +uint64_t br_sha384_state(const br_sha384_context *ctx, void *out); + +/** + * \brief Restore SHA-384 running state. + * + * The running state for SHA-384 is set to the provided values. + * + * \param ctx pointer to the context structure. + * \param stb source buffer for the running state. + * \param count the injected total byte length. + */ +void br_sha384_set_state(br_sha384_context *ctx, + const void *stb, uint64_t count); + +/** + * \brief Symbolic identifier for SHA-512. + */ +#define br_sha512_ID 6 + +/** + * \brief SHA-512 output size (in bytes). + */ +#define br_sha512_SIZE 64 + +/** + * \brief Constant vtable for SHA-512. + */ +extern const br_hash_class br_sha512_vtable; + +#ifdef BR_DOXYGEN_IGNORE +/** + * \brief SHA-512 context. + * + * First field is a pointer to the vtable; it is set by the initialisation + * function. Other fields are not supposed to be accessed by user code. + */ +typedef struct { + /** + * \brief Pointer to vtable for this context. + */ + const br_hash_class *vtable; +} br_sha512_context; +#else +typedef br_sha384_context br_sha512_context; +#endif + +/** + * \brief SHA-512 context initialisation. + * + * This function initialises or resets a context for a new SHA-512 + * computation. It also sets the vtable pointer. + * + * \param ctx pointer to the context structure. + */ +void br_sha512_init(br_sha512_context *ctx); + +#ifdef BR_DOXYGEN_IGNORE +/** + * \brief Inject some data bytes in a running SHA-512 computation. + * + * The provided context is updated with some data bytes. If the number + * of bytes (`len`) is zero, then the data pointer (`data`) is ignored + * and may be `NULL`, and this function does nothing. + * + * \param ctx pointer to the context structure. + * \param data pointer to the injected data. + * \param len injected data length (in bytes). + */ +void br_sha512_update(br_sha512_context *ctx, const void *data, size_t len); +#else +#define br_sha512_update br_sha384_update +#endif + +/** + * \brief Compute SHA-512 output. + * + * The SHA-512 output for the concatenation of all bytes injected in the + * provided context since the last initialisation or reset call, is + * computed and written in the buffer pointed to by `out`. The context + * itself is not modified, so extra bytes may be injected afterwards + * to continue that computation. + * + * \param ctx pointer to the context structure. + * \param out destination buffer for the hash output. + */ +void br_sha512_out(const br_sha512_context *ctx, void *out); + +#ifdef BR_DOXYGEN_IGNORE +/** + * \brief Save SHA-512 running state. + * + * The running state for SHA-512 (output of the last internal block + * processing) is written in the buffer pointed to by `out`. The + * number of bytes injected since the last initialisation or reset + * call is returned. The context is not modified. + * + * \param ctx pointer to the context structure. + * \param out destination buffer for the running state. + * \return the injected total byte length. + */ +uint64_t br_sha512_state(const br_sha512_context *ctx, void *out); +#else +#define br_sha512_state br_sha384_state +#endif + +#ifdef BR_DOXYGEN_IGNORE +/** + * \brief Restore SHA-512 running state. + * + * The running state for SHA-512 is set to the provided values. + * + * \param ctx pointer to the context structure. + * \param stb source buffer for the running state. + * \param count the injected total byte length. + */ +void br_sha512_set_state(br_sha512_context *ctx, + const void *stb, uint64_t count); +#else +#define br_sha512_set_state br_sha384_set_state +#endif + +/* + * "md5sha1" is a special hash function that computes both MD5 and SHA-1 + * on the same input, and produces a 36-byte output (MD5 and SHA-1 + * concatenation, in that order). State size is also 36 bytes. + */ + +/** + * \brief Symbolic identifier for MD5+SHA-1. + * + * MD5+SHA-1 is the concatenation of MD5 and SHA-1, computed over the + * same input. It is not one of the functions identified in TLS, so + * we give it a symbolic identifier of value 0. + */ +#define br_md5sha1_ID 0 + +/** + * \brief MD5+SHA-1 output size (in bytes). + */ +#define br_md5sha1_SIZE 36 + +/** + * \brief Constant vtable for MD5+SHA-1. + */ +extern const br_hash_class br_md5sha1_vtable; + +/** + * \brief MD5+SHA-1 context. + * + * First field is a pointer to the vtable; it is set by the initialisation + * function. Other fields are not supposed to be accessed by user code. + */ +typedef struct { + /** + * \brief Pointer to vtable for this context. + */ + const br_hash_class *vtable; +#ifndef BR_DOXYGEN_IGNORE + unsigned char buf[64]; + uint64_t count; + uint32_t val_md5[4]; + uint32_t val_sha1[5]; +#endif +} br_md5sha1_context; + +/** + * \brief MD5+SHA-1 context initialisation. + * + * This function initialises or resets a context for a new SHA-512 + * computation. It also sets the vtable pointer. + * + * \param ctx pointer to the context structure. + */ +void br_md5sha1_init(br_md5sha1_context *ctx); + +/** + * \brief Inject some data bytes in a running MD5+SHA-1 computation. + * + * The provided context is updated with some data bytes. If the number + * of bytes (`len`) is zero, then the data pointer (`data`) is ignored + * and may be `NULL`, and this function does nothing. + * + * \param ctx pointer to the context structure. + * \param data pointer to the injected data. + * \param len injected data length (in bytes). + */ +void br_md5sha1_update(br_md5sha1_context *ctx, const void *data, size_t len); + +/** + * \brief Compute MD5+SHA-1 output. + * + * The MD5+SHA-1 output for the concatenation of all bytes injected in the + * provided context since the last initialisation or reset call, is + * computed and written in the buffer pointed to by `out`. The context + * itself is not modified, so extra bytes may be injected afterwards + * to continue that computation. + * + * \param ctx pointer to the context structure. + * \param out destination buffer for the hash output. + */ +void br_md5sha1_out(const br_md5sha1_context *ctx, void *out); + +/** + * \brief Save MD5+SHA-1 running state. + * + * The running state for MD5+SHA-1 (output of the last internal block + * processing) is written in the buffer pointed to by `out`. The + * number of bytes injected since the last initialisation or reset + * call is returned. The context is not modified. + * + * \param ctx pointer to the context structure. + * \param out destination buffer for the running state. + * \return the injected total byte length. + */ +uint64_t br_md5sha1_state(const br_md5sha1_context *ctx, void *out); + +/** + * \brief Restore MD5+SHA-1 running state. + * + * The running state for MD5+SHA-1 is set to the provided values. + * + * \param ctx pointer to the context structure. + * \param stb source buffer for the running state. + * \param count the injected total byte length. + */ +void br_md5sha1_set_state(br_md5sha1_context *ctx, + const void *stb, uint64_t count); + +/** + * \brief Aggregate context for configurable hash function support. + * + * The `br_hash_compat_context` type is a type which is large enough to + * serve as context for all standard hash functions defined above. + */ +typedef union { + const br_hash_class *vtable; + br_md5_context md5; + br_sha1_context sha1; + br_sha224_context sha224; + br_sha256_context sha256; + br_sha384_context sha384; + br_sha512_context sha512; + br_md5sha1_context md5sha1; +} br_hash_compat_context; + +/* + * The multi-hasher is a construct that handles hashing of the same input + * data with several hash functions, with a single shared input buffer. + * It can handle MD5, SHA-1, SHA-224, SHA-256, SHA-384 and SHA-512 + * simultaneously, though which functions are activated depends on + * the set implementation pointers. + */ + +/** + * \brief Multi-hasher context structure. + * + * The multi-hasher runs up to six hash functions in the standard TLS list + * (MD5, SHA-1, SHA-224, SHA-256, SHA-384 and SHA-512) in parallel, over + * the same input. + * + * The multi-hasher does _not_ follow the OOP structure with a vtable. + * Instead, it is configured with the vtables of the hash functions it + * should run. Structure fields are not supposed to be accessed directly. + */ +typedef struct { +#ifndef BR_DOXYGEN_IGNORE + unsigned char buf[128]; + uint64_t count; + uint32_t val_32[25]; + uint64_t val_64[16]; + const br_hash_class *impl[6]; +#endif +} br_multihash_context; + +/** + * \brief Clear a multi-hasher context. + * + * This should always be called once on a given context, _before_ setting + * the implementation pointers. + * + * \param ctx the multi-hasher context. + */ +void br_multihash_zero(br_multihash_context *ctx); + +/** + * \brief Set a hash function implementation. + * + * Implementations shall be set _after_ clearing the context (with + * `br_multihash_zero()`) but _before_ initialising the computation + * (with `br_multihash_init()`). The hash function implementation + * MUST be one of the standard hash functions (MD5, SHA-1, SHA-224, + * SHA-256, SHA-384 or SHA-512); it may also be `NULL` to remove + * an implementation from the multi-hasher. + * + * \param ctx the multi-hasher context. + * \param id the hash function symbolic identifier. + * \param impl the hash function vtable, or `NULL`. + */ +static inline void +br_multihash_setimpl(br_multihash_context *ctx, + int id, const br_hash_class *impl) +{ + /* + * This code relies on hash functions ID being values 1 to 6, + * in the MD5 to SHA-512 order. + */ + ctx->impl[id - 1] = impl; +} + +/** + * \brief Get a hash function implementation. + * + * This function returns the currently configured vtable for a given + * hash function (by symbolic ID). If no such function was configured in + * the provided multi-hasher context, then this function returns `NULL`. + * + * \param ctx the multi-hasher context. + * \param id the hash function symbolic identifier. + * \return the hash function vtable, or `NULL`. + */ +static inline const br_hash_class * +br_multihash_getimpl(const br_multihash_context *ctx, int id) +{ + return ctx->impl[id - 1]; +} + +/** + * \brief Reset a multi-hasher context. + * + * This function prepares the context for a new hashing computation, + * for all implementations configured at that point. + * + * \param ctx the multi-hasher context. + */ +void br_multihash_init(br_multihash_context *ctx); + +/** + * \brief Inject some data bytes in a running multi-hashing computation. + * + * The provided context is updated with some data bytes. If the number + * of bytes (`len`) is zero, then the data pointer (`data`) is ignored + * and may be `NULL`, and this function does nothing. + * + * \param ctx pointer to the context structure. + * \param data pointer to the injected data. + * \param len injected data length (in bytes). + */ +void br_multihash_update(br_multihash_context *ctx, + const void *data, size_t len); + +/** + * \brief Compute a hash output from a multi-hasher. + * + * The hash output for the concatenation of all bytes injected in the + * provided context since the last initialisation or reset call, is + * computed and written in the buffer pointed to by `dst`. The hash + * function to use is identified by `id` and must be one of the standard + * hash functions. If that hash function was indeed configured in the + * multi-hasher context, the corresponding hash value is written in + * `dst` and its length (in bytes) is returned. If the hash function + * was _not_ configured, then nothing is written in `dst` and 0 is + * returned. + * + * The context itself is not modified, so extra bytes may be injected + * afterwards to continue the hash computations. + * + * \param ctx pointer to the context structure. + * \param id the hash function symbolic identifier. + * \param dst destination buffer for the hash output. + * \return the hash output length (in bytes), or 0. + */ +size_t br_multihash_out(const br_multihash_context *ctx, int id, void *dst); + +/** + * \brief Type for a GHASH implementation. + * + * GHASH is a sort of keyed hash meant to be used to implement GCM in + * combination with a block cipher (with 16-byte blocks). + * + * The `y` array has length 16 bytes and is used for input and output; in + * a complete GHASH run, it starts with an all-zero value. `h` is a 16-byte + * value that serves as key (it is derived from the encryption key in GCM, + * using the block cipher). The data length (`len`) is expressed in bytes. + * The `y` array is updated. + * + * If the data length is not a multiple of 16, then the data is implicitly + * padded with zeros up to the next multiple of 16. Thus, when using GHASH + * in GCM, this method may be called twice, for the associated data and + * for the ciphertext, respectively; the zero-padding implements exactly + * the GCM rules. + * + * \param y the array to update. + * \param h the GHASH key. + * \param data the input data (may be `NULL` if `len` is zero). + * \param len the input data length (in bytes). + */ +typedef void (*br_ghash)(void *y, const void *h, const void *data, size_t len); + +/** + * \brief GHASH implementation using multiplications (mixed 32-bit). + * + * This implementation uses multiplications of 32-bit values, with a + * 64-bit result. It is constant-time (if multiplications are + * constant-time). + * + * \param y the array to update. + * \param h the GHASH key. + * \param data the input data (may be `NULL` if `len` is zero). + * \param len the input data length (in bytes). + */ +void br_ghash_ctmul(void *y, const void *h, const void *data, size_t len); + +/** + * \brief GHASH implementation using multiplications (strict 32-bit). + * + * This implementation uses multiplications of 32-bit values, with a + * 32-bit result. It is usually somewhat slower than `br_ghash_ctmul()`, + * but it is expected to be faster on architectures for which the + * 32-bit multiplication opcode does not yield the upper 32 bits of the + * product. It is constant-time (if multiplications are constant-time). + * + * \param y the array to update. + * \param h the GHASH key. + * \param data the input data (may be `NULL` if `len` is zero). + * \param len the input data length (in bytes). + */ +void br_ghash_ctmul32(void *y, const void *h, const void *data, size_t len); + +/** + * \brief GHASH implementation using multiplications (64-bit). + * + * This implementation uses multiplications of 64-bit values, with a + * 64-bit result. It is constant-time (if multiplications are + * constant-time). It is substantially faster than `br_ghash_ctmul()` + * and `br_ghash_ctmul32()` on most 64-bit architectures. + * + * \param y the array to update. + * \param h the GHASH key. + * \param data the input data (may be `NULL` if `len` is zero). + * \param len the input data length (in bytes). + */ +void br_ghash_ctmul64(void *y, const void *h, const void *data, size_t len); + +/** + * \brief GHASH implementation using the `pclmulqdq` opcode (part of the + * AES-NI instructions). + * + * This implementation is available only on x86 platforms where the + * compiler supports the relevant intrinsic functions. Even if the + * compiler supports these functions, the local CPU might not support + * the `pclmulqdq` opcode, meaning that a call will fail with an + * illegal instruction exception. To safely obtain a pointer to this + * function when supported (or 0 otherwise), use `br_ghash_pclmul_get()`. + * + * \param y the array to update. + * \param h the GHASH key. + * \param data the input data (may be `NULL` if `len` is zero). + * \param len the input data length (in bytes). + */ +void br_ghash_pclmul(void *y, const void *h, const void *data, size_t len); + +/** + * \brief Obtain the `pclmul` GHASH implementation, if available. + * + * If the `pclmul` implementation was compiled in the library (depending + * on the compiler abilities) _and_ the local CPU appears to support the + * opcode, then this function will return a pointer to the + * `br_ghash_pclmul()` function. Otherwise, it will return `0`. + * + * \return the `pclmul` GHASH implementation, or `0`. + */ +br_ghash br_ghash_pclmul_get(void); + +/** + * \brief GHASH implementation using the POWER8 opcodes. + * + * This implementation is available only on POWER8 platforms (and later). + * To safely obtain a pointer to this function when supported (or 0 + * otherwise), use `br_ghash_pwr8_get()`. + * + * \param y the array to update. + * \param h the GHASH key. + * \param data the input data (may be `NULL` if `len` is zero). + * \param len the input data length (in bytes). + */ +void br_ghash_pwr8(void *y, const void *h, const void *data, size_t len); + +/** + * \brief Obtain the `pwr8` GHASH implementation, if available. + * + * If the `pwr8` implementation was compiled in the library (depending + * on the compiler abilities) _and_ the local CPU appears to support the + * opcode, then this function will return a pointer to the + * `br_ghash_pwr8()` function. Otherwise, it will return `0`. + * + * \return the `pwr8` GHASH implementation, or `0`. + */ +br_ghash br_ghash_pwr8_get(void); + +#ifdef __cplusplus +} +#endif + +#endif \ No newline at end of file diff --git a/CommSerial.cpp b/CommSerial.cpp new file mode 100644 index 0000000..4506746 --- /dev/null +++ b/CommSerial.cpp @@ -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++; + } + } +} \ No newline at end of file diff --git a/Config.cpp b/Config.cpp new file mode 100644 index 0000000..4c0a467 --- /dev/null +++ b/Config.cpp @@ -0,0 +1,158 @@ +#include "HermitCrab.h" +#include "AHT2x.h" +#include "Config.h" +#include "TimeManager.h" +#include + +#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"); +} diff --git a/Config.h b/Config.h new file mode 100644 index 0000000..54e7d7d --- /dev/null +++ b/Config.h @@ -0,0 +1,197 @@ +# ifndef __CONFIG_H +# define __CONFIG_H + +#include +#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 \ No newline at end of file diff --git a/ConnectWiFi.cpp b/ConnectWiFi.cpp new file mode 100644 index 0000000..dbf5453 --- /dev/null +++ b/ConnectWiFi.cpp @@ -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; + } +} \ No newline at end of file diff --git a/ConnectWiFi.h b/ConnectWiFi.h new file mode 100644 index 0000000..69c3ba4 --- /dev/null +++ b/ConnectWiFi.h @@ -0,0 +1,13 @@ +#ifndef __CONNECT_WIFI_H +#define __CONNECT_WIFI_H +#ifdef ESP8266 +#include +#else +#include +#endif + +void checkWiFi(unsigned long tick); +void WiFiEvent(WiFiEvent_t event); +inline bool isWiFiConnected() { return WiFi.status() == WL_CONNECTED; }; +#endif + diff --git a/HCUpdate.h b/HCUpdate.h new file mode 100644 index 0000000..16c8c8c --- /dev/null +++ b/HCUpdate.h @@ -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 +#include +#include +#include +#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 THandlerFunction_Progress; + + UpdateClass(); + UpdateClass(int httpClientTimeout); + + int update(WiFiClientSecure& client, String &url, uint16_t port, String& uri, + String ¤tVersion, 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 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) diff --git a/HCUpdater.cpp b/HCUpdater.cpp new file mode 100644 index 0000000..3ebbb21 --- /dev/null +++ b/HCUpdater.cpp @@ -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 +#include +#include +#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) \ No newline at end of file diff --git a/HCesp.ino b/HCesp.ino new file mode 100644 index 0000000..a9f9d6a --- /dev/null +++ b/HCesp.ino @@ -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(timeinfo.tm_sec); + g_nMinute = static_cast(timeinfo.tm_min); + g_nHour = static_cast(timeinfo.tm_hour); + g_nDay = static_cast(timeinfo.tm_mday); + g_nMonth = static_cast(timeinfo.tm_mon) + 1; + g_nYear = timeinfo.tm_year + 1900; + +} + +inline void setZCD() { + // ZCD + status.zcdAC = zcdACCount; + zcdACCount = 0; + status.zcdLoad = zcdLoadCount; + zcdLoadCount = 0; + if (status.zcdAC < 118 || status.zcdAC > 122) { + status.nFlags |= FLAG_ZCD_AC; + } + else { + status.nFlags &= ~FLAG_ZCD_AC; + } + if (status.zcdLoad < 118 || status.zcdLoad > 122) { + status.nFlags |= FLAG_ZCD_LOAD; + } + else { + status.nFlags &= ~FLAG_ZCD_LOAD; + } +} \ No newline at end of file diff --git a/HermitCrab.h b/HermitCrab.h new file mode 100644 index 0000000..8bfb40f --- /dev/null +++ b/HermitCrab.h @@ -0,0 +1,243 @@ +#ifndef __HERMIT_CRAB_H +#define __HERMIT_CRAB_H +#include + +#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 diff --git a/HermitCrab.vcxproj b/HermitCrab.vcxproj new file mode 100644 index 0000000..6441600 --- /dev/null +++ b/HermitCrab.vcxproj @@ -0,0 +1,72 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 16.0 + {9EFFA72D-8A81-44D2-B7B4-37C53EBAD29F} + + + + Application + true + v142 + + + Application + false + v142 + + + Application + true + v142 + + + Application + false + v142 + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/HermitCrab.vcxproj.user b/HermitCrab.vcxproj.user new file mode 100644 index 0000000..88a5509 --- /dev/null +++ b/HermitCrab.vcxproj.user @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/History.cpp b/History.cpp new file mode 100644 index 0000000..450df18 --- /dev/null +++ b/History.cpp @@ -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); +} + + + diff --git a/History.h b/History.h new file mode 100644 index 0000000..6079091 --- /dev/null +++ b/History.h @@ -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 + diff --git a/LED0.cpp b/LED0.cpp new file mode 100644 index 0000000..5bddf47 --- /dev/null +++ b/LED0.cpp @@ -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; + } +}; diff --git a/LED0.h b/LED0.h new file mode 100644 index 0000000..1e91455 --- /dev/null +++ b/LED0.h @@ -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 \ No newline at end of file diff --git a/NTC_10K.cpp b/NTC_10K.cpp new file mode 100644 index 0000000..85e14d7 --- /dev/null +++ b/NTC_10K.cpp @@ -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; +} diff --git a/NTC_10K.h b/NTC_10K.h new file mode 100644 index 0000000..2aefd5b --- /dev/null +++ b/NTC_10K.h @@ -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 \ No newline at end of file diff --git a/OTA.cpp b/OTA.cpp new file mode 100644 index 0000000..f194867 --- /dev/null +++ b/OTA.cpp @@ -0,0 +1,134 @@ +#define NO_GLOBAL_UPDATE +#include +#include +#include +#include +#include + +#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 ¤tVersion, short nDeviceType, bool bForceUpdate); + int result = hcUpdate.update(client, url, UPDATE_PORT, uri, version, (short)config.m_nDeviceType, true); + + + + return false; +} \ No newline at end of file diff --git a/OTA.h b/OTA.h new file mode 100644 index 0000000..fc7c5b8 --- /dev/null +++ b/OTA.h @@ -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 \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..49cc8ef Binary files /dev/null and b/README.md differ diff --git a/SSD1306.cpp b/SSD1306.cpp new file mode 100644 index 0000000..bae93c6 --- /dev/null +++ b/SSD1306.cpp @@ -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(); +} diff --git a/SSD1306.h b/SSD1306.h new file mode 100644 index 0000000..58e2531 --- /dev/null +++ b/SSD1306.h @@ -0,0 +1,86 @@ +#ifndef SSD1306_H +#define SSD1306_H + +#include +#include + +#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 diff --git a/Setup.cpp b/Setup.cpp new file mode 100644 index 0000000..9d32cef --- /dev/null +++ b/Setup.cpp @@ -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 +#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(); +} \ No newline at end of file diff --git a/Task0.ino b/Task0.ino new file mode 100644 index 0000000..a9f9d03 --- /dev/null +++ b/Task0.ino @@ -0,0 +1,128 @@ +#include "HermitCrab.h" +#include "Config.h" +#include "ConnectWiFi.h" +#include "UI.h" +#include +#include + +#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 +// ================================================================================== diff --git a/TimeManager.cpp b/TimeManager.cpp new file mode 100644 index 0000000..3c187cd --- /dev/null +++ b/TimeManager.cpp @@ -0,0 +1,92 @@ +#include "HermitCrab.h" +#include "TimeManager.h" +#include "ConnectWiFi.h" +#include +#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); +} diff --git a/TimeManager.h b/TimeManager.h new file mode 100644 index 0000000..8420002 --- /dev/null +++ b/TimeManager.h @@ -0,0 +1,33 @@ +#ifndef TIMEMANAGER_H +#define TIMEMANAGER_H + +#include + +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 \ No newline at end of file diff --git a/UI.cpp b/UI.cpp new file mode 100644 index 0000000..9b790d3 --- /dev/null +++ b/UI.cpp @@ -0,0 +1,1152 @@ + +#include "HermitCrab.h" +#include "Config.h" +#include "ConnectWiFi.h" +#include "TimeManager.h" +#include "WiFiHost.h" + +#include "UI.h" +#include // 35 +#include // 25 +#include +#include + +#define TAG_UI "UI" +// Buttons +#define DEBOUNCE_DELAY 100 +#define LONG_PRESS_DURATION 1500 + +// Display +#define SPACING 2 +#define FONT_DESCENT 3 +#define POS_X_UNIT 112 +#define WIDTH_UNIT 16 +#define HEIGHT_UNIT 16 + +#define POS_Y_BOTTOM (SCREEN_HEIGHT - 1) +#define WIDTH_D3 20 +#define POS_X_D3 (POS_X_UNIT - WIDTH_D3 - SPACING) +#define HEIGHT_D3 26 + +#define WIDTH_DOT 7 +#define POS_X_DOT (POS_X_D3 - WIDTH_DOT - SPACING) +#define HEIGHT_DOT 7 + +#define WIDTH_D2 26 +#define POS_X_D2 (POS_X_DOT - WIDTH_D2 - SPACING) +#define HEIGHT_D2 36 + +#define WIDTH_D1 WIDTH_D2 +#define POS_X_D1 (POS_X_D2 - WIDTH_D1 - SPACING) +#define HEIGHT_D1 HEIGHT_D2 + +#define WIDTH_D0 WIDTH_D1 +#define POS_X_D0 (POS_X_D1 - WIDTH_D1 - SPACING) +#define HEIGHT_D0 HEIGHT_D1 + +enum MAIN_MODE { + MODE_TEMP, + MODE_HUMID, + MODE_CLOCK +}; + +//Adafruit_SSD1306 ssd1306(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1); +CUI ui; + +const uint8_t logo[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x3F, 0x3F, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x01, 0xC0, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x01, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x26, 0x17, 0xC0, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x88, 0x20, 0x30, 0x00, 0x00, 0x80, 0x04, 0x02, 0x02, 0xC1, 0xFC, 0x00, 0x00, 0x80, + 0x04, 0x06, 0x5E, 0x03, 0xC3, 0x00, 0x00, 0x80, 0x04, 0x04, 0xF8, 0x03, 0x00, 0xC0, 0x00, 0x80, + 0x04, 0x00, 0xF0, 0x07, 0x00, 0x30, 0x00, 0x80, 0x08, 0x11, 0x18, 0x0D, 0x00, 0xC8, 0x03, 0x00, + 0x08, 0x12, 0x20, 0x0C, 0x81, 0xFC, 0x05, 0x00, 0x10, 0x26, 0x50, 0x0F, 0x81, 0xFE, 0x0A, 0x00, + 0x08, 0x64, 0xF0, 0x1D, 0x86, 0x67, 0x14, 0x00, 0x08, 0x41, 0x80, 0x18, 0xC7, 0x31, 0xF8, 0x00, + 0x08, 0xCB, 0x80, 0x3B, 0xC6, 0x1B, 0x20, 0x00, 0x08, 0xCB, 0x80, 0x7C, 0x66, 0x1F, 0xE0, 0x00, + 0x09, 0xDF, 0xB0, 0x10, 0x60, 0x39, 0xE0, 0x00, 0x09, 0x87, 0xE0, 0x38, 0xF8, 0x79, 0x90, 0x00, + 0x0B, 0xAE, 0x00, 0x7C, 0x7C, 0xF1, 0xFF, 0x00, 0x13, 0x6E, 0x00, 0x3C, 0x60, 0x98, 0xFD, 0x80, + 0x13, 0x08, 0x40, 0x7C, 0x01, 0x9E, 0xFD, 0xC0, 0x17, 0x1C, 0x00, 0xFC, 0x03, 0x8D, 0xFB, 0xC0, + 0x16, 0x5C, 0x02, 0x3C, 0x0E, 0xFF, 0xFB, 0xE0, 0x0E, 0x5C, 0x04, 0x78, 0x1F, 0x9F, 0x99, 0xF0, + 0x0C, 0x9C, 0x60, 0x78, 0x77, 0xFF, 0xFF, 0xF8, 0x04, 0x9F, 0xC0, 0xB3, 0xFF, 0xF6, 0x9F, 0x7C, + 0x06, 0x9F, 0x01, 0x1F, 0x7E, 0xFF, 0xFF, 0xBC, 0x06, 0x1C, 0x00, 0x7F, 0xFB, 0x16, 0xEF, 0xFC, + 0x02, 0x48, 0x01, 0xF7, 0xFF, 0xC7, 0xCF, 0xFE, 0x01, 0x08, 0x03, 0xBE, 0xF7, 0xF4, 0x0F, 0xFA, + 0x00, 0x90, 0x0F, 0xFA, 0xFF, 0xB4, 0x07, 0x7E, 0x00, 0x40, 0x1D, 0xFF, 0x77, 0x42, 0x07, 0xBA, + 0x00, 0x40, 0x2F, 0xF7, 0x7B, 0x41, 0x06, 0x5B, 0x00, 0x41, 0xFF, 0x6E, 0xDC, 0xC3, 0x07, 0x7F, + 0x00, 0x27, 0x7D, 0x3C, 0xDF, 0xCB, 0x87, 0x5B, 0x00, 0x3F, 0xFF, 0xF6, 0xCE, 0x59, 0xC6, 0xF9, + 0x00, 0x1E, 0xF7, 0x7A, 0xC7, 0xFB, 0xC7, 0x6D, 0x00, 0x0B, 0xFE, 0xDF, 0xE8, 0x9B, 0x86, 0x7D, + 0x00, 0x0B, 0xC5, 0x8F, 0xF0, 0xC3, 0x80, 0x3F, 0x00, 0x17, 0x85, 0x85, 0xE8, 0xFB, 0xC0, 0x37, + 0x00, 0x3E, 0x0B, 0x05, 0x68, 0xBE, 0xE0, 0x37, 0x00, 0x7C, 0x13, 0x05, 0xF8, 0xBF, 0xF0, 0x16, + 0x00, 0xB8, 0x26, 0x09, 0x70, 0xFD, 0xF0, 0x1E, 0x01, 0x70, 0x24, 0x0A, 0x70, 0xFF, 0xF8, 0x1C, + 0x03, 0xC0, 0x58, 0x0A, 0x50, 0xDF, 0xB8, 0x1C, 0x07, 0x80, 0x98, 0x0E, 0x50, 0xFB, 0xF8, 0x1C, + 0x1F, 0x00, 0xB0, 0x16, 0x70, 0x7D, 0xF0, 0x18, 0x3C, 0x01, 0x60, 0x14, 0x60, 0x3D, 0xF0, 0x00, + 0x78, 0x01, 0xE0, 0x14, 0x60, 0x1E, 0xF0, 0x00, 0xF0, 0x03, 0x80, 0x18, 0xE0, 0x07, 0xE0, 0x00, + 0xC0, 0x07, 0x00, 0x38, 0xE0, 0x03, 0x80, 0x00, 0x00, 0x0E, 0x00, 0x31, 0xC0, 0x00, 0x00, 0x00, + 0x00, 0x1E, 0x00, 0x71, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x60, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; + +const uint8_t degreeC[] = { + 0x60, 0x00, + 0x90, 0x00, + 0x90, 0x00, + 0x63, 0xE4, + 0x0E, 0x1C, + 0x1C, 0x04, + 0x18, 0x00, + 0x38, 0x00, + 0x38, 0x00, + 0x38, 0x00, + 0x38, 0x00, + 0x38, 0x00, + 0x1C, 0x04, + 0x0E, 0x08, + 0x07, 0xF0, + 0x00, 0x00 +}; + +const uint8_t percentSign[] = { + 0x00, 0x00, + 0x38, 0x18, + 0x7C, 0x30, + 0xC6, 0x30, + 0xC6, 0x60, + 0xC6, 0xc0, + 0x7C, 0xC0, + 0x39, 0x80, + 0x01, 0x9C, + 0x03, 0x3E, + 0x03, 0x63, + 0x06, 0x63, + 0x0C, 0x63, + 0x0C, 0x3E, + 0x18, 0x1C, + 0x00, 0x00 +}; + + +enum UI_ITEM { + ITEM_CLOCK, + + // Sensor + ITEM_TEMP1, + ITEM_TEMP2, + ITEM_TEMP3, + ITEM_HUMID, + ITEM_HUMID2, + + // SV + ITEM_TEMP_TARGET, + ITEM_HUMID_TARGET, + + // CV + ITEM_HEAT1, + ITEM_HEAT2, + ITEM_MIST, + ITEM_FAN, + ITEM_MOTOR, + ITEM_LIGHT, + + ITEM_HEAT1_MANUAL, + ITEM_HEAT2_MANUAL, + ITEM_MIST_MANUAL, + ITEM_FAN_MANUAL, + ITEM_MOTOR_MANUAL, + ITEM_LIGHT_MANUAL, + ITEM_CHECK_AC, + + ITEM_COUNT +}; + +char *title[] = { + "Time:", + + "Temp1", "Temp2", "Temp3", + + "RH1", "RH2", + + "TT", "RHT", + + "Heat1", "Heat2", "Mist", "Fan", "Pump", "Lum", + + "Ht1 M", "Ht2 M", "Mst M", "Fan M", "Pmp M", "Lum M", + + "Chk AC" }; + +uint8_t *unit[] = { (uint8_t *) degreeC, (uint8_t *) percentSign, nullptr }; +const uint16_t main_unit_idx[] = { 0, 1, 2 }; +const uint16_t item_unit_idx[] = { + 2, // Time + 0, 0, 0, // Temp + 1, 1, // Humid + 0, 1, // SV + 1, 1, 1, 1, 1, 1, // CV + 2, 2, 2, 2, 2, 2, // Manual + 2 }; + +const bool set_idx[] = { + false, + false, false, false, + false, false, + + true, true, + + true, true, true, true, true, true, + true, true, true, true, true, true, + + true }; + +const bool fineControl[] = { + false, + false, false, false, + false, false, + + true, true, + + false, false, false, false, false, false, + true, true, true, true, true, true, + + true }; + +// +// Buttons +// +void buttonSetISR(); +void buttonUpISR(); +void buttonDownISR(); + + +// Constructor for CUI class +//CUI::CUI(Adafruit_SSD1306 &display) +CUI::CUI() + : SSD1306() +{} + +void CUI::setup() { + // Initialize the display + ESP_LOGI(TAG_UI," UI - setup()"); + bOK = false; + bDot = false; + bButtonChanged = false; + m_nMessageMode = 0; + m_nMainMode = 0; + m_nItemMode = 0; + m_nItem = 0; + m_nValue = 0; + m_pUnit = (uint8_t *) unit[item_unit_idx[m_nItem]]; + m_nD0 = m_nD1 = m_nD2 = m_nD3 = 0; + m_pDUnit = nullptr; + + lastMessageMode = 1; + lastMainMode = 1; + lastItemMode = 1; + lastItem = 999; + lastValue = 999; + lastUnit = nullptr; + + lastD0 = 999; + lastD1 = 999; + lastD2 = 999; + lastD3 = 999; + lastpDUnit = nullptr; + + // Check if device exists + for (int i = 0; i < 5; i++) { + Wire.beginTransmission(i2caddr); + if (Wire.endTransmission() == 0) { + bOK = true; + ESP_LOGI(TAG_UI," UI - device Found at 0x%02X\n", i2caddr); + break; + } + delay(50); + } + + if (!bOK) { + ESP_LOGI(TAG_UI," UI - device NOT found at 0x%02X\n", i2caddr); + return; + } + + // Init Display hardware + begin(SSD1306_SWITCHCAPVCC, i2caddr); + //setContrast(contrast / 2); + setTextColor(SSD1306_WHITE); + updateScreen(); + + // Display Logo + int x = (width() - 64); + int y = (height() - 64) / 2; + drawBitmap(x, y, logo, 64, 64, 1); + updateScreen(); + ssd1306_hscroll(true, 0, 7, x, 32); + + // Box + boxMode = { 0, HEIGHT_UNIT, 24, 8 }; + boxTitle = { 0, 0, 64, 16 }; + boxValue = { 0, 0, 0, 16 }; + boxUnit = { POS_X_UNIT, 0, WIDTH_UNIT, HEIGHT_UNIT }; + + boxDUnit = {POS_X_UNIT, POS_Y_BOTTOM - HEIGHT_UNIT, WIDTH_UNIT, HEIGHT_UNIT}; // 16 + boxD3 = {POS_X_D3, POS_Y_BOTTOM - HEIGHT_D3, WIDTH_D3, HEIGHT_D3}; // 17 + 2 + boxDot = {POS_X_DOT, POS_Y_BOTTOM - HEIGHT_DOT, WIDTH_DOT, HEIGHT_DOT}; // 7 + 2 + boxD2 = {POS_X_D2, POS_Y_BOTTOM - HEIGHT_D2, WIDTH_D2, HEIGHT_D2}; // 17 + 2 + boxD1 = {POS_X_D1, POS_Y_BOTTOM - HEIGHT_D1, WIDTH_D1, HEIGHT_D1}; // 17 + 2 + boxD0 = {POS_X_D0, POS_Y_BOTTOM - HEIGHT_D0, WIDTH_D0, HEIGHT_D0}; // 17 + 2 + + + // + // Buttons + // + pinMode(PIN_SW_SET, INPUT_PULLUP); + pinMode(PIN_SW_UP, INPUT_PULLUP); + pinMode(PIN_SW_DOWN, INPUT_PULLUP); + attachInterrupt(digitalPinToInterrupt(PIN_SW_SET), buttonSetISR, CHANGE); + attachInterrupt(digitalPinToInterrupt(PIN_SW_UP), buttonUpISR, CHANGE); + attachInterrupt(digitalPinToInterrupt(PIN_SW_DOWN), buttonDownISR, CHANGE); + initButtonState(); +} + +void CUI::start() { + vTaskDelay(1500/portTICK_PERIOD_MS); + clearDisplayBuffer(); + updateScreen(); + initButtonState(); + //getBoundaries(); +} + +void CUI::getBoundaries() { + // Display Boundary + { + int16_t x, y; + uint16_t w, h; + char sz[2]; + sz[1] = 0; + int nMaxW24 = 0; + int nMaxH24 = 0; + int nMaxW18 = 0; + int nMaxH18 = 0; + int nMaxW9 = 0; + int nMaxH9 = 0; + int nMaxW24A = 0; + int nMaxH24A = 0; + int nMaxW18A = 0; + int nMaxH18A = 0; + int nMaxW9A = 0; + int nMaxH9A = 0; + + for (int i = 0; i < 10; i++) { + sz[0] = 0x30 + i; + setFont(&FreeSansBold24pt7b); + getTextBounds((const char *)sz, 0, 0, &x, &y, &w, &h); + ESP_LOGI(TAG_UI,"TextBound24 - '%c' w(%d) h(%d)\n", sz[0], w, h); + if (w > nMaxW24) nMaxW24 = w; + if (h > nMaxH24) nMaxH24 = h; + + setFont(&FreeSans18pt7b); + getTextBounds((const char *)sz, 0, 0, &x, &y, &w, &h); + ESP_LOGI(TAG_UI,"TextBound18 - '%c' w(%d) h(%d)\n", sz[0], w, h); + if (w > nMaxW18) nMaxW18 = w; + if (h > nMaxH18) nMaxH18 = h; + + setFont(&FreeSans9pt7b); + getTextBounds((const char *)sz, 0, 0, &x, &y, &w, &h); + ESP_LOGI(TAG_UI,"TextBound18 - '%c' w(%d) h(%d)\n", sz[0], w, h); + if (w > nMaxW9) nMaxW9 = w; + if (h > nMaxH9) nMaxH9 = h; + } + + for (int i = 0; i < 26; i++) { + sz[0] = 'A' + i; + setFont(&FreeSansBold24pt7b); + getTextBounds((const char *)sz, 0, 0, &x, &y, &w, &h); + ESP_LOGI(TAG_UI,"TextBound24 - '%c' w(%d) h(%d)\n", sz[0], w, h); + if (w > nMaxW24A) nMaxW24A = w; + if (h > nMaxH24A) nMaxH24A = h; + + setFont(&FreeSans18pt7b); + getTextBounds((const char *)sz, 0, 0, &x, &y, &w, &h); + ESP_LOGI(TAG_UI,"TextBound18 - '%c' w(%d) h(%d)\n", sz[0], w, h); + if (w > nMaxW18A) nMaxW18A = w; + if (h > nMaxH18A) nMaxH18A = h; + + setFont(&FreeSans9pt7b); + getTextBounds((const char *)sz, 0, 0, &x, &y, &w, &h); + ESP_LOGI(TAG_UI,"TextBound18 - '%c' w(%d) h(%d)\n", sz[0], w, h); + if (w > nMaxW9A) nMaxW9A = w; + if (h > nMaxH9A) nMaxH9A = h; + } + + { + sz[0] = ':'; + setFont(&FreeSansBold24pt7b); + getTextBounds((const char *)sz, 0, 0, &x, &y, &w, &h); + ESP_LOGI(TAG_UI,"TextBound24 - '%c' w(%d) h(%d)\n", sz[0], w, h); + + setFont(&FreeSans18pt7b); + getTextBounds((const char *)sz, 0, 0, &x, &y, &w, &h); + ESP_LOGI(TAG_UI,"TextBound18 - '%c' w(%d) h(%d)\n", sz[0], w, h); + + setFont(&FreeSans9pt7b); + getTextBounds((const char *)sz, 0, 0, &x, &y, &w, &h); + ESP_LOGI(TAG_UI,"TextBound18 - '%c' w(%d) h(%d)\n", sz[0], w, h); + } + + { + sz[0] = '.'; + setFont(&FreeSansBold24pt7b); + getTextBounds((const char *)sz, 0, 0, &x, &y, &w, &h); + ESP_LOGI(TAG_UI,"TextBound24 - '%c' w(%d) h(%d)\n", sz[0], w, h); + + setFont(&FreeSans18pt7b); + getTextBounds((const char *)sz, 0, 0, &x, &y, &w, &h); + ESP_LOGI(TAG_UI,"TextBound18 - '%c' w(%d) h(%d)\n", sz[0], w, h); + + setFont(&FreeSans9pt7b); + getTextBounds((const char *)sz, 0, 0, &x, &y, &w, &h); + ESP_LOGI(TAG_UI,"TextBound18 - '%c' w(%d) h(%d)\n", sz[0], w, h); + } + + ESP_LOGI(TAG_UI,"Font24 Max w(%d) h(%d)\n", nMaxW24, nMaxH24); + ESP_LOGI(TAG_UI,"Font24A Max w(%d) h(%d)\n", nMaxW24A, nMaxH24A); + + ESP_LOGI(TAG_UI,"Font18 Max w(%d) h(%d)\n", nMaxW18, nMaxH18); + ESP_LOGI(TAG_UI,"Font18A Max w(%d) h(%d)\n", nMaxW18A, nMaxH18A); + + ESP_LOGI(TAG_UI,"Font_9 Max w(%d) h(%d)\n", nMaxW9, nMaxH9); + ESP_LOGI(TAG_UI,"Font_9A Max w(%d) h(%d)\n", nMaxW9A, nMaxH9A); + } +} + +MY_IRAM_ATTR void CUI::message(uint8_t lineNo, char *sz) { + int16_t x, y; + uint16_t w, h; + + if (!bOK) return; + + getTextBounds((const char *)sz, 0, 0, &x, &y, &w, &h); + if (w > 128) w = 128; + fillRect(0,lineNo * 8,w,8, SSD1306_BLACK); + setFont(NULL); + setTextSize(1); + setCursor(0, lineNo * 8); + print(sz); + updateRegion(lineNo,lineNo,0,w); +} + +// Main function to monitor and update display +MY_IRAM_ATTR void CUI::updateDisplay(unsigned long tickSecond) { + if (!bOK) return; + static unsigned long lastUpdate = 0; + static unsigned long lastMainModeChange = 0; + + bool bUpdateTop = false; + bool bUpdateBottom = false; + + bUpdateTop = displayTop(); + if (tickSecond != lastUpdate) { + bUpdateBottom = displayBottom(); + lastUpdate = tickSecond; + } + + if (bUpdateTop && bUpdateBottom) { + updateScreen(); + } else if (bUpdateTop) { + updateRegion(0, 1, 0, SCREEN_WIDTH - 1); + } else if (bUpdateBottom) { + updateRegion(3, 7, 0, SCREEN_WIDTH - 1); + } + + if (tickSecond - lastMainModeChange >= 20) { + lastMainMode = m_nMainMode; + m_nMainMode = (m_nMainMode + 1) % 2; + lastMainModeChange = tickSecond; + } +} + +MY_IRAM_ATTR void CUI::updateDisplayTop(unsigned long tick) { + if (!bOK) return; + + if (displayTop()) { + updateRegion(0, 1, 0, SCREEN_WIDTH - 1); + } +} + +MY_IRAM_ATTR void CUI::updateDisplayBottom(unsigned long tickSecond) { + if (!bOK) return; + + static unsigned long lastMainModeChange = 0; + + if (displayBottom()) { + updateRegion(3, 7, 0, SCREEN_WIDTH - 1); + } + + if (tickSecond - lastMainModeChange >= 20) { + lastMainMode = m_nMainMode; + m_nMainMode = (m_nMainMode + 1) % 2; + lastMainModeChange = tickSecond; + } +} + +MY_IRAM_ATTR bool CUI::displayTop() { + static uint8_t lastHour = 255, lastMin = 255, lastSec = 255; + bool bUpdateTop = false; + char sz[32]; + int16_t x, y; + uint16_t w, h; + int16_t firstC = SCREEN_WIDTH - 1; + int16_t lastC = 0; + int16_t endX = SCREEN_WIDTH - WIDTH_UNIT - SPACING * 2; + + // Message Mode + if (!bButtonChanged) { + if (config.bCheckAC && !isWiFiConnected()) m_nMessageMode = MESSAGE_MODE::MODE_NO_WIFI; + else if (config.bCheckAC && (status.nFlags & FLAG_ZCD_AC)) m_nMessageMode = MESSAGE_MODE::MODE_AC; + else if (config.bCheckAC && (status.nFlags & FLAG_ZCD_LOAD)) m_nMessageMode = MESSAGE_MODE::MODE_LOAD; + else m_nMessageMode = MESSAGE_MODE::MODE_NONE; + + if (m_nMessageMode != lastMessageMode) { + char sz[32]; + switch(m_nMessageMode) { + case MESSAGE_MODE::MODE_NO_WIFI: + strcpy(sz, "No WiFi! "); + break; + case MESSAGE_MODE::MODE_AC: + strcpy(sz, "Check AC! "); + break; + case MESSAGE_MODE::MODE_LOAD: + strcpy(sz, "Check Load! "); + break; + } + + if (m_nMessageMode != MESSAGE_MODE::MODE_NONE) { + fillRect(0, 0, SCREEN_WIDTH, HEIGHT_UNIT, SSD1306_BLACK); + setFont(&FreeSans9pt7b); + setCursor(boxTitle.x, boxTitle.y + 15 - FONT_DESCENT); + getTextBounds((const char *)sz, 0, 0, &x, &y, &w, &h); + print(sz); + updateRegion(0, 1, 0, w - 1); + lastItemMode = 999; + lastMessageMode = m_nMessageMode; + return false; + } + } else { + if (m_nMessageMode != MESSAGE_MODE::MODE_NONE) { + //ESP_LOGI(TAG_UI,"UI - Message NOT CHANGED and MODE is NOT NORMAL"); + return false; + } + } + } + lastMessageMode = MESSAGE_MODE::MODE_NONE; + + // ItemMode + if (m_nItemMode != lastItemMode) { + setFont(NULL); + setTextSize(1); + boxMode.w = 24; + fillRect(boxMode.x, boxMode.y, boxMode.w, boxMode.h, SSD1306_BLACK); + switch(m_nItemMode) { + case ITEM_MODE::MODE_SET: + setCursor(boxMode.x, boxMode.y); + print("SET"); + //ESP_LOGI(TAG_UI,"UI - Item Mode Change - SET"); + break; + case ITEM_MODE::MODE_CONFIG: + setCursor(boxMode.x, boxMode.y); + print("CFG"); + //ESP_LOGI(TAG_UI,"UI - Item Mode Change - CONFIG"); + break; + case ITEM_MODE::MODE_NORMAL: + //ESP_LOGI(TAG_UI,"UI - Item Mode Change - NORMAL"); + break; + } + updateRegion(2,2,0,boxMode.w); + lastItemMode = m_nItemMode; + lastItem = 999; + lastValue = m_nValue + 1111; + } + + // Top + setFont(&FreeSans9pt7b); + + if (m_nItem != lastItem) { + + // Title + fillRect(0, 0, SCREEN_WIDTH, HEIGHT_UNIT, SSD1306_BLACK); + setCursor(boxTitle.x, boxTitle.y + 15 - FONT_DESCENT); + getTextBounds((const char *)title[m_nItem], 0, 0, &x, &y, &w, &h); + boxTitle.w = w + 2; + print(title[m_nItem]); + + // Value + if (m_nItem == UI_ITEM::ITEM_CLOCK) { + sprintf(sz, "%02d:%02d.%02d", g_nHour, g_nMinute, g_nSecond); + getTextBounds((const char *)sz, 0, 0, &x, &y, &w, &h); + boxValue.x = boxTitle.w + 8; + boxValue.w = SCREEN_WIDTH - boxValue.x; + lastHour = g_nHour; + lastMin = g_nMinute; + lastSec = g_nSecond; + } else { + if (m_nItem < ITEM_HEAT1_MANUAL) { + sprintf(sz, "%.1f", m_nValue / 10.0f); + } + else { + if (m_nItem < ITEM_CHECK_AC) + sprintf(sz, "%s", m_nValue ? "Manual" : "Auto"); + else + sprintf(sz, "%s", m_nValue ? "Yes" : "No"); + endX = SCREEN_WIDTH - 1; + } + getTextBounds((const char *)sz, 0, 0, &x, &y, &w, &h); + int16_t newX = endX - w - SPACING; + uint16_t newW = endX - newX; + boxValue.x = newX; + boxValue.w = newW; + lastValue = m_nValue; + } + setCursor(boxValue.x, boxValue.y + 15 - FONT_DESCENT); + print(sz); + + // Item Unit + if (m_nItem != UI_ITEM::ITEM_CLOCK && m_nItem < UI_ITEM::ITEM_HEAT1_MANUAL) { + m_pUnit = (uint8_t *) unit[item_unit_idx[m_nItem]]; + drawBitmap(boxUnit.x, boxUnit.y, m_pUnit, boxUnit.w, boxUnit.h, SSD1306_WHITE); + lastUnit = m_pUnit; + } else { + m_pUnit = nullptr; + } + + lastItem = m_nItem; + bUpdateTop = true; + firstC = 0; + lastC = 127; + return true; + } + + // value + if (m_nItem == UI_ITEM::ITEM_CLOCK) { + if (lastHour != g_nHour || lastMin != g_nMinute || lastSec != g_nSecond) { + lastHour = g_nHour; + lastMin = g_nMinute; + lastSec = g_nSecond; + fillRect(boxValue.x, boxValue.y, boxValue.w, boxValue.h, SSD1306_BLACK); + sprintf(sz, "%02d:%02d.%02d", g_nHour, g_nMinute, g_nSecond); + getTextBounds((const char *)sz, 0, 0, &x, &y, &w, &h); + boxValue.x = boxTitle.w + 8; + boxValue.w = SCREEN_WIDTH - boxValue.x; + + setCursor(boxValue.x, boxValue.y + 15 - FONT_DESCENT); + print(sz); + + bUpdateTop = true; + firstC = boxValue.x; + lastC = 127; + } + } else if (m_nValue != lastValue) { + fillRect(boxValue.x, boxValue.y, boxValue.w + SPACING, boxValue.h, SSD1306_BLACK); + if (m_nItem < ITEM_HEAT1_MANUAL) { + sprintf(sz, "%.1f", m_nValue / 10.0f); + } + else { + if (m_nItem < ITEM_CHECK_AC) + sprintf(sz, "%s", m_nValue ? "Manual" : "Auto"); + else + sprintf(sz, "%s", m_nValue ? "Yes" : "No"); + endX = SCREEN_WIDTH - SPACING - 1; + } + getTextBounds((const char *)sz, 0, 0, &x, &y, &w, &h); + int16_t newX = endX - w - SPACING; + uint16_t newW = endX - newX; + + setCursor(newX, boxValue.y + 15 - FONT_DESCENT); + print(sz); + + bUpdateTop = true; + firstC = boxValue.x < newX ? boxValue.x : newX; + lastC = endX + SPACING; + boxValue.x = newX; + boxValue.w = newW; + lastValue = m_nValue; + } + + + + if (bUpdateTop && lastC - firstC > 0) { + //unsigned long m = millis(); + updateRegion(0, 1, firstC, lastC); + //m = millis() - m; + //ESP_LOGI(TAG_UI,"UI - Update Top(%d~%d): %d msec\n", firstC, lastC, m); + } + return false; +} + +MY_IRAM_ATTR bool CUI::displayBottom() { + bool bUnitChange = false; + bool bD3Change = false; + bool bD2Change = false; + bool bD1Change = false; + bool bD0Change = false; + int firstC = SCREEN_WIDTH - 1; + int lastC = 0; + char sz[32]; + static uint8_t bLastNTP = 0xFF; + static uint8_t bLastBLE = ~FLAG_BLE_NODATA; + static uint8_t bLastConn = 0xFF; + static uint8_t bLastWiFi = 0xFF; + + // Bottom + // Update temperature and humidity if they have changed + int16_t value; + switch (m_nMainMode) { + case MAIN_MODE::MODE_TEMP: + value = status.nTemp1; + if (value < 0) value = 0; + m_pDUnit = (uint8_t *) °reeC[0]; + break; + case MAIN_MODE::MODE_HUMID: + value = status.nHumid1; + if (value < 0) value = 0; + m_pDUnit = (uint8_t *) &percentSign[0]; + break; + default: + value = 0; + break; + } + + // Unit + if (m_pDUnit != lastpDUnit) { + fillRect(boxDUnit.x, boxDUnit.y, boxDUnit.w, boxDUnit.h, SSD1306_BLACK); + drawBitmap( boxDUnit.x, boxDUnit.y, + m_pDUnit, + boxDUnit.w, boxDUnit.h, SSD1306_WHITE); + lastpDUnit = m_pDUnit; + bUnitChange = true; + firstC = boxDUnit.x; + lastC = boxDUnit.x + boxDUnit.w - 1; + } + + // Value + m_nD3 = value % 10; + value /= 10; + m_nD2 = value % 10; + value /= 10; + m_nD1 = value % 10; + m_nD0 = value / 10; + + // Update Tenths digit + if (m_nD3 != lastD3) { + fillRect(boxD3.x, boxD3.y, boxD3.w, boxD3.h, SSD1306_BLACK); + setFont(&FreeSans18pt7b); // 25 pixels high + sprintf(sz, "%d", m_nD3); + setCursor(boxD3.x, SCREEN_HEIGHT - FONT_DESCENT); + print(sz); + lastD3 = m_nD3; + bD3Change = true; + firstC = boxD3.x; + int end = boxD3.x + boxD3.w - 1; + if (lastC < end) + lastC = end; + } + + setFont(&FreeSansBold24pt7b); // 35 pixels high + // Dot + if (!bDot) + { + setCursor(boxDot.x, SCREEN_HEIGHT - FONT_DESCENT); + print("."); + bDot = true; + firstC = boxDot.x; + int end = boxDot.x + boxDot.w - 1; + if (lastC < end) + lastC = end; + } + + // Update One's digit + if (m_nD2 != lastD2) { + fillRect(boxD2.x, boxD2.y, boxD2.w, boxD2.h, SSD1306_BLACK); + sprintf(sz, "%d", m_nD2); + setCursor(boxD2.x, SCREEN_HEIGHT - FONT_DESCENT); + print(sz); + lastD2 = m_nD2; + bD2Change = true; + firstC = boxD2.x; + int end = boxD2.x + boxD2.w - 1; + if (lastC < end) + lastC = end; + } + + // Update Ten's digit + if (m_nD1 != lastD1) { + fillRect(boxD1.x, boxD1.y, boxD1.w, boxD1.h, SSD1306_BLACK); + if (m_nD0 > 0 || m_nD1 > 0) { + sprintf(sz, "%d", m_nD1); + setCursor(boxD1.x, SCREEN_HEIGHT - FONT_DESCENT); + print(sz); + } + lastD1 = m_nD1; + bD1Change = true; + firstC = boxD1.x; + int end = boxD1.x + boxD1.w - 1; + if (lastC < end) + lastC = end; + } + + // Update Hundred's digit + if (m_nD0 != lastD0) { + fillRect(boxD0.x, boxD0.y, boxD0.w, boxD0.h, SSD1306_BLACK); + if (m_nD0 > 0) { + sprintf(sz, "%d", m_nD0); + setCursor(boxD0.x, SCREEN_HEIGHT - FONT_DESCENT); + print(sz); + } + lastD0 = m_nD0; + bD0Change = true; + firstC = boxD0.x; + int end = boxD0.x + boxD0.w - 1; + if (lastC < end) + lastC = end; + } + + uint8_t sp = (bD0Change || bD1Change || bD2Change) ? 3 : 4; + int updateWidth = lastC - firstC; + if (updateWidth > 96) { + return true; + } + else + if (updateWidth > 0) { + //unsigned long m = millis(); + updateRegion(sp, 7, firstC, lastC); + //m = millis() - m; + //ESP_LOGI(TAG_UI,"UI - Update Bottom(%d~%d): %d msec\n", firstC, lastC, m); + } + + // Bottom Left + { + bool bUpdate = false; + setFont(NULL); + setTextSize(1); + + // WiFi Status + uint8_t bWiFi = isWiFiConnected(); + if (bWiFi != bLastWiFi) { + if (!isWiFiConnected()) { + setCursor(0, 7 * 8); + print('W'); + } else { + fillRect(0, 7 * 8, 8, 8, SSD1306_BLACK); + } + bLastWiFi = bWiFi; + bUpdate = true; + } + + // Time Manager + uint8_t bNTP = timeManager.hasNTPUpdate() ? 1 : 0; + if (bNTP != bLastNTP) { + if (!timeManager.hasNTPUpdate()) { + setCursor(0, 6 * 8); + print('T'); + } + else { + fillRect(0, 6 * 8, 8, 8, SSD1306_BLACK); + } + bLastNTP = bNTP; + bUpdate = true; + } + + // Client Connection + uint8_t bConn = host.isConnected(); + if (bConn != bLastConn) { + if (bConn) { + setCursor(0, 7 * 8); + print('C'); + } + else { + fillRect(0, 7 * 8, 8, 8, SSD1306_BLACK); + } + bLastConn = bConn; + bUpdate = true; + } + + if (bUpdate) { + updateRegion(6,7,0,7); + lastMainMode = m_nMainMode; + } + } + return false; +} + +// +// Buttons +// +MY_IRAM_ATTR void CUI::loopButton(unsigned long tickMillis) { + //if (host.isConnected()) { + // initButtonState(); + // return; + //} + static unsigned long lastButtonAction = 0; + + checkButtonStates(tickMillis); + if (bButtonSetDown || bButtonSetUp || bButtonUpDown || bButtonUpUp || bButtonDownDown || bButtonDownUp) { + bButtonChanged = true; + lastButtonAction = tickMillis; + } else + if (tickMillis - lastButtonAction > 20000) { + bButtonChanged = false; + } + + if (bButtonSetUp) { + switch(m_nItemMode) { + case ITEM_MODE::MODE_NORMAL: + if (set_idx[m_nItem]) + m_nItemMode = buttonSetDownDuration > 1000 ? MODE_CONFIG : MODE_SET; + break; + case ITEM_MODE::MODE_SET: + if (set_idx[m_nItem]) { + switch(m_nItem) { + case ITEM_TEMP_TARGET: config.nTempTarget = m_nValue; break; + case ITEM_HUMID_TARGET: config.nHumidTarget = m_nValue; break; + + case ITEM_HEAT1: status.nHeater1Duty = m_nValue * 10; break; + case ITEM_HEAT2: status.nHeater2Duty = m_nValue * 10; break; + case ITEM_MIST: + status.nMistDuty = m_nValue * 10; + break; + case ITEM_FAN: + status.nFanDuty = m_nValue; + break; + case ITEM_MOTOR: status.nMotorDuty = m_nValue; + break; + case ITEM_LIGHT: + status.nLightTargetDuty = m_nValue; + break; + + case ITEM_HEAT1_MANUAL: + status.nFlags = m_nValue ? status.nFlags | FLAG_MANUAL_HEATER1 : status.nFlags & ~FLAG_MANUAL_HEATER1; + break; + case ITEM_HEAT2_MANUAL: + status.nFlags = m_nValue ? status.nFlags | FLAG_MANUAL_HEATER2 : status.nFlags & ~FLAG_MANUAL_HEATER2; + break; + case ITEM_MIST_MANUAL: + status.nFlags = m_nValue ? status.nFlags | FLAG_MANUAL_MIST : status.nFlags & ~FLAG_MANUAL_MIST; + break; + case ITEM_FAN_MANUAL: + status.nFlags = m_nValue ? status.nFlags | FLAG_MANUAL_FAN : status.nFlags & ~FLAG_MANUAL_FAN; + break; + case ITEM_MOTOR_MANUAL: + status.nFlags = m_nValue ? status.nFlags | FLAG_MANUAL_MOTOR : status.nFlags & ~FLAG_MANUAL_MOTOR; + break; + case ITEM_LIGHT_MANUAL: + status.nFlags = m_nValue ? status.nFlags | FLAG_MANUAL_LIGHT : status.nFlags & ~FLAG_MANUAL_LIGHT; + break; + case ITEM_CHECK_AC: + config.bCheckAC = m_nValue ? true : false; + } + m_nItemMode = MODE_NORMAL; + } + break; + case ITEM_MODE::MODE_CONFIG: + m_nItemMode = MODE_NORMAL; + break; + } + } + else { + switch (m_nItemMode) { + case MODE_NORMAL: + if (bButtonUpUp) { + m_nItem = m_nItem > 0 ? --m_nItem : UI_ITEM::ITEM_COUNT - 1; + lastValue = m_nValue + 1; + } + if (bButtonDownUp) { + m_nItem = m_nItem < ITEM_COUNT - 1 ? ++m_nItem : 0; + lastValue = m_nValue + 1; + } + break; + case MODE_SET: + if (bButtonUpUp) { + if (fineControl[m_nItem]) { + if (m_nItem >= ITEM_HEAT1_MANUAL) { + m_nValue = m_nValue ? 0 : 1; + } else if (m_nValue < 1000) { + m_nValue++; + } + } + else { + m_nValue = (m_nValue / 10 + 1) * 10; + if (m_nValue > 1000) m_nValue = 1000; + } + } + if (bButtonDownUp) { + if (fineControl[m_nItem]) { + if (m_nItem >= ITEM_HEAT1_MANUAL) { + m_nValue = m_nValue ? 0 : 1; + } else if (m_nValue > 0) { + m_nValue--; + } + } + else { + m_nValue = (m_nValue / 10 - 1) * 10; + if (m_nValue < 0) m_nValue = 0; + } + } + break; + } + } + + if (m_nItemMode == ITEM_MODE::MODE_NORMAL || !set_idx[m_nItem]) { + switch(m_nItem) { + case ITEM_TEMP1: m_nValue = status.nTemp1; break; + case ITEM_TEMP2: m_nValue = status.nTemp2; break; + case ITEM_TEMP3: m_nValue = status.nTemp3; break; + case ITEM_HUMID: m_nValue = status.nHumid1; break; + case ITEM_HUMID2:m_nValue = status.nHumid2; break; + + case ITEM_TEMP_TARGET: m_nValue = config.nTempTarget; break; + case ITEM_HUMID_TARGET: m_nValue = config.nHumidTarget; break; + + case ITEM_HEAT1: m_nValue = status.nHeater1Duty / 10; break; + case ITEM_HEAT2: m_nValue = status.nHeater2Duty / 10; break; + case ITEM_MIST: m_nValue = status.nMistDuty / 10; break; + case ITEM_FAN: m_nValue = status.nFanDuty; break; + case ITEM_MOTOR: m_nValue = status.nMotorDuty; break; + case ITEM_LIGHT: m_nValue = status.nLightTargetDuty; break; + + case ITEM_HEAT1_MANUAL: m_nValue = status.nFlags & FLAG_MANUAL_HEATER1 ? 1 : 0; break; + case ITEM_HEAT2_MANUAL: m_nValue = status.nFlags & FLAG_MANUAL_HEATER2 ? 1 : 0; break; + case ITEM_MIST_MANUAL: m_nValue = status.nFlags & FLAG_MANUAL_MIST ? 1 : 0; break; + case ITEM_FAN_MANUAL: m_nValue = status.nFlags & FLAG_MANUAL_FAN ? 1 : 0; break; + case ITEM_MOTOR_MANUAL: m_nValue = status.nFlags & FLAG_MANUAL_MOTOR ? 1 : 0; break; + case ITEM_LIGHT_MANUAL: m_nValue = status.nFlags & FLAG_MANUAL_LIGHT ? 1 : 0; break; + + case ITEM_CHECK_AC: m_nValue = config.bCheckAC ? 1 : 0; break; + } + } + + bButtonSetUp = false; + bButtonUpUp = false; + bButtonDownUp = false; +} + +void CUI::initButtonState() { + buttonSetChangeTime = 0; // Time when button was pressed + buttonUpChangeTime = 0; // Time when button was pressed + buttonDownChangeTime = 0; // Time when button was pressed + + buttonSetDownTime = 0; // Time when button was pressed + buttonUpDownTime = 0; // Time when button was pressed + buttonDownDownTime = 0; // Time when button was pressed + + buttonSetDownDuration = 0; // Time when button was pressed + buttonUpDownDuration = 0; // Time when button was pressed + buttonDownDownDuration = 0; // Time when button was pressed + + bButtonSetChanged = false; + bButtonSetUp = false; + bButtonSetDown = false; + + bButtonUpChanged = false; + bButtonUpUp = false; + bButtonUpDown = false; + + bButtonDownChanged = false; + bButtonDownUp = false; + bButtonDownDown = false; + + m_nMainMode = 0; + m_nItemMode = 0; + m_nItem = 0; + m_nValue = 0; + m_pUnit = (uint8_t *) unit[item_unit_idx[m_nItem]]; + m_nD0 = m_nD1 = m_nD2 = m_nD3 = 0; + + lastMainMode = 1; + lastItemMode = 1; + lastItem = UI_ITEM::ITEM_COUNT; + lastValue = 1; + lastUnit = nullptr; + lastD0 = lastD1 = lastD2 = lastD3 = 1; +} + +void CUI::checkButtonStates(unsigned long currentMillis) { + if (bButtonSetChanged) { + // Compare with the last interrupt time to ensure debounce delay + if (currentMillis - buttonSetChangeTime > DEBOUNCE_DELAY) { + if (digitalRead(PIN_SW_SET) == LOW) { + // Button pressed + buttonSetDownTime = buttonSetChangeTime; + bButtonSetDown = true; + } else { + // Button released + if (bButtonSetDown) { + bButtonSetUp = true; + bButtonSetDown = false; + buttonSetDownDuration = currentMillis - buttonSetDownTime; + //ESP_LOGI(TAG_UI,"UI Button - SET button RELEASED. Down for %dms\n", buttonSetDownDuration); + } + } + //lastProcessedTime = currentMillis; // Update processed time + bButtonSetChanged = false; // Reset the flag + } + } + + if (bButtonUpChanged) { + // Compare with the last interrupt time to ensure debounce delay + if (currentMillis - buttonUpChangeTime > DEBOUNCE_DELAY) { + if (digitalRead(PIN_SW_UP) == LOW) { + // Button pressed + buttonUpDownTime = buttonUpChangeTime; + bButtonUpDown = true; + } else { + // Button released + if (bButtonUpDown) { + bButtonUpUp = true; + bButtonUpDown = false; + buttonUpDownDuration = currentMillis - buttonUpDownTime; + //ESP_LOGI(TAG_UI,"UI Button - UP button RELEASED. Down for %dms\n", buttonUpDownDuration); + } + } + //lastProcessedTime = currentMillis; // Update processed time + bButtonUpChanged = false; // Reset the flag + } + } + + if (bButtonDownChanged) { + // Compare with the last interrupt time to ensure debounce delay + if (currentMillis - buttonDownChangeTime > DEBOUNCE_DELAY) { + if (digitalRead(PIN_SW_DOWN) == LOW) { + // Button pressed + buttonDownDownTime = buttonDownChangeTime; + bButtonDownDown = true; + } else { + // Button released + if (bButtonDownDown) { + bButtonDownUp = true; + bButtonDownDown = false; + buttonDownDownDuration = currentMillis - buttonDownDownTime; + //ESP_LOGI(TAG_UI,"UI Button - DOWN button RELEASED. Down for %dms\n", buttonDownDownDuration); + } + } + //lastProcessedTime = currentMillis; // Update processed time + bButtonDownChanged = false; // Reset the flag + } + } +} + +// ISR for the Set button handling +IRAM_ATTR void buttonSetISR() { + // Record the time of the button interrupt and set a flag + ui.buttonSetChangeTime = millis(); + ui.bButtonSetChanged = true; // Flag for main loop to process +} + +// ISR for the Up button handling +IRAM_ATTR void buttonUpISR() { + ui.buttonUpChangeTime = millis(); + ui.bButtonUpChanged = true; // Flag for main loop to process +} + +// ISR for the Down button handling +IRAM_ATTR void buttonDownISR() { + ui.buttonDownChangeTime = millis(); + ui.bButtonDownChanged = true; // Flag for main loop to process +} \ No newline at end of file diff --git a/UI.h b/UI.h new file mode 100644 index 0000000..82f2883 --- /dev/null +++ b/UI.h @@ -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; diff --git a/UPnPClient.cpp b/UPnPClient.cpp new file mode 100644 index 0000000..ef39e39 --- /dev/null +++ b/UPnPClient.cpp @@ -0,0 +1,491 @@ +#include +#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("urn:schemas-upnp-org:service:WANIPConnection:1"); + if (servicePos == -1) { + ESP_LOGI(TAG_UPNP,"WANIPConnection service not found in XML."); + return ""; + } + + // Find the start of the controlURL tag within the block + int controlStart = xml.indexOf("", servicePos); + if (controlStart == -1) { + ESP_LOGI(TAG_UPNP,"controlURL not found in service block."); + return ""; + } + controlStart += 12; // Move past "" + + // Find the closing tag + int controlEnd = xml.indexOf("", controlStart); + if (controlEnd == -1) { + ESP_LOGI(TAG_UPNP,"Malformed XML: Missing ."); + 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), + "" + "" + "" + "" + "" + "%d" + "TCP" + "%s" + "%s" + "" + "" + "", + 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), + "" + "" + "" + "" + "%d" + "TCP" + "%d" + "%s" + "1" + "HC_%04X" + "0" + "", + 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), + "" + "" + "" + "" + "" + ""); + + 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, ""); + 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), + "" + "" + "" + "" + "" + ""); + + 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, ""); + 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; +}; diff --git a/UPnPClient.h b/UPnPClient.h new file mode 100644 index 0000000..9296cee --- /dev/null +++ b/UPnPClient.h @@ -0,0 +1,37 @@ +#include +#include +#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(); +}; diff --git a/WiFiHost.cpp b/WiFiHost.cpp new file mode 100644 index 0000000..1abeb52 --- /dev/null +++ b/WiFiHost.cpp @@ -0,0 +1,885 @@ +#include +#undef INADDR_NONE +#include +#include +#include +#include +#include +#include // For struct in_addr +#include +#include +#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(); +} diff --git a/WiFiHost.h b/WiFiHost.h new file mode 100644 index 0000000..43e2d16 --- /dev/null +++ b/WiFiHost.h @@ -0,0 +1,284 @@ +#ifndef __WIFI_HOST_H +#define __WIFI_HOST_H +#include "HermitCrab.h" +#ifdef ESP8266 +#include +#else +#include +#endif +#include + +#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 \ No newline at end of file diff --git a/c_cpp_properties.json b/c_cpp_properties.json new file mode 100644 index 0000000..06b099d --- /dev/null +++ b/c_cpp_properties.json @@ -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 +} \ No newline at end of file diff --git a/zcd.cpp b/zcd.cpp new file mode 100644 index 0000000..e33bc89 --- /dev/null +++ b/zcd.cpp @@ -0,0 +1,185 @@ +#include "HermitCrab.h" +#include "Config.h" +#include "zcd.h" +#include +#include +#include +#include + +#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 + } +} \ No newline at end of file diff --git a/zcd.h b/zcd.h new file mode 100644 index 0000000..be8d297 --- /dev/null +++ b/zcd.h @@ -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 \ No newline at end of file