工具开发-ESP32程序管理系统 | 系统设计
好的设计创造好的工具。
最近在玩ESP32开发板,连一些电路用ESP32进行一些简单控制,由此我产生了一个想法,如何用同一块ESP32控制多个电路而不用每次都重新烧录对应的代码?
我想到可以用一个menu菜单来组织各个功能程序,我提前写好每个功能的代码,通过一些方式组织起来,用按键和屏幕进行选择,这样就可以实现不同功能便捷切换了。
比如现在我想用GPIO18控制一个led亮起,我就选中控制led亮起的程序,提前写好或是提供选择让它使用GPIO18。过了一会我拔掉了led把电路换到了舵机上,此时我不必重新烧入舵机的控制程序,我可以直接在菜单上选择舵机程序,同样让它使用GPIO18,这样就完成了一个丝滑的切换。
如果你对程序的设计有些疑问,不妨看看我的思路。本篇我们将创造一个简单的架构,完成基础的设计,下一篇我们将思考具体实现,最后我们将进行优化。
架构设计
每编写一个程序前应该先想想你的程序要实现什么功能,哪些功能可以为一个模块,哪些模块之间相互联系,它们之间的调用链如何,依赖关系如何。
我开始思考这个问题。
纵观全局
首先,我的程序必须有一个菜单界面,供我选择不同的功能进行启动等其它操作。
再想想,我要有一个模块来专门负责组织这些程序,让它们有一个统一的入口而不是一个程序一个写法。要为它们实现一个统一的协议,给它们一个对外的接口,外部调用这个接口并告诉接口它要操作的对象,然后就能方便的操作。
然后呢,我们肯定要告诉这个控制模块我们有什么程序,这样它才能以此来组织程序,外部才能正常的调用它们。
现在,我们已经有了一个基础的流程,大概是这样的:1
2
31.告诉管理器有什么程序
2.通过菜单选择要执行的程序
3.调用接口,程序开始执行
接下来我们就要开始细化了。
管理器与协议设计
首先,管理器要有什么功能才能组织这些程序?
没错,它要有一个添加程序的方法,让我们告诉它这个程序你可以调用。
似乎还是不够,我们要再给它一个可以列举出所有程序的数据结构,在C++中它可以是数组,这样,管理器就可以便捷的知道自己有哪些可调用的程序,并以此为索引进行调用。
好了,到此管理器的核心功能就差不多了。
接下来我们要定一个统一的标准,让各个程序实现一个统一的协议,便于我们调用。
所谓协议,就是所有程序一定要遵循的结构规范。
一个程序模块,可能有如下信息:
- name名称
- 一句简短的描述
- 调用入口函数
有了这些,我们就可以在菜单上显示它的名称,或许还有它的描述,然后确认后就可以调用它的入口函数开始执行这个程序。
定完协议,我们管理器添加程序的函数也能有一个大致的框架了。
这个函数必须接收一个名称,一个入口函数,一个可选的描述。由此向数组中添加一个新的程序对象,存储下来以便后续使用。
菜单详细设计
再来想想菜单。
菜单要选择程序,肯定需要知道当前有哪些程序,这要怎么做呢?别忘了我们前面告诉了管理器我们有什么程序,那菜单需要的时候直接问管理器就可以了。
所以就需要给管理器再实现一个功能,用于告诉菜单有什么程序。
然后,当我们在菜单选择到一个程序时,我们肯定要执行它,如何来做?
管理器作为所有程序的组织者,它一定有一个执行方法,传入一个关联到程序的参数,由管理器调用对应程序的入口函数,这样一个程序就可以被执行了。
至此,我们菜单的设计也基本完成了。
结合语言
当我们有了架构形状,我们就可以开始将它们和编程语言进行结合,来思考我们设计的功能如何转化为编程语言来实现。
对于ESP32开发,我们常用到Arduino框架,语言为C++,所以接下来我们将思考如何把架构翻译为代码。
类型(模块)
我们提到,将有多个程序交给管理器管理,所以每个程序为一个对象或一个函数才能让我们更好的进行组织。为了代码的可扩展性,防止我们写出难以理解的屎山逻辑,我们将选用对象来进行组织。
现在,每个程序都是一个对象。
管理器让程序启动,所以,对于程序对象,我们应该实现一个方法,让它能被调用,从而启动运行。
接着,管理器要对程序进行管理,调度,要实现程序的添加,执行等功能,所以它需要一系列函数来执行对应操作。对于一个有如此多功能,功能之间又有联系的模块,毫无疑问类与对象就是最好的选择。
同样,要正常使用,短期来说,管理器对象应该具有添加,执行,返回程序列表的方法。
接着,我们要考虑菜单,菜单作为一个高级的模块,用于控制各个程序的运行,我们要好好研究研究。
首先,菜单应该能收到按键的输入,这样我们才能正常进行选择以及确认等操作,需要高频检查按键是否被按下。在Arduino框架下,main.cpp里的loop()函数会无限循环执行,所以我们可以将菜单的按键检测功能放在这里,每次循环都检查一次,频率非常之高。
那么作为一个能检测按键,能控制程序运行,能向管理器请求以获取程序信息并供我们选择的模块,对象应该还是最优解。
到这里不难发现,对于执行层的模块,对象是组织它们的一个不错的方式,这也是C++强的一个方面。
类型 (数据)
数据的组织也是重要的一方面。我们要维护一个存储程序信息的结构,让我们能获取它的信息,又能便利地执行它。
前面提到,一个程序有多个属性,这就需要我们用一个合适的数据类型把它们关联起来。
在C++中,结构体(struct)是一个不错的选择。
为每个程序创建一个struct,让我们便利地管理。1
2
3
4
5struct Program{
const char* name;
const char* dsc;
void (*run)();
};
在上述代码示例中,我们创建了一个结构体类型,用于存储程序的名称,描述,以及入口函数的指针。
那么解决了一个程序的存储,我们有那么多程序,要怎么关联呢?
数组让我们可以存储一类东西。
1 | Program programs[16]; |
利用上述代码,我们创建了一个能存储16个程序结构体的数组。
1 | programs[0] = { |
再以如此形式,将此数组的第一个结构体初始化为此示例结构,即添加一个新的程序,此过程我们可以称为“注册”。
综合应用
如此就有了一个基础的链条。
我们需要把程序信息交给管理器的注册方法,管理器将把信息以特定格式写入程序数组,完成一次注册。
显示菜单时,菜单向管理器请求程序列表,管理器依照program结构体数组返回一个合适的表单。
当我们在菜单选择一个程序并确认,菜单将调用管理器的执行方法,传入程序的名称(或索引等可以指向该程序的字段),管理器依据传入的参数找到对应程序的索引。
1 | programs[0].run(); |
管理器内部以索引执行程序的run方法。
程序执行……
结语
本次,我们构造了一个简单的执行框架,并依据C++的一些特性设计了基础的数据类型与执行机制。
现在的代码并不是可用的状态,后续我们将继续优化当前结构,在此基础上构建出一个最小可用版本,并逐步拓展与完善,最终完成一个完整的可用版本。请静候下篇的详细讲解。
当然,过程不会一帆风顺,程序设计中我们会面对各种问题,包括稍不留神的语法错误,甚至是编译器无法检出的内存问题。
但请记住,错误让我们完善代码,完善的代码让我们放心使用程序解决问题。