/*
* Copyright (C) 2023 by Multi-Tech Systems
*
* This file is part of libmts-io.
*
* libmts-io is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* libmts-io is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with libmts-io. If not, see .
*
*/
#include
#include
#include
#include
#include
using namespace MTS::IO;
bool SequansRadio::resetRadio(uint32_t iTimeoutMillis) {
printInfo("%s| Rebooting radio", getName().c_str());
if (sendBasicCommand("AT^RESET") == SUCCESS) {
if (iTimeoutMillis > 5000) {
MTS::Thread::sleep(5000);
iTimeoutMillis -= 5000;
}
return resetConnection(iTimeoutMillis);
}
return false;
}
ICellularRadio::CODE SequansRadio::getVendorFirmware(std::string& sVendorFirmware) {
return getFirmware(sVendorFirmware);
}
ICellularRadio::CODE SequansRadio::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+CGMM");
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 SequansRadio::getIccid(std::string& sIccid) {
printTrace("%s| Get ICCID", getName().c_str());
const int iEFiccidId = 0x2FE2;
const uint8_t iOffsetHigh = 0;
const uint8_t iOffsetLow = 0;
const uint8_t iNumBytes = 10;
CODE rc;
std::string sEFiccidContent;
rc = simAccessReadBinary(iEFiccidId, iOffsetLow, iOffsetHigh, iNumBytes, sEFiccidContent);
if (rc != SUCCESS) {
printError("%s| Failed to determine the SIM ICCID", getName().c_str());
return rc;
}
int iEFiccidLen = sEFiccidContent.size();
if (iEFiccidLen != 20) {
printError("%s| Invalid length of the raw ICCID value: expected [20], actual: [%d]", getName().c_str(), iEFiccidLen);
return FAILURE;
}
// sEFiccidContent is a raw Binary-Coded Decimal string with swapped nibbles.
for (int i = 0; i < iEFiccidLen; i+=2) {
std::string sPart = sEFiccidContent.substr(i, 2);
std::swap(sPart[0], sPart[1]);
sIccid.append(sPart);
}
printDebug("%s| Got ICCID [%s]", getName().c_str(), sIccid.c_str());
return SUCCESS;
}
ICellularRadio::CODE SequansRadio::getService(std::string& sService) {
printTrace("%s| Get Service", getName().c_str());
sService = ICellularRadio::VALUE_NOT_SUPPORTED;
std::string sCmd("AT+COPS?");
std::string sPrefix("+COPS:");
std::string sResult;
CODE rc;
rc = sendBasicQuery(sCmd, sPrefix, sResult, 100);
if (SUCCESS != rc) {
printWarning("%s| Unable to get Service from radio using command [%s]", getName().c_str(), sCmd.c_str());
return rc;
}
std::vector vParts = MTS::Text::split(sResult, ',');
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 1 : sService = "GPRS" ; break; // GSM Compact
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 8 : sService = "EGPRS" ; break; // EC-GSM-IoT
case 9 : sService = "LTE" ; break; // E-UTRAN (NB-S1 mode)
default: sService = ICellularRadio::VALUE_UNKNOWN; break;
}
printDebug("%s| Service ID: [%d][%s]", getName().c_str(), iAccessTechnology, sService.c_str());
return SUCCESS;
}
ICellularRadio::CODE SequansRadio::getNetwork(std::string& sNetwork) {
printTrace("%s| Get Network", getName().c_str());
sNetwork = ICellularRadio::VALUE_NOT_SUPPORTED;
std::string sCmd("AT+COPS?");
std::string sPrefix("+COPS:");
std::string sResult;
CODE rc;
rc = sendBasicQuery(sCmd, sPrefix, sResult, 100);
if (SUCCESS != rc) {
printWarning("%s| Unable to get network name from radio using command [%s]", getName().c_str(), sCmd.c_str());
return rc;
}
std::vector vParts = MTS::Text::split(sResult, ",");
if (vParts.size() > 3) {
const std::string sValue = vParts[2];
// +COPS: 0,2,"315010",7
// ^start ^end
size_t start = sValue.find("\"") + 1;
size_t end = sValue.find("\"", start);
sNetwork = sValue.substr(start, end-start);
} else {
sNetwork = ""; // Not connected to any network
}
return SUCCESS;
}
// AT+SQNQRCCI? -- This command queries and reports several information from camped cell.
// +SQNQRCCI: [,,,,,,,,,,]
ICellularRadio::CODE SequansRadio::getNetworkStatusSQN(Json::Value& jData, Json::Value& jDebug) {
std::string sCmd;
std::string sResult;
std::string sPrefix;
std::vector vParts;
CODE rc;
sCmd = "AT+SQNQRCCI?";
sPrefix = "+SQNQRCCI:";
rc = sendBasicQuery(sCmd, sPrefix, sResult, 200);
if (SUCCESS != rc) {
printDebug("%s| Network Status command returned unexpected response: [%s][%s]", getName().c_str(), sCmd.c_str(), sResult.c_str());
return rc;
}
// ,,,,,,,,,,
// [0] [1] [2] [3] [4] [5] [6] [7] [8] [9] [10]
vParts = MTS::Text::split(sResult, ",");
if (vParts.size() < 11) {
printDebug("%s| Network Status command reponse 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 FAILURE;
}
jData[ICellularRadio::KEY_MCC] = vParts[0];
jData[ICellularRadio::KEY_MNC] = vParts[1];
jData[ICellularRadio::KEY_CID] = vParts[2];
jData["tac"] = vParts[4];
jData[ICellularRadio::KEY_CHANNEL] = vParts[8];
jData[ICellularRadio::KEY_ABND] = "EUTRAN BAND" + vParts[10]; // concat string to convert to common format
return SUCCESS;
}
// AT+CESQ? -- Execution command returns received signal quality parameters.
// +CESQ: ,,,,,
ICellularRadio::CODE SequansRadio::getNetworkStatusSignal(Json::Value& jData, Json::Value& jDebug) {
std::string sCmd;
std::string sResult;
std::string sPrefix;
std::vector vParts;
CODE rc;
sCmd = "AT+CESQ";
sPrefix = "+CESQ:";
rc = sendBasicQuery(sCmd, sPrefix, sResult, 200);
if (SUCCESS != rc) {
printDebug("%s| Network Status command returned unexpected response: [%s][%s]", getName().c_str(), sCmd.c_str(), sResult.c_str());
return rc;
}
// ,,,,,
// [0] [1] [2] [3] [4] [5]
vParts = MTS::Text::split(sResult, ",");
if (vParts.size() < 6) {
printDebug("%s| Network Status command response is an unknown format: [%s][%s]", getName().c_str(), sCmd.c_str(), sResult.c_str());
return FAILURE;
}
int iValue;
if (MTS::Text::parse(iValue, vParts[5])) {
do {
if (255 == iValue) {
break; // invalid, not known or not detectable
}
if (iValue > 97 || iValue < 0) {
printDebug("%s| Network Status command returned unexpected value of RSRP: [%s][%d]", getName().c_str(), sCmd.c_str(), iValue);
break;
}
// converting value to rsrp dBm according to documentation
jDebug["rsrp"] = std::to_string(iValue - 141);
} while (false);
}
if (MTS::Text::parse(iValue, vParts[4])) {
do {
if (255 == iValue) {
break; // invalid, not known or not detectable
}
if (iValue > 34 || iValue < 0) {
printDebug("%s| Network Status command returned unexpected value of RSRQ: [%s][%d]", getName().c_str(), sCmd.c_str(), iValue);
break;
}
// converting value to rsrq in dBm according to documentation
jDebug["rsrq"] = std::to_string((iValue-40.0)/2.0);
} while (false);
}
return SUCCESS;
}
// AT+SQNQRUP? -- This command queries and reports the uplink power of the current LTE network.
// +SQNQRUP: ,
ICellularRadio::CODE SequansRadio::getNetworkStatusTxPower(Json::Value& jData, Json::Value& jDebug) {
std::string sCmd;
std::string sResult;
std::string sPrefix;
std::vector vParts;
int iValue;
CODE rc;
sCmd = "AT+SQNQRUP?";
sPrefix = "+SQNQRUP:";
// the key must be present
jDebug[ICellularRadio::KEY_TXPWR] = "";
rc = sendBasicQuery(sCmd, sPrefix, sResult, 200);
if (SUCCESS != rc) {
printDebug("%s| Network Status command returned unexpected response: [%s][%s]", getName().c_str(), sCmd.c_str(), sResult.c_str());
return rc;
}
// ,
// [0] [1]
vParts = MTS::Text::split(sResult, ",");
if (vParts.size() < 2) {
printDebug("%s| Network Status command response is an unknown format: [%s][%s]", getName().c_str(), sCmd.c_str(), sResult.c_str());
return FAILURE;
}
double fTxPow;
if (MTS::Text::parse(fTxPow, vParts[0])) {
do {
if (-21474836.0 == fTxPow) {
break; // invalid, not known or not detectable
}
if (fTxPow < -255 || fTxPow > 99) {
printDebug("%s| Network Status command returned unexpected value of txPower: [%s][%d]", getName().c_str(), sCmd.c_str(), iValue);
break;
}
jDebug[ICellularRadio::KEY_TXPWR] = vParts[0];
} while (false);
}
return SUCCESS;
}
ICellularRadio::CODE SequansRadio::getNetworkStatus(Json::Value& jData) {
printTrace("%s| Get Network Status", getName().c_str());
// NOTE: Only the LTE radios from Sequans are supported at the moment
Json::Value jDebug;
std::string sResult;
CODE rcMainStatus;
getCommonNetworkStats(jData);
rcMainStatus = getNetworkStatusSQN(jData, jDebug);
getNetworkStatusSignal(jData, jDebug);
getNetworkStatusTxPower(jData, jDebug);
if (SUCCESS == getImsi(sResult)) {
jData[ICellularRadio::KEY_IMSI] = sResult;
}
if (SUCCESS == getNetwork(sResult)) {
jData[ICellularRadio::KEY_NETWORK] = sResult;
}
// Fill the debug information only if the main tower information was fetched successfully.
if (SUCCESS == rcMainStatus) {
jDebug[ICellularRadio::KEY_RSSIDBM] = jData[ICellularRadio::KEY_RSSIDBM];
jDebug[ICellularRadio::KEY_SD] = "PS";
jData[ICellularRadio::KEY_DEBUG] = jDebug;
}
printTrace("%s| Network Status:\n%s\n", getName().c_str(), jData.toStyledString().c_str());
return SUCCESS;
}
ICellularRadio::CODE MTS::IO::SequansRadio::convertSignalStrengthTodBm(const int32_t &iRssi, int32_t &iDBm) {
const int dbmSteps = 2;
const int minValue = -113;
const int maxValue = -51;
const int rssiOffset = 0;
int rawDbm;
if (iRssi < 0 || iRssi > 99) {
return FAILURE; // invalid, not known or not detectable
}
rawDbm = minValue + ((iRssi - rssiOffset) * dbmSteps);
iDBm = std::min(maxValue, rawDbm);
return SUCCESS;
}
ICellularRadio::CODE SequansRadio::convertdBmToSignalStrength(const int32_t& iDBm, int32_t& iRssi) {
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 SequansRadio::setMdn(const Json::Value& jArgs) {
printTrace("%s| Set MDN", getName().c_str());
return NOT_APPLICABLE;
}
ICellularRadio::CODE SequansRadio::setUeModeOfOperation(ICellularRadio::UE_MODES_OF_OPERATION mode) {
printTrace("%s| Set UE Mode Of Operation", getName().c_str());
std::string sValue;
switch (mode) {
case ICellularRadio::UE_MODES_OF_OPERATION::PS_MODE1:
sValue = "3";
break;
case ICellularRadio::UE_MODES_OF_OPERATION::PS_MODE2:
sValue = "0";
break;
case ICellularRadio::UE_MODES_OF_OPERATION::CS_PS_MODE1:
sValue = "1";
break;
case ICellularRadio::UE_MODES_OF_OPERATION::CS_PS_MODE2:
sValue = "2";
break;
default:
printError("%s| Set UE Mode Of Operation: invalid argument", getName().c_str());
return INVALID_ARGS;
}
const int dTimeout = 1000; // ms
const std::string sCmd = "AT+CEMODE=" + sValue;
return sendBasicCommand(sCmd, dTimeout);
}
ICellularRadio::CODE SequansRadio::getUeModeOfOperation(ICellularRadio::UE_MODES_OF_OPERATION& mode) {
printTrace("%s| Get UE Mode Of Operation", getName().c_str());
std::string sCmd = "AT+CEMODE?";
std::string sPrefix = "+CEMODE:";
std::string sResult;
if (SUCCESS != sendBasicQuery(sCmd, sPrefix, sResult, 1000)) {
printError("%s| Unable to get UE Mode Of Operation from radio using command [%s]", getName().c_str(), sCmd.c_str());
return FAILURE;
}
uint8_t uiValue;
if (!MTS::Text::parse(uiValue, sResult)) {
printError("%s| Unable to parse CEMODE from response [%s]", getName().c_str(), sResult.c_str());
return FAILURE;
}
CODE rc;
switch (uiValue) {
case 0:
mode = ICellularRadio::UE_MODES_OF_OPERATION::PS_MODE2;
rc = SUCCESS;
break;
case 1:
mode = ICellularRadio::UE_MODES_OF_OPERATION::CS_PS_MODE1;
rc = SUCCESS;
break;
case 2:
mode = ICellularRadio::UE_MODES_OF_OPERATION::CS_PS_MODE2;
rc = SUCCESS;
break;
case 3:
mode = ICellularRadio::UE_MODES_OF_OPERATION::PS_MODE1;
rc = SUCCESS;
break;
default:
printError("%s| Unable to parse CEMODE from response [%s]", getName().c_str(), sResult.c_str());
mode = ICellularRadio::UE_MODES_OF_OPERATION::UNKNOWN_MODE;
rc = FAILURE;
break;
}
return rc;
}
ICellularRadio::CODE SequansRadio::setRxDiversity(const Json::Value& jArgs) {
return NOT_APPLICABLE;
}
const std::vector SequansRadio::getRegistrationCommands() {
return { "CEREG" };
}
SequansRadio::SequansRadio(const std::string& sName, const std::string& sRadioPort)
: CellularRadio (sName, sRadioPort)
{
}
ICellularRadio::CODE SequansRadio::getIsSimInserted(bool& bData) {
printTrace("%s| Get SIM insertion status", getName().c_str());
// there is no command to check SIM presence
return NOT_APPLICABLE;
}
ICellularRadio::CODE SequansRadio::getSimLockAttempts(int& iAttemptsPin, int& iAttemptsPuk) {
printTrace("%s| Get SIM unlock attempts left", getName().c_str());
CODE rc = FAILURE;
rc = getSimLockAttemptsByType("SIM PIN", iAttemptsPin);
if (rc != SUCCESS) {
return rc;
}
rc = getSimLockAttemptsByType("SIM PUK", iAttemptsPuk);
if (rc != SUCCESS) {
return rc;
}
return SUCCESS;
}
ICellularRadio::CODE SequansRadio::getSimLockAttemptsByType(const std::string& sArg, int& iAttempts) {
std::string sCmd("AT+CPINR");
std::string sPrefix("+CPINR:");
std::string sResult;
int iValue;
CODE rc = FAILURE;
sCmd += "=\"";
sCmd += sArg;
sCmd += "\"";
printDebug("%s| Sending SIM lock attempt query [%s]", getName().c_str(), sCmd.c_str());
rc = sendBasicQuery(sCmd, sPrefix, sResult, 500);
if (SUCCESS != rc) {
return rc;
}
std::vector vParts = MTS::Text::split(sResult, ',');
if (vParts.size() < 3 || ! MTS::Text::parse(iValue, vParts[1])) {
printWarning("%s| Unable to parse SIM unlock attempts from response [%s]", getName().c_str(), sResult.c_str());
return FAILURE;
}
iAttempts = iValue;
return SUCCESS;
}
const std::vector& SequansRadio::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", "ATI1", "at!=showVersion",
// All carrier profiles that are supported:
"AT+SQNCTM=?",
// Current operator profile on the radio side:
"AT+SQNCTM?", "AT+CGSN",
// SIM card information:
"AT+CRSM=176,12258,0,0,0", "AT+CPIN?", "AT+CPINR=\"SIM PIN\"", "AT+CPINR=\"SIM PUK\"",
// Operating mode of the radio:
"AT+CFUN?", "AT^AUTOATT?", "AT+SQNAUTOCONNECT?",
// Low-level network settings:
"AT+WS46?", "AT+CEMODE?",
// Data connection configuration:
"AT+CGDCONT?", "AT+CGAUTH?",
// Registration and connection to the tower:
"AT+CSQ", "AT+COPS?", "AT+CEREG?", "AT+SQNMONI=0",
"AT+SQNQRCCI?", "AT+CESQ", "AT+SQNQRUP?",
// Data connection status:
"AT+CGACT?", "AT+CGCONTRDP=1", "AT+CGCONTRDP=2", "AT+CGCONTRDP=3"
};
return vCommands;
}
ICellularRadio::CODE SequansRadio::getTimeUTC(void) {
printTrace("%s| Get Time in UTC", getName().c_str());
// Set year format in YYYY first, in case it is in YY format to get accurate year
std::string sCmdCSDF("AT+CSDF=1,2");
std::string sRes = sendCommand(sCmdCSDF);
size_t endr = sRes.find(ICellularRadio::RSP_OK);
if (endr == std::string::npos) {
printWarning("%s| Unable to set year format for radio using command [%s]", getName().c_str(), sCmdCSDF.c_str());
return FAILURE;
}
// Set year format in YYYY first, in case it is in YY format to get accurate year
std::string sCmdCTZU("AT+CTZU=1");
sRes = sendCommand(sCmdCTZU);
size_t endc = sRes.find(ICellularRadio::RSP_OK);
if (endc == std::string::npos) {
printWarning("%s| Unable to set automatic time zone update for radio using command [%s]", getName().c_str(), sCmdCTZU.c_str());
return FAILURE;
}
return SUCCESS;
}