普通青年的状态机,纯C语言

我们第一次接触到状态机,是在数字电路课程里。计数器、串行奇偶检校、 检验

三个1连续出现的报错电路 等,都需要状态机作为模型。实现这些功能的电路,与

状态机的状态转换图、状态转换表都是等价的。

后来,我们再接触状态机,是在编译原理课程里。状态机用于描述与正则表达式匹

配的字符串。

再后来,我们在GUI界面设计中,需要设置一些控件在某些条件下 禁用,某些条件

下使能,某些条件下打个对号。这也可以用状态机模型来控制。

1. 不要写成 消息响应/事件处理

状态机和消息响应都是 双层 switch-case 结构。不同的是,状态机的外层是状

态,内层是消息;消息响应外层是消息,内层是状态。

有的同学会说,那又有多大的区别呢?代码只是外在形式而非本质,它所反应的是

你对模型的理解,或者说,对于问题,你使用了哪种模型。

消息响应适合于这样的情形:有很多种消息,对于同一种消息,你的程序总是给出

同一种反应。打个比方,你女朋友喜欢吃冰淇淋,任何时候你给她 买,她都高

兴,或者转怒为喜,或者转悲为喜,总之,会置心情为"喜"。这种情形,适合用消

息响应解决。

而状态机适合于另一种情形,你的程序是"有状态的",它在不同的情况 (状态)

下,会对同一消息做出不同的反应。状态,是一种数据,但是它影响流程的行为。

按面向对象的观点,数据与流程间的这种高内聚关系,非常适合用 类 来实现。这

是题外话,我们回到女朋友和冰淇淋间的关系。你女朋友可能并非在任何情况下吃

了冰淇淋都高兴,比如刚刚吃完十个八个的时候...这与她当前的状 态有关。

状态机中,我们需要掌握的核心的数据是:当前状态,当前消息,将迁移到的状

态,在迁移中发生的动作。

在状态机代码之前,请先看一段消息响应机制,VC生成的win32api代码大抵如此。

我们随便找来一段片断看看:

1 LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam,

LPARAM lParam)

2 {

3 int wmId, wmEvent;

4 PAINTSTRUCT ps;

5 HDC hdc;

6 switch (message)

7 {

8 case WM_COMMAND:

9 wmId = LOWORD(wParam);

10 wmEvent = HIWORD(wParam);

11 case ID_MENU_GO: .... break;

12 case IDM_ABOUT: .... break;

13 case IDM_EXIT: .... break;

14 default:

15 return DefWindowProc(hWnd, message, wParam, lParam);

16 }

17 break;

18 case WM_PAINT: .... break;

19 case WM_DESTROY: ... break;

20 case WM_KEYDOWN: ... break;

21 default: return DefWindowProc(hWnd, message, wParam, lParam);

22 }

23 return 0;

24 }

第6行开始到第22行结束,对每个消息给出一个响应。没错,win32api也把这个传

进来的东西称为 message。这是很典型的适合消息响应机制的情形,程序对于相同

的消息,处理的方法总是相同的。

我们常常错误地把状态机写成了消息响应,消息这部分处理得不错,但是,由于没

有很好地记录和迁移状态,写起来容易把自己写糊涂了。无他,用错 了工具。拿

螺丝刀打孔,不是工具差,而是工程师选错了工具。

2. 状态机实例,录音机

实例得是相对简单的,不然我们很容易淹没在细节之中,没有足够精力去关注状态

机本身的机制了。假设我们仿真一台录音机...

我们先假设你见过录音机。录音机是一种曾经先进的设备,有一个或两个"卡",可

以放进磁带。"卡"前面有几个按键,这几个按键上的标识因为图 形简单且示意性

强,现在还在广泛使用。它们分别是 播放 > 、暂停 || 、快进 >> 、快退<< 、

录音 O 、停止 []。

这几个按键之间是有一定的"互斥关系"的。比如当播放键按下时,我们不应该能把

快进键按下。当然,淘气的同学可能这样干过,我们会听到"咔咔"的声音,然后是

家长骂败家玩艺的声音。可以就"互斥关系"开始写程序,但是我觉得这样有点 麻烦。

我们认为,这种"互斥关系"是因为录音机是"有状态的"。所以,我们打算用状态机

来实现。状态转换图是这样的。请读图的时候关注这四点:当前 状态,当前消

息,将迁移到的状态,在迁移中发生的动作 (本例中没有) 。

digraph state

{

graph [ nodesep=1.2];

rankdir = LR;

播放 -> 暂停 [label="按下 || "];

暂停 -> 播放 [label="按下 || "];

暂停 -> 停止 [label="按下 []"];

停止 -> 播放 [label="按下 >"];

播放 -> 停止 [label="按下 []"];

停止 -> 快退 [label="按下 <<"];

停止 -> 快进 [label="按下 >>"];

快进 -> 停止 [label="按下 []"];

快退 -> 停止 [label="按下 []"];

停止 -> 录音 [label="按下 O"];

录音 -> 停止 [label="按下 []"];

}

备注:我实在想不起来 暂停 和 停止 之间的关系了,似乎是这样的,又似乎不

是。反正大概是那么个意思,不影响对状态机的理解,就这么地吧。

接下来是C代码实现。

3. 接口 及 测试

看到以下代码,有的同学会说,你这不就是主程序么,为什么要把小标题叫做接

口。因为,它规定了我们的状态机函数将是什么样子的。

1 enum message { play, stop, forward, backward, record, pause };

2

3 int main(int argc, char *argv[])

4 {

5 char c=0x00;

6 while(1)

7 {

8 c = getchar();

9 switch(c)

10 {

11 case ' ': state_change(pause); break;

12 case 'p': state_change(play); break;

13 case 'r': state_change(record); break;

14 case 's': state_change(stop); break;

15 case 'f': state_change(forward); break;

16 case 'b': state_change(backward); break;

17 case 'q': return EXIT_SUCCESS;

18 }

19 }

20 return EXIT_SUCCESS;

21 }

上述代码规定了,状态机迁移函数的原型/签名是 void state_change(enum

message m)。

测试的时候,我们这样做:./state < test.intest.in的内容是"psfsbspq",测

试时期待看到输出的状态迁移过程。之所以这样做,而不是每次从控制台手动输

入,是因为每 次测试的内容都应该是相同的--相同的输入,程序有相同的反应--

可重现性。或者说,DRY原则。

一个非常值得我们注意的问题。在上述接口中,我们看不到"状态"。事实上,我们

将会定义:

enum state { s_stop, s_play, s_forward, s_backward, s_pause, s_record };

但是,接口以外的代码,是 *不应该* (是不应该,不是 不必要,是一定不要) 知

道状态的,既不应该知道当前状态,也不应该知道将要迁移到哪个状态,也不应该

知道在迁移过程中应该做什么动作。如果接口以外的代码知道了这些,就侵入了

状态机的隐私,子系统的边界就模糊了。而契约的首要任务就是规定边界,规定国

家与个人、个人与个人、个人与集体的边界。

这一原则,早在195X年,软件工程刚刚开始的时候就确立了,是最初确立的原则,

即 信息隐藏。后面的原则,都是它的儿子孙子。有个比喻讲过这个道理。当你在

超市出口付款的时候,你会自己把钱从钱夹里拿出来递给售货员,而不会转过身去

对她 说,"在我屁股兜里,你自己掏吧,别忘了把零钱放回来。"这既增加了假设

--你极端信任她,也增加了她的责任。

接口,最主要的任务就是为了明确责任,把责任分布在子系统边界两侧。其次才是

规定调用的方法,即边界长什么样。

4. 状态迁移

以下是状态机的代码片断。

1 enum state { s_stop, s_play, s_forward, s_backward, s_pause, s_record};

2 void state_change(enum message m)

