/*
* 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);
}