zoukankan      html  css  js  c++  java
  • 高通与At指令:ATFWD解析

    背景

    本章的内容是适用于AP侧AT指令开发调试的有关人员。

    主要是介绍高通实现的ATFWD框架。在这需要说明一下的是,或许你对AT Command很了解了,但是却貌似都不知道ATFWD,这很正常,严格来说,ATFWD都不算属于AT Command框架的一部分,只是高通对扩展的at命令做的一个扩展实现。

    我们之前说到,ATCommands以处理方式可以分有两类,一类是直接在modem下进行处理的,还有一部分是在AP侧进行处理更加方便有效的。

    而对于APSide的AT命令,高通也提供了一套框架进行实现,在这我们就这一块做详细的学习。

    同样的,AT命令通过ATCoP从串口传来并被解析,而对于APSide的AT命令我们会通过allow_list[]数组注册,这个时候modem会判断传来的命令是不是AP相关的,如果是,通过qmi通讯将AT命令传到AP侧进行处理,而在AP侧的流程便是通过ATFWD框架实现的。

    因此,只要有ATCoP,那么有关的指令就需要注册到ATCoP中

    在某个新基线上移植AT指令,发现有问题,因此收集了这个系列的 文章 作为 这方面的知识补充。

    原文作者:laozhuxinlu,本文有删改。

    AT指令在产线中是一类比较重要的问题, 一天没来得及解决,则会拖延生产的有关进度。

    ATFWD 主要是与 含有 modem 的有关异构处理器有关的。

    如果 对应的高通平台 没有 modem 处理器(例如 SDM845),则使用 port-bridge 的方式进行 AT 实现,后续我们会讲到。

    代码解析

    在vendor下,一般是在vendor/qcom/proprietary/telephony-apps/ATFWD-daemon目录下,我们能看到ATFWD的具体实现:

    有时候也可能存在于vendor/qcom/proprietary/data/ATFWD-daemon

    • Android.mk:编译一个主进程(ATFWD-daemon)到system/bin下面做实时监听从modem下传来的AT命令。
    • atfwd_daemon.c:主进程的main函数定义,做AT命令的注册已经QMI(Qualcom Message Interface 高通信息接口 )初始化,并循环监听传来的AT命令并处理返回。
    • sendcmd.cpp:初始化获取binder服务,以此实现将AT命令传到实际处理的地方。
    • IAtCmdFwd.cpp:对binder服务的定义。

    main

    ATFWD-daemon进程的入口是main()函数

    /*=========================================================================
      FUNCTION:  main
    
    ===========================================================================*/
    /*!
    @brief
      Initialize the QMI connection and register the ATFWD event listener.
      argv[1] if provided, gives the name of the qmi port to open.
      Default is "rmnet_sdio0".
    
    */
    /*=========================================================================*/
    int main (int argc, char **argv)
    {
        AtCmdResponse *response;
        int i, connectionResult, initType;
    
        userHandle = userHandleSMD = -1;
        i = connectionResult = 0;
    
        printf("*** Starting ATFWD-daemon *** 
    ");
        (void) getTargetFromSysProperty();
    
        if ( !is_supported_qcci() )
        {
            if (!strncmp(ATFWD_DATA_TARGET_APQ, target,
                         strlen(target))) {
                printf("APQ baseband : Explicitly stopping ATFWD service....
    ");
                stopSelf();
                return -1;
            }
    
            if (argc >= 2) {
                qmiPort = argv[1];
            } else {
                qmiPort = getDefaultPort();
                if( NULL == qmiPort ) {
                    qmiPort = DEFAULT_QMI_PORT;
                }
            }
    
            if (argc >= 3) {
                secondaryPort = argv[2];
            } else if (!strncmp(ATFWD_DATA_TARGET_SVLTE2A, target, strlen(target))) {
                /* For SVLTE type II targets, Modem currently exposes two ATCOP ports.
                * One bridged from USB to SDIO, directly talking to 9k modem
                * Another bridged from USB to SMD, directly talking to 8k
                * Therefore given this modem architecture, ATFWD-daemon needs to
                * listen to both the modems( 8k & 9K).
                * Register with 8k modem
                */
                secondaryPort = DEFAULT_SMD_PORT;
            } else if (!strncmp(ATFWD_DATA_TARGET_SGLTE, target, strlen(target))) {
                // For SGLTE targets, Register with the SMUX port.
                secondaryPort = QMI_PORT_RMNET_SMUX_0;
            }
        }
    
        printf("init all signals
    ");
        signalInit();
    
        pthread_mutexattr_t attr;
        pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
        pthread_mutex_init(&ctrMutex, &attr);
        pthread_cond_init(&ctrCond, NULL);
    
        printf("Explicitly disbling qmux 
    ");
        qmi_cci_qmux_xport_unregister(QMI_CLIENT_QMUX_RMNET_INSTANCE_0);
        qmi_cci_qmux_xport_unregister(QMI_CLIENT_QMUX_RMNET_USB_INSTANCE_0);
        qmi_cci_qmux_xport_unregister(QMI_CLIENT_QMUX_RMNET_SMUX_INSTANCE_0);
        qmi_cci_qmux_xport_unregister(QMI_CLIENT_QMUX_RMNET_MHI_INSTANCE_0);
        printf("Disabling QMUX complete...
    ");
    
        //Get QMI service object
        printf("getting at svc obj for access terminal QMI svc
    ");
        qmi_at_svc_obj = at_get_service_object_v01();
    
        for (initType = INIT_QMI; initType != INIT_MAX; initType++) {
            connectionResult = 0;
            tryInit (initType, &connectionResult);
            printf(" tryinit complete with connectresult: %d
    ", connectionResult);
            if (connectionResult < 0)
            {
                if ( !is_supported_qcci() )
                {
                    if (qmiHandle >= 0) {
                        qmi_release(qmiHandle);
                    }
                }
                else
                {
                    printf("Release qmi_client...
    ");
                    qmi_client_release(qmi_at_svc_client);
                    qmi_at_svc_client = NULL;
                }
    
                stopSelf();
                return -1;
            }
        }
        else
        {
            if (!registerATCommands())
            {
                stopSelf();
                return -1;
            }
        }
    
        while (1) {
            pthread_mutex_lock(&ctrMutex);
            while (!isNewCommandAvailable()) {
                printf("Waiting for ctrCond");
                pthread_cond_wait(&ctrCond, &ctrMutex);
                printf("Recieved ctrCond: p: %d, S:%d, nr: %d",regForPrimaryPort, regForSecondaryPort, newRequest );
            }
    
            if ( !is_supported_qcci() )
            {
                if (regForPrimaryPort == 1) {
                    if (qmiPort) {
                        printf("Rcvd pthread notification for primary QMI port registration");
                        initAtcopServiceAndRegisterCommands(qmiPort, &userHandle);
                    } else {
                        printf("Notification for primary QMI port registration when NOT valid, ignore...");
                    }
                    regForPrimaryPort = 0;
                }
    
                if (regForSecondaryPort == 1) {
                    if (secondaryPort) {
                        printf("Rcvd pthread notification for secondary QMI port registration");
                        initAtcopServiceAndRegisterCommands(secondaryPort, &userHandleSMD);
                    } else {
                        printf("Notification for secondary QMI port registration when NOT valid, ignore...");
                    }
                    regForSecondaryPort = 0;
                }
                if(userHandle < 0 && userHandleSMD < 0)
                {
                    printf("userhandle(s) for both 8k and 9k modems NOT valid -- bail out");
                    if (qmiHandle >= 0)
                    {
                        qmi_release(qmiHandle);
                    }
                    stopSelf();
                    return -1;
                }
            }
            else
            {
                if ( regForPrimaryPort == 1)
                {
                    printf("Registering for primary port (QCCI).");
                    connectionResult = 0;
                    tryInit (INIT_QMI_SRVC, &connectionResult);
                    printf(" init result: %d
    ", connectionResult);
                    if (connectionResult < 0)
                    {
                        printf("Release qmi_client...
    ");
                        qmi_client_release(qmi_at_svc_client);
                        qmi_at_svc_client = NULL;
                        stopSelf();
                        return -1;
                    }
                    if (!registerATCommands())
                    {
                        printf("Register for primary port (QCCI) failed.");
                        stopSelf();
                        return -1;
                    }
                    regForPrimaryPort=0;
                }
    
                if ( regForServiceUp == 1 )
                {
                    regForServiceUpEvent();
                }
            }
    
            if (newRequest == 1) {
                printf("pthread notified for new request; sending response.");
                response = sendit(&fwdcmd);
                if (response == NULL) {
                    printf("Response processing complete、Invalid cmd resp.");
                    sendInvalidCommandResponse();
                    printf("Invalid response sending complete.");
                } else {
                    printf("Response processing complete、Sending response.");
                    sendResponse(response);
                    printf("Send response complete.");
                }
    
                if (fwdcmd.name) free(fwdcmd.name);
                if (fwdcmd.tokens) {
                    for (i = 0; i < fwdcmd.ntokens; i++) {
                        free(fwdcmd.tokens[i]);
                    }
                    free(fwdcmd.tokens);
                }
                freeAtCmdResponse(response);
                newRequest = 0;
                printf("New request processing complete.");
            }
    
            pthread_mutex_unlock(&ctrMutex);
        }
    
        return 0;
    }
    

    tryInit

    在main中会调用tryInit():实现三块的初始化:

    • 首先是QMI的初始化;其次是进行QMI连接的初始化:这两块主要是实现能接受到从modem下传上来的At命令。
    • 最后是对binder服务的获取初始化:以便能把相应的命令通过binder通讯方式传到相应的地方进行处理。
    void tryInit (atfwd_init_type_t type, int *result) {
        LOGI("ATFWD :Going to tryInit ATFWD daemon
    ");
        int retryCnt = 1;
    
        for (; retryCnt <= ATFWD_MAX_RETRY_ATTEMPTS; retryCnt++) {
            LOGI("ATFWD :retryCnt <= ATFWD_MAX_RETRY_ATTEMPTS
    ");
            qmiErrorCode = 0;
            switch (type) {
                // 初始化QMI
                case INIT_QMI:
                    LOGI("ATFWD :Going to qmi_init(atfwdSysEventHandler)
    ");
                    qmiHandle = qmi_init(atfwdSysEventHandler, NULL);
                    *result = qmiHandle;
                    break;
                // 连接QMI(实现能接受到从modem下传上来的At命令)
                case INIT_QMI_SRVC:
                    LOGI("ATFWD :Going to qmi_connection_init(qmiPort, &qmiErrorCode)
    ");
                    *result = qmi_connection_init(qmiPort, &qmiErrorCode);
                    break;
                // 获取binder服务,以便能把相应的命令通过binder通讯方式传到相应的地方进行处理
                case INIT_ATFWD_SRVC:
                    LOGI("ATFWD :Going to initializeAtFwdService(case INIT_ATFWD_SRVC)
    ");
                    *result = initializeAtFwdService();
                    break;
                default:
                    LOGI("Invalid type %d", type);
                    return;
            }
            LOGI("ATFWD :result : %d 	 ,Init step :%d 	 ,qmiErrorCode: %d", *result, type, qmiErrorCode);
            if (*result >= 0 && qmiErrorCode == 0) {
                break;
            }
            sleep(retryCnt * ATFWD_RETRY_DELAY);
        }
    
        return;
    }
    

    initAtcopServiceAndRegisterCommands

    初始化并注册所有其添加的At命令;

    // 对应的命令
    qmi_atcop_at_cmd_fwd_req_type atCmdFwdReqType[] = {
    
        { //AT command fwd type
            1, // Number of commands
            {
                { QMI_ATCOP_AT_CMD_NOT_ABORTABLE, "+CKPD"},
            }
        },
        { //AT command fwd type
            1, // Number of commands
            {
                { QMI_ATCOP_AT_CMD_NOT_ABORTABLE, "+CTSA"},
            }
        },
        { //AT command fwd type
            1, // Number of commands
            {
                { QMI_ATCOP_AT_CMD_NOT_ABORTABLE, "+CFUN"},
            }
        },
        // ...
    };
    
    void initAtcopServiceAndRegisterCommands(const char *port, int *handle) {
        int i, nErrorCnt, nCommands;
        i = nErrorCnt = 0;
    
        initAtCopServiceByPort(port, handle);
    
        if (*handle > 0) {
            nCommands = sizeof(atCmdFwdReqType) / sizeof(atCmdFwdReqType[0]);
            printf("Trying to register %d commands:
    ", nCommands);
            for (i = 0; i < nCommands ; i++) {
                printf("cmd%d: %s
    ", i, atCmdFwdReqType[i].qmi_atcop_at_cmd_fwd_req_type[0].at_cmd_name);
    
                qmiErrorCode = 0;
                int registrationStatus = qmi_atcop_reg_at_command_fwd_req(*handle, 
                                                  &atCmdFwdReqType[i], &qmiErrorCode);
                printf("qmi_atcop_reg_at_command_fwd_req: %d", qmiErrorCode);
                if (registrationStatus < 0 || qmiErrorCode != 0) {
                    printf("Could not register AT command : %s with the QMI Interface - Err code:%d
    ",
                         atCmdFwdReqType[i].qmi_atcop_at_cmd_fwd_req_type[0].at_cmd_name, qmiErrorCode);
                    nErrorCnt++;
                    qmiErrorCode = 0;
                }
            }
    
            if(nErrorCnt == nCommands) {
                printf("AT commands registration failure..、Release client handle: %d
    ", *handle);
                qmi_atcop_srvc_release_client(*handle, &qmiErrorCode);
                *handle = -1;
                return;
            }
        } else {
            printf("ATcop Service Init failed
    ");
            return;
        }
    
        printf("Registered AT Commands event handler
    ");
        return;
    }
    

    等待新命令

    此后,函数会循环在while(1)中,当modem下有传来需要处理的命令的时候,newRequest会置为1,走if(newRequest == 1){……}

    if (newRequest == 1) {
        LOGI("pthread notified for new request
    ");
        response = sendit(&fwdcmd);
        if (response == NULL) {
            sendInvalidCommandResponse();
        } else {
            sendResponse(response);
        }
    
        if (fwdcmd.name) free(fwdcmd.name);
        if (fwdcmd.tokens) {
            for (i = 0; i < fwdcmd.ntokens; i++) {
                free(fwdcmd.tokens[i]);
            }
            free(fwdcmd.tokens);
        }
        freeAtCmdResponse(response);
        newRequest = 0;
    }
    

    main()函数下调用sendit(&fwdcmd)函数将命令传递出去,并将返回的结果给response数据结构;

    Sendit

    typedef struct {
      int opcode;
      char *name; // 指令名称, AT+abc --> abc
      int ntokens; // 有多少个参数
      char **tokens; // 参数数组
    } AtCmd;
    
    typedef struct {
      int result;
      char *response;
    } AtCmdResponse;
    
    extern "C" AtCmdResponse *sendit(const AtCmd *cmd)
    {
        AtCmdResponse *result = NULL;
        result = new AtCmdResponse;
        result->response = NULL;
        LOGI("sendit");
    
        LOGE("%s:%d peeta", __func__, __LINE__);
        if(strcasecmp(cmd->name, "+QFCT")==0){
            LOGI("ATFWD AtCmdFwd QFCT");
            if(NULL != cmd->tokens) {
                LOGI("ATFWD AtCmdFwd Tokens Not NULL ntokens=%d",cmd->ntokens);
                if(cmd->ntokens == 0 || cmd->tokens[0] == NULL){
                    LOGI("ATFWD AtCmdFwd Tokens[0] is NULL");
                    quec_qfct_handle(result);
                }else if(0 == strncmp("wifi-kill",cmd->tokens[0],strlen("wifi-kill"))){
                    //  char *args[5] = { PTT_SOCKET_BIN, "-f", "-d", "-v", NULL };
                    LOGI("ATFWD AtCmdFwd:%s",cmd->tokens[0]);
                    property_set("wifi.ptt_socket_app", "false");
                    property_set("wifi.p_socket_app", "true");
                    //...
            }else{
                LOGI("ATFWD AtCmdFwd Tokens is NULL");
                quec_qfct_handle(result);
            }
        }else if(strcasecmp(cmd->name, "+QGMR")==0)
        {
            quec_qgmr_handle(cmd,result);
        }
    
        return result;
    }
    

    如果使用到了binder,还可以这样:sendit()函数调用processCommand()函数,processCommand是继承于BpInterface类实现的,我们通过Parcel数据将数据写入data下,然后通过调用唤起RPC进行binder数据通讯:remote()->transact(processAtCmd,data, &reply);

    extern "C" AtCmdResponse *sendit(const AtCmd *cmd)
    {
        AtCmdResponse *result;
    
        if (!cmd) return NULL;
    
        result = gAtCmdFwdService->processCommand(*cmd);
    
        return result;
    }
    

    以上主要就是一个ATCommand在ATFWD下的大致流程了。

    总结

    简单的说,那就是一个进程,进行数据中转的进程:数据从modem下传上来先通过venderril,再从venderril下传到framework(或者别的什么地方)下进行处理,ATFWD便是venderril下的一个中转站。其中与modem的通讯方式采用QMI,与framework采用bingerserver方式通讯。

    ATFWD调试技巧

    如果在AT指令的实现中遇到了某些问题,可以按照下面的流程进行分析。

    0、确保ATFWD进程正常执行,如果没有,则根据log确定 是 中途退出(没走完流程)还是 Android系统的权限问题。

    AT Command流程分析之具体实现

    主要是介绍作为一个AT Command的开发者,具体如何参与到代码的开发。当然,这里主要是介绍一些基本的开发工作……

    想必从前面的学习,你已经了解到AT命令执行的大致流程,基于这个流程,AT Command的功能开发也主要是包括在两个方面:

    • BP Side类型的AT命令开发
    • AP Side类型的AT命令开发

    BP侧

    首先是BP Side类型的AT命令开发,或者说如何在ATCoP上去扩展实现实现一个AT命令。

    我们知道AT命令分有以下几种类型,在这我们以最常见的扩展AT命令为例,命名:”+CLAY”。

    • 基本 AT 命令(basic_table)
    • 寄存器 AT 命令(sreg_table)
    • 扩展 AT 命令(extended_table)
    • 厂商 AT 命令(vendor_table)

    定义指针变量

    在dsati.h下的dsatetsi_ext_action_index_enum_type枚举数组中添加一个指针变量如下:

    DSATETSI_EXT_ACT_CLAY_ETSI_IDX = 14084
    

    建立AT命令和处理函数的映射

    在dsatetsictab.c下的dsatetsi_ext_action_table_ex []数据下添加映射:

    //...
    {DSATETSI_EXT_ACT_CLAY_ETSI_IDX,  dsatetsime_exec_clay_cmd  }
    // ...
    

    定义AT命令

    如果想要定义一个at命令,需要首先确定它的命令表项,也就是name、属性、参数情况、处理函数指针等……

    下面我们增加的是一个最简单的命令,name是”+CLAY”,属性是无参数。

    在dsatetsictab_ex.c下的dsatetsi_ext_action_table []数组中添加:

    { "+CLAY", READ_ONLY | COMMON_CMD,
     SPECIAL_NONE, 0,DSATETSI_EXT_ACT_CLAY_ETSI_IDX 
    }
    

    具体的含义请参见 AT Command流程分析之AtCop解析模块。

    声明处理函数

    上面完成以后就能定义其实际的处理函数了,在定义之前,我们先要声明一下,在dsatetsime.h下添加:

    dsat_result_enum_type  dsatetsime_exec_clay_cmd (
    
      dsat_mode_enum_typemode, /*AT command mode: */
    
      constdsati_cmd_type *parse_table, /*Ptr to cmd in parse table */
    
      consttokens_struct_type *tok_ptr, /*Command tokens from parser */
    
      dsm_item_type*res_buff_ptr /* Place to put response */
    
    );
    

    定义处理函数

    dsat_result_enum_type dsatetsime_exec_clay_cmd (
        dsat_mode_enum_typemode, /*AT command mode: */
        constdsati_cmd_type *parse_table, /*Ptr to cmd in parse table */
        consttokens_struct_type *tok_ptr, /*Command tokens from parser */
        dsm_item_type*res_buff_ptr /* Place to put response */
    ){
    
        dsat_result_enum_type result= DSAT_OK;
    
        if(tok_ptr->op == NA){
            res_buff_ptr->used =(word) snprintf ((char*)res_buff_ptr->data_ptr,
                                                 res_buff_ptr->size,
                                                 "%s: %s,%s",
                                                 "+CLAY",
                                                 "hello",
                                                 "world");
        }
    
        else if(tok_ptr->op ==(NA|EQ|QU)){} //针对其他的语法格式进行处理
        else if(tok_ptr->op ==(NA|QU)){}    //针对其他的语法格式进行处理
        else if(tok_ptr->op ==(NA|EQ|AR)){} //针对其他的语法格式进行处理
        else{ result= DSAT_ERROR;}          //针对错误的语法格式进行处理
    
        return result;
    }
    

    至此,一个BP Site的自定义AT 命令便开发完成了,这里需要注意的是,这边只是举例实现,而且对AT+CLAY的命令类型作为扩展命令开发的,就所以流程仅供参考……

    AP侧

    那么,要想扩展添加一个AP Site的AT命令又该如何呢?

    首先要确认ATFWD在设备中已添加注册并正常运行(可在system/bin下去查看)

    同样的,这里就AT+CLAY举例实现……

    1、在Modem侧添加自定义的AT Command的注册。

    */amss_8909/modem_proc/datamodem/interface/atcop/src/dsatclient_ex.c下的LOCAL byte allowed_list[][MAX_CMD_SIZE]数组中添加定义:

      LOCAL byte allowed_list[][MAX_CMD_SIZE]={……,"+CLAY",""};
    

    2、 AP侧的Vendor下添加AT Command的注册。

    */vendor/qcom/proprietary/data/ATFWD-daemon/atfwd_daemon.c下的qmi_atcop_at_cmd_fwd_req_type atCmdFwdReqType[]数组中添加定义:

        { //AT command fwd type
            1, // Number of commands
            {
                { QMI_ATCOP_AT_CMD_NOT_ABORTABLE, "+CLAY"},
            }
        },
    

    3、 添加在framework侧的实际处理并返回处理结果。之前我们说过,AP Site的AT命令在串口经过modem通过QMI通讯方式将命令传到Vendor Ril下,但是实际上是需要将该命令传到framework下去处理的,这个时候需要用到binder通讯方式将命令传到framework下去实际处理。

    在Vendor下的binder通讯发送在/vendor/qcom/proprietary/data/ATFWD-daemon/IAtCmdFwd.cpp下去实现的:

    virtual AtCmdResponse *processCommand(const AtCmd &cmd)
    {
        // ...
    
        data.writeInterfaceToken(IAtCmdFwdService::getInterfaceDescriptor());
        data.writeInt32(1);                     //specify there is an input parameter
        data.writeInt32(cmd.opcode);            //opcode
        String16 cmdname(cmd.name);
        s16 = strdup8to16(cmd.name, &len);
        data.writeString16(s16, len);            //command name
        free(s16);
        data.writeInt32(cmd.ntokens);
        for (int i=0; i < cmd.ntokens; i++) {
            s16 = strdup8to16(cmd.tokens[i], &len);
            data.writeString16(s16,len);
            free(s16);
        }
    
        status_t status = remote()->transact(processAtCmd, data, &reply);//RPC call
        LOGI("Status: %d",status);
        if (status != NO_ERROR) {
            LOGE("Error in RPC Call to AtCdmFwd Service (Exception occurred?)");
            return NULL;
        }
        // ...
    }
    

    实际就是通过

    status_t status = remote()->transact(processAtCmd, data, &reply);//RPC call
    

    实现了发送,最后返回的处理结果在status下。

    4、在framework侧的处理实现:

    status_t BnAtCmdFwdService::onTransact(uint32_t code, const Parcel& data,
    Parcel* reply, uint32_t flags) {
        case processAtCmd: {
                 // ...……   //接受到binder通讯传来的数据
           }
        }
    

    最后就是针对接收到的具体的值进行处理。

    如果说我的文章对你有用,只不过是我站在巨人的肩膀上再继续努力罢了。
    若在页首无特别声明,本篇文章由 Schips 经过整理后发布。
    博客地址:https://www.cnblogs.com/schips/
  • 相关阅读:
    Windows SDK编程(Delphi版) 之 应用基础,楔子
    一个小问题引发的论证思考
    Delphi 组件开发教程指南(7)继续模拟动画显示控件
    用PyInstaller将python转成可执行文件exe笔记
    使用 .Net Memory Profiler 诊断 .NET 应用内存泄漏(方法与实践)
    Microsof Office SharePoint 2007 工作流开发环境搭建
    How to monitor Web server performance by using counter logs in System Monitor in IIS
    LINQ之Order By
    window 性能监视器
    内存泄露检测工具
  • 原文地址:https://www.cnblogs.com/schips/p/at_command_in_qualcomm_3_atfwd.html
Copyright © 2011-2022 走看看