3 {

4 static enum state s=s_stop;

5 switch (s)

6 {

7 case s_play:

8 if(m==stop)

9 {

10 s = s_stop;

11 printf("stop.n");

12 }

13 else if (m==pause)

14 {

15 s = s_pause;

16 printf("pause");

17 }

18 break;

我们还是要关注那四个关键点: (1) 当前状态, (2) 当前消息, (3) 将迁移到

哪个状态, (4) 迁移中会做哪些动作。

(1) 当前状态必然是第1行的枚举类型中的一个。我们初始化状态为 停止,见第4行。

在第5行到第7行,我们的双重 switch-case 的外层 按当前状态分类,如下。

5 switch (s)

6 {

7 case s_play:

下面还有很多 case,第1行的枚举类型中的每一个状态,都有一个 case。

(2) 当前消息。如果当前状态是第7行了,那么,当前消息由双层 switch-case的

内层,即第8行,第13行的 if...else if 来响应。

(3) 将迁移到哪个状态。在 s_play状态 (第7行) 接收到 stop 消息 (第8行)的

话,将迁移到 s_stop 状态,即第10行。

(4) 在迁移中会做哪些动作,如果还是这个状态这个消息,会做的动作是 第11

行,打印一段文字描述接下来的状态。

在函数 void state_change(enum message m) 中,维护了当前状态,规定了在某

种状态下-接收到某个消息,会迁移到哪个状态,在状态迁移中做哪些动作。

主函数在调用state_change时,是通过这一接口,向状态机发送一个消息;由状态

机对这个消息做出适合自己当前状态的响应--状态迁 移、动作。主函数所看到

的,是一个多彩或善变的女人,而她之所以对同一消息做出不同响应的原因,在她

的内心深入保留着,那是她不会对你说的状 态,以及状态迁移中的波澜壮阔。即

使表面上善变的状态机,也是可以理解和预测的,如果她对你倘开心扉,允许你一

行一行把附录A中的代码读完, 了解所有的 switch-case,了解所有的状态下她将

会如何响应每一种消息。

附录A 完整代码

1 #include <stdlib.h>

2 #include <stdio.h>

3

4

5 //recorder

6

7 enum state { s_stop, s_play, s_forward, s_backward, s_pause, s_record };

8 enum message { play, stop, forward, backward, record, pause };

9

10

11 void state_change(enum message m)

12 {

13 static enum state s=s_stop;

14 switch (s)

15 {

16 case s_play:

17 if(m==stop)

18 {

19 s = s_stop;

20 printf("stop.n");

21 }

22 else if (m==pause)

23 {

24 s = s_pause;

25 printf("pause");

26 }

27 break;

28 case s_pause:

29 if(m==pause)

30 {

31 s = s_play;

32 printf("play.n");

33 }

34 else if(m==stop)

35 {

36 s = s_stop;

37 printf("stop.n");

38 }

39 break;

40 case s_stop:

41 if(m==play)

42 {

43 s = s_play;

44 printf("play.n");

45 }

46 if(m==backward)

47 {

48 s = s_backward;

49 printf("backward.n");

50 }

51 if(m==forward)

52 {

53 s = s_forward;

54 printf("forward.n");

55 }

56 if(m==record)

57 {

58 s = s_record;

59 printf("record.n");

60 }

61 break;

62 case s_forward:

63 if(m==stop)

64 {

65 s = s_stop;

66 printf("stop.n");

67 }

68 break;

69 case s_backward:

70 if(m==stop)

71 {

72 s = s_stop;

73 printf("stop.n");

74 }

75 break;

76 case s_record:

77 if(m==stop)

78 {

79 s = s_stop;

80 printf("stop.n");

81 }

82 break;

83

84

85 }

86

87 }

88

89

90 int main(int argc, char *argv[])

91 {

92 char c=0x00;

93 while(1)

94 {

95 c = getchar();

96 switch(c)

97 {

98 case ' ': state_change(pause); break;

99 case 'p': state_change(play); break;

100 case 'r': state_change(record); break;

101 case 's': state_change(stop); break;

102 case 'f': state_change(forward); break;

103 case 'b': state_change(backward); break;

104 case 'q': return EXIT_SUCCESS;

105 }

106

107

108 }

109

110 return EXIT_SUCCESS;

111 }

附录B 状态图源代码 in graphviz

digraph state

{

graph [ nodesep=1.2];

rankdir = LR;

播放 -> 暂停 [label="按下 || "];

暂停 -> 播放 [label="按下 || "];

暂停 -> 停止 [label="按下 []"];

停止 -> 播放 [label="按下 >"];

播放 -> 停止 [label="按下 []"];

停止 -> 快退 [label="按下 <<"];

停止 -> 快进 [label="按下 >>"];

快进 -> 停止 [label="按下 []"];

快退 -> 停止 [label="按下 []"];

停止 -> 录音 [label="按下 O"];

录音 -> 停止 [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" -> "0x02" [label="0x02"];

"0xff" -> "0xff" [label="不是0x02"];

"0x02" -> "0x00" [label="任意字符 / (count=1,该字符填入卡号末尾)",

color=red, fontcolor=red];

"0x00"-> "0x00" [label="任意字符 & count<4 / (count+=1,该字符填入

卡号末尾)", color=red, fontcolor=red];

"0x00" -> "0x0d" [label="0x0d & count>=4"];

"0x00" -> "0xff" [label="不是0x0d & count>=4"];

"0x0d" -> "0x0a" [label="0x0a"];

"0x0d" -> "0xff" [label="不是0x0a"];

"0x0a" -> "0x03" [label="0x03"];

"0x0a" -> "0xff" [label="不是0x03"];

"0x03" -> "0xff" [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"];

"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]

俄罗斯方块:win32api开发

俄罗斯方块:win32api开发

本文简述一门课程,演示win32api开发俄罗斯方块的开发过程。假设学生学习过C

语言,没学过或者学习C++不好,刚刚开始学习 win32api程序设计,还不懂消息循

环和注册窗口类。

1. 背景和原则

我这学期讲一门课,本科三年级,学生满员17人。一般接近满员,最低一次5人,

那天据林同学说,其他的同学都去看足球赛了。

课程名字叫做算法与程序设计实践3。第一堂课我照例要解释:到了"3"这个阶段,

就不讲算法了,只有实践。不过,后来看看算法也还是有一点应 用,比如从一个

线性表里删除符合条件的元素们,在线性表里查找符合条件的元素,这种难度的。

课是在机房上的,大部分时间学生和教师都看着显示,所以一学期下来,好多同学

和我见面可能都不太认识。不过我们对代码的形成过程更熟悉一些。

我试图贯彻下述原则:学生应该看到教师编程的过程,而不仅仅是结果;学生应该

看到在编辑器和编译器中的代码,而不是WORD或PPT里的;学 生应该先学会临模教

师的编程过程,而没有能力直接临模结果;学生甚至应该看到教师的错误及错误的

解决过程、教师的无知及检索过程,学生不应该 看到事先排练的完美的编程过程

和全知全能的教师,那样的过程和专家,学生模仿时无从下手。

所以,我课前不准备,在课堂上无意犯各种错误--偶尔演示学生们容易犯的错误--

及解决。在LOG文件中记录我们的计划和当前的进度,在画图 里画下原型。

所以,我假装对某些API和函数不熟悉,演示在MSDN和互联网中查找手册和解决方

案的步骤。单独做一些技术原型验证对API的调用结果的猜 想,而不是在工程的过

程中在项目代码中测试技术。有时,我知道问题在哪里,但是要先列出各种可能,

然后一一验证猜想(而不是直接解决,这似乎 是计算机本科生非常容易犯的错误,

如果解决了就认定那是问题的原因)。除了这两点,其余的时间我应该尽可能诚实。

有时候,学生会告诉我哪里错了,先于我发现问题的原因。这令我享受这样的教学

过程。

最终,我们--以我编码为主--实现了WIN32API开发的俄罗斯方块。

选择俄罗斯方块的原因,是因为小游戏的业务逻辑足够复杂,保证学生了解在相对

复杂的业务逻辑时的面临的问题和编程行为与toy作品不同;所使 用的到技术较

少,避免过多的机制 (数据库、网络等)分散学生的注意力,保证学生把精力集中

在对业务逻辑上。

选择win32api是课堂上投票的结果。选择C语言而没有使用C++有两个原因。一是学

生的C++掌握通常并不熟练;二是我希望学生能在项 目中发现面向对象的必要性和

优点,而不是仅因为学习过哪些语言而在工程中选用;三是希望演示用C也可以实

现基于对象的程序设计 (不是面向对象,不包括继承,仅包括方法与数据的内聚)。

2. 技术原型

涉及到的技术原型,要在工程开始前建立小项目,以验证对这些技术的掌握和对效

果的猜想。

要实验的技术列表,来源于需求。我们先不写代码,口头描述需求,然后分解需求

到所需的技术。这样就形成了技术列表。这个过程中,同时也形成了 定义,包括

名词和动词表。

这些技术原型也限定了除C语言以外需要掌握的技术,在这次开发当中。

技术原型包括:

* 使用GDI画图、擦除。用于画小块和移动小块。移动是根据视觉暂留在新的位置

上画图,并把旧位置上的小块以底色重画。

* 键盘消息响应。用于在不暂停小块下落的情况下接受玩家通过按键操纵小块左

移、右移、旋转、快速下落。

* 特定范围的随机数生成。用于在创建新的小块时,决定是哪个类型。类型计有S

型、L型、凸形、田形,及它们的旋转。

* 计时器 (timer),用于驱动小块定时下落,判断是否该清除一行,计分,刷新工

作区 (重画) 等。

* 在工作区输出文字。用于调试和显示分数。

最终形成的原型部分代码量如下。代码在附件中的 prototype目录下

画图 (及消息循环) ,draw,226行

擦除,eraser,263行

在工作区输出文字,textout,201行

按键消息响应,key,207行

随机数,random, 31行

计时器,timer,214行

3. 开发过程的里程碑

技术原型确定以后,再重新回到需求,并把需求排期。争取每次课程限定完成一个

功能。

需求排期遵循的原则是:优先完成对其他功能无信赖的部分;优先完成核心功能。

以下是开发过程中的里程碑。

1) 生成块。

2) 计时器驱动,块自动下降

3) 键盘控制块 旋转、快速下降、左移、右移

4) 落到底或粘在底部已存在块上 (if (conficted || touch_bottom) stick)

5) 删除一行:删除一行,把之上的行下降一行

6) 计分:消除一行和多行分值不同

