.net平台手机管理软件开发 (一) 简介 通过几个月零零碎碎地学习各方面知识之后在这个大二的寒假笔者终于用VB.Net写出了西门子手机的辅助软件——Siemens Support Tool。虽然我没有最终的完成这个软件的所有设计,但核心的功能已经开发完成,界面也基本到位,我的学习的目的也就达到了。在2月12日正式停止开发的以后,我想到把寒假20几天的辛苦历程作一个总结,为以后重温这部分知识起到一定的作用,也为广大编程爱好者提供一些帮助和启发。笔者才疏学浅,编程也是零碎时间自学过来的,所以有缺陷和谬误请大家斧正。 (二) 为何要设计?设计目的? 用过西门子手机的都知道西门子手机在人性化设计方面做得比较好,但是让笔者失望的是电脑端上使用的官方软件SDS,其缺点是操作不方便,速度比较慢,感觉人性化设计不到位。后来官方推出了用于65系列的Mobile Phone Manager,界面很好看,但安装后大于120MB的容量及其较慢的速度又让人大跌眼镜。 GhostMobile(简称GM)是我用过的国产非官方软件里面比较好的一款,但经常出现传输中一直等待的情况。估计作者并没有超时设计。另外GM文件传输速度很慢,短信管理不方便,后来也由于作者使用了新的手机,也就放弃了GM。后来又找到一款Siemens Mobile Control,简称SiMoCo,是国外的非官方软件。800多kb的身躯及其速度、功能方面都超过了官方及GM,令人刮目相看,一度成为我最喜欢的软件。但用后发现一些问题,软件过于专业,选项太多,对中文的支持不好。 所以最终的目的就是做一款能够实现文件传输、短信、便签、任务、重要记事管理的软件。 (三) 准备工作 2004的暑假我已经做了一部分,实现了基本的文件传输和短信功能,当时取名叫作M55 File Transfer Tool。后来在东北手机网上公开了,有一些GM无法连接的手机我的软件都可以连接,所以到现在为止有些网友还在使用我的这个软件。但由于知识不够,做得不是很理想,经常出现问题。 短信部分是官方网站下载的ATC_Command_Set_For_L55_Platform,详细地讲述了55平台上的AT指令集。其实SMS部分的AT指令各大手机厂商都是通用的,已基本上属于同一的指令集了。但是发现文件传输是OBEX却不是那么简单。 官方并没有任何开发文档说明数据传输使用的是什么协议,我用Serial Monitor监视到了数据传输的过程,全是HEX代码,不知道是什么意思。那段时间一度陷入迷茫状态,不知道如何下手。后来在google搜索,又在CSDN里发了一些帖子求救,但却没有一个能够明确说明的。有一个网友的留言给了我一些线索,他说可能是蓝牙协议里面的部分。 这条线索给我了极大的鼓舞,因为后来,顺藤摸瓜找到了红外线传输协议,意外地发现了IrOBEX的描述协议竟然和监视到的HEX代码的结构一样。随后经过仔细的研究发现就是OBEX协议,此协议可以作为上层协议用在红外线协议、蓝牙协议等。此过程大约经历了2个多月。其实现在看来这个问题简单了,手机的工厂模式的串口监视里面就会显示当前使用的协议。当数据传输开始时,会自动从GIPSY变为OBEX。但那个时候哪知道呢? 跨越了OBEX协议的障碍以后我写了一个OBEX-Multithread类,写得很垃圾,把十六进制转换成字符串,然后再转回来在发送。中间使用了string作字符串操作,速度很慢,测试以后只能勉强超过GM的传输速度。 后来借着Serial Monitor监视GM读取手机通讯薄的原理,发现通讯薄是在\telecom\pb目录里面,但是这个目录在手机里面是隐藏的,无法直接访问。由于原来写得OBEX库很糟糕,只能对应文件传输,对于这个特殊文件夹里面的文件都无法操作。修改了之后效果不好,遂放弃了OBEX-Multithread。 由于学习的原因,中途也只得停下来准备期末考试和六级。中途无聊的时候研究IrMC里面的vCard、vNote、vCalc格式,基本弄懂了如何同步通讯薄、便签、日历。2005年1月14日,放假回家了就正式开始动工,把所有的东西都重新写,对我来说,这是一个巨大的挑战。 (四) AT指令简介 AT指令在当代手机通讯中起着重要的作用,能够通过AT指令控制手机的许多行为,包括拨叫号码、按键控制、传真、GPRS等。西门子M55手机为我提供了很多的AT指令,网络上关于AT指令的资料也很多,我这里提取一些比较重要的做个简单解释。其他的手机也基本上通用,更详细的资料请查阅手机生产商的资料。 欲使用AT命令,可以安装微软的超级终端程序,选择好端口连接速度以后就可以正常使用了。 AT指令用法 1、 测试命令(Test Command) 2、 读取命令(Read Command) 3、 执行命令(Execute Command) 注:并不是所有的AT指令都支持1和2。 常用基本AT指令
以上这些指令都用于与手机连接的时候初始化用。取得手机IMEI及IMSI可以给使程序支持更多的手机连接并且保持数据独立。 短信部分
以上命令是短消息部分最经常使用的命令。具体条目及使用方法会在后面讲解。 (五) OBEX介绍 一、什么是OBEX,它有什么用途? OBEX全称为Object Exchange,中文对象交换,所以称之为对象交换协议。它在此软件当中有着核心地位,文件传输和IrMC同步都会使用到它。 OBEX协议构建在IrDA架构的上层. OBEX协议通过简单的使用“PUT”和“GET”命令实现在不同的设备、不同的平台之间方便、高效的交换信息。支持的设备广泛,例如PC,PDA,电话,摄像头,自动答录机,计算器,数据采集器,手表等等。 OBEX协议定义了一种柔性的概念——objects。也即是对象。这些对象可以包括文件,诊断信息,电子商务卡片,银行的存款等等。Objects在这里没有高级的技术含义,而是视你的应用而定。 OBEX协议小到可作“命令和控制”功能,例如对电视机,录像机等的操作。大道可以做很复杂的操作,例如数据库的事务处理和同步。 OBEX能够具有以下几个特点: 1、 友好的应用——可实现快速开发。 2、 紧缩——可用在资源有限的小型设备上。 3、 跨平台 4、 柔性的数据支持。 5、 方便的作为其他Internet传输协议的上层协议。 6、 可扩展性——提供了对未来需求的扩充支持而不影响以存在的实现。例如可扩展安全,数据压缩等。 7、 可测试可调试。 更为具体的关于OBEX的介绍请查阅IrOBEX协议。 二、OBEX对象模型 关于Headers 对象模型回答了对象是如何在OBEX协议描述的。这个模型必须包括被传输的对象和对对象的描述。为了做到这点,OBEX定义了Headers的概念。 一个Header反映了对象的一个方面,例如名字、长度、描述文字或者对象本身。例如,一个文件对象demo.txt会包含它的名字,一个类型标示为“text”,长度和文件本身。 Headers的构成 Headers简单的由和组成,简称为和。 HI由一个字节组成,指出了Header包含的内容以及它的格式。HV包含了一个或者多个字节,其结构由HI所决定。 所有的Header都是可选的,取决于设备的类型和事务的种类。你可以使用所有的Header,或者一些,或者没有。ID可以使Header可解析以及与传输顺序无关,也可以使不支持的Header被忽略掉。 HI又可以分为两部分,高2位和低6位。高2位确定了HI的编码方式(见表二),低6位确定了HI的意义(见表三)。两个表都是我从IrOBEX中的表摘抄并部分翻译过来的。 表二
表三
关于常用Header的更详尽的解释,更详尽信息请参考IrOBEX 1、Name 2、Length 3、Time Time描述了对象的最后修改的时间。使用ISO8601格式。 本地时间格式:YYYYMMDDTHHMMSS UTC时间格式:YYYYMMDDTHHMMSSZ 格式中的T可以方便的区分日期和时间。UTC时间使用Z作为标记。建议使用UTC时间。 Body Header由HI、一个2Byte长度的描述和整个的对象本身。End-of-Body组成和Body组成一样,但标识了这是对象的最后一部分。如果对象本来就很小,就直接使用End-of-Body。
三、请求(Request)和回应(Response) OBEX使用Request和Response作为最基本的操作。请求的每个Request必然有一个Response,否则可认为Request失败。 Request由一个或多个的Packet(包)组成,每个包的结构如下表 Request数据包结构
由于每个Request可能有多个Packet,opcode的最高位称为Final bit。如果被设置为1,那么说明这是Request的最后一个Packet。例如:当用PUT操作发送一个大文件时,会有几个Packet作为一个Request。那么只有最后一个Packet的FinalBit设置为1。
Response也由一个或多个Packet组成,每个包的结构如下表 Response数据包结构
同样的ResponseCode的最高位也叫做FinalBit。ResponseData可能包含对象和Header,或者其它信息。 下表列出了了常见的opcode和responseCode,更详尽的请参考IrOBEX 1.2文档。 opcode
*总是设置FinalBit
ResponseCode
四、说明。 1、 Connect(连接) 此操作初始化会话然后设置参数。其Request格式为
注:OBEX版本现在为1.0。 Response格式为:
对于更多关于Connect的说明请参考IrOBEX 2、 Disconnect(断开当前会话) 此操作断开OBEX会话。例如断开当前FolderListing Service,然后使用Connect连接到IrMC Sync Service实现同步通讯薄等功能。 Disconnect格式为:
成功的断开会返回0Xa0,拒绝会返回0xD3 3、 Put操作 Put操作从客户端发送一个对象到服务端。一般至少含有Name和Length两个Header。对于文件而言有可能还有Date/Time Header。 Put操作的格式如下:
回应格式如下:
关于Put操作的更详细的用法将在文件传输部分作解释。 4、 Get操作 Get操作从服务端返回一个对象。 Get操作的格式如下:
回应格式如下:
关于Get操作更详细的用法将在文件传输部分作解释。 5、 Abort操作 Abort操作中断一个多包操作(例如发送一个大文件)。Abort操作可以包含描述中断原因的Description Header。 Abort操作的格式如下:
Abort对应的应该是一个成功的操作(0xA0),指明这个操作已被接收并且服务端已经重新与客户端同步。如果返回的另外的值,客户端应该Disconnect。 6、 SetPath SetPath操作用于切换对方的路径。通常使用一个Name Header用于指定对方路径名称。如果为空,则返回默认目录,通常为根目录 SetPath操作格式如下: (六) OBEX应用——文件传输部分 在手机数据传输方面基本OBEX应用分为 l 文件传输 l IrMC同步 文件传输又可以细分为以下基本操作 l 初始化连接 l 断开连接 l 设置路径 l 取得目录信息 l 创建目录 l 上传下载文件 l 删除文件或空目录 在笔者的软件当中设计了OBEX这个类,里面包含了以上所有的基本操作。另外针对M55的服务端的特殊性又设计了更名、取得磁盘空间信息、移动、拷贝文件的功能。具体请参考源代码。 下面具体讲述各个操作的细节。 l 初始化连接 初始化连接包括了使手机进入OBEX状态再到发送Connect指令的一系列过程。具体流程参考下图。 ATÆAT^SQWE=0ÆAT^SQWE=3ÆConnectÆ连接到Folder-Listing Service 其中AT^SQWE=0和AT^SQWE=3是西门子特有的隐藏的AT指令,甚至在官方的AT指令集里面都没有提到。其作用是初始化手机到OBEX模式。 发送Connect指令收到手机回复以后确定Max Packet Length等参数。最后连接到Folder-Listing Service进行文件操作。如果需要IrMC同步的话,在Connect后直接连接到IrMC Sync Service即可,手机立刻进入同步模式,所有的应用程序退出。 在笔者的程序中当中首先使用AT指令确定手机当前的工作,如果超时,尝试发送+++并等待1秒钟以便手机从不正常的OBEX状态中退出。然后在此发送AT,成功后即进行文件操作,否则引发一个错误。 l 断开连接 这里的断开连接是指从OBEX模式退出到AT状态中。在AT指令中,连续发送三个0×2B然后等待一秒钟以上即可退出数据模式进入常规AT模式。 l 设置路径 程序中使用SetPath操作设置路径。需要注意的是,可以设计两种风格的过程:一种使用绝对路径,另一种使用相对路径。 对于手机而言,经笔者实践证明,使用绝对路径要比使用相对路径方便,而且更准确,但效率上稍逊,特别是在多层目录的情况下。由于手机没有能够返回当前路径的方法,所以相对路径总难以控制,只有通过程序控制,极容易出现错误。所以建议使用绝对路径。 如果使用绝对路径,那么路径名将呈现\path1\path2这种形式。首先回到根目录,然后一级一级的到达目的。在笔者的OBEX类当中,可以看到BacktoRoot这个过程。其作用就是把当前程序路径切换到根目录以免引起混乱。 l 取得目录信息 要取得一个目录的文件信息可以用发送Get指令。该Get指令需要一个TypeHeader,其值为x-obex/folder-listing 随后服务端会返回一个xml文件,包含了整个目录的信息。例如子目录名、文件名、大小、最后修改时间。再通过xml解析后就会得到目录信息。 l 创建目录 当SetPath的Flags的Bit1设为1并且无法找到目录时,服务端会创建这个目录并进入。 请注意,再次使用SetPath并将Flags的Bit0设为1返回上层目录,否则容易引起混乱。 l 上传下载文件 使用Put和Get命令可以实现上传和下载文件。注意所有的文件操作都在SetPath所指定的目录下进行。 使用Put命令实现上传时,至少需要提供NameHeader,BodyHeader,一般还要提供LengthHeader,DateTimeHeader。大文件传输需要正确处理BodyHeader,不能超出了服务端最大所能接收的Packet的大小,否则会出现错误。 需要注意的是如果当前文件存在,那么Put命令不会覆盖已有的文件而是追加,导致错误。传输文件之前需要首先删除同名文件。 使用Get命令实现下载时,只需要提供NameHeader即可。具体例子参考上一节。 l 删除文件或空目录 使用Put命令,其NameHeader指定为文件名或目录名并将BodyHeader设置为空即可。 非空目录无法删除,会返回错误。
(七) IrMC简介 要实现通讯薄、日历、便签的同步,需要用到IrDA协议里面的IrMC部分。 IrMC全称为Ir Mobile Communications。它定义了利用IR无线传输的设备之间通讯的规则。IrMC协议文档详细叙述了IrMC的方方面面,可以在我的主页(http://dream-world.)下载到PDF版本。 下面主要讲述我在开发当中所用到的部分以及实现方法。更为详细的资料请参考IrMC协议。 Phonebook 在手机软件桌面端通讯薄的管理是整个软件的必备功能之一,利用其信息可以方便的和Outlook等软件实现同步,实现更强大的功能。 通讯薄的管理分为读取、删除、增添、修改。通过这几个功能的组合可以实现更为强大的同步功能。下面分条概述。 l 读取 n 读取所有的Entry 1. 读取指定LUID的Entry l 删除 1. 连接到IrMC_Sync_Service(发送0×80, 0×0, 0×13, 0×10, 0×0, 0×40, 0×6, 0×46, 0×0, 0xC, 0×49, 0×52, 0×4D, 0×43, 0×2D, 0×53, 0×59, 0×4E, 0×43)使手机处于同步状态。 2. 得到ChangeCount ChangeCount是手机里面储存修改次数的一个数值,主要用于同步。具体相关资料请参考IrMC协议。 3. 构建数据包。 4. 使用OBEX的PUT命令,传输一个xxxx.vcf(xxxx指LUID)空文件到\telecom\pb\luid覆盖即可。 5. 发送0×81, 0×0, 0×3断开IrMC_Sync_service l 添加 1. 连接到IrMC_Sync_Service 2. 得到ChangeCount 3. 构建数据包 4. OBEX的PUT命令上传到\telecom\pb\.vcf文件即可。 5. 发送0×81, 0×0, 0×3断开IrMC_Sync_service l 修改
Notes 便签是大多数手机都提供的功能,能够方便的记录简短的信息。在我的M55手机上能够储存150Byte的信息,也就是150个英文或者75个汉字。软件通过管理便签可以与Outlook等软件同步,实现更高级的功能。 管理Notes的方法和Phonebook类似。得到全部Notes的vNote只需要获取\telecom\nt.vnt即可。删除、添加、修改只需要把\telecom\pb\luid改为\telecom\nt\luid即可。在此不再赘述。
Calendar 管理Calendar的方法和Phonebook类似。得到全部vCalendar只需要获取\telecom\cal.vcs。删除、添加、修改只需要把\telecom\pb\luid改为\telecom\cal\luid即可。在此不再赘述。 但值得注意的是vCalendar的结构 BEGIN:VCALENDAR VERSION:1.0 BEGIN:VEVENT …. END:VEVENT BEGIN:VTODO … END:VTODO END:VCALENDAR 完整的vCalender包含了至少一个vEvent或者一个vTodo,因此在添加、修改vEvent、vTodo时要将其补充为一个完整的vCalender结构,否则服务端会拒绝操作。
遇到的问题: 在实际操作中,遇到问题最多的地方在Phonebook部分。我的手机第一次同步的时候经常出现数据库被锁的情况,用SiMoCo读取也是一样,说明是手机拒绝写操作。这时候关闭手机再重新启动就好了。至于原因,我还没有搞清楚,希望能有高人指点 (八) vCard、vNote、vCalender格式简介 vCard称为电子商务卡片,主要用于记录通讯薄的联系人信息等,方面不同设备之间的数据交换。自笔者的M55手机中,可以发送一条短信到对方,其中包含了vCard格式的联系人信息,西门子其他型号的手机可以接收解码存储。另外通过手机红外线传输到电脑上的联系人也是用的vCard格式。如果安装了Outlook,则可以直接打开vCard并看到其包含的信息。下面主要简要介绍一下vCard格式,其他vNote、vCalender格式和vCard相近,就不再赘述。更详细的资料请参考vCard Specification,在笔者主页有相关下载。 关于vCard、vNote、vCalender的.Net简单编码解码器请参阅SIEMENS SUPPORT TOOL源代码中的IrMC部分。 vCard Object(vCard对象) 一个vCard数据流可以包含一个或者多个vCard Object。在数据流中一个vCard Object定义为以“BEGIN:VCARD”开始并以“END:VCARD”结束的数据。如果只有到达了数据流尾都没有出现“END:VCARD”,则整个vCard Object包含从“BEGIN:VCARD”开始到数据流结束的地方。 vCard Property(vCard属性) vCard是一个或多个Property的集合。一个Property是唯一命名的值。一系列的Property可以在vCard中成为一组。 vCard Property的格式如下: PropertyName[‘;’ PropertyParameters]’:’PropertyValue 注: 1、 PropertyName及PropertyParameters不区分大小写。 2、 PropertyParameters可选,可以为零个或多个,与ProperyName以分号相隔,与PropertyValue以冒号相隔。 3、 vCard可以分多行呈现。由于在这个软件里面应用得不多,所以笔者也没有钻研具体实现方法。可以参考vCard Specification。 例如TEL;HOME;+86111222333其PropertyName为TEL,PropertyParameters为HOME,PropertyValue为+86111222333。 Encoding(编码) vCard默认的编码方式是7-Bit。默认编码方式可以使用ENCODING属性参数(Property parameter)改变。其值为可以为BASE64;QUOTED-PRINTABLE;8BIT。这个参数可以用在任何的Property里。 例如: X-ESI-CATEGORIES;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:=E5=AE=B6=E4=BA=BA 下面简要说明QUOTED-PRINTABLE编码方式,更为详细的资料请参考相关文档: ASCII可显示字符基本保持不变。Unicode字符或者UTF8编码字符使用等号加其对应16进制代码表示。例如上述CHARSET为UTF8的字符=E5=AE=B6=E4=BA=BA对应的UTF8编码0xE5,0xAE,0xB6代表中文“家”,其他的代表“人”。另外如果其中有可显示ASCII码,保持原样输出。 例如ENCODING=QUOTED-PRINTABLE:Home=E5=AE=B6People=E4=BA=BA 解码后为“Home家People人”。 Character Set(字符集) 默认的字符集是ASCII。可以通过CHARSET参数改变默认的字符集。其参数可取的值为所有IANA(Internet Assigned Numbers Authority)注册的字符集。这个参数可以用于任何Property,但某些Property并不起作用。 例如: X-ESI-CATEGORIES;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:=E5=AE=B6=E4=BA=BA vCard例子: BEGIN:VCARD VERSION:2.1 X-IRMC-LUID:1017646 X-ESI-CATEGORIES;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:=E5=AE=B6=E4=BA=BA N:test ADR:;;Street;city;;610000;country ORG:company TEL;HOME:123456 TEL;WORK:123456 TEL;CELL:123456 TEL;FAX:123456 TEL;FAX;HOME:123456 EMAIL;INTERNET:a@a.ao EMAIL;HOME;INTERNET:b@g URL:http BDAY:1985-04-23 END:VCARD (九) 短信部分——PDU简介及其格式 PDU是大多数手机短信通讯的核心,仅有少数手机只支持Text模式(例如笔者的MOTO C330)。PDU模式比起Text模式可以提供能为强大的功能,但其编码较Text模式困难。无论哪种模式,我们都可以通过AT指令控制终端实现短信的发送、接收、删除等管理。下面主要介绍PDU的构成及编码解码。 PDU的构成 PDU是由一串由“0-9”及“A-F”组成。表面上看起来就是一组16进制的数所组成的。 下面举一个发送和接收的例子。 1、 手机发送的一个PDU串: 0891683108200805F011190D91683188902848F40008FF108FD9662F4E0067616D4B8BD577ED4FE 对比3GPP协议得到:(二进制代码从左到右依次为高位->低位) 短信中心地址字段 08 地址长度:8个字节,包括其后的91 91 地址类型:10010001 Bits 3,2,1:Numbering-plan-identification:一般默认为0001,表示电话号码类型的。 683108200805F0 短信中心号码:一个字节内反转,8613800280500,如果长度为奇数则需要加“F”补齐 FirstOctet字段 11 包含TP-MTI(2bit),TP-RD(1bit),TP-VPF(2bit),TP-RP(1bit),TP-UDHI(1bit),TP-SRR(1bit) 二进制表示形式:0 0 0 10 0 01 TP-MTI:01 TP-Message-Type-Indicator(消息类型指示符) Bit1,0:01 指示为SMS-SUBMIT类型 TP-Reject-Duplicates(是否拒绝相同重复消息) Bit2:0 指示短消息中心接受未转发的具有相同TP-MR的消息。 TP-VPF:10 TP-Validity-Period-Format(有效期格式) Bit4,3:10 指示使用相对格式。 TP-SRR:0 TP-Status-Report-Request Bit5:0 指示不使用状态报告。 TP-UDHI:0 TP-RP:0 消息参考值TP-MR 19 TP-Message-Reference 对方号码字段 0D91683188902848F4 其结构与短信中心号码字段部分类似,不再赘述。 协议标识TP-PID 00 TP-Protocol-Identifier(上层协议指示),一般设置为00,表示普通GSM,点对点 08 TP-Data-Coding-Scheme(数据编码设置),指示TP-UD的编码方式。08代表Unicode方式。00为7Bit编码 有效期TP-VP FF TP-Validity-Period(有效期)。FF表示最大。 用户数据长度TP-UDL 10 TP-User-Data-Length(用户数据长度) 0×10长度。注意不同编码下用户长度定义不同。 用户数据TP-UD 8FD9662F4E0067616D4B8BD577ED4FE TP-User-Data
2、 手机接收的PDU串 0891683108200805F0040D91683188902848F4000850208151754500108FD9662F4E0067616D4B8BD577ED4FE1 短信中心地址字段 0891683108200805F0:+861380280500 FirstOctet 04 其二进制代码:00000100 TP-MTI:00 TP-MMS(TP-More-Message-to-Send):1 短信中心没有更多的消息发送 TP-SRI:0 TP-UDHI:0 TP-RP:0 发送方号码 0D91683188902848F4:+8613880982844 协议标识 编码方式 08 TP-DCS Unicode编码 短信中心时间戳 用户数据长度 10 TP-DHL 用户数据 8FD9662F4E0067616D4B8BD577ED4FE1 TP-UD 中文“这是一条测试短信”的Unicode编码 (十) 短信部分——VB.NET解码PDU 早在2004年1月份我就开始初步的研究PDU的编码解码原理,对于PDU也有比较深刻的认识。随后按照3GPP协议写了一个PDU Decoder,后来写成PDU Decoder文章发表在CodeProject上面,有几个好心的外国网友给我指出了一些BUG,现在成了一个比较完善的Decoder。具体的源码在http://blog.csdn.net/hesicong/archive/2004/09/24/115356.aspx 。本文讲解编码器的构成以及我所使用的解码方法及技巧。 解码器的构成 NameSpace SMS Decoder MustInheritClass SMSBase Class EMS_RECEIVED Class EMS_SUBMIT Class SMS_RECEIVED Class SMS_STATUS_REPORT Class SMS_SUBMIT Class PDUDecoder SMSBase部分 SMSBase类是必须继承类,它包含了PDU的基本结构以及一些相关辅助函数,是最基本的类,其他的类都是从SMSBase继承的。通过SMSBase的Shared函数GetSMSType可以得到PDU的类型,从而确定使用的Class。 SMSBase包含了所有短信类型所共有的基本信息部分以及一个指示短信类型的枚举SMSType,继承的类扩展其特有的基本信息部分。 Public SCAddressLength As Byte ‘Service Center Address length Public SCAddressType As Byte ‘Service Center Type[See GSM 03.40] Public SCAddressValue As String ‘Service Center nuber Public FirstOctet As Byte ‘See GSM 03.40
Public TP_PID As Byte Public TP_DCS As Byte Public TP_UDL As Byte Public TP_UD As String Public Text As String Public Type As SMSType Public UserData As String
Public Enum SMSType SMS_RECEIVED = 0 SMS_STATUS_REPORT = 2 SMS_SUBMIT = 1 EMS_RECEIVED = 64 ‘It is “Reserved” on my phone?? EMS_SUBMIT = 65 End Enum
SMSBase中定义了一个必须重写的过程GetOrignalData,其参数为PDUCode,目的是为了得到PDU的基本信息。不同的短信类型具有不同的解码过程,所以作为一个必须重写的函数。 Public MustOverride Sub GetOrignalData(ByVal PDUCode As String)
SMSBase中还有一系列的辅助函数,具体实现方法见源代码: 处理PDU代码的: 处理PDU代码我运用了自称为“按需裁减”的技巧,就是把需要的数据提取出来解码,然后从原PDUCode中删除这一部分,在传递给下一个函数处理。这样就不用考略具体的偏移量,简化了操作,增强了适应性。为了能够减少返回处理过的PDUCode麻烦,我使用了ByRef,执行过程以后PDUCode就自动被裁减了。 ‘Get a byte from PDU string Shared Function GetByte(ByRef PDUCode As String) As Byte ‘Get a string of certain length Shared Function GetString(ByRef PDUCode As String, ByVal Length As Integer) As String ‘Get date from SCTS format Shared Function GetDate(ByRef SCTS As String) As Date ‘Swap two bit Shared Function Swap(ByRef TwoBitStr As String) As String ‘Get phone address Shared Function GetAddress(ByRef Address As String) As String Shared Function GetSMSType(ByVal PDUCode As String) As SMSBase.SMSType TP-UD解码部分: TP-UD的解码的任务主要集中在Unicode的解码和7BitCode的解码。其中Unicode的解码很方便,只需要将两个字节的PDUCode通过Val函数转换成为数字,在通过ChrW函数即可得到。 而7BitCode就显得比较难,下面以Test四个字符简单介绍其基本原理,具体的编码方式请参考相关资料。 Byte1 11010100 0xD4 Byte2 11110010 0xF2 Byte3 10011100 0×9C Byte4 00001110 0×0E 注:各字符二进制代码: T:1010100 e:1100101 s:1110011 t:1110100 从这个例子可以看出一个Byte包含了一个字符的ASCII码的二进制部分及后续字符的二进制部分的低位。这样8个字符可以压缩成为7个Byte,SMS中140Byte的TP-UD长度就可以容纳160个英文字母。 通过观察可以看出,只要我们从后到前把所有的二进制代码拼接到一块,就能够方便的处理,上面例子通过拼接后得到: 00001110100111001111001011010100 我们可以直接通过从后往前的按7个一组的原则进行截取在处理就可以得到解码后的代码。为了编程的方便,我设计了一个简单易懂的解码过程,比起通过做乘除法来进行运算的简单,但最终效率不及它。但我想在普通场合应用也绰绰有余了。 1、 Decode7Bit得到一个PDU的TP-UD部分 2、 InvertHexString反转十六进制代码:例如123456=〉563412 3、 Binary字符串得到反转后的十六进制代码的二进制表示。注意这里依然使用字符串来表示二进制,为了便于“拼接”和“切割” 4、 根据charCount所提供的字符数(来自TP_UDL)按7个一组从字符串位往前截取,并用Chr函数转换成ASCII码。 以下是一些函数的声明部分,具体函数请参见Blog内的PDUDecoder ‘Deoce a unicode string Shared Function DecodeUnicode(ByVal strUnicode As String) As String ‘Decode 7bit to English Shared Function InvertHexString(ByVal HexString As String) As String Shared Function ByteToBinary(ByVal Dec As Byte) As String Shared Function BinaryToInt(ByVal Binary As String) As Integer Shared Function Decode7Bit(ByVal str7BitCode As String, ByVal charCount As Integer) As String
SMS_SUBMIT、SMS_RECEIVED、SMS_STATUS_REPORT 由于SMS_RECEIVED、SMS_STATUS_REPORT与SMS_SUBMIT比较相似,所以我重点讲讲SMS_SUBMIT。 当用SMSBase的GetSMSType确定一个PDUCode为SMS_SUBMIT时,就可以声明一个SMS_SUBMIT类的实例,通过传递此PDUCode作为构造函数的参数。构造函数立即调用GetOrignalData函数解码。 参考协议知道SMS_SUBMIT比SMSBase多出以下部分: Public TP_MR As Byte Public DesAddressLength As Byte Public DesAddressType As Byte Public DesAddressValue As String Public TP_VP As Byte 参考协议我们可以很方便的得到GetOrignalData函数的实现: Public Overrides Sub GetOrignalData(ByVal PDUCode As String) SCAddressLength = GetByte(PDUCode) SCAddressType = GetByte(PDUCode) SCAddressValue = GetAddress((GetString(PDUCode, (SCAddressLength - 1) * 2))) FirstOctet = GetByte(PDUCode)
TP_MR = GetByte(PDUCode)
DesAddressLength = GetByte(PDUCode) DesAddressType = GetByte(PDUCode) DesAddressLength += DesAddressLength Mod 2 DesAddressValue = GetAddress((GetString(PDUCode, DesAddressLength)))
TP_PID = GetByte(PDUCode) TP_DCS = GetByte(PDUCode) TP_VP = GetByte(PDUCode) TP_UDL = GetByte(PDUCode) TP_UD = GetString(PDUCode, TP_UDL * 2) End Sub 这就完成了整个解码过程,通过SMSBase的巧妙设计,此解码过程显得简单方便。
EMS_SUBMIT、EMS_RECEIVED 对于EMS(增强型短信),其基本结构和SMS类似,主要的区别就是Information Element(IE)。所以EMS_SUBMIT继承了SMS_SUBMIT,EMS_RECEIVED继承了SMS_RECEIVED 参考3GPP协议EMS部分我们可以做出以下的结构和定义 Public Structure InfoElem ‘See document “How to create EMS” Public Identifier As Byte Public Length As Byte Public Data As String End Structure Public TP_UDHL As Byte 为了得到IE我写了一个函数: Shared Function GetIE(ByVal IECode As String) As InfoElem() Dim tmp As String = IECode, t As Integer = 0 Dim result() As InfoElem Do Until IECode = “” ReDim Preserve result(t) With result(t) .Identifier = GetByte(IECode) .Length = GetByte(IECode) .Data = GetString(IECode, .Length * 2) End With t += 1 Loop Return result End Function 然后参考协议可以写出GetOrignalData函数。具体就不再赘述。
PDUDecoder 这个类的由一个结构,一个重要的解码函数,组成。 结构定义了需要取得的基本信息,可以视需要修改。我这里提供一个范例 Public Structure BaseInfo Public SourceNumber As String Public DestinationNumber As String Public ReceivedDate As Date Public Text As String Public Type As SMS.Decoder.SMSBase.SMSType Public EMSTotolPiece As Integer Public EMSCurrentPiece As Integer Public StatusFromReport As SMS_STATUS_REPORT.EnumStatus Public DestinationReceivedDate End Structure 解码函数的声明如下: Public Shared Function Decode(ByVal PDUCode As String) As BaseInfo 内部主要处理步骤如下(源代码请参考PDUDecoder) 1. 根据SMSBase的GetSMSType函数得到短信类型SMSType 2. 根据SMSType生成对应的类的实例 3. 解码PDU,得到基本结构 4. 通过基本结构得到BaseInfo结构里面需要的数据 5. 通过decode7bit或者decodeUnicode函数得到TP_UD数据
到此为止,这就是整个PDU Decoder的详细介绍,具体使用可以参见Siemens Support Tool里面相关部分,在此不再赘述 PDU的编码器的工作原理是解码器的逆过程。根据需要编码器只需要编码发送的PDU代码,工作相对简单。本文讲解编码思路,具体代码请参考Blog中PDUEncoder部分 我把PDU的编码分为两部分,SMS和EMS。EMS部分我只提供了ConcatenatedShortMessage的编码器。这是超长短信的编码,用得最多。 SMS编码 编码一个SMS一般需要如下的信息: TP_Data_Coding_Scheme TP_UD编码方式 TP_Destination_Address 对方号码 TP_Message_Reference 参考号码 TP_Status_Report_Request 状态报告 TP_User_Data 用户信息 TP_Validity_Priod 有效期 ServiceCenterNumber 短信中心号码 所以在编码器中存在以上的属性,并在Set中加入了处理代码,将可读信息转换成对应的十六进制信息。 特别注意的是TP_User_Data属性,它可以根据用户数据编码自动设置TP_UDL。对于纯英文编码,TP_UDL为所有的字符数;对于Unicode编码,由于一个字符由两个字节表示,TP_UDL为所有的字符数*2。注意检查TP_User_Data的长度,对于SMS来说编码后的TP_UD长度不能超过140字节。也就是说英文160个字符(140/7*8),中文70个字符。 对于TP_UD的编码在解码器中也有说明,在此不再赘述。 我还设计了几个枚举变量: ENUM_TP_DCS 编码方式 ENUM_TP_SRI 状态报告 ENUM_TP_VALID_PERIOD 有效期 ENUM_TP_VPF 有效期格式 这些枚举变量可以简化输入,也利于日后扩充。 当以上内容设置好以后,基本上一个短信的架子就出来了。此时调用GetSMSPDUCode进行组合,简单的把十六进制拼接起来就形成了一个完整的PDU代码。 EMS——ConcatenatedShortMessage部分 编码EMS较SMS复杂,但每条EMS的基础还是SMS,所以我直接继承了SMS类。区别主要是要处理好TP_UD和IE。对于ConcatenatedShortMessage,由于其IE和TP_UDHL占据了TP_UD的部分空间,所以每条短信英文只能容纳133字符,中文66字符。我们可以通过此信息得到短信条数。 如果TP_DCS为Unicode编码,则短信条目为: 如果为7bit,则为: TotalMessages = (tp_ud.Length \ 266) - ((tp_ud.Length Mod 266) = 0)+1 注意在程序中我为了简化以后的数组操作,就没有加一。 确定了短信条数以后通过一个循环就可以提取出每条短信的TP_UD。 Select Case tp_dcs Case ENUM_TP_DCS.UCS2 tmpTP_UD = Mid(TP_UD, i * 66 * 4 + 1, 66 * 4)’When TP_UDL is odd, the max length of an Unicode string in PDU code is 66 Charactor.See [3GPP TS 23.040 V6.5.0 (2004-09] 9.2.3.24.1 Case ENUM_TP_DCS.DefaultAlphabet tmpTP_UD = Mid(tp_ud, i * 133 * 2 + 1, 133 * 2) End Select 此后还需要编码IE部分,关键代码是确定TP_UDL的值。对于TP_DCS为7bit来说确定此值显得比较复杂,弄不好容易出现多一个少一个的错误。 If tp_dcs = ENUM_TP_DCS.UCS2 Then TP_UDL = tmpTP_UD.Length / 2 + 6 + 1 ‘6: length of IE End If If tp_dcs = ENUM_TP_DCS.DefaultAlphabet Then TP_UDL = Fix((tmpTP_UD.Length + 7 * 2) * 4 / 7) ‘6:length of IE End If 然后根据3GPP里关于EMS的结构的说明就可以编写出EMS PDU的处理程序。详见原代码。 如果需要扩展EMS以适应更多种类的EMS,可以参考3GPP写出更为强大的编码程序。但最关键的还是需要处理好IE以及TP_UDL。 (十二) 短信部分——通过RS232发送和接收短信 通常,发送和接收短信的终端都是通过串行接口连接电脑,这类设备用得比较多的是GSM Modem和手机。这类设备通常都支持PDU模式,但仍有少数设备只支持Text模式。 设备硬件连接好以后可以通过发送AT指令测试设备是否连接正确能否正常通讯。在这里我使用Windows自带的“超级终端”工具进行通讯。此工具可以在程序——附件——通讯里面找到,如果没有请确认是否安装了此组件。或者在运行里面输入“hypertrm”也可以快速启动“超级终端”。 以下所有范例均以Siemens M55手机作为终端,有可能跟你设备返回的不同。具体参数清参阅设备相关AT指令集。本手机AT指令集在我主页上有下载。 准备工作: 1、 测试连接:“AT8 ”测试终端是否连接正确。成功后返回“OK”。 2、 设置回显:(此步骤为了测试方便)ATE18 3、 查阅及设置字符集: 4、 取得短信中心号码: 5、 查询并设置SMS格式: 6、 查询并设置短信储存位置: 查询短信: 1、 查询具有相同状态的所有短信 0891683108200805F011620D91683194041338F50000FF0530972D8603 3——状态:发送 20——PDU串长度 2、 查询特定序号的短信 注意:PDU串长度表示PDU中除去短信中心部分剩下的代码的长度的1/2。例如上述PDU中PDU长度部分为11620D91683195041338F50000FF0530972D8603,40个字符,表示20个字节。
储存PDU > 0891683108200805F011620D91683195041338F50000FF0530972D8603 返回:+CMGW: 85
发送PDU串 1、 发送输入的PDU串 2、 发送指定序号的PDU串
接收短信 接收刚收到的短信有两种方法:轮询终端;使用事件 轮询终端可以定期的使用AT+CMGL=0指令读取未读取得指令。方法简单,但许多时候都在做无用功,效率低下,一般不建议采用。下面主要讲解事件法: 指令: 参数: 0——缓存在终端 1——直接发送到TE mt: 0——接收到新的SMS不返回事件 1——如果接收到的SMS存储在ME,则返回 2——除了Class2 SMS,新的SMS直接发送到终端,返回: 3——Class3 SMS使用mt=2的方法返回,其他类型的使用mt=1的方法返回。 bm: 0——小区广播不通知 2——新的小区广播通知,返回 3——Class3格式的小区广播通知,使用bm=2格式 ds: 0——状态报告不通知 1——新的状态报告通知,返回: 2——如果新的状态报告存储到ME,则返回: +CDSI:, brf: 1——始终为1 例: 一般我们使用AT+CNMI=1,1,0,2,1 当收到新的短信时终端返回: +CMTI:ME,5 新的状态报告: +CDSI:ME,6 程序可以通过判断返回值并使用AT+CMGR指令返回新到短信 |
|