/* * Copyright (C) 2018 by Multi-Tech Systems * * This file is part of libmts-io. * * libmts-io 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 2 of the License, or * (at your option) any later version. * * libmts-io 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 libmts-io. If not, see . * */ /*! \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) { } ME910C1NVRadio::CODE ME910C1NVRadio::getCarrier(std::string& sCarrier) { sCarrier = "Verizon"; return SUCCESS; } ME910C1NVRadio::CODE ME910C1NVRadio::doGetFirmwareNumbers(std::string &sFirmware, std::string &sFirmwareBuild) { CODE rc = FAILURE; rc = getFirmware(sFirmware); if (rc != SUCCESS){ return rc; } rc = getFirmwareBuild(sFirmwareBuild); if (rc != SUCCESS){ return rc; } return rc; } ME910C1NVRadio::CODE ME910C1NVRadio::doFumoReadConfig(const Json::Value& jArgs, Json::Value &jConfig) { 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; } ME910C1NVRadio::CODE ME910C1NVRadio::doFumoSetup(const Json::Value &jConfig, UpdateCb& stepCb) { 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 = 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 = 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 = 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 = sendBasicCommand(sCmd, 60 * 1000); if (rc != SUCCESS) { if(stepCb) { stepCb(Json::Value("FUMO Error: Failed to activate PDP context")); } break; } } while (0); return rc; } ME910C1NVRadio::CODE ME910C1NVRadio::doFumoFtp(const Json::Value &jConfig, UpdateCb& stepCb) { 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 = 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 = 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 = 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 = 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"; 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 = sendBasicCommand("AT"); if (rc == SUCCESS) { break; } 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 // CODE rcclose = 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; } ME910C1NVRadio::CODE ME910C1NVRadio::doFumoCleanup(const Json::Value &jConfig, UpdateCb& stepCb) { CODE rc = FAILURE; std::string sCmd; std::string sContextId = jConfig[KEY_FUMO_PDPID].asString(); // // Deactivate PDP context // sCmd = "AT#SGACT=" + sContextId + ",0"; rc = sendBasicCommand(sCmd, 10000); if (rc != SUCCESS) { if(stepCb) { stepCb(Json::Value("FUMO Error: Failed to deactivate PDP context")); } } return rc; } ME910C1NVRadio::CODE ME910C1NVRadio::doFumoApplyFirmware(const Json::Value &jConfig, UpdateCb& stepCb) { CODE rc = FAILURE; if (jConfig.isMember(KEY_FUMO_DRYRUN)) { if(stepCb) { stepCb(Json::Value("FUMO Info: applying the radio firmware")); } return SUCCESS; } rc = 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; } ME910C1NVRadio::CODE ME910C1NVRadio::doFumoWaitNewFirmware(const Json::Value &jConfig, UpdateCb& stepCb) { std::string sFirmware; std::string sFirmwareBuild; 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 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; } ME910C1NVRadio::CODE ME910C1NVRadio::doFumoPerform(const Json::Value &jConfig, UpdateCb& stepCb) { 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; } ME910C1NVRadio::CODE ME910C1NVRadio::updateFumo(const Json::Value& jArgs, UpdateCb& stepCb) { Json::Value jConfig(Json::objectValue); 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); }