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"];
"0xff" :e -> "0xff" : 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]
=======================

读卡器的状态机, 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]
=======================

增加PDF文件对比度的粗浅原理,及方法

增加PDF文件对比度的粗浅原理,及方法

最近的照片在[http://www.douban.com/photos/album/134230762/]。

电子书扫描版跟照片差不多,一个可能的问题是看起来乌突突的,黑的不够黑,白
的不够白,像是蒙了一层雾。用picasa的luck功能或者 snapseed的automatic功
能,都能把图片上的雾去掉。但是同样的功能用在PDF上的软件我没有找到。

但是如果弄懂了原理--让我想起 倚天屠龙记 里的杨左史,长戟大刀蛾眉刺各种武
器的招法都是可以溶入拳法之中的。

就黑白 (严格地说,灰度)图片而言,"去雾"的基本原理是增加对比度。人眼看到
的全黑、全白,以及其间所有的过渡色调,在计算机图片中都是用数字来表示的。
人眼 是非常精密和拥有极大检测范围的光学仪器,从明亮到直视的太阳,到电影
院里黑暗的座号,都能看到。而计算机图片只能展示人眼可承受的范围中非 常小
的一部分。这部分中最重要的信息(对于黑白图片而言是全部)是亮度。计算机图片
用数字的大小来表示亮度,越亮的,也就是越白的,用较大的 数字,越暗的,也
就是越黑的,用较小的数字表示。

这样,比如说,图片上的每个像素,从最暗到最亮,就对应着从0到255之间的数
字。各种图像处理软件中的gamma校正就是用来指定这种对应 关系的。这种对应可
能是线性的,也可能是非线性的,在非线性的情况下,可能数字变化了很多,而亮
度变化非常小--类似于你调洗澡时的冷热水, 扭了半天,还是挺凉,突然就热得
能烫突鲁皮。你可能觉得,这不是有毛病吗?是的,这种非线性有时会带来麻烦,
但是,同时它还有个优点,那就是 识别度 好。

我们规定一个数值,低于它就是全黑,不管数值如何变化;再规定一个数值,高于
它就是全白,不管数值如何变化。这两个数值就是黑到灰和灰到白的 阀值。之所
以低于某数就视为全黑,是因为我们可能不希望呈现这部分暗调的细节,之所以高
于某数就视为全白,是因为我们可能希望把这些都表现为 高光。

捕捉到的现实是一回事,如何呈现,是完全不同的另一回事。二猫昨天问到,为什
么书里净写些吓人的事情,既然它们很少发生。我说,如果灰姑娘的 故事真的完
全写实,那么,整个故事里你大部分时间看到的都不是她遇到王子的舞会和红舞
鞋,而是灰姑娘成天给她后妈和后姐们做饭做饭做饭做饭做 饭做饭做饭……做饭

同样,一本PDF电子书如果扫描不当,也可能呈现了过多的暗部和高光的细节,而
灰色调子的过渡层次不够分明。暗部和高光的范围可以通过调整黑 和白的阀值完
成,下面会继续介绍;灰色调子的过渡层次,可以通过细调gamma校正完成,本文
不讨论,请自学。

所以,提高有雾电子书的对比度,原理就是重新设置黑和白的阀值。以上是原理部
分,以下是操作方法。

知乎上有人说,把pdf中的图片都导出来,用photoshop处理调整gamma或者设置黑
和白的阀值,然后再导入成PDF。原理正是如 此,但是这需要安装高大上的收费工
具photoshop和acrobat professional,还可能需要一页一页操作 (?) ,有些杀
鸡用牛刀的意思。下面介绍成批处理的方法。

第1步 抽样,使用 ImageMagick 把 PDF 转换为图片。事实上,我们只需要其中的
一两张,不过我不知道如何完成。ImageMagick是linux下和windows下都有的工
具,支持命令行。

$ convert 1.pdf 1.jpg

这样,得到了一大批jpg图片。

第2步 尝试,使用 GIMP 确定白色和黑色的阀值。用GIMP也行,用photoshop也
行,凡是能调灰度图片阀值的,都行。调到你感觉最好,没错,就是"感觉",这是
个主观标 准。然后把白色和黑色的阀值记下来。GIMP是linux下的photoshop。

以上两步不做也行,那么,接下来的阀值就可以靠猜、靠经验、靠多次尝试。

第3步 使用 ImageMagick 修改对比度

$ convert -level 38%,99% 1.pdf 2.pdf

这行命令的意思是:低于38%的,视为黑,高于99%的,视为白,把源文件1.pdf转
换后存为目标文件2.pdf。

因为调整阀值的同时,也自动调整gamma线,使之更陡峭,所以灰色调子的层次会
更鲜明一些。



原理讲了一大堆,还是只是精浅的介绍,而方法中真正起作用的只是一行命令。所
以,这个故事告诉我们,知道怎么干很容易,而知道为什么这么干会 成功就难得
多。当然,唯其如此,才能把长戟大刀蛾眉刺的功夫用在拳法掌法这些徒手格斗之
中。或者,唯其如此,才能把阴影透视和色彩模型应用在 画妆上。

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

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

[http://giftdotyoung.blogspot.com]

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