Flipper Zero通用红外遥控应用开发:事件驱动与模块化设计实践
1. 项目概述一个为Flipper Zero打造的通用红外遥控应用如果你手头有一台Flipper Zero并且对它的红外遥控功能仅限于控制家里的电视和空调感到意犹未尽那么kala13x/flipper-xremote这个项目绝对值得你花时间深入研究。简单来说这是一个为Flipper Zero开源掌上安全工具开发的第三方应用程序它的核心目标是将Flipper Zero从一个“万能遥控器”升级为一个“通用遥控器平台”。这听起来可能有点绕让我解释一下。Flipper Zero自带的红外功能很强能学习和发射信号但它的界面和数据库是固定的。如果你想控制一个冷门的设备或者想按照自己的习惯重新编排遥控器布局自带的App就显得有些力不从心。而xremote项目通过提供一个高度可定制、模块化的应用框架允许你为几乎任何红外设备创建专属的、界面友好的遥控器。它不只是发射一个信号而是构建了一套完整的用户交互逻辑。这个项目适合谁呢首先是Flipper Zero的硬核玩家和开发者他们不满足于现成功能渴望深度定制。其次是智能家居爱好者尤其是那些拥有大量非标准或老旧红外设备的用户xremote能帮你把它们统一到Flipper Zero这个便携终端上。最后它也是一个绝佳的嵌入式GUI开发学习案例你能从中看到如何在资源受限的设备上实现清晰的状态管理和界面响应。2. 核心设计思路模块化与状态管理flipper-xremote的成功很大程度上归功于其清晰、前沿的设计哲学。它没有选择常见的“一个巨大函数处理所有按钮”的简单模式而是采用了更接近现代前端或应用开发的架构基于事件和状态管理的模块化设计。理解这一点是理解整个项目如何运作的关键。2.1 为什么选择事件驱动架构在嵌入式开发尤其是Flipper Zero这种内存和算力都有限的设备上传统的“轮询大循环”模式很容易导致代码臃肿、响应迟缓且难以维护。xremote面临的挑战是需要响应物理按键、处理红外信号发送、更新屏幕显示并且要能方便地扩展新的遥控器面板。事件驱动架构完美地解决了这些问题。它将“发生了什么”如按键按下和“如何处理”分离开。应用程序的核心是一个事件分发器它不关心具体业务逻辑只负责接收来自硬件按键、红外接收头或软件内部产生的“事件”然后将这些事件派发给注册了的“事件处理器”。这样做的好处显而易见解耦与可维护性添加一个新的遥控器类型比如一个投影仪面板你只需要编写这个投影仪专属的事件处理逻辑和界面绘制代码然后将其注册到系统中即可。完全不需要修改主循环或其他遥控器的代码。响应性系统可以立即响应高优先级事件如退出请求而不必等待一个冗长的任务执行完毕。资源友好在没有事件时CPU可以进入低功耗状态这对于Flipper Zero的续航至关重要。在xremote中你会在代码里频繁看到view_dispatcher_send_event、view_dispatcher_send_custom_event这样的函数调用这就是其事件驱动核心的体现。2.2 状态机管理复杂界面流的核心有了事件我们还需要知道“当前处于什么状态”。一个遥控器应用可能有很多界面主菜单、电视遥控面板、空调遥控面板、信号学习界面、设置界面等。用户在这些界面间切换每个界面下相同的按键比如“OK”键可能需要触发不同的动作。xremote使用有限状态机来优雅地管理这一切。你可以把状态机想象成一个地铁线路图每个“站点”就是一个应用状态如XRemoteViewId定义的主菜单、遥控器等而“事件”如按下某个按键、接收到某个命令就是触发换乘的指令。例如当前状态是主菜单用户按下“OK”键产生一个事件。状态机根据当前状态和接收到的事件决定跳转到电视遥控器状态并加载电视遥控器的视图和事件处理器。在电视遥控器状态下按下“OK”键事件则被派发给电视遥控器专属的事件处理器其逻辑可能是“发送红外信号打开电视菜单”。这种设计使得界面导航逻辑变得极其清晰和可预测完全避免了用一堆if-else嵌套来判断当前界面的混乱局面。在代码中这通常通过view_dispatcher_switch_to_view函数和各个View的event_callback回调函数协作实现。2.3 视图与模型的分离这是另一个关键设计模式。xremote严格区分了数据Model和显示View。模型存储当前遥控器面板的所有数据。例如对于空调遥控器模型里可能保存着当前温度、模式制冷/制热、风速、风向等。模型是纯数据不关心如何显示。视图负责将模型中的数据绘制到屏幕上。它从模型中读取数据然后调用Flipper的图形API画出按钮、图标、文本和状态指示器。当用户操作事件导致数据变化时比如按“升温”键控制器会先更新模型温度值1然后通知视图“数据变了请重绘”。视图于是根据新的模型数据重新渲染屏幕。这种MVCModel-View-Controller或其变体的优势在于数据一致性所有显示都源于同一个数据源避免了状态不同步的Bug。界面复用可以为一个数据模型设计多个不同的视图比如简洁视图和详细视图。便于测试可以单独测试模型逻辑无需启动完整的图形界面。在xremote中每个遥控器类型如xremote_app_signal通常都包含了自己的模型数据结构和对应的视图渲染函数。3. 项目结构深度解析与关键文件理解了设计思路我们再来看看代码仓库的物理结构。一个清晰的项目结构是大型项目可维护性的基石。flipper-xremote的目录组织很好地体现了其模块化思想。flipper-xremote/ ├── applications/ │ └── xremote/ # 主应用程序目录 │ ├── xremote_app.h/.c # 应用核心初始化、主循环、状态机管理 │ ├── xremote_view.h/.c # 基础视图抽象与导航管理 │ ├── views/ │ │ ├── xremote_view_menu.c # 主菜单视图 │ │ ├── xremote_view_remote.c # 通用遥控器面板视图核心 │ │ └── ... (其他特定视图) │ ├── models/ │ │ ├── xremote_signal_model.h/.c # 红外信号数据模型 │ │ └── ... (其他数据模型) │ ├── helpers/ # 通用工具函数如红外信号处理 │ └── assets/ # 图标、字体等资源文件 ├── infrared/ # 红外信号数据库与编码/解码逻辑可选/扩展 └── documentation/ # 说明文档我们来重点剖析几个核心文件xremote_app.c应用的心脏这个文件包含了应用的入口点xremote_app()。它的主要职责是初始化分配内存、初始化Flipper的各个子系统存储、GUI、红外。创建视图分发器实例化整个应用的事件总线ViewDispatcher。注册视图创建主菜单、遥控器视图等并将它们添加到分发器中。启动主循环调用view_dispatcher_run进入事件监听和分发状态直到收到退出事件。资源清理在退出时释放所有分配的内存和资源。这里是应用生命周期管理的地方代码结构通常非常规整像一本操作手册。xremote_view_remote.c遥控器交互的核心这是最复杂的文件之一它实现了通用遥控器面板。其核心是一个xremote_view_remote_t的结构体里面包含了指向当前活动模型的指针如XRemoteSignalModel*。指向视图对象的指针View*。当前选中的按钮索引、界面布局信息等。它的draw_callback函数决定了屏幕上每一个按钮的位置、图标和文字。而input_callback函数则捕获用户的按键输入将其转化为特定的事件如XRemoteEventSignalSend并发送给事件分发器。xremote_signal_model.c数据的管家这个文件定义了红外信号的数据结构。一个典型的信号模型可能包含信号名称如“Sony TV Power”。红外协议类型如NEC、RC5、Samsung等。编码后的红外数据一个InfraredSignal数组。其他元数据如所属设备分类、使用频率等。它提供了一系列API如xremote_signal_model_load()从文件加载信号库xremote_signal_model_get_signal()根据索引获取特定信号。所有视图需要显示或发送信号时都必须通过模型层来获取数据保证了数据源的唯一性。实操心得阅读代码的顺序对于新手我建议按这个顺序阅读源码1. 从xremote_app.c的main函数开始理清应用启动流程。2. 查看xremote_view_menu.c理解最简单的菜单如何工作。3. 重点攻克xremote_view_remote.c这是交互核心。4. 最后再看模型和辅助文件。这个顺序由简入繁更容易建立整体认知。4. 如何为xremote添加一个新的遥控器类型理论说得再多不如动手实践。为xremote添加一个全新的遥控器类型比如为一个特定的音响系统创建遥控面板是理解其架构最好的方式。下面我将一步步拆解这个过程。4.1 第一步定义数据模型首先我们需要思考这个音响遥控器有哪些状态需要保存。假设我们的音响有开关状态、输入源蓝牙、AUX、光纤、音量0-100、高低音调节-10 到 10。我们在models/目录下创建新文件xremote_speaker_model.h/.c。xremote_speaker_model.h#pragma once #include stdint.h #include stdbool.h #ifdef __cplusplus extern C { #endif // 定义音响输入源枚举 typedef enum { SpeakerSourceBluetooth, SpeakerSourceAUX, SpeakerSourceOptical, SpeakerSourceCount } SpeakerSource; // 定义主模型结构体 typedef struct { bool power_on; // 开关状态 SpeakerSource source; // 当前输入源 uint8_t volume; // 音量 (0-100) int8_t bass; // 低音 (-10 ~ 10) int8_t treble; // 高音 (-10 ~ 10) // 可以添加更多字段如预设均衡器模式等 } XRemoteSpeakerModel; // 模型操作API XRemoteSpeakerModel* xremote_speaker_model_alloc(); void xremote_speaker_model_free(XRemoteSpeakerModel* model); void xremote_speaker_model_set_power(XRemoteSpeakerModel* model, bool on); bool xremote_speaker_model_get_power(const XRemoteSpeakerModel* model); void xremote_speaker_model_set_source(XRemoteSpeakerModel* model, SpeakerSource source); SpeakerSource xremote_speaker_model_get_source(const XRemoteSpeakerModel* model); // ... 其他getter/setter函数 #ifdef __cplusplus } #endifxremote_speaker_model.c这里需要实现头文件中声明的所有函数。例如xremote_speaker_model_alloc会调用malloc分配内存并初始化默认值如关机状态、音量50、音调0。setter函数在修改值后通常需要通知视图更新这可以通过回调函数机制实现但为了简化我们可以先实现基础的数据存储。4.2 第二步创建专属视图接下来在views/目录下创建xremote_view_speaker.c和对应的头文件。这个视图负责两件事绘制界面和处理输入。绘制回调 (draw_callback) 这个函数会被系统定期调用或当模型数据变更时调用。你需要在这里使用Flipper的Canvas API画出整个遥控器界面。static void xremote_view_speaker_draw(Canvas* canvas, void* context) { furi_assert(context); XRemoteSpeakerModel* model context; canvas_clear(canvas); canvas_set_font(canvas, FontPrimary); // 1. 绘制标题 canvas_draw_str_aligned(canvas, 64, 2, AlignCenter, AlignTop, Hi-Fi Speaker); // 2. 绘制电源状态 canvas_set_font(canvas, FontSecondary); canvas_draw_str(canvas, 5, 15, Power:); canvas_draw_str(canvas, 45, 15, model-power_on ? ON : OFF); // 3. 绘制输入源使用图标或文字 canvas_draw_str(canvas, 5, 27, Source:); const char* source_str Unknown; switch(model-source) { case SpeakerSourceBluetooth: source_str BT; break; case SpeakerSourceAUX: source_str AUX; break; case SpeakerSourceOptical: source_str OPT; break; default: break; } canvas_draw_str(canvas, 45, 27, source_str); // 4. 绘制音量条一个简单的进度条 canvas_draw_str(canvas, 5, 39, Volume:); canvas_draw_frame(canvas, 45, 32, 50, 8); // 外框 uint8_t bar_width (model-volume * 48) / 100; // 计算填充宽度 canvas_draw_box(canvas, 46, 33, bar_width, 6); // 填充矩形 // 5. 绘制高低音调节用“-”和“”表示 char buffer[16]; snprintf(buffer, sizeof(buffer), Bass:%2d, model-bass); canvas_draw_str(canvas, 5, 51, buffer); snprintf(buffer, sizeof(buffer), Treble:%2d, model-treble); canvas_draw_str(canvas, 5, 63, buffer); // 6. 绘制当前选中的按钮高亮需要根据状态计算 // ... (这部分逻辑更复杂涉及焦点管理) }输入回调 (input_callback) 这个函数接收物理按键事件。static bool xremote_view_speaker_input(InputEvent* event, void* context) { furi_assert(context); XRemoteViewSpeaker* speaker_view context; if(event-type InputTypePress || event-type InputTypeRepeat) { switch(event-key) { case InputKeyUp: // 向上键例如增加音量 if(speaker_view-model-volume 100) { speaker_view-model-volume 5; // 这里应该发送一个红外信号暂时只更新UI view_commit_model(speaker_view-view, true); // 请求重绘 } return true; // 事件已处理 case InputKeyDown: // 向下键减少音量 if(speaker_view-model-volume 0) { speaker_view-model-volume - 5; view_commit_model(speaker_view-view, true); } return true; case InputKeyOk: // OK键切换电源 speaker_view-model-power_on !speaker_view-model-power_on; view_commit_model(speaker_view-view, true); return true; case InputKeyBack: // Back键退出当前视图返回上一级 // 这通常通过向ViewDispatcher发送一个自定义事件来实现 view_dispatcher_send_custom_event(speaker_view-view_dispatcher, XRemoteEventBack); return true; default: break; } } return false; // 事件未处理传递给其他处理器 }4.3 第三步集成红外信号发送到目前为止我们的视图只能更新屏幕上的数据。真正的遥控器需要发射红外信号。我们需要将模型状态映射到具体的红外指令。首先你需要用Flipper Zero学习你的音响的各个红外指令开关、音量、音量-、切换输入源等并将它们保存为.ir文件或者直接获取其原始信号编码。然后在模型或视图中我们需要调用Flipper的红外API来发送信号。通常我们会有一个红外信号管理器。修改我们的按键处理逻辑case InputKeyOk: { speaker_view-model-power_on !speaker_view-model-power_on; // 根据新的电源状态发送对应的红外信号 InfraredSignal* signal NULL; if(speaker_view-model-power_on) { signal infrared_get_signal_by_name(speaker_power_on); // 假设的函数从数据库获取信号 } else { signal infrared_get_signal_by_name(speaker_power_off); } if(signal) { infrared_send(signal); // 发送红外信号 } view_commit_model(speaker_view-view, true); return true; }为了更优雅我们可以创建一个xremote_speaker_controller.c文件专门处理“什么状态下该发送什么信号”的业务逻辑让视图只负责交互和显示控制器负责指挥模型和红外模块。4.4 第四步将新视图注册到主应用最后我们需要在xremote_app.c的初始化阶段创建我们的音响视图并将其注册到视图分发器中。// 在 application_alloc 函数中 XRemoteViewSpeaker* speaker_view xremote_view_speaker_alloc(); // 将视图添加到分发器并分配一个唯一的视图ID view_dispatcher_add_view(app-view_dispatcher, XRemoteViewIdSpeaker, xremote_view_speaker_get_view(speaker_view));同时需要在主菜单视图中添加一个选项让用户能够进入这个新的音响遥控界面。这通常通过修改xremote_view_menu.c的菜单项数组来实现。注意事项内存管理与资源释放Flipper Zero内存有限因此必须严格管理。在_alloc函数中分配的所有资源内存、计时器、文件描述符等都必须在对应的_free函数中释放。在视图的enter_callback和exit_callback中也要妥善处理资源的临时分配和释放避免内存泄漏。这是嵌入式开发与PC开发一个显著不同的地方需要时刻警惕。5. 红外信号数据库的构建与管理xremote的强大之处在于它能管理成百上千个红外信号。这些信号如何组织、存储和快速检索是一个至关重要的工程问题。项目通常采用一种层次化的数据库结构。5.1 信号存储格式Flipper Zero原生的红外信号以.ir文件格式存储这是一种简单的二进制或文本格式包含了协议名、地址、命令等信息。xremote可以直接利用这些文件但为了更高效的管理它可能会建立自己的索引。一种常见的做法是使用一个主索引文件如infrared_db.index它是一个文本或二进制文件记录了所有已知信号的信息# 格式信号ID | 设备类型 | 设备品牌 | 信号名称 | 协议 | 文件名 1 | TV | Sony | Power | NEC | /ext/infrared/sony_tv_power.ir 2 | TV | Sony | VolumeUp | NEC | /ext/infrared/sony_tv_vol_up.ir 3 | AC | Gree | Power | GREE | /ext/infrared/gree_ac_power.ir ...应用程序启动时会加载这个索引文件到内存中构建一个哈希表或搜索树从而实现通过设备类型、品牌、信号名称快速定位到具体的.ir文件路径。5.2 动态学习与入库xremote通常也会集成信号学习功能。其流程如下进入“学习模式”视图。用户用原装遥控器对准Flipper Zero按下按键。Flipper的红外接收模块捕获原始信号解码器尝试匹配已知协议NEC, RC5, Samsung等。解码成功后将信号数据协议、地址、命令以及用户输入的信号名称暂存。用户选择将该信号归入某个设备如“客厅索尼电视”。程序将信号数据序列化写入一个新的.ir文件如livingroom_sony_tv_power.ir并更新内存中的索引和数据库文件。这个过程涉及底层红外驱动、协议解码、文件IO和数据库更新是项目中比较复杂的部分之一。5.3 信号匹配与容错现实世界中红外信号可能存在干扰或变形。一个健壮的遥控器应用需要有一定的容错能力。xremote可能实现以下策略协议自动检测当学习信号时尝试用所有已实现的解码器进行解码选择置信度最高的一个。信号验证学习后立即用Flipper发射该信号并让用户确认设备是否有响应确保学习正确。重复信号过滤在快速按键时防止因按键抖动或用户连按导致同一信号被重复发送多次干扰设备。这可以通过在发送函数中加入一个简单的“冷却时间”来实现。6. 性能优化与内存管理实战在只有256KB RAM的Flipper Zero上运行一个功能丰富的GUI应用优化是必不可少的。xremote项目中有许多值得学习的优化技巧。6.1 视图的惰性加载与卸载Flipper的GUI系统通常一次只显示一个视图。xremote利用这一点不会在启动时就初始化所有遥控器视图。它采用“惰性加载”策略当用户从主菜单选择“电视遥控器”时系统才调用xremote_view_tv_alloc()创建电视视图并加载对应的红外信号模型。当用户按下返回键离开电视视图时系统调用xremote_view_tv_free()立即释放该视图及其模型占用的内存。下次再进入时重新创建。虽然创建/销毁有微小开销但极大地节省了常驻内存使得应用可以支持大量不同类型的遥控器而不会导致内存不足。6.2 图形渲染优化屏幕刷新是耗电大户。xremote的视图在绘制时遵循以下原则局部刷新如果只有部分数据变化如音量条长度理想情况下应该只重绘变化的区域而不是整个屏幕。Flipper的Canvas API可能支持canvas_set_clip等函数来实现。xremote会判断模型变更的属性决定是局部重绘还是全屏重绘。避免浮点数运算嵌入式CPU处理浮点数很慢。所有界面计算如进度条宽度、图标位置都使用整数运算。例如计算音量条宽度bar_width (volume * 48) / 100。使用图标字体或位图复杂的图标预先渲染为位图或包含在图标字体中绘制时直接canvas_draw_icon或canvas_draw_glyph这比用线条实时绘制要快得多。帧率控制非交互状态下降低视图的刷新频率。例如静态的遥控器界面可能每秒只刷新1次甚至不刷新而正在播放动画或进度条变化时才提高到10-20帧。6.3 事件处理的防抖与节流物理按键可能会产生抖动导致一次按下被识别为多次事件。xremote在输入回调中通常会加入防抖逻辑static uint32_t last_key_press_time 0; static const uint32_t DEBOUNCE_DELAY_MS 50; // 防抖延时50毫秒 if(event-type InputTypePress) { uint32_t now furi_get_tick(); if(now - last_key_press_time DEBOUNCE_DELAY_MS) { return false; // 忽略此次按下认为是抖动 } last_key_press_time now; // ... 处理按键逻辑 }对于“长按”增加音量这种需要连续触发InputTypeRepeat的功能则需要合理设置重复的初始延时和间隔避免触发过快导致设备响应不过来或界面卡顿。7. 调试、测试与常见问题排查开发像xremote这样的复杂应用调试是家常便饭。以下是一些基于实际经验的调试技巧和常见问题。7.1 利用Flipper的日志系统Flipper SDK提供了FURI_LOG系列宏如FURI_LOG_I,FURI_LOG_W,FURI_LOG_E。在关键函数入口、出口以及错误分支添加日志是定位问题最有效的手段。bool my_function(XRemoteModel* model) { FURI_LOG_I(MyTag, Function entered, model ptr: %p, model); if(model NULL) { FURI_LOG_E(MyTag, Model is NULL!); return false; } // ... 业务逻辑 FURI_LOG_D(MyTag, Operation completed, value is %d, some_value); return true; }通过Flipper的CLI命令行界面或像qFlipper这样的桌面工具可以实时查看这些日志输出。7.2 常见问题与解决方案速查表问题现象可能原因排查步骤与解决方案应用启动即崩溃1. 内存分配失败。2. 关键资源如视图分发器初始化失败。3. 访问了空指针。1. 检查所有_alloc函数的返回值是否为NULL。2. 在xremote_app.c的初始化阶段每一步后添加日志看在哪一步崩溃。3. 使用furi_check或furi_assert确保指针有效。按键无响应1. 输入回调函数未正确注册或返回了false。2. 视图未获得焦点。3. 事件被其他视图或层拦截。1. 在input_callback函数开头添加日志确认事件是否到达。2. 检查view_set_input_callback是否被调用。3. 确保当前视图在视图分发器中处于活动状态。屏幕显示错乱或残留1. 绘制回调函数逻辑错误未清除画布。2. 局部刷新区域计算错误。3. 多线程/中断导致绘制冲突罕见。1. 确保draw_callback第一行是canvas_clear(canvas)。2. 检查所有绘图坐标和边界计算防止越界。3. 确保绘图操作都在主GUI线程中进行。红外信号无法控制设备1. 信号未正确学习/解码。2. 发射频率或占空比不对。3. 设备不支持该协议或编码。1. 使用Flipper自带的红外应用重新学习并测试信号是否有效。2. 检查infrared_send调用的参数特别是协议类型。3. 尝试用原始信号Raw模式学习和发射绕过协议解码。添加新遥控器后编译失败1. 头文件未正确包含。2. 函数未实现或签名不匹配。3. 链接器找不到符号。1. 检查.c文件是否包含了对应的.h文件。2. 检查在application.fam中是否添加了新源文件。3. 确保所有声明的函数都有定义且参数和返回类型完全一致。内存使用量持续增长内存泄漏1._alloc的资源未在_free中释放。2. 在循环或事件回调中重复分配内存未释放。1. 使用malloc/free要成对出现仔细核对。2. 检查enter_callback中分配的资源是否在exit_callback中释放。3. 利用Flipper的堆跟踪工具如果可用辅助分析。7.3 模拟器测试在将固件刷入实体设备前强烈建议使用Flipper的官方模拟器如qFlipper或基于Unicorn的模拟器进行测试。模拟器可以快速验证应用逻辑和界面布局。进行压力测试如快速连续按键而不用担心设备变砖。配合调试器进行单步调试这是实体设备难以做到的。虽然模拟器无法测试真实的红外收发但对于GUI逻辑和状态机流程的验证它是不可或缺的工具。8. 进阶扩展思路与社区生态当你掌握了xremote的基本原理后就可以思考如何将其变得更强大甚至为社区做贡献。1. 支持更多输入方式加速度计控制利用Flipper Zero内置的加速度计实现“摇一摇”切换设备、“翻转”静音等手势操作。GPIO扩展通过GPIO引脚连接外部按钮或旋钮为遥控器增加实体按键提升盲操作的体验。2. 增强红外能力宏命令将一系列红外信号如“开电视-切换HDMI1-开音响-调音量”录制为一个宏一键执行复杂操作。信号分析与破解集成更高级的信号分析功能如显示信号的波形图、尝试破解未知的滚动码协议等需注意法律合规性。红外信号中继/放大通过外接电路增强红外发射功率控制距离更远的设备。3. 改善用户体验语音反馈结合蜂鸣器为不同的操作提供不同的提示音效。主题系统允许用户自定义界面颜色、图标和布局。云端同步设计一个格式让用户可以将自己配置好的遥控器数据库导出、分享或从社区仓库导入。4. 与其他Flipper应用集成BadUSB联动通过BadUSB模拟键盘鼠标在电脑上打开媒体播放器然后自动用红外控制音响切换到AUX输入实现跨设备的自动化场景。Sub-GHz联动用Sub-GHz频段控制电动窗帘同时用红外打开投影仪和音响打造家庭影院模式。flipper-xremote项目本身就是一个优秀的范例展示了如何在Flipper Zero上构建一个结构清晰、可扩展的复杂应用。通过阅读和修改它的代码你不仅能打造出最适合自己的万能遥控器更能深入理解事件驱动、状态机、模型-视图分离这些在嵌入式乃至大型软件开发中都至关重要的设计模式。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2599529.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!