Crazy_GF

论坛元老
  • 主题:2
  • 回复:3
  • 金钱:88235
  • 积分:103248
本帖最后由 Crazy_GF 于 2014-8-14 14:44 编辑

英文原文:A complete project with Ardu
参与翻译(3人):realZ, LeoXu, Ley

介绍:

————————————————————————————————————
我有一个上小学的女儿. 作为一位父亲,当然该负起责任. 我喜欢自己作为父亲的角色,对此我毫无疑问. 但因此所带给我的困扰则是无止境的数学学习事务. 有时候这真的让我很头疼. 2+2, 5+6, 4+3,一遍又一遍. 那才刚开始呢, 现在又多了减法: 5-4, 10-4 .. 而每个人都知道这是没有结束可言的. 我抱怨的够多了,因而决定利用此项技术. 你知道的,技术为人而生, 而现在是为我而存在. 到这里来,我亲爱的 Arduino, 我需要你.
在最开始的时候,我希望这个项目能很容易实现. 我预计所有需要做的事情就是写一些函数来展示一些数字,并且为了根据趣味性,也许要来点蜂鸣声和一些LED灯光. 然后,情况在我开始精心考虑它的时候发生了改变, 出现了一些硬件管理问题, 然后是内容管理问题. 我们这个小小的 Arduino 应用程序变成了一个认真把握的东西,这导致我写下了这篇文章. 让我们先从需求开始吧.
计划需求:
—————————————————————————————————————————
  • 系统可以显示一个菜单,提供一些基本的操作:加、减、乘、除 。
  • 用户(我的女儿)可以用键盘从菜单上选择一种算数运算来学习。
  • 会有一些难度级别:在选择运算后,难度级别会显示出来。
  • 根据选择的难度级别,会随机显示出一些问题,用户可以用键盘回答这些问题。
  • 用户可以在确认前修改自己的答案。
  • 在确认答案后,根据正确与否会显示出一条信息。
  • 如果三次答错,将会显示出正确答案。
  • 用户可以浏览菜单(点开菜单并选择菜单项)。
  • 系统应具有音频和视频警告API,错误信息可以通过该API发送。
  • 每种算术运算有一个限时小测环节。
  • 限时小测随机从简到难给出问题。  
  • 测试后会显示出统计数据(回答了多少题目,答对了多少题目)。
  • 在用户接近时系统可以引起她的注意。
  • 可以有一些数学以外的娱乐环节,比如让她唱首歌、亲自己的爸爸等等。如果不这么做,用户将无法继续使用系统。
  • 警告API可以用来在娱乐环节做一些有趣的事情。


硬件 :
—————————————————————————————————————————
我们在这个项目中需要什么:



看看下面的硬件设计:
注意一些和上面不匹配的部分:
  • LCD应该是字符LCD。
  • Arduino UNO应该用Mega替代。


(我用Arduino UNO开启了这个项目,但是因为内存的需要后来改用Arduino Mega继续这个项目。开始的时候,Arduino UNO工作的很好。但是,当代码量增加,我无法将RAM使用量控制在Arduino UNO的容量之内,然后就想你所想的,我最终启用了Arduino Mega,它有8K的SRAM。)

软件设计:

—————————————————————————————————————————

图1:设计的概览
系统被分为2个主要部分。就像你在图1中看到的,第一个模块负责硬件的管理。
  • 输入系统:我们有两个不同的键盘,他们被统一在一起,对外提供统一的接口。统一的键盘信息将在(矩阵键盘或模拟键盘上的)任何按键被按下时告知注册的客户端。
  • 输出系统:具有附加功能的字符液晶面板。
  • 发信系统:统一发信子系统。它由一个LED和一个数字蜂鸣器组成,它将不同的信号转化为目标客户端的编码。它将不同的信号转换为客户端代码,客户端代码可以根据需求运行各种代码。
  • 运动检测:用PIR传感器实现运动检测。当有人被检测到,它触发一个信号来引起注意。


表现层负责与用户的交互。它包含图形界面的处理(菜单和页面)并包含管理子系统。
  • UI管理:在这个子系统中,我们定义了图像对象。一个菜单列表被显示出来供用户选择。一个菜单项可以显示出子菜单或者一个页面。用户可以通过输入显示在其上的索引来选择菜单项。通过按'Escape'键来返回上级菜单。如果一个菜单项是一个页面,选择后将会将这个页面显示出来。页面可以显示出其上面的信息,并等待用户输入来改变它的内容,此时按下'Escape'键就会显示出用户菜单。如果用户输入错误,可以通过按'Backspace'键删除答案。根据答案的正确与否,相应的信号将会被触发。
  • 内容管理:这个子系统提供显示在屏幕上的内容,包括各种算术运算的各种难度级别的生成算法。客户端代码(页面)会向这个子系统请求内容。


简单的类框图如下。这些图像展示了基本的框架,帮助你更容易的理解实际的类实现。
硬件管理:
—————————————————————————————————————————
MFK_InputDevice将Keypad2和AdKeyboard统一为同一个接口。它处理它们的事件,并向其客户端提供一组新的编码,如下所示。

图2:输入子系统
按键映射:
KeypadButtonKeyValue (hex)
Matrix0 '0'0x30
Matrix 1 '1' 0x31
Matrix 2 '2'  0x32
Matrix 3 '3' 0x33
Matrix 4 '4' 0x34
Matrix 5 '5' 0x35
Matrix 6 '6' 0x36
Matrix 7 '7' 0x37
Matrix 8'8' 0x38
Matrix 9'9' 0x39
Matrix *Escape0x1B
Matrix #Enter0x0D
AD S1Backspace0x08
ADS2F10x80
ADS3F20x81
ADS4F30x82
ADS5F40x83

MFK_OutputDevice继承自SerialLCD类。它结合SerialLCD类的功能,并对其进行了增强。
图3:输出子系统

一个信号模式从信号源产生。一个模式连同它的索引被储存在信号控制器中。想要启动一个信号模式非常简单,只要用它的索引从信号控制器调用它。

图4:发信系统
在硬件管理层顶层的是MFK_Hardware类。它只会所有其他硬件设备,对客户端隐藏多余的复杂性。举例来说,PIRMotionandSignalController没有被暴露给客户端。但是输入和输出设备必须要向外界开放,因为UI系统需要对这些功能的直接访问。信号模式也是在这个类里构建的,可以通过索引来访问他们。
图5:硬件管理
表现层:
—————————————————————————————————————————
这一层负责与用户进行交互,它提供了视觉元素和内容。
ContentFactory根据toContentTypeEnum和ContentLevelEnum创建ContentProviders。客户端得到ContentFactory的实例,之后他可以请求一个content provider。

图6:内容管理
VisualItem是所有的视觉元素(菜单和页面)的基类。它还将硬件管理和呈现结合起来。'show' 和'msgbox'方法通过调用和回调VisualItem提供的方法使用输出设备(MFK_OutputDevice)和输入设备(MFK_InputDevice) 。'msgbox'方法也有能力启动一个信号模式,只要调用硬件(MFK_Hardware)的'signal'方法就可以了。
菜单就像他的名字一样,提供了一系列的菜单项可以选择。用户可以通过一个菜单项前面的索引选择它,然后菜单 'show' VisualItem。
Pageis是显示内容的视觉工具。除了娱乐内容,它会等待用户的输入。用户'Enter'她的答案,显示信息告知她对错。显示信息之后,就需要从内容管理获取新的内容。
Chapter是ContentProvider和Page之间的中间类。当一个页面被第一次显示时,与其相关的chapter和ContentProvider就会被创建。用户的答案直接由chapter处理,并由chapter判断其对错。Chapter也对页面内的学习会话进行统计。FunChapter是一种不向用户要求答案的chapter,QuizChapter是限时的chapter。在一个quiz chapter中,问题只有在时间截止之前才能回答。
图7:UI管理
实现:
—————————————————————————————————————————
我希望你已经清楚了系统的通用结构。现在,是时候深入到代码中去,那里是真正的乐趣开始的地方。
我想以MathForKid.ino开始。它是上传到Arduino主板上的主要代码。
  1. // File: MathForKid.ino
  2. // hardware management
  3. MFK_Hardware* hw;
  4.   
  5. // presentation
  6. Menu* mainMenu;
  7.   
  8. void setup() {
  9.     // for debugging purposes
  10.     Serial.begin(9600);
  11.    
  12.     // get the instance and initialize it
  13.     hw = MFK_Hardware::getInstance();
  14.     hw->begin();
  15.   
  16.     // create user interface
  17.     CreateUI();
  18.     // show the main menu
  19.     mainMenu->show();
  20. }
  21.   
  22. void loop() {
  23.     // update hardware
  24.     hw->update();
  25.      
  26.     // update active visual item
  27.     VisualItem *v = VisualItem::getActiveItem();
  28.     if(v!=NULL)
  29.         v->update();
  30. }
复制代码
就这么多。在Arduino上运行你的应用吧。好吧,也许解释一下会更好。
正如我在“软件设计”那部分开头所说的,我们有两个部分:一个用来硬件管理,另一个用来展示。它们在代码顶部定义为全局变量,我们在 'setup'函数中将它们初始化。'loop'函数调用代码来更新它们。
事实上,CreateUI方法也是在这个文件中实现的。当用户开始交互时,它创建用户接口。mainMenu、所有的子菜单和一切页面都是这个方法产生的,chapter的属性也是其赋予的。
  1. void CreateUI() {
  2.     mainMenu = new Menu("main");
  3.   
  4.     // addition
  5.     Menu* m = new Menu("+");
  6.     mainMenu->addMenuItem(m);
  7.   
  8.     // level-1 page
  9.     Page* p = new Page("L1");
  10.     p->setChapterProperties(Chapter::NormalChapter, \
  11.             ContentFactory::Addition, ContentFactory::Level1Content);
  12.     m->addMenuItem(p);
  13.   
  14.     // level-2 page
  15.     p = new Page("L2");
  16.     p->setChapterProperties(Chapter::NormalChapter, \
  17.             ContentFactory::Addition, ContentFactory::Level2Content);
  18.     m->addMenuItem(p);
  19. ...
复制代码

我们接着来看这个应用的设计模式。
就像你所想的,MFK_Hardware是Facade模式的一个例子。它将底层的硬件管理问题隐藏起来,并对客户端提供了干净的接口。它同时也是 Singleton模式的代表,因为整个系统运行时其只产生一个实例。为了实现这个功能,MFK_Hardware的构造器、复制和赋值操作都被声明为私有方法。
  1. // File: MFK_Hardware.h
  2. // private constructor to achieve singleton pattern
  3.     MFK_Hardware();
  4.     MFK_Hardware(MFK_Hardware const&); // copy disabled
  5.     void operator=(MFK_Hardware const&); // assigment disabled
复制代码

你只能通过getInstance静态方法访问它们,这个方法是公共的:
  1. // File: MFK_Hardware.h
  2. // static method to get the instance
  3.     static MFK_Hardware* getInstance() {
  4.         static MFK_Hardware hw;
  5.         return &hw;
  6.     };
复制代码

MFK_InputDevice也是Facade模式的一个好例子。它将两个不同的输入设备(矩阵键盘和模拟键盘)映射为同一个设备。不止这样,它还具有Observer模式的特征。它是MFK_InputDeviceClient类型的ClientOwner,客户端可以在MFK_InputDeviceClient中注册/注销其对MFK_InputDevice的监听。这样,当状态变化的时候(在这里就是按键被按下),所有的客户端都会被告知。我在这个项目的很多地方都使用了这个模式。
  1. // File: MFK_InputDevice.h
  2. template<>
  3. class MFK_InputDevice<MFK_InputDeviceClient>:
  4.     public ClientOwner<MFK_InputDeviceClient> {
  5. private:
  6.     ...
  7.     // informs registered clients
  8.     void informClients(char);
复制代码
  1. // File: MFK_InputDevice.cpp
  2. void MFK_InputDevice<MFK_InputDeviceClient>::informClients(char c) {
  3.     for(int i=0; i<5; i++) {
  4.         if(this->client[i]!=NULL)
  5.             this->client[i]->invokeMFKInputCallback(c);
  6.     }
  7. }
复制代码

ClientOwner实现了register/unregister功能。一个MFK_InputDeviceClient重载invokeMFKInputCallback回调方法并将其自己注册到ClientOwner。之后MFK_InputDevice可以通过它提供的回调方法告知它状态的变化。VisualItem继承了MFK_InputDeviceClient,因此一个visual item可以将自己注册到MFK_InputDevice并监听输入。
ContentFactory类和ContentProvider类也有一些有趣的属性。ContentFactory是一个Singleton,它也有Factory模式的特征。它为客户端产生内容。但ContentProvider的输出依赖于客户端的请求。
  1. // File: ContentFactory.cpp
  2. ContentProvider* ContentFactory::getContentProvider(ContentTypeEnum op,\
  3.         ContentLevelEnum level) {
  4.     if(this->contentProvider[op][level] != NULL)
  5.         return this->contentProvider[op][level];
  6.   
  7.     ContentProvider *p=NULL;
  8.   
  9.     switch(op) {
  10.         case Addition:
  11.             switch(level) {
  12.                 case Level1Content:
  13.                     p = new ContentProvider(ContentP_0_0);
  14.                     break;
  15. ...
  16.     return p;
  17. }
复制代码

ContentProvider类和Capter类可以被当做Strategy模式的一个例子。一个content provider在一个chapter环境下运行。ContentProvider通过为不同实例准备的算法提供信息(在这里就是问题和答案)。
  1. // File: Chapter.cpp
  2. char* Chapter::next() {
  3.     char *retval=NULL;
  4. ...
  5.     retval = this->contentP[0]->getContent(Chapter::CONTENT, \
  6.                     Chapter::ANSWER);
  7. ...
  8.     return retval;
  9. }
复制代码

总结:
—————————————————————————————————————————     
你懂的,开发应用不只是敲代码。好的需求带来好的设计,好的设计带来好的软件。什么是好?这在很多软件工程的书中都有回答(这不是本文的主题)。为了更好地开发软件,我们需要知道设计模式。(顺便问一句,这段中一共出现了多少个好呢?)
设计模式,像它的名字一样,概括了一类问题的解决方案。因此,不要再造轮子了,为了更好的工作,我们需要有关设计模式的基础知识。
但是,将设计模式应用到一个问题上比看起来的要难。你不能让问题去适应模式,如果你改变原有问题那只会让你的软件变得更糟。我所做的,就是对模式进行修改,让它适应我的需求。如果当前问题不需要一种结构,我不会为未来的预期设想对结构做任何改动。你可能会觉得这很显然,但是我已经见过很多项目就是因为这个原因延期了。在软件开发中,时间代表金钱,时间就等于金钱。你做多余的事情就意味着金钱的损失,对你和客户都一样。
我会继续研究这个项目。我知道一些部分可以做的更好,如果你有任何建议我非常欢迎。(娱乐环节还没有做,不是flash library就是我代码中的调用有问题,所以我没能完成它)
参考资料:
—————————————————————————————————————————
发布历史:
—————————————————————————————————————————
这是一个进行中的项目。源代码在文章发布后有所改动,你可以在https://github.com/ozanoner/arduino下载到最新的版本。



本文转载自:开源中国社区