以下功能在本学期没有实现。

7) 生成新块前,在预览区显示下一个块

8) 分数积累到一定程度 (?),加快块下落的速度

开发过程以git版本控制方式记录了历史,每个重要功能一次commit,以日期作为

message。

4. 定义

我们在开发前用示意图约定了一些定义,作为词汇表。排版原因,我在这里有文字

解释一下。

俄罗斯方块元素:工作区上绘图的最小单位,是一个小方格。俄罗斯方块的名字

Terris 即四元素,因为每个当前块由4个元素组成。

数组元素:即C语言中的数组元素,数组中的某一个。提出这个定义是为了区别于

俄罗斯方块的元素。

当前块 (current block) :正在移动的由四个元素构成的块。有S型、L型、田字

型等类型。

已存在的块 (exist block) :堆积在工作区底部的,已经粘成一团的元素。

像素坐标,世界坐标。像素坐标是由GDI绘图定义的,世界坐标由我们定义,以元

素为单位,左上是原点 (0,0) ,向右向下递增。

stick。当前块接触到已存在的块,或者当前块接触到工作区底部,此时应该把当

前块加入到已存在的块中,然后生成新的当前块;如果导致已存 在的块中某一行

充满元素,需要按游戏规则删除此行,然后把已存在的块中此行以上的元素降落一行。

5. 数据结构及流程

以下介绍当前块、已存在块、键盘操作、删除已存在块中的一行的数据结构和流程。

5.1 当前块

当前块中,包括当前块的以下数据:当前坐标,上一次的坐标 (用以擦除) ,当前

类型 (接下来会解释),上一次的类型 (用于旋转)。结构体如下,整个程序中只有

这个结构体的唯一实例。

struct struct_block{

int x;

int y; /* row 0, col 0 */

int old_x;

int old_y;

int* type;

int* old_type;

};

当前块的类型使用数组实现,如下,分别是一字型、田字型、凸字型。

int line_v_block[]={0, 0, 0, 1, 0, 2, 0, 3};

int line_h_block[]={0,0,1,0,2,0,3,0};

int tian_block[]={0, 0, 0, 1, 1, 0, 1, 1};

int tu_v_block[]={0,1,1,0,1,1,2,1};

int tu_h_block[]={0,1,1,0,1,1,1,2};

数组中的每两个数值 (数据中的元素)代表一个当前块中的元素的坐标,计8个数值

代表4个元素。

生成块时,

current_block.type = line_v_block;

指定了当前块的元素。

绘图时,遍历"类型数组",把每个元素绘出。无论何种类型,都遵循这一流程,从

而实现"以数据作为代码":类型数组即数据,遍历"类型数 组"、在旋转时改变类

型等即为引擎。

旋转的代码示例,改变类型 (的指针) :

if(current_block.type == line_v_block)

{

current_block.type = line_h_block;

}

平移的代码示例,改变横坐标:

current_block.x -= 1;

自动下降的代码示例,改变上一次的纵坐标和当前纵坐标。

if(! is_conflicted() && ! is_touch_bottom())

{

current_block.old_y = current_block.y;

current_block.y = current_block.y + 1;

}

else

{

stick();

generate_block();

}

快速下降:

纵坐标 增加 所有元素中到达底部 (或已存在块中同一横坐标的顶) 的最短距离。

貌似题外话,helper函数:is_conflicted(),判断当前块是否接触到已存在

块;is_touch_bottom(),判断 当前块是否触底;匹配横坐标,给出当前块的底坐

标;求当前块距离底部的最短距离。等等。

开发helper函数的目的,是为了使程序整体流程清晰。保障整体清晰的方法之一,

是要求每个函数内容不得超过一屏。如果超过了,就需要折解 出 helper 函数。

在主流程中调用 helper 函数,而把helper函数体移出主流程,这样主流程代码长

度就下降了。这和小学写作文的时候,老师要求先拉大纲是一个道理。经常有同学

说,在开发过程中 会发现新的功能,在开发遇到新的技术,没有做原型的,因此

难以把握大纲。这都说明把握大纲和做计划的能力还差,需要通过练习来训练。这

和小学 生写着写着作文发现需要查字典,或者写跑题了,是一个道理。我们的成

长并非认识的字多了,而是能预见到将会用到哪些字 (甚至表达手法、写作素材)。

此外,在面向对象中,有些的函数会成为game (或者 current block 或者 exist

block )的成员函数。这在开发中会认识到,如果它们与数据能内聚在一个类中,

该是多么方便,因此了解面向对象的在信息隐藏方面的优势。这些函数应归属于哪

个类, 是由哪个类承担这个责任决定的。

5.2 已存在块

已存在块中包括以下数据结构:块的长度 (事实上,是块的长度*2,代码中以横坐

标和纵坐标作为两个数组元素) ,已存在块数组。如下。

int exist_block_size=0;

int exist_block[(maxx+1)*(maxy+1)];

这种数据结构,及当前块的数据结构,把横纵坐标无差别地,不以结构体地方式放

在数组中,在后续开发中带来了麻烦。不过由于课程时间有限,后 来,我未对此

做出修改。应该逐渐演化程序结构,形成以元素作为结构体的数组。再开发出一些

helper甚至成员函数,遍历时以俄罗斯方块元素 为单位,而不是当前代码中的以

数组元素为单位。

对已存在块数据结构操作的函数之一是 stick,用于在当前块触底 (或触及已存在

块)时,把当前块中的元素移到已存在块中。

有不少helper函数,基本都是通过遍历 exist_block,按匹配条件读其中的坐标。

包括:匹配横坐标,给出已存在块的顶坐标 int get_exist_block_top(int x)。

5.3 键盘操作 & 动作序列

玩家操作块这一操作,由键盘消息响应开始。我们不在键盘响应中处理这一事件,

而是只在这里记住这个动作,加入动作序列中。这是后来的版本。最 初的版本,

我们也不在键盘响应中处理事件,而是调用 block.cpp 中的函数。原则是:凡依

赖win32api的,放在 tetris.cpp 中,如 timer, 键盘响应,绘图;凡是与业务逻

辑有关,平台无关的,放在 block.cpp 中。接收向上箭头,是键盘响应,平台相

关,所以放在 tetris.cpp 中;此时调用的 rotate,用于改变当前块的类型或坐

标,平台无关,所以放在 block.cpp 中。

动作序列的数据结构如下。在动作序列数组buffer_action_seq中,数组动作元素

(动作) 的类型是 枚举 action。

enum action{ action_left=1, action_right=2, action_speed_down=3,

action_rotate=4, action_down_auto=5, action_na=0};

action buffer_action_seq[action_size]={action_na};

int buffer_action_cursor = 0;

由玩家触发键盘消息开始,流程如下。

1)键盘消息响应:

buffer_action_seq[buffer_action_cursor++] = action_rotate;在动作序列中加

入一个动作。这对应于设计模式中的 commander 模式要解决的问题。

2)在timer中自动下降

timer中 buffer_action_seq[buffer_action_cursor++] = action_down_auto; 在

动作序列中加入一个动作。

3)在timer中触发WM_PAINT

timer 中 InvalidateRect 触发 WM_PAINT

4)WM_PAINT中执行动作序列

erase_old_block_seq(hdc);

erase_old_block_seq (hdc) 遍历动作序列,按每个动作改变当前块坐标,然后擦

除由于动作产生的旧块。遍历动作序列以后,就完成了自上个 timer 周期以来所

有的动作,擦除了这期间产生的所有旧块。

void erase_old_block_seq(HDC hdc) 片断如下:

for (i = 0; i < buffer_action_cursor; i++)

