.net平台手机管理软件开发(10)—— 短信部分 VB.NET解码PDU

(十) **短信部分——VB.NET**解码PDU

早在2004年1月份我就开始初步的研究PDU的编码解码原理,对于PDU也有比较深刻的认识。随后按照3GPP协议写了一个PDU Decoder,后来写成PDU Decoder文章发表在CodeProject上面,有几个好心的外国网友给我指出了一些BUG,现在成了一个比较完善的Decoder。本文讲解编码器的构成以及我所使用的解码方法及技巧。
解码器的构成
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 0x9C
Byte4 00001110 0x0E

注:各字符二进制代码:
T:1010100 e:1100101 s:1110011 t:1110100

从这个例子可以看出一个Byte包含了一个字符的ASCII码的二进制部分及后续字符的二进制部分的低位。这样8个字符可以压缩成为7个Byte,SMS中140Byte的TP-UD长度就可以容纳160个英文字母。

通过观察可以看出,只要我们从后到前把所有的二进制代码拼接到一块,就能够方便的处理,上面例子通过拼接后得到:
00001110100111001111001011010100

我们可以直接通过从后往前的按7个一组的原则进行截取在处理就可以得到解码后的代码。为了编程的方便,我设计了一个简单易懂的解码过程,比起通过做乘除法来进行运算的简单,但最终效率不及它。但我想在普通场合应用也绰绰有余了。
. Decode7Bit得到一个PDU的TP-UD部分
. InvertHexString反转十六进制代码:例如123456=〉563412
. Binary字符串得到反转后的十六进制代码的二进制表示。注意这里依然使用字符串来表示二进制,为了便于“拼接”和“切割”
. 根据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里面相关部分,在此不再赘述。

.net平台手机管理软件开发(9)—— 短信部分之PDU简介及其格式

(九) **短信部分——PDU**简介及其格式
PDU是大多数手机短信通讯的核心,仅有少数手机只支持Text模式(例如笔者的MOTO C330)。PDU模式比起Text模式可以提供能为强大的功能,但其编码较Text模式困难。无论哪种模式,我们都可以通过AT指令控制终端实现短信的发送、接收、删除等管理。下面主要介绍PDU的构成及编码解码。
PDU**的构成** PDU是由一串由“0-9”及“A-F”组成。表面上看起来就是一组16进制的数所组成的。

下面举一个发送和接收的例子。
. 手机发送的一个PDU串:
0891683108200805F011190D91683188902848F40008FF108FD9662F4E0067616D4B8BD577ED4FE

对比3GPP协议得到:(二进制代码从左到右依次为高位->低位)
短信中心地址字段
08 地址长度:8个字节,包括其后的91

91 地址类型:10010001
Bit7:1。始终为1
Bits 6,5,4:Type-of-Number(号码类型):001,代表Internation Number。也即是号码前加“+”。注意:对某些比较特殊的号码,例如手机与小灵通的互通时,这里不能设置为001,而要设置成000,代表号码前没有“+”,否则无法接收。
Bits 3,2,1:Numbering-plan-identification:一般默认为0001,表示电话号码类型的。
683108200805F**0** 短信中心号码:一个字节内反转,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-RD**:0** 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-User-Data-Header-Indicator(用户数据头标示)
Bit6:0 指示这是一个SMS消息,没有用户数据头。EMS消息需要设置。

TP-RP**:0
** TP-Reply-Path(回复路径)
Bit7:0 指示没有设置回复路径。
消息参考值TP-MR
19 TP-Message-Reference
** **对方号码字段
** **0D91683188902848F4

其结构与短信中心号码字段部分类似,不再赘述。
协议**标识**TP-PID

** 00 TP-Protocol-Identifier(上层协议指示),一般设置为00,表示普通GSM,点对点
** **
编码方法TP-DCS** 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(用户数据长度)**** 0x10长度。注意不同编码下用户长度定义不同。

用户数据TP-UD****

8FD9662F4E0067616D4B8BD577ED4FE TP-User-Data

中文“这是一条测试短信”的Unicode编码

. 手机接收的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

协议标识
** 00 TP-DCS 点对点
**编码方式

08 TP-DCS Unicode编码

短信中心时间戳
50208151754500 TP-SCTS 字节反转05/02/18 15:57:45 最后的00代表时区,这里为0

用户数据长度
10 TP-DHL

用户数据
8FD9662F4E0067616D4B8BD577ED4FE1 TP-UD

中文“这是一条测试短信”的Unicode编码

手机——小灵通互发短信PDU编码注意事项

昨天花了一些时间解决了网友GSM Modem与小灵通发送短信的问题,发现是由于在小灵通号码之前默认加了“+”的缘故。

在 PDU编码中有一个Address Field,其中有一个Address Type段,其值在很多文章里面说固定为0x91。其实这是不对的。按照3GPP 23040-650对于这个字段的说明,0x91是国际通用的,也就是在号码之前加一个“+”号。但对于现在小灵通的 106+区号+号码 这样的格式,将Address Type固定为0x91就变成了 +106+区号+号码 格式,短信中心可能会认为是国际短信,可能发送到其他国家,也有可能发送失败。

