diff --git a/hal/inc/hal_platform.h b/hal/inc/hal_platform.h index 81b85c1c61..67e6358440 100644 --- a/hal/inc/hal_platform.h +++ b/hal/inc/hal_platform.h @@ -434,6 +434,10 @@ #define HAL_PLATFORM_NCP_COUNT (0) #endif // HAL_PLATFORM_NCP_COUNT +#ifndef HAL_PLATFORM_NCP_FW_UPDATE +#define HAL_PLATFORM_NCP_FW_UPDATE (0) +#endif // HAL_PLATFORM_NCP_FW_UPDATE + #ifndef HAL_PLATFORM_WIFI_COMPAT #define HAL_PLATFORM_WIFI_COMPAT (0) #endif // HAL_PLATFORM_WIFI_COMPAT diff --git a/services/inc/diagnostics.h b/services/inc/diagnostics.h index b6b30ceba7..099469b995 100644 --- a/services/inc/diagnostics.h +++ b/services/inc/diagnostics.h @@ -48,6 +48,7 @@ #define DIAG_NAME_NETWORK_CELLULAR_CELL_GLOBAL_IDENTITY_MOBILE_NETWORK_CODE "net:cell:cgi:mnc" #define DIAG_NAME_NETWORK_CELLULAR_CELL_GLOBAL_IDENTITY_LOCATION_AREA_CODE "net:cell:cgi:lac" #define DIAG_NAME_NETWORK_CELLULAR_CELL_GLOBAL_IDENTITY_CELL_ID "net:cell:cgi:ci" +#define DIAG_NAME_NETWORK_NCP_FW_UPDATE_STATUS "net:ncpfw:stat" #define DIAG_NAME_CLOUD_CONNECTION_STATUS "cloud:stat" #define DIAG_NAME_CLOUD_CONNECTION_ERROR_CODE "cloud:err" #define DIAG_NAME_CLOUD_DISCONNECTS "cloud:dconn" @@ -95,6 +96,7 @@ typedef enum diag_id { DIAG_ID_NETWORK_CELLULAR_CELL_GLOBAL_IDENTITY_MOBILE_NETWORK_CODE = 41, // net:cell:cgi:mnc DIAG_ID_NETWORK_CELLULAR_CELL_GLOBAL_IDENTITY_LOCATION_AREA_CODE = 42, // net:cell:cgi:lac DIAG_ID_NETWORK_CELLULAR_CELL_GLOBAL_IDENTITY_CELL_ID = 43, // net:cell:cgi:ci + DIAG_ID_NETWORK_NCP_FW_UPDATE_STATUS = 50, // net:nfu:stat DIAG_ID_CLOUD_CONNECTION_STATUS = 10, // cloud:stat DIAG_ID_CLOUD_CONNECTION_ERROR_CODE = 13, // cloud:err DIAG_ID_CLOUD_DISCONNECTS = 14, // cloud:dconn diff --git a/services/inc/ncp_fw_update.h b/services/inc/ncp_fw_update.h new file mode 100644 index 0000000000..5ee885af80 --- /dev/null +++ b/services/inc/ncp_fw_update.h @@ -0,0 +1,230 @@ +/* + * Copyright (c) 2021 Particle Industries, Inc. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see . + */ + +#pragma once + +#include "hal_platform.h" +#include "system_tick_hal.h" +#include "core_hal.h" +#include "system_defs.h" +#include "system_mode.h" +#include "system_network.h" +#include "cellular_hal.h" +#include "platform_ncp.h" +#include "diagnostics.h" +#include "spark_wiring_diagnostics.h" +#include "ncp_fw_update_dynalib.h" + +#if HAL_PLATFORM_NCP +#include "at_parser.h" +#include "at_response.h" +#endif // HAL_PLATFORM_NCP + +#if HAL_PLATFORM_NCP_FW_UPDATE + +struct NcpFwUpdateCallbacks { + uint16_t size; + uint16_t reserved; + + // System.updatesEnabled() / system_update.h + int (*system_get_flag)(system_flag_t flag, uint8_t* value, void*); + + // system_cloud.h + bool (*spark_cloud_flag_connected)(void); + void (*spark_cloud_flag_connect)(void); + void (*spark_cloud_flag_disconnect)(void); + + // system_cloud_internal.h + bool (*publishEvent)(const char* event, const char* data, unsigned flags); + + // system_mode.h + System_Mode_TypeDef (*system_mode)(void); +}; +PARTICLE_STATIC_ASSERT(NcpFwUpdateCallbacks_size, sizeof(NcpFwUpdateCallbacks) == (sizeof(void*) * 7)); + +namespace particle { + +namespace services { + +const system_tick_t NCP_FW_MODEM_POWER_ON_TIMEOUT = 60000; +const system_tick_t NCP_FW_MODEM_INSTALL_START_TIMEOUT = 5 * 60000; +const system_tick_t NCP_FW_MODEM_INSTALL_FINISH_TIMEOUT = 30 * 60000; +const system_tick_t NCP_FW_MODEM_CLOUD_CONNECT_TIMEOUT = 5 * 60000; +const uint32_t NCP_FW_UBLOX_R510_ENG_VERSION = 100000000; +const int NCP_FW_UUFWINSTALL_COMPLETE = 128; + +enum NcpFwUpdateState { + FW_UPDATE_IDLE_STATE = 0, + FW_UPDATE_QUALIFY_FLAGS_STATE = 1, + FW_UPDATE_QUALIFY_MODEM_ON_STATE = 2, + FW_UPDATE_QUALIFY_RETRY_STATE = 3, + FW_UPDATE_SETUP_CLOUD_CONNECT_STATE = 4, + FW_UPDATE_SETUP_CLOUD_CONNECTING_STATE = 5, + FW_UPDATE_SETUP_CLOUD_CONNECTED_STATE = 6, + FW_UPDATE_DOWNLOAD_CLOUD_DISCONNECT_STATE = 7, + FW_UPDATE_DOWNLOAD_CELL_DISCONNECTING_STATE = 8, + FW_UPDATE_DOWNLOAD_CELL_CONNECTING_STATE = 9, + FW_UPDATE_DOWNLOAD_HTTPS_SETUP_STATE = 10, + FW_UPDATE_DOWNLOAD_READY_STATE = 11, + FW_UPDATE_INSTALL_CELL_DISCONNECTING_STATE = 12, + FW_UPDATE_INSTALL_CELL_DISCONNECTED_STATE = 13, + FW_UPDATE_INSTALL_STATE_STARTING = 14, + FW_UPDATE_INSTALL_STATE_WAITING = 15, + FW_UPDATE_FINISHED_POWER_OFF_STATE = 16, + FW_UPDATE_FINISHED_POWERING_OFF_STATE = 17, + FW_UPDATE_FINISHED_CLOUD_CONNECTING_STATE = 18, + FW_UPDATE_FINISHED_CLOUD_CONNECTED_STATE = 19, + FW_UPDATE_FINISHED_IDLE_STATE = 20, +}; + +enum NcpFwUpdateStatus { + FW_UPDATE_IDLE_STATUS = 0, + FW_UPDATE_DOWNLOADING_STATUS = 1, + FW_UPDATE_UPDATING_STATUS = 2, + FW_UPDATE_SUCCESS_STATUS = 3, + FW_UPDATE_NONE_STATUS = -1, // for diagnostics + FW_UPDATE_FAILED_STATUS = -2, + FW_UPDATE_FAILED_DOWNLOAD_RETRY_MAX_STATUS = -3, + FW_UPDATE_FAILED_START_INSTALL_TIMEOUT_STATUS = -4, + FW_UPDATE_FAILED_INSTALL_AT_ERROR_STATUS = -5, + FW_UPDATE_FAILED_SAME_VERSION_STATUS = -6, + FW_UPDATE_FAILED_INSTALL_TIMEOUT_STATUS = -7, + FW_UPDATE_FAILED_POWER_OFF_TIMEOUT_STATUS = -8, + FW_UPDATE_FAILED_CLOUD_CONNECT_TIMEOUT_STATUS = -9, + FW_UPDATE_FAILED_PUBLISH_RESULT_STATUS = -10, +}; + +struct HTTPSresponse { + int command; + int result; + int status_code; + char md5_sum[40]; + int err_class; + int err_code; +}; + +const int NCP_FW_DATA_HEADER = 0x5259ADE5; +const int NCP_FW_DATA_FOOTER = 0x1337ACE5; +struct NcpFwUpdateData { + uint32_t header; // NCP_FW_DATA_HEADER; + NcpFwUpdateState state; // FW_UPDATE_IDLE_STATE; + NcpFwUpdateStatus status; // FW_UPDATE_IDLE_STATUS; + int firmwareVersion; // 0; + int startingFirmwareVersion; // 0; + int updateVersion; // 0; + uint8_t isUserConfig; // 0; + NcpFwUpdateConfig userConfigData; // 0; + uint32_t footer; // NCP_FW_DATA_FOOTER; +}; + +/** + * struct NcpFwUpdateConfig { + * const uint32_t start_version; + * const uint32_t end_version; + * const char filename[256]; + * const char md5sum[32]; + * }; + */ +const NcpFwUpdateConfig NCP_FW_UPDATE_CONFIG[] = { + // { 3140001, 103140001, "SARA-R510S-01B-00-ES-0314A0001_SARA-R510S-01B-00-XX-0314ENG0099A0001.upd", "09c1a98d03c761bcbea50355f9b2a50f" }, + // { 103140001, 3140001, "SARA-R510S-01B-00-XX-0314ENG0099A0001_SARA-R510S-01B-00-ES-0314A0001.upd", "136caf2883457093c9e41fda3c6a44e3" }, + // { 2060001, 99010001, "SARA-R510S-00B-01_FW02.06_A00.01_IP_SARA-R510S-00B-01_FW99.01_A00.01.upd", "ccfdc48c0a45198d6e168b30d0740959" }, + // { 99010001, 2060001, "SARA-R510S-00B-01_FW99.01_A00.01_SARA-R510S-00B-01_FW02.06_A00.01_IP.upd", "5fd6c0d3d731c097605895b86f28c2cf" }, +}; + +const size_t NCP_FW_UPDATE_CONFIG_SIZE = sizeof(NCP_FW_UPDATE_CONFIG) / sizeof(NCP_FW_UPDATE_CONFIG[0]); + +class NcpFwUpdate { +public: + /** + * Get the singleton instance of this class. + */ + static NcpFwUpdate* instance(); + + NcpFwUpdate(); + ~NcpFwUpdate(); + + int checkUpdate(const NcpFwUpdateConfig* userConfigData); + void init(NcpFwUpdateCallbacks* callbacks); + int process(); + int getStatusDiagnostics(); + +private: + + NcpFwUpdateState ncpFwUpdateState_; + NcpFwUpdateStatus ncpFwUpdateStatus_; + NcpFwUpdateStatus ncpFwUpdateStatusDiagnostics_; + int foatReady_; + system_tick_t startInstallTimer_; + system_tick_t atOkCheckTimer_; + int lastRespCode_; + int atResp_; + int atResponsive_; + int startingFirmwareVersion_; + int firmwareVersion_; + int updateVersion_; + bool isUserConfig_; + system_tick_t cooldownTimer_; + system_tick_t cooldownTimeout_; + bool initialized_; + + union { + NcpFwUpdateData var; + uint8_t data[sizeof(NcpFwUpdateData)]; + } ncpFwUpdateData_; + + /** + * Functional callbacks that provide key system services to this NCP FW Update class. + */ + NcpFwUpdateCallbacks* ncpFwUpdateCallbacks_; + + static int cbUUHTTPCR_(int type, const char* buf, int len, HTTPSresponse* data); + static int cbUHTTPER_(int type, const char* buf, int len, HTTPSresponse* data); + static int cbULSTFILE_(int type, const char* buf, int len, int* data); + static int cbATI9_(int type, const char* buf, int len, int* val); + static int cbUPSND_(int type, const char* buf, int len, int* data); + static int cbCOPS_(int type, const char* buf, int len, int* data); + static int httpRespCallback_(AtResponseReader* reader, const char* prefix, void* data); + int getAppFirmwareVersion_(); + int setupHTTPSProperties_(); + void cooldown_(system_tick_t timer); + void updateCooldown_(); + bool inCooldown_(); + void validateNcpFwUpdateData_(); + int firmwareUpdateForVersion_(const int version); + int saveNcpFwUpdateData_(); + int recallNcpFwUpdateData_(); + int deleteNcpFwUpdateData_(); +}; + +class NcpFwUpdateDiagnostics: public AbstractUnsignedIntegerDiagnosticData { +public: + NcpFwUpdateDiagnostics() : + AbstractUnsignedIntegerDiagnosticData(DIAG_ID_NETWORK_NCP_FW_UPDATE_STATUS, DIAG_NAME_NETWORK_NCP_FW_UPDATE_STATUS) { + } + + virtual int get(IntType& val) override { + val = particle::services::NcpFwUpdate::instance()->getStatusDiagnostics(); + return 0; // OK + } +}; + +} // namespace services + +} // namespace particle + +#endif // #if HAL_PLATFORM_NCP_FW_UPDATE diff --git a/services/inc/ncp_fw_update_dynalib.h b/services/inc/ncp_fw_update_dynalib.h new file mode 100644 index 0000000000..c338eff6af --- /dev/null +++ b/services/inc/ncp_fw_update_dynalib.h @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2021 Particle Industries, Inc. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see . + */ + +#pragma once + +#ifdef __cplusplus +struct NcpFwUpdateConfig { + int start_version; + int end_version; + char filename[255 + 1]; + char md5sum[32 + 1]; +}; +#else +typedef struct NcpFwUpdateConfig NcpFwUpdateConfig; +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +#if !defined(PARTICLE_USER_MODULE) || defined(PARTICLE_USE_UNSTABLE_API) +int ncp_fw_udpate_check(const NcpFwUpdateConfig* data, void* reserved); +#endif // !defined(PARTICLE_USER_MODULE) || defined(PARTICLE_USE_UNSTABLE_API) + +#ifdef __cplusplus +} +#endif diff --git a/services/inc/services_dynalib.h b/services/inc/services_dynalib.h index dde8014436..651f528d17 100644 --- a/services/inc/services_dynalib.h +++ b/services/inc/services_dynalib.h @@ -28,6 +28,7 @@ #ifdef DYNALIB_EXPORT #include "nanopb_misc.h" #include +#include "ncp_fw_update_dynalib.h" #ifdef PB_WITHOUT_64BIT #define pb_int64_t int32_t #define pb_uint64_t uint32_t @@ -114,6 +115,7 @@ DYNALIB_FN(BASE_IDX + 0, services, set_system_error_message, void(const char*, . DYNALIB_FN(BASE_IDX + 1, services, clear_system_error_message, void()) DYNALIB_FN(BASE_IDX + 2, services, get_system_error_message, const char*(int)) DYNALIB_FN(BASE_IDX + 3, services, jsmn_parse, int(jsmn_parser*, const char*, size_t, jsmntok_t*, unsigned int, void*)) +DYNALIB_FN(BASE_IDX + 4, services, ncp_fw_udpate_check, int(const NcpFwUpdateConfig*, void*)) DYNALIB_END(services) diff --git a/services/inc/system_cache.h b/services/inc/system_cache.h index b1fcda2d58..801c39303a 100644 --- a/services/inc/system_cache.h +++ b/services/inc/system_cache.h @@ -23,7 +23,8 @@ namespace particle { namespace services { enum class SystemCacheKey : uint16_t { WIFI_NCP_FIRMWARE_VERSION = 0x0000, - WIFI_NCP_MAC_ADDRESS = 0x0001 + WIFI_NCP_MAC_ADDRESS = 0x0001, + NCP_FW_UPDATE_DATA = 0x0002, }; class SystemCache { diff --git a/services/inc/system_defs.h b/services/inc/system_defs.h index 776f373e08..2969ea1b22 100644 --- a/services/inc/system_defs.h +++ b/services/inc/system_defs.h @@ -94,6 +94,71 @@ typedef enum network_disconnect_reason { NETWORK_DISCONNECT_REASON_UNKNOWN = 7 ///< Unspecified disconnection reason. } network_disconnect_reason; +typedef enum system_flag_t { + /** + * When 0, no OTA update is pending. + * When 1, an OTA update is pending, and will start when the SYSTEM_FLAG_OTA_UPDATES_FLAG + * is set. + */ + SYSTEM_FLAG_OTA_UPDATE_PENDING, + + /** + * When 0, OTA updates are not started. + * When 1, OTA updates are started. Default. + */ + SYSTEM_FLAG_OTA_UPDATE_ENABLED, + + /* + * When 0, no reset is pending. + * When 1, a reset is pending. The system will perform the reset + * when SYSTEM_FLAG_RESET_ENABLED is set to 1. + */ + SYSTEM_FLAG_RESET_PENDING, + + /** + * When 0, the system is not able to perform a system reset. + * When 1, thee system will reset the device when a reset is pending. + */ + SYSTEM_FLAG_RESET_ENABLED, + + /** + * A persistent flag that when set will cause the system to startup + * in listening mode. The flag is automatically cleared on reboot. + */ + SYSTEM_FLAG_STARTUP_LISTEN_MODE, + + /** + * Enable/Disable use of serial1 during setup. + */ + SYSTEM_FLAG_WIFITESTER_OVER_SERIAL1, + + /** + * Enable/disable publishing of last reset info to the cloud. + */ + SYSTEM_FLAG_PUBLISH_RESET_INFO, + + /** + * When 0, the system doesn't reset network connection on cloud connection errors. + * When 1 (default), the system resets network connection after a number of failed attempts to + * connect to the cloud. + */ + SYSTEM_FLAG_RESET_NETWORK_ON_CLOUD_ERRORS, + + /** + * Enable/Disable runtime power management peripheral detection + */ + SYSTEM_FLAG_PM_DETECTION, + + /** + * When 0, OTA updates are only applied when SYSTEM_FLAG_OTA_UPDATE_ENABLED is set. + * When 1, OTA updates are applied irrespective of the value of SYSTEM_FLAG_OTA_UPDATE_ENABLED. + */ + SYSTEM_FLAG_OTA_UPDATE_FORCED, + + SYSTEM_FLAG_MAX + +} system_flag_t; + #ifdef __cplusplus #include "enumflags.h" diff --git a/services/src/ncp_fw_update.cpp b/services/src/ncp_fw_update.cpp new file mode 100644 index 0000000000..b989f367ce --- /dev/null +++ b/services/src/ncp_fw_update.cpp @@ -0,0 +1,1026 @@ +/* + * Copyright (c) 2021 Particle Industries, Inc. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation, either + * version 3 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see . + */ + +/* + R510 Firmware Update Background Check: + ===================== + 1. Baked into sara_ncp_client.cpp init process + 2. When ATI9 is used to determine the modem version (thus, only when modem is on), + check if there is an update firmware version available. + 3. Set a flag for Cellular.updatePending() if update available. + + + R510 Firmware Update + ===================== + 0. (Blocking Call) Once Cellular.startUpdate() is called, initialize state_/status_ from Idle to Qualify. + 1. Check ncpId() == PLATFORM_NCP_SARA_R510 + 2. Is System.enableUpdates() == true? + 3. Check modem firmware version - compatible upgrade version will be baked into Device OS with MD5 sum. + 4. Reboot into Safe Mode to start the update process + 5. Particle.connect() + 6. Publish a "spark/device/ncp/update" system event that Device OS has "started" a modem update. + 7. Particle.disconnect() + 8. Disable PPP link and Cellular.connect(), timeout after 10 minutes + 9. Setup HTTPS security options + 10. If an existing FOAT file is present, delete it since there is no way to validate what it is after it's present. + 11. Start the download based our database entry + 12. Download is only complete when the the MD5SUM URC of the file is received and verified + 13. Cellular.disconnect() + 14. Apply the firmware update + 15. Sit in a tight loop while the modem is updating, it will take about 18 to 20 minutes + a. waiting for final INSTALL URC of 128 + b. polling for AT/OK every 10 seconds + c. monitoring a timeout counter of 40 minutes + 16. Save the download/install result to be published once connected to the Cloud again + 17. Re-enable PPP link and Power off the modem + 18. Particle.connect() + 19. Publish a "spark/device/ncp/update" system event that Device OS has finished with "success" or "failed" + 20. Reset the system to exit Safe Mode + 21. On next init, add result status to device diagnostics + + TODO: + ==================== + Done - Implement ncpId() check for R510 + Done - Implement System feature flag + Done - Implement reboot/exit Safe Mode + Done - Move ncp_fw_update.cpp to services/ instead of system/ since we have more space available there + Done - Add correct cipher settings + Done - Save state_, status_, firmwareVersion_ variables in retained system memory to ensure + Done - we complete step INSTALL/WAITING + Done - we do enter and exit safe mode fw updating cleanly + Done - we do not get stuck in a fail-retry loop + Done - Refactor retained structure to use systemCache instead + - New system events must be worked into Device Service, add a story for this. + Done - Adjust the 40 minute timeout dynamically based on the progress of the INSTALL URCs? + Done - Gen 3, extends timer 5 minutes for every received URC. + Done - When do we retry failures? Quick loops can retry a few times, but long ones shouldn't + Done - Create a database of starting firmware version / desired firmware version / filename / MD5SUM value, similar to APN DB. + Done - Make sure there is a final status log + Done - Allow user firmware to pass a database entry that overrides the system firmware update database + Done - Add final status to Device Diagnostics + ? - add modem version to Device Diagnostics + Done - Remove callbacks + Done - Remove Gen 2 code + Done - Gen 3 + Done - allow normal connection first + Done - optionally avoid connecting PPP + Done - enable PDP context with a few AT commands + Done - Remove System feature flag + - implement background update check, only perform checks if modem is on. + - ncp_fw_udpate_check should drive an update check, implement no argument as system check + - implement Cellular.updatesPending() API + - implement Cellular.startUpdate() API + - add 10 minute cellular connect timeout when PPP link disabled + - add URC handler for +CGEV: ME PDN DEACT 1 or +CGEV: ME DETACH or +UUPSDD: 0 to wait for cellular disconnected +*/ + +#include "logging.h" +LOG_SOURCE_CATEGORY("system.ncp.update"); + +#include "ncp_fw_update.h" +#include "cellular_enums_hal.h" +#include "platform_ncp.h" +#if HAL_PLATFORM_NCP +#include "network/ncp/cellular/cellular_network_manager.h" +#include "network/ncp/cellular/cellular_ncp_client.h" +#include "network/ncp/cellular/ncp.h" +#include "system_cache.h" +#endif // HAL_PLATFORM_NCP +#include "check.h" +#include "delay_hal.h" +#include "platform_headers.h" +#include "system_network.h" + +#if HAL_PLATFORM_NCP_FW_UPDATE + +namespace particle { + +namespace services { + +namespace { + +HTTPSresponse g_httpsResp = {}; +int g_respCode = 0; +NcpFwUpdateDiagnostics g_ncpFwUpdateDiagnostics; + +} // namespace + +NcpFwUpdate::NcpFwUpdate() { + foatReady_ = 0; + startInstallTimer_ = 0; + atOkCheckTimer_ = 0; + g_respCode = -1; + lastRespCode_ = -1; + atResponsive_ = -1; + atResp_ = 0; + startingFirmwareVersion_ = -1; // Keep these initialized differently to prevent + firmwareVersion_ = -2; // code from thinking an update was successful + updateVersion_ = -3; // | + isUserConfig_ = false; + cooldownTimer_ = 0; + cooldownTimeout_ = 0; + initialized_ = false; + ncpFwUpdateState_ = FW_UPDATE_IDLE_STATE; + ncpFwUpdateStatus_ = FW_UPDATE_IDLE_STATUS; + ncpFwUpdateStatusDiagnostics_ = FW_UPDATE_NONE_STATUS; // initialize to none, only set when update process complete. +} + +void NcpFwUpdate::init(NcpFwUpdateCallbacks* callbacks) { + this->ncpFwUpdateCallbacks_ = callbacks; + memset(ncpFwUpdateData_.data, 0, sizeof(ncpFwUpdateData_.data)); + recallNcpFwUpdateData_(); + validateNcpFwUpdateData_(); + if (system_mode() != System_Mode_TypeDef::SAFE_MODE) { + // If not in safe mode, make sure to reset the firmware update state. + if (ncpFwUpdateData_.var.state == FW_UPDATE_FINISHED_IDLE_STATE) { + ncpFwUpdateState_ = FW_UPDATE_IDLE_STATE; + if (ncpFwUpdateData_.var.status != FW_UPDATE_IDLE_STATUS) { + LOG(INFO, "Firmware update finished, %s status: %d", (ncpFwUpdateData_.var.status == FW_UPDATE_SUCCESS_STATUS) ? "success" : "failed", ncpFwUpdateData_.var.status); + + ncpFwUpdateStatusDiagnostics_ = ncpFwUpdateData_.var.status; + + ncpFwUpdateData_.var.status = FW_UPDATE_IDLE_STATUS; + saveNcpFwUpdateData_(); + } + } else if (ncpFwUpdateData_.var.state == FW_UPDATE_INSTALL_STATE_WAITING) { + LOG(INFO, "Resuming update in Safe Mode!"); + HAL_Delay_Milliseconds(200); + system_reset(SYSTEM_RESET_MODE_SAFE, 0, 0, SYSTEM_RESET_FLAG_NO_WAIT, nullptr); + } else { + ncpFwUpdateState_ = FW_UPDATE_IDLE_STATE; // default to disable updates + ncpFwUpdateStatus_ = FW_UPDATE_IDLE_STATUS; + } + } else { + // Ensure we recall the previously set state + ncpFwUpdateState_ = ncpFwUpdateData_.var.state; + ncpFwUpdateStatus_ = ncpFwUpdateData_.var.status; + firmwareVersion_ = ncpFwUpdateData_.var.firmwareVersion; + startingFirmwareVersion_ = ncpFwUpdateData_.var.startingFirmwareVersion; + updateVersion_ = ncpFwUpdateData_.var.updateVersion; + isUserConfig_ = ncpFwUpdateData_.var.isUserConfig; + } + initialized_ = true; +} + +int NcpFwUpdate::getStatusDiagnostics() { + return ncpFwUpdateStatusDiagnostics_; +} + +NcpFwUpdate* NcpFwUpdate::instance() { + static NcpFwUpdate instance; + return &instance; +} + +NcpFwUpdate::~NcpFwUpdate() { + // clean up? +} + +int NcpFwUpdate::checkUpdate(const NcpFwUpdateConfig* userConfigData) { + if (!userConfigData) { + return SYSTEM_ERROR_INVALID_ARGUMENT; + } + if (ncpFwUpdateStatus_ != FW_UPDATE_IDLE_STATUS) { + return SYSTEM_ERROR_INVALID_STATE; + } + + validateNcpFwUpdateData_(); + LOG(INFO, "NcpFwUpdateConfig sv:%lu ev:%lu file:%s md5:%s", + userConfigData->start_version, + userConfigData->end_version, + userConfigData->filename, + userConfigData->md5sum); + memcpy(&ncpFwUpdateData_.var.userConfigData, userConfigData, sizeof(NcpFwUpdateConfig)); + LOG(INFO, "NcpFwUpdateConfig sv:%lu ev:%lu file:%s md5:%s", + ncpFwUpdateData_.var.userConfigData.start_version, + ncpFwUpdateData_.var.userConfigData.end_version, + ncpFwUpdateData_.var.userConfigData.filename, + ncpFwUpdateData_.var.userConfigData.md5sum); + ncpFwUpdateData_.var.isUserConfig = true; + isUserConfig_ = true; + cooldownTimer_ = 0; + cooldownTimeout_ = 0; + ncpFwUpdateState_ = FW_UPDATE_QUALIFY_FLAGS_STATE; + + return 0; +} + +void NcpFwUpdate::validateNcpFwUpdateData_() { + if (ncpFwUpdateData_.var.header != NCP_FW_DATA_HEADER || ncpFwUpdateData_.var.footer != NCP_FW_DATA_FOOTER) { + memset(ncpFwUpdateData_.data, 0, sizeof(ncpFwUpdateData_.data)); + ncpFwUpdateData_.var.header = NCP_FW_DATA_HEADER; + ncpFwUpdateData_.var.state = FW_UPDATE_IDLE_STATE; + ncpFwUpdateData_.var.status = FW_UPDATE_IDLE_STATUS; + ncpFwUpdateData_.var.footer = NCP_FW_DATA_FOOTER; + + saveNcpFwUpdateData_(); + + isUserConfig_ = false; + ncpFwUpdateState_ = FW_UPDATE_IDLE_STATE; + ncpFwUpdateStatus_ = FW_UPDATE_IDLE_STATUS; + } +} + +int NcpFwUpdate::saveNcpFwUpdateData_() { + union { + NcpFwUpdateData var; + uint8_t data[sizeof(NcpFwUpdateData)]; + } tempData; + int result = SystemCache::instance().get(SystemCacheKey::NCP_FW_UPDATE_DATA, tempData.data, sizeof(tempData.data)); + if (result != sizeof(tempData.data) || /* memcmp(tempData.data, ncpFwUpdateData_.data, sizeof(tempData.data) != 0)) { // better but not working */ + tempData.var.header != ncpFwUpdateData_.var.header || + tempData.var.footer != ncpFwUpdateData_.var.footer || + tempData.var.state != ncpFwUpdateData_.var.state || + tempData.var.status != ncpFwUpdateData_.var.status || + tempData.var.firmwareVersion != ncpFwUpdateData_.var.firmwareVersion || + tempData.var.startingFirmwareVersion != ncpFwUpdateData_.var.startingFirmwareVersion || + tempData.var.updateVersion != ncpFwUpdateData_.var.updateVersion || + tempData.var.isUserConfig != ncpFwUpdateData_.var.isUserConfig || + tempData.var.userConfigData.start_version != ncpFwUpdateData_.var.userConfigData.start_version || + tempData.var.userConfigData.start_version != ncpFwUpdateData_.var.userConfigData.end_version || + strcmp(tempData.var.userConfigData.filename, ncpFwUpdateData_.var.userConfigData.filename) != 0 || + strcmp(tempData.var.userConfigData.md5sum, ncpFwUpdateData_.var.userConfigData.md5sum) != 0) { + LOG(INFO, "Reading ncpFwUpdateData header:%x footer:%x state:%d status:%d fv:%lu sfv:%lu uv:%lu", + tempData.var.header, + tempData.var.footer, + tempData.var.state, + tempData.var.status, + tempData.var.firmwareVersion, + tempData.var.startingFirmwareVersion, + tempData.var.updateVersion); + LOG(INFO, "iuc:%d sv:%lu ev:%lu file:%s md5:%s", + tempData.var.isUserConfig, + tempData.var.userConfigData.start_version, + tempData.var.userConfigData.end_version, + tempData.var.userConfigData.filename, + tempData.var.userConfigData.md5sum); + LOG(INFO, "Writing cached ncpFwUpdateData, size: %d", result); + result = SystemCache::instance().set(SystemCacheKey::NCP_FW_UPDATE_DATA, ncpFwUpdateData_.data, sizeof(ncpFwUpdateData_.data)); + } + return (result < 0) ? result : 0; +} + +int NcpFwUpdate::recallNcpFwUpdateData_() { + union { + NcpFwUpdateData var; + uint8_t data[sizeof(NcpFwUpdateData)]; + } tempData; + int result = SystemCache::instance().get(SystemCacheKey::NCP_FW_UPDATE_DATA, tempData.data, sizeof(tempData.data)); + if (result != sizeof(tempData.data)) { + return SYSTEM_ERROR_NOT_FOUND; + } + LOG(INFO, "Reading cached ncpFwUpdateData"); + memcpy(ncpFwUpdateData_.data, tempData.data, sizeof(ncpFwUpdateData_.data)); + LOG(INFO, "Reading ncpFwUpdateData header:%x footer:%x state:%d status:%d fv:%lu sfv:%lu uv:%lu", + tempData.var.header, + tempData.var.footer, + tempData.var.state, + tempData.var.status, + tempData.var.firmwareVersion, + tempData.var.startingFirmwareVersion, + tempData.var.updateVersion); + LOG(INFO, "iuc:%d sv:%lu ev:%lu file:%s md5:%s", + tempData.var.isUserConfig, + tempData.var.userConfigData.start_version, + tempData.var.userConfigData.end_version, + tempData.var.userConfigData.filename, + tempData.var.userConfigData.md5sum); + return 0; +} + +int NcpFwUpdate::deleteNcpFwUpdateData_() { + LOG(INFO, "Deleting cached ncpFwUpdateData"); + int result = SystemCache::instance().del(SystemCacheKey::NCP_FW_UPDATE_DATA); + return (result < 0) ? result : 0; +} + +int NcpFwUpdate::firmwareUpdateForVersion_(const int version) { + for (size_t i = 0; i < NCP_FW_UPDATE_CONFIG_SIZE; ++i) { + if (version == NCP_FW_UPDATE_CONFIG[i].start_version) { + return i; + } + } + return SYSTEM_ERROR_NOT_FOUND; +} + +int NcpFwUpdate::process() { + SPARK_ASSERT(initialized_); + + // Make state changes more obvious + static NcpFwUpdateState lastState = FW_UPDATE_IDLE_STATE; + if (ncpFwUpdateState_ != lastState) { + LOG(INFO, "=========================== ncpFwUpdateState_: %d", ncpFwUpdateState_); + } + lastState = ncpFwUpdateState_; + + validateNcpFwUpdateData_(); + + if (inCooldown_()) { + updateCooldown_(); + } else { + switch (ncpFwUpdateState_) { + + case FW_UPDATE_QUALIFY_FLAGS_STATE: + { + ncpFwUpdateState_ = FW_UPDATE_QUALIFY_MODEM_ON_STATE; + // Check NCP version + if (platform_primary_ncp_identifier() != PLATFORM_NCP_SARA_R510) { + LOG(ERROR, "PLATFORM_NCP != SARA_R510"); + ncpFwUpdateState_ = FW_UPDATE_IDLE_STATE; + break; + } + LOG(INFO, "PLATFORM_NCP == SARA_R510"); + // Check if System.updatesEnabled() + uint8_t updatesEnabled = 0; + ncpFwUpdateCallbacks_->system_get_flag(SYSTEM_FLAG_OTA_UPDATE_ENABLED, &updatesEnabled, nullptr); + if (!updatesEnabled) { + LOG(ERROR, "System updates disabled"); + ncpFwUpdateState_ = FW_UPDATE_IDLE_STATE; + break; + } + LOG(INFO, "System updates enabled"); + // Make sure device is on + if (!network_is_on(NETWORK_INTERFACE_CELLULAR, nullptr)) { // !Cellular.isOn() + LOG(INFO, "Turning Modem ON..."); + network_on(NETWORK_INTERFACE_CELLULAR, 0, 0, nullptr); // Cellular.on() + } + startInstallTimer_ = HAL_Timer_Get_Milli_Seconds(); + } + break; + + case FW_UPDATE_QUALIFY_MODEM_ON_STATE: + { + if (network_is_on(NETWORK_INTERFACE_CELLULAR, nullptr)) { // Cellular.isOn() + LOG(INFO, "Modem is on!"); + // Check firmware version requires an upgrade + firmwareVersion_ = getAppFirmwareVersion_(); + LOG(INFO, "App firmware: %d", firmwareVersion_); + if (firmwareVersion_ == 0) { + LOG(ERROR, "Modem has unknown firmware version"); + ncpFwUpdateState_ = FW_UPDATE_QUALIFY_RETRY_STATE; + break; + } + if ((!isUserConfig_ && firmwareUpdateForVersion_(firmwareVersion_) == SYSTEM_ERROR_NOT_FOUND) || + (isUserConfig_ && firmwareVersion_ != ncpFwUpdateData_.var.userConfigData.start_version)) { + LOG(INFO, "No firmware update available"); + ncpFwUpdateState_ = FW_UPDATE_IDLE_STATE; + break; + } + LOG(INFO, "Setup Fully Qualified!"); + // If not in Safe Mode, reset now into Safe Mode! + ncpFwUpdateState_ = FW_UPDATE_SETUP_CLOUD_CONNECT_STATE; + ncpFwUpdateData_.var.state = FW_UPDATE_SETUP_CLOUD_CONNECT_STATE; + ncpFwUpdateData_.var.firmwareVersion = firmwareVersion_; + saveNcpFwUpdateData_(); + if (system_mode() != System_Mode_TypeDef::SAFE_MODE) { + LOG(INFO, "Resetting into Safe Mode!"); + HAL_Delay_Milliseconds(200); + system_reset(SYSTEM_RESET_MODE_SAFE, 0, 0, SYSTEM_RESET_FLAG_NO_WAIT, nullptr); + // Goodbye! See you next time through in Safe Mode ;-) + } + } else if (HAL_Timer_Get_Milli_Seconds() - startInstallTimer_ >= NCP_FW_MODEM_POWER_ON_TIMEOUT) { + LOG(ERROR, "Modem did not power on!"); + ncpFwUpdateState_ = FW_UPDATE_QUALIFY_RETRY_STATE; + } + } + break; + + case FW_UPDATE_QUALIFY_RETRY_STATE: + { + static int qualify_retries = 0; + if (++qualify_retries < 2) { + ncpFwUpdateState_ = FW_UPDATE_QUALIFY_FLAGS_STATE; + } else { + ncpFwUpdateState_ = FW_UPDATE_IDLE_STATE; + } + } + break; + + case FW_UPDATE_SETUP_CLOUD_CONNECT_STATE: + { + if (!ncpFwUpdateCallbacks_->spark_cloud_flag_connected()) { // !Particle.connected() + LOG(INFO, "Connect to Cloud..."); + ncpFwUpdateCallbacks_->spark_cloud_flag_connect(); // Particle.connect() + } + ncpFwUpdateState_ = FW_UPDATE_SETUP_CLOUD_CONNECTING_STATE; + ncpFwUpdateStatus_ = FW_UPDATE_DOWNLOADING_STATUS; + ncpFwUpdateStatusDiagnostics_ = ncpFwUpdateStatus_; // ready our diagnostics status value before connecting to the cloud + } + break; + + case FW_UPDATE_SETUP_CLOUD_CONNECTING_STATE: + { + if (ncpFwUpdateCallbacks_->spark_cloud_flag_connected()) { // Particle.connected() + LOG(INFO, "Connected to Cloud."); + ncpFwUpdateState_ = FW_UPDATE_SETUP_CLOUD_CONNECTED_STATE; + } else { + ncpFwUpdateCallbacks_->spark_cloud_flag_connect(); // Particle.connect() + } + } + break; + + case FW_UPDATE_SETUP_CLOUD_CONNECTED_STATE: + { + if (ncpFwUpdateCallbacks_->spark_cloud_flag_connected()) { // Particle.connected() + LOG(INFO, "publishEvent"); + if (ncpFwUpdateCallbacks_->publishEvent("spark/device/ncp/update", "started", /*flags PUBLISH_EVENT_FLAG_PRIVATE*/1)) { + ncpFwUpdateState_ = FW_UPDATE_DOWNLOAD_CLOUD_DISCONNECT_STATE; + LOG(INFO, "Ready to start download..."); + cooldown_(20000); + } else { + static int publish_start_retries = 0; + if (++publish_start_retries < 2) { + ncpFwUpdateState_ = FW_UPDATE_SETUP_CLOUD_CONNECTING_STATE; + } else { + ncpFwUpdateState_ = FW_UPDATE_IDLE_STATE; + } + cooldown_(1000); + } + } else { + ncpFwUpdateState_ = FW_UPDATE_SETUP_CLOUD_CONNECTING_STATE; + } + } + break; + + case FW_UPDATE_DOWNLOAD_CLOUD_DISCONNECT_STATE: + { + if (ncpFwUpdateCallbacks_->spark_cloud_flag_connected()) { // Particle.connected() + ncpFwUpdateCallbacks_->spark_cloud_flag_disconnect(); // Particle.disconnect() + } else { + LOG(INFO, "Disconnected from Cloud."); + if (network_ready(0, 0, 0)) { // Cellular.ready() + LOG(INFO, "Disconnecting Cellular..."); + network_disconnect(0, NETWORK_DISCONNECT_REASON_USER, 0); + ncpFwUpdateState_ = FW_UPDATE_DOWNLOAD_CELL_DISCONNECTING_STATE; + cooldown_(10000); + } + } + } + break; + + case FW_UPDATE_DOWNLOAD_CELL_DISCONNECTING_STATE: + { + if (!network_ready(0, 0, 0)) { // Cellular.ready() + LOG(INFO, "Disconnected Cellular."); + ncpFwUpdateState_ = FW_UPDATE_DOWNLOAD_CELL_CONNECTING_STATE; + } + } + break; + + case FW_UPDATE_DOWNLOAD_CELL_CONNECTING_STATE: + { + // TODO: Add 10 minute timeout here + if (!network_ready(0, 0, 0)) { // Cellular.ready() + LOG(INFO, "Connecting Cellular..."); + cellular_start_ncp_firmware_update(true, nullptr); // ensure we don't connect PPP + network_connect(0, 0, 0, 0); // Cellular.connect() + ncpFwUpdateState_ = FW_UPDATE_DOWNLOAD_HTTPS_SETUP_STATE; + } + } + break; + + case FW_UPDATE_DOWNLOAD_HTTPS_SETUP_STATE: + { + static int https_setup_retries = 0; + int ret = setupHTTPSProperties_(); + if (ret) { + LOG(INFO, "HTTPS setup error: %d", ret); + if (++https_setup_retries > 2) { + ncpFwUpdateState_ = FW_UPDATE_IDLE_STATE; + } + cooldown_(10000); + break; + } + ncpFwUpdateState_ = FW_UPDATE_DOWNLOAD_READY_STATE; + } + break; + + case FW_UPDATE_DOWNLOAD_READY_STATE: + { + bool modemFirmwareDownloadComplete = false; + foatReady_ = 0; + memset(&g_httpsResp, 0, sizeof(g_httpsResp)); + +/* +a2773f2abb80df2886dd29b07f089504 SARA-R510S-00B-00_FW02.05_A00.01_IP_SARA-R510S-00B-01_FW02.06_A00.01_IP.upd +48b2d022041ea85899a15351c06a18d2 SARA-R510S-00B-01_FW02.06_A00.01_IP.dof +252ea04a324e9aab8a69678cfe097465 SARA-R510S-00B-01_FW02.06_A00.01_IP.upd +ccfdc48c0a45198d6e168b30d0740959 SARA-R510S-00B-01_FW02.06_A00.01_IP_SARA-R510S-00B-01_FW99.01_A00.01.upd +5fd6c0d3d731c097605895b86f28c2cf SARA-R510S-00B-01_FW99.01_A00.01_SARA-R510S-00B-01_FW02.06_A00.01_IP.upd +09c1a98d03c761bcbea50355f9b2a50f SARA-R510S-01B-00-ES-0314A0001-005K00_SARA-R510S-01B-00-XX-0314ENG0099A0001-005K00.upd +09c1a98d03c761bcbea50355f9b2a50f SARA-R510S-01B-00-ES-0314A0001_SARA-R510S-01B-00-XX-0314ENG0099A0001.upd +136caf2883457093c9e41fda3c6a44e3 SARA-R510S-01B-00-XX-0314ENG0099A0001-005K00_SARA-R510S-01B-00-ES-0314A0001-005K00.upd +136caf2883457093c9e41fda3c6a44e3 SARA-R510S-01B-00-XX-0314ENG0099A0001_SARA-R510S-01B-00-ES-0314A0001.upd + +*/ + // There doesn't appear to be a way to test what updatePackage.bin actually IS (MD5 sum or APP version) + // so if one exists, we must delete it before attempting to upgrade or else the FW INSTALL will + // basically end early without the right version being applied. + LOG(INFO, "Existing FOAT file?"); + foatReady_ = 0; + cellular_command((_CALLBACKPTR_MDM)cbULSTFILE_, (void*)&foatReady_, 10000, "AT+ULSTFILE=0,\"FOAT\"\r\n"); + if (foatReady_) { + if (RESP_OK == cellular_command(nullptr, nullptr, 10000, "AT+UDELFILE=\"updatePackage.bin\",\"FOAT\"\r\n")) { + LOG(INFO, "updatePackage.bin deleted."); + } + } + + auto mgr = cellularNetworkManager(); + auto client = mgr->ncpClient(); + auto parser = client->atParser(); + // +UUHTTPCR: 0,100,1,200,"ccfdc48c0a45198d6e168b30d0740959" + parser->addUrcHandler("+UUHTTPCR", httpRespCallback_, client); + + LOG(INFO, "Starting download..."); +// DEBUG set to 0 to disable download for testing +#if 1 + const int fwIdx = firmwareUpdateForVersion_(firmwareVersion_); + if (isUserConfig_ || fwIdx != SYSTEM_ERROR_NOT_FOUND) { + NcpFwUpdateConfig* configData; + if (isUserConfig_) { + configData = &ncpFwUpdateData_.var.userConfigData; + } else { + configData = (NcpFwUpdateConfig*)&NCP_FW_UPDATE_CONFIG[fwIdx]; + } + updateVersion_ = configData->end_version; + ncpFwUpdateData_.var.updateVersion = updateVersion_; + saveNcpFwUpdateData_(); + // 99010001 -> 02060001 firmware (takes 1.8 - 2.8 minutes) + int resp = cellular_command((_CALLBACKPTR_MDM)cbUUHTTPCR_, (void*)&g_httpsResp, 5*60000, "AT+UHTTPC=0,100,\"/%s\"\r\n", configData->filename); + if (resp != RESP_ERROR) { + if (g_httpsResp.command == 100 && g_httpsResp.result == 1 && g_httpsResp.status_code == 200 && strcmp(g_httpsResp.md5_sum, configData->md5sum)) { + modemFirmwareDownloadComplete = true; + } + if (!modemFirmwareDownloadComplete) { + system_tick_t start = HAL_Timer_Get_Milli_Seconds(); + while (HAL_Timer_Get_Milli_Seconds() - start < 300000 && !modemFirmwareDownloadComplete) { + if (g_httpsResp.command == 100 && g_httpsResp.result == 1 && g_httpsResp.status_code == 200 && !strcmp(g_httpsResp.md5_sum, configData->md5sum)) { + modemFirmwareDownloadComplete = true; + } + HAL_Delay_Milliseconds(100); + if (g_httpsResp.command == 100 && g_httpsResp.result == 0) { + break; + } + } + } + } + } +#else + // DEBUG + modemFirmwareDownloadComplete = true; +#endif + if (modemFirmwareDownloadComplete) { + LOG(INFO, "command: %d, result: %d, status_code: %d, md5:[%s]", + g_httpsResp.command, g_httpsResp.result, g_httpsResp.status_code, g_httpsResp.md5_sum); + LOG(INFO, "Download complete and verified."); + ncpFwUpdateState_ = FW_UPDATE_INSTALL_CELL_DISCONNECTING_STATE; + LOG(INFO, "Disconnecting Cellular..."); + network_disconnect(0, NETWORK_DISCONNECT_REASON_USER, 0); + } else { + LOG(INFO, "Download failed!!"); + cellular_command((_CALLBACKPTR_MDM)cbUHTTPER_, (void*)&g_httpsResp, 10000, "AT+UHTTPER=0\r\n"); + LOG(INFO, "UHTTPER class: %d, code: %d", + g_httpsResp.err_class, g_httpsResp.err_code); + static int download_retries = 0; + if (++download_retries >= 3) { + ncpFwUpdateState_ = FW_UPDATE_FINISHED_IDLE_STATE; + ncpFwUpdateStatus_ = FW_UPDATE_FAILED_DOWNLOAD_RETRY_MAX_STATUS; + } + } + cooldown_(10000); + } + break; + + case FW_UPDATE_INSTALL_CELL_DISCONNECTING_STATE: + { + // FIXME: This needs to wait for + // 0000091895 [ncp.at] TRACE: < +CGEV: ME PDN DEACT 1 + // 0000091896 [ncp.at] TRACE: < +CGEV: ME DETACH + // 0000091896 [ncp.at] TRACE: < +UUPSDD: 0 + // or + // COPS: 0 (no ACT) + if (!network_ready(0, 0, 0)) { // Cellular.ready() + LOG(INFO, "Disconnected Cellular."); + ncpFwUpdateState_ = FW_UPDATE_INSTALL_STATE_STARTING; + } + } + break; + + case FW_UPDATE_INSTALL_STATE_STARTING: + { + atOkCheckTimer_ = HAL_Timer_Get_Milli_Seconds(); + g_respCode = -1; + lastRespCode_ = -1; + LOG(INFO, "Installing firmware, prepare to wait 25 - 32 minutes..."); + ncpFwUpdateStatus_ = FW_UPDATE_UPDATING_STATUS; + +// DEBUG set to 0 to disable update command +#if 1 + if (RESP_OK == cellular_command(nullptr, nullptr, 60000, "AT+UFWINSTALL=1,115200\r\n")) { + ncpFwUpdateData_.var.state = FW_UPDATE_INSTALL_STATE_WAITING; + ncpFwUpdateData_.var.status = ncpFwUpdateStatus_; + saveNcpFwUpdateData_(); + // Wait for AT interface to become unresponsive, since we need to rely on that to break out of our install later + atResponsive_ = 1; + while (atResponsive_ && HAL_Timer_Get_Milli_Seconds() - atOkCheckTimer_ < NCP_FW_MODEM_INSTALL_START_TIMEOUT) { + atResponsive_ = (RESP_OK == cellular_command(nullptr, nullptr, 3000, "AT\r\n")); + if (atResponsive_) { + HAL_Delay_Milliseconds(3000); + } + } + if (atResponsive_) { + LOG(ERROR, "5 minute timeout waiting for INSTALL to start"); + ncpFwUpdateStatus_ = FW_UPDATE_FAILED_START_INSTALL_TIMEOUT_STATUS; + ncpFwUpdateState_ = FW_UPDATE_FINISHED_POWER_OFF_STATE; + } else { + cellular_start_ncp_firmware_update(true, nullptr); + ncpFwUpdateState_ = FW_UPDATE_INSTALL_STATE_WAITING; + } + } else { + LOG(ERROR, "AT+UFWINSTALL failed to respond"); + ncpFwUpdateStatus_ = FW_UPDATE_FAILED_INSTALL_AT_ERROR_STATUS; + ncpFwUpdateState_ = FW_UPDATE_FINISHED_POWER_OFF_STATE; + } + cooldown_(10000); +#else + // DEBUG + ncpFwUpdateState_ = FW_UPDATE_INSTALL_STATE_WAITING; + ncpFwUpdateStatus_ = FW_UPDATE_FAILED_STATUS; +#endif + } + break; + + case FW_UPDATE_INSTALL_STATE_WAITING: + { + startingFirmwareVersion_ = firmwareVersion_; + ncpFwUpdateData_.var.startingFirmwareVersion = startingFirmwareVersion_; + ncpFwUpdateData_.var.state = ncpFwUpdateState_; + ncpFwUpdateData_.var.status = ncpFwUpdateStatus_; + saveNcpFwUpdateData_(); + cellular_start_ncp_firmware_update(true, nullptr); // ensure lower ncp layers play nice + startInstallTimer_ = HAL_Timer_Get_Milli_Seconds(); + atOkCheckTimer_ = HAL_Timer_Get_Milli_Seconds(); + g_respCode = -1; + lastRespCode_ = -1; + // Wait for the install to take place, 18 - 20 or more minutes! + while (true) { + if (HAL_Timer_Get_Milli_Seconds() - atOkCheckTimer_ >= 10000) { + atOkCheckTimer_ = HAL_Timer_Get_Milli_Seconds(); + atResp_ = cellular_command(nullptr, nullptr, 1000, "AT\r\n"); + } + if (g_respCode != lastRespCode_) { + // Extend our timeout if we are receiving response updates + startInstallTimer_ = HAL_Timer_Get_Milli_Seconds(); + LOG(INFO, "INSTALL: %d%%", g_respCode); + lastRespCode_ = g_respCode; + if (g_respCode == NCP_FW_UUFWINSTALL_COMPLETE) { + ncpFwUpdateStatus_ = FW_UPDATE_SUCCESS_STATUS; + break; + } + } + if (atResp_ == RESP_OK) { + // We appear to be responsive again, if we missed the firmware UUFWINSTALL: 128 URC + // See if we can break out of here with verifying an updated or same as before FW revision + firmwareVersion_ = getAppFirmwareVersion_(); + LOG(INFO, "App fV: %d, sfV: %d, uV: %d", firmwareVersion_, startingFirmwareVersion_, updateVersion_); + if (firmwareVersion_ == updateVersion_) { + ncpFwUpdateStatus_ = FW_UPDATE_SUCCESS_STATUS; // force a FW update success status + break; + } else if (firmwareVersion_ == startingFirmwareVersion_) { + ncpFwUpdateStatus_ = FW_UPDATE_FAILED_SAME_VERSION_STATUS; + break; // fail early + } + HAL_Delay_Milliseconds(20000); // slow down the log output + } + if (HAL_Timer_Get_Milli_Seconds() - startInstallTimer_ >= NCP_FW_MODEM_INSTALL_FINISH_TIMEOUT) { + LOG(ERROR, "Install process timed out!"); + ncpFwUpdateStatus_ = FW_UPDATE_FAILED_INSTALL_TIMEOUT_STATUS; + } + } + + if (ncpFwUpdateStatus_ == FW_UPDATE_SUCCESS_STATUS) { + LOG(INFO, "Firware update success."); + } else { + LOG(ERROR, "Firmware update failed!!"); + } + // Power cycle the modem... who know's what odd state we are in. + ncpFwUpdateState_ = FW_UPDATE_FINISHED_POWER_OFF_STATE; + } + break; + + case FW_UPDATE_FINISHED_POWER_OFF_STATE: + { + LOG(INFO, "Power cycling modem and reconnecting to Cloud..."); + ncpFwUpdateData_.var.state = FW_UPDATE_FINISHED_POWER_OFF_STATE; + ncpFwUpdateData_.var.status = ncpFwUpdateStatus_; + saveNcpFwUpdateData_(); + cellular_start_ncp_firmware_update(false, nullptr); + network_off(NETWORK_INTERFACE_CELLULAR, 0, 0, nullptr); // Cellular.off() + startInstallTimer_ = HAL_Timer_Get_Milli_Seconds(); + ncpFwUpdateState_ = FW_UPDATE_FINISHED_POWERING_OFF_STATE; + } + break; + + case FW_UPDATE_FINISHED_POWERING_OFF_STATE: + { + if (HAL_Timer_Get_Milli_Seconds() - startInstallTimer_ > 60000) { + LOG(ERROR, "Powering modem off timed out!"); + ncpFwUpdateState_ = FW_UPDATE_FINISHED_IDLE_STATE; + ncpFwUpdateStatus_ = FW_UPDATE_FAILED_POWER_OFF_TIMEOUT_STATUS; + } + if (network_is_off(NETWORK_INTERFACE_CELLULAR, nullptr)) { + LOG(INFO, "Powering modem off success"); + startInstallTimer_ = HAL_Timer_Get_Milli_Seconds(); + ncpFwUpdateState_ = FW_UPDATE_FINISHED_CLOUD_CONNECTING_STATE; + ncpFwUpdateData_.var.state = FW_UPDATE_FINISHED_CLOUD_CONNECTING_STATE; + ncpFwUpdateData_.var.status = ncpFwUpdateStatus_; + saveNcpFwUpdateData_(); + } + } + break; + + case FW_UPDATE_FINISHED_CLOUD_CONNECTING_STATE: + { + if (HAL_Timer_Get_Milli_Seconds() - startInstallTimer_ >= NCP_FW_MODEM_CLOUD_CONNECT_TIMEOUT) { + static int cloud_connecting_retries = 0; + if (++cloud_connecting_retries < 2) { + ncpFwUpdateState_ = FW_UPDATE_FINISHED_POWER_OFF_STATE; + } else { + ncpFwUpdateState_ = FW_UPDATE_FINISHED_IDLE_STATE; + ncpFwUpdateStatus_ = FW_UPDATE_FAILED_CLOUD_CONNECT_TIMEOUT_STATUS; + } + } else if (ncpFwUpdateCallbacks_->spark_cloud_flag_connected()) { // Particle.connected() + ncpFwUpdateState_ = FW_UPDATE_FINISHED_CLOUD_CONNECTED_STATE; + cooldown_(5000); + } else { + ncpFwUpdateStatusDiagnostics_ = ncpFwUpdateStatus_; // ready our diagnostics status value before connecting to the cloud + ncpFwUpdateCallbacks_->spark_cloud_flag_connect(); // Particle.connect() + cooldown_(1000); + } + } + break; + + case FW_UPDATE_FINISHED_CLOUD_CONNECTED_STATE: + { + if (ncpFwUpdateCallbacks_->spark_cloud_flag_connected()) { // Particle.connected() + if (!ncpFwUpdateCallbacks_->publishEvent("spark/device/ncp/update", (ncpFwUpdateStatus_ == FW_UPDATE_SUCCESS_STATUS) ? "success" : "failed", /*flags PUBLISH_EVENT_FLAG_PRIVATE*/1)) { + ncpFwUpdateStatus_ = FW_UPDATE_FAILED_PUBLISH_RESULT_STATUS; + } + } else { + ncpFwUpdateStatus_ = FW_UPDATE_FAILED_PUBLISH_RESULT_STATUS; + } + ncpFwUpdateState_ = FW_UPDATE_FINISHED_IDLE_STATE; + } + break; + + case FW_UPDATE_FINISHED_IDLE_STATE: + { + ncpFwUpdateData_.var.state = FW_UPDATE_FINISHED_IDLE_STATE; + ncpFwUpdateData_.var.status = ncpFwUpdateStatus_; + saveNcpFwUpdateData_(); + ncpFwUpdateState_ = FW_UPDATE_IDLE_STATE; + } + break; + + case FW_UPDATE_IDLE_STATE: + default: + { + ncpFwUpdateState_ = FW_UPDATE_IDLE_STATE; + if (ncpFwUpdateStatus_ != FW_UPDATE_IDLE_STATUS && + system_mode() == System_Mode_TypeDef::SAFE_MODE) { + ncpFwUpdateData_.var.isUserConfig = false; + isUserConfig_ = false; + saveNcpFwUpdateData_(); + LOG(INFO, "Resetting out of Safe Mode!"); + HAL_Delay_Milliseconds(200); + // Reset with graceful disconnect from Cloud + system_reset(SYSTEM_RESET_MODE_NORMAL, 0, 0, 0, nullptr); + } + } + break; + } // switch end + } // Not in cooldown + + return SYSTEM_ERROR_NONE; +} + +// static +int NcpFwUpdate::cbUUHTTPCR_(int type, const char* buf, int len, HTTPSresponse* data) +{ + int command = 0; + int result = 0; + int status_code = 0; + char md5_sum[40] = {0}; + if (data && (type == TYPE_PLUS || type == TYPE_UNKNOWN)) { + // +UUHTTPCR: 0,100,1,200,"ccfdc48c0a45198d6e168b30d0740959" + if (sscanf(buf, "%*[\r\n]+UUHTTPCR: %*d,%d,%d,%d,\"%32s\"", &command, &result, &status_code, md5_sum) >= 3) { + data->command = command; + data->result = result; + data->status_code = status_code; + memcpy(data->md5_sum, &md5_sum, sizeof(md5_sum)); + } + } + return WAIT; +} + +// static +int NcpFwUpdate::cbUHTTPER_(int type, const char* buf, int len, HTTPSresponse* data) +{ + int err_class = 0; + int err_code = 0; + if (data && (type == TYPE_PLUS || type == TYPE_UNKNOWN)) { + // +UHTTPER: 0,10,22 + if (sscanf(buf, "%*[\r\n]+UHTTPER: %*d,%d,%d", &err_class, &err_code) >= 2) { + data->err_class = err_class; + data->err_code = err_code; + } + } + return WAIT; +} + +// static +int NcpFwUpdate::cbULSTFILE_(int type, const char* buf, int len, int* data) +{ + if (data && (type == TYPE_PLUS || type == TYPE_UNKNOWN)) { + if (strstr(buf, "+ULSTFILE: \"updatePackage.bin\"")) { + *data = 1; + } + } + return WAIT; +} + +// static +int NcpFwUpdate::cbATI9_(int type, const char* buf, int len, int* val) +{ + int major1 = 0; + int minor1 = 0; + int major2 = 0; + int minor2 = 0; + char eng[10] = {0}; + if (val && (type == TYPE_PLUS || type == TYPE_UNKNOWN)) { + if (sscanf(buf, "%*[\r\nL0.0.00.00.]%d.%d%*[,A.]%d.%d", &major1, &minor1, &major2, &minor2) == 4) { + *val = major1 * 1000000 + minor1 * 10000 + major2 * 100 + minor2; + } else if (sscanf(buf, "%*[\r\nL0.0.00.00.]%d.%d%8[^,]%*[,A.]%d.%d", &major1, &minor1, eng, &major2, &minor2) == 5) { + *val = major1 * 1000000 + minor1 * 10000 + major2 * 100 + minor2; + if (strstr(eng, "_ENG")) { + *val += NCP_FW_UBLOX_R510_ENG_VERSION; // Add leading 1 for _ENGxxxx firmware + } + } + } + return WAIT; +} + +// static +int NcpFwUpdate::cbUPSND_(int type, const char* buf, int len, int* data) +{ + int val = 0; + if (data && (type == TYPE_PLUS || type == TYPE_UNKNOWN)) { + // +UPSND: 0,8,1 - IP Connection + // +UPSND: 0,8,0 - No IP Connection + if (sscanf(buf, "%*[\r\n]+UPSND: %*d,%*d,%d", &val) >= 1) { + *data = val; + } + } + return WAIT; +} + +// static +int NcpFwUpdate::cbCOPS_(int type, const char* buf, int len, int* data) +{ + int act; + // +COPS: 0,2,"310410",7 + // +COPS: 0,0,"AT&T",7 + if (data && (type == TYPE_PLUS || type == TYPE_UNKNOWN)) { + if (sscanf(buf, "\r\n+COPS: %*d,%*d,\"%*6[0-9]\",%d", &act) >= 1) { + if (act >= 7) { + *data = 1; + } + } else if (sscanf(buf, "\r\n+COPS: %*d,%*d,\"%*[^\"]\",%d", &act) >= 1) { + if (act >= 7) { + *data = 1; + } + } + } + return WAIT; +} + +int NcpFwUpdate::getAppFirmwareVersion_() { + // ATI9 (get version and app version) + // example output + // "02.05,A00.01" R510 (older) - v2050001 + // "02.06,A00.01" R510 (newer) - v2060001 + // "03.15_ENG0001,A00.01" (engineering) - v103150001 + // "03.15,A00.01" (newest) - v3150001 + // "08.70,A00.02" G350 (older) - v8700002 + // "08.90,A01.13" G350 (newer) - v8900113 + // "L0.0.00.00.05.06,A.02.00" (memory issue) - v5060200 + // "L0.0.00.00.05.07,A.02.02" (demonstrator) - v5070202 + // "L0.0.00.00.05.08,A.02.04" (maintenance) - v5080204 + int appVer = 0; + cellular_command((_CALLBACKPTR_MDM)cbATI9_, (void*)&appVer, 10000, "ATI9\r\n"); + return appVer; +} + +int NcpFwUpdate::setupHTTPSProperties_() { + // Wait for registration, using COPS AcT to determine this instead of CEREG. + unsigned int registered = 0; + uint32_t start = HAL_Timer_Get_Milli_Seconds(); + do { + cellular_command((_CALLBACKPTR_MDM)cbCOPS_, (void*)®istered, 10000, "AT+COPS?\r\n"); + if (!registered) HAL_Delay_Milliseconds(15000); + } while (!registered && HAL_Timer_Get_Milli_Seconds() - start < 15 * 60 * 1000); + + cellular_command(nullptr, nullptr, 10000, "AT+CMEE=2\r\n"); + cellular_command(nullptr, nullptr, 10000, "AT+CGPADDR=0\r\n"); + // Activate data connection + int actVal = 0; + cellular_command((_CALLBACKPTR_MDM)cbUPSND_, (void*)&actVal, 10000, "AT+UPSND=0,8\r\n"); + if (actVal != 1) { + cellular_command(nullptr, nullptr, 10000, "AT+UPSD=0,100,1\r\n"); + cellular_command(nullptr, nullptr, 10000, "AT+UPSD=0,0,0\r\n"); + int r = cellular_command(nullptr, nullptr, 180000, "AT+UPSDA=0,3\r\n"); + if (r == RESP_ERROR) { + cellular_command(nullptr, nullptr, 10000, "AT+CEER\r\n"); + } + } + + // Setup security settings + LOG(INFO, "setupHTTPSProperties_"); + if (RESP_OK != cellular_command(nullptr, nullptr, 10000, "AT+USECPRF=2,0,3\r\n")) return -1; // Highest level (3) root cert checks + if (RESP_OK != cellular_command(nullptr, nullptr, 10000, "AT+USECPRF=2,1,3\r\n")) return -2; // Minimum TLS v1.2 + if (RESP_OK != cellular_command(nullptr, nullptr, 10000, "AT+USECPRF=2,2,99,\"C0\",\"2F\"\r\n")) return -3; // Cipher suite + if (RESP_OK != cellular_command(nullptr, nullptr, 10000, "AT+USECPRF=2,3,\"ubx_digicert_global_root_ca\"\r\n")) return -4; // Cert name + if (RESP_OK != cellular_command(nullptr, nullptr, 10000, "AT+USECPRF=2,4,\"fw-ftp.staging.particle.io\"\r\n")) return -5; // Expected server hostname + if (RESP_OK != cellular_command(nullptr, nullptr, 10000, "AT+USECPRF=2,10,\"fw-ftp.staging.particle.io\"\r\n")) return -6; // SNI (Server Name Indication) + if (RESP_OK != cellular_command(nullptr, nullptr, 10000, "AT+UHTTP=0,1,\"fw-ftp.staging.particle.io\"\r\n")) return -7; + if (RESP_OK != cellular_command(nullptr, nullptr, 10000, "AT+UHTTP=0,5,443\r\n")) return -8; + if (RESP_OK != cellular_command(nullptr, nullptr, 10000, "AT+UHTTP=0,6,1,2\r\n")) return -9; + return 0; +} + +// static +int NcpFwUpdate::httpRespCallback_(AtResponseReader* reader, const char* prefix, void* data) { + // const auto self = (SaraNcpClient*)data; + int a, b, c; + char s[40]; + char atResponse[64] = {}; + // FIXME: Can't get CHECK_PARSER_URC to work, do we need self->parserError(_r); ? + const auto resp = reader->readLine(atResponse, sizeof(atResponse)); + if (resp < 0) { + return resp; + } + int r = ::sscanf(atResponse, "+UUHTTPCR: %*d,%d,%d,%d,\"%32s\"", &a, &b, &c, s); + CHECK_TRUE(r >= 3, SYSTEM_ERROR_AT_RESPONSE_UNEXPECTED); + + g_httpsResp.command = a; + g_httpsResp.result = b; + g_httpsResp.status_code = c; + memcpy(g_httpsResp.md5_sum, &s, sizeof(s)); + LOG(INFO, "UUHTTPCR matched"); + + return SYSTEM_ERROR_NONE; +} + +void NcpFwUpdate::cooldown_(system_tick_t timer) { + cooldownTimeout_ = timer; + cooldownTimer_ = HAL_Timer_Get_Milli_Seconds(); +} +void NcpFwUpdate::updateCooldown_() { + if (cooldownTimer_ && HAL_Timer_Get_Milli_Seconds() - cooldownTimer_ >= cooldownTimeout_) { + cooldownTimer_ = 0; + cooldownTimeout_ = 0; + } +} +bool NcpFwUpdate::inCooldown_() { + return cooldownTimer_ > 0; +} + +} // namespace services + +} // namespace particle + +int ncp_fw_udpate_check(const NcpFwUpdateConfig* userConfigData, void* reserved) { + // LOG(INFO,"CHECK"); + return particle::services::NcpFwUpdate::instance()->checkUpdate(userConfigData); +} + +#else // #if HAL_PLATFORM_NCP_FW_UPDATE + +int ncp_fw_udpate_check(const NcpFwUpdateConfig* userConfigData, void* reserved) { + return 0; +} + +#endif // #if HAL_PLATFORM_NCP_FW_UPDATE diff --git a/system/inc/system_update.h b/system/inc/system_update.h index c47709a05b..fc97c06a76 100644 --- a/system/inc/system_update.h +++ b/system/inc/system_update.h @@ -76,73 +76,6 @@ int Spark_Finish_Firmware_Update(FileTransfer::Descriptor& file, uint32_t flags, */ int Spark_Save_Firmware_Chunk(FileTransfer::Descriptor& file, const uint8_t* chunk, void* reserved); -typedef enum -{ - /** - * When 0, no OTA update is pending. - * When 1, an OTA update is pending, and will start when the SYSTEM_FLAG_OTA_UPDATES_FLAG - * is set. - */ - SYSTEM_FLAG_OTA_UPDATE_PENDING, - - /** - * When 0, OTA updates are not started. - * When 1, OTA updates are started. Default. - */ - SYSTEM_FLAG_OTA_UPDATE_ENABLED, - - /* - * When 0, no reset is pending. - * When 1, a reset is pending. The system will perform the reset - * when SYSTEM_FLAG_RESET_ENABLED is set to 1. - */ - SYSTEM_FLAG_RESET_PENDING, - - /** - * When 0, the system is not able to perform a system reset. - * When 1, thee system will reset the device when a reset is pending. - */ - SYSTEM_FLAG_RESET_ENABLED, - - /** - * A persistent flag that when set will cause the system to startup - * in listening mode. The flag is automatically cleared on reboot. - */ - SYSTEM_FLAG_STARTUP_LISTEN_MODE, - - /** - * Enable/Disable use of serial1 during setup. - */ - SYSTEM_FLAG_WIFITESTER_OVER_SERIAL1, - - /** - * Enable/disable publishing of last reset info to the cloud. - */ - SYSTEM_FLAG_PUBLISH_RESET_INFO, - - /** - * When 0, the system doesn't reset network connection on cloud connection errors. - * When 1 (default), the system resets network connection after a number of failed attempts to - * connect to the cloud. - */ - SYSTEM_FLAG_RESET_NETWORK_ON_CLOUD_ERRORS, - - /** - * Enable/Disable runtime power management peripheral detection - */ - SYSTEM_FLAG_PM_DETECTION, - - /** - * When 0, OTA updates are only applied when SYSTEM_FLAG_OTA_UPDATE_ENABLED is set. - * When 1, OTA updates are applied irrespective of the value of SYSTEM_FLAG_OTA_UPDATE_ENABLED. - */ - SYSTEM_FLAG_OTA_UPDATE_FORCED, - - SYSTEM_FLAG_MAX - -} system_flag_t; - - void system_shutdown_if_needed(); void system_pending_shutdown(System_Reset_Reason reason); diff --git a/system/src/main.cpp b/system/src/main.cpp index ea8399e768..5d4114a8e6 100644 --- a/system/src/main.cpp +++ b/system/src/main.cpp @@ -85,6 +85,14 @@ #include "network/ncp/cellular/ncp.h" #endif +#if HAL_PLATFORM_NCP_FW_UPDATE +#include "ncp_fw_update.h" + +// // FIXME: DEBUG!!!!! +// #include "debug_output_handler.h" +// spark::Serial1LogHandler g_logHandlerSerial1(115200, LOG_LEVEL_ALL); // FIXME: DEBUG!!!!! +#endif + #if HAL_PLATFORM_RADIO_STACK #include "radio_common.h" #endif @@ -798,6 +806,19 @@ void app_setup_and_loop(void) Network_Setup(threaded); // todo - why does this come before system thread initialization? +#if HAL_PLATFORM_NCP_FW_UPDATE + NcpFwUpdateCallbacks ncpFwUpdateCallbacks; + memset(&ncpFwUpdateCallbacks, 0, sizeof(ncpFwUpdateCallbacks)); + ncpFwUpdateCallbacks.size = sizeof(ncpFwUpdateCallbacks); + ncpFwUpdateCallbacks.system_get_flag = system_get_flag; + ncpFwUpdateCallbacks.spark_cloud_flag_connected = spark_cloud_flag_connected; + ncpFwUpdateCallbacks.spark_cloud_flag_connect = spark_cloud_flag_connect; + ncpFwUpdateCallbacks.spark_cloud_flag_disconnect = spark_cloud_flag_disconnect; + ncpFwUpdateCallbacks.publishEvent = publishEvent; + ncpFwUpdateCallbacks.system_mode = system_mode; + services::NcpFwUpdate::instance()->init(&ncpFwUpdateCallbacks); +#endif + #if PLATFORM_THREADING if (threaded) { diff --git a/system/src/system_task.cpp b/system/src/system_task.cpp index 9793ba5a80..bd1fc27d0a 100644 --- a/system/src/system_task.cpp +++ b/system/src/system_task.cpp @@ -53,6 +53,7 @@ #include "system_threading.h" #include "spark_wiring_interrupts.h" #include "spark_wiring_led.h" +#include "ncp_fw_update.h" #if HAL_PLATFORM_BLE #include "ble_hal.h" @@ -489,6 +490,10 @@ void Spark_Idle_Events(bool force_events/*=false*/) manage_cloud_connection(force_events); system::FirmwareUpdate::instance()->process(); + +#if HAL_PLATFORM_NCP_FW_UPDATE + services::NcpFwUpdate::instance()->process(); +#endif } else {