在某个新基线上移植AT指令,发现有问题,因此收集了这个系列的 文章 作为 这方面的知识补充。
原文作者:laozhuxinlu
本文有删改。
参考:https://www.cnblogs.com/hengfeng/archive/2009/12/05/1617537.html
以Q&A的形式的一些基本概念的介绍,让大家能够初步的认识什么是ATCommand,知道它是干嘛的,包括涉及到的一些相关知识。
AT 即Attention。AT命令集是从TE(TerminalEquipment:终端设备)或DTE(DataTerminal Equipment:数据终端设备)向TA(Terminal Adapter:终端适配器)或DCE(DataCircuit Terminating Equipment:数据电路终端设备)发送的。通过TA,TE发送AT命令来控制MS(Mobile Station:移动台)的功能与GSM网络业务进行交互。
总结来说,ATCommand是一种调制解调器的命令语言,由计算机(超级终端)发出的被用来控制和执行模块功能,通过uart串口进行通讯的命令操作方式,通过串口将命令集发送到设备端,然后设备端(手机)自有一套处理框架流程对不同的AT命令进行解析,判断,然后进行相对应的处理,并返回其对应的处理结果。
ATCommand在手机通讯中,主要是提供给用户AT控制指令控制手机行为,比如说呼叫、短信、电话本、数据业务、补充业务、传真、GPRS等;同时包括对手机的一些参数的获取、设置及上层UI的改变等等……
在这里介绍的架构主要是针对高通平台的Android设备,高通平台提供了自有的一套流程框架(ATCoP)处理ATCommand。
ATCoP(AT Command Process)接收串口(SerialPort)处传来的AT Command,进行解析(Parse),根据解析的结果到AT Command Tables中寻找相应的表项,若匹配,则执行对应的处理函数;或者如果处于ap侧处理的列表命令,会发送到ap侧进行处理返回(AP测需要实现相应的AT Client)。
后面针对于AtCoP会有更加具体的讲解,包括modem中对APSide的ATCommand的具体处理流程,会以一个单个模块来具体讲述。
一般而言,命令主要分为下列3部分。
前缀:由字符AT构成,他来源于单词立正attention的头字母。
主体:主体由命令,参数和可用到的数值组成。
终止字符
ATCommands的语法格式其实就是AT Commands的输入格式,总体上说AT命令有四种形式:
1)无参数指令(基本命令语法)。 一种简洁的指令,格式:AT[+|&]<command>
举例:显示本机号码“AT+CNUM
”,恢复出厂设置命令“AT&F
”。
2)查询指令(读命令语法)。 查询该指令当前设置的值,格式:AT[+|&]<command>?
。举例:电话本存储区可选择的值“AT+CPBS?
”。
3)帮助指令(测试命令语法)。用来列出该命令各参数的所有的可能值(并非各个组合都可能支持),格式:AT[+|&]<command>=?
。举例:AT+CMGL=?
。
4)带参数指令(扩展命令语法)。用于设置相应的参数值, 比较常用的一种格式,它为指令提供了强大的灵活性,格式:AT[+|&]<command>=<par1>,<par2>,<par3>?
针对不同的AT Commands测试的输入格式是不一样的。甚至大部分都是多种测试的输入格式,具体格式在相应的AT Commands的协议介绍中有详细说明。
不同的命令其大致的语法格式是差不多的,当然也有一些自定义的AT命令,但其基本格式还是需要按照标准来定义。
注意:对于上面所说的四种格式,对某个命令而言并不是都存在四种格式,有些命令就一种格式,有些支持两种或者三种四种格式,具体的需要查询协议介绍。
AT Commands的大致分类:
具体可参见AT命令手册
一般命令
呼叫控制命令、网络业务命令
安全性命令
电话本命令、短消息命令
补充数据命令、数据命令
传真命令
串口控制命令
自定义的一些AT命令
…………
注意:这里所谓的AP Side和BP Side的AT命令是根据最终在哪处理来区分的。
对于大部分的AT命令几乎都是BP侧的,但是有一些特别的命令(比如说自定义的命令,或者在AP侧才有接口或者处理更加方便的),对于这些命令我们会发到AP侧,在AP侧有专门的AT Client对其进行处理并返回处理结果。
后面章节中对AP Side的AT命令会有更加详细的介绍。
ATCoP(AT Command Processor) 这是一套AT命令的处理框架,位于BP侧中。
注意:由于我没有拿到modem的代码,因此下面的文章关于BP的大部分是无法修改的。
ATFWD?或许很多人看到这个词会觉得很陌生,的确,因为这一块属不属于AT模块,都不好说。
所有的AT命令都是通过串口到modem的,而ATFWD是高通就AP Side的AT命令的实现所做的一套框架:
TE:Terminal Equipment,终端设备,与DTE等价。比如一个计算机它是和信息网络的一端相接的可提供必要功能的设备这些功能使得用户通过接入协议能接入网络 如发送信息和接收信息也可指由线路 电路 信道 数据链路的终端或起点组成的设备。
TA:Terminal Adapter,终端适配器,与DCE等价。提供终端适配功能的物理实体是一种接口设备。
DCE: Data Circuit terminating Equipment,数据电路终接设备。一种接口设备在线路之间进行代码或信号转换 同数据终端设备实现接口能够建立保持和释放数据终端设备与数据传输线之间的连接。
DTE: Data Terminal Equipment,数据终端设备。它具有向计算机输入和接收计算机输出的能力与数据通信线路连接的通信控制能力以及一定的数据处理能力。
ME:Mobile Equipment,移动设备。比如GSM话机就属于ME移动台中的一种发射机或接收机或发射机与接收机二者的组合
该篇主题主要介绍的是ATCommandProcessor。
ATCoP是什么?ATCommandProcessor,是高通AMSS(modem)software下的一套对AT命令的具体实现模块,也只有真正弄懂ATCoP,才能真正的了解AT命令有关软件的实现。
下面,我将就:
AtCoP具体功能
AtCoP实现架构
AtCoP处理流程
At命令解析(ATCommand Parser)
At命令表(ATCommand Table)
这四个方面来做具体介绍,学习完,便能结合具体的实例具体的去分析整个的AT命令实现,也只有在深入了解ATCoP的基础上,才能实现对ATCommand的修改和新增。
ATCoP,ATCommand processor,AT命令处理器。是对AT命令具体软件实现的模块,通过ATCoP,我们可以实现对AT命令的修改和新增。
这个部分是由modem侧(BP侧)实现的。
基本上它遵循以下过程:
串口(sio)接收到串口发送过来的字符串--> AT Command Parser --> 产生一个重要的Token结构,它包含了命令名称,接收到的参数,以及response的buffer--> 之后AT Command ProcessorProcess 通过Token里面的信息查调用相应的AT 命令处理函数 -->处理完成后产生相应的response给TE。
简单的来说,ATCoP接收串口(Serial Port)处传来的ATCommand,进行解析(Parse),根据解析的结果到ATCommand Tables中寻找相应的表项,若匹配,则执行对应的处理函数,处理完以后response其对应的返回数据到串口。
目前,Qualcomm(高通)DMSS采用IS-707AT Command Set作为它的DataServices的命令集。
下面列举出与ATCoP相关的一些主要的资源目录:
filename | Description |
---|---|
Dsat.h | ATCoP外部模块使用的定义,函数和数据结构 |
Dsat.h | ATCoP外部模块使用的定义,函数和数据结构 |
Dsati.h | ATCoP内部使用的定义,函数和数据结构 |
Dsatprep.c | 接收自串口设备的数据的预处理 |
Dsatpar.c | AT命令解析器,将命令行的AT命令解析到token结构中 |
Dsatcmdp.c | AT命令处理器,查找token结构中的包含的命令并从命令列表中调用相应的命令处理函数处理命令 |
Dsatrsp.c | 产生AT命令响应和格式化 |
Dsatutil.c | 产生AT命令处理器 |
Dsatparm.c | 通用AT参数类型命令过程 |
Dsatarm.h | ATCoP内部使用的通用AT参数类型命令处理定义,函数和数据结构 |
Dsatact.c | 通用ATactive type命令处理函数 |
Dsatact.h | 通用AT动作类型命令处理的定义、函数和数据结构,供ATCoP内部模块使用 |
Dsatvend.c | 通用AT指定提供商类型命令处理 |
Dsatvend.h | 通用AT指定提供商类型命令处理的定义、函数和数据结构,供ATCoP内部模块使用 |
Dsatctab.c | 通用AT命令表 |
Dsatctab.h | 通用AT命令表定义、函数和数据结构,供ATCoP内部模块使用 |
Dsatcmif.c | 通用呼叫管理接口 |
Dsatcmif.h | 通用呼叫管理接口定义、函数和数据结构,供ATCoP内部模块使用 |
Dsatvoice.c | 通用语音呼叫处理控制 |
Dsatvoice.h | 通用语音呼叫处理控制的定义、函数和数据结构,供ATCoP内部模块使用 |
Dsatetsicall.c | ETSI呼叫控制命令处理 |
Dsatetsicall.h | ETSI呼叫控制命令处理的定义、函数和数据结构,供ATCoP内部模块使用 |
Dsatetsicmif.c | ETSI命令呼叫管理接口 |
Dsatetsicmif.h | ETSI命令呼叫管理接口的定义、函数和数据结构,供ATCoP内部模块使用 |
Dsatetsipkt.c | ETSI包数据命令处理 |
Dsatetsipkt.h | ETSI包数据命令处理的定义、函数和数据结构,供ATCoP内部模块使用 |
Dsatetsime.c | ETSI移动设备命令处理 |
Dsatetsime.h | ETSI移动设备命令处理的定义、函数和数据结构,供ATCoP内部模块使用 |
Dsatetsismsc.c | ETSI短消息服务命令处理 |
Dsatetsismsa.c | ETSI短消息服务异步事件处理 |
Dsatetsismsu.c | ETSI短消息服务命令处理实体 |
Dsatetsisms.h | ETSI短消息服务命令处理的定义、函数和数据结构,供ATCoP内部模块使用 |
Dsatetsismsi.h | ETSI短消息服务命令处理的定义、函数和数据结构,供ATCoP内部模块的短消息服务单元使用 |
Dsatetsictab.c | ETSIAT命令表 |
Dsatetsictab.h | ETSIAT命令表的定义、函数和数据结构,供ATCoP内部模块使用 |
Dsatetsitgt.c | 顶层AT命令表,命令表指针数组,ETSI指定目标的命令表,同步事件处理表。定义ETSI目标支持的AT命令集 |
Dsatgsmfax.c | GSMfax命令处理 |
Dsatgsmfax.h | GSMfax命令处理的定义、函数和数据结构,供ATCoP内部模块使用 |
数据服务任务源文件列表:
filename | Description |
---|---|
Dstask.h | 数据服务任务的外部或内部模块使用的定义,函数和数据结构 |
Dsatsk.c | 数据服务任务和顶层分发 |
对修改或者新增一个AT命令主要涉及到的一些文件:
首先我们来看一张图:
ATCoP的基本架构主要有一下几个部分:
1.通过串口设备(SIO)接收的AT命令数据,首先由SIO数据预处理,产生一个null-terminated命令行并由DS分发给AT命令解析器。
2.产生的null-terminated命令行由AT命令解析器解析,解析器为每个要解析的命令产生一个token结构,并送到处理队列由AT命令处理器处理。在AT命令处理器被调用前,解析器将每个命令的token结构放入队列中。
3.AT命令处理器完成对每个token结构进行表查找,同时将该token结构从队列中移除。如果查找到,对应的处理函数被调用处理该命令;AT命令在命令表中定义,每个命令表入口包含对应命令执行函数的指针。
4.AT命令响应产生器将命令响应数据格式化,产生结果编码,并将响应数据发送给DTE。
5.ATCOP每次处理一条AT命令行命令,如果任何命令行的命令产生一个错误,在错误前就会产生命令处理的响应,同时产生一个错误代码,不再对该命令进行后续处理。
下面图同样展示的是ATCoP的实现框架,从USB接收到AT命令,到初始化创建SIO,再到命令的实际处理到返回,可详读下面这张图。
其实在ATCoP的实现架构就已经简单介绍到了ATCoP对AT命令的处理流程,在这我们将对其进行更加具体的详解。
ATCoP处理控制流大致可以分有三个步:
Initial Parsing:初始化解析
CommandParsing:命令解析
Command Execution:命令执行处理(包括返回结果)
如果在表中查找到该命令,调用表中对应的处理函数执行该命令。命令执行后如果有返回数据时,返回的响应数据在函数dsat_fmt_response中格式化。每个token结构都进行这样的处理。最后调用函数dsatrsp_send_response把命令响应送到DTE。
注:对于异步AT命令处理流程与正常AT命令略有不同,在命令预处理、命令解析过程都是一样的,在命令处理过程中(process_at_cmd_line),如果命令处理函数返回DSAT_OK,说明命令处理完成调用dsat_fmt_response函数格式化响应数据并发送,正常的命令处理流程;如果命令处理函数返回DSAT_ASYNC_CMD说明当 前 命 令 是 异 步 命 令 , 此 时 函 数process_at_cmd_line 设置 变 量dsatcmdp_processing_async_cmd= TRUE,表示当前正在处理异步命令,然后返回,不再进行后续处理,直到该命令处理完成,函数返回DSAT_OK(未必一定是DSAT_OK,当返回不是DSAT_ASYNC_CMD和DSAT_ASYNC_EVENT时,说明异步命令/事件处理完成)。当DS收到异步事件经任务分发器,再次调用dsat_process_async_cmd函数,在该函数中通过查找异步事件表async_event_table,调用相应的事件处理函数继续处理,如果事件处理函数返回值不是DSAT_ASYNC_CMD或DSAT_ASYNC_EVENT,说明异步事件处理完成,调用process_at_cmd_line继续处理命令行的命令。
下面我们可以通过一个流程图更加直观的了解ATCoP的处理流程:
基本遵循过程:
SIOData Preprocessor接收串口发送过来的字符串(这里是AT Command),并向DS TASK发送信号要求其处理;DS TASK 知晓并获得控制权后,由AT Command Parser解析AT Command,将得到的结果存入相应的token结构中(包含了命令名称,接收到的参数,以及response的buffer);AT Command Processor到AT Command Tables匹配相应的表项;AT Command Response Generator根据匹配的结果调用对应的Command Processing Function进行处理;处理完成后产生相应的response给 TE。
ATCommand Parser对命令行进行解析时,将解析的结果存到token中,并在下一步到Parse Table中进行匹配。
下面就以代码流程具体展示:
DS_AUTODETECT_SRVC_MODE模式下,串口检测到A字符,则发送DS_1ST_SIO_RX_SIG给DSTASK
DSTASK调用dsi_mgr()进行分发处理
ds_process_rxbuf_event()
switch(dsi_callstate){
caseDS_IDLE_CSTATE:
ds_atcop_process_sio_command();
}
/*从watermark中取出data尝试建立一个命令行*/
while(cc!=NULL)
{
cc = cce & 0x7F //只使用高7位
switch (at_state)
{
case HUNT:
if (UPCASE (cc) == ‘A‘)
// 转到FOUND_A状态
break;
case FOUND_A:
if (UPCASE (cc) == ‘/‘)
//从buf取出上个AT命令来执行(ds_atcop()),执行完毕后转到HUNT状态
else if (UPCASE (cc) == ‘T‘)
//转到CAT状态
else if (UPCASE (cc) != ‘A‘)
//转到HUNT状态
break;
case CAT:
if (cc != ds_atcop_s3_val) // if not EOL
{
if (cc != ds_atcop_s5_val) // if not backspace
{
if OVERFLOW
//转到ERROR状态
else if (cc >= 0x20 && cc <= 0x7E)
//fill buffer;
} // if backspace
else
// remove the most immediate character from the buffer
}
else
if EOL
//对命令行进行处理(ds_atcop()),处理完毕后转到HUNT状态
break;
case ERROR:
//执行相应出错处理,处理完毕后转到HUNT状态
break;
}
}
//ds_atcop() 对命令行进行处理
switch (UPCASE (*curr_char_ptr)){
case ‘+‘: /* Extended format specified in IS-99 */
case ‘$‘: /* Extended format proprietary command */
//对命令行进行解析
curr_char_ptr = ds_atcop_parse_ex (curr_char_ptr, &tok);
if SUCCEED
//根据解析的结果到parse table匹配,进行相应处理
ds_atcop_exec_param_cmd ();
//若匹配不到,则强制解析
if (ds_atcop_result == DS_ATCOP_DO_HARD_PARSE)
ds_atcop_hard_to_parse();
break;
}
//If not originating or answering a call
ds_atcop_fmt_response(); //generate a final result code
if (ds_atcop_result == DS_ATCOP_CXT_ORIG){
ds_atcop_discard_results();
}
else if (mode == DS_ATCOP_CMD){
ds_atcop_send_results();//发送
}
其中涉及到的Token Struct数据结构:
typedef struct
{
byte working_at_line[MAX_LINE_SIZE]; // Stores command lines to be processed. Each line
// is referenced by a line number
byte *name; // The name of the AT command
unsigned int op; // Syntax flags. Can be one of four valid values (NA,
// EQ, QU, AR) or an invalid value
byte * arg[MAX_ARG]; // AT command arguments
unsigned int args_found; // Keeps track of the number of AT command
//arguments found
} tokens_struct_type;
AT命令的处理是由命令表驱动的,ATCOP实现的命令表是一个分级的表结构,主要分为:
主表(master table)
子表(sub table)
命令表(command table)。
其中主表是一个二维的数组,数组的行表示AT命令的分类,分为:
基本AT命令(basic_table)
寄存器AT命令(sreg_table)
扩展AT命令(extended_table)
厂商AT命令(vendor_table)
四大类;数组的列表示是ETSI模式还是其它模式的AT命令。
基本命令的格式为:[]
其中或者是单个字母(A-Z),或者是“&”字符接单个字母。
是一个十进制数,可以是一位,也可以是多位。最前面的0会被忽略。默认为0。如果一个不带的基本命令带了,则返回TOO MANYPARAMETERS。
所有以字母“S”开头的命令统称为S寄存器命令,格式如下:
S? S=
S 寄存器命令名由字母“S”接上一个十进制数构成,这个十进制数称为寄存器序号(parameternumber)。如果寄存器序号不被识别,说明不存在该命令,返回COMMANDNOT SUPPORT。
每个 S寄存器保存一个字符。命令名后面如果接“?”表示是READ命令,返回此S寄存器当前保存的字符的ASCII 码值,以3 位十进制数表示,位数不足的前面补0;如果接“=”表示是SET命令,将值对应的字符替换此S寄存器保存的字符。
扩展命令均由“+”开头,厂商定义的命令也是由一个特殊符号开头,例如“$”,“%”等。本文中所实现的命令均为扩展命令。所有的扩展命令和厂商定义命令又可以分为两类:Actioncommand和Parameter command。
action command 指完成某个具体的动作,而不仅仅是与MS 本地的参数打交道的命令,例如AT+CMGS 等。actioncommand 可以带参数也可以不带。Actioncommand 包含EXECUTION 命令和TEST 命令。
(1)EXECUTION命令
EXECUTION 命令格式如下:
[=]
[=]
, 中间以“,”分隔表示多个参数。对于有默认值的参数,可以在命令中省略,此时以默认值代替。
如果所有的参数都省略,则后面的“=”也一并略去。如果不被识别,则表示此命令不存在,返回COMMAND NOTSUPPORT
。可识别的前提下,如果不能带参数的命令带了参数,或者参数个数超出定义,则返回TOOMANY PARAMETERS。
(2)TEST命令
TEST 命令格式:=?
如果 MS不能识别,返回COMMAND NOT SUPPORT。如果MS可以识别,且命令是不带参数的,则返回OK。如果命令带参数,则先返回各个参数的可取值范围,最后再加上OK。
parameter command包括与MS本地的参数打交道的命令,这些参数有些会影响到atcioncommand的执行。parametercommand又分为SET命令、READ命令和TEST命令。
(1)SET命令
命令格式为:带1个参数:[=]
带多个参数:[=]
,表示多个参数,中间以“,”分隔。
SET命令用于设置参数。对于有默认值的参数,可以在命令中省略,此时以默认值代替。如果所有的参数都省略,则后面的“=”也一并略去。如果不被识别,则表示此命令不存在,返回COMMAND NOTSUPPORT。可识别的前提下,如果不能带参数的命令 带了 参数 ,或者 参数 个数 超出 定义, 则返回TOO MANYPARAMETERS。
(2)READ命令
命令格式:?
READ 命令用于读取参数当前值。
(3)TEST命令
命令格式:=?
如果 MS不能识别,返回COMMAND NOTSUPPORT。如果MS可以识别,且命令是不带参数的,则返回OK。如果命令带参数,则先返回各个参数的可取值范围,最后再加上OK。
命令表项(主表定义dsati.h)
typedef struct dsati_cmd_struct
{
byte name[20]; // AT cmd 的名字,包含"+", "$" 等
uint32 attrib; // AT cmd 的属性
byte special; // AT cmd 的 special processing code
byte compound; // 传递的参数个数(若参数为字符串,则是它的最大长度)
const void *val_ptr; // 指向参数的指针
const void *def_lim_ptr; // 定义了参数的默认值以及取值范围
dsat_result_enum_type (*proc_func)( dsat_mode_enum_type,
const struct dsati_cmd_struct*,
const tokens_struct_type*,
dsm_item_type* );
boolean (*abort_func)( const struct dsati_cmd_struct* );
} dsati_cmd_type;
针对不同的软件版本可能对应的dsati_cmd_type定义略有差别。
详解:
1.name: AT命令名,包括需要处理的+,\(,&和终止的NULL。如+IPR,\)QCDMG, S6,&C,Z。
2.attitude: 32位的掩码,用来指定单个或多个命令属性。表4.1列出了AT命令的所有属性,后面给出了具有该属性的命令。
3.special: 如果有需要,就指定处理编码,否则就是SPECIAL_NONE,指定处理编码定义在dsati.h。只是用在与外部软件的兼容性时。
表1 AT命令属性列表
2)dsati_cmd_struct*是dsati_cmd_struct结构入口指针,对于包含这个命令表入口指定命令。
3)token_struct_type*是一个定义好的token structure解析器指针,这个token结构包含处理该命令所要求的信息。
4)dsm_item_type*是DSMbuffer的指针,存储命令响应。如果命令响应超出了一个DSMbuffer的容量,可以将多个DSMbuffer可以链接到一起。函数返回类型
应该是表 .3列出值中的一个。
表4.3函数返回类型表
8.abortfunction ptr
函数指针通过命令表入口调用定义的abort命令,函数指针值不是NULL表示命令表入口定义的命令是可以abort的。函数指针参数是:dsati_cmd_struct*是dsati_cmd_struct结构入口指针,用于指示包含该命令表入口的指定命令。函数返回类型是一个Boolen表示:如果值是TRUE,表示数据调用可以通过DsmgrAbort,否则不需要任何动作。
9.dflm_type
定义AT命令中数字参数的最大值和最小值参数,这里的数字参数一定是连续的取值类型。如果参数取值为{0,1,255}这种参数应该设置为list类型。Default_v为默认值,lower和upper为最小和最大取值。
10.def_list_type
用于定义LIST类型的参数取值范围,其中:
Default_v代表参数的默认值,它是指向list_v数组的指针。
List_v是一个8-byte字符串的数组指针,代表该参数允许的所有值,数组的最后一项必须是NULL来终止参数列表。AT命令处理器完成该数组的字符串匹配,以决定参数值是否在有效的范围内。
List_t是一个字符串指针,逐字返回测试命令的响应。
AT命令如果含有多个参数,每个参数都关联于def_list_type结构。
11.mixed_def_s_type和dsat_mixed_param_val_type
Mixed_def_s_type用于存储AT命令中混合参数的默认值和可能值,混合参数类型表示AT命令的参数可以是不同类型的,其允许值范围也是个集合,如果是数值类型需要指定在某一范围内,如果是字符串类型则限定长度。如果命令有N个不同类型参数,默认的可能值范围包含一个长度为N的mixed_def_s_type数组,类型参数i是数组下标,如mixed_argus[]定义为mixed_argus[i].attrib。如果AT命令有N种不同类型参数,参数的当前值包含在长度为N的dsat_mixed_param_val_type数组中。该数组用于联系mixed_def_s_type数组Dsat_mixed_param_val_type数组下标为i的元素,在mixed_def_s_type数组对应i分量为该AT命令的默认和可能的范围值。
其实,针对于ATCoP的学习是比较混乱的,因为本身的内容就比较混杂,如果不是对整个架构比较清楚的话,很难学得清楚,并且就后面所要展开的AT Commands的自定义实现是在这一章的基础上实现的。在这我主要也是就ATCoP的标准协议文档进行翻译理解,并结合网上和个人的一些总结进行整理,希望有帮助。
就这一块的理解问题应该是挺多的,欢迎大家提出来一些探讨探讨!
ATCoP(AT Command Processor)是高通平台上对于AT命令处理的模块。基本上它遵循以下过程:
串口(sio)接收到串口发送过来的字符串--> AT Command Parser --> 产生一个重要的Token结构,它包含了命令名称,接收到的参数,以及response的buffer--> 之后AT Command ProcessorProcess 通过Token里面的信息查调用相应的AT 命令处理函数 -->处理完成后产生相应的response给TE。
我们在USB MODEM上需要实现一个对PIN码校验的功能,即UIM的PIN功能开启以后,需要在PC端提示用户输入PIN码,但连续输入3次错误后需输入PUK解锁。
我们不需要修改高通默认的处理流程,只要在command table中添加相应的命令、处理函数:
{ "+CPIN", READ_ONLY|RESTRICTED,
SPECIAL_NONE, 0, NULL, NULL,
dsat707_exec_CPIN_cmd, NULL },
其中+CPIN就是我们定义的一个AT命令,dsat707_exec_CPIN_cmd就是对应的处理函数。
我们设想的+CPIN功能是:
A. 查询命令: AT+CPIN?
返回:
READY 表示UIM没有
UIM PIN 表示UIM开启了PIN码功能,用户插入USB MODEM后需要输PIN码
UIM PUK 表示连续3次输入错误PIN,需要输入PUK解锁
VERIFIED 表示PIN码输入正确
UNBLOCKED 表示PUK输入正确,解锁成功
BLOCKED 如果是返回这个,很不幸的告诉你,你的卡GAME OVER!
B. 设置命令: AT+CPIN=1234 用户输入PIN码验证
AT+CPIN=12345678,1234 用户输入PUK以及新PIN码来解锁
OK,知道了具体的需求,就可以在处理函数dsat707_exec_CPIN_cmd中实现了:
先判读接收到的+CPIN是什么格式的,是查询命令,设置命令,还是其他。
我们可以通过Token结构中的tok_ptr->op参数来判断,如果tok_ptr->op == (NA|QU)表示查询命令;tok_ptr->op == (NA|EQ|AR)表示设置命令。
在查询命令中,我们只要通过获得UIM卡的PIN状态,把相应的状态以字符串的形式告诉PC端就可以了。当然可以在这里调用GSDI 的接口来查询PIN状态,但是在dsatme_init_me中已经向GSDI注册了一个回调函数:gsdi_pin_event_cb_func。 一旦GSDI检测到PIN码状态发生改变时,就会调用这个回调函数。
所以我们可以定义一个全局变量,在回调函数中设置这个全局变量,之后在dsat707_exec_CPIN_cmd中判断这个全局变量,发送相应字符串。
在设置命令中,我们要限制用户只能在UIM PIN和UIM PUK 这两种状态才允许执行。在这里遇到了一个很大的难题,就是我们可以容易的通过GSDI的接口来解PIN和解PUK,但是要如何告知其他的service以及UI我们已经解锁了呢?? 可以采用一种“笨”办法,就是不通过直接调用GSDI接口来解PIN,而是模拟出一连串的按键,发送给UI解锁。比如接收到AT+CPIN=1234后,我们可以连续发送1,2,3,4,SEL一共5个KEY EVENT给UI,这样就如同用户在手机上按键解锁了。
来电---ATCoP中对同步事件的处理
同步事件的处理,我的理解就是ATCoP在其他task注册了一个同步事件的回调函数,一旦同步事件发生时,其他task就会调用这个回调函数来通知ATCoP调用相应的同步事件处理函数。一个典型的例子就是来电过程的处理:
1. dsatcmif_init_cmif -> cmif_register_cb_func---向CM task注册来电,或者对方应答回调
/* Register the CM call event callback function */
cm_status = cm_mm_client_call_reg (
dsatcm_client_id,
cmif_call_event_cb_func,
CM_CLIENT_EVENT_REG,
CM_CALL_EVENT_INCOM,
CM_CALL_EVENT_CONNECT,
NULL
);
2. 一旦有来电消息,cmif_call_event_cb_func构造事件的命令放入cmd buffer
cmd_buf->hdr.cmd_id = DS_AT_CM_CALL_INFO_CMD;
cmd_buf->cmd.call_event.event_info = *event_ptr;
cmd_buf->cmd.call_event.event = event;
ds_put_cmd(cmd_buf);---->调用(void)rex_set_sigs( &ds_tcb, DS_CMD_Q_SIG );通知ds_task来处理
3. 在 ds_task 中调用 dsi_process_cmds()处理命令
4. 调用dsat_process_async_cmd( cmd_ptr );
查找表async_event_table event_handler_func_ptr = async_event_table[i].event_handler[operating_mode];
调用相应的处理函数result_code = event_handler_func_ptr( dsatcmdp_at_state, cmd_ptr );
这里调用dsatcmif_cm_call_event_handler
调用来电处理函数 ,在这个函数里面会主动发送"Ring"字符串来通知PC端由来电了。
result = dsatvoice_call_event_incom(&cmd_ptr->cmd.call_event.event_info);
## AT Command流程分析之ATFWD解析
主要是介绍高通实现的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框架实现的。
在vendor下,一般是在vendor/qcom/proprietary/telephony-apps/ATFWD-daemon目录下,有时候也可能存在于vendor/qcom/proprietary/data/ATFWD-daemon下,我们能看到ATFWD的具体实现:
- 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()函数
```c
/*=========================================================================
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 *** \n");
(void) getTargetFromSysProperty();
if ( !is_supported_qcci() )
{
if (!strncmp(ATFWD_DATA_TARGET_APQ, target,
strlen(target))) {
printf("APQ baseband : Explicitly stopping ATFWD service....\n");
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\n");
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 \n");
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...\n");
//Get QMI service object
printf("getting at svc obj for access terminal QMI svc\n");
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\n", connectionResult);
if (connectionResult < 0)
{
if ( !is_supported_qcci() )
{
if (qmiHandle >= 0) {
qmi_release(qmiHandle);
}
}
else
{
printf("Release qmi_client...\n");
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\n", connectionResult);
if (connectionResult < 0)
{
printf("Release qmi_client...\n");
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;
}
在main中会调用tryInit()
:实现三块的初始化:
void tryInit (atfwd_init_type_t type, int *result) {
LOGI("ATFWD :Going to tryInit ATFWD daemon\n");
int retryCnt = 1;
for (; retryCnt <= ATFWD_MAX_RETRY_ATTEMPTS; retryCnt++) {
LOGI("ATFWD :retryCnt <= ATFWD_MAX_RETRY_ATTEMPTS\n");
qmiErrorCode = 0;
switch (type) {
// 初始化QMI
case INIT_QMI:
LOGI("ATFWD :Going to qmi_init(atfwdSysEventHandler)\n");
qmiHandle = qmi_init(atfwdSysEventHandler, NULL);
*result = qmiHandle;
break;
// 连接QMI(实现能接受到从modem下传上来的At命令)
case INIT_QMI_SRVC:
LOGI("ATFWD :Going to qmi_connection_init(qmiPort, &qmiErrorCode)\n");
*result = qmi_connection_init(qmiPort, &qmiErrorCode);
break;
// 获取binder服务,以便能把相应的命令通过binder通讯方式传到相应的地方进行处理
case INIT_ATFWD_SRVC:
LOGI("ATFWD :Going to initializeAtFwdService(case INIT_ATFWD_SRVC)\n");
*result = initializeAtFwdService();
break;
default:
LOGI("Invalid type %d", type);
return;
}
LOGI("ATFWD :result : %d \t ,Init step :%d \t ,qmiErrorCode: %d", *result, type, qmiErrorCode);
if (*result >= 0 && qmiErrorCode == 0) {
break;
}
sleep(retryCnt * ATFWD_RETRY_DELAY);
}
return;
}
初始化并注册所有其添加的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:\n", nCommands);
for (i = 0; i < nCommands ; i++) {
printf("cmd%d: %s\n", 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\n",
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\n", *handle);
qmi_atcop_srvc_release_client(*handle, &qmiErrorCode);
*handle = -1;
return;
}
} else {
printf("ATcop Service Init failed\n");
return;
}
printf("Registered AT Commands event handler\n");
return;
}
此后,函数会循环在while(1)中,当modem下有传来需要处理的命令的时候,newRequest会置为1,走if(newRequest == 1){……}
if (newRequest == 1) {
LOGI("pthread notified for new request\n");
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数据结构;
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方式通讯。
主要是介绍作为一个AT Command的开发者,具体如何参与到代码的开发。当然,这里主要是介绍一些基本的开发工作……
想必从前面的学习,你已经了解到AT命令执行的大致流程,基于这个流程,AT Command的功能开发也主要是包括在两个方面:
首先是BP Side类型的AT命令开发,或者说如何在ATCoP上去扩展实现实现一个AT命令。
我们知道AT命令分有以下几种类型,在这我们以最常见的扩展AT命令为例,命名:”+CLAY”。
在dsati.h下的dsatetsi_ext_action_index_enum_type枚举数组中添加一个指针变量如下:
DSATETSI_EXT_ACT_CLAY_ETSI_IDX = 14084
在dsatetsictab.c下的dsatetsi_ext_action_table_ex []数据下添加映射:
//...
{DSATETSI_EXT_ACT_CLAY_ETSI_IDX, dsatetsime_exec_clay_cmd }
// ...
如果想要定义一个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 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通讯传来的数据
}
}
最后就是针对接收到的具体的值进行处理。
原文:https://www.cnblogs.com/schips/p/at_command_instantiation_in_qualcomm.html