所以,对于这种情况,将0x91改成0x81,即可解决。

对于程序的流程,希望能够增加“+”号的判断。如果号码前面有“+”,AddressType值为0x91,否则值为0x81。

.net平台手机管理软件开发(8)—— vCard、vNote、vCalender格式简介

(八) 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

注:
. PropertyName及PropertyParameters不区分大小写。
. PropertyParameters可选,可以为零个或多个,与ProperyName以分号相隔,与PropertyValue以冒号相隔。
. 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

.net平台手机管理软件开发(7)——IrMC简介

(七) IrMC简介

要实现通讯薄、日历、便签的同步,需要用到IrDA协议里面的IrMC部分。
IrMC全称为Ir Mobile Communications。它定义了利用IR无线传输的设备之间通讯的规则。IrMC协议文档详细叙述了IrMC的方方面面,可以在我的主页(http://dream-world.nease.net)下载到PDF版本。

下面主要讲述我在开发当中所用到的部分以及实现方法。更为详细的资料请参考IrMC协议。
Phonebook

在手机软件桌面端通讯薄的管理是整个软件的必备功能之一,利用其信息可以方便的和Outlook等软件实现同步,实现更强大的功能。

通讯薄的管理分为读取、删除、增添、修改。通过这几个功能的组合可以实现更为强大的同步功能。下面分条概述。
l 读取

n 读取所有的Entry

使用OBEX的GET命令取得telecompb.vcf文件。

得到的的文件是一个vCard文件,里面包含了所有的通讯薄。其格式为vCard格式。关于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

也可以通过X-IRMC-LUID所给的LUID号码直接取得相应的vCard。

  1. 读取指定LUID的Entry

使用OBEX的GET命令取得telecompbluidxxxx.vcf,其中xxxx代表了LUID号码。

得到的依然是一个vCard文件,只不过只包含特定LUID号码的vCard信息。可以简单的通过Outlook查看vCard所包含的信息。

l 删除

欲删除一个Entry

  1. 连接到IrMC_Sync_Service(发送0x80, 0x0, 0x13, 0x10, 0x0, 0x40, 0x6, 0x46, 0x0, 0xC, 0x49, 0x52, 0x4D, 0x43, 0x2D, 0x53, 0x59, 0x4E, 0x43)使手机处于同步状态。

  2. 得到ChangeCount
    ChangeCount是手机里面储存修改次数的一个数值,主要用于同步。具体相关资料请参考IrMC协议。

  3. 构建数据包。
    Opcode:PUT
    NameHeader:xxxx.vcf(xxxx为欲删除的entry的LUID)
    AppParam:0x11,ChangCount字符串形式长度(Integer),ChangeCount的ANSI形式

  4. 使用OBEX的PUT命令,传输一个xxxx.vcf(xxxx指LUID)空文件到telecompbluid覆盖即可。

  5. 发送0x81, 0x0, 0x3断开IrMC_Sync_service

l 添加

欲添加一个文件:

  1. 连接到IrMC_Sync_Service

  2. 得到ChangeCount

  3. 构建数据包
    Opcode:PUT
    NameHeader:.vcf
    AppHeader:0x11,ChangeCount字符串长度(Interger),ChangCount的ANSI形式
    Body或者End-of-Body Header:vcf文件内容

  4. OBEX的PUT命令上传到telecompb.vcf文件即可。

注意:文件名取名为“.vcf”。

  1. 发送0x81, 0x0, 0x3断开IrMC_Sync_service

l 修改

修改过程与添加过程相似,只是将文件名改为欲修改的vCard的LUID.vcf就行了。

Notes

便签是大多数手机都提供的功能,能够方便的记录简短的信息。在我的M55手机上能够储存150Byte的信息,也就是150个英文或者75个汉字。软件通过管理便签可以与Outlook等软件同步,实现更高级的功能。

管理Notes的方法和Phonebook类似。得到全部Notes的vNote只需要获取telecomnt.vnt即可。删除、添加、修改只需要把telecompbluid改为telecomntluid即可。在此不再赘述。
Calendar

日历功能提供了事件提醒功能,分为重要记事(vEvent)和任务(vTodo)。通过管理日历,同样可以实现和Outlook同步,实现电脑和手机的同步。

管理Calendar的方法和Phonebook类似。得到全部vCalendar只需要获取telecomcal.vcs。删除、添加、修改只需要把telecompbluid改为telecomcalluid即可。在此不再赘述。

但值得注意的是vCalendar的结构
BEGIN:VCALENDAR
VERSION:1.0
BEGIN:VEVENT
….
END:VEVENT
BEGIN:VTODO
… END:VTODO
END:VCALENDAR

完整的vCalender包含了至少一个vEvent或者一个vTodo,因此在添加、修改vEvent、vTodo时要将其补充为一个完整的vCalender结构,否则服务端会拒绝操作。

遇到的问题:

在实际操作中,遇到问题最多的地方在Phonebook部分。我的手机第一次同步的时候经常出现数据库被锁的情况,用SiMoCo读取也是一样,说明是手机拒绝写操作。这时候关闭手机再重新启动就好了。至于原因,我还没有搞清楚,希望能有高人指点。