20140629

geek青年的状态机,查表,纯C语言实现

geek青年的状态机,查表,纯C语言实现

1. 问题的提出,抽象

建一,不止是他,不少人跟我讨论过这样的问题:如何才能保证在需求变更、扩充
的情况下,程序的主体部分不动呢?

这是一个非常深刻和艰难的问题。在进入实质讨论之前,我们还得先明确什么是"
主体",就是我们不希望动的那一部分是什么。事实上,没有什么" 主体",这是被
我们主观划分的,代码中有一部分是不动的,另一部分是动的。而追求永恒(一劳
永逸?) ,是我们的天性吧。

我们希望实现一段程序,换一些东西,游戏就由 双截龙 变成了 超级玛丽,再换
一点东西,就变成了 魂斗罗。只要招些美工,再招些脚本作者,所有的程序员就
可以--解雇了。

这看起来不太现实,那么我们来看一段类似的,但是更现实一点的。我们希望实现
一段程序,在每轮迭代/循环中,这段代码都能完成我们需要做的任 务,虽然这些
任务可能在每轮迭代中有所不同。在数学归纳法,在 sigma 符号的的周围,甚至
在积分符号的周围,都在发生这样的事情。

这些梦想或者已经实现的技术,都基于"抽象"。我们试图找到在不同的情境 (动
作、需求) 下那些相同的部分。我们对具体事件做抽象,并且期待抽象的结果适用
于所有的具体的事例。这样,原来的很多工作就成为 应用抽象的理论 的过程,不
再需要创造力,因此也不再能吸引我们。那么,我们再对抽象的结果继续抽象,直
到形而上。

2. 状态机的引擎

引擎,就是上文中提到的开发出一个游戏,然后能衍生出很多游戏的技术。代码的
核心部分、流程部分不会改变,只有数据 (甚至可以在外部文件中) 才随需求的变
化而变化。

状态机,也可以用引擎实现。实现这一目标的技术也存在已久,就是查表。查表的
经典案例是 求三角函数 (在一定精度下),常量时间复杂度的解决方案 就是查
表。事先把三角函数在不同度数下的值都求出来,放在hash表 (?) 里。你要查哪
个度数,我就去查哪个度数对应的函数值。

在这个案例里,查表的那段代码,不随三角函数由sin变成cos或tan而发生任何变
化。这就是引擎,被查的表就是数据。

3. 接口

我们期待的接口跟前一篇普通青年中的完全一样。在主函数中调用 void
state_change(enum message m) 向状态机传递消息,用 test.in 作为测试用例。
主函数还知道,一共就这样几种消息:

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

4. 状态迁移表

在讲如何查表前,我们先设计 表 本身。我们期待表格能够描述 状态迁移 中的要
素。记得么,一共4个。 (1) 当前状态, (2)当前消息, (3)将迁移到的状态,
(4)在状态迁移中的动作。我们期待能用表格,而不是如普通青年一文中用代码
(switch-case)的方式描述。因为我们相 信,改表格比改代码容易。

状态迁移表与状态迁移图完全等价。

表格看起来像下面这样,如果想像划上竖线效果更佳。。

1 struct transition fsm[transition_num] = {
2 /* current_state, message/event, next_state*/
3 {s_play, stop, s_stop},
4 {s_play, pause, s_pause},
5 {s_pause, pause, s_play},
6 {s_pause, stop, s_stop},
7 {s_stop, forward, s_forward},
8 {s_stop, play, s_play},
9 {s_stop, backward, s_backward},
10 {s_stop, record, s_record},
11 {s_forward, stop, s_stop},
12 {s_backward, stop, s_stop},
13 {s_record, stop, s_stop} };

每一行,都是一组状态迁移的匹配。第一列是当前状态,第二列是接收到的消息,
第三列是在此种情况下将迁移到的状态。每增加一个迁移的匹配,我 们就按这样
的规则增加一行。这规定了状态机迁移中4要素里的3条,剩下的那条是在迁移中的
动作,后面再介绍。

当然,为了遵循C语言的语法,我们需要在此前就定义 (1) 状态枚举、 (2) 消息
枚举,还有 (3) 迁移的结构体。如下。

1 enum state { s_stop='s', s_play='p', s_forward='f', s_backward='b',
s_pause='_', s_record='r' };
2 enum message { play, stop, forward, backward, record, pause };
3
4 struct transition {
5 enum state current;
6 enum message m;
7 enum state next;
8 };

我们还需要定义一共多少条迁移规则,是为了我们还没有写出来的代码准备的,不
过此处已经用到,所以定义如下。

1 #define transition_num 11

5. 迁移时的动作

我们希望把迁移时的动作放在每个状态到达之处。即,每个状态都可以有一些"副
作用"。这与迁移时的动作是等价的,证明略去。如果仅想在迁移时 写代码,也可
以利用这种方法实现。

状态机的动作 表格如下:

1 struct state_action state_action_map[state_num] = {
2 {s_stop, do_stop},
3 {s_play, do_play},
4 {s_forward, do_forward},
5 {s_backward, do_backward},
6 {s_pause, do_pause},
7 {s_record, do_record}};

每一行,是一个状态对应的动作。第一列是状态,第二列是对应的动作。这样,每
增加一个状态 (如果它有对应动作),就在这里加入一行;动作对应的函数需要实
现,后面会介绍。

类似于状态迁移图,为了遵循C语言语法,我们需要在此前声明如下。

