From bfcef5e9d1e384cf34ebef0f7cc98858a8445827 Mon Sep 17 00:00:00 2001 From: Mykyta Dorokhin Date: Tue, 8 May 2018 14:05:38 +0300 Subject: Add FTP FOTA upgrade functionality for ME910C1-NV radios Signed-off-by: Jeff Hatch --- include/mts/MTS_IO_ME910C1NVRadio.h | 28 +- src/MTS_IO_CellularRadio.cpp | 2 + src/MTS_IO_ME910C1NVRadio.cpp | 581 +++++++++++++++++++++++++++++++++++- 3 files changed, 607 insertions(+), 4 deletions(-) diff --git a/include/mts/MTS_IO_ME910C1NVRadio.h b/include/mts/MTS_IO_ME910C1NVRadio.h index 44dca6b..3d0b3b0 100644 --- a/include/mts/MTS_IO_ME910C1NVRadio.h +++ b/include/mts/MTS_IO_ME910C1NVRadio.h @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015 by Multi-Tech Systems + * Copyright (C) 2018 by Multi-Tech Systems * * This file is part of libmts-io. * @@ -21,8 +21,8 @@ /*! \file MTS_IO_ME910C1NVRadio.h \brief A brief description - \date Jan 19, 2015 - \author sgodinez + \date May 1, 2018 + \author mykyta.dorokhin A more elaborate description */ @@ -42,12 +42,34 @@ namespace MTS { ME910C1NVRadio(const std::string& sPort); virtual ~ME910C1NVRadio(){}; + virtual CODE updateFumo(const Json::Value& jArgs, UpdateCb& stepCb); virtual CODE getCarrier(std::string& sCarrier); protected: + CODE doGetFirmwareNumbers(std::string &sFirmware, std::string &sFirmwareBuild); + private: + static const std::string KEY_FUMO_PDPID; //!< PDP context id (default 3) + static const std::string KEY_FUMO_PDPTYPE; //!< PDP context type (default IPV4V6) + static const std::string KEY_FUMO_APN; //!< APN (default empty) + static const std::string KEY_FUMO_ADDRESS; //!< FTP server address + static const std::string KEY_FUMO_DIR; //!< Directory + static const std::string KEY_FUMO_FILE; //!< Name of the upgrade file + static const std::string KEY_FUMO_USER; //!< Username + static const std::string KEY_FUMO_PASSWORD; //!< Password + static const std::string KEY_FUMO_DRYRUN; //!< If set, do not apply the downloaded firmware + + CODE doFumoPerform(const Json::Value &jConfig, UpdateCb& stepCb); + CODE doFumoReadConfig(const Json::Value& jArgs, Json::Value &jConfig); + CODE doFumoSetup(const Json::Value &jConfig, UpdateCb& stepCb); + CODE doFumoFtp(const Json::Value &jConfig, UpdateCb& stepCb); + CODE doFumoCleanup(const Json::Value &jConfig, UpdateCb& stepCb); + CODE doFumoApplyFirmware(const Json::Value &jConfig, UpdateCb& stepCb); + CODE doFumoWaitNewFirmware(const Json::Value &jConfig, UpdateCb& stepCb); + std::string m_sFw; + std::string m_sFwBuild; }; } } diff --git a/src/MTS_IO_CellularRadio.cpp b/src/MTS_IO_CellularRadio.cpp index 7366166..6b49546 100644 --- a/src/MTS_IO_CellularRadio.cpp +++ b/src/MTS_IO_CellularRadio.cpp @@ -227,6 +227,8 @@ bool CellularRadio::resetConnection(uint32_t iTimeoutMillis) { printWarning("%s| Failed to re-open radio port [%s]", m_sName.c_str(), m_sRadioPort.c_str()); } else { printInfo("%s| Successfully re-opened radio port [%s]", m_sName.c_str(), m_sRadioPort.c_str()); + printDebug("%s| Recovering 'echo' after connection reset", m_sName.c_str()); // see CellularRadio::initialize + setEcho(m_bEchoEnabled); break; } diff --git a/src/MTS_IO_ME910C1NVRadio.cpp b/src/MTS_IO_ME910C1NVRadio.cpp index bc2282f..e9a4874 100644 --- a/src/MTS_IO_ME910C1NVRadio.cpp +++ b/src/MTS_IO_ME910C1NVRadio.cpp @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017 by Multi-Tech Systems + * Copyright (C) 2018 by Multi-Tech Systems * * This file is part of libmts-io. * @@ -21,17 +21,32 @@ /*! \file MTS_IO_ME910C1NVRadio.cpp \brief A brief description + \date May 1, 2018 + \author mykyta.dorokhin A more elaborate description */ +#include #include #include +#include +#include #include using namespace MTS::IO; const std::string ME910C1NVRadio::MODEL_NAME("ME910C1-NV"); +const std::string ME910C1NVRadio::KEY_FUMO_PDPID("pdpid"); // optional (default : "3") +const std::string ME910C1NVRadio::KEY_FUMO_PDPTYPE("pdptype"); // optional (default : "IPV4V6") +const std::string ME910C1NVRadio::KEY_FUMO_APN("apn"); // optional (default : "") +const std::string ME910C1NVRadio::KEY_FUMO_ADDRESS("address"); +const std::string ME910C1NVRadio::KEY_FUMO_DIR("dir"); +const std::string ME910C1NVRadio::KEY_FUMO_FILE("file"); +const std::string ME910C1NVRadio::KEY_FUMO_USER("user"); +const std::string ME910C1NVRadio::KEY_FUMO_PASSWORD("password"); +const std::string ME910C1NVRadio::KEY_FUMO_DRYRUN("dryrun"); + ME910C1NVRadio::ME910C1NVRadio(const std::string& sPort) : ME910Radio(MODEL_NAME, sPort) { @@ -42,3 +57,567 @@ CellularRadio::CODE ME910C1NVRadio::getCarrier(std::string& sCarrier) { sCarrier = "Verizon"; return SUCCESS; } + +CellularRadio::CODE ME910C1NVRadio::doGetFirmwareNumbers(std::string &sFirmware, std::string &sFirmwareBuild) { + CellularRadio::CODE rc = FAILURE; + + rc = getFirmware(sFirmware); + if (rc != SUCCESS){ + return rc; + } + + rc = getFirmwareBuild(sFirmwareBuild); + if (rc != SUCCESS){ + return rc; + } + + return rc; +} + +CellularRadio::CODE ME910C1NVRadio::doFumoReadConfig(const Json::Value& jArgs, Json::Value &jConfig) +{ + CellularRadio::CODE rc = INVALID_ARGS; + std::string sPath; + + do + { + if (!jArgs["config-file"].isString()) { + rc = INVALID_ARGS; + break; + } + + sPath = jArgs["config-file"].asString(); + + std::ifstream file(sPath.c_str()); + if (!file.is_open()) { + printError("Failed to open file [%s]", sPath.c_str()); + break; + } + + file.seekg(0, std::ios::end); + size_t size = file.tellg(); + std::string buffer(size, ' '); + file.seekg(0); + file.read(&buffer[0], size); + file.close(); + + Json::Features features = Json::Features::strictMode(); + Json::Reader reader(features); + if (!reader.parse(buffer, jConfig)) { + printError("Error parsing FOTA configuration file"); + break; + } + + // + // set default values if missing + // + if (!jConfig.isMember(KEY_FUMO_PDPID)) { + jConfig[KEY_FUMO_PDPID] = std::string("3"); + } + + if (!jConfig.isMember(KEY_FUMO_PDPTYPE)) { + jConfig[KEY_FUMO_PDPTYPE] = std::string("IPV4V6"); + } + + if (!jConfig.isMember(KEY_FUMO_APN)) { + jConfig[KEY_FUMO_APN] = std::string(""); + } + + // + // validate + // + if (!jConfig[KEY_FUMO_PDPID].isString()) { + printError("Error loading FOTA configuration: PDP context id is not set"); + break; + } + + if (jConfig[KEY_FUMO_PDPID].asString().empty()) { + printError("Error loading FOTA configuration: context id is empty"); + break; + } + + if (!jConfig[KEY_FUMO_PDPTYPE].isString()) { + printError("Error loading FOTA configuration: PDP type is not set"); + break; + } + + if (jConfig[KEY_FUMO_PDPTYPE].asString().empty()) { + printError("Error loading FOTA configuration: PDP type is empty"); + break; + } + + // Note : allow empty APN + if (!jConfig[KEY_FUMO_APN].isString()) { + printError("Error loading FOTA configuration: APN is not set"); + break; + } + + if (!jConfig[KEY_FUMO_ADDRESS].isString()) { + printError("Error loading FOTA configuration: address is not set"); + break; + } + + if (jConfig[KEY_FUMO_ADDRESS].asString().empty()) { + printError("Error loading FOTA configuration: address is empty"); + break; + } + + // Note: allow empty dir + if (!jConfig[KEY_FUMO_DIR].isString()) { + printError("Error loading FOTA configuration: directory is not set"); + break; + } + + if (!jConfig[KEY_FUMO_FILE].isString()) { + printError("Error loading FOTA configuration: filename is not set"); + break; + } + + if (jConfig[KEY_FUMO_FILE].asString().empty()) { + printError("Error loading FOTA configuration: filename is empty"); + break; + } + + // Note: allow empty username/password + if (!jConfig[KEY_FUMO_USER].isString()) { + printError("Error loading FOTA configuration: username is not set"); + break; + } + + if (!jConfig[KEY_FUMO_PASSWORD].isString()) { + printError("Error loading FOTA configuration: password is not set"); + break; + } + + rc = SUCCESS; + } + while(0); + + return rc; +} + +CellularRadio::CODE ME910C1NVRadio::doFumoSetup(const Json::Value &jConfig, UpdateCb& stepCb) +{ + CellularRadio::CODE rc = FAILURE; + std::string sCmd; + + std::string sContextId = jConfig[KEY_FUMO_PDPID].asString(); + std::string sApn = jConfig[KEY_FUMO_APN].asString(); + std::string sPdpType = jConfig[KEY_FUMO_PDPTYPE].asString(); + + + do + { + // + // Execution command is used to activate or deactivate either the GSM + // context or the specified PDP context. + // + // AT#SGACT=,[,,] + // + sCmd = "AT#SGACT=" + sContextId + ",0"; + rc = CellularRadio::sendBasicCommand(sCmd); + if (rc != SUCCESS) { + if(stepCb) { + stepCb(Json::Value("FUMO Error: Failed to deactivate PDP context")); + } + break; + } + + // + // Read current Firmware numbers (let it be after AT#SGACT not to confuse users) + // + rc = doGetFirmwareNumbers(m_sFw, m_sFwBuild); + if (rc != SUCCESS) { + if(stepCb) { + stepCb(Json::Value("FUMO Error: Failed to obtain current firmware version")); + } + break; + } + + // + // Set command specifies PDP context parameter values for a PDP context identified by + // the (local) context identification parameter . + // + // AT+CGDCONT= [[,[,[,[,[,[,[,...[,pdN]]]]]]]]] + // + sCmd = "AT+CGDCONT=" + sContextId + ",\"" + sPdpType + "\""; + if (!sApn.empty()) { + sCmd += ",\"" + sApn + "\""; + } + rc = CellularRadio::sendBasicCommand(sCmd, 1000); + if (rc != SUCCESS) { + if(stepCb) { + stepCb(Json::Value("FUMO Error: Failed to setup PDP context")); + } + break; + } + + // + // Set command sets the socket configuration parameters. + // + // AT#SCFG==,,,,, + // - socket connection identifier + // - PDP context identifier + // - packet size to be used by the TCP/UDP/IP stack for data sending. + // - exchange timeout (or socket inactivity timeout); if there’s no + // data exchange within this timeout period the connection is closed (timeout value in seconds). + // - connection timeout; if we can’t establish a connection to the + // remote within this timeout period, an error is raised timeout value in hundreds of milliseconds. + // - data sending timeout; after this period data are sent also if they’re + // less than max packet size (timeout value in hundreds of milliseconds). + // + sCmd = "AT#SCFG=1," + sContextId + ",300,90,600,50"; + rc = CellularRadio::sendBasicCommand(sCmd); + if (rc != SUCCESS) { + if(stepCb) { + stepCb(Json::Value("FUMO Error: Failed to set connection configuration parameters")); + } + break; + } + + // + // Activate PDP context + // + sCmd = "AT#SGACT=" + sContextId + ",1"; + rc = CellularRadio::sendBasicCommand(sCmd, 60 * 1000); + if (rc != SUCCESS) { + if(stepCb) { + stepCb(Json::Value("FUMO Error: Failed to activate PDP context")); + } + break; + } + } + while (0); + + return rc; +} + +CellularRadio::CODE ME910C1NVRadio::doFumoFtp(const Json::Value &jConfig, UpdateCb& stepCb) +{ + CellularRadio::CODE rc = FAILURE; + std::string sCmd; + std::string sResult; + + // + // Set command sets the time-out used when opening either the FTP control + // channel or the FTP traffic channel. + // + // AT#FTPTO= [] + // - time-out in 100 ms units + // + rc = CellularRadio::sendBasicCommand("AT#FTPTO=2400"); + if (rc != SUCCESS) { + if(stepCb) { + stepCb(Json::Value("FUMO Error: Failed to setup connection timeout")); + } + return rc; + } + + // + // Execution command opens an FTP connection toward the FTP server. + // + // AT#FTPOPEN=[,,[,]] + // + sCmd = "AT#FTPOPEN="; + sCmd += "\"" + jConfig[KEY_FUMO_ADDRESS].asString() + "\","; + sCmd += "\"" + jConfig[KEY_FUMO_USER].asString() + "\","; + sCmd += "\"" + jConfig[KEY_FUMO_PASSWORD].asString() + "\",1"; + rc = CellularRadio::sendBasicCommand(sCmd, 60 * 1000); + if (rc != SUCCESS) { + if(stepCb) { + stepCb(Json::Value("FUMO Error: Failed to open connection")); + } + return rc; + } + + if (stepCb) { + stepCb(Json::Value("FUMO Info: connection opened")); + } + + do + { + // + // Set command, issued during an FTP connection, sets the file transfer type. + // + // AT#FTPTYPE=[] + // - file transfer type: + // 0 - binary + // 1 - ascii + // + rc = CellularRadio::sendBasicCommand("AT#FTPTYPE=0", 1000); + if (rc != SUCCESS) { + if(stepCb) { + stepCb(Json::Value("FUMO Error: failed to set file transfer type")); + } + break; + } + + // + // Execution command, issued during an FTP connection, changes the + // working directory on FTP server. + // + // AT#FTPCWD=[] + // + sCmd = "AT#FTPCWD=\"/"; + if (!jConfig[KEY_FUMO_DIR].asString().empty()) { + sCmd += jConfig[KEY_FUMO_DIR].asString() + "/"; + } + sCmd += "\""; + rc = CellularRadio::sendBasicCommand(sCmd, 60 * 1000); + if (rc != SUCCESS) { + if(stepCb) { + stepCb(Json::Value("FUMO Error: failed to change working directory on the server")); + } + break; + } + + if (stepCb) { + stepCb(Json::Value("FUMO Info: downloading the firmware")); + } + + // + // Start FTP transfer + // + sCmd = "AT#FTPGETOTA="; + sCmd += "\"" + jConfig[KEY_FUMO_FILE].asString() + "\",1,1"; + CellularRadio::sendBasicCommand(sCmd); + + // + // Noticed that after successful AT#FTPGETOTA the radio resets the connection. + // and the response code (OK, ERROR.. ) not always reach the host. Therefore + // we send the AT#FTPGETOTA with relatively small timeout and then poll with AT + // until we get valid response. After that, using AT#FTPMSG we can check the + // result of the last FTP command (which is AT#FTPGETOTA in our case). + MTS::Timer oTimer; + + oTimer.start(); + + while (oTimer.getSeconds() < (30 * 60)) // 30 min + { + MTS::Thread::sleep(5000); + + rc = CellularRadio::sendBasicCommand("AT"); + if (rc == SUCCESS) { + break; + } + + CellularRadio::resetConnection(1); + } + oTimer.stop(); + + if (rc != SUCCESS) { + if(stepCb) { + stepCb(Json::Value("FUMO Error: unable to obtain radio after reset")); + } + break; + } + + // + // Now check the FTP status + // + std::string sResult = sendCommand("AT#FTPMSG"); + + printTrace("RADIO| AT#FTPMSG result [%s]", sResult.c_str()); + + if (sResult.find(RSP_OK) == std::string::npos) { + rc = FAILURE; + if(stepCb) { + stepCb(Json::Value("FUMO Error: failed to download the firmware file")); + } + break; + } + if (sResult.find("#FTPMSG: 550") != std::string::npos) { + // FTP(550) : File not found + rc = FAILURE; + if(stepCb) { + stepCb(Json::Value("FUMO Error: file not found")); + } + break; + } + if (sResult.find("#FTPMSG: 226") == std::string::npos) { + // FTP(226) : Successfully transferred + rc = FAILURE; + if(stepCb) { + stepCb(Json::Value("FUMO Error: failed to download the firmware file")); + } + break; + } + } + while (0); + + // + // Execution command closes an FTP connection. + // + // AT#FTPCLOSE + // + CellularRadio::CODE rcclose = CellularRadio::sendBasicCommand("AT#FTPCLOSE", 60 * 1000); + if (rcclose != SUCCESS && rc == SUCCESS) { + if(stepCb) { + // Only one "FUMO Error" message should be sent + stepCb(Json::Value("FUMO Error: Failed to close FTP connection")); + } + rc = rcclose; + } + + if (rc == SUCCESS) { + if(stepCb) { + stepCb(Json::Value("FUMO Info: firmware downloaded successfully")); + } + } + + return rc; +} + +CellularRadio::CODE ME910C1NVRadio::doFumoCleanup(const Json::Value &jConfig, UpdateCb& stepCb) +{ + CellularRadio::CODE rc = FAILURE; + std::string sCmd; + + std::string sContextId = jConfig[KEY_FUMO_PDPID].asString(); + + // + // Deactivate PDP context + // + sCmd = "AT#SGACT=" + sContextId + ",0"; + rc = CellularRadio::sendBasicCommand(sCmd, 10000); + if (rc != SUCCESS) { + if(stepCb) { + stepCb(Json::Value("FUMO Error: Failed to deactivate PDP context")); + } + } + return rc; +} + +CellularRadio::CODE ME910C1NVRadio::doFumoApplyFirmware(const Json::Value &jConfig, UpdateCb& stepCb) +{ + CellularRadio::CODE rc = FAILURE; + + if (jConfig.isMember(KEY_FUMO_DRYRUN)) { + if(stepCb) { + stepCb(Json::Value("FUMO Info: applying the radio firmware")); + } + return SUCCESS; + } + + rc = CellularRadio::sendBasicCommand("AT#OTAUP=0", 10000); + if (rc != SUCCESS) { + if(stepCb) { + stepCb(Json::Value("FUMO Error: failed to apply the firmware")); + } + return rc; + } + + if(stepCb) { + stepCb(Json::Value("FUMO Info: applying the radio firmware")); + } + + return rc; +} + +CellularRadio::CODE ME910C1NVRadio::doFumoWaitNewFirmware(const Json::Value &jConfig, UpdateCb& stepCb) +{ + std::string sFirmware; + std::string sFirmwareBuild; + CellularRadio::CODE rc = FAILURE; + + if (jConfig.isMember(KEY_FUMO_DRYRUN)) { + if(stepCb) { + stepCb(Json::Value("FUMO done: radio firmware applied successfully")); + } + return SUCCESS; + } + + // The radio is expected to send "#OTAEV: Module Upgraded To New Fw" unsolicited message + // on success. However, for some reason, we do not see this message. + + MTS::Timer oTimer; + oTimer.start(); + + while (oTimer.getSeconds() < (5 * 60)) { // 5 minutes + + MTS::Thread::sleep(10000); + + if (doGetFirmwareNumbers(sFirmware, sFirmwareBuild) != SUCCESS) { + // The radio is probably unavailable + CellularRadio::resetConnection(100); + continue; + } + + if (sFirmware == m_sFw && sFirmwareBuild == m_sFwBuild) { + // Have the same firmware. The radio resets several time + // before the firmware is actually get upgraded. So keep polling. + continue; + } + + // The firmware numbers have changed + rc = SUCCESS; + break; + } + oTimer.stop(); + + if (rc == SUCCESS) { + if(stepCb) { + stepCb(Json::Value("FUMO done: radio firmware applied successfully")); + } + } + else { + if(stepCb) { + stepCb(Json::Value("FUMO error: radio firmware has not been updated")); + } + } + + return rc; +} + + +CellularRadio::CODE ME910C1NVRadio::doFumoPerform(const Json::Value &jConfig, UpdateCb& stepCb) +{ + CellularRadio::CODE rc = FAILURE; + + UpdateCb dummyCb; + + // Set the PDP context for the FOTA + rc = doFumoSetup(jConfig, stepCb); + if (rc != SUCCESS) { + return rc; + } + + // Download FW over FTP + rc = doFumoFtp(jConfig, stepCb); + if (rc != SUCCESS) { + doFumoCleanup(jConfig, dummyCb); + return rc; + } + + // Clean up before applying the FW file + rc = doFumoCleanup(jConfig, stepCb); + if (rc != SUCCESS) { + return rc; + } + + // Apply the FW file + rc = doFumoApplyFirmware(jConfig, stepCb); + if (rc != SUCCESS) { + return rc; + } + + rc = doFumoWaitNewFirmware(jConfig, stepCb); + + return rc; +} + +CellularRadio::CODE ME910C1NVRadio::updateFumo(const Json::Value& jArgs, UpdateCb& stepCb) +{ + Json::Value jConfig(Json::objectValue); + CellularRadio::CODE rc = FAILURE; + + rc = doFumoReadConfig(jArgs, jConfig); + if (rc != SUCCESS) { + if(stepCb) { + stepCb(Json::Value("FUMO Error: bad configuration parameters")); + } + return rc; + } + + return doFumoPerform(jConfig, stepCb); +} -- cgit v1.2.3