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 { void onResult(const NimBLEAdvertisedDevice* advertisedDevice) override {
uint16_t len; uint16_t len;
bthome_t *pTuyaData = (bthome_t *) advertisedDevice->getServiceData(&len); bthome_t *pTuyaData = (bthome_t *) advertisedDevice->getServiceData((uint16_t *) &len);
// Tuya // Tuya
if (len == sizeof(bthome_t) && pTuyaData->info == 0x40) { if (len == sizeof(bthome_t) && pTuyaData->info == 0x40) {
if (config.nTemp1SensorType == TEMP_SENSOR_TYPE::BLE_TUYA) { if (config.nTemp1SensorType == TEMP_SENSOR_TYPE::BLE_TUYA) {

View File

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

View File

@ -81,7 +81,7 @@ void CONFIG_TYPE::init() {
for (int i = 0; i < 17; i = i + 8) { for (int i = 0; i < 17; i = i + 8) {
m_nChipId |= ((ESP.getEfuseMac() >> (40 - i)) & 0xff) << i; m_nChipId |= ((ESP.getEfuseMac() >> (40 - i)) & 0xff) << i;
} }
m_nPublicPort = (uint16_t)(m_nChipId & 0xFFFF); m_nPublicPort = 3939;
m_nDeviceType = THIS_DEVICE_TYPE; 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 <HTTPClient.h>
#include <esp_task_wdt.h> #include <esp_task_wdt.h>
#include "HCUpdate.h" #include "src/ESPUpdate.h"
#include "HermitCrab.h" #include "HermitCrab.h"
#include "Config.h" #include "Config.h"
#include "ConnectWiFi.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); 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) 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; WiFiClientSecure client;
ESPUpdateClass ESPUpdate;
client.setCACert(rootCACertificate); 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; 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(); setupSensor();
ui.setup(); ui.setup();
ui.message(0, "WiFi..."); ui.message(0, (char *) "WiFi...");
setupWiFi(); setupWiFi();
if (aht25.sensor() || aht10_0x39.sensor()) { if (aht25.sensor() || aht10_0x39.sensor()) {
ui.message(4, "Sensor... OK!"); ui.message(4, (char *) "Sensor... OK!");
} else { } else {
ui.message(4, "Sensor... None!"); ui.message(4, (char *) "Sensor... None!");
} }
ui.message(5, "ZCD..."); ui.message(5, (char *) "ZCD...");
setupZCD(); 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); //if (!isWiFiConnected) delay(3000);
ble.setupConnect(config.nBLESensorAddr, config.nBLESensorAddr2); ble.setupConnect(config.nBLESensorAddr, config.nBLESensorAddr2);
@ -111,8 +111,6 @@ void setupConfig() {
config.load(); config.load();
history.loadPID(); history.loadPID();
config.m_nDeviceType = THIS_DEVICE_TYPE; config.m_nDeviceType = THIS_DEVICE_TYPE;
if (config.m_nPublicPort == 3939)
config.m_nPublicPort = (uint16_t)(config.m_nChipId & 0xFFFF);
} }
void setupWiFi() { void setupWiFi() {
@ -146,7 +144,7 @@ void setupWiFi() {
if (WiFi.status() == WL_CONNECTED) { if (WiFi.status() == WL_CONNECTED) {
ledcWrite(PIN_LED_WIFI, PWM_FULL); // LED_OFF ledcWrite(PIN_LED_WIFI, PWM_FULL); // LED_OFF
ui.message(0, "WiFi...OK!"); ui.message(0, (char *) "WiFi...OK!");
DPRINTLN("WiFi - Connected at SETUP"); DPRINTLN("WiFi - Connected at SETUP");
DPRINTF("WiFi - SSID(%s) PW(%s) IP(%s)\n", config.ssid, config.pw, WiFi.localIP().toString().c_str()); DPRINTF("WiFi - SSID(%s) PW(%s) IP(%s)\n", config.ssid, config.pw, WiFi.localIP().toString().c_str());
g_bWiFiHasBeenConnected = true; g_bWiFiHasBeenConnected = true;
@ -162,35 +160,35 @@ void setupWiFi() {
void setupPostWiFi(bool bBoot = false) { void setupPostWiFi(bool bBoot = false) {
if (WiFi.status() == WL_CONNECTED) { if (WiFi.status() == WL_CONNECTED) {
// Time // Time
if (bBoot) ui.message(1, "Time..."); if (bBoot) ui.message(1, (char *) "Time...");
timeManager.begin(); timeManager.begin();
vTaskDelay((bBoot ? 500 : 250)/portTICK_PERIOD_MS); vTaskDelay((bBoot ? 500 : 250)/portTICK_PERIOD_MS);
timeManager.checkNTPResponse(); timeManager.checkNTPResponse();
if (bBoot) { if (bBoot) {
if (timeManager.hasNTPUpdate()) { if (timeManager.hasNTPUpdate()) {
ui.message(1, "Time...OK!"); ui.message(1, (char *) "Time...OK!");
// OTA // OTA
DPRINTLN("Setup - TimeManager.begin()"); DPRINTLN("Setup - TimeManager.begin()");
DPRINTLN("\n===============================\n"); DPRINTLN("\n===============================\n");
DPRINTLN(" Trying OTA"); DPRINTLN(" Trying OTA");
ui.message(2, "Update check..."); ui.message(2, (char *) "Update check...");
checkOTA(true); checkOTA(true);
ui.message(2, "Update check...OK!"); ui.message(2, (char *) "Update check...OK!");
DPRINTLN(" OTA Process completed!"); DPRINTLN(" OTA Process completed!");
DPRINTLN("===============================\n"); DPRINTLN("===============================\n");
} else { } else {
ui.message(2, "Update check SKIPPED!"); ui.message(2, (char *) "Update check SKIPPED!");
timeManager.sendNTPRequest(); timeManager.sendNTPRequest();
} }
} }
// Host // Host
if (bBoot) ui.message(3, "Server..."); if (bBoot) ui.message(3, (char *) "Server...");
host.Setup(); host.Setup();
DPRINTLN("Setup - host.Setup()"); DPRINTLN("Setup - host.Setup()");
if (bBoot) ui.message(3, "Server...OK!"); if (bBoot) ui.message(3, (char *) "Server...OK!");
g_bWiFiSetupExecuted = true; 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 "zcd.h"
#include "AHT2x.h" #include "AHT2x.h"
#include "WiFiHost.h" #include "WiFiHost.h"
#include "UPnPClient.h" //#include "UPnPClient.h"
@ -64,15 +64,10 @@ void CWiFiHost::Setup()
.sig2 = SIGNATURE2 }; .sig2 = SIGNATURE2 };
clientPacket = hostPacket; clientPacket = hostPacket;
//if (m_nPublicPort == 0) //m_nPublicPort = config.m_nPublicPort;
{
if (config.m_nPublicPort == 0)
config.m_nPublicPort = (uint16_t)(config.m_nChipId & 0xFFFF);
m_nPublicPort = config.m_nPublicPort;
}
wifiServer = WiFiServer(SERVER_PORT, 1); wifiServer = WiFiServer(SERVER_PORT, 1);
wifiExternal = WiFiServer(m_nPublicPort, 1); //wifiExternal = WiFiServer(m_nPublicPort, 1);
//wifiStatus = WIFI_NOT_CONNECTED; //wifiStatus = WIFI_NOT_CONNECTED;
m_nDataSend_sent = 0; m_nDataSend_sent = 0;
@ -96,36 +91,36 @@ void CWiFiHost::Setup()
m_nLastHeartBeatSentTime = m_nLastHeartBeatSentTime =
m_nLastUDPBroadcastTime = millis(); m_nLastUDPBroadcastTime = millis();
m_bClientConnected = false; m_bClientConnected = false;
m_dwPublicIP = 0; //m_dwPublicIP = 0;
wifiClient.stop(); wifiClient.stop();
if (isWiFiConnected()) if (isWiFiConnected())
{ {
// UPnP Client // UPnP Client
{ // {
uint32_t ip = WiFi.gatewayIP(); // uint32_t ip = WiFi.gatewayIP();
uint16_t port = m_nPublicPort; // uint16_t port = m_nPublicPort;
CUpnpClient upnp; // //CUpnpClient upnp;
if (upnp.registerUPnP(&ip, &port)) { // //if (upnp.registerUPnP(&ip, &port)) {
status.nFlags |= FLAG_UPNP; // // status.nFlags |= FLAG_UPNP;
} else { // //} else {
status.nFlags &= ~FLAG_UPNP; // status.nFlags &= ~FLAG_UPNP;
} // //}
if (ip != 0) // if (ip != 0)
m_dwPublicIP = ip; // m_dwPublicIP = ip;
if (port != m_nPublicPort) { // if (port != m_nPublicPort) {
config.m_nPublicPort = port; // config.m_nPublicPort = port;
config.save(); // config.save();
m_nPublicPort = port; // m_nPublicPort = port;
} // }
} // }
// Server // Server
wifiServer.begin(SERVER_PORT, 1); wifiServer.begin(SERVER_PORT, 1);
wifiExternal.begin(m_nPublicPort, 1); //wifiExternal.begin(m_nPublicPort, 1);
m_nLastReceivedTime = millis(); m_nLastReceivedTime = millis();
m_bClientConnected = false; m_bClientConnected = false;
@ -150,7 +145,7 @@ MY_IRAM_ATTR void CWiFiHost::Stop() {
// Stop server // Stop server
wifiServer.stop(); wifiServer.stop();
wifiExternal.stop(); //wifiExternal.stop();
m_bClientConnected = false; m_bClientConnected = false;
// Stop Client // Stop Client
@ -171,87 +166,86 @@ MY_IRAM_ATTR void CWiFiHost::CloseConnection()
IRAM_ATTR void CWiFiHost::Loop(unsigned long clock) IRAM_ATTR void CWiFiHost::Loop(unsigned long clock)
{ {
static unsigned long lastReceivedTime = 0;
if (!isWiFiConnected()) return; 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) { 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: case MODE_PACKET:
// Client Connected CheckClient(clock);
if (m_bClientConnected && wifiClient.connected())
{
CheckClient(clock);
if (m_bClientConnected) { // Timeout Check (60 seconds)
if (clock - m_nLastReceivedTime > 60000) if (clock - m_nLastReceivedTime > 60000) {
{ ESP_LOGW(TAG_WIFI_HOST, "Host: Timeout - Dropping Connection");
ESP_LOGI(TAG_WIFI_HOST,"Host: dropping connection for no HB in 60 seconds"); wifiClient.stop();
wifiClient.stop(); m_bClientConnected = false;
m_bClientConnected = false;
}
// Send HeartBeat
if (clock - m_nLastHeartBeatSentTime >= 1000) {
SendHeartBeat();
m_nLastHeartBeatSentTime = clock;
}
}
}
if (!m_bClientConnected || !wifiClient.connected()) {
m_nMode = MODE_WAITING; 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; break;
case MODE_SEND: case MODE_SEND:
// SendData returns true only when the CURRENT buffer is empty
if (SendData(clock)) { if (SendData(clock)) {
if (m_bSendHistoryPending) { // Check if there is a second part of the history ring buffer
// Mark pending to send the second part: from the start to head-1 if (m_bSendHistoryPending && m_nPendingHistoryCount > 0) {
//SendData(history.getRingData2() /* &ring[0] */, sizeof(STATUS_TYPE) * head); ESP_LOGI(TAG_WIFI_HOST, "WiFi Host: Preparing History Part 2");
//m_bSendHistoryPending = true;
//m_nPendingHistoryCount = history.getHead(); // Setup the next chunk
SendData(history.getRingData2(), sizeof(STATUS_TYPE) * m_nPendingHistoryCount);
// Clear flags so we don't loop here forever
m_bSendHistoryPending = false; m_bSendHistoryPending = false;
if (m_nPendingHistoryCount > 0) { 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); // We stay in MODE_SEND. The next Loop() turn will start sending Part 2.
m_nPendingHistoryCount = 0;
}
} else { } else {
// All data (including history parts) is done
m_nMode = MODE_PACKET; m_nMode = MODE_PACKET;
} }
} }
break; break;
case MODE_RECV: case MODE_RECV:
if (ReceiveData(clock)) m_nMode = MODE_PACKET; if (ReceiveData(clock)) {
m_nMode = MODE_PACKET;
}
break; break;
} }
} }
@ -271,8 +265,8 @@ MY_IRAM_ATTR void CWiFiHost::SendHeartBeat(unsigned long clock) {
// External Heartbeat // External Heartbeat
UDP_CONFIG_TYPE pktConfig; UDP_CONFIG_TYPE pktConfig;
pktConfig.udp = packetUDP; pktConfig.udp = packetUDP;
pktConfig.udp.m_nPort = m_nPublicPort; pktConfig.udp.m_nPort = 3939;
pktConfig.udp.dwIPAddress = m_dwPublicIP; pktConfig.udp.dwIPAddress = (uint32_t)WiFi.localIP();
pktConfig.udp.status = status; pktConfig.udp.status = status;
if (((uint32_t)m_cExternalServerIPAddress) != (uint32_t)0l) 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) IRAM_ATTR void CWiFiHost::CheckClient(unsigned long clock)
{ {
bool bLED = false; // 1. Quick exit if no data is ready to be read
static TCP_PACKET cpkt;
int available = wifiClient.available(); int available = wifiClient.available();
if (available >= sizeof(TCP_PACKET)) { if (available < sizeof(TCP_PACKET)) return;
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 static TCP_PACKET cpkt;
ESP_LOGI(TAG_WIFI_HOST,"Invalid Packet %s%s size: %d/%d\n",
cpkt.sig1 == SIGNATURE1 ? "SIG1 OK" : "", // 2. Peek or Read the packet
cpkt.sig2 == SIGNATURE2 ? "SIG2 OK" : "", // We use readBytes because we already verified 'available' >= size
cpkt.len, sizeof(TCP_PACKET)); int count = wifiClient.readBytes((uint8_t*)&cpkt, sizeof(TCP_PACKET));
// Shift the buffer data off by 1 byte for the next cycle
//buffIndexC = sizeof(TCP_PACKET) - 1; if (count == sizeof(TCP_PACKET)) {
//memcpy(pC, &pC[1], buffIndexC); // 3. Validation Logic
while (m_bClientConnected && bool sigMatch = (cpkt.sig1 == SIGNATURE1 && cpkt.sig2 == SIGNATURE2);
wifiClient.connected() && bool lenMatch = (cpkt.len == sizeof(TCP_PACKET));
wifiClient.readBytes((uint8_t *) &cpkt, sizeof(cpkt)) > 0) {
}; 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; 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) if (!m_pDataSend_data || m_nDataSend_size == 0) return true;
{
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) { // Calculate how much is left
int16_t sentCount = wifiClient.write(&m_pDataSend_data[m_nDataSend_sent], count); size_t remaining = m_nDataSend_size - m_nDataSend_sent;
if (sentCount != count) {
ESP_LOGI(TAG_WIFI_HOST,"WiFiHost - SendData() sent(%d) is not count(%d)\n", sentCount, count); // Send a "Chunk" (e.g., 512 bytes).
if (sentCount <= 0) // Sending too much at once blocks the CPU.
yield(); // Sending too little makes the WiFi overhead too high.
} size_t chunkSize = (remaining > 512) ? 512 : remaining;
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); size_t written = wifiClient.write((const uint8_t*)(m_pDataSend_data + m_nDataSend_sent), chunkSize);
}
//else { if (written > 0) {
// ESP_LOGI(TAG_WIFI_HOST,"WiFiHost - SendData() avail is 0"); m_nDataSend_sent += written;
// 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) { // If we aren't done, return false to stay in MODE_SEND for the next Loop()
ESP_LOGI(TAG_WIFI_HOST, " SendDdata: DataSend completed!"); if (m_nDataSend_sent < m_nDataSend_size) {
return true; 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) IRAM_ATTR size_t CWiFiHost::ReceiveData(uint8_t* data, size_t size)

View File

@ -219,7 +219,7 @@ public:
void CloseConnection(); void CloseConnection();
void SendHeartBeat(unsigned long clock); void SendHeartBeat(unsigned long clock);
void MonitorUDP(); 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: private:
//void setupUDP(); //void setupUDP();
@ -269,11 +269,11 @@ private:
unsigned long m_nLastHeartBeatSentTime; unsigned long m_nLastHeartBeatSentTime;
unsigned long m_nLastUDPBroadcastTime; unsigned long m_nLastUDPBroadcastTime;
volatile bool m_bClientConnected; volatile bool m_bClientConnected;
uint32_t m_dwPublicIP; //uint32_t m_dwPublicIP;
uint16_t m_nPublicPort; //uint16_t m_nPublicPort;
IPAddress m_cExternalServerIPAddress; IPAddress m_cExternalServerIPAddress;
WiFiUDP udpLocal, udpExternal; WiFiUDP udpLocal, udpExternal;
WiFiServer wifiServer, wifiExternal; WiFiServer wifiServer; //, wifiExternal;
WiFiClient wifiClient; WiFiClient wifiClient;
UDP_PACKET packetUDP; UDP_PACKET packetUDP;
TCP_PACKET hostPacket; TCP_PACKET hostPacket;

View File

@ -1,22 +1,37 @@
#if defined(ESP32)
/* /*
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD * [HCUpdate.cpp] - UNIFIED FIRMWARE UPDATE IMPLEMENTATION
* * --------------------------------------------------------------------------------------------
* SPDX-License-Identifier: Apache-2.0 * 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 "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 <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) { static const char *_err2str(uint8_t _error) {
if (_error == UPDATE_ERROR_OK) { if (_error == UPDATE_ERROR_OK) {
@ -51,73 +66,158 @@ static const char *_err2str(uint8_t _error) {
return ("UNKNOWN"); 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; HTTPClient http;
if (!http.begin(client, host, port, uri)) { if (!http.begin(client, host, port, uri)) {
ESP_LOGI(TAG_FW,"OTA - httpClient begin failed\n"); 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(), int ret = handleUpdate(http, currentVersion, projectTag);
http.header("update") && http.header("update").toInt() == 1 ? "Download" : "Check Only" ); int size = 0;
//is there an image to download if (ret < 0) {
if (size >= 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) { if (!http.header("update") || http.header("update").toInt() == 0) {
ESP_LOGI(TAG_FW,"OTA - No Firmware available"); ESP_LOGI(TAG_FW,"OTA - No Firmware available for this project");
} else if (!http.header("version") || strcmp(http.header("version").c_str(), HC__VERSION) <= 0) { ret = 204;
ESP_LOGI(TAG_FW,"OTA - Firmware is upto Date"); }
} else { else if (!http.header("version") || strcmp(http.header("version").c_str(), currentVersion) <= 0) {
//image avaliabe to download & update ESP_LOGI(TAG_FW,"OTA - Firmware is already up to Date");
ret = 304;
}
else {
if (!bForceUpdate) { 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 { } else {
ESP_LOGI(TAG_FW,"OTA - Downloading & Installing V%s Firmware\n", http.header("version").c_str()); ESP_LOGI(TAG_FW,"OTA - Downloading & Installing V%s Firmware\n", http.header("version").c_str());
}
if (bForceUpdate) {
if (http_downloadUpdate(http) == 0) { if (http_downloadUpdate(http) == 0) {
http.end(); //end connection http.end();
ESP_LOGI(TAG_FW,"OTA - Firmware Update Successful, rebooting");
ESP.restart(); 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 http.end();
return 0; 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.useHTTP10(true);
http.setTimeout(_httpClientTimeout); http.setTimeout(_httpClientTimeout);
http.setFollowRedirects(_followRedirects); http.setFollowRedirects(_followRedirects);
// =============================================================
// CHIPSET SPECIFIC IDENTIFICATION
// =============================================================
#if defined(ESP32)
http.setUserAgent("ESP32-http-Update"); 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-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; http.addHeader("Cache-Control", "no-cache");
for (int i = 0; i < 17; i = i + 8) { http.addHeader("X-ESP-PROJECT", projectTag);
nChipId |= ((ESP.getEfuseMac() >> (40 - i)) & 0xff) << i; http.addHeader("X-ESP-VERSION", currentVersion);
}
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"};
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 *); size_t headerkeyssize = sizeof(headerkeys) / sizeof(char *);
http.collectHeaders(headerkeys, headerkeyssize); http.collectHeaders(headerkeys, headerkeyssize);
@ -125,67 +225,48 @@ int UpdateClass::handleUpdate(HTTPClient& http, const String& currentVersion, sh
int len = http.getSize(); int len = http.getSize();
if (code == HTTP_CODE_OK) { 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) { } else if (code < 0) {
ESP_LOGI(TAG_FW,"Error: %s\n", http.errorToString(code).c_str()); return code;
ESP_LOGI(TAG_FW, "%s", http.getString());
return code; //error code should be minus between -1 to -11
} else { } else {
ESP_LOGI(TAG_FW,"Error: HTTP Server response code %i\n", code); return -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) { int ESPUpdateClass::http_downloadUpdate(HTTPClient &httpClient) {
WiFiClient *stream = httpClient.getStreamPtr(); WiFiClient *stream = httpClient.getStreamPtr();
int written = 0; int written = 0;
// Check content length and begin update
int fileSize = httpClient.getSize(); int fileSize = httpClient.getSize();
if (fileSize <= 0) { if (fileSize <= 0) {
ESP_LOGI(TAG_FW,"Invalid content length");
return 1; return 1;
} }
if (!Update.begin(fileSize, U_FLASH)) { if (!ESPUpdate.begin(fileSize, U_FLASH)) {
ESP_LOGI(TAG_FW,"Update begin failed!");
return 1; return 1;
} }
// Download and write the update
while (httpClient.connected() && written < fileSize) { while (httpClient.connected() && written < fileSize) {
size_t availableBytes = stream->available(); size_t availableBytes = stream->available();
if (availableBytes) { 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 bytesRead = stream->readBytes(buffer, min(availableBytes, sizeof(buffer)));
int bytesWritten = Update.write(buffer, bytesRead); int bytesWritten = ESPUpdate.write(buffer, bytesRead);
if (bytesWritten != bytesRead) { if (bytesWritten != bytesRead) {
ESP_LOGI(TAG_FW,"Update write failed!"); ESPUpdate.end();
Update.end();
return 1; return 1;
} }
written += bytesWritten; written += bytesWritten;
} }
} }
// Finalize update if (ESPUpdate.end()) {
if (Update.end()) { return ESPUpdate.isFinished() ? 0 : 1;
if (Update.isFinished()) {
ESP_LOGI(TAG_FW,"Update successful!");
return 0;
} else {
ESP_LOGI(TAG_FW,"Update not finished!");
return 1;
}
} else { } else {
ESP_LOGI(TAG_FW,"Update end failed!");
return 1; return 1;
} }
} }
#if defined(ESP32)
static bool _partitionIsBootable(const esp_partition_t *partition) { static bool _partitionIsBootable(const esp_partition_t *partition) {
uint8_t buf[ENCRYPTED_BLOCK_SIZE]; uint8_t buf[ENCRYPTED_BLOCK_SIZE];
if (!partition) { 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)) { if (!ESP.partitionRead(partition, 0, (uint32_t *)buf, ENCRYPTED_BLOCK_SIZE)) {
return false; return false;
} }
if (buf[0] != ESP_IMAGE_HEADER_MAGIC) { if (buf[0] != ESP_IMAGE_HEADER_MAGIC) {
return false; return false;
} }
return true; return true;
} }
bool UpdateClass::_enablePartition(const esp_partition_t *partition) { bool ESPUpdateClass::_enablePartition(const esp_partition_t *partition) {
if (!partition) { if (!partition) {
return false; return false;
} }
return ESP.partitionWrite(partition, 0, (uint32_t *)_skipBuffer, ENCRYPTED_BLOCK_SIZE); return ESP.partitionWrite(partition, 0, (uint32_t *)_skipBuffer, ENCRYPTED_BLOCK_SIZE);
} }
#endif
UpdateClass::UpdateClass() ESPUpdateClass::ESPUpdateClass()
: _error(0) : _error(0)
, _cryptKey(0) , _cryptKey(0)
, _cryptBuffer(0) , _cryptBuffer(0)
@ -227,7 +308,7 @@ UpdateClass::UpdateClass()
, _httpClientTimeout(8000) , _httpClientTimeout(8000)
{} {}
UpdateClass::UpdateClass(int httpClientTimeout) ESPUpdateClass::ESPUpdateClass(int httpClientTimeout)
: _error(0) : _error(0)
, _cryptKey(0) , _cryptKey(0)
, _cryptBuffer(0) , _cryptBuffer(0)
@ -246,19 +327,18 @@ UpdateClass::UpdateClass(int httpClientTimeout)
, _httpClientTimeout(httpClientTimeout) , _httpClientTimeout(httpClientTimeout)
{} {}
UpdateClass &UpdateClass::onProgress(THandlerFunction_Progress fn) { ESPUpdateClass &ESPUpdateClass::onProgress(THandlerFunction_Progress fn) {
_progress_callback = fn; _progress_callback = fn;
return *this; return *this;
} }
void UpdateClass::_reset() { void ESPUpdateClass::_reset() {
if (_buffer) { if (_buffer) {
delete[] _buffer; delete[] _buffer;
} }
if (_skipBuffer) { if (_skipBuffer) {
delete[] _skipBuffer; delete[] _skipBuffer;
} }
_cryptBuffer = nullptr; _cryptBuffer = nullptr;
_buffer = nullptr; _buffer = nullptr;
_skipBuffer = nullptr; _skipBuffer = nullptr;
@ -268,34 +348,41 @@ void UpdateClass::_reset() {
_command = U_FLASH; _command = U_FLASH;
if (_ledPin != -1) { if (_ledPin != -1) {
digitalWrite(_ledPin, !_ledOn); // off digitalWrite(_ledPin, !_ledOn);
} }
} }
bool UpdateClass::canRollBack() { bool ESPUpdateClass::canRollBack() {
if (_buffer) { //Update is running #if defined(ESP32)
if (_buffer) {
return false; return false;
} }
const esp_partition_t *partition = esp_ota_get_next_update_partition(NULL); const esp_partition_t *partition = esp_ota_get_next_update_partition(NULL);
return _partitionIsBootable(partition); return _partitionIsBootable(partition);
#else
return false;
#endif
} }
bool UpdateClass::rollBack() { bool ESPUpdateClass::rollBack() {
if (_buffer) { //Update is running #if defined(ESP32)
if (_buffer) {
return false; return false;
} }
const esp_partition_t *partition = esp_ota_get_next_update_partition(NULL); const esp_partition_t *partition = esp_ota_get_next_update_partition(NULL);
return _partitionIsBootable(partition) && !esp_ota_set_boot_partition(partition); 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) { if (_size > 0) {
log_w("already running");
return false; return false;
} }
_ledPin = ledPin; _ledPin = ledPin;
_ledOn = !!ledOn; // 0(LOW) or 1(HIGH) _ledOn = !!ledOn;
_reset(); _reset();
_error = 0; _error = 0;
@ -307,19 +394,19 @@ bool UpdateClass::begin(size_t size, int command, int ledPin, uint8_t ledOn, con
return false; return false;
} }
#if defined(ESP32)
if (command == U_FLASH) { if (command == U_FLASH) {
_partition = esp_ota_get_next_update_partition(NULL); _partition = esp_ota_get_next_update_partition(NULL);
if (!_partition) { if (!_partition) {
_error = UPDATE_ERROR_NO_PARTITION; _error = UPDATE_ERROR_NO_PARTITION;
return false; return false;
} }
log_d("OTA Partition: %s", _partition->label);
} else if (command == U_SPIFFS) { } else if (command == U_SPIFFS) {
_partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_SPIFFS, label); _partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_SPIFFS, label);
_paroffset = 0; _paroffset = 0;
if (!_partition) { if (!_partition) {
_partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_FAT, NULL); _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) { if (!_partition) {
_error = UPDATE_ERROR_NO_PARTITION; _error = UPDATE_ERROR_NO_PARTITION;
return false; return false;
@ -327,7 +414,6 @@ bool UpdateClass::begin(size_t size, int command, int ledPin, uint8_t ledOn, con
} }
} else { } else {
_error = UPDATE_ERROR_BAD_ARGUMENT; _error = UPDATE_ERROR_BAD_ARGUMENT;
log_e("bad command %u", command);
return false; return false;
} }
@ -335,14 +421,12 @@ bool UpdateClass::begin(size_t size, int command, int ledPin, uint8_t ledOn, con
size = _partition->size; size = _partition->size;
} else if (size > _partition->size) { } else if (size > _partition->size) {
_error = UPDATE_ERROR_SIZE; _error = UPDATE_ERROR_SIZE;
log_e("too large %u > %u", size, _partition->size);
return false; return false;
} }
#endif
//initialize
_buffer = new (std::nothrow) uint8_t[SPI_FLASH_SEC_SIZE]; _buffer = new (std::nothrow) uint8_t[SPI_FLASH_SEC_SIZE];
if (!_buffer) { if (!_buffer) {
log_e("_buffer allocation failed");
return false; return false;
} }
_size = size; _size = size;
@ -351,7 +435,7 @@ bool UpdateClass::begin(size_t size, int command, int ledPin, uint8_t ledOn, con
return true; 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 (setCryptKey(cryptKey)) {
if (setCryptMode(cryptMode)) { if (setCryptMode(cryptMode)) {
setCryptAddress(cryptAddress); setCryptAddress(cryptAddress);
@ -362,50 +446,46 @@ bool UpdateClass::setupCrypt(const uint8_t *cryptKey, size_t cryptAddress, uint8
return false; return false;
} }
bool UpdateClass::setCryptKey(const uint8_t *cryptKey) { bool ESPUpdateClass::setCryptKey(const uint8_t *cryptKey) {
if (!cryptKey) { if (!cryptKey) {
if (_cryptKey) { if (_cryptKey) {
delete[] _cryptKey; delete[] _cryptKey;
_cryptKey = 0; _cryptKey = 0;
log_d("AES key unset");
} }
return false; //key cleared, no key to decrypt with return false;
} }
//initialize
if (!_cryptKey) { if (!_cryptKey) {
_cryptKey = new (std::nothrow) uint8_t[ENCRYPTED_KEY_SIZE]; _cryptKey = new (std::nothrow) uint8_t[ENCRYPTED_KEY_SIZE];
} }
if (!_cryptKey) { if (!_cryptKey) {
log_e("new failed");
return false; return false;
} }
memcpy(_cryptKey, cryptKey, ENCRYPTED_KEY_SIZE); memcpy(_cryptKey, cryptKey, ENCRYPTED_KEY_SIZE);
return true; 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) { if (cryptMode >= U_AES_DECRYPT_NONE && cryptMode <= U_AES_DECRYPT_ON) {
_cryptMode = cryptMode; _cryptMode = cryptMode;
} else { } else {
log_e("bad crypt mode argument %i", cryptMode);
return false; return false;
} }
return true; return true;
} }
void UpdateClass::_abort(uint8_t err) { void ESPUpdateClass::_abort(uint8_t err) {
_reset(); _reset();
_error = err; _error = err;
} }
void UpdateClass::abort() { void ESPUpdateClass::abort() {
_abort(UPDATE_ERROR_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); memcpy(tweaked_key, _cryptKey, ENCRYPTED_KEY_SIZE);
if (_cryptCfg == 0) { 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}; 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 key_idx = 0;
int bit_len = 0; int bit_len = 0;
uint32_t tweak = 0; uint32_t tweak = 0;
cryptAddress &= 0x00ffffe0; //bit 23-5 cryptAddress &= 0x00ffffe0;
cryptAddress <<= 8; //bit23 shifted to bit31(MSB) cryptAddress <<= 8;
while (pattern_idx < sizeof(pattern)) { while (pattern_idx < sizeof(pattern)) {
tweak = cryptAddress << (23 - pattern[pattern_idx]); //bit shift for small patterns tweak = cryptAddress << (23 - pattern[pattern_idx]);
// alternative to: tweak = rotl32(tweak,8 - bit_len); tweak = (tweak << (8 - bit_len)) | (tweak >> (24 + 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;
bit_len += pattern[pattern_idx++] - 4; //add number of bits in next pattern(23-4 = 19bits = 23bit to 5bit)
while (bit_len > 7) { while (bit_len > 7) {
tweaked_key[key_idx++] ^= tweak; //XOR byte tweaked_key[key_idx++] ^= tweak;
// alternative to: tweak = rotl32(tweak, 8); tweak = (tweak << 8) | (tweak >> 24);
tweak = (tweak << 8) | (tweak >> 24); //compiler should optimize to use rotate(fast)
bit_len -= 8; 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) { 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}; const uint8_t cfg_bits[] = {67, 65, 63, 61};
key_idx = 0; key_idx = 0;
pattern_idx = 0; pattern_idx = 0;
while (key_idx < ENCRYPTED_KEY_SIZE) { while (key_idx < ENCRYPTED_KEY_SIZE) {
bit_len += cfg_bits[pattern_idx]; 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) { 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]; 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] &= (0xff >> bit_len);
tweaked_key[key_idx] |= (_cryptKey[key_idx] & (~(0xff >> bit_len))); tweaked_key[key_idx] |= (_cryptKey[key_idx] & (~(0xff >> bit_len)));
} }
key_idx++; key_idx++;
bit_len -= 8; bit_len -= 8;
} }
} else { //keep tweaked key bits } else {
while (bit_len > 0) { 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] &= (~(0xff >> bit_len));
tweaked_key[key_idx] |= (_cryptKey[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) { if (!_cryptKey) {
log_w("AES key not set");
return false; return false;
} }
if (_bufferLen % ENCRYPTED_BLOCK_SIZE != 0) { if (_bufferLen % ENCRYPTED_BLOCK_SIZE != 0) {
log_e("buffer size error");
return false; return false;
} }
if (!_cryptBuffer) { if (!_cryptBuffer) {
_cryptBuffer = new (std::nothrow) uint8_t[ENCRYPTED_BLOCK_SIZE]; _cryptBuffer = new (std::nothrow) uint8_t[ENCRYPTED_BLOCK_SIZE];
} }
if (!_cryptBuffer) { if (!_cryptBuffer) {
log_e("new failed");
return false; return false;
} }
uint8_t tweaked_key[ENCRYPTED_KEY_SIZE]; //tweaked crypt key uint8_t tweaked_key[ENCRYPTED_KEY_SIZE];
int done = 0; int done = 0;
/* #if defined(ESP32)
Mbedtls functions will be replaced with esp_aes functions when hardware acceleration is available mbedtls_aes_context ctx;
To Do:
Replace mbedtls for the cases where there's no hardware acceleration
*/
mbedtls_aes_context ctx; //initialize AES
mbedtls_aes_init(&ctx); mbedtls_aes_init(&ctx);
while ((_bufferLen - done) >= ENCRYPTED_BLOCK_SIZE) { while ((_bufferLen - done) >= ENCRYPTED_BLOCK_SIZE) {
for (int i = 0; i < ENCRYPTED_BLOCK_SIZE; i++) { 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) { 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)) { if (mbedtls_aes_setkey_enc(&ctx, tweaked_key, 256)) {
return false; return false;
} }
@ -504,49 +572,40 @@ bool UpdateClass::_decryptBuffer() {
return false; 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; return false;
} }
for (int i = 0; i < ENCRYPTED_BLOCK_SIZE; i++) { 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; done += ENCRYPTED_BLOCK_SIZE;
} }
#endif
return true; return true;
} }
bool UpdateClass::_writeBuffer() { bool ESPUpdateClass::_writeBuffer() {
//first bytes of loading image, check to see if loading image needs decrypting
if (!_progress) { if (!_progress) {
_cryptMode &= U_AES_DECRYPT_MODE_MASK; _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))) { 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 _cryptMode |= U_AES_IMAGE_DECRYPTING_BIT;
log_d("Decrypting OTA Image");
} }
} }
//check if data in buffer needs decrypting
if (_cryptMode & U_AES_IMAGE_DECRYPTING_BIT) { if (_cryptMode & U_AES_IMAGE_DECRYPTING_BIT) {
if (!_decryptBuffer()) { if (!_decryptBuffer()) {
_abort(UPDATE_ERROR_DECRYPT); _abort(UPDATE_ERROR_DECRYPT);
return false; return false;
} }
} }
//first bytes of new firmware
uint8_t skip = 0; uint8_t skip = 0;
if (!_progress && _command == U_FLASH) { if (!_progress && _command == U_FLASH) {
//check magic
if (_buffer[0] != ESP_IMAGE_HEADER_MAGIC) { if (_buffer[0] != ESP_IMAGE_HEADER_MAGIC) {
_abort(UPDATE_ERROR_MAGIC_BYTE); _abort(UPDATE_ERROR_MAGIC_BYTE);
return false; 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; skip = ENCRYPTED_BLOCK_SIZE;
_skipBuffer = new (std::nothrow) uint8_t[skip]; _skipBuffer = new (std::nothrow) uint8_t[skip];
if (!_skipBuffer) { if (!_skipBuffer) {
log_e("_skipBuffer allocation failed");
return false; return false;
} }
memcpy(_skipBuffer, _buffer, skip); memcpy(_skipBuffer, _buffer, skip);
@ -554,14 +613,16 @@ bool UpdateClass::_writeBuffer() {
if (!_progress && _progress_callback) { if (!_progress && _progress_callback) {
_progress_callback(0, _size); _progress_callback(0, _size);
} }
// =============================================================
// CHIPSET SPECIFIC PHYSICAL WRITE
// =============================================================
#if defined(ESP32)
size_t offset = _partition->address + _progress; size_t offset = _partition->address + _progress;
bool block_erase = bool block_erase = (_size - _progress >= SPI_FLASH_BLOCK_SIZE) && (offset % SPI_FLASH_BLOCK_SIZE == 0);
(_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;
bool part_head_sectors = bool part_tail_sectors = offset >= (_partition->address + _size) / SPI_FLASH_BLOCK_SIZE * SPI_FLASH_BLOCK_SIZE;
_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 (block_erase || part_head_sectors || part_tail_sectors) {
if (!ESP.partitionEraseRange(_partition, _progress, block_erase ? SPI_FLASH_BLOCK_SIZE : SPI_FLASH_SEC_SIZE)) { if (!ESP.partitionEraseRange(_partition, _progress, block_erase ? SPI_FLASH_BLOCK_SIZE : SPI_FLASH_SEC_SIZE)) {
_abort(UPDATE_ERROR_ERASE); _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)) 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)) { && !ESP.partitionWrite(_partition, _progress + skip, (uint32_t *)_buffer + skip / sizeof(uint32_t), _bufferLen - skip)) {
_abort(UPDATE_ERROR_WRITE); _abort(UPDATE_ERROR_WRITE);
return false; 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) { if (!_progress && _command == U_FLASH) {
_buffer[0] = ESP_IMAGE_HEADER_MAGIC; _buffer[0] = ESP_IMAGE_HEADER_MAGIC;
} }
@ -589,7 +657,7 @@ bool UpdateClass::_writeBuffer() {
return true; return true;
} }
bool UpdateClass::_verifyHeader(uint8_t data) { bool ESPUpdateClass::_verifyHeader(uint8_t data) {
if (_command == U_FLASH) { if (_command == U_FLASH) {
if (data != ESP_IMAGE_HEADER_MAGIC) { if (data != ESP_IMAGE_HEADER_MAGIC) {
_abort(UPDATE_ERROR_MAGIC_BYTE); _abort(UPDATE_ERROR_MAGIC_BYTE);
@ -602,13 +670,13 @@ bool UpdateClass::_verifyHeader(uint8_t data) {
return false; return false;
} }
bool UpdateClass::_verifyEnd() { bool ESPUpdateClass::_verifyEnd() {
#if defined(ESP32)
if (_command == U_FLASH) { if (_command == U_FLASH) {
if (!_enablePartition(_partition) || !_partitionIsBootable(_partition)) { if (!_enablePartition(_partition) || !_partitionIsBootable(_partition)) {
_abort(UPDATE_ERROR_READ); _abort(UPDATE_ERROR_READ);
return false; return false;
} }
if (esp_ota_set_boot_partition(_partition)) { if (esp_ota_set_boot_partition(_partition)) {
_abort(UPDATE_ERROR_ACTIVATE); _abort(UPDATE_ERROR_ACTIVATE);
return false; return false;
@ -619,10 +687,17 @@ bool UpdateClass::_verifyEnd() {
_reset(); _reset();
return true; return true;
} }
#elif defined(ESP8266)
if (!EPUpdate.end()) {
return false;
}
_reset();
return true;
#endif
return false; return false;
} }
bool UpdateClass::setMD5(const char *expected_md5) { bool ESPUpdateClass::setMD5(const char *expected_md5) {
if (strlen(expected_md5) != 32) { if (strlen(expected_md5) != 32) {
return false; return false;
} }
@ -631,24 +706,20 @@ bool UpdateClass::setMD5(const char *expected_md5) {
return true; return true;
} }
bool UpdateClass::end(bool evenIfRemaining) { bool ESPUpdateClass::end(bool evenIfRemaining) {
if (hasError() || _size == 0) { if (hasError() || _size == 0) {
return false; return false;
} }
if (!isFinished() && !evenIfRemaining) { if (!isFinished() && !evenIfRemaining) {
log_e("premature end: res:%u, pos:%u/%u\n", getError(), progress(), _size);
_abort(UPDATE_ERROR_ABORT); _abort(UPDATE_ERROR_ABORT);
return false; return false;
} }
if (evenIfRemaining) { if (evenIfRemaining) {
if (_bufferLen > 0) { if (_bufferLen > 0) {
_writeBuffer(); _writeBuffer();
} }
_size = progress(); _size = progress();
} }
_md5.calculate(); _md5.calculate();
if (_target_md5.length()) { if (_target_md5.length()) {
if (_target_md5 != _md5.toString()) { if (_target_md5 != _md5.toString()) {
@ -656,22 +727,18 @@ bool UpdateClass::end(bool evenIfRemaining) {
return false; return false;
} }
} }
return _verifyEnd(); 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()) { if (hasError() || !isRunning()) {
return 0; return 0;
} }
if (len > remaining()) { if (len > remaining()) {
_abort(UPDATE_ERROR_SPACE); _abort(UPDATE_ERROR_SPACE);
return 0; return 0;
} }
size_t left = len; size_t left = len;
while ((_bufferLen + left) > SPI_FLASH_SEC_SIZE) { while ((_bufferLen + left) > SPI_FLASH_SEC_SIZE) {
size_t toBuff = SPI_FLASH_SEC_SIZE - _bufferLen; size_t toBuff = SPI_FLASH_SEC_SIZE - _bufferLen;
memcpy(_buffer + _bufferLen, data + (len - left), toBuff); memcpy(_buffer + _bufferLen, data + (len - left), toBuff);
@ -691,7 +758,7 @@ size_t UpdateClass::write(uint8_t *data, size_t len) {
return len; return len;
} }
size_t UpdateClass::writeStream(Stream &data) { size_t ESPUpdateClass::writeStream(Stream &data) {
size_t written = 0; size_t written = 0;
size_t toRead = 0; size_t toRead = 0;
int timeout_failures = 0; int timeout_failures = 0;
@ -713,17 +780,13 @@ size_t UpdateClass::writeStream(Stream &data) {
while (remaining()) { while (remaining()) {
if (_ledPin != -1) { if (_ledPin != -1) {
digitalWrite(_ledPin, _ledOn); // Switch LED on digitalWrite(_ledPin, _ledOn);
} }
size_t bytesToRead = SPI_FLASH_SEC_SIZE - _bufferLen; size_t bytesToRead = SPI_FLASH_SEC_SIZE - _bufferLen;
if (bytesToRead > remaining()) { if (bytesToRead > remaining()) {
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; toRead = 0;
timeout_failures = 0; timeout_failures = 0;
while (!toRead) { while (!toRead) {
@ -739,7 +802,7 @@ size_t UpdateClass::writeStream(Stream &data) {
} }
if (_ledPin != -1) { if (_ledPin != -1) {
digitalWrite(_ledPin, !_ledOn); // Switch LED off digitalWrite(_ledPin, !_ledOn);
} }
_bufferLen += toRead; _bufferLen += toRead;
if ((_bufferLen == remaining() || _bufferLen == SPI_FLASH_SEC_SIZE) && !_writeBuffer()) { if ((_bufferLen == remaining() || _bufferLen == SPI_FLASH_SEC_SIZE) && !_writeBuffer()) {
@ -748,40 +811,34 @@ size_t UpdateClass::writeStream(Stream &data) {
written += toRead; written += toRead;
#if CONFIG_FREERTOS_UNICORE #if CONFIG_FREERTOS_UNICORE
delay(1); // Fix solo WDT delay(1);
#endif #endif
} }
return written; return written;
} }
void UpdateClass::printError(Print &out) { void ESPUpdateClass::printError(Print &out) {
out.println(_err2str(_error)); out.println(_err2str(_error));
} }
const char *UpdateClass::errorString() { const char *ESPUpdateClass::errorString() {
return _err2str(_error); return _err2str(_error);
} }
bool UpdateClass::_chkDataInBlock(const uint8_t *data, size_t len) const { bool ESPUpdateClass::_chkDataInBlock(const uint8_t *data, size_t len) const {
// check 32-bit aligned blocks only
if (!len || len % sizeof(uint32_t)) { if (!len || len % sizeof(uint32_t)) {
return true; return true;
} }
size_t dwl = len / sizeof(uint32_t); size_t dwl = len / sizeof(uint32_t);
do { 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; return true;
} }
data += sizeof(uint32_t); data += sizeof(uint32_t);
} while (--dwl); } while (--dwl);
return false; return false;
} }
#if !defined(NO_GLOBAL_INSTANCES) && !defined(NO_GLOBAL_UPDATE) #if !defined(NO_GLOBAL_INSTANCES) && !defined(NO_GLOBAL_UPDATE)
UpdateClass Update; ESPUpdateClass ESPUpdate;
#endif #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";
?>