Initial commit: HermitCrab project structure
This commit is contained in:
commit
3276c9527d
BIN
.gitignore
vendored
Normal file
BIN
.gitignore
vendored
Normal file
Binary file not shown.
BIN
.vs/HermitCrab/v16/.suo
Normal file
BIN
.vs/HermitCrab/v16/.suo
Normal file
Binary file not shown.
BIN
.vs/HermitCrab/v16/Browse.VC.db
Normal file
BIN
.vs/HermitCrab/v16/Browse.VC.db
Normal file
Binary file not shown.
40
.vscode/c_cpp_properties.json
vendored
Normal file
40
.vscode/c_cpp_properties.json
vendored
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
{
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Arduino",
|
||||
"includePath": [
|
||||
"${workspaceFolder}/**",
|
||||
"C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/hardware/esp32/3.0.5/libraries/**",
|
||||
"C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/hardware/esp32/3.0.5/cores/esp32/**",
|
||||
"C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/tools/esp32-arduino-libs/idf-release_v5.1-33fbade6/esp32/include/**",
|
||||
"C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/tools/esp32-arduino-libs/idf-release_v5.1-33fbade6/esp32/include/freertos/FreeRTOS-Kernel/include/**",
|
||||
"C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/tools/esp32-arduino-libs/idf-release_v5.1-33fbade6/esp32/include/freertos/FreeRTOS-Kernel/portable/xtensa/include/**",
|
||||
"C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/tools/esp32-arduino-libs/idf-release_v5.1-33fbade6/esp32/include/freertos/esp_additions/include/**",
|
||||
"C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/tools/esp32-arduino-libs/idf-release_v5.1-33fbade6/esp32/include/freertos/esp_additions/arch/xtensa/include/**",
|
||||
"C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/tools/esp32-arduino-libs/idf-release_v5.1-33fbade6/esp32/qio_qspi/include/**",
|
||||
"C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/hardware/esp32/3.0.5/variants/esp32/**",
|
||||
|
||||
"C:/Users/hxyi/AppData/Local/Arduino15/packages/esp8266/hardware/esp8266/3.1.2/cores/esp8266/**",
|
||||
"C:/Users/hxyi/AppData/Local/Arduino15/packages/esp8266/hardware/esp8266/3.1.2/libraries/**",
|
||||
"C:/Users/hxyi/AppData/Local/Arduino15/packages/esp8266/hardware/esp8266/3.1.2/libraries/ESP8266WiFi/src/**",
|
||||
"C:/Users/hxyi/AppData/Local/Arduino15/packages/esp8266/hardware/esp8266/3.1.2/tools/SDK/include/**",
|
||||
|
||||
"D:/Projects/libraries/ESP-TuyaBLE/src/**",
|
||||
"D:/Projects/libraries/NimBLE-Arduino/src/**",
|
||||
"D:/Projects/libraries/NimBLE-Arduino/src/nimble/nimble/host/include/host/**"
|
||||
],
|
||||
"defines": [
|
||||
"_DEBUG",
|
||||
"UNICODE",
|
||||
"_UNICODE",
|
||||
"ESP32"
|
||||
],
|
||||
"windowsSdkVersion": "10.0.19041.0",
|
||||
"compilerPath": "C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/tools/esp-x32/2302/bin/xtensa-esp32-elf-gcc.exe",
|
||||
"cStandard": "c11",
|
||||
"cppStandard": "c++17",
|
||||
"intelliSenseMode": "gcc-x64"
|
||||
}
|
||||
],
|
||||
"version": 4
|
||||
}
|
||||
69
.vscode/settings.json
vendored
Normal file
69
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
{
|
||||
"files.associations": {
|
||||
"\"*.ino\"": "\"cpp\"",
|
||||
"array": "cpp",
|
||||
"atomic": "cpp",
|
||||
"bit": "cpp",
|
||||
"*.tcc": "cpp",
|
||||
"cctype": "cpp",
|
||||
"chrono": "cpp",
|
||||
"clocale": "cpp",
|
||||
"cmath": "cpp",
|
||||
"compare": "cpp",
|
||||
"concepts": "cpp",
|
||||
"cstdarg": "cpp",
|
||||
"cstddef": "cpp",
|
||||
"cstdint": "cpp",
|
||||
"cstdio": "cpp",
|
||||
"cstdlib": "cpp",
|
||||
"cstring": "cpp",
|
||||
"ctime": "cpp",
|
||||
"cwchar": "cpp",
|
||||
"cwctype": "cpp",
|
||||
"deque": "cpp",
|
||||
"list": "cpp",
|
||||
"map": "cpp",
|
||||
"set": "cpp",
|
||||
"string": "cpp",
|
||||
"unordered_map": "cpp",
|
||||
"unordered_set": "cpp",
|
||||
"vector": "cpp",
|
||||
"exception": "cpp",
|
||||
"algorithm": "cpp",
|
||||
"functional": "cpp",
|
||||
"iterator": "cpp",
|
||||
"memory": "cpp",
|
||||
"memory_resource": "cpp",
|
||||
"netfwd": "cpp",
|
||||
"numeric": "cpp",
|
||||
"optional": "cpp",
|
||||
"random": "cpp",
|
||||
"ratio": "cpp",
|
||||
"string_view": "cpp",
|
||||
"system_error": "cpp",
|
||||
"tuple": "cpp",
|
||||
"type_traits": "cpp",
|
||||
"utility": "cpp",
|
||||
"initializer_list": "cpp",
|
||||
"iomanip": "cpp",
|
||||
"iosfwd": "cpp",
|
||||
"iostream": "cpp",
|
||||
"istream": "cpp",
|
||||
"limits": "cpp",
|
||||
"new": "cpp",
|
||||
"numbers": "cpp",
|
||||
"ostream": "cpp",
|
||||
"span": "cpp",
|
||||
"sstream": "cpp",
|
||||
"stdexcept": "cpp",
|
||||
"streambuf": "cpp",
|
||||
"cinttypes": "cpp",
|
||||
"typeinfo": "cpp",
|
||||
"variant": "cpp"
|
||||
},
|
||||
|
||||
"workbench.colorCustomizations": {
|
||||
"editor.background": "#0f0f0f" // You can change this hex value to any darker shade you prefer
|
||||
}
|
||||
|
||||
}
|
||||
207
AHT2x.cpp
Normal file
207
AHT2x.cpp
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
#include "AHT2x.h"
|
||||
|
||||
#define AHT_STATUS_BUSY 0x01
|
||||
#define AHT_STATUS_CALIBRATED 0x10
|
||||
#define AHT_CMD_INIT 0xBE
|
||||
#define AHT_CMD_TRIGGER 0xAC
|
||||
#define AHT_CMD_RESET 0xBA
|
||||
#define AHT_CRC_POLYNOMIAL 0x31
|
||||
#define AHT_CRC_MSB 0x80
|
||||
#define AHT_CRC_INIT 0xFF
|
||||
|
||||
|
||||
#ifndef DPRINTF
|
||||
#define DPRINTF(...)
|
||||
#endif
|
||||
|
||||
AHT2x aht25(Wire);
|
||||
AHT2x aht10_0x39(Wire);
|
||||
|
||||
const uint8_t cmd_init[3] = { 0xBE, 0x08, 0x00 };
|
||||
|
||||
AHT2x::AHT2x(TwoWire &i2c) : _i2c(i2c) {};
|
||||
|
||||
bool AHT2x::setup(uint8_t address, bool crc) {
|
||||
m_nAddress = address;
|
||||
_active_crc = crc;
|
||||
m_nErrorCount = 0;
|
||||
m_nTemp = -9999;
|
||||
m_nRH = -9999;
|
||||
bRequested = false;
|
||||
m_bSensor = false;
|
||||
m_bScan = false;
|
||||
tickRequested = millis();
|
||||
delay(40);
|
||||
|
||||
if (scan()) {
|
||||
while (!isCalibrated()) {
|
||||
calibrate();
|
||||
}
|
||||
|
||||
requestMeasurement(tickRequested);
|
||||
delay(82);
|
||||
humid[3] = -9999;
|
||||
temp[3] = -9999;
|
||||
if (readSensor(tickRequested + 82)) {
|
||||
for (int i = 0; i < 3; i++) {
|
||||
humid[i] = humid[3];
|
||||
temp[i] = temp[3];
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool AHT2x::scan() {
|
||||
//DPRINTF("AHTx0 - Scanning AHTx0 Device at %X\n", m_nAddress);
|
||||
int i;
|
||||
for (i = 0; i < 5; i++) {
|
||||
Wire.beginTransmission(m_nAddress);
|
||||
if (Wire.endTransmission() == 0) {
|
||||
DPRINTF("AHTx0 - FOUND I2C Device at %X\n", m_nAddress);
|
||||
bool ret = false;
|
||||
//sendCommand(m_nAddress, 0xBE, 0x08, 0x00)) { // AHT20 init command
|
||||
uint8_t cmd[3] = { 0xBE, 0x08, 0x00 };
|
||||
_i2c.beginTransmission(m_nAddress);
|
||||
_i2c.write((uint8_t *)cmd_init, 3);
|
||||
if (_i2c.endTransmission() == 0) {
|
||||
m_bSensor = true;
|
||||
DPRINTF("AHT2x - Found AHT2x at %X\n", m_nAddress);
|
||||
break;
|
||||
}
|
||||
}
|
||||
delay(2);
|
||||
}
|
||||
if (i == 5) {
|
||||
DPRINTF("AHTx0 - I2C Transmission Error at %X\n", m_nAddress);
|
||||
}
|
||||
return m_bSensor;
|
||||
}
|
||||
|
||||
bool AHT2x::isReady() {
|
||||
if (status() & AHT_STATUS_BUSY) {
|
||||
return false;
|
||||
}
|
||||
return measure();
|
||||
}
|
||||
|
||||
bool AHT2x::isCalibrated() {
|
||||
return status() & AHT_STATUS_CALIBRATED;
|
||||
}
|
||||
|
||||
void AHT2x::reset() {
|
||||
uint8_t cmd = AHT_CMD_RESET;
|
||||
_i2c.beginTransmission(m_nAddress);
|
||||
_i2c.write(cmd);
|
||||
_i2c.endTransmission();
|
||||
delay(20);
|
||||
while (!isCalibrated()) {
|
||||
calibrate();
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t AHT2x::status() {
|
||||
_i2c.requestFrom(m_nAddress, (uint8_t)6);
|
||||
for (int i = 0; i < 6; i++) {
|
||||
_buf[i] = _i2c.read();
|
||||
}
|
||||
if (!_active_crc || (crc8() == _buf[6])) {
|
||||
return _buf[0];
|
||||
}
|
||||
return AHT_STATUS_BUSY;
|
||||
}
|
||||
|
||||
void AHT2x::calibrate() {
|
||||
uint8_t cmd[] = {AHT_CMD_INIT, 0x08, 0x00};
|
||||
_i2c.beginTransmission(m_nAddress);
|
||||
_i2c.write(cmd, 3);
|
||||
_i2c.endTransmission();
|
||||
delay(10);
|
||||
}
|
||||
|
||||
void AHT2x::requestMeasurement(unsigned long tick) {
|
||||
uint8_t cmd[] = {AHT_CMD_TRIGGER, 0x33, 0x00};
|
||||
_i2c.beginTransmission(m_nAddress);
|
||||
_i2c.write(cmd, 3);
|
||||
_i2c.endTransmission();
|
||||
tickRequested = tick;
|
||||
bRequested = true;
|
||||
}
|
||||
|
||||
bool AHT2x::readSensor(unsigned long tick) {
|
||||
if (m_bScan) {
|
||||
// Re-Scan sensor for any hardware change or failure
|
||||
scan();
|
||||
m_bScan = false;
|
||||
}
|
||||
|
||||
bool ret = false;
|
||||
if (bRequested) {
|
||||
if (tick - tickRequested > 80) {
|
||||
_i2c.requestFrom(m_nAddress, (uint8_t)6);
|
||||
ret = true;
|
||||
for (int i = 0; i < 6; i++) {
|
||||
if (Wire.available()) {
|
||||
_buf[i] = _i2c.read();
|
||||
} else {
|
||||
ret = false; // If data is unavailable, return false
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (ret && (!_active_crc || (crc8() == _buf[6]))) {
|
||||
humid[0] = humid[1];
|
||||
humid[1] = humid[2];
|
||||
humid[2] = humid[3];
|
||||
uint32_t hum32 = (_buf[1] << 12) | (_buf[2] << 4) | (_buf[3] >> 4);
|
||||
humid[3] = (int16_t) roundf(hum32 * 10000.0f / 0x100000);
|
||||
if (m_nRH > 2000 && m_nRH <= 10000) {
|
||||
if (humid[3] - m_nRH < -1500 || humid[3] - m_nRH> 1500)
|
||||
humid[3] = m_nRH;
|
||||
}
|
||||
m_nRH = (int16_t)((humid[0] + humid[1] + humid[2] + humid[3]) / 4);
|
||||
|
||||
temp[0] = temp[1];
|
||||
temp[1] = temp[2];
|
||||
temp[2] = temp[3];
|
||||
uint32_t temp32 = ((_buf[3] & 0xF) << 16) | (_buf[4] << 8) | _buf[5];
|
||||
temp[3] = (int16_t) roundf(temp32 * 20000.0f / 0x100000 - 5000);
|
||||
if (m_nTemp > 2000 && m_nTemp <= 3000) {
|
||||
if (temp[3] - m_nTemp < -1000 || temp[3] - m_nTemp> 1000)
|
||||
temp[3] = m_nTemp;
|
||||
}
|
||||
m_nTemp = (int16_t)((temp[0] + temp[1] + temp[2] + temp[3]) / 4);
|
||||
m_nErrorCount = 0;
|
||||
ret = true;
|
||||
} else {
|
||||
if (++m_nErrorCount > 4) {
|
||||
m_nTemp = -9999;
|
||||
m_nRH = -9999;
|
||||
m_nErrorCount = 0;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// tick - tickRequested <= 80
|
||||
// do nothing;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
requestMeasurement(tick);
|
||||
return ret;
|
||||
}
|
||||
|
||||
uint8_t AHT2x::crc8() {
|
||||
uint8_t crc = AHT_CRC_INIT;
|
||||
for (int i = 0; i < 6; i++) {
|
||||
crc ^= _buf[i];
|
||||
for (int j = 0; j < 8; j++) {
|
||||
if (crc & AHT_CRC_MSB) {
|
||||
crc = (crc << 1) ^ AHT_CRC_POLYNOMIAL;
|
||||
} else {
|
||||
crc <<= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
43
AHT2x.h
Normal file
43
AHT2x.h
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
#ifndef __AHT2x_H
|
||||
#define __AHT2x_H
|
||||
#include <Wire.h>
|
||||
|
||||
#define AHT_I2C_ADDR 0x38
|
||||
|
||||
class AHT2x {
|
||||
public:
|
||||
AHT2x(TwoWire &i2c);
|
||||
bool setup(uint8_t address = AHT_I2C_ADDR, bool crc = false);
|
||||
bool scan();
|
||||
bool isReady();
|
||||
bool isCalibrated();
|
||||
void reset();
|
||||
bool readSensor(unsigned long tick);
|
||||
inline int16_t getHumidity() const { return m_nRH; }
|
||||
inline int16_t getTemperature() const { return m_nTemp; }
|
||||
inline void setScanFlag(bool bScan) { m_bScan = bScan; }
|
||||
inline bool sensor() { return m_bSensor; }
|
||||
|
||||
private:
|
||||
TwoWire &_i2c;
|
||||
uint8_t m_nAddress;
|
||||
bool _active_crc;
|
||||
uint8_t _buf[7];
|
||||
int16_t m_nRH, m_nTemp;
|
||||
int16_t humid[4], temp[4];
|
||||
|
||||
uint8_t status();
|
||||
void calibrate();
|
||||
bool measure();
|
||||
uint8_t crc8();
|
||||
void requestMeasurement(unsigned long tick);
|
||||
|
||||
bool bRequested;
|
||||
bool m_bSensor;
|
||||
bool m_bScan;
|
||||
unsigned long tickRequested;
|
||||
uint8_t m_nErrorCount;
|
||||
};
|
||||
|
||||
extern class AHT2x aht25, aht10_0x39;
|
||||
#endif
|
||||
433
BLEScan.cpp
Normal file
433
BLEScan.cpp
Normal file
|
|
@ -0,0 +1,433 @@
|
|||
#include <NimBLEDevice.h>
|
||||
#include <BLEScan.h>
|
||||
#include <WiFi.h>
|
||||
#include "Config.h"
|
||||
#include "HermitCrab.h"
|
||||
#include "ConnectWiFi.h"
|
||||
|
||||
CBLEScan ble;
|
||||
std::string serviceData;
|
||||
char BLE_SSID[32];
|
||||
char BLE_PW[32];
|
||||
|
||||
#define TAG_BLE "BLE_SCAN"
|
||||
|
||||
#define ADVERTISE_INTERVAL_MS 5000 // Expected advertisement interval
|
||||
#define SCAN_SHORT_MS 300 // Short scan time
|
||||
#define SCAN_LONG_MS 12000 // Longer scan if missed
|
||||
|
||||
// Xiaomi - The remote service and characteristic UUIDs
|
||||
static BLEUUID serviceUUID("ebe0ccb0-7a0a-4b0c-8a1a-6ff2997da3a6");
|
||||
static BLEUUID charUUID("ebe0ccc1-7a0a-4b0c-8a1a-6ff2997da3a6");
|
||||
|
||||
//#define TUYA_BLE_NAME "THB1-xxxxxx" // sensor name
|
||||
//#define SENSOR_BLE_NAME
|
||||
//#define SENSOR_BLE_MAC "38:1F:8D:77:73:34" // Tuya sensor MAC address
|
||||
#define WIFI_BLE_NAME "HC_" // Name of PC/Android BLE advertiser
|
||||
|
||||
// UUIDs for environmental sensing and characteristics
|
||||
#define ENV_SENSING_UUID "181A" // Environmental Sensing UUID
|
||||
#define TEMP_UUID "2A6E" // Temperature UUID
|
||||
#define HUMIDITY_UUID "2A6F" // Humidity UUID
|
||||
|
||||
// Tuya sensor advertisement data structure (in network order)
|
||||
typedef struct __attribute__((packed)) _bthome_t {
|
||||
//uint8_t flag[3]; // Advertise type flags
|
||||
uint8_t info; // = 0x40 BtHomeID_Info (not encrypted)
|
||||
uint8_t p_id; // = BtHomeID_PacketId
|
||||
uint8_t pid; // PacketId (measurement count)
|
||||
|
||||
uint8_t b_id; // = BtHomeID_battery
|
||||
uint8_t battery_level; // 0..100 %
|
||||
uint8_t t_id; // = BtHomeID_temperature
|
||||
int16_t temp; // x 0.001 degree
|
||||
uint8_t h_id; // = BtHomeID_humidity
|
||||
uint16_t humid; // x 0.01 %
|
||||
uint8_t v_id; // = BtHomeID_voltage
|
||||
uint16_t battery_mv; // x 0.001 V
|
||||
} bthome_t;
|
||||
|
||||
// Callback when a device is found during scan
|
||||
class HCScanCallbacks : public NimBLEScanCallbacks {
|
||||
/**
|
||||
* @brief Called when a new device is discovered, before the scan result is received (if applicable).
|
||||
* @param [in] advertisedDevice The device which was discovered.
|
||||
*/
|
||||
void onDiscovered(const NimBLEAdvertisedDevice* advertisedDevice) override {
|
||||
static uint8_t flag = 0;
|
||||
static char ssid[32];
|
||||
static char pw[32];
|
||||
if (isWiFiConnected()) return;
|
||||
|
||||
// Retrieve manufacturer data (SSID or password)
|
||||
const std::string& manufacturerData = advertisedDevice->getManufacturerData();
|
||||
|
||||
// Check for specific manufacturer data signatures (SIGNATURE1 for SSID, SIGNATURE2 for password)
|
||||
uint16_t manufacturerID = * (uint16_t *) &manufacturerData[0];
|
||||
if (manufacturerID == SIGNATURE2 - 1) { // For SSID (SIGNATURE1)
|
||||
strncpy(ssid, (char *) &manufacturerData[2], std::min(sizeof(ssid) - 1, manufacturerData.length() - 1));
|
||||
ssid[sizeof(ssid) - 1] = '\0'; // Ensure null termination
|
||||
flag |= 1;
|
||||
//DPRINTF("BLE Scan - SSID(\"%s\")\n", (char *) &manufacturerData[2]);
|
||||
} else if (manufacturerID == SIGNATURE2) { // For password (SIGNATURE2)
|
||||
strncpy(pw, (char *)&manufacturerData[2], std::min(sizeof(pw) - 1, manufacturerData.length() - 1));
|
||||
pw[sizeof(pw) - 1] = '\0'; // Ensure null termination
|
||||
flag |= 2;
|
||||
//DPRINTF("BLE Scan - PW(\"%s\")\n", (char *) &manufacturerData[2]);
|
||||
} else {
|
||||
flag = 0;
|
||||
}
|
||||
|
||||
if (flag == 3) {
|
||||
strncpy(BLE_SSID, ssid, sizeof(BLE_SSID) - 1);
|
||||
BLE_SSID[sizeof(BLE_SSID) - 1] = 0;
|
||||
strncpy(BLE_PW, pw, sizeof(BLE_PW) - 1);
|
||||
BLE_PW[sizeof(BLE_PW) - 1] = 0;
|
||||
flag = 0;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @brief Called when a new scan result is complete, including scan response data (if applicable).
|
||||
* @param [in] advertisedDevice The device for which the complete result is available.
|
||||
*/
|
||||
void onResult(const NimBLEAdvertisedDevice* advertisedDevice) override {
|
||||
uint16_t len;
|
||||
bthome_t *pTuyaData = (bthome_t *) advertisedDevice->getServiceData(&len);
|
||||
// Tuya
|
||||
if (len == sizeof(bthome_t) && pTuyaData->info == 0x40) {
|
||||
if (config.nTemp1SensorType == TEMP_SENSOR_TYPE::BLE_TUYA) {
|
||||
// Look for the custom Tuya sensor data structure
|
||||
ble.setData(pTuyaData->temp, pTuyaData->humid, pTuyaData->battery_level);
|
||||
} else if (config.nTemp2SensorType == TEMP_SENSOR_TYPE::BLE_TUYA) {
|
||||
// Look for the custom Tuya sensor data structure
|
||||
ble.setData2(pTuyaData->temp, pTuyaData->humid, pTuyaData->battery_level);
|
||||
}
|
||||
} else
|
||||
// InkBird
|
||||
if (config.nTemp1SensorType == TEMP_SENSOR_TYPE::BLE_INKBIRD ||
|
||||
config.nTemp2SensorType == TEMP_SENSOR_TYPE::BLE_INKBIRD) {
|
||||
// Process Inkbird Sensors
|
||||
std::string manufacturerData = advertisedDevice->getManufacturerData();
|
||||
if (manufacturerData.length() >= 7) {
|
||||
const uint8_t* data = reinterpret_cast<const uint8_t*>(manufacturerData.data());
|
||||
|
||||
// Inkbird data format: [unknown][unknown][temp_LSB][temp_MSB][humidity][voltage_LSB][voltage_MSB]
|
||||
int16_t temp = (data[2] | (data[3] << 8)); // Little-endian, scale by 0.01
|
||||
uint16_t humi = data[4]; // Humidity byte (0-100%)
|
||||
uint16_t batteryLevel = map(data[5] | (data[6] << 8), 2300, 3200, 0, 100); // Convert voltage to battery %
|
||||
|
||||
if (config.nTemp1SensorType == TEMP_SENSOR_TYPE::BLE_INKBIRD) {
|
||||
ble.setData(temp, humi, batteryLevel);
|
||||
} else if (config.nTemp2SensorType == TEMP_SENSOR_TYPE::BLE_INKBIRD) {
|
||||
ble.setData2(temp, humi, batteryLevel);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Called when a scan operation ends.
|
||||
* @param [in] scanResults The results of the scan that ended.
|
||||
* @param [in] reason The reason code for why the scan ended.
|
||||
*/
|
||||
void onScanEnd(const NimBLEScanResults& scanResults, int reason) override {};
|
||||
} hcScanCallbacks;
|
||||
|
||||
// Connection based Notification from Xiaomi devices
|
||||
static void notifyCallback(
|
||||
BLERemoteCharacteristic* pBLERemoteCharacteristic,
|
||||
uint8_t* pData,
|
||||
size_t length,
|
||||
bool isNotify)
|
||||
{
|
||||
uint16_t voltage;
|
||||
if (config.nTemp1SensorType == TEMP_SENSOR_TYPE::BLE_XIAOMI_MIJIA) {
|
||||
voltage = pData[3] | (pData[4] << 8); // little endian
|
||||
ble.setData(pData[0] | (pData[1] << 8), pData[2] * 100, map(voltage, 2300, 3200, 0, 100));
|
||||
} else if (config.nTemp2SensorType == TEMP_SENSOR_TYPE::BLE_XIAOMI_MIJIA) {
|
||||
voltage = pData[3] | (pData[4] << 8); // little endian
|
||||
ble.setData2(pData[0] | (pData[1] << 8), pData[2] * 100, map(voltage, 2300, 3200, 0, 100));
|
||||
}
|
||||
}
|
||||
|
||||
// Function to setup BLE
|
||||
void CBLEScan::setupConnect(uint64_t addr, uint64_t addr2) {
|
||||
m_nAddr = addr;
|
||||
m_nAddr2 = addr2;
|
||||
|
||||
m_nLatestBLEAdvertise =
|
||||
m_nLatestBLEAdvertise2 = millis();
|
||||
m_nLastReceive =
|
||||
m_nLastReceive2 = m_nLatestBLEAdvertise;
|
||||
m_bDataReceived =
|
||||
m_bDataReceived2 = false;
|
||||
|
||||
NimBLEDevice::init("");
|
||||
status.nFlags &= ~FLAG_BLE_BATT;
|
||||
status.nFlags &= ~FLAG_BLE_NODATA;
|
||||
status.nFlags &= ~FLAG_BLE_LOST;
|
||||
|
||||
m_nTemp = -9999;
|
||||
m_nHumid = -9999;
|
||||
m_nBatteryLevel = 0;
|
||||
m_nTemp2 = -9999;
|
||||
m_nHumid2 = -9999;
|
||||
m_nBatteryLevel2 = 0;
|
||||
m_bConnected = false;
|
||||
m_bConnectionInProgress = false;
|
||||
pClient = nullptr;
|
||||
|
||||
if ( config.nTemp1SensorType == TEMP_SENSOR_TYPE::BLE_XIAOMI_MIJIA) {
|
||||
connect(NimBLEAddress(m_nAddr, BLE_ADDR_PUBLIC), false);
|
||||
} else if ( config.nTemp2SensorType == TEMP_SENSOR_TYPE::BLE_XIAOMI_MIJIA) {
|
||||
connect(NimBLEAddress(m_nAddr2, BLE_ADDR_PUBLIC), false);
|
||||
}
|
||||
}
|
||||
|
||||
void CBLEScan::setupScan() {
|
||||
NimBLEAddress cTargetAddress;
|
||||
|
||||
if (config.nBLEScanInterval == 143 || config.nBLEScanInterval == 77 ||
|
||||
config.nBLEScanInterval == 139 || config.nBLEScanInterval == 91)
|
||||
m_nInterval = config.nBLEScanInterval;
|
||||
else
|
||||
m_nInterval = 143;
|
||||
|
||||
serviceData.reserve(256);
|
||||
NimBLEScan* pBLEScan = NimBLEDevice::getScan();
|
||||
pBLEScan->setScanCallbacks((NimBLEScanCallbacks *) &hcScanCallbacks, true);
|
||||
pBLEScan->setActiveScan(false);
|
||||
pBLEScan->setDuplicateFilter(false);
|
||||
pBLEScan->setMaxResults(0);
|
||||
m_bContinuousScan = true;
|
||||
m_bScanStarted = false;
|
||||
|
||||
if (config.nTemp1SensorType == TEMP_SENSOR_TYPE::BLE_TUYA ||
|
||||
config.nTemp1SensorType == TEMP_SENSOR_TYPE::BLE_INKBIRD) {
|
||||
cTargetAddress = NimBLEAddress(m_nAddr, BLE_ADDR_PUBLIC);
|
||||
pBLEScan->setTargetAddress(cTargetAddress);
|
||||
startScan();
|
||||
} else if (config.nTemp2SensorType == TEMP_SENSOR_TYPE::BLE_TUYA ||
|
||||
config.nTemp2SensorType == TEMP_SENSOR_TYPE::BLE_INKBIRD) {
|
||||
cTargetAddress = NimBLEAddress(m_nAddr2, BLE_ADDR_PUBLIC);
|
||||
pBLEScan->setTargetAddress(cTargetAddress);
|
||||
startScan();
|
||||
} else if (!isWiFiConnected()) {
|
||||
startScan();
|
||||
}
|
||||
}
|
||||
|
||||
void CBLEScan::startScan() {
|
||||
DPRINTF("Starting BLE scan... %d(%d)\n", m_nInterval, 5000 % m_nInterval);
|
||||
NimBLEScan* pBLEScan = NimBLEDevice::getScan();
|
||||
pBLEScan->setInterval(m_nInterval); // Interval between scan windows in ms
|
||||
pBLEScan->setWindow(m_nInterval - 3); // Length of time the scanner listens in ms
|
||||
if (m_bContinuousScan) {
|
||||
// 85(15), 77(5), 82(2), 61(2) 122(2), 139(4), 91(5), 143(5), 135(-5)
|
||||
pBLEScan->start(0, false, false); // Continuous scan
|
||||
} else {
|
||||
pBLEScan->start(12000, false, true); // Continuous scan
|
||||
}
|
||||
m_bScanStarted = true;
|
||||
}
|
||||
|
||||
bool CBLEScan::connect(NimBLEAddress pAddress, bool bAsync) {
|
||||
DPRINTLN("BLE Connect: Connecting...");
|
||||
if (pClient == nullptr) {
|
||||
pClient = NimBLEDevice::createClient();
|
||||
pClient->setConnectTimeout(30000);
|
||||
}
|
||||
|
||||
m_bConnectionInProgress = false;
|
||||
bool ret = pClient->connect(pAddress, true, bAsync, true);
|
||||
|
||||
if (bAsync) {
|
||||
m_bConnectionInProgress = true;
|
||||
m_bConnected = false;
|
||||
} else {
|
||||
m_bConnected = ret;
|
||||
}
|
||||
|
||||
if (!bAsync && m_bConnected) {
|
||||
NimBLERemoteService* pRemoteService = pClient->getService(serviceUUID);
|
||||
if (!pRemoteService) {
|
||||
DPRINTLN("BLE Connect: Service not found");
|
||||
return false;
|
||||
}
|
||||
|
||||
pRemoteCharacteristic = pRemoteService->getCharacteristic(charUUID);
|
||||
if (!pRemoteCharacteristic) {
|
||||
DPRINTLN("BLE Connect: Characteristic not found");
|
||||
return false;
|
||||
}
|
||||
|
||||
pRemoteCharacteristic->subscribe(true, notifyCallback);
|
||||
DPRINTF("BLE Connect: Connected to %s\n", pAddress.toString().c_str());
|
||||
}
|
||||
return m_bConnected;
|
||||
}
|
||||
|
||||
void CBLEScan::loop(unsigned long clock) {
|
||||
unsigned long gap;
|
||||
|
||||
if (config.nTemp1SensorType == TEMP_SENSOR_TYPE::BLE_XIAOMI_MIJIA ||
|
||||
config.nTemp2SensorType == TEMP_SENSOR_TYPE::BLE_XIAOMI_MIJIA) {
|
||||
if (!m_bConnected) {
|
||||
if (pClient->isConnected()) {
|
||||
m_bConnectionInProgress = false;
|
||||
NimBLERemoteService* pRemoteService = pClient->getService(serviceUUID);
|
||||
if (!pRemoteService) {
|
||||
DPRINTLN("BLE Connect: Service not found");
|
||||
return;
|
||||
}
|
||||
|
||||
pRemoteCharacteristic = pRemoteService->getCharacteristic(charUUID);
|
||||
if (!pRemoteCharacteristic) {
|
||||
DPRINTLN("BLE Connect: Characteristic not found");
|
||||
return;
|
||||
}
|
||||
|
||||
pRemoteCharacteristic->subscribe(true, notifyCallback);
|
||||
DPRINTLN("BLE Connect: Connected within the Loop");
|
||||
m_bConnected = true;
|
||||
} else if (!m_bConnectionInProgress) {
|
||||
if ( config.nTemp1SensorType == TEMP_SENSOR_TYPE::BLE_XIAOMI_MIJIA) {
|
||||
connect(NimBLEAddress(m_nAddr, BLE_ADDR_PUBLIC), true);
|
||||
} else if ( config.nTemp2SensorType == TEMP_SENSOR_TYPE::BLE_XIAOMI_MIJIA) {
|
||||
connect(NimBLEAddress(m_nAddr2, BLE_ADDR_PUBLIC), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (m_bConnected) {
|
||||
pClient->disconnect();
|
||||
m_bConnected = false;
|
||||
m_bConnectionInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (config.nTemp1SensorType == TEMP_SENSOR_TYPE::BLE_TUYA ||
|
||||
config.nTemp1SensorType == TEMP_SENSOR_TYPE::BLE_INKBIRD) {
|
||||
if (m_bScanStarted) {
|
||||
gap = clock - m_nLatestBLEAdvertise;
|
||||
if (gap < 6050) {
|
||||
status.nFlags &= ~FLAG_BLE_NODATA;
|
||||
status.nFlags &= ~FLAG_BLE_LOST;
|
||||
}
|
||||
else if (gap < 8150) {
|
||||
status.nFlags |= FLAG_BLE_NODATA;
|
||||
status.nFlags &= ~FLAG_BLE_LOST;
|
||||
if (config.bBLETest) m_nHumid = 0;
|
||||
}
|
||||
else if (gap > 60100 && gap < 63200) {
|
||||
//DPRINTF("GAP Reached... %d", gap);
|
||||
status.nFlags |= (FLAG_BLE_NODATA | FLAG_BLE_LOST);
|
||||
m_nTemp = -9999;
|
||||
m_nHumid = -9999;
|
||||
m_nBatteryLevel = 0;
|
||||
}
|
||||
} else {
|
||||
startScan();
|
||||
}
|
||||
} else if (config.nTemp2SensorType == TEMP_SENSOR_TYPE::BLE_TUYA ||
|
||||
config.nTemp2SensorType == TEMP_SENSOR_TYPE::BLE_INKBIRD) {
|
||||
if (m_bScanStarted) {
|
||||
gap = clock - m_nLatestBLEAdvertise2;
|
||||
if (gap > 6050 && gap < 8150) {
|
||||
if (config.bBLETest) m_nHumid2 = 0;
|
||||
}
|
||||
else if (gap > 60100 && gap < 63200) {
|
||||
m_nTemp2 = -9999;
|
||||
m_nHumid2 = -9999;
|
||||
m_nBatteryLevel2 = 0;
|
||||
}
|
||||
} else {
|
||||
startScan();
|
||||
}
|
||||
}
|
||||
#ifdef BLE_DEBUG
|
||||
{
|
||||
static int col = 0;
|
||||
static int hit = 0;
|
||||
static int miss = 0;
|
||||
static int hit_total = 0;
|
||||
static int miss_total = 0;
|
||||
|
||||
|
||||
if (m_bDataReceived) {
|
||||
gap = m_nLatestBLEAdvertise - m_nLastReceive;
|
||||
int cnt = (gap + 2500)/5000 - 1;
|
||||
for (int i = 0; i < cnt; i++)
|
||||
{
|
||||
Serial.print(cnt);
|
||||
miss++;
|
||||
if (++col >=60) {
|
||||
hit_total += hit;
|
||||
miss_total += miss;
|
||||
DPRINTF(" Hit: %2d, Miss: %2d Total(Hit: %3d, Miss: %3d, Ratio %.1f%%) Mem: %d\n",
|
||||
hit, miss, hit_total, miss_total, (float)hit_total * 100.0f / (hit_total + miss_total),
|
||||
ESP.getFreeHeap());
|
||||
col = 0;
|
||||
hit = 0;
|
||||
miss = 0;
|
||||
}
|
||||
}
|
||||
Serial.print('.');
|
||||
hit++;
|
||||
if (++col >=60) {
|
||||
hit_total += hit;
|
||||
miss_total += miss;
|
||||
DPRINTF(" Hit: %d, Miss: %d Total(Hit: %d, Miss: %d, Ratio %.1f%%) Mem: %d\n",
|
||||
hit, miss, hit_total, miss_total, (float)hit_total * 100.0f / (hit_total + miss_total),
|
||||
ESP.getFreeHeap());
|
||||
col = 0;
|
||||
hit = 0;
|
||||
miss = 0;
|
||||
}
|
||||
|
||||
m_bDataReceived = false;
|
||||
status.nFlags &= ~FLAG_BLE_NODATA;
|
||||
}
|
||||
/*
|
||||
// Interval Scan
|
||||
// {
|
||||
// NimBLEScan* pScan = NimBLEDevice::getScan();
|
||||
// if (g_bBLEDataReceived) {
|
||||
// Serial.printf("BLE(%d) - Temp: %.2f°C Hum: %.2f%% Batt: %d Mem: %d (Stop)\n",
|
||||
// tickMillis - lastReceived,
|
||||
// g_nBLETemp / 100.0f,
|
||||
// g_nBLEHumid / 100.0f,
|
||||
// g_nBLEBatt,
|
||||
// ESP.getFreeHeap() );
|
||||
// status.nFlags &= ~FLAG_BLE_NODATA;
|
||||
// g_bBLEDataReceived = false;
|
||||
// if (pScan->isScanning()) {
|
||||
// pScan->stop();
|
||||
// pScan->clearResults();
|
||||
// }
|
||||
// lastErrorMessage = tickMillis;
|
||||
// lastReceived = lastTickMillis;
|
||||
// } else {
|
||||
// unsigned long gap = tickMillis - g_lastBLEAdvertise;
|
||||
// if (!pScan->isScanning()) {
|
||||
// if (gap > 4910 && gap < 4960) {
|
||||
// pScan->start(200, false, true);
|
||||
// Serial.printf("Starting Scan for 200ms at %d\n", tickMillis - g_lastBLEAdvertise);
|
||||
// } else if (gap > 9500) {
|
||||
// pScan->start(5200, false, true);
|
||||
// Serial.printf("Starting Scan for 5200ms at %d\n", tickMillis - g_lastBLEAdvertise);
|
||||
// }
|
||||
// }
|
||||
// if (gap > 10050 && tickMillis - lastErrorMessage > 10050) {
|
||||
// Serial.printf("No BLE Data for % seconds\n", tickMillis - lastErrorMessage);
|
||||
// status.nFlags |= FLAG_BLE_NODATA;
|
||||
// lastErrorMessage = tickMillis;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
*/
|
||||
}
|
||||
#endif
|
||||
}
|
||||
72
BLEScan.h
Normal file
72
BLEScan.h
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
#ifndef __BLE_SCAN_H
|
||||
#define __BLE_SCAN_H
|
||||
|
||||
class NimBLEClient;
|
||||
class NimBLERemoteCharacteristic;
|
||||
class NimBLEAddress;
|
||||
|
||||
class CBLEScan {
|
||||
public:
|
||||
void setupConnect(uint64_t addr, uint64_t addr2);
|
||||
void setupScan();
|
||||
void loop(unsigned long clock);
|
||||
void startScan();
|
||||
bool connect(NimBLEAddress addr, bool bAsync);
|
||||
inline int16_t getTemp() { return m_nTemp; };
|
||||
inline int16_t getHumid() { return m_nHumid; };
|
||||
inline uint8_t getBatteyLevel() { return m_nBatteryLevel; };
|
||||
inline int16_t getTemp2() { return m_nTemp2; };
|
||||
inline int16_t getHumid2() { return m_nHumid2; };
|
||||
inline uint8_t getBatteyLevel2() { return m_nBatteryLevel2; };
|
||||
|
||||
inline void setData(int16_t temp, uint16_t humid, uint8_t bLevel) {
|
||||
m_nTemp = temp;
|
||||
m_nHumid = humid;
|
||||
m_nBatteryLevel = bLevel;
|
||||
m_bDataReceived = true;
|
||||
m_nLastReceive = m_nLatestBLEAdvertise;
|
||||
m_nLatestBLEAdvertise = millis();
|
||||
}
|
||||
|
||||
inline void setData2(int16_t temp, uint16_t humid, uint8_t bLevel) {
|
||||
m_nTemp2 = temp;
|
||||
m_nHumid2 = humid;
|
||||
m_nBatteryLevel2 = bLevel;
|
||||
m_bDataReceived2 = true;
|
||||
m_nLastReceive2 = m_nLatestBLEAdvertise2;
|
||||
m_nLatestBLEAdvertise2 = millis();
|
||||
}
|
||||
|
||||
inline void setScanType(bool bContinuous) { m_bContinuousScan = bContinuous; };
|
||||
|
||||
private:
|
||||
bool m_bContinuousScan;
|
||||
bool m_bScanStarted;
|
||||
bool m_bConnected;
|
||||
bool m_bConnectionInProgress;
|
||||
int16_t m_nInterval;
|
||||
uint64_t m_nAddr, m_nAddr2;
|
||||
|
||||
int16_t m_nTemp;
|
||||
int16_t m_nHumid;
|
||||
uint8_t m_nBatteryLevel;
|
||||
|
||||
int16_t m_nTemp2;
|
||||
int16_t m_nHumid2;
|
||||
uint8_t m_nBatteryLevel2;
|
||||
|
||||
unsigned long m_nLatestBLEAdvertise;
|
||||
unsigned long m_nLastReceive;
|
||||
bool m_bDataReceived;
|
||||
|
||||
unsigned long m_nLatestBLEAdvertise2;
|
||||
unsigned long m_nLastReceive2;
|
||||
bool m_bDataReceived2;
|
||||
|
||||
NimBLEClient *pClient;
|
||||
NimBLERemoteCharacteristic *pRemoteCharacteristic;
|
||||
};
|
||||
|
||||
extern CBLEScan ble;
|
||||
|
||||
#endif
|
||||
154
CommSerial.cpp
Normal file
154
CommSerial.cpp
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
#include "HermitCrab.h"
|
||||
#include "Config.h"
|
||||
#include "History.h"
|
||||
#include "zcd.h"
|
||||
|
||||
const char *strDeviceType[] = {
|
||||
"None",
|
||||
"Test",
|
||||
"ESP8266",
|
||||
"ON_OFF",
|
||||
"ZCD",
|
||||
"CAM",
|
||||
"Beta",
|
||||
"Beta_BLE",
|
||||
"End"
|
||||
};
|
||||
|
||||
MY_IRAM_ATTR void checkSerial(unsigned long tick)
|
||||
{
|
||||
static char buffer[256];
|
||||
static short idx = 0;
|
||||
static unsigned long val;
|
||||
|
||||
while (Serial.available() > 0) {
|
||||
if (idx > 254) {
|
||||
idx = 0;
|
||||
ESP_LOGI(TAG,"SrialHost: Buffer OverFlow");
|
||||
}
|
||||
|
||||
buffer[idx] = Serial.read();
|
||||
if (buffer[idx] == '\n') {
|
||||
if (idx >= 1) {
|
||||
buffer[idx] = 0;
|
||||
|
||||
switch(buffer[0])
|
||||
{
|
||||
case 'T': // Temp Target
|
||||
if (idx > 1) {
|
||||
val = atoi(&buffer[1]);
|
||||
if (val >= 25 && val <= 35) {
|
||||
config.nTempTarget = val * 10;
|
||||
} else if (val >= 250 && val <= 350) {
|
||||
config.nTempTarget = val;
|
||||
}
|
||||
}
|
||||
Serial.printf("%s SerialSet: Temp Target %d.%d°C Duty(%.2f%%)\n",
|
||||
printStatus(tick, false), config.nTempTarget / 10, config.nTempTarget % 10,
|
||||
status.nHeater1Duty / 100.0f);
|
||||
break;
|
||||
case 't': // Temp Target Night
|
||||
if (idx > 1) {
|
||||
val = atoi(&buffer[1]);
|
||||
if (val >= 25 && val <= 35) {
|
||||
config.nTempTargetNight = val * 10;
|
||||
} else if (val >= 250 && val <= 350) {
|
||||
config.nTempTargetNight = val;
|
||||
}
|
||||
}
|
||||
Serial.printf("%s SerialSet: Temp Target Night %d.%d°C Duty(%.2f%%)\n",
|
||||
printStatus(tick, false),
|
||||
config.nTempTargetNight / 10, config.nTempTargetNight % 10,
|
||||
status.nHeater1Duty/ 100.0f);
|
||||
break;
|
||||
case 'H': // Humidity Target
|
||||
case 'h':
|
||||
if (idx > 1) {
|
||||
val = atoi(&buffer[1]);
|
||||
if (val >= 20 && val <= 95) {
|
||||
config.nHumidTarget = val * 10;
|
||||
} else if (val >= 200 && val <= 950) {
|
||||
config.nHumidTarget = val;
|
||||
}
|
||||
}
|
||||
Serial.printf("%s SerialSet: Humid Target(%d.%d%%) Mist %s (Duty: %d)\n",
|
||||
printStatus(tick),
|
||||
config.nHumidTarget / 10, config.nHumidTarget % 10,
|
||||
status.nMistDuty > 0 ? "ON" : "OFF",
|
||||
status.nMistDuty);
|
||||
break;
|
||||
case 'M': // MistOn time
|
||||
case 'm': // MistDelay time
|
||||
if (idx > 1) {
|
||||
val = atoi(&buffer[1]);
|
||||
if (val >= 0 && val <= 1023) {
|
||||
status.nMistDuty = val;
|
||||
}
|
||||
}
|
||||
Serial.printf("%s SerialSet: Mist %s (Duty: %d)\n",
|
||||
printStatus(tick), status.nMistDuty > 0 ? "ON" : "OFF", status.nMistDuty);
|
||||
break;
|
||||
case 'l': //Light1
|
||||
case 'L':
|
||||
if (idx > 2) {
|
||||
val = atoi(&buffer[1]);
|
||||
if (val >= PWM_OFF && val <= PWM_FULL) {
|
||||
status.nLightTargetDuty = val;
|
||||
}
|
||||
}
|
||||
Serial.printf("%s SerialSet: Light1 %s (%d --> %d)\n",
|
||||
printStatus(tick), status.nLightDuty > 0 ? "ON" : "OFF",
|
||||
status.nLightDuty, status.nLightTargetDuty);
|
||||
break;
|
||||
case 'd': // Display Sensor
|
||||
if (idx < 2) {
|
||||
bShowSensor = !bShowSensor;
|
||||
} else {
|
||||
val = atoi(&buffer[1]);
|
||||
bShowSensor = val == 0 ? false : true;
|
||||
}
|
||||
Serial.printf("%s SerialSet: DisplaySensor %s\n", printStatus(tick), bShowSensor ? "On" : "Off");
|
||||
break;
|
||||
case 'p': // Print
|
||||
case 'P':
|
||||
break;
|
||||
case 's': // Save Config
|
||||
case 'S':
|
||||
history.savePID();
|
||||
config.save();
|
||||
Serial.printf("%s Config Saved\n", printStatus(tick));
|
||||
break;
|
||||
case 'Y': // Device Type
|
||||
case 'y':
|
||||
if (idx > 1) {
|
||||
val = atoi(&buffer[1]);
|
||||
char *sz = NULL;
|
||||
if (val > TYPE_NONE && val < TYPE_DEVICE_END) {
|
||||
config.m_nDeviceType = val;
|
||||
Serial.printf("%s SeriaSet: DeviceType %s\n", printStatus(tick), strDeviceType[val]);
|
||||
} else {
|
||||
Serial.printf("%s SerialSet: Invalid DeviceType\n");
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'Z':
|
||||
case 'z':
|
||||
if (idx > 1) {
|
||||
val = atoi(&buffer[1]);
|
||||
if (val >= 0 && val <= 10000) {
|
||||
//ESP_LOGI(TAG,"%s SerialSet: Set Heater Duty %.1f%%\n", printStatus(tick), dutyPercent);
|
||||
status.nHeater1Duty = val;
|
||||
setHeater1Duty(status.nHeater1Duty);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Clear Buffer
|
||||
idx = 0;
|
||||
} else {
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
}
|
||||
158
Config.cpp
Normal file
158
Config.cpp
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
#include "HermitCrab.h"
|
||||
#include "AHT2x.h"
|
||||
#include "Config.h"
|
||||
#include "TimeManager.h"
|
||||
#include <Preferences.h>
|
||||
|
||||
#ifndef SIGNATURE1
|
||||
#define SIGNATURE1 ((uint16_t) 0xC8AB)
|
||||
#endif
|
||||
#ifndef SIGNATURE2
|
||||
#define SIGNATURE2 ((uint16_t) 0x4E81)
|
||||
#endif
|
||||
|
||||
Preferences preferences;
|
||||
CONFIG_TYPE config;
|
||||
|
||||
// Function to initialize default values for the config structure
|
||||
void CONFIG_TYPE::init() {
|
||||
m_nSignature1 = SIGNATURE1;
|
||||
m_nSignature2 = SIGNATURE2;
|
||||
|
||||
// Block 1 - Control and Environment
|
||||
bSmartControl = false;
|
||||
bNightControl = false;
|
||||
bControlTemperature = true;
|
||||
bControlHumidity = true;
|
||||
bEnableIO = true;
|
||||
bAC2_OnOff = false;
|
||||
|
||||
nNightStartHour = 18;
|
||||
nNightStartMin = 0;
|
||||
nNightEndHour = 6;
|
||||
nNightEndMin = 0;
|
||||
|
||||
// Block 2 - Sensor and Temperature/Humidity Settings
|
||||
nTempTarget = 260;
|
||||
nTempTargetNight = 260;
|
||||
nTemp1Offset = 0;
|
||||
nTemp2Offset = 0;
|
||||
nTemp3Offset = 0;
|
||||
nHumidTarget = 880;
|
||||
nHumidTargetNight = 880;
|
||||
nHumid1Offset = 0;
|
||||
nHumid2Offset = 0;
|
||||
nTemp1SensorType = TEMP_SENSOR_TYPE::BLE_TUYA;
|
||||
nTemp2SensorType = TEMP_SENSOR_TYPE::AHT20;
|
||||
|
||||
Kp_Temp1 = 3.0f; // Load Kp for Temperature control
|
||||
Kd_Temp1 = 750.0f; // Load Kd for Temperature control
|
||||
LR_Temp1 = 0.05f; // Load learning rate for Temperature
|
||||
|
||||
Kp_Humidity = 2.0f; // Load Kp for Humidity control
|
||||
Kd_Humidity = 150.0f; // Load Kd for Humidity control
|
||||
LR_Humidity = 0.08f; // Load learning rate for Humidity
|
||||
|
||||
// Block 3 - AC1 and AC2
|
||||
// Day/Night Min/Max/Start On/Off THigh/Low Time B/E Period Hum H/L
|
||||
config.ac1 = {CONTROL_TEMP_HEAT_PID, 0, 1,1, 0, 1000, 0, 250,0, 920,880, 0,24*60-1, 60, 5, 920,880 };
|
||||
config.ac2 = {CONTROL_TEMP_HEAT, 0, 0,0, 0, 1000, 0, 250,0, 920,880, 0,25*60-1, 60, 5, 920,880 };
|
||||
|
||||
// Block 4 = Mist and Fan
|
||||
// Day/Night Min/Max/Start On/Off THigh/Low Time B/E Period Hum H/L
|
||||
config.mist = {CONTROL_HUMIDITY_INC_PID, 0, 1,1, 0, 1000, 0, 250,0, 920,880, 0,25*60-1, 60, 5, 920,880 };
|
||||
config.fan = {CONTROL_TEMP_COOL, 0, 1,1, 0, 1000, 0, 250,0, 920,880, 0,25*60-1, 60, 5, 920,880 };
|
||||
|
||||
// Block 5 - Motor and Light
|
||||
// Day/Night Min/Max/Start On/Off THigh/Low Time B/E Period Hum H/L
|
||||
config.motor = {CONTROL_PERIOD, 0, 1,1, 0, 1000, 0, 250,0, 920,880, 0,25*60-1, 60, 5, 920,880 };
|
||||
config.light = {CONTROL_DAY_NIGHT, 0, 1,1, 0, 1000, 0, 250,0, 920,880, 0,25*60-1, 60, 5, 920,880 };
|
||||
|
||||
|
||||
// Block 6 - Environment and Operations
|
||||
bSendStatusSerial = false;
|
||||
bConfigSaved = false;
|
||||
bStatusSaved = false;
|
||||
|
||||
//
|
||||
// Reserved
|
||||
//
|
||||
// TBD
|
||||
for (int i = 0; i < 17; i = i + 8) {
|
||||
m_nChipId |= ((ESP.getEfuseMac() >> (40 - i)) & 0xff) << i;
|
||||
}
|
||||
m_nPublicPort = (uint16_t)(m_nChipId & 0xFFFF);
|
||||
m_nDeviceType = THIS_DEVICE_TYPE;
|
||||
|
||||
//
|
||||
// WiFi Client Only
|
||||
//
|
||||
m_nDisplayTime = 1800;
|
||||
m_nDisplayTempHigh = 40;
|
||||
m_nDisplayTempLow = 20;
|
||||
m_nTempHigh = 20;
|
||||
m_fShowRealTime = 0x000F;
|
||||
|
||||
// Names
|
||||
strcpy(m_sDeviceName, "Beta X");
|
||||
strcpy(m_sMake, "VisionSoft");
|
||||
strcpy(m_sModel, "HermitCrab");
|
||||
strncpy(m_sVersion, HC__VERSION, 11);
|
||||
|
||||
|
||||
// WiFi - SSID and Password
|
||||
strcpy(ssid, "RECALL");
|
||||
strcpy(pw, "BBBB9999");
|
||||
ESP_LOGI(TAG,"Config Initialized");
|
||||
}
|
||||
|
||||
// Function to load the configuration block from preferences
|
||||
bool CONFIG_TYPE::load() {
|
||||
preferences.begin("HermitCrab", false); // Open preferences in read-write mode
|
||||
|
||||
// Check if config has been saved previously
|
||||
size_t len = preferences.getBytesLength("config_data");
|
||||
if (len != sizeof(config)) {
|
||||
ESP_LOGI(TAG,"\nPreferences - First Time");
|
||||
// First time, initialize default config
|
||||
ESP_LOGI(TAG, "Config Size Error: %d out of %d\n", len, sizeof(config));
|
||||
init();
|
||||
save(); // Save defaults
|
||||
preferences.end();
|
||||
return true; // Indicates that it was initialized
|
||||
}
|
||||
|
||||
// Load the structure as a block of data
|
||||
preferences.getBytes("config_data", &config, sizeof(config));
|
||||
if (m_nSignature1 != SIGNATURE1 ||
|
||||
m_nSignature2 != SIGNATURE2) {
|
||||
ESP_LOGI(TAG, "Config Load: Signature Mismatch %X %X - %X %X\n",
|
||||
m_nSignature1, m_nSignature2,
|
||||
SIGNATURE1, SIGNATURE2);
|
||||
}
|
||||
preferences.end();
|
||||
ESP_LOGI(TAG,"Config Loaded");
|
||||
bConfigSaved = false;
|
||||
bStatusSaved = false;
|
||||
for (int i = 0; i < 17; i = i + 8) {
|
||||
m_nChipId |= ((ESP.getEfuseMac() >> (40 - i)) & 0xff) << i;
|
||||
}
|
||||
strncpy(m_sVersion, HC__VERSION, 11);
|
||||
m_sVersion[11] = 0;
|
||||
m_nDeviceType = THIS_DEVICE_TYPE;
|
||||
return true; // Config loaded successfully
|
||||
}
|
||||
|
||||
// Function to save the entire configuration block to preferences
|
||||
void CONFIG_TYPE::save() {
|
||||
// Update before save
|
||||
config.statusSave = status;
|
||||
config.bStatusSaved = true;
|
||||
|
||||
// Save the entire config structure as one block
|
||||
preferences.begin("HermitCrab", false); // Open preferences in read-write mode
|
||||
preferences.putBytes("config_data", &config, sizeof(config));
|
||||
preferences.end(); // Close preferences
|
||||
bConfigSaved = true;
|
||||
ESP_LOGI(TAG,"Config Saved");
|
||||
}
|
||||
197
Config.h
Normal file
197
Config.h
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
# ifndef __CONFIG_H
|
||||
# define __CONFIG_H
|
||||
|
||||
#include <Preferences.h>
|
||||
#include "HermitCrab.h"
|
||||
|
||||
|
||||
extern const char *strDeviceType[];
|
||||
|
||||
|
||||
enum ENUM_DEVICE_TYPE {
|
||||
TYPE_NONE = 0,
|
||||
TYPE_TEST,
|
||||
TYPE_ESP8266,
|
||||
TYPE_ON_OFF,
|
||||
TYPE_ZCD,
|
||||
TYPE_CAM,
|
||||
TYPE_BETA,
|
||||
TYPE_BETA_BLE,
|
||||
TYPE_DEVICE_END,
|
||||
TYPE_CLIENT = 1000,
|
||||
TYPE_CLIENT_WIN,
|
||||
TYPE_CLIENT_ANDROID
|
||||
};
|
||||
|
||||
enum HEATER_TYPE {
|
||||
TYPE_BULB,
|
||||
TYPE_IR,
|
||||
TYPE_CERAMIC,
|
||||
TYPE_ZCD3,
|
||||
TYPE_ZCD4,
|
||||
TYPE_ZCD5,
|
||||
TYPE_ZCD6,
|
||||
TYPE_ZCD_CONTROL,
|
||||
TYPE_FLUORESCENT = 16,
|
||||
TYPE_LED,
|
||||
TYPE_UAV,
|
||||
TYPE_HEATER_END
|
||||
};
|
||||
|
||||
enum DEVICE_CONTROL_TYPE {
|
||||
CONTROL_NONE,
|
||||
CONTROL_TEMP_HEAT,
|
||||
CONTROL_TEMP_COOL,
|
||||
CONTROL_HUMIDITY_INC,
|
||||
CONTROL_HUMIDITY_DEC,
|
||||
CONTROL_DAY_NIGHT,
|
||||
CONTROL_TIME,
|
||||
CONTROL_PERIOD,
|
||||
CONTROL_TEMP_HEAT_PID,
|
||||
CONTROL_TEMP_COOL_PID,
|
||||
CONTROL_HUMIDITY_INC_PID,
|
||||
CONTROL_HUMIDITY_DEC_PID
|
||||
};
|
||||
|
||||
// Enumeration for the sensor
|
||||
enum TEMP_SENSOR_TYPE {
|
||||
SENSOR_NONE = 0,
|
||||
AHT20, // AHT20 on 0x38 - default
|
||||
AHT2x, // AHT25 on 0x38 - default
|
||||
AHT10_0x39, // AHT10 on 0x39 - altanative
|
||||
NTC,
|
||||
BLE_TUYA,
|
||||
BLE_XIAOMI_MIJIA,
|
||||
BLE_INKBIRD
|
||||
};
|
||||
|
||||
#pragma pack(push) /* push current alignment to stack */
|
||||
#pragma pack(1) /* set alignment to 1 byte boundary */
|
||||
|
||||
typedef struct CONFIG_STRUCT {
|
||||
uint16_t m_nSignature1;
|
||||
|
||||
// Block 1 - Control and EnvironMent
|
||||
// Offset 2
|
||||
bool bSmartControl;
|
||||
bool bNightControl;
|
||||
bool bControlTemperature;
|
||||
bool bControlHumidity;
|
||||
bool bEnableIO;
|
||||
bool bCheckAC;
|
||||
bool bAC2_OnOff;
|
||||
bool bdummy;
|
||||
uint8_t nNightStartHour, nNightStartMin, nNightEndHour, nNightEndMin;
|
||||
float Kp_Temp2; // Load Kp for Temperature control
|
||||
float Kd_Temp2; // Load Kd for Temperature control
|
||||
float LR_Temp2; // Load learning rate for Temperature
|
||||
float Kp_Temp3; // Load Kp for Temperature control
|
||||
float Kd_Temp3; // Load Kd for Temperature control
|
||||
float LR_Temp3; // Load learning rate for Temperature
|
||||
union {
|
||||
uint64_t nBLESensorAddr2;
|
||||
uint8_t nBLESensorAddrBytes2[8];
|
||||
};
|
||||
char bExtra[64 - 8 * sizeof(bool) - 4 * sizeof(uint8_t) - 6 * sizeof(float) - sizeof(uint64_t)];
|
||||
|
||||
// Block 2 - Sensor and TargetTemperature and Himidity
|
||||
// Offset 64 + 2
|
||||
int16_t nTempTarget, nTempTargetNight; // Target Temperature
|
||||
int16_t nTemp1Offset, nTemp2Offset, nTemp3Offset;
|
||||
uint16_t nHumidTarget, nHumidTargetNight;
|
||||
int16_t nHumid1Offset, nHumid2Offset;
|
||||
uint8_t nTemp1SensorType; // TempSensor Type enum
|
||||
uint8_t nTemp2SensorType; // TempSensor Type enum
|
||||
float Kp_Temp1; // Load Kp for Temperature control
|
||||
float Kd_Temp1; // Load Kd for Temperature control
|
||||
float LR_Temp1; // Load learning rate for Temperature
|
||||
float Kp_Humidity; // Load Kp for humidity control
|
||||
float Kd_Humidity; // Load Kd for humidity control
|
||||
float LR_Humidity; // Load learning rate for humidity
|
||||
int16_t nTempSafety;
|
||||
union {
|
||||
uint64_t nBLESensorAddr;
|
||||
uint8_t nBLESensorAddrBytes[8];
|
||||
};
|
||||
uint8_t bNTCNegativePolarity;
|
||||
uint8_t nBLEScanInterval;
|
||||
uint8_t bBLETest;
|
||||
char nTempExtra[64 - 10 * sizeof(int16_t) - 6 * sizeof(float) - 5 * sizeof(uint8_t) - sizeof(uint64_t)];
|
||||
|
||||
// Block 3 - AC1 and AC2
|
||||
// Offset 128 + 2
|
||||
DEVICE_PARAM_TYPE ac1;
|
||||
DEVICE_PARAM_TYPE ac2;
|
||||
|
||||
// Block 4 - Mist and Fan
|
||||
// Offset 192 + 2
|
||||
DEVICE_PARAM_TYPE mist;
|
||||
DEVICE_PARAM_TYPE fan;
|
||||
|
||||
// Block 5 - Motor and Light
|
||||
// Offset 256 + 2
|
||||
DEVICE_PARAM_TYPE motor;
|
||||
DEVICE_PARAM_TYPE light;
|
||||
|
||||
// Block 6 - Environment and Operations
|
||||
// Offset 320 + 2
|
||||
bool bSendStatusSerial;
|
||||
bool bConfigSaved;
|
||||
bool bStatusSaved;
|
||||
char nEnvExtra[32 - 3 * sizeof(bool)];
|
||||
// Offset 352 + 2
|
||||
STATUS_TYPE statusSave;
|
||||
|
||||
// Block 7 - ID and Client Display
|
||||
// Offset 384 + 2
|
||||
uint32_t m_nChipId;
|
||||
uint16_t m_nDeviceType;
|
||||
uint16_t m_nPublicPort;
|
||||
uint16_t m_nExtraTBD[4];
|
||||
// WiFi Client Only
|
||||
uint16_t m_nDisplayTime;
|
||||
int16_t m_nDisplayTempHigh;
|
||||
int16_t m_nDisplayTempLow;
|
||||
int16_t m_nTempHigh;
|
||||
uint16_t m_fShowRealTime;
|
||||
uint16_t m_fShowHistory;
|
||||
uint32_t m_nEpochTime;
|
||||
uint32_t m_nTimeOffset;
|
||||
uint8_t m_bFahrenheit;
|
||||
char nLongExtra[64 - 3 * sizeof(uint32_t) - 12 * sizeof(uint16_t)- 1 * sizeof(uint8_t)];
|
||||
|
||||
// Block 8 and 9 - WiFi AP ssid and pw
|
||||
// Offset 448 + 2 & 512 + 2
|
||||
char ssid[64], pw[64];
|
||||
|
||||
// Block 10
|
||||
// Offset 576 + 2
|
||||
char m_sDeviceName[32];
|
||||
char m_sMake[32];
|
||||
|
||||
// Block 11
|
||||
// Offset 640 + 2
|
||||
char m_sModel[32];
|
||||
char m_sVersion[12];
|
||||
char nModelExtra[64 - 44];
|
||||
|
||||
// Offset 704 + 2
|
||||
uint16_t m_nSignature2;
|
||||
|
||||
#ifdef ESP32
|
||||
// ConfigStruct Size: 708
|
||||
public:
|
||||
void init();
|
||||
bool load();
|
||||
void save();
|
||||
//bool saveToServer();
|
||||
#endif
|
||||
} CONFIG_TYPE;
|
||||
#pragma pack(pop) // Restore previous alignment setting
|
||||
|
||||
extern class Preferences preferences;
|
||||
extern CONFIG_TYPE config;
|
||||
extern char BLE_SSID[32];
|
||||
extern char BLE_PW[32];
|
||||
|
||||
#endif
|
||||
99
ConnectWiFi.cpp
Normal file
99
ConnectWiFi.cpp
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
#include "ConnectWiFi.h"
|
||||
#include "Config.h"
|
||||
|
||||
extern bool g_bWiFiSetupExecuted;
|
||||
extern bool g_bWiFiHasBeenConnected;
|
||||
void setupPostWiFi(bool bBoot = false);
|
||||
|
||||
void WiFiEvent(WiFiEvent_t event) {
|
||||
switch (event) {
|
||||
case IP_EVENT_STA_GOT_IP:
|
||||
DPRINTF("WiFi connected, IP: %s\n", WiFi.localIP().toString().c_str());
|
||||
g_bWiFiHasBeenConnected = true;
|
||||
ledcWrite(PIN_LED_WIFI, PWM_FULL * 9 / 10); // LED_OFF
|
||||
if (!g_bWiFiSetupExecuted) setupPostWiFi(false);
|
||||
break;
|
||||
case WIFI_EVENT_STA_DISCONNECTED:
|
||||
DPRINTLN("WiFi disconnected.");
|
||||
ledcWrite(PIN_LED_WIFI, PWM_OFF); // LED_ON
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Function to compare current Wi-Fi credentials with the ones in config
|
||||
void checkAndUpdateWiFiCredentials() {
|
||||
// Check if the SSID or PW are different from the current Wi-Fi credentials
|
||||
if (strncmp(BLE_SSID, config.ssid, sizeof(BLE_SSID)) ||
|
||||
strncmp(BLE_PW, config.pw, sizeof(BLE_PW))) {
|
||||
DPRINTF("BLE Credentials SSID(%s) PW(%S)\n", BLE_SSID, BLE_PW);
|
||||
DPRINTF("Cfg Credentials SSID(%s) PW(%S)\n", config.ssid, config.pw);
|
||||
DPRINTLN("Wi-Fi credentials changed! Saving new credentials...");
|
||||
strncpy(config.ssid, BLE_SSID, sizeof(BLE_SSID));
|
||||
strncpy(config.pw, BLE_PW, sizeof(BLE_PW));
|
||||
// Save the new credentials
|
||||
config.save();
|
||||
|
||||
// Restart ESP32 to apply the new credentials
|
||||
ESP.restart();
|
||||
}
|
||||
}
|
||||
|
||||
IRAM_ATTR void checkWiFi(unsigned long tickMillis) {
|
||||
static unsigned long lastAttempt = 0;
|
||||
static bool bConnecting = false;
|
||||
|
||||
wl_status_t status = WiFi.status();
|
||||
|
||||
// Connected
|
||||
if (status == WL_CONNECTED) {
|
||||
bConnecting = false;
|
||||
g_bWiFiHasBeenConnected = true;
|
||||
return; // Already connected, no need to proceed further
|
||||
}
|
||||
|
||||
checkAndUpdateWiFiCredentials();
|
||||
|
||||
// Connecting
|
||||
if (bConnecting) {
|
||||
if (tickMillis - lastAttempt < 60000) {
|
||||
// give 30 seconds for connection try
|
||||
return;
|
||||
}
|
||||
|
||||
// Connection Failure
|
||||
DPRINTF("Connection timed out! Status: %d\n", status);
|
||||
//WiFi.disconnect(false, true);
|
||||
bConnecting = false;
|
||||
g_bWiFiHasBeenConnected = false;
|
||||
}
|
||||
|
||||
// Not Connected 1 - Reconnect
|
||||
if (status == WL_DISCONNECTED && g_bWiFiHasBeenConnected) {
|
||||
DPRINTLN("Attempting WiFi reconnection...");
|
||||
WiFi.reconnect();
|
||||
ledcWrite(PIN_LED_WIFI, PWM_FULL * 4 / 5); // Light Blink
|
||||
lastAttempt = tickMillis;
|
||||
bConnecting = true;
|
||||
}
|
||||
|
||||
|
||||
// Not Conneccted 2 - Connect
|
||||
if (tickMillis - lastAttempt > 300000) { // Retry every 5 minutes
|
||||
DPRINTF("Loop: Connecting to WiFi: SSID: '%s', PW: '%s'\n", config.ssid, config.pw);
|
||||
WiFi.disconnect(); // Stop Wi-Fi connection attempt
|
||||
// delay(50);
|
||||
// WiFi.mode(WIFI_OFF);
|
||||
// delay(100);
|
||||
// WiFi.mode(WIFI_STA);
|
||||
// delay(50);
|
||||
wl_status_t ret = WiFi.begin(config.ssid, config.pw);
|
||||
DPRINTF(" WiFi.Begin(%s,%s) returned %d\n", config.ssid, config.pw, ret);
|
||||
lastAttempt = tickMillis;
|
||||
|
||||
ledcWrite(PIN_LED_WIFI, PWM_FULL * 4 / 5); // Light Blink
|
||||
lastAttempt = tickMillis;
|
||||
bConnecting = true;
|
||||
}
|
||||
}
|
||||
13
ConnectWiFi.h
Normal file
13
ConnectWiFi.h
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
#ifndef __CONNECT_WIFI_H
|
||||
#define __CONNECT_WIFI_H
|
||||
#ifdef ESP8266
|
||||
#include <ESP8266WiFi.h>
|
||||
#else
|
||||
#include <WiFi.h>
|
||||
#endif
|
||||
|
||||
void checkWiFi(unsigned long tick);
|
||||
void WiFiEvent(WiFiEvent_t event);
|
||||
inline bool isWiFiConnected() { return WiFi.status() == WL_CONNECTED; };
|
||||
#endif
|
||||
|
||||
291
HCUpdate.h
Normal file
291
HCUpdate.h
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
#if defined(ESP32)
|
||||
#ifndef ESP32UPDATER_H
|
||||
#define ESP32UPDATER_H
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <MD5Builder.h>
|
||||
#include <functional>
|
||||
#include <WiFiClientSecure.h>
|
||||
#include "esp_partition.h"
|
||||
|
||||
#define UPDATE_ERROR_OK (0)
|
||||
#define UPDATE_ERROR_WRITE (1)
|
||||
#define UPDATE_ERROR_ERASE (2)
|
||||
#define UPDATE_ERROR_READ (3)
|
||||
#define UPDATE_ERROR_SPACE (4)
|
||||
#define UPDATE_ERROR_SIZE (5)
|
||||
#define UPDATE_ERROR_STREAM (6)
|
||||
#define UPDATE_ERROR_MD5 (7)
|
||||
#define UPDATE_ERROR_MAGIC_BYTE (8)
|
||||
#define UPDATE_ERROR_ACTIVATE (9)
|
||||
#define UPDATE_ERROR_NO_PARTITION (10)
|
||||
#define UPDATE_ERROR_BAD_ARGUMENT (11)
|
||||
#define UPDATE_ERROR_ABORT (12)
|
||||
#define UPDATE_ERROR_DECRYPT (13)
|
||||
|
||||
#define UPDATE_SIZE_UNKNOWN 0xFFFFFFFF
|
||||
|
||||
#define U_FLASH 0
|
||||
#define U_SPIFFS 100
|
||||
#define U_AUTH 200
|
||||
|
||||
#define ENCRYPTED_BLOCK_SIZE 16
|
||||
#define ENCRYPTED_TWEAK_BLOCK_SIZE 32
|
||||
#define ENCRYPTED_KEY_SIZE 32
|
||||
|
||||
#define U_AES_DECRYPT_NONE 0
|
||||
#define U_AES_DECRYPT_AUTO 1
|
||||
#define U_AES_DECRYPT_ON 2
|
||||
#define U_AES_DECRYPT_MODE_MASK 3
|
||||
#define U_AES_IMAGE_DECRYPTING_BIT 4
|
||||
|
||||
#define SPI_SECTORS_PER_BLOCK 16 // usually large erase block is 32k/64k
|
||||
#define SPI_FLASH_BLOCK_SIZE (SPI_SECTORS_PER_BLOCK * SPI_FLASH_SEC_SIZE)
|
||||
|
||||
enum HTTPUpdateResult {
|
||||
HTTP_UPDATE_FAILED,
|
||||
HTTP_UPDATE_NO_UPDATES,
|
||||
HTTP_UPDATE_OK
|
||||
};
|
||||
|
||||
class UpdateClass {
|
||||
public:
|
||||
typedef std::function<void(size_t, size_t)> THandlerFunction_Progress;
|
||||
|
||||
UpdateClass();
|
||||
UpdateClass(int httpClientTimeout);
|
||||
|
||||
int update(WiFiClientSecure& client, String &url, uint16_t port, String& uri,
|
||||
String ¤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<typename T> size_t write(T &data) {
|
||||
size_t written = 0;
|
||||
if (hasError() || !isRunning()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t available = data.available();
|
||||
while (available) {
|
||||
if (_bufferLen + available > remaining()) {
|
||||
available = remaining() - _bufferLen;
|
||||
}
|
||||
if (_bufferLen + available > 4096) {
|
||||
size_t toBuff = 4096 - _bufferLen;
|
||||
data.read(_buffer + _bufferLen, toBuff);
|
||||
_bufferLen += toBuff;
|
||||
if (!_writeBuffer()) {
|
||||
return written;
|
||||
}
|
||||
written += toBuff;
|
||||
} else {
|
||||
data.read(_buffer + _bufferLen, available);
|
||||
_bufferLen += available;
|
||||
written += available;
|
||||
if (_bufferLen == remaining()) {
|
||||
if (!_writeBuffer()) {
|
||||
return written;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (remaining() == 0) {
|
||||
return written;
|
||||
}
|
||||
available = data.available();
|
||||
}
|
||||
return written;
|
||||
}
|
||||
|
||||
/*
|
||||
check if there is a firmware on the other OTA partition that you can bootinto
|
||||
*/
|
||||
bool canRollBack();
|
||||
/*
|
||||
set the other OTA partition as bootable (reboot to enable)
|
||||
*/
|
||||
bool rollBack();
|
||||
|
||||
private:
|
||||
void _reset();
|
||||
void _abort(uint8_t err);
|
||||
void _cryptKeyTweak(size_t cryptAddress, uint8_t *tweaked_key);
|
||||
bool _decryptBuffer();
|
||||
bool _writeBuffer();
|
||||
bool _verifyHeader(uint8_t data);
|
||||
bool _verifyEnd();
|
||||
bool _enablePartition(const esp_partition_t *partition);
|
||||
bool _chkDataInBlock(const uint8_t *data, size_t len) const; // check if block contains any data or is empty
|
||||
|
||||
int _httpClientTimeout;
|
||||
followRedirects_t _followRedirects = HTTPC_DISABLE_FOLLOW_REDIRECTS;
|
||||
uint8_t _error;
|
||||
uint8_t *_cryptKey;
|
||||
uint8_t *_cryptBuffer;
|
||||
uint8_t *_buffer;
|
||||
uint8_t *_skipBuffer;
|
||||
size_t _bufferLen;
|
||||
size_t _size;
|
||||
THandlerFunction_Progress _progress_callback;
|
||||
uint32_t _progress;
|
||||
uint32_t _paroffset;
|
||||
uint32_t _command;
|
||||
const esp_partition_t *_partition;
|
||||
|
||||
String _target_md5;
|
||||
MD5Builder _md5;
|
||||
|
||||
int _ledPin;
|
||||
uint8_t _ledOn;
|
||||
|
||||
uint8_t _cryptMode;
|
||||
size_t _cryptAddress;
|
||||
uint8_t _cryptCfg;
|
||||
};
|
||||
|
||||
#if !defined(NO_GLOBAL_INSTANCES) && !defined(NO_GLOBAL_UPDATE)
|
||||
extern UpdateClass Update;
|
||||
#endif
|
||||
|
||||
#endif // defined(ESP32UPDATER_H)
|
||||
#endif // defined(ESP32)
|
||||
787
HCUpdater.cpp
Normal file
787
HCUpdater.cpp
Normal file
|
|
@ -0,0 +1,787 @@
|
|||
#if defined(ESP32)
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
#include "HermitCrab.h"
|
||||
|
||||
#include "Arduino.h"
|
||||
#include "spi_flash_mmap.h"
|
||||
#include "esp_ota_ops.h"
|
||||
#include "esp_image_format.h"
|
||||
#include "mbedtls/aes.h"
|
||||
#include <WiFi.h>
|
||||
#include <WiFiClient.h>
|
||||
#include <HTTPClient.h>
|
||||
#include "HCUpdate.h"
|
||||
|
||||
#define TAG_FW "FW Upate"
|
||||
|
||||
static const char *_err2str(uint8_t _error) {
|
||||
if (_error == UPDATE_ERROR_OK) {
|
||||
return ("No Error");
|
||||
} else if (_error == UPDATE_ERROR_WRITE) {
|
||||
return ("Flash Write Failed");
|
||||
} else if (_error == UPDATE_ERROR_ERASE) {
|
||||
return ("Flash Erase Failed");
|
||||
} else if (_error == UPDATE_ERROR_READ) {
|
||||
return ("Flash Read Failed");
|
||||
} else if (_error == UPDATE_ERROR_SPACE) {
|
||||
return ("Not Enough Space");
|
||||
} else if (_error == UPDATE_ERROR_SIZE) {
|
||||
return ("Bad Size Given");
|
||||
} else if (_error == UPDATE_ERROR_STREAM) {
|
||||
return ("Stream Read Timeout");
|
||||
} else if (_error == UPDATE_ERROR_MD5) {
|
||||
return ("MD5 Check Failed");
|
||||
} else if (_error == UPDATE_ERROR_MAGIC_BYTE) {
|
||||
return ("Wrong Magic Byte");
|
||||
} else if (_error == UPDATE_ERROR_ACTIVATE) {
|
||||
return ("Could Not Activate The Firmware");
|
||||
} else if (_error == UPDATE_ERROR_NO_PARTITION) {
|
||||
return ("Partition Could Not be Found");
|
||||
} else if (_error == UPDATE_ERROR_BAD_ARGUMENT) {
|
||||
return ("Bad Argument");
|
||||
} else if (_error == UPDATE_ERROR_ABORT) {
|
||||
return ("Aborted");
|
||||
} else if (_error == UPDATE_ERROR_DECRYPT) {
|
||||
return ("Decryption error");
|
||||
}
|
||||
return ("UNKNOWN");
|
||||
}
|
||||
|
||||
int UpdateClass::update(WiFiClientSecure& client, String& host, uint16_t port, String&uri, String& currentVersion, short nDeviceType, bool bForceUpdate)
|
||||
{
|
||||
HTTPClient http;
|
||||
if (!http.begin(client, host, port, uri)) {
|
||||
ESP_LOGI(TAG_FW,"OTA - httpClient begin failed\n");
|
||||
return HTTP_UPDATE_FAILED;
|
||||
}
|
||||
int size = handleUpdate(http, currentVersion, nDeviceType);
|
||||
|
||||
ESP_LOGI(TAG_FW,"OTA - size(%d) Server Version: %s Mode: %s\n", size, http.header("version").c_str(),
|
||||
http.header("update") && http.header("update").toInt() == 1 ? "Download" : "Check Only" );
|
||||
|
||||
//is there an image to download
|
||||
if (size >= 0) {
|
||||
if (!http.header("update") || http.header("update").toInt() == 0) {
|
||||
ESP_LOGI(TAG_FW,"OTA - No Firmware available");
|
||||
} else if (!http.header("version") || strcmp(http.header("version").c_str(), HC__VERSION) <= 0) {
|
||||
ESP_LOGI(TAG_FW,"OTA - Firmware is upto Date");
|
||||
} else {
|
||||
//image avaliabe to download & update
|
||||
if (!bForceUpdate) {
|
||||
ESP_LOGI(TAG_FW,"OTA - Found V%s Firmware\n", http.header("version").c_str());
|
||||
} else {
|
||||
ESP_LOGI(TAG_FW,"OTA - Downloading & Installing V%s Firmware\n", http.header("version").c_str());
|
||||
}
|
||||
|
||||
if (bForceUpdate) {
|
||||
if (http_downloadUpdate(http) == 0) {
|
||||
http.end(); //end connection
|
||||
ESP_LOGI(TAG_FW,"OTA - Firmware Update Successful, rebooting");
|
||||
ESP.restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
http.end(); //end connection
|
||||
return 0;
|
||||
}
|
||||
|
||||
int UpdateClass::handleUpdate(HTTPClient& http, const String& currentVersion, short nDeviceType)
|
||||
{
|
||||
HTTPUpdateResult ret = HTTP_UPDATE_FAILED;
|
||||
|
||||
// use HTTP/1.0 for update since the update handler not support any transfer Encoding
|
||||
http.useHTTP10(true);
|
||||
http.setTimeout(_httpClientTimeout);
|
||||
http.setFollowRedirects(_followRedirects);
|
||||
http.setUserAgent("ESP32-http-Update");
|
||||
http.addHeader("Cache-Control", "no-cache");
|
||||
http.addHeader("X-ESP32-DEVICE-TYPE", String(nDeviceType));
|
||||
http.addHeader("X-ESP32-VERSION", HC__VERSION);
|
||||
http.addHeader("X-ESP32-STA-MAC", WiFi.macAddress());
|
||||
|
||||
unsigned long nChipId = 0;
|
||||
for (int i = 0; i < 17; i = i + 8) {
|
||||
nChipId |= ((ESP.getEfuseMac() >> (40 - i)) & 0xff) << i;
|
||||
}
|
||||
http.addHeader(F("X-ESP32-Chip-ID"), String(nChipId));
|
||||
http.addHeader(F("x-ESP32-free-space"), String(ESP.getFreeSketchSpace()));
|
||||
http.addHeader(F("x-ESP32-sketch-size"), String(ESP.getSketchSize()));
|
||||
http.addHeader(F("x-ESP32-sketch-md5"), String(ESP.getSketchMD5()));
|
||||
//http.addHeader(F("x-ESP32-chip-size"), String(ESP.getFlashChipRealSize()));
|
||||
http.addHeader(F("x-ESP32-sdk-version"), ESP.getSdkVersion());
|
||||
|
||||
//set headers to look for to get returned values in servers http response to our http request
|
||||
const char *headerkeys[] = {"update", "version"}; //server returns update 0=no update found, 1=update found, version=version of update found
|
||||
size_t headerkeyssize = sizeof(headerkeys) / sizeof(char *);
|
||||
http.collectHeaders(headerkeys, headerkeyssize);
|
||||
|
||||
int code = http.GET();
|
||||
int len = http.getSize();
|
||||
|
||||
if (code == HTTP_CODE_OK) {
|
||||
return (len > 0 ? len : 0); //return 0 or length of image to download
|
||||
} else if (code < 0) {
|
||||
ESP_LOGI(TAG_FW,"Error: %s\n", http.errorToString(code).c_str());
|
||||
ESP_LOGI(TAG_FW, "%s", http.getString());
|
||||
return code; //error code should be minus between -1 to -11
|
||||
} else {
|
||||
ESP_LOGI(TAG_FW,"Error: HTTP Server response code %i\n", code);
|
||||
ESP_LOGI(TAG_FW, "%s", http.getString());
|
||||
return -code; //return code should be minus between -100 to -511
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
int UpdateClass::http_downloadUpdate(HTTPClient &httpClient) {
|
||||
WiFiClient *stream = httpClient.getStreamPtr();
|
||||
int written = 0;
|
||||
|
||||
// Check content length and begin update
|
||||
int fileSize = httpClient.getSize();
|
||||
if (fileSize <= 0) {
|
||||
ESP_LOGI(TAG_FW,"Invalid content length");
|
||||
return 1;
|
||||
}
|
||||
if (!Update.begin(fileSize, U_FLASH)) {
|
||||
ESP_LOGI(TAG_FW,"Update begin failed!");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Download and write the update
|
||||
while (httpClient.connected() && written < fileSize) {
|
||||
size_t availableBytes = stream->available();
|
||||
if (availableBytes) {
|
||||
uint8_t buffer[128]; // Buffer to hold incoming data
|
||||
int bytesRead = stream->readBytes(buffer, min(availableBytes, sizeof(buffer)));
|
||||
int bytesWritten = Update.write(buffer, bytesRead);
|
||||
if (bytesWritten != bytesRead) {
|
||||
ESP_LOGI(TAG_FW,"Update write failed!");
|
||||
Update.end();
|
||||
return 1;
|
||||
}
|
||||
written += bytesWritten;
|
||||
}
|
||||
}
|
||||
|
||||
// Finalize update
|
||||
if (Update.end()) {
|
||||
if (Update.isFinished()) {
|
||||
ESP_LOGI(TAG_FW,"Update successful!");
|
||||
return 0;
|
||||
} else {
|
||||
ESP_LOGI(TAG_FW,"Update not finished!");
|
||||
return 1;
|
||||
}
|
||||
} else {
|
||||
ESP_LOGI(TAG_FW,"Update end failed!");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static bool _partitionIsBootable(const esp_partition_t *partition) {
|
||||
uint8_t buf[ENCRYPTED_BLOCK_SIZE];
|
||||
if (!partition) {
|
||||
return false;
|
||||
}
|
||||
if (!ESP.partitionRead(partition, 0, (uint32_t *)buf, ENCRYPTED_BLOCK_SIZE)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (buf[0] != ESP_IMAGE_HEADER_MAGIC) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UpdateClass::_enablePartition(const esp_partition_t *partition) {
|
||||
if (!partition) {
|
||||
return false;
|
||||
}
|
||||
return ESP.partitionWrite(partition, 0, (uint32_t *)_skipBuffer, ENCRYPTED_BLOCK_SIZE);
|
||||
}
|
||||
|
||||
UpdateClass::UpdateClass()
|
||||
: _error(0)
|
||||
, _cryptKey(0)
|
||||
, _cryptBuffer(0)
|
||||
, _buffer(0)
|
||||
, _skipBuffer(0)
|
||||
, _bufferLen(0)
|
||||
, _size(0)
|
||||
, _progress_callback(NULL)
|
||||
, _progress(0)
|
||||
, _paroffset(0)
|
||||
, _command(U_FLASH)
|
||||
, _partition(NULL)
|
||||
, _cryptMode(U_AES_DECRYPT_AUTO)
|
||||
, _cryptAddress(0)
|
||||
, _cryptCfg(0xf)
|
||||
, _httpClientTimeout(8000)
|
||||
{}
|
||||
|
||||
UpdateClass::UpdateClass(int httpClientTimeout)
|
||||
: _error(0)
|
||||
, _cryptKey(0)
|
||||
, _cryptBuffer(0)
|
||||
, _buffer(0)
|
||||
, _skipBuffer(0)
|
||||
, _bufferLen(0)
|
||||
, _size(0)
|
||||
, _progress_callback(NULL)
|
||||
, _progress(0)
|
||||
, _paroffset(0)
|
||||
, _command(U_FLASH)
|
||||
, _partition(NULL)
|
||||
, _cryptMode(U_AES_DECRYPT_AUTO)
|
||||
, _cryptAddress(0)
|
||||
, _cryptCfg(0xf)
|
||||
, _httpClientTimeout(httpClientTimeout)
|
||||
{}
|
||||
|
||||
UpdateClass &UpdateClass::onProgress(THandlerFunction_Progress fn) {
|
||||
_progress_callback = fn;
|
||||
return *this;
|
||||
}
|
||||
|
||||
void UpdateClass::_reset() {
|
||||
if (_buffer) {
|
||||
delete[] _buffer;
|
||||
}
|
||||
if (_skipBuffer) {
|
||||
delete[] _skipBuffer;
|
||||
}
|
||||
|
||||
_cryptBuffer = nullptr;
|
||||
_buffer = nullptr;
|
||||
_skipBuffer = nullptr;
|
||||
_bufferLen = 0;
|
||||
_progress = 0;
|
||||
_size = 0;
|
||||
_command = U_FLASH;
|
||||
|
||||
if (_ledPin != -1) {
|
||||
digitalWrite(_ledPin, !_ledOn); // off
|
||||
}
|
||||
}
|
||||
|
||||
bool UpdateClass::canRollBack() {
|
||||
if (_buffer) { //Update is running
|
||||
return false;
|
||||
}
|
||||
const esp_partition_t *partition = esp_ota_get_next_update_partition(NULL);
|
||||
return _partitionIsBootable(partition);
|
||||
}
|
||||
|
||||
bool UpdateClass::rollBack() {
|
||||
if (_buffer) { //Update is running
|
||||
return false;
|
||||
}
|
||||
const esp_partition_t *partition = esp_ota_get_next_update_partition(NULL);
|
||||
return _partitionIsBootable(partition) && !esp_ota_set_boot_partition(partition);
|
||||
}
|
||||
|
||||
bool UpdateClass::begin(size_t size, int command, int ledPin, uint8_t ledOn, const char *label) {
|
||||
if (_size > 0) {
|
||||
log_w("already running");
|
||||
return false;
|
||||
}
|
||||
|
||||
_ledPin = ledPin;
|
||||
_ledOn = !!ledOn; // 0(LOW) or 1(HIGH)
|
||||
|
||||
_reset();
|
||||
_error = 0;
|
||||
_target_md5 = emptyString;
|
||||
_md5 = MD5Builder();
|
||||
|
||||
if (size == 0) {
|
||||
_error = UPDATE_ERROR_SIZE;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (command == U_FLASH) {
|
||||
_partition = esp_ota_get_next_update_partition(NULL);
|
||||
if (!_partition) {
|
||||
_error = UPDATE_ERROR_NO_PARTITION;
|
||||
return false;
|
||||
}
|
||||
log_d("OTA Partition: %s", _partition->label);
|
||||
} else if (command == U_SPIFFS) {
|
||||
_partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_SPIFFS, label);
|
||||
_paroffset = 0;
|
||||
if (!_partition) {
|
||||
_partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_FAT, NULL);
|
||||
_paroffset = 0x1000; //Offset for ffat, assuming size is already corrected
|
||||
if (!_partition) {
|
||||
_error = UPDATE_ERROR_NO_PARTITION;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_error = UPDATE_ERROR_BAD_ARGUMENT;
|
||||
log_e("bad command %u", command);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (size == UPDATE_SIZE_UNKNOWN) {
|
||||
size = _partition->size;
|
||||
} else if (size > _partition->size) {
|
||||
_error = UPDATE_ERROR_SIZE;
|
||||
log_e("too large %u > %u", size, _partition->size);
|
||||
return false;
|
||||
}
|
||||
|
||||
//initialize
|
||||
_buffer = new (std::nothrow) uint8_t[SPI_FLASH_SEC_SIZE];
|
||||
if (!_buffer) {
|
||||
log_e("_buffer allocation failed");
|
||||
return false;
|
||||
}
|
||||
_size = size;
|
||||
_command = command;
|
||||
_md5.begin();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UpdateClass::setupCrypt(const uint8_t *cryptKey, size_t cryptAddress, uint8_t cryptConfig, int cryptMode) {
|
||||
if (setCryptKey(cryptKey)) {
|
||||
if (setCryptMode(cryptMode)) {
|
||||
setCryptAddress(cryptAddress);
|
||||
setCryptConfig(cryptConfig);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UpdateClass::setCryptKey(const uint8_t *cryptKey) {
|
||||
if (!cryptKey) {
|
||||
if (_cryptKey) {
|
||||
delete[] _cryptKey;
|
||||
_cryptKey = 0;
|
||||
log_d("AES key unset");
|
||||
}
|
||||
return false; //key cleared, no key to decrypt with
|
||||
}
|
||||
//initialize
|
||||
if (!_cryptKey) {
|
||||
_cryptKey = new (std::nothrow) uint8_t[ENCRYPTED_KEY_SIZE];
|
||||
}
|
||||
if (!_cryptKey) {
|
||||
log_e("new failed");
|
||||
return false;
|
||||
}
|
||||
memcpy(_cryptKey, cryptKey, ENCRYPTED_KEY_SIZE);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UpdateClass::setCryptMode(const int cryptMode) {
|
||||
if (cryptMode >= U_AES_DECRYPT_NONE && cryptMode <= U_AES_DECRYPT_ON) {
|
||||
_cryptMode = cryptMode;
|
||||
} else {
|
||||
log_e("bad crypt mode argument %i", cryptMode);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void UpdateClass::_abort(uint8_t err) {
|
||||
_reset();
|
||||
_error = err;
|
||||
}
|
||||
|
||||
void UpdateClass::abort() {
|
||||
_abort(UPDATE_ERROR_ABORT);
|
||||
}
|
||||
|
||||
void UpdateClass::_cryptKeyTweak(size_t cryptAddress, uint8_t *tweaked_key) {
|
||||
memcpy(tweaked_key, _cryptKey, ENCRYPTED_KEY_SIZE);
|
||||
if (_cryptCfg == 0) {
|
||||
return; //no tweaking needed, use crypt key as-is
|
||||
}
|
||||
|
||||
const uint8_t pattern[] = {23, 23, 23, 14, 23, 23, 23, 12, 23, 23, 23, 10, 23, 23, 23, 8};
|
||||
int pattern_idx = 0;
|
||||
int key_idx = 0;
|
||||
int bit_len = 0;
|
||||
uint32_t tweak = 0;
|
||||
cryptAddress &= 0x00ffffe0; //bit 23-5
|
||||
cryptAddress <<= 8; //bit23 shifted to bit31(MSB)
|
||||
while (pattern_idx < sizeof(pattern)) {
|
||||
tweak = cryptAddress << (23 - pattern[pattern_idx]); //bit shift for small patterns
|
||||
// alternative to: tweak = rotl32(tweak,8 - bit_len);
|
||||
tweak = (tweak << (8 - bit_len)) | (tweak >> (24 + bit_len)); //rotate to line up with end of previous tweak bits
|
||||
bit_len += pattern[pattern_idx++] - 4; //add number of bits in next pattern(23-4 = 19bits = 23bit to 5bit)
|
||||
while (bit_len > 7) {
|
||||
tweaked_key[key_idx++] ^= tweak; //XOR byte
|
||||
// alternative to: tweak = rotl32(tweak, 8);
|
||||
tweak = (tweak << 8) | (tweak >> 24); //compiler should optimize to use rotate(fast)
|
||||
bit_len -= 8;
|
||||
}
|
||||
tweaked_key[key_idx] ^= tweak; //XOR remaining bits, will XOR zeros if no remaining bits
|
||||
}
|
||||
if (_cryptCfg == 0xf) {
|
||||
return; //return with fully tweaked key
|
||||
}
|
||||
|
||||
//some of tweaked key bits need to be restore back to crypt key bits
|
||||
const uint8_t cfg_bits[] = {67, 65, 63, 61};
|
||||
key_idx = 0;
|
||||
pattern_idx = 0;
|
||||
while (key_idx < ENCRYPTED_KEY_SIZE) {
|
||||
bit_len += cfg_bits[pattern_idx];
|
||||
if ((_cryptCfg & (1 << pattern_idx)) == 0) { //restore crypt key bits
|
||||
while (bit_len > 0) {
|
||||
if (bit_len > 7 || ((_cryptCfg & (2 << pattern_idx)) == 0)) { //restore a crypt key byte
|
||||
tweaked_key[key_idx] = _cryptKey[key_idx];
|
||||
} else { //MSBits restore crypt key bits, LSBits keep as tweaked bits
|
||||
tweaked_key[key_idx] &= (0xff >> bit_len);
|
||||
tweaked_key[key_idx] |= (_cryptKey[key_idx] & (~(0xff >> bit_len)));
|
||||
}
|
||||
key_idx++;
|
||||
bit_len -= 8;
|
||||
}
|
||||
} else { //keep tweaked key bits
|
||||
while (bit_len > 0) {
|
||||
if (bit_len < 8 && ((_cryptCfg & (2 << pattern_idx)) == 0)) { //MSBits keep as tweaked bits, LSBits restore crypt key bits
|
||||
tweaked_key[key_idx] &= (~(0xff >> bit_len));
|
||||
tweaked_key[key_idx] |= (_cryptKey[key_idx] & (0xff >> bit_len));
|
||||
}
|
||||
key_idx++;
|
||||
bit_len -= 8;
|
||||
}
|
||||
}
|
||||
pattern_idx++;
|
||||
}
|
||||
}
|
||||
|
||||
bool UpdateClass::_decryptBuffer() {
|
||||
if (!_cryptKey) {
|
||||
log_w("AES key not set");
|
||||
return false;
|
||||
}
|
||||
if (_bufferLen % ENCRYPTED_BLOCK_SIZE != 0) {
|
||||
log_e("buffer size error");
|
||||
return false;
|
||||
}
|
||||
if (!_cryptBuffer) {
|
||||
_cryptBuffer = new (std::nothrow) uint8_t[ENCRYPTED_BLOCK_SIZE];
|
||||
}
|
||||
if (!_cryptBuffer) {
|
||||
log_e("new failed");
|
||||
return false;
|
||||
}
|
||||
uint8_t tweaked_key[ENCRYPTED_KEY_SIZE]; //tweaked crypt key
|
||||
int done = 0;
|
||||
|
||||
/*
|
||||
Mbedtls functions will be replaced with esp_aes functions when hardware acceleration is available
|
||||
|
||||
To Do:
|
||||
Replace mbedtls for the cases where there's no hardware acceleration
|
||||
*/
|
||||
|
||||
mbedtls_aes_context ctx; //initialize AES
|
||||
mbedtls_aes_init(&ctx);
|
||||
while ((_bufferLen - done) >= ENCRYPTED_BLOCK_SIZE) {
|
||||
for (int i = 0; i < ENCRYPTED_BLOCK_SIZE; i++) {
|
||||
_cryptBuffer[(ENCRYPTED_BLOCK_SIZE - 1) - i] = _buffer[i + done]; //reverse order 16 bytes to decrypt
|
||||
}
|
||||
if (((_cryptAddress + _progress + done) % ENCRYPTED_TWEAK_BLOCK_SIZE) == 0 || done == 0) {
|
||||
_cryptKeyTweak(_cryptAddress + _progress + done, tweaked_key); //update tweaked crypt key
|
||||
if (mbedtls_aes_setkey_enc(&ctx, tweaked_key, 256)) {
|
||||
return false;
|
||||
}
|
||||
if (mbedtls_aes_setkey_dec(&ctx, tweaked_key, 256)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (mbedtls_aes_crypt_ecb(&ctx, MBEDTLS_AES_ENCRYPT, _cryptBuffer, _cryptBuffer)) { //use MBEDTLS_AES_ENCRYPT to decrypt flash code
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < ENCRYPTED_BLOCK_SIZE; i++) {
|
||||
_buffer[i + done] = _cryptBuffer[(ENCRYPTED_BLOCK_SIZE - 1) - i]; //reverse order 16 bytes from decrypt
|
||||
}
|
||||
done += ENCRYPTED_BLOCK_SIZE;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UpdateClass::_writeBuffer() {
|
||||
//first bytes of loading image, check to see if loading image needs decrypting
|
||||
if (!_progress) {
|
||||
_cryptMode &= U_AES_DECRYPT_MODE_MASK;
|
||||
if ((_cryptMode == U_AES_DECRYPT_ON) || ((_command == U_FLASH) && (_cryptMode & U_AES_DECRYPT_AUTO) && (_buffer[0] != ESP_IMAGE_HEADER_MAGIC))) {
|
||||
_cryptMode |= U_AES_IMAGE_DECRYPTING_BIT; //set to decrypt the loading image
|
||||
log_d("Decrypting OTA Image");
|
||||
}
|
||||
}
|
||||
//check if data in buffer needs decrypting
|
||||
if (_cryptMode & U_AES_IMAGE_DECRYPTING_BIT) {
|
||||
if (!_decryptBuffer()) {
|
||||
_abort(UPDATE_ERROR_DECRYPT);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
//first bytes of new firmware
|
||||
uint8_t skip = 0;
|
||||
if (!_progress && _command == U_FLASH) {
|
||||
//check magic
|
||||
if (_buffer[0] != ESP_IMAGE_HEADER_MAGIC) {
|
||||
_abort(UPDATE_ERROR_MAGIC_BYTE);
|
||||
return false;
|
||||
}
|
||||
|
||||
//Stash the first 16 bytes of data and set the offset so they are
|
||||
//not written at this point so that partially written firmware
|
||||
//will not be bootable
|
||||
skip = ENCRYPTED_BLOCK_SIZE;
|
||||
_skipBuffer = new (std::nothrow) uint8_t[skip];
|
||||
if (!_skipBuffer) {
|
||||
log_e("_skipBuffer allocation failed");
|
||||
return false;
|
||||
}
|
||||
memcpy(_skipBuffer, _buffer, skip);
|
||||
}
|
||||
if (!_progress && _progress_callback) {
|
||||
_progress_callback(0, _size);
|
||||
}
|
||||
size_t offset = _partition->address + _progress;
|
||||
bool block_erase =
|
||||
(_size - _progress >= SPI_FLASH_BLOCK_SIZE) && (offset % SPI_FLASH_BLOCK_SIZE == 0); // if it's the block boundary, than erase the whole block from here
|
||||
bool part_head_sectors =
|
||||
_partition->address % SPI_FLASH_BLOCK_SIZE
|
||||
&& offset < (_partition->address / SPI_FLASH_BLOCK_SIZE + 1) * SPI_FLASH_BLOCK_SIZE; // sector belong to unaligned partition heading block
|
||||
bool part_tail_sectors =
|
||||
offset >= (_partition->address + _size) / SPI_FLASH_BLOCK_SIZE * SPI_FLASH_BLOCK_SIZE; // sector belong to unaligned partition tailing block
|
||||
if (block_erase || part_head_sectors || part_tail_sectors) {
|
||||
if (!ESP.partitionEraseRange(_partition, _progress, block_erase ? SPI_FLASH_BLOCK_SIZE : SPI_FLASH_SEC_SIZE)) {
|
||||
_abort(UPDATE_ERROR_ERASE);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// try to skip empty blocks on unencrypted partitions
|
||||
if ((_partition->encrypted || _chkDataInBlock(_buffer + skip / sizeof(uint32_t), _bufferLen - skip))
|
||||
&& !ESP.partitionWrite(_partition, _progress + skip, (uint32_t *)_buffer + skip / sizeof(uint32_t), _bufferLen - skip)) {
|
||||
_abort(UPDATE_ERROR_WRITE);
|
||||
return false;
|
||||
}
|
||||
|
||||
//restore magic or md5 will fail
|
||||
if (!_progress && _command == U_FLASH) {
|
||||
_buffer[0] = ESP_IMAGE_HEADER_MAGIC;
|
||||
}
|
||||
_md5.add(_buffer, _bufferLen);
|
||||
_progress += _bufferLen;
|
||||
_bufferLen = 0;
|
||||
if (_progress_callback) {
|
||||
_progress_callback(_progress, _size);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UpdateClass::_verifyHeader(uint8_t data) {
|
||||
if (_command == U_FLASH) {
|
||||
if (data != ESP_IMAGE_HEADER_MAGIC) {
|
||||
_abort(UPDATE_ERROR_MAGIC_BYTE);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} else if (_command == U_SPIFFS) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UpdateClass::_verifyEnd() {
|
||||
if (_command == U_FLASH) {
|
||||
if (!_enablePartition(_partition) || !_partitionIsBootable(_partition)) {
|
||||
_abort(UPDATE_ERROR_READ);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (esp_ota_set_boot_partition(_partition)) {
|
||||
_abort(UPDATE_ERROR_ACTIVATE);
|
||||
return false;
|
||||
}
|
||||
_reset();
|
||||
return true;
|
||||
} else if (_command == U_SPIFFS) {
|
||||
_reset();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UpdateClass::setMD5(const char *expected_md5) {
|
||||
if (strlen(expected_md5) != 32) {
|
||||
return false;
|
||||
}
|
||||
_target_md5 = expected_md5;
|
||||
_target_md5.toLowerCase();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UpdateClass::end(bool evenIfRemaining) {
|
||||
if (hasError() || _size == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isFinished() && !evenIfRemaining) {
|
||||
log_e("premature end: res:%u, pos:%u/%u\n", getError(), progress(), _size);
|
||||
_abort(UPDATE_ERROR_ABORT);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (evenIfRemaining) {
|
||||
if (_bufferLen > 0) {
|
||||
_writeBuffer();
|
||||
}
|
||||
_size = progress();
|
||||
}
|
||||
|
||||
_md5.calculate();
|
||||
if (_target_md5.length()) {
|
||||
if (_target_md5 != _md5.toString()) {
|
||||
_abort(UPDATE_ERROR_MD5);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return _verifyEnd();
|
||||
}
|
||||
|
||||
size_t UpdateClass::write(uint8_t *data, size_t len) {
|
||||
if (hasError() || !isRunning()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (len > remaining()) {
|
||||
_abort(UPDATE_ERROR_SPACE);
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t left = len;
|
||||
|
||||
while ((_bufferLen + left) > SPI_FLASH_SEC_SIZE) {
|
||||
size_t toBuff = SPI_FLASH_SEC_SIZE - _bufferLen;
|
||||
memcpy(_buffer + _bufferLen, data + (len - left), toBuff);
|
||||
_bufferLen += toBuff;
|
||||
if (!_writeBuffer()) {
|
||||
return len - left;
|
||||
}
|
||||
left -= toBuff;
|
||||
}
|
||||
memcpy(_buffer + _bufferLen, data + (len - left), left);
|
||||
_bufferLen += left;
|
||||
if (_bufferLen == remaining()) {
|
||||
if (!_writeBuffer()) {
|
||||
return len - left;
|
||||
}
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
size_t UpdateClass::writeStream(Stream &data) {
|
||||
size_t written = 0;
|
||||
size_t toRead = 0;
|
||||
int timeout_failures = 0;
|
||||
|
||||
if (hasError() || !isRunning()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (_command == U_FLASH && !_cryptMode) {
|
||||
if (!_verifyHeader(data.peek())) {
|
||||
_reset();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (_ledPin != -1) {
|
||||
pinMode(_ledPin, OUTPUT);
|
||||
}
|
||||
|
||||
while (remaining()) {
|
||||
if (_ledPin != -1) {
|
||||
digitalWrite(_ledPin, _ledOn); // Switch LED on
|
||||
}
|
||||
size_t bytesToRead = SPI_FLASH_SEC_SIZE - _bufferLen;
|
||||
if (bytesToRead > remaining()) {
|
||||
bytesToRead = remaining();
|
||||
}
|
||||
|
||||
/*
|
||||
Init read&timeout counters and try to read, if read failed, increase counter,
|
||||
wait 100ms and try to read again. If counter > 300 (30 sec), give up/abort
|
||||
*/
|
||||
toRead = 0;
|
||||
timeout_failures = 0;
|
||||
while (!toRead) {
|
||||
toRead = data.readBytes(_buffer + _bufferLen, bytesToRead);
|
||||
if (toRead == 0) {
|
||||
timeout_failures++;
|
||||
if (timeout_failures >= 300) {
|
||||
_abort(UPDATE_ERROR_STREAM);
|
||||
return written;
|
||||
}
|
||||
delay(100);
|
||||
}
|
||||
}
|
||||
|
||||
if (_ledPin != -1) {
|
||||
digitalWrite(_ledPin, !_ledOn); // Switch LED off
|
||||
}
|
||||
_bufferLen += toRead;
|
||||
if ((_bufferLen == remaining() || _bufferLen == SPI_FLASH_SEC_SIZE) && !_writeBuffer()) {
|
||||
return written;
|
||||
}
|
||||
written += toRead;
|
||||
|
||||
#if CONFIG_FREERTOS_UNICORE
|
||||
delay(1); // Fix solo WDT
|
||||
#endif
|
||||
}
|
||||
return written;
|
||||
}
|
||||
|
||||
void UpdateClass::printError(Print &out) {
|
||||
out.println(_err2str(_error));
|
||||
}
|
||||
|
||||
const char *UpdateClass::errorString() {
|
||||
return _err2str(_error);
|
||||
}
|
||||
|
||||
bool UpdateClass::_chkDataInBlock(const uint8_t *data, size_t len) const {
|
||||
// check 32-bit aligned blocks only
|
||||
if (!len || len % sizeof(uint32_t)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t dwl = len / sizeof(uint32_t);
|
||||
|
||||
do {
|
||||
if (*(uint32_t *)data ^ 0xffffffff) { // for SPI NOR flash empty blocks are all one's, i.e. filled with 0xff byte
|
||||
return true;
|
||||
}
|
||||
|
||||
data += sizeof(uint32_t);
|
||||
} while (--dwl);
|
||||
return false;
|
||||
}
|
||||
|
||||
#if !defined(NO_GLOBAL_INSTANCES) && !defined(NO_GLOBAL_UPDATE)
|
||||
UpdateClass Update;
|
||||
#endif
|
||||
|
||||
#endif // defined(ESP32)
|
||||
683
HCesp.ino
Normal file
683
HCesp.ino
Normal file
|
|
@ -0,0 +1,683 @@
|
|||
#include "HermitCrab.h"
|
||||
#include "Config.h"
|
||||
#include "AHT2x.h"
|
||||
#include "NTC_10K.h"
|
||||
#include "ZCD.h"
|
||||
#include "History.h"
|
||||
#include "TimeManager.h"
|
||||
#include "ConnectWiFi.h"
|
||||
#include "WiFiHost.h"
|
||||
#include "OTA.h"
|
||||
#include "UI.h"
|
||||
#include "BLEScan.h"
|
||||
|
||||
#if defined(ESP32)
|
||||
#include "esp_wifi.h"
|
||||
#endif
|
||||
#define TAG_MAIN "Main"
|
||||
STATUS_TYPE status;
|
||||
|
||||
|
||||
// Time
|
||||
volatile unsigned short g_nYear, g_nMonth, g_nDay, g_nHour, g_nMinute, g_nSecond;
|
||||
|
||||
// Environment
|
||||
bool bShowSensor = false;
|
||||
time_t now;
|
||||
struct tm timeinfo;
|
||||
|
||||
void readSensors();
|
||||
void controlAC1(short temp, unsigned long tick);
|
||||
void controlAC2(short temp, unsigned long tick);
|
||||
void controlMist(short humid, unsigned long tick);
|
||||
void controlFan(short temp, unsigned long tick);
|
||||
void controlMotor(short hour, short min, unsigned long tick);
|
||||
void controlLight(short hour, short min, unsigned long tick);
|
||||
void controlFanDuty();
|
||||
void controlMotorDuty();
|
||||
void controlLightDuty();
|
||||
|
||||
|
||||
// ==================================================================================
|
||||
//
|
||||
// Arduino Loop - controls
|
||||
//
|
||||
// ==================================================================================
|
||||
MY_IRAM_ATTR void loop() {
|
||||
static unsigned long lastTickSecond = 0;
|
||||
static uint8_t lastSecond = -1;
|
||||
unsigned long tickMillis = millis();
|
||||
unsigned long tickSecond = tickMillis / 1000;
|
||||
|
||||
|
||||
// Un-Conditional Loop
|
||||
{
|
||||
//ESP_LOGI(TAG_MAIN,"Checking WiFi2");
|
||||
checkWiFi(tickMillis);
|
||||
|
||||
//ESP_LOGI(TAG_MAIN,"Host Loop");
|
||||
host.Loop(tickMillis);
|
||||
|
||||
// UI Button Check
|
||||
ui.loopButton(tickMillis);
|
||||
}
|
||||
|
||||
// Every Second
|
||||
if (tickSecond != lastTickSecond)
|
||||
{
|
||||
// Time and ZCD
|
||||
setZCD();
|
||||
setTime();
|
||||
|
||||
// Temperature and Humidity
|
||||
readSensors();
|
||||
//ble.loop(tickMillis);
|
||||
|
||||
// Fan, Motor, Light Duties
|
||||
controlFanDuty();
|
||||
controlMotorDuty();
|
||||
controlLightDuty();
|
||||
|
||||
// Add to History - every minutes
|
||||
if (g_nSecond == 0 && lastSecond != g_nSecond) {
|
||||
history.add(status);
|
||||
}
|
||||
lastSecond = g_nSecond;
|
||||
|
||||
// Every 10 Second
|
||||
switch(tickSecond % 10) {
|
||||
case 1: // Every 5 second - xx:xx-x7
|
||||
if (bShowSensor) {
|
||||
ESP_LOGI(TAG_MAIN, "%s\n", printStatus(tickSecond, true));
|
||||
}
|
||||
break;
|
||||
case 2: // AC1
|
||||
controlAC1(status.nTemp1, tickSecond);
|
||||
break;
|
||||
case 3: // AC2
|
||||
controlAC2(status.nTemp1, tickSecond);
|
||||
break;
|
||||
case 4: // Mist
|
||||
controlMist(status.nHumid1, tickSecond);
|
||||
break;
|
||||
case 5: // Fan Control
|
||||
controlFan(status.nTemp1, tickSecond);
|
||||
break;
|
||||
case 6: // Motor Control
|
||||
controlMotor(g_nHour, g_nMinute, tickSecond);
|
||||
break;
|
||||
case 7: // Light Control
|
||||
controlLight(g_nHour, g_nMinute, tickSecond);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
lastTickSecond = tickSecond;
|
||||
}
|
||||
yield();
|
||||
}
|
||||
// ==================================================================================
|
||||
// End of Main Loop
|
||||
// ==================================================================================
|
||||
|
||||
void readSensors() {
|
||||
switch (config.nTemp1SensorType) {
|
||||
case TEMP_SENSOR_TYPE::AHT20:
|
||||
case TEMP_SENSOR_TYPE::AHT2x:
|
||||
status.nTemp1 = (aht25.getTemperature() + 5) / 10 + config.nTemp1Offset;
|
||||
status.nHumid1 = (aht25.getHumidity() + 5) / 10 + config.nHumid1Offset;
|
||||
break;
|
||||
case TEMP_SENSOR_TYPE::AHT10_0x39:
|
||||
status.nTemp1 = (aht10_0x39.getTemperature() + 5) / 10 + config.nTemp1Offset;
|
||||
status.nHumid1 = (aht10_0x39.getHumidity() + 5) / 10 + config.nHumid1Offset;
|
||||
break;
|
||||
case TEMP_SENSOR_TYPE::NTC:
|
||||
status.nTemp1 = ntc.getTemp() + config.nTemp1Offset;
|
||||
status.nHumid1 = 0;
|
||||
break;
|
||||
case TEMP_SENSOR_TYPE::BLE_TUYA:
|
||||
case TEMP_SENSOR_TYPE::BLE_XIAOMI_MIJIA:
|
||||
status.nTemp1 = (ble.getTemp() + 5) / 10 + config.nTemp1Offset;
|
||||
status.nHumid1 = (ble.getHumid() + 5) / 10 + config.nHumid1Offset;
|
||||
if (ble.getBatteyLevel() > 30) status.nFlags &= ~FLAG_BLE_BATT;
|
||||
else status.nFlags |= FLAG_BLE_BATT;
|
||||
break;
|
||||
default:
|
||||
status.nTemp1 = 0;
|
||||
status.nHumid1 = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
switch (config.nTemp2SensorType) {
|
||||
case TEMP_SENSOR_TYPE::AHT20:
|
||||
case TEMP_SENSOR_TYPE::AHT2x:
|
||||
status.nTemp2 = (aht25.getTemperature() + 5) / 10 + config.nTemp2Offset;
|
||||
status.nHumid2 = (aht25.getHumidity() + 5) / 10 + config.nHumid2Offset;
|
||||
break;
|
||||
case TEMP_SENSOR_TYPE::AHT10_0x39:
|
||||
status.nTemp2 = (aht10_0x39.getTemperature() + 5) / 10 + config.nTemp2Offset;
|
||||
status.nHumid2 = (aht10_0x39.getHumidity() + 5) / 10 + config.nHumid2Offset;
|
||||
break;
|
||||
case TEMP_SENSOR_TYPE::NTC:
|
||||
status.nTemp2 = ntc.getTemp() + config.nTemp2Offset;
|
||||
status.nHumid2 = 0;
|
||||
break;
|
||||
case TEMP_SENSOR_TYPE::BLE_TUYA:
|
||||
case TEMP_SENSOR_TYPE::BLE_XIAOMI_MIJIA:
|
||||
status.nTemp2 = (ble.getTemp2() + 5) / 10 + config.nTemp2Offset;
|
||||
status.nHumid2 = (ble.getHumid2() + 5) / 10 + config.nHumid2Offset;
|
||||
if (ble.getBatteyLevel2() > 30) status.nFlags &= ~FLAG_BLE_BATT;
|
||||
else status.nFlags |= FLAG_BLE_BATT;
|
||||
break;
|
||||
default:
|
||||
status.nTemp2 = 0;
|
||||
status.nHumid2 = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
status.nTemp3 = ntc.getTemp() + config.nTemp3Offset;
|
||||
}
|
||||
|
||||
// ==================================================================================
|
||||
// Device Control Functions
|
||||
// ==================================================================================
|
||||
MY_IRAM_ATTR void controlAC1(short temp, unsigned long tick) {
|
||||
uint16_t max = config.ac1.dutyMax * 10;
|
||||
// Check Safety
|
||||
if (temp >= config.nTempSafety) {
|
||||
setHeater1Duty(0);
|
||||
status.nHeater1Duty = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// Manual Control
|
||||
if (status.nFlags & FLAG_MANUAL_HEATER1) {
|
||||
if (status.nHeater1Duty > max) status.nHeater1Duty = max;
|
||||
setHeater1Duty(status.nHeater1Duty);
|
||||
return;
|
||||
}
|
||||
|
||||
// Day & Night
|
||||
bool bNight = isNight(g_nHour, g_nMinute);
|
||||
if (!config.ac1.bDay && !bNight || !config.ac1.bNight && bNight) {
|
||||
setHeater1Duty(0);
|
||||
status.nHeater1Duty = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// No Sensor
|
||||
if ((status.nTemp1 - config.nTemp1Offset) == 0 &&
|
||||
(status.nHumid1 - config.nHumid1Offset) == 0) {
|
||||
setHeater1Duty(0);
|
||||
status.nHeater1Duty = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t setPoint;
|
||||
int16_t duty;
|
||||
|
||||
//DPRINTF("AC1 Start: %d°C set: %d duty: %d\n", temp, setPoint, status.nHeater1Duty);
|
||||
switch(config.ac1.nControlType) {
|
||||
case CONTROL_TEMP_HEAT_PID:
|
||||
{
|
||||
setPoint = config.bNightControl && isNight(g_nHour, g_nMinute) ?
|
||||
config.nTempTargetNight: config.nTempTarget;
|
||||
duty = history.calculateDutyForTemp1(setPoint, temp, status.nHeater1Duty);
|
||||
//DPRINTF("AC1: %d°C set: %d duty: %d --> %d\n", temp, setPoint, status.nHeater1Duty, duty);
|
||||
status.nHeater1Duty = duty;
|
||||
if (duty > 0)
|
||||
duty = map(duty, 1, 10000, config.ac1.dutyMin * 10, config.ac1.dutyMax * 10);
|
||||
setHeater1Duty(duty);
|
||||
}
|
||||
break;
|
||||
case CONTROL_TEMP_COOL_PID:
|
||||
case CONTROL_HUMIDITY_INC_PID:
|
||||
case CONTROL_HUMIDITY_DEC_PID:
|
||||
break;
|
||||
default:
|
||||
duty = controlDevice(&config.ac1, 0) * 10;
|
||||
status.nHeater1Duty = duty;
|
||||
if (duty > 0)
|
||||
duty = map(duty, 1, 10000, config.ac1.dutyMin * 10, config.ac1.dutyMax * 10);
|
||||
setHeater1Duty(duty);
|
||||
break;
|
||||
}
|
||||
//DPRINTF("AC1 End: %d°C set: %d duty: %d\n", temp, setPoint, status.nHeater1Duty);
|
||||
|
||||
}
|
||||
|
||||
MY_IRAM_ATTR void controlAC2(short temp, unsigned long tick) {
|
||||
uint16_t max = config.ac2.dutyMax * 10;
|
||||
|
||||
// Check Safety
|
||||
if (temp >= config.nTempSafety) {
|
||||
setHeater2Duty(0);
|
||||
status.nHeater2Duty = 0;
|
||||
//DPRINTLN("Safety Temp");
|
||||
return;
|
||||
}
|
||||
|
||||
// Manual Control
|
||||
if (status.nFlags & FLAG_MANUAL_HEATER2) {
|
||||
if (status.nHeater2Duty > max) status.nHeater2Duty = max;
|
||||
setHeater2Duty(status.nHeater2Duty);
|
||||
return;
|
||||
}
|
||||
|
||||
// Day & Night
|
||||
bool bNight = isNight(g_nHour, g_nMinute);
|
||||
if (!config.ac2.bDay && !bNight || !config.ac2.bNight && bNight) {
|
||||
setHeater2Duty(0);
|
||||
status.nHeater2Duty = 0;
|
||||
//DPRINTLN("Day/Night Reject");
|
||||
return;
|
||||
}
|
||||
|
||||
// No Sensor
|
||||
if ((status.nTemp1 - config.nTemp1Offset) == 0 &&
|
||||
(status.nHumid1 - config.nHumid1Offset) == 0) {
|
||||
setHeater2Duty(0);
|
||||
status.nHeater2Duty = 0;
|
||||
//DPRINTLN("No Sensor");
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t setPoint;
|
||||
int16_t duty;
|
||||
switch(config.ac2.nControlType) {
|
||||
case CONTROL_TEMP_HEAT_PID:
|
||||
{
|
||||
setPoint = config.bNightControl && isNight(g_nHour, g_nMinute) ?
|
||||
config.nTempTargetNight: config.nTempTarget;
|
||||
duty = history.calculateDutyForTemp1(setPoint, temp, status.nHeater2Duty);
|
||||
//DPRINTF("AC2: %d°C set: %d duty: %d --> %d\n", temp, setPoint, status.nHeater2Duty, duty);
|
||||
status.nHeater2Duty = duty;
|
||||
if (duty > 0)
|
||||
duty = map(duty, 1, 10000, config.ac1.dutyMin * 10, config.ac1.dutyMax * 10);
|
||||
setHeater2Duty(duty);
|
||||
}
|
||||
break;
|
||||
case CONTROL_TEMP_COOL_PID:
|
||||
case CONTROL_HUMIDITY_INC_PID:
|
||||
case CONTROL_HUMIDITY_DEC_PID:
|
||||
break;
|
||||
default:
|
||||
status.nHeater2Duty = controlDevice(&config.ac2, 1) * 10;
|
||||
if (status.nHeater2Duty > 0)
|
||||
duty = map(status.nHeater2Duty, 1, 10000, config.ac2.dutyMin * 10, config.ac2.dutyMax * 10);
|
||||
setHeater2Duty(duty);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
MY_IRAM_ATTR void controlMist(short humid, unsigned long tick) {
|
||||
uint16_t duty = 0;
|
||||
bool bNight = isNight(g_nHour, g_nMinute);
|
||||
|
||||
// Manual Control
|
||||
if (status.nFlags & FLAG_MANUAL_MIST) {
|
||||
duty = status.nMistDuty;
|
||||
} else
|
||||
// Day & Night
|
||||
if (!config.mist.bDay && !bNight || !config.mist.bNight && bNight) {
|
||||
duty = 0;
|
||||
} else {
|
||||
if (status.nTemp1 != 0 && status.nHumid1 != 0) {
|
||||
switch(config.mist.nControlType) {
|
||||
case CONTROL_HUMIDITY_INC_PID:
|
||||
{
|
||||
uint16_t setPoint;
|
||||
setPoint = config.bNightControl && isNight(g_nHour, g_nMinute) ?
|
||||
config.nHumidTargetNight: config.nHumidTarget;
|
||||
duty = history.calculateMistDuty(setPoint, humid, status.nMistDuty);
|
||||
//DPRINTF("Mist: %d%% set: %d duty: %d --> %d\n", humid, setPoint, status.nMistDuty, duty);
|
||||
}
|
||||
break;
|
||||
case CONTROL_TEMP_HEAT_PID:
|
||||
case CONTROL_TEMP_COOL_PID:
|
||||
case CONTROL_HUMIDITY_DEC_PID:
|
||||
duty = status.nMistDuty = 0;
|
||||
break;
|
||||
default:
|
||||
duty = controlDevice(&config.mist, 2) * 10;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
duty = 0;
|
||||
}
|
||||
}
|
||||
status.nMistDuty = duty;
|
||||
|
||||
// Control Duty
|
||||
if (duty > PWM_OFF)
|
||||
duty = map(duty, 1, 10000, config.mist.dutyMin, config.mist.dutyMax);
|
||||
ledcWrite(PIN_MIST, duty);
|
||||
}
|
||||
|
||||
MY_IRAM_ATTR void controlFan(short temp, unsigned long tick) {
|
||||
if (status.nFlags & FLAG_MANUAL_FAN ) return;
|
||||
|
||||
status.nFanDuty = controlDevice(&config.fan, 3);
|
||||
}
|
||||
|
||||
MY_IRAM_ATTR void controlMotor(short hour, short min, unsigned long tick) {
|
||||
if (status.nFlags & FLAG_MANUAL_MOTOR ) return;
|
||||
|
||||
status.nMotorDuty = controlDevice(&config.motor, 4);
|
||||
}
|
||||
|
||||
MY_IRAM_ATTR void controlLight(short hour, short min, unsigned long tick) {
|
||||
if (status.nFlags & FLAG_MANUAL_LIGHT ) return;
|
||||
|
||||
status.nLightTargetDuty = controlDevice(&config.light, 5);
|
||||
}
|
||||
|
||||
MY_IRAM_ATTR uint16_t controlDevice(DEVICE_PARAM_TYPE *pDevice, int idx) {
|
||||
uint16_t duty;
|
||||
switch(idx) {
|
||||
case 0: duty = status.nHeater1Duty / 10; break; // AC1
|
||||
case 1: duty = status.nHeater2Duty / 10; break; // AC2
|
||||
case 2: duty = status.nMistDuty / 10; break; // Mist
|
||||
case 3: duty = status.nFanDuty; break; // Fan
|
||||
case 4: duty = status.nMotorDuty; break; // Motor
|
||||
case 5: duty = status.nLightDuty; break; // Light
|
||||
default: duty = 0; break;
|
||||
}
|
||||
|
||||
bool bNight = isNight(g_nHour, g_nMinute);
|
||||
if (!pDevice->bDay && !bNight || !pDevice->bNight && bNight)
|
||||
return 0;
|
||||
|
||||
switch (pDevice->nControlType) {
|
||||
case CONTROL_TEMP_HEAT:
|
||||
if (status.nTemp1 < pDevice->tempLow) {
|
||||
duty = pDevice->dutyOn;
|
||||
}
|
||||
else if (status.nTemp1 > pDevice->tempHigh) {
|
||||
duty = pDevice->dutyOff;
|
||||
}
|
||||
break;
|
||||
case CONTROL_TEMP_COOL:
|
||||
if (status.nTemp1 > pDevice->tempHigh)
|
||||
duty = pDevice->dutyOn;
|
||||
else if (status.nTemp1 < pDevice->tempLow)
|
||||
duty = pDevice->dutyOff;
|
||||
break;
|
||||
case CONTROL_HUMIDITY_DEC:
|
||||
if (status.nHumid1 > pDevice->humidHigh)
|
||||
duty = pDevice->dutyOn;
|
||||
else if (status.nHumid1 < pDevice->humidLow)
|
||||
duty = pDevice->dutyOff;
|
||||
break;
|
||||
case CONTROL_HUMIDITY_INC:
|
||||
if (status.nHumid1 < pDevice->humidLow)
|
||||
duty = pDevice->dutyOn;
|
||||
else if (status.nHumid1 > pDevice->humidHigh)
|
||||
duty = pDevice->dutyOff;
|
||||
break;
|
||||
case CONTROL_DAY_NIGHT:
|
||||
{
|
||||
duty = isNight(g_nHour, g_nMinute) ? pDevice->dutyNight : pDevice->dutyDay;
|
||||
}
|
||||
break;
|
||||
case CONTROL_TIME:
|
||||
{
|
||||
uint16_t time = g_nHour * 60 + g_nMinute;
|
||||
if (pDevice->timeBegin < pDevice->timeEnd) {
|
||||
if (time >= pDevice->timeBegin && time < pDevice->timeEnd)
|
||||
duty = pDevice->dutyOn;
|
||||
else
|
||||
duty = pDevice->dutyOff;
|
||||
} else {
|
||||
if (time >= pDevice->timeEnd && time < pDevice->timeBegin)
|
||||
duty = pDevice->dutyOff;
|
||||
else
|
||||
duty = pDevice->dutyOn;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case CONTROL_PERIOD:
|
||||
{
|
||||
static unsigned long periodStartTime[6] = {0}; // Tracks start of the current period (in seconds)
|
||||
static unsigned long totalElapsedSeconds[6] = {0}; // Tracks elapsed time within the current period
|
||||
static unsigned long lastCurrentTime[6] = {0xFFFFFFFF};
|
||||
|
||||
unsigned long time = g_nHour * 3600UL + g_nMinute * 60UL + g_nSecond;
|
||||
unsigned long period = pDevice->periodPeriod * 60;
|
||||
if (lastCurrentTime[idx] == 0xFFFFFFFF) {
|
||||
lastCurrentTime[idx] = time;
|
||||
}
|
||||
if (time < lastCurrentTime[idx]) {
|
||||
// Handle day rollover (23:59:59 -> 00:00:00)
|
||||
totalElapsedSeconds[idx] += (24UL * 3600) + time - lastCurrentTime[idx]; // Normal time increment
|
||||
} else {
|
||||
totalElapsedSeconds[idx] += time - lastCurrentTime[idx]; // Normal time increment
|
||||
}
|
||||
lastCurrentTime[idx] = time;
|
||||
|
||||
// Reset totalElapsedSeconds when it exceeds the period
|
||||
if (totalElapsedSeconds[idx] >= periodStartTime[idx] + period) {
|
||||
totalElapsedSeconds[idx] -= period; // Wrap around to start a new period
|
||||
periodStartTime[idx] = totalElapsedSeconds[idx]; // Reset the start time
|
||||
duty = pDevice->dutyOn; // Start of the new period
|
||||
}
|
||||
|
||||
// Turn off LED if the onDuration has passed within the current period
|
||||
if (totalElapsedSeconds[idx] >= periodStartTime[idx] + pDevice->periodOn) {
|
||||
duty = pDevice->dutyOff; // Start of the new period
|
||||
} else {
|
||||
duty = pDevice->dutyOn;
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
duty = 0;
|
||||
}
|
||||
return duty;
|
||||
}
|
||||
|
||||
MY_IRAM_ATTR void controlFanDuty() {
|
||||
static uint16_t nFanDuty = 0;
|
||||
|
||||
if (nFanDuty != status.nFanDuty) {
|
||||
if (nFanDuty == 0) {
|
||||
// Start-up
|
||||
nFanDuty = status.nFanDuty < config.fan.dutyStart ? config.fan.dutyStart : status.nFanDuty;
|
||||
} else {
|
||||
nFanDuty = status.nFanDuty;
|
||||
}
|
||||
|
||||
int duty = 0;
|
||||
if (nFanDuty > 0) {
|
||||
duty = map(nFanDuty, 1, 1000, config.fan.dutyMin, config.fan.dutyMax);
|
||||
}
|
||||
ledcWrite(PIN_FAN, duty);
|
||||
}
|
||||
}
|
||||
|
||||
MY_IRAM_ATTR void controlMotorDuty() {
|
||||
static uint16_t nMotorDuty = 0;
|
||||
|
||||
if (nMotorDuty != status.nMotorDuty) {
|
||||
// Start-up
|
||||
if (nMotorDuty == 0) {
|
||||
nMotorDuty = status.nMotorDuty < config.motor.dutyStart ? config.motor.dutyStart : status.nMotorDuty;
|
||||
} else {
|
||||
nMotorDuty = status.nMotorDuty;
|
||||
}
|
||||
|
||||
int duty = 0;
|
||||
if (nMotorDuty > 0) {
|
||||
duty = map(nMotorDuty, 1, 1000, config.motor.dutyMin, config.motor.dutyMax);
|
||||
}
|
||||
ledcWrite(PIN_MOTOR, duty);
|
||||
}
|
||||
}
|
||||
|
||||
MY_IRAM_ATTR void controlLightDuty() {
|
||||
if (status.nLightDuty != status.nLightTargetDuty) {
|
||||
int16_t step = status.nLightDuty < status.nLightTargetDuty ? 1 : -1;
|
||||
status.nLightDuty += step;
|
||||
int16_t duty = map(status.nLightDuty, 0, 1000, config.light.dutyMin, config.light.dutyMax);
|
||||
ledcWrite(PIN_LIGHT, duty);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
MY_IRAM_ATTR bool isNight(unsigned char currentHour, unsigned char currentMin) {
|
||||
// Check if the current time is within the night range
|
||||
|
||||
if (config.nNightStartHour < config.nNightEndHour ||
|
||||
(config.nNightStartHour == config.nNightEndHour && config.nNightStartMin < config.nNightEndMin)) {
|
||||
// Case 1: Night starts and ends on the same day (e.g., 22:00 to 06:00)
|
||||
if ((currentHour > config.nNightStartHour || (currentHour == config.nNightStartHour && currentMin >= config.nNightStartMin)) &&
|
||||
(currentHour < config.nNightEndHour || (currentHour == config.nNightEndHour && currentMin <= config.nNightEndMin))) {
|
||||
return true; // It's night time
|
||||
}
|
||||
} else {
|
||||
// Case 2: Night crosses midnight (e.g., 22:00 to 06:00)
|
||||
if ((currentHour > config.nNightStartHour || (currentHour == config.nNightStartHour && currentMin >= config.nNightStartMin)) ||
|
||||
(currentHour < config.nNightEndHour || (currentHour == config.nNightEndHour && currentMin <= config.nNightEndMin))) {
|
||||
return true; // It's night time
|
||||
}
|
||||
}
|
||||
|
||||
return false; // It's day time
|
||||
}
|
||||
|
||||
// ======================================================================
|
||||
//
|
||||
// Utilities
|
||||
//
|
||||
// ======================================================================
|
||||
MY_IRAM_ATTR char *printStatus(unsigned long tick, bool bLong) {
|
||||
static char szStatus[256] = { 0 };
|
||||
//if (config.bSendStatusSerial)
|
||||
{
|
||||
// Build Status String
|
||||
char strHeat1[32], strHeat2[32], strMist[32], strLight1[32], strLight2[32];
|
||||
strHeat1[0] = 0;
|
||||
strHeat2[0] = 0;
|
||||
strMist[0] = 0;
|
||||
strLight1[0] = 0;
|
||||
strLight2[0] = 0;
|
||||
|
||||
sprintf(strHeat1, "T(%2d.%d/%2d.%d°C H%2d.%d%%)",
|
||||
status.nTemp1 / 10, status.nTemp1 % 10,
|
||||
config.nTempTarget / 10, config.nTempTarget % 10,
|
||||
status.nHeater1Duty / 100, status.nHeater1Duty % 100 );
|
||||
|
||||
sprintf(strMist, "H(%2d.%d/%2d.%d%% M%2d.%d%%)",
|
||||
status.nHumid1 / 10, status.nHumid1 % 10,
|
||||
config.nHumidTarget / 10, config.nHumidTarget % 10,
|
||||
status.nMistDuty / 10, status.nMistDuty % 10);
|
||||
|
||||
sprintf(strLight1, "L(%2d.%d%%)", status.nLightDuty / 10, status.nLightDuty % 10);
|
||||
|
||||
sprintf(szStatus, "%s %s %s %s H(Kp %.2f, Kd %.2f) M(Kp %.2f, %.2f)",
|
||||
printTime(bLong), strHeat1, strMist, strLight1,
|
||||
history.getKpTemperature(), history.getKdTemperature(),
|
||||
history.getKpHumidity(), history.getKdHumidity());
|
||||
|
||||
// Send out to clients
|
||||
// ESP_LOGI(TAG_MAIN,"%s\n", szStatus);
|
||||
}
|
||||
return szStatus;
|
||||
}
|
||||
|
||||
MY_IRAM_ATTR char *printTime(bool bLong) {
|
||||
static char szBuff[48];
|
||||
|
||||
// Get current time from the system clock
|
||||
time_t now;
|
||||
time(&now); // Get current system time in seconds
|
||||
struct tm *timeinfo = localtime(&now);
|
||||
|
||||
if (bLong) {
|
||||
// Calculate uptime in seconds
|
||||
/*
|
||||
unsigned long uptime = now - timeManager.getFirstNTPTime();
|
||||
|
||||
// Calculate days, hours, minutes, seconds for uptime
|
||||
int days = uptime / 86400; // Seconds in a day
|
||||
uptime %= 86400;
|
||||
int hours = uptime / 3600; // Seconds in an hour
|
||||
uptime %= 3600;
|
||||
int minutes = uptime / 60; // Seconds in a minute
|
||||
int seconds = uptime % 60;
|
||||
|
||||
// Format the current time
|
||||
char szDays[8], szHours[8];
|
||||
if (days > 0)
|
||||
sprintf(szDays, "%d ", days);
|
||||
else
|
||||
szDays[0] = 0;
|
||||
|
||||
if (hours > 0)
|
||||
sprintf(szHours, "%d:", hours);
|
||||
else
|
||||
szHours[0] = 0;
|
||||
*/
|
||||
|
||||
sprintf(szBuff, "%04d-%02d-%02d %02d:%02d:%02d",
|
||||
timeinfo->tm_year + 1900, // Year
|
||||
timeinfo->tm_mon + 1, // Month (0-11)
|
||||
timeinfo->tm_mday, // Day of month
|
||||
timeinfo->tm_hour, // Hour
|
||||
timeinfo->tm_min, // Minute
|
||||
timeinfo->tm_sec // Second
|
||||
);
|
||||
} else {
|
||||
sprintf(szBuff, "%02d:%02d:%02d",
|
||||
timeinfo->tm_hour, // Hour
|
||||
timeinfo->tm_min, // Minute
|
||||
timeinfo->tm_sec);
|
||||
}
|
||||
return szBuff;
|
||||
}
|
||||
|
||||
inline void setTime() {
|
||||
// Get time from System Clock - UTC
|
||||
time(&now); // Get current system time in seconds (still in local time settings)
|
||||
status.now = (uint32_t) now; // Store UTC time for external communication
|
||||
|
||||
// UTC time_r(&now, &timeinfo);
|
||||
//gmtime_r(&now, &timeinfo);
|
||||
//ESP_LOGI(TAG_MAIN,"Time - UTC: %2d:%02d:%02d ", timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec, g_nSecond);
|
||||
|
||||
// local time
|
||||
now += config.m_nTimeOffset;
|
||||
gmtime_r(&now, &timeinfo);
|
||||
//ESP_LOGI(TAG_MAIN,"Local: %4d-%02d-%02d %2d:%02d:%02d (UTC%c%d)\n",
|
||||
// g_nYear, g_nMonth, g_nDay,
|
||||
// timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec,
|
||||
// config.m_nTimeOffset >= 0 ? '+' : '-', (uint16_t)(config.m_nTimeOffset / 3600));
|
||||
|
||||
g_nSecond = static_cast<char>(timeinfo.tm_sec);
|
||||
g_nMinute = static_cast<char>(timeinfo.tm_min);
|
||||
g_nHour = static_cast<char>(timeinfo.tm_hour);
|
||||
g_nDay = static_cast<char>(timeinfo.tm_mday);
|
||||
g_nMonth = static_cast<char>(timeinfo.tm_mon) + 1;
|
||||
g_nYear = timeinfo.tm_year + 1900;
|
||||
|
||||
}
|
||||
|
||||
inline void setZCD() {
|
||||
// ZCD
|
||||
status.zcdAC = zcdACCount;
|
||||
zcdACCount = 0;
|
||||
status.zcdLoad = zcdLoadCount;
|
||||
zcdLoadCount = 0;
|
||||
if (status.zcdAC < 118 || status.zcdAC > 122) {
|
||||
status.nFlags |= FLAG_ZCD_AC;
|
||||
}
|
||||
else {
|
||||
status.nFlags &= ~FLAG_ZCD_AC;
|
||||
}
|
||||
if (status.zcdLoad < 118 || status.zcdLoad > 122) {
|
||||
status.nFlags |= FLAG_ZCD_LOAD;
|
||||
}
|
||||
else {
|
||||
status.nFlags &= ~FLAG_ZCD_LOAD;
|
||||
}
|
||||
}
|
||||
243
HermitCrab.h
Normal file
243
HermitCrab.h
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
#ifndef __HERMIT_CRAB_H
|
||||
#define __HERMIT_CRAB_H
|
||||
#include <Arduino.h>
|
||||
|
||||
#ifndef ESP32
|
||||
#define ESP32
|
||||
#endif
|
||||
|
||||
#define USE_BLE
|
||||
#ifdef USE_BLE
|
||||
#define THIS_DEVICE_TYPE ENUM_DEVICE_TYPE::TYPE_BETA_BLE
|
||||
#define MY_IRAM_ATTR
|
||||
#else
|
||||
#define THIS_DEVICE_TYPE ENUM_DEVICE_TYPE::TYPE_BETA
|
||||
#define MY_IRAM_ATTR IRAM_ATTR
|
||||
#endif
|
||||
|
||||
|
||||
// Define DEBUG flag
|
||||
#ifndef DEBUG
|
||||
#define DEBUG 1 // Set to 0 to disable debug output
|
||||
#endif
|
||||
#undef DEBUG
|
||||
|
||||
//#define BLE_DEBUG
|
||||
#ifdef BLE_DEBUG
|
||||
#ifndef DEBUG
|
||||
#define DEBUG 1
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// Debug print macros
|
||||
#if defined(DEBUG)
|
||||
#define DPRINT(...) Serial.print(__VA_ARGS__)
|
||||
#define DPRINTLN(...) Serial.println(__VA_ARGS__)
|
||||
#define DPRINTF(...) Serial.printf(__VA_ARGS__)
|
||||
#else
|
||||
#define DPRINT(...) // Do nothing
|
||||
#define DPRINTLN(...) // Do nothing
|
||||
#define DPRINTF(...) // Do nothing
|
||||
#endif
|
||||
|
||||
#ifndef SIGNATURE1
|
||||
#define SIGNATURE1 ((uint16_t) 0xC8AB)
|
||||
#endif
|
||||
#ifndef SIGNATURE2
|
||||
#define SIGNATURE2 ((uint16_t) 0x4E81)
|
||||
#endif
|
||||
|
||||
|
||||
#define KALVIN (273.125f)
|
||||
#define LIGHT_ON HIGH
|
||||
#define LIGHT_OFF LOW
|
||||
#define HUMID_ON HIGH
|
||||
#define HUMID_OFF LOW
|
||||
#define HEATER_ON HIGH
|
||||
#define HEATER_OFF LOW
|
||||
#define LED_ON LOW
|
||||
#define LED_OFF HIGH
|
||||
#define SEGMENT_ON LOW
|
||||
#define SEGMENT_OFF HIGH
|
||||
|
||||
// =======================
|
||||
// PIN Definitions
|
||||
//
|
||||
// LEFT
|
||||
#define PIN_NTC 39 // Input Only
|
||||
#define PIN_ZCD_LOAD 34 // Input Only
|
||||
#define PIN_ZCD_AC 35 // Input Only
|
||||
#define PIN_HEATER1 32
|
||||
#define PIN_HEATER2 33
|
||||
#define PIN_LIGHT 25
|
||||
#define PIN_MOTOR 26
|
||||
#define PIN_FAN 27
|
||||
#define PIN_MIST 14 // Outputs PWM signal at boot
|
||||
#define PIN_NONE_1 12 // boot fails if pulled high, strapping pin
|
||||
|
||||
// BOTTOM
|
||||
#define PIN_NONE_2 13
|
||||
#define PIN_NONE_6 15 // Outputs PWM signal at Boot, strapping pin
|
||||
#define PIN_NONE_5 2
|
||||
|
||||
// RIGHT
|
||||
#define PIN_LED_WIFI 0 // pulled up outputs PWM signal at boot, must be LOW to enter flashing mode
|
||||
#define PIN_NONE_4 4
|
||||
#define PIN_LED_HEATER2 16
|
||||
#define PIN_LED_HEATER1 17
|
||||
#define PIN_SW_DOWN 5
|
||||
#define PIN_SW_UP 18
|
||||
#define PIN_SW_SET 19
|
||||
#define PIN_SDA 21
|
||||
#define PIN_RX 3
|
||||
#define PIN_TX 1
|
||||
#define PIN_SCL 22
|
||||
#define PIN_NONE_3 23
|
||||
// End of PIN Definition
|
||||
// =======================
|
||||
|
||||
#define PWM_RESOLUTION 10 // 10-bit resolution
|
||||
#define PWM_MOTOR_RESOLUTION 8
|
||||
|
||||
#define PWM_AP_CHANNEL 9
|
||||
#define PWM_SC_CHANNEL 10
|
||||
#define PWM_AP_FREQ 1
|
||||
#define PWM_SC_FREQ 2
|
||||
|
||||
#define PWM_MIST_CHANNEL 8 // MIST
|
||||
#define PWM_MIST_FREQ 1 // MIST
|
||||
|
||||
#define PWM_HEATER1_CHANNEL 2
|
||||
#define PWM_HEATER2_CHANNEL 3
|
||||
#define PWM_LIGHT_CHANNEL 4
|
||||
#define PWM_MOTOR_CHANNEL 5
|
||||
#define PWM_FAN_CHANNEL 6
|
||||
|
||||
#define PWM_1KHZ_FREQ 1000 // 1 kHz frequency
|
||||
#define PWM_25KHZ_FREQ 25000 // 25KHz
|
||||
|
||||
#define PWM_FULL 1023
|
||||
#define PWM_OFF 0
|
||||
#define TAG "HC"
|
||||
|
||||
enum EVENT_TYPE {
|
||||
// Temperature
|
||||
|
||||
// Humidity
|
||||
|
||||
// Light
|
||||
|
||||
// Fan
|
||||
|
||||
// Connection
|
||||
WiFi_Conneted,
|
||||
WiFi_Disconnected,
|
||||
BT_Connected,
|
||||
BT_Disconnected
|
||||
};
|
||||
|
||||
#define FLAG_MANUAL_HEATER1 (0x0001)
|
||||
#define FLAG_MANUAL_HEATER2 (0x0002)
|
||||
#define FLAG_MANUAL_MIST (0x0004)
|
||||
#define FLAG_MANUAL_FAN (0x0008)
|
||||
|
||||
#define FLAG_MANUAL_MOTOR (0x0010)
|
||||
#define FLAG_MANUAL_LIGHT (0x0020)
|
||||
#define FLAG_ZCD_AC (0x0040)
|
||||
#define FLAG_ZCD_LOAD (0x0080)
|
||||
|
||||
#define FLAG_ZCD (0x0100)
|
||||
#define FLAG_UPNP (0x0200)
|
||||
#define FLAG_BLE_BATT (0x0400)
|
||||
#define FLAG_BLE_NODATA (0x0800)
|
||||
#define FLAG_BLE_LOST (0x1000)
|
||||
|
||||
|
||||
#pragma pack(push) /* push current alignment to stack */
|
||||
#pragma pack(1) /* set alignment to 1 byte boundary */
|
||||
typedef struct DEVICE_PARAM_STRUCT {
|
||||
uint8_t nControlType;
|
||||
uint8_t x;
|
||||
uint8_t bDay, bNight;
|
||||
//4
|
||||
uint16_t dutyMin, dutyMax, dutyStart;
|
||||
//6 + 4 = 10
|
||||
union {
|
||||
uint16_t dutyDay;
|
||||
uint16_t dutyOn;
|
||||
uint16_t dutyDayOrOn;
|
||||
};
|
||||
union {
|
||||
uint16_t dutyNight;
|
||||
uint16_t dutyOff;
|
||||
uint16_t dutyNightOrOff;
|
||||
};
|
||||
//4 + 10 = 14
|
||||
int16_t tempHigh, tempLow;
|
||||
uint16_t timeBegin, timeEnd;
|
||||
uint16_t periodPeriod, periodOn;
|
||||
uint16_t humidHigh, humidLow;
|
||||
//16 + 14 = 30
|
||||
|
||||
uint8_t extra[32 - 30];
|
||||
} DEVICE_PARAM_TYPE;
|
||||
|
||||
typedef struct STATUS_STRUCT {
|
||||
// Sensor
|
||||
int16_t nTemp1;
|
||||
int16_t nTemp2;
|
||||
int16_t nTemp3;
|
||||
int16_t nHumid1;
|
||||
int16_t nHumid2;
|
||||
// 10
|
||||
|
||||
// Control
|
||||
uint16_t nHeater1Duty; // (0..10000)
|
||||
uint16_t nHeater2Duty; // (0..10000)
|
||||
uint16_t nMistDuty; //
|
||||
uint16_t nFanDuty; //
|
||||
uint16_t nMotorDuty;
|
||||
uint16_t nLightDuty;
|
||||
uint16_t nLightTargetDuty;
|
||||
// 24
|
||||
|
||||
// Current Time
|
||||
uint32_t now;
|
||||
// 28
|
||||
|
||||
uint16_t nFlags;
|
||||
// 30
|
||||
|
||||
// AC status
|
||||
uint8_t zcdAC;
|
||||
uint8_t zcdLoad;
|
||||
// 32
|
||||
} STATUS_TYPE;
|
||||
#pragma pack(pop) /* restore original alignment from stack */
|
||||
|
||||
// =======================================================
|
||||
|
||||
char *printStatus(unsigned long tick, bool bLong = false);
|
||||
char *printTime(bool bLong = false);
|
||||
void checkSerial(unsigned long tick);
|
||||
void checkWiFi(unsigned long tick);
|
||||
void checkWiFiHost(unsigned long tick);
|
||||
|
||||
void core0Task(void *pvParameters);
|
||||
|
||||
// =======================================================
|
||||
|
||||
// Status
|
||||
extern STATUS_TYPE status;
|
||||
|
||||
|
||||
// Time
|
||||
extern volatile unsigned short g_nYear, g_nMonth, g_nDay, g_nHour, g_nMinute, g_nSecond;
|
||||
|
||||
// Environment
|
||||
extern bool bShowSensor;
|
||||
extern const char *COMPANY_NAME;
|
||||
extern const char *SERVICE_NAME;
|
||||
extern const char *HC__VERSION;
|
||||
|
||||
#endif
|
||||
72
HermitCrab.vcxproj
Normal file
72
HermitCrab.vcxproj
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|Win32">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>Win32</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|Win32">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>Win32</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Debug|x64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>16.0</VCProjectVersion>
|
||||
<ProjectGuid>{9EFFA72D-8A81-44D2-B7B4-37C53EBAD29F}</ProjectGuid>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<PlatformToolset>v142</PlatformToolset>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<PlatformToolset>v142</PlatformToolset>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<PlatformToolset>v142</PlatformToolset>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<PlatformToolset>v142</PlatformToolset>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ImportGroup Label="ExtensionSettings">
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="Shared">
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
<PropertyGroup />
|
||||
<ItemDefinitionGroup>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Label="ExtensionTargets">
|
||||
</ImportGroup>
|
||||
</Project>
|
||||
4
HermitCrab.vcxproj.user
Normal file
4
HermitCrab.vcxproj.user
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup />
|
||||
</Project>
|
||||
214
History.cpp
Normal file
214
History.cpp
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
#include "History.h"
|
||||
#include "Config.h"
|
||||
|
||||
extern class Preferences preferences;
|
||||
CHistory history;
|
||||
|
||||
CHistory::CHistory()
|
||||
: head(0), tail(0), count(0), lastTemp(0), lastHumid(0)
|
||||
{
|
||||
Kd_Humidity = 3.6; // Load Kd for humidity
|
||||
Kd_Humidity = 1.8;
|
||||
LR_Humidity = 0.25;
|
||||
|
||||
Kp_Temp1 = 3.6;
|
||||
Kd_Temp1 = 1.8;
|
||||
LR_Temp1 = 0.25;
|
||||
|
||||
head = 0;
|
||||
tail = 0;
|
||||
count = 0;
|
||||
|
||||
lastTemp = 0;
|
||||
lastHumid = 0;
|
||||
for (int i = 0; i < RING_SIZE; i++)
|
||||
ring[i] = {{0}};
|
||||
}
|
||||
|
||||
void CHistory::init(int16_t _lastTemp, int16_t _lastHumid) {
|
||||
lastTemp = _lastTemp;
|
||||
lastHumid = _lastHumid;
|
||||
}
|
||||
|
||||
void CHistory::loadPID() {
|
||||
Kp_Humidity = config.Kp_Humidity;
|
||||
Kd_Humidity = config.Kd_Humidity;
|
||||
LR_Humidity = config.LR_Humidity;
|
||||
|
||||
Kp_Temp1 = config.Kp_Temp1;
|
||||
Kd_Temp1 = config.Kd_Temp1;
|
||||
LR_Temp1 = config.LR_Temp1;
|
||||
|
||||
Kp_Temp2 = config.Kp_Temp2;
|
||||
Kd_Temp2 = config.Kd_Temp2;
|
||||
LR_Temp2 = config.LR_Temp2;
|
||||
|
||||
Kp_Temp3 = config.Kp_Temp3;
|
||||
Kd_Temp3 = config.Kd_Temp3;
|
||||
LR_Temp3 = config.LR_Temp3;
|
||||
|
||||
}
|
||||
|
||||
void CHistory::savePID() {
|
||||
config.Kp_Humidity = Kp_Humidity;
|
||||
config.Kd_Humidity = Kd_Humidity;
|
||||
config.LR_Humidity = LR_Humidity;
|
||||
|
||||
config.Kp_Temp1 = Kp_Temp1;
|
||||
config.Kd_Temp1 = Kd_Temp1;
|
||||
config.LR_Temp1 = LR_Temp1;
|
||||
|
||||
config.Kp_Temp2 = Kp_Temp2;
|
||||
config.Kd_Temp2 = Kd_Temp2;
|
||||
config.LR_Temp2 = LR_Temp2;
|
||||
|
||||
config.Kp_Temp3 = Kp_Temp3;
|
||||
config.Kd_Temp3 = Kd_Temp3;
|
||||
config.LR_Temp3 = LR_Temp3;
|
||||
}
|
||||
|
||||
// Method to add status data to the history buffer
|
||||
MY_IRAM_ATTR short CHistory::add(STATUS_TYPE &status) {
|
||||
|
||||
ring[head] = status;
|
||||
|
||||
if (++head >= RING_SIZE) head = 0; // Mask with 0xFF for wrap-around
|
||||
if (count < RING_SIZE) {
|
||||
count++;
|
||||
} else {
|
||||
if (++tail > RING_SIZE) tail = 0; // Advance tail if buffer is full
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// Main PD Control method for heater duty calculation
|
||||
MY_IRAM_ATTR int16_t CHistory::calculateDutyForTemp1(int16_t setpoint, int16_t curTemp, int16_t lastDuty) {
|
||||
// Calculate Proportional term
|
||||
float currentError = setpoint - curTemp;
|
||||
float P = Kp_Temp1 * currentError;
|
||||
|
||||
// Calculate Derivative term based on temperature slope
|
||||
float timeInterval = 10.0; // Assuming a 5-second interval between updates; adjust as needed
|
||||
float slope = (curTemp - lastTemp) / timeInterval;
|
||||
float D = Kd_Temp1 * (-slope); // Negative sign to counteract the rising trend
|
||||
|
||||
// Limit magnitute of change
|
||||
int16_t diff = roundf(P + D);
|
||||
if (diff > 500) diff = 500;
|
||||
else if (diff < -500) diff = -500;
|
||||
|
||||
// Calculate new heater duty cycle
|
||||
int16_t newDuty = lastDuty + diff;
|
||||
|
||||
// Constrain the duty cycle within the range of 0 to 10000
|
||||
if (newDuty < 0) newDuty = 0;
|
||||
else if (newDuty > 10000) newDuty = 10000;
|
||||
|
||||
// Adjust Kp and Kd using gradient descent based on the current and previous errors
|
||||
adjustGainsUsingGradientDescent(Kp_Temp1, Kd_Temp1, currentError, setpoint, lastTemp, 100.0f);
|
||||
if (Kp_Temp1 < 0.5f * config.Kp_Temp1) Kp_Temp1 = 0.5f * config.Kp_Temp1;
|
||||
else if (Kp_Temp1 > 2.0f * config.Kp_Temp1) Kp_Temp1 = 2.0f * config.Kp_Temp1;
|
||||
if (Kd_Temp1 < 0.5f * config.Kd_Temp1) Kd_Temp1 = 0.5f * config.Kd_Temp1;
|
||||
else if (Kd_Temp1 > 2.0f * config.Kd_Temp1) Kd_Temp1 = 2.0f * config.Kd_Temp1;
|
||||
|
||||
lastTemp = curTemp;
|
||||
return newDuty;
|
||||
}
|
||||
|
||||
|
||||
MY_IRAM_ATTR int16_t CHistory::calculateDutyForTemp2(int16_t setpoint, int16_t curTemp, int16_t lastDuty) {
|
||||
// Calculate Proportional term
|
||||
float currentError = setpoint - curTemp;
|
||||
float P = Kp_Temp2 * currentError;
|
||||
|
||||
// Calculate Derivative term based on temperature slope
|
||||
float timeInterval = 10.0; // Assuming a 5-second interval between updates; adjust as needed
|
||||
float slope = (curTemp - lastTemp) / timeInterval;
|
||||
float D = Kd_Temp2 * (-slope); // Negative sign to counteract the rising trend
|
||||
|
||||
// Limit magnitute of change
|
||||
int16_t diff = roundf(P + D);
|
||||
if (diff > 500) diff = 500;
|
||||
else if (diff < -500) diff = -500;
|
||||
|
||||
// Calculate new heater duty cycle
|
||||
int16_t newDuty = lastDuty + diff;
|
||||
|
||||
// Constrain the duty cycle within the range of 0 to 10000
|
||||
if (newDuty < 0) newDuty = 0;
|
||||
else if (newDuty > 10000) newDuty = 10000;
|
||||
|
||||
// Adjust Kp and Kd using gradient descent based on the current and previous errors
|
||||
adjustGainsUsingGradientDescent(Kp_Temp2, Kd_Temp2, currentError, setpoint, lastTemp, 100.0f);
|
||||
if (Kp_Temp2 < 0.5f * config.Kp_Temp2) Kp_Temp2 = 0.5f * config.Kp_Temp2;
|
||||
else if (Kp_Temp2 > 2.0f * config.Kp_Temp2) Kp_Temp2 = 2.0f * config.Kp_Temp2;
|
||||
if (Kd_Temp2 < 0.5f * config.Kd_Temp2) Kd_Temp2 = 0.5f * config.Kd_Temp2;
|
||||
else if (Kd_Temp2 > 2.0f * config.Kd_Temp2) Kd_Temp2 = 2.0f * config.Kd_Temp2;
|
||||
|
||||
lastTemp = curTemp;
|
||||
return newDuty;
|
||||
}
|
||||
|
||||
|
||||
MY_IRAM_ATTR int16_t CHistory::calculateMistDuty(uint16_t setpoint, uint16_t curHumid, int16_t lastDuty) {
|
||||
// Calculate Proportional (P) term based on the error (difference between setpoint and current humidity)
|
||||
float currentError = setpoint - curHumid;
|
||||
float P = Kp_Humidity * currentError;
|
||||
|
||||
// Calculate Derivative (D) term based on humidity slope
|
||||
float timeInterval = 10.0; // Assuming a 10-second interval between updates; adjust as needed
|
||||
float slope = (curHumid - lastHumid) / timeInterval;
|
||||
float D = Kd_Humidity * (-slope); // Negative sign to counteract the rising or falling trend
|
||||
|
||||
// Limit magnitute of change
|
||||
int16_t diff = roundf(P + D);
|
||||
if (diff > 500) diff = 500;
|
||||
else if (diff < -500) diff = -500;
|
||||
|
||||
// Calculate the new mist duty cycle
|
||||
int16_t newDuty = lastDuty + diff;
|
||||
|
||||
// Constrain the duty cycle within the allowable range (0 to 511 for 50% duty cycle)
|
||||
if (newDuty < 0) newDuty = 0;
|
||||
else if (newDuty > 10000) newDuty = 10000;
|
||||
|
||||
// Adjust Kp and Kd using gradient descent based on current and previous errors
|
||||
adjustGainsUsingGradientDescent(Kp_Humidity, Kd_Humidity, currentError, setpoint, lastHumid, 300.0f);
|
||||
if (Kp_Humidity < 0.5f * config.Kp_Humidity) Kp_Humidity = 0.5f * config.Kp_Humidity;
|
||||
else if (Kp_Humidity > 2.0f * config.Kp_Humidity) Kp_Humidity = 2.0f * config.Kp_Humidity;
|
||||
if (Kd_Humidity < 0.5f * config.Kd_Humidity) Kd_Humidity = 0.5f * config.Kd_Humidity;
|
||||
else if (Kd_Humidity > 2.0f * config.Kd_Humidity) Kd_Humidity = 2.0f * config.Kd_Humidity;
|
||||
|
||||
lastHumid = curHumid;
|
||||
return newDuty;
|
||||
}
|
||||
|
||||
// Method to adjust Kp and Kd using gradient descent based on current and previous errors
|
||||
MY_IRAM_ATTR void CHistory::adjustGainsUsingGradientDescent(float &Kp, float &Kd, float currentError, uint16_t setpoint, float prevTemperature, float MAX_EXPECTED_ERROR) {
|
||||
// Normalize the error (assuming a max expected error for normalization)
|
||||
float normalizedError = currentError / MAX_EXPECTED_ERROR;
|
||||
|
||||
// Calculate change in temperature as the slope
|
||||
float temperatureSlope = (setpoint - prevTemperature);
|
||||
|
||||
// Define limits for adjusting Kp and Kd
|
||||
const float proportionalLimitFactorKp = 0.25f;
|
||||
const float proportionalLimitFactorKd = 0.25f;
|
||||
|
||||
// Scale the gradients based on normalized error and temperature slope
|
||||
float limitedProportionalGradient = proportionalLimitFactorKp * Kp;
|
||||
float limitedDerivativeGradient = proportionalLimitFactorKd * Kd;
|
||||
|
||||
// Update Kp and Kd using their respective gradients, ensuring adjustments are within limits
|
||||
Kp -= LR_Temp1 * fmin(fabs(normalizedError), limitedProportionalGradient) * (normalizedError < 0 ? 1 : -1);
|
||||
Kd -= LR_Temp1 * fmin(fabs(temperatureSlope), limitedDerivativeGradient) * (temperatureSlope < 0 ? 1 : -1);
|
||||
|
||||
// Constrain Kp and Kd to avoid going below minimum threshold values
|
||||
const float MIN_KP = 0.01f;
|
||||
const float MIN_KD = 0.01f;
|
||||
Kp = max(Kp, MIN_KP);
|
||||
Kd = max(Kd, MIN_KD);
|
||||
}
|
||||
|
||||
|
||||
|
||||
69
History.h
Normal file
69
History.h
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
#ifndef __HISTORY_H
|
||||
#define __HISTORY_H
|
||||
|
||||
#include "HermitCrab.h" // Assuming CURRENT_STATUS structure is defined elsewhere
|
||||
|
||||
#define RING_SIZE (256 * 7)
|
||||
//#define RING_MASK (RING_SIZE - 1)
|
||||
|
||||
class CHistory {
|
||||
private:
|
||||
int16_t head;
|
||||
int16_t tail;
|
||||
int16_t count;
|
||||
STATUS_TYPE ring[RING_SIZE]; // Ring buffer
|
||||
|
||||
float Kp_Humidity;
|
||||
float Kd_Humidity;
|
||||
float LR_Humidity;
|
||||
|
||||
float Kp_Temp1;
|
||||
float Kd_Temp1;
|
||||
float LR_Temp1;
|
||||
|
||||
float Kp_Temp2;
|
||||
float Kd_Temp2;
|
||||
float LR_Temp2;
|
||||
|
||||
float Kp_Temp3;
|
||||
float Kd_Temp3;
|
||||
float LR_Temp3;
|
||||
|
||||
int16_t lastTemp, lastHumid;
|
||||
|
||||
public:
|
||||
CHistory();
|
||||
|
||||
void loadPID();
|
||||
void savePID();
|
||||
void init(int16_t lastTemp, int16_t lastHumid);
|
||||
|
||||
inline float getKpTemperature() { return Kp_Temp1;}
|
||||
inline float getKdTemperature() { return Kd_Temp1;}
|
||||
inline float getLRTemperature() { return LR_Temp1;}
|
||||
inline float getKpHumidity() { return Kp_Humidity;}
|
||||
inline float getKdHumidity() { return Kd_Humidity;}
|
||||
inline float getLRHumidity() { return LR_Humidity;}
|
||||
inline float getKpHeater1() { return Kp_Temp1; }
|
||||
inline float getKdHeater1() { return Kd_Temp1; }
|
||||
inline float getKpMist() { return Kp_Humidity; }
|
||||
inline float getKdMist() { return Kd_Humidity; }
|
||||
|
||||
int16_t add(STATUS_TYPE &status);
|
||||
int16_t calculateDutyForTemp1(int16_t setPoint, int16_t curTemp, int16_t lastDuty);
|
||||
int16_t calculateDutyForTemp2(int16_t setPoint, int16_t curTemp, int16_t lastDuty);
|
||||
int16_t calculateMistDuty(uint16_t setPpoint, uint16_t curHumid, int16_t lastDuty);
|
||||
inline int16_t getRingCount() { return count; };
|
||||
inline int16_t getRingSize() { return RING_SIZE; }
|
||||
inline uint8_t *getRingData1() { return (uint8_t *) &ring[tail]; }
|
||||
inline uint8_t *getRingData2() { return (uint8_t *) &ring[0]; }
|
||||
inline int16_t getRingHead() { return head; }
|
||||
inline int16_t getRingTail() { return tail; }
|
||||
|
||||
private:
|
||||
void adjustGainsUsingGradientDescent(float &Kp, float &Kd, float currentError, uint16_t setpoint, float prevTemperature, float MAX_EXPECTED_ERROR);
|
||||
};
|
||||
|
||||
extern CHistory history;
|
||||
#endif // CHISTORY_H
|
||||
|
||||
52
LED0.cpp
Normal file
52
LED0.cpp
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
#include "LED0.h"
|
||||
|
||||
CLED0 led0;
|
||||
|
||||
void CLED0::setup(uint8_t _pin, uint16_t _freq, uint16_t _channel) {
|
||||
freq = _freq;
|
||||
channel = _channel;
|
||||
pin = _pin;
|
||||
bPWMMode = true;
|
||||
bAC = false;
|
||||
bLoad = false;
|
||||
duty = 0;
|
||||
|
||||
ledcAttachChannel(pin, _freq, PWM_RESOLUTION, channel);
|
||||
setDuty(duty);
|
||||
};
|
||||
|
||||
MY_IRAM_ATTR void CLED0::setFreq(uint16_t _freq) {
|
||||
if (freq != _freq) {
|
||||
if (_freq == 0) {
|
||||
ledcDetach(pin);
|
||||
pinMode(pin, OUTPUT);
|
||||
digitalWrite(pin, LED_OFF);
|
||||
bPWMMode = false;
|
||||
}
|
||||
else {
|
||||
ledcAttachChannel(pin, _freq, PWM_RESOLUTION, channel);
|
||||
bPWMMode = true;
|
||||
}
|
||||
freq = _freq;
|
||||
}
|
||||
}
|
||||
|
||||
MY_IRAM_ATTR void CLED0::setDuty() {
|
||||
uint16_t _duty;
|
||||
|
||||
if (bAC) _duty = LED0_DUTY_AC;
|
||||
else if (bLoad) _duty = LED0_DUTY_LOAD;
|
||||
else _duty = duty;
|
||||
|
||||
ledcWrite(PIN_LED_WIFI, PWM_FULL * (100 - _duty) / 100); // Light Blink
|
||||
}
|
||||
|
||||
MY_IRAM_ATTR void CLED0::setDuty(uint16_t _duty) {
|
||||
if (duty != _duty) {
|
||||
if (bPWMMode)
|
||||
ledcWrite(PIN_LED_WIFI, PWM_FULL * (100 - _duty) / 100); // Light Blink
|
||||
else
|
||||
digitalWrite(pin, _duty ? LED_ON : LED_OFF);
|
||||
duty = _duty;
|
||||
}
|
||||
};
|
||||
38
LED0.h
Normal file
38
LED0.h
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
#ifndef __LED0_H
|
||||
#define __LED0_H
|
||||
|
||||
#ifndef __HERMIT_CRAB_H
|
||||
#include "HermitCrab.h"
|
||||
#endif
|
||||
|
||||
#define LED0_DUTY_BOOT 20
|
||||
#define LED0_DUTY_CONNECTED 0
|
||||
#define LED0_DUTY_CONNECTING 5
|
||||
#define LED0_DUTY_SMART_CONFIG 100
|
||||
#define LED0_DUTY_CLIENT 1
|
||||
#define LED0_DUTY_AC 90
|
||||
#define LED0_DUTY_LOAD 80
|
||||
|
||||
class CLED0 {
|
||||
public:
|
||||
void setup(uint8_t _pin, uint16_t _freq, uint16_t _channel);
|
||||
void loop();
|
||||
void setFreq(uint16_t _freq);
|
||||
void setDuty(uint16_t _duty);
|
||||
void setDuty();
|
||||
inline void setAC() { bAC = true; };
|
||||
inline void clearAC() { bAC = false; };
|
||||
inline void setLoad() { bLoad = true; };
|
||||
inline void clearLoad() { bLoad = false; };
|
||||
|
||||
private:
|
||||
uint16_t channel;
|
||||
uint16_t freq;
|
||||
uint16_t duty;
|
||||
uint8_t pin;
|
||||
bool bPWMMode;
|
||||
bool bAC, bLoad;
|
||||
};
|
||||
|
||||
extern CLED0 led0;
|
||||
#endif
|
||||
127
NTC_10K.cpp
Normal file
127
NTC_10K.cpp
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
#include "Arduino.h"
|
||||
|
||||
#include "HermitCrab.h"
|
||||
#include "NTC_10K.h"
|
||||
#include "Config.h"
|
||||
|
||||
const float resistance[] = {
|
||||
3360850.37, // -40°C
|
||||
1973470.32, // -35°C
|
||||
1179560.43, // -30°C
|
||||
718858.73, // -25°C
|
||||
445267.47, // -20°C
|
||||
281046.96, // -15°C
|
||||
180321.36, // -10°C
|
||||
117081.11, // -5°C
|
||||
77147.90, // 0°C
|
||||
51471.97, // 5°C
|
||||
34838.43, // 10°C
|
||||
23847.63, // 15°C
|
||||
|
||||
16594.38, // 20°C
|
||||
12307.39, // 21°C
|
||||
11739.87, // 22°C
|
||||
11203.64, // 23°C
|
||||
10696.86, // 24°C
|
||||
10217.84, // 25°C
|
||||
9527.52, // 26°C
|
||||
9076.66, // 27°C
|
||||
8656.02, // 28°C
|
||||
8263.81, // 29°C
|
||||
7897.84, // 30°C
|
||||
|
||||
5868.86, // 35°C
|
||||
4383.72, // 40°C
|
||||
3315.12, // 45°C
|
||||
2527.73, // 50°C
|
||||
1942.15, // 55°C
|
||||
1505.11, // 60°C
|
||||
1174.71, // 65°C
|
||||
926.23, // 70°C
|
||||
735.99, // 75°C
|
||||
588.91, // 80°C
|
||||
474.43, // 85°C
|
||||
384.48, // 90°C
|
||||
313.88, // 95°C
|
||||
258.87, // 100°C
|
||||
215.64, // 105°C
|
||||
181.10, // 110°C
|
||||
153.01, // 115°C
|
||||
129.77, // 120°C
|
||||
110.27, // 125°C
|
||||
94.03, // 130°C
|
||||
80.13, // 135°C
|
||||
68.18, // 140°C
|
||||
57.99, // 145°C
|
||||
49.30 // 150°C
|
||||
};
|
||||
|
||||
const int16_t temp_C[] = {
|
||||
-40, -35, -30, -25, -20, -15, -10, -5, 0, 5, 10, 15,
|
||||
20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
|
||||
35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100,
|
||||
105, 110, 115, 120, 125, 130, 135, 140, 145, 150 };
|
||||
|
||||
NTC_10K ntc;
|
||||
|
||||
void NTC_10K::setup(bool bNegativePolarity) {
|
||||
m_bNegativePolarity = bNegativePolarity;
|
||||
_vRef = 3.3f;
|
||||
_RESO = 4095;
|
||||
for (int i = 0; i < 16; i++) temps[i] = 0;
|
||||
temp_idx = 0;
|
||||
temp_sum = 0;
|
||||
|
||||
pinMode(PIN_NTC, INPUT); // Set PIN_NTC as input
|
||||
analogReadResolution(12); // Set ADC resolution to 12 bits (0-4095)
|
||||
analogSetAttenuation(ADC_11db); // Set attenuation for full-scale 3.3V
|
||||
}
|
||||
|
||||
void NTC_10K::readSensor() {
|
||||
float Vin;
|
||||
int16_t temp;
|
||||
static int16_t lastTemp = 0;
|
||||
|
||||
int adcValue = analogRead(PIN_NTC); // Read ADC value from PIN_NTC
|
||||
|
||||
// Calculate the input voltage from the ADC reading
|
||||
Vin = (float)adcValue * _vRef / _RESO;
|
||||
|
||||
// Calculate the resistance of the thermistor
|
||||
// Calculate the resistance of the thermistor (adjusted for inverted ADC behavior)
|
||||
|
||||
float r;
|
||||
if (m_bNegativePolarity) {
|
||||
// NTC is connected to Negative
|
||||
r = (Vin / (_vRef - Vin)) * rRef;
|
||||
} else {
|
||||
// NTC is connected to Positive
|
||||
r = ((_vRef - Vin) / Vin) * rRef;
|
||||
}
|
||||
|
||||
// Find the index of the resistance in the table where r is between resistance[i-1] and resistance[i]
|
||||
int i = 0;
|
||||
while (i < sizeof(resistance) / sizeof(resistance[0]) - 1 && resistance[i] > r) {
|
||||
i++;
|
||||
}
|
||||
|
||||
// If r is out of range, return the closest extreme temperature
|
||||
if (i == 0 || i == sizeof(resistance) / sizeof(resistance[0]) - 1) {
|
||||
if (lastTemp != 0) temp = lastTemp;
|
||||
else temp = 0;
|
||||
}
|
||||
else {
|
||||
// Interpolate between resistance[i-1] and resistance[i]
|
||||
float m = (temp_C[i] - temp_C[i - 1]) / (resistance[i] - resistance[i - 1]); // Slope
|
||||
float b = temp_C[i - 1] - (m * resistance[i - 1]); // Intercept
|
||||
temp = (int16_t) roundf((m * r + b) * 10.0f);
|
||||
}
|
||||
|
||||
// Return the temperature as an integer scaled by 10 (e.g., 25.3°C => 253)
|
||||
temp_sum -= temps[temp_idx];
|
||||
temps[temp_idx++] = temp;
|
||||
temp_idx &= NTC_MASK;
|
||||
temp_sum += temp;
|
||||
lastTemp = temp;
|
||||
m_nTemp = temp_sum / NTC_COUNT;
|
||||
}
|
||||
32
NTC_10K.h
Normal file
32
NTC_10K.h
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
#ifndef __NTC_H
|
||||
#define __NTC_H
|
||||
|
||||
#ifndef rRef
|
||||
#define rRef 10000
|
||||
#endif
|
||||
|
||||
#define NTC_COUNT 32
|
||||
#define NTC_MASK (NTC_COUNT - 1)
|
||||
|
||||
class NTC_10K {
|
||||
private:
|
||||
//lookup table
|
||||
//float resistance[64];
|
||||
//int16_t temp_C[64];
|
||||
int16_t temps[NTC_COUNT];
|
||||
int16_t temp_sum;
|
||||
int16_t temp_idx;
|
||||
|
||||
float _vRef;
|
||||
int _RESO;
|
||||
bool m_bNegativePolarity;
|
||||
int16_t m_nTemp;
|
||||
|
||||
public:
|
||||
void setup(bool bNegativePolarity);
|
||||
void readSensor();
|
||||
inline int16_t getTemp() { return m_nTemp; };
|
||||
};
|
||||
|
||||
extern NTC_10K ntc;
|
||||
#endif
|
||||
134
OTA.cpp
Normal file
134
OTA.cpp
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
#define NO_GLOBAL_UPDATE
|
||||
#include <string.h>
|
||||
#include <WiFi.h>
|
||||
#include <WiFiClientSecure.h>
|
||||
#include <HTTPClient.h>
|
||||
#include <esp_task_wdt.h>
|
||||
|
||||
#include "HCUpdate.h"
|
||||
#include "HermitCrab.h"
|
||||
#include "Config.h"
|
||||
#include "ConnectWiFi.h"
|
||||
|
||||
#define TAG_OTA "OTA"
|
||||
|
||||
// ==============================================================
|
||||
//
|
||||
// OTA
|
||||
//
|
||||
// ==============================================================
|
||||
const char *HC__VERSION = "20250405001";
|
||||
#define UPDATE_PORT ((uint16_t) 443)
|
||||
String url = "visionsoft.kr";
|
||||
String uri = "/hc/hc_firmware_update.php";
|
||||
const char *HTTPUPDATE_USERAGRENT = "ESP32-http-Update";
|
||||
const char *COMPANY_NAME = "VisionSoft";
|
||||
const char *SERVICE_NAME = "HermitCrab";
|
||||
|
||||
const char* rootCACertificate = \
|
||||
"-----BEGIN CERTIFICATE-----\n" \
|
||||
"MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw\n" \
|
||||
"TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh\n" \
|
||||
"cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4\n" \
|
||||
"WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu\n" \
|
||||
"ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY\n" \
|
||||
"MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc\n" \
|
||||
"h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+\n" \
|
||||
"0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U\n" \
|
||||
"A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW\n" \
|
||||
"T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH\n" \
|
||||
"B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC\n" \
|
||||
"B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv\n" \
|
||||
"KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn\n" \
|
||||
"OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn\n" \
|
||||
"jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw\n" \
|
||||
"qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI\n" \
|
||||
"rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV\n" \
|
||||
"HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq\n" \
|
||||
"hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL\n" \
|
||||
"ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ\n" \
|
||||
"3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK\n" \
|
||||
"NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5\n" \
|
||||
"ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur\n" \
|
||||
"TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC\n" \
|
||||
"jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc\n" \
|
||||
"oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq\n" \
|
||||
"4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA\n" \
|
||||
"mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d\n" \
|
||||
"emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=\n" \
|
||||
"-----END CERTIFICATE-----\n";
|
||||
|
||||
String getSketchSHA256(); // Function to retrieve current sketch hash
|
||||
|
||||
// Callback function for OTA progress
|
||||
void onOTAProgress(int current, int total) {
|
||||
ESP_LOGD(TAG_OTA,"OTA -- Progress: %d%%\n", (current * 100) / total);
|
||||
}
|
||||
|
||||
//==========================================================================
|
||||
String urlEncode(const String &url, const char *safeChars = "-_.~") {
|
||||
String encoded = "";
|
||||
char temp[4];
|
||||
|
||||
for (int i = 0; i < url.length(); i++) {
|
||||
temp[0] = url.charAt(i);
|
||||
if (temp[0] == 32) { //space
|
||||
encoded.concat('+');
|
||||
} else if ((temp[0] >= 48 && temp[0] <= 57) /*0-9*/
|
||||
|| (temp[0] >= 65 && temp[0] <= 90) /*A-Z*/
|
||||
|| (temp[0] >= 97 && temp[0] <= 122) /*a-z*/
|
||||
|| (strchr(safeChars, temp[0]) != NULL) /* "=&-_.~" */
|
||||
) {
|
||||
encoded.concat(temp[0]);
|
||||
} else { //character needs encoding
|
||||
snprintf(temp, 4, "%%%02X", temp[0]);
|
||||
encoded.concat(temp);
|
||||
}
|
||||
}
|
||||
return encoded;
|
||||
}
|
||||
|
||||
//==========================================================================
|
||||
bool addQuery(String *query, const String name, const String value) {
|
||||
if (name.length() && value.length()) {
|
||||
if (query->length() < 3) {
|
||||
*query = "?";
|
||||
} else {
|
||||
query->concat('&');
|
||||
}
|
||||
query->concat(urlEncode(name));
|
||||
query->concat('=');
|
||||
query->concat(urlEncode(value));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
//==========================================================================
|
||||
bool checkOTA(bool bForceUpdate)
|
||||
{
|
||||
|
||||
// Set callbacks
|
||||
//Update.onStart(onOTAStart);
|
||||
//Update.onEnd(onOTAEnd);
|
||||
String query = "";
|
||||
addQuery(&query, "cmd", (bForceUpdate ? "download" : "check")); //action command
|
||||
uri.concat(query);
|
||||
String version = String(HC__VERSION);
|
||||
ESP_LOGI(TAG_OTA,"OTA - URL: %s\n", url.c_str());
|
||||
ESP_LOGI(TAG_OTA,"OTA - URI: %s\n", uri.c_str());
|
||||
ESP_LOGI(TAG_OTA,"OTA - Ver: %s\n", HC__VERSION);
|
||||
|
||||
|
||||
WiFiClientSecure client;
|
||||
client.setCACert(rootCACertificate);
|
||||
UpdateClass hcUpdate(5000);
|
||||
hcUpdate.onProgress(onOTAProgress);
|
||||
//int update(WiFiClient& client, String &url, uint16_t port, String& uri,
|
||||
// String ¤tVersion, short nDeviceType, bool bForceUpdate);
|
||||
int result = hcUpdate.update(client, url, UPDATE_PORT, uri, version, (short)config.m_nDeviceType, true);
|
||||
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
10
OTA.h
Normal file
10
OTA.h
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
#ifndef __OTA_H
|
||||
#define __OTA_H
|
||||
|
||||
extern const char *COMPANY_NAME;
|
||||
extern const char *SERVICE_NAME;
|
||||
extern const char *HC__VERSION;
|
||||
extern const char* rootCACertificate;
|
||||
bool checkOTA(bool bForceUpdate);
|
||||
|
||||
#endif
|
||||
225
SSD1306.cpp
Normal file
225
SSD1306.cpp
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
#include "HermitCrab.h"
|
||||
#include "SSD1306.h"
|
||||
|
||||
//#define DISPLAY_WIDTH SCREEN_WIDTH
|
||||
//#define DISPLAY_HEIGHT SCREEN_HEIGHT
|
||||
|
||||
|
||||
#define WIRE_WRITE Wire.write
|
||||
#define SETWIRECLOCK Wire.setClock(400000UL) ///< Set before I2C transfer
|
||||
#define RESWIRECLOCK Wire.setClock(100000UL) ///< Restore after I2C xfer
|
||||
#define TRANSACTION_START SETWIRECLOCK
|
||||
#define TRANSACTION_END RESWIRECLOCK
|
||||
#define WIRE_MAX I2C_BUFFER_LENGTH
|
||||
|
||||
|
||||
// Constructor for SSD1306 class
|
||||
//SSD1306::SSD1306(Adafruit_SSD1306 &display)
|
||||
SSD1306::SSD1306()
|
||||
: Adafruit_GFX(SCREEN_WIDTH, SCREEN_HEIGHT)
|
||||
, i2caddr(DISPLAY_I2C_ADDRESS)
|
||||
{}
|
||||
|
||||
void SSD1306::dim(bool dim) {
|
||||
// the range of contrast to too small to be really useful
|
||||
// it is useful to dim the display
|
||||
TRANSACTION_START;
|
||||
ssd1306_command1(SSD1306_SETCONTRAST);
|
||||
ssd1306_command1(dim ? 0 : contrast);
|
||||
TRANSACTION_END;
|
||||
}
|
||||
|
||||
void SSD1306::setContrast(uint8_t con) {
|
||||
// the range of contrast to too small to be really useful
|
||||
// it is useful to dim the display
|
||||
TRANSACTION_START;
|
||||
ssd1306_command1(SSD1306_SETCONTRAST);
|
||||
ssd1306_command1(100 * con / contrast);
|
||||
TRANSACTION_END;
|
||||
}
|
||||
|
||||
bool SSD1306::begin(uint8_t vcs, uint8_t addr) {
|
||||
vccstate = vcs;
|
||||
i2caddr = addr;
|
||||
|
||||
TRANSACTION_START;
|
||||
|
||||
// Init sequence
|
||||
static const uint8_t init1[] = {SSD1306_DISPLAYOFF, // 0xAE
|
||||
SSD1306_SETDISPLAYCLOCKDIV, // 0xD5
|
||||
0x80, // the suggested ratio 0x80
|
||||
SSD1306_SETMULTIPLEX,
|
||||
SCREEN_HEIGHT - 1}; // 0xA8
|
||||
ssd1306_commandList(init1, sizeof(init1));
|
||||
|
||||
static const uint8_t init2[] = {SSD1306_SETDISPLAYOFFSET, // 0xD3
|
||||
0x0, // no offset
|
||||
SSD1306_SETSTARTLINE | 0x0, // line #0
|
||||
SSD1306_CHARGEPUMP}; // 0x8D
|
||||
ssd1306_commandList(init2, sizeof(init2));
|
||||
|
||||
ssd1306_command1((vccstate == SSD1306_EXTERNALVCC) ? 0x10 : 0x14);
|
||||
|
||||
static const uint8_t init3[] = {SSD1306_MEMORYMODE, // 0x20
|
||||
0x00, // 0x0 act like ks0108
|
||||
SSD1306_SEGREMAP | 0x1,
|
||||
SSD1306_COMSCANDEC};
|
||||
ssd1306_commandList(init3, sizeof(init3));
|
||||
|
||||
uint8_t comPins = 0x02;
|
||||
contrast = 0x8F;
|
||||
|
||||
if ((WIDTH == 128) && (HEIGHT == 32)) {
|
||||
comPins = 0x02;
|
||||
contrast = 0x8F;
|
||||
} else if ((WIDTH == 128) && (HEIGHT == 64)) {
|
||||
comPins = 0x12;
|
||||
contrast = (vccstate == SSD1306_EXTERNALVCC) ? 0x9F : 0xCF;
|
||||
} else if ((WIDTH == 96) && (HEIGHT == 16)) {
|
||||
comPins = 0x2; // ada x12
|
||||
contrast = (vccstate == SSD1306_EXTERNALVCC) ? 0x10 : 0xAF;
|
||||
} else {
|
||||
// Other screen varieties -- TBD
|
||||
}
|
||||
|
||||
ssd1306_command1(SSD1306_SETCOMPINS);
|
||||
ssd1306_command1(comPins);
|
||||
ssd1306_command1(SSD1306_SETCONTRAST);
|
||||
ssd1306_command1(contrast);
|
||||
|
||||
ssd1306_command1(SSD1306_SETPRECHARGE); // 0xd9
|
||||
ssd1306_command1((vccstate == SSD1306_EXTERNALVCC) ? 0x22 : 0xF1);
|
||||
static const uint8_t init5[] = {
|
||||
SSD1306_SETVCOMDETECT, // 0xDB
|
||||
0x40,
|
||||
SSD1306_DISPLAYALLON_RESUME, // 0xA4
|
||||
SSD1306_NORMALDISPLAY, // 0xA6
|
||||
SSD1306_DEACTIVATE_SCROLL,
|
||||
SSD1306_DISPLAYON}; // Main screen turn on
|
||||
ssd1306_commandList(init5, sizeof(init5));
|
||||
|
||||
TRANSACTION_END;
|
||||
clearDisplayBuffer();
|
||||
return true;
|
||||
}
|
||||
|
||||
MY_IRAM_ATTR void SSD1306::updateScreen() {
|
||||
TRANSACTION_START;
|
||||
uint8_t dlist1[6];
|
||||
dlist1[0] = SSD1306_PAGEADDR;
|
||||
dlist1[1] = 0;
|
||||
dlist1[2] = (height() / 8) - 1;
|
||||
dlist1[3] = SSD1306_COLUMNADDR;
|
||||
dlist1[4] = 0;
|
||||
dlist1[5] = width() - 1;
|
||||
ssd1306_commandList(dlist1, sizeof(dlist1));
|
||||
|
||||
uint16_t count = height() * width() / 8;
|
||||
uint8_t *ptr = getBuffer();
|
||||
|
||||
Wire.beginTransmission(i2caddr);
|
||||
WIRE_WRITE((uint8_t)0x40);
|
||||
uint16_t bytesOut = 1;
|
||||
// Loop through each page in the range
|
||||
while (count--) {
|
||||
if (bytesOut >= WIRE_MAX) {
|
||||
Wire.endTransmission();
|
||||
Wire.beginTransmission(i2caddr);
|
||||
WIRE_WRITE((uint8_t)0x40);
|
||||
bytesOut = 1;
|
||||
}
|
||||
WIRE_WRITE(*ptr++);
|
||||
bytesOut++;
|
||||
}
|
||||
Wire.endTransmission(); // End transmission for the page
|
||||
|
||||
TRANSACTION_END;
|
||||
}
|
||||
|
||||
MY_IRAM_ATTR void SSD1306::updateRegion(uint8_t pageStart, uint8_t pageEnd, uint8_t colStart = 0, uint8_t colEnd = 127) {
|
||||
TRANSACTION_START;
|
||||
uint8_t dlist1[6];
|
||||
dlist1[0] = SSD1306_PAGEADDR;
|
||||
dlist1[1] = pageStart;
|
||||
dlist1[2] = pageEnd;
|
||||
dlist1[3] = SSD1306_COLUMNADDR;
|
||||
dlist1[4] = colStart;
|
||||
dlist1[5] = colEnd;
|
||||
ssd1306_commandList(dlist1, sizeof(dlist1));
|
||||
|
||||
uint16_t count = (colEnd - colStart + 1) * (pageEnd - pageStart + 1);
|
||||
uint8_t *ptr = getBuffer() + pageStart * SCREEN_WIDTH + colStart;
|
||||
uint16_t cols = colEnd - colStart + 1;
|
||||
uint16_t offset = SCREEN_WIDTH - cols;
|
||||
|
||||
Wire.beginTransmission(i2caddr);
|
||||
WIRE_WRITE((uint8_t)0x40);
|
||||
uint16_t bytesOut = 1;
|
||||
// Loop through each page in the range
|
||||
for (uint8_t page = pageStart; page <= pageEnd; page++) {
|
||||
// Send column data for the current page
|
||||
for (uint8_t col = 0; col < cols; col++) {
|
||||
if (bytesOut >= WIRE_MAX) {
|
||||
Wire.endTransmission();
|
||||
Wire.beginTransmission(i2caddr);
|
||||
WIRE_WRITE((uint8_t)0x40);
|
||||
bytesOut = 1;
|
||||
}
|
||||
WIRE_WRITE(*ptr++);
|
||||
bytesOut++;
|
||||
}
|
||||
ptr += offset;
|
||||
}
|
||||
Wire.endTransmission(); // End transmission for the page
|
||||
TRANSACTION_END;
|
||||
}
|
||||
|
||||
MY_IRAM_ATTR void SSD1306::ssd1306_hscroll(uint8_t dir, uint8_t pageStart, uint8_t pageEnd, uint8_t offset, uint8_t interval) {
|
||||
ssd1306_command1(dir ? SSD1306_RIGHT_HORIZONTAL_SCROLL : SSD1306_LEFT_HORIZONTAL_SCROLL);
|
||||
ssd1306_command1(0x00); // Dummy
|
||||
ssd1306_command1(pageStart);
|
||||
ssd1306_command1(interval);
|
||||
ssd1306_command1(pageEnd);
|
||||
ssd1306_command1(offset);
|
||||
ssd1306_command1(SSD1306_ACTIVATE_SCROLL);
|
||||
}
|
||||
|
||||
MY_IRAM_ATTR void SSD1306::drawPixel(int16_t x, int16_t y, uint16_t color) {
|
||||
if ((x >= 0) && (x < width()) && (y >= 0) && (y < height())) {
|
||||
switch (color) {
|
||||
case SSD1306_WHITE:
|
||||
buffer[x + (y / 8) * WIDTH] |= (1 << (y & 7));
|
||||
break;
|
||||
case SSD1306_BLACK:
|
||||
buffer[x + (y / 8) * WIDTH] &= ~(1 << (y & 7));
|
||||
break;
|
||||
case SSD1306_INVERSE:
|
||||
buffer[x + (y / 8) * WIDTH] ^= (1 << (y & 7));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MY_IRAM_ATTR void SSD1306::ssd1306_commandList(const uint8_t *c, uint8_t n) {
|
||||
Wire.beginTransmission(i2caddr);
|
||||
WIRE_WRITE((uint8_t)0x00); // Co = 0, D/C = 0
|
||||
uint16_t bytesOut = 1;
|
||||
while (n--) {
|
||||
if (bytesOut >= WIRE_MAX) {
|
||||
Wire.endTransmission();
|
||||
Wire.beginTransmission(i2caddr);
|
||||
WIRE_WRITE((uint8_t)0x00); // Co = 0, D/C = 0
|
||||
bytesOut = 1;
|
||||
}
|
||||
WIRE_WRITE(*c++);
|
||||
bytesOut++;
|
||||
}
|
||||
Wire.endTransmission();
|
||||
}
|
||||
|
||||
MY_IRAM_ATTR void SSD1306::ssd1306_command1(uint8_t c) {
|
||||
Wire.beginTransmission(i2caddr);
|
||||
WIRE_WRITE((uint8_t)0x00); // Co = 0, D/C = 0
|
||||
WIRE_WRITE(c);
|
||||
Wire.endTransmission();
|
||||
}
|
||||
86
SSD1306.h
Normal file
86
SSD1306.h
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
#ifndef SSD1306_H
|
||||
#define SSD1306_H
|
||||
|
||||
#include <Adafruit_GFX.h>
|
||||
#include <Wire.h>
|
||||
|
||||
#define SCREEN_WIDTH 128
|
||||
#define SCREEN_HEIGHT 64
|
||||
|
||||
#define DISPLAY_I2C_ADDRESS_32 (0x3C)
|
||||
#define DISPLAY_I2C_ADDRESS_64 (0x3D)
|
||||
#define DISPLAY_I2C_ADDRESS DISPLAY_I2C_ADDRESS_32
|
||||
|
||||
#define SSD1306_BLACK 0 ///< Draw 'off' pixels
|
||||
#define SSD1306_WHITE 1 ///< Draw 'on' pixels
|
||||
#define SSD1306_INVERSE 2 ///< Invert pixels
|
||||
#define SSD1306_MEMORYMODE 0x20 ///< See datasheet
|
||||
#define SSD1306_COLUMNADDR 0x21 ///< See datasheet
|
||||
#define SSD1306_PAGEADDR 0x22 ///< See datasheet
|
||||
#define SSD1306_SETCONTRAST 0x81 ///< See datasheet
|
||||
#define SSD1306_CHARGEPUMP 0x8D ///< See datasheet
|
||||
#define SSD1306_SEGREMAP 0xA0 ///< See datasheet
|
||||
#define SSD1306_DISPLAYALLON_RESUME 0xA4 ///< See datasheet
|
||||
#define SSD1306_DISPLAYALLON 0xA5 ///< Not currently used
|
||||
#define SSD1306_NORMALDISPLAY 0xA6 ///< See datasheet
|
||||
#define SSD1306_INVERTDISPLAY 0xA7 ///< See datasheet
|
||||
#define SSD1306_SETMULTIPLEX 0xA8 ///< See datasheet
|
||||
#define SSD1306_DISPLAYOFF 0xAE ///< See datasheet
|
||||
#define SSD1306_DISPLAYON 0xAF ///< See datasheet
|
||||
#define SSD1306_COMSCANINC 0xC0 ///< Not currently used
|
||||
#define SSD1306_COMSCANDEC 0xC8 ///< See datasheet
|
||||
#define SSD1306_SETDISPLAYOFFSET 0xD3 ///< See datasheet
|
||||
#define SSD1306_SETDISPLAYCLOCKDIV 0xD5 ///< See datasheet
|
||||
#define SSD1306_SETPRECHARGE 0xD9 ///< See datasheet
|
||||
#define SSD1306_SETCOMPINS 0xDA ///< See datasheet
|
||||
#define SSD1306_SETVCOMDETECT 0xDB ///< See datasheet
|
||||
|
||||
#define SSD1306_SETLOWCOLUMN 0x00 ///< Not currently used
|
||||
#define SSD1306_SETHIGHCOLUMN 0x10 ///< Not currently used
|
||||
#define SSD1306_SETSTARTLINE 0x40 ///< See datasheet
|
||||
|
||||
#define SSD1306_EXTERNALVCC 0x01 ///< External display voltage source
|
||||
#define SSD1306_SWITCHCAPVCC 0x02 ///< Gen. display voltage from 3.3V
|
||||
|
||||
#define SSD1306_RIGHT_HORIZONTAL_SCROLL 0x26 ///< Init rt scroll
|
||||
#define SSD1306_LEFT_HORIZONTAL_SCROLL 0x27 ///< Init left scroll
|
||||
#define SSD1306_VERTICAL_AND_RIGHT_HORIZONTAL_SCROLL 0x29 ///< Init diag scroll
|
||||
#define SSD1306_VERTICAL_AND_LEFT_HORIZONTAL_SCROLL 0x2A ///< Init diag scroll
|
||||
#define SSD1306_DEACTIVATE_SCROLL 0x2E ///< Stop scroll
|
||||
#define SSD1306_ACTIVATE_SCROLL 0x2F ///< Start scroll
|
||||
#define SSD1306_SET_VERTICAL_SCROLL_AREA 0xA3 ///< Set scroll range
|
||||
|
||||
#define SSD1306_LCDWIDTH SCREEN_WIDTH ///< DEPRECATED: width w/SSD1306_128_64 defined
|
||||
#define SSD1306_LCDHEIGHT SCREEN_HEIGHT ///< DEPRECATED: height w/SSD1306_128_64 defined
|
||||
|
||||
class SSD1306 : public Adafruit_GFX {
|
||||
public:
|
||||
// Constructor
|
||||
//CUI(Adafruit_SSD1306 &display);
|
||||
SSD1306();
|
||||
void dim(bool dim);
|
||||
void setContrast(uint8_t contrast);
|
||||
|
||||
protected:
|
||||
bool bOK;
|
||||
uint8_t buffer[SCREEN_WIDTH * SCREEN_HEIGHT / 8];
|
||||
int8_t i2caddr; ///< I2C address initialized when begin method is called.
|
||||
uint8_t contrast; ///< normal contrast setting for this device
|
||||
|
||||
bool begin(uint8_t switchvcc = SSD1306_SWITCHCAPVCC, uint8_t i2caddr = DISPLAY_I2C_ADDRESS);
|
||||
inline uint8_t *getBuffer(void) { return buffer; };
|
||||
inline void clearDisplayBuffer(void) { memset(buffer, 0, WIDTH * ((HEIGHT + 7) / 8)); };
|
||||
void updateScreen();
|
||||
void updateRegion(uint8_t pageStart, uint8_t pageEnd, uint8_t colStart , uint8_t colEnd);
|
||||
void ssd1306_hscroll(uint8_t dir, uint8_t pageStart, uint8_t pageEnd, uint8_t offset, uint8_t interval);
|
||||
|
||||
private:
|
||||
int8_t vccstate; ///< VCC selection, set by begin method.
|
||||
uint32_t wireClk; ///< Wire speed for SSD1306 transfers
|
||||
uint32_t restoreClk; ///< Wire speed following SSD1306 transfers
|
||||
|
||||
virtual void drawPixel(int16_t x, int16_t y, uint16_t color);
|
||||
void ssd1306_commandList(const uint8_t *c, uint8_t n);
|
||||
void ssd1306_command1(uint8_t c);
|
||||
};
|
||||
#endif
|
||||
302
Setup.cpp
Normal file
302
Setup.cpp
Normal file
|
|
@ -0,0 +1,302 @@
|
|||
#include "HermitCrab.h"
|
||||
#include "Config.h"
|
||||
#include "AHT2x.h"
|
||||
#include "NTC_10K.h"
|
||||
#include "ZCD.h"
|
||||
#include "History.h"
|
||||
#include "ConnectWiFi.h"
|
||||
#include "WiFiHost.h"
|
||||
#include "TimeManager.h"
|
||||
#include "OTA.h"
|
||||
#include "UI.h"
|
||||
#include "LED0.h"
|
||||
#include "BLEScan.h"
|
||||
#include <esp_wifi.h>
|
||||
#include "esp_coexist.h"
|
||||
|
||||
#define TAG_SETUP "TAG_SETUP"
|
||||
// Task handle
|
||||
TaskHandle_t TaskHandle_0;
|
||||
|
||||
bool g_bWiFiSetupExecuted = false;
|
||||
bool g_bWiFiHasBeenConnected = false;
|
||||
|
||||
extern STATUS_TYPE status;
|
||||
extern CHistory history;
|
||||
|
||||
void setupConfig();
|
||||
void setupStatus();
|
||||
void restoreStatus();
|
||||
void setupWiFi();
|
||||
void setupPostWiFi(bool bBoot);
|
||||
void setupPins();
|
||||
void setupSensor();
|
||||
void setupZCD();
|
||||
void setup_BLE();
|
||||
void scanI2C();
|
||||
|
||||
void setup() {
|
||||
// put your setup code here, to run once:
|
||||
#ifdef DEBUG
|
||||
Serial.begin(115200);
|
||||
#endif
|
||||
//esp_log_level_set("*", ESP_LOG_INFO); // Global log level
|
||||
//esp_log_level_set("BLE_POLL", ESP_LOG_INFO); // Module-specific level
|
||||
//esp_coex_preference_set(ESP_COEX_PREFER_BT);
|
||||
|
||||
DPRINTLN(" **********************");
|
||||
DPRINTF(" SETUP - Start - ver. %s type: %d\n", HC__VERSION, THIS_DEVICE_TYPE);
|
||||
DPRINTLN(" **********************");
|
||||
g_bWiFiHasBeenConnected = false;
|
||||
g_bWiFiHasBeenConnected = false;
|
||||
g_nYear = 2024;
|
||||
g_nMonth = 10;
|
||||
g_nDay = 15;
|
||||
g_nHour = 0;
|
||||
g_nMinute = 0;
|
||||
g_nSecond = 0;
|
||||
bShowSensor = false;
|
||||
|
||||
led0.setup(PIN_LED_WIFI, PWM_AP_FREQ, PWM_AP_CHANNEL);
|
||||
led0.setDuty(20);
|
||||
|
||||
setupConfig();
|
||||
setupStatus();
|
||||
|
||||
setupPins();
|
||||
scanI2C();
|
||||
setupSensor();
|
||||
|
||||
ui.setup();
|
||||
ui.message(0, "WiFi...");
|
||||
setupWiFi();
|
||||
|
||||
if (aht25.sensor() || aht10_0x39.sensor()) {
|
||||
ui.message(4, "Sensor... OK!");
|
||||
} else {
|
||||
ui.message(4, "Sensor... None!");
|
||||
}
|
||||
|
||||
ui.message(5, "ZCD...");
|
||||
setupZCD();
|
||||
ui.message(5, "ZCD... OK!");
|
||||
|
||||
ui.message(6, "Setup OK!");
|
||||
//if (!isWiFiConnected) delay(3000);
|
||||
ble.setupConnect(config.nBLESensorAddr, config.nBLESensorAddr2);
|
||||
|
||||
// Restore Status
|
||||
restoreStatus();
|
||||
|
||||
// Create a task pinned to core 0
|
||||
xTaskCreatePinnedToCore(
|
||||
core0Task, // Function to run as a task
|
||||
"Task0", // Task name
|
||||
10240, // Stack size in words
|
||||
NULL, // Task input parameter
|
||||
0, // Priority of the task
|
||||
&TaskHandle_0, // Task handle
|
||||
0 // Core 0
|
||||
);
|
||||
DPRINTLN("Setup Completed\n========================\n");
|
||||
}
|
||||
|
||||
|
||||
// ======================================================================
|
||||
//
|
||||
// Setup
|
||||
//
|
||||
// ======================================================================
|
||||
void setupConfig() {
|
||||
config.load();
|
||||
history.loadPID();
|
||||
config.m_nDeviceType = THIS_DEVICE_TYPE;
|
||||
if (config.m_nPublicPort == 3939)
|
||||
config.m_nPublicPort = (uint16_t)(config.m_nChipId & 0xFFFF);
|
||||
}
|
||||
|
||||
void setupWiFi() {
|
||||
// Set WiFiEvent
|
||||
WiFi.onEvent(WiFiEvent);
|
||||
strncpy(BLE_SSID, config.ssid, sizeof(BLE_SSID));
|
||||
strncpy(BLE_PW, config.pw, sizeof(BLE_PW));
|
||||
// Connect WiFi for OTA
|
||||
if (config.ssid[0] && config.pw[0]) {
|
||||
#if defined(ESP32)
|
||||
esp_wifi_set_max_tx_power(84);
|
||||
#elif defined(ESP8266)
|
||||
WiFi.setOutputPower(20.5f);
|
||||
pinMode(16,OUTPUT);
|
||||
digitalWrite(16, LOW);
|
||||
int c = 0;
|
||||
#endif
|
||||
|
||||
DPRINTF("BOOT: Connecting to WiFi: SSID: '%s', PW: '%s'\n", config.ssid, config.pw);
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.begin(config.ssid, config.pw);
|
||||
|
||||
unsigned long beginTime = millis();
|
||||
while (WiFi.status() != WL_CONNECTED) {
|
||||
delay(250);
|
||||
DPRINT('.');
|
||||
if (millis() - beginTime > 30000)
|
||||
break;
|
||||
}
|
||||
DPRINTLN();
|
||||
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
ledcWrite(PIN_LED_WIFI, PWM_FULL); // LED_OFF
|
||||
ui.message(0, "WiFi...OK!");
|
||||
DPRINTLN("WiFi - Connected at SETUP");
|
||||
DPRINTF("WiFi - SSID(%s) PW(%s) IP(%s)\n", config.ssid, config.pw, WiFi.localIP().toString().c_str());
|
||||
g_bWiFiHasBeenConnected = true;
|
||||
|
||||
setupPostWiFi(true);
|
||||
} else {
|
||||
DPRINTLN("WiFi - ** NOT ** Connected at SETUP.");
|
||||
//WiFi.disconnect(false, true, 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void setupPostWiFi(bool bBoot = false) {
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
// Time
|
||||
if (bBoot) ui.message(1, "Time...");
|
||||
timeManager.begin();
|
||||
vTaskDelay((bBoot ? 500 : 250)/portTICK_PERIOD_MS);
|
||||
timeManager.checkNTPResponse();
|
||||
|
||||
if (bBoot) {
|
||||
if (timeManager.hasNTPUpdate()) {
|
||||
ui.message(1, "Time...OK!");
|
||||
|
||||
// OTA
|
||||
DPRINTLN("Setup - TimeManager.begin()");
|
||||
DPRINTLN("\n===============================\n");
|
||||
DPRINTLN(" Trying OTA");
|
||||
ui.message(2, "Update check...");
|
||||
checkOTA(true);
|
||||
ui.message(2, "Update check...OK!");
|
||||
DPRINTLN(" OTA Process completed!");
|
||||
DPRINTLN("===============================\n");
|
||||
} else {
|
||||
ui.message(2, "Update check SKIPPED!");
|
||||
timeManager.sendNTPRequest();
|
||||
}
|
||||
}
|
||||
|
||||
// Host
|
||||
if (bBoot) ui.message(3, "Server...");
|
||||
host.Setup();
|
||||
DPRINTLN("Setup - host.Setup()");
|
||||
if (bBoot) ui.message(3, "Server...OK!");
|
||||
g_bWiFiSetupExecuted = true;
|
||||
}
|
||||
}
|
||||
|
||||
void setupPins() {
|
||||
pinMode(PIN_HEATER1, OUTPUT);
|
||||
pinMode(PIN_HEATER2, OUTPUT);
|
||||
digitalWrite(PIN_HEATER1, HEATER_OFF);
|
||||
digitalWrite(PIN_HEATER2, HEATER_OFF);
|
||||
|
||||
//ledcAttachChannel(PIN_LED_WIFI, PWM_AP_FREQ, PWM_RESOLUTION, PWM_AP_CHANNEL);
|
||||
//ledcWrite(PIN_LED_WIFI, PWM_FULL * 4 / 5); // Light Blink
|
||||
|
||||
ledcAttachChannel(PIN_MIST, PWM_MIST_FREQ, PWM_RESOLUTION, PWM_MIST_CHANNEL);
|
||||
ledcWrite(PIN_MIST, PWM_OFF);
|
||||
|
||||
ledcAttachChannel(PIN_LED_HEATER1, PWM_1KHZ_FREQ, PWM_RESOLUTION, PWM_HEATER1_CHANNEL);
|
||||
ledcAttachChannel(PIN_LED_HEATER2, PWM_1KHZ_FREQ, PWM_RESOLUTION, PWM_HEATER2_CHANNEL);
|
||||
ledcAttachChannel(PIN_LIGHT, PWM_1KHZ_FREQ, PWM_RESOLUTION, PWM_LIGHT_CHANNEL);
|
||||
|
||||
ledcAttachChannel(PIN_MOTOR, PWM_25KHZ_FREQ, PWM_RESOLUTION, PWM_MOTOR_CHANNEL);
|
||||
ledcAttachChannel(PIN_FAN, PWM_25KHZ_FREQ, PWM_RESOLUTION, PWM_FAN_CHANNEL);
|
||||
|
||||
ledcWrite(PIN_LED_HEATER1, PWM_FULL - PWM_OFF);
|
||||
ledcWrite(PIN_LED_HEATER2, PWM_FULL - PWM_OFF);
|
||||
ledcWrite(PIN_LIGHT, PWM_OFF);
|
||||
ledcWrite(PIN_MOTOR, PWM_OFF);
|
||||
ledcWrite(PIN_FAN, PWM_OFF);
|
||||
}
|
||||
|
||||
void setupStatus() {
|
||||
if (config.bStatusSaved) {
|
||||
status = config.statusSave;
|
||||
config.bStatusSaved = false;
|
||||
}
|
||||
|
||||
|
||||
// init sensor and counter
|
||||
status.nTemp1 = 0;
|
||||
status.nTemp2 = 0;
|
||||
status.nTemp3 = 0;
|
||||
status.nHumid1 = 0;
|
||||
status.nHumid2 = 0;
|
||||
status.zcdAC = 0;
|
||||
status.zcdLoad = 0;
|
||||
status.nFlags = 0x00;
|
||||
}
|
||||
|
||||
void restoreStatus() {
|
||||
if (isWiFiConnected()) {
|
||||
if (timeManager.hasNTPUpdate()) {
|
||||
time_t now;
|
||||
time(&now);
|
||||
uint32_t gap = (uint32_t)now - config.statusSave.now;
|
||||
DPRINTF("Reboot in %.1f seconds\n", gap / 1000.0f);
|
||||
if (gap < 60000 && config.bStatusSaved) {
|
||||
status = config.statusSave;
|
||||
status.nFlags |= (uint16_t)(config.statusSave.nFlags & 0xFF);
|
||||
status.nLightDuty = 0;
|
||||
config.bStatusSaved = false;
|
||||
DPRINTLN(" Status Restored!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void scanI2C() {
|
||||
Wire.begin();
|
||||
|
||||
DPRINTLN("I2C - Scanning...");
|
||||
for (byte addr = 1; addr < 127; addr++) {
|
||||
Wire.beginTransmission(addr);
|
||||
if (Wire.endTransmission() == 0) {
|
||||
DPRINTF("I2C - Found device at address: 0x%02X\n", addr);
|
||||
}
|
||||
}
|
||||
DPRINTLN(" Scanning Done.");
|
||||
}
|
||||
|
||||
void setupSensor() {
|
||||
// AHTx0
|
||||
if (aht25.setup()) {
|
||||
delay(82);
|
||||
aht25.readSensor(millis());
|
||||
DPRINTF("AHTx0 initialized successfully at 0x38. Temp: %.2f, Humid: %.2f%%\n",
|
||||
aht25.getTemperature() / 100.0f, aht25.getHumidity() / 100.0f);
|
||||
}
|
||||
|
||||
delay(10);
|
||||
if (aht10_0x39.setup(0x39)) { //begin(PIN_SCL, PIN_SDA, AHT10_ADDRESS_0X39)) {
|
||||
delay(82);
|
||||
if (aht10_0x39.readSensor(millis())) {
|
||||
//aht10_0x39.initBuffer();
|
||||
}
|
||||
DPRINTF("AHTx0 initialized successfully at 0x39. Temp: %.2f, Humid: %.2f%%\n",
|
||||
aht10_0x39.getTemperature() / 100.0f , aht10_0x39.getHumidity() / 100.0f );
|
||||
}
|
||||
delay(10);
|
||||
|
||||
history.init(status.nTemp1, status.nHumid1);
|
||||
|
||||
if (!aht25.sensor() && !aht10_0x39.sensor()) {
|
||||
DPRINTF("AHTx0 initialization failed. SCL:%d SDA:%d\n", PIN_SCL, PIN_SDA );
|
||||
}
|
||||
|
||||
// Temp3 - NTC
|
||||
ntc.setup(config.bNTCNegativePolarity);
|
||||
status.nTemp3 = ntc.getTemp();
|
||||
}
|
||||
128
Task0.ino
Normal file
128
Task0.ino
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
#include "HermitCrab.h"
|
||||
#include "Config.h"
|
||||
#include "ConnectWiFi.h"
|
||||
#include "UI.h"
|
||||
#include <esp_task_wdt.h>
|
||||
#include <BLEScan.h>
|
||||
|
||||
#include "TimeManager.h"
|
||||
#include "WiFiHost.h"
|
||||
#include "NTC_10K.h"
|
||||
#include "AHT2x.h"
|
||||
|
||||
#define TAG_TASK0 "Task0"
|
||||
|
||||
extern bool g_bWiFiSetupExecuted;
|
||||
|
||||
void setup_BLE();
|
||||
|
||||
// ==================================================================================
|
||||
//
|
||||
// Core0 Loop - Connection and communication
|
||||
//
|
||||
// ==================================================================================
|
||||
MY_IRAM_ATTR void core0Task(void *pvParameters) {
|
||||
ESP_LOGI(TAG_TASK0,"Core 0 Task Started");
|
||||
wl_status_t lastWiFiStatus = WL_DISCONNECTED;
|
||||
unsigned long tickMillis = millis();
|
||||
unsigned long tickSecond;
|
||||
unsigned long lastTick = tickMillis / 1000;
|
||||
unsigned long lastTickMillis = tickMillis;
|
||||
unsigned long lastSensorUpdate1 = tickMillis;
|
||||
unsigned long lastSensorUpdate2 = tickMillis + 2500;
|
||||
uint16_t tick1000, lastTick1000 = tickMillis % 1000;;
|
||||
uint16_t tick250, lastTick250 = tickMillis % 250;
|
||||
uint8_t slot;
|
||||
uint8_t lastSlot = tickMillis / 50;
|
||||
|
||||
esp_task_wdt_add(NULL); // NULL for the current task
|
||||
ui.start();
|
||||
|
||||
ble.setupScan();
|
||||
|
||||
while (true) {
|
||||
esp_task_wdt_reset();
|
||||
tickMillis = millis();
|
||||
tick250 = tickMillis % 250;
|
||||
tick1000 = tickMillis % 1000;
|
||||
tickSecond = tickMillis / 1000;
|
||||
slot = tick1000 / 50;
|
||||
|
||||
//===============================================================================
|
||||
// Loop top
|
||||
// Once in a second loop
|
||||
if (tick1000 != lastTick1000) {
|
||||
if (slot != lastSlot) {
|
||||
switch (slot) {
|
||||
case 1:
|
||||
case 6:
|
||||
case 11:
|
||||
case 16: // UI Display
|
||||
ui.updateDisplayTop(tickSecond);
|
||||
break;
|
||||
case 2:
|
||||
case 7:
|
||||
case 12:
|
||||
case 17: // NTC Temp Sensor
|
||||
ntc.readSensor();
|
||||
break;
|
||||
case 3: // NTP - Time
|
||||
if (isWiFiConnected()) {
|
||||
if (timeManager.getTime(tickMillis)) {
|
||||
ESP_LOGI(TAG_TASK0,"NTP time loaded: %s", printTime());
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 4:
|
||||
case 9:
|
||||
case 14:
|
||||
case 19:
|
||||
ntc.readSensor();
|
||||
break;
|
||||
case 5: // Heartbeat
|
||||
if (isWiFiConnected()) {
|
||||
host.SendHeartBeat(tickMillis);
|
||||
}
|
||||
break;
|
||||
|
||||
case 8: // BLE
|
||||
ble.loop(tickMillis);
|
||||
break;
|
||||
|
||||
case 13: // ATH2x - 0x38
|
||||
aht25.readSensor(tickMillis);
|
||||
break;
|
||||
|
||||
case 15: // ATH0x - 0x39
|
||||
aht10_0x39.readSensor(tickMillis);
|
||||
break;
|
||||
|
||||
case 18: // UI Bottom
|
||||
ui.updateDisplayBottom(tickSecond);
|
||||
break;
|
||||
default: // 0 10
|
||||
break;
|
||||
}
|
||||
lastSlot = slot;
|
||||
}
|
||||
|
||||
lastTick1000 = tick1000;
|
||||
lastTick250 = tick250;
|
||||
} // end of - if (tick1000 != lastTick1000)
|
||||
// =====================================================
|
||||
|
||||
// Unconditional Loop
|
||||
host.MonitorUDP();
|
||||
|
||||
// Loop end
|
||||
//==========================================================================
|
||||
lastTickMillis = tickMillis;
|
||||
esp_task_wdt_reset();
|
||||
} // end of - While(True)
|
||||
|
||||
ESP_LOGI(TAG_TASK0,"Core 0 Task Exit");
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
// ==================================================================================
|
||||
// End of Core0 Loop
|
||||
// ==================================================================================
|
||||
92
TimeManager.cpp
Normal file
92
TimeManager.cpp
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
#include "HermitCrab.h"
|
||||
#include "TimeManager.h"
|
||||
#include "ConnectWiFi.h"
|
||||
#include <sys/time.h>
|
||||
#include "Config.h"
|
||||
#define TAG_TIME "Time"
|
||||
TimeManager timeManager;
|
||||
|
||||
void TimeManager::begin() {
|
||||
setenv("TZ", "UTC", 1); // Set time zone to Asia/Seoul (UTC+9)
|
||||
tzset(); // Apply the timezone setting
|
||||
|
||||
// Initialize UDP only if WiFi is connected
|
||||
if (isWiFiConnected() && !udpInitialized) {
|
||||
udpInitialized = udp.begin(localPort);
|
||||
}
|
||||
// Send an initial NTP request on startup
|
||||
sendNTPRequest();
|
||||
lastNTPRequestMillis = millis();
|
||||
}
|
||||
|
||||
void TimeManager::Stop() {
|
||||
if (udpInitialized) {
|
||||
udp.stop();
|
||||
}
|
||||
}
|
||||
|
||||
bool TimeManager::getTime(unsigned long tickMillis) {
|
||||
// Check WiFi Connection
|
||||
if (!isWiFiConnected()) {
|
||||
//ESP_LOGI(TAG_TIME,"TimeManager - getTime called while not connected");
|
||||
if (udpInitialized) udpInitialized = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if 30 minutes have passed since last NTP request
|
||||
if (udpInitialized &&
|
||||
((tickMillis - lastNTPRequestMillis >= NTP_REQUEST_INTERVAL) ||
|
||||
!bHasNTPTime && (tickMillis - lastNTPRequestMillis >= NTP_FAILED_INTERNAL))) {
|
||||
sendNTPRequest();
|
||||
lastNTPRequestMillis = tickMillis;
|
||||
}
|
||||
|
||||
// Check if there is an NTP response, and if so, update the system clock
|
||||
return checkNTPResponse();
|
||||
}
|
||||
|
||||
MY_IRAM_ATTR void TimeManager::sendNTPRequest() {
|
||||
byte ntpPacket[48] = {0};
|
||||
ntpPacket[0] = 0b11100011; // LI, Version, Mode
|
||||
ntpPacket[1] = 0; // Stratum, or type of clock
|
||||
ntpPacket[2] = 6; // Polling Interval
|
||||
ntpPacket[3] = 0xEC; // Peer Clock Precision
|
||||
|
||||
udp.beginPacket(ntpServer, 123); // NTP requests are sent to port 123
|
||||
udp.write(ntpPacket, 48);
|
||||
udp.endPacket();
|
||||
}
|
||||
|
||||
MY_IRAM_ATTR bool TimeManager::checkNTPResponse() {
|
||||
if (udp.parsePacket() == 0) {
|
||||
// No response yet
|
||||
return false;
|
||||
}
|
||||
|
||||
byte ntpBuffer[48];
|
||||
udp.read(ntpBuffer, 48);
|
||||
|
||||
// Extract and set the time if response is valid
|
||||
unsigned long epochTime = (unsigned long)ntpBuffer[40] << 24 |
|
||||
(unsigned long)ntpBuffer[41] << 16 |
|
||||
(unsigned long)ntpBuffer[42] << 8 |
|
||||
(unsigned long)ntpBuffer[43];
|
||||
epochTime -= 2208988800UL; // Convert from NTP to Unix time
|
||||
// epochTime += (9 * 3600); // GMT+9
|
||||
if (!bHasNTPTime) {
|
||||
bHasNTPTime = true;
|
||||
firstNTPTime = epochTime;
|
||||
config.m_nEpochTime = epochTime;
|
||||
}
|
||||
setSystemClock(epochTime);
|
||||
// ESP_LOGI(TAG_TIME,printTime());
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
void TimeManager::setSystemClock(unsigned long epochTime) {
|
||||
struct timeval now;
|
||||
now.tv_sec = epochTime;
|
||||
now.tv_usec = 0;
|
||||
settimeofday(&now, NULL);
|
||||
}
|
||||
33
TimeManager.h
Normal file
33
TimeManager.h
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
#ifndef TIMEMANAGER_H
|
||||
#define TIMEMANAGER_H
|
||||
|
||||
#include <WiFiUdp.h>
|
||||
|
||||
class TimeManager {
|
||||
public:
|
||||
void begin();
|
||||
void Stop();
|
||||
bool getTime(unsigned long tick); // Called every second to handle requests and updates
|
||||
inline bool hasNTPUpdate() { return bHasNTPTime; };
|
||||
inline unsigned long getFirstNTPTime() { return firstNTPTime; }
|
||||
bool checkNTPResponse();
|
||||
void sendNTPRequest();
|
||||
|
||||
private:
|
||||
WiFiUDP udp;
|
||||
const char* ntpServer = "pool.ntp.org";
|
||||
const unsigned int localPort = 2390;
|
||||
bool udpInitialized = false;
|
||||
bool bHasNTPTime = false;
|
||||
unsigned long firstNTPTime = 0;
|
||||
|
||||
|
||||
unsigned long lastNTPRequestMillis = 0;
|
||||
const unsigned long NTP_REQUEST_INTERVAL = 30 * 60 * 1000; // 30 minutes
|
||||
const unsigned long NTP_FAILED_INTERNAL = 1 * 60 * 1000; // 1 minute
|
||||
|
||||
void setSystemClock(unsigned long epochTime);
|
||||
};
|
||||
|
||||
extern TimeManager timeManager;
|
||||
#endif // TIMEMANAGER_H
|
||||
103
UI.h
Normal file
103
UI.h
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
#ifndef CUI_H
|
||||
#define CUI_H
|
||||
|
||||
#include "SSD1306.h"
|
||||
|
||||
#define WIDTH SCREEN_WIDTH
|
||||
#define HEIGHT SCREEN_HEIGHT
|
||||
#define DISPLAY_PAGE_COUNT (SCREEN_HEIGHT / 8)
|
||||
|
||||
enum ITEM_MODE {
|
||||
MODE_NORMAL,
|
||||
MODE_SET,
|
||||
MODE_CONFIG,
|
||||
};
|
||||
|
||||
enum MESSAGE_MODE {
|
||||
MODE_NONE,
|
||||
MODE_NO_WIFI,
|
||||
MODE_AC,
|
||||
MODE_LOAD
|
||||
};
|
||||
|
||||
typedef struct box_struct {
|
||||
int16_t x, y;
|
||||
uint16_t w, h;
|
||||
} BOX_TYPE;
|
||||
|
||||
class CUI : public SSD1306 {
|
||||
public:
|
||||
// Constructor
|
||||
//CUI(Adafruit_SSD1306 &display);
|
||||
CUI();
|
||||
void setup();
|
||||
void start();
|
||||
void updateDisplay(unsigned long tick);
|
||||
void updateDisplayTop(unsigned long tick);
|
||||
void updateDisplayBottom(unsigned long tick);
|
||||
void message(uint8_t lineNo, char *szMessage);
|
||||
void progress(uint8_t lineNo, uint8_t progress);
|
||||
void loopButton(unsigned long tickMillis);
|
||||
inline void setItemMode(uint16_t mode) { m_nItemMode = mode; };
|
||||
|
||||
// Shared variables with ISR
|
||||
volatile bool bButtonSetChanged;
|
||||
volatile bool bButtonUpChanged;
|
||||
volatile bool bButtonDownChanged;
|
||||
volatile unsigned long buttonSetChangeTime; // Time when button was pressed
|
||||
volatile unsigned long buttonUpChangeTime; // Time when button was pressed
|
||||
volatile unsigned long buttonDownChangeTime; // Time when button was pressed
|
||||
|
||||
private:
|
||||
uint16_t m_nMessageMode, lastMessageMode;
|
||||
uint16_t m_nMainMode, lastMainMode; // Temp, Humid, Clock
|
||||
uint16_t m_nItemMode, lastItemMode; // Normal, Set, Config
|
||||
uint16_t m_nItem, lastItem; // Temp, Hum, Heat ...
|
||||
int16_t m_nValue, lastValue;
|
||||
uint8_t *m_pUnit, *lastUnit;
|
||||
uint16_t m_nD0, lastD0;
|
||||
uint16_t m_nD1, lastD1;
|
||||
uint16_t m_nD2, lastD2;
|
||||
uint16_t m_nD3, lastD3;
|
||||
uint8_t *m_pDUnit, *lastpDUnit;
|
||||
BOX_TYPE boxMode, boxTitle, boxValue, boxUnit, boxD0, boxD1, boxD2, boxDot, boxD3, boxDUnit;
|
||||
|
||||
bool bDot;
|
||||
bool bButtonChanged;
|
||||
|
||||
|
||||
|
||||
// Helper function to update digits
|
||||
bool displayTop();
|
||||
bool displayBottom();
|
||||
//bool displayTemperature();
|
||||
//bool displayHumidity();
|
||||
//void displayClock() {};
|
||||
void getBoundaries();
|
||||
|
||||
// Buttons
|
||||
unsigned long buttonSetDownTime;
|
||||
unsigned long buttonUpDownTime;
|
||||
unsigned long buttonDownDownTime;
|
||||
|
||||
unsigned int buttonSetDownDuration;
|
||||
unsigned int buttonUpDownDuration;
|
||||
unsigned int buttonDownDownDuration;
|
||||
|
||||
bool bButtonSetUp; // Flag for menu button
|
||||
bool bButtonSetDown; // Flag for menu button
|
||||
|
||||
bool bButtonUpUp; // Flag for up button
|
||||
bool bButtonUpDown; // Flag for down button
|
||||
|
||||
|
||||
bool bButtonDownUp; // Flag for up button
|
||||
bool bButtonDownDown; // Flag for down button
|
||||
|
||||
void initButtonState();
|
||||
void checkButtonStates(unsigned long tickMillis);
|
||||
|
||||
};
|
||||
#endif
|
||||
|
||||
extern CUI ui;
|
||||
491
UPnPClient.cpp
Normal file
491
UPnPClient.cpp
Normal file
|
|
@ -0,0 +1,491 @@
|
|||
#include <HTTPClient.h>
|
||||
#include "HermitCrab.h"
|
||||
#include "UPnpClient.h"
|
||||
#include "Config.h"
|
||||
#include "WiFiHost.h"
|
||||
#define TAG_UPNP "UPnP"
|
||||
|
||||
|
||||
bool CUpnpClient::registerUPnP(uint32_t *pip, uint16_t *pport) {
|
||||
routerIP = *pip; // Gateway IP address
|
||||
publicPort = *pport; // host.m_nPublicPort = config.m_nPublicPort
|
||||
publicIP = 0UL;
|
||||
bool bSuccess = false;
|
||||
ESP_LOGI(TAG_UPNP,"UPnP %s(%D)\n", routerIP.toString().c_str(), publicPort);
|
||||
|
||||
// Step 1 - Discover UPnP service.
|
||||
if (discoverUPnP()) {
|
||||
// Step 2 - Check if mapping already exists
|
||||
if (!(bSuccess = requestPortMappingEntry())) {
|
||||
// Step 3 - Request new mapping
|
||||
bSuccess = requestPortForwarding();
|
||||
}
|
||||
} else {
|
||||
ESP_LOGI(TAG_UPNP," UPnP discovery failed.");
|
||||
}
|
||||
|
||||
if (bSuccess) {
|
||||
// Extract external IP
|
||||
requestExternalIP();
|
||||
ESP_LOGI(TAG_UPNP," Public IP(Port): %s(%d)\n", publicIP.toString().c_str(), publicPort);
|
||||
|
||||
|
||||
// Extract external port assigned by UPnP
|
||||
// requestExternalPort();
|
||||
// ESP_LOGI(TAG_UPNP,"Assigned External Port: %d\n", publicPort);
|
||||
|
||||
*pport = publicPort; // External server port
|
||||
*pip = publicIP; // External IP address
|
||||
return true;
|
||||
} else {
|
||||
ESP_LOGI(TAG_UPNP," UPnP PortForwarding failed.");
|
||||
}
|
||||
return bSuccess;
|
||||
}
|
||||
|
||||
bool CUpnpClient::discoverUPnP() {
|
||||
//ESP_LOGI(TAG_UPNP,"\n\n** Sending SSDP discovery request...");
|
||||
WiFiUDP udp;
|
||||
bool ret = false;
|
||||
|
||||
// SSDP M-SEARCH Request
|
||||
const char *ssdpRequest =
|
||||
"M-SEARCH * HTTP/1.1\r\n"
|
||||
"HOST: 239.255.255.250:1900\r\n"
|
||||
"MAN: \"ssdp:discover\"\r\n"
|
||||
"MX: 3\r\n"
|
||||
"ST: urn:schemas-upnp-org:device:InternetGatewayDevice:1\r\n"
|
||||
"\r\n";
|
||||
|
||||
udp.beginMulticast(SSDP_MULTICAST_IP, SSDP_PORT);
|
||||
udp.beginPacket(SSDP_MULTICAST_IP, SSDP_PORT);
|
||||
udp.write((const uint8_t *)ssdpRequest, strlen(ssdpRequest)); // Fix length
|
||||
udp.endPacket();
|
||||
|
||||
unsigned long startTime = millis();
|
||||
while (millis() - startTime < SEARCH_TIMEOUT) {
|
||||
int packetSize = udp.parsePacket();
|
||||
if (packetSize > 0) {
|
||||
udp.read(buffer, sizeof(buffer) - 1);
|
||||
buffer[packetSize] = '\0';
|
||||
|
||||
//ESP_LOGI(TAG_UPNP,"SSDP response received:");
|
||||
//ESP_LOGI(TAG_UPNP,buffer);
|
||||
|
||||
// Check if the response contains "ST: urn:schemas-upnp-org:device:InternetGatewayDevice:1"
|
||||
char *stField = strstr(buffer, "ST: urn:schemas-upnp-org:device:InternetGatewayDevice:1");
|
||||
if (stField) {
|
||||
// Extract router's UPnP service URL
|
||||
char *location = strstr(buffer, "LOCATION: ");
|
||||
if (location) {
|
||||
location += 9; // Move past "LOCATION: "
|
||||
while (*location == ' ') location++; // skip blanks
|
||||
char *end = strchr(location, '\r');
|
||||
if (end) {
|
||||
*end = '\0';
|
||||
//ESP_LOGI(TAG_UPNP,"Router UPnP URL: %s\n", location);
|
||||
routerLocation = String(location);
|
||||
|
||||
// Extract IP and Port from the Location URL
|
||||
int firstColonPos = routerLocation.indexOf(':'); // Find the first colon (for the protocol)
|
||||
int secondColonPos = routerLocation.indexOf(':', firstColonPos + 1); // Find the second colon (IP:PORT)
|
||||
|
||||
if (secondColonPos != -1) {
|
||||
routerIPString = routerLocation.substring(routerLocation.indexOf("://") + 3, secondColonPos); // Extract IP
|
||||
routerPort = routerLocation.substring(secondColonPos + 1, routerLocation.indexOf('/', secondColonPos)).toInt(); // Extract Port
|
||||
}
|
||||
|
||||
routerIP.fromString(routerIPString);
|
||||
//ESP_LOGI(TAG_UPNP,"Router UPnP IP(Port): %s(%d)\n", routerIP.toString().c_str(), routerPort);
|
||||
|
||||
String xml = fetchUPnPDescription(routerLocation);
|
||||
if (!xml.isEmpty()) {
|
||||
// ESP_LOGI(TAG_UPNP,xml);
|
||||
// ESP_LOGI(TAG_UPNP,"\n--- End Of XML ----\n");
|
||||
|
||||
// Parse the XML and get the controlURL
|
||||
// String parseXML(const String &xml);
|
||||
controlURL = parseXML(xml);
|
||||
if (!controlURL.isEmpty()) {
|
||||
ret = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
udp.stop();
|
||||
|
||||
if (!ret) {
|
||||
DPRINTLN("No SSDP response from UPnP device.");
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
String CUpnpClient::fetchUPnPDescription(const String &location) {
|
||||
HTTPClient http;
|
||||
WiFiClient client;
|
||||
|
||||
//ESP_LOGI(TAG_UPNP,"Fetching UPnP XML from: \"%s\"\n", location.c_str());
|
||||
|
||||
http.begin(client, location);
|
||||
int httpCode = http.GET();
|
||||
|
||||
if (httpCode > 0) {
|
||||
//ESP_LOGI(TAG_UPNP,"HTTP Response Code: %d\n", httpCode);
|
||||
if (httpCode == HTTP_CODE_OK) {
|
||||
String xmlContent = http.getString();
|
||||
http.end();
|
||||
return xmlContent;
|
||||
}
|
||||
}
|
||||
else {
|
||||
//ESP_LOGI(TAG_UPNP,"HTTP Request failed, error: %s\n", http.errorToString(httpCode).c_str());
|
||||
}
|
||||
|
||||
http.end();
|
||||
return "";
|
||||
}
|
||||
|
||||
String CUpnpClient::parseXML(const String &xml) {
|
||||
// Locate the WANIPConnection service
|
||||
int servicePos = xml.indexOf("<serviceType>urn:schemas-upnp-org:service:WANIPConnection:1</serviceType>");
|
||||
if (servicePos == -1) {
|
||||
ESP_LOGI(TAG_UPNP,"WANIPConnection service not found in XML.");
|
||||
return "";
|
||||
}
|
||||
|
||||
// Find the start of the controlURL tag within the <service> block
|
||||
int controlStart = xml.indexOf("<controlURL>", servicePos);
|
||||
if (controlStart == -1) {
|
||||
ESP_LOGI(TAG_UPNP,"controlURL not found in service block.");
|
||||
return "";
|
||||
}
|
||||
controlStart += 12; // Move past "<controlURL>"
|
||||
|
||||
// Find the closing tag
|
||||
int controlEnd = xml.indexOf("</controlURL>", controlStart);
|
||||
if (controlEnd == -1) {
|
||||
ESP_LOGI(TAG_UPNP,"Malformed XML: Missing </controlURL>.");
|
||||
return "";
|
||||
}
|
||||
|
||||
// Extract and clean the controlURL
|
||||
String controlURL = xml.substring(controlStart, controlEnd);
|
||||
controlURL.trim(); // Remove any spaces or newlines
|
||||
|
||||
//ESP_LOGI(TAG_UPNP,"Extracted controlURL: %s\n", controlURL.c_str());
|
||||
return controlURL;
|
||||
}
|
||||
|
||||
int CUpnpClient::sendSoapRequest(const char *request, char *response, size_t responseSize) {
|
||||
WiFiClient client;
|
||||
if (!client.connect(routerIP, routerPort)) { // UPnP uses port 1900 for SOAP requests
|
||||
ESP_LOGI(TAG_UPNP,"Failed to connect to router: %s\n", routerIP.toString().c_str());
|
||||
return -1;
|
||||
}
|
||||
|
||||
client.print("POST /control?WANIPConn1 HTTP/1.1\r\n");
|
||||
client.print("Host: ");
|
||||
client.print(routerIP);
|
||||
client.print("\r\n");
|
||||
client.print("Content-Type: text/xml; charset=\"utf-8\"\r\n");
|
||||
client.print("SOAPAction: \"urn:schemas-upnp-org:service:WANIPConnection:1#GetSpecificPortMappingEntry\"\r\n");
|
||||
client.print("Content-Length: ");
|
||||
client.print(strlen(request));
|
||||
client.print("\r\n\r\n");
|
||||
client.print(request);
|
||||
|
||||
unsigned long startMillis = millis();
|
||||
while (!client.available() && millis() - startMillis < 5000) {
|
||||
delay(10); // Wait for response
|
||||
}
|
||||
|
||||
int index = 0;
|
||||
while (client.available() && index < responseSize - 1) {
|
||||
response[index++] = client.read();
|
||||
}
|
||||
response[index] = '\0';
|
||||
|
||||
client.stop();
|
||||
return index;
|
||||
}
|
||||
|
||||
bool CUpnpClient::requestPortMappingEntry() {
|
||||
ESP_LOGI(TAG_UPNP,"** Requesting UPnP Port Mapping Entry...");
|
||||
char soapRequest[512];
|
||||
char mappingName[32];
|
||||
sprintf(mappingName, "HC_%04X", publicPort);
|
||||
|
||||
snprintf(soapRequest, sizeof(soapRequest),
|
||||
"<?xml version=\"1.0\"?>"
|
||||
"<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" "
|
||||
"s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
|
||||
"<s:Body>"
|
||||
"<u:GetSpecificPortMappingEntry xmlns:u=\"urn:schemas-upnp-org:service:WANIPConnection:1\">"
|
||||
"<NewRemoteHost></NewRemoteHost>"
|
||||
"<NewExternalPort>%d</NewExternalPort>"
|
||||
"<NewProtocol>TCP</NewProtocol>"
|
||||
"<NewInternalClient>%s</NewInternalClient>"
|
||||
"<NewPortMappingDescription>%s</NewPortMappingDescription>"
|
||||
"</u:GetSpecificPortMappingEntry>"
|
||||
"</s:Body>"
|
||||
"</s:Envelope>",
|
||||
publicPort,
|
||||
WiFi.localIP().toString().c_str(),
|
||||
mappingName);
|
||||
|
||||
char response[1024];
|
||||
int responseLen = sendSoapRequest(soapRequest, response, sizeof(response));
|
||||
|
||||
if (responseLen > 0) {
|
||||
if (strstr(response, mappingName)) {
|
||||
//ESP_LOGI(TAG_UPNP," Port mapping already exists.");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
ESP_LOGI(TAG_UPNP," Port mapping not found.");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool CUpnpClient::requestPortForwarding() {
|
||||
// Sending port-forwarding request to the router's external IP or gateway
|
||||
ESP_LOGI(TAG_UPNP,"** Requesting UPnP Port Forwarding...");
|
||||
|
||||
if (controlURL.isEmpty()) {
|
||||
ESP_LOGI(TAG_UPNP,"Error: controlURL is empty.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure controlURL is correctly formatted (handle absolute/relative cases)
|
||||
String postURL = controlURL;
|
||||
if (postURL.startsWith("http://") || postURL.startsWith("https://")) {
|
||||
int pathStart = postURL.indexOf('/', 8); // Skip "http://"
|
||||
if (pathStart != -1) {
|
||||
postURL = postURL.substring(pathStart); // Extract path only
|
||||
ESP_LOGI(TAG_UPNP,"PostURL: \"%s\"\n", postURL.c_str());
|
||||
} else {
|
||||
ESP_LOGI(TAG_UPNP,"Invalid controlURL format.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// XML body
|
||||
char xmlBody[800];
|
||||
snprintf(xmlBody, sizeof(xmlBody),
|
||||
"<?xml version=\"1.0\"?>"
|
||||
"<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
|
||||
"<s:Body><u:AddPortMapping xmlns:u=\"urn:schemas-upnp-org:service:WANIPConnection:1\">"
|
||||
"<NewRemoteHost></NewRemoteHost>"
|
||||
"<NewExternalPort>%d</NewExternalPort>"
|
||||
"<NewProtocol>TCP</NewProtocol>"
|
||||
"<NewInternalPort>%d</NewInternalPort>"
|
||||
"<NewInternalClient>%s</NewInternalClient>"
|
||||
"<NewEnabled>1</NewEnabled>"
|
||||
"<NewPortMappingDescription>HC_%04X</NewPortMappingDescription>"
|
||||
"<NewLeaseDuration>0</NewLeaseDuration>"
|
||||
"</u:AddPortMapping></s:Body></s:Envelope>",
|
||||
publicPort, publicPort,
|
||||
WiFi.localIP().toString().c_str(),
|
||||
publicPort);
|
||||
//ESP_LOGI(TAG_UPNP,"XML Body: ");
|
||||
//ESP_LOGI(TAG_UPNP,xmlBody);
|
||||
|
||||
|
||||
// Calculate the correct content length
|
||||
int contentLength = strlen(xmlBody);
|
||||
|
||||
snprintf(buffer, sizeof(buffer),
|
||||
"POST %s HTTP/1.1\r\n"
|
||||
"Host: %s:%d\r\n"
|
||||
"Content-Type: text/xml; charset=\"utf-8\"\r\n"
|
||||
"SOAPAction: \"urn:schemas-upnp-org:service:WANIPConnection:1#AddPortMapping\"\r\n"
|
||||
"Content-Length: %d\r\n"
|
||||
"Connection: Close\r\n"
|
||||
"\r\n"
|
||||
"%s", // Append XML body after headers
|
||||
controlURL.c_str(),
|
||||
routerIP.toString().c_str(), routerPort,
|
||||
contentLength,
|
||||
xmlBody);
|
||||
|
||||
//ESP_LOGI(TAG_UPNP,"==== HTTP Request ===\n\n%s\n\n--- End Of HTTP ---\n", buffer);
|
||||
|
||||
// Connect and send
|
||||
WiFiClient client;
|
||||
if (!client.connect(routerIP, routerPort)) {
|
||||
ESP_LOGI(TAG_UPNP,"Failed to connect to the router. %s:%d\n", routerIP.toString().c_str(), routerPort);
|
||||
return false;
|
||||
}
|
||||
|
||||
client.print(buffer);
|
||||
|
||||
// Read response
|
||||
String response;
|
||||
uint32_t timeout = millis() + 5000;
|
||||
while (client.available() == 0) {
|
||||
if (millis() > timeout) {
|
||||
ESP_LOGI(TAG_UPNP,"Router did not respond to port mapping request.");
|
||||
client.stop();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
while (client.available()) {
|
||||
response += client.readString();
|
||||
}
|
||||
client.stop();
|
||||
return true;
|
||||
};
|
||||
|
||||
bool CUpnpClient::requestExternalIP() {
|
||||
//ESP_LOGI(TAG_UPNP,"\n\nRequesting External IP via UPnP...");
|
||||
|
||||
// XML Request Body
|
||||
char xmlBody[300];
|
||||
snprintf(xmlBody, sizeof(xmlBody),
|
||||
"<?xml version=\"1.0\"?>"
|
||||
"<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
|
||||
"<s:Body>"
|
||||
"<u:GetExternalIPAddress xmlns:u=\"urn:schemas-upnp-org:service:WANIPConnection:1\"/>"
|
||||
"</s:Body>"
|
||||
"</s:Envelope>");
|
||||
|
||||
int contentLength = strlen(xmlBody);
|
||||
|
||||
// HTTP Request
|
||||
snprintf(buffer, sizeof(buffer),
|
||||
"POST %s HTTP/1.1\r\n"
|
||||
"Host: %s:%d\r\n"
|
||||
"Content-Type: text/xml; charset=\"utf-8\"\r\n"
|
||||
"SOAPAction: \"urn:schemas-upnp-org:service:WANIPConnection:1#GetExternalIPAddress\"\r\n"
|
||||
"Content-Length: %d\r\n"
|
||||
"Connection: Close\r\n"
|
||||
"\r\n"
|
||||
"%s",
|
||||
controlURL.c_str(),
|
||||
routerIP.toString().c_str(), routerPort,
|
||||
contentLength,
|
||||
xmlBody);
|
||||
|
||||
// Connect and Send
|
||||
WiFiClient client;
|
||||
if (!client.connect(routerIP, routerPort)) {
|
||||
DPRINTLN("Failed to connect to router for External IP request.");
|
||||
return false;
|
||||
}
|
||||
|
||||
client.print(buffer);
|
||||
|
||||
// Read Response
|
||||
String response;
|
||||
uint32_t timeout = millis() + 5000;
|
||||
while (client.available() == 0) {
|
||||
if (millis() > timeout) {
|
||||
DPRINTLN("Router did not respond to External IP request.");
|
||||
client.stop();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
while (client.available()) {
|
||||
response += client.readString();
|
||||
}
|
||||
client.stop();
|
||||
|
||||
//ESP_LOGI(TAG_UPNP,"External IP Response:");
|
||||
//ESP_LOGI(TAG_UPNP,response);
|
||||
|
||||
// Extract External IP from XML
|
||||
char *sz = (char *)(response.c_str());
|
||||
char *extIP = strstr(sz, "<NewExternalIPAddress>");
|
||||
if (extIP) {
|
||||
extIP += 22;
|
||||
char *end = strchr(extIP, '<');
|
||||
if (end) *end = '\0';
|
||||
|
||||
publicIP = IPAddress(extIP);
|
||||
DPRINTF("UPnP - External IP: %s\n", extIP);
|
||||
return true;
|
||||
}
|
||||
|
||||
DPRINTLN("Failed to extract External IP.");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool CUpnpClient::requestExternalPort() {
|
||||
//ESP_LOGI(TAG_UPNP,"\n\nRequesting External Port via UPnP...");
|
||||
|
||||
// XML Request Body for getting External Port
|
||||
char xmlBody[300];
|
||||
snprintf(xmlBody, sizeof(xmlBody),
|
||||
"<?xml version=\"1.0\"?>"
|
||||
"<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">"
|
||||
"<s:Body>"
|
||||
"<u:GetExternalPort xmlns:u=\"urn:schemas-upnp-org:service:WANIPConnection:1\"/>"
|
||||
"</s:Body>"
|
||||
"</s:Envelope>");
|
||||
|
||||
int contentLength = strlen(xmlBody);
|
||||
|
||||
// HTTP Request to get External Port
|
||||
snprintf(buffer, sizeof(buffer),
|
||||
"POST %s HTTP/1.1\r\n"
|
||||
"Host: %s:%d\r\n"
|
||||
"Content-Type: text/xml; charset=\"utf-8\"\r\n"
|
||||
"SOAPAction: \"urn:schemas-upnp-org:service:WANIPConnection:1#GetExternalPort\"\r\n"
|
||||
"Content-Length: %d\r\n"
|
||||
"Connection: Close\r\n"
|
||||
"\r\n"
|
||||
"%s",
|
||||
controlURL.c_str(),
|
||||
routerIP.toString().c_str(), routerPort,
|
||||
contentLength,
|
||||
xmlBody);
|
||||
|
||||
// Connect and Send
|
||||
WiFiClient client;
|
||||
if (!client.connect(routerIP, routerPort)) {
|
||||
ESP_LOGI(TAG_UPNP,"Failed to connect to router for External Port request.");
|
||||
return false;
|
||||
}
|
||||
|
||||
client.print(buffer);
|
||||
|
||||
// Read Response
|
||||
String response;
|
||||
uint32_t timeout = millis() + 5000;
|
||||
while (client.available() == 0) {
|
||||
if (millis() > timeout) {
|
||||
ESP_LOGI(TAG_UPNP,"Router did not respond to External Port request.");
|
||||
client.stop();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
while (client.available()) {
|
||||
response += client.readString();
|
||||
}
|
||||
client.stop();
|
||||
|
||||
//ESP_LOGI(TAG_UPNP,"External Port Response:");
|
||||
//ESP_LOGI(TAG_UPNP,response);
|
||||
|
||||
// Extract External Port from XML
|
||||
char *sz = (char *)(response.c_str());
|
||||
char *extPort = strstr(sz, "<NewExternalPort>");
|
||||
if (extPort) {
|
||||
extPort += 17;
|
||||
char *end = strchr(extPort, '<');
|
||||
if (end) *end = '\0';
|
||||
|
||||
publicPort = atoi(extPort);
|
||||
ESP_LOGI(TAG_UPNP,"External Port: %d\n", publicPort);
|
||||
return true;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG_UPNP,"Failed to extract External Port.");
|
||||
return false;
|
||||
};
|
||||
37
UPnPClient.h
Normal file
37
UPnPClient.h
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
#include <WiFi.h>
|
||||
#include <WiFiUdp.h>
|
||||
#define SSDP_MULTICAST_IP IPAddress("239.255.255.250")
|
||||
#define SSDP_PORT 1900
|
||||
#define UPNP_HTTP_PORT 5000 // Most routers use 5000, but it can vary
|
||||
#define SEARCH_TIMEOUT 5000 // 2 seconds timeout
|
||||
|
||||
class CUpnpClient {
|
||||
private:
|
||||
IPAddress routerIP;
|
||||
String routerIPString;
|
||||
uint16_t routerPort;
|
||||
|
||||
IPAddress publicIP;
|
||||
uint16_t publicPort;
|
||||
|
||||
uint32_t lastDiscoveryTime;
|
||||
String routerLocation;
|
||||
String controlURL;
|
||||
char buffer[1024]; // Stack-based buffer for responses
|
||||
|
||||
public:
|
||||
CUpnpClient() : publicPort(0), lastDiscoveryTime(0) {}
|
||||
|
||||
bool registerUPnP(uint32_t *pip, uint16_t *pport);
|
||||
|
||||
private:
|
||||
String fetchUPnPDescription(const String &location);
|
||||
String parseXML(const String &xml);
|
||||
int sendSoapRequest(const char *request, char *response, size_t responseSize);
|
||||
|
||||
bool discoverUPnP();
|
||||
bool requestPortMappingEntry();
|
||||
bool requestPortForwarding();
|
||||
bool requestExternalIP();
|
||||
bool requestExternalPort();
|
||||
};
|
||||
885
WiFiHost.cpp
Normal file
885
WiFiHost.cpp
Normal file
|
|
@ -0,0 +1,885 @@
|
|||
#include <lwip/sockets.h>
|
||||
#undef INADDR_NONE
|
||||
#include <lwip/netdb.h>
|
||||
#include <WiFiServer.h>
|
||||
#include <HTTPClient.h>
|
||||
#include <WiFiClientSecure.h>
|
||||
#include <esp_task_wdt.h>
|
||||
#include <netinet/in.h> // For struct in_addr
|
||||
#include <esp_task_wdt.h>
|
||||
#include <esp_system.h>
|
||||
#include "OTA.h"
|
||||
|
||||
#include "HermitCrab.h"
|
||||
#include "Config.h"
|
||||
#include "ConnectWiFi.h"
|
||||
#include "TimeManager.h"
|
||||
#include "History.h"
|
||||
#include "zcd.h"
|
||||
#include "AHT2x.h"
|
||||
#include "WiFiHost.h"
|
||||
#include "UPnPClient.h"
|
||||
|
||||
|
||||
|
||||
#define TAG_WIFI_HOST "WiFi Host"
|
||||
#define TCP_PACKET_SIZE_MAX 1460
|
||||
|
||||
//WiFiServer wifiServer(SERVER_PORT);
|
||||
//WiFiClient wifiClient;
|
||||
CWiFiHost host;
|
||||
CONFIG_TYPE configCopy;
|
||||
|
||||
void CWiFiHost::Setup()
|
||||
{
|
||||
// UDP Packet
|
||||
packetUDP = {
|
||||
.sig1 = SIGNATURE1,
|
||||
.m_nSize = sizeof(UDP_PACKET),
|
||||
.m_nMessage = UDP_MESSAGE::MESSAGE_HEARTBEAT,
|
||||
.m_nDeviceType = config.m_nDeviceType,
|
||||
.m_nResetReason = RESET_REASON_CHIP_POWER_ON,
|
||||
.m_nChipID = config.m_nChipId,
|
||||
.m_nDeviceID = 0,
|
||||
//.m_nVersion = (uint32_t)atoll(&HC__VERSION[2]),
|
||||
.dwIPAddress = 0,
|
||||
.m_nPort = UDP_PORT,
|
||||
//.m_MACAddress = "",
|
||||
//.m_sDeviceName = "",
|
||||
.status = {0},
|
||||
.sig2 = SIGNATURE2 };
|
||||
packetUDP.m_nVersion = (uint32_t)atoll(&HC__VERSION[2]);
|
||||
strncpy((char *)packetUDP.m_MACAddress, WiFi.macAddress().c_str(), 17);
|
||||
packetUDP.m_MACAddress[17] = 0;
|
||||
strcpy(packetUDP.m_sDeviceName, config.m_sDeviceName);
|
||||
|
||||
// TCP Packet
|
||||
hostPacket = {
|
||||
.sig1 = SIGNATURE1,
|
||||
.len = sizeof(TCP_PACKET),
|
||||
.cmd = ENUM_COMMAND::CMD_HELLO,
|
||||
.op = OPERATION_MODE::MODE_WAITING,
|
||||
.time = 0,
|
||||
.status = {0},
|
||||
.sig2 = SIGNATURE2 };
|
||||
clientPacket = hostPacket;
|
||||
|
||||
//if (m_nPublicPort == 0)
|
||||
{
|
||||
if (config.m_nPublicPort == 0)
|
||||
config.m_nPublicPort = (uint16_t)(config.m_nChipId & 0xFFFF);
|
||||
m_nPublicPort = config.m_nPublicPort;
|
||||
}
|
||||
|
||||
wifiServer = WiFiServer(SERVER_PORT, 1);
|
||||
wifiExternal = WiFiServer(m_nPublicPort, 1);
|
||||
|
||||
//wifiStatus = WIFI_NOT_CONNECTED;
|
||||
m_nDataSend_sent = 0;
|
||||
m_nDataReceive_size = 0;
|
||||
m_pDataSend_data = nullptr;
|
||||
m_nDataReceive_received = 0;
|
||||
m_nDataReceive_size = 0;
|
||||
m_pDataReceive_data = nullptr;
|
||||
|
||||
externalServerIP = IPAddress((uint32_t) 0UL);
|
||||
//sockfd = -1;
|
||||
//connectStartTime = 0;
|
||||
//isConnecting = false;
|
||||
|
||||
// Operation
|
||||
m_nMode = MODE_WAITING;
|
||||
m_bHelloSent = false;
|
||||
m_bSendHistoryPending = false;
|
||||
|
||||
m_nLastReceivedTime =
|
||||
m_nLastHeartBeatSentTime =
|
||||
m_nLastUDPBroadcastTime = millis();
|
||||
m_bClientConnected = false;
|
||||
m_dwPublicIP = 0;
|
||||
wifiClient.stop();
|
||||
|
||||
if (isWiFiConnected())
|
||||
{
|
||||
// UPnP Client
|
||||
{
|
||||
uint32_t ip = WiFi.gatewayIP();
|
||||
uint16_t port = m_nPublicPort;
|
||||
CUpnpClient upnp;
|
||||
|
||||
if (upnp.registerUPnP(&ip, &port)) {
|
||||
status.nFlags |= FLAG_UPNP;
|
||||
} else {
|
||||
status.nFlags &= ~FLAG_UPNP;
|
||||
}
|
||||
|
||||
if (ip != 0)
|
||||
m_dwPublicIP = ip;
|
||||
if (port != m_nPublicPort) {
|
||||
config.m_nPublicPort = port;
|
||||
config.save();
|
||||
m_nPublicPort = port;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Server
|
||||
wifiServer.begin(SERVER_PORT, 1);
|
||||
wifiExternal.begin(m_nPublicPort, 1);
|
||||
m_nLastReceivedTime = millis();
|
||||
m_bClientConnected = false;
|
||||
|
||||
// UDP
|
||||
packetUDP.dwIPAddress = WiFi.localIP();
|
||||
udpLocal.begin(UDP_PORT);
|
||||
udpExternal.begin(UDP_EXTERNAL_PORT);
|
||||
|
||||
if (m_cExternalServerIPAddress == IPAddress((uint32_t) 0l)) {
|
||||
if (WiFi.hostByName("visionsoft.kr", m_cExternalServerIPAddress)) {
|
||||
DPRINTF("WiFi - ExternalServer IP resolved as \"%s\"\n", m_cExternalServerIPAddress.toString().c_str());
|
||||
} else {
|
||||
m_cExternalServerIPAddress = IPAddress((uint32_t) 0);
|
||||
DPRINTF("WiFi - ExternalServer IP NOT resolved\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MY_IRAM_ATTR void CWiFiHost::Stop() {
|
||||
CloseConnection();
|
||||
|
||||
// Stop server
|
||||
wifiServer.stop();
|
||||
wifiExternal.stop();
|
||||
m_bClientConnected = false;
|
||||
|
||||
// Stop Client
|
||||
if (wifiClient)
|
||||
wifiClient.stop();
|
||||
|
||||
// Stop UDP
|
||||
udpLocal.stop(); // or udpLocal.end(); depending on your preference
|
||||
udpExternal.stop(); // or udpExternal.end(); depending on your preference
|
||||
}
|
||||
|
||||
MY_IRAM_ATTR void CWiFiHost::CloseConnection()
|
||||
{
|
||||
if (wifiClient && wifiClient.connected()) wifiClient.stop();
|
||||
m_bClientConnected = false;
|
||||
m_nMode = MODE_WAITING;
|
||||
}
|
||||
|
||||
IRAM_ATTR void CWiFiHost::Loop(unsigned long clock)
|
||||
{
|
||||
static unsigned long lastReceivedTime = 0;
|
||||
if (!isWiFiConnected()) return;
|
||||
|
||||
switch (m_nMode) {
|
||||
case MODE_WAITING: // Expecting connection from clients
|
||||
if (m_bClientConnected)
|
||||
{
|
||||
ESP_LOGI(TAG_WIFI_HOST,"Host: dropping connection for not connected");
|
||||
if (wifiClient && wifiClient.connected()) {
|
||||
ESP_LOGI(TAG_WIFI_HOST,"Host: stopping wifi client");
|
||||
wifiClient.stop();
|
||||
}
|
||||
m_bClientConnected = false;
|
||||
}
|
||||
|
||||
// Accept from internal XOR external port
|
||||
wifiClient = wifiServer.accept();
|
||||
if (!wifiClient || !wifiClient.connected()) {
|
||||
wifiClient = wifiExternal.accept();
|
||||
}
|
||||
|
||||
if (wifiClient && wifiClient.connected())
|
||||
{
|
||||
ESP_LOGI(TAG_WIFI_HOST,"Host: Connection Accepted");
|
||||
wifiClient.setNoDelay(true);
|
||||
m_nLastReceivedTime = clock;
|
||||
m_bClientConnected = true;
|
||||
m_bHelloSent = false;
|
||||
m_nMode = MODE_PACKET;
|
||||
|
||||
// LED
|
||||
ledcWrite(PIN_LED_WIFI, PWM_FULL - 10); // Amost ON
|
||||
}
|
||||
break;
|
||||
case MODE_PACKET:
|
||||
// Client Connected
|
||||
if (m_bClientConnected && wifiClient.connected())
|
||||
{
|
||||
CheckClient(clock);
|
||||
|
||||
if (m_bClientConnected) {
|
||||
if (clock - m_nLastReceivedTime > 60000)
|
||||
{
|
||||
ESP_LOGI(TAG_WIFI_HOST,"Host: dropping connection for no HB in 60 seconds");
|
||||
wifiClient.stop();
|
||||
m_bClientConnected = false;
|
||||
}
|
||||
|
||||
// Send HeartBeat
|
||||
if (clock - m_nLastHeartBeatSentTime >= 1000) {
|
||||
SendHeartBeat();
|
||||
m_nLastHeartBeatSentTime = clock;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!m_bClientConnected || !wifiClient.connected()) {
|
||||
m_nMode = MODE_WAITING;
|
||||
// LED
|
||||
ledcWrite(PIN_LED_WIFI, PWM_FULL - 2); // Almost Off
|
||||
}
|
||||
break;
|
||||
case MODE_SEND:
|
||||
if (SendData(clock)) {
|
||||
if (m_bSendHistoryPending) {
|
||||
// Mark pending to send the second part: from the start to head-1
|
||||
//SendData(history.getRingData2() /* &ring[0] */, sizeof(STATUS_TYPE) * head);
|
||||
//m_bSendHistoryPending = true;
|
||||
//m_nPendingHistoryCount = history.getHead();
|
||||
m_bSendHistoryPending = false;
|
||||
if (m_nPendingHistoryCount > 0) {
|
||||
ESP_LOGI(TAG_WIFI_HOST,"WfFi Host - SendData - 2nd part of History (%d)\n", m_nPendingHistoryCount);
|
||||
SendData(history.getRingData2(), sizeof(STATUS_TYPE) * m_nPendingHistoryCount);
|
||||
m_nPendingHistoryCount = 0;
|
||||
}
|
||||
} else {
|
||||
m_nMode = MODE_PACKET;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case MODE_RECV:
|
||||
if (ReceiveData(clock)) m_nMode = MODE_PACKET;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
MY_IRAM_ATTR void CWiFiHost::SendHeartBeat(unsigned long clock) {
|
||||
if (!isWiFiConnected()) {
|
||||
//ESP_LOGI(TAG_WIFI_HOST,"WiFiHost - SendHeartBeat() called while not connected!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Send Heartbeats to external server and to local devices
|
||||
if (clock - m_nLastUDPBroadcastTime > 1000) {
|
||||
static int count = 55;
|
||||
|
||||
// UDP Heartbeat
|
||||
if (++count >= 60) {
|
||||
// External Heartbeat
|
||||
UDP_CONFIG_TYPE pktConfig;
|
||||
pktConfig.udp = packetUDP;
|
||||
pktConfig.udp.m_nPort = m_nPublicPort;
|
||||
pktConfig.udp.dwIPAddress = m_dwPublicIP;
|
||||
pktConfig.udp.status = status;
|
||||
|
||||
if (((uint32_t)m_cExternalServerIPAddress) != (uint32_t)0l)
|
||||
udpExternal.beginPacket(m_cExternalServerIPAddress, (uint16_t)UDP_EXTERNAL_PORT);
|
||||
else
|
||||
udpExternal.beginPacket("visionsoft.kr", (uint16_t)UDP_EXTERNAL_PORT);
|
||||
|
||||
if (config.bConfigSaved) {
|
||||
// Config is save locally. Save it on cloud
|
||||
config.bConfigSaved = false;
|
||||
pktConfig.udp.m_nMessage = MESSAGE_CONFIG_SAVE;
|
||||
pktConfig.con = config;
|
||||
udpExternal.write((uint8_t*)&pktConfig, sizeof(pktConfig));
|
||||
ESP_LOGI(TAG_WIFI_HOST,"HeartBeat Packet *Config Save* sent out to external Server");
|
||||
} else {
|
||||
// Send only UDP packet
|
||||
udpExternal.write((uint8_t*)&(pktConfig.udp), sizeof(UDP_PACKET));
|
||||
}
|
||||
udpExternal.endPacket();
|
||||
count = 0;
|
||||
} else if (!m_bClientConnected) {
|
||||
// Local AP broadcast
|
||||
packetUDP.m_nMessage = UDP_MESSAGE::MESSAGE_HEARTBEAT;
|
||||
packetUDP.dwIPAddress = WiFi.localIP();
|
||||
packetUDP.m_nPort = SERVER_PORT;
|
||||
strcpy(packetUDP.m_sCompanyName, COMPANY_NAME);
|
||||
strcpy(packetUDP.m_sService, SERVICE_NAME);
|
||||
udpLocal.beginPacket((IPAddress)0xFFFFFFFF, (uint16_t)UDP_PORT);
|
||||
udpLocal.write((uint8_t*)&packetUDP, sizeof(UDP_PACKET));
|
||||
udpLocal.endPacket();
|
||||
}
|
||||
m_nLastUDPBroadcastTime = clock;
|
||||
}
|
||||
}
|
||||
|
||||
MY_IRAM_ATTR void CWiFiHost::MonitorUDP() {
|
||||
// Listen for UDP External port
|
||||
IPAddress ip;
|
||||
int size = udpExternal.parsePacket();
|
||||
if (size >= sizeof(packetUDP)) {
|
||||
if ((udpExternal.read((char *) &packetUDP, sizeof(packetUDP)) == sizeof(packetUDP)) &&
|
||||
packetUDP.sig1 == SIGNATURE1 &&
|
||||
packetUDP.sig2 == SIGNATURE2 &&
|
||||
packetUDP.m_nChipID == config.m_nChipId) {
|
||||
|
||||
switch (packetUDP.m_nMessage) {
|
||||
case MESSAGE_IP: // Extenal IP and Port set
|
||||
// if (packetUDP.m_nChipID == config.m_nChipId) {
|
||||
// m_dwPublicIP = packetUDP.dwIPAddress;
|
||||
// m_nPublicPort = packetUDP.m_nPort;
|
||||
// ip = IPAddress(m_dwPublicIP);
|
||||
// ESP_LOGI(TAG_WIFI_HOST,"External IP(%s) and Port(%d)\n", ip.toString().c_str(), m_nPublicPort);
|
||||
// } else {
|
||||
// ESP_LOGI(TAG_WIFI_HOST,"External Server Response for other device Received\n");
|
||||
// }
|
||||
break;
|
||||
case MESSAGE_CONFIG_SEND:
|
||||
if (size == sizeof(UDP_PACKET) + sizeof(CONFIG_STRUCT)) {
|
||||
if (udpExternal.read((char*) &configCopy, sizeof(configCopy))) {
|
||||
ESP_LOGI(TAG_WIFI_HOST,"CONFIG received from the external DB server\n");
|
||||
}
|
||||
} else {
|
||||
char *buffer[256];
|
||||
while((size = udpExternal.parsePacket()) > 0) {
|
||||
// Discard any leftover data
|
||||
udpExternal.read((char*) &buffer, sizeof(buffer));
|
||||
}
|
||||
}
|
||||
break;
|
||||
case MESSAGE_QUERY_IP:
|
||||
// if (m_nMode == MODE_WAITING) {
|
||||
// // Get sender's IP and port
|
||||
// externalServerIP = udpExternal.remoteIP();
|
||||
// uint16_t senderPort = udpExternal.remotePort();
|
||||
// ESP_LOGI(TAG_WIFI_HOST,"External Server - MESSAGE_QUERY_IP from %s:%d\n", externalServerIP.toString().c_str(), senderPort);
|
||||
// m_nMode = MODE_EXTERNAL_SERVER;
|
||||
// }
|
||||
break;
|
||||
case MESSAGE_RESET:
|
||||
Restart();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IRAM_ATTR void CWiFiHost::CheckClient(unsigned long clock)
|
||||
{
|
||||
bool bLED = false;
|
||||
static TCP_PACKET cpkt;
|
||||
int available = wifiClient.available();
|
||||
if (available >= sizeof(TCP_PACKET)) {
|
||||
uint8_t* pC = (uint8_t*)&cpkt;
|
||||
int count = wifiClient.readBytes(pC, sizeof(TCP_PACKET));
|
||||
if (count == sizeof(TCP_PACKET)) {
|
||||
// Now we have a full packet size data
|
||||
if (cpkt.sig1 == SIGNATURE1 &&
|
||||
cpkt.sig2 == SIGNATURE2 &&
|
||||
cpkt.len == sizeof(TCP_PACKET)) {
|
||||
// Process the completed PACKET
|
||||
ProcessPacket(cpkt);
|
||||
m_nLastReceivedTime = clock;
|
||||
return;
|
||||
}
|
||||
|
||||
// Invalid Packet - remove the first byte off from the buff
|
||||
ESP_LOGI(TAG_WIFI_HOST,"Invalid Packet %s%s size: %d/%d\n",
|
||||
cpkt.sig1 == SIGNATURE1 ? "SIG1 OK" : "",
|
||||
cpkt.sig2 == SIGNATURE2 ? "SIG2 OK" : "",
|
||||
cpkt.len, sizeof(TCP_PACKET));
|
||||
// Shift the buffer data off by 1 byte for the next cycle
|
||||
//buffIndexC = sizeof(TCP_PACKET) - 1;
|
||||
//memcpy(pC, &pC[1], buffIndexC);
|
||||
while (m_bClientConnected &&
|
||||
wifiClient.connected() &&
|
||||
wifiClient.readBytes((uint8_t *) &cpkt, sizeof(cpkt)) > 0) {
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IRAM_ATTR void CWiFiHost::ProcessPacket(TCP_PACKET& pkt)
|
||||
{
|
||||
switch (pkt.cmd)
|
||||
{
|
||||
// System
|
||||
case CMD_HEARTBEAT:
|
||||
m_nLastReceivedTime = millis();
|
||||
//ESP_LOGI(TAG_WIFI_HOST,"H");
|
||||
break;
|
||||
case CMD_HELLO:
|
||||
case CMD_HELLO_DEBUG:
|
||||
{
|
||||
// Send the Config Data to PC
|
||||
// Send Packet Back
|
||||
ESP_LOGI(TAG_WIFI_HOST,"WiFi - HELLO received");
|
||||
pkt.u16[0] = SIGNATURE1;
|
||||
pkt.n16[1] = sizeof(CONFIG_TYPE);
|
||||
pkt.u16[2] = SIGNATURE2;
|
||||
config.m_nChipId;
|
||||
SendPacket(pkt);
|
||||
|
||||
// Send Data Packets
|
||||
ESP_LOGI(TAG_WIFI_HOST,"WiFi - Config Send pending...");
|
||||
int sent = SendData((uint8_t*)&config, sizeof(CONFIG_TYPE));
|
||||
m_bHelloSent = true;
|
||||
}
|
||||
break;
|
||||
case CMD_DROP_CONNECTION:
|
||||
wifiClient.stop();
|
||||
m_bClientConnected = false;
|
||||
m_nMode = MODE_WAITING;
|
||||
ESP_LOGI(TAG_WIFI_HOST,"WiFI - Client requets to DROP CONNECTION");
|
||||
break;
|
||||
case CMD_RESET_REASON:
|
||||
pkt.n16[0] = esp_reset_reason();
|
||||
SendPacket(pkt);
|
||||
break;
|
||||
case CMD_SAVE_RESTART:
|
||||
if (pkt.u16[0] == SIGNATURE1 &&
|
||||
pkt.u16[1] == SIGNATURE2) {
|
||||
yield();
|
||||
Restart();
|
||||
}
|
||||
break;
|
||||
case CMD_RESET_RESTART:
|
||||
if (pkt.u16[0] == SIGNATURE1 &&
|
||||
pkt.u16[1] == SIGNATURE2) {
|
||||
pkt.cmd = CMD_DROP_CONNECTION;
|
||||
SendPacket(pkt);
|
||||
yield();
|
||||
Restart();
|
||||
}
|
||||
break;
|
||||
case CMD_SEND_HISTORY:
|
||||
if (pkt.u16[0] == SIGNATURE1 && pkt.u16[1] == SIGNATURE2) {
|
||||
int16_t count = history.getRingCount();
|
||||
if (count > 0 ) {
|
||||
// inform client the count of history
|
||||
pkt.op = count;
|
||||
SendPacket(pkt);
|
||||
int16_t size = history.getRingSize();
|
||||
if (count < size) {
|
||||
ESP_LOGI(TAG_WIFI_HOST,"WfFi Host - SendData - Whold part of History (%d)\n", count);
|
||||
SendData(history.getRingData1()/* &ring[tail] */, sizeof(STATUS_TYPE) * count);
|
||||
} else {
|
||||
int count1st = size - history.getRingTail();
|
||||
ESP_LOGI(TAG_WIFI_HOST,"WfFi Host - SendData - 1st part of History Total(%d), 1st(%d) H(%d) T(%d)\n",
|
||||
count, count1st, history.getRingHead(), history.getRingTail());
|
||||
SendData(history.getRingData1(), sizeof(STATUS_TYPE) * count1st),
|
||||
// Mark pending to send the second part: from the start to head-1
|
||||
//SendData(history.getRingData2() /* &ring[0] */, sizeof(STATUS_TYPE) * head);
|
||||
m_bSendHistoryPending = true;
|
||||
m_nPendingHistoryCount = history.getRingHead();
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case CMD_PAUSE:
|
||||
break;
|
||||
case CMD_RESUME:
|
||||
break;
|
||||
case CMD_RESET_SENSOR:
|
||||
aht25.setScanFlag(true);
|
||||
aht10_0x39.setScanFlag(true);
|
||||
break;
|
||||
|
||||
// Config
|
||||
case CMD_INIT_CONFIG:
|
||||
if (pkt.u16[0] == SIGNATURE1 &&
|
||||
pkt.u16[1] == sizeof(CONFIG_STRUCT) &&
|
||||
pkt.u16[2] == SIGNATURE2) {
|
||||
config.init();
|
||||
config.save();
|
||||
}
|
||||
break;
|
||||
case CMD_LOAD_CONFIG:
|
||||
if (pkt.u16[0] == SIGNATURE1 &&
|
||||
pkt.u16[1] == sizeof(CONFIG_STRUCT) &&
|
||||
pkt.u16[2] == SIGNATURE2) {
|
||||
config.load();
|
||||
history.loadPID();
|
||||
}
|
||||
break;
|
||||
case CMD_SAVE_CONFIG:
|
||||
if (pkt.u16[0] == SIGNATURE1 &&
|
||||
pkt.u16[1] == sizeof(CONFIG_STRUCT) &&
|
||||
pkt.u16[2] == SIGNATURE2) {
|
||||
config.save();
|
||||
}
|
||||
break;
|
||||
case CMD_RECV_CONFIG:
|
||||
// Receive Confif Data from PC
|
||||
if (pkt.u16[0] == SIGNATURE1 &&
|
||||
pkt.u16[1] == sizeof(CONFIG_STRUCT) &&
|
||||
pkt.u16[2] == SIGNATURE2) {
|
||||
ReceiveData((uint8_t *)&configCopy, sizeof(CONFIG_TYPE));
|
||||
m_bReceiveConfigPending = true;
|
||||
ESP_LOGI(TAG_WIFI_HOST,"WiFi - Receive Config initiated...");
|
||||
}
|
||||
break;
|
||||
case CMD_SEND_CONFIG:
|
||||
if (pkt.u16[0] == SIGNATURE1 &&
|
||||
pkt.u16[1] == sizeof(CONFIG_STRUCT) &&
|
||||
pkt.u16[2] == SIGNATURE2) {
|
||||
SendPacket(pkt);
|
||||
SendData((const uint8_t *)&config, (unsigned int) sizeof(config));
|
||||
ESP_LOGI(TAG_WIFI_HOST,"WiFi - Send Config initiated...");
|
||||
}
|
||||
break;
|
||||
case CMD_SEND_CONFIG_SERVER:
|
||||
|
||||
case CMD_LOAD_CONFIG_SERVER:
|
||||
break;
|
||||
|
||||
// PID
|
||||
case CMD_INIT_PID_PARAM:
|
||||
history.savePID();
|
||||
break;
|
||||
case CMD_LOAD_PID_PARAM:
|
||||
history.loadPID();
|
||||
break;
|
||||
case CMD_SAVE_PID_PARAM:
|
||||
history.savePID();
|
||||
break;
|
||||
case CMD_SET_PID:
|
||||
config.Kp_Temp1 = pkt.f[0];
|
||||
config.Kd_Temp1 = pkt.f[1];
|
||||
config.LR_Temp1 = pkt.f[2];
|
||||
config.Kp_Humidity = pkt.f[3];
|
||||
config.Kd_Humidity = pkt.f[4];
|
||||
config.LR_Humidity = pkt.f[5];
|
||||
history.loadPID();
|
||||
break;
|
||||
case CMD_GET_PID:
|
||||
pkt.f[0] = config.Kp_Temp1;
|
||||
pkt.f[1] = config.Kd_Temp1;
|
||||
pkt.f[2] = config.LR_Temp1;
|
||||
pkt.f[3] = config.Kp_Humidity;
|
||||
pkt.f[4] = config.Kd_Humidity;
|
||||
pkt.f[5] = config.LR_Humidity;
|
||||
SendPacket(pkt);
|
||||
break;
|
||||
|
||||
// Control
|
||||
case CMD_SET_CONTROL:
|
||||
config.bSmartControl = pkt.by[0] ? true : false;
|
||||
config.bNightControl = pkt.by[1] ? true : false;
|
||||
config.bControlTemperature = pkt.by[2] ? true : false;
|
||||
config.bControlHumidity = pkt.by[3] ? true : false;
|
||||
break;
|
||||
case CMD_GET_CONTROL:
|
||||
pkt.by[0] = config.bSmartControl ? 0xFF : 0;
|
||||
pkt.by[1] = config.bNightControl ? 0xFF : 0;
|
||||
pkt.by[2] = config.bControlTemperature ? 0xFF : 0;
|
||||
pkt.by[3] = config.bControlHumidity ? 0xFF : 0;
|
||||
SendPacket(pkt);
|
||||
break;
|
||||
|
||||
// Operation
|
||||
case CMD_SET_TEMP_TARGET:
|
||||
config.nTempTarget = pkt.u16[0];
|
||||
ESP_LOGI(TAG_WIFI_HOST,"WiFi - TempTarget changed to: %d\n", pkt.u16[0]);
|
||||
break;
|
||||
case CMD_SET_TEMP_TARGET_NIGHT:
|
||||
config.nTempTargetNight = pkt.u16[0];
|
||||
ESP_LOGI(TAG_WIFI_HOST,"WiFi - TempTargetNight changed to: %d\n", pkt.u16[0]);
|
||||
break;
|
||||
case CMD_SET_HUMID_TARGET:
|
||||
config.nHumidTarget = pkt.u16[0];
|
||||
ESP_LOGI(TAG_WIFI_HOST,"WiFi - HumidTarget changed to: %d\n", pkt.u16[0]);
|
||||
break;
|
||||
case CMD_SET_SENSOR_OFFSET:
|
||||
config.nTemp1Offset = pkt.n16[0];
|
||||
config.nHumid1Offset = pkt.n16[1];
|
||||
if (pkt.n16[2] > 1) {
|
||||
config.nTemp2Offset = pkt.n16[2];
|
||||
config.nHumid2Offset = pkt.n16[3];
|
||||
if (pkt.n16[2] > 2)
|
||||
config.nTemp3Offset = pkt.n16[4];
|
||||
}
|
||||
ESP_LOGI(TAG_WIFI_HOST,"WiFi - SensorOffset changed to: %d, %d\n", pkt.n16[0], pkt.n16[1]);
|
||||
break;
|
||||
|
||||
case CMD_SET_AC1_PARAM:
|
||||
config.ac1 = pkt.device;
|
||||
break;
|
||||
case CMD_SET_AC2_PARAM:
|
||||
config.ac2 = pkt.device;
|
||||
break;
|
||||
case CMD_SET_MIST_PARAM:
|
||||
config.mist = pkt.device;
|
||||
break;
|
||||
case CMD_SET_FAN_PARAM:
|
||||
config.fan = pkt.device;
|
||||
break;
|
||||
case CMD_SET_MOTOR_PARAM:
|
||||
config.motor = pkt.device;
|
||||
break;
|
||||
case CMD_SET_LIGHT_PARAM:
|
||||
config.light = pkt.device;
|
||||
break;
|
||||
|
||||
// Status
|
||||
case CMD_SET_HEATER1_DUTY:
|
||||
status.nHeater1Duty = pkt.u16[0];
|
||||
if (pkt.u16[1])
|
||||
status.nFlags |= FLAG_MANUAL_HEATER1;
|
||||
else
|
||||
status.nFlags &= ~FLAG_MANUAL_HEATER1;
|
||||
if (status.nHeater1Duty == 0) {
|
||||
setHeater1Duty(0);
|
||||
}
|
||||
break;
|
||||
case CMD_SET_HEATER2_DUTY:
|
||||
status.nHeater2Duty = pkt.u16[0];
|
||||
if (pkt.u16[1])
|
||||
status.nFlags |= FLAG_MANUAL_HEATER2;
|
||||
else
|
||||
status.nFlags &= ~FLAG_MANUAL_HEATER2;
|
||||
if (status.nHeater2Duty == 0) {
|
||||
setHeater2Duty(0);
|
||||
}
|
||||
break;
|
||||
case CMD_SET_MIST_DUTY:
|
||||
status.nMistDuty = pkt.u16[0];
|
||||
if (pkt.u16[1])
|
||||
status.nFlags |= FLAG_MANUAL_MIST;
|
||||
else
|
||||
status.nFlags &= ~FLAG_MANUAL_MIST;
|
||||
break;
|
||||
case CMD_SET_FAN_DUTY:
|
||||
status.nFanDuty = pkt.u16[0];
|
||||
if (pkt.u16[1])
|
||||
status.nFlags |= FLAG_MANUAL_FAN;
|
||||
else
|
||||
status.nFlags &= ~FLAG_MANUAL_FAN;
|
||||
|
||||
break;
|
||||
case CMD_SET_MOTOR_DUTY:
|
||||
status.nMotorDuty = pkt.u16[0];
|
||||
if (pkt.u16[1])
|
||||
status.nFlags |= FLAG_MANUAL_MOTOR;
|
||||
else
|
||||
status.nFlags &= ~FLAG_MANUAL_MOTOR;
|
||||
break;
|
||||
case CMD_SET_LIGHT_DUTY:
|
||||
status.nLightTargetDuty = pkt.u16[0];
|
||||
if (pkt.u16[1])
|
||||
status.nFlags |= FLAG_MANUAL_LIGHT;
|
||||
else
|
||||
status.nFlags &= ~FLAG_MANUAL_LIGHT;
|
||||
break;
|
||||
|
||||
// Manual Operation
|
||||
case CMD_SET_MANUAL_HEATER1:
|
||||
if (pkt.u16[0])
|
||||
status.nFlags |= FLAG_MANUAL_HEATER1;
|
||||
else
|
||||
status.nFlags &= ~FLAG_MANUAL_HEATER1;
|
||||
break;
|
||||
case CMD_SET_MANUAL_HEATER2:
|
||||
if (pkt.u16[0])
|
||||
status.nFlags |= FLAG_MANUAL_HEATER2;
|
||||
else
|
||||
status.nFlags &= ~FLAG_MANUAL_HEATER2;
|
||||
break;
|
||||
case CMD_SET_MANUAL_MIST:
|
||||
if (pkt.u16[0])
|
||||
status.nFlags |= FLAG_MANUAL_MIST;
|
||||
else
|
||||
status.nFlags &= ~FLAG_MANUAL_MIST;
|
||||
break;
|
||||
case CMD_SET_MANUAL_FAN:
|
||||
if (pkt.u16[0])
|
||||
status.nFlags |= FLAG_MANUAL_FAN;
|
||||
else
|
||||
status.nFlags &= ~FLAG_MANUAL_FAN;
|
||||
break;
|
||||
case CMD_SET_MANUAL_LIGHT:
|
||||
if (pkt.u16[0])
|
||||
status.nFlags |= FLAG_MANUAL_LIGHT;
|
||||
else
|
||||
status.nFlags &= ~FLAG_MANUAL_LIGHT;
|
||||
break;
|
||||
|
||||
// Time
|
||||
case CMD_SET_TIME_NIGHT:
|
||||
config.nNightStartHour = pkt.u16[0];
|
||||
config.nNightStartMin = pkt.u16[1];
|
||||
config.nNightEndHour = pkt.u16[2];
|
||||
config.nNightEndMin = pkt.u16[3];
|
||||
break;
|
||||
case CMD_SET_WIFI_CLIENT_DISPLAY:
|
||||
config.m_nDisplayTempHigh = pkt.n16[0];
|
||||
config.m_nDisplayTempLow = pkt.n16[1];
|
||||
config.m_nDisplayTime = pkt.n16[2];
|
||||
config.m_fShowRealTime = pkt.n16[3];
|
||||
config.m_fShowHistory = pkt.n16[4];
|
||||
ESP_LOGI(TAG_WIFI_HOST,"WiFi - Client Display Settings changed.");
|
||||
break;
|
||||
|
||||
default:
|
||||
ESP_LOGI(TAG_WIFI_HOST,"WiFi - Packet Received Type: %d\n", pkt.cmd);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
IRAM_ATTR int CWiFiHost::SendPacket(TCP_PACKET& pkt)
|
||||
{
|
||||
size_t sent = 0;
|
||||
if (m_bClientConnected && wifiClient && wifiClient.connected())
|
||||
{
|
||||
sent = wifiClient.write((char*)&pkt, sizeof(TCP_PACKET));
|
||||
}
|
||||
return sent;
|
||||
}
|
||||
|
||||
IRAM_ATTR size_t CWiFiHost::SendData(const uint8_t* data, size_t size) {
|
||||
if (data != nullptr) {
|
||||
m_nMode = MODE_SEND;
|
||||
m_pDataSend_data = (char *) data;
|
||||
m_nDataSend_size = size;
|
||||
m_nDataSend_sent = 0;
|
||||
ESP_LOGI(TAG_WIFI_HOST,"WfFi Host - SendData(size: %d)\n", size);
|
||||
return size;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
IRAM_ATTR bool CWiFiHost::SendData(unsigned long clock)
|
||||
{
|
||||
if (m_nDataSend_sent < m_nDataSend_size)
|
||||
{
|
||||
bool connected = wifiClient.connected();
|
||||
if (m_bClientConnected && wifiClient && connected) {
|
||||
size_t count = m_nDataSend_size - m_nDataSend_sent;
|
||||
if (count > TCP_PACKET_SIZE_MAX) count = TCP_PACKET_SIZE_MAX;
|
||||
//size_t avail = wifiClient.availableForWrite();
|
||||
//if (avail < count) {
|
||||
// ESP_LOGI(TAG_WIFI_HOST,"WiFiHost - SendData() avail(%d) is less than count(%d)\n", avail, count);
|
||||
// //count = avail;
|
||||
//}
|
||||
|
||||
if (count > 0) {
|
||||
int16_t sentCount = wifiClient.write(&m_pDataSend_data[m_nDataSend_sent], count);
|
||||
if (sentCount != count) {
|
||||
ESP_LOGI(TAG_WIFI_HOST,"WiFiHost - SendData() sent(%d) is not count(%d)\n", sentCount, count);
|
||||
if (sentCount <= 0)
|
||||
yield();
|
||||
}
|
||||
m_nDataSend_sent += sentCount;
|
||||
ESP_LOGI(TAG_WIFI_HOST,"WiFiHost - SendData() size(%d) sent(%d) total_sent(%d)\n", m_nDataSend_size, count, m_nDataSend_sent);
|
||||
}
|
||||
//else {
|
||||
// ESP_LOGI(TAG_WIFI_HOST,"WiFiHost - SendData() avail is 0");
|
||||
// yield();
|
||||
//}
|
||||
} else {
|
||||
m_nMode = MODE_WAITING;
|
||||
m_pDataSend_data = nullptr;
|
||||
m_nDataSend_size = 0;
|
||||
m_nDataSend_sent = 0;
|
||||
ESP_LOGI(TAG_WIFI_HOST," SendData: Connection lost - reset sendData()!");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (m_nDataSend_sent == m_nDataSend_size) {
|
||||
ESP_LOGI(TAG_WIFI_HOST, " SendDdata: DataSend completed!");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
IRAM_ATTR size_t CWiFiHost::ReceiveData(uint8_t* data, size_t size)
|
||||
{
|
||||
m_nMode = MODE_RECV;
|
||||
m_pDataReceive_data = (char *) data;
|
||||
m_nDataReceive_size = size;
|
||||
m_nDataReceive_received = 0;
|
||||
#ifdef ESP8266
|
||||
digitalWrite(PIN_EXTRA, LED_ON);
|
||||
#endif
|
||||
return size;
|
||||
}
|
||||
|
||||
IRAM_ATTR bool CWiFiHost::ReceiveData(unsigned long clock)
|
||||
{
|
||||
// Receive Data
|
||||
size_t nSize = 0;
|
||||
if (m_nDataReceive_received < m_nDataReceive_size)
|
||||
{
|
||||
if (m_bClientConnected && wifiClient && wifiClient.connected()) {
|
||||
int16_t count = m_nDataReceive_size - m_nDataReceive_received;
|
||||
if (count > TCP_PACKET_SIZE_MAX) count = TCP_PACKET_SIZE_MAX;
|
||||
nSize = wifiClient.readBytes(&m_pDataReceive_data[m_nDataReceive_received], count);
|
||||
if (nSize > 0) {
|
||||
m_nLastReceivedTime = clock;
|
||||
m_nDataReceive_received += nSize;
|
||||
}
|
||||
} else {
|
||||
m_nMode = MODE_WAITING;
|
||||
m_pDataReceive_data = nullptr;
|
||||
m_nDataReceive_size = 0;
|
||||
m_nDataReceive_received = 0;
|
||||
ESP_LOGI(TAG_WIFI_HOST," SendData: Connection lost - reset receiveData()!");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for any pending action
|
||||
if (m_nDataReceive_received == m_nDataReceive_size) {
|
||||
if (m_bReceiveConfigPending && m_pDataReceive_data == (char *)&configCopy) {
|
||||
config = configCopy;
|
||||
history.loadPID();
|
||||
ESP_LOGI(TAG_WIFI_HOST,"WiFi - Config Received");
|
||||
m_bReceiveConfigPending = false;
|
||||
}
|
||||
ESP_LOGI(TAG_WIFI_HOST," ReceiveData: Data Receive Completed!");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nSize <= 0 && clock - m_nLastReceivedTime > 5000) {
|
||||
ESP_LOGI(TAG_WIFI_HOST," ReceiveData: TimeOut Abort!");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
IRAM_ATTR void CWiFiHost::SendHeartBeat() {
|
||||
if (m_bHelloSent) {
|
||||
hostPacket.cmd = CMD_HEARTBEAT;
|
||||
time_t now = time(NULL); // Get current time in seconds
|
||||
status.now = (uint32_t) time(NULL);
|
||||
//status.uptime = now - timeManager.getFirstNTPTime();
|
||||
hostPacket.status = status;
|
||||
SendPacket(hostPacket);
|
||||
}
|
||||
}
|
||||
|
||||
MY_IRAM_ATTR void CWiFiHost::Restart() {
|
||||
if (isWiFiConnected()) {
|
||||
if (m_bClientConnected && m_bClientConnected && wifiClient && wifiClient.connected()) {
|
||||
hostPacket.cmd = CMD_DROP_CONNECTION;
|
||||
SendPacket(hostPacket);
|
||||
}
|
||||
}
|
||||
vTaskDelay(500/portTICK_PERIOD_MS);
|
||||
|
||||
// stop all network sockets
|
||||
Stop(); // Stop Sockets
|
||||
|
||||
vTaskDelay(50/portTICK_PERIOD_MS);
|
||||
|
||||
// Turn Off
|
||||
setHeater1Duty(0);
|
||||
setHeater2Duty(0);
|
||||
ledcWrite(PIN_MIST, 0);
|
||||
ledcWrite(PIN_FAN, 0);
|
||||
ledcWrite(PIN_MOTOR, 0);
|
||||
ledcWrite(PIN_LIGHT, 0);
|
||||
vTaskDelay(50/portTICK_PERIOD_MS);
|
||||
config.statusSave = status;
|
||||
config.save();
|
||||
vTaskDelay(50/portTICK_PERIOD_MS);
|
||||
ESP.restart();
|
||||
}
|
||||
284
WiFiHost.h
Normal file
284
WiFiHost.h
Normal file
|
|
@ -0,0 +1,284 @@
|
|||
#ifndef __WIFI_HOST_H
|
||||
#define __WIFI_HOST_H
|
||||
#include "HermitCrab.h"
|
||||
#ifdef ESP8266
|
||||
#include <ESP8266WiFi.h>
|
||||
#else
|
||||
#include <WiFi.h>
|
||||
#endif
|
||||
#include <WiFiUdp.h>
|
||||
|
||||
#define UDP_PORT 4949
|
||||
#define SERVER_PORT 3939
|
||||
#define UDP_EXTERNAL_PORT (UDP_PORT + 1)
|
||||
#define TCP_EXTERNAL_PORT (SERVER_PORT + 1)
|
||||
#define CONNECT_TIMEOUT_MS 5000
|
||||
|
||||
enum UDP_MESSAGE {
|
||||
MESSAGE_HEARTBEAT,
|
||||
MESSAGE_IP,
|
||||
MESSAGE_NOTFOUND,
|
||||
MESSAGE_CONFIG_SAVE,
|
||||
MESSAGE_CONFIG_SEND,
|
||||
MESSAGE_AP,
|
||||
MESSAGE_QUERY_IP,
|
||||
MESSAGE_UPDATE_STATUS,
|
||||
MESSAGE_UPDATE_CONFIG,
|
||||
|
||||
|
||||
|
||||
MESSAGE_COUNT,
|
||||
MESSAGE_RESET = UDP_EXTERNAL_PORT,
|
||||
};
|
||||
|
||||
enum OPCODE_TYPE {
|
||||
OP_RECV_ONLY,
|
||||
OP_RECV_SAVE,
|
||||
};
|
||||
|
||||
enum OPERATION_MODE {
|
||||
MODE_WAITING,
|
||||
MODE_PACKET,
|
||||
MODE_SEND,
|
||||
MODE_RECV,
|
||||
MODE_EXTERNAL_SERVER
|
||||
};
|
||||
|
||||
enum ENUM_COMMAND
|
||||
{
|
||||
// Command
|
||||
CMD_HEARTBEAT = 1, // 1 HeartBeat - prevents disconnect from the host
|
||||
|
||||
// Connection
|
||||
CMD_HELLO = 101, // 0 Hello
|
||||
CMD_DROP_CONNECTION,
|
||||
CMD_RESET_REASON,
|
||||
|
||||
// Device Command
|
||||
CMD_SAVE_RESTART = 201,
|
||||
CMD_RESET_RESTART,
|
||||
CMD_SEND_HISTORY,
|
||||
CMD_PAUSE,
|
||||
CMD_RESUME,
|
||||
CMD_RESET_SENSOR,
|
||||
CMD_RESET_DISPLAY,
|
||||
|
||||
// OTA Update
|
||||
CMD_UPDATE_CHECK = 301,
|
||||
CMD_UPDATE_AVAILABLE,
|
||||
CMD_UPDATE_FORCED,
|
||||
|
||||
// Config
|
||||
CMD_INIT_CONFIG = 1201,
|
||||
CMD_LOAD_CONFIG,
|
||||
CMD_SAVE_CONFIG,
|
||||
CMD_SEND_CONFIG,
|
||||
CMD_RECV_CONFIG,
|
||||
CMD_SEND_CONFIG_SERVER,
|
||||
CMD_LOAD_CONFIG_SERVER,
|
||||
|
||||
// PID
|
||||
CMD_INIT_PID_PARAM = 1301,
|
||||
CMD_LOAD_PID_PARAM,
|
||||
CMD_SAVE_PID_PARAM,
|
||||
CMD_SET_PID,
|
||||
CMD_GET_PID,
|
||||
|
||||
// Control
|
||||
CMD_SET_CONTROL = 1401,
|
||||
CMD_GET_CONTROL,
|
||||
|
||||
// Operation
|
||||
CMD_SET_TEMP_TARGET = 1501,
|
||||
CMD_SET_TEMP_TARGET_NIGHT,
|
||||
CMD_SET_HUMID_TARGET,
|
||||
CMD_SET_SENSOR_OFFSET,
|
||||
CMD_SET_AC1_PARAM,
|
||||
CMD_SET_AC2_PARAM,
|
||||
CMD_SET_MIST_PARAM,
|
||||
CMD_SET_FAN_PARAM,
|
||||
CMD_SET_MOTOR_PARAM,
|
||||
CMD_SET_LIGHT_PARAM,
|
||||
|
||||
// Status
|
||||
CMD_SET_HEATER1_DUTY = 2101,
|
||||
CMD_SET_HEATER2_DUTY,
|
||||
CMD_SET_MIST_DUTY,
|
||||
CMD_SET_FAN_DUTY,
|
||||
CMD_SET_FAN_DUTY_AUTO,
|
||||
CMD_SET_MOTOR_DUTY,
|
||||
CMD_SET_MOTOR_DUTY_AUTO,
|
||||
CMD_SET_LIGHT_DUTY,
|
||||
|
||||
// Manual Operations
|
||||
CMD_SET_MANUAL_HEATER1 = 2201,
|
||||
CMD_SET_MANUAL_HEATER2,
|
||||
CMD_SET_MANUAL_MIST,
|
||||
CMD_SET_MANUAL_FAN,
|
||||
CMD_SET_MANUAL_MOTOR,
|
||||
CMD_SET_MANUAL_LIGHT,
|
||||
|
||||
// Time
|
||||
CMD_SET_TIME_HEATER1 = 2301,
|
||||
CMD_SET_TIME_HEATER2,
|
||||
CMD_SET_TIME_MIST,
|
||||
CMD_SET_TIME_FAN,
|
||||
CMD_SET_TIME_MOTOR,
|
||||
CMD_SET_TIME_LIGHT,
|
||||
CMD_SET_TIME_NIGHT,
|
||||
|
||||
// UI
|
||||
CMD_SET_WIFI_CLIENT_DISPLAY = 2401, // Graph TempHigh, TempLow, TimeScale etc.
|
||||
CMD_SET_OLED_CONTRAST,
|
||||
CMD_SET_OLED_DISPLAY_CHECKAC,
|
||||
|
||||
// Debugging
|
||||
CMD_HELLO_DEBUG = 9101,
|
||||
CMD_SHOW_CODE,
|
||||
COMPUTE_TIME_MAX,
|
||||
CMD_INVALID = 9999
|
||||
};
|
||||
|
||||
#pragma pack(push) /* push current alignment to stack */
|
||||
#pragma pack(1) /* set alignment to 1 byte boundary */
|
||||
// TCP Packet
|
||||
typedef struct TCP_PACKET_STRUCT
|
||||
{
|
||||
uint16_t sig1;
|
||||
uint16_t len;
|
||||
uint16_t cmd;
|
||||
uint16_t op; // 8
|
||||
uint32_t time; // 4 : 12
|
||||
union {
|
||||
char ch[40];
|
||||
uint8_t by[40];
|
||||
int16_t n16[20];
|
||||
uint16_t u16[20];
|
||||
int32_t n32[10];
|
||||
uint32_t u32[10];
|
||||
float f[10];
|
||||
STATUS_TYPE status;
|
||||
DEVICE_PARAM_TYPE device;
|
||||
}; //40 : 52
|
||||
uint16_t sig2; // 2 : 54
|
||||
} TCP_PACKET;
|
||||
|
||||
// UDP Packet
|
||||
typedef struct UDP_PACKET_STRUCT
|
||||
{
|
||||
uint16_t sig1;
|
||||
uint16_t m_nSize;
|
||||
uint16_t m_nMessage;
|
||||
uint16_t m_nDeviceType;
|
||||
uint16_t m_nResetReason;
|
||||
// 10
|
||||
uint32_t m_nChipID;
|
||||
uint32_t m_nDeviceID;
|
||||
// 18
|
||||
union {
|
||||
uint32_t m_nVersion;
|
||||
uint32_t clock;
|
||||
};
|
||||
// 22
|
||||
union {
|
||||
uint32_t dwIPAddress;
|
||||
uint8_t byIPAddress[4];
|
||||
};
|
||||
// 26
|
||||
uint16_t m_nPort;
|
||||
// 28
|
||||
uint8_t m_MACAddress[20];
|
||||
// 48
|
||||
char m_sDeviceName[32];
|
||||
// 80
|
||||
union {
|
||||
char m_sService[32];
|
||||
STATUS_TYPE status; //
|
||||
};
|
||||
// 112
|
||||
char m_sCompanyName[32];
|
||||
// 144
|
||||
uint16_t sig2;
|
||||
// Size: 146
|
||||
} UDP_PACKET;
|
||||
|
||||
typedef struct {
|
||||
UDP_PACKET udp;
|
||||
CONFIG_TYPE con;
|
||||
} UDP_CONFIG_TYPE;
|
||||
|
||||
#pragma pack(pop) /* restore original alignment from stack */
|
||||
|
||||
class CWiFiHost
|
||||
{
|
||||
public:
|
||||
void Setup();
|
||||
void Stop();
|
||||
void Loop(unsigned long clock);
|
||||
inline bool isConnected() { return m_bClientConnected; }
|
||||
void CloseConnection();
|
||||
void SendHeartBeat(unsigned long clock);
|
||||
void MonitorUDP();
|
||||
inline void setPublicIPPort(uint32_t ip, uint16_t port) { m_dwPublicIP = ip; m_nPublicPort = port; }
|
||||
|
||||
private:
|
||||
//void setupUDP();
|
||||
//void loopUDP(unsigned long clock);
|
||||
//bool checkOTA(bool bForceUpdate);
|
||||
|
||||
void CheckClient(unsigned long clock);
|
||||
void ProcessPacket(TCP_PACKET& pkt);
|
||||
void SendHeartBeat();
|
||||
int SendConfig();
|
||||
//void FillPacket();
|
||||
|
||||
private:
|
||||
int SendPacket(TCP_PACKET& pkt);
|
||||
size_t SendData(const uint8_t* data, size_t size);
|
||||
bool SendData(unsigned long clockMills);
|
||||
size_t ReceiveData(uint8_t* data, size_t size);
|
||||
bool ReceiveData(unsigned long clockMills);
|
||||
void Restart();
|
||||
|
||||
// Data Send/Receive Mode
|
||||
char *m_pDataSend_data;
|
||||
size_t m_nDataSend_size;
|
||||
size_t m_nDataSend_sent;
|
||||
|
||||
char *m_pDataReceive_data;
|
||||
size_t m_nDataReceive_size;
|
||||
size_t m_nDataReceive_received;
|
||||
|
||||
bool m_bReceiveConfigPending;
|
||||
bool m_bSendHistoryPending;
|
||||
int16_t m_nPendingHistoryCount;
|
||||
|
||||
// SocketConnection
|
||||
IPAddress externalServerIP;
|
||||
//int sockfd = -1;
|
||||
//unsigned long connectStartTime = 0;
|
||||
//bool isConnecting = false;
|
||||
|
||||
//bool initiateConnection(unsigned long clock);
|
||||
//bool checkConnection(unsigned long clock);
|
||||
|
||||
// Operation
|
||||
uint8_t m_nMode;
|
||||
bool m_bHelloSent;
|
||||
unsigned long m_nLastReceivedTime;
|
||||
unsigned long m_nLastHeartBeatSentTime;
|
||||
unsigned long m_nLastUDPBroadcastTime;
|
||||
volatile bool m_bClientConnected;
|
||||
uint32_t m_dwPublicIP;
|
||||
uint16_t m_nPublicPort;
|
||||
IPAddress m_cExternalServerIPAddress;
|
||||
WiFiUDP udpLocal, udpExternal;
|
||||
WiFiServer wifiServer, wifiExternal;
|
||||
WiFiClient wifiClient;
|
||||
UDP_PACKET packetUDP;
|
||||
TCP_PACKET hostPacket;
|
||||
TCP_PACKET clientPacket;
|
||||
};
|
||||
|
||||
extern CWiFiHost host;
|
||||
#endif
|
||||
39
c_cpp_properties.json
Normal file
39
c_cpp_properties.json
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Arduino",
|
||||
"includePath": [
|
||||
"${workspaceFolder}/**",
|
||||
"C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/hardware/esp32/3.0.5/libraries/**",
|
||||
"C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/hardware/esp32/3.0.5/cores/esp32/**",
|
||||
"C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/tools/esp32-arduino-libs/idf-release_v5.1-33fbade6/esp32/include/**",
|
||||
"C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/tools/esp32-arduino-libs/idf-release_v5.1-33fbade6/esp32/include/freertos/FreeRTOS-Kernel/include/**",
|
||||
"C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/tools/esp32-arduino-libs/idf-release_v5.1-33fbade6/esp32/include/freertos/FreeRTOS-Kernel/portable/xtensa/include/**",
|
||||
"C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/tools/esp32-arduino-libs/idf-release_v5.1-33fbade6/esp32/include/freertos/esp_additions/include/**",
|
||||
"C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/tools/esp32-arduino-libs/idf-release_v5.1-33fbade6/esp32/include/freertos/esp_additions/arch/xtensa/include/**",
|
||||
"C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/tools/esp32-arduino-libs/idf-release_v5.1-33fbade6/esp32/qio_qspi/include/**",
|
||||
"C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/hardware/esp32/3.0.5/variants/esp32/**",
|
||||
|
||||
"C:/Users/hxyi/AppData/Local/Arduino15/packages/esp8266/hardware/esp8266/3.1.2/cores/esp8266/**",
|
||||
"C:/Users/hxyi/AppData/Local/Arduino15/packages/esp8266/hardware/esp8266/3.1.2/libraries/**",
|
||||
"C:/Users/hxyi/AppData/Local/Arduino15/packages/esp8266/hardware/esp8266/3.1.2/libraries/ESP8266WiFi/src/**",
|
||||
"C:/Users/hxyi/AppData/Local/Arduino15/packages/esp8266/hardware/esp8266/3.1.2/tools/SDK/include/**",
|
||||
|
||||
"D:/Projects/libraries/ESP-TuyaBLE/src/**",
|
||||
"D:/Projects/libraries/NimBLE-Arduino/src/**"
|
||||
],
|
||||
"defines": [
|
||||
"_DEBUG",
|
||||
"UNICODE",
|
||||
"_UNICODE",
|
||||
"ESP32"
|
||||
],
|
||||
"windowsSdkVersion": "10.0.19041.0",
|
||||
"compilerPath": "C:/Users/hxyi/AppData/Local/Arduino15/packages/esp32/tools/esp-x32/2302/bin/xtensa-esp32-elf-gcc.exe",
|
||||
"cStandard": "c11",
|
||||
"cppStandard": "c++17",
|
||||
"intelliSenseMode": "gcc-x64"
|
||||
}
|
||||
],
|
||||
"version": 4
|
||||
}
|
||||
185
zcd.cpp
Normal file
185
zcd.cpp
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
#include "HermitCrab.h"
|
||||
#include "Config.h"
|
||||
#include "zcd.h"
|
||||
#include <math.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
#include <driver/gptimer.h>
|
||||
|
||||
#define TAG_ZCD "ZCD"
|
||||
// Constants
|
||||
#define EFFECTIVE_POWER 0.86
|
||||
#define LEADING_TIME_RATIO 0.06
|
||||
|
||||
// ESP32 Clock Constants
|
||||
const uint32_t AC_CYCLE_TIME_CLOCKS = 8333; // Half cycle of 60Hz AC in clock cycles
|
||||
const uint32_t EFFECTIVE_HALF_CYCLE = EFFECTIVE_POWER * AC_CYCLE_TIME_CLOCKS; // Effective half cycle in clock cycles
|
||||
const uint32_t LEADING_PULSE_COUNT = EFFECTIVE_HALF_CYCLE * LEADING_TIME_RATIO; // Leading pulse count
|
||||
const uint32_t MAX_PULSE_COUNT = LEADING_PULSE_COUNT + EFFECTIVE_HALF_CYCLE; // Maximum valid pulse count
|
||||
|
||||
|
||||
volatile uint32_t dutyHeater1; // Calculated timerHeater1 count for TRIAC firing
|
||||
volatile uint32_t dutyHeater2; // Calculated timerZCD count for TRIAC firing
|
||||
volatile uint8_t zcdACCount;
|
||||
volatile uint8_t zcdLoadCount;
|
||||
|
||||
hw_timer_t *timerHeater1;
|
||||
hw_timer_t *timerHeater2;
|
||||
|
||||
|
||||
short getHeater1Duty() {
|
||||
if (dutyHeater1 == 0) return 0;
|
||||
if (dutyHeater1 >= LEADING_PULSE_COUNT + EFFECTIVE_HALF_CYCLE) return 10000;
|
||||
return round(10000.0f * (dutyHeater1 - LEADING_PULSE_COUNT) / EFFECTIVE_HALF_CYCLE);
|
||||
}
|
||||
|
||||
short getHeater2Duty() {
|
||||
if (dutyHeater2 == 0) return 0;
|
||||
if (dutyHeater2 >= LEADING_PULSE_COUNT + EFFECTIVE_HALF_CYCLE) return 10000;
|
||||
return round(10000.0f * (dutyHeater1 - LEADING_PULSE_COUNT) / EFFECTIVE_HALF_CYCLE);
|
||||
}
|
||||
|
||||
// Function to set the duty based on percentage (0 to 10000)
|
||||
IRAM_ATTR void setHeater1Duty(short duty) {
|
||||
if (duty <= 0) {
|
||||
dutyHeater1 = 0; // If 0% duty, no pulse (turn off TRIAC)
|
||||
} else if (duty >= 10000) {
|
||||
// 100% duty corresponds to the leading pulse + full effective half cycle
|
||||
dutyHeater1 = LEADING_PULSE_COUNT;
|
||||
} else {
|
||||
// Map duty to power ratio (0 to 1)
|
||||
float powerRatio = (float) duty / 10000.0f;
|
||||
|
||||
// Calculate the angle in radians using the inverse cosine directly
|
||||
float angleRadians = acosf(1.0f - 2.0f * powerRatio);
|
||||
|
||||
// Convert angle to time delay (in clock cycles)
|
||||
// Normalized angle (0 to PI) maps to half-cycle (0 to EFFECTIVE_HALF_CYCLE)
|
||||
uint32_t pulseCount = (angleRadians / M_PI) * EFFECTIVE_HALF_CYCLE;
|
||||
|
||||
dutyHeater1 = LEADING_PULSE_COUNT + EFFECTIVE_HALF_CYCLE - pulseCount;
|
||||
}
|
||||
|
||||
uint32_t nDuty = duty * PWM_FULL / 10000;
|
||||
ledcWrite(PIN_LED_HEATER1, PWM_FULL - nDuty);
|
||||
ESP_LOGD(TAG_ZCD,"Set Duty: %.2f%%, Timer Count: %u clock cycles", duty, dutyHeater1);
|
||||
}
|
||||
|
||||
// Function to set the duty based on percentage (0 to 10000)
|
||||
IRAM_ATTR void setHeater2Duty(short duty) {
|
||||
if (config.bAC2_OnOff) {
|
||||
if (duty >= 10000) {
|
||||
digitalWrite(PIN_HEATER2, HEATER_ON);
|
||||
duty = 10000;
|
||||
} else {
|
||||
digitalWrite(PIN_HEATER2, HEATER_OFF);
|
||||
duty = 0;
|
||||
}
|
||||
dutyHeater2 = 0;
|
||||
} else {
|
||||
if (duty <= 0) {
|
||||
dutyHeater2 = 0; // If 0% duty, no pulse (turn off TRIAC)
|
||||
} else if (duty >= 10000) {
|
||||
// 100% duty corresponds to the leading pulse + full effective half cycle
|
||||
dutyHeater2 = LEADING_PULSE_COUNT;
|
||||
} else {
|
||||
// Map duty to power ratio (0 to 1)
|
||||
float powerRatio = (float) duty / 10000.0f;
|
||||
|
||||
// Calculate the angle in radians using the inverse cosine directly
|
||||
float angleRadians = acosf(1.0f - 2.0f * powerRatio);
|
||||
|
||||
// Convert angle to time delay (in clock cycles)
|
||||
// Normalized angle (0 to PI) maps to half-cycle (0 to EFFECTIVE_HALF_CYCLE)
|
||||
uint32_t pulseCount = (angleRadians / M_PI) * EFFECTIVE_HALF_CYCLE;
|
||||
|
||||
dutyHeater2 = LEADING_PULSE_COUNT + EFFECTIVE_HALF_CYCLE - pulseCount;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t nDuty = duty * PWM_FULL / 10000;
|
||||
ledcWrite(PIN_LED_HEATER2, PWM_FULL - nDuty);
|
||||
ESP_LOGD(TAG_ZCD,"Set Duty: %.2f%%, Timer Count: %u clock cycles\n", duty, dutyHeater1);
|
||||
}
|
||||
|
||||
|
||||
void ARDUINO_ISR_ATTR onTimer1() {
|
||||
digitalWrite(PIN_HEATER1, HIGH); // Fire TRIAC
|
||||
delayMicroseconds(10); // Short pulse to trigger TRIAC
|
||||
digitalWrite(PIN_HEATER1, LOW); // Turn off TRIAC trigger
|
||||
}
|
||||
|
||||
void ARDUINO_ISR_ATTR onTimer2() {
|
||||
digitalWrite(PIN_HEATER2, HIGH); // Fire TRIAC
|
||||
delayMicroseconds(10); // Short pulse to trigger TRIAC
|
||||
digitalWrite(PIN_HEATER2, LOW); // Turn off TRIAC trigger
|
||||
}
|
||||
|
||||
// Zero-Cross Detection Interrupt Service Routine
|
||||
void ARDUINO_ISR_ATTR zcdACISR() {
|
||||
uint32_t clock = micros();
|
||||
static uint32_t lastClock = 0l;
|
||||
|
||||
if (clock - lastClock < 8000)
|
||||
return;
|
||||
lastClock = clock;
|
||||
|
||||
zcdACCount++;
|
||||
|
||||
// Heater 1
|
||||
if (dutyHeater1 == MAX_PULSE_COUNT) {
|
||||
onTimer1();
|
||||
}
|
||||
else if (dutyHeater1 >= LEADING_PULSE_COUNT && dutyHeater1 < MAX_PULSE_COUNT) {
|
||||
// Stop the timer, configure new alarm, then explicitly start
|
||||
timerStop(timerHeater1); // Stop any existing timer action
|
||||
timerWrite(timerHeater1, 0); // Reset counter to 0
|
||||
timerAlarm(timerHeater1, dutyHeater1, false, 0); // Set alarm with updated duty
|
||||
timerStart(timerHeater1); // Start the timer explicitly
|
||||
}
|
||||
// Heater 2
|
||||
if (dutyHeater2 == MAX_PULSE_COUNT) {
|
||||
onTimer2();
|
||||
}
|
||||
else if (dutyHeater2 >= LEADING_PULSE_COUNT && dutyHeater2 < MAX_PULSE_COUNT) {
|
||||
// Stop the timer, configure new alarm, then explicitly start
|
||||
timerStop(timerHeater2); // Stop any existing timer action
|
||||
timerWrite(timerHeater2, 0); // Reset counter to 0
|
||||
timerAlarm(timerHeater2, dutyHeater2, false, 0); // Set alarm with updated duty
|
||||
timerStart(timerHeater2); // Start the timer explicitly
|
||||
}
|
||||
}
|
||||
|
||||
void ARDUINO_ISR_ATTR zcdLoadISR() {
|
||||
zcdLoadCount++;
|
||||
}
|
||||
|
||||
void setupZCD() {
|
||||
pinMode(PIN_ZCD_AC, INPUT);
|
||||
pinMode(PIN_ZCD_LOAD, INPUT);
|
||||
pinMode(PIN_HEATER1, OUTPUT);
|
||||
pinMode(PIN_HEATER2, OUTPUT);
|
||||
|
||||
dutyHeater1 = 0; // Calculated timerHeater1 count for TRIAC firing
|
||||
dutyHeater2 = 0; // Calculated timerZCD count for TRIAC firing
|
||||
zcdACCount = 0;
|
||||
zcdLoadCount = 0;
|
||||
timerHeater1 = NULL;
|
||||
|
||||
attachInterrupt(PIN_ZCD_AC, zcdACISR, CHANGE); // Attach zero-cross detection ISR
|
||||
attachInterrupt(PIN_ZCD_LOAD, zcdLoadISR, CHANGE); // Attach zero-cross detection ISR
|
||||
|
||||
// Initialize and configure the timer
|
||||
if ((timerHeater1 = timerBegin(1000000)) != NULL) {
|
||||
timerAttachInterrupt(timerHeater1, &onTimer1); // Attach TRIAC firing routine
|
||||
timerStop(timerHeater1); // Ensure timer is stopped initially
|
||||
timerStart(timerHeater1); // Explicitly start the timer after setup
|
||||
|
||||
}
|
||||
|
||||
if ((timerHeater2 = timerBegin(1000000)) != NULL) {
|
||||
timerAttachInterrupt(timerHeater2, &onTimer2); // Attach TRIAC firing routine
|
||||
timerStop(timerHeater2); // Ensure timer is stopped initially
|
||||
timerStart(timerHeater2); // Explicitly start the timer after setup
|
||||
}
|
||||
}
|
||||
13
zcd.h
Normal file
13
zcd.h
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
#ifndef __ZCD_H
|
||||
#define __ZCD_H
|
||||
extern volatile uint8_t zcdACCount, zcdLoadCount;
|
||||
|
||||
// ZCD
|
||||
void setupZCD();
|
||||
void setHeater1Duty(short duty);
|
||||
void setHeater2Duty(short duty);
|
||||
short getHeater1Duty();
|
||||
#if defined(ESP8266)
|
||||
void setMistDuty(short duty);
|
||||
#endif
|
||||
#endif
|
||||
Loading…
Reference in New Issue
Block a user