20140629

读卡器的状态机, python实现

读卡器的状态机, python实现

1. 问题的提出,及状态机简介

ZHUMAO整了个门禁用的读卡器,比以前那种更好,不需要发指令就能读,只要刷
卡,读卡器就向串口上写数据。仍然是串口的,还是韦根协 议。"刷卡就向上写"
避免了轮询读卡器,效率更高,代码也容易了。不过,也造成一个问题。下发命
令,然后轮询读的模式下,如果在串口线上只有 一个读卡器,不需要对输入的数
据特别检验和处理,接收到的数据一定是对的,按协议读入多少个字符,然后按偏
移量取有效的部分就行了。"刷卡就 向上写"的模式,需要保证对齐,必须从刷卡
后产生的第一字符开始读,读到最后一个,要避免从中间读起。如果中间出现了噪
音之类的干扰,由于不 能下发指令要求对齐 (或者开始读),就再也找不到开始位
置,数据全乱。

解决这个问题的方案是状态机。

状态机是个著名的数学模型,在数字电路、编译原理、面向对象系统分析与设计、
形式语言与状态机中都有提及。状态机效率很不错,刘典同学曾经用 状态机模型
写程序参加过CSDN上的比赛,检验IP地址是否合法,获得过第二名。状态机描述问
题清晰,邦哥和亮哥曾经用状态机重写安卓程序的 界面部分,把原来"朴素"方法
可能出现的BUG都去除了。

正确的思考方法是有效的工具,在解决问题中非常重要。人类通常不懈于在猛兽面
前炫耀速度和力量,而是使用弩箭和陷阱。所以,工具对于成为人类 多么重要。
所以,不用状态机,而依靠单纯的智力是多么愚蠢。

2. 问题描述

该型号读卡器上传的数据看起来是这样的,以十六进制表示,"02 XX XX XX XX 0d
0a 03"。其中的4个 XX 表示卡号,是我们感兴趣的部分。其余的部分必须匹配,
才能说明读卡器正常工作,卡号有效。

主程序准备写成这样:

1 if __name__ == '__main__':
2 s = state_machine()
3 t = serial.Serial('COM3')
4 while True:
5 str = s.go(t)
6 print str.upper()

其中第2行初始化一个 state_machine 类的实例。在第4行开始的循环中,每次迭
代都调用 s.go(t),把串口传给状态机。在状态机的go中,读很多次串口,直到遇
到一次完整有效的卡号,作为返回值,在第6行中打印出来。这个串口t如果改为
在 state_machine 的构造函数中,会更好一些。不过我对python语法不熟,大部
分时间都消耗在查语法手册上了,头昏眼花,当时就写成了这样。

当然,真实程序的目标不是打印卡号,而是用卡号作为检索条件,去数据库里查询
和更新一些数据。

3. 状态机

开始写状态机,才发现 python 居然没有 switch-case。可见我对语法得多么不熟。

根据 "02 XX XX XX XX 0d 0a 03",状态转换如图所示。状态图中的关键是,我在
哪个状态,接收到哪个消息后,会迁移到哪个状态,在迁移的过程中,会做哪些动作。


4. 状态机代码解释

代码如附录A所示。state_machine 是一个类 类型。

成员变量,count用于计数,在0x00状态 (及0x02状态) 一共接收了几个字符,这
些字符应该添加到有效卡号 (成员变量ret)的末尾。成员变量state,是当前的状
态,其初始状态是 0xff。

成员函数 str2hex 是从zhumao那里抄来的,用于把二进制转为十六进制文本形式。

成员函数 go (self, ser) 是核心部分。每次调用 go,它会从串口读入一个字
符,并把这个字符作为发送给状态机的消息;状态机根据自己的 (1) 当前状态
state,然后再根据这个 (2) 消息 c,判定 (3)应该迁移到哪个状态, (4)应在迁
移时做哪些动作。

在状态图中,(1) 当前状态标记为椭圆,箭尾所指的那个椭圆, (2)消息,标记为
线上的文字,斜线"/"左边的部分, (3)应该迁移到哪个状态,箭头所指的那个椭
圆, (4)在迁移时做的动作,标记为线上的文字,斜线右边的部分。

成员函数 go,一旦读到的字符可以拼成一个有效的卡号 (包括0x03也已读入),就
给出卡号作为返回值,退出 go 函数,控制权转交回主函数。如果尚未形成有效的
卡号,就继续在 go 里面转。

如果你想测试,还没有找到读卡器。那么把第12行改成从一个二进制文进中读入。

附录A 状态机代码

1 class state_machine:
2 count = 0
3 state = 0xff # 02 XX XX XX XX 0d 0a 03
4 ret = ""
5 def str2hex(self, c):
6 hvol = ord(c)
7 hhex = '%02x'%hvol
8 return hhex
9
10 def go(self, ser):
11 while True:
12 c = ser.read(1)
13 c = self.str2hex(c)
14 # print self.state
15 # print c
16 # print self.ret
17 # print
18 if self.state == 0xff:
19 if c == '02':
20 self.state = 0x02
21 self.ret = ""
22 continue
23 if self.state == 0x02:
24 self.state = 0x00
25 self.count = 0
26 self.count=self.count+1
27 self.ret = self.ret + c
28 continue
29 if self.state == 0x00:
30 if self.count<4:
31 self.count=self.count+1
32 self.ret = self.ret + c
33 self.state = 0x00
34 continue
35 else:
36 if c == '0d':
37 self.state = 0x0d
38 continue
39 else:
40 self.state = 0xff
41 self.ret = ""
42 continue
43 if self.state == 0x0d:
44 if c == '0a':
45 self.state = 0x0a
46 continue
47 else:
48 self.state = 0xff
49 continue
50 if self.state == 0x0a:
51 if c == '03':
52 self.state = 0x03
53 self.state = 0xff
54 return self.ret
55 else:
56 self.state = 0xff
57 continue
58 else:
59 continue

附录 B 状态机图示的源代码 in graphviz

digraph state
{
graph [ nodesep=1.2]
start -> "0xff" ;
"0x00" [color=red];
"0xff" :e -> "0x02" : e [label="0x02"];
"0x02" :e -> "0x00" : e [label="任意字符 / (count=1,该字符填入卡号
末尾)", color=red, fontcolor=red];
"0x00"-> "0x00" : e [label="任意字符 & count<4 / (count+=1,该字符
填入卡号末尾)", color=red, fontcolor=red];
"0x00" :e -> "0x0d" : e [label="0x0d & count>=4"];
"0x00" :e -> "0xff" : e [label="不是0x0d & count>=4"];
"0x0d" :e -> "0x0a" : e [label="0x0a"];
"0x0d" :e -> "0xff" : e [label="不是0x0a"];
"0x0a" :e -> "0x03" : e [label="0x03"];
"0x0a" :e -> "0xff" : e [label="不是0x03"];
"0x03" :e -> "0xff" : e [label="无条件"];

}


--------------------

博客会手工同步到以下地址:

[http://giftdotyoung.blogspot.com]

[http://blog.csdn.net/younggift]
=======================

No comments: