diff --git a/ci/unit_tests.sh b/ci/unit_tests.sh index 8869b20222..f2eabb0d8b 100755 --- a/ci/unit_tests.sh +++ b/ci/unit_tests.sh @@ -24,7 +24,7 @@ rm -rf .build/* mkdir -p .build/ cd .build/ cmake $cmake_args .. -make all test coverage +make all test coverage CTEST_OUTPUT_ON_FAILURE=TRUE cmake_unit_tests=$? diff --git a/hal/inc/cellular_hal.h b/hal/inc/cellular_hal.h index 26c8a32371..2198bf2779 100644 --- a/hal/inc/cellular_hal.h +++ b/hal/inc/cellular_hal.h @@ -265,7 +265,32 @@ cellular_result_t cellular_process(void* reserved, void* reserved1); /** * Start NCP FW Update */ -int cellular_start_ncp_firmware_update(bool update = false, void* reserved = NULL); +int cellular_start_ncp_firmware_update(bool update, void* reserved); + +/** + * Get NCP firmware version as a uint32_t + */ +int cellular_get_ncp_firmware_version(uint32_t* version, void* reserved); + +/** + * Get modem firmware update status result + */ +int cellular_update_status(void* reserved); + +/** + * Enable modem firmware updates (blocking call, requires a pending update) + */ +int cellular_enable_updates(void* reserved); + +/** + * Add URC handler + */ +int cellular_add_urc_handler(const char* prefix, hal_cellular_urc_callback_t cb, void* context); + +/** + * Remove URC handler + */ +int cellular_remove_urc_handler(const char* prefix); #ifdef __cplusplus } diff --git a/hal/inc/cellular_hal_constants.h b/hal/inc/cellular_hal_constants.h index 9e7da6fed9..7fb9b0b5d2 100644 --- a/hal/inc/cellular_hal_constants.h +++ b/hal/inc/cellular_hal_constants.h @@ -37,6 +37,8 @@ typedef int (*_CALLBACKPTR_MDM)(int type, const char* buf, int len, void* param) typedef void (*_CELLULAR_SMS_CB_MDM)(void* data, int index); +typedef int (*hal_cellular_urc_callback_t)(const char* data, void* context); + #ifdef __cplusplus // Todo - is storing raw string pointers correct here? These will only be valid // If they are stored as constants in the application. diff --git a/hal/inc/hal_dynalib_cellular.h b/hal/inc/hal_dynalib_cellular.h index 2c00e1c472..5edd6d98d6 100644 --- a/hal/inc/hal_dynalib_cellular.h +++ b/hal/inc/hal_dynalib_cellular.h @@ -84,6 +84,8 @@ DYNALIB_FN(36, hal_cellular, cellular_credentials_clear, int(void*)) DYNALIB_FN(37, hal_cellular, cellular_global_identity, cellular_result_t(CellularGlobalIdentity*, void*)) DYNALIB_FN(38, hal_cellular, cellular_registration_timeout_set, cellular_result_t(system_tick_t, void*)) DYNALIB_FN(39, hal_cellular, cellular_urcs, cellular_result_t(bool, void*)) +DYNALIB_FN(40, hal_cellular, cellular_update_status, int(void*)) +DYNALIB_FN(41, hal_cellular, cellular_enable_updates, int(void*)) DYNALIB_END(hal_cellular) diff --git a/hal/inc/hal_platform.h b/hal/inc/hal_platform.h index 73c5e15bc1..5007286dd8 100644 --- a/hal/inc/hal_platform.h +++ b/hal/inc/hal_platform.h @@ -409,6 +409,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/hal/network/ncp/cellular/cellular_hal.cpp b/hal/network/ncp/cellular/cellular_hal.cpp index 26e31000e6..d48fd0ff54 100644 --- a/hal/network/ncp/cellular/cellular_hal.cpp +++ b/hal/network/ncp/cellular/cellular_hal.cpp @@ -23,6 +23,7 @@ #include "ifapi.h" #include "system_network.h" // FIXME: For network_interface_index +#include "spark_wiring_vector.h" #include "str_util.h" #include "endian_util.h" @@ -35,6 +36,8 @@ #include "cellular_enums_hal.h" #include "cellular_ncp_dev_mapping.h" +#include "ncp_fw_update.h" + #include namespace { @@ -106,6 +109,37 @@ hal_net_access_tech_t fromCellularAccessTechnology(CellularAccessTechnology rat) } } +struct CellularHalUrcHandler { + CellularHalUrcHandler(const char* prefix, hal_cellular_urc_callback_t callback, void* context) : + prefix(prefix), + callback(callback), + context(context) { + } + const char* prefix; + hal_cellular_urc_callback_t callback; + void* context; +}; + +Vector> sUrcHandlers; + +static int commonUrcHandler(AtResponseReader* reader, const char* prefix, void* data) { + auto handler = static_cast(data); + + const size_t atResponseSize = 64; + std::unique_ptr atResponse(new(std::nothrow) char[atResponseSize]); + CHECK_TRUE(atResponse.get(), SYSTEM_ERROR_NO_MEMORY); + + const auto n = reader->readLine(atResponse.get(), atResponseSize - 1); + if (n < 0) { + return n; + } + atResponse[n] = '\0'; + handler->callback(atResponse.get(), handler->context); + atResponse.reset(); + + return SYSTEM_ERROR_NONE; +} + } // unnamed int cellular_on(void* reserved) { @@ -506,6 +540,43 @@ int cellular_command(_CALLBACKPTR_MDM cb, void* param, system_tick_t timeout_ms, return mdmTypeToResult(mdmType); } +int cellular_add_urc_handler(const char* prefix, hal_cellular_urc_callback_t cb, void* context) { + const auto mgr = cellularNetworkManager(); + CHECK_TRUE(mgr, SYSTEM_ERROR_UNKNOWN); + const auto client = mgr->ncpClient(); + CHECK_TRUE(client, SYSTEM_ERROR_UNKNOWN); + const auto parser = client->atParser(); + + const NcpClientLock lock(client); + + auto entry = std::make_unique(prefix, cb, context); + CHECK_TRUE(entry, SYSTEM_ERROR_NO_MEMORY); + sUrcHandlers.append(std::move(entry)); + auto handler = sUrcHandlers.last().get(); + + return parser->addUrcHandler(prefix, commonUrcHandler, handler); +} + +int cellular_remove_urc_handler(const char* prefix) { + const auto mgr = cellularNetworkManager(); + CHECK_TRUE(mgr, SYSTEM_ERROR_UNKNOWN); + const auto client = mgr->ncpClient(); + CHECK_TRUE(client, SYSTEM_ERROR_UNKNOWN); + const auto parser = client->atParser(); + + const NcpClientLock lock(client); + + parser->removeUrcHandler(prefix); + for (int i = 0; i < sUrcHandlers.size(); ++i) { + if (strcmp(sUrcHandlers.at(i).get()->prefix, prefix) == 0) { + sUrcHandlers.removeAt(i); + break; + } + } + + return SYSTEM_ERROR_NONE; +} + int cellular_data_usage_set(CellularDataHal* data, void* reserved) { return SYSTEM_ERROR_NOT_SUPPORTED; } @@ -608,3 +679,28 @@ int cellular_start_ncp_firmware_update(bool update, void* reserved) { CHECK(client->startNcpFwUpdate(update)); return SYSTEM_ERROR_NONE; } + +int cellular_get_ncp_firmware_version(uint32_t* version, void* reserved) { + const auto mgr = cellularNetworkManager(); + CHECK_TRUE(mgr, SYSTEM_ERROR_UNKNOWN); + const auto client = mgr->ncpClient(); + CHECK_TRUE(client, SYSTEM_ERROR_UNKNOWN); + CHECK(client->getNcpFirmwareVersion(version)); + return SYSTEM_ERROR_NONE; +} + +int cellular_update_status(void* reserved) { +#if HAL_PLATFORM_NCP_FW_UPDATE + return services::SaraNcpFwUpdate::instance()->updateStatus(); +#else + return SYSTEM_ERROR_NOT_SUPPORTED; +#endif +} + +int cellular_enable_updates(void* reserved) { +#if HAL_PLATFORM_NCP_FW_UPDATE + return services::SaraNcpFwUpdate::instance()->enableUpdates(); +#else + return SYSTEM_ERROR_NOT_SUPPORTED; +#endif +} diff --git a/hal/network/ncp/cellular/cellular_ncp_client.h b/hal/network/ncp/cellular/cellular_ncp_client.h index fba676fdbd..bca090ee04 100644 --- a/hal/network/ncp/cellular/cellular_ncp_client.h +++ b/hal/network/ncp/cellular/cellular_ncp_client.h @@ -133,6 +133,7 @@ class CellularNcpClient: public NcpClient { virtual int getMtu() = 0; virtual int urcs(bool enable) = 0; virtual int startNcpFwUpdate(bool update) = 0; + virtual int getNcpFirmwareVersion(uint32_t* version) = 0; }; inline CellularNcpClientConfig::CellularNcpClientConfig() : diff --git a/hal/network/ncp_client/quectel/quectel_ncp_client.cpp b/hal/network/ncp_client/quectel/quectel_ncp_client.cpp index 0f71e32636..2a5cf47f4a 100644 --- a/hal/network/ncp_client/quectel/quectel_ncp_client.cpp +++ b/hal/network/ncp_client/quectel/quectel_ncp_client.cpp @@ -1590,6 +1590,10 @@ int QuectelNcpClient::startNcpFwUpdate(bool update) { return 0; } +int QuectelNcpClient::getNcpFirmwareVersion(uint32_t* version) { + return 0; +} + void QuectelNcpClient::connectionState(NcpConnectionState state) { if (ncpState_ == NcpState::DISABLED) { return; diff --git a/hal/network/ncp_client/quectel/quectel_ncp_client.h b/hal/network/ncp_client/quectel/quectel_ncp_client.h index 811329e8c4..9cc2a6906e 100644 --- a/hal/network/ncp_client/quectel/quectel_ncp_client.h +++ b/hal/network/ncp_client/quectel/quectel_ncp_client.h @@ -74,6 +74,7 @@ class QuectelNcpClient: public CellularNcpClient { virtual int getMtu() override; virtual int urcs(bool enable) override; virtual int startNcpFwUpdate(bool update) override; + virtual int getNcpFirmwareVersion(uint32_t* version) override; auto getMuxer() { return &muxer_; diff --git a/hal/network/ncp_client/sara/sara_ncp_client.cpp b/hal/network/ncp_client/sara/sara_ncp_client.cpp index 1329d9f4b3..db2860f221 100644 --- a/hal/network/ncp_client/sara/sara_ncp_client.cpp +++ b/hal/network/ncp_client/sara/sara_ncp_client.cpp @@ -46,6 +46,8 @@ LOG_SOURCE_CATEGORY("ncp.client"); #include #include "enumclass.h" +#include "ncp_fw_update.h" + #undef LOG_COMPILE_TIME_LEVEL #define LOG_COMPILE_TIME_LEVEL LOG_LEVEL_ALL @@ -89,14 +91,19 @@ inline system_tick_t millis() { return HAL_Timer_Get_Milli_Seconds(); } +inline uint32_t compileModemFwVersion(uint32_t major1, uint32_t minor1, uint32_t major2, uint32_t minor2) { + return major1 * 10000000 + minor1 * 100000 + major2 * 1000 + minor2 * 10; +} + const auto UBLOX_NCP_DEFAULT_SERIAL_BAUDRATE = 115200; const auto UBLOX_NCP_RUNTIME_SERIAL_BAUDRATE_U2 = 921600; const auto UBLOX_NCP_RUNTIME_SERIAL_BAUDRATE_R4 = 460800; -const auto UBLOX_NCP_R4_APP_FW_VERSION_MEMORY_LEAK_ISSUE = 200; -const auto UBLOX_NCP_R4_APP_FW_VERSION_NO_HW_FLOW_CONTROL_MIN = 200; -const auto UBLOX_NCP_R4_APP_FW_VERSION_NO_HW_FLOW_CONTROL_MAX = 203; -const auto UBLOX_NCP_R4_APP_FW_VERSION_LATEST_02B_01 = 204; -const auto UBLOX_NCP_R4_APP_FW_VERSION_0512 = 219; +const uint32_t UBLOX_NCP_R4_APP_FW_VERSION_MEMORY_LEAK_ISSUE = 50602000; // L0.0.00.00.05.06,A.02.00 +const uint32_t UBLOX_NCP_R4_APP_FW_VERSION_NO_HW_FLOW_CONTROL_MIN = 50602000; // L0.0.00.00.05.06,A.02.00 +const uint32_t UBLOX_NCP_R4_APP_FW_VERSION_NO_HW_FLOW_CONTROL_MAX = 50802040; // less than L0.0.00.00.05.08,A.02.04 +const uint32_t UBLOX_NCP_R4_APP_FW_VERSION_LATEST_02B_01 = 50802040; // L0.0.00.00.05.08,A.02.04 +const uint32_t UBLOX_NCP_R4_APP_FW_VERSION_0512 = 51202190; // L0.0.00.00.05.12,A.02.19 +const uint32_t UBLOX_ENG_VERSION = 1; // gets added to version to signify _ENGxxxx release const auto UBLOX_NCP_MAX_MUXER_FRAME_SIZE = 1509; const auto UBLOX_NCP_KEEPALIVE_PERIOD = 5000; // milliseconds @@ -179,11 +186,11 @@ int SaraNcpClient::init(const NcpClientConfig& conf) { powerOnTime_ = 0; registeredTime_ = 0; if (ncpId() == PLATFORM_NCP_SARA_R410) { - memoryIssuePresent_ = true; // default to safe state until we determine modem firmware version - oldFirmwarePresent_ = true; // default to safe state until we determine modem firmware version + r410MemoryIssuePresent_ = true; // default to safe state until we determine modem firmware version + r410OldFirmwarePresent_ = true; // default to safe state until we determine modem firmware version } else { - memoryIssuePresent_ = false; - oldFirmwarePresent_ = false; + r410MemoryIssuePresent_ = false; + r410OldFirmwarePresent_ = false; } parserError_ = 0; ready_ = false; @@ -350,6 +357,44 @@ int SaraNcpClient::initParser(Stream* stream) { } return SYSTEM_ERROR_NONE; }, this)); + // +UFWPREVAL: + CHECK(parser_.addUrcHandler("+UFWPREVAL", [](AtResponseReader* reader, const char* prefix, void* data) -> int { + const auto self = (SaraNcpClient*)data; + int respCode = -1; + char atResponse[32] = {}; + CHECK_PARSER_URC(reader->readLine(atResponse, sizeof(atResponse))); + int r = ::sscanf(atResponse, "+UFWPREVAL: %d", &respCode); + CHECK_TRUE(r >= 1, SYSTEM_ERROR_AT_RESPONSE_UNEXPECTED); + + if (respCode >= 0 && respCode <= 100) { + self->firmwareInstallRespCodeR510_ = respCode; + self->firmwareUpdateR510_ = true; + } + LOG_DEBUG(TRACE, "UFWPREVAL matched: %d", self->firmwareInstallRespCodeR510_); + + return SYSTEM_ERROR_NONE; + }, this)); + // +UUFWINSTALL: + CHECK(parser_.addUrcHandler("+UUFWINSTALL", [](AtResponseReader* reader, const char* prefix, void* data) -> int { + const auto self = (SaraNcpClient*)data; + int respCode = -1; + char atResponse[32] = {}; + CHECK_PARSER_URC(reader->readLine(atResponse, sizeof(atResponse))); + int r = ::sscanf(atResponse, "+UUFWINSTALL: %d", &respCode); + CHECK_TRUE(r >= 1, SYSTEM_ERROR_AT_RESPONSE_UNEXPECTED); + + if (respCode >= 0 && respCode <= UUFWINSTALL_COMPLETE) { + self->firmwareInstallRespCodeR510_ = respCode; + if (respCode == UUFWINSTALL_COMPLETE) { + self->firmwareUpdateR510_ = false; + } else { + self->firmwareUpdateR510_ = true; + } + } + LOG_DEBUG(TRACE, "UUFWINSTALL matched: %d", self->firmwareInstallRespCodeR510_); + + return SYSTEM_ERROR_NONE; + }, this)); return SYSTEM_ERROR_NONE; } @@ -504,7 +549,7 @@ int SaraNcpClient::dataChannelWrite(int id, const uint8_t* data, size_t size) { CHECK_TRUE(connState_ == NcpConnectionState::CONNECTED, SYSTEM_ERROR_INVALID_STATE); CHECK_FALSE(muxerDataStream_->enabled(), SYSTEM_ERROR_INVALID_STATE); - if (ncpId() == PLATFORM_NCP_SARA_R410 && fwVersion_ <= UBLOX_NCP_R4_APP_FW_VERSION_NO_HW_FLOW_CONTROL_MAX) { + if (ncpId() == PLATFORM_NCP_SARA_R410 && fwVersion_ < UBLOX_NCP_R4_APP_FW_VERSION_NO_HW_FLOW_CONTROL_MAX) { if ((HAL_Timer_Get_Milli_Seconds() - lastWindow_) >= UBLOX_NCP_R4_WINDOW_SIZE_MS) { const int windowCount = ((HAL_Timer_Get_Milli_Seconds() - lastWindow_) / UBLOX_NCP_R4_WINDOW_SIZE_MS); lastWindow_ += UBLOX_NCP_R4_WINDOW_SIZE_MS * windowCount; @@ -527,7 +572,7 @@ int SaraNcpClient::dataChannelWrite(int id, const uint8_t* data, size_t size) { LOG_DEBUG(WARN, "Remote side flow control"); err = 0; } - if (ncpId() == PLATFORM_NCP_SARA_R410 && fwVersion_ <= UBLOX_NCP_R4_APP_FW_VERSION_NO_HW_FLOW_CONTROL_MAX) { + if (ncpId() == PLATFORM_NCP_SARA_R410 && fwVersion_ < UBLOX_NCP_R4_APP_FW_VERSION_NO_HW_FLOW_CONTROL_MAX) { bytesInWindow_ += size; } if (err) { @@ -633,7 +678,7 @@ int SaraNcpClient::getImei(char* buf, size_t size) { } int SaraNcpClient::getTxDelayInDataChannel() { - if (ncpId() == PLATFORM_NCP_SARA_R410 && fwVersion_ <= UBLOX_NCP_R4_APP_FW_VERSION_NO_HW_FLOW_CONTROL_MAX) { + if (ncpId() == PLATFORM_NCP_SARA_R410 && fwVersion_ < UBLOX_NCP_R4_APP_FW_VERSION_NO_HW_FLOW_CONTROL_MAX) { return UBLOX_NCP_R4_WINDOW_SIZE_MS * 2; } return SYSTEM_ERROR_NONE; @@ -1124,7 +1169,7 @@ int SaraNcpClient::selectNetworkProf(ModemState& state) { if (netConf_.netProv() == CellularNetworkProvider::TWILIO) { // _oldFirmwarePresent: u-blox firmware 05.06* and 05.07* does not have // UMNOPROF=100 available. Default to UMNOPROF=0 in that case. - if (oldFirmwarePresent_) { + if (r410OldFirmwarePresent_) { if (static_cast(curProf) == UbloxSaraUmnoprof::SW_DEFAULT) { break; } else { @@ -1137,7 +1182,8 @@ int SaraNcpClient::selectNetworkProf(ModemState& state) { // KORE AT&T or 3rd Party SIM else { // Hard code ATT for R410 05.12 firmware versions or R510 Kore AT&T SIMs - if (fwVersion_ == UBLOX_NCP_R4_APP_FW_VERSION_0512 || ncpId() == PLATFORM_NCP_SARA_R510) { + if ((ncpId() == PLATFORM_NCP_SARA_R410 && fwVersion_ >= UBLOX_NCP_R4_APP_FW_VERSION_0512) + || ncpId() == PLATFORM_NCP_SARA_R510) { if (netConf_.netProv() == CellularNetworkProvider::KORE_ATT) { newProf = static_cast(UbloxSaraUmnoprof::ATT); } @@ -1440,34 +1486,80 @@ int SaraNcpClient::changeBaudRate(unsigned int baud) { return serial_->setBaudRate(baud); } -int SaraNcpClient::getAppFirmwareVersion() { +// public +int SaraNcpClient::getNcpFirmwareVersion(uint32_t* version) { + const NcpClientLock lock(this); + // ATI9 (get version and app version) // example output - // v113: "08.90,A01.13" G350 (newer) - // v2: "08.70,A00.02" G350 (older) - // v200: "L0.0.00.00.05.06,A.02.00" (R410 memory issue) - // v202: "L0.0.00.00.05.07,A.02.02" (R410 demonstrator) - // v204: "L0.0.00.00.05.08,A.02.04" (R410 maintenance) - // v1: "02.06,A00.01" (R510) + // "02.05,A00.01" R510 (older) - v20500010 + // "02.06,A00.01" R510 (newer) - v20600010 + // "03.15_ENG0001,A00.01" (engineering) - v31500011 + // "03.15,A00.01" (newest) - v31500010 + // "99.01,A00.01" (certification) - v990100010 + // "08.70,A00.02" G350 (older) - v87000020 + // "08.90,A01.13" G350 (newer) - v89001130 + // "L0.0.00.00.05.06,A.02.00" (memory issue) - v50602000 + // "L0.0.00.00.05.07,A.02.02" (demonstrator) - v50702020 + // "L0.0.00.00.05.08,A.02.04" (maintenance) - v50802040 + // Encoded as: + // Max Long Unsigned Int = 4294967295 + // RAABBCCDDE + // |||||||||| + // |||||||||E = 1 for _ENGxxxx version + // |||||||DD- = minor2 + // |||||CC--- = major2 + // |||BB----- = minor1 + // |AA------- = major1 + // R--------- = reserved + uint32_t major1 = 0; + uint32_t minor1 = 0; + uint32_t major2 = 0; + uint32_t minor2 = 0; + uint32_t ver = 0; + char eng[11] = {}; + char atResponse[64] = {}; auto resp = parser_.sendCommand("ATI9"); - int ver = 0; - int major = 0; - int minor = 0; - int n = CHECK_PARSER(resp.scanf("%*[^,],%*[A.]%d.%d", &major, &minor)); + CHECK_PARSER(resp.readLine(atResponse, sizeof(atResponse))); + if (::sscanf(atResponse, "L0.0.00.00.%lu.%lu%*[,A.]%lu.%lu", &major1, &minor1, &major2, &minor2) == 4) { + ver = compileModemFwVersion(major1, minor1, major2, minor2); + } else if (::sscanf(atResponse, "%lu.%lu%*[,A.]%lu.%lu", &major1, &minor1, &major2, &minor2) == 4) { + ver = compileModemFwVersion(major1, minor1, major2, minor2); + } else if (::sscanf(atResponse, "%lu.%lu%10[^,]%*[,A.]%lu.%lu", &major1, &minor1, eng, &major2, &minor2) == 5) { + // Accept 8 chars _ENG0001 up to 10 chars _ENG000001 + ver = compileModemFwVersion(major1, minor1, major2, minor2); + if (strstr(eng, "_ENG")) { + ver += UBLOX_ENG_VERSION; // Add leading 1 for _ENGxxxx firmware + } + } int r = resp.readResult(); - if (r == AtResponse::OK && n == 2) { - ver = major * 100 + minor; + if (r != AtResponse::OK) { + ver = 0; } - LOG(TRACE, "App firmware: %d", ver); // will be reported as 0 in case of error - return ver; + LOG(TRACE, "Modem FW ver: %lu", ver); // will be reported as 0 in case of error + *version = ver; + + return SYSTEM_ERROR_NONE; } int SaraNcpClient::initReady(ModemState state) { CHECK(waitAtResponse(5000)); - fwVersion_ = getAppFirmwareVersion(); + if (getNcpFirmwareVersion(&fwVersion_) != SYSTEM_ERROR_NONE) { + fwVersion_ = 0; + } +#if HAL_PLATFORM_NCP_FW_UPDATE + if (ncpId() == PLATFORM_NCP_SARA_R510) { + // Not checking for errors because this can cause an init loop + services::SaraNcpFwUpdate::instance()->checkUpdate(fwVersion_); + } +#endif + // XXX: Strip off the _ENGxxxx digit for rest of system firmware. It is only required for SaraNcpFwUpdate. + fwVersion_ = fwVersion_ - fwVersion_ % 10; + // L0.0.00.00.05.06,A.02.00 has a memory issue - memoryIssuePresent_ = (ncpId() == PLATFORM_NCP_SARA_R410) ? (fwVersion_ == UBLOX_NCP_R4_APP_FW_VERSION_MEMORY_LEAK_ISSUE) : false; - oldFirmwarePresent_ = (ncpId() == PLATFORM_NCP_SARA_R410) ? (fwVersion_ < UBLOX_NCP_R4_APP_FW_VERSION_LATEST_02B_01) : false; + r410MemoryIssuePresent_ = (ncpId() == PLATFORM_NCP_SARA_R410) ? (fwVersion_ == UBLOX_NCP_R4_APP_FW_VERSION_MEMORY_LEAK_ISSUE) : false; + r410OldFirmwarePresent_ = (ncpId() == PLATFORM_NCP_SARA_R410) ? (fwVersion_ >= UBLOX_NCP_R4_APP_FW_VERSION_MEMORY_LEAK_ISSUE && + fwVersion_ < UBLOX_NCP_R4_APP_FW_VERSION_LATEST_02B_01) : false; // Select either internal or external SIM card slot depending on the configuration CHECK(selectSimCard(state)); // Make sure flow control is enabled as well @@ -1481,8 +1573,9 @@ int SaraNcpClient::initReady(ModemState state) { CHECK(changeBaudRate(UBLOX_NCP_RUNTIME_SERIAL_BAUDRATE_U2)); } else { // There is a set of other revisions which do not have hardware flow control - if (!(fwVersion_ >= UBLOX_NCP_R4_APP_FW_VERSION_NO_HW_FLOW_CONTROL_MIN && - fwVersion_ <= UBLOX_NCP_R4_APP_FW_VERSION_NO_HW_FLOW_CONTROL_MAX)) { + if (ncpId() == PLATFORM_NCP_SARA_R410 && + !(fwVersion_ >= UBLOX_NCP_R4_APP_FW_VERSION_NO_HW_FLOW_CONTROL_MIN && + fwVersion_ < UBLOX_NCP_R4_APP_FW_VERSION_NO_HW_FLOW_CONTROL_MAX)) { // Change the baudrate to 460800 // NOTE: ignoring AT errors just in case to accommodate for some revisions // potentially not supporting anything other than 115200 @@ -2059,7 +2152,7 @@ int SaraNcpClient::enterDataMode() { } int SaraNcpClient::getMtu() { - if (ncpId() == PLATFORM_NCP_SARA_R410 && fwVersion_ <= UBLOX_NCP_R4_APP_FW_VERSION_NO_HW_FLOW_CONTROL_MAX) { + if (ncpId() == PLATFORM_NCP_SARA_R410 && fwVersion_ < UBLOX_NCP_R4_APP_FW_VERSION_NO_HW_FLOW_CONTROL_MAX) { return UBLOX_NCP_R4_NO_HW_FLOW_CONTROL_MTU; } return 0; @@ -2172,7 +2265,7 @@ void SaraNcpClient::resetRegistrationState() { void SaraNcpClient::checkRegistrationState() { if (connState_ != NcpConnectionState::DISCONNECTED) { if ((csd_.registered() && psd_.registered()) || eps_.registered()) { - if (memoryIssuePresent_ && connState_ != NcpConnectionState::CONNECTED) { + if (r410MemoryIssuePresent_ && connState_ != NcpConnectionState::CONNECTED) { registeredTime_ = millis(); // start registered timer for memory issue power off delays } connectionState(NcpConnectionState::CONNECTED); @@ -2457,7 +2550,7 @@ int SaraNcpClient::modemPowerOff() { // If memory issue is present, ensure we don't force a power off too soon // to avoid hitting the 124 day memory housekeeping issue // TODO: Add ATOK check and AT+CPWROFF command attempt first? - if (memoryIssuePresent_) { + if (r410MemoryIssuePresent_) { waitForPowerOff(); } // R410 @@ -2556,7 +2649,7 @@ int SaraNcpClient::modemHardReset(bool powerOff) { } else { // If memory issue is present, ensure we don't force a power off too soon // to avoid hitting the 124 day memory housekeeping issue - if (memoryIssuePresent_) { + if (r410MemoryIssuePresent_) { waitForPowerOff(); } // R410 - Low pulse for 10s diff --git a/hal/network/ncp_client/sara/sara_ncp_client.h b/hal/network/ncp_client/sara/sara_ncp_client.h index bb61e594bb..010b2e58cb 100644 --- a/hal/network/ncp_client/sara/sara_ncp_client.h +++ b/hal/network/ncp_client/sara/sara_ncp_client.h @@ -74,6 +74,7 @@ class SaraNcpClient: public CellularNcpClient { virtual int getMtu() override; virtual int urcs(bool enable) override; virtual int startNcpFwUpdate(bool update) override; + virtual int getNcpFirmwareVersion(uint32_t* version) override; private: AtParser parser_; @@ -110,9 +111,9 @@ class SaraNcpClient: public CellularNcpClient { system_tick_t imsiCheckTime_; system_tick_t registeredTime_; system_tick_t powerOnTime_; - unsigned int fwVersion_ = 0; - bool memoryIssuePresent_; - bool oldFirmwarePresent_; + uint32_t fwVersion_ = 0; + bool r410MemoryIssuePresent_; + bool r410OldFirmwarePresent_; unsigned registrationTimeout_; unsigned registrationInterventions_; volatile bool inFlowControl_ = false; @@ -163,7 +164,6 @@ class SaraNcpClient: public CellularNcpClient { bool modemPowerState() const; int modemSetUartState(bool state) const; void waitForPowerOff(); - int getAppFirmwareVersion(); int waitAtResponseFromPowerOn(ModemState& modemState); int disablePsmEdrx(); int checkSimReadiness(bool checkForRfReset = false); diff --git a/hal/src/boron/hal_platform_config.h b/hal/src/boron/hal_platform_config.h index 43ebfbb972..00aa02dee2 100644 --- a/hal/src/boron/hal_platform_config.h +++ b/hal/src/boron/hal_platform_config.h @@ -15,6 +15,7 @@ #define HAL_PLATFORM_NCP_COUNT (1) #define HAL_PLATFORM_BROKEN_MTU (1) #define HAL_PLATFORM_MAX_CLOUD_CONNECT_TIME (9*60*1000) +#define HAL_PLATFORM_NCP_FW_UPDATE (1) #if PLATFORM_ID == PLATFORM_BORON diff --git a/hal/src/gcc/hal_platform_config.h b/hal/src/gcc/hal_platform_config.h index 55cd21acf2..d996f4cf48 100644 --- a/hal/src/gcc/hal_platform_config.h +++ b/hal/src/gcc/hal_platform_config.h @@ -43,8 +43,13 @@ #define HAL_PLATFORM_CLOUD_TCP 0 #endif +#ifndef HAL_PLATFORM_NCP #define HAL_PLATFORM_NCP (0) +#endif + +#ifndef HAL_PLATFORM_NCP_AT #define HAL_PLATFORM_NCP_AT (0) +#endif #define HAL_PLATFORM_USB_CDC (1) diff --git a/services/inc/diagnostics.h b/services/inc/diagnostics.h index b6b30ceba7..4cf9771840 100644 --- a/services/inc/diagnostics.h +++ b/services/inc/diagnostics.h @@ -48,6 +48,8 @@ #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_NETWORK_NCP_FW_UPDATE_ERROR_CODE "net:ncpfw:err" #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 +97,8 @@ 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_NETWORK_NCP_FW_UPDATE_ERROR_CODE = 51, // net:nfu:err 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..05ee3d876e --- /dev/null +++ b/services/inc/ncp_fw_update.h @@ -0,0 +1,303 @@ +/* + * 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 + +#if MODULE_FUNCTION != 2 // BOOTLOADER + +#include "hal_platform.h" + +#if HAL_PLATFORM_NCP_FW_UPDATE +// STATIC_ASSERT already defined from static_assert.h as PARTICLE_STATIC_ASSERT +#ifdef STATIC_ASSERT +#undef STATIC_ASSERT +#endif +// app_util.h unconditionally defines STATIC_ASSERT +#include "static_recursive_mutex.h" +// Allow STATIC_ASSERT to be defined as PARTICLE_STATIC_ASSERT in cellular_hal.h below +#ifdef STATIC_ASSERT +#undef STATIC_ASSERT +#endif +#endif // HAL_PLATFORM_NCP_FW_UPDATE + +#include "system_tick_hal.h" +#include "system_defs.h" +#include "system_mode.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 + +// Change to 0 for debugging faster +#define SARA_NCP_FW_UPDATE_ENABLE_DOWNLOAD (1) +#define SARA_NCP_FW_UPDATE_ENABLE_INSTALL (1) + +// Change to 1 for debugging +#define SARA_NCP_FW_UPDATE_ENABLE_DEBUG_LOGGING (0) + +struct SaraNcpFwUpdateCallbacks { + 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); +}; +PARTICLE_STATIC_ASSERT(SaraNcpFwUpdateCallbacks_size, sizeof(SaraNcpFwUpdateCallbacks) == (sizeof(void*) * 6)); + +#ifdef UNIT_TEST +system_error_t setupHTTPSProperties_impl(void); +int sendCommandWithArgs(_CALLBACKPTR_MDM cb, void* param, system_tick_t timeout_ms, const char* format, va_list args); +#endif + +namespace particle { + +namespace services { + +enum SaraNcpFwUpdateState { + FW_UPDATE_STATE_IDLE = 0, + FW_UPDATE_STATE_QUALIFY_FLAGS = 1, + FW_UPDATE_STATE_SETUP_CLOUD_CONNECT = 2, + FW_UPDATE_STATE_SETUP_CLOUD_CONNECTING = 3, + FW_UPDATE_STATE_SETUP_CLOUD_CONNECTED = 4, + FW_UPDATE_STATE_DOWNLOAD_CLOUD_DISCONNECT = 5, + FW_UPDATE_STATE_DOWNLOAD_CELL_DISCONNECTING = 6, + FW_UPDATE_STATE_DOWNLOAD_CELL_CONNECTING = 7, + FW_UPDATE_STATE_DOWNLOAD_HTTPS_SETUP = 8, + FW_UPDATE_STATE_DOWNLOAD_READY = 9, + FW_UPDATE_STATE_INSTALL_CELL_DISCONNECTING = 10, + FW_UPDATE_STATE_INSTALL_STARTING = 11, + FW_UPDATE_STATE_INSTALL_WAITING = 12, + FW_UPDATE_STATE_FINISHED_POWER_OFF = 13, + FW_UPDATE_STATE_FINISHED_POWERING_OFF = 14, + FW_UPDATE_STATE_FINISHED_CLOUD_CONNECTING = 15, + FW_UPDATE_STATE_FINISHED_CLOUD_CONNECTED = 16, + FW_UPDATE_STATE_FINISHED_IDLE = 17, +}; +PARTICLE_STATIC_ASSERT(SaraNcpFwUpdateState_size, FW_UPDATE_STATE_IDLE == 0 && FW_UPDATE_STATE_FINISHED_IDLE == 17); + +enum SaraNcpFwUpdateStatus { + FW_UPDATE_STATUS_IDLE = 0, + FW_UPDATE_STATUS_DOWNLOADING = 1, + FW_UPDATE_STATUS_UPDATING = 2, + FW_UPDATE_STATUS_SUCCESS = 3, + FW_UPDATE_STATUS_FAILED = 4, + FW_UPDATE_STATUS_NONE = 5, // for diagnostics +}; +PARTICLE_STATIC_ASSERT(SaraNcpFwUpdateStatus_size, FW_UPDATE_STATUS_IDLE == 0 && FW_UPDATE_STATUS_NONE == 5); + +const system_tick_t NCP_FW_MODEM_INSTALL_ATOK_INTERVAL = 10000; +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 system_tick_t NCP_FW_MODEM_CLOUD_DISCONNECT_TIMEOUT = 1 * 60000; +const system_tick_t NCP_FW_MODEM_CELLULAR_CONNECT_TIMEOUT = 10 * 60000; +const system_tick_t NCP_FW_MODEM_DOWNLOAD_TIMEOUT = 5 * 60000; +const system_tick_t NCP_FW_MODEM_POWER_OFF_TIMEOUT = 1 * 60000; +const int NCP_FW_UBLOX_DEFAULT_CID = 1; +const int NCP_FW_UUFWINSTALL_COMPLETE = 128; + +/** + * struct SaraNcpFwUpdateConfig { + * uint16_t size; + * uint32_t start_version; + * uint32_t end_version; + * char filename[256]; + * char md5sum[32]; + * }; + */ +const SaraNcpFwUpdateConfig SARA_NCP_FW_UPDATE_CONFIG[] = { + // { sizeof(SaraNcpFwUpdateConfig), 31400010, 31400011, "SARA-R510S-01B-00-ES-0314A0001_SARA-R510S-01B-00-XX-0314ENG0099A0001.upd", "09c1a98d03c761bcbea50355f9b2a50f" }, + // { sizeof(SaraNcpFwUpdateConfig), 31400011, 31400010, "SARA-R510S-01B-00-XX-0314ENG0099A0001_SARA-R510S-01B-00-ES-0314A0001.upd", "136caf2883457093c9e41fda3c6a44e3" }, + // { sizeof(SaraNcpFwUpdateConfig), 20600010, 990100010, "SARA-R510S-00B-01_FW02.06_A00.01_IP_SARA-R510S-00B-01_FW99.01_A00.01.upd", "ccfdc48c0a45198d6e168b30d0740959" }, + // { sizeof(SaraNcpFwUpdateConfig), 990100010, 20600010, "SARA-R510S-00B-01_FW99.01_A00.01_SARA-R510S-00B-01_FW02.06_A00.01_IP.upd", "5fd6c0d3d731c097605895b86f28c2cf" }, +}; +const size_t SARA_NCP_FW_UPDATE_CONFIG_SIZE = sizeof(SARA_NCP_FW_UPDATE_CONFIG) / sizeof(SARA_NCP_FW_UPDATE_CONFIG[0]); + +struct __attribute__((packed)) SaraNcpFwUpdateData { + uint16_t size; // sizeof(SaraNcpFwUpdateData) + SaraNcpFwUpdateState state; // FW_UPDATE_STATE_IDLE; + SaraNcpFwUpdateStatus status; // FW_UPDATE_STATUS_IDLE; + system_error_t error; // SYSTEM_ERROR_NONE; + uint32_t firmwareVersion; // 0; + uint32_t startingFirmwareVersion; // 0; + uint32_t updateVersion; // 0; + uint8_t updateAvailable; // SYSTEM_NCP_FW_UPDATE_STATUS_UNKNOWN; + uint8_t isUserConfig; // 0; + SaraNcpFwUpdateConfig userConfigData; // 0; +}; + +struct HTTPSresponse { + volatile bool valid; + int command; + int result; + int status_code; + char md5_sum[40]; +}; + +struct HTTPSerror { + int err_class; + int err_code; +}; + +class SaraNcpFwUpdate { +public: + /** + * Get the singleton instance of this class. + */ + static SaraNcpFwUpdate* instance(); + + void init(SaraNcpFwUpdateCallbacks callbacks); + int process(); + int getStatusDiagnostics(); + int getErrorDiagnostics(); + int setConfig(const SaraNcpFwUpdateConfig* userConfigData = nullptr); + int checkUpdate(uint32_t version = 0); + int enableUpdates(); + int updateStatus(); + int lock(); + int unlock(); + +protected: + SaraNcpFwUpdate(); + ~SaraNcpFwUpdate() = default; + + SaraNcpFwUpdateState saraNcpFwUpdateState_; + SaraNcpFwUpdateState saraNcpFwUpdateLastState_; + SaraNcpFwUpdateStatus saraNcpFwUpdateStatus_; + system_error_t saraNcpFwUpdateError_; + SaraNcpFwUpdateStatus saraNcpFwUpdateStatusDiagnostics_; + system_error_t saraNcpFwUpdateErrorDiagnostics_; + uint32_t startingFirmwareVersion_; + uint32_t firmwareVersion_; + uint32_t updateVersion_; + int updateAvailable_; + int downloadRetries_; + int finishedCloudConnectingRetries_; + volatile int cgevDeactProfile_; + system_tick_t startTimer_; + system_tick_t atOkCheckTimer_; + system_tick_t cooldownTimer_; + system_tick_t cooldownTimeout_; + bool isUserConfig_; + bool initialized_; + SaraNcpFwUpdateData saraNcpFwUpdateData_ = {}; + /** + * Functional callbacks that provide key system services to this NCP FW Update class. + */ + SaraNcpFwUpdateCallbacks saraNcpFwUpdateCallbacks_ = {}; + HTTPSresponse httpsResp_ = {}; + + StaticRecursiveMutex mutex_; + + static int cbUHTTPER(int type, const char* buf, int len, HTTPSerror* data); + static int cbULSTFILE(int type, const char* buf, int len, int* data); + static int httpRespCallback(AtResponseReader* reader, const char* prefix, void* data); + static int cgevCallback(AtResponseReader* reader, const char* prefix, void* data); + uint32_t getNcpFirmwareVersion(); + system_error_t setupHTTPSProperties(); + void cooldown(system_tick_t timer); + void updateCooldown(); + bool inCooldown(); + int validateSaraNcpFwUpdateData(); + int firmwareUpdateForVersion(uint32_t version); + int getConfigData(SaraNcpFwUpdateConfig& configData); + int saveSaraNcpFwUpdateData(); + int recallSaraNcpFwUpdateData(); + int deleteSaraNcpFwUpdateData(); + void logSaraNcpFwUpdateData(SaraNcpFwUpdateData& data); +}; + +class SaraNcpFwUpdateLock { +public: + SaraNcpFwUpdateLock() + : locked_(false) { + lock(); + } + ~SaraNcpFwUpdateLock() { + if (locked_) { + unlock(); + } + } + SaraNcpFwUpdateLock(SaraNcpFwUpdateLock&& lock) + : locked_(lock.locked_) { + lock.locked_ = false; + } + void lock() { + SaraNcpFwUpdate::instance()->lock(); + locked_ = true; + } + void unlock() { + SaraNcpFwUpdate::instance()->unlock(); + locked_ = false; + } + SaraNcpFwUpdateLock(const SaraNcpFwUpdateLock&) = delete; + SaraNcpFwUpdateLock& operator=(const SaraNcpFwUpdateLock&) = delete; + +private: + bool locked_; +}; + +#ifndef UNIT_TEST +class NcpFwUpdateStatusDiagnostics: public AbstractUnsignedIntegerDiagnosticData { +public: + NcpFwUpdateStatusDiagnostics() : + AbstractUnsignedIntegerDiagnosticData(DIAG_ID_NETWORK_NCP_FW_UPDATE_STATUS, DIAG_NAME_NETWORK_NCP_FW_UPDATE_STATUS) { + } + + virtual int get(IntType& val) override { + val = particle::services::SaraNcpFwUpdate::instance()->getStatusDiagnostics(); + return 0; // OK + } +}; +class NcpFwUpdateErrorDiagnostics: public AbstractIntegerDiagnosticData { +public: + NcpFwUpdateErrorDiagnostics() : + AbstractIntegerDiagnosticData(DIAG_ID_NETWORK_NCP_FW_UPDATE_ERROR_CODE, DIAG_NAME_NETWORK_NCP_FW_UPDATE_ERROR_CODE) { + } + + virtual int get(IntType& val) override { + val = particle::services::SaraNcpFwUpdate::instance()->getErrorDiagnostics(); + return 0; // OK + } +}; +#endif // UNIT_TEST + +} // namespace services + +} // namespace particle + +#endif // #if HAL_PLATFORM_NCP_FW_UPDATE + +#endif // #if MODULE_FUNCTION != 2 // BOOTLOADER + diff --git a/services/inc/ncp_fw_update_dynalib.h b/services/inc/ncp_fw_update_dynalib.h new file mode 100644 index 0000000000..6dab1ac1b3 --- /dev/null +++ b/services/inc/ncp_fw_update_dynalib.h @@ -0,0 +1,42 @@ +/* + * 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 SaraNcpFwUpdateConfig { + uint16_t size; + uint32_t start_version; + uint32_t end_version; + char filename[255 + 1]; + char md5sum[32 + 1]; +}; +#else +typedef struct SaraNcpFwUpdateConfig SaraNcpFwUpdateConfig; +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +#if !defined(PARTICLE_USER_MODULE) || defined(PARTICLE_USE_UNSTABLE_API) +int sara_ncp_fw_update_config(const SaraNcpFwUpdateConfig* 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 17483a3c3a..17e4a9fba5 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 @@ -88,6 +89,7 @@ DYNALIB_FN(41, services, clear_system_error_message, void()) DYNALIB_FN(42, services, get_system_error_message, const char*(int)) DYNALIB_FN(43, services, jsmn_parse, int(jsmn_parser*, const char*, size_t, jsmntok_t*, unsigned int, void*)) DYNALIB_FN(44, services, panic_set_hook, void(const PanicHook, void*)) +DYNALIB_FN(45, services, sara_ncp_fw_update_config, int(const SaraNcpFwUpdateConfig*, void*)) DYNALIB_END(services) diff --git a/services/inc/system_cache.h b/services/inc/system_cache.h index b1fcda2d58..6ec4a448b1 100644 --- a/services/inc/system_cache.h +++ b/services/inc/system_cache.h @@ -17,13 +17,16 @@ #pragma once +#ifndef UNIT_TEST #include "tlv_file.h" +#endif // UNIT_TEST namespace particle { namespace services { enum class SystemCacheKey : uint16_t { WIFI_NCP_FIRMWARE_VERSION = 0x0000, - WIFI_NCP_MAC_ADDRESS = 0x0001 + WIFI_NCP_MAC_ADDRESS = 0x0001, + SARA_NCP_FW_UPDATE_DATA = 0x0002, }; class SystemCache { @@ -44,8 +47,10 @@ class SystemCache { protected: SystemCache(); +#ifndef UNIT_TEST private: settings::TlvFile tlv_; +#endif // UNIT_TEST }; } } // particle::service diff --git a/services/inc/system_defs.h b/services/inc/system_defs.h index 776f373e08..2007bc4c5d 100644 --- a/services/inc/system_defs.h +++ b/services/inc/system_defs.h @@ -94,6 +94,93 @@ 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; + +/** + * NCP Firmware Update Available for Cellular Wiring API Cellular.updateStatus() + */ +typedef enum system_ncp_fw_update_available_t { + /** + * The system will check locally for firmware updates when the modem is initialized. + */ + SYSTEM_NCP_FW_UPDATE_UNKNOWN = 0, + /** + * No firmware update available. + */ + SYSTEM_NCP_FW_UPDATE_NOT_AVAILABLE = 1, + /** + * A firmware update is available. + */ + SYSTEM_NCP_FW_UPDATE_PENDING = 2, + /** + * A firmware update is in progress. + */ + SYSTEM_NCP_FW_UPDATE_IN_PROGRESS = 3 +} system_ncp_fw_update_available_t; + #ifdef __cplusplus #include "enumflags.h" diff --git a/services/inc/system_error.h b/services/inc/system_error.h index 77f505e275..d46e277131 100644 --- a/services/inc/system_error.h +++ b/services/inc/system_error.h @@ -50,6 +50,28 @@ (BAD_DATA, "Invalid data format", -280), \ (OUT_OF_RANGE, "Out of range", -290), \ (DEPRECATED, "Deprecated", -300), \ + (SARA_NCP_FW_UPDATE_QUALIFY_FLAGS, "Qualify flags", -400), /* -499 ... -400: SaraNcpFwUpdate errors */ \ + (SARA_NCP_FW_UPDATE_CLOUD_CONNECT_ON_ENTRY_TIMEOUT, "Cloud conn. on entry timeout", -405), \ + (SARA_NCP_FW_UPDATE_PUBLISH_START, "Publish start err", -410), \ + (SARA_NCP_FW_UPDATE_SETUP_CELLULAR_STILL_CONNECTED, "Setup cell still conn.", -415), \ + (SARA_NCP_FW_UPDATE_SETUP_CELLULAR_CONNECT_TIMEOUT, "Setup cell conn. timeout", -420), \ + (SARA_NCP_FW_UPDATE_HTTPS_SETUP_1, "HTTPS err 1", -430), \ + (SARA_NCP_FW_UPDATE_HTTPS_SETUP_2, "HTTPS err 2", -431), \ + (SARA_NCP_FW_UPDATE_HTTPS_SETUP_3, "HTTPS err 3", -432), \ + (SARA_NCP_FW_UPDATE_HTTPS_SETUP_4, "HTTPS err 4", -433), \ + (SARA_NCP_FW_UPDATE_HTTPS_SETUP_5, "HTTPS err 5", -434), \ + (SARA_NCP_FW_UPDATE_HTTPS_SETUP_6, "HTTPS err 6", -435), \ + (SARA_NCP_FW_UPDATE_HTTPS_SETUP_7, "HTTPS err 7", -436), \ + (SARA_NCP_FW_UPDATE_HTTPS_SETUP_8, "HTTPS err 8", -437), \ + (SARA_NCP_FW_UPDATE_HTTPS_SETUP_9, "HTTPS err 9", -438), \ + (SARA_NCP_FW_UPDATE_DOWNLOAD_RETRY_MAX, "DL retry max err", -440), \ + (SARA_NCP_FW_UPDATE_START_INSTALL_TIMEOUT, "Install start timeout", -445), \ + (SARA_NCP_FW_UPDATE_INSTALL_AT_ERROR, "Install AT err", -450), \ + (SARA_NCP_FW_UPDATE_SAME_VERSION, "Same version err", -455), \ + (SARA_NCP_FW_UPDATE_INSTALL_TIMEOUT, "Install timeout", -460), \ + (SARA_NCP_FW_UPDATE_POWER_OFF_TIMEOUT, "Power off timeout", -465), \ + (SARA_NCP_FW_UPDATE_CLOUD_CONNECT_ON_EXIT_TIMEOUT, "Cloud conn. on exit timeout", -470), \ + (SARA_NCP_FW_UPDATE_PUBLISH_RESULT, "Publish result err", -475), \ (COAP, "CoAP error", -1000), /* -1199 ... -1000: CoAP errors */ \ (COAP_4XX, "CoAP: 4xx", -1100), \ (COAP_5XX, "CoAP: 5xx", -1132), \ diff --git a/services/src/ncp_fw_update.cpp b/services/src/ncp_fw_update.cpp new file mode 100644 index 0000000000..ba89b5678c --- /dev/null +++ b/services/src/ncp_fw_update.cpp @@ -0,0 +1,1057 @@ +/* + * 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 . + */ + +#if MODULE_FUNCTION != 2 // BOOTLOADER + +#include "ncp_fw_update.h" +#include "system_error.h" +#if HAL_PLATFORM_NCP_FW_UPDATE + +#include "logging.h" +#define SARA_NCP_FW_UPDATE_LOG_CATEGORY "system.ncp.update" +LOG_SOURCE_CATEGORY(SARA_NCP_FW_UPDATE_LOG_CATEGORY); + +#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 "system_network.h" + +#if SARA_NCP_FW_UPDATE_ENABLE_DEBUG_LOGGING +#define NCPFW_LOG_DEBUG(_level, _fmt, ...) LOG_DEBUG_C(_level, SARA_NCP_FW_UPDATE_LOG_CATEGORY, _fmt, ##__VA_ARGS__) +#else +#define NCPFW_LOG_DEBUG(_level, _fmt, ...) +#endif // SARA_NCP_FW_UPDATE_ENABLE_DEBUG_LOGGING + +// #ifndef UNIT_TEST +#define FMT_LU "lu" +#define NCPFW_LOG(_level, _fmt, ...) LOG_C(_level, SARA_NCP_FW_UPDATE_LOG_CATEGORY, _fmt, ##__VA_ARGS__) +// #else +// #include +// #define FMT_LU PRIu32 +// #define NCPFW_LOG(_level, _fmt, ...) printf(_fmt"\r\n", ##__VA_ARGS__) +// #endif + +#ifndef UNIT_TEST +#define SEND_COMMAND cellular_command +#else +#define SEND_COMMAND sendCommand +#endif + +namespace particle { + +namespace services { + +namespace { + +#define CHECK_NCPID(x) \ + do { \ + const bool _ok = (bool)(platform_primary_ncp_identifier() == (x)); \ + if (!_ok) { \ + return SYSTEM_ERROR_NOT_SUPPORTED; \ + } \ + } while (false) + +inline bool inSafeMode() { + return system_mode() == System_Mode_TypeDef::SAFE_MODE; +} + +inline void delay(system_tick_t delay_ms) { + return HAL_Delay_Milliseconds(delay_ms); +} + +inline system_tick_t millis() { + return HAL_Timer_Get_Milli_Seconds(); +} + +#ifndef UNIT_TEST +static int 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 cbCOPS(int type, const char* buf, int len, bool* 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 = true; + } + } else if (sscanf(buf, "\r\n+COPS: %*d,%*d,\"%*[^\"]\",%d", &act) >= 1) { + if (act >= 7) { + *data = true; + } + } + } + return WAIT; +} +system_error_t setupHTTPSProperties_impl() { + // Wait for registration, using COPS AcT to determine this instead of CEREG. + bool registered = false; + uint32_t start = millis(); + do { + cellular_command((_CALLBACKPTR_MDM)cbCOPS, (void*)®istered, 10000, "AT+COPS?\r\n"); + if (!registered) { + delay(15000); + } + } while (!registered && millis() - start <= NCP_FW_MODEM_CELLULAR_CONNECT_TIMEOUT); + if (!registered) { + return SYSTEM_ERROR_INVALID_STATE; + } + // TODO: We should really have some single source of default registration timeout somewhere, + // right now this is spread out through Quectel, u-blox NCP and Electron modem implementations. + // Tests could also use such a definition. + + 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 + NCPFW_LOG_DEBUG(INFO, "setupHTTPSProperties_"); + if (RESP_OK != cellular_command(nullptr, nullptr, 10000, "AT+USECPRF=2,0,3\r\n")) { + return SYSTEM_ERROR_SARA_NCP_FW_UPDATE_HTTPS_SETUP_1; // Highest level (3) root cert checks + } + if (RESP_OK != cellular_command(nullptr, nullptr, 10000, "AT+USECPRF=2,1,3\r\n")) { + return SYSTEM_ERROR_SARA_NCP_FW_UPDATE_HTTPS_SETUP_2; // Minimum TLS v1.2 + } + if (RESP_OK != cellular_command(nullptr, nullptr, 10000, "AT+USECPRF=2,2,99,\"C0\",\"2F\"\r\n")) { + return SYSTEM_ERROR_SARA_NCP_FW_UPDATE_HTTPS_SETUP_3; // Cipher suite + } + if (RESP_OK != cellular_command(nullptr, nullptr, 10000, "AT+USECPRF=2,3,\"ubx_digicert_global_root_ca\"\r\n")) { + return SYSTEM_ERROR_SARA_NCP_FW_UPDATE_HTTPS_SETUP_4; // Cert name + } + if (RESP_OK != cellular_command(nullptr, nullptr, 10000, "AT+USECPRF=2,4,\"fw.particle.io\"\r\n")) { + return SYSTEM_ERROR_SARA_NCP_FW_UPDATE_HTTPS_SETUP_5; // Expected server hostname + } + if (RESP_OK != cellular_command(nullptr, nullptr, 10000, "AT+USECPRF=2,10,\"fw.particle.io\"\r\n")) { + return SYSTEM_ERROR_SARA_NCP_FW_UPDATE_HTTPS_SETUP_6; // SNI (Server Name Indication) + } + if (RESP_OK != cellular_command(nullptr, nullptr, 10000, "AT+UHTTP=0,1,\"fw.particle.io\"\r\n")) { + return SYSTEM_ERROR_SARA_NCP_FW_UPDATE_HTTPS_SETUP_7; + } + if (RESP_OK != cellular_command(nullptr, nullptr, 10000, "AT+UHTTP=0,5,443\r\n")) { + return SYSTEM_ERROR_SARA_NCP_FW_UPDATE_HTTPS_SETUP_8; + } + if (RESP_OK != cellular_command(nullptr, nullptr, 10000, "AT+UHTTP=0,6,1,2\r\n")) { + return SYSTEM_ERROR_SARA_NCP_FW_UPDATE_HTTPS_SETUP_9; + } + + return SYSTEM_ERROR_NONE; +} +#else +// For mocks, we need to removed the variable arguments and expose a function signature sendCommandWithArgs +int sendCommand(_CALLBACKPTR_MDM cb, void* param, system_tick_t timeout_ms, const char* format, ...) { + va_list args; + va_start(args, format); + const int resp = sendCommandWithArgs(cb, param, timeout_ms, format, args); + va_end(args); + return resp; +} +#endif // UNIT_TEST + +} // namespace + +SaraNcpFwUpdate::SaraNcpFwUpdate() : + saraNcpFwUpdateState_(FW_UPDATE_STATE_IDLE), + saraNcpFwUpdateLastState_(saraNcpFwUpdateState_), + saraNcpFwUpdateStatus_(FW_UPDATE_STATUS_IDLE), + saraNcpFwUpdateStatusDiagnostics_(FW_UPDATE_STATUS_NONE), // initialize to none, only set when update process complete. + saraNcpFwUpdateErrorDiagnostics_(SYSTEM_ERROR_NONE), // initialize to none, only set when update process complete. + startingFirmwareVersion_(1), // Keep these initialized differently to prevent + firmwareVersion_(0), // code from thinking an update was successful + updateVersion_(2), // | + updateAvailable_(SYSTEM_NCP_FW_UPDATE_UNKNOWN), + downloadRetries_(0), + finishedCloudConnectingRetries_(0), + cgevDeactProfile_(0), + startTimer_(0), + atOkCheckTimer_(0), + cooldownTimer_(0), + cooldownTimeout_(0), + isUserConfig_(false), + initialized_(false) { +} + +void SaraNcpFwUpdate::init(SaraNcpFwUpdateCallbacks callbacks) { + this->saraNcpFwUpdateCallbacks_ = callbacks; + memset(&saraNcpFwUpdateData_, 0, sizeof(saraNcpFwUpdateData_)); + recallSaraNcpFwUpdateData(); + if (validateSaraNcpFwUpdateData() != SYSTEM_ERROR_NONE) { + recallSaraNcpFwUpdateData(); + } + if (!inSafeMode()) { + // If not in safe mode, make sure to reset the firmware update state. + if (saraNcpFwUpdateData_.state == FW_UPDATE_STATE_FINISHED_IDLE) { + saraNcpFwUpdateState_ = FW_UPDATE_STATE_IDLE; + if (saraNcpFwUpdateData_.status != FW_UPDATE_STATUS_IDLE) { + NCPFW_LOG(INFO, "Firmware update = %s, status: %d error: %d", (saraNcpFwUpdateData_.status == FW_UPDATE_STATUS_SUCCESS) ? "success" : "failure", saraNcpFwUpdateData_.status, saraNcpFwUpdateData_.error); + saraNcpFwUpdateStatusDiagnostics_ = saraNcpFwUpdateData_.status; + saraNcpFwUpdateErrorDiagnostics_ = saraNcpFwUpdateData_.error; + saraNcpFwUpdateData_.status = FW_UPDATE_STATUS_IDLE; + saraNcpFwUpdateData_.error = SYSTEM_ERROR_NONE; + saraNcpFwUpdateData_.updateAvailable = SYSTEM_NCP_FW_UPDATE_UNKNOWN; + saveSaraNcpFwUpdateData(); + } + saraNcpFwUpdateStatus_ = FW_UPDATE_STATUS_IDLE; + saraNcpFwUpdateError_ = SYSTEM_ERROR_NONE; + updateAvailable_ = SYSTEM_NCP_FW_UPDATE_UNKNOWN; + } else if (saraNcpFwUpdateData_.state == FW_UPDATE_STATE_INSTALL_WAITING) { + NCPFW_LOG(INFO, "Resuming update in Safe Mode!"); + delay(200); + system_reset(SYSTEM_RESET_MODE_SAFE, 0, 0, SYSTEM_RESET_FLAG_NO_WAIT, nullptr); + } else { + saraNcpFwUpdateState_ = FW_UPDATE_STATE_IDLE; // default to disable updates + saraNcpFwUpdateStatus_ = FW_UPDATE_STATUS_IDLE; + saraNcpFwUpdateError_ = SYSTEM_ERROR_NONE; + updateAvailable_ = SYSTEM_NCP_FW_UPDATE_UNKNOWN; + } + } else { + // Ensure we recall the previously set state + saraNcpFwUpdateState_ = saraNcpFwUpdateData_.state; + // If we come up in Safe Mode as FINISHED, ensure we have an IDLE status + // to prevent a dependency issue causing a safe mode loop + if (saraNcpFwUpdateState_ == FW_UPDATE_STATE_FINISHED_IDLE) { + saraNcpFwUpdateData_.status = FW_UPDATE_STATUS_IDLE; + saraNcpFwUpdateData_.error = SYSTEM_ERROR_NONE; + saraNcpFwUpdateData_.updateAvailable = SYSTEM_NCP_FW_UPDATE_UNKNOWN; + } + saraNcpFwUpdateStatus_ = saraNcpFwUpdateData_.status; + saraNcpFwUpdateError_ = saraNcpFwUpdateData_.error; + firmwareVersion_ = saraNcpFwUpdateData_.firmwareVersion; + startingFirmwareVersion_ = saraNcpFwUpdateData_.startingFirmwareVersion; + updateVersion_ = saraNcpFwUpdateData_.updateVersion; + isUserConfig_ = saraNcpFwUpdateData_.isUserConfig; + updateAvailable_ = saraNcpFwUpdateData_.updateAvailable; + } + initialized_ = true; +} + +int SaraNcpFwUpdate::getStatusDiagnostics() { + return saraNcpFwUpdateStatusDiagnostics_; +} + +int SaraNcpFwUpdate::getErrorDiagnostics() { + return saraNcpFwUpdateErrorDiagnostics_; +} + +SaraNcpFwUpdate* SaraNcpFwUpdate::instance() { + static SaraNcpFwUpdate instance; + return &instance; +} + +int SaraNcpFwUpdate::lock() { + return mutex_.lock(); +} + +int SaraNcpFwUpdate::unlock() { + return mutex_.unlock(); +} + +int SaraNcpFwUpdate::setConfig(const SaraNcpFwUpdateConfig* userConfigData) { + SaraNcpFwUpdateLock lk; + CHECK_NCPID(PLATFORM_NCP_SARA_R510); + if (saraNcpFwUpdateStatus_ != FW_UPDATE_STATUS_IDLE) { + return SYSTEM_ERROR_INVALID_STATE; + } + + if (userConfigData) { + validateSaraNcpFwUpdateData(); + memcpy(&saraNcpFwUpdateData_.userConfigData, userConfigData, sizeof(SaraNcpFwUpdateConfig)); + logSaraNcpFwUpdateData(saraNcpFwUpdateData_); + isUserConfig_ = true; + } else { + isUserConfig_ = false; + } + saraNcpFwUpdateData_.isUserConfig = isUserConfig_; + + // Check if update avail with/without userConfigData + CHECK(checkUpdate()); + + return SYSTEM_ERROR_NONE; +} + +// Check if firmware version requires an upgrade +int SaraNcpFwUpdate::checkUpdate(uint32_t version /* default = 0 */) { + SaraNcpFwUpdateLock lk; + CHECK_NCPID(PLATFORM_NCP_SARA_R510); + // Prevent power cycling the modem during an update from altering the state of member variables + if (updateAvailable_ == SYSTEM_NCP_FW_UPDATE_IN_PROGRESS) { + return SYSTEM_ERROR_INVALID_STATE; + } + + if (!version) { + firmwareVersion_ = getNcpFirmwareVersion(); + if (firmwareVersion_ == 0) { + NCPFW_LOG(ERROR, "Modem has unknown firmware version or powered off"); + return SYSTEM_ERROR_INVALID_STATE; + } + } else { + firmwareVersion_ = version; + } + if ((!isUserConfig_ && firmwareUpdateForVersion(firmwareVersion_) == SYSTEM_ERROR_NOT_FOUND) || + (isUserConfig_ && firmwareVersion_ != saraNcpFwUpdateData_.userConfigData.start_version)) { + NCPFW_LOG(INFO, "No firmware update available"); + updateAvailable_ = SYSTEM_NCP_FW_UPDATE_NOT_AVAILABLE; + } else { + NCPFW_LOG(INFO, "NCP FW Update Fully Qualified!"); + saraNcpFwUpdateData_.firmwareVersion = firmwareVersion_; + updateAvailable_ = SYSTEM_NCP_FW_UPDATE_PENDING; + saraNcpFwUpdateData_.updateAvailable = updateAvailable_; + saveSaraNcpFwUpdateData(); + } + + // Reset some variables used in the update process + cooldownTimer_ = 0; + cooldownTimeout_ = 0; + downloadRetries_ = 0; + finishedCloudConnectingRetries_ = 0; + + return SYSTEM_ERROR_NONE; +} + +int SaraNcpFwUpdate::enableUpdates() { + SaraNcpFwUpdateLock lk; + CHECK_NCPID(PLATFORM_NCP_SARA_R510); + if (!initialized_ || + updateAvailable_ != SYSTEM_NCP_FW_UPDATE_PENDING || + !network_is_on(NETWORK_INTERFACE_CELLULAR, nullptr) || + network_is_off(NETWORK_INTERFACE_CELLULAR, nullptr)) { + return SYSTEM_ERROR_INVALID_STATE; + } + + saraNcpFwUpdateState_ = FW_UPDATE_STATE_QUALIFY_FLAGS; + updateAvailable_ = SYSTEM_NCP_FW_UPDATE_IN_PROGRESS; + + return SYSTEM_ERROR_NONE; +} + +int SaraNcpFwUpdate::updateStatus() { + CHECK_NCPID(PLATFORM_NCP_SARA_R510); + if (!initialized_) { + return SYSTEM_ERROR_INVALID_STATE; + } + + return updateAvailable_; +} + +int SaraNcpFwUpdate::validateSaraNcpFwUpdateData() { + if (saraNcpFwUpdateData_.size != sizeof(SaraNcpFwUpdateData)) { + memset(&saraNcpFwUpdateData_, 0, sizeof(saraNcpFwUpdateData_)); + saraNcpFwUpdateData_.size = sizeof(SaraNcpFwUpdateData); + saraNcpFwUpdateData_.state = FW_UPDATE_STATE_IDLE; + saraNcpFwUpdateData_.status = FW_UPDATE_STATUS_IDLE; + // saraNcpFwUpdateData_.updateAvailable = SYSTEM_NCP_FW_UPDATE_UNKNOWN; // 0 + // saraNcpFwUpdateData_.isUserConfig = false; // 0 + + saveSaraNcpFwUpdateData(); + + isUserConfig_ = false; + updateAvailable_ = SYSTEM_NCP_FW_UPDATE_UNKNOWN; // 0 + saraNcpFwUpdateState_ = FW_UPDATE_STATE_IDLE; + saraNcpFwUpdateStatus_ = FW_UPDATE_STATUS_IDLE; + saraNcpFwUpdateError_ = SYSTEM_ERROR_NONE; + NCPFW_LOG(INFO, "saraNcpFwUpdateData_ initialized"); + return SYSTEM_ERROR_BAD_DATA; + } + + return SYSTEM_ERROR_NONE; +} + +void SaraNcpFwUpdate::logSaraNcpFwUpdateData(SaraNcpFwUpdateData& data) { + NCPFW_LOG(INFO, "saraNcpFwUpdateData size:%u state:%d status:%d error:%d fv:%" FMT_LU " sfv:%" FMT_LU " uv:%" FMT_LU "", + data.size, + data.state, + data.status, + data.error, + data.firmwareVersion, + data.startingFirmwareVersion, + data.updateVersion); + NCPFW_LOG(INFO, "ua:%d iuc:%d sv:%" FMT_LU " ev:%" FMT_LU " file:%s md5:%s", + data.updateAvailable, + data.isUserConfig, + data.userConfigData.start_version, + data.userConfigData.end_version, + data.userConfigData.filename, + data.userConfigData.md5sum); +} + +int SaraNcpFwUpdate::saveSaraNcpFwUpdateData() { + SaraNcpFwUpdateData tempData = {}; + int result = SystemCache::instance().get(SystemCacheKey::SARA_NCP_FW_UPDATE_DATA, (uint8_t*)&tempData, sizeof(tempData)); + if (result != sizeof(tempData) || + /* memcmp(tempData, saraNcpFwUpdateData_, sizeof(tempData) != 0)) { // not reliable */ + tempData.size != saraNcpFwUpdateData_.size || + tempData.state != saraNcpFwUpdateData_.state || + tempData.status != saraNcpFwUpdateData_.status || + tempData.error != saraNcpFwUpdateData_.error || + tempData.firmwareVersion != saraNcpFwUpdateData_.firmwareVersion || + tempData.startingFirmwareVersion != saraNcpFwUpdateData_.startingFirmwareVersion || + tempData.updateVersion != saraNcpFwUpdateData_.updateVersion || + tempData.isUserConfig != saraNcpFwUpdateData_.isUserConfig || + tempData.updateAvailable != saraNcpFwUpdateData_.updateAvailable || + tempData.userConfigData.start_version != saraNcpFwUpdateData_.userConfigData.start_version || + tempData.userConfigData.start_version != saraNcpFwUpdateData_.userConfigData.end_version || + strcmp(tempData.userConfigData.filename, saraNcpFwUpdateData_.userConfigData.filename) != 0 || + strcmp(tempData.userConfigData.md5sum, saraNcpFwUpdateData_.userConfigData.md5sum) != 0) { + logSaraNcpFwUpdateData(tempData); + NCPFW_LOG(INFO, "Writing cached saraNcpFwUpdateData, size: %d", saraNcpFwUpdateData_.size); + result = SystemCache::instance().set(SystemCacheKey::SARA_NCP_FW_UPDATE_DATA, (uint8_t*)&saraNcpFwUpdateData_, sizeof(saraNcpFwUpdateData_)); + } + return (result < 0) ? result : SYSTEM_ERROR_NONE; +} + +int SaraNcpFwUpdate::recallSaraNcpFwUpdateData() { + SaraNcpFwUpdateData tempData; + int result = SystemCache::instance().get(SystemCacheKey::SARA_NCP_FW_UPDATE_DATA, (uint8_t*)&tempData, sizeof(tempData)); + if (result != sizeof(tempData)) { + return SYSTEM_ERROR_NOT_FOUND; + } + NCPFW_LOG(INFO, "Reading cached saraNcpFwUpdateData"); + memcpy(&saraNcpFwUpdateData_, &tempData, sizeof(saraNcpFwUpdateData_)); + logSaraNcpFwUpdateData(tempData); + return SYSTEM_ERROR_NONE; +} + +int SaraNcpFwUpdate::deleteSaraNcpFwUpdateData() { + NCPFW_LOG_DEBUG(INFO, "Deleting cached saraNcpFwUpdateData"); + int result = SystemCache::instance().del(SystemCacheKey::SARA_NCP_FW_UPDATE_DATA); + return (result < 0) ? result : SYSTEM_ERROR_NONE; +} + +int SaraNcpFwUpdate::firmwareUpdateForVersion(uint32_t version) { + for (size_t i = 0; i < SARA_NCP_FW_UPDATE_CONFIG_SIZE; ++i) { + if (version == SARA_NCP_FW_UPDATE_CONFIG[i].start_version) { + return i; + } + } + return SYSTEM_ERROR_NOT_FOUND; +} + +int SaraNcpFwUpdate::getConfigData(SaraNcpFwUpdateConfig& configData) { + const int fwIdx = firmwareUpdateForVersion(firmwareVersion_); + if (isUserConfig_ || fwIdx != SYSTEM_ERROR_NOT_FOUND) { + if (isUserConfig_) { + memcpy(&configData, &saraNcpFwUpdateData_.userConfigData, sizeof(configData)); + } else { + memcpy(&configData, &SARA_NCP_FW_UPDATE_CONFIG[fwIdx], sizeof(configData)); + } + return SYSTEM_ERROR_NONE; + } + + return SYSTEM_ERROR_NOT_FOUND; +} + +int SaraNcpFwUpdate::process() { + CHECK_NCPID(PLATFORM_NCP_SARA_R510); +#ifndef UNIT_TEST + SPARK_ASSERT(initialized_); +#else + if (!initialized_) { + return SYSTEM_ERROR_INVALID_STATE; + } +#endif + +#ifdef SARA_NCP_FW_UPDATE_ENABLE_DEBUG_LOGGING + // Make state changes more obvious + if (saraNcpFwUpdateState_ != saraNcpFwUpdateLastState_) { + NCPFW_LOG(INFO, "======================= saraNcpFwUpdateState: %d", saraNcpFwUpdateState_); + } + saraNcpFwUpdateLastState_ = saraNcpFwUpdateState_; +#endif // SARA_NCP_FW_UPDATE_ENABLE_DEBUG_LOGGING + + validateSaraNcpFwUpdateData(); + + if (inCooldown()) { + updateCooldown(); + return SYSTEM_ERROR_NONE; + } + + switch (saraNcpFwUpdateState_) { + + case FW_UPDATE_STATE_QUALIFY_FLAGS: + { + bool failedChecks = false; + // Check NCP version + if (platform_primary_ncp_identifier() != PLATFORM_NCP_SARA_R510) { + NCPFW_LOG(ERROR, "PLATFORM_NCP != SARA_R510"); + failedChecks = true; + } + // Make sure update is in progress + if (updateAvailable_ != SYSTEM_NCP_FW_UPDATE_IN_PROGRESS) { + NCPFW_LOG(ERROR, "Unexpected status updateAvailable_: %d", updateAvailable_); + failedChecks = true; + } + if (failedChecks) { + saraNcpFwUpdateState_ = FW_UPDATE_STATE_FINISHED_IDLE; + saraNcpFwUpdateStatus_ = FW_UPDATE_STATUS_FAILED; + saraNcpFwUpdateError_ = SYSTEM_ERROR_SARA_NCP_FW_UPDATE_QUALIFY_FLAGS; + break; + } + + // If not in Safe Mode, reset now into Safe Mode! + saraNcpFwUpdateState_ = FW_UPDATE_STATE_SETUP_CLOUD_CONNECT; + saraNcpFwUpdateData_.state = saraNcpFwUpdateState_; + saraNcpFwUpdateData_.firmwareVersion = firmwareVersion_; + saraNcpFwUpdateData_.updateAvailable = updateAvailable_; + saveSaraNcpFwUpdateData(); + if (!inSafeMode()) { + NCPFW_LOG(INFO, "Resetting into Safe Mode to update modem firmware"); + delay(200); + system_reset(SYSTEM_RESET_MODE_SAFE, 0, 0, SYSTEM_RESET_FLAG_NO_WAIT, nullptr); + // Goodbye! See you next time through in Safe Mode ;-) + } + } + break; + + case FW_UPDATE_STATE_SETUP_CLOUD_CONNECT: + { + if (!saraNcpFwUpdateCallbacks_.spark_cloud_flag_connected()) { // !Particle.connected() + NCPFW_LOG_DEBUG(INFO, "Connect to Cloud..."); + saraNcpFwUpdateCallbacks_.spark_cloud_flag_connect(); // Particle.connect() + } + startTimer_ = millis(); + saraNcpFwUpdateState_ = FW_UPDATE_STATE_SETUP_CLOUD_CONNECTING; + saraNcpFwUpdateStatus_ = FW_UPDATE_STATUS_DOWNLOADING; + saraNcpFwUpdateStatusDiagnostics_ = saraNcpFwUpdateStatus_; // ready our diagnostics status value before connecting to the cloud + saraNcpFwUpdateErrorDiagnostics_ = saraNcpFwUpdateError_; + } + break; + + case FW_UPDATE_STATE_SETUP_CLOUD_CONNECTING: + { + if (millis() - startTimer_ >= NCP_FW_MODEM_CLOUD_CONNECT_TIMEOUT) { + saraNcpFwUpdateState_ = FW_UPDATE_STATE_FINISHED_IDLE; + saraNcpFwUpdateStatus_ = FW_UPDATE_STATUS_FAILED; + saraNcpFwUpdateError_ = SYSTEM_ERROR_SARA_NCP_FW_UPDATE_CLOUD_CONNECT_ON_ENTRY_TIMEOUT; + } else if (saraNcpFwUpdateCallbacks_.spark_cloud_flag_connected()) { // Particle.connected() + NCPFW_LOG_DEBUG(INFO, "Connected to Cloud."); + saraNcpFwUpdateState_ = FW_UPDATE_STATE_SETUP_CLOUD_CONNECTED; + } else { + cooldown(1000); + } + } + break; + + case FW_UPDATE_STATE_SETUP_CLOUD_CONNECTED: + { + saraNcpFwUpdateState_ = FW_UPDATE_STATE_FINISHED_IDLE; + if (saraNcpFwUpdateCallbacks_.spark_cloud_flag_connected()) { // Particle.connected() + if (saraNcpFwUpdateCallbacks_.publishEvent("spark/device/ncp/update", "started", /*flags PUBLISH_EVENT_FLAG_PRIVATE*/1)) { + NCPFW_LOG_DEBUG(INFO, "Ready to start download..."); + saraNcpFwUpdateState_ = FW_UPDATE_STATE_DOWNLOAD_CLOUD_DISCONNECT; + } else { + saraNcpFwUpdateStatus_ = FW_UPDATE_STATUS_FAILED; + saraNcpFwUpdateError_ = SYSTEM_ERROR_SARA_NCP_FW_UPDATE_PUBLISH_START; + } + } else { + saraNcpFwUpdateStatus_ = FW_UPDATE_STATUS_FAILED; + saraNcpFwUpdateError_ = SYSTEM_ERROR_SARA_NCP_FW_UPDATE_PUBLISH_START; + } + } + break; + + case FW_UPDATE_STATE_DOWNLOAD_CLOUD_DISCONNECT: + { + if (saraNcpFwUpdateCallbacks_.spark_cloud_flag_connected()) { // Particle.connected() + saraNcpFwUpdateCallbacks_.spark_cloud_flag_disconnect(); // Particle.disconnect() + } else { + NCPFW_LOG_DEBUG(INFO, "Disconnected from Cloud."); + if (network_ready(0, 0, 0)) { // Cellular.ready() + NCPFW_LOG_DEBUG(INFO, "Disconnecting Cellular..."); + // network_disconnect() doesn't wait for final OK response from CFUN=0 + // wait for DETACH: +CGEV: ME PDN DEACT 1 + cgevDeactProfile_ = 0; + cellular_add_urc_handler("+CGEV", [](const char* data, void* context) -> int { + const auto self = (SaraNcpFwUpdate*)context; + int profile; + + int r = ::sscanf(data, "+CGEV: ME PDN DEACT %d", &profile); + // do not CHECK_TRUE as we intend to ignore +CGEV: ME DETACH + if (r >= 1) { + self->cgevDeactProfile_ = profile; + } + + return SYSTEM_ERROR_NONE; + }, this); + startTimer_ = millis(); + network_disconnect(0, NETWORK_DISCONNECT_REASON_USER, 0); + saraNcpFwUpdateState_ = FW_UPDATE_STATE_DOWNLOAD_CELL_DISCONNECTING; + } + } + } + break; + + case FW_UPDATE_STATE_DOWNLOAD_CELL_DISCONNECTING: + { + cellular_lock(nullptr); // protect against concurrent access to httpsResp_ & cgevDeactProfile_ + const int tempCgevDeactProfile = cgevDeactProfile_; + cellular_unlock(nullptr); + if (tempCgevDeactProfile == NCP_FW_UBLOX_DEFAULT_CID) { // Default CID detached + saraNcpFwUpdateState_ = FW_UPDATE_STATE_DOWNLOAD_CELL_CONNECTING; + cooldown(1000); // allow other disconnect URCs to pass + } + if (millis() - startTimer_ >= NCP_FW_MODEM_CLOUD_DISCONNECT_TIMEOUT) { + saraNcpFwUpdateState_ = FW_UPDATE_STATE_DOWNLOAD_CELL_CONNECTING; + } + } + break; + + case FW_UPDATE_STATE_DOWNLOAD_CELL_CONNECTING: + { + if (!network_ready(0, 0, 0)) { // Cellular.ready() + NCPFW_LOG_DEBUG(INFO, "Disconnected Cellular. Reconnecting without PPP..."); + cellular_start_ncp_firmware_update(true, nullptr); // ensure we don't connect PPP + network_connect(0, 0, 0, 0); // Cellular.connect() + saraNcpFwUpdateState_ = FW_UPDATE_STATE_DOWNLOAD_HTTPS_SETUP; + } else { + saraNcpFwUpdateState_ = FW_UPDATE_STATE_FINISHED_IDLE; + saraNcpFwUpdateStatus_ = FW_UPDATE_STATUS_FAILED; + saraNcpFwUpdateError_ = SYSTEM_ERROR_SARA_NCP_FW_UPDATE_SETUP_CELLULAR_STILL_CONNECTED; + } + } + break; + + case FW_UPDATE_STATE_DOWNLOAD_HTTPS_SETUP: + { + auto ret = setupHTTPSProperties(); + if (ret < 0) { + NCPFW_LOG_DEBUG(INFO, "HTTPS setup error: %d", ret); + saraNcpFwUpdateState_ = FW_UPDATE_STATE_FINISHED_IDLE; + if (ret == SYSTEM_ERROR_INVALID_STATE) { + saraNcpFwUpdateError_ = SYSTEM_ERROR_SARA_NCP_FW_UPDATE_SETUP_CELLULAR_CONNECT_TIMEOUT; + } else { + saraNcpFwUpdateError_ = ret; + } + saraNcpFwUpdateStatus_ = FW_UPDATE_STATUS_FAILED; + break; + } + saraNcpFwUpdateState_ = FW_UPDATE_STATE_DOWNLOAD_READY; + } + break; + + case FW_UPDATE_STATE_DOWNLOAD_READY: + { + bool modemFirmwareDownloadComplete = false; + int foatReady = 0; + memset(&httpsResp_, 0, sizeof(httpsResp_)); + + // 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. + NCPFW_LOG_DEBUG(INFO, "Existing FOAT file?"); + SEND_COMMAND((_CALLBACKPTR_MDM)cbULSTFILE, (void*)&foatReady, 10000, "AT+ULSTFILE=0,\"FOAT\"\r\n"); + if (foatReady) { + if (RESP_OK == SEND_COMMAND(nullptr, nullptr, 10000, "AT+UDELFILE=\"updatePackage.bin\",\"FOAT\"\r\n")) { + NCPFW_LOG_DEBUG(INFO, "updatePackage.bin deleted."); + } + } + + cellular_add_urc_handler("+UUHTTPCR", [](const char* data, void* context) -> int { + const auto self = (SaraNcpFwUpdate*)context; + int a, b, c; + char s[40]; + + int r = ::sscanf(data, "+UUHTTPCR: %*d,%d,%d,%d,\"%32s\"", &a, &b, &c, s); + CHECK_TRUE(r >= 3, SYSTEM_ERROR_AT_RESPONSE_UNEXPECTED); + + self->httpsResp_.valid = false; // make following lines atomic + self->httpsResp_.command = a; + self->httpsResp_.result = b; + self->httpsResp_.status_code = c; + if (r > 3) { + memcpy(self->httpsResp_.md5_sum, &s, sizeof(s)); + } + self->httpsResp_.valid = true; + + return SYSTEM_ERROR_NONE; + }, this); + + NCPFW_LOG_DEBUG(INFO, "Starting download..."); +#if SARA_NCP_FW_UPDATE_ENABLE_DOWNLOAD + SaraNcpFwUpdateConfig configData; + if (getConfigData(configData) != SYSTEM_ERROR_NOT_FOUND) { + updateVersion_ = configData.end_version; + saraNcpFwUpdateData_.updateVersion = updateVersion_; + saveSaraNcpFwUpdateData(); + // 99010001 -> 02060001 firmware (takes 1.8 - 2.8 minutes) + int resp = SEND_COMMAND(nullptr, nullptr, NCP_FW_MODEM_DOWNLOAD_TIMEOUT, "AT+UHTTPC=0,100,\"/%s\"\r\n", configData.filename); + // int resp = RESP_OK; + if (resp != RESP_ERROR) { + cellular_lock(nullptr); // protect against concurrent access to httpsResp_ & cgevDeactProfile_ + if (httpsResp_.valid && httpsResp_.command == 100 && httpsResp_.result == 1 && httpsResp_.status_code == 200 && strcmp(httpsResp_.md5_sum, configData.md5sum)) { + modemFirmwareDownloadComplete = true; + } + cellular_unlock(nullptr); + if (!modemFirmwareDownloadComplete) { + system_tick_t start = millis(); + while (millis() - start < NCP_FW_MODEM_DOWNLOAD_TIMEOUT && !modemFirmwareDownloadComplete) { + cellular_lock(nullptr); // protect against concurrent access to httpsResp_ & cgevDeactProfile_ + if (httpsResp_.valid && httpsResp_.command == 100 && httpsResp_.result == 1 && httpsResp_.status_code == 200 && !strcmp(httpsResp_.md5_sum, configData.md5sum)) { + modemFirmwareDownloadComplete = true; + } + if (httpsResp_.valid && httpsResp_.command == 100 && httpsResp_.result == 0) { + cellular_unlock(nullptr); + break; + } + cellular_unlock(nullptr); + } + } + } + } + + cellular_remove_urc_handler("+UUHTTPCR"); +#else // SARA_NCP_FW_UPDATE_ENABLE_DOWNLOAD + // DEBUG + modemFirmwareDownloadComplete = true; +#endif + if (modemFirmwareDownloadComplete) { + cellular_lock(nullptr); // protect against concurrent access to httpsResp_ & cgevDeactProfile_ + NCPFW_LOG_DEBUG(INFO, "command: %d, result: %d, status_code: %d, md5:[%s]", + httpsResp_.command, httpsResp_.result, httpsResp_.status_code, httpsResp_.md5_sum); + NCPFW_LOG_DEBUG(INFO, "Download complete and verified."); + saraNcpFwUpdateState_ = FW_UPDATE_STATE_INSTALL_CELL_DISCONNECTING; + NCPFW_LOG_DEBUG(INFO, "Disconnecting Cellular..."); + cgevDeactProfile_ = 0; // waiting for 1 + cellular_unlock(nullptr); + startTimer_ = millis(); + network_disconnect(0, NETWORK_DISCONNECT_REASON_USER, 0); + } else { + NCPFW_LOG_DEBUG(INFO, "Download failed!!"); + HTTPSerror httpsError = {}; + SEND_COMMAND((_CALLBACKPTR_MDM)cbUHTTPER, (void*)&httpsError, 10000, "AT+UHTTPER=0\r\n"); + NCPFW_LOG_DEBUG(INFO, "UHTTPER class: %d, code: %d", + httpsError.err_class, httpsError.err_code); + if (++downloadRetries_ >= 3) { + saraNcpFwUpdateState_ = FW_UPDATE_STATE_FINISHED_IDLE; + saraNcpFwUpdateStatus_ = FW_UPDATE_STATUS_FAILED; + saraNcpFwUpdateError_ = SYSTEM_ERROR_SARA_NCP_FW_UPDATE_DOWNLOAD_RETRY_MAX; + } + } + } + break; + + case FW_UPDATE_STATE_INSTALL_CELL_DISCONNECTING: + { + cellular_lock(nullptr); // protect against concurrent access to httpsResp_ & cgevDeactProfile_ + const int tempCgevDeactProfile = cgevDeactProfile_; + cellular_unlock(nullptr); + if (tempCgevDeactProfile == NCP_FW_UBLOX_DEFAULT_CID) { // Default CID detached + cellular_unlock(nullptr); + NCPFW_LOG_DEBUG(INFO, "Disconnected Cellular."); + cellular_remove_urc_handler("+CGEV"); + saraNcpFwUpdateState_ = FW_UPDATE_STATE_INSTALL_STARTING; + cooldown(1000); // allow other disconnect URCs to pass + } + if (millis() - startTimer_ >= NCP_FW_MODEM_CLOUD_DISCONNECT_TIMEOUT) { + saraNcpFwUpdateState_ = FW_UPDATE_STATE_INSTALL_STARTING; + } + } + break; + + case FW_UPDATE_STATE_INSTALL_STARTING: + { + atOkCheckTimer_ = millis(); + NCPFW_LOG_DEBUG(INFO, "Installing firmware, prepare to wait 25 - 32 minutes..."); + saraNcpFwUpdateStatus_ = FW_UPDATE_STATUS_UPDATING; + +#if SARA_NCP_FW_UPDATE_ENABLE_INSTALL + if (RESP_OK == SEND_COMMAND(nullptr, nullptr, 60000, "AT+UFWINSTALL=1,115200\r\n")) { + saraNcpFwUpdateData_.state = FW_UPDATE_STATE_INSTALL_WAITING; + saraNcpFwUpdateData_.status = saraNcpFwUpdateStatus_; + saraNcpFwUpdateData_.error = saraNcpFwUpdateError_; + saveSaraNcpFwUpdateData(); + // Wait for AT interface to become unresponsive, since we need to rely on that to break out of our install later + bool atResponsive = true; + while (atResponsive && millis() - atOkCheckTimer_ < NCP_FW_MODEM_INSTALL_START_TIMEOUT) { + atResponsive = (RESP_OK == SEND_COMMAND(nullptr, nullptr, 3000, "AT\r\n")); + if (atResponsive) { + delay(3000); + } + } + if (atResponsive) { + NCPFW_LOG_DEBUG(ERROR, "5 minute timeout waiting for INSTALL to start"); + saraNcpFwUpdateState_ = FW_UPDATE_STATE_FINISHED_POWER_OFF; + saraNcpFwUpdateStatus_ = FW_UPDATE_STATUS_FAILED; + saraNcpFwUpdateError_ = SYSTEM_ERROR_SARA_NCP_FW_UPDATE_START_INSTALL_TIMEOUT; + } else { + cellular_start_ncp_firmware_update(true, nullptr); + saraNcpFwUpdateState_ = FW_UPDATE_STATE_INSTALL_WAITING; + } + } else { + NCPFW_LOG_DEBUG(ERROR, "AT+UFWINSTALL failed to respond"); + saraNcpFwUpdateState_ = FW_UPDATE_STATE_FINISHED_POWER_OFF; + saraNcpFwUpdateStatus_ = FW_UPDATE_STATUS_FAILED; + saraNcpFwUpdateError_ = SYSTEM_ERROR_SARA_NCP_FW_UPDATE_INSTALL_AT_ERROR; + } + cooldown(10000); +#else // SARA_NCP_FW_UPDATE_ENABLE_INSTALL + // DEBUG + saraNcpFwUpdateState_ = FW_UPDATE_STATE_INSTALL_WAITING; + saraNcpFwUpdateStatus_ = FW_UPDATE_STATUS_FAILED; + saraNcpFwUpdateError_ = SYSTEM_ERROR_SARA_NCP_FW_UPDATE_START_INSTALL_TIMEOUT; +#endif + } + break; + + case FW_UPDATE_STATE_INSTALL_WAITING: + { + startingFirmwareVersion_ = firmwareVersion_; + saraNcpFwUpdateData_.startingFirmwareVersion = startingFirmwareVersion_; + saraNcpFwUpdateData_.state = saraNcpFwUpdateState_; + saraNcpFwUpdateData_.status = saraNcpFwUpdateStatus_; + saraNcpFwUpdateData_.error = saraNcpFwUpdateError_; + saveSaraNcpFwUpdateData(); + cellular_start_ncp_firmware_update(true, nullptr); // ensure lower ncp layers play nice + startTimer_ = millis(); + atOkCheckTimer_ = millis(); + int atResp = RESP_ERROR; + // Wait for the install to take place, 18 - 20 or more minutes! + while (true) { + if (millis() - atOkCheckTimer_ >= NCP_FW_MODEM_INSTALL_ATOK_INTERVAL) { + atOkCheckTimer_ = millis(); + atResp = SEND_COMMAND(nullptr, nullptr, 1000, "AT\r\n"); + } + 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_ = getNcpFirmwareVersion(); + NCPFW_LOG_DEBUG(INFO, "App fV: %" FMT_LU ", sfV: %" FMT_LU ", uV: %" FMT_LU, firmwareVersion_, startingFirmwareVersion_, updateVersion_); + if (firmwareVersion_ == updateVersion_) { + saraNcpFwUpdateStatus_ = FW_UPDATE_STATUS_SUCCESS; // force a FW update success status + break; + } else if (firmwareVersion_ == startingFirmwareVersion_) { + saraNcpFwUpdateStatus_ = FW_UPDATE_STATUS_FAILED; + saraNcpFwUpdateError_ = SYSTEM_ERROR_SARA_NCP_FW_UPDATE_SAME_VERSION; + break; // fail early + } + delay(20000); // slow down the log output + } + if (millis() - startTimer_ >= NCP_FW_MODEM_INSTALL_FINISH_TIMEOUT) { + NCPFW_LOG_DEBUG(ERROR, "Install process timed out!"); + saraNcpFwUpdateStatus_ = FW_UPDATE_STATUS_FAILED; + saraNcpFwUpdateError_ = SYSTEM_ERROR_SARA_NCP_FW_UPDATE_INSTALL_TIMEOUT; + break; + } + } + + if (saraNcpFwUpdateStatus_ == FW_UPDATE_STATUS_SUCCESS) { + NCPFW_LOG_DEBUG(INFO, "Firware update success."); + } else { + NCPFW_LOG_DEBUG(ERROR, "Firmware update failed!!"); + } + // Power cycle the modem... who know's what odd state we are in. + saraNcpFwUpdateState_ = FW_UPDATE_STATE_FINISHED_POWER_OFF; + } + break; + + case FW_UPDATE_STATE_FINISHED_POWER_OFF: + { + NCPFW_LOG_DEBUG(INFO, "Power cycling modem and reconnecting to Cloud..."); + saraNcpFwUpdateData_.state = FW_UPDATE_STATE_FINISHED_POWER_OFF; + saraNcpFwUpdateData_.status = saraNcpFwUpdateStatus_; + saraNcpFwUpdateData_.error = saraNcpFwUpdateError_; + saveSaraNcpFwUpdateData(); + cellular_start_ncp_firmware_update(false, nullptr); + network_off(NETWORK_INTERFACE_CELLULAR, 0, 0, nullptr); // Cellular.off() + startTimer_ = millis(); + saraNcpFwUpdateState_ = FW_UPDATE_STATE_FINISHED_POWERING_OFF; + } + break; + + case FW_UPDATE_STATE_FINISHED_POWERING_OFF: + { + if (millis() - startTimer_ >= NCP_FW_MODEM_POWER_OFF_TIMEOUT) { + NCPFW_LOG_DEBUG(ERROR, "Powering modem off timed out!"); + saraNcpFwUpdateState_ = FW_UPDATE_STATE_FINISHED_IDLE; + saraNcpFwUpdateStatus_ = FW_UPDATE_STATUS_FAILED; + saraNcpFwUpdateError_ = SYSTEM_ERROR_SARA_NCP_FW_UPDATE_POWER_OFF_TIMEOUT; + } + if (network_is_off(NETWORK_INTERFACE_CELLULAR, nullptr)) { + NCPFW_LOG_DEBUG(INFO, "Powering modem off success"); + startTimer_ = millis(); + saraNcpFwUpdateState_ = FW_UPDATE_STATE_FINISHED_CLOUD_CONNECTING; + saraNcpFwUpdateData_.state = saraNcpFwUpdateState_; + saraNcpFwUpdateData_.status = saraNcpFwUpdateStatus_; + saraNcpFwUpdateData_.error = saraNcpFwUpdateError_; + saveSaraNcpFwUpdateData(); + } + } + break; + + case FW_UPDATE_STATE_FINISHED_CLOUD_CONNECTING: + { + if (millis() - startTimer_ >= NCP_FW_MODEM_CLOUD_CONNECT_TIMEOUT) { + if (++finishedCloudConnectingRetries_ < 2) { + saraNcpFwUpdateState_ = FW_UPDATE_STATE_FINISHED_POWER_OFF; + } else { + saraNcpFwUpdateState_ = FW_UPDATE_STATE_FINISHED_IDLE; + saraNcpFwUpdateStatus_ = FW_UPDATE_STATUS_FAILED; + saraNcpFwUpdateError_ = SYSTEM_ERROR_SARA_NCP_FW_UPDATE_CLOUD_CONNECT_ON_EXIT_TIMEOUT; + } + } else if (saraNcpFwUpdateCallbacks_.spark_cloud_flag_connected()) { // Particle.connected() + saraNcpFwUpdateState_ = FW_UPDATE_STATE_FINISHED_CLOUD_CONNECTED; + } else { + saraNcpFwUpdateStatusDiagnostics_ = saraNcpFwUpdateStatus_; // ready our diagnostics status value before connecting to the cloud + saraNcpFwUpdateErrorDiagnostics_ = saraNcpFwUpdateError_; + saraNcpFwUpdateCallbacks_.spark_cloud_flag_connect(); // Particle.connect() + cooldown(1000); + } + } + break; + + case FW_UPDATE_STATE_FINISHED_CLOUD_CONNECTED: + { + if (saraNcpFwUpdateCallbacks_.spark_cloud_flag_connected()) { // Particle.connected() + if (!saraNcpFwUpdateCallbacks_.publishEvent("spark/device/ncp/update", (saraNcpFwUpdateStatus_ == FW_UPDATE_STATUS_SUCCESS) ? "success" : "failed", /*flags PUBLISH_EVENT_FLAG_PRIVATE*/1)) { + saraNcpFwUpdateStatus_ = FW_UPDATE_STATUS_FAILED; + saraNcpFwUpdateError_ = SYSTEM_ERROR_SARA_NCP_FW_UPDATE_PUBLISH_RESULT; + } + } else { + saraNcpFwUpdateStatus_ = FW_UPDATE_STATUS_FAILED; + saraNcpFwUpdateError_ = SYSTEM_ERROR_SARA_NCP_FW_UPDATE_PUBLISH_RESULT; + } + saraNcpFwUpdateState_ = FW_UPDATE_STATE_FINISHED_IDLE; + } + break; + + case FW_UPDATE_STATE_FINISHED_IDLE: + { + saraNcpFwUpdateData_.state = FW_UPDATE_STATE_FINISHED_IDLE; + saraNcpFwUpdateData_.status = saraNcpFwUpdateStatus_; + saraNcpFwUpdateData_.error = saraNcpFwUpdateError_; + saraNcpFwUpdateData_.updateAvailable = SYSTEM_NCP_FW_UPDATE_UNKNOWN; // clear persistence after attempting an update + saraNcpFwUpdateData_.isUserConfig = false; // clear persistence after attempting an update + saveSaraNcpFwUpdateData(); + saraNcpFwUpdateState_ = FW_UPDATE_STATE_IDLE; + } + break; + + case FW_UPDATE_STATE_IDLE: + default: + { + saraNcpFwUpdateState_ = FW_UPDATE_STATE_IDLE; + if (saraNcpFwUpdateStatus_ != FW_UPDATE_STATUS_IDLE && inSafeMode()) { + NCPFW_LOG_DEBUG(INFO, "Resetting out of Safe Mode!"); + delay(200); + // Reset with graceful disconnect from Cloud + system_reset(SYSTEM_RESET_MODE_NORMAL, 0, 0, 0, nullptr); + } + } + break; + } // switch end + + return SYSTEM_ERROR_NONE; +} + +// static +int SaraNcpFwUpdate::cbUHTTPER(int type, const char* buf, int len, HTTPSerror* 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 SaraNcpFwUpdate::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; +} + +uint32_t SaraNcpFwUpdate::getNcpFirmwareVersion() { + uint32_t version = 0; + if (cellular_get_ncp_firmware_version(&version, nullptr) != SYSTEM_ERROR_NONE) { + return 0; // specifically 0 here for version error + } + + return version; +} + +system_error_t SaraNcpFwUpdate::setupHTTPSProperties() { + return setupHTTPSProperties_impl(); +} + +void SaraNcpFwUpdate::cooldown(system_tick_t timer) { + cooldownTimeout_ = timer; + cooldownTimer_ = millis(); +} +void SaraNcpFwUpdate::updateCooldown() { + if (cooldownTimer_ && millis() - cooldownTimer_ >= cooldownTimeout_) { + cooldownTimer_ = 0; + cooldownTimeout_ = 0; + } +} +bool SaraNcpFwUpdate::inCooldown() { + return cooldownTimer_ > 0; +} + +} // namespace services + +} // namespace particle + +int sara_ncp_fw_update_config(const SaraNcpFwUpdateConfig* userConfigData, void* reserved) { + return particle::services::SaraNcpFwUpdate::instance()->setConfig(userConfigData); +} + +#else // #if HAL_PLATFORM_NCP_FW_UPDATE + +int sara_ncp_fw_update_config(const SaraNcpFwUpdateConfig* userConfigData, void* reserved) { + return SYSTEM_ERROR_NONE; +} + +#endif // #if HAL_PLATFORM_NCP_FW_UPDATE + +#endif // #if MODULE_FUNCTION != 2 \ No newline at end of file diff --git a/system/inc/system_update.h b/system/inc/system_update.h index 4ccf7d426c..443161d4d0 100644 --- a/system/inc/system_update.h +++ b/system/inc/system_update.h @@ -76,72 +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; - /** * Firmware update status. */ diff --git a/system/src/main.cpp b/system/src/main.cpp index 936f4b7409..bc32fab564 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" + +// UNCOMMENT TO DEBUG SAFE MODE +// #include "debug_output_handler.h" +// spark::Serial1LogHandler g_logHandlerSerial1(115200, LOG_LEVEL_ALL); +#endif + #if HAL_PLATFORM_RADIO_STACK #include "radio_common.h" #endif @@ -798,6 +806,18 @@ void app_setup_and_loop(void) Network_Setup(threaded); // todo - why does this come before system thread initialization? +#if HAL_PLATFORM_NCP_FW_UPDATE + SaraNcpFwUpdateCallbacks saraNcpFwUpdateCallbacks; + memset(&saraNcpFwUpdateCallbacks, 0, sizeof(saraNcpFwUpdateCallbacks)); + saraNcpFwUpdateCallbacks.size = sizeof(saraNcpFwUpdateCallbacks); + saraNcpFwUpdateCallbacks.system_get_flag = system_get_flag; + saraNcpFwUpdateCallbacks.spark_cloud_flag_connected = spark_cloud_flag_connected; + saraNcpFwUpdateCallbacks.spark_cloud_flag_connect = spark_cloud_flag_connect; + saraNcpFwUpdateCallbacks.spark_cloud_flag_disconnect = spark_cloud_flag_disconnect; + saraNcpFwUpdateCallbacks.publishEvent = publishEvent; + services::SaraNcpFwUpdate::instance()->init(saraNcpFwUpdateCallbacks); +#endif + #if PLATFORM_THREADING if (threaded) { diff --git a/system/src/system_task.cpp b/system/src/system_task.cpp index 261c0e85b1..be513e41a9 100644 --- a/system/src/system_task.cpp +++ b/system/src/system_task.cpp @@ -57,6 +57,7 @@ #if HAL_PLATFORM_IFAPI #include "system_listening_mode.h" #endif +#include "ncp_fw_update.h" #if HAL_PLATFORM_BLE #include "ble_hal.h" @@ -505,6 +506,10 @@ void Spark_Idle_Events(bool force_events/*=false*/) if (system_mode() != SAFE_MODE) { manage_listening_mode_flag(); } + +#if HAL_PLATFORM_NCP_FW_UPDATE + services::SaraNcpFwUpdate::instance()->process(); +#endif } else { diff --git a/test/unit_tests/CMakeLists.txt b/test/unit_tests/CMakeLists.txt index fc25563ff9..6f8542f6ab 100644 --- a/test/unit_tests/CMakeLists.txt +++ b/test/unit_tests/CMakeLists.txt @@ -71,6 +71,7 @@ add_subdirectory(services) add_subdirectory(wiring) add_subdirectory(hal) add_subdirectory(system) +add_subdirectory(ncp_fw_update) # Create `coverage` target in the `make` command add_custom_target( coverage diff --git a/test/unit_tests/README.md b/test/unit_tests/README.md index cbb99a0fd3..8433cc827c 100644 --- a/test/unit_tests/README.md +++ b/test/unit_tests/README.md @@ -7,7 +7,7 @@ Building and running tests Create a build directory: ```bash -rm -rf .build && mkdir .build && cd .build +rm -rf build && mkdir build && cd build ``` Generate build files: @@ -16,8 +16,44 @@ Generate build files: cmake .. ``` +Build and run the tests: + +```bash +device-os/test/unit_tests/build $ make all test +``` + +Build and run just one set of tests (ncp_fw_update): + +```bash +device-os/test/unit_tests/build/ $ cd ncp_fw_update +device-os/test/unit_tests/build/ncp_fw_update $ make all test +``` + Build and run the tests and coverage: ```bash -make all test coverage +device-os/test/unit_tests/build $ make all test coverage ``` + +Build and run the tests and coverage, and output verbose errors on failure: + +```bash +device-os/test/unit_tests/build $ make all test coverage CTEST_OUTPUT_ON_FAILURE=TRUE +``` + +Enable verbose output for monitoring all of those printf() statements while debugging tests +(note: please don't forget to disable these printf's when finished debugging) + +```bash +device-os/test/unit_tests/build $ make all test coverage ARGS=--verbose +``` + + +MacOSX +------ + +Github issue: https://github.com/Homebrew/homebrew-core/issues/67427 + +You may run into an error `ld: library not found for -licudata` and will need to export the following before running `make` + +`export LIBRARY_PATH=${LIBRARY_PATH}:/usr/local/opt/icu4c/lib` diff --git a/test/unit_tests/ncp_fw_update/CMakeLists.txt b/test/unit_tests/ncp_fw_update/CMakeLists.txt new file mode 100644 index 0000000000..64d4d97752 --- /dev/null +++ b/test/unit_tests/ncp_fw_update/CMakeLists.txt @@ -0,0 +1,48 @@ +set(target_name ncp_fw_update) + +# Create test executable +add_executable( ${target_name} + ${DEVICE_OS_DIR}/services/src/ncp_fw_update.cpp + ${TEST_DIR}/stub/system_cache.cpp + ncp_fw_update.cpp + stubs.cpp + main.cpp +) + +# Set defines specific to target +target_compile_definitions( ${target_name} + PRIVATE PLATFORM_ID=3 + PRIVATE HAL_PLATFORM_NCP_FW_UPDATE=1 + PRIVATE HAL_PLATFORM_NCP=1 + PRIVATE HAL_PLATFORM_NCP_AT=1 + PRIVATE HAL_PLATFORM_WIFI_SERIAL=0 +) + +# Set compiler flags specific to target +target_compile_options( ${target_name} + PRIVATE ${COVERAGE_CFLAGS} +) + +# Set include path specific to target +target_include_directories( ${target_name} + PRIVATE ${TEST_DIR} + PRIVATE ${TEST_DIR}/stub + PRIVATE ${DEVICE_OS_DIR}/hal/ + PRIVATE ${DEVICE_OS_DIR}/hal/inc/ + PRIVATE ${DEVICE_OS_DIR}/hal/shared/ + PRIVATE ${DEVICE_OS_DIR}/system/inc/ + PRIVATE ${DEVICE_OS_DIR}/services/inc/ + PRIVATE ${DEVICE_OS_DIR}/wiring/inc/ + PRIVATE ${DEVICE_OS_DIR}/dynalib/inc/ + PRIVATE ${DEVICE_OS_DIR}/hal/src/gcc/ + PRIVATE ${DEVICE_OS_DIR}/hal/network/ncp/ + PRIVATE ${DEVICE_OS_DIR}/hal/network/ncp/at_parser/ + PRIVATE ${THIRD_PARTY_DIR}/hippomocks +) + +# Link against dependencies specific to target + +# Add tests to `test` target +catch_discover_tests( ${target_name} + TEST_PREFIX ${target_name}_ +) diff --git a/test/unit_tests/ncp_fw_update/main.cpp b/test/unit_tests/ncp_fw_update/main.cpp new file mode 100644 index 0000000000..840b80c3a8 --- /dev/null +++ b/test/unit_tests/ncp_fw_update/main.cpp @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2019 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 . + */ + +#define CATCH_CONFIG_MAIN + +#include diff --git a/test/unit_tests/ncp_fw_update/ncp_fw_update.cpp b/test/unit_tests/ncp_fw_update/ncp_fw_update.cpp new file mode 100644 index 0000000000..2c16e925be --- /dev/null +++ b/test/unit_tests/ncp_fw_update/ncp_fw_update.cpp @@ -0,0 +1,1385 @@ +/* + * 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 . + */ + +#include +#include "ncp_fw_update.h" + +#include +#include + +#include +#include "system_mode.h" +#ifdef INFO +#undef INFO +#endif +#ifdef WARN +#undef WARN +#endif +#include "system_network.h" +#include "cellular_enums_hal.h" + +using namespace particle::services; +using namespace particle; + +namespace { + int system_get_flag(system_flag_t flag, uint8_t* value, void*) { + return 0; + } + + bool spark_cloud_flag_connected(void) { + return false; + } + + void spark_cloud_flag_connect(void) { + } + + void spark_cloud_flag_disconnect(void) { + } + + bool publishEvent(const char* event, const char* data, unsigned flags) { + return false; + } + + const SaraNcpFwUpdateConfig saraNcpFwUpdateConfigUpgrade = { + .size = sizeof(SaraNcpFwUpdateConfig), + .start_version = 31400010, + .end_version = 31400011, + /*filename*/ "SARA-R510S-01B-00-ES-0314A0001_SARA-R510S-01B-00-XX-0314ENG0099A0001.upd", + /*md5sum*/ "09c1a98d03c761bcbea50355f9b2a50f" + }; + + const SaraNcpFwUpdateConfig saraNcpFwUpdateConfigDowngrade = { + .size = sizeof(SaraNcpFwUpdateConfig), + .start_version = 31400011, + .end_version = 31400010, + /*filename*/ "SARA-R510S-01B-00-XX-0314ENG0099A0001_SARA-R510S-01B-00-ES-0314A0001.upd", + /*md5sum*/ "09c1a98d03c761bcbea50355f9b2a50f" + }; +} + +class SaraNcpFwUpdateMocks { +public: + + SaraNcpFwUpdateMocks(MockRepository* mocks) : + mocks_(mocks), + system_mode_(AUTOMATIC), + mock_modem_version_(0), + mock_network_is_on_(false), + mock_network_is_off_(false), + mock_platform_ncp_id_(PLATFORM_NCP_SARA_R510), + mock_spark_cloud_flag_connected_(false), + mock_spark_cloud_flag_connect_(false) { + mocks_->OnCallFunc(system_mode).Do([&]() -> System_Mode_TypeDef { + return system_mode_; + }); + mocks_->OnCallFunc(cellular_get_ncp_firmware_version).Do([&](uint32_t* version, void* reserved) -> int { + *version = mock_modem_version_; + return SYSTEM_ERROR_NONE; + }); + mocks_->OnCallFunc(network_is_on).Do([&](network_handle_t network, void* reserved) -> bool { + return mock_network_is_on_; + }); + mocks_->OnCallFunc(network_is_off).Do([&](network_handle_t network, void* reserved) -> bool { + return mock_network_is_off_; + }); + mocks_->OnCallFunc(platform_primary_ncp_identifier).Do([&]() -> PlatformNCPIdentifier { + return mock_platform_ncp_id_; + }); + mocks_->OnCallFunc(spark_cloud_flag_connected).Do([&]() -> bool { + return mock_spark_cloud_flag_connected_; + }); + mocks_->OnCallFunc(spark_cloud_flag_connect).Do([&]() { + mock_spark_cloud_flag_connect_ = true; + }); + mocks_->OnCallFunc(spark_cloud_flag_disconnect).Do([&]() { + mock_spark_cloud_flag_connect_ = false; + }); + }; + + void setSystemMode(System_Mode_TypeDef mode) { + system_mode_ = mode; + } + void setModemVersion(uint32_t v) { + mock_modem_version_ = v; + } + void setNetworkIsOn(bool state) { + mock_network_is_on_ = state; + } + void setNetworkIsOff(bool state) { + mock_network_is_off_ = state; + } + void setNcpId(PlatformNCPIdentifier ncpid) { + mock_platform_ncp_id_ = ncpid; + } + void setSparkCloudFlagConnected(bool connected) { + mock_spark_cloud_flag_connected_ = connected; + } + bool getSparkCloudFlagConnect() { + return mock_spark_cloud_flag_connect_; + } + +private: + MockRepository* mocks_; + System_Mode_TypeDef system_mode_; + uint32_t mock_modem_version_; + bool mock_network_is_on_; + bool mock_network_is_off_; + PlatformNCPIdentifier mock_platform_ncp_id_; + bool mock_spark_cloud_flag_connected_; + bool mock_spark_cloud_flag_connect_; +}; + +class SaraNcpFwUpdateTest : public SaraNcpFwUpdate { +public: + + SaraNcpFwUpdateTest() : + SaraNcpFwUpdate() { + }; + + SaraNcpFwUpdateState getSaraNcpFwUpdateState() const { + return saraNcpFwUpdateState_; + } + void setSaraNcpFwUpdateState(SaraNcpFwUpdateState state) { + saraNcpFwUpdateState_ = state; + } + SaraNcpFwUpdateState getSaraNcpFwUpdateLastState() const { + return saraNcpFwUpdateLastState_; + } + void setSaraNcpFwUpdateLastState(SaraNcpFwUpdateState state) { + saraNcpFwUpdateLastState_ = state; + } + SaraNcpFwUpdateStatus getSaraNcpFwUpdateStatus() const { + return saraNcpFwUpdateStatus_; + } + void setSaraNcpFwUpdateStatus(SaraNcpFwUpdateStatus status) { + saraNcpFwUpdateStatus_ = status; + } + system_error_t getSaraNcpFwUpdateError() const { + return saraNcpFwUpdateError_; + } + void setSaraNcpFwUpdateError(system_error_t error) { + saraNcpFwUpdateError_ = error; + } + SaraNcpFwUpdateStatus getSaraNcpFwUpdateStatusDiagnostics() const { + return saraNcpFwUpdateStatusDiagnostics_; + } + system_error_t getSaraNcpFwUpdateErrorDiagnostics() const { + return saraNcpFwUpdateErrorDiagnostics_; + } + uint32_t getStartingFirmwareVersion() const { + return startingFirmwareVersion_; + } + uint32_t getFirmwareVersion() const { + return firmwareVersion_; + } + uint32_t getUpdateVersion() const { + return updateVersion_; + } + int getUpdateAvailable() const { + return updateAvailable_; + } + void setUpdateAvailable(uint32_t ua) { + updateAvailable_ = ua; + } + int getDownloadRetries() const { + return downloadRetries_; + } + int getFinishedCloudConnectingRetries() const { + return finishedCloudConnectingRetries_; + } + volatile int getCgevDeactProfile() const { + return cgevDeactProfile_; + } + void setCgevDeactProfile(int profile) { + cgevDeactProfile_ = profile; + } + system_tick_t getStartTimer() const { + return startTimer_; + } + system_tick_t getAtOkCheckTimer() const { + return atOkCheckTimer_; + } + system_tick_t getCooldownTimer() const { + return cooldownTimer_; + } + system_tick_t getCooldownTimeout() const { + return cooldownTimeout_; + } + bool callInCooldown() { + return inCooldown(); + } + void callUpdateCooldown() { + return updateCooldown(); + } + void callCooldown(system_tick_t delay_ms) { + return cooldown(delay_ms); + } + bool getIsUserConfig() const { + return isUserConfig_; + } + bool getInitialized() const { + return initialized_; + } + + void setSaraNcpFwUpdateData(SaraNcpFwUpdateData data) { + memcpy(&saraNcpFwUpdateData_, &data, sizeof(saraNcpFwUpdateData_)); + } + SaraNcpFwUpdateData getSaraNcpFwUpdateData() { + return saraNcpFwUpdateData_; + } + SaraNcpFwUpdateCallbacks getSaraNcpFwUpdateCallbacks() { + return saraNcpFwUpdateCallbacks_; + } + HTTPSresponse getHttpsResp() { + return httpsResp_; + } + void setHttpsResp(HTTPSresponse data) { + memcpy(&httpsResp_, &data, sizeof(httpsResp_)); + } + + void callValidateSaraNcpFwUpdateData() { + validateSaraNcpFwUpdateData(); + } + void logSaraNcpFwUpdateData(const SaraNcpFwUpdateData& data) { + printf("saraNcpFwUpdateData size:%u state:%d status:%d error:%d fv:%" PRIu32 " sfv:%" PRIu32 " uv:%" PRIu32 "\r\n", + data.size, + data.state, + data.status, + data.error, + data.firmwareVersion, + data.startingFirmwareVersion, + data.updateVersion); + printf("ua:%d iuc:%d sv:%" PRIu32 " ev:%" PRIu32 " file:%s md5:%s\r\n", + data.updateAvailable, + data.isUserConfig, + data.userConfigData.start_version, + data.userConfigData.end_version, + data.userConfigData.filename, + data.userConfigData.md5sum); + } + int callSaveSaraNcpFwUpdateData() { + return saveSaraNcpFwUpdateData(); + } + int callRecallSaraNcpFwUpdateData() { + return recallSaraNcpFwUpdateData(); + } + int callDeleteSaraNcpFwUpdateData() { + return deleteSaraNcpFwUpdateData(); + } + int callGetConfigData(SaraNcpFwUpdateConfig& configData) { + return getConfigData(configData); + } + void reset() { + saraNcpFwUpdateState_ = FW_UPDATE_STATE_IDLE; + saraNcpFwUpdateLastState_ = saraNcpFwUpdateState_; + saraNcpFwUpdateStatus_ = FW_UPDATE_STATUS_IDLE; + saraNcpFwUpdateError_ = SYSTEM_ERROR_NONE; + saraNcpFwUpdateStatusDiagnostics_ = FW_UPDATE_STATUS_NONE; + saraNcpFwUpdateErrorDiagnostics_ = SYSTEM_ERROR_NONE; + startingFirmwareVersion_ = 1; + firmwareVersion_ = 0; + updateVersion_ = 2; + updateAvailable_ = SYSTEM_NCP_FW_UPDATE_UNKNOWN; + downloadRetries_ = 0; + finishedCloudConnectingRetries_ = 0; + cgevDeactProfile_ = 0; + startTimer_ = 0; + atOkCheckTimer_ = 0; + cooldownTimer_ = 0; + cooldownTimeout_ = 0; + isUserConfig_ = false; + initialized_ = false; + } + int setupHTTPSProperties() { + return SYSTEM_ERROR_NONE; + } + + +private: +}; + +TEST_CASE("SaraNcpFwUpdate") { + + SECTION("put fail early REQUIRE's in this section!") { + + MockRepository mocks; + SaraNcpFwUpdateMocks ncpMocks(&mocks); + SaraNcpFwUpdateTest ncpTest; + REQUIRE(ncpTest.callDeleteSaraNcpFwUpdateData() == SYSTEM_ERROR_NONE); // start clean + + SaraNcpFwUpdateCallbacks saraNcpFwUpdateCallbacks = {}; + saraNcpFwUpdateCallbacks.size = sizeof(saraNcpFwUpdateCallbacks); + saraNcpFwUpdateCallbacks.system_get_flag = system_get_flag; + saraNcpFwUpdateCallbacks.spark_cloud_flag_connected = spark_cloud_flag_connected; + saraNcpFwUpdateCallbacks.spark_cloud_flag_connect = spark_cloud_flag_connect; + saraNcpFwUpdateCallbacks.spark_cloud_flag_disconnect = spark_cloud_flag_disconnect; + saraNcpFwUpdateCallbacks.publishEvent = publishEvent; + + // Production debugging setttings - with verbose errors + const char* SARA_NCP_FW_UPDATE_ENABLE_DOWNLOAD_SETTING = SARA_NCP_FW_UPDATE_ENABLE_DOWNLOAD ? "SARA_NCP_FW_UPDATE_ENABLE_DOWNLOAD = 1" : "SARA_NCP_FW_UPDATE_ENABLE_DOWNLOAD = 0"; + const char* SARA_NCP_FW_UPDATE_ENABLE_INSTALL_SETTING = SARA_NCP_FW_UPDATE_ENABLE_INSTALL ? "SARA_NCP_FW_UPDATE_ENABLE_INSTALL = 1" : "SARA_NCP_FW_UPDATE_ENABLE_INSTALL = 0"; + const char* SARA_NCP_FW_UPDATE_ENABLE_DEBUG_LOGGING_SETTING = SARA_NCP_FW_UPDATE_ENABLE_DEBUG_LOGGING ? "SARA_NCP_FW_UPDATE_ENABLE_DEBUG_LOGGING = 1" : "SARA_NCP_FW_UPDATE_ENABLE_DEBUG_LOGGING = 0"; + REQUIRE(SARA_NCP_FW_UPDATE_ENABLE_DOWNLOAD_SETTING == "SARA_NCP_FW_UPDATE_ENABLE_DOWNLOAD = 1"); + REQUIRE(SARA_NCP_FW_UPDATE_ENABLE_INSTALL_SETTING == "SARA_NCP_FW_UPDATE_ENABLE_INSTALL = 1"); + REQUIRE(SARA_NCP_FW_UPDATE_ENABLE_DEBUG_LOGGING_SETTING == "SARA_NCP_FW_UPDATE_ENABLE_DEBUG_LOGGING = 0"); + + SECTION("init") { + REQUIRE(system_mode() == AUTOMATIC); + + SECTION("init member variables") { + CHECK(ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_IDLE); + CHECK(ncpTest.getSaraNcpFwUpdateLastState() == FW_UPDATE_STATE_IDLE); + CHECK(ncpTest.getSaraNcpFwUpdateStatus() == FW_UPDATE_STATUS_IDLE); + CHECK(ncpTest.getSaraNcpFwUpdateError() == SYSTEM_ERROR_NONE); + CHECK(ncpTest.getSaraNcpFwUpdateStatusDiagnostics() == FW_UPDATE_STATUS_NONE); + CHECK(ncpTest.getSaraNcpFwUpdateErrorDiagnostics() == SYSTEM_ERROR_NONE); + CHECK(ncpTest.getStartingFirmwareVersion() == 1); + CHECK(ncpTest.getFirmwareVersion() == 0); + CHECK(ncpTest.getUpdateVersion() == 2); + CHECK(ncpTest.getUpdateAvailable() == SYSTEM_NCP_FW_UPDATE_UNKNOWN); + CHECK(ncpTest.getDownloadRetries() == 0); + CHECK(ncpTest.getFinishedCloudConnectingRetries() == 0); + CHECK(ncpTest.getCgevDeactProfile() == 0); + CHECK(ncpTest.getStartTimer() == 0); + CHECK(ncpTest.getAtOkCheckTimer() == 0); + CHECK(ncpTest.getCooldownTimer() == 0); + CHECK(ncpTest.getCooldownTimeout() == 0); + CHECK(ncpTest.getIsUserConfig() == false); + CHECK(ncpTest.getInitialized() == false); + + // initialized_ == false; + CHECK(ncpTest.enableUpdates() == SYSTEM_ERROR_INVALID_STATE); + CHECK(ncpTest.updateStatus() == SYSTEM_ERROR_INVALID_STATE); + } + + SECTION("initialize callbacks") { + REQUIRE(ncpTest.getInitialized() == false); + ncpTest.init(saraNcpFwUpdateCallbacks); + SaraNcpFwUpdateCallbacks testCb = ncpTest.getSaraNcpFwUpdateCallbacks(); + CHECK(testCb.size == sizeof(saraNcpFwUpdateCallbacks)); + CHECK(testCb.system_get_flag == system_get_flag); + CHECK(testCb.spark_cloud_flag_connected == spark_cloud_flag_connected); + CHECK(testCb.spark_cloud_flag_connect == spark_cloud_flag_connect); + CHECK(testCb.spark_cloud_flag_disconnect == spark_cloud_flag_disconnect); + CHECK(testCb.publishEvent == publishEvent); + CHECK(ncpTest.getInitialized() == true); + } + + SECTION("init() system_mode AUTOMATIC - state FW_UPDATE_STATE_IDLE - first boot") { + // fresh first boot + REQUIRE(ncpTest.callDeleteSaraNcpFwUpdateData() == SYSTEM_ERROR_NONE); + + REQUIRE(ncpTest.getInitialized() == false); + ncpTest.init(saraNcpFwUpdateCallbacks); + CHECK(ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_IDLE); + CHECK(ncpTest.getSaraNcpFwUpdateStatus() == FW_UPDATE_STATUS_IDLE); + CHECK(ncpTest.getSaraNcpFwUpdateError() == SYSTEM_ERROR_NONE); + CHECK(ncpTest.getUpdateAvailable() == SYSTEM_NCP_FW_UPDATE_UNKNOWN); + CHECK(ncpTest.getInitialized() == true); + } + + SECTION("init() system_mode AUTOMATIC - state FW_UPDATE_STATE_INSTALL_WAITING - resuming update") { + // Store in cache + const SaraNcpFwUpdateData saraNcpFwUpdateData = { + .size = sizeof(SaraNcpFwUpdateData), + .state = FW_UPDATE_STATE_INSTALL_WAITING, + .status = FW_UPDATE_STATUS_UPDATING, + .error = SYSTEM_ERROR_NONE, + .firmwareVersion = 2, + .startingFirmwareVersion = 3, + .updateVersion = 4, + .updateAvailable = 1, + .isUserConfig = 0, + .userConfigData = {} + }; + ncpTest.setSaraNcpFwUpdateData(saraNcpFwUpdateData); + REQUIRE(ncpTest.callSaveSaraNcpFwUpdateData() == SYSTEM_ERROR_NONE); + + mocks.ExpectCallFunc(system_reset).With(SYSTEM_RESET_MODE_SAFE, 0, 0, SYSTEM_RESET_FLAG_NO_WAIT, _).Return(0); + REQUIRE(ncpTest.getInitialized() == false); + ncpTest.init(saraNcpFwUpdateCallbacks); + CHECK(ncpTest.getInitialized() == true); + } + + SECTION("init() system_mode AUTOMATIC - state FW_UPDATE_STATE_FINISHED_IDLE - finished update") { + // Store in cache + const SaraNcpFwUpdateData saraNcpFwUpdateData = { + .size = sizeof(SaraNcpFwUpdateData), + .state = FW_UPDATE_STATE_FINISHED_IDLE, + .status = FW_UPDATE_STATUS_SUCCESS, + .error = SYSTEM_ERROR_NONE, + .firmwareVersion = 2, + .startingFirmwareVersion = 3, + .updateVersion = 4, + .updateAvailable = 1, + .isUserConfig = 0, + .userConfigData = {} + }; + ncpTest.setSaraNcpFwUpdateData(saraNcpFwUpdateData); + REQUIRE(ncpTest.callSaveSaraNcpFwUpdateData() == SYSTEM_ERROR_NONE); + + // mocks.ExpectCallFunc(system_reset).With(SYSTEM_RESET_MODE_SAFE, 0, 0, SYSTEM_RESET_FLAG_NO_WAIT, _).Return(0); + REQUIRE(ncpTest.getInitialized() == false); + ncpTest.init(saraNcpFwUpdateCallbacks); + CHECK(ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_IDLE); + CHECK(ncpTest.getSaraNcpFwUpdateStatus() == FW_UPDATE_STATUS_IDLE); + CHECK(ncpTest.getSaraNcpFwUpdateData().status == FW_UPDATE_STATUS_IDLE); + CHECK(ncpTest.getSaraNcpFwUpdateData().error == SYSTEM_ERROR_NONE); + CHECK(ncpTest.getSaraNcpFwUpdateData().updateAvailable == SYSTEM_NCP_FW_UPDATE_UNKNOWN); + CHECK(ncpTest.getUpdateAvailable() == SYSTEM_NCP_FW_UPDATE_UNKNOWN); + CHECK(ncpTest.getSaraNcpFwUpdateStatusDiagnostics() == saraNcpFwUpdateData.status); + CHECK(ncpTest.getSaraNcpFwUpdateErrorDiagnostics() == saraNcpFwUpdateData.error); + CHECK(ncpTest.getInitialized() == true); + } + + SECTION("init() system_mode SAFE_MODE - state FW_UPDATE_STATE_SETUP_CLOUD_CONNECT") { + ncpMocks.setSystemMode(SAFE_MODE); + REQUIRE(system_mode() == SAFE_MODE); + + // Store in cache + const SaraNcpFwUpdateData saraNcpFwUpdateData = { + .size = sizeof(SaraNcpFwUpdateData), + .state = FW_UPDATE_STATE_SETUP_CLOUD_CONNECT, + .status = FW_UPDATE_STATUS_DOWNLOADING, // give it some status other than IDLE + .error = SYSTEM_ERROR_NONE, + .firmwareVersion = 2, + .startingFirmwareVersion = 3, + .updateVersion = 4, + .updateAvailable = 1, + .isUserConfig = 1, + .userConfigData = {} + }; + ncpTest.setSaraNcpFwUpdateData(saraNcpFwUpdateData); + REQUIRE(ncpTest.callSaveSaraNcpFwUpdateData() == SYSTEM_ERROR_NONE); + + REQUIRE(ncpTest.getInitialized() == false); + ncpTest.init(saraNcpFwUpdateCallbacks); + CHECK(ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_SETUP_CLOUD_CONNECT); + CHECK(ncpTest.getSaraNcpFwUpdateStatus() == FW_UPDATE_STATUS_DOWNLOADING); + CHECK(ncpTest.getSaraNcpFwUpdateData().status == saraNcpFwUpdateData.status); + CHECK(ncpTest.getSaraNcpFwUpdateData().error == SYSTEM_ERROR_NONE); + CHECK(ncpTest.getSaraNcpFwUpdateData().firmwareVersion == saraNcpFwUpdateData.firmwareVersion); + CHECK(ncpTest.getSaraNcpFwUpdateData().updateVersion == saraNcpFwUpdateData.updateVersion); + CHECK(ncpTest.getSaraNcpFwUpdateData().isUserConfig == saraNcpFwUpdateData.isUserConfig); + CHECK(ncpTest.getSaraNcpFwUpdateData().updateAvailable == saraNcpFwUpdateData.updateAvailable); + CHECK(ncpTest.getInitialized() == true); + } + + SECTION("init() system_mode SAFE_MODE - state FW_UPDATE_STATE_FINISHED_IDLE") { + ncpMocks.setSystemMode(SAFE_MODE); + REQUIRE(system_mode() == SAFE_MODE); + + // Store in cache + const SaraNcpFwUpdateData saraNcpFwUpdateData = { + .size = sizeof(SaraNcpFwUpdateData), + .state = FW_UPDATE_STATE_FINISHED_IDLE, + .status = FW_UPDATE_STATUS_IDLE, + .error = SYSTEM_ERROR_NONE, + .firmwareVersion = 2, + .startingFirmwareVersion = 3, + .updateVersion = 4, + .updateAvailable = 1, + .isUserConfig = 1, + .userConfigData = {} + }; + ncpTest.setSaraNcpFwUpdateData(saraNcpFwUpdateData); + REQUIRE(ncpTest.callSaveSaraNcpFwUpdateData() == SYSTEM_ERROR_NONE); + + REQUIRE(ncpTest.getInitialized() == false); + ncpTest.init(saraNcpFwUpdateCallbacks); + CHECK(ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_FINISHED_IDLE); + CHECK(ncpTest.getSaraNcpFwUpdateStatus() == FW_UPDATE_STATUS_IDLE); + CHECK(ncpTest.getSaraNcpFwUpdateData().status == FW_UPDATE_STATUS_IDLE); + CHECK(ncpTest.getSaraNcpFwUpdateData().error == SYSTEM_ERROR_NONE); + CHECK(ncpTest.getSaraNcpFwUpdateData().firmwareVersion == saraNcpFwUpdateData.firmwareVersion); + CHECK(ncpTest.getSaraNcpFwUpdateData().updateVersion == saraNcpFwUpdateData.updateVersion); + CHECK(ncpTest.getSaraNcpFwUpdateData().isUserConfig == saraNcpFwUpdateData.isUserConfig); + CHECK(ncpTest.getUpdateAvailable() == SYSTEM_NCP_FW_UPDATE_UNKNOWN); + CHECK(ncpTest.getSaraNcpFwUpdateData().updateAvailable == SYSTEM_NCP_FW_UPDATE_UNKNOWN); + CHECK(ncpTest.getInitialized() == true); + } + + SECTION("setConfig / checkUpdate / enableUpdates") { + ncpTest.init(saraNcpFwUpdateCallbacks); + + // ncpTest.logSaraNcpFwUpdateData(ncpTest.getSaraNcpFwUpdateData()); + + // setConfig - invalid state at checkUpdate due to modem error + ncpMocks.setModemVersion(0); + REQUIRE(ncpTest.updateStatus() == SYSTEM_NCP_FW_UPDATE_UNKNOWN); + REQUIRE(ncpTest.setConfig(nullptr) == SYSTEM_ERROR_INVALID_STATE); + REQUIRE(ncpTest.getIsUserConfig() == false); + REQUIRE(ncpTest.getSaraNcpFwUpdateData().isUserConfig == false); + + // setConfig - disable isUserConfig by sending nullptr - update not available + ncpMocks.setModemVersion(99999999); + REQUIRE(ncpTest.setConfig(nullptr) == SYSTEM_ERROR_NONE); + REQUIRE(ncpTest.updateStatus() == SYSTEM_NCP_FW_UPDATE_NOT_AVAILABLE); + REQUIRE(ncpTest.getIsUserConfig() == false); + REQUIRE(ncpTest.getSaraNcpFwUpdateData().isUserConfig == false); + REQUIRE(ncpTest.getUpdateAvailable() == SYSTEM_NCP_FW_UPDATE_NOT_AVAILABLE); + + // setConfig - enable isUserConfig by sending proper config - update not available + ncpMocks.setModemVersion(99999999); + REQUIRE(ncpTest.setConfig(&saraNcpFwUpdateConfigUpgrade) == SYSTEM_ERROR_NONE); + REQUIRE(ncpTest.updateStatus() == SYSTEM_NCP_FW_UPDATE_NOT_AVAILABLE); + REQUIRE(ncpTest.getIsUserConfig() == true); + REQUIRE(ncpTest.getSaraNcpFwUpdateData().isUserConfig == true); + REQUIRE(ncpTest.getUpdateAvailable() == SYSTEM_NCP_FW_UPDATE_NOT_AVAILABLE); + + // setConfig - enable isUserConfig by sending proper config - update available + ncpMocks.setModemVersion(31400010); + REQUIRE(ncpTest.setConfig(&saraNcpFwUpdateConfigUpgrade) == SYSTEM_ERROR_NONE); + REQUIRE(ncpTest.updateStatus() == SYSTEM_NCP_FW_UPDATE_PENDING); + REQUIRE(ncpTest.getIsUserConfig() == true); + REQUIRE(ncpTest.getSaraNcpFwUpdateData().firmwareVersion == ncpTest.getFirmwareVersion()); + REQUIRE(ncpTest.getSaraNcpFwUpdateData().isUserConfig == true); + REQUIRE(ncpTest.getSaraNcpFwUpdateData().updateAvailable == SYSTEM_NCP_FW_UPDATE_PENDING); + + // checkUpdate / enableUpdates / updateStatus + ncpMocks.setNetworkIsOn(false); + ncpMocks.setNetworkIsOff(false); + REQUIRE(ncpTest.enableUpdates() == SYSTEM_ERROR_INVALID_STATE); + REQUIRE(ncpTest.updateStatus() == SYSTEM_NCP_FW_UPDATE_PENDING); + ncpMocks.setNetworkIsOn(true); + ncpMocks.setNetworkIsOff(true); + REQUIRE(ncpTest.enableUpdates() == SYSTEM_ERROR_INVALID_STATE); + REQUIRE(ncpTest.updateStatus() == SYSTEM_NCP_FW_UPDATE_PENDING); + ncpMocks.setNetworkIsOn(false); + ncpMocks.setNetworkIsOff(true); + REQUIRE(ncpTest.enableUpdates() == SYSTEM_ERROR_INVALID_STATE); + REQUIRE(ncpTest.updateStatus() == SYSTEM_NCP_FW_UPDATE_PENDING); + ncpMocks.setNetworkIsOn(true); + ncpMocks.setNetworkIsOff(false); + REQUIRE(ncpTest.enableUpdates() == SYSTEM_ERROR_NONE); + REQUIRE(ncpTest.updateStatus() == SYSTEM_NCP_FW_UPDATE_IN_PROGRESS); + REQUIRE(ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_QUALIFY_FLAGS); + // updateAvailable == SYSTEM_NCP_FW_UPDATE_IN_PROGRESS blocks checkUpdate() + REQUIRE(ncpTest.checkUpdate() == SYSTEM_ERROR_INVALID_STATE); + + // delete cache - recall cache - not found + REQUIRE(ncpTest.callDeleteSaraNcpFwUpdateData() == SYSTEM_ERROR_NONE); + REQUIRE(ncpTest.callRecallSaraNcpFwUpdateData() == SYSTEM_ERROR_NOT_FOUND); + + // Check some reset variables + REQUIRE(ncpTest.getCooldownTimer() == 0); + REQUIRE(ncpTest.getCooldownTimeout() == 0); + REQUIRE(ncpTest.getDownloadRetries() == 0); + REQUIRE(ncpTest.getFinishedCloudConnectingRetries() == 0); + } + + SECTION("NCP_ID != PLATFORM_NCP_SARA_R510") { + ncpMocks.setNcpId(PLATFORM_NCP_SARA_R410); + CHECK(ncpTest.setConfig(nullptr) == SYSTEM_ERROR_NOT_SUPPORTED); + CHECK(ncpTest.checkUpdate() == SYSTEM_ERROR_NOT_SUPPORTED); + CHECK(ncpTest.enableUpdates() == SYSTEM_ERROR_NOT_SUPPORTED); + CHECK(ncpTest.updateStatus() == SYSTEM_ERROR_NOT_SUPPORTED); + CHECK(ncpTest.process() == SYSTEM_ERROR_NOT_SUPPORTED); + } + } // SECTION INIT + + SECTION("process") { + REQUIRE(system_mode() == AUTOMATIC); + + SECTION("process() uninitialized / initialized") { + CHECK(ncpTest.getInitialized() == false); + CHECK(ncpTest.process() == SYSTEM_ERROR_INVALID_STATE); + + ncpTest.init(saraNcpFwUpdateCallbacks); + CHECK(ncpTest.getInitialized() == true); + CHECK(ncpTest.process() == SYSTEM_ERROR_NONE); + } + + SECTION("cooldown") { + ncpTest.init(saraNcpFwUpdateCallbacks); + CHECK(ncpTest.getCooldownTimer() == 0); + ncpTest.callCooldown(2); + CHECK(ncpTest.getCooldownTimeout() == 2); + CHECK(ncpTest.callInCooldown() == true); + ncpTest.callUpdateCooldown(); // cooldown--; + CHECK(ncpTest.callInCooldown() == true); + CHECK(ncpTest.process() == SYSTEM_ERROR_NONE); // cooldown--; + CHECK(ncpTest.getCooldownTimeout() == 0); + CHECK(ncpTest.getCooldownTimer() == 0); + CHECK(ncpTest.callInCooldown() == false); + } + + SECTION("setConfig upgrade to 31400011") { + ncpTest.init(saraNcpFwUpdateCallbacks); + + // setConfig - enable isUserConfig by sending proper config - update available + ncpMocks.setModemVersion(31400010); + REQUIRE(ncpTest.setConfig(&saraNcpFwUpdateConfigUpgrade) == SYSTEM_ERROR_NONE); + REQUIRE(ncpTest.updateStatus() == SYSTEM_NCP_FW_UPDATE_PENDING); + REQUIRE(ncpTest.getIsUserConfig() == true); + REQUIRE(ncpTest.getSaraNcpFwUpdateData().firmwareVersion == ncpTest.getFirmwareVersion()); + REQUIRE(ncpTest.getSaraNcpFwUpdateData().isUserConfig == true); + REQUIRE(ncpTest.getSaraNcpFwUpdateData().updateAvailable == SYSTEM_NCP_FW_UPDATE_PENDING); + + // checkUpdate / enableUpdates + ncpMocks.setNetworkIsOn(true); + ncpMocks.setNetworkIsOff(false); + REQUIRE(ncpTest.enableUpdates() == SYSTEM_ERROR_NONE); + REQUIRE(ncpTest.updateStatus() == SYSTEM_NCP_FW_UPDATE_IN_PROGRESS); + REQUIRE(ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_QUALIFY_FLAGS); + + //============================================ + // FW_UPDATE_STATE_QUALIFY_FLAGS + //============================================ + + // Force a bad updateAvailable_ for FW_UPDATE_STATE_QUALIFY_FLAGS + ncpTest.setUpdateAvailable(SYSTEM_NCP_FW_UPDATE_PENDING); + REQUIRE(ncpTest.process() == SYSTEM_ERROR_NONE); + REQUIRE(ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_FINISHED_IDLE); + REQUIRE(ncpTest.getSaraNcpFwUpdateStatus() == FW_UPDATE_STATUS_FAILED); + REQUIRE(ncpTest.getSaraNcpFwUpdateError() == SYSTEM_ERROR_SARA_NCP_FW_UPDATE_QUALIFY_FLAGS); + // Restore proper state + ncpTest.setUpdateAvailable(SYSTEM_NCP_FW_UPDATE_IN_PROGRESS); + ncpTest.setSaraNcpFwUpdateState(FW_UPDATE_STATE_QUALIFY_FLAGS); + ncpTest.setSaraNcpFwUpdateStatus(FW_UPDATE_STATUS_IDLE); + + mocks.ExpectCallFunc(system_reset).With(SYSTEM_RESET_MODE_SAFE, 0, 0, SYSTEM_RESET_FLAG_NO_WAIT, _).Return(0); + REQUIRE(ncpTest.process() == SYSTEM_ERROR_NONE); + REQUIRE(ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_SETUP_CLOUD_CONNECT); + REQUIRE(ncpTest.getSaraNcpFwUpdateStatus() == FW_UPDATE_STATUS_IDLE); + REQUIRE(ncpTest.getSaraNcpFwUpdateData().state == FW_UPDATE_STATE_SETUP_CLOUD_CONNECT); + REQUIRE(ncpTest.getSaraNcpFwUpdateData().firmwareVersion == ncpTest.getFirmwareVersion()); + REQUIRE(ncpTest.getSaraNcpFwUpdateData().updateAvailable == SYSTEM_NCP_FW_UPDATE_IN_PROGRESS); + ncpTest.reset(); // simulate reset of device + + //============================================ + // FW_UPDATE_STATE_SETUP_CLOUD_CONNECT + //============================================ + + ncpMocks.setSystemMode(SAFE_MODE); + REQUIRE(system_mode() == SAFE_MODE); + + REQUIRE(ncpTest.getInitialized() == false); + ncpTest.init(saraNcpFwUpdateCallbacks); + REQUIRE(ncpTest.getInitialized() == true); + + // Run state already connected + system_tick_t tempTimer = HAL_Timer_Get_Milli_Seconds(); + ncpMocks.setSparkCloudFlagConnected(true); + mocks.ExpectCallFunc(spark_cloud_flag_connected).Return(true); + REQUIRE(ncpTest.process() == SYSTEM_ERROR_NONE); + REQUIRE(ncpTest.getStartTimer() == tempTimer + 1); + // Restore proper state + ncpMocks.setSparkCloudFlagConnected(false); + mocks.ExpectCallFunc(spark_cloud_flag_connected).Return(false); + ncpTest.setSaraNcpFwUpdateState(FW_UPDATE_STATE_SETUP_CLOUD_CONNECT); + + REQUIRE(ncpMocks.getSparkCloudFlagConnect() == false); + REQUIRE(ncpTest.process() == SYSTEM_ERROR_NONE); + REQUIRE(ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_SETUP_CLOUD_CONNECTING); + REQUIRE(ncpTest.getSaraNcpFwUpdateStatus() == FW_UPDATE_STATUS_DOWNLOADING); + REQUIRE(ncpTest.getSaraNcpFwUpdateData().status == FW_UPDATE_STATUS_IDLE); + REQUIRE(ncpTest.getSaraNcpFwUpdateData().firmwareVersion == ncpTest.getFirmwareVersion()); + REQUIRE(ncpTest.getSaraNcpFwUpdateData().isUserConfig == true); + REQUIRE(ncpTest.getSaraNcpFwUpdateStatusDiagnostics() == FW_UPDATE_STATUS_DOWNLOADING); + REQUIRE(ncpTest.getSaraNcpFwUpdateErrorDiagnostics() == SYSTEM_ERROR_NONE); + REQUIRE(ncpMocks.getSparkCloudFlagConnect() == true); + REQUIRE(ncpTest.getStartTimer() == tempTimer + 2); + + //============================================ + // FW_UPDATE_STATE_SETUP_CLOUD_CONNECTING + //============================================ + + // Connect timeout case + while (ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_SETUP_CLOUD_CONNECTING) { + ncpTest.process(); + } + REQUIRE(HAL_Timer_Get_Milli_Seconds() >= tempTimer + NCP_FW_MODEM_CLOUD_CONNECT_TIMEOUT); + REQUIRE(ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_FINISHED_IDLE); + REQUIRE(ncpTest.getSaraNcpFwUpdateStatus() == FW_UPDATE_STATUS_FAILED); + REQUIRE(ncpTest.getSaraNcpFwUpdateError() == SYSTEM_ERROR_SARA_NCP_FW_UPDATE_CLOUD_CONNECT_ON_ENTRY_TIMEOUT); + + // Restore proper state + tempTimer = HAL_Timer_Get_Milli_Seconds(); + ncpTest.setSaraNcpFwUpdateState(FW_UPDATE_STATE_SETUP_CLOUD_CONNECT); // back to set startTimer again + ncpTest.setSaraNcpFwUpdateStatus(FW_UPDATE_STATUS_IDLE); + REQUIRE(ncpTest.process() == SYSTEM_ERROR_NONE); + // Connect successful case + ncpMocks.setSparkCloudFlagConnected(true); + while (ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_SETUP_CLOUD_CONNECTING) { + ncpTest.process(); + } + REQUIRE(HAL_Timer_Get_Milli_Seconds() < tempTimer + NCP_FW_MODEM_CLOUD_CONNECT_TIMEOUT); + REQUIRE(ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_SETUP_CLOUD_CONNECTED); + REQUIRE(ncpTest.getSaraNcpFwUpdateStatus() == FW_UPDATE_STATUS_DOWNLOADING); + + //============================================ + // FW_UPDATE_STATE_SETUP_CLOUD_CONNECTED + //============================================ + + // Failed to publish case + mocks.ExpectCallFunc(publishEvent).Return(false); + REQUIRE(ncpTest.process() == SYSTEM_ERROR_NONE); + REQUIRE(ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_FINISHED_IDLE); + REQUIRE(ncpTest.getSaraNcpFwUpdateStatus() == FW_UPDATE_STATUS_FAILED); + REQUIRE(ncpTest.getSaraNcpFwUpdateError() == SYSTEM_ERROR_SARA_NCP_FW_UPDATE_PUBLISH_START); + + // Restore proper state + ncpTest.setSaraNcpFwUpdateState(FW_UPDATE_STATE_SETUP_CLOUD_CONNECTED); // back to set startTimer again + ncpTest.setSaraNcpFwUpdateStatus(FW_UPDATE_STATUS_DOWNLOADING); + // Connected false case + ncpMocks.setSparkCloudFlagConnected(false); + REQUIRE(ncpTest.process() == SYSTEM_ERROR_NONE); + REQUIRE(ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_FINISHED_IDLE); + REQUIRE(ncpTest.getSaraNcpFwUpdateStatus() == FW_UPDATE_STATUS_FAILED); + REQUIRE(ncpTest.getSaraNcpFwUpdateError() == SYSTEM_ERROR_SARA_NCP_FW_UPDATE_PUBLISH_START); + + // Restore proper state + ncpMocks.setSparkCloudFlagConnected(true); + ncpTest.setSaraNcpFwUpdateState(FW_UPDATE_STATE_SETUP_CLOUD_CONNECTED); // back to set startTimer again + ncpTest.setSaraNcpFwUpdateStatus(FW_UPDATE_STATUS_DOWNLOADING); + // Publish successful case + mocks.ExpectCallFunc(publishEvent).Return(true); + REQUIRE(ncpTest.process() == SYSTEM_ERROR_NONE); + REQUIRE(ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_DOWNLOAD_CLOUD_DISCONNECT); + REQUIRE(ncpTest.getSaraNcpFwUpdateStatus() == FW_UPDATE_STATUS_DOWNLOADING); + + //============================================ + // FW_UPDATE_STATE_DOWNLOAD_CLOUD_DISCONNECT + //============================================ + + // Connected successful case + REQUIRE(ncpMocks.getSparkCloudFlagConnect() == true); + REQUIRE(ncpTest.process() == SYSTEM_ERROR_NONE); + REQUIRE(ncpMocks.getSparkCloudFlagConnect() == false); + ncpMocks.setSparkCloudFlagConnected(false); + + // Network not ready case + mocks.ExpectCallFunc(network_ready).Return(false); + REQUIRE(ncpTest.process() == SYSTEM_ERROR_NONE); + REQUIRE(ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_DOWNLOAD_CLOUD_DISCONNECT); + REQUIRE(ncpTest.getSaraNcpFwUpdateStatus() == FW_UPDATE_STATUS_DOWNLOADING); + + // Network ready case + mocks.ExpectCallFunc(network_ready).Return(true); + tempTimer = HAL_Timer_Get_Milli_Seconds(); + REQUIRE(ncpTest.process() == SYSTEM_ERROR_NONE); + REQUIRE(ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_DOWNLOAD_CELL_DISCONNECTING); + REQUIRE(ncpTest.getSaraNcpFwUpdateStatus() == FW_UPDATE_STATUS_DOWNLOADING); + REQUIRE(ncpTest.getStartTimer() == tempTimer + 1); + REQUIRE(ncpTest.getCgevDeactProfile() == 0); + // TODO: Add test for cellular_add_urc_handler? + + //============================================ + // FW_UPDATE_STATE_DOWNLOAD_CELL_DISCONNECTING + //============================================ + + // Disonnect timeout case + while (ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_DOWNLOAD_CELL_DISCONNECTING) { + ncpTest.process(); + } + REQUIRE(HAL_Timer_Get_Milli_Seconds() >= tempTimer + NCP_FW_MODEM_CLOUD_DISCONNECT_TIMEOUT); + REQUIRE(ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_DOWNLOAD_CELL_CONNECTING); + + // Restore proper state - back one state set startTimer again + mocks.ExpectCallFunc(network_ready).Return(true); + tempTimer = HAL_Timer_Get_Milli_Seconds(); + ncpTest.setSaraNcpFwUpdateState(FW_UPDATE_STATE_DOWNLOAD_CLOUD_DISCONNECT); + ncpTest.setSaraNcpFwUpdateStatus(FW_UPDATE_STATUS_DOWNLOADING); + REQUIRE(ncpTest.process() == SYSTEM_ERROR_NONE); + // Connect successful case + ncpTest.setCgevDeactProfile(NCP_FW_UBLOX_DEFAULT_CID); + while (ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_DOWNLOAD_CELL_DISCONNECTING) { + ncpTest.process(); + } + REQUIRE(HAL_Timer_Get_Milli_Seconds() < tempTimer + NCP_FW_MODEM_CLOUD_DISCONNECT_TIMEOUT); + REQUIRE(ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_DOWNLOAD_CELL_CONNECTING); + REQUIRE(ncpTest.getSaraNcpFwUpdateStatus() == FW_UPDATE_STATUS_DOWNLOADING); + + //============================================ + // FW_UPDATE_STATE_DOWNLOAD_CELL_CONNECTING + //============================================ + + // Cellular remains connected + mocks.ExpectCallFunc(network_ready).Return(true); + while (ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_DOWNLOAD_CELL_CONNECTING) { + ncpTest.process(); + } + REQUIRE(HAL_Timer_Get_Milli_Seconds() >= tempTimer + 1000); // cooldown from previous state + REQUIRE(ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_FINISHED_IDLE); + REQUIRE(ncpTest.getSaraNcpFwUpdateStatus() == FW_UPDATE_STATUS_FAILED); + REQUIRE(ncpTest.getSaraNcpFwUpdateError() == SYSTEM_ERROR_SARA_NCP_FW_UPDATE_SETUP_CELLULAR_STILL_CONNECTED); + + // Restore proper state - back one state set startTimer again + ncpTest.setSaraNcpFwUpdateState(FW_UPDATE_STATE_DOWNLOAD_CELL_CONNECTING); + ncpTest.setSaraNcpFwUpdateStatus(FW_UPDATE_STATUS_DOWNLOADING); + // Cellular disconnected successful case + mocks.ExpectCallFunc(network_ready).Return(false); + mocks.ExpectCallFunc(cellular_start_ncp_firmware_update).Return(SYSTEM_ERROR_NONE); + mocks.ExpectCallFunc(network_connect); + REQUIRE(ncpTest.process() == SYSTEM_ERROR_NONE); + ncpTest.setSaraNcpFwUpdateState(FW_UPDATE_STATE_DOWNLOAD_HTTPS_SETUP); + ncpTest.setSaraNcpFwUpdateStatus(FW_UPDATE_STATUS_DOWNLOADING); + + //============================================ + // FW_UPDATE_STATE_DOWNLOAD_HTTPS_SETUP + //============================================ + + // HTTPS Setup fails invalid state + mocks.ExpectCallFunc(setupHTTPSProperties_impl).Return(SYSTEM_ERROR_INVALID_STATE); + REQUIRE(ncpTest.process() == SYSTEM_ERROR_NONE); + REQUIRE(ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_FINISHED_IDLE); + REQUIRE(ncpTest.getSaraNcpFwUpdateStatus() == FW_UPDATE_STATUS_FAILED); + REQUIRE(ncpTest.getSaraNcpFwUpdateError() == SYSTEM_ERROR_SARA_NCP_FW_UPDATE_SETUP_CELLULAR_CONNECT_TIMEOUT); + + // Restore proper state + ncpTest.setSaraNcpFwUpdateState(FW_UPDATE_STATE_DOWNLOAD_HTTPS_SETUP); + ncpTest.setSaraNcpFwUpdateStatus(FW_UPDATE_STATUS_DOWNLOADING); + // HTTPS Setup fails https setup error + mocks.ExpectCallFunc(setupHTTPSProperties_impl).Return(SYSTEM_ERROR_SARA_NCP_FW_UPDATE_HTTPS_SETUP_1); // TODO: Convert to specific NCP_FW_ERROR code from system_error.h + REQUIRE(ncpTest.process() == SYSTEM_ERROR_NONE); + REQUIRE(ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_FINISHED_IDLE); + REQUIRE(ncpTest.getSaraNcpFwUpdateStatus() == FW_UPDATE_STATUS_FAILED); + REQUIRE(ncpTest.getSaraNcpFwUpdateError() == SYSTEM_ERROR_SARA_NCP_FW_UPDATE_HTTPS_SETUP_1); + + // Restore proper state + ncpTest.setSaraNcpFwUpdateState(FW_UPDATE_STATE_DOWNLOAD_HTTPS_SETUP); + ncpTest.setSaraNcpFwUpdateStatus(FW_UPDATE_STATUS_DOWNLOADING); + // HTTPS Setup succeeds + mocks.ExpectCallFunc(setupHTTPSProperties_impl).Return(SYSTEM_ERROR_NONE); + REQUIRE(ncpTest.process() == SYSTEM_ERROR_NONE); + REQUIRE(ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_DOWNLOAD_READY); + REQUIRE(ncpTest.getSaraNcpFwUpdateStatus() == FW_UPDATE_STATUS_DOWNLOADING); + + //============================================ + // FW_UPDATE_STATE_DOWNLOAD_READY + //============================================ + + // UHTTPC RESP_ERROR, download timerout & download retries + Call &command1 = mocks.OnCallFunc(sendCommandWithArgs) + .Do([&](_CALLBACKPTR_MDM cb, void* param, system_tick_t timeout_ms, const char* format, va_list args) -> int { + if (!strcmp(format, "AT+ULSTFILE=0,\"FOAT\"\r\n")) { + // printf("ULSTFILE\r\n"); + *((int*)param) = 1; // file present + } + return RESP_OK; + }); + Call &command2 = mocks.ExpectCallFunc(sendCommandWithArgs) + .Do([&](_CALLBACKPTR_MDM cb, void* param, system_tick_t timeout_ms, const char* format, va_list args) -> int { + if (!strcmp(format, "AT+UDELFILE=\"updatePackage.bin\",\"FOAT\"\r\n")) { + // printf("UDELFILE\r\n"); + } + return RESP_OK; + }).After(command1); + tempTimer = HAL_Timer_Get_Milli_Seconds(); + REQUIRE(ncpTest.process() == SYSTEM_ERROR_NONE); + REQUIRE(ncpTest.getDownloadRetries() == 1); + REQUIRE(ncpTest.process() == SYSTEM_ERROR_NONE); + REQUIRE(ncpTest.getDownloadRetries() == 2); + REQUIRE(ncpTest.process() == SYSTEM_ERROR_NONE); + REQUIRE(ncpTest.getDownloadRetries() == 3); + REQUIRE(ncpTest.getUpdateVersion() == saraNcpFwUpdateConfigUpgrade.end_version); + REQUIRE(ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_FINISHED_IDLE); + REQUIRE(ncpTest.getSaraNcpFwUpdateStatus() == FW_UPDATE_STATUS_FAILED); + REQUIRE(ncpTest.getSaraNcpFwUpdateError() == SYSTEM_ERROR_SARA_NCP_FW_UPDATE_DOWNLOAD_RETRY_MAX); + REQUIRE(HAL_Timer_Get_Milli_Seconds() >= tempTimer + NCP_FW_MODEM_DOWNLOAD_TIMEOUT * 3); + + // Restore proper state + ncpTest.setSaraNcpFwUpdateState(FW_UPDATE_STATE_DOWNLOAD_READY); + ncpTest.setSaraNcpFwUpdateStatus(FW_UPDATE_STATUS_DOWNLOADING); + // UHTTPC error + mocks.OnCallFunc(sendCommandWithArgs) + .Do([&](_CALLBACKPTR_MDM cb, void* param, system_tick_t timeout_ms, const char* format, va_list args) -> int { + if (!strcmp(format, "AT+UHTTPC=0,100,\"/%s\"\r\n")) { + // printf("UHTTPC\r\n"); + HTTPSresponse h = {}; + h.valid = true; + h.command = 100; + h.result = 0; + h.status_code = 0; + memcpy(&h.md5_sum, &saraNcpFwUpdateConfigUpgrade.md5sum, sizeof(saraNcpFwUpdateConfigUpgrade.md5sum)); + ncpTest.setHttpsResp(h); + } + return RESP_OK; + }); + REQUIRE(ncpTest.process() == SYSTEM_ERROR_NONE); + REQUIRE(ncpTest.getDownloadRetries() >= 3); + REQUIRE(ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_FINISHED_IDLE); + REQUIRE(ncpTest.getSaraNcpFwUpdateStatus() == FW_UPDATE_STATUS_FAILED); + REQUIRE(ncpTest.getSaraNcpFwUpdateError() == SYSTEM_ERROR_SARA_NCP_FW_UPDATE_DOWNLOAD_RETRY_MAX); + + // Restore proper state + ncpTest.setSaraNcpFwUpdateState(FW_UPDATE_STATE_DOWNLOAD_READY); + ncpTest.setSaraNcpFwUpdateStatus(FW_UPDATE_STATUS_DOWNLOADING); + // UHTTPC success + mocks.OnCallFunc(sendCommandWithArgs) + .Do([&](_CALLBACKPTR_MDM cb, void* param, system_tick_t timeout_ms, const char* format, va_list args) -> int { + if (!strcmp(format, "AT+UHTTPC=0,100,\"/%s\"\r\n")) { + // printf("UHTTPC\r\n"); + HTTPSresponse h = {}; + h.valid = true; + h.command = 100; + h.result = 1; + h.status_code = 200; + memcpy(&h.md5_sum, &saraNcpFwUpdateConfigUpgrade.md5sum, sizeof(saraNcpFwUpdateConfigUpgrade.md5sum)); + ncpTest.setHttpsResp(h); + } + return RESP_OK; + }); + REQUIRE(ncpTest.process() == SYSTEM_ERROR_NONE); + REQUIRE(ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_INSTALL_CELL_DISCONNECTING); + REQUIRE(ncpTest.getSaraNcpFwUpdateStatus() == FW_UPDATE_STATUS_DOWNLOADING); + REQUIRE(ncpTest.getCgevDeactProfile() == 0); + tempTimer = HAL_Timer_Get_Milli_Seconds(); + + //============================================ + // FW_UPDATE_STATE_INSTALL_CELL_DISCONNECTING + //============================================ + + // Disonnect timeout case + while (ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_INSTALL_CELL_DISCONNECTING) { + ncpTest.process(); + } + REQUIRE(HAL_Timer_Get_Milli_Seconds() >= tempTimer + NCP_FW_MODEM_CLOUD_DISCONNECT_TIMEOUT); + REQUIRE(ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_INSTALL_STARTING); + + // Restore proper state - back one state set startTimer again + tempTimer = HAL_Timer_Get_Milli_Seconds(); + ncpTest.setSaraNcpFwUpdateState(FW_UPDATE_STATE_DOWNLOAD_READY); + ncpTest.setSaraNcpFwUpdateStatus(FW_UPDATE_STATUS_DOWNLOADING); + REQUIRE(ncpTest.process() == SYSTEM_ERROR_NONE); + // Disconnect successful case + ncpTest.setCgevDeactProfile(NCP_FW_UBLOX_DEFAULT_CID); + while (ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_INSTALL_CELL_DISCONNECTING) { + ncpTest.process(); + } + REQUIRE(HAL_Timer_Get_Milli_Seconds() < tempTimer + NCP_FW_MODEM_CLOUD_DISCONNECT_TIMEOUT); + REQUIRE(ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_INSTALL_STARTING); + REQUIRE(ncpTest.getSaraNcpFwUpdateStatus() == FW_UPDATE_STATUS_DOWNLOADING); + + //============================================ + // FW_UPDATE_STATE_INSTALL_STARTING + //============================================ + + // UFWINSTALL error + mocks.OnCallFunc(sendCommandWithArgs) + .Do([&](_CALLBACKPTR_MDM cb, void* param, system_tick_t timeout_ms, const char* format, va_list args) -> int { + int ret = RESP_OK; + if (!strcmp(format, "AT+UFWINSTALL=1,115200\r\n")) { + // printf("UFWINSTALL 1\r\n"); + ret = RESP_ERROR; + } + return ret; + }); + // Wait for cooldown from previous state + while (ncpTest.getSaraNcpFwUpdateStatus() == FW_UPDATE_STATUS_DOWNLOADING) { + ncpTest.process(); + } + REQUIRE(HAL_Timer_Get_Milli_Seconds() >= tempTimer + 1000); // cooldown from previous state + REQUIRE(ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_FINISHED_POWER_OFF); + REQUIRE(ncpTest.getSaraNcpFwUpdateStatus() == FW_UPDATE_STATUS_FAILED); + REQUIRE(ncpTest.getSaraNcpFwUpdateError() == SYSTEM_ERROR_SARA_NCP_FW_UPDATE_INSTALL_AT_ERROR); + + // Restore proper state + ncpTest.setSaraNcpFwUpdateState(FW_UPDATE_STATE_INSTALL_STARTING); + ncpTest.setSaraNcpFwUpdateStatus(FW_UPDATE_STATUS_DOWNLOADING); + ncpTest.setSaraNcpFwUpdateError(SYSTEM_ERROR_NONE); + // UFWINSTALL failure - 5 minute timeout waiting for INSTALL to start + mocks.OnCallFunc(sendCommandWithArgs) + .Do([&](_CALLBACKPTR_MDM cb, void* param, system_tick_t timeout_ms, const char* format, va_list args) -> int { + int ret = RESP_OK; + if (!strcmp(format, "AT+UFWINSTALL=1,115200\r\n")) { + // printf("UFWINSTALL 2\r\n"); + } + return ret; + }); + // Wait for 10s cooldown + while (ncpTest.getSaraNcpFwUpdateStatus() == FW_UPDATE_STATUS_DOWNLOADING) { + tempTimer = HAL_Timer_Get_Milli_Seconds(); + ncpTest.process(); + } + // REQUIRE(ncpTest.process() == SYSTEM_ERROR_NONE); + REQUIRE(HAL_Timer_Get_Milli_Seconds() >= tempTimer + NCP_FW_MODEM_INSTALL_START_TIMEOUT); + REQUIRE(ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_FINISHED_POWER_OFF); + REQUIRE(ncpTest.getSaraNcpFwUpdateStatus() == FW_UPDATE_STATUS_FAILED); + REQUIRE(ncpTest.getSaraNcpFwUpdateError() == SYSTEM_ERROR_SARA_NCP_FW_UPDATE_START_INSTALL_TIMEOUT); + REQUIRE(ncpTest.getSaraNcpFwUpdateData().status == FW_UPDATE_STATUS_UPDATING); + REQUIRE(ncpTest.getSaraNcpFwUpdateData().error == SYSTEM_ERROR_NONE); + + // Restore proper state + ncpTest.setSaraNcpFwUpdateState(FW_UPDATE_STATE_INSTALL_STARTING); + ncpTest.setSaraNcpFwUpdateStatus(FW_UPDATE_STATUS_DOWNLOADING); + ncpTest.setSaraNcpFwUpdateError(SYSTEM_ERROR_NONE); + // UFWINSTALL success + int responsiveATcount = 10; + mocks.ExpectCallFunc(cellular_start_ncp_firmware_update).With(true, _).Return(SYSTEM_ERROR_NONE); + mocks.OnCallFunc(sendCommandWithArgs) + .Do([&](_CALLBACKPTR_MDM cb, void* param, system_tick_t timeout_ms, const char* format, va_list args) -> int { + int ret = RESP_OK; + if (!strcmp(format, "AT+UFWINSTALL=1,115200\r\n")) { + // printf("UFWINSTALL 3\r\n"); + } + if (!strcmp(format, "AT\r\n")) { + if (responsiveATcount-- > 0) { + // printf("AT OK 1\r\n"); + ret = RESP_OK; + } else { + // printf("AT TIMEOUT 1\r\n"); + ret = WAIT; // timeout + } + } + return ret; + }); + // Wait for 10s cooldown + while (ncpTest.getSaraNcpFwUpdateStatus() == FW_UPDATE_STATUS_DOWNLOADING) { + tempTimer = HAL_Timer_Get_Milli_Seconds(); + ncpTest.process(); + } + REQUIRE(HAL_Timer_Get_Milli_Seconds() < tempTimer + NCP_FW_MODEM_INSTALL_START_TIMEOUT); + REQUIRE(ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_INSTALL_WAITING); + REQUIRE(ncpTest.getSaraNcpFwUpdateStatus() == FW_UPDATE_STATUS_UPDATING); + REQUIRE(ncpTest.getSaraNcpFwUpdateData().status == FW_UPDATE_STATUS_UPDATING); + REQUIRE(ncpTest.getSaraNcpFwUpdateData().error == SYSTEM_ERROR_NONE); + + //============================================ + // FW_UPDATE_STATE_INSTALL_WAITING + //============================================ + + // UFWINSTALL timeout & AT polling interval + system_tick_t atTimer; + mocks.ExpectCallFunc(cellular_start_ncp_firmware_update).With(true, _).Return(SYSTEM_ERROR_NONE); + mocks.OnCallFunc(sendCommandWithArgs) + .Do([&](_CALLBACKPTR_MDM cb, void* param, system_tick_t timeout_ms, const char* format, va_list args) -> int { + int ret = RESP_OK; + if (!strcmp(format, "AT\r\n")) { + // printf("AT\r\n"); + if (atTimer < tempTimer + (10 * NCP_FW_MODEM_INSTALL_ATOK_INTERVAL)) { + // CHECK 10 times + REQUIRE(HAL_Timer_Get_Milli_Seconds() >= atTimer + NCP_FW_MODEM_INSTALL_ATOK_INTERVAL); + } + atTimer = HAL_Timer_Get_Milli_Seconds(); + ret = WAIT; // timeout + } + return ret; + }); + // wait for previous state 10s cooldown + while (ncpTest.getSaraNcpFwUpdateStatus() == FW_UPDATE_STATUS_UPDATING) { + tempTimer = HAL_Timer_Get_Milli_Seconds(); // start of startTimer_ + atTimer = HAL_Timer_Get_Milli_Seconds(); // start of atOkCheckTimer_ + ncpTest.process(); + } + REQUIRE(ncpTest.getStartingFirmwareVersion() == saraNcpFwUpdateConfigUpgrade.start_version); + REQUIRE(ncpTest.getSaraNcpFwUpdateData().startingFirmwareVersion == saraNcpFwUpdateConfigUpgrade.start_version); + REQUIRE(ncpTest.getSaraNcpFwUpdateData().state == FW_UPDATE_STATE_INSTALL_WAITING); + REQUIRE(ncpTest.getSaraNcpFwUpdateData().status == FW_UPDATE_STATUS_UPDATING); + REQUIRE(ncpTest.getSaraNcpFwUpdateData().error == SYSTEM_ERROR_NONE); + REQUIRE(ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_FINISHED_POWER_OFF); + REQUIRE(ncpTest.getSaraNcpFwUpdateStatus() == FW_UPDATE_STATUS_FAILED); + REQUIRE(ncpTest.getSaraNcpFwUpdateError() == SYSTEM_ERROR_SARA_NCP_FW_UPDATE_INSTALL_TIMEOUT); + REQUIRE(HAL_Timer_Get_Milli_Seconds() >= tempTimer + NCP_FW_MODEM_INSTALL_FINISH_TIMEOUT); + + // Restore proper state + ncpTest.setSaraNcpFwUpdateState(FW_UPDATE_STATE_INSTALL_WAITING); + ncpTest.setSaraNcpFwUpdateStatus(FW_UPDATE_STATUS_UPDATING); + ncpTest.setSaraNcpFwUpdateError(SYSTEM_ERROR_NONE); + // UFWINSTALL unresponsive, then responsive after some time with the same firmware version as started with + const int unresponsiveATcountMax = 120; // represents ~20 minutes + int unresponsiveATcount = unresponsiveATcountMax; + tempTimer = HAL_Timer_Get_Milli_Seconds(); // start of startTimer_ + atTimer = HAL_Timer_Get_Milli_Seconds(); // start of atOkCheckTimer_ + mocks.ExpectCallFunc(cellular_start_ncp_firmware_update).With(true, _).Return(SYSTEM_ERROR_NONE); + mocks.OnCallFunc(sendCommandWithArgs) + .Do([&](_CALLBACKPTR_MDM cb, void* param, system_tick_t timeout_ms, const char* format, va_list args) -> int { + int ret = RESP_OK; + if (!strcmp(format, "AT\r\n")) { + if (unresponsiveATcount-- > 0) { + // printf("AT TIMEOUT 2\r\n"); + ret = WAIT; // timeout + } else { + // printf("AT OK 2\r\n"); + // DO NOT UPGRADE TO LATEST VERSION + // ncpMocks.setModemVersion(saraNcpFwUpdateConfigUpgrade.end_version); + ret = RESP_OK; + } + if (unresponsiveATcount > (unresponsiveATcountMax - 10)) { + // CHECK 10 times + REQUIRE(HAL_Timer_Get_Milli_Seconds() >= atTimer + NCP_FW_MODEM_INSTALL_ATOK_INTERVAL); + } + atTimer = HAL_Timer_Get_Milli_Seconds(); + } + return ret; + }); + REQUIRE(ncpTest.process() == SYSTEM_ERROR_NONE); + REQUIRE(ncpTest.getStartingFirmwareVersion() == saraNcpFwUpdateConfigUpgrade.start_version); + REQUIRE(ncpTest.getSaraNcpFwUpdateData().startingFirmwareVersion == saraNcpFwUpdateConfigUpgrade.start_version); + REQUIRE(ncpTest.getSaraNcpFwUpdateData().state == FW_UPDATE_STATE_INSTALL_WAITING); + REQUIRE(ncpTest.getSaraNcpFwUpdateData().status == FW_UPDATE_STATUS_UPDATING); + REQUIRE(ncpTest.getSaraNcpFwUpdateData().error == SYSTEM_ERROR_NONE); + REQUIRE(ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_FINISHED_POWER_OFF); + REQUIRE(ncpTest.getSaraNcpFwUpdateStatus() == FW_UPDATE_STATUS_FAILED); + REQUIRE(ncpTest.getSaraNcpFwUpdateError() == SYSTEM_ERROR_SARA_NCP_FW_UPDATE_SAME_VERSION); + REQUIRE(HAL_Timer_Get_Milli_Seconds() < tempTimer + NCP_FW_MODEM_INSTALL_FINISH_TIMEOUT); + + // Restore proper state + ncpTest.setSaraNcpFwUpdateState(FW_UPDATE_STATE_INSTALL_WAITING); + ncpTest.setSaraNcpFwUpdateStatus(FW_UPDATE_STATUS_UPDATING); + ncpTest.setSaraNcpFwUpdateError(SYSTEM_ERROR_NONE); + // UFWINSTALL unresponsive, then responsive after some time with desired updated firmware version + unresponsiveATcount = unresponsiveATcountMax; + tempTimer = HAL_Timer_Get_Milli_Seconds(); // start of startTimer_ + atTimer = HAL_Timer_Get_Milli_Seconds(); // start of atOkCheckTimer_ + mocks.ExpectCallFunc(cellular_start_ncp_firmware_update).With(true, _).Return(SYSTEM_ERROR_NONE); + mocks.OnCallFunc(sendCommandWithArgs) + .Do([&](_CALLBACKPTR_MDM cb, void* param, system_tick_t timeout_ms, const char* format, va_list args) -> int { + int ret = RESP_OK; + if (!strcmp(format, "AT\r\n")) { + if (unresponsiveATcount-- > 0) { + // printf("AT TIMEOUT 2\r\n"); + ret = WAIT; // timeout + } else { + // printf("AT OK 2\r\n"); + ncpMocks.setModemVersion(saraNcpFwUpdateConfigUpgrade.end_version); + ret = RESP_OK; + } + if (unresponsiveATcount > (unresponsiveATcountMax - 10)) { + // CHECK 10 times + REQUIRE(HAL_Timer_Get_Milli_Seconds() >= atTimer + NCP_FW_MODEM_INSTALL_ATOK_INTERVAL); + } + atTimer = HAL_Timer_Get_Milli_Seconds(); + } + return ret; + }); + REQUIRE(ncpTest.process() == SYSTEM_ERROR_NONE); + REQUIRE(ncpTest.getStartingFirmwareVersion() == saraNcpFwUpdateConfigUpgrade.start_version); + REQUIRE(ncpTest.getSaraNcpFwUpdateData().startingFirmwareVersion == saraNcpFwUpdateConfigUpgrade.start_version); + REQUIRE(ncpTest.getFirmwareVersion() == saraNcpFwUpdateConfigUpgrade.end_version); + REQUIRE(ncpTest.getSaraNcpFwUpdateData().state == FW_UPDATE_STATE_INSTALL_WAITING); + REQUIRE(ncpTest.getSaraNcpFwUpdateData().status == FW_UPDATE_STATUS_UPDATING); + REQUIRE(ncpTest.getSaraNcpFwUpdateData().error == SYSTEM_ERROR_NONE); + REQUIRE(ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_FINISHED_POWER_OFF); + REQUIRE(ncpTest.getSaraNcpFwUpdateStatus() == FW_UPDATE_STATUS_SUCCESS); + REQUIRE(HAL_Timer_Get_Milli_Seconds() < tempTimer + NCP_FW_MODEM_INSTALL_FINISH_TIMEOUT); + + //============================================ + // FW_UPDATE_STATE_FINISHED_POWER_OFF + //============================================ + + mocks.ExpectCallFunc(cellular_start_ncp_firmware_update).With(false, _).Return(SYSTEM_ERROR_NONE); + mocks.ExpectCallFunc(network_off).With(NETWORK_INTERFACE_CELLULAR, 0, 0, _); + REQUIRE(ncpTest.process() == SYSTEM_ERROR_NONE); + REQUIRE(ncpTest.getSaraNcpFwUpdateData().state == FW_UPDATE_STATE_FINISHED_POWER_OFF); + REQUIRE(ncpTest.getSaraNcpFwUpdateData().status == FW_UPDATE_STATUS_SUCCESS); + REQUIRE(ncpTest.getSaraNcpFwUpdateData().error == SYSTEM_ERROR_NONE); + REQUIRE(ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_FINISHED_POWERING_OFF); + tempTimer = HAL_Timer_Get_Milli_Seconds(); + + //============================================ + // FW_UPDATE_STATE_FINISHED_POWERING_OFF + //============================================ + + // Power off timeout case + while (ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_FINISHED_POWERING_OFF) { + ncpTest.process(); + } + REQUIRE(HAL_Timer_Get_Milli_Seconds() >= tempTimer + NCP_FW_MODEM_POWER_OFF_TIMEOUT); + REQUIRE(ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_FINISHED_IDLE); + REQUIRE(ncpTest.getSaraNcpFwUpdateStatus() == FW_UPDATE_STATUS_FAILED); + REQUIRE(ncpTest.getSaraNcpFwUpdateError() == SYSTEM_ERROR_SARA_NCP_FW_UPDATE_POWER_OFF_TIMEOUT); + + // Restore proper state - back one state set startTimer again + ncpTest.setSaraNcpFwUpdateState(FW_UPDATE_STATE_FINISHED_POWER_OFF); + ncpTest.setSaraNcpFwUpdateStatus(FW_UPDATE_STATUS_SUCCESS); + ncpTest.setSaraNcpFwUpdateError(SYSTEM_ERROR_NONE); + mocks.ExpectCallFunc(cellular_start_ncp_firmware_update).With(false, _).Return(SYSTEM_ERROR_NONE); + mocks.ExpectCallFunc(network_off).With(NETWORK_INTERFACE_CELLULAR, 0, 0, _); + REQUIRE(ncpTest.process() == SYSTEM_ERROR_NONE); + REQUIRE(ncpTest.getSaraNcpFwUpdateData().state == FW_UPDATE_STATE_FINISHED_POWER_OFF); + REQUIRE(ncpTest.getSaraNcpFwUpdateData().status == FW_UPDATE_STATUS_SUCCESS); + REQUIRE(ncpTest.getSaraNcpFwUpdateData().error == SYSTEM_ERROR_NONE); + REQUIRE(ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_FINISHED_POWERING_OFF); + tempTimer = HAL_Timer_Get_Milli_Seconds(); + // Power off success case + ncpMocks.setNetworkIsOff(true); + mocks.ExpectCallFunc(network_is_off).With(NETWORK_INTERFACE_CELLULAR, _).Return(true); + REQUIRE(ncpTest.process() == SYSTEM_ERROR_NONE); + REQUIRE(ncpTest.getSaraNcpFwUpdateData().state == FW_UPDATE_STATE_FINISHED_CLOUD_CONNECTING); + REQUIRE(ncpTest.getSaraNcpFwUpdateData().status == FW_UPDATE_STATUS_SUCCESS); + REQUIRE(ncpTest.getSaraNcpFwUpdateData().error == SYSTEM_ERROR_NONE); + REQUIRE(ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_FINISHED_CLOUD_CONNECTING); + REQUIRE(HAL_Timer_Get_Milli_Seconds() < tempTimer + NCP_FW_MODEM_POWER_OFF_TIMEOUT); + tempTimer = HAL_Timer_Get_Milli_Seconds(); + + //============================================ + // FW_UPDATE_STATE_FINISHED_CLOUD_CONNECTING + //============================================ + + // Particle connect case + ncpMocks.setSparkCloudFlagConnected(false); + mocks.ExpectCallFunc(spark_cloud_flag_connect); + REQUIRE(ncpTest.process() == SYSTEM_ERROR_NONE); + REQUIRE(ncpTest.getSaraNcpFwUpdateStatusDiagnostics() == FW_UPDATE_STATUS_SUCCESS); + REQUIRE(ncpTest.getSaraNcpFwUpdateErrorDiagnostics() == SYSTEM_ERROR_NONE); + + // Cloud connected succesfully case + ncpMocks.setSparkCloudFlagConnected(true); + // wait for previous case 1sec cooldown + while (ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_FINISHED_CLOUD_CONNECTING) { + ncpTest.process(); + } + REQUIRE(ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_FINISHED_CLOUD_CONNECTED); + + // Restore proper state - back one states to set startTimer again + ncpTest.setSaraNcpFwUpdateState(FW_UPDATE_STATE_FINISHED_POWERING_OFF); + ncpTest.setSaraNcpFwUpdateStatus(FW_UPDATE_STATUS_SUCCESS); + ncpTest.setSaraNcpFwUpdateError(SYSTEM_ERROR_NONE); + ncpMocks.setNetworkIsOff(true); + mocks.ExpectCallFunc(network_is_off).With(NETWORK_INTERFACE_CELLULAR, _).Return(true); + REQUIRE(ncpTest.process() == SYSTEM_ERROR_NONE); + REQUIRE(ncpTest.getSaraNcpFwUpdateData().state == FW_UPDATE_STATE_FINISHED_CLOUD_CONNECTING); + REQUIRE(ncpTest.getSaraNcpFwUpdateData().status == FW_UPDATE_STATUS_SUCCESS); + REQUIRE(ncpTest.getSaraNcpFwUpdateData().error == SYSTEM_ERROR_NONE); + REQUIRE(ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_FINISHED_CLOUD_CONNECTING); + REQUIRE(HAL_Timer_Get_Milli_Seconds() < tempTimer + NCP_FW_MODEM_POWER_OFF_TIMEOUT); + tempTimer = HAL_Timer_Get_Milli_Seconds(); + // Try to connect - but timeout + ncpMocks.setSparkCloudFlagConnected(false); + mocks.ExpectCallFunc(spark_cloud_flag_connect); + REQUIRE(ncpTest.process() == SYSTEM_ERROR_NONE); + REQUIRE(ncpTest.getSaraNcpFwUpdateStatusDiagnostics() == FW_UPDATE_STATUS_SUCCESS); + REQUIRE(ncpTest.getSaraNcpFwUpdateErrorDiagnostics() == SYSTEM_ERROR_NONE); + // Process multiple cooldowns waiting for timeout + while (ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_FINISHED_CLOUD_CONNECTING) { + ncpTest.process(); + } + REQUIRE(HAL_Timer_Get_Milli_Seconds() > tempTimer + NCP_FW_MODEM_CLOUD_CONNECT_TIMEOUT); + REQUIRE(ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_FINISHED_POWER_OFF); + REQUIRE(ncpTest.getFinishedCloudConnectingRetries() == 1); + // POWER_OFF -> POWERING_OFF + mocks.ExpectCallFunc(cellular_start_ncp_firmware_update).With(false, _).Return(SYSTEM_ERROR_NONE); + mocks.ExpectCallFunc(network_off).With(NETWORK_INTERFACE_CELLULAR, 0, 0, _); + REQUIRE(ncpTest.process() == SYSTEM_ERROR_NONE); + // POWERING_OFF -> CLOUD_CONNECTING + mocks.ExpectCallFunc(network_is_off).With(NETWORK_INTERFACE_CELLULAR, _).Return(true); + REQUIRE(ncpTest.process() == SYSTEM_ERROR_NONE); + // Process multiple cooldowns waiting for timeout + while (ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_FINISHED_CLOUD_CONNECTING) { + ncpTest.process(); + } + REQUIRE(ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_FINISHED_IDLE); + REQUIRE(ncpTest.getSaraNcpFwUpdateStatus() == FW_UPDATE_STATUS_FAILED); + REQUIRE(ncpTest.getSaraNcpFwUpdateError() == SYSTEM_ERROR_SARA_NCP_FW_UPDATE_CLOUD_CONNECT_ON_EXIT_TIMEOUT); + REQUIRE(ncpTest.getFinishedCloudConnectingRetries() == 2); + + // Restore proper state - back two states to set startTimer again + ncpTest.setSaraNcpFwUpdateState(FW_UPDATE_STATE_FINISHED_POWER_OFF); + ncpTest.setSaraNcpFwUpdateStatus(FW_UPDATE_STATUS_SUCCESS); + ncpTest.setSaraNcpFwUpdateError(SYSTEM_ERROR_NONE); + tempTimer = HAL_Timer_Get_Milli_Seconds(); + // POWER_OFF -> POWERING_OFF + mocks.ExpectCallFunc(cellular_start_ncp_firmware_update).With(false, _).Return(SYSTEM_ERROR_NONE); + mocks.ExpectCallFunc(network_off).With(NETWORK_INTERFACE_CELLULAR, 0, 0, _); + REQUIRE(ncpTest.process() == SYSTEM_ERROR_NONE); + // POWERING_OFF -> CLOUD_CONNECTING + ncpMocks.setNetworkIsOff(true); + mocks.ExpectCallFunc(network_is_off).With(NETWORK_INTERFACE_CELLULAR, _).Return(true); + REQUIRE(ncpTest.process() == SYSTEM_ERROR_NONE); + REQUIRE(ncpTest.getSaraNcpFwUpdateData().state == FW_UPDATE_STATE_FINISHED_CLOUD_CONNECTING); + REQUIRE(ncpTest.getSaraNcpFwUpdateData().status == FW_UPDATE_STATUS_SUCCESS); + REQUIRE(ncpTest.getSaraNcpFwUpdateData().error == SYSTEM_ERROR_NONE); + REQUIRE(ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_FINISHED_CLOUD_CONNECTING); + // Cloud connected succesfully case + ncpMocks.setSparkCloudFlagConnected(true); + REQUIRE(ncpTest.process() == SYSTEM_ERROR_NONE); + REQUIRE(ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_FINISHED_CLOUD_CONNECTED); + REQUIRE(HAL_Timer_Get_Milli_Seconds() < tempTimer + NCP_FW_MODEM_CLOUD_CONNECT_TIMEOUT); + + //============================================ + // FW_UPDATE_STATE_FINISHED_CLOUD_CONNECTED + //============================================ + + // Connected false case + ncpMocks.setSparkCloudFlagConnected(false); + mocks.ExpectCallFunc(spark_cloud_flag_connected).Return(false); + REQUIRE(ncpTest.process() == SYSTEM_ERROR_NONE); + REQUIRE(ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_FINISHED_IDLE); + REQUIRE(ncpTest.getSaraNcpFwUpdateStatus() == FW_UPDATE_STATUS_FAILED); + REQUIRE(ncpTest.getSaraNcpFwUpdateError() == SYSTEM_ERROR_SARA_NCP_FW_UPDATE_PUBLISH_RESULT); + + // Restore proper state + ncpTest.setSaraNcpFwUpdateState(FW_UPDATE_STATE_FINISHED_CLOUD_CONNECTED); + ncpTest.setSaraNcpFwUpdateStatus(FW_UPDATE_STATUS_SUCCESS); + ncpTest.setSaraNcpFwUpdateError(SYSTEM_ERROR_NONE); + ncpMocks.setSparkCloudFlagConnected(true); + // Failed to publish case + mocks.ExpectCallFunc(publishEvent).Return(false); + REQUIRE(ncpTest.process() == SYSTEM_ERROR_NONE); + REQUIRE(ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_FINISHED_IDLE); + REQUIRE(ncpTest.getSaraNcpFwUpdateStatus() == FW_UPDATE_STATUS_FAILED); + REQUIRE(ncpTest.getSaraNcpFwUpdateError() == SYSTEM_ERROR_SARA_NCP_FW_UPDATE_PUBLISH_RESULT); + + // Restore proper state + ncpTest.setSaraNcpFwUpdateState(FW_UPDATE_STATE_FINISHED_CLOUD_CONNECTED); + ncpTest.setSaraNcpFwUpdateStatus(FW_UPDATE_STATUS_FAILED); // simulated failure + ncpTest.setSaraNcpFwUpdateError(SYSTEM_ERROR_SARA_NCP_FW_UPDATE_POWER_OFF_TIMEOUT); // simulated error + // Publish a failed status case + mocks.ExpectCallFunc(publishEvent).Return(true); + REQUIRE(ncpTest.process() == SYSTEM_ERROR_NONE); + REQUIRE(ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_FINISHED_IDLE); + REQUIRE(ncpTest.getSaraNcpFwUpdateStatus() == FW_UPDATE_STATUS_FAILED); + REQUIRE(ncpTest.getSaraNcpFwUpdateError() == SYSTEM_ERROR_SARA_NCP_FW_UPDATE_POWER_OFF_TIMEOUT); + + // Restore proper state + ncpTest.setSaraNcpFwUpdateState(FW_UPDATE_STATE_FINISHED_CLOUD_CONNECTED); + ncpTest.setSaraNcpFwUpdateStatus(FW_UPDATE_STATUS_SUCCESS); + ncpTest.setSaraNcpFwUpdateError(SYSTEM_ERROR_NONE); + // Publish a failed status case + mocks.ExpectCallFunc(publishEvent).Return(true); + REQUIRE(ncpTest.process() == SYSTEM_ERROR_NONE); + REQUIRE(ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_FINISHED_IDLE); + REQUIRE(ncpTest.getSaraNcpFwUpdateStatus() == FW_UPDATE_STATUS_SUCCESS); + + //============================================ + // FW_UPDATE_STATE_FINISHED_IDLE + //============================================ + + REQUIRE(ncpTest.process() == SYSTEM_ERROR_NONE); + REQUIRE(ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_IDLE); + REQUIRE(ncpTest.getSaraNcpFwUpdateStatus() == FW_UPDATE_STATUS_SUCCESS); + REQUIRE(ncpTest.getSaraNcpFwUpdateData().state == FW_UPDATE_STATE_FINISHED_IDLE); + REQUIRE(ncpTest.getSaraNcpFwUpdateData().status == FW_UPDATE_STATUS_SUCCESS); + REQUIRE(ncpTest.getSaraNcpFwUpdateData().error == SYSTEM_ERROR_NONE); + REQUIRE(ncpTest.getSaraNcpFwUpdateData().updateAvailable == SYSTEM_NCP_FW_UPDATE_UNKNOWN); + REQUIRE(ncpTest.getSaraNcpFwUpdateData().isUserConfig == false); + + //============================================ + // FW_UPDATE_STATE_IDLE + //============================================ + + ncpMocks.setSystemMode(SAFE_MODE); + mocks.ExpectCallFunc(system_reset).With(SYSTEM_RESET_MODE_NORMAL, 0, 0, 0, _).Return(0); + REQUIRE(ncpTest.process() == SYSTEM_ERROR_NONE); + REQUIRE(ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_IDLE); + + ncpMocks.setSystemMode(AUTOMATIC); + mocks.NeverCallFunc(system_reset).With(SYSTEM_RESET_MODE_NORMAL, 0, 0, 0, _).Return(0); + REQUIRE(ncpTest.process() == SYSTEM_ERROR_NONE); + REQUIRE(ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_IDLE); + + ncpMocks.setSystemMode(SAFE_MODE); + ncpTest.setSaraNcpFwUpdateStatus(FW_UPDATE_STATUS_IDLE); + mocks.NeverCallFunc(system_reset).With(SYSTEM_RESET_MODE_NORMAL, 0, 0, 0, _).Return(0); + REQUIRE(ncpTest.process() == SYSTEM_ERROR_NONE); + REQUIRE(ncpTest.getSaraNcpFwUpdateState() == FW_UPDATE_STATE_IDLE); + } + } // SECTION PROCESS + } // SECTION("put fail early tests in here!") { +} diff --git a/test/unit_tests/ncp_fw_update/stubs.cpp b/test/unit_tests/ncp_fw_update/stubs.cpp new file mode 100644 index 0000000000..c041ac0d22 --- /dev/null +++ b/test/unit_tests/ncp_fw_update/stubs.cpp @@ -0,0 +1,106 @@ +/* + * 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 . + */ + +#include "system_mode.h" +#include "timer_hal.h" +#include "delay_hal.h" +#include "platform_ncp.h" +#include "system_network.h" +#include "cellular_hal.h" + +uint32_t HAL_Timer_Get_Milli_Seconds() { + static uint32_t millis = 0; + return ++millis; +} + +void HAL_Delay_Milliseconds(uint32_t ms) { +} + +System_Mode_TypeDef system_mode() { + return DEFAULT; +} + +int system_reset(unsigned mode, unsigned reason, unsigned value, unsigned flags, void* reserved) { + return 0; +} + +PlatformNCPIdentifier platform_primary_ncp_identifier() { + return PLATFORM_NCP_SARA_R510; +} + +void network_connect(network_handle_t network, uint32_t flags, uint32_t param1, void* reserved) { +} + +bool network_connecting(network_handle_t network, uint32_t param1, void* reserved) { + return false; +} + +void network_disconnect(network_handle_t network, uint32_t reason, void* reserved) { +} + +bool network_ready(network_handle_t network, uint32_t type, void* reserved) { + return false; +} + +void network_on(network_handle_t network, uint32_t flags, uint32_t param1, void* reserved) { +} + +void network_off(network_handle_t network, uint32_t flags, uint32_t param1, void* reserved) { +} + +bool network_is_on(network_handle_t network, void* reserved) { + return false; +} + +bool network_is_off(network_handle_t network, void* reserved) { + return false; +} + +int cellular_start_ncp_firmware_update(bool update, void* reserved) { + return 0; +} + +int cellular_command(_CALLBACKPTR_MDM cb, void* param, system_tick_t timeout_ms, const char* format, ...) { + return 0; +} + +int sendCommandWithArgs(_CALLBACKPTR_MDM cb, void* param, system_tick_t timeout_ms, const char* format, va_list args) { + return 0; +} + +int cellular_get_ncp_firmware_version(uint32_t* version, void* reserved) { + return 0; +} + +int cellular_add_urc_handler(const char* prefix, hal_cellular_urc_callback_t cb, void* context) { + return 0; +} + +int cellular_remove_urc_handler(const char* prefix) { + return 0; +} + +int setupHTTPSProperties_impl() { + return 0; +} + +int cellular_lock(void* reserved) { + return 0; +} + +void cellular_unlock(void* reserved) { +} diff --git a/test/unit_tests/services/CMakeLists.txt b/test/unit_tests/services/CMakeLists.txt index b619b22f91..e642108746 100644 --- a/test/unit_tests/services/CMakeLists.txt +++ b/test/unit_tests/services/CMakeLists.txt @@ -54,6 +54,9 @@ target_include_directories( ${target_name} PRIVATE ${DEVICE_OS_DIR}/hal/src/gcc PRIVATE ${DEVICE_OS_DIR}/system/inc PRIVATE ${DEVICE_OS_DIR}/wiring/inc + PRIVATE ${DEVICE_OS_DIR}/hal/ + PRIVATE ${DEVICE_OS_DIR}/hal/network/ncp/ + PRIVATE ${DEVICE_OS_DIR}/hal/network/ncp/at_parser/ PRIVATE ${THIRD_PARTY_DIR}/hippomocks ) diff --git a/test/unit_tests/stub/static_recursive_mutex.h b/test/unit_tests/stub/static_recursive_mutex.h new file mode 100644 index 0000000000..0acdc36864 --- /dev/null +++ b/test/unit_tests/stub/static_recursive_mutex.h @@ -0,0 +1,38 @@ +/* + * 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 + +class StaticRecursiveMutex { +public: + StaticRecursiveMutex() { + } + + ~StaticRecursiveMutex() { + } + + bool lock(unsigned timeout = 0) { + (void) timeout; + return true; + } + + bool unlock() { + return true; + } + +private: +}; diff --git a/test/unit_tests/stub/system_cache.cpp b/test/unit_tests/stub/system_cache.cpp new file mode 100644 index 0000000000..22a1a93f1c --- /dev/null +++ b/test/unit_tests/stub/system_cache.cpp @@ -0,0 +1,67 @@ +/* + * 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 . + */ + +#include +#include +#include +#include +#include "system_cache.h" +#include "system_error.h" +#include "enumclass.h" +#include "underlying_type.h" + +using namespace particle::services; +using namespace particle; +namespace { + +std::unordered_map::Type, std::vector> sCache; + +} + +SystemCache::SystemCache() { +} + +SystemCache& SystemCache::instance() { + static SystemCache cache; + return cache; +} + +int SystemCache::get(SystemCacheKey key, void* value, size_t length) { + auto it = sCache.find(to_underlying(key)); + if (it != sCache.end()) { + auto& vec = it->second; + auto len = std::min(length, vec.size()); + uint8_t data[length]; + std::copy(vec.begin(), vec.end(), data); + memcpy(value, data, len); + return vec.size(); + } + return SYSTEM_ERROR_NOT_FOUND; +} + +int SystemCache::set(SystemCacheKey key, const void* value, size_t length) { + std::vector vec; + vec.resize(length); + memcpy(&vec[0], value, length); + sCache[to_underlying(key)] = vec; + return vec.size(); +} + +int SystemCache::del(SystemCacheKey key) { + sCache.erase(to_underlying(key)); + return SYSTEM_ERROR_NONE; +} diff --git a/user/tests/wiring/api/cellular.cpp b/user/tests/wiring/api/cellular.cpp index 63ac1517d1..d9e655f088 100644 --- a/user/tests/wiring/api/cellular.cpp +++ b/user/tests/wiring/api/cellular.cpp @@ -154,4 +154,12 @@ test(api_cellular_registration_timeout_set) { API_COMPILE(cellular_registration_timeout_set(60 * 60 * 1000, nullptr)); // 60 minutes } +#if HAL_PLATFORM_NCP_FW_UPDATE +test (api_cellular_update) +{ + API_COMPILE(Cellular.enableUpdates()); // Disabled by default + API_COMPILE(Cellular.updateStatus()); +} +#endif // HAL_PLATFORM_NCP_FW_UPDATE + #endif diff --git a/wiring/inc/spark_wiring_cellular.h b/wiring/inc/spark_wiring_cellular.h index be6fcd68f0..57b6405d96 100644 --- a/wiring/inc/spark_wiring_cellular.h +++ b/wiring/inc/spark_wiring_cellular.h @@ -177,6 +177,18 @@ class CellularClass : public NetworkClass { cellular_unlock(nullptr); } + +#if HAL_PLATFORM_NCP_FW_UPDATE + int enableUpdates() + { + return cellular_enable_updates(nullptr); + } + + int updateStatus() + { + return cellular_update_status(nullptr); + } +#endif // HAL_PLATFORM_NCP_FW_UPDATE };