/*
* 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 "mts/MTS_IO_QuectelRadio.h"
#include
#include
#include
#include
#include
#include
using namespace MTS::IO;
const size_t QuectelRadio::FILE_CHUNK_SIZE = 2048;
const std::string QuectelRadio::CMD_ABORT_UPLOAD = "+++";
// It is strongly recommended to use DOS 8.3 file name format for .
const std::string QuectelRadio::VALUE_MTS_DELTA_NAME = "mtsdelta.zip";
const std::string QuectelRadio::VALUE_MTS_DELTA_PATH = "/data/ufs/" + QuectelRadio::VALUE_MTS_DELTA_NAME;
const std::string QuectelRadio::VALUE_QNVR_DISABLE_VOICE = "0102000000000000";
QuectelRadio::QuectelRadio(const std::string& sName, const std::string& sRadioPort)
: CellularRadio (sName, sRadioPort)
{
}
bool QuectelRadio::resetRadio(uint32_t iTimeoutMillis) {
printInfo("%s| Rebooting radio", getName().c_str());
if(sendBasicCommand("AT+CFUN=1,1") == SUCCESS) {
if(iTimeoutMillis > 5000) {
MTS::Thread::sleep(5000);
iTimeoutMillis -= 5000;
}
return resetConnection(iTimeoutMillis);
}
return false;
}
ICellularRadio::CODE QuectelRadio::getVendorFirmware(std::string& sVendorFirmware) {
printTrace("%s| Get Quectel-specific firmware version", getName().c_str());
sVendorFirmware = ICellularRadio::VALUE_NOT_SUPPORTED;
std::string sCmd("AT+QGMR");
std::string sResult = sendCommand(sCmd);
size_t pos = sResult.find(ICellularRadio::RSP_OK);
if (pos == std::string::npos) {
printWarning("%s| Unable to get firmware from radio using command [%s]", getName().c_str(), sCmd.c_str());
return FAILURE;
}
sVendorFirmware = MTS::Text::trim(sResult.substr(0, pos));
if(sVendorFirmware.size() == 0) {
printWarning("%s| Unable to get firmware from radio using command [%s]", getName().c_str(), sCmd.c_str());
return FAILURE;
}
return SUCCESS;
}
ICellularRadio::CODE QuectelRadio::getModel(std::string& sModel) {
printTrace("%s| Get Model", getName().c_str());
//Always returns SUCCESS because the model should be m_sName
sModel = getName();
std::string sCmd("AT+GMM");
std::string sResult = sendCommand(sCmd);
if (sResult.find("OK") == std::string::npos) {
printWarning("%s| Unable to get model from radio. Returning [%s]", getName().c_str(), getName().c_str());
return SUCCESS;
} else {
sModel = extractModelFromResult(sResult);
if(sModel.size() == 0) {
printWarning("%s| Unable to get model from radio. Returning [%s]", getName().c_str(), getName().c_str());
return SUCCESS;
}
}
printDebug("%s| Extracted [%s] from [%s] query", getName().c_str(), sModel.c_str(), sCmd.c_str());
if(sModel != getName()) {
printWarning("%s| Model identified [%s] does not match expected [%s]. Returning [%s]",
getName().c_str(), sModel.c_str(), getName().c_str(), sModel.c_str());
}
return SUCCESS;
}
ICellularRadio::CODE QuectelRadio::getIccid(std::string& sIccid) {
printTrace("%s| Get ICCID", getName().c_str());
sIccid = ICellularRadio::VALUE_NOT_SUPPORTED;
// AT+QCCID execution can take up to 300ms according to the datasheet. Setting timeout to 500ms just for sure.
std::string sCmd("AT+QCCID");
std::string sResult = sendCommand(sCmd, DEFAULT_BAIL_STRINGS, 500);
size_t end = sResult.find(ICellularRadio::RSP_OK);
if (end == std::string::npos) {
printWarning("%s| Unable to get ICCID from radio using command [%s]", getName().c_str(), sCmd.c_str());
return FAILURE;
}
size_t start = sResult.find("+QCCID:");
if(start != std::string::npos) {
start += sizeof("+QCCID:");
sIccid = MTS::Text::trim(sResult.substr(start, end-start));
if(sIccid.size() == 0) {
printWarning("%s| Unable to get ICCID from radio using command [%s]", getName().c_str(), sCmd.c_str());
return FAILURE;
}
}
return SUCCESS;
}
ICellularRadio::CODE QuectelRadio::getService(std::string& sService) {
printTrace("%s| Get Service", getName().c_str());
sService = ICellularRadio::VALUE_NOT_SUPPORTED;
std::string sCmd("AT+COPS?");
std::string sResult = sendCommand(sCmd);
size_t end = sResult.find(ICellularRadio::RSP_OK);
if (end == std::string::npos) {
printWarning("%s| Unable to get Service from radio using command [%s]", getName().c_str(), sCmd.c_str());
return FAILURE;
}
size_t start = sResult.find(":") + 1; //Position right after "+COPS:"
std::vector vParts = MTS::Text::split(MTS::Text::trim(sResult.substr(start, end-start)), ',');
int32_t iAccessTechnology;
// +COPS: [,[,][,]]
if (vParts.size() < 4 || !MTS::Text::parse(iAccessTechnology, vParts[3])) {
printWarning("%s| Unable to get Service from radio using command [%s]", getName().c_str(), sCmd.c_str());
return FAILURE;
}
switch(iAccessTechnology) {
case 0 : sService = "GPRS" ; break; // GSM
case 2 : sService = "WCDMA" ; break; // UTRAN
case 3 : sService = "EGPRS" ; break; // GSM W/EGPRS
case 4 : sService = "HSDPA" ; break; // UTRAN W/HSDPA
case 5 : sService = "WCDMA" ; break; // UTRAN W/HSUPA
case 6 : sService = "HSDPA" ; break; // UTRAN W/HSDPA and HSUPA
case 7 : sService = "LTE" ; break; // E-UTRAN
case 100 : sService = "CDMA" ; break; // CDMA
default: sService = ICellularRadio::VALUE_UNKNOWN; break;
}
printDebug("%s| Service ID: [%d][%s]", getName().c_str(), iAccessTechnology, sService.c_str());
return SUCCESS;
}
ICellularRadio::CODE QuectelRadio::getNetwork(std::string& sNetwork) {
/*
* TODO: Refactor using MccMncTable once it'll be corrected.
*
* The proper way to determine the current network is to do that
* by MCC and MNC fetched from the `getNetworkStatus` and `AT+QENG` command.
* By using MCC and MNC from `AT+QENG` we can fetch the name of the network
* reported by a currently connected base station even if the SIM card is
* not installed or if we are currently working is a roaming mode.
*
* Until MccMncTable implementation is not fixed, we are using the name
* of a currently selected operator (AT+COPS).
*/
printTrace("%s| Get Network", getName().c_str());
sNetwork = ICellularRadio::VALUE_NOT_SUPPORTED;
std::string sCmd("AT+COPS?");
std::string sResult = sendCommand(sCmd);
size_t end = sResult.find(ICellularRadio::RSP_OK);
if (end == std::string::npos) {
printWarning("%s| Unable to get network name from radio using command [%s]", getName().c_str(), sCmd.c_str());
return FAILURE;
}
// +COPS: [, , ,]
// +COPS: vParts[0],vParts[1],vParts[2],vParts[3]
size_t start = sResult.find(":") + 1; //Position right after "+COPS:"
std::vector vParts = MTS::Text::split(MTS::Text::trim(sResult.substr(start)), ",");
if(vParts.size() > 3) {
const std::string sValue = vParts[2];
// +COPS: 0,0,"CHN-UNICOM UNICOM",7
// ^start ^end
// +COPS: 0,0,"AT&T",7
// ^st ^end
size_t start = sValue.find("\"") + 1;
size_t end = sValue.find_first_of(" \"", start);
sNetwork = sValue.substr(start, end-start);
} else {
sNetwork = ""; // Not connected to any network
}
return SUCCESS;
}
/* AT+QENG="servingcell" - Query the information of serving cells
(GSM network)
+QENG:"servingscell",,"GSM",,,,,,,,,,,,,,,,,,,,,,,,
(WCDMA network)
+QENG:"servingcell",,"WCDMA",,,,,,,,,,,,,,
(LTE Network)
+QENG:"servingcell",,"LTE",,,,,,,,,,,,,,,
The following modes are NOT currently handled:
- TD-SCDMA mode;
- CDMA mode;
- HDR mode;
- SRLTE mode.
In the case of TD-SCDMA mode:
+QENG:"servingscell",,"TDSCDMA",,,,,,,,
In the case of CDMA mode or CDMA+HDR mode:
+QENG:"servingscell",,"CDMA",,,,,,,,
[+QENG:"servingscell",,"HDR",,,,,,,,]
In the case of SRLTE mode:
+QENG:"servingscell",,"CDMA",,,,,,,,
+QENG:"servingcell",,"LTE",,,,,,,,,,,,,,
*/
CellularRadio::CODE QuectelRadio::getNetworkStatus(Json::Value& jData) {
const std::string RAT_GSM = "GSM";
const std::string RAT_WCDMA = "WCDMA";
const std::string RAT_LTE = "LTE";
ACTIVEBAND abnd;
SERVICEDOMAIN sd;
std::string sValue;
std::string sRat; // Radio Access Technology which is currently used
printTrace("%s| Get Network Status", getName().c_str());
//Always get common network stats because this should never fail
//This way the basic stats are always returned even if AT+QENG fails below
getCommonNetworkStats(jData);
// IMSI is not provided by AT+QENG. Fetch it separately to keep the same interface
if (getImsi(sValue) == SUCCESS) {
jData[ICellularRadio::KEY_IMSI] = sValue;
}
// Network Name is not explicitly provided by AT+QENG. Fetch it separately to keep the same interface
// TODO: Replace with lookup by MCC and MNC once MccMncTable is fixed.
if (getNetwork(sValue) == SUCCESS) {
jData[ICellularRadio::KEY_NETWORK] = sValue;
}
std::string sCmd;
std::string sResult;
sCmd = "AT+QENG=\"servingcell\"";
sResult = sendCommand(sCmd, DEFAULT_BAIL_STRINGS, 200);
if (sResult.find("+QENG: \"servingcell\"") == std::string::npos) {
printDebug("%s| Network Status command returned unexpected response: [%s][%s]", getName().c_str(), sCmd.c_str(), sResult.c_str());
printTrace("%s| Network Status:\n%s\n", getName().c_str(), jData.toStyledString().c_str());
return SUCCESS; //return SUCCESS because getCommonNetworkStats() succeeded at top of this function
}
size_t start = sResult.find(":") + 1; //Position right after "+QENG:"
size_t end = sResult.rfind(ICellularRadio::RSP_OK);
std::vector vParts = MTS::Text::split(MTS::Text::trim(sResult.substr(start, end-start)), ",");
Json::Value jDebug;
Json::Value jQuectelDebug;
if (vParts.size() < 3) {
printDebug("%s| Network Status command response is an unknown format: [%s][%s]", getName().c_str(), sCmd.c_str(), sResult.c_str());
printTrace("%s| Network Status:\n%s\n", getName().c_str(), jData.toStyledString().c_str());
return SUCCESS; //return SUCCESS because getCommonNetworkStats() succeeded at top of this function
} else {
// UE state and Access technology, Quectel-specific information
jQuectelDebug["state"] = vParts[1];
sRat = MTS::Text::trim(vParts[2], '"');
jQuectelDebug["rat"] = sRat;
}
// +QENG:"servingscell",,"GSM",,,,,,,,,,,,,,,,,,,,,,,,
// +QENG: [0], [1], [2], [3], [4], [5], [6], [7], [8], [9], [10], [11], [12], [13],[14],[15], [16], [17],[18],[19], [20], [21], [22], [23], [24], [25], [26]
if (sRat == RAT_GSM) {
//Parse as GSM Network Format
jData[ICellularRadio::KEY_MCC] = vParts[3];
jData[ICellularRadio::KEY_MNC] = vParts[4];
jData[ICellularRadio::KEY_LAC] = vParts[5];
jData[ICellularRadio::KEY_CID] = vParts[6];
jQuectelDebug["bsic"] = vParts[7];
jData[ICellularRadio::KEY_CHANNEL] = vParts[8];
if (convertToActiveBand(vParts[9], abnd) == SUCCESS && convertActiveBandToString(abnd, sValue) == SUCCESS) {
jData[ICellularRadio::KEY_ABND] = sValue;
}
jData[ICellularRadio::KEY_RSSIDBM] = vParts[10]; // Values already negative. No need to substract 111 as stated in a datasheet
jData[ICellularRadio::KEY_TXPWR] = vParts[11];
jQuectelDebug["rla"] = vParts[12];
jQuectelDebug["drx"] = vParts[13];
jQuectelDebug["c1"] = vParts[14];
jQuectelDebug["c2"] = vParts[15];
jQuectelDebug["gprs"] = vParts[16];
jQuectelDebug["tch"] = vParts[17];
jQuectelDebug["ts"] = vParts[18];
jQuectelDebug["ta"] = vParts[19];
jQuectelDebug["maio"] = vParts[20];
jQuectelDebug["hsn"] = vParts[21];
jQuectelDebug["rxlevsub"] = vParts[22];
jQuectelDebug["rxlevfull"] = vParts[23];
jQuectelDebug["rxqualsub"] = vParts[24];
jQuectelDebug["rxqualfull"] = vParts[25];
jQuectelDebug["voicecodec"] = vParts[26];
// Service Domain is not provided by AT+QENG. Fetch it separately to keep the same interface
if (getServiceDomain(sd) == SUCCESS && convertServiceDomainToString(sd, sValue) == SUCCESS) {
jData[ICellularRadio::KEY_SD] = sValue;
}
// The following fields can NOT be fetched for Quectel in GSM mode: RAC, MM, RR, NOM
jData["quectelDebug"] = jQuectelDebug;
}
// +QENG:"servingcell",,"WCDMA",,,,,,,,,,,,,,
// +QENG: [0], [1], [2], [3], [4], [5], [6], [7], [8], [9], [10], [11], [12],[13], [14], [15], [16]
else if(sRat == RAT_WCDMA) {
//Parse as WCDMA Network Format
jData[ICellularRadio::KEY_MCC] = vParts[3];
jData[ICellularRadio::KEY_MNC] = vParts[4];
jData[ICellularRadio::KEY_LAC] = vParts[5];
jData[ICellularRadio::KEY_CID] = vParts[6];
jData[ICellularRadio::KEY_CHANNEL] = vParts[7];
jDebug[ICellularRadio::KEY_PSC] = vParts[8];
jData[ICellularRadio::KEY_RAC] = vParts[9];
jDebug[ICellularRadio::KEY_RSCP] = vParts[10];
jDebug[ICellularRadio::KEY_ECIO] = vParts[11];
jQuectelDebug["phych"] = vParts[12];
jQuectelDebug["sf"] = vParts[13];
jQuectelDebug["slot"] = vParts[14];
jQuectelDebug["speechCode"] = vParts[15];
jQuectelDebug["comMod"] = vParts[16];
// The following fields can NOT be fetched for Quectel in WCDMA mode: TXPWR, DRX, MM, RR, NOM, BLER
// RSSI is not provided by AT+QENG in WCDMA mode. It was filled above by the getCommonNetworkStats
// Service Domain is not provided by AT+QENG. Fetch it separately to keep the same interface
if (getServiceDomain(sd) == SUCCESS && convertServiceDomainToString(sd, sValue) == SUCCESS) {
jDebug[ICellularRadio::KEY_SD] = sValue;
}
// BLER is not provided by AT+QENG. Set to constant
jDebug[ICellularRadio::KEY_BLER] = "000";
// Get the radio band given the channel (UARFCN)
RadioBandMap radioBandMap(vParts[7], ICellularRadio::VALUE_TYPE_CDMA);
jData[ICellularRadio::KEY_ABND] = radioBandMap.getRadioBandName();
jData["quectelDebug"] = jQuectelDebug;
jData[ICellularRadio::KEY_DEBUG] = jDebug;
}
// +QENG:"servingcell",,"LTE",,,,,,,,,,,,,,,
// +QENG: [0], [1], [2], [3], [4], [5], [6], [7], [8], [9], [10], [11], [12], [13], [14], [15], [16], [17]
else if(sRat == RAT_LTE) {
//Parse as LTE Network Format
jQuectelDebug["isTdd"] = vParts[3];
jData[ICellularRadio::KEY_MCC] = vParts[4];
jData[ICellularRadio::KEY_MNC] = vParts[5];
jData[ICellularRadio::KEY_CID] = vParts[6];
jQuectelDebug["pcid"] = vParts[7];
jData[ICellularRadio::KEY_CHANNEL] = vParts[8];
jQuectelDebug["freqBandInd"] = vParts[9];
jQuectelDebug["ulBandwidth"] = vParts[10];
jQuectelDebug["dlBandwidth"] = vParts[11];
jData["tac"] = vParts[12];
jDebug["rsrp"] = vParts[13];
jDebug["rsrq"] = vParts[14];
jDebug[ICellularRadio::KEY_RSSIDBM] = vParts[15];
jQuectelDebug["sinr"] = vParts[16];
jQuectelDebug["srxlev"] = vParts[17];
// Get the radio band given the channel (EARFCN)
RadioBandMap radioBandMap(vParts[8], ICellularRadio::VALUE_TYPE_LTE);
jData[ICellularRadio::KEY_ABND] = radioBandMap.getRadioBandName();
// Service Domain is not provided by AT+QENG. Fetch it separately to keep the same interface
if (getServiceDomain(sd) == SUCCESS && convertServiceDomainToString(sd, sValue) == SUCCESS) {
jDebug[ICellularRadio::KEY_SD] = sValue;
}
// LAC is not provided by AT+QENG in LTE mode. Use another command instead
jData[ICellularRadio::KEY_LAC] = queryLteLac();
jData["quectelDebug"] = jQuectelDebug;
jData[ICellularRadio::KEY_DEBUG] = jDebug;
}
printTrace("%s| Network Status:\n%s\n", getName().c_str(), jData.toStyledString().c_str());
return SUCCESS;
}
ICellularRadio::CODE QuectelRadio::convertSignalStrengthTodBm(const int32_t& iRssi, int32_t& iDbm) {
int dbmSteps, minValue, maxValue, rssiOffset;
int rawDbm;
if(iRssi >= 0 && iRssi < 99) {
// normal scaling
dbmSteps = 2;
minValue = -113;
maxValue = -51;
rssiOffset = 0;
} else if(iRssi >= 100 && iRssi < 199) {
// TD-SCDMA scaling
dbmSteps = 1;
minValue = -116;
maxValue = -25;
rssiOffset = 100;
} else {
return FAILURE; // invalid, not known or not detectable
}
rawDbm = minValue + ((iRssi - rssiOffset) * dbmSteps);
iDbm = std::min(maxValue, rawDbm);
return SUCCESS;
}
ICellularRadio::CODE QuectelRadio::convertdBmToSignalStrength(const int32_t& iDBm, int32_t& iRssi) {
//Quectel Conversion FOR NORMAL SCALING
const int dbmSteps = 2;
const int minValue = -113;
const int rssiOffset = 0;
if (iDBm < -113) {
iRssi = 0;
} else if (iDBm > -51) {
iRssi = 31;
} else {
iRssi = ((iDBm - minValue) / dbmSteps) + rssiOffset;
}
return SUCCESS;
}
ICellularRadio::CODE QuectelRadio::setMdn(const Json::Value& jArgs) {
printTrace("%s| Set MDN", getName().c_str());
return NOT_APPLICABLE;
}
ICellularRadio::CODE QuectelRadio::startOmaDm(ICellularRadio::UpdateCb& stepCb) {
printTrace("%s| Start OMA DM procedure", getName().c_str());
// TODO: All the timeout values below are empirically defined.
// Feel free to update them if you get any verified information.
const int32_t iTimeoutOk = 3 * 1000; // 3 seconds
const int32_t iTimeoutStart = 5 * 1000; // 5 seconds
const int32_t iTimeoutEnd = 160 * 1000; // 2 minutes 40 seconds
const int32_t iTimeoutAbort = 3 * 1000; // 3 seconds
const std::string sCmdOdmStart = "AT+QODM=\"dme\",2,\"ui\"";
const std::string sCmdOdmAbort = "AT+QODM=\"dme\",2,\"kill\"";
const std::string sOdmStarted = "DM Start";
const std::string sOdmFinished = "DM End";
const std::string sOdmAbnormal = "DME Abnormal";
const std::vector vOdmStartedStrings{ sOdmStarted };
const std::vector vOdmFinishedStrings{ sOdmFinished, sOdmAbnormal };
CODE eCode;
do {
// Send command and expect "OK" in iTimeoutOk milliseconds
eCode = sendBasicCommand(sCmdOdmStart, iTimeoutOk);
if (eCode != SUCCESS) {
printError("%s| OMA DM procedure can not be started", getName().c_str());
callNextStep(stepCb, "OMA DM Error: OMA DM can not be started");
break;
}
// Wait for the "Start" response
std::string sResponse = waitResponse(vOdmStartedStrings, iTimeoutStart);
printDebug("%s| Radio returned: [%s]", getName().c_str(), sResponse.c_str());
// Received something unexpected or nothing at all?
if (sResponse.find(sOdmStarted) == std::string::npos) {
printError("%s| OMA DM procedure failed due to timeout", getName().c_str());
callNextStep(stepCb, "OMA DM Error: OMA DM failed due to timeout");
eCode = FAILURE;
break;
}
// Got "DM Started" message from the radio
printTrace("%s| OMA DM started", getName().c_str());
callNextStep(stepCb, "OMA DM Info: OMA DM started");
// Wait for the "End" or "Abnormal" response
sResponse = waitResponse(vOdmFinishedStrings, iTimeoutEnd);
printDebug("%s| Radio returned: [%s]", getName().c_str(), sResponse.c_str());
// Received "Abnormal"?
if (sResponse.find(sOdmAbnormal) != std::string::npos) {
printError("%s| OMA DM procedure failed due to internal error: [%s]", getName().c_str(), sResponse.c_str());
callNextStep(stepCb, "OMA DM Error: OMA DM failed due to internal error");
eCode = FAILURE;
break;
}
// Received something unexpected or nothing at all?
if (sResponse.find(sOdmFinished) == std::string::npos) {
printError("%s| OMA DM procedure failed due to timeout", getName().c_str());
callNextStep(stepCb, "OMA DM Error: OMA DM failed due to timeout");
sendBasicCommand(sCmdOdmAbort, iTimeoutAbort); // abort the procedure
eCode = FAILURE;
break;
}
// Got "DM End" message from the radio
printTrace("%s| OMA DM finished", getName().c_str());
callNextStep(stepCb, "OMA DM Info: OMA DM finished");
eCode = SUCCESS;
} while (false);
return eCode;
}
ICellularRadio::CODE QuectelRadio::updateFumoLocal(int fd, ICellularRadio::UpdateCb& stepCb) {
CODE rc;
rc = fumoLocalInject(fd, stepCb);
if (rc != SUCCESS) {
return rc;
}
rc = fumoLocalApply(stepCb);
(void)fumoLocalCleanup(); // try to cleanup the injected file but cleanup errors are not fatal
return rc;
}
ICellularRadio::CODE QuectelRadio::fumoLocalInject(int fd, ICellularRadio::UpdateCb& stepCb) {
CODE rc = FAILURE;
bool bIsFilePresent = false;
do {
callNextStep(stepCb, "FUMO Info: downloading the firmware");
rc = checkFile(bIsFilePresent, VALUE_MTS_DELTA_NAME);
if (rc != SUCCESS) {
printError("Failed to check if the delta file was already download.");
callNextStep(stepCb, "FUMO Error: failed to download the firmware file");
break;
}
if (bIsFilePresent) {
rc = fumoLocalCleanup();
}
if (rc != SUCCESS) {
printError("Failed to remove the previous delta file.");
callNextStep(stepCb, "FUMO Error: failed to download the firmware file");
break;
}
rc = uploadFile(fd, VALUE_MTS_DELTA_NAME, stepCb);
if (rc != SUCCESS) {
printError("Failed to inject the delta file.");
callNextStep(stepCb, "FUMO Error: failed to download the firmware file");
break;
}
callNextStep(stepCb, "FUMO Info: firmware downloaded successfully");
} while (false);
return rc;
}
ICellularRadio::CODE QuectelRadio::fumoLocalCleanup() {
printTrace("Removing the delta upgrade file: %s", VALUE_MTS_DELTA_NAME.c_str());
return removeFile(VALUE_MTS_DELTA_NAME);
}
ICellularRadio::CODE QuectelRadio::fumoLocalApply(ICellularRadio::UpdateCb& stepCb) {
const std::string sFotaUrcPrefix = "+QIND: \"FOTA\""; // prefix for URC notification messages
const std::vector vFotaBailStrings{ sFotaUrcPrefix };
ICellularRadio::CODE rc;
std::string sCmd;
std::string sResponse;
rc = getVendorFirmware(m_sQuectelFirmware);
if (rc != SUCCESS) {
callNextStep(stepCb, "FUMO Error: Failed to obtain current firmware version");
return rc;
}
printInfo("Current firmware version: %s", m_sQuectelFirmware.c_str());
// Send "AT+QFOTADL" command to start the upgrade. OK response follows shortly.
sCmd = "AT+QFOTADL=\"";
sCmd += VALUE_MTS_DELTA_PATH;
sCmd += "\"";
rc = sendBasicCommand(sCmd, 10000);
if (rc != SUCCESS) {
printError("FUMO failed, OK not received from the radio");
callNextStep(stepCb, "FUMO Error: failed to apply the firmware");
return rc;
}
const uint32_t duDetachTimeout = 15000; // wait up to 15 seconds for the radio to detach
const uint32_t duAttachTimeout = 30000; // wait up to 30 seconds for the radio to attach
const int dMaxAttempts = 5; // the radio makes 5 attempts to update the firmware
for (int i = 0; i < dMaxAttempts; i++) {
printInfo("Waiting for the radio to enter recovery mode");
callNextStep(stepCb, "FUMO Info: waiting for the radio to enter recovery mode");
// Wait for new FOTA responses. Exits preliminary if radio detached.
sResponse = waitResponse(vFotaBailStrings, duDetachTimeout);
printTrace("Radio response: [%s]", sResponse.c_str());
if (i == 0 && sResponse.find(sFotaUrcPrefix) != std::string::npos) {
// FOTA responce code received before radio detach, image can't be applied.
// EG25-G radio prints a message in +QIND: "FOTA",ERR_CODE format in this case.
std::string sErrorCode = getFumoEarlyErrorCode(sResponse);
printError("Preliminary termination of FUMO procedure: [%s]", sErrorCode.c_str());
callNextStep(stepCb, "FUMO Error: radio returned error code " + sErrorCode);
// We got a response from the radio but FOTA failed
rc = FAILURE;
break;
}
// It's now detached. Try to reconnect
if (!resetConnection(duAttachTimeout)) {
printError("Can't connect to the radio in %d ms", (duAttachTimeout));
callNextStep(stepCb, "FUMO Error: unable to obtain radio after reset");
break;
}
// It's now back on the bus. Wait for the URC messages.
printInfo("Applying the radio firmware");
callNextStep(stepCb, "FUMO Info: applying the radio firmware");
rc = fumoWaitUpgradeFinished(stepCb);
if (rc == ERROR) {
// unrecoverable error
callNextStep(stepCb, "FUMO Error: failed to apply the firmware, consider radio reset");
break;
}
if (rc != SUCCESS) {
// attempt failed, radio reboots and starts its next attempt
printError("Failed to apply the firmware, attempts left: %d", (dMaxAttempts - i - 1));
callNextStep(stepCb, "FUMO Error: failed to apply the firmware");
continue;
}
// Wait for the radio to finish update and reboot
printTrace("Waiting for the radio to come up");
callNextStep(stepCb, "FUMO Info: waiting for the radio to enter normal mode");
rc = fumoWaitNewFirmware(stepCb);
break;
}
if (rc == SUCCESS) {
printInfo("Radio firmware applied successfully");
callNextStep(stepCb, "FUMO Done: radio firmware applied successfully");
} else {
printError("Radio firmware has not been updated");
callNextStep(stepCb, "FUMO Error: radio firmware has not been updated");
}
return rc;
}
ICellularRadio::CODE QuectelRadio::getServiceDomain(ICellularRadio::SERVICEDOMAIN& sd) {
printTrace("%s| Get Service Domain", getName().c_str());
std::string sCmd("AT+QCFG=\"servicedomain\"");
std::string sResult = sendCommand(sCmd);
size_t end = sResult.find(ICellularRadio::RSP_OK);
if (end == std::string::npos) {
printWarning("%s| Unable to get service domain using command [%s]", getName().c_str(), sCmd.c_str());
return FAILURE;
}
// +QCFG: "servicedomain",
size_t start = sResult.find(",") + 1; // Position right after comma
std::string sServiceDomain = MTS::Text::trim(sResult.substr(start, end-start));
int iValue = -1;
if (!MTS::Text::parse(iValue, sServiceDomain)) {
printWarning("%s| Failed to parse service domain from command output [%s]", getName().c_str(), sCmd.c_str());
return FAILURE;
}
switch (iValue) {
case 0: sd = SERVICEDOMAIN::CS_ONLY; break;
case 1: sd = SERVICEDOMAIN::PS_ONLY; break;
case 2: sd = SERVICEDOMAIN::CSPS; break;
default: return FAILURE; // Unknown
}
return SUCCESS;
}
ICellularRadio::CODE QuectelRadio::getIsSimInserted(bool& bData) {
printTrace("%s| Get SIM insertion status", getName().c_str());
// AT+QSIMSTAT? execution can take up to 300ms according to the datasheet. Setting timeout to 500ms just for sure.
std::string sCmd("AT+QSIMSTAT?");
std::string sResult = sendCommand(sCmd, DEFAULT_BAIL_STRINGS, 500);
const std::string sPrefix = "+QSIMSTAT: ";
size_t start = sResult.find(sPrefix);
size_t end = sResult.rfind(ICellularRadio::RSP_OK);
if (end == std::string::npos) {
printWarning("%s| Unable to get SIM insertion status from radio using command [%s]", getName().c_str(), sCmd.c_str());
return FAILURE;
}
if (start == std::string::npos) {
printDebug("%s| AT+QSIMSTAT? returned unexpected response: [%s][%s]", getName().c_str(), sCmd.c_str(), sResult.c_str());
return FAILURE;
}
// +QSIMSTAT: ,
start += sPrefix.size();
std::vector vParts = MTS::Text::split(MTS::Text::trim(sResult.substr(start, end-start)), ',');
if(vParts.size() != 2) {
printWarning("%s| Unable to parse SIM insertion status from response [%s]", getName().c_str(), sResult.c_str());
return FAILURE;
}
if (vParts[1] == "1") { // Inserted
bData = true;
} else { // Removed or Unknown, before (U)SIM initialization
bData = false;
}
return SUCCESS;
}
ICellularRadio::CODE QuectelRadio::getSimLockAttempts(int& iAttemptsPin, int& iAttemptsPuk) {
printTrace("%s| Get SIM unlock attempts left", getName().c_str());
// AT+QPINC execution can take more time that expected. Set timeout to 2s just to be sure.
std::string sCmd("AT+QPINC=\"SC\"");
std::string sResult = sendCommand(sCmd, DEFAULT_BAIL_STRINGS, 2000);
const std::string sPrefix = "+QPINC: \"SC\",";
size_t start = sResult.find(sPrefix);
size_t end = sResult.rfind(ICellularRadio::RSP_OK);
if (end == std::string::npos) {
printWarning("%s| Unable to get SIM unlock attempts from radio using command [%s]", getName().c_str(), sCmd.c_str());
return FAILURE;
}
if (start == std::string::npos) {
printDebug("%s| AT+QPINC returned unexpected response: [%s][%s]", getName().c_str(), sCmd.c_str(), sResult.c_str());
return FAILURE;
}
// +QPINC: ,,
// [x] ,[0] ,[1]
start += sPrefix.size();
std::vector vParts = MTS::Text::split(MTS::Text::trim(sResult.substr(start, end-start)), ',');
if(vParts.size() != 2) {
printWarning("%s| Unable to parse SIM unlock attempts left from response [%s]", getName().c_str(), sResult.c_str());
return FAILURE;
}
if (!MTS::Text::parse(iAttemptsPin, vParts[0])) {
printWarning("%s| Unable to parse SIM PIM unlock attempts from response [%s]", getName().c_str(), sResult.c_str());
return FAILURE;
}
if (!MTS::Text::parse(iAttemptsPuk, vParts[1])) {
printWarning("%s| Unable to parse SIM PUK unlock attempts from response [%s]", getName().c_str(), sResult.c_str());
return FAILURE;
}
return SUCCESS;
}
ICellularRadio::CODE QuectelRadio::convertToActiveBand(const std::string& sQuectelBand, ICellularRadio::ACTIVEBAND& band) {
int iQuectelBand = -1;
if (!MTS::Text::parse(iQuectelBand, sQuectelBand)) {
return FAILURE; // probably "-", other band
}
switch (iQuectelBand) {
case 0: band = ACTIVEBAND::DCS_1800; break;
case 1: band = ACTIVEBAND::PCS_1900; break;
default: return FAILURE; // actually, this case should never happen
}
return SUCCESS;
}
ICellularRadio::CODE QuectelRadio::setCellularMode(CELLULAR_MODES networks) {
std::string prefNet;
unsigned int prefOnly = 0, prefCount = 0;
for (int i = sizeof(networks)*CHAR_BIT-1; i>=0; --i) {
switch (1<(1), (1024 / FILE_CHUNK_SIZE));
const size_t nAcksPerChunk = std::max(static_cast(1), (FILE_CHUNK_SIZE / 1024));
const std::string sAckString(nAcksPerChunk, 'A');
size_t dPayloadLength;
size_t nChunks;
CODE rc;
rc = getFileSize(fd, dPayloadLength);
if (rc != SUCCESS) {
return rc;
}
rc = sizeToChunks(dPayloadLength, FILE_CHUNK_SIZE, nChunks);
if (rc != SUCCESS) {
return rc;
}
printTrace("File size: %d bytes and %d chunks", dPayloadLength, nChunks);
printTrace("Starting file upload...");
// Start file upload, set transmission timeouts and enable ACK mode
rc = startFileUpload(sTargetFilename, dPayloadLength, uFileTimeout, true);
if (rc != SUCCESS) {
return rc;
}
printTrace("File upload started.");
callNextStep(stepCb, "FILE Info: Started file upload for " + sTargetFilename);
uint16_t dChecksum = 0;
size_t nChunksPerCent = (nChunks / 100) + 1;
size_t nFragmentLength = 0;
std::array vBuffer;
std::string sResponse;
// ACK waiter callback - wait for ACK and populate result code
IsNeedMoreData waitAck = [&rc, &sAckString](const std::string& /*iterationData*/, const std::string& allData)->bool {
if (allData.find(sAckString) != std::string::npos) {
// No data needed
rc = SUCCESS;
return false;
}
// We need more data
rc = NO_RESPONSE;
return true;
};
for (size_t iChunk = 0; iChunk < nChunks; iChunk++) {
rc = readChunk(fd, vBuffer.data(), vBuffer.size(), nFragmentLength);
if (rc != SUCCESS) {
break;
}
// we got our fragment, calculate checksum and flush the data
uint16_t dFragmentChecksum = getQuectelChecksum(vBuffer.data(), nFragmentLength);
updateQuectelChecksum(dChecksum, dFragmentChecksum);
rc = sendData(vBuffer.data(), nFragmentLength);
if (rc != SUCCESS) {
// failed to send data
break;
}
// Every 1024 bytes
if ((iChunk) && (iChunk % nChunksPerAck == 0)) {
// Wait for ACK for up to 1 second and populate rc variable
sResponse = waitResponse(waitAck, 1000);
if (rc != SUCCESS) {
// timeout or end of execution - check later
break;
}
}
if (stepCb && ((iChunk % nChunksPerCent) == 0)) {
size_t dPercentsCompleted = iChunk / nChunksPerCent;
callNextStep(stepCb, "FILE Info: Uploaded " + MTS::Text::format(dPercentsCompleted) + "%");
}
}
printTrace("Waiting for acknoledge from the radio");
std::string sExpectedResult = "+QFUPL: ";
sExpectedResult += MTS::Text::format(dPayloadLength);
sExpectedResult += ",";
sExpectedResult += MTS::Text::toLowerCase(MTS::Text::formatHex(dChecksum));
// Wait for confirmation of successful upload completion
sResponse += waitResponse(DEFAULT_BAIL_STRINGS, iUploadResultTimeout);
if (sResponse.find(sExpectedResult) != std::string::npos) {
printDebug("Radio returned: [%s]", sResponse.c_str());
printTrace("Upload finished, checksum matched");
callNextStep(stepCb, "FILE Info: Upload finished successfully");
return SUCCESS;
}
printError("Upload failed: checksum mismatch. Expected: [%s], Actual: [%s]", sExpectedResult.c_str(), sResponse.c_str());
callNextStep(stepCb, "FILE Error: Upload failed due to internal error");
abortFileUpload();
return FAILURE;
}
ICellularRadio::CODE QuectelRadio::removeFile(const std::string& sTargetFilename) {
printTrace("Removing file [%s] from the radio memory", sTargetFilename.c_str());
const int dTimeout = 1000; //ms
const std::string sCmd = "AT+QFDEL=\"" + sTargetFilename + "\"";
std::string sResult = sendCommand(sCmd, ICellularRadio::DEFAULT_BAIL_STRINGS, dTimeout);
if (sResult.find(ICellularRadio::RSP_OK) == std::string::npos) {
printError("Failed to remove file [%s]: [%s]", sTargetFilename.c_str(), sResult.c_str());
return FAILURE;
}
printTrace("File [%s] removed", sTargetFilename.c_str());
return SUCCESS;
}
ICellularRadio::CODE QuectelRadio::checkFile(bool& bIsFilePresent, const std::string& sTargetFilename) {
printTrace("Checking status of the [%s] file", sTargetFilename.c_str());
const int dTimeout = 1000; //ms
const std::string sCmd = "AT+QFLST"; // list all files in the UFS memory
std::string sResult = sendCommand(sCmd, ICellularRadio::DEFAULT_BAIL_STRINGS, dTimeout);
if (sResult.rfind(ICellularRadio::RSP_OK) == std::string::npos) {
printError("Unable to list files from the radio memory: [%s]", sResult.c_str());
return FAILURE;
}
// UFS files in +QFLST output may or may not have a "UFS:" prefix
const std::string sExpectedFull = "+QFLST: \"UFS:" + sTargetFilename + "\"";
const std::string sExpectedAlt = "+QFLST: \"" + sTargetFilename + "\"";
// File is present if its name is present with or without "UFS:" prefix in QFLST output
bool bIsPresentFull = (sResult.find(sExpectedFull) != std::string::npos);
bool bIsPresentAlt = (sResult.find(sExpectedAlt) != std::string::npos);
bIsFilePresent = (bIsPresentFull || bIsPresentAlt);
return SUCCESS;
}
uint16_t QuectelRadio::getQuectelChecksum(const void* data, size_t nBytes) {
auto castData = static_cast(data);
uint16_t iChecksum = 0;
for (size_t i = 0; i < nBytes; i += 2) {
// If the number of the characters is odd, set the last character as the high 8 bit, and the low 8 bit as 0,
// and then use an XOR operator to calculate the checksum.
bool bHasLowByte = (i + 1 < nBytes);
uint8_t high = castData[i];
uint8_t low = bHasLowByte ? (castData[i+1]) : (0);
uint16_t iFragment = bytesToUint16(high, low);
updateQuectelChecksum(iChecksum, iFragment);
}
return iChecksum;
}
ICellularRadio::CODE QuectelRadio::fumoWaitUpgradeFinished(ICellularRadio::UpdateCb& stepCb) {
const uint32_t duUrcTimeout = 4 * 60 * 1000; // wait for 4 minutes for the next URC message
const uint32_t duAttachTimeout = 30 * 1000; // wait up to 30 seconds for the radio to attach
const std::string sFotaUrcPrefix = "+QIND: \"FOTA\""; // prefix for the URC notification messages
const std::string sFotaUrcStart = "\"START\"";
const std::string sFotaUrcProgress = "\"UPDATING\"";
const std::string sFotaUrcEnd = "\"END\"";
const std::vector vFotaBailStrings{ sFotaUrcPrefix };
bool bFinished = false;
CODE rc = FAILURE;
std::string sResponse;
while (!bFinished) { // breaks on "FOTA","END"
sResponse = waitResponse(vFotaBailStrings, duUrcTimeout);
printTrace("Radio response: [%s]", sResponse.c_str());
if (sResponse.empty()) {
// Radio detached again. Try to reconnect
if (!resetConnection(duAttachTimeout)) {
printError("Can't connect to the radio in %d ms", (duAttachTimeout));
callNextStep(stepCb, "FUMO Error: unable to obtain radio after second reset");
return ERROR;
}
sResponse = waitResponse(vFotaBailStrings, duUrcTimeout);
printTrace("Radio response: [%s]", sResponse.c_str());
}
if (sResponse.find(sFotaUrcPrefix) == std::string::npos) {
printError("No URC messages from the radio in %d ms", duUrcTimeout);
callNextStep(stepCb, "FUMO Error: timeout, radio is not responding");
rc = ERROR;
break;
}
// Occasionally sendCommand returns multiple lines in one chunk.
// Handle each line separately for such cases.
const auto vLines = MTS::Text::split(sResponse, '\r');
for (const auto& sLine : vLines) {
const auto& sTrimmedLine = MTS::Text::trim(sLine);
if (sTrimmedLine.empty()) {
// whitespace characters only, ignore
continue;
}
printTrace("Processing line: [%s]", sTrimmedLine.c_str());
if (sTrimmedLine.find(sFotaUrcPrefix) == std::string::npos) {
printDebug("URC message not found, line skipped");
continue;
}
const auto vParts = MTS::Text::split(sTrimmedLine, ',', 3);
const std::string& sStage = getByIndex(vParts, 1, "NOT_DEFINED");
if (sStage == sFotaUrcEnd) {
// FOTA finished
printTrace("Got FOTA END message");
const std::string& sCode = getByIndex(vParts, 2, "-1");
if (sCode == "0") {
// finished successfully
rc = SUCCESS;
bFinished = true;
break;
}
// attempt failed, the radio attempts to recover
callNextStep(stepCb, "FUMO Error: radio returned error code " + sCode);
bFinished = true;
break;
} else if (sStage == sFotaUrcStart) {
printTrace("Got FOTA START message");
} else if (sStage == sFotaUrcProgress) {
printTrace("Got FOTA progress message");
const std::string& sPercents = getByIndex(vParts, 2, "0");
printInfo("FOTA progress: [%s]", sPercents.c_str());
callNextStep(stepCb, "FUMO Info: firmware apply progress " + sPercents);
} else {
printInfo("FOTA unexpected URC code: [%s]", sLine.c_str());
}
}
}
return rc;
}
ICellularRadio::CODE QuectelRadio::fumoWaitNewFirmware(ICellularRadio::UpdateCb& stepCb) {
MTS::Timer oTimer;
oTimer.start();
std::string sQuectelFirmware;
CODE rc = ERROR;
while (oTimer.getSeconds() < (5 * 60)) { // 5 minutes
MTS::Thread::sleep(10000);
// sendBasicCommand "eats" and clears all the extra data present in the buffer,
// both commands shall succeed
if (sendBasicCommand("AT") != SUCCESS || getVendorFirmware(sQuectelFirmware) != SUCCESS) {
// The radio is probably unavailable
resetConnection(100);
continue;
}
printInfo("Firmware version before the upgrade: %s", m_sQuectelFirmware.c_str());
printInfo("Current firmware version: %s", sQuectelFirmware.c_str());
if (sQuectelFirmware == m_sQuectelFirmware) {
// Radio will not reset anymore, firmware version left the same, not updated
printError("Radio firmware version not changed after upgrade");
rc = FAILURE;
break;
}
// The firmware numbers have changed
rc = SUCCESS;
break;
}
oTimer.stop();
if (rc == ERROR) {
printError("Radio is not responding");
callNextStep(stepCb, "FUMO Error: unable to obtain radio after reset");
}
return rc;
}
std::string QuectelRadio::getFumoEarlyErrorCode(const std::string& sRadioInput) {
const std::string sFotaPrefix = "+QIND: \"FOTA\",";
const char cLineEnd = ICellularRadio::CR;
// sRadioInput may contain several lines. We need to find the line with '+QIND: "FOTA",' in it
auto pLineStart = sRadioInput.find(sFotaPrefix);
if (pLineStart == std::string::npos) {
// FOTA line not found at all
return "-1";
}
// Parse the error code
auto pErrorStart = pLineStart + sFotaPrefix.length();
auto pLineEnd = sRadioInput.find(cLineEnd, pErrorStart);
// Filter the error code
std::string sSubString = sRadioInput.substr(pErrorStart, pLineEnd - pErrorStart);
std::string sResult = MTS::Text::trim(sSubString);
return sResult;
}
ICellularRadio::CODE QuectelRadio::startFileUpload(const std::string& sTargetFilename, size_t nBytes, uint16_t uRxTimeout, bool bAckEnabled) {
const std::vector vBailStrings{ ICellularRadio::RSP_CONNECT, ICellularRadio::RSP_ERROR };
const int dTimeout = 1000; //ms
std::string sCommand, sResult;
// Format: AT+QFUPL=[,[,[,]]
sCommand = "AT+QFUPL=\"";
sCommand += sTargetFilename;
sCommand += "\",";
sCommand += MTS::Text::format(nBytes);
sCommand += ",";
sCommand += MTS::Text::format(uRxTimeout);
if (bAckEnabled) {
// ACK mode enabled
sCommand += ",1";
}
sResult = sendCommand(sCommand, vBailStrings, dTimeout);
if (sResult.find(ICellularRadio::RSP_CONNECT) == std::string::npos) {
printError("Radio is not ready to accept the file: [%s]", sResult.c_str());
return FAILURE;
}
return SUCCESS;
}
ICellularRadio::CODE QuectelRadio::abortFileUpload() {
/*
* To prevent the “+++” from being mistaken for data, the following sequence should be followed:
* 1) Do not input any character within 1s or longer before inputting “+++”.
* 2) Input “+++” within 1s, and no other characters can be inputted during the time.
* 3) Do not input any character within 1s after “+++” has been inputted.
*/
sleep(1);
return sendBasicCommand(CMD_ABORT_UPLOAD, 2000, 0x00);
}
ICellularRadio::CODE QuectelRadio::setUeModeOfOperation(ICellularRadio::UE_MODES_OF_OPERATION mode) {
printTrace("%s| Set UE Mode Of Operation", getName().c_str());
std::string sDomain, sPreference;
switch (mode) {
case ICellularRadio::UE_MODES_OF_OPERATION::CS_MODE1:
sDomain = "0";
sPreference = "00";
break;
case ICellularRadio::UE_MODES_OF_OPERATION::CS_MODE2:
sDomain = "0";
sPreference = "01";
break;
case ICellularRadio::UE_MODES_OF_OPERATION::PS_MODE1:
sDomain = "1";
sPreference = "00";
break;
case ICellularRadio::UE_MODES_OF_OPERATION::PS_MODE2:
sDomain = "1";
sPreference = "01";
break;
case ICellularRadio::UE_MODES_OF_OPERATION::CS_PS_MODE1:
sDomain = "2";
sPreference = "00";
break;
case ICellularRadio::UE_MODES_OF_OPERATION::CS_PS_MODE2:
sDomain = "2";
sPreference = "01";
break;
default:
printError("%s| Set UE Mode Of Operation: invalid argument", getName().c_str());
return INVALID_ARGS;
}
CODE rc;
const int dTimeout = 1000; // ms
std::string sCommand = "AT+QNVFW=\"/nv/item_files/modem/mmode/ue_usage_setting\"," + sPreference;
rc = sendBasicCommand(sCommand, dTimeout);
if (rc != SUCCESS) {
printError("%s| Voice/data preference configuration failed with code [%d]", getName().c_str(), rc);
return rc;
}
sCommand = "AT+QCFG=\"servicedomain\"," + sDomain + ",0";
rc = sendBasicCommand(sCommand, dTimeout);
if (rc != SUCCESS) {
printError("%s| Service domain configuration failed with code [%d]", getName().c_str(), rc);
return rc;
}
return SUCCESS;
}
ICellularRadio::CODE QuectelRadio::getUeUsageSetting(QuectelRadio::UE_USAGE_SETTING& us) {
printTrace("%s| Get UE Usage Setting", getName().c_str());
std::string sCmd("AT+QNVFR=\"/nv/item_files/modem/mmode/ue_usage_setting\"");
std::string sResult = sendCommand(sCmd);
size_t end = sResult.find(ICellularRadio::RSP_OK);
if (end == std::string::npos) {
printError("%s| Unable to get UE Usage Setting [%s]", getName().c_str(), sResult.c_str());
return FAILURE;
}
// +QNVFR:
const std::string sLabel = "+QNVFR: ";
size_t start = sResult.find(sLabel);
if (start == std::string::npos) {
printError("%s| Failed to parse UE Usage Setting from output [%s]", getName().c_str(), sResult.c_str());
return FAILURE;
}
start += sLabel.length();
const std::string sPreference = MTS::Text::trim(sResult.substr(start, end - start));
if (convertToUeUsageSetting(sPreference, us) != SUCCESS) {
printError("%s| Unable to convert [%s] to UE Usage Setting", getName().c_str(), sPreference.c_str());
return FAILURE;
}
return SUCCESS;
}
ICellularRadio::CODE QuectelRadio::convertToUeUsageSetting(const std::string& sSetting, QuectelRadio::UE_USAGE_SETTING& us) {
if (sSetting == "00") {
us = QuectelRadio::UE_USAGE_SETTING::MODE_1;
return SUCCESS;
}
if (sSetting == "01") {
us = QuectelRadio::UE_USAGE_SETTING::MODE_2;
return SUCCESS;
}
us = QuectelRadio::UE_USAGE_SETTING::UNKNOWN_MODE;
return FAILURE;
}
ICellularRadio::CODE QuectelRadio::getUeModeOfOperation(ICellularRadio::UE_MODES_OF_OPERATION& mode) {
printTrace("%s| Get UE Mode Of Operation", getName().c_str());
SERVICEDOMAIN sd;
UE_USAGE_SETTING us;
if (getServiceDomain(sd) != SUCCESS) {
return FAILURE;
}
printTrace("%s| Retrieved servicedomain [%d]", getName().c_str(), sd);
if (getUeUsageSetting(us) != SUCCESS) {
return FAILURE;
}
printTrace("%s| Retrieved ue_usage_setting [%d]", getName().c_str(), us);
if (sd == ICellularRadio::SERVICEDOMAIN::CS_ONLY && us == QuectelRadio::UE_USAGE_SETTING::MODE_1) {
mode = ICellularRadio::UE_MODES_OF_OPERATION::CS_MODE1;
return SUCCESS;
}
if (sd == ICellularRadio::SERVICEDOMAIN::CS_ONLY && us == QuectelRadio::UE_USAGE_SETTING::MODE_2) {
mode = ICellularRadio::UE_MODES_OF_OPERATION::CS_MODE2;
return SUCCESS;
}
if (sd == ICellularRadio::SERVICEDOMAIN::PS_ONLY && us == QuectelRadio::UE_USAGE_SETTING::MODE_1) {
mode = ICellularRadio::UE_MODES_OF_OPERATION::PS_MODE1;
return SUCCESS;
}
if (sd == ICellularRadio::SERVICEDOMAIN::PS_ONLY && us == QuectelRadio::UE_USAGE_SETTING::MODE_2) {
mode = ICellularRadio::UE_MODES_OF_OPERATION::PS_MODE2;
return SUCCESS;
}
if (sd == ICellularRadio::SERVICEDOMAIN::CSPS && us == QuectelRadio::UE_USAGE_SETTING::MODE_1) {
mode = ICellularRadio::UE_MODES_OF_OPERATION::CS_PS_MODE1;
return SUCCESS;
}
if (sd == ICellularRadio::SERVICEDOMAIN::CSPS && us == QuectelRadio::UE_USAGE_SETTING::MODE_2) {
mode = ICellularRadio::UE_MODES_OF_OPERATION::CS_PS_MODE2;
return SUCCESS;
}
printError("%s| Unknown combination of servicedomain [%d] and ue_usage_setting [%d]", getName().c_str(), sd, us);
mode = ICellularRadio::UE_MODES_OF_OPERATION::UNKNOWN_MODE;
return FAILURE;
}
ICellularRadio::CODE QuectelRadio::disableVoiceSupport() {
printTrace("%s| Disable Voice Support", getName().c_str());
CODE rc;
const int dTimeout = 1000; // ms
std::string sCommand = "AT+QNVFW=\"/nv/item_files/ims/IMS_enable\",00";
rc = sendBasicCommand(sCommand, dTimeout);
if (rc != SUCCESS) {
printError("%s| Failed to disable IMS support: [%d]", getName().c_str(), rc);
return rc;
}
sCommand = "AT+QNVW=5280,0,\"0102000000000000\"";
rc = sendBasicCommand(sCommand, dTimeout);
if (rc != SUCCESS) {
printError("%s| Failed to disable voice support via +QNVW=5280,0: [%d]", getName().c_str(), rc);
return rc;
}
sCommand = "AT+QNVFW=\"/nv/item_files/modem/mmode/sms_only\",01";
rc = sendBasicCommand(sCommand, dTimeout);
if (rc != SUCCESS) {
printError("%s| Failed to enable \"SMS only\" registration flag: [%d]", getName().c_str(), rc);
return rc;
}
return SUCCESS;
}
ICellularRadio::CODE QuectelRadio::getVoiceSupport(bool& bVoiceEnabled, bool& bSmsOnly) {
printTrace("%s| Get Voice Support status", getName().c_str());
CODE rc;
bool bImsEnabled = false;
const int dTimeout = 1000; // ms
std::string sResult;
std::string sCommand;
std::string sLabel;
bVoiceEnabled = false;
bSmsOnly = false;
printTrace("%s| Get IP Multimedia Subsystem status", getName().c_str());
sCommand = "AT+QNVFR=\"/nv/item_files/ims/IMS_enable\"";
sLabel = "+QNVFR: ";
rc = sendBasicQuery(sCommand, sLabel, sResult, dTimeout);
if (rc != SUCCESS) {
return rc;
}
if (sResult != "00") {
bImsEnabled = true;
}
printTrace("%s| Get +QNVR=5280,0 status", getName().c_str());
sCommand = "AT+QNVR=5280,0";
sLabel = "+QNVR: ";
rc = sendBasicQuery(sCommand, sLabel, sResult, dTimeout);
if (rc != SUCCESS) {
return rc;
}
sResult = MTS::Text::strip(sResult, '"');
if (bImsEnabled || sResult != VALUE_QNVR_DISABLE_VOICE) {
bVoiceEnabled = true;
}
printTrace("%s| Get \"SMS only\" registration status", getName().c_str());
sCommand = "AT+QNVFR=\"/nv/item_files/modem/mmode/sms_only\"";
sLabel = "+QNVFR: ";
rc = sendBasicQuery(sCommand, sLabel, sResult, dTimeout);
if (rc != SUCCESS) {
return rc;
}
if (sResult == "01") {
bSmsOnly = true;
}
return SUCCESS;
}
ICellularRadio::CODE QuectelRadio::getSelectedBandsRaw(std::string& sRawBands) {
printTrace("%s| Acquiring selected bands", getName().c_str());
CODE rc;
const std::string sCommand = "AT+QCFG=\"band\"";
const std::string sLabel = "+QCFG: \"band\",";
const int dTimeout = 1000;
std::string sResult;
rc = sendBasicQuery(sCommand, sLabel, sResult, dTimeout);
if (rc != SUCCESS) {
return rc;
}
std::vector vParts = MTS::Text::split(sResult, ',');
uint8_t iNumBandParams = 0;
if (vParts.size() > 0) {
uint16_t iSelectedBands = 0;
// Duplicate the value to the first two fields
// contains information for both the GSM and WCDMA bands
if (!isContainsSignChar(vParts[0]) && MTS::Text::parseHex(iSelectedBands, MTS::Text::trim(vParts[0])) ) {
sRawBands = MTS::Text::formatHex(iSelectedBands) + "," + MTS::Text::formatHex(iSelectedBands);
iNumBandParams++;
} else {
printWarning("%s| Error during parse number from string: [%s]. Assuming that no GSM and WCDMA bands selected", getName().c_str(), vParts[0].c_str());
sRawBands = "ffff,ffff";
}
} else {
sRawBands = "ffff,ffff";
}
if (vParts.size() > 1) {
uint64_t iSelectedBands = 0;
if (!isContainsSignChar(vParts[1]) && MTS::Text::parseHex(iSelectedBands, MTS::Text::trim(vParts[1]))) {
sRawBands += "," + MTS::Text::formatHex(iSelectedBands); // LTE bands
iNumBandParams++;
} else {
printWarning("%s| Error during parse number from string: [%s]. Assuming that no LTE bands selected", getName().c_str(), vParts[0].c_str());
sRawBands += ",ffffffffffffffff";
}
} else {
sRawBands += ",ffffffffffffffff";
}
// All other fragments - ignored for now.
// Return success if at least one band param was extracted; otherwise failure
return (iNumBandParams > 0) ? SUCCESS : FAILURE;
}
bool MTS::IO::QuectelRadio::isContainsSignChar(const std::string& str) {
if (str.find_first_of("+-") == std::string::npos) {
return false;
}
return true;
}
ICellularRadio::CODE QuectelRadio::isDivctlSupported(bool& bSupported) {
const std::string sCommand = "AT+QCFG=\"divctl\"";
const std::string sLabel = "+QCFG: \"divctl\",";
const int dTimeout = 1000; // ms
std::string sResult;
CODE rc;
rc = sendBasicQuery(sCommand, sLabel, sResult, dTimeout);
if (rc == ERROR) {
bSupported = false;
return SUCCESS;
} else if (rc == SUCCESS) {
if (sResult.find("(\"lte\",\"wcdma\")") != std::string::npos) {
bSupported = true;
} else {
bSupported = false;
}
return rc;
} else {
return rc;
}
}
ICellularRadio::CODE QuectelRadio::setRxDiversity(const Json::Value& jArgs) {
if (jArgs["enabled"].asString() != "1" && jArgs["enabled"].asString() != "0") {
return FAILURE;
}
ICellularRadio::CODE rc;
std::string sCmd;
bool bSupported;
rc = isDivctlSupported(bSupported);
if (rc != SUCCESS) {
printError("%s| Failed to determine the AT+QCFG=\"divctl\" support: [%d]", getName().c_str(), rc);
return rc;
}
if (!bSupported) {
/* Old command string for EG95 radios: AT+QCFG="diversity",(0-1) */
sCmd = "AT+QCFG=\"diversity\",";
sCmd += jArgs["enabled"].asString();
return sendBasicCommand(sCmd);
}
std::string sValue;
/* Reverse obtained value because the new command string works in the following way:
0 - enable,
1 - disable, use the main antenna as the only antenna.
2 - disable, use the diversity antenna as the only antenna.
*/
if (jArgs["enabled"].asString() == "1") {
sValue = "0";
} else {
sValue = "1";
}
/* New command string for EG95 radios:
AT+QCFG=\"divctl\",\"lte\",(0-2)
AT+QCFG=\"divctl\",\"wcdma\",(0-2)
*/
const int dTimeout = 1000; // ms
sCmd = "AT+QCFG=\"divctl\",\"lte\",";
sCmd += sValue;
rc = sendBasicCommand(sCmd, dTimeout);
if (rc != SUCCESS) {
printError("%s| Failed to set diversity for LTE network mode: [%d]", getName().c_str(), rc);
return rc;
}
sCmd = "AT+QCFG=\"divctl\",\"wcdma\",";
sCmd += sValue;
rc = sendBasicCommand(sCmd, dTimeout);
if (rc != SUCCESS) {
printError("%s| Failed to set diversity for WCDMA network mode: [%d]", getName().c_str(), rc);
return rc;
}
return SUCCESS;
}
const std::vector& QuectelRadio::getDiagCommands(bool) {
// Declare as static to initialize only when used, but cache the results.
const static std::vector vCommands {
// Radio model and firmware:
"AT+CGMI", "AT+CGMM", "AT+CGMR", "AT+QGMR",
// All carrier profiles that are supported:
"AT+QMBNCFG=\"LIST\"",
// Current operator profile on the radio side:
"AT+QMBNCFG=\"SELECT\"", "AT+CGSN",
// SIM card information:
"AT+QSIMSTAT?", "AT+QCCID", "AT+CPIN?", "AT+QPINC=\"SC\"",
// Operating mode of the radio:
"AT+CFUN?",
// Cellular Mode (RAT selection):
"AT+QCFG=\"nwscanseq\"", "AT+QCFG=\"nwscanmode\"",
// Cellular Diversity configuration:
"AT+QCFG=\"divctl\",\"lte\"", "AT+QCFG=\"divctl\",\"wcdma\"", "AT+QCFG=\"diversity\"",
// Voice call support (AT&T, T-Mobile):
"AT+QNVFR=\"/nv/item_files/ims/IMS_enable\"",
"AT+QNVFR=\"/nv/item_files/modem/mmode/sms_only\"",
"AT+QNVR=5280,0",
// UE Mode of Operation (CEMODE; AT&T):
"AT+QCFG=\"servicedomain\"",
"AT+QNVFR=\"/nv/item_files/modem/mmode/ue_usage_setting\"",
// Data connection configuration:
"AT+CGDCONT?", "AT+QICSGP=1", "AT+QICSGP=2", "AT+QICSGP=3",
// Registration and connection to the tower:
"AT+CIND?", "AT+CSQ", "AT+COPS?", "AT+CREG?", "AT+CGREG?", "AT+CEREG?",
"AT+QENG=\"servingcell\"",
// Data connection status:
"AT+CGACT?", "AT+CGCONTRDP=1", "AT+CGCONTRDP=2", "AT+CGCONTRDP=3"
};
return vCommands;
}
ICellularRadio::CODE QuectelRadio::setTimeFormat() {
printTrace("%s| Set standard time format", getName().c_str());
ICellularRadio::CODE rc;
// Set year format in YYYY first, in case it is in YY format to get accurate year
std::string sCmdCSDF("AT+CSDF=1,2");
rc = sendBasicCommand(sCmdCSDF);
if (rc != SUCCESS) {
printError("%s| Unable to set year format for radio using command [%s]", getName().c_str(), sCmdCSDF.c_str());
return rc;
}
// Set command enables the automatic time zone update
std::string sCmdCTZU("AT+CTZU=1");
rc = sendBasicCommand(sCmdCTZU);
if (rc != SUCCESS) {
printError("%s| Unable to set automatic time zone update for radio using command [%s]", getName().c_str(), sCmdCTZU.c_str());
return rc;
}
return SUCCESS;
}