{

switch (buffer_action_seq[i])

{

case action_left:

move_left();

erase_old_block(hdc);

break;

在序列里的每个动作中,move_left 改坐标, erase_old_block(hdc) 擦除旧块.

5)WM_PAINT画新的当前块和已存在块

draw_current_block(hdc);

draw_exist_block(hdc);

因为重绘比计算花费的时间要多,作为性能优化,如果当前块与旧块坐标完全相

同,不重画。

另,另一个版本的动作序列,不使用枚举和swtich-case,通过把函数作为消息传

递给责任者,实现disptach:

void (*next_action)() = move_still;

next_action = move_left

其中 move_left是一个函数。next_action这样的元素 (类型是函数) 组成一个数

组,作为动作序列。执行动作序列时,用下面这样的代码:

while ( next_action++ != action_termination )

next_action;

由于 next_action 既是函数,也是数组元素的指针,因此上述代码不是伪代码,

而是可以执行的。这类似于 jump table 技术,数组元素的类型函数,可以遍历数

组,执行元素对应的函数。

5.4 删除一行 & 计分数

每个 timer 中,都调用 void kill_all_full_lines()。它遍历 exist block,凡

符合满行条件的,调用 kill_block_in_line 删除该行,调用

move_exist_block_down_line 把该行以上的 exist_block 下降一行。

这三个 helper 函数都是通过遍历 exist block 中的每个元素,匹配坐标条件,

然后删除数组元素或者改变数组元素的值。如前所述,由于 exist block 封装中

未使用 俄罗斯方块元素,所以这些遍历都写得非常丑陋。

删除一行以后,累积删除的行数。全删以后,根据删除的行数进行 switch-case,

向全局变量 score 累加分数。在下个timer中,把 score 用 textout 输出到工作区。

6. 回顾和检讨

6.1 数据结构,封装,循环条件

由于最初的 (也是最终的)数据结构设计偷了懒,后来又没有足够的时间修改,此

前已经提及两次,exist block的结构过于贴近平台,而远离需求。exist block的

颗粒度太低,是以 int 为类型的 数组元素,对应于需求中的 俄罗斯方块元素 中

的横纵坐标之一。某个数组元素到底是横坐标还是纵坐标,到底是第几个俄罗斯方

块元素,这些都需要由代码实现。这样,按需求写helper函数的时候,遍 历的元

素选取、终止条件,都遇到了麻烦。我在课堂上写作时需要考虑,有时还会错。经

验说明,当我需要仔细考虑,或者讲述时间较长时,学生听懂 可能已经有相当难

度了。终止条件错误的bug,在代码中存在两三处,导致在 exist block够多时,

即游戏进行一段时间,工作区中会出现莫名其妙的俄罗斯方块元素。这个bug在最

后阶段才解决。

这个故事告诉我们,设计不好,对编码实现的难度要求就会提高。战略失误,战役

和战斗就不容易打。领导决策肤浅,要求下属跑死,结果也是白扯。 道理都是一

样的。

6.2 不要对付过去

在开发中间的某堂课,我们发现当前块移动时后面留了尾迹,擦得不干净。这些那

堂课快结束了。为了能让学生在课后重复我课堂上的工作,所以我" 对付"了代

码,由局部刷新改为刷新整个工作区,包括背景。这样尾迹表面上清除掉了。

之后,延续了这段"对付"的代码。直到期末将至,我才发现这段"对付"掩盖了另一

些bug,坐标移动的bug导致除非刷新整个工作区就有尾 迹。这个bug在最后阶段才

解决。

6.3 并行,timer

有文章指出,初学者非常不容易理解的程序概念包括:赋值、递归和迭代、并行。

本程序中有几个埋得比较深的bug,是由于我对并行没有足够警惕 造成的。

timer, 键盘响应,WM_PAINT会并行发生。当其中一个未处理完的时候,另一个可

能就开始执行;甚至timer未处理完的时候,另一个timer也可能会开 始。而这些

并行的代码,都调用了 block.cpp。比如有时导致其中一个正改坐标尚未完成,另

一个开始刷新工作区,这样工作区里就出现个元素,位置是乱七八糟的。

并行的处理,需要 原子操作、进程间通信、避免重入 等概念。上述提到的动作序

列,目的之一就是希望擦除旧的当前块这一动作只在 timer 中发生。

在本课程中,应该不期待学生具备这些操作系统中的知识。不过我还没有想到该如

何设计才能规避这些知识。不过我猜应该类似于不用线程也能设计出 贪吃蛇,应

该有依赖更浅显知识的设计手段,比如单纯轮询,而不用事件响应、消息循环。有

哪位知道,请赐教,谢谢。

6.4 猜想后,应该先验证,然后再修改

学生们通常把验证猜想和实施解决归约成了一步,我也经常如此。下文中的他们,

包括我。

他们观察到问题,然后做出猜想。这是正常步骤。

但是他们不以实验验证猜想是正确的,急急按猜想修改代码。如果问题消失了,

好,他们假设抓住了问题的原因;如果问题还在,就再做个猜想,然后 又马上修

改。甚至更糟糕,没有退回到上一步的起点,就在当前工作代码上"继续"修改,让

各个猜想累加起来,最终问题解决的时候甚至不知道是什 么原因。

应该先设计实验,按猜想的模型,如果怎样就会怎样。验证猜想以后,再去解决。

比如假设由于 timer 和 keyboard事件响应 同步导致画图混乱,那么,不应该着

争写进程通信,而是 应该先选用简单粗暴的手段 去除同步,以更大的颗粒度作为

原子操作,验证猜想。如果猜想正确,现象应该有所改变。虽然影响性能和效果,

但这并不是准备最终采用的代码,只是用来验证猜 想的。当猜想验证以后,再去

想效果更好的方案真正解决,比如建立个变量作为信号灯。

6.5 不要轻易更换技术方案,试图绕过问题

这个方面,我最初是发现计算机本科的同学倾向强烈。经常有方案,明明再向前一

步就能解决,他们却在此时换了方案。问为什么。答:因为这个技术 解决不了这

个问题。

确定"不"是极其困难的,甚至比确定"能"要难上很多。你不能,并非就能确定这个

方案不能。

需要充分了解你所使用的技术,对它能够完成的任务有足够和明确的自信。同时,

对用来替换的方案能解决何种问题,也应该明确。做原型验证,根据 理论推论,

这些都是解决之道。见到工具,拿来就用,偏听偏信别人的评论,就太草率了;一

旦发现并非万能良药,转身就去寻找就的手段,这就更草 率了。

6.6 版本控制

为了让学生能看到开发的过程,我上课时用文件系统做了版本控制,每次课一个目

录,有时压缩成zip。课程结束以后,一个版本一个版本加入 git,然后commit,

操作了两个小时(?),其间又担心整错了,苦不堪言。

下次一定要从最开始就做版本控制。还要在 commit 前把 debug, pch, sdf 等二

进制垃圾手动删除。

7. 附件

附件是以git版本控制的代码及日志,在这里[http://download.csdn.net/detail

/younggift /7499881]。

protype下是技术原型。

tetris下的是俄罗斯方块项目本身。早先的版本是VS2010的,最后一天的是VS2012

的。你可以仅代码部分添加进win32工程, 以适应你的VS版本,或者dev c++版本。

log0.txt是课堂上的日志。log1.txt是最后一天前期的日志。log2.doc是最后一天

后期的日志,因为需要截图,所以改成用 word。

pic.bmp是图片,用来说明定义的。

branch是一个分支,我忘了它是否加入了 trunk,留在那里备用,以防遗漏。

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

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

[http://giftdotyoung.blogspot.com]

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

电子书阅读及工具

电子书阅读及工具

我现在看电子书,除了计算机以外,还常使用 ipad mini 和 kindle。所讨论的工

具都在这三个平台上。

后半部分讲的内容才是工具,前半部分是历史回顾。只对工具感兴趣的,请向下翻页。

1.

我最初读的电子读物,应该是 qbasic 的手册。当时还不知道有电子读物或者手册

这回事,常问张仕鹏师兄问题,只要见他在机房,就死皮赖脸地去问。有一次问到

红色的颜色编号到底是多少,他终于受 不了,告诉我按F1键,说:你自己看手册

吧。然后,我就开始漫长的手册阅读生涯,也因此有幸学会 RTFM 比身边常有一个

高手要重要得多。感谢张师兄教会我这一点。

此后很多年,也有不少同学问过我类似的问题,比如向上箭头的扫描码是什么,某

个函数的参数是什么类型,线程的使用。我都会想起张师兄,然后说:你 自己看

手册吧。他们有的回答,那太长了,有的回答,是英文的啊。我有时苦口婆心讲一

遍手册里的内容,也有时苦口婆心地讲"未来的世界就是这样了, 你越来越没有汉

语可看了",更多的时候我就只是闭嘴。人家需要的是具体的帮助,不是人生导师。

qbasic的手册当然只能是英文的。我讲的是另一个时代的故事,跟现在有所不同,

那个时候还不兴管师兄叫做师兄或者学长,我就只是直接叫张仕鹏 的名字,他比

我高两届。那个时候也没有互联网,没有在线词典,我也没钱买文曲星,只有后来

才买得起的一本40块钱的英汉大词典,太厚,不能每次搬 着去机房。而

且,qbasic的手册是不拆不扣的英文的。如同现在的手册,它也会援引其他的章

节,那么就跳过去,看完再跳回来。

我和我的同学,就是这样学习了qbasic,还有 turbo C的使用。看纸质书,图书馆

可以借到;看手册,随机带着。系里确实也开了C语言课,不过那是我们自己学会

以后很久的事了。

我本科毕业前读的最有份量的电子书是 windows资源大全,也是英文的。它的纸质

品后来我看到过,一寸多厚。我根据其中的一章学会了 借助 novell netware操作

系统做win95无盘工作站。毕业论文的一部分跟它有关。答辩委员会里有幸遇到了

后来的校长。他问:你这工作里有什么创新啊。我说:啥 也没有,就依据了一本

书--然后用手比划了一下厚度。就这样通过了答辩。

那本书的电子文档其实非常小,似乎一张或两张1.44M的软盘就可以拷下。似乎是

从win95光盘中解压出来的,是chm更可能是hlp格式的。

后来读了很多格式,txt的,word的,pdf的,不一而足。从最初到现在,都有人跟

我提到过电子书的诸多缺点,没有油墨的清香,没有情调不能 煮茶而读,记笔记

不方便,不习惯,版权等等。

我依然保持使用电子读物的习惯,甚至大批购买纸质品的同时,也仍然大量阅读电

子书,因为反对电子书的理由不足以说服我。油墨的清香、情调、版权这 些事,

适合矫情 (我长期以为这两个字是嚼性...)的人群。对于穷人来说,逮到什么版本

就是什么版本,有的读就不错,还要什么自行车啊。"不习惯"这件事,习惯习惯就

好 了。我们翻纸质品书页的习惯,也不过是训练出来的,这里既没有感情,也没

有文化可言。

记笔记确实是个问题。不过,我长期不把笔记写在书页上,因为书是图书馆的。涂

抹要罚款。所以,只要再准备一张纸,或者笔记本,把笔记都写在上面就 好。所

以,电子书也可以一样处理,笔记写在他处。

2. 工具

目前在所有的系统和格式下,我阅读时的要求都包括以下几个方面:书籍管理、格

式转换、上传下载、切白边、做笔记、阅读本身。

2.1 书籍管理

最佳的书籍管理工具是 目录系统 (文件系统) 本身,可以使用windows里的资源管

理器 建立和删除目录,通过文件名标注书的内容。加过批注的,我会加上*.young

批注.pdf 这样的字样。找书的时候,在子目录下搜索文件名。

另一个我常用的书籍管理工具是 calibre [http://www.calibre-ebook.com/]。

不同的操作系统版本,我用过 linux 和 windows 下的,都不错。可以标注书目的

书名、作者、出版社等信息。还可以加标签,不同于目录,这样每本书可以属于几

个类别。不过我的经验是,标注的工作确实总觉得 需要,但是有了这个功能以

后,基本不用。我们读书的数量,可能还远远没有达到以后找不到的程度。

我还曾经用过论文管理工具,endnote什么的。能导出作者、篇名、期刊、年月

等,写论文的时候确实方便。我问过导师,你咋不用endnote 呢。他用文件系统管

理,写每篇论文的时候把参考文献也放在论文旁边的目录里。他说:一共就引那么

二三十篇,不值得用工具;各种工具都会过时 (或者有适用条件?),但是文件系

统总是有效。

2.2 格式转换

kindle适合看小说,非PDF扫描的,ipad mini适合看漫画和有图的文档,还有扫描

版的电子书。台式机和笔记本适合看各种书,只是不能窝在椅子里,因为那样太远

就看不清显示器了。

它们能阅读的格式各有不同,效果也有好坏。所以我需要根据环境和需要,把电子

书由一种格式转换为另一种。

最常用的是 calibre。它能把word,pdf,txt,html转成 kindle 的 mob 格式,还能

转为适合 ipad 的 pdf 或 epub 格式。格式转换以后,可以用 calibre 架设www

服务器,用 kindle/ipad 从内网连接下载电子书。

另一种常用的转换工具是 amazon 本身提供的邮箱,注册的时候会提供给你。从认

证过的邮箱向它发邮件,标题是 convert ,把word/pdf/txt电子书作为附件。然

后过一会儿,可以用kindle从网上同步到这本书,已经转换为 kindle 的 azw 格式。

txt有时会遇到问题。一个是编码,calibre识别gb2312或utf-8有问题,可能会转

出乱码来。我也不知道怎么解决。这种时候,只好 用 amazon 的邮箱来转换。另

一个是自动换行,有时会转出大段大段没换行的段落来。使用阅读器apabi看,自

动换行还没出过问题。

2.3 上传下载

电子书到手,可能会需要从一个设备转到另一个设备,这就是我说的上传下载。

如果是小文件,在 PC机 和 ipad 之间,可以使用百度云。百度云支持在ipad上导

出并用别的阅读工具打开电子书。批注以后,如果阅读工具支持导出,也可以导出

到百度云,再上传。

准备在kindle上阅读的小文件,可以使用 amazon 的邮件,由PC机发送到邮箱,再

由kindle从网上同步下来。

如果是大的文件,100M以下,可以用 calibre 在PC机架设 www 服务器,用

kindle 或 ipad 的浏览器下载。只走内网,速度很快。

ipad 上的 goodreader 也可以架设 www服务器,用PC的浏览器从 ipad 下载 到

PC,我用这个功能保存在页面上做笔记加了批注的PDF文件。goodreader也支持从

pc的浏览器上传文件到ipad。

如果是更大的文件,就得把 ipad/kindle 通过USB线接到计算机上了。

2.4 切白边

纸书有白边,而 ipad/kindle 的屏幕以外本来就存在边缘,再加上纸书扫描的白

边,字或画面就显得有点小。对我这样视力不好的人,格式不友好。所以需要切白

边。白边切掉了,字就大了不 少。

我常用的切白边工具是 briss [http://sourceforge.net/projects/briss/]。

个java程序。载入电子书的时候有点慢,它把所有的页面都读进 来,然后叠印显

示出来,就像很多层琉酸纸那样。这样,一目了然地能够看到哪些部分是不能切掉

的。briss会自动判断白边范围,作为建议值。我的 经验,它的判断相当准确。你

也可以手动设置,设置好边界以后,另存一个文件。另存出来的文件并不比原来的

文件大多少,而且保存这一动作一点也不费 时间,瞬间完成,因为据说 briss 并

没有"真正"切掉白边,而是设置了新的显示范围。

另一个切白边的工具是 goodreader,它在阅读文件的同时,可以设置显示范围。

以上两个工具,显示范围都可以设置为奇偶页不同。

2.5 做笔记

我读电子书做笔记,常用的方法包括:另找张纸记 (或另记一个文件) ,拷屏然后

用画图类的工具在上面记,在电子书的页上上画 (或写字,或圈范围)。

另记一个文件的,我用 mybase, org-mode,txt/word+文件系统。

在画面上批注并另存的,一般用 evernote 保存归类。

直接在画面上批注的,用 goodreader, acrobat reader。

2.6 阅读工具

我用过的阅读工具,觉得不错的如下。

kindle原生系统,没有使用多看。事实上,没试过多看,所以不知道好坏,用

kindle的时间没发现有必要从英文菜单改为中文的。

goodreader,ipad mini 上的。能做笔记,能上传下载,能读很多格式。

ibook,是 ipad mini 上的,苹果产的。如果不做笔记看小说,还不错。

acrobat reader,ipad mini上有,pc端也有。能做笔记。

apabi,是 ipad mini上的,读txt格式好。

calibre 不是阅读工具,因为功能强大,总结一下。能上传下载,能图书管理,能

格式转换。

2.7 没有解决的问题

还有些问题没有解决。比如竖版阅读还是很令人头疼,看一行就要翻一下。要是有

工具能识别出来转成横版就好了。

3.

既使有这些工具,书也还是得一行一行由你自己来读。就像即使这个世界摆在你的

面前,人生也还得你自己亲自来过。说到这里,我想起在QQ上请同学帮 忙做题的

同学,总是想说:以后你的工资,也由别人代领吗?或者想说:你喜欢的,从来就

不是读书本身,而是读书可能带来的利益。

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

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

[http://giftdotyoung.blogspot.com]

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

从ssh登录缓慢说起

我在单位开了台实验用机,用SSH远程登录。之前一直用得不错,这两天发现断了。联系了两位相关的同事帮忙,分别对虚拟机和网络测试,都正常。我晚上回家再测,由于白天有了对比,晚上等待就更耐心一些--不到30秒,登录成功了。登录以后,速度还可以。
这说明网络、虚拟机都没啥问题。我以前遇到过SSH登录缓慢而登录以后速度不错的情况,比如我的WINDWOS机器连接LINUX用sftp的时候。后来解决了,这次的解决方案也大致如下。
解决方法:
修改 /etc/ssh/sshd_config,这是ssh服务端的设置文件。
1. UseDNS no
2. 把
GSSAPIAuthentication yes
改为
GSSAPIAuthentication no
然后重启sshd,/etc/init.d/sshd restart
再重新连接,登录速度快了。
导致以上问题的原因的基本原理是,ssh客户端在连接ssh服务端的时候,服务端根据客户端的IP地址去反向解析这个IP地址的域名。如果反向解析的过程中需要连接的外部世界的服务器速度很慢,或者干脆是不可达的,那么ssh服务端会等到DNS反向解析这个动作超时,这会导致十几秒或长时间ssh客户端傻等在那。没耐心如我者,就会判定网断了或者服务器完蛋了。
ssh服务端的这些动作可以禁止掉,不过开发者认为这些动作是必要的,安全中不应缺少的组成部分,所以默认设置了。这让我想起些别的。
Thunderbird,似乎译作 雷鸟(?)是非常著名的电子邮件客户端。似乎跟当年的foxmail一样著名,或者更著名。foxmail的作者后来加盟了腾讯,开发出了个著名产品,叫做微信。我用过一段时间Thunderbird,觉得还不错。后来,后来....这个故事告诉我们,时代并非总是越变越好的。
后来,我用thunderbird收gmail的邮件。之所以要这么干,是因为连接gmail的速度令人难以忍受。令人难以忍受的意思并非慢得不行,因为它有时也飞快,瞬间完成载入。令人难以忍受的原因是,速度并不稳定,抽疯一样,有时飞快,有时极慢。我慢慢养成了一个不良习惯,每次载入慢的时候,就开始在心里或者出声地骂相关责任人。
我以为用thunderbird收邮件,批量都收下来以后再看,感觉会好些。没有想到的是,或者说thunderbird的开发者也没有想到的事情发生了。软件工程中有所谓"你永远也不知道用户会多么愚蠢地使用你开发的软件",thunderbird开发组也绝对不能想到地球上还有我们这样一群用户的境遇吧。当我点击某封邮件,邮件头显示的是这封邮件的标题,但是邮件的正文仍然是刚刚读过的一封,直到几分钟以后,邮件的正文才刷新出来。如果没注意,会以为A标题正对应着B正文。Thunderbird开发组可能绝不能想到,这世界上有个地方收gmail邮件的时候慢到需要进度条指示,如果进度条有百分比更好。
sshd的开发组可能也不能想到,有些地方,虽非物理隔离的内网,但是许多地方亦不可达。饱食者不能理解何不食肉糜,那些优秀的软件产品的设计者可能也不能理解我们吧。就像微软的WORD和VS咔咔升级,越整工具栏和浮动工具栏越多,最后能编程和写文字的地方只剩下了一小点儿。当然,他们是有道理的,因为他们的目标用户是用大~~~显示器的。我当年用15英寸CRT球面显示器坚持了N多年,所以才养成了全屏编辑的习惯。当然,微软们还有另一个道理,就是我们中大多数人没有付钱,因此并非他们的目标用户。这与另一个案例类似。当年光荣公司宣布三国某版本游戏不再支持简体中文,我们中国不少用户义愤填膺,在BBS里一顿骂,骂光荣之愚蠢,竟然放弃大陆这么大的市场。几楼以后,有个网友悠悠地说:你们买过光荣用户哪怕一个拷贝吧,好意思说自己是用户是市场?
但是 thunderbird 和 sshd 不同,它们都是免费的,gmail也是免费的,整个地球都可以尽情拥有,而我们似乎不在其中。有个片子你看过没,叫做《日本锁国》。
--------------------
博客会手工同步到以下地址:

德惠也有星巴克

德惠,是包师弟的家乡,我们在谈论我编的一个笑话时提到过。那个笑话是这样的。我说,你知道中国为什么有很多城市,它们的街道名都是用别的城市命名的吗?比如长春有个北京大街,北京有个长椿街,长春还有通化路,台北也有个通化路。那都是用来玩穿越的。你站在长春的北京大街上,大喊一声,我要穿越,就到了北京,站在台北的通化路上大喊,就到了通化。包师弟说,德惠有个德惠路,大喊一声...就又回到了德惠。再喊一声,又到了德惠。
这表明包师弟对递归的有深刻的理解。
德惠你都没听说过?那三棵榆树呢,你也没听说过?二道白河也没听说过,金厂子铁厂子也没听说过,江南三道喇豁也不知道?你真是哪儿也没去过的土老冒啊。
我前几天去了趟德惠。不穿越去的,坐火车。绿皮车,就是小资们说的"坐绿皮车"去旅行的绿皮车。一路无话,在德惠也无话,有意思的是回来的时候。
回来准备坐动车,还是高铁来着,从德惠西出发。德惠有两个火车站,德惠站跑传统的火车,德惠西跑动车和高铁。打车10元就到了,司机一直把我们拉到售票厅门口,还有十来米就上台阶了。这说明德惠人民出行一般不提前买票,现买现走。我前一天到达的时候先买了票,所以又走了几步去候车大厅。
候车大厅只有一层楼,即使是现在这么暖和的时候也阴森森的,而距离发车还早,得等几个小时,所以我说,"要么去看电影,要么找个地方喝咖啡吧。"
我们打车先去电影院。好在刚好有一辆出租车在火车站,我们抢上前去。之所以要"抢",并不是怕别的旅客跟我们抢,而是怕出租车跑了,可能半天就没有下一辆了。
去电影院。司机师傅介绍说,一般去看电影的都是学生。我问,德惠有哪些学校啊。答,有实验中学,有三中,还有几中来着。我明白了,看电影的估计都是初高中生,不是大学生。我问,有几个电影院。答,一个。问,旁边有咖啡厅吗?答,咖啡厅和西餐厅什么的在城市的另一边,10块钱车费就不够了。我问,哪个方向,司机师傅向右摆了下头:你们从这里走下去,没多远就到了。我心想,没多远,10块钱却不够拐过去的...大哥你真能呼悠啊。
电影院前面站满了中学生,不到一百人吧。因为长期在学校工作,因为讲课,所以对几十人到一百人这样的规模很容易估算出来。我转了一圈没有找到售票口。
问某位同学,请问在哪买票啊。答:里面。问:怎么进去啊。答:有票才能进。
我心想,包师弟,你的学弟学妹的递归也学得可好了,你知道么?
但是这不能难住我,我再问:那你的票从来来的呢?答:学校发的。
我说:谢谢啊。咱们还是喝咖啡去吧。
这样故事终于转到正题。德惠也有咖啡厅。百度地图和谷歌地图都显示,一共有三家咖啡厅。我打了个车,到咖啡厅附近,并且坚决拒绝了去司机师傅去冷饮厅的建议。大下雨天的,我腰疼得要命,眼瞅要突的样子,喝什么冷饮。
出租车直奔两家咖啡厅密集排布的那个地方。一家叫做金典,另一家叫做莱茵河德惠分店。在一条街上,那条街被某条路分成两段,金典和莱茵河各处一段。金典我记得长春也有,莱茵河,我一下子想起了欧洲的咖啡,还有德国的火腿。恩,很值得一去。
我们从这条街的一头走到另一头。这一头KTV云集,在走向另一头的过程中逐渐转变成烧烤一条街。烧烤之密集,比东岭南街我家附近有过之无不及。
但是没有咖啡!
难道这能难倒我吗?地图上,还有一家咖啡,只是离这两家远一些。星巴克。打车,去星巴克。司机师傅说,哪?
我说:你不用担心,我能找到。就在新惠街上,往那个方向走,快到头了就是,中间会经过什么什么和什么地方。司机师傅说,你说的是惠新街吧?我低头看看地图,没错,新惠街。看看GPS定位,2100米精度。不过,根据周围别的街道名,差不多就是,新惠 或者 惠新。
没错,走吧。没多久,我们应该到了,中央公馆,位于城西的一片高尚小区。但是,没有找到星巴克。我们努力找,看到了积水的路面,因为马路已经到头了,还有一大堆一大堆的沙子,路边的公馆看起来没有装修完,应该是玻璃的地方只有空洞洞的窗户。
二猫妈眼尖,指着外面叫道:我看到啦,星巴克的标志。其实她从来没有去过星巴克。我也看到了,绿色的,像个LOGO那样的画面,可能吧,就是星巴克。我之所以没有看到,一是我并不认识星巴克的标志,我一般靠别人领,或者闻味,二是我眼神不好,更重要的是第三,星巴克标志的前面是一大堆黄沙子,这太令我难以联想了。
我们得出定论,这里就是星巴克无疑,但是它貌似还没有开业。我腹诽再三,关于中国人的民族性,比如没开业就挂地图上宣传,倒闭了也不撤掉,通知什么事情开始,从不通知结束,还有清洁工经常把拖布或者"小心地滑"支在厕所门口,24小时也不撤掉,所以人们只好按丛林法则,谁胆大先越界谁就是赢家。腹诽以后,我说,师傅,我再给你钱,你把我们拉到咖啡厅吧。
他说,我知道莱茵河,我见过。
太好了,那就是莱茵河。我们原路返回,我说,对对,我们刚刚就是从这里打车,莱茵河确实就应该在附近,只是我们没有找到。
师傅把车停下来,指着三间没有装修的门面,上面全是胶合板的本色,他说:这三家中间的那家,原来就是莱茵河,它就应该在这里。
明白了...星巴克没开业,莱茵河倒闭了。
我说:不去金典了,我们去哪个有咖啡的西餐厅吧。很快找到一家,叫做...名字忘了。门口墙上有个金灿灿的欧洲风格满身肌肉的男像上半身。先前找金典和莱茵河的时候我看到过他,还想拍照来着,没想到有咖啡。是家西餐之类的店。
进门问,有咖啡么?答:有。一楼有几个包间,二楼有很多包间,此外去卫生间的路弯弯曲曲,两侧有很多包间。每个包间里刚好放俩皮沙发,相对而视,中间是个木头桌子,上面好像铺了玻璃。墙上特意做成很多疙瘩的样子,我敲了下,塑料的,里面是空的。因为是硬质材料,所以不吸音,屋子虽小,听起来有点像食堂那样拢音。颜色...不得不说,从小到大,我都把那种颜色叫做"鸡疤疤色儿"。
好在有极品蓝山,来一壶,40元。应该可以消磨半个下午。
咖啡是装在陶瓷的茶壶里的。壶的造型,似乎是一只小猪的变形,挺可爱的。杯子倒不是玻璃杯。我正瞪大我的小眼睛凝视这壶,二猫妈开始倒。咖啡的颜色也像茶水,跟壶挺匹配的。我没端起来喝,还在那瞅,二猫妈说,"比速溶咖啡还淡啊。"我说,"恩,我看出来了,从颜色上。"
原来的壶把掉了,换成了做旧的铁丝,外面缠上细丝。我把玩这壶把再三,开始一杯杯喝。咖啡,微温。
后来,把我们原来吃剩的半袋香瓜子也翻出来,还有半袋腰果 (?),就着咖啡,都吃了。小半桌子果核,最后都包吧包吧带走扔了。
进站安检的时候,有个穿制服的喊,"谁的包,那个,是那个包。"
是我的。我把包搁桌子上,"怎么了?""里面是不是有把刀?""对,有把瑞士军刀。"我拿出来递给他,"心想,这把刀不违反任何条例,你要是敢没收..."他把刀打开,试试刀锋,掂了半天,折回去,折好的刀拿在手里探出大半截,不吱声,开始注视屏幕。我等了一会儿,心想,这是完事了的意思吧,也没吱声,伸手把刀拿过来,又想了想,没说谢谢,刀装包里,找个空座开始看书。
后来过来个人坐我旁边,腿抖啊抖的。半天,停了一会,换另一条腿抖。我快被他抖晕车了,就换到对面坐,他把自己的包从另一侧拎过来,放在我原来的位置上。此后再无故事。天快黑的时候,我一路轨道交通,回到家里。
我以后还会再去德惠的,并且带上自己的咖啡和滴滤漏斗。
--------------------
博客会手工同步到以下地址:

在北京丢东西

在北京丢东西
我妈说,我从小丢过帽子手套无数。我印象里丢在图书馆的确实有几次,等想起来返回的时候图书馆已经关门了,大冬天只好光着脑袋回家,冻得够呛。不过,如果说我是非常粗心大意,也不公平。坐同事同学的车,在副驾驶的时候,经常被告之,可以把书包放在后排,但是我总是坚持抱着,因为怕扔下忘了。
所以,你看,我还算小心。十年不怎么丢东西,但是丢一次顶上别人丢十年的了。
上一次在北京丢东西是1996年,这一次丢东西是最近没多久。
1996年,我大学二年级的暑假,是我第一次去北京,觉得天安门挺小,人民英雄纪念碑挺矮,紫禁城挺旧。那时候还没觉得北京是大农村。当时我本科还没毕业,暑假找家教的同学介绍我的计算机相关的活儿。连学带干一个假期,假期快结束的时候跟着老板臧老师一起去北京,买计算机配件回长春组装。现在想来,工作上也不怎么需要我去,奖励和提携的成份居多吧。
同行的还有两位公子,也是工作人员。曹公子带了个非常好的相机,日本原装的,变焦镜头可以伸缩三档,银白色。另一位公子我一时没想起来姓什么,我和他一起去了天安门。走累了的时候,我们在天安门西侧坐了一会儿,他把相机放马路牙子上,歇够了我们就离开了。等想起来回去找的时候,就像我扔在图书馆的帽子手套,从此再无缘得见。
我陪了曹同学相机的一半价格,其中有跟李岸同学借的一笔钱。然后,从此毅然决然地走上了IT之路,为了挣钱还钱。当时不少同学在热烈地讨论谁该被任命为物理系学生会科协计算机分会的会长 (注:该职务与学生会一般成员,现在叫干事吧,与干事平级。因为科协是与宣传部啊什么的平级,协会会长算部长,计算机分会会长算部员。),而我要去挣钱啦,所以这一切完全是浮云。后来有同学评论我看淡一切,才不是,是被逼的。实践证明,我们的美好愿望总是落空,IT挣钱也没有多快,尤其我成天干活,连食堂也不吃了,很费。所以,这笔欠款直到1997年,我大三,在大四学生毕业的时候,我卖了一大批书,才把全部其余的钱还清了。卖出的书里有不少好的,我现在还在怀念他们。
这就是第一次在北京丢东西,印象深刻。从此以后,我当然加倍小心。所以,后来能够做相当长时间计算机和网络的系统运维工作,因为小心。所以,后来还写了不少代码,不怎么使用单步调试,因为小心。但是,又是实践证明,我们的美好愿望总是落空。前不久,又在北京丢一次东西。
我妈问,丢东西的地方人多吗?我没有回答人的多少,我说了地点:北京火车站站前。
我从后排座下了出租车。或者,把书包就扔在车里了,或者,扔在了马路牙子上。然后,拉着手提箱施施然离开了。我跟大哥去了麦当劳,我还喝了咖啡。所以可以推断,在下车前后,我一定还说了一句,咱们还有时间,去喝杯咖啡吧。
等离开麦当劳的时候,我才发现缺了一个书包。这时,距离下出租车已经2个小时了。按常人理解的顺序我介绍一下遗失的物品吧:佳能EOS 50D单反相机机身一部及比原装更好的镜头一个,佳能 ixus 860 IS 相机一部。以上两部相机里当天的照片。Kindle三代键盘版一部,ipad mini一部。一堆U盘,几乎是我的全部,其中64G的两个,16G的一个,都是3.0的。还有一堆U盘,三四个吧,每个都是8G的。还有几个U盘,想不起来了。《西方哲学史》纸质书前三分之一,上面做了笔记。此次出行的调研笔记,全部。
我们电话了出租车司机,没有发现。我们愿望出价,还是没有发现。我报了案,去火车站不远的派出所查看监控录像。我努力看了,我的视力不足以找到自己。大哥又看了一遍,他找到了我,不幸的是,此时我被一辆经过的车挡住了,地上是否有书包都看不清楚。
总之,找不到了。
为了进一步灭绝你的希望,我得提醒你一下,书包里有一盒我的名片,上面有电话,有地址,有电子邮件。EOS 50 D的包里还有同事樊老师的一张名片。
我多么希望拾到的人能留下所有的一切,但是把我的笔记和做了笔记的纸质书发给我。邮费也可以我出。唉,想想就心情沮丧。我还曾经跑步两三个街区追上一辆自行车,把别人掉在路上的一条烟还给他呢。恩,这世界不公平。
同学们对我进行了全方位多角度的安慰。从破财免灾有好事,到以后一定要小心啊,还有没事没事,身外之物。说实话,我感受到了各位的心意,不过通常,也就是这样。
后来,大哥开导成功了。丢东西的当天,火车也没坐成,第二天飞回长春。落了地,坐车上高速。大哥此时开时发话,说,你不是一直说自己信仰共产主义吗,那就不应该执着这些东西啊,都共产了啊。
我说:有的东西是纪念品,总应该是我的吧。
大哥说:那你就在心里寄托心里纪念呗,东西还是大家的。
我说:那我的笔记呐,那总是我的吧。
大哥说:知识是以笔记为载体的。如果你知识是你的为借口,而拒绝共享载体,那所有的东西都可以照此处理了。
我说:但是捡到东西的人他也用不到笔记,而对我非常重要啊。
我接着说:也不对,东西是否有用,有什么用,也不能是我一个人说了算的。因为东西是大家的。
于是,我释然了。就让捡到东西的人去玩儿吧,或者卖掉,让花低价买的人去玩儿吧。楚人失之楚人得之,谁得谁玩吧。不过据说 ipad mini 有相当强的加密,要刷机的话,需要口令。我估计一般人猜不出来我设的口令。所以,这件东西算是废了,谁也玩不到了。
不到两周,我换了 ipad mini 2, 换了 kindle paper white 2,换了二猫妈的佳能 ixus 860 is。我开始看《西方哲学史》的第二部分,中世纪部分。同学们纷纷送给我硬盘和U盘,安慰我受伤的心灵。单反等有了钱再调研再买。
所以,身外之物,一切皆可失去。只是,我偶而还是偷偷希望,哪天快递小伙电话我,打开包裹一看,啊,《西方哲学史》的第一部分。当然,我更相信杨过和沙隆巴斯说的,人生不如意,十有八九。
--------------------
博客会手工同步到以下地址: