diff --git a/README.md b/README.md index 76a551f..42ca354 100644 --- a/README.md +++ b/README.md @@ -410,7 +410,7 @@ Note that the following options can break Nuki Hub and cause bootloops that will - Char buffer size (min 4096, max 65536): Set the character buffer size, needs to be enlarged to support large amounts of auth/keypad/timecontrol/authorization entries. Default 4096. - Task size Network (min 12288, max 65536): Set the Network task stack size, needs to be enlarged to support large amounts of auth/keypad/timecontrol/authorization entries. Default 12288. - Task size Nuki (min 8192, max 65536): Set the Nuki task stack size. Default 8192. -- BLE General timeout in ms (min 3000, max 65536): General timeout for communication with Nuki devices, default 3000ms. Mainly used when retrieving Nuki keypad authorizations +- BLE General timeout in ms (min 3000, max 65536): General timeout for communication with Nuki devices, default 10000ms. Mainly used when retrieving Nuki keypad authorizations - BLE Command timeout in ms (min 3000, max 65536): Command timeout for communication with Nuki devices, default 3000ms. - Max auth log entries (min 1, max 100): The maximum amount of log entries that will be requested from the lock/opener, default 5. - Max keypad entries (min 1, max 200): The maximum amount of keypad codes that will be requested from the lock/opener, default 10. diff --git a/apply_patches.py b/apply_patches.py new file mode 100644 index 0000000..b70dff8 --- /dev/null +++ b/apply_patches.py @@ -0,0 +1,29 @@ +from os.path import join, isfile + +Import("env") + +FRAMEWORK_DIR = env.PioPlatform().get_package_dir("framework-arduinoespressif32") +patchflag_path = join(FRAMEWORK_DIR, ".hosted-patching-done") + +# patch file only if we didn't do it before +if not isfile(join(FRAMEWORK_DIR, ".hosted-patching-done")): + original_file = join(FRAMEWORK_DIR, "cores", "esp32", "esp32-hal-hosted.c") + patched_file = join("resources", "esp32-hal-hosted.c.patch") + + assert isfile(original_file) and isfile(patched_file) + + env.Execute("patch %s %s" % (original_file, patched_file)) + # env.Execute("touch " + patchflag_path) + + original_file = join(FRAMEWORK_DIR, "cores", "esp32", "esp32-hal-hosted.h") + patched_file = join("resources", "esp32-hal-hosted.h.patch") + + assert isfile(original_file) and isfile(patched_file) + + env.Execute("patch %s %s" % (original_file, patched_file)) + + def _touch(path): + with open(path, "w") as fp: + fp.write("") + + env.Execute(lambda *args, **kwargs: _touch(patchflag_path)) \ No newline at end of file diff --git a/lib/nuki_ble b/lib/nuki_ble index b04185e..b70c93e 160000 --- a/lib/nuki_ble +++ b/lib/nuki_ble @@ -1 +1 @@ -Subproject commit b04185e0f27f4fc38a27a7d2e95f60620f8d1bb1 +Subproject commit b70c93e12053b1aa5fedc1cdd9f9141730f29238 diff --git a/platformio.ini b/platformio.ini index b7ca22a..d6e435b 100644 --- a/platformio.ini +++ b/platformio.ini @@ -13,7 +13,7 @@ default_envs = esp32 boards_dir = boards [env] -platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.33/platform-espressif32.zip +platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.34/platform-espressif32.zip platform_packages = framework = arduino, espidf build_type = release @@ -188,6 +188,10 @@ build_flags = extends = env:esp32 board_build.embed_txtfiles = board = esp32-p4 +extra_scripts = + pre:pio_package_pre.py + #pre:apply_patches.py + post:pio_package_post.py board_build.cmake_extra_args = -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.release.defaults;sdkconfig.ramoptimize.defaults;sdkconfig.defaults.esp32-p4" custom_component_remove = diff --git a/src/Config.h b/src/Config.h index 97675f1..246e9a9 100644 --- a/src/Config.h +++ b/src/Config.h @@ -5,8 +5,8 @@ #define NUKI_HUB_VERSION "9.14" #define NUKI_HUB_VERSION_INT (uint32_t)914 #define NUKI_HUB_BUILD "unknownbuildnr" -#define NUKI_HUB_DATE "2025-10-17" -#define NUKI_HUB_DATE "2025-10-17" +#define NUKI_HUB_DATE "2025-11-23" +#define NUKI_HUB_DATE "2025-11-23" #define GITHUB_LATEST_RELEASE_URL (char*)"https://github.com/technyon/nuki_hub/releases/latest" #define GITHUB_OTA_MANIFEST_URL (char*)"https://raw.githubusercontent.com/technyon/nuki_hub/binary/ota/manifest.json" @@ -249,3 +249,7 @@ #define NETWORK_TASK_SIZE 12288 #define HTTPD_TASK_SIZE 8192 + +#ifndef CHUNK_SIZE +#define CHUNK_SIZE 1400 +#endif diff --git a/src/NukiOpenerWrapper.cpp b/src/NukiOpenerWrapper.cpp index b8c4ea4..15fedf0 100644 --- a/src/NukiOpenerWrapper.cpp +++ b/src/NukiOpenerWrapper.cpp @@ -69,7 +69,7 @@ void NukiOpenerWrapper::initialize() _nukiOpener.setEventHandler(this); _nukiOpener.setConnectTimeout(2); _nukiOpener.setDisconnectTimeout(2000); - _nukiOpener.setGeneralTimeout(_preferences->getInt(preference_ble_general_timeout, 3000)); + _nukiOpener.setGeneralTimeout(_preferences->getInt(preference_ble_general_timeout, 10000)); _nukiOpener.setCommandTimeout(_preferences->getInt(preference_ble_command_timeout, 3000)); _hassEnabled = _preferences->getBool(preference_mqtt_hass_enabled, false); diff --git a/src/NukiWrapper.cpp b/src/NukiWrapper.cpp index 654bef2..09e45b7 100644 --- a/src/NukiWrapper.cpp +++ b/src/NukiWrapper.cpp @@ -75,7 +75,7 @@ void NukiWrapper::initialize() _nukiLock.setEventHandler(this); _nukiLock.setConnectTimeout(2); _nukiLock.setDisconnectTimeout(2000); - _nukiLock.setGeneralTimeout(_preferences->getInt(preference_ble_general_timeout, 3000)); + _nukiLock.setGeneralTimeout(_preferences->getInt(preference_ble_general_timeout, 10000)); _nukiLock.setCommandTimeout(_preferences->getInt(preference_ble_command_timeout, 3000)); _hassEnabled = _preferences->getBool(preference_mqtt_hass_enabled, false); diff --git a/src/PreferencesKeys.h b/src/PreferencesKeys.h index c89de71..255326b 100644 --- a/src/PreferencesKeys.h +++ b/src/PreferencesKeys.h @@ -48,6 +48,7 @@ #define preference_ota_main_url (char*)"otaMainUrl" #define preference_ota_updater_url (char*)"otaUpdUrl" #define preference_buffer_size (char*)"buffsize" +#define preference_force_hosted_update (char*)"frcHstdUpd" // CHANGE DOES NOT REQUIRE REBOOT TO TAKE EFFECT #define preference_find_best_rssi (char*)"nwbestrssi" @@ -262,13 +263,14 @@ inline void initPreferences(Preferences* preferences) preferences->putBool(preference_cred_bypass_boot_btn_enabled, false); preferences->putBool(preference_publish_config, false); preferences->putBool(preference_config_from_mqtt, false); + preferences->putBool(preference_force_hosted_update, false); preferences->putInt(preference_mqtt_broker_port, 1883); preferences->putInt(preference_buffer_size, CHAR_BUFFER_SIZE); preferences->putInt(preference_task_size_network, NETWORK_TASK_SIZE); - preferences->putInt(preference_task_size_nuki, NUKI_TASK_SIZE); - preferences->putInt(preference_ble_general_timeout, 3000); - preferences->putInt(preference_ble_command_timeout, 3000); + preferences->putInt(preference_task_size_nuki, NUKI_TASK_SIZE); + preferences->putInt(preference_ble_general_timeout, 10000); + preferences->putInt(preference_ble_command_timeout, 3000); preferences->putInt(preference_authlog_max_entries, MAX_AUTHLOG); preferences->putInt(preference_keypad_max_entries, MAX_KEYPAD); preferences->putInt(preference_timecontrol_max_entries, MAX_TIMECONTROL); @@ -561,7 +563,7 @@ private: preference_cred_session_lifetime, preference_cred_session_lifetime_remember, preference_cred_session_lifetime_duo, preference_cred_session_lifetime_duo_remember, preference_cred_duo_approval, preference_cred_bypass_boot_btn_enabled, preference_cred_bypass_gpio_high, preference_cred_bypass_gpio_low, preference_publish_config, preference_config_from_mqtt, preference_totp_secret, preference_cred_session_lifetime_totp, preference_cred_session_lifetime_totp_remember, preference_bypass_secret, - preference_admin_secret, preference_ble_general_timeout, preference_ble_command_timeout + preference_admin_secret, preference_ble_general_timeout, preference_ble_command_timeout, preference_force_hosted_update }; std::vector _redact = { @@ -582,7 +584,7 @@ private: preference_debug_connect, preference_debug_communication, preference_debug_readable_data, preference_debug_hex_data, preference_debug_command, preference_lock_force_id, preference_lock_force_doorsensor, preference_lock_force_keypad, preference_opener_force_id, preference_opener_force_keypad, preference_mqtt_ssl_enabled, preference_hybrid_reboot_on_disconnect, preference_lock_gemini_enabled, preference_enable_debug_mode, preference_cred_duo_enabled, preference_cred_duo_approval, - preference_publish_config, preference_config_from_mqtt + preference_publish_config, preference_config_from_mqtt, preference_force_hosted_update }; std::vector _bytePrefs = { diff --git a/src/WebCfgServer.cpp b/src/WebCfgServer.cpp index 7727526..f593b33 100644 --- a/src/WebCfgServer.cpp +++ b/src/WebCfgServer.cpp @@ -24,6 +24,20 @@ extern const uint8_t x509_crt_imported_bundle_bin_start[] asm("_binary_x509_crt_ extern const uint8_t x509_crt_imported_bundle_bin_end[] asm("_binary_x509_crt_bundle_end"); extern bool timeSynced; +#if defined(CONFIG_ESP_HOSTED_ENABLE_BT_NIMBLE) || defined(CONFIG_ESP_WIFI_REMOTE_ENABLED) +#include "esp_hosted.h" +static esp_hosted_coprocessor_fwver_t slave_version_struct = { + .major1 = 0, + .minor1 = 0, + .patch1 = 0 +}; +static esp_hosted_coprocessor_fwver_t host_version_struct = { + .major1 = ESP_HOSTED_VERSION_MAJOR_1, + .minor1 = ESP_HOSTED_VERSION_MINOR_1, + .patch1 = ESP_HOSTED_VERSION_PATCH_1 +}; +#endif + #ifndef NUKI_HUB_UPDATER #include #include @@ -3573,6 +3587,16 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S restartServicesNoReconnect = true; } } + else if(key == "FRCHSTUPD") + { + if(_preferences->getBool(preference_force_hosted_update, false) != (value == "1")) + { + _preferences->putBool(preference_force_hosted_update, (value == "1")); + Log->print("Setting changed: "); + Log->println(key); + configChanged = true; + } + } else if(key == "CHECKUPDATE") { if(_preferences->getBool(preference_check_updates, false) != (value == "1")) @@ -3817,7 +3841,7 @@ bool WebCfgServer::processArgs(PsychicRequest *request, PsychicResponse* resp, S { if(value.toInt() > 2999 && value.toInt() < 65537) { - if(_preferences->getInt(preference_ble_general_timeout, 3000) != value.toInt()) + if(_preferences->getInt(preference_ble_general_timeout, 10000) != value.toInt()) { _preferences->putInt(preference_ble_general_timeout, value.toInt()); Log->print("Setting changed: "); @@ -5749,14 +5773,15 @@ esp_err_t WebCfgServer::buildAdvancedConfigHtml(PsychicRequest *request, Psychic response.print(_preferences->getBool(preference_enable_bootloop_reset, false) ? "Enabled" : "Disabled"); response.print(""); printCheckBox(&response, "DISNTWNOCON", "Disable Network if not connected within 60s", _preferences->getBool(preference_disable_network_not_connected, false), ""); - printCheckBox(&response, "WEBLOG", "Enable WebSerial logging", _preferences->getBool(preference_webserial_enabled), ""); + printCheckBox(&response, "WEBLOG", "Enable WebSerial logging", _preferences->getBool(preference_force_hosted_update, false), ""); + printCheckBox(&response, "FRCHSTUPD", "Force slave Hosted update on next boot", _preferences->getBool(preference_webserial_enabled), ""); printCheckBox(&response, "BTLPRST", "Enable Bootloop prevention (Try to reset these settings to default on bootloop)", true, ""); printInputField(&response, "BUFFSIZE", "Char buffer size (min 4096, max 65536)", _preferences->getInt(preference_buffer_size, CHAR_BUFFER_SIZE), 6, ""); response.print("Advised minimum char buffer size based on current settings"); printInputField(&response, "TSKNTWK", "Task size Network (min 12288, max 65536)", _preferences->getInt(preference_task_size_network, NETWORK_TASK_SIZE), 6, ""); response.print("Advised minimum network task size based on current settings"); printInputField(&response, "TSKNUKI", "Task size Nuki (min 8192, max 65536)", _preferences->getInt(preference_task_size_nuki, NUKI_TASK_SIZE), 6, ""); - printInputField(&response, "BLEGENTIMEOUT", "BLE General timeout in ms (min 3000, max 65536)", _preferences->getInt(preference_ble_general_timeout, 3000), 6, ""); + printInputField(&response, "BLEGENTIMEOUT", "BLE General timeout in ms (min 10000, max 65536)", _preferences->getInt(preference_ble_general_timeout, 10000), 6, ""); printInputField(&response, "BLECMDTIMEOUT", "BLE Command timeout in ms (min 3000, max 65536)", _preferences->getInt(preference_ble_command_timeout, 3000), 6, ""); printInputField(&response, "ALMAX", "Max auth log entries (min 1, max 100)", _preferences->getInt(preference_authlog_max_entries, MAX_AUTHLOG), 3, "id=\"inputmaxauthlog\""); printInputField(&response, "KPMAX", "Max keypad entries (min 1, max 200)", _preferences->getInt(preference_keypad_max_entries, MAX_KEYPAD), 3, "id=\"inputmaxkeypad\""); @@ -6263,6 +6288,11 @@ esp_err_t WebCfgServer::buildInfoHtml(PsychicRequest *request, PsychicResponse* response.print(_preferences->getString(preference_updater_build, "")); response.print("\nUpdater build date: "); response.print(_preferences->getString(preference_updater_date, "")); + #if defined(CONFIG_ESP_HOSTED_ENABLE_BT_NIMBLE) || defined(CONFIG_ESP_WIFI_REMOTE_ENABLED) + response.printf("\nHost hosted firmware version: %u.%u.%u", host_version_struct.major1, host_version_struct.minor1, host_version_struct.patch1); + esp_hosted_get_coprocessor_fwversion(&slave_version_struct); + response.printf("\nSlave hosted firmware version: %u.%u.%u", slave_version_struct.major1, slave_version_struct.minor1, slave_version_struct.patch1); + #endif response.print("\nUptime (min): "); response.print(espMillis() / 1000 / 60); response.print("\nConfig version: "); diff --git a/src/idf_component.yml b/src/idf_component.yml index 9b41c95..9926dd3 100644 --- a/src/idf_component.yml +++ b/src/idf_component.yml @@ -9,7 +9,7 @@ dependencies: espressif/libsodium: "^1.0.20~2" espressif/esp_hosted: - version: "2.6.0" + version: "2.6.6" rules: - if: "target in [esp32p4]" diff --git a/src/main.cpp b/src/main.cpp index 719baf3..23cf8c8 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -28,6 +28,12 @@ bool nuki_hub_https_server_enabled = false; #include "esp_psram.h" #endif +#if defined(CONFIG_ESP_HOSTED_ENABLE_BT_NIMBLE) || defined(CONFIG_ESP_WIFI_REMOTE_ENABLED) +#include "esp_hosted.h" +#include "esp_hosted_ota.h" +#include "esp_hosted_api_types.h" +#endif + #ifndef NUKI_HUB_UPDATER #include "SerialReader.h" #include "NukiWrapper.h" @@ -114,8 +120,10 @@ bool lockStarted = false; bool openerStarted = false; bool bleScannerStarted = false; bool webSerialEnabled = false; +bool forceHostedUpdate = false; uint8_t partitionType = -1; +uint8_t http_err = 0; int lastHTTPeventId = -1; bool doOta = false; bool restartReason_isValid; @@ -124,6 +132,339 @@ RestartReason currentRestartReason = RestartReason::NotApplicable; TaskHandle_t otaTaskHandle = nullptr; TaskHandle_t networkTaskHandle = nullptr; +esp_err_t _http_event_handler(esp_http_client_event_t *evt) +{ + if (lastHTTPeventId != int(evt->event_id)) + { + Log->println(""); + switch (evt->event_id) + { + case HTTP_EVENT_ERROR: + Log->println("HTTP_EVENT_ERROR"); + http_err = 1; + break; + case HTTP_EVENT_ON_CONNECTED: + Log->println("HTTP_EVENT_ON_CONNECTED"); + break; + case HTTP_EVENT_HEADER_SENT: + Log->println("HTTP_EVENT_HEADER_SENT"); + break; + case HTTP_EVENT_ON_HEADER: + Log->printf("HTTPS_EVENT_ON_HEADER: %s=%s\n", evt->header_key, evt->header_value); + if (strcmp(evt->header_key, "Content-Length") == 0) { + Log->printf("Content-Length: %s bytes\n", evt->header_value); + } + break; + case HTTP_EVENT_ON_DATA: + Log->println("HTTP_EVENT_ON_DATA"); + break; + case HTTP_EVENT_ON_FINISH: + Log->println("HTTP_EVENT_ON_FINISH"); + break; + case HTTP_EVENT_DISCONNECTED: + Log->println("HTTP_EVENT_DISCONNECTED"); + break; + case HTTP_EVENT_REDIRECT: + Log->println("HTTP_EVENT_REDIRECT"); + break; + } + } + else + { + Log->print("."); + } + lastHTTPeventId = int(evt->event_id); + wdt_hal_context_t rtc_wdt_ctx = RWDT_HAL_CONTEXT_DEFAULT(); + wdt_hal_write_protect_disable(&rtc_wdt_ctx); + wdt_hal_feed(&rtc_wdt_ctx); + wdt_hal_write_protect_enable(&rtc_wdt_ctx); + + return ESP_OK; +} + +#if defined(CONFIG_ESP_HOSTED_ENABLE_BT_NIMBLE) || defined(CONFIG_ESP_WIFI_REMOTE_ENABLED) +static esp_err_t parse_image_header_from_buffer(const uint8_t* buffer, size_t buffer_size, size_t* firmware_size, char* app_version_str, size_t version_str_len) +{ + esp_image_header_t image_header; + esp_image_segment_header_t segment_header; + esp_app_desc_t app_desc; + size_t offset = 0; + size_t total_size = 0; + + /* Check if buffer has enough data for image header */ + if (buffer_size < sizeof(image_header)) { + Log->println("Buffer too small for image header verification"); + return ESP_ERR_INVALID_SIZE; + } + + /* Read image header from buffer */ + memcpy(&image_header, buffer + offset, sizeof(image_header)); + + /* Validate magic number */ + if (image_header.magic != ESP_IMAGE_HEADER_MAGIC) { + Log->printf("Invalid image magic: 0x%" PRIx8 "\n", image_header.magic); + return ESP_ERR_INVALID_ARG; + } + + Log->printf("Image header: magic=0x%" PRIx8 ", segment_count=%" PRIu8 ", hash_appended=%" PRIu8 "\n", image_header.magic, image_header.segment_count, image_header.hash_appended); + + /* Calculate total size by reading all segments */ + offset = sizeof(image_header); + total_size = sizeof(image_header); + + for (int i = 0; i < image_header.segment_count; i++) { + /* Check if buffer has enough data for segment header */ + if (buffer_size < offset + sizeof(segment_header)) { + Log->println("Buffer too small to read all segment headers, using partial verification"); + break; + } + + /* Read segment header from buffer */ + memcpy(&segment_header, buffer + offset, sizeof(segment_header)); + + Log->printf("Segment %d: data_len=%" PRIu32 ", load_addr=0x%" PRIx32 "\n", i, segment_header.data_len, segment_header.load_addr); + + /* Add segment header size + data size */ + total_size += sizeof(segment_header) + segment_header.data_len; + offset += sizeof(segment_header) + segment_header.data_len; + + /* Read app description from the first segment */ + if (i == 0) { + size_t app_desc_offset = sizeof(image_header) + sizeof(segment_header); + if (buffer_size >= app_desc_offset + sizeof(app_desc)) { + memcpy(&app_desc, buffer + app_desc_offset, sizeof(app_desc)); + strncpy(app_version_str, app_desc.version, version_str_len - 1); + app_version_str[version_str_len - 1] = '\0'; + Log->printf("Found app description: version='%s', project_name='%s'\n", app_desc.version, app_desc.project_name); + } else { + Log->println("Buffer too small to read app description"); + strncpy(app_version_str, "unknown", version_str_len - 1); + app_version_str[version_str_len - 1] = '\0'; + } + } + } + + /* Add padding to align to 16 bytes */ + size_t padding = (16 - (total_size % 16)) % 16; + if (padding > 0) { + Log->printf("Adding %u bytes of padding for alignment\n", (unsigned int)padding); + total_size += padding; + } + + /* Add the checksum byte (always present) */ + total_size += 1; + Log->println("Added 1 byte for checksum"); + + /* Add SHA256 hash if appended */ + bool has_hash = (image_header.hash_appended == 1); + if (has_hash) { + total_size += 32; // SHA256 hash is 32 bytes + Log->println("Added 32 bytes for SHA256 hash (hash_appended=1)"); + } else { + Log->println("No SHA256 hash appended (hash_appended=0)"); + } + + *firmware_size = total_size; + Log->printf("Total image size: %u bytes\n", (unsigned int)*firmware_size); + + return ESP_OK; +} + +esp_err_t ota_https_perform(const char* image_url) +{ + uint8_t *ota_chunk = NULL; + esp_err_t err = ESP_OK; + int data_read = 0; + int ota_failed = 0; + + if ((image_url == NULL) || (image_url[0] == '\0')) { + Log->println("Invalid image URL"); + return ESP_HOSTED_SLAVE_OTA_FAILED; + } + + // Validate HTTPS URL + if (strncmp(image_url, "https://", 8) != 0) { + Log->println("URL must use HTTPS protocol"); + return ESP_HOSTED_SLAVE_OTA_FAILED; + } + + Log->printf("Starting HTTPS OTA from URL: %s\n", image_url); + + esp_http_client_config_t config = { + .url = image_url, + .timeout_ms = 30000, + .event_handler = _http_event_handler, + .transport_type = HTTP_TRANSPORT_OVER_SSL, // Force HTTPS + .buffer_size = 8192, // Larger buffer for SSL + .buffer_size_tx = 4096, // Increased TX buffer + .skip_cert_common_name_check = false, // Always validate CN in production + .crt_bundle_attach = esp_crt_bundle_attach, + .keep_alive_enable = true, + .keep_alive_idle = 5, + .keep_alive_interval = 5, + .keep_alive_count = 3, + }; + + esp_http_client_handle_t client = esp_http_client_init(&config); + if (client == NULL) { + Log->println("Failed to initialize HTTPS client"); + return ESP_HOSTED_SLAVE_OTA_FAILED; + } + + /* Open connection */ + Log->println("Opening HTTPS connection..."); + if ((err = esp_http_client_open(client, 0)) != ESP_OK) { + Log->printf("Failed to open HTTPS connection: %s\n", esp_err_to_name(err)); + Log->println("Common causes:"); + Log->println(" - Certificate CN doesn't match server IP"); + Log->println(" - Server not running or unreachable"); + Log->println(" - WiFi connection issues"); + Log->println(" - Firewall blocking port 443"); + esp_http_client_cleanup(client); + return ESP_HOSTED_SLAVE_OTA_FAILED; + } + + if (http_err) { + Log->println("Exiting OTA, due to http failure"); + esp_http_client_cleanup(client); + http_err = 0; + return ESP_HOSTED_SLAVE_OTA_FAILED; + } + + /* Fetch headers */ + Log->println("Fetching HTTPS headers..."); + int64_t content_length = esp_http_client_fetch_headers(client); + + int http_status = esp_http_client_get_status_code(client); + if (http_status != 200) { + Log->printf("HTTPS request failed with status: %d\n", http_status); + esp_http_client_cleanup(client); + return ESP_HOSTED_SLAVE_OTA_FAILED; + } + + if (content_length <= 0) { + Log->println("HTTP client fetch headers failed"); + Log->printf("HTTP GET Status = %d, content_length = %" PRId64 "\n", esp_http_client_get_status_code(client), esp_http_client_get_content_length(client)); + esp_http_client_close(client); + esp_http_client_cleanup(client); + return ESP_HOSTED_SLAVE_OTA_FAILED; + } + + Log->printf("HTTP GET Status = %d, content_length = %" PRId64 "\n", esp_http_client_get_status_code(client), esp_http_client_get_content_length(client)); + + /* Begin OTA */ + Log->println("Preparing OTA"); + if ((err = esp_hosted_slave_ota_begin()) != ESP_OK) { + Log->printf("esp_hosted_slave_ota_begin failed: %s\n", esp_err_to_name(err)); + Log->printf("esp_ota_begin failed, error=%s\n", esp_err_to_name(err)); + esp_http_client_close(client); + esp_http_client_cleanup(client); + return ESP_HOSTED_SLAVE_OTA_FAILED; + } + + ota_chunk = (uint8_t*)calloc(1, CHUNK_SIZE); + if (!ota_chunk) { + Log->println("Failed to allocate OTA chunk memory"); + esp_http_client_close(client); + esp_http_client_cleanup(client); + return ESP_HOSTED_SLAVE_OTA_FAILED; + } + + Log->println("Starting OTA data transfer over HTTPS"); + + /* Read and write OTA data */ + bool header_verified = false; + int chunk_count = 0; + + while ((data_read = esp_http_client_read(client, (char*)ota_chunk, CHUNK_SIZE)) > 0) { + Log->printf("Read image length %d\n", data_read); + + /* Verify image header from the first chunk */ + if (!header_verified && chunk_count == 0) { + size_t firmware_size; + char app_version[32]; + + Log->printf("Verifying image header from first chunk (%d bytes)\n", data_read); + if ((err = parse_image_header_from_buffer(ota_chunk, data_read, &firmware_size, app_version, sizeof(app_version))) != ESP_OK) { + Log->printf("Image header verification failed: %s\n", esp_err_to_name(err)); + ota_failed = 1; + break; + } + + Log->printf("Image verified - Size: %u bytes, Version: %s\n", (unsigned int)firmware_size, app_version); + + #ifdef CONFIG_OTA_VERSION_CHECK_SLAVEFW_SLAVE + /* Get current running slave firmware version and compare */ + esp_hosted_coprocessor_fwver_t current_slave_version = {0}; + esp_err_t version_ret = esp_hosted_get_coprocessor_fwversion(¤t_slave_version); + + if (version_ret == ESP_OK) { + char current_version_str[32]; + snprintf(current_version_str, sizeof(current_version_str), "%" PRIu32 ".%" PRIu32 ".%" PRIu32, + current_slave_version.major1, current_slave_version.minor1, current_slave_version.patch1); + + Log->printf("Current slave firmware version: %s\n", current_version_str); + Log->printf("New slave firmware version: %s\n", app_version); + + if (strcmp(app_version, current_version_str) == 0) { + Log->printf("Current slave firmware version (%s) is the same as new version (%s). Skipping OTA.\n", current_version_str, app_version); + /* Cleanup and return success */ + free(ota_chunk); + esp_http_client_close(client); + esp_http_client_cleanup(client); + return ESP_HOSTED_SLAVE_OTA_NOT_REQUIRED; + } + + Log->printf("Version differs - proceeding with OTA from %s to %s\n", current_version_str, app_version); + } else { + Log->printf("Could not get current slave firmware version (error: %s), proceeding with OTA\n", esp_err_to_name(version_ret)); + } + #else + Log->printf("Version check disabled - proceeding with OTA (new firmware version: %s)\n", app_version); + #endif + + header_verified = true; + } + + if ((err = esp_hosted_slave_ota_write(ota_chunk, data_read)) != ESP_OK) { + Log->printf("esp_hosted_slave_ota_write failed: %s\n", esp_err_to_name(err)); + ota_failed = 1; + break; + } + + chunk_count++; + } + + /* Cleanup resources */ + free(ota_chunk); + esp_http_client_close(client); + esp_http_client_cleanup(client); + + /* Check for read errors */ + if (data_read < 0) { + Log->println("Error: HTTPS data read error"); + ota_failed = 1; + } + + /* End OTA */ + if ((err = esp_hosted_slave_ota_end()) != ESP_OK) { + Log->printf("esp_ota_end failed, error=%s\n", esp_err_to_name(err)); + esp_http_client_close(client); + esp_http_client_cleanup(client); + return ESP_HOSTED_SLAVE_OTA_FAILED; + } + + /* Final result */ + if (ota_failed) { + Log->println("********* Slave OTA Failed *******************"); + return ESP_HOSTED_SLAVE_OTA_FAILED; + } else { + Log->println("********* Slave OTA Complete *******************"); + return ESP_HOSTED_SLAVE_OTA_COMPLETED; + } +} +#endif + ssize_t write_fn(void* cookie, const char* buf, ssize_t size) { Log->write((uint8_t *)buf, (size_t)size); @@ -474,6 +815,13 @@ void restartServices(bool reconnect) Log->println("Deinit BLE device"); BLEDevice::deinit(false); Log->println("Deinit BLE device done"); + + #if defined(CONFIG_ESP_HOSTED_ENABLE_BT_NIMBLE) || defined(CONFIG_ESP_WIFI_REMOTE_ENABLED) + if (hostedIsBLEActive()) + { + hostedDeinitBLE(); + } + #endif } if (esp_task_wdt_status(NULL) == ESP_OK) @@ -545,6 +893,10 @@ void restartServices(bool reconnect) Log->println("Starting web server done"); } } + + #if defined(CONFIG_ESP_HOSTED_ENABLE_BT_NIMBLE) || defined(CONFIG_ESP_WIFI_REMOTE_ENABLED) + hostedInitBLE(); + #endif } #endif @@ -584,6 +936,37 @@ void networkTask(void *pvParameters) if(connected && reroute) { + #if !defined(NUKI_HUB_UPDATER) && (defined(CONFIG_ESP_HOSTED_ENABLE_BT_NIMBLE) || defined(CONFIG_ESP_WIFI_REMOTE_ENABLED)) + //if (hostedHasUpdate() || forceHostedUpdate) + if (forceHostedUpdate) + { + int ret; + forceHostedUpdate = false; + preferences->putBool(preference_force_hosted_update, false); + + Log->printf("Update URL: %s", hostedGetUpdateURL()); + ret = ota_https_perform(hostedGetUpdateURL()); + //ret = ota_https_perform("https://raw.githubusercontent.com/technyon/nuki_hub/binary/ota/hosted/network_adapter.bin"); + + if (ret == ESP_HOSTED_SLAVE_OTA_COMPLETED) { + Log->printf("Hosted OTA completed successfully"); + ret = esp_hosted_slave_ota_activate(); + if (ret == ESP_OK) { + Log->printf("Hosted Slave will reboot with new firmware"); + Log->printf("********* Restarting host to avoid sync issues **********************"); + vTaskDelay(pdMS_TO_TICKS(2000)); + esp_restart(); + } else { + Log->printf("Failed to activate Hosted OTA: %s", esp_err_to_name(ret)); + } + } else if (ret == ESP_HOSTED_SLAVE_OTA_NOT_REQUIRED) { + Log->printf("Hosted OTA not required"); + } else { + Log->printf("Hosted OTA failed: %s", esp_err_to_name(ret)); + } + } + #endif + if(preferences->getBool(preference_update_time, false)) { esp_netif_sntp_start(); @@ -873,6 +1256,7 @@ void bootloopDetection() { uint64_t cmp = IS_VALID_DETECT; bool bootloopIsValid = (bootloopValidDetect == cmp); + Log->print("Bootloop counter valid: "); Log->println(bootloopIsValid); if(!bootloopIsValid) @@ -891,10 +1275,11 @@ void bootloopDetection() Log->print("Bootloop counter incremented: "); Log->println(bootloopCounter); - if(bootloopCounter == 10) + if(bootloopCounter == 10 && preferences->getBool(preference_enable_bootloop_reset, false)) { Log->print("Bootloop detected."); + preferences->putInt(preference_network_hardware, 15); preferences->putInt(preference_buffer_size, CHAR_BUFFER_SIZE); preferences->putInt(preference_task_size_network, NETWORK_TASK_SIZE); preferences->putInt(preference_task_size_nuki, NUKI_TASK_SIZE); @@ -908,52 +1293,6 @@ void bootloopDetection() } #endif -esp_err_t _http_event_handler(esp_http_client_event_t *evt) -{ - if (lastHTTPeventId != int(evt->event_id)) - { - Log->println(""); - switch (evt->event_id) - { - case HTTP_EVENT_ERROR: - Log->println("HTTP_EVENT_ERROR"); - break; - case HTTP_EVENT_ON_CONNECTED: - Log->print("HTTP_EVENT_ON_CONNECTED"); - break; - case HTTP_EVENT_HEADER_SENT: - Log->print("HTTP_EVENT_HEADER_SENT"); - break; - case HTTP_EVENT_ON_HEADER: - Log->print("HTTP_EVENT_ON_HEADER"); - break; - case HTTP_EVENT_ON_DATA: - Log->print("HTTP_EVENT_ON_DATA"); - break; - case HTTP_EVENT_ON_FINISH: - Log->println("HTTP_EVENT_ON_FINISH"); - break; - case HTTP_EVENT_DISCONNECTED: - Log->println("HTTP_EVENT_DISCONNECTED"); - break; - case HTTP_EVENT_REDIRECT: - Log->print("HTTP_EVENT_REDIRECT"); - break; - } - } - else - { - Log->print("."); - } - lastHTTPeventId = int(evt->event_id); - wdt_hal_context_t rtc_wdt_ctx = RWDT_HAL_CONTEXT_DEFAULT(); - wdt_hal_write_protect_disable(&rtc_wdt_ctx); - wdt_hal_feed(&rtc_wdt_ctx); - wdt_hal_write_protect_enable(&rtc_wdt_ctx); - - return ESP_OK; -} - void otaTask(void *pvParameter) { esp_task_wdt_add(NULL); @@ -1032,6 +1371,8 @@ void otaTask(void *pvParameter) restartEsp(RestartReason::OTAAborted); } + + void setupTasks(bool ota) { // configMAX_PRIORITIES is 25 @@ -1205,6 +1546,8 @@ void setup() logCoreDump(); } + forceHostedUpdate = preferences->getBool(preference_force_hosted_update, false); + if (SPIFFS.begin(true)) { listDir(SPIFFS, "/", 1); @@ -1346,10 +1689,11 @@ void setup() } } #else - if(preferences->getBool(preference_enable_bootloop_reset, false)) - { - bootloopDetection(); - } + bootloopDetection(); + +#if defined(CONFIG_ESP_HOSTED_ENABLE_BT_NIMBLE) || defined(CONFIG_ESP_WIFI_REMOTE_ENABLED) + hostedInitBLE(); +#endif Log->print("Nuki Hub version "); Log->println(NUKI_HUB_VERSION); diff --git a/src/networkDevices/WifiDevice.cpp b/src/networkDevices/WifiDevice.cpp index f4dff8c..4376338 100644 --- a/src/networkDevices/WifiDevice.cpp +++ b/src/networkDevices/WifiDevice.cpp @@ -21,58 +21,36 @@ const String WifiDevice::deviceName() const void WifiDevice::initialize() { - if (_hostname != "fakep4forhosted") + ssid = _preferences->getString(preference_wifi_ssid, ""); + ssid.trim(); + pass = _preferences->getString(preference_wifi_pass, ""); + pass.trim(); + WiFi.setHostname(_hostname.c_str()); + + WiFi.onEvent([&](WiFiEvent_t event, WiFiEventInfo_t info) { - ssid = _preferences->getString(preference_wifi_ssid, ""); - ssid.trim(); - pass = _preferences->getString(preference_wifi_pass, ""); - pass.trim(); - WiFi.setHostname(_hostname.c_str()); + onWifiEvent(event, info); + }); - WiFi.onEvent([&](WiFiEvent_t event, WiFiEventInfo_t info) + if(isWifiConfigured()) + { + Log->println(String("Attempting to connect to saved SSID ") + String(ssid)); + _openAP = false; + if(_preferences->getBool(preference_find_best_rssi, false)) { - onWifiEvent(event, info); - }); - - if(isWifiConfigured()) - { - Log->println(String("Attempting to connect to saved SSID ") + String(ssid)); - _openAP = false; - if(_preferences->getBool(preference_find_best_rssi, false)) - { - scan(false, true); - } - else - { - WiFi.mode(WIFI_STA); - connect(); - } + scan(false, true); } else { - Log->println("No SSID or Wifi password saved, opening AP"); - _openAP = true; - scan(false, true); + WiFi.mode(WIFI_STA); + connect(); } } else { - WiFi.disconnect(true); - WiFi.mode(WIFI_STA); - WiFi.disconnect(); - - int loop = 0; - while (!_wifiClientStarted && loop < 50) - { - if (esp_task_wdt_status(NULL) == ESP_OK) - { - esp_task_wdt_reset(); - } - vTaskDelay(100 / portTICK_PERIOD_MS); - loop++; - } - - Log->println("Dummy WiFi device for Hosted on P4 done"); + Log->println("No SSID or Wifi password saved, opening AP"); + _openAP = true; + scan(false, true); } return; } diff --git a/src/util/NetworkDeviceInstantiator.cpp b/src/util/NetworkDeviceInstantiator.cpp index 65f03ce..892bc55 100644 --- a/src/util/NetworkDeviceInstantiator.cpp +++ b/src/util/NetworkDeviceInstantiator.cpp @@ -11,9 +11,6 @@ NetworkDevice *NetworkDeviceInstantiator::Create(NetworkDeviceType networkDeviceType, String hostname, Preferences *preferences, IPConfiguration *ipConfiguration) { NetworkDevice* device = nullptr; -#if defined(CONFIG_IDF_TARGET_ESP32P4) - bool fakedevice = true; -#endif switch (networkDeviceType) { @@ -183,10 +180,13 @@ NetworkDevice *NetworkDeviceInstantiator::Create(NetworkDeviceType networkDevice #ifndef CONFIG_IDF_TARGET_ESP32H2 else { + #if defined(CONFIG_ESP_HOSTED_ENABLE_BT_NIMBLE) || defined(CONFIG_ESP_WIFI_REMOTE_ENABLED) + if (!hostedIsWiFiActive()) + { + hostedInitWiFi(); + } + #endif device = new WifiDevice(hostname, preferences, ipConfiguration); -#if defined(CONFIG_IDF_TARGET_ESP32P4) - fakedevice = false; -#endif } #endif } @@ -216,16 +216,22 @@ NetworkDevice *NetworkDeviceInstantiator::Create(NetworkDeviceType networkDevice #endif #ifndef CONFIG_IDF_TARGET_ESP32H2 case NetworkDeviceType::WiFi: + #if defined(CONFIG_ESP_HOSTED_ENABLE_BT_NIMBLE) || defined(CONFIG_ESP_WIFI_REMOTE_ENABLED) + if (!hostedIsWiFiActive()) + { + hostedInitWiFi(); + } + #endif device = new WifiDevice(hostname, preferences, ipConfiguration); -#if defined(CONFIG_IDF_TARGET_ESP32P4) - fakedevice = false; -#endif break; default: + #if defined(CONFIG_ESP_HOSTED_ENABLE_BT_NIMBLE) || defined(CONFIG_ESP_WIFI_REMOTE_ENABLED) + if (!hostedIsWiFiActive()) + { + hostedInitWiFi(); + } + #endif device = new WifiDevice(hostname, preferences, ipConfiguration); -#if defined(CONFIG_IDF_TARGET_ESP32P4) - fakedevice = false; -#endif break; #else default: @@ -242,17 +248,5 @@ NetworkDevice *NetworkDeviceInstantiator::Create(NetworkDeviceType networkDevice #endif } -#if defined(CONFIG_IDF_TARGET_ESP32P4) - if (fakedevice) - { - Log->println("Create dummy WiFi device for Hosted on P4"); - NetworkDevice* device2 = nullptr; - device2 = new WifiDevice("fakep4forhosted", preferences, ipConfiguration); - device2->initialize(); - delete device2; - device2 = NULL; - } -#endif - return device; } diff --git a/updater/apply_patches.py b/updater/apply_patches.py new file mode 100644 index 0000000..b70dff8 --- /dev/null +++ b/updater/apply_patches.py @@ -0,0 +1,29 @@ +from os.path import join, isfile + +Import("env") + +FRAMEWORK_DIR = env.PioPlatform().get_package_dir("framework-arduinoespressif32") +patchflag_path = join(FRAMEWORK_DIR, ".hosted-patching-done") + +# patch file only if we didn't do it before +if not isfile(join(FRAMEWORK_DIR, ".hosted-patching-done")): + original_file = join(FRAMEWORK_DIR, "cores", "esp32", "esp32-hal-hosted.c") + patched_file = join("resources", "esp32-hal-hosted.c.patch") + + assert isfile(original_file) and isfile(patched_file) + + env.Execute("patch %s %s" % (original_file, patched_file)) + # env.Execute("touch " + patchflag_path) + + original_file = join(FRAMEWORK_DIR, "cores", "esp32", "esp32-hal-hosted.h") + patched_file = join("resources", "esp32-hal-hosted.h.patch") + + assert isfile(original_file) and isfile(patched_file) + + env.Execute("patch %s %s" % (original_file, patched_file)) + + def _touch(path): + with open(path, "w") as fp: + fp.write("") + + env.Execute(lambda *args, **kwargs: _touch(patchflag_path)) \ No newline at end of file diff --git a/updater/platformio.ini b/updater/platformio.ini index 7b6dd8d..ad0ef4a 100644 --- a/updater/platformio.ini +++ b/updater/platformio.ini @@ -13,7 +13,7 @@ default_envs = updater_esp32 boards_dir = ../boards [env] -platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.33/platform-espressif32.zip +platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.34/platform-espressif32.zip platform_packages = framework = arduino, espidf build_type = release @@ -149,6 +149,10 @@ board_build.cmake_extra_args = extends = env:updater_esp32 board_build.embed_txtfiles = board = esp32-p4 +extra_scripts = + pre:pio_package_pre.py + #pre:apply_patches.py + post:pio_package_post.py board_build.cmake_extra_args = -DSDKCONFIG_DEFAULTS="sdkconfig.defaults;sdkconfig.defaults.esp32-p4" custom_component_remove = diff --git a/updater/src/idf_component.yml b/updater/src/idf_component.yml index 9b3fc41..f55ac8e 100644 --- a/updater/src/idf_component.yml +++ b/updater/src/idf_component.yml @@ -3,7 +3,7 @@ dependencies: idf: ">=5.5" espressif/esp_hosted: - version: "*" + version: "2.6.6" #override_path: "../../resources/espressif__esp_hosted" rules: - if: "target in [esp32p4]"