/*
 * 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 = 1024;
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;
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 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 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 WCDMA 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 = sendCommand("", vOdmStartedStrings, iTimeoutStart, 0x00);
        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 = sendCommand("", vOdmFinishedStrings, iTimeoutEnd, 0x00);
        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) {
    ICellularRadio::CODE rc;
    std::string sCmd;
    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 = 10000;  // wait for 10 seconds for the radio to detach
    const uint32_t duAttachTimeout = 30000;  // wait for 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 the radio to detach from the USB bus
        MTS::Thread::sleep(duDetachTimeout);
        // 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< vBuffer;
    for (size_t iChunk = 1; iChunk < (nChunks + 1); 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) {
            break;
        }
        if (stepCb && ((iChunk % nChunksPerCent) == 0)) {
            size_t dPercentsCompleted = iChunk / nChunksPerCent;
            callNextStep(stepCb, "FILE Info: Uploaded " + MTS::Text::format(dPercentsCompleted) + "%");
        }
    }
    if (rc != SUCCESS) {
        // cancel upload and terminate
        callNextStep(stepCb, "FILE Error: Upload failed due to internal error");
        abortFileUpload();
        return rc;
    }
    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));
    // "send" empty string to read acknoledge string
    std::string sResult = sendCommand("", DEFAULT_BAIL_STRINGS, 3000, 0x00);
    if (sResult.find(sExpectedResult) != std::string::npos) {
        printDebug("Radio returned: [%s]", sResult.c_str());
        printTrace("Upload finished, checksum matched");
    } else {
        printError("Upload failed: checksum mismatch. Expected: [%s], Actual: [%s]", sExpectedResult.c_str(), sResult.c_str());
        abortFileUpload();
        rc = FAILURE;
    }
    if (rc == SUCCESS) {
        callNextStep(stepCb, "FILE Info: Upload finished successfully");
    } else {
        callNextStep(stepCb, "FILE Error: Upload failed due to internal error");
    }
    return rc;
}
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;
    }
    const std::string sExpected = "+QFLST: \"UFS:" + sTargetFilename + "\"";
    bIsFilePresent = (sResult.find(sExpected) != std::string::npos);
    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 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 };
    CODE rc = FAILURE;
    std::string sResponse;
    while (true) {  // breaks on "FOTA","END"
        sResponse = sendCommand("", vFotaBailStrings, duUrcTimeout, 0x00);
        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;
        }
        const auto vParts = MTS::Text::split(MTS::Text::trim(sResponse), ',', 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;
                break;
            }
            // attempt failed, the radio attempts to recover
            callNextStep(stepCb, "FUMO Error: radio returned error code " + sCode);
            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]", sResponse.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);
        if (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;
}
ICellularRadio::CODE QuectelRadio::startFileUpload(const std::string& sTargetFilename, size_t nBytes) {
    const std::vector vBailStrings{ ICellularRadio::RSP_CONNECT, ICellularRadio::RSP_ERROR };
    const int dTimeout = 1000; //ms
    std::string sCommand, sResult;
    sCommand = "AT+QFUPL=\"";
    sCommand += sTargetFilename;
    sCommand += "\",";
    sCommand += MTS::Text::format(nBytes);
    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);
}