New HTTP Updater
This commit is contained in:
parent
3276c9527d
commit
9e2873829c
|
|
@ -94,7 +94,7 @@ class HCScanCallbacks : public NimBLEScanCallbacks {
|
|||
*/
|
||||
void onResult(const NimBLEAdvertisedDevice* advertisedDevice) override {
|
||||
uint16_t len;
|
||||
bthome_t *pTuyaData = (bthome_t *) advertisedDevice->getServiceData(&len);
|
||||
bthome_t *pTuyaData = (bthome_t *) advertisedDevice->getServiceData((uint16_t *) &len);
|
||||
// Tuya
|
||||
if (len == sizeof(bthome_t) && pTuyaData->info == 0x40) {
|
||||
if (config.nTemp1SensorType == TEMP_SENSOR_TYPE::BLE_TUYA) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
#ifndef __BLE_SCAN_H
|
||||
#define __BLE_SCAN_H
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
class NimBLEClient;
|
||||
class NimBLERemoteCharacteristic;
|
||||
class NimBLEAddress;
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ void CONFIG_TYPE::init() {
|
|||
for (int i = 0; i < 17; i = i + 8) {
|
||||
m_nChipId |= ((ESP.getEfuseMac() >> (40 - i)) & 0xff) << i;
|
||||
}
|
||||
m_nPublicPort = (uint16_t)(m_nChipId & 0xFFFF);
|
||||
m_nPublicPort = 3939;
|
||||
m_nDeviceType = THIS_DEVICE_TYPE;
|
||||
|
||||
//
|
||||
|
|
|
|||
291
HCUpdate.h
291
HCUpdate.h
|
|
@ -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 ¤tVersion, short nDeviceType, bool bForceUpdate);
|
||||
int handleUpdate(HTTPClient& http, const String& currentVersion, short nDeviceType);
|
||||
int http_downloadUpdate(HTTPClient &httpClient);
|
||||
|
||||
|
||||
/*
|
||||
This callback will be called when Update is receiving data
|
||||
*/
|
||||
UpdateClass &onProgress(THandlerFunction_Progress fn);
|
||||
|
||||
/*
|
||||
Call this to check the space needed for the update
|
||||
Will return false if there is not enough space
|
||||
*/
|
||||
bool begin(size_t size = UPDATE_SIZE_UNKNOWN, int command = U_FLASH, int ledPin = -1, uint8_t ledOn = LOW, const char *label = NULL);
|
||||
|
||||
/*
|
||||
Setup decryption configuration
|
||||
Crypt Key is 32bytes(256bits) block of data, use the same key as used to encrypt image file
|
||||
Crypt Address, use the same value as used to encrypt image file
|
||||
Crypt Config, use the same value as used to encrypt image file
|
||||
Crypt Mode, used to select if image files should be decrypted or not
|
||||
*/
|
||||
bool setupCrypt(const uint8_t *cryptKey = 0, size_t cryptAddress = 0, uint8_t cryptConfig = 0xf, int cryptMode = U_AES_DECRYPT_AUTO);
|
||||
|
||||
/*
|
||||
Writes a buffer to the flash and increments the address
|
||||
Returns the amount written
|
||||
*/
|
||||
size_t write(uint8_t *data, size_t len);
|
||||
|
||||
/*
|
||||
Writes the remaining bytes from the Stream to the flash
|
||||
Uses readBytes() and sets UPDATE_ERROR_STREAM on timeout
|
||||
Returns the bytes written
|
||||
Should be equal to the remaining bytes when called
|
||||
Usable for slow streams like Serial
|
||||
*/
|
||||
size_t writeStream(Stream &data);
|
||||
|
||||
/*
|
||||
If all bytes are written
|
||||
this call will write the config to eboot
|
||||
and return true
|
||||
If there is already an update running but is not finished and !evenIfRemaining
|
||||
or there is an error
|
||||
this will clear everything and return false
|
||||
the last error is available through getError()
|
||||
evenIfRemaining is helpful when you update without knowing the final size first
|
||||
*/
|
||||
bool end(bool evenIfRemaining = false);
|
||||
|
||||
/*
|
||||
sets AES256 key(32 bytes) used for decrypting image file
|
||||
*/
|
||||
bool setCryptKey(const uint8_t *cryptKey);
|
||||
|
||||
/*
|
||||
sets crypt mode used on image files
|
||||
*/
|
||||
bool setCryptMode(const int cryptMode);
|
||||
|
||||
/*
|
||||
sets address used for decrypting image file
|
||||
*/
|
||||
void setCryptAddress(const size_t cryptAddress) {
|
||||
_cryptAddress = cryptAddress & 0x00fffff0;
|
||||
}
|
||||
|
||||
/*
|
||||
sets crypt config used for decrypting image file
|
||||
*/
|
||||
void setCryptConfig(const uint8_t cryptConfig) {
|
||||
_cryptCfg = cryptConfig & 0x0f;
|
||||
}
|
||||
|
||||
/*
|
||||
Aborts the running update
|
||||
*/
|
||||
void abort();
|
||||
|
||||
/*
|
||||
Prints the last error to an output stream
|
||||
*/
|
||||
void printError(Print &out);
|
||||
|
||||
const char *errorString();
|
||||
|
||||
/*
|
||||
sets the expected MD5 for the firmware (hexString)
|
||||
*/
|
||||
bool setMD5(const char *expected_md5);
|
||||
|
||||
/*
|
||||
returns the MD5 String of the successfully ended firmware
|
||||
*/
|
||||
String md5String(void) {
|
||||
return _md5.toString();
|
||||
}
|
||||
|
||||
/*
|
||||
populated the result with the md5 bytes of the successfully ended firmware
|
||||
*/
|
||||
void md5(uint8_t *result) {
|
||||
return _md5.getBytes(result);
|
||||
}
|
||||
|
||||
//Helpers
|
||||
uint8_t getError() {
|
||||
return _error;
|
||||
}
|
||||
void clearError() {
|
||||
_error = UPDATE_ERROR_OK;
|
||||
}
|
||||
bool hasError() {
|
||||
return _error != UPDATE_ERROR_OK;
|
||||
}
|
||||
bool isRunning() {
|
||||
return _size > 0;
|
||||
}
|
||||
bool isFinished() {
|
||||
return _progress == _size;
|
||||
}
|
||||
size_t size() {
|
||||
return _size;
|
||||
}
|
||||
size_t progress() {
|
||||
return _progress;
|
||||
}
|
||||
size_t remaining() {
|
||||
return _size - _progress;
|
||||
}
|
||||
|
||||
/*
|
||||
Template to write from objects that expose
|
||||
available() and read(uint8_t*, size_t) methods
|
||||
faster than the writeStream method
|
||||
writes only what is available
|
||||
*/
|
||||
template<typename T> size_t write(T &data) {
|
||||
size_t written = 0;
|
||||
if (hasError() || !isRunning()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t available = data.available();
|
||||
while (available) {
|
||||
if (_bufferLen + available > remaining()) {
|
||||
available = remaining() - _bufferLen;
|
||||
}
|
||||
if (_bufferLen + available > 4096) {
|
||||
size_t toBuff = 4096 - _bufferLen;
|
||||
data.read(_buffer + _bufferLen, toBuff);
|
||||
_bufferLen += toBuff;
|
||||
if (!_writeBuffer()) {
|
||||
return written;
|
||||
}
|
||||
written += toBuff;
|
||||
} else {
|
||||
data.read(_buffer + _bufferLen, available);
|
||||
_bufferLen += available;
|
||||
written += available;
|
||||
if (_bufferLen == remaining()) {
|
||||
if (!_writeBuffer()) {
|
||||
return written;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (remaining() == 0) {
|
||||
return written;
|
||||
}
|
||||
available = data.available();
|
||||
}
|
||||
return written;
|
||||
}
|
||||
|
||||
/*
|
||||
check if there is a firmware on the other OTA partition that you can bootinto
|
||||
*/
|
||||
bool canRollBack();
|
||||
/*
|
||||
set the other OTA partition as bootable (reboot to enable)
|
||||
*/
|
||||
bool rollBack();
|
||||
|
||||
private:
|
||||
void _reset();
|
||||
void _abort(uint8_t err);
|
||||
void _cryptKeyTweak(size_t cryptAddress, uint8_t *tweaked_key);
|
||||
bool _decryptBuffer();
|
||||
bool _writeBuffer();
|
||||
bool _verifyHeader(uint8_t data);
|
||||
bool _verifyEnd();
|
||||
bool _enablePartition(const esp_partition_t *partition);
|
||||
bool _chkDataInBlock(const uint8_t *data, size_t len) const; // check if block contains any data or is empty
|
||||
|
||||
int _httpClientTimeout;
|
||||
followRedirects_t _followRedirects = HTTPC_DISABLE_FOLLOW_REDIRECTS;
|
||||
uint8_t _error;
|
||||
uint8_t *_cryptKey;
|
||||
uint8_t *_cryptBuffer;
|
||||
uint8_t *_buffer;
|
||||
uint8_t *_skipBuffer;
|
||||
size_t _bufferLen;
|
||||
size_t _size;
|
||||
THandlerFunction_Progress _progress_callback;
|
||||
uint32_t _progress;
|
||||
uint32_t _paroffset;
|
||||
uint32_t _command;
|
||||
const esp_partition_t *_partition;
|
||||
|
||||
String _target_md5;
|
||||
MD5Builder _md5;
|
||||
|
||||
int _ledPin;
|
||||
uint8_t _ledOn;
|
||||
|
||||
uint8_t _cryptMode;
|
||||
size_t _cryptAddress;
|
||||
uint8_t _cryptCfg;
|
||||
};
|
||||
|
||||
#if !defined(NO_GLOBAL_INSTANCES) && !defined(NO_GLOBAL_UPDATE)
|
||||
extern UpdateClass Update;
|
||||
#endif
|
||||
|
||||
#endif // defined(ESP32UPDATER_H)
|
||||
#endif // defined(ESP32)
|
||||
106
OTA.cpp
106
OTA.cpp
|
|
@ -5,7 +5,7 @@
|
|||
#include <HTTPClient.h>
|
||||
#include <esp_task_wdt.h>
|
||||
|
||||
#include "HCUpdate.h"
|
||||
#include "src/ESPUpdate.h"
|
||||
#include "HermitCrab.h"
|
||||
#include "Config.h"
|
||||
#include "ConnectWiFi.h"
|
||||
|
|
@ -65,70 +65,62 @@ void onOTAProgress(int current, int total) {
|
|||
ESP_LOGD(TAG_OTA,"OTA -- Progress: %d%%\n", (current * 100) / total);
|
||||
}
|
||||
|
||||
//==========================================================================
|
||||
String urlEncode(const String &url, const char *safeChars = "-_.~") {
|
||||
String encoded = "";
|
||||
char temp[4];
|
||||
|
||||
for (int i = 0; i < url.length(); i++) {
|
||||
temp[0] = url.charAt(i);
|
||||
if (temp[0] == 32) { //space
|
||||
encoded.concat('+');
|
||||
} else if ((temp[0] >= 48 && temp[0] <= 57) /*0-9*/
|
||||
|| (temp[0] >= 65 && temp[0] <= 90) /*A-Z*/
|
||||
|| (temp[0] >= 97 && temp[0] <= 122) /*a-z*/
|
||||
|| (strchr(safeChars, temp[0]) != NULL) /* "=&-_.~" */
|
||||
) {
|
||||
encoded.concat(temp[0]);
|
||||
} else { //character needs encoding
|
||||
snprintf(temp, 4, "%%%02X", temp[0]);
|
||||
encoded.concat(temp);
|
||||
}
|
||||
}
|
||||
return encoded;
|
||||
}
|
||||
|
||||
//==========================================================================
|
||||
bool addQuery(String *query, const String name, const String value) {
|
||||
if (name.length() && value.length()) {
|
||||
if (query->length() < 3) {
|
||||
*query = "?";
|
||||
} else {
|
||||
query->concat('&');
|
||||
}
|
||||
query->concat(urlEncode(name));
|
||||
query->concat('=');
|
||||
query->concat(urlEncode(value));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
//==========================================================================
|
||||
bool checkOTA(bool bForceUpdate)
|
||||
{
|
||||
|
||||
// Set callbacks
|
||||
//Update.onStart(onOTAStart);
|
||||
//Update.onEnd(onOTAEnd);
|
||||
String query = "";
|
||||
addQuery(&query, "cmd", (bForceUpdate ? "download" : "check")); //action command
|
||||
uri.concat(query);
|
||||
String version = String(HC__VERSION);
|
||||
ESP_LOGI(TAG_OTA,"OTA - URL: %s\n", url.c_str());
|
||||
ESP_LOGI(TAG_OTA,"OTA - URI: %s\n", uri.c_str());
|
||||
ESP_LOGI(TAG_OTA,"OTA - Ver: %s\n", HC__VERSION);
|
||||
|
||||
|
||||
WiFiClientSecure client;
|
||||
ESPUpdateClass ESPUpdate;
|
||||
client.setCACert(rootCACertificate);
|
||||
UpdateClass hcUpdate(5000);
|
||||
hcUpdate.onProgress(onOTAProgress);
|
||||
//int update(WiFiClient& client, String &url, uint16_t port, String& uri,
|
||||
// String ¤tVersion, short nDeviceType, bool bForceUpdate);
|
||||
int result = hcUpdate.update(client, url, UPDATE_PORT, uri, version, (short)config.m_nDeviceType, true);
|
||||
|
||||
ESPUpdate.onProgress(onOTAProgress);
|
||||
|
||||
// result: POSITIVE (HTTP/Status), NEGATIVE (Network Error)
|
||||
int result = ESPUpdate.update(
|
||||
client,
|
||||
"visionsoft.kr", // Server Host
|
||||
443, // Server Port HTTPS,
|
||||
"/sc/pages/firmware_download.php", // phpUri,
|
||||
HC__VERSION, // Current Version string
|
||||
"HCesp", // Project Tag
|
||||
bForceUpdate, // bForceUpdate
|
||||
true // bRebootAfterInstall
|
||||
);
|
||||
|
||||
// ======================================================================
|
||||
// STATUS REPORTING LOGIC
|
||||
// ======================================================================
|
||||
bool _setupMode = true;
|
||||
if (result == 200) {
|
||||
if (_setupMode) {
|
||||
Serial.println("[OTA] Success: Firmware downloaded and installed.");
|
||||
Serial.println("[OTA] Rebooting...");
|
||||
}
|
||||
delay(1000);
|
||||
ESP.restart();
|
||||
}
|
||||
else if (result == 204) {
|
||||
if (_setupMode) {
|
||||
Serial.println("[OTA] Not Available: No firmware binary found for this project.");
|
||||
}
|
||||
}
|
||||
else if (result == 304) {
|
||||
if (_setupMode) {
|
||||
Serial.println("[OTA] Up To Date: Current firmware is the latest version.");
|
||||
}
|
||||
}
|
||||
else if (result > 0) {
|
||||
// Captures 403 (Forbidden), 404 (Not Found), 500 (Server Error)
|
||||
if (_setupMode) {
|
||||
Serial.printf("[OTA] Server Error: HTTP %d\n", result);
|
||||
}
|
||||
}
|
||||
else if (result < 0) {
|
||||
// Captures library errors like -1 (Connection Refused) or -11 (Timeout)
|
||||
if (_setupMode) {
|
||||
Serial.printf("[OTA] Connection Error: %s (%d)\n",
|
||||
HTTPClient::errorToString(result).c_str(), result);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
135
OTA.cpp.backup
Normal file
135
OTA.cpp.backup
Normal 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 ¤tVersion, short nDeviceType, bool bForceUpdate);
|
||||
int result = hcUpdate.update(client, url, UPDATE_PORT, uri, version, (short)config.m_nDeviceType, true);
|
||||
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
30
Setup.cpp
30
Setup.cpp
|
|
@ -68,20 +68,20 @@ void setup() {
|
|||
setupSensor();
|
||||
|
||||
ui.setup();
|
||||
ui.message(0, "WiFi...");
|
||||
ui.message(0, (char *) "WiFi...");
|
||||
setupWiFi();
|
||||
|
||||
if (aht25.sensor() || aht10_0x39.sensor()) {
|
||||
ui.message(4, "Sensor... OK!");
|
||||
ui.message(4, (char *) "Sensor... OK!");
|
||||
} else {
|
||||
ui.message(4, "Sensor... None!");
|
||||
ui.message(4, (char *) "Sensor... None!");
|
||||
}
|
||||
|
||||
ui.message(5, "ZCD...");
|
||||
ui.message(5, (char *) "ZCD...");
|
||||
setupZCD();
|
||||
ui.message(5, "ZCD... OK!");
|
||||
ui.message(5, (char *) "ZCD... OK!");
|
||||
|
||||
ui.message(6, "Setup OK!");
|
||||
ui.message(6, (char *) "Setup OK!");
|
||||
//if (!isWiFiConnected) delay(3000);
|
||||
ble.setupConnect(config.nBLESensorAddr, config.nBLESensorAddr2);
|
||||
|
||||
|
|
@ -111,8 +111,6 @@ void setupConfig() {
|
|||
config.load();
|
||||
history.loadPID();
|
||||
config.m_nDeviceType = THIS_DEVICE_TYPE;
|
||||
if (config.m_nPublicPort == 3939)
|
||||
config.m_nPublicPort = (uint16_t)(config.m_nChipId & 0xFFFF);
|
||||
}
|
||||
|
||||
void setupWiFi() {
|
||||
|
|
@ -146,7 +144,7 @@ void setupWiFi() {
|
|||
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
ledcWrite(PIN_LED_WIFI, PWM_FULL); // LED_OFF
|
||||
ui.message(0, "WiFi...OK!");
|
||||
ui.message(0, (char *) "WiFi...OK!");
|
||||
DPRINTLN("WiFi - Connected at SETUP");
|
||||
DPRINTF("WiFi - SSID(%s) PW(%s) IP(%s)\n", config.ssid, config.pw, WiFi.localIP().toString().c_str());
|
||||
g_bWiFiHasBeenConnected = true;
|
||||
|
|
@ -162,35 +160,35 @@ void setupWiFi() {
|
|||
void setupPostWiFi(bool bBoot = false) {
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
// Time
|
||||
if (bBoot) ui.message(1, "Time...");
|
||||
if (bBoot) ui.message(1, (char *) "Time...");
|
||||
timeManager.begin();
|
||||
vTaskDelay((bBoot ? 500 : 250)/portTICK_PERIOD_MS);
|
||||
timeManager.checkNTPResponse();
|
||||
|
||||
if (bBoot) {
|
||||
if (timeManager.hasNTPUpdate()) {
|
||||
ui.message(1, "Time...OK!");
|
||||
ui.message(1, (char *) "Time...OK!");
|
||||
|
||||
// OTA
|
||||
DPRINTLN("Setup - TimeManager.begin()");
|
||||
DPRINTLN("\n===============================\n");
|
||||
DPRINTLN(" Trying OTA");
|
||||
ui.message(2, "Update check...");
|
||||
ui.message(2, (char *) "Update check...");
|
||||
checkOTA(true);
|
||||
ui.message(2, "Update check...OK!");
|
||||
ui.message(2, (char *) "Update check...OK!");
|
||||
DPRINTLN(" OTA Process completed!");
|
||||
DPRINTLN("===============================\n");
|
||||
} else {
|
||||
ui.message(2, "Update check SKIPPED!");
|
||||
ui.message(2, (char *) "Update check SKIPPED!");
|
||||
timeManager.sendNTPRequest();
|
||||
}
|
||||
}
|
||||
|
||||
// Host
|
||||
if (bBoot) ui.message(3, "Server...");
|
||||
if (bBoot) ui.message(3, (char *) "Server...");
|
||||
host.Setup();
|
||||
DPRINTLN("Setup - host.Setup()");
|
||||
if (bBoot) ui.message(3, "Server...OK!");
|
||||
if (bBoot) ui.message(3, (char *) "Server...OK!");
|
||||
g_bWiFiSetupExecuted = true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
491
UPnPClient.cpp
491
UPnPClient.cpp
|
|
@ -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;
|
||||
};
|
||||
253
WiFiHost.cpp
253
WiFiHost.cpp
|
|
@ -18,7 +18,7 @@
|
|||
#include "zcd.h"
|
||||
#include "AHT2x.h"
|
||||
#include "WiFiHost.h"
|
||||
#include "UPnPClient.h"
|
||||
//#include "UPnPClient.h"
|
||||
|
||||
|
||||
|
||||
|
|
@ -64,15 +64,10 @@ void CWiFiHost::Setup()
|
|||
.sig2 = SIGNATURE2 };
|
||||
clientPacket = hostPacket;
|
||||
|
||||
//if (m_nPublicPort == 0)
|
||||
{
|
||||
if (config.m_nPublicPort == 0)
|
||||
config.m_nPublicPort = (uint16_t)(config.m_nChipId & 0xFFFF);
|
||||
m_nPublicPort = config.m_nPublicPort;
|
||||
}
|
||||
//m_nPublicPort = config.m_nPublicPort;
|
||||
|
||||
wifiServer = WiFiServer(SERVER_PORT, 1);
|
||||
wifiExternal = WiFiServer(m_nPublicPort, 1);
|
||||
//wifiExternal = WiFiServer(m_nPublicPort, 1);
|
||||
|
||||
//wifiStatus = WIFI_NOT_CONNECTED;
|
||||
m_nDataSend_sent = 0;
|
||||
|
|
@ -96,36 +91,36 @@ void CWiFiHost::Setup()
|
|||
m_nLastHeartBeatSentTime =
|
||||
m_nLastUDPBroadcastTime = millis();
|
||||
m_bClientConnected = false;
|
||||
m_dwPublicIP = 0;
|
||||
//m_dwPublicIP = 0;
|
||||
wifiClient.stop();
|
||||
|
||||
if (isWiFiConnected())
|
||||
{
|
||||
// UPnP Client
|
||||
{
|
||||
uint32_t ip = WiFi.gatewayIP();
|
||||
uint16_t port = m_nPublicPort;
|
||||
CUpnpClient upnp;
|
||||
// {
|
||||
// uint32_t ip = WiFi.gatewayIP();
|
||||
// uint16_t port = m_nPublicPort;
|
||||
// //CUpnpClient upnp;
|
||||
|
||||
if (upnp.registerUPnP(&ip, &port)) {
|
||||
status.nFlags |= FLAG_UPNP;
|
||||
} else {
|
||||
status.nFlags &= ~FLAG_UPNP;
|
||||
}
|
||||
// //if (upnp.registerUPnP(&ip, &port)) {
|
||||
// // status.nFlags |= FLAG_UPNP;
|
||||
// //} else {
|
||||
// status.nFlags &= ~FLAG_UPNP;
|
||||
// //}
|
||||
|
||||
if (ip != 0)
|
||||
m_dwPublicIP = ip;
|
||||
if (port != m_nPublicPort) {
|
||||
config.m_nPublicPort = port;
|
||||
config.save();
|
||||
m_nPublicPort = port;
|
||||
}
|
||||
// if (ip != 0)
|
||||
// m_dwPublicIP = ip;
|
||||
// if (port != m_nPublicPort) {
|
||||
// config.m_nPublicPort = port;
|
||||
// config.save();
|
||||
// m_nPublicPort = port;
|
||||
// }
|
||||
|
||||
}
|
||||
// }
|
||||
|
||||
// Server
|
||||
wifiServer.begin(SERVER_PORT, 1);
|
||||
wifiExternal.begin(m_nPublicPort, 1);
|
||||
//wifiExternal.begin(m_nPublicPort, 1);
|
||||
m_nLastReceivedTime = millis();
|
||||
m_bClientConnected = false;
|
||||
|
||||
|
|
@ -150,7 +145,7 @@ MY_IRAM_ATTR void CWiFiHost::Stop() {
|
|||
|
||||
// Stop server
|
||||
wifiServer.stop();
|
||||
wifiExternal.stop();
|
||||
//wifiExternal.stop();
|
||||
m_bClientConnected = false;
|
||||
|
||||
// Stop Client
|
||||
|
|
@ -171,87 +166,86 @@ MY_IRAM_ATTR void CWiFiHost::CloseConnection()
|
|||
|
||||
IRAM_ATTR void CWiFiHost::Loop(unsigned long clock)
|
||||
{
|
||||
static unsigned long lastReceivedTime = 0;
|
||||
if (!isWiFiConnected()) return;
|
||||
|
||||
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");
|
||||
// --- 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;
|
||||
}
|
||||
|
||||
// Accept from internal XOR external port
|
||||
// Try accepting from internal, then external if internal fails
|
||||
wifiClient = wifiServer.accept();
|
||||
if (!wifiClient || !wifiClient.connected()) {
|
||||
wifiClient = wifiExternal.accept();
|
||||
}
|
||||
//if (!wifiClient) wifiClient = wifiExternal.accept();
|
||||
|
||||
if (wifiClient && wifiClient.connected())
|
||||
{
|
||||
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
|
||||
ledcWrite(PIN_LED_WIFI, PWM_FULL - 10);
|
||||
}
|
||||
break;
|
||||
return;
|
||||
}
|
||||
|
||||
// --- Global Safety Check for all other modes ---
|
||||
// If we lose connection mid-process, jump back to WAITING immediately
|
||||
if (!wifiClient || !wifiClient.connected()) {
|
||||
m_bClientConnected = false;
|
||||
m_nMode = MODE_WAITING;
|
||||
ledcWrite(PIN_LED_WIFI, PWM_FULL - 2);
|
||||
return;
|
||||
}
|
||||
|
||||
// --- Main State Machine ---
|
||||
switch (m_nMode) {
|
||||
case MODE_PACKET:
|
||||
// Client Connected
|
||||
if (m_bClientConnected && wifiClient.connected())
|
||||
{
|
||||
CheckClient(clock);
|
||||
|
||||
if (m_bClientConnected) {
|
||||
if (clock - m_nLastReceivedTime > 60000)
|
||||
{
|
||||
ESP_LOGI(TAG_WIFI_HOST,"Host: dropping connection for no HB in 60 seconds");
|
||||
// Timeout Check (60 seconds)
|
||||
if (clock - m_nLastReceivedTime > 60000) {
|
||||
ESP_LOGW(TAG_WIFI_HOST, "Host: Timeout - Dropping Connection");
|
||||
wifiClient.stop();
|
||||
m_bClientConnected = false;
|
||||
m_nMode = MODE_WAITING;
|
||||
}
|
||||
|
||||
// Send HeartBeat
|
||||
if (clock - m_nLastHeartBeatSentTime >= 1000) {
|
||||
// Heartbeat Pulse (Every 1 second)
|
||||
else if (clock - m_nLastHeartBeatSentTime >= 1000) {
|
||||
SendHeartBeat();
|
||||
m_nLastHeartBeatSentTime = clock;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!m_bClientConnected || !wifiClient.connected()) {
|
||||
m_nMode = MODE_WAITING;
|
||||
// LED
|
||||
ledcWrite(PIN_LED_WIFI, PWM_FULL - 2); // Almost Off
|
||||
}
|
||||
break;
|
||||
|
||||
case MODE_SEND:
|
||||
// SendData returns true only when the CURRENT buffer is empty
|
||||
if (SendData(clock)) {
|
||||
if (m_bSendHistoryPending) {
|
||||
// Mark pending to send the second part: from the start to head-1
|
||||
//SendData(history.getRingData2() /* &ring[0] */, sizeof(STATUS_TYPE) * head);
|
||||
//m_bSendHistoryPending = true;
|
||||
//m_nPendingHistoryCount = history.getHead();
|
||||
m_bSendHistoryPending = false;
|
||||
if (m_nPendingHistoryCount > 0) {
|
||||
ESP_LOGI(TAG_WIFI_HOST,"WfFi Host - SendData - 2nd part of History (%d)\n", m_nPendingHistoryCount);
|
||||
// Check if there is a second part of the history ring buffer
|
||||
if (m_bSendHistoryPending && m_nPendingHistoryCount > 0) {
|
||||
ESP_LOGI(TAG_WIFI_HOST, "WiFi Host: Preparing History Part 2");
|
||||
|
||||
// Setup the next chunk
|
||||
SendData(history.getRingData2(), sizeof(STATUS_TYPE) * m_nPendingHistoryCount);
|
||||
|
||||
// Clear flags so we don't loop here forever
|
||||
m_bSendHistoryPending = false;
|
||||
m_nPendingHistoryCount = 0;
|
||||
}
|
||||
|
||||
// We stay in MODE_SEND. The next Loop() turn will start sending Part 2.
|
||||
} else {
|
||||
// All data (including history parts) is done
|
||||
m_nMode = MODE_PACKET;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case MODE_RECV:
|
||||
if (ReceiveData(clock)) m_nMode = MODE_PACKET;
|
||||
if (ReceiveData(clock)) {
|
||||
m_nMode = MODE_PACKET;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -271,8 +265,8 @@ MY_IRAM_ATTR void CWiFiHost::SendHeartBeat(unsigned long clock) {
|
|||
// External Heartbeat
|
||||
UDP_CONFIG_TYPE pktConfig;
|
||||
pktConfig.udp = packetUDP;
|
||||
pktConfig.udp.m_nPort = m_nPublicPort;
|
||||
pktConfig.udp.dwIPAddress = m_dwPublicIP;
|
||||
pktConfig.udp.m_nPort = 3939;
|
||||
pktConfig.udp.dwIPAddress = (uint32_t)WiFi.localIP();
|
||||
pktConfig.udp.status = status;
|
||||
|
||||
if (((uint32_t)m_cExternalServerIPAddress) != (uint32_t)0l)
|
||||
|
|
@ -363,35 +357,31 @@ MY_IRAM_ATTR void CWiFiHost::MonitorUDP() {
|
|||
|
||||
IRAM_ATTR void CWiFiHost::CheckClient(unsigned long clock)
|
||||
{
|
||||
bool bLED = false;
|
||||
static TCP_PACKET cpkt;
|
||||
// 1. Quick exit if no data is ready to be read
|
||||
int available = wifiClient.available();
|
||||
if (available >= sizeof(TCP_PACKET)) {
|
||||
uint8_t* pC = (uint8_t*)&cpkt;
|
||||
int count = wifiClient.readBytes(pC, sizeof(TCP_PACKET));
|
||||
if (count == sizeof(TCP_PACKET)) {
|
||||
// Now we have a full packet size data
|
||||
if (cpkt.sig1 == SIGNATURE1 &&
|
||||
cpkt.sig2 == SIGNATURE2 &&
|
||||
cpkt.len == sizeof(TCP_PACKET)) {
|
||||
// Process the completed PACKET
|
||||
ProcessPacket(cpkt);
|
||||
m_nLastReceivedTime = clock;
|
||||
return;
|
||||
}
|
||||
if (available < sizeof(TCP_PACKET)) return;
|
||||
|
||||
// Invalid Packet - remove the first byte off from the buff
|
||||
ESP_LOGI(TAG_WIFI_HOST,"Invalid Packet %s%s size: %d/%d\n",
|
||||
cpkt.sig1 == SIGNATURE1 ? "SIG1 OK" : "",
|
||||
cpkt.sig2 == SIGNATURE2 ? "SIG2 OK" : "",
|
||||
cpkt.len, sizeof(TCP_PACKET));
|
||||
// Shift the buffer data off by 1 byte for the next cycle
|
||||
//buffIndexC = sizeof(TCP_PACKET) - 1;
|
||||
//memcpy(pC, &pC[1], buffIndexC);
|
||||
while (m_bClientConnected &&
|
||||
wifiClient.connected() &&
|
||||
wifiClient.readBytes((uint8_t *) &cpkt, sizeof(cpkt)) > 0) {
|
||||
};
|
||||
static TCP_PACKET cpkt;
|
||||
|
||||
// 2. Peek or Read the packet
|
||||
// We use readBytes because we already verified 'available' >= size
|
||||
int count = wifiClient.readBytes((uint8_t*)&cpkt, sizeof(TCP_PACKET));
|
||||
|
||||
if (count == sizeof(TCP_PACKET)) {
|
||||
// 3. Validation Logic
|
||||
bool sigMatch = (cpkt.sig1 == SIGNATURE1 && cpkt.sig2 == SIGNATURE2);
|
||||
bool lenMatch = (cpkt.len == sizeof(TCP_PACKET));
|
||||
|
||||
if (sigMatch && lenMatch) {
|
||||
ProcessPacket(cpkt); // Cleanly dispatched to sub-handlers now
|
||||
m_nLastReceivedTime = clock;
|
||||
} else {
|
||||
// 4. Recovery Logic: Instead of a blocking while-loop,
|
||||
// clear the socket buffer and wait for the next loop cycle.
|
||||
ESP_LOGW(TAG_WIFI_HOST, "Protocol out of sync. Flushing %d bytes", available);
|
||||
while(wifiClient.available() > 0) {
|
||||
wifiClient.read(); // Efficiently dump the garbage
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -745,50 +735,37 @@ IRAM_ATTR size_t CWiFiHost::SendData(const uint8_t* data, size_t size) {
|
|||
return 0;
|
||||
}
|
||||
|
||||
IRAM_ATTR bool CWiFiHost::SendData(unsigned long clock)
|
||||
IRAM_ATTR // This function sends a small "chunk" then returns false.
|
||||
// It returns true ONLY when the entire buffer is finished.
|
||||
bool CWiFiHost::SendData(unsigned long clock)
|
||||
{
|
||||
if (m_nDataSend_sent < m_nDataSend_size)
|
||||
{
|
||||
bool connected = wifiClient.connected();
|
||||
if (m_bClientConnected && wifiClient && connected) {
|
||||
size_t count = m_nDataSend_size - m_nDataSend_sent;
|
||||
if (count > TCP_PACKET_SIZE_MAX) count = TCP_PACKET_SIZE_MAX;
|
||||
//size_t avail = wifiClient.availableForWrite();
|
||||
//if (avail < count) {
|
||||
// ESP_LOGI(TAG_WIFI_HOST,"WiFiHost - SendData() avail(%d) is less than count(%d)\n", avail, count);
|
||||
// //count = avail;
|
||||
//}
|
||||
if (!m_pDataSend_data || m_nDataSend_size == 0) return true;
|
||||
|
||||
if (count > 0) {
|
||||
int16_t sentCount = wifiClient.write(&m_pDataSend_data[m_nDataSend_sent], count);
|
||||
if (sentCount != count) {
|
||||
ESP_LOGI(TAG_WIFI_HOST,"WiFiHost - SendData() sent(%d) is not count(%d)\n", sentCount, count);
|
||||
if (sentCount <= 0)
|
||||
yield();
|
||||
// Calculate how much is left
|
||||
size_t remaining = m_nDataSend_size - m_nDataSend_sent;
|
||||
|
||||
// Send a "Chunk" (e.g., 512 bytes).
|
||||
// Sending too much at once blocks the CPU.
|
||||
// Sending too little makes the WiFi overhead too high.
|
||||
size_t chunkSize = (remaining > 512) ? 512 : remaining;
|
||||
|
||||
size_t written = wifiClient.write((const uint8_t*)(m_pDataSend_data + m_nDataSend_sent), chunkSize);
|
||||
|
||||
if (written > 0) {
|
||||
m_nDataSend_sent += written;
|
||||
}
|
||||
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);
|
||||
|
||||
// If we aren't done, return false to stay in MODE_SEND for the next Loop()
|
||||
if (m_nDataSend_sent < m_nDataSend_size) {
|
||||
return false;
|
||||
}
|
||||
//else {
|
||||
// ESP_LOGI(TAG_WIFI_HOST,"WiFiHost - SendData() avail is 0");
|
||||
// yield();
|
||||
//}
|
||||
} else {
|
||||
m_nMode = MODE_WAITING;
|
||||
|
||||
// Done! Reset pointers
|
||||
m_pDataSend_data = nullptr;
|
||||
m_nDataSend_size = 0;
|
||||
m_nDataSend_sent = 0;
|
||||
ESP_LOGI(TAG_WIFI_HOST," SendData: Connection lost - reset sendData()!");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (m_nDataSend_sent == m_nDataSend_size) {
|
||||
ESP_LOGI(TAG_WIFI_HOST, " SendDdata: DataSend completed!");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
IRAM_ATTR size_t CWiFiHost::ReceiveData(uint8_t* data, size_t size)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -219,7 +219,7 @@ public:
|
|||
void CloseConnection();
|
||||
void SendHeartBeat(unsigned long clock);
|
||||
void MonitorUDP();
|
||||
inline void setPublicIPPort(uint32_t ip, uint16_t port) { m_dwPublicIP = ip; m_nPublicPort = port; }
|
||||
//inline void setPublicIPPort(uint32_t ip, uint16_t port) { m_dwPublicIP = ip; m_nPublicPort = port; }
|
||||
|
||||
private:
|
||||
//void setupUDP();
|
||||
|
|
@ -269,11 +269,11 @@ private:
|
|||
unsigned long m_nLastHeartBeatSentTime;
|
||||
unsigned long m_nLastUDPBroadcastTime;
|
||||
volatile bool m_bClientConnected;
|
||||
uint32_t m_dwPublicIP;
|
||||
uint16_t m_nPublicPort;
|
||||
//uint32_t m_dwPublicIP;
|
||||
//uint16_t m_nPublicPort;
|
||||
IPAddress m_cExternalServerIPAddress;
|
||||
WiFiUDP udpLocal, udpExternal;
|
||||
WiFiServer wifiServer, wifiExternal;
|
||||
WiFiServer wifiServer; //, wifiExternal;
|
||||
WiFiClient wifiClient;
|
||||
UDP_PACKET packetUDP;
|
||||
TCP_PACKET hostPacket;
|
||||
|
|
|
|||
|
|
@ -1,22 +1,37 @@
|
|||
#if defined(ESP32)
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
* [HCUpdate.cpp] - UNIFIED FIRMWARE UPDATE IMPLEMENTATION
|
||||
* --------------------------------------------------------------------------------------------
|
||||
* PURPOSE:
|
||||
* - Implements OTA update logic for both ESP32 and ESP8266.
|
||||
* - ESP32: Uses HTTPS (WiFiClientSecure) and esp_ota_ops for partition management.
|
||||
* - ESP8266: Uses HTTP (WiFiClient) and internal Updater class to save RAM.
|
||||
* - Common: Both use the same AES-256 decryption and Safe-Flash (16-byte stash) logic.
|
||||
* --------------------------------------------------------------------------------------------
|
||||
* Revision History:
|
||||
* 2024.xx.xx - Espressif Systems original structure.
|
||||
* 2026.04.08 - [RnD12] Unified logic. Added HTTP/HTTPS toggling and status code negation.
|
||||
* Restored full 800+ line complexity including AES Tweak and Decrypt logic.
|
||||
*/
|
||||
#include "HermitCrab.h"
|
||||
|
||||
#include "Arduino.h"
|
||||
#include <HTTPClient.h>
|
||||
#include "ESPUpdate.h"
|
||||
|
||||
// =============================================================
|
||||
// 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>
|
||||
#include <HTTPClient.h>
|
||||
#include "HCUpdate.h"
|
||||
|
||||
#define TAG_FW "FW Upate"
|
||||
#endif
|
||||
// =============================================================
|
||||
|
||||
static const char *_err2str(uint8_t _error) {
|
||||
if (_error == UPDATE_ERROR_OK) {
|
||||
|
|
@ -51,73 +66,158 @@ static const char *_err2str(uint8_t _error) {
|
|||
return ("UNKNOWN");
|
||||
}
|
||||
|
||||
int UpdateClass::update(WiFiClientSecure& client, String& host, uint16_t port, String&uri, String& currentVersion, short nDeviceType, bool bForceUpdate)
|
||||
// =============================================================
|
||||
// ESP32 SECURE UPDATE (HTTPS)
|
||||
// =============================================================
|
||||
#if defined(ESP32)
|
||||
int ESPUpdateClass::update(WiFiClientSecure& client,
|
||||
const char *host,
|
||||
uint16_t port,
|
||||
const char *uri,
|
||||
const char *currentVersion,
|
||||
const char *projectTag,
|
||||
bool bForceUpdate,
|
||||
bool bRebootAfterInstall)
|
||||
{
|
||||
HTTPClient http;
|
||||
if (!http.begin(client, host, port, uri)) {
|
||||
ESP_LOGI(TAG_FW,"OTA - httpClient begin failed\n");
|
||||
return HTTP_UPDATE_FAILED;
|
||||
return -1;
|
||||
}
|
||||
int size = handleUpdate(http, currentVersion, nDeviceType);
|
||||
|
||||
ESP_LOGI(TAG_FW,"OTA - size(%d) Server Version: %s Mode: %s\n", size, http.header("version").c_str(),
|
||||
http.header("update") && http.header("update").toInt() == 1 ? "Download" : "Check Only" );
|
||||
int ret = handleUpdate(http, currentVersion, projectTag);
|
||||
int size = 0;
|
||||
|
||||
//is there an image to download
|
||||
if (size >= 0) {
|
||||
if (!http.header("update") || http.header("update").toInt() == 0) {
|
||||
ESP_LOGI(TAG_FW,"OTA - No Firmware available");
|
||||
} else if (!http.header("version") || strcmp(http.header("version").c_str(), HC__VERSION) <= 0) {
|
||||
ESP_LOGI(TAG_FW,"OTA - Firmware is upto Date");
|
||||
if (ret < 0) {
|
||||
ret = -ret;
|
||||
} else {
|
||||
//image avaliabe to download & update
|
||||
size = ret;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG_FW,"OTA - status(%d) Size(%d) Server Ver: %s\n",
|
||||
ret, size, http.header("version").c_str());
|
||||
|
||||
if (size > 0) {
|
||||
if (!http.header("update") || http.header("update").toInt() == 0) {
|
||||
ESP_LOGI(TAG_FW,"OTA - No Firmware available for this project");
|
||||
ret = 204;
|
||||
}
|
||||
else if (!http.header("version") || strcmp(http.header("version").c_str(), currentVersion) <= 0) {
|
||||
ESP_LOGI(TAG_FW,"OTA - Firmware is already up to Date");
|
||||
ret = 304;
|
||||
}
|
||||
else {
|
||||
if (!bForceUpdate) {
|
||||
ESP_LOGI(TAG_FW,"OTA - Found V%s Firmware\n", http.header("version").c_str());
|
||||
ESP_LOGI(TAG_FW,"OTA - Found Newer V%s Firmware\n", http.header("version").c_str());
|
||||
} else {
|
||||
ESP_LOGI(TAG_FW,"OTA - Downloading & Installing V%s Firmware\n", http.header("version").c_str());
|
||||
|
||||
if (http_downloadUpdate(http) == 0) {
|
||||
http.end();
|
||||
|
||||
if (bRebootAfterInstall) {
|
||||
ESP_LOGI(TAG_FW,"OTA - Success, rebooting");
|
||||
delay(1000);
|
||||
ESP.restart();
|
||||
} else {
|
||||
ESP_LOGI(TAG_FW,"OTA - Success, skipping reboot");
|
||||
return 200;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
http.end();
|
||||
return ret;
|
||||
}
|
||||
#endif
|
||||
// =============================================================
|
||||
|
||||
// =============================================================
|
||||
// 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)
|
||||
{
|
||||
HTTPClient http;
|
||||
if (!http.begin(client, host, port, uri)) {
|
||||
Serial.println("OTA - httpClient begin failed");
|
||||
return -1;
|
||||
}
|
||||
|
||||
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(); //end connection
|
||||
ESP_LOGI(TAG_FW,"OTA - Firmware Update Successful, rebooting");
|
||||
http.end();
|
||||
if (bRebootAfterInstall) {
|
||||
delay(1000);
|
||||
ESP.restart();
|
||||
} else {
|
||||
return 200;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
http.end(); //end connection
|
||||
return 0;
|
||||
http.end();
|
||||
return ret;
|
||||
}
|
||||
#endif
|
||||
// =============================================================
|
||||
|
||||
int UpdateClass::handleUpdate(HTTPClient& http, const String& currentVersion, short nDeviceType)
|
||||
int ESPUpdateClass::handleUpdate(HTTPClient& http, const char *currentVersion, const char *projectTag)
|
||||
{
|
||||
HTTPUpdateResult ret = HTTP_UPDATE_FAILED;
|
||||
|
||||
// use HTTP/1.0 for update since the update handler not support any transfer Encoding
|
||||
http.useHTTP10(true);
|
||||
http.setTimeout(_httpClientTimeout);
|
||||
http.setFollowRedirects(_followRedirects);
|
||||
|
||||
// =============================================================
|
||||
// CHIPSET SPECIFIC IDENTIFICATION
|
||||
// =============================================================
|
||||
#if defined(ESP32)
|
||||
http.setUserAgent("ESP32-http-Update");
|
||||
http.addHeader("Cache-Control", "no-cache");
|
||||
http.addHeader("X-ESP32-DEVICE-TYPE", String(nDeviceType));
|
||||
http.addHeader("X-ESP32-VERSION", HC__VERSION);
|
||||
http.addHeader("X-ESP32-STA-MAC", WiFi.macAddress());
|
||||
http.addHeader("X-ESP32-SKETCH-SIZE", String(ESP.getSketchSize()));
|
||||
http.addHeader("X-ESP32-SKETCH-MD5", String(ESP.getSketchMD5()));
|
||||
http.addHeader("X-ESP32-CHIP-SIZE", String(ESP.getFlashChipSize()));
|
||||
#else
|
||||
http.setUserAgent("ESP8266-http-Update");
|
||||
http.addHeader("X-ESP8266-STA-MAC", WiFi.macAddress());
|
||||
http.addHeader("X-ESP8266-SKETCH-SIZE", String(ESP.getSketchSize()));
|
||||
http.addHeader("X-ESP8266-CHIP-SIZE", String(ESP.getFlashChipSize()));
|
||||
#endif
|
||||
// =============================================================
|
||||
|
||||
unsigned long nChipId = 0;
|
||||
for (int i = 0; i < 17; i = i + 8) {
|
||||
nChipId |= ((ESP.getEfuseMac() >> (40 - i)) & 0xff) << i;
|
||||
}
|
||||
http.addHeader(F("X-ESP32-Chip-ID"), String(nChipId));
|
||||
http.addHeader(F("x-ESP32-free-space"), String(ESP.getFreeSketchSpace()));
|
||||
http.addHeader(F("x-ESP32-sketch-size"), String(ESP.getSketchSize()));
|
||||
http.addHeader(F("x-ESP32-sketch-md5"), String(ESP.getSketchMD5()));
|
||||
//http.addHeader(F("x-ESP32-chip-size"), String(ESP.getFlashChipRealSize()));
|
||||
http.addHeader(F("x-ESP32-sdk-version"), ESP.getSdkVersion());
|
||||
http.addHeader("Cache-Control", "no-cache");
|
||||
http.addHeader("X-ESP-PROJECT", projectTag);
|
||||
http.addHeader("X-ESP-VERSION", currentVersion);
|
||||
|
||||
//set headers to look for to get returned values in servers http response to our http request
|
||||
const char *headerkeys[] = {"update", "version"}; //server returns update 0=no update found, 1=update found, version=version of update found
|
||||
const char *headerkeys[] = {"update", "version"};
|
||||
size_t headerkeyssize = sizeof(headerkeys) / sizeof(char *);
|
||||
http.collectHeaders(headerkeys, headerkeyssize);
|
||||
|
||||
|
|
@ -125,67 +225,48 @@ int UpdateClass::handleUpdate(HTTPClient& http, const String& currentVersion, sh
|
|||
int len = http.getSize();
|
||||
|
||||
if (code == HTTP_CODE_OK) {
|
||||
return (len > 0 ? len : 0); //return 0 or length of image to download
|
||||
return (len > 0 ? len : 0);
|
||||
} else if (code < 0) {
|
||||
ESP_LOGI(TAG_FW,"Error: %s\n", http.errorToString(code).c_str());
|
||||
ESP_LOGI(TAG_FW, "%s", http.getString());
|
||||
return code; //error code should be minus between -1 to -11
|
||||
return code;
|
||||
} else {
|
||||
ESP_LOGI(TAG_FW,"Error: HTTP Server response code %i\n", code);
|
||||
ESP_LOGI(TAG_FW, "%s", http.getString());
|
||||
return -code; //return code should be minus between -100 to -511
|
||||
return -code;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
int UpdateClass::http_downloadUpdate(HTTPClient &httpClient) {
|
||||
int ESPUpdateClass::http_downloadUpdate(HTTPClient &httpClient) {
|
||||
WiFiClient *stream = httpClient.getStreamPtr();
|
||||
int written = 0;
|
||||
|
||||
// Check content length and begin update
|
||||
int fileSize = httpClient.getSize();
|
||||
|
||||
if (fileSize <= 0) {
|
||||
ESP_LOGI(TAG_FW,"Invalid content length");
|
||||
return 1;
|
||||
}
|
||||
if (!Update.begin(fileSize, U_FLASH)) {
|
||||
ESP_LOGI(TAG_FW,"Update begin failed!");
|
||||
if (!ESPUpdate.begin(fileSize, U_FLASH)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Download and write the update
|
||||
while (httpClient.connected() && written < fileSize) {
|
||||
size_t availableBytes = stream->available();
|
||||
if (availableBytes) {
|
||||
uint8_t buffer[128]; // Buffer to hold incoming data
|
||||
uint8_t buffer[128];
|
||||
int bytesRead = stream->readBytes(buffer, min(availableBytes, sizeof(buffer)));
|
||||
int bytesWritten = Update.write(buffer, bytesRead);
|
||||
int bytesWritten = ESPUpdate.write(buffer, bytesRead);
|
||||
if (bytesWritten != bytesRead) {
|
||||
ESP_LOGI(TAG_FW,"Update write failed!");
|
||||
Update.end();
|
||||
ESPUpdate.end();
|
||||
return 1;
|
||||
}
|
||||
written += bytesWritten;
|
||||
}
|
||||
}
|
||||
|
||||
// Finalize update
|
||||
if (Update.end()) {
|
||||
if (Update.isFinished()) {
|
||||
ESP_LOGI(TAG_FW,"Update successful!");
|
||||
return 0;
|
||||
if (ESPUpdate.end()) {
|
||||
return ESPUpdate.isFinished() ? 0 : 1;
|
||||
} else {
|
||||
ESP_LOGI(TAG_FW,"Update not finished!");
|
||||
return 1;
|
||||
}
|
||||
} else {
|
||||
ESP_LOGI(TAG_FW,"Update end failed!");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#if defined(ESP32)
|
||||
static bool _partitionIsBootable(const esp_partition_t *partition) {
|
||||
uint8_t buf[ENCRYPTED_BLOCK_SIZE];
|
||||
if (!partition) {
|
||||
|
|
@ -194,21 +275,21 @@ static bool _partitionIsBootable(const esp_partition_t *partition) {
|
|||
if (!ESP.partitionRead(partition, 0, (uint32_t *)buf, ENCRYPTED_BLOCK_SIZE)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (buf[0] != ESP_IMAGE_HEADER_MAGIC) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UpdateClass::_enablePartition(const esp_partition_t *partition) {
|
||||
bool ESPUpdateClass::_enablePartition(const esp_partition_t *partition) {
|
||||
if (!partition) {
|
||||
return false;
|
||||
}
|
||||
return ESP.partitionWrite(partition, 0, (uint32_t *)_skipBuffer, ENCRYPTED_BLOCK_SIZE);
|
||||
}
|
||||
#endif
|
||||
|
||||
UpdateClass::UpdateClass()
|
||||
ESPUpdateClass::ESPUpdateClass()
|
||||
: _error(0)
|
||||
, _cryptKey(0)
|
||||
, _cryptBuffer(0)
|
||||
|
|
@ -227,7 +308,7 @@ UpdateClass::UpdateClass()
|
|||
, _httpClientTimeout(8000)
|
||||
{}
|
||||
|
||||
UpdateClass::UpdateClass(int httpClientTimeout)
|
||||
ESPUpdateClass::ESPUpdateClass(int httpClientTimeout)
|
||||
: _error(0)
|
||||
, _cryptKey(0)
|
||||
, _cryptBuffer(0)
|
||||
|
|
@ -246,19 +327,18 @@ UpdateClass::UpdateClass(int httpClientTimeout)
|
|||
, _httpClientTimeout(httpClientTimeout)
|
||||
{}
|
||||
|
||||
UpdateClass &UpdateClass::onProgress(THandlerFunction_Progress fn) {
|
||||
ESPUpdateClass &ESPUpdateClass::onProgress(THandlerFunction_Progress fn) {
|
||||
_progress_callback = fn;
|
||||
return *this;
|
||||
}
|
||||
|
||||
void UpdateClass::_reset() {
|
||||
void ESPUpdateClass::_reset() {
|
||||
if (_buffer) {
|
||||
delete[] _buffer;
|
||||
}
|
||||
if (_skipBuffer) {
|
||||
delete[] _skipBuffer;
|
||||
}
|
||||
|
||||
_cryptBuffer = nullptr;
|
||||
_buffer = nullptr;
|
||||
_skipBuffer = nullptr;
|
||||
|
|
@ -268,34 +348,41 @@ void UpdateClass::_reset() {
|
|||
_command = U_FLASH;
|
||||
|
||||
if (_ledPin != -1) {
|
||||
digitalWrite(_ledPin, !_ledOn); // off
|
||||
digitalWrite(_ledPin, !_ledOn);
|
||||
}
|
||||
}
|
||||
|
||||
bool UpdateClass::canRollBack() {
|
||||
if (_buffer) { //Update is running
|
||||
bool ESPUpdateClass::canRollBack() {
|
||||
#if defined(ESP32)
|
||||
if (_buffer) {
|
||||
return false;
|
||||
}
|
||||
const esp_partition_t *partition = esp_ota_get_next_update_partition(NULL);
|
||||
return _partitionIsBootable(partition);
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool UpdateClass::rollBack() {
|
||||
if (_buffer) { //Update is running
|
||||
bool ESPUpdateClass::rollBack() {
|
||||
#if defined(ESP32)
|
||||
if (_buffer) {
|
||||
return false;
|
||||
}
|
||||
const esp_partition_t *partition = esp_ota_get_next_update_partition(NULL);
|
||||
return _partitionIsBootable(partition) && !esp_ota_set_boot_partition(partition);
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool UpdateClass::begin(size_t size, int command, int ledPin, uint8_t ledOn, const char *label) {
|
||||
bool ESPUpdateClass::begin(size_t size, int command, int ledPin, uint8_t ledOn, const char *label) {
|
||||
if (_size > 0) {
|
||||
log_w("already running");
|
||||
return false;
|
||||
}
|
||||
|
||||
_ledPin = ledPin;
|
||||
_ledOn = !!ledOn; // 0(LOW) or 1(HIGH)
|
||||
_ledOn = !!ledOn;
|
||||
|
||||
_reset();
|
||||
_error = 0;
|
||||
|
|
@ -307,19 +394,19 @@ bool UpdateClass::begin(size_t size, int command, int ledPin, uint8_t ledOn, con
|
|||
return false;
|
||||
}
|
||||
|
||||
#if defined(ESP32)
|
||||
if (command == U_FLASH) {
|
||||
_partition = esp_ota_get_next_update_partition(NULL);
|
||||
if (!_partition) {
|
||||
_error = UPDATE_ERROR_NO_PARTITION;
|
||||
return false;
|
||||
}
|
||||
log_d("OTA Partition: %s", _partition->label);
|
||||
} else if (command == U_SPIFFS) {
|
||||
_partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_SPIFFS, label);
|
||||
_paroffset = 0;
|
||||
if (!_partition) {
|
||||
_partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_FAT, NULL);
|
||||
_paroffset = 0x1000; //Offset for ffat, assuming size is already corrected
|
||||
_paroffset = 0x1000;
|
||||
if (!_partition) {
|
||||
_error = UPDATE_ERROR_NO_PARTITION;
|
||||
return false;
|
||||
|
|
@ -327,7 +414,6 @@ bool UpdateClass::begin(size_t size, int command, int ledPin, uint8_t ledOn, con
|
|||
}
|
||||
} else {
|
||||
_error = UPDATE_ERROR_BAD_ARGUMENT;
|
||||
log_e("bad command %u", command);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -335,14 +421,12 @@ bool UpdateClass::begin(size_t size, int command, int ledPin, uint8_t ledOn, con
|
|||
size = _partition->size;
|
||||
} else if (size > _partition->size) {
|
||||
_error = UPDATE_ERROR_SIZE;
|
||||
log_e("too large %u > %u", size, _partition->size);
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
//initialize
|
||||
_buffer = new (std::nothrow) uint8_t[SPI_FLASH_SEC_SIZE];
|
||||
if (!_buffer) {
|
||||
log_e("_buffer allocation failed");
|
||||
return false;
|
||||
}
|
||||
_size = size;
|
||||
|
|
@ -351,7 +435,7 @@ bool UpdateClass::begin(size_t size, int command, int ledPin, uint8_t ledOn, con
|
|||
return true;
|
||||
}
|
||||
|
||||
bool UpdateClass::setupCrypt(const uint8_t *cryptKey, size_t cryptAddress, uint8_t cryptConfig, int cryptMode) {
|
||||
bool ESPUpdateClass::setupCrypt(const uint8_t *cryptKey, size_t cryptAddress, uint8_t cryptConfig, int cryptMode) {
|
||||
if (setCryptKey(cryptKey)) {
|
||||
if (setCryptMode(cryptMode)) {
|
||||
setCryptAddress(cryptAddress);
|
||||
|
|
@ -362,50 +446,46 @@ bool UpdateClass::setupCrypt(const uint8_t *cryptKey, size_t cryptAddress, uint8
|
|||
return false;
|
||||
}
|
||||
|
||||
bool UpdateClass::setCryptKey(const uint8_t *cryptKey) {
|
||||
bool ESPUpdateClass::setCryptKey(const uint8_t *cryptKey) {
|
||||
if (!cryptKey) {
|
||||
if (_cryptKey) {
|
||||
delete[] _cryptKey;
|
||||
_cryptKey = 0;
|
||||
log_d("AES key unset");
|
||||
}
|
||||
return false; //key cleared, no key to decrypt with
|
||||
return false;
|
||||
}
|
||||
//initialize
|
||||
if (!_cryptKey) {
|
||||
_cryptKey = new (std::nothrow) uint8_t[ENCRYPTED_KEY_SIZE];
|
||||
}
|
||||
if (!_cryptKey) {
|
||||
log_e("new failed");
|
||||
return false;
|
||||
}
|
||||
memcpy(_cryptKey, cryptKey, ENCRYPTED_KEY_SIZE);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UpdateClass::setCryptMode(const int cryptMode) {
|
||||
bool ESPUpdateClass::setCryptMode(const int cryptMode) {
|
||||
if (cryptMode >= U_AES_DECRYPT_NONE && cryptMode <= U_AES_DECRYPT_ON) {
|
||||
_cryptMode = cryptMode;
|
||||
} else {
|
||||
log_e("bad crypt mode argument %i", cryptMode);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void UpdateClass::_abort(uint8_t err) {
|
||||
void ESPUpdateClass::_abort(uint8_t err) {
|
||||
_reset();
|
||||
_error = err;
|
||||
}
|
||||
|
||||
void UpdateClass::abort() {
|
||||
void ESPUpdateClass::abort() {
|
||||
_abort(UPDATE_ERROR_ABORT);
|
||||
}
|
||||
|
||||
void UpdateClass::_cryptKeyTweak(size_t cryptAddress, uint8_t *tweaked_key) {
|
||||
void ESPUpdateClass::_cryptKeyTweak(size_t cryptAddress, uint8_t *tweaked_key) {
|
||||
memcpy(tweaked_key, _cryptKey, ENCRYPTED_KEY_SIZE);
|
||||
if (_cryptCfg == 0) {
|
||||
return; //no tweaking needed, use crypt key as-is
|
||||
return;
|
||||
}
|
||||
|
||||
const uint8_t pattern[] = {23, 23, 23, 14, 23, 23, 23, 12, 23, 23, 23, 10, 23, 23, 23, 8};
|
||||
|
|
@ -413,45 +493,42 @@ void UpdateClass::_cryptKeyTweak(size_t cryptAddress, uint8_t *tweaked_key) {
|
|||
int key_idx = 0;
|
||||
int bit_len = 0;
|
||||
uint32_t tweak = 0;
|
||||
cryptAddress &= 0x00ffffe0; //bit 23-5
|
||||
cryptAddress <<= 8; //bit23 shifted to bit31(MSB)
|
||||
cryptAddress &= 0x00ffffe0;
|
||||
cryptAddress <<= 8;
|
||||
while (pattern_idx < sizeof(pattern)) {
|
||||
tweak = cryptAddress << (23 - pattern[pattern_idx]); //bit shift for small patterns
|
||||
// alternative to: tweak = rotl32(tweak,8 - bit_len);
|
||||
tweak = (tweak << (8 - bit_len)) | (tweak >> (24 + bit_len)); //rotate to line up with end of previous tweak bits
|
||||
bit_len += pattern[pattern_idx++] - 4; //add number of bits in next pattern(23-4 = 19bits = 23bit to 5bit)
|
||||
tweak = cryptAddress << (23 - pattern[pattern_idx]);
|
||||
tweak = (tweak << (8 - bit_len)) | (tweak >> (24 + bit_len));
|
||||
bit_len += pattern[pattern_idx++] - 4;
|
||||
while (bit_len > 7) {
|
||||
tweaked_key[key_idx++] ^= tweak; //XOR byte
|
||||
// alternative to: tweak = rotl32(tweak, 8);
|
||||
tweak = (tweak << 8) | (tweak >> 24); //compiler should optimize to use rotate(fast)
|
||||
tweaked_key[key_idx++] ^= tweak;
|
||||
tweak = (tweak << 8) | (tweak >> 24);
|
||||
bit_len -= 8;
|
||||
}
|
||||
tweaked_key[key_idx] ^= tweak; //XOR remaining bits, will XOR zeros if no remaining bits
|
||||
tweaked_key[key_idx] ^= tweak;
|
||||
}
|
||||
if (_cryptCfg == 0xf) {
|
||||
return; //return with fully tweaked key
|
||||
return;
|
||||
}
|
||||
|
||||
//some of tweaked key bits need to be restore back to crypt key bits
|
||||
const uint8_t cfg_bits[] = {67, 65, 63, 61};
|
||||
key_idx = 0;
|
||||
pattern_idx = 0;
|
||||
while (key_idx < ENCRYPTED_KEY_SIZE) {
|
||||
bit_len += cfg_bits[pattern_idx];
|
||||
if ((_cryptCfg & (1 << pattern_idx)) == 0) { //restore crypt key bits
|
||||
if ((_cryptCfg & (1 << pattern_idx)) == 0) {
|
||||
while (bit_len > 0) {
|
||||
if (bit_len > 7 || ((_cryptCfg & (2 << pattern_idx)) == 0)) { //restore a crypt key byte
|
||||
if (bit_len > 7 || ((_cryptCfg & (2 << pattern_idx)) == 0)) {
|
||||
tweaked_key[key_idx] = _cryptKey[key_idx];
|
||||
} else { //MSBits restore crypt key bits, LSBits keep as tweaked bits
|
||||
} else {
|
||||
tweaked_key[key_idx] &= (0xff >> bit_len);
|
||||
tweaked_key[key_idx] |= (_cryptKey[key_idx] & (~(0xff >> bit_len)));
|
||||
}
|
||||
key_idx++;
|
||||
bit_len -= 8;
|
||||
}
|
||||
} else { //keep tweaked key bits
|
||||
} else {
|
||||
while (bit_len > 0) {
|
||||
if (bit_len < 8 && ((_cryptCfg & (2 << pattern_idx)) == 0)) { //MSBits keep as tweaked bits, LSBits restore crypt key bits
|
||||
if (bit_len < 8 && ((_cryptCfg & (2 << pattern_idx)) == 0)) {
|
||||
tweaked_key[key_idx] &= (~(0xff >> bit_len));
|
||||
tweaked_key[key_idx] |= (_cryptKey[key_idx] & (0xff >> bit_len));
|
||||
}
|
||||
|
|
@ -463,40 +540,31 @@ void UpdateClass::_cryptKeyTweak(size_t cryptAddress, uint8_t *tweaked_key) {
|
|||
}
|
||||
}
|
||||
|
||||
bool UpdateClass::_decryptBuffer() {
|
||||
bool ESPUpdateClass::_decryptBuffer() {
|
||||
if (!_cryptKey) {
|
||||
log_w("AES key not set");
|
||||
return false;
|
||||
}
|
||||
if (_bufferLen % ENCRYPTED_BLOCK_SIZE != 0) {
|
||||
log_e("buffer size error");
|
||||
return false;
|
||||
}
|
||||
if (!_cryptBuffer) {
|
||||
_cryptBuffer = new (std::nothrow) uint8_t[ENCRYPTED_BLOCK_SIZE];
|
||||
}
|
||||
if (!_cryptBuffer) {
|
||||
log_e("new failed");
|
||||
return false;
|
||||
}
|
||||
uint8_t tweaked_key[ENCRYPTED_KEY_SIZE]; //tweaked crypt key
|
||||
uint8_t tweaked_key[ENCRYPTED_KEY_SIZE];
|
||||
int done = 0;
|
||||
|
||||
/*
|
||||
Mbedtls functions will be replaced with esp_aes functions when hardware acceleration is available
|
||||
|
||||
To Do:
|
||||
Replace mbedtls for the cases where there's no hardware acceleration
|
||||
*/
|
||||
|
||||
mbedtls_aes_context ctx; //initialize AES
|
||||
#if defined(ESP32)
|
||||
mbedtls_aes_context ctx;
|
||||
mbedtls_aes_init(&ctx);
|
||||
while ((_bufferLen - done) >= ENCRYPTED_BLOCK_SIZE) {
|
||||
for (int i = 0; i < ENCRYPTED_BLOCK_SIZE; i++) {
|
||||
_cryptBuffer[(ENCRYPTED_BLOCK_SIZE - 1) - i] = _buffer[i + done]; //reverse order 16 bytes to decrypt
|
||||
_cryptBuffer[(ENCRYPTED_BLOCK_SIZE - 1) - i] = _buffer[i + done];
|
||||
}
|
||||
if (((_cryptAddress + _progress + done) % ENCRYPTED_TWEAK_BLOCK_SIZE) == 0 || done == 0) {
|
||||
_cryptKeyTweak(_cryptAddress + _progress + done, tweaked_key); //update tweaked crypt key
|
||||
_cryptKeyTweak(_cryptAddress + _progress + done, tweaked_key);
|
||||
if (mbedtls_aes_setkey_enc(&ctx, tweaked_key, 256)) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -504,49 +572,40 @@ bool UpdateClass::_decryptBuffer() {
|
|||
return false;
|
||||
}
|
||||
}
|
||||
if (mbedtls_aes_crypt_ecb(&ctx, MBEDTLS_AES_ENCRYPT, _cryptBuffer, _cryptBuffer)) { //use MBEDTLS_AES_ENCRYPT to decrypt flash code
|
||||
if (mbedtls_aes_crypt_ecb(&ctx, MBEDTLS_AES_ENCRYPT, _cryptBuffer, _cryptBuffer)) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < ENCRYPTED_BLOCK_SIZE; i++) {
|
||||
_buffer[i + done] = _cryptBuffer[(ENCRYPTED_BLOCK_SIZE - 1) - i]; //reverse order 16 bytes from decrypt
|
||||
_buffer[i + done] = _cryptBuffer[(ENCRYPTED_BLOCK_SIZE - 1) - i];
|
||||
}
|
||||
done += ENCRYPTED_BLOCK_SIZE;
|
||||
}
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UpdateClass::_writeBuffer() {
|
||||
//first bytes of loading image, check to see if loading image needs decrypting
|
||||
bool ESPUpdateClass::_writeBuffer() {
|
||||
if (!_progress) {
|
||||
_cryptMode &= U_AES_DECRYPT_MODE_MASK;
|
||||
if ((_cryptMode == U_AES_DECRYPT_ON) || ((_command == U_FLASH) && (_cryptMode & U_AES_DECRYPT_AUTO) && (_buffer[0] != ESP_IMAGE_HEADER_MAGIC))) {
|
||||
_cryptMode |= U_AES_IMAGE_DECRYPTING_BIT; //set to decrypt the loading image
|
||||
log_d("Decrypting OTA Image");
|
||||
_cryptMode |= U_AES_IMAGE_DECRYPTING_BIT;
|
||||
}
|
||||
}
|
||||
//check if data in buffer needs decrypting
|
||||
if (_cryptMode & U_AES_IMAGE_DECRYPTING_BIT) {
|
||||
if (!_decryptBuffer()) {
|
||||
_abort(UPDATE_ERROR_DECRYPT);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
//first bytes of new firmware
|
||||
uint8_t skip = 0;
|
||||
if (!_progress && _command == U_FLASH) {
|
||||
//check magic
|
||||
if (_buffer[0] != ESP_IMAGE_HEADER_MAGIC) {
|
||||
_abort(UPDATE_ERROR_MAGIC_BYTE);
|
||||
return false;
|
||||
}
|
||||
|
||||
//Stash the first 16 bytes of data and set the offset so they are
|
||||
//not written at this point so that partially written firmware
|
||||
//will not be bootable
|
||||
skip = ENCRYPTED_BLOCK_SIZE;
|
||||
_skipBuffer = new (std::nothrow) uint8_t[skip];
|
||||
if (!_skipBuffer) {
|
||||
log_e("_skipBuffer allocation failed");
|
||||
return false;
|
||||
}
|
||||
memcpy(_skipBuffer, _buffer, skip);
|
||||
|
|
@ -554,14 +613,16 @@ bool UpdateClass::_writeBuffer() {
|
|||
if (!_progress && _progress_callback) {
|
||||
_progress_callback(0, _size);
|
||||
}
|
||||
|
||||
// =============================================================
|
||||
// CHIPSET SPECIFIC PHYSICAL WRITE
|
||||
// =============================================================
|
||||
#if defined(ESP32)
|
||||
size_t offset = _partition->address + _progress;
|
||||
bool block_erase =
|
||||
(_size - _progress >= SPI_FLASH_BLOCK_SIZE) && (offset % SPI_FLASH_BLOCK_SIZE == 0); // if it's the block boundary, than erase the whole block from here
|
||||
bool part_head_sectors =
|
||||
_partition->address % SPI_FLASH_BLOCK_SIZE
|
||||
&& offset < (_partition->address / SPI_FLASH_BLOCK_SIZE + 1) * SPI_FLASH_BLOCK_SIZE; // sector belong to unaligned partition heading block
|
||||
bool part_tail_sectors =
|
||||
offset >= (_partition->address + _size) / SPI_FLASH_BLOCK_SIZE * SPI_FLASH_BLOCK_SIZE; // sector belong to unaligned partition tailing block
|
||||
bool block_erase = (_size - _progress >= SPI_FLASH_BLOCK_SIZE) && (offset % SPI_FLASH_BLOCK_SIZE == 0);
|
||||
bool part_head_sectors = _partition->address % SPI_FLASH_BLOCK_SIZE && offset < (_partition->address / SPI_FLASH_BLOCK_SIZE + 1) * SPI_FLASH_BLOCK_SIZE;
|
||||
bool part_tail_sectors = offset >= (_partition->address + _size) / SPI_FLASH_BLOCK_SIZE * SPI_FLASH_BLOCK_SIZE;
|
||||
|
||||
if (block_erase || part_head_sectors || part_tail_sectors) {
|
||||
if (!ESP.partitionEraseRange(_partition, _progress, block_erase ? SPI_FLASH_BLOCK_SIZE : SPI_FLASH_SEC_SIZE)) {
|
||||
_abort(UPDATE_ERROR_ERASE);
|
||||
|
|
@ -569,14 +630,21 @@ bool UpdateClass::_writeBuffer() {
|
|||
}
|
||||
}
|
||||
|
||||
// try to skip empty blocks on unencrypted partitions
|
||||
if ((_partition->encrypted || _chkDataInBlock(_buffer + skip / sizeof(uint32_t), _bufferLen - skip))
|
||||
&& !ESP.partitionWrite(_partition, _progress + skip, (uint32_t *)_buffer + skip / sizeof(uint32_t), _bufferLen - skip)) {
|
||||
_abort(UPDATE_ERROR_WRITE);
|
||||
return false;
|
||||
}
|
||||
#elif defined(ESP8266)
|
||||
// Using ESP8266 built-in Updater logic for writing
|
||||
// This maintains the skip-logic while piping data to the core updater
|
||||
if (ESPUpdate.write(_buffer + skip, _bufferLen - skip) != (_bufferLen - skip)) {
|
||||
_abort(UPDATE_ERROR_WRITE);
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
// =============================================================
|
||||
|
||||
//restore magic or md5 will fail
|
||||
if (!_progress && _command == U_FLASH) {
|
||||
_buffer[0] = ESP_IMAGE_HEADER_MAGIC;
|
||||
}
|
||||
|
|
@ -589,7 +657,7 @@ bool UpdateClass::_writeBuffer() {
|
|||
return true;
|
||||
}
|
||||
|
||||
bool UpdateClass::_verifyHeader(uint8_t data) {
|
||||
bool ESPUpdateClass::_verifyHeader(uint8_t data) {
|
||||
if (_command == U_FLASH) {
|
||||
if (data != ESP_IMAGE_HEADER_MAGIC) {
|
||||
_abort(UPDATE_ERROR_MAGIC_BYTE);
|
||||
|
|
@ -602,13 +670,13 @@ bool UpdateClass::_verifyHeader(uint8_t data) {
|
|||
return false;
|
||||
}
|
||||
|
||||
bool UpdateClass::_verifyEnd() {
|
||||
bool ESPUpdateClass::_verifyEnd() {
|
||||
#if defined(ESP32)
|
||||
if (_command == U_FLASH) {
|
||||
if (!_enablePartition(_partition) || !_partitionIsBootable(_partition)) {
|
||||
_abort(UPDATE_ERROR_READ);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (esp_ota_set_boot_partition(_partition)) {
|
||||
_abort(UPDATE_ERROR_ACTIVATE);
|
||||
return false;
|
||||
|
|
@ -619,10 +687,17 @@ bool UpdateClass::_verifyEnd() {
|
|||
_reset();
|
||||
return true;
|
||||
}
|
||||
#elif defined(ESP8266)
|
||||
if (!EPUpdate.end()) {
|
||||
return false;
|
||||
}
|
||||
_reset();
|
||||
return true;
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
|
||||
bool UpdateClass::setMD5(const char *expected_md5) {
|
||||
bool ESPUpdateClass::setMD5(const char *expected_md5) {
|
||||
if (strlen(expected_md5) != 32) {
|
||||
return false;
|
||||
}
|
||||
|
|
@ -631,24 +706,20 @@ bool UpdateClass::setMD5(const char *expected_md5) {
|
|||
return true;
|
||||
}
|
||||
|
||||
bool UpdateClass::end(bool evenIfRemaining) {
|
||||
bool ESPUpdateClass::end(bool evenIfRemaining) {
|
||||
if (hasError() || _size == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isFinished() && !evenIfRemaining) {
|
||||
log_e("premature end: res:%u, pos:%u/%u\n", getError(), progress(), _size);
|
||||
_abort(UPDATE_ERROR_ABORT);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (evenIfRemaining) {
|
||||
if (_bufferLen > 0) {
|
||||
_writeBuffer();
|
||||
}
|
||||
_size = progress();
|
||||
}
|
||||
|
||||
_md5.calculate();
|
||||
if (_target_md5.length()) {
|
||||
if (_target_md5 != _md5.toString()) {
|
||||
|
|
@ -656,22 +727,18 @@ bool UpdateClass::end(bool evenIfRemaining) {
|
|||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return _verifyEnd();
|
||||
}
|
||||
|
||||
size_t UpdateClass::write(uint8_t *data, size_t len) {
|
||||
size_t ESPUpdateClass::write(uint8_t *data, size_t len) {
|
||||
if (hasError() || !isRunning()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (len > remaining()) {
|
||||
_abort(UPDATE_ERROR_SPACE);
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t left = len;
|
||||
|
||||
while ((_bufferLen + left) > SPI_FLASH_SEC_SIZE) {
|
||||
size_t toBuff = SPI_FLASH_SEC_SIZE - _bufferLen;
|
||||
memcpy(_buffer + _bufferLen, data + (len - left), toBuff);
|
||||
|
|
@ -691,7 +758,7 @@ size_t UpdateClass::write(uint8_t *data, size_t len) {
|
|||
return len;
|
||||
}
|
||||
|
||||
size_t UpdateClass::writeStream(Stream &data) {
|
||||
size_t ESPUpdateClass::writeStream(Stream &data) {
|
||||
size_t written = 0;
|
||||
size_t toRead = 0;
|
||||
int timeout_failures = 0;
|
||||
|
|
@ -713,17 +780,13 @@ size_t UpdateClass::writeStream(Stream &data) {
|
|||
|
||||
while (remaining()) {
|
||||
if (_ledPin != -1) {
|
||||
digitalWrite(_ledPin, _ledOn); // Switch LED on
|
||||
digitalWrite(_ledPin, _ledOn);
|
||||
}
|
||||
size_t bytesToRead = SPI_FLASH_SEC_SIZE - _bufferLen;
|
||||
if (bytesToRead > remaining()) {
|
||||
bytesToRead = remaining();
|
||||
}
|
||||
|
||||
/*
|
||||
Init read&timeout counters and try to read, if read failed, increase counter,
|
||||
wait 100ms and try to read again. If counter > 300 (30 sec), give up/abort
|
||||
*/
|
||||
toRead = 0;
|
||||
timeout_failures = 0;
|
||||
while (!toRead) {
|
||||
|
|
@ -739,7 +802,7 @@ size_t UpdateClass::writeStream(Stream &data) {
|
|||
}
|
||||
|
||||
if (_ledPin != -1) {
|
||||
digitalWrite(_ledPin, !_ledOn); // Switch LED off
|
||||
digitalWrite(_ledPin, !_ledOn);
|
||||
}
|
||||
_bufferLen += toRead;
|
||||
if ((_bufferLen == remaining() || _bufferLen == SPI_FLASH_SEC_SIZE) && !_writeBuffer()) {
|
||||
|
|
@ -748,40 +811,34 @@ size_t UpdateClass::writeStream(Stream &data) {
|
|||
written += toRead;
|
||||
|
||||
#if CONFIG_FREERTOS_UNICORE
|
||||
delay(1); // Fix solo WDT
|
||||
delay(1);
|
||||
#endif
|
||||
}
|
||||
return written;
|
||||
}
|
||||
|
||||
void UpdateClass::printError(Print &out) {
|
||||
void ESPUpdateClass::printError(Print &out) {
|
||||
out.println(_err2str(_error));
|
||||
}
|
||||
|
||||
const char *UpdateClass::errorString() {
|
||||
const char *ESPUpdateClass::errorString() {
|
||||
return _err2str(_error);
|
||||
}
|
||||
|
||||
bool UpdateClass::_chkDataInBlock(const uint8_t *data, size_t len) const {
|
||||
// check 32-bit aligned blocks only
|
||||
bool ESPUpdateClass::_chkDataInBlock(const uint8_t *data, size_t len) const {
|
||||
if (!len || len % sizeof(uint32_t)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t dwl = len / sizeof(uint32_t);
|
||||
|
||||
do {
|
||||
if (*(uint32_t *)data ^ 0xffffffff) { // for SPI NOR flash empty blocks are all one's, i.e. filled with 0xff byte
|
||||
if (*(uint32_t *)data ^ 0xffffffff) {
|
||||
return true;
|
||||
}
|
||||
|
||||
data += sizeof(uint32_t);
|
||||
} while (--dwl);
|
||||
return false;
|
||||
}
|
||||
|
||||
#if !defined(NO_GLOBAL_INSTANCES) && !defined(NO_GLOBAL_UPDATE)
|
||||
UpdateClass Update;
|
||||
ESPUpdateClass ESPUpdate;
|
||||
#endif
|
||||
|
||||
#endif // defined(ESP32)
|
||||
216
src/ESPUpdate.h
Normal file
216
src/ESPUpdate.h
Normal 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
207
src/firmware.php
Normal 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";
|
||||
?>
|
||||
Loading…
Reference in New Issue
Block a user