New HTTP Updater

This commit is contained in:
Heuideog Yi @ PC RnD1 2026-04-09 03:42:34 +09:00
parent 3276c9527d
commit 9e2873829c
14 changed files with 1030 additions and 1228 deletions

View File

@ -94,7 +94,7 @@ class HCScanCallbacks : public NimBLEScanCallbacks {
*/
void onResult(const NimBLEAdvertisedDevice* advertisedDevice) override {
uint16_t len;
bthome_t *pTuyaData = (bthome_t *) advertisedDevice->getServiceData(&len);
bthome_t *pTuyaData = (bthome_t *) advertisedDevice->getServiceData((uint16_t *) &len);
// Tuya
if (len == sizeof(bthome_t) && pTuyaData->info == 0x40) {
if (config.nTemp1SensorType == TEMP_SENSOR_TYPE::BLE_TUYA) {

View File

@ -1,6 +1,8 @@
#ifndef __BLE_SCAN_H
#define __BLE_SCAN_H
#include <Arduino.h>
class NimBLEClient;
class NimBLERemoteCharacteristic;
class NimBLEAddress;

View File

@ -81,7 +81,7 @@ void CONFIG_TYPE::init() {
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_nPublicPort = 3939;
m_nDeviceType = THIS_DEVICE_TYPE;
//

View File

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

106
OTA.cpp
View File

@ -5,7 +5,7 @@
#include <HTTPClient.h>
#include <esp_task_wdt.h>
#include "HCUpdate.h"
#include "src/ESPUpdate.h"
#include "HermitCrab.h"
#include "Config.h"
#include "ConnectWiFi.h"
@ -65,70 +65,62 @@ 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;
ESPUpdateClass ESPUpdate;
client.setCACert(rootCACertificate);
UpdateClass hcUpdate(5000);
hcUpdate.onProgress(onOTAProgress);
//int update(WiFiClient& client, String &url, uint16_t port, String& uri,
// String &currentVersion, short nDeviceType, bool bForceUpdate);
int result = hcUpdate.update(client, url, UPDATE_PORT, uri, version, (short)config.m_nDeviceType, true);
ESPUpdate.onProgress(onOTAProgress);
// result: POSITIVE (HTTP/Status), NEGATIVE (Network Error)
int result = ESPUpdate.update(
client,
"visionsoft.kr", // Server Host
443, // Server Port HTTPS,
"/sc/pages/firmware_download.php", // phpUri,
HC__VERSION, // Current Version string
"HCesp", // Project Tag
bForceUpdate, // bForceUpdate
true // bRebootAfterInstall
);
// ======================================================================
// STATUS REPORTING LOGIC
// ======================================================================
bool _setupMode = true;
if (result == 200) {
if (_setupMode) {
Serial.println("[OTA] Success: Firmware downloaded and installed.");
Serial.println("[OTA] Rebooting...");
}
delay(1000);
ESP.restart();
}
else if (result == 204) {
if (_setupMode) {
Serial.println("[OTA] Not Available: No firmware binary found for this project.");
}
}
else if (result == 304) {
if (_setupMode) {
Serial.println("[OTA] Up To Date: Current firmware is the latest version.");
}
}
else if (result > 0) {
// Captures 403 (Forbidden), 404 (Not Found), 500 (Server Error)
if (_setupMode) {
Serial.printf("[OTA] Server Error: HTTP %d\n", result);
}
}
else if (result < 0) {
// Captures library errors like -1 (Connection Refused) or -11 (Timeout)
if (_setupMode) {
Serial.printf("[OTA] Connection Error: %s (%d)\n",
HTTPClient::errorToString(result).c_str(), result);
}
}
return false;
}

135
OTA.cpp.backup Normal file
View File

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

View File

@ -68,20 +68,20 @@ void setup() {
setupSensor();
ui.setup();
ui.message(0, "WiFi...");
ui.message(0, (char *) "WiFi...");
setupWiFi();
if (aht25.sensor() || aht10_0x39.sensor()) {
ui.message(4, "Sensor... OK!");
ui.message(4, (char *) "Sensor... OK!");
} else {
ui.message(4, "Sensor... None!");
ui.message(4, (char *) "Sensor... None!");
}
ui.message(5, "ZCD...");
ui.message(5, (char *) "ZCD...");
setupZCD();
ui.message(5, "ZCD... OK!");
ui.message(5, (char *) "ZCD... OK!");
ui.message(6, "Setup OK!");
ui.message(6, (char *) "Setup OK!");
//if (!isWiFiConnected) delay(3000);
ble.setupConnect(config.nBLESensorAddr, config.nBLESensorAddr2);
@ -111,8 +111,6 @@ 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() {
@ -146,7 +144,7 @@ void setupWiFi() {
if (WiFi.status() == WL_CONNECTED) {
ledcWrite(PIN_LED_WIFI, PWM_FULL); // LED_OFF
ui.message(0, "WiFi...OK!");
ui.message(0, (char *) "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;
@ -162,35 +160,35 @@ void setupWiFi() {
void setupPostWiFi(bool bBoot = false) {
if (WiFi.status() == WL_CONNECTED) {
// Time
if (bBoot) ui.message(1, "Time...");
if (bBoot) ui.message(1, (char *) "Time...");
timeManager.begin();
vTaskDelay((bBoot ? 500 : 250)/portTICK_PERIOD_MS);
timeManager.checkNTPResponse();
if (bBoot) {
if (timeManager.hasNTPUpdate()) {
ui.message(1, "Time...OK!");
ui.message(1, (char *) "Time...OK!");
// OTA
DPRINTLN("Setup - TimeManager.begin()");
DPRINTLN("\n===============================\n");
DPRINTLN(" Trying OTA");
ui.message(2, "Update check...");
ui.message(2, (char *) "Update check...");
checkOTA(true);
ui.message(2, "Update check...OK!");
ui.message(2, (char *) "Update check...OK!");
DPRINTLN(" OTA Process completed!");
DPRINTLN("===============================\n");
} else {
ui.message(2, "Update check SKIPPED!");
ui.message(2, (char *) "Update check SKIPPED!");
timeManager.sendNTPRequest();
}
}
// Host
if (bBoot) ui.message(3, "Server...");
if (bBoot) ui.message(3, (char *) "Server...");
host.Setup();
DPRINTLN("Setup - host.Setup()");
if (bBoot) ui.message(3, "Server...OK!");
if (bBoot) ui.message(3, (char *) "Server...OK!");
g_bWiFiSetupExecuted = true;
}
}

View File

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

View File

@ -18,7 +18,7 @@
#include "zcd.h"
#include "AHT2x.h"
#include "WiFiHost.h"
#include "UPnPClient.h"
//#include "UPnPClient.h"
@ -64,15 +64,10 @@ void CWiFiHost::Setup()
.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;
}
//m_nPublicPort = config.m_nPublicPort;
wifiServer = WiFiServer(SERVER_PORT, 1);
wifiExternal = WiFiServer(m_nPublicPort, 1);
//wifiExternal = WiFiServer(m_nPublicPort, 1);
//wifiStatus = WIFI_NOT_CONNECTED;
m_nDataSend_sent = 0;
@ -96,36 +91,36 @@ void CWiFiHost::Setup()
m_nLastHeartBeatSentTime =
m_nLastUDPBroadcastTime = millis();
m_bClientConnected = false;
m_dwPublicIP = 0;
//m_dwPublicIP = 0;
wifiClient.stop();
if (isWiFiConnected())
{
// UPnP Client
{
uint32_t ip = WiFi.gatewayIP();
uint16_t port = m_nPublicPort;
CUpnpClient upnp;
// {
// 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 (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;
}
// 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);
//wifiExternal.begin(m_nPublicPort, 1);
m_nLastReceivedTime = millis();
m_bClientConnected = false;
@ -150,7 +145,7 @@ MY_IRAM_ATTR void CWiFiHost::Stop() {
// Stop server
wifiServer.stop();
wifiExternal.stop();
//wifiExternal.stop();
m_bClientConnected = false;
// Stop Client
@ -171,87 +166,86 @@ MY_IRAM_ATTR void CWiFiHost::CloseConnection()
IRAM_ATTR void CWiFiHost::Loop(unsigned long clock)
{
static unsigned long lastReceivedTime = 0;
if (!isWiFiConnected()) return;
// --- State: WAITING (Accepting new clients) ---
if (m_nMode == MODE_WAITING) {
// If we think we are connected but the object is dead, reset
if (m_bClientConnected) {
wifiClient.stop();
m_bClientConnected = false;
}
// Try accepting from internal, then external if internal fails
wifiClient = wifiServer.accept();
//if (!wifiClient) 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;
ledcWrite(PIN_LED_WIFI, PWM_FULL - 10);
}
return;
}
// --- Global Safety Check for all other modes ---
// If we lose connection mid-process, jump back to WAITING immediately
if (!wifiClient || !wifiClient.connected()) {
m_bClientConnected = false;
m_nMode = MODE_WAITING;
ledcWrite(PIN_LED_WIFI, PWM_FULL - 2);
return;
}
// --- Main State Machine ---
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);
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()) {
// Timeout Check (60 seconds)
if (clock - m_nLastReceivedTime > 60000) {
ESP_LOGW(TAG_WIFI_HOST, "Host: Timeout - Dropping Connection");
wifiClient.stop();
m_bClientConnected = false;
m_nMode = MODE_WAITING;
// LED
ledcWrite(PIN_LED_WIFI, PWM_FULL - 2); // Almost Off
}
// Heartbeat Pulse (Every 1 second)
else if (clock - m_nLastHeartBeatSentTime >= 1000) {
SendHeartBeat();
m_nLastHeartBeatSentTime = clock;
}
break;
case MODE_SEND:
// SendData returns true only when the CURRENT buffer is empty
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();
// Check if there is a second part of the history ring buffer
if (m_bSendHistoryPending && m_nPendingHistoryCount > 0) {
ESP_LOGI(TAG_WIFI_HOST, "WiFi Host: Preparing History Part 2");
// Setup the next chunk
SendData(history.getRingData2(), sizeof(STATUS_TYPE) * m_nPendingHistoryCount);
// Clear flags so we don't loop here forever
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;
}
m_nPendingHistoryCount = 0;
// We stay in MODE_SEND. The next Loop() turn will start sending Part 2.
} else {
// All data (including history parts) is done
m_nMode = MODE_PACKET;
}
}
break;
case MODE_RECV:
if (ReceiveData(clock)) m_nMode = MODE_PACKET;
if (ReceiveData(clock)) {
m_nMode = MODE_PACKET;
}
break;
}
}
@ -271,8 +265,8 @@ MY_IRAM_ATTR void CWiFiHost::SendHeartBeat(unsigned long clock) {
// External Heartbeat
UDP_CONFIG_TYPE pktConfig;
pktConfig.udp = packetUDP;
pktConfig.udp.m_nPort = m_nPublicPort;
pktConfig.udp.dwIPAddress = m_dwPublicIP;
pktConfig.udp.m_nPort = 3939;
pktConfig.udp.dwIPAddress = (uint32_t)WiFi.localIP();
pktConfig.udp.status = status;
if (((uint32_t)m_cExternalServerIPAddress) != (uint32_t)0l)
@ -363,35 +357,31 @@ MY_IRAM_ATTR void CWiFiHost::MonitorUDP() {
IRAM_ATTR void CWiFiHost::CheckClient(unsigned long clock)
{
bool bLED = false;
static TCP_PACKET cpkt;
// 1. Quick exit if no data is ready to be read
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;
}
if (available < sizeof(TCP_PACKET)) 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) {
};
static TCP_PACKET cpkt;
// 2. Peek or Read the packet
// We use readBytes because we already verified 'available' >= size
int count = wifiClient.readBytes((uint8_t*)&cpkt, sizeof(TCP_PACKET));
if (count == sizeof(TCP_PACKET)) {
// 3. Validation Logic
bool sigMatch = (cpkt.sig1 == SIGNATURE1 && cpkt.sig2 == SIGNATURE2);
bool lenMatch = (cpkt.len == sizeof(TCP_PACKET));
if (sigMatch && lenMatch) {
ProcessPacket(cpkt); // Cleanly dispatched to sub-handlers now
m_nLastReceivedTime = clock;
} else {
// 4. Recovery Logic: Instead of a blocking while-loop,
// clear the socket buffer and wait for the next loop cycle.
ESP_LOGW(TAG_WIFI_HOST, "Protocol out of sync. Flushing %d bytes", available);
while(wifiClient.available() > 0) {
wifiClient.read(); // Efficiently dump the garbage
}
}
}
}
@ -745,49 +735,36 @@ IRAM_ATTR size_t CWiFiHost::SendData(const uint8_t* data, size_t size) {
return 0;
}
IRAM_ATTR bool CWiFiHost::SendData(unsigned long clock)
IRAM_ATTR // This function sends a small "chunk" then returns false.
// It returns true ONLY when the entire buffer is finished.
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 (!m_pDataSend_data || m_nDataSend_size == 0) return true;
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;
}
// Calculate how much is left
size_t remaining = m_nDataSend_size - m_nDataSend_sent;
// Send a "Chunk" (e.g., 512 bytes).
// Sending too much at once blocks the CPU.
// Sending too little makes the WiFi overhead too high.
size_t chunkSize = (remaining > 512) ? 512 : remaining;
size_t written = wifiClient.write((const uint8_t*)(m_pDataSend_data + m_nDataSend_sent), chunkSize);
if (written > 0) {
m_nDataSend_sent += written;
}
if (m_nDataSend_sent == m_nDataSend_size) {
ESP_LOGI(TAG_WIFI_HOST, " SendDdata: DataSend completed!");
return true;
// If we aren't done, return false to stay in MODE_SEND for the next Loop()
if (m_nDataSend_sent < m_nDataSend_size) {
return false;
}
return false;
// Done! Reset pointers
m_pDataSend_data = nullptr;
m_nDataSend_size = 0;
m_nDataSend_sent = 0;
return true;
}
IRAM_ATTR size_t CWiFiHost::ReceiveData(uint8_t* data, size_t size)

View File

@ -219,7 +219,7 @@ public:
void CloseConnection();
void SendHeartBeat(unsigned long clock);
void MonitorUDP();
inline void setPublicIPPort(uint32_t ip, uint16_t port) { m_dwPublicIP = ip; m_nPublicPort = port; }
//inline void setPublicIPPort(uint32_t ip, uint16_t port) { m_dwPublicIP = ip; m_nPublicPort = port; }
private:
//void setupUDP();
@ -269,11 +269,11 @@ private:
unsigned long m_nLastHeartBeatSentTime;
unsigned long m_nLastUDPBroadcastTime;
volatile bool m_bClientConnected;
uint32_t m_dwPublicIP;
uint16_t m_nPublicPort;
//uint32_t m_dwPublicIP;
//uint16_t m_nPublicPort;
IPAddress m_cExternalServerIPAddress;
WiFiUDP udpLocal, udpExternal;
WiFiServer wifiServer, wifiExternal;
WiFiServer wifiServer; //, wifiExternal;
WiFiClient wifiClient;
UDP_PACKET packetUDP;
TCP_PACKET hostPacket;

View File

@ -1,22 +1,37 @@
#if defined(ESP32)
/*
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
* [HCUpdate.cpp] - UNIFIED FIRMWARE UPDATE IMPLEMENTATION
* --------------------------------------------------------------------------------------------
* PURPOSE:
* - Implements OTA update logic for both ESP32 and ESP8266.
* - ESP32: Uses HTTPS (WiFiClientSecure) and esp_ota_ops for partition management.
* - ESP8266: Uses HTTP (WiFiClient) and internal Updater class to save RAM.
* - Common: Both use the same AES-256 decryption and Safe-Flash (16-byte stash) logic.
* --------------------------------------------------------------------------------------------
* Revision History:
* 2024.xx.xx - Espressif Systems original structure.
* 2026.04.08 - [RnD12] Unified logic. Added HTTP/HTTPS toggling and status code negation.
* Restored full 800+ line complexity including AES Tweak and Decrypt logic.
*/
#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"
#include "ESPUpdate.h"
#define TAG_FW "FW Upate"
// =============================================================
// CHIPSET SPECIFIC HEADERS
// =============================================================
#if defined(ESP32)
#include "spi_flash_mmap.h"
#include "esp_ota_ops.h"
#include "esp_image_format.h"
#include "mbedtls/aes.h"
#include <WiFi.h>
#define TAG_FW "FW Update"
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#include <WiFiClient.h>
#endif
// =============================================================
static const char *_err2str(uint8_t _error) {
if (_error == UPDATE_ERROR_OK) {
@ -51,73 +66,158 @@ static const char *_err2str(uint8_t _error) {
return ("UNKNOWN");
}
int UpdateClass::update(WiFiClientSecure& client, String& host, uint16_t port, String&uri, String& currentVersion, short nDeviceType, bool bForceUpdate)
// =============================================================
// ESP32 SECURE UPDATE (HTTPS)
// =============================================================
#if defined(ESP32)
int ESPUpdateClass::update(WiFiClientSecure& client,
const char *host,
uint16_t port,
const char *uri,
const char *currentVersion,
const char *projectTag,
bool bForceUpdate,
bool bRebootAfterInstall)
{
HTTPClient http;
if (!http.begin(client, host, port, uri)) {
ESP_LOGI(TAG_FW,"OTA - httpClient begin failed\n");
return HTTP_UPDATE_FAILED;
return -1;
}
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" );
int ret = handleUpdate(http, currentVersion, projectTag);
int size = 0;
//is there an image to download
if (size >= 0) {
if (ret < 0) {
ret = -ret;
} else {
size = ret;
}
ESP_LOGI(TAG_FW,"OTA - status(%d) Size(%d) Server Ver: %s\n",
ret, size, http.header("version").c_str());
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
ESP_LOGI(TAG_FW,"OTA - No Firmware available for this project");
ret = 204;
}
else if (!http.header("version") || strcmp(http.header("version").c_str(), currentVersion) <= 0) {
ESP_LOGI(TAG_FW,"OTA - Firmware is already up to Date");
ret = 304;
}
else {
if (!bForceUpdate) {
ESP_LOGI(TAG_FW,"OTA - Found V%s Firmware\n", http.header("version").c_str());
ESP_LOGI(TAG_FW,"OTA - Found Newer 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();
if (bRebootAfterInstall) {
ESP_LOGI(TAG_FW,"OTA - Success, rebooting");
delay(1000);
ESP.restart();
} else {
ESP_LOGI(TAG_FW,"OTA - Success, skipping reboot");
return 200;
}
}
}
}
}
http.end(); //end connection
return 0;
http.end();
return ret;
}
#endif
// =============================================================
int UpdateClass::handleUpdate(HTTPClient& http, const String& currentVersion, short nDeviceType)
// =============================================================
// ESP8266 PLAIN UPDATE (HTTP)
// =============================================================
#if defined(ESP8266)
int ESPUpdateClass::update(WiFiClient& client,
const char *host,
uint16_t port,
const char *uri,
const char *currentVersion,
const char *projectTag,
bool bForceUpdate,
bool bRebootAfterInstall)
{
HTTPUpdateResult ret = HTTP_UPDATE_FAILED;
HTTPClient http;
if (!http.begin(client, host, port, uri)) {
Serial.println("OTA - httpClient begin failed");
return -1;
}
// use HTTP/1.0 for update since the update handler not support any transfer Encoding
int ret = handleUpdate(http, currentVersion, projectTag);
int size = 0;
if (ret < 0) {
ret = -ret;
} else {
size = ret;
}
if (size > 0) {
if (http.header("update").toInt() == 0) {
ret = 204;
}
else if (strcmp(http.header("version").c_str(), currentVersion) <= 0) {
ret = 304;
}
else {
if (bForceUpdate) {
if (http_downloadUpdate(http) == 0) {
http.end();
if (bRebootAfterInstall) {
delay(1000);
ESP.restart();
} else {
return 200;
}
}
}
}
}
http.end();
return ret;
}
#endif
// =============================================================
int ESPUpdateClass::handleUpdate(HTTPClient& http, const char *currentVersion, const char *projectTag)
{
http.useHTTP10(true);
http.setTimeout(_httpClientTimeout);
http.setFollowRedirects(_followRedirects);
// =============================================================
// CHIPSET SPECIFIC IDENTIFICATION
// =============================================================
#if defined(ESP32)
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());
http.addHeader("X-ESP32-SKETCH-SIZE", String(ESP.getSketchSize()));
http.addHeader("X-ESP32-SKETCH-MD5", String(ESP.getSketchMD5()));
http.addHeader("X-ESP32-CHIP-SIZE", String(ESP.getFlashChipSize()));
#else
http.setUserAgent("ESP8266-http-Update");
http.addHeader("X-ESP8266-STA-MAC", WiFi.macAddress());
http.addHeader("X-ESP8266-SKETCH-SIZE", String(ESP.getSketchSize()));
http.addHeader("X-ESP8266-CHIP-SIZE", String(ESP.getFlashChipSize()));
#endif
// =============================================================
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());
http.addHeader("Cache-Control", "no-cache");
http.addHeader("X-ESP-PROJECT", projectTag);
http.addHeader("X-ESP-VERSION", currentVersion);
//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
const char *headerkeys[] = {"update", "version"};
size_t headerkeyssize = sizeof(headerkeys) / sizeof(char *);
http.collectHeaders(headerkeys, headerkeyssize);
@ -125,67 +225,48 @@ int UpdateClass::handleUpdate(HTTPClient& http, const String& currentVersion, sh
int len = http.getSize();
if (code == HTTP_CODE_OK) {
return (len > 0 ? len : 0); //return 0 or length of image to download
return (len > 0 ? len : 0);
} 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
return code;
} 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 -code;
}
return ret;
}
int UpdateClass::http_downloadUpdate(HTTPClient &httpClient) {
WiFiClient *stream = httpClient.getStreamPtr();
int ESPUpdateClass::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!");
if (!ESPUpdate.begin(fileSize, U_FLASH)) {
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
uint8_t buffer[128];
int bytesRead = stream->readBytes(buffer, min(availableBytes, sizeof(buffer)));
int bytesWritten = Update.write(buffer, bytesRead);
int bytesWritten = ESPUpdate.write(buffer, bytesRead);
if (bytesWritten != bytesRead) {
ESP_LOGI(TAG_FW,"Update write failed!");
Update.end();
ESPUpdate.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;
}
if (ESPUpdate.end()) {
return ESPUpdate.isFinished() ? 0 : 1;
} else {
ESP_LOGI(TAG_FW,"Update end failed!");
return 1;
}
}
#if defined(ESP32)
static bool _partitionIsBootable(const esp_partition_t *partition) {
uint8_t buf[ENCRYPTED_BLOCK_SIZE];
if (!partition) {
@ -194,21 +275,21 @@ static bool _partitionIsBootable(const esp_partition_t *partition) {
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) {
bool ESPUpdateClass::_enablePartition(const esp_partition_t *partition) {
if (!partition) {
return false;
}
return ESP.partitionWrite(partition, 0, (uint32_t *)_skipBuffer, ENCRYPTED_BLOCK_SIZE);
}
#endif
UpdateClass::UpdateClass()
ESPUpdateClass::ESPUpdateClass()
: _error(0)
, _cryptKey(0)
, _cryptBuffer(0)
@ -227,7 +308,7 @@ UpdateClass::UpdateClass()
, _httpClientTimeout(8000)
{}
UpdateClass::UpdateClass(int httpClientTimeout)
ESPUpdateClass::ESPUpdateClass(int httpClientTimeout)
: _error(0)
, _cryptKey(0)
, _cryptBuffer(0)
@ -246,19 +327,18 @@ UpdateClass::UpdateClass(int httpClientTimeout)
, _httpClientTimeout(httpClientTimeout)
{}
UpdateClass &UpdateClass::onProgress(THandlerFunction_Progress fn) {
ESPUpdateClass &ESPUpdateClass::onProgress(THandlerFunction_Progress fn) {
_progress_callback = fn;
return *this;
}
void UpdateClass::_reset() {
void ESPUpdateClass::_reset() {
if (_buffer) {
delete[] _buffer;
}
if (_skipBuffer) {
delete[] _skipBuffer;
}
_cryptBuffer = nullptr;
_buffer = nullptr;
_skipBuffer = nullptr;
@ -268,34 +348,41 @@ void UpdateClass::_reset() {
_command = U_FLASH;
if (_ledPin != -1) {
digitalWrite(_ledPin, !_ledOn); // off
digitalWrite(_ledPin, !_ledOn);
}
}
bool UpdateClass::canRollBack() {
if (_buffer) { //Update is running
bool ESPUpdateClass::canRollBack() {
#if defined(ESP32)
if (_buffer) {
return false;
}
const esp_partition_t *partition = esp_ota_get_next_update_partition(NULL);
return _partitionIsBootable(partition);
#else
return false;
#endif
}
bool UpdateClass::rollBack() {
if (_buffer) { //Update is running
bool ESPUpdateClass::rollBack() {
#if defined(ESP32)
if (_buffer) {
return false;
}
const esp_partition_t *partition = esp_ota_get_next_update_partition(NULL);
return _partitionIsBootable(partition) && !esp_ota_set_boot_partition(partition);
#else
return false;
#endif
}
bool UpdateClass::begin(size_t size, int command, int ledPin, uint8_t ledOn, const char *label) {
bool ESPUpdateClass::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)
_ledOn = !!ledOn;
_reset();
_error = 0;
@ -307,19 +394,19 @@ bool UpdateClass::begin(size_t size, int command, int ledPin, uint8_t ledOn, con
return false;
}
#if defined(ESP32)
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
_paroffset = 0x1000;
if (!_partition) {
_error = UPDATE_ERROR_NO_PARTITION;
return false;
@ -327,7 +414,6 @@ bool UpdateClass::begin(size_t size, int command, int ledPin, uint8_t ledOn, con
}
} else {
_error = UPDATE_ERROR_BAD_ARGUMENT;
log_e("bad command %u", command);
return false;
}
@ -335,14 +421,12 @@ bool UpdateClass::begin(size_t size, int command, int ledPin, uint8_t ledOn, con
size = _partition->size;
} else if (size > _partition->size) {
_error = UPDATE_ERROR_SIZE;
log_e("too large %u > %u", size, _partition->size);
return false;
}
#endif
//initialize
_buffer = new (std::nothrow) uint8_t[SPI_FLASH_SEC_SIZE];
if (!_buffer) {
log_e("_buffer allocation failed");
return false;
}
_size = size;
@ -351,7 +435,7 @@ bool UpdateClass::begin(size_t size, int command, int ledPin, uint8_t ledOn, con
return true;
}
bool UpdateClass::setupCrypt(const uint8_t *cryptKey, size_t cryptAddress, uint8_t cryptConfig, int cryptMode) {
bool ESPUpdateClass::setupCrypt(const uint8_t *cryptKey, size_t cryptAddress, uint8_t cryptConfig, int cryptMode) {
if (setCryptKey(cryptKey)) {
if (setCryptMode(cryptMode)) {
setCryptAddress(cryptAddress);
@ -362,50 +446,46 @@ bool UpdateClass::setupCrypt(const uint8_t *cryptKey, size_t cryptAddress, uint8
return false;
}
bool UpdateClass::setCryptKey(const uint8_t *cryptKey) {
bool ESPUpdateClass::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
return false;
}
//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) {
bool ESPUpdateClass::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) {
void ESPUpdateClass::_abort(uint8_t err) {
_reset();
_error = err;
}
void UpdateClass::abort() {
void ESPUpdateClass::abort() {
_abort(UPDATE_ERROR_ABORT);
}
void UpdateClass::_cryptKeyTweak(size_t cryptAddress, uint8_t *tweaked_key) {
void ESPUpdateClass::_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
return;
}
const uint8_t pattern[] = {23, 23, 23, 14, 23, 23, 23, 12, 23, 23, 23, 10, 23, 23, 23, 8};
@ -413,45 +493,42 @@ void UpdateClass::_cryptKeyTweak(size_t cryptAddress, uint8_t *tweaked_key) {
int key_idx = 0;
int bit_len = 0;
uint32_t tweak = 0;
cryptAddress &= 0x00ffffe0; //bit 23-5
cryptAddress <<= 8; //bit23 shifted to bit31(MSB)
cryptAddress &= 0x00ffffe0;
cryptAddress <<= 8;
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)
tweak = cryptAddress << (23 - pattern[pattern_idx]);
tweak = (tweak << (8 - bit_len)) | (tweak >> (24 + bit_len));
bit_len += pattern[pattern_idx++] - 4;
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)
tweaked_key[key_idx++] ^= tweak;
tweak = (tweak << 8) | (tweak >> 24);
bit_len -= 8;
}
tweaked_key[key_idx] ^= tweak; //XOR remaining bits, will XOR zeros if no remaining bits
tweaked_key[key_idx] ^= tweak;
}
if (_cryptCfg == 0xf) {
return; //return with fully tweaked key
return;
}
//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
if ((_cryptCfg & (1 << pattern_idx)) == 0) {
while (bit_len > 0) {
if (bit_len > 7 || ((_cryptCfg & (2 << pattern_idx)) == 0)) { //restore a crypt key byte
if (bit_len > 7 || ((_cryptCfg & (2 << pattern_idx)) == 0)) {
tweaked_key[key_idx] = _cryptKey[key_idx];
} else { //MSBits restore crypt key bits, LSBits keep as tweaked bits
} else {
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
} else {
while (bit_len > 0) {
if (bit_len < 8 && ((_cryptCfg & (2 << pattern_idx)) == 0)) { //MSBits keep as tweaked bits, LSBits restore crypt key bits
if (bit_len < 8 && ((_cryptCfg & (2 << pattern_idx)) == 0)) {
tweaked_key[key_idx] &= (~(0xff >> bit_len));
tweaked_key[key_idx] |= (_cryptKey[key_idx] & (0xff >> bit_len));
}
@ -463,40 +540,31 @@ void UpdateClass::_cryptKeyTweak(size_t cryptAddress, uint8_t *tweaked_key) {
}
}
bool UpdateClass::_decryptBuffer() {
bool ESPUpdateClass::_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
uint8_t tweaked_key[ENCRYPTED_KEY_SIZE];
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
#if defined(ESP32)
mbedtls_aes_context ctx;
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
_cryptBuffer[(ENCRYPTED_BLOCK_SIZE - 1) - i] = _buffer[i + done];
}
if (((_cryptAddress + _progress + done) % ENCRYPTED_TWEAK_BLOCK_SIZE) == 0 || done == 0) {
_cryptKeyTweak(_cryptAddress + _progress + done, tweaked_key); //update tweaked crypt key
_cryptKeyTweak(_cryptAddress + _progress + done, tweaked_key);
if (mbedtls_aes_setkey_enc(&ctx, tweaked_key, 256)) {
return false;
}
@ -504,49 +572,40 @@ bool UpdateClass::_decryptBuffer() {
return false;
}
}
if (mbedtls_aes_crypt_ecb(&ctx, MBEDTLS_AES_ENCRYPT, _cryptBuffer, _cryptBuffer)) { //use MBEDTLS_AES_ENCRYPT to decrypt flash code
if (mbedtls_aes_crypt_ecb(&ctx, MBEDTLS_AES_ENCRYPT, _cryptBuffer, _cryptBuffer)) {
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
_buffer[i + done] = _cryptBuffer[(ENCRYPTED_BLOCK_SIZE - 1) - i];
}
done += ENCRYPTED_BLOCK_SIZE;
}
#endif
return true;
}
bool UpdateClass::_writeBuffer() {
//first bytes of loading image, check to see if loading image needs decrypting
bool ESPUpdateClass::_writeBuffer() {
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");
_cryptMode |= U_AES_IMAGE_DECRYPTING_BIT;
}
}
//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);
@ -554,14 +613,16 @@ bool UpdateClass::_writeBuffer() {
if (!_progress && _progress_callback) {
_progress_callback(0, _size);
}
// =============================================================
// CHIPSET SPECIFIC PHYSICAL WRITE
// =============================================================
#if defined(ESP32)
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
bool block_erase = (_size - _progress >= SPI_FLASH_BLOCK_SIZE) && (offset % SPI_FLASH_BLOCK_SIZE == 0);
bool part_head_sectors = _partition->address % SPI_FLASH_BLOCK_SIZE && offset < (_partition->address / SPI_FLASH_BLOCK_SIZE + 1) * SPI_FLASH_BLOCK_SIZE;
bool part_tail_sectors = offset >= (_partition->address + _size) / SPI_FLASH_BLOCK_SIZE * SPI_FLASH_BLOCK_SIZE;
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);
@ -569,14 +630,21 @@ bool UpdateClass::_writeBuffer() {
}
}
// 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;
}
#elif defined(ESP8266)
// Using ESP8266 built-in Updater logic for writing
// This maintains the skip-logic while piping data to the core updater
if (ESPUpdate.write(_buffer + skip, _bufferLen - skip) != (_bufferLen - skip)) {
_abort(UPDATE_ERROR_WRITE);
return false;
}
#endif
// =============================================================
//restore magic or md5 will fail
if (!_progress && _command == U_FLASH) {
_buffer[0] = ESP_IMAGE_HEADER_MAGIC;
}
@ -589,7 +657,7 @@ bool UpdateClass::_writeBuffer() {
return true;
}
bool UpdateClass::_verifyHeader(uint8_t data) {
bool ESPUpdateClass::_verifyHeader(uint8_t data) {
if (_command == U_FLASH) {
if (data != ESP_IMAGE_HEADER_MAGIC) {
_abort(UPDATE_ERROR_MAGIC_BYTE);
@ -602,13 +670,13 @@ bool UpdateClass::_verifyHeader(uint8_t data) {
return false;
}
bool UpdateClass::_verifyEnd() {
bool ESPUpdateClass::_verifyEnd() {
#if defined(ESP32)
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;
@ -619,10 +687,17 @@ bool UpdateClass::_verifyEnd() {
_reset();
return true;
}
#elif defined(ESP8266)
if (!EPUpdate.end()) {
return false;
}
_reset();
return true;
#endif
return false;
}
bool UpdateClass::setMD5(const char *expected_md5) {
bool ESPUpdateClass::setMD5(const char *expected_md5) {
if (strlen(expected_md5) != 32) {
return false;
}
@ -631,24 +706,20 @@ bool UpdateClass::setMD5(const char *expected_md5) {
return true;
}
bool UpdateClass::end(bool evenIfRemaining) {
bool ESPUpdateClass::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()) {
@ -656,22 +727,18 @@ bool UpdateClass::end(bool evenIfRemaining) {
return false;
}
}
return _verifyEnd();
}
size_t UpdateClass::write(uint8_t *data, size_t len) {
size_t ESPUpdateClass::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);
@ -691,7 +758,7 @@ size_t UpdateClass::write(uint8_t *data, size_t len) {
return len;
}
size_t UpdateClass::writeStream(Stream &data) {
size_t ESPUpdateClass::writeStream(Stream &data) {
size_t written = 0;
size_t toRead = 0;
int timeout_failures = 0;
@ -713,17 +780,13 @@ size_t UpdateClass::writeStream(Stream &data) {
while (remaining()) {
if (_ledPin != -1) {
digitalWrite(_ledPin, _ledOn); // Switch LED on
digitalWrite(_ledPin, _ledOn);
}
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) {
@ -739,7 +802,7 @@ size_t UpdateClass::writeStream(Stream &data) {
}
if (_ledPin != -1) {
digitalWrite(_ledPin, !_ledOn); // Switch LED off
digitalWrite(_ledPin, !_ledOn);
}
_bufferLen += toRead;
if ((_bufferLen == remaining() || _bufferLen == SPI_FLASH_SEC_SIZE) && !_writeBuffer()) {
@ -748,40 +811,34 @@ size_t UpdateClass::writeStream(Stream &data) {
written += toRead;
#if CONFIG_FREERTOS_UNICORE
delay(1); // Fix solo WDT
delay(1);
#endif
}
return written;
}
void UpdateClass::printError(Print &out) {
void ESPUpdateClass::printError(Print &out) {
out.println(_err2str(_error));
}
const char *UpdateClass::errorString() {
const char *ESPUpdateClass::errorString() {
return _err2str(_error);
}
bool UpdateClass::_chkDataInBlock(const uint8_t *data, size_t len) const {
// check 32-bit aligned blocks only
bool ESPUpdateClass::_chkDataInBlock(const uint8_t *data, size_t len) const {
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
if (*(uint32_t *)data ^ 0xffffffff) {
return true;
}
data += sizeof(uint32_t);
} while (--dwl);
return false;
}
#if !defined(NO_GLOBAL_INSTANCES) && !defined(NO_GLOBAL_UPDATE)
UpdateClass Update;
ESPUpdateClass ESPUpdate;
#endif
#endif // defined(ESP32)

216
src/ESPUpdate.h Normal file
View File

@ -0,0 +1,216 @@
/*
* [HCUpdate.h] - UNIFIED FIRMWARE UPDATE CLASS
* --------------------------------------------------------------------------------------------
* PURPOSE:
* - Provides a single interface for OTA updates across ESP32 and ESP8266 chipsets.
* - ESP32: Uses WiFiClientSecure for encrypted transport (HTTPS/443).
* - ESP8266: Uses WiFiClient for lightweight plain transport (HTTP/80) to save RAM.
* - Features: AES-256 decryption, "Safe Flash" 16-byte stashing, and MD5 verification.
* --------------------------------------------------------------------------------------------
* Revision History:
* 2024.xx.xx - Espressif Systems original structure.
* 2026.04.08 - [RnD12] Unified for ESP32/ESP8266 with conditional signatures.
* Added bRebootAfterInstall and sign-negated error reporting support.
*/
#ifndef ESP_UPDATE_H
#define ESP_UPDATE_H
#include <Arduino.h>
#include <MD5Builder.h>
#include <functional>
#include <HTTPClient.h>
// ======================================================================
// CHIPSET SPECIFIC INCLUDES
// ======================================================================
#if defined(ESP32)
#include <WiFiClientSecure.h>
#include "esp_partition.h"
#elif defined(ESP8266)
#include <WiFiClient.h>
// ESP8266 uses internal flash layout without esp_partition.h
#endif
// ======================================================================
#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 ESPUpdateClass {
public:
typedef std::function<void(size_t, size_t)> THandlerFunction_Progress;
ESPUpdateClass();
ESPUpdateClass(int httpClientTimeout);
// ======================================================================
// CONDITIONAL UPDATE SIGNATURES
// ======================================================================
#if defined(ESP32)
/**
* ESP32 Secure ESPUpdate: Requires WiFiClientSecure for TLS/HTTPS
*/
int update(WiFiClientSecure& client, const char *host, uint16_t port, const char *uri,
const char *currentVersion, const char *projectTag, bool bForceUpdate, bool bRebootAfterInstall = true);
#elif defined(ESP8266)
/**
* ESP8266 Plain ESPUpdate: Uses WiFiClient to preserve heap RAM
*/
int update(WiFiClient& client, const char *host, uint16_t port, const char *uri,
const char *currentVersion, const char *projectTag, bool bForceUpdate, bool bRebootAfterInstall = true);
#endif
// ======================================================================
int handleUpdate(HTTPClient& http, const char *currentVersion, const char *projectTag);
int http_downloadUpdate(HTTPClient &httpClient);
ESPUpdateClass &onProgress(THandlerFunction_Progress fn);
bool begin(size_t size = UPDATE_SIZE_UNKNOWN, int command = U_FLASH, int ledPin = -1, uint8_t ledOn = LOW, const char *label = NULL);
bool setupCrypt(const uint8_t *cryptKey = 0, size_t cryptAddress = 0, uint8_t cryptConfig = 0xf, int cryptMode = U_AES_DECRYPT_AUTO);
size_t write(uint8_t *data, size_t len);
size_t writeStream(Stream &data);
bool end(bool evenIfRemaining = false);
bool setCryptKey(const uint8_t *cryptKey);
bool setCryptMode(const int cryptMode);
void setCryptAddress(const size_t cryptAddress) {
_cryptAddress = cryptAddress & 0x00fffff0;
}
void setCryptConfig(const uint8_t cryptConfig) {
_cryptCfg = cryptConfig & 0x0f;
}
void abort();
void printError(Print &out);
const char *errorString();
bool setMD5(const char *expected_md5);
String md5String(void) {
return _md5.toString();
}
void md5(uint8_t *result) {
return _md5.getBytes(result);
}
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<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;
}
bool canRollBack();
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();
// ======================================================================
// CHIPSET SPECIFIC PRIVATE MEMBERS
// ======================================================================
#if defined(ESP32)
bool _enablePartition(const esp_partition_t *partition);
const esp_partition_t *_partition;
#endif
// ======================================================================
bool _chkDataInBlock(const uint8_t *data, size_t len) const;
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;
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 ESPUpdateClass ESPUpdate;
#endif
#endif // ESP_UPDATE_H

207
src/firmware.php Normal file
View File

@ -0,0 +1,207 @@
<?php
/**
* [firmware.php] - Structured Firmware Management for ESP8266 and ESP32
* 역할:
* 1. ESP 바이너리 파일 업로드 관리 (파일명 구조화)
* 2. 업로드 입력된 파일명(Project) 추출하여 메타데이터와 결합
* 3. 파일명 규칙: [Project].[Chipset].[VersionDate].[VersionSeq].bin
* 4. 화면 표시: Project, Chipset, Version (Date-Seq) 분리 출력
*/
// =========================================================
$pageTitle = "Firmware Upload";
require_once __DIR__ . "/../includes/header.php";
// =========================================================
// ---------------------------------------------------------
// 0. 경로 설정
// ---------------------------------------------------------
$currentTime = date("Y-m-d H:i:s");
$firmwareBaseDir = realpath(__DIR__ . '/../firmware') . DIRECTORY_SEPARATOR;
if (!is_dir($firmwareBaseDir)) {
mkdir($firmwareBaseDir, 0775, true);
}
// ---------------------------------------------------------
// 1. POST 요청 처리
// ---------------------------------------------------------
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// [A] 파일 삭제 로직
if (isset($_POST['delete-file']) && $isLoggedIn && $userRole === 'admin') {
$filePath = $_POST['delete-file'];
if (file_exists($filePath) && strpos(realpath($filePath), $firmwareBaseDir) === 0) {
if (unlink($filePath)) {
$_SESSION['success_message'] = "[$currentTime] Firmware removed successfully.";
}
}
header("Location: firmware.php"); exit;
}
// [B] 파일 업로드
if (isset($_POST['upload-firmware']) && isset($_FILES['bin-file'])) {
$rawName = $_FILES['bin-file']['name'];
$nameParts = explode('.', $rawName);
$project = preg_replace("/[^a-zA-Z0-9]/", "-", $nameParts[0]);
$chipset = $_POST['chipset'];
$vDate = preg_replace("/[^0-9]/", "", $_POST['version_date']);
$vSeq = sprintf("%03d", (int)$_POST['version_seq']);
$fileExt = strtolower(pathinfo($rawName, PATHINFO_EXTENSION));
$newFileName = "{$project}.{$chipset}.{$vDate}.{$vSeq}.{$fileExt}";
$destPath = $firmwareBaseDir . $newFileName;
if ($fileExt === 'bin') {
if (move_uploaded_file($_FILES['bin-file']['tmp_name'], $destPath)) {
$_SESSION['success_message'] = "[$currentTime] Project '$project' uploaded as: $newFileName";
} else {
$_SESSION['error_message'] = "[$currentTime] Upload failed.";
}
} else {
$_SESSION['error_message'] = "[$currentTime] Only .bin files allowed.";
}
header("Location: firmware.php"); exit;
}
}
// ---------------------------------------------------------
// 2. 데이터 준비 및 파싱 (표시용)
// ---------------------------------------------------------
$itemsPerPage = 10;
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
if ($page < 1) $page = 1;
$offset = ($page - 1) * $itemsPerPage;
$allFiles = [];
if (is_dir($firmwareBaseDir)) {
$files = array_diff(scandir($firmwareBaseDir), array('.', '..'));
foreach ($files as $file) {
$path = $firmwareBaseDir . $file;
if (is_file($path)) {
$parts = explode('.', pathinfo($file, PATHINFO_FILENAME));
$pName = $parts[0] ?? "Unknown";
$pChip = strtoupper($parts[1] ?? "-");
$pDate = $parts[2] ?? "00000000";
$pSeq = $parts[3] ?? "000";
$allFiles[] = [
'full_name' => $file,
'project' => $pName,
'chipset' => $pChip,
'version' => "{$pDate}-{$pSeq}",
'path' => $path,
'size' => filesize($path),
'date' => date("Y-m-d H:i", filemtime($path))
];
}
}
}
usort($allFiles, function($a, $b) { return filemtime($b['path']) - filemtime($a['path']); });
$totalItems = count($allFiles);
$totalPages = ceil($totalItems / $itemsPerPage);
$displayFiles = array_slice($allFiles, $offset, $itemsPerPage);
// ---------------------------------------------------------
// 3. 화면 출력
// ---------------------------------------------------------
echo '<div class="container" style="max-width: 1000px; margin: 0 auto; padding: 20px; font-family: sans-serif;">';
echo "<h2>📂 Firmware Repository</h2>";
echo "<table style='width:100%; border-collapse: collapse; margin-bottom: 20px;' border='1'>";
echo "<thead style='background: #f4f4f4;'>
<tr>
<th style='padding:10px;'>Project</th>
<th style='padding:10px;'>Chipset</th>
<th style='padding:10px;'>Version (Date-Seq)</th>
<th style='padding:10px;'>Size</th>
<th style='padding:10px;'>Upload Date</th>
<th style='padding:10px;'>Action</th>
</tr>
</thead><tbody>";
foreach ($displayFiles as $f) {
echo "<tr>
<td style='padding:8px;'><strong>".htmlspecialchars($f['project'])."</strong></td>
<td style='padding:8px; text-align:center;'>".htmlspecialchars($f['chipset'])."</td>
<td style='padding:8px; text-align:center;'><code>".htmlspecialchars($f['version'])."</code></td>
<td style='padding:8px; text-align:right;'>".round($f['size']/1024, 1)." KB</td>
<td style='padding:8px; text-align:center; font-size:0.9em; color:#666;'>{$f['date']}</td>
<td style='padding:8px; text-align:center;'>
<form method='POST' onsubmit='return confirm(\"Delete this firmware?\");' style='display:inline;'>
<input type='hidden' name='delete-file' value='".htmlspecialchars($f['path'])."'>
<button type='submit' style='color:#e74c3c; cursor:pointer; background:none; border:1px solid #e74c3c; border-radius:3px;'>Del</button>
</form>
</td>
</tr>";
}
if (empty($displayFiles)) echo "<tr><td colspan='6' style='text-align:center; padding:30px; color:gray;'>No firmware files found.</td></tr>";
echo "</tbody></table>";
if ($totalPages > 1) {
echo "<div style='text-align:center; margin-bottom: 40px;'>";
for ($i = 1; $i <= $totalPages; $i++) {
$active = ($i == $page) ? "background:#3498db; color:white; border-color:#3498db;" : "color:#3498db;";
echo "<a href='?page=$i' style='display:inline-block; padding:5px 12px; margin:0 2px; text-decoration:none; border:1px solid #ddd; border-radius:4px; $active'>$i</a>";
}
echo "</div>";
}
echo "<hr style='margin: 50px 0; border: 0; border-top: 1px dashed #bbb;'>";
if ($isLoggedIn && $userRole === 'admin') {
?>
<div style="background: #fdfdfd; padding: 25px; border-radius: 8px; border: 1px solid #eee; box-shadow: 0 2px 5px rgba(0,0,0,0.05);">
<h3 style="margin-top:0;">Firmware Upload</h3>
<form action="firmware.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="upload-firmware" value="1">
<!-- Chipset Row -->
<div style="display:flex; align-items:center; margin-bottom:20px;">
<label style="font-weight:bold; width:120px;">Chipset Target:</label>
<div style="display:flex; align-items:center;">
<input type="radio" name="chipset" value="ESP32" checked id="esp32">
<label for="esp32" style="margin-left:5px;">ESP32</label>
<input type="radio" name="chipset" value="ESP8266" style="margin-left:25px;" id="esp8266">
<label for="esp8266" style="margin-left:5px;">ESP8266</label>
</div>
</div>
<!-- Version Row -->
<div style="display:flex; align-items:center; margin-bottom:20px;">
<label style="font-weight:bold; width:120px;">Version:</label>
<div style="display:flex; align-items:center; flex:1; max-width: 400px;">
<div style="flex:2; margin-right: 10px;">
<input type="text" name="version_date" value="<?php echo date('Ymd'); ?>" required
style="width:100%; padding:8px; border:1px solid #ccc; border-radius:4px;" placeholder="YYYYMMDD">
</div>
<span style="color: #666; font-weight: bold; margin-left: 15px;"> - </span>
<div style="flex:1; margin-left: 10px;">
<input type="text" name="version_seq" value="001" required
style="width:100%; padding:8px; border:1px solid #ccc; border-radius:4px;" placeholder="Seq">
</div>
</div>
</div>
<!-- File Row -->
<div style="margin-bottom:25px;">
<label style="font-weight:bold; display:block; margin-bottom:8px;">Select Binary File (.bin):</label>
<input type="file" name="bin-file" accept=".bin" required style="padding:5px; border:1px solid #ddd; width:100%; background:#fff;">
</div>
<button type="submit" style="padding: 12px 30px; background: #27ae60; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight:bold; width:100%;">
Confirm & Upload
</button>
</form>
</div>
<?php
}
echo '</div>';
require_once __DIR__ . "/../includes/footer.php";
?>