/* * Copyright (C) 2019 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 . * */ #include #include #include #include #include #include using namespace MTS::IO; const std::string ME910C1WWRadio::MODEL_NAME("ME910C1-WW"); const std::string ME910C1WWRadio::KEY_FUMO_PDPID("pdpid"); // optional (default : "3") const std::string ME910C1WWRadio::KEY_FUMO_PDPTYPE("pdptype"); // optional (default : "IPV4V6") const std::string ME910C1WWRadio::KEY_FUMO_APN("apn"); // optional (default : "") const std::string ME910C1WWRadio::KEY_FUMO_ADDRESS("address"); const std::string ME910C1WWRadio::KEY_FUMO_DIR("dir"); const std::string ME910C1WWRadio::KEY_FUMO_FILE("file"); const std::string ME910C1WWRadio::KEY_FUMO_USER("user"); const std::string ME910C1WWRadio::KEY_FUMO_PASSWORD("password"); const std::string ME910C1WWRadio::KEY_FUMO_DRYRUN("dryrun"); ME910C1WWRadio::ME910C1WWRadio(const std::string& sPort) : ME910Radio(MODEL_NAME, sPort) { } ICellularRadio::CODE ME910C1WWRadio::setActiveFirmware(const Json::Value& jArgs) { ICellularRadio::CODE rc; // Set command allows enabling a specific firmware image on products // embedding 2 different firmware images: // // "AT#FWSWITCH=[,]" // - Firmware Image To Be Enabled // 0 – Image 1 (Default) // 1 – Image 2 // - Setting Storage Configuration // 0 – Save the value in RAM // 1 – Save the value in NVM printTrace("%s| Set Active Firmware Image Number", getName().c_str()); if(!jArgs["fwid"].isString()) { return INVALID_ARGS; } // ME910C1-WW uses code "2" for World-Wide mode if (jArgs["fwid"].asString() != "2" && jArgs["fwid"].asString() != "1" && jArgs["fwid"].asString() != "0") { return INVALID_ARGS; } // Test command to check if firmware switch is supported rc = sendBasicCommand("AT#FWSWITCH=?"); if (rc == ERROR) { printTrace("%s| FWSWITCH is not supported", getName().c_str()); return NOT_APPLICABLE; } else if (rc != SUCCESS) { return rc; } std::string sCmd = "AT#FWSWITCH="; sCmd += jArgs["fwid"].asString(); sCmd += ",1"; printTrace("%s| Issuing %s command", getName().c_str(), sCmd.c_str()); return sendBasicCommand(sCmd, 5000); } ICellularRadio::CODE ME910C1WWRadio::getActiveFirmware(std::string& sFwId) { std::string sCmd; ICellularRadio::CODE rc; // // Read command reports the current active firmware image: // AT#FWSWITCH? // #FWSWITCH: 1 // // OK // printTrace("%s| Get Active Firmware Image Number", getName().c_str()); // Test command to check if firmware switch is supported sCmd = "AT#FWSWITCH=?"; rc = sendBasicCommand(sCmd); if (rc == ERROR) { printTrace("%s| FWSWITCH is not supported", getName().c_str()); return NOT_APPLICABLE; } else if (rc != SUCCESS) { return rc; } sCmd = "AT#FWSWITCH?"; std::string sResult = sendCommand(sCmd); size_t end = sResult.find(ICellularRadio::RSP_OK); if (end == std::string::npos) { printWarning("%s| Unable to get active image number from radio using command [%s]", getName().c_str(), sCmd.c_str()); return FAILURE; } size_t start = sResult.find("#FWSWITCH:") + sizeof("#FWSWITCH:"); sFwId = MTS::Text::trim(sResult.substr(start, end-start)); if(sFwId.size() == 0) { printWarning("%s| Firmware Image Number is empty", getName().c_str()); return FAILURE; } return SUCCESS; } ICellularRadio::CODE ME910C1WWRadio::doGetFirmwareNumbers(std::string &sFirmware, std::string &sFirmwareBuild) { ICellularRadio::CODE rc = FAILURE; rc = getFirmware(sFirmware); if (rc != SUCCESS){ return rc; } rc = getFirmwareBuild(sFirmwareBuild); if (rc != SUCCESS){ return rc; } return rc; } ICellularRadio::CODE ME910C1WWRadio::doFumoReadConfig(const Json::Value& jArgs, Json::Value &jConfig) { ICellularRadio::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(); #if defined(JSONCPP_VERSION_HEXA) && (JSONCPP_VERSION_HEXA > 0x010600) // > 1.6.0 Json::CharReaderBuilder builder; std::istringstream ss(buffer); if (!Json::parseFromStream(builder, ss, &jConfig, NULL)) { printError("Error parsing FOTA configuration file"); break; } #else Json::Features features = Json::Features::strictMode(); Json::Reader reader(features); if (!reader.parse(buffer, jConfig)) { printError("Error parsing FOTA configuration file"); break; } #endif // // 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; } ICellularRadio::CODE ME910C1WWRadio::doFumoSetup(const Json::Value &jConfig, UpdateCb& stepCb) { ICellularRadio::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; } ICellularRadio::CODE ME910C1WWRadio::doFumoFtp(const Json::Value &jConfig, UpdateCb& stepCb) { ICellularRadio::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(ICellularRadio::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 // ICellularRadio::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; } ICellularRadio::CODE ME910C1WWRadio::doFumoCleanup(const Json::Value &jConfig, UpdateCb& stepCb) { ICellularRadio::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; } ICellularRadio::CODE ME910C1WWRadio::doFumoApplyFirmware(const Json::Value &jConfig, UpdateCb& stepCb) { ICellularRadio::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; } ICellularRadio::CODE ME910C1WWRadio::doFumoWaitNewFirmware(const Json::Value &jConfig, UpdateCb& stepCb) { std::string sFirmware; std::string sFirmwareBuild; ICellularRadio::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; } ICellularRadio::CODE ME910C1WWRadio::doFumoPerform(const Json::Value &jConfig, UpdateCb& stepCb) { ICellularRadio::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; } ICellularRadio::CODE ME910C1WWRadio::updateFumo(const Json::Value& jArgs, UpdateCb& stepCb) { Json::Value jConfig(Json::objectValue); ICellularRadio::CODE rc = FAILURE; std::string sFwId; do { rc = getActiveFirmware(sFwId); if (rc != SUCCESS) { if(stepCb) { stepCb(Json::Value("FUMO Error: failed to obtain current active firmware id")); } break; } // For Verizon Only if (sFwId != "1") { if(stepCb) { stepCb(Json::Value("FUMO Error: fumo is not supported")); } break; } rc = doFumoReadConfig(jArgs, jConfig); if (rc != SUCCESS) { if(stepCb) { stepCb(Json::Value("FUMO Error: bad configuration parameters")); } break; } rc = doFumoPerform(jConfig, stepCb); } while(0); return rc; }