1 #define state_num 6
2 typedef void (*action_foo)() ;
3
4 enum state { s_stop='s', s_play='p', s_forward='f', s_backward='b',
s_pause='_', s_record='r' };
5
6 /* action starts */
7 void do_stop() {printf ("I am in state stop and should doing something
here.\n");}
8 void do_play() {printf ("I am in state play and should doing something
here.\n");}
9 void do_forward() {printf ("I am in state forward and should doing
something here.\n");}
10 void do_backward() {printf ("I am in state backward and should doing
something here.\n");}
11 void do_pause() {printf ("I am in state pause and should doing
something here.\n");}
12 void do_record() {printf ("I am in state record and should doing
something here.\n");}
13
14 struct state_action {
15 enum state m_state;
16 action_foo foo;
17 };

第1行,是状态的数量。第2行和第7行到第12行,以及第16行,使用了函数指针(指
向函数的指针,一个指针,它的基类型是一个函数),用于 表示要执行的动作。第
4行,是状态枚举。第14行到第17行,是 状态-动作 对应关系的结构体。

第7行至第12行,是动作的执行部分。当增加的状态需要动作时,程序员要在此处
加入一个函数,它遵守第2行的签名约定。

6. 引擎

如果表格的数据结构已定,代码就好写了。我们的引擎代码的核心部分是查表,遍
历表格,找到与当前状态、当前消息匹配的将迁移到的状态。

我们还是自顶向下,假设 查表部分已经完成,为主函数提供与 普通青年一文相同
的接口--而内部实现是不同的。

1 void state_change(enum message m)
2 {
3 static state = s_stop;
4 enum state next;
5 int index = 0;
6
7 index = lookup_transition(state, m, fsm);
8 if(index!=ERR)
9 {
10 state = fsm[index].next;
11 lookup_action(state, state_action_map)();
12 }
13 return;
14 }

如第3行如示,初始状态是 停止。在第7行,我们引用了一个尚未写好的函
数,lookup_transition。虽然函数还不存在,不过我们能猜出来它的作用,查
表,找到 当前状态是 state,当前消息是 m 时所对应的表项的下标 index。fsm
参数是为了可能有多个状态迁移表设计的,此处可以略过。

当查找到 index 以后,且 index 不是 ERR (没找到),就可以令 下一个状态为
state = fsm[index].next,见第10行。

以上,完成了状态迁移4要素中的3个:当前状态、当前消息、将迁移到的状态。

第11行,完成的功能是执行与状态对应的动作。这里又用到函数指针。在代码
lookup_action(state, state_action_map)() 中,lookup_action(state,
state_action_map) 用于找到状态 state 对应的动作,后面的 "()",是因为这个
动作是一个函数指针,可以使用这样的方式执行这个指针指向的函数。与上文中的
fsm 参数类似,state_action_map是为了应对有多个状态-动作表的情况,这里可
以略过。

无论数据 (状态迁移、状态-动作)如何变化,引擎代码都不会变化。所以,甚至可
以把引擎放在静态或动态链接库里,或者把数据放在外部文件里,运行时再载入,
从而提 高部署时的灵活性。

7. 查表

刚刚用到的两个未定义的函数 lookup_transition(state, m, fsm) 和
lookup_action(state, state_action_map) 都使用了查表的方法。

代码如下。可以看出,二者的结构非常类似,遍历数组 (for循环) ,找到符合条
件的元素 (if判断),然后把该元素的索引或者该元素结构体的某个成员返回。

ERR 和 ACTION_NOT_FOUND 是用来容错的,万一表格有误,没有查到匹配的项。

1 int const ERR = -1;
2 int lookup_transition (enum state s, enum message m, struct transition
* t)
3 {
4 int ret=ERR;
5 int i;
6 for(i=0;i<transition_num;++i)
7 {
8 if(t[i].current == s && t[i].m == m)
9 {
10 ret = i;
11 }
12 }
13 return ret;
14 }
15
16 action_foo ACTION_NOT_FOUND = NULL;
17 action_foo lookup_action(enum state s, struct state_action* a)
18 {
19 action_foo ret = ACTION_NOT_FOUND;
20 int i=0;
21 for (i=0;i<state_num;++i)
22 {
23 if(s == a[i].m_state)
24 {
25 ret = a[i].foo;
26 }
27 }
28 return ret;
29 }

8. 总结

geek青年,从接口上看,与普通青年并无不同。甚至在情况相对简单 (状态少、状
态迁移种类少) 的时候,代码量比普通青年还有不如。那么,geek青年的长处在哪
里呢?

古人云:沧海横流方显英雄本色。古人又云:大丈夫山崩于前不变色,海啸于后不
动容。

geek青年的长处在于,他始终如一,无论遇到的情形是多么糟糕多么恶劣,他始终
没有变化。这个世界上,总需要一些因素,一些承诺,不随任何 易变的感情、任
何旁人不能承受的痛苦或诱惑而变化,稳定地坚持。这才能让我们对这个世界保留
一丝希望,未来才能够和值得期待。

这一篇和上一篇的代码在这里 [http://download.csdn.net/detail/younggift
/7569627]。

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

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

[http://giftdotyoung.blogspot.com]

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

普通青年的状态机,纯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]

20140615

俄罗斯方块: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]

20140601

电子书阅读及工具

电子书阅读及工具

我现在看电子书,除了计算机以外,还常使用 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]