/* * 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; } ICellularRadio::CODE QuectelRadio::convertStringToPdpContextType(const std::string& sStringType, std::string& sIntType) { CODE rc = FAILURE; if ("IP" == sStringType) { sIntType = "1"; rc = SUCCESS; } else if ("IPV6" == sStringType) { sIntType = "2"; rc = SUCCESS; } else if ("IPV4V6" == sStringType) { sIntType = "3"; rc = SUCCESS; } return rc; } std::vector QuectelRadio::getSupportedPdpContextAuthTypes() const { return { VALUE_PDP_CONTEXT_AUTH_TYPE_NONE, VALUE_PDP_CONTEXT_AUTH_TYPE_PAP, VALUE_PDP_CONTEXT_AUTH_TYPE_CHAP, VALUE_PDP_CONTEXT_AUTH_TYPE_PAP_CHAP }; } ICellularRadio::CODE QuectelRadio::isPdpContextAuthSupported(bool& isSupported) { std::string sCmd("AT+QICSGP=?"); return isCommandSupported(sCmd, isSupported); } ICellularRadio::CODE QuectelRadio::fillPdpContextAuthFields(Json::Value& jData) { CODE rc; std::string sCmd("AT+QICSGP="); std::string sResult; uint8_t iValue; // iterate over PDP contex IDs to get context settings for (std::string & sContextId : jData.getMemberNames()) { // +QICSGP: ,,,, // +QICSGP: 0,"","","",0 -- if the context does not exist rc = sendBasicQuery(sCmd+sContextId, "+QICSGP:", sResult, 300); if (SUCCESS != rc) { return rc; } // 1,"test","login","password",1 // [0] [1] [2] [3] [4] auto vAuthParams = MTS::Text::split(sResult, ","); if (vAuthParams.size() < 5) { printError("%s| Failed to parse PDP context authentication string [%s]", getName().c_str(), sResult.c_str()); return FAILURE; } if (! MTS::Text::parse(iValue, vAuthParams[4])) { printError("%s| Failed to parse PDP context authentication type [%s]", getName().c_str(), vAuthParams[4].c_str()); return FAILURE; } rc = convertPdpContextAuthTypeToString(static_cast(iValue), sResult); if (SUCCESS != rc) { return rc; } jData[sContextId][KEY_PDP_CONTEXT_AUTH_TYPE] = sResult; if (iValue != PDP_CONTEXT_AUTH_TYPE::NONE) { jData[sContextId][KEY_PDP_CONTEXT_AUTH_USERNAME] = MTS::Text::trim(vAuthParams[2], '"'); jData[sContextId][KEY_PDP_CONTEXT_AUTH_PASSWORD] = MTS::Text::trim(vAuthParams[3], '"'); } } return SUCCESS; } // AT+QICSGP=[,,[,,)[,[,]]]] ICellularRadio::CODE QuectelRadio::setPdpContextAuth(const PdpContextInfo& pdpContext) { printTrace("%s| Setting PDP context authentication to the radio", getName().c_str()); CODE rc; std::string sContextType; std::string sCmd = "AT+QICSGP="; PDP_CONTEXT_AUTH_TYPE eAuthType; rc = convertStringToPdpContextType(pdpContext.sIpMode, sContextType); if (SUCCESS != rc) { return rc; } rc = convertStringToPdpContextAuthType(pdpContext.sAuthType, eAuthType); if (SUCCESS != rc) { return rc; } sCmd += pdpContext.sId + "," + sContextType + ",\"" + pdpContext.sApn + "\""; if (PDP_CONTEXT_AUTH_TYPE::NONE == eAuthType) { sCmd += ",\"\",\"\"," + std::to_string(eAuthType); } else { sCmd += ",\"" + pdpContext.sUsername + "\",\"" + pdpContext.sPassword +"\"," + std::to_string(eAuthType); } return sendBasicCommand(sCmd); } 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=3"); 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; }