65.9K
CodeProject 正在变化。 阅读更多。
Home

Arduino 中的设计模式应用

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.62/5 (10投票s)

2014年2月18日

CPOL

11分钟阅读

viewsIcon

58062

downloadIcon

601

一个使用Arduino的示例项目。

引言

我有一个上小学的女儿。作为父亲,我当然有一些责任。我喜欢当父亲,这一点我没问题。让我困扰的是那些没完没了的数学学习时间。有时真的让我抓狂。一遍又一遍地2+2,5+6,4+3。那是开始,现在轮到减法了:5-4,10-4……大家都知道这没完没了。我说够了,决定利用技术。你知道,技术是为了人们,现在是为了我。来吧,我亲爱的Arduino,我需要你。

最初我曾希望这个项目会很容易实现。我只需要编写一些函数来显示数字,可能再加一个蜂鸣器和一些LED让它更有趣。然而,在我开始 细致地 思考它之后,情况发生了变化。出现了一些硬件管理问题,然后是UI管理,接着是内容管理。我们这个小小的Arduino应用程序变成了一个需要更认真对待的东西,这促使我写了这篇文章。让我们从需求开始。

项目需求

  • 系统应在显示屏上显示一个菜单,用于基本运算:加、减、乘、除。
  • 用户(我的女儿)可以使用键盘从菜单中选择一个算术运算进行学习。
  • 应有几个难度级别。选择运算后,显示屏上会显示难度级别选择。
  • 根据难度级别,显示屏上会显示随机题目,用户可以使用键盘回答题目。
  • 用户可以在输入答案前进行更正。
  • 输入答案后,将显示一条消息,说明答案是否正确。
  • 连续回答错误3次后,将显示正确答案。
  • 用户可以浏览菜单(菜单向上或选择菜单项)。
  • 系统应具有音频和视觉警告基础设施。消息将伴随这些基础设施。
  • 每项运算都将有一个限时测验部分。
  • 测验部分将开始随机提问,从最简单的级别开始,然后上升到更高的难度级别。
  • 测验结束后将显示统计数据(提问多少道题,答对多少道)。
  • 当她过来时,系统可以引起她的注意。
  • 除了数学题之外,还应该有一些有趣的部分,引导用户做一些事情,比如“唱歌”、“亲吻你的爸爸”等等。否则,用户验收测试绝对会失败!
  • 警告基础设施可以作为娱乐部分有趣的材料。

硬件

本项目需要什么

  1. Arduino Mega
  2. 串行LCD
  3. 矩阵键盘
  4. 模拟键盘
  5. PIR运动传感器
  6. LED和400欧姆电阻
  7. 数字蜂鸣器模块
  8. 连接线

有关硬件设计,请参阅以下内容。

关于零件不匹配的注意事项

  1. LCD是串行LCD。
  2. Arduino UNO应替换为Mega。

(我开始这个项目时使用的是Arduino UNO,但由于内存需求更大,我决定继续使用Arduino Mega。起初,Arduino UNO一切都很好。然而,当代码开始变大时,过了某个点,我就无法将RAM使用量控制在Arduino UNO容量以下了,正如你所料,最终换成了Arduino Mega,它拥有8K的SRAM。)

软件设计


图1:设计概述。

系统可以分为两个主要部分。正如你在图1中看到的,第一部分负责硬件管理。

  • 输入系统:我们有两种不同的键盘,它们被合并成一个,如其他组件所示。统一键盘在任何键盘(矩阵键盘或adkeyboard)按下任何按键时通知已注册的客户端。
  • 输出系统:具有附加功能的串行LCD。
  • 信号:统一信号子系统。它由LED和数字蜂鸣器组成,并提供为客户端代码定义不同信号模式的能力。客户端代码可以根据需要启动任何模式。
  • 运动检测:带PIR传感器的运动检测能力。当检测到有人时,它会触发信号以引起注意。

表示层负责用户交互。它包含视觉项目(菜单和页面)的基础设施和内容管理子系统。

  • UI管理:在此子系统中,定义了视觉项目。菜单在显示屏上列出项目供用户选择。菜单项可能代表子菜单或页面。用户可以通过按键盘上相应的键,使用显示的索引选择菜单项。通过按“Escape”键可以向上导航菜单。如果选定的菜单项是页面,则显示该页面。页面负责显示与之关联的内容。页面等待用户输入以更改其内容,如果按“Escape”键,则会显示所有者菜单。在页面中,按“Enter”键将评估答案。如果用户犯了错误,可以通过按“Backspace”键删除其答案。根据答案的正确性,会触发相应的信号。
  • 内容管理:此子系统提供要在屏幕上显示的内容。提供了不同级别算术运算的算法。客户端代码(页面)从此子系统请求内容。

简化的类图如下。这些图像显示了基本结构,以便于理解实际的类实现。

硬件管理

MFK_InputDeviceKeypad2AdKeyboard合并为一个设备。它处理它们的事件,并为客户端生成一个新事件,其中包含一组新的按键,如下所示。

 


 

图2:输入子系统。

按键映射

键盘 Button 值(十六进制)
矩阵 0 '0' 0x30
矩阵 1 '1' 0x31
矩阵 2 '2' 0x32
矩阵 3 '3' 0x33
矩阵 4 '4' 0x34
矩阵 5 '5' 0x35
矩阵 6 '6' 0x36
矩阵 7 '7' 0x37
矩阵 8 '8' 0x38
矩阵 9 '9' 0x39
矩阵 * 转义 0x1B
矩阵 # Enter 0x0D
AD S1 退格键 0x08
AD S2 F1 0x80
AD S3 F2 0x81
AD S4 F3 0x82
AD S5 F4 0x83

 

 

MFK_OutputDevice派生自SerialLCD类。它通过组合SerialLCD的功能来增强该项目的SerialLCD

 

 

图3:输出子系统。



信号模式是通过使用信号源构建的。模式应与索引一起存储在信号控制器中。启动信号模式的唯一方法是通过其索引从信号控制器调用它。


图4:信号子系统。

在硬件管理层之上,放置了MFK_Hardware类。它协调所有其他硬件设备,并向客户端隐藏冗余的复杂性。例如,PIRMotion和SignalController不向客户端公开。但是,输入和输出设备必须暴露给外部世界,因为UI系统需要直接访问它们的功能。信号模式也在该类中构建。它们可以通过其命名的索引准备好使用。

图5:硬件管理

演示

此层负责用户交互。它提供视觉元素和内容。

ContentFactory根据ContentTypeEnumContentLevelEnum创建ContentProvider。客户端获取内容工厂的实例,然后可以请求内容提供程序。


图6:内容管理

VisualItem是视觉元素(菜单和页面)的基类。它还将硬件管理和表示绑定在一起。“show”和“msgbox”函数使用输出设备(MFK_OutputDevice)和输入设备(MFK_InputDevice)调用VisualItem提供的回调函数。“msgbox”函数还能够通过调用硬件(MFK_Hardware)的“signal”函数来启动信号模式。

Menu顾名思义,提供一个菜单项列表供选择。用户可以通过其索引选择一个项目,菜单会显示该VisualItem

Page是显示内容的视觉项。除了趣味内容外,它期望用户输入。用户输入答案,然后显示一条消息通知用户。消息显示后,需要从内容管理中获取新内容。

ChapterContentProviderPage之间的中间类。当第一次显示页面时,会创建其章节和相关的ContentProvider。用户答案被定向到章节,并在章节中评估答案的正确性。Chapter还保存了该页面学习会话的统计数据。FunChapter是一种章节,用户不需要对其进行回答。QuizChapter是限时章节。在测验章节中,会一直提问直到时间结束。

 

图7:UI管理

实现

希望系统的总体结构清晰。现在,是时候深入研究代码了,这正是本文标题的意义所在。

我想从MathForKid.ino开始。这是上传到Arduino Mega的主代码。

 

// File: MathForKid.ino
// hardware management
MFK_Hardware* hw;
 
// presentation
Menu* mainMenu;
 
void setup() {
    // for debugging purposes
    Serial.begin(9600);
  
    // get the instance and initialize it
    hw = MFK_Hardware::getInstance();
    hw->begin();
 
    // create user interface
    CreateUI();
    // show the main menu
    mainMenu->show();
}
 
void loop() {
    // update hardware
    hw->update();
    
    // update active visual item
    VisualItem *v = VisualItem::getActiveItem();
    if(v!=NULL)
        v->update();
}
 

 

就这些了。在Arduino上运行你的应用程序。好吧,也许稍微解释一下会更好 微笑 | :)

正如我在“软件设计”部分的开头所说,我们有两个主要组件:一个用于硬件管理,一个用于表示。它们在代码顶部全局定义,并在“setup”函数中进行初始化。 “loop”函数调用代码来更新它们。

实际上,CreateUI函数也在此文件中实现。它创建用户交互发生的用户界面。在此函数中创建mainMenu、所有子菜单和所有页面,并为页面指定章节属性。

void CreateUI() {
    mainMenu = new Menu("main");
 
    // addition
    Menu* m = new Menu("+");
    mainMenu->addMenuItem(m);
 
    // level-1 page
    Page* p = new Page("L1");
    p->setChapterProperties(Chapter::NormalChapter, \
            ContentFactory::Addition, ContentFactory::Level1Content);
    m->addMenuItem(p);
 
    // level-2 page
    p = new Page("L2");
    p->setChapterProperties(Chapter::NormalChapter, \
            ContentFactory::Addition, ContentFactory::Level2Content);
    m->addMenuItem(p);
...

让我们继续讨论具体的应用设计模式。

正如你可能预料到的,MFK_Hardware是**外观(facade)**模式的一个例子。它隐藏了底层硬件管理问题的复杂性,并为客户端提供了一个干净的接口。它也是一个**单例(singleton)**,因为在整个系统中它只有一个实例。为了实现这一点,它的构造函数、复制和赋值运算符在类的声明的私有部分被声明。

    
// File: MFK_Hardware.h
// private constructor to achieve singleton pattern
    MFK_Hardware();
    MFK_Hardware(MFK_Hardware const&); // copy disabled
    void operator=(MFK_Hardware const&); // assigment disabled 

你只能使用getInstance静态函数来访问它,它在公共部分。

    
// File: MFK_Hardware.h
// static method to get the instance 
    static MFK_Hardware* getInstance() {
        static MFK_Hardware hw;
        return &hw;
    };

来自 http://www.dofactory.com/Patterns/Patterns.aspx

  • 外观(Facade):一个单一类,代表整个子系统(结构型模式)。
  • 单例(Singleton):一个类,只能存在一个实例(创建型模式)。

MFK_InputDevice也是**外观(facade)**模式的一个很好的例子。它将两个不同的输入设备(矩阵键盘和adkeyboard)合并为一个。此外,它还扮演着**观察者(observer)**模式的主题(subject)角色。它是一个类型为MFK_InputDeviceClientClientOwner,客户端可以向MFK_InputDevice注册/注销。因此,当状态发生变化时(在这种情况下是按键),所有客户端都会收到通知。我在这个项目中很多地方都使用了这种模式。

// File: MFK_InputDevice.h
template<>
class MFK_InputDevice<MFK_InputDeviceClient>:
    public ClientOwner<MFK_InputDeviceClient> {
private:
    ...
    // informs registered clients
    void informClients(char);
... 
// File: MFK_InputDevice.cpp
void MFK_InputDevice<MFK_InputDeviceClient>::informClients(char c) {
    for(int i=0; i<5; i++) {
        if(this->client[i]!=NULL)
            this->client[i]->invokeMFKInputCallback(c);
    }
}

ClientOwner实现了注册/注销函数。MFK_InputDeviceClient覆盖了invokeMFKInputCallback函数并将其自身注册到ClientOwner。然后MFK_InputDevice可以通过它们提供的回调函数通知它们。VisualItem继承自MFK_InputDeviceClient,因此视觉项可以注册到MFK_InputDevice以观察它。

来自 http://www.dofactory.com/Patterns/Patterns.aspx

  • 观察者(Observer):一种将变化通知给多个类的方法(行为型模式)。

ContentFactoryContentProvider类也有一些有趣的特性。ContentFactory是**单例(singleton)**,可以被认为是某种形式的**工厂(factory)**模式。它为客户端构建内容提供程序,但内容提供程序的具体实现根据客户端的需求而变化。

// File: ContentFactory.cpp
ContentProvider* ContentFactory::getContentProvider(ContentTypeEnum op,\
        ContentLevelEnum level) {
    if(this->contentProvider[op][level] != NULL)
        return this->contentProvider[op][level];
 
    ContentProvider *p=NULL;
 
    switch(op) {
        case Addition:
            switch(level) {
                case Level1Content:
                    p = new ContentProvider(ContentP_0_0);
                    break;
...
    return p;
}

 

 

来自 http://www.dofactory.com/Patterns/Patterns.aspx

  • 工厂(Factory):创建多个派生类实例(创建型模式)。

ContentProvider可以被认为是与Chapter一起的**策略(strategy)**模式的一个例子。章节是内容提供程序所在的上下文。ContentProvider为每个实例提供不同的算法结果(在这种情况下是问题和答案)。
 

// File: Chapter.cpp
char* Chapter::next() {
    char *retval=NULL;
...
    retval = this->contentP[0]->getContent(Chapter::CONTENT, \
                    Chapter::ANSWER);
...
    return retval;
}

来自 http://www.dofactory.com/Patterns/Patterns.aspx

  • 策略(Strategy):将算法封装在一个类中(行为型模式)。

结论

你知道,开发应用程序不仅仅是编码。更好的需求收集 leads to 更好的设计。更好的设计 leads to 更好的软件,而“更好”在许多软件工程书籍中都有定义(这不是本文的主要主题)。为了开发更好的软件,最好了解设计模式。(顺便问一下,这段话里有多少个“更好”?)

顾名思义,设计模式是解决一类问题的方案的泛化。因此,为了不重复造轮子,了解设计模式的基本知识可以使我们的工作更轻松。

然而,将设计模式应用于问题与了解它不同。你不能强迫你的问题去匹配设计模式。当你试图将你的实际问题变成别的东西时,情况会变得更糟。我所做的是根据我的需求量身定制模式。如果我在当前问题域中不需要某个结构,我不会因为对未来版本的期望而做任何事情。你可能认为这个陈述非常明显,但我见过许多项目因此而延期。在软件开发中,时间不仅仅是金钱,而是字面意义上的金钱。你过度的任何行为都会导致金钱损失,无论是对你还是对你的客户。

在codeproject上有很多关于设计模式的好文章。我特别推荐你阅读 的设计模式系列。为Ian鼓掌。

我将继续研究这个项目。我知道有些地方可以做得更好,任何建议都将不胜感激。(趣味部分仍然缺失,我不得不注释掉它们,因为flashflash lib或我的代码中的调用存在问题。)

参考文献

  1. http://www.dofactory.com/Patterns/Patterns.aspx
  2. http://www.cplusplus.com/doc/tutorial/
  3. http://arduino.cc/
  4. http://arduiniana.org/libraries/flash/

     

历史

这是一个正在进行中的项目。源代码可能在本文发布后发生更改。你可以从 https://github.com/ozanoner/arduino 下载最新版本。


 

© . All rights reserved.