公司动态
基于Arduino与蓝牙的番茄钟计时器:状态机、非阻塞延迟与串口协议实践
1. 项目概述一个基于蓝牙的番茄钟计时器最近在折腾一个挺有意思的小项目用一块Bluno Beetle开发板搭配几个简单的模块做了一个蓝牙控制的番茄钟计时器。这个项目的核心想法很简单摆脱手机用一个物理设备来专注执行番茄工作法。你可能会问手机App不香吗说实话手机干扰太多一个通知弹窗就可能打断好不容易进入的“心流”状态。所以一个独立的、能通过手机蓝牙简单设置然后就可以离线专注运行的计时器对我来说更有实际意义。Bluno Beetle这块板子很有意思它集成了Arduino Leonardo的核心和一颗蓝牙4.0芯片体积小巧非常适合做这种需要无线通信的嵌入式小玩意儿。整个项目我把它分成了几个部分来做这篇先聊聊最核心的计时逻辑和蓝牙指令解析的实现也就是PT1部分。后续会加上显示模块比如OLED和物理反馈模块比如蜂鸣器或继电器控制其他设备让它在计时结束时能有更明确的提示。如果你手头也有类似的支持蓝牙的Arduino开发板比如ESP32、HM-10模块搭配的Arduino Nano等这个项目的思路完全可以复用。2. 核心硬件选型与电路设计思路2.1 主控与蓝牙方案为什么是Bluno Beetle在项目启动时主控的选择有几个方向经典的Arduino Uno加独立蓝牙模块如HC-05/06、功能更强大的ESP32开发板或者像Bluno Beetle这样的集成方案。我最终选择Bluno Beetle主要基于以下几点考量首先是集成度与体积。Bluno Beetle将ATmega32U4与Arduino Leonardo兼容和TI CC2540蓝牙4.0芯片做到了同一块邮票大小的板子上。这意味着我不需要再为UART串口通信、电平转换、模块供电和额外的连线费心。对于追求作品小型化、整洁化的项目来说这是一个巨大的优势。你只需要一根Micro USB线就能同时完成供电、程序烧录和串口调试非常方便。其次是蓝牙协议的易用性。Bluno Beetle的蓝牙部分被设计成一个透明的串口Serial通道。在Arduino代码中你只需要使用Serial对象进行读写所有通过蓝牙接收到的数据都会自动进入串口缓冲区反之亦然。这极大地降低了蓝牙通信的编程门槛让我可以像操作有线串口一样处理无线数据把精力集中在应用逻辑上。最后是功耗与生态。虽然在这个常供电的桌面设备中功耗不是首要考虑因素但CC2540芯片的低功耗特性为未来将其改造为电池供电的便携设备留下了可能。此外DFRobot为Bluno系列提供了完善的Arduino库支持和文档社区资源也比较丰富遇到问题更容易找到解决方案。注意如果你使用其他蓝牙模块如HC-05通常需要将它的TX、RX引脚连接到Arduino的RX、TX并设置为AT模式或透传模式代码中则使用SoftwareSerial库来创建一个软串口对象进行通信。虽然步骤稍多但核心的串口通信逻辑是相通的。2.2 外围电路与扩展规划虽然PT1阶段主要验证核心逻辑但为了项目的完整性我在设计之初就考虑了后续的扩展。这里简要说明一下整体电路框架主控核心Bluno Beetle。通过Micro USB口供电和编程。显示模块规划中计划使用一个0.96英寸的I2C接口OLED屏幕用于显示倒计时时间、当前番茄钟状态专注/休息。只需要将OLED的SDA和SCL引脚分别连接到Bluno Beetle的D2SDA和D3SCL即可。I2C通信占用引脚少库支持成熟。提示模块规划中包括一个有源蜂鸣器连接一个IO口如D8通过三极管驱动用于发出“滴滴”声。更进一步的我计划引入一个继电器模块。这就是为什么相关热词中会出现“继电器”和“74HC154”。关于74HC154与继电器的联想74HC154是一个4线-16线译码器。如果我想控制多个继电器比如控制台灯、风扇等多个办公设备直接用一个IO口控制一个继电器会快速耗尽有限的IO资源。使用74HC154只需要4个IO口作为地址输入就能在16个输出引脚上产生唯一的低电平信号从而通过简单的三极管电路控制多达16个继电器。这是一种在IO紧张时扩展控制能力的经典思路。在PT1我们先聚焦核心这个多路控制方案会在后续硬件部分详细展开。电源目前通过USB供电足矣。如果未来改为电池供电可能需要考虑增加一个稳压模块如AMS1117-3.3V/5V来提供稳定电压。整个系统的数据流是这样的手机APP或任何蓝牙串口终端发送指令 - Bluno Beetle蓝牙接收并解析 - 内部计时逻辑更新状态 - 驱动显示和提示设备。PT1我们重点攻克前三个环节。3. 软件架构与核心逻辑实现3.1 番茄钟状态机设计任何计时器的核心都是一个状态机。对于番茄工作法一个典型循环包括一段专注工作时间通常25分钟一段短休息时间通常5分钟如此循环数次后有一段长休息时间通常15-30分钟。我们需要用代码清晰地定义这些状态及其转换关系。我定义了以下几个核心状态enum PomodoroState { STATE_IDLE, // 空闲等待开始 STATE_FOCUS, // 专注工作倒计时 STATE_SHORT_BREAK,// 短休息倒计时 STATE_LONG_BREAK // 长休息倒计时 };状态之间的转换由时间和外部指令触发开始指令从STATE_IDLE进入STATE_FOCUS并加载专注时长如25分钟开始倒计时。专注时间到从STATE_FOCUS进入STATE_SHORT_BREAK或STATE_LONG_BREAK根据已完成番茄钟个数判断并加载相应的休息时长。休息时间到从休息状态回到STATE_FOCUS除非用户手动暂停或停止。暂停/继续/停止指令这些外部指令可以在任何状态介入改变状态机的运行。用一个全局变量currentState来记录当前状态再配合一个remainingTime变量以秒为单位记录当前状态的剩余时间主循环loop()的核心逻辑框架就出来了。3.2 精准计时与非阻塞延迟在嵌入式开发中实现精准计时同时又不能阻塞主循环否则无法响应蓝牙指令是关键挑战。绝对不能使用delay()函数这里我使用了Arduino经典的millis()方法来实现非阻塞定时。基本原理是在状态为活跃如STATE_FOCUS时记录一个“上一次更新时间戳”lastUpdateTime。在每次loop()循环中检查当前millis()与lastUpdateTime的差值是否大于等于1000毫秒1秒。如果是则说明1秒时间到执行remainingTime--并更新lastUpdateTime。这样计时精度依赖于millis()函数本身在Arduino上足够准确并且主循环在等待1秒间隔的过程中可以飞速空转随时处理其他任务如检查串口数据。代码片段示意unsigned long currentMillis millis(); if ((currentState ! STATE_IDLE) (currentMillis - lastUpdateTime 1000)) { lastUpdateTime currentMillis; remainingTime--; // 检查时间是否耗尽 if (remainingTime 0) { switch (currentState) { case STATE_FOCUS: pomodoroCount; if (pomodoroCount POMODOROS_FOR_LONG_BREAK) { currentState STATE_LONG_BREAK; remainingTime LONG_BREAK_DURATION; } else { currentState STATE_SHORT_BREAK; remainingTime SHORT_BREAK_DURATION; } // 触发计时结束提示未来接蜂鸣器/继电器 triggerAlert(); break; // ... 处理其他状态超时 } } }3.3 蓝牙指令协议设计与解析为了让手机APP能够控制这个番茄钟我们需要定义一套简单的文本指令协议。设计原则是简单、易读、易解析。我采用了“指令头参数”的格式用换行符\n作为指令结束符。定义的指令集如下START开始一个番茄钟专注周期。PAUSE暂停当前计时。RESUME从暂停状态恢复计时。STOP停止当前计时回到空闲状态。SET FOCUS 1500设置专注时长参数为秒数1500秒25分钟。SET SHORT 300设置短休息时长300秒5分钟。SET LONG 1800设置长休息时长1800秒30分钟。STATUS?查询当前状态和剩余时间。在Arduino端我们需要在loop()中持续检查Serial即蓝牙虚拟串口是否有数据到达。由于指令是以行为单位使用Serial.readStringUntil(\n)可以方便地读取一整条指令。然后使用String类的函数如startsWith(),indexOf(),substring()来解析指令头和参数。一个健壮的解析函数需要考虑去除指令首尾空白字符、处理大小写通常统一转为大写、验证参数有效性等。例如解析SET指令的代码可能如下void parseCommand(String cmd) { cmd.trim(); // 去除首尾空格 cmd.toUpperCase(); // 统一转为大写避免大小写敏感问题 if (cmd.startsWith(SET)) { int spaceIndex cmd.indexOf( , 4); // 查找第二个空格分隔类型和值 if (spaceIndex ! -1) { String type cmd.substring(4, spaceIndex); String valStr cmd.substring(spaceIndex 1); long value valStr.toInt(); if (type FOCUS value 0) { FOCUS_DURATION value; Serial.println(OK FOCUS SET); } else if (type SHORT value 0) { SHORT_BREAK_DURATION value; Serial.println(OK SHORT SET); } // ... 处理其他类型 else { Serial.println(ERROR INVALID SET CMD); } } } else if (cmd START) { // 处理开始指令 if (currentState STATE_IDLE) { currentState STATE_FOCUS; remainingTime FOCUS_DURATION; lastUpdateTime millis(); Serial.println(OK STARTED); } } // ... 解析其他指令 }实操心得串口缓冲区与指令完整性在实际测试中我发现如果手机端发送指令过快或者指令处理较慢可能会发生指令被截断或粘包的情况。为了提高鲁棒性我增加了串口缓冲区大小的检查并确保每条指令都以\n结尾。此外在解析指令后总是通过Serial.println()返回一个明确的响应如 “OK”、“ERROR”这让手机端的调试和用户反馈非常清晰。这也是一个良好的通信习惯。4. Arduino代码实现与关键点剖析4.1 全局变量与常量定义首先我们需要定义整个项目用到的全局变量和常量。清晰的变量命名和合理的结构是代码可维护性的基础。// 番茄钟状态定义 enum PomodoroState { STATE_IDLE, STATE_FOCUS, STATE_SHORT_BREAK, STATE_LONG_BREAK }; // 默认时间常量单位秒 const unsigned long DEFAULT_FOCUS 25 * 60; // 25分钟 const unsigned long DEFAULT_SHORT_BREAK 5 * 60; // 5分钟 const unsigned long DEFAULT_LONG_BREAK 15 * 60; // 15分钟 const int POMODOROS_FOR_LONG_BREAK 4; // 每4个番茄钟进行一次长休息 // 可配置的时间变量运行时可通过蓝牙修改 unsigned long FOCUS_DURATION DEFAULT_FOCUS; unsigned long SHORT_BREAK_DURATION DEFAULT_SHORT_BREAK; unsigned long LONG_BREAK_DURATION DEFAULT_LONG_BREAK; // 运行状态变量 PomodoroState currentState STATE_IDLE; unsigned long remainingTime 0; // 当前状态剩余秒数 unsigned long lastUpdateTime 0; // 用于非阻塞计时的上一次更新时间戳 int pomodoroCount 0; // 已完成的标准番茄钟计数专注短休息 bool isPaused false; // 暂停标志 unsigned long pauseStartTime 0; // 暂停开始的时间戳 unsigned long accumulatedPauseTime 0; // 累计暂停时间用于补偿计时4.2 Setup函数初始化setup()函数负责一次性初始化工作。对于Bluno Beetle蓝牙串口是自动初始化的我们只需要设置波特率。虽然蓝牙通信本身不关心波特率它有自己的无线速率但虚拟串口映射需要这个设置来与上位机匹配。void setup() { // 初始化串口通信波特率设置为115200。这个速率在Bluno Beetle上稳定且与多数串口调试工具兼容。 Serial.begin(115200); // 等待串口连接稳定对于USB虚拟串口不是必须但对蓝牙连接初始化有益。 while (!Serial) { ; // 等待串口端口连接。对于Leonardo/Micro/Beetle只有USB虚拟串口需要。 } Serial.println(Bluno Beetle Pomodoro Timer Ready.); Serial.println(Commands: START, PAUSE, RESUME, STOP, SET FOCUS/SHORT/LONG seconds, STATUS?); // 初始化其他硬件引脚为后续扩展预留 // pinMode(BUZZER_PIN, OUTPUT); // pinMode(RELAY_PIN, OUTPUT); // ... 其他初始化 }注意Serial.begin(115200)中的波特率必须与你在手机蓝牙串口APP如“串口调试助手”、“BLE调试器”中设置的波特率一致否则会出现乱码。115200是一个通用且较高的速率能减少数据传输延迟。4.3 Loop函数主循环逻辑loop()函数是程序的心脏它需要以极高的频率循环执行处理三件核心任务非阻塞计时更新、蓝牙指令检查与解析、以及状态更新后的输出未来驱动显示和提示。void loop() { unsigned long currentMillis millis(); // 获取当前时间戳 // 任务1非阻塞计时逻辑 handleTiming(currentMillis); // 任务2检查并解析蓝牙指令 handleBluetoothCommand(); // 任务3状态更新与反馈当前为串口打印未来可改为驱动OLED updateStatusDisplay(currentMillis); // 这里可以添加一个短暂的延迟来降低CPU占用率但非必须。 // delay(10); }让我们深入handleTiming这个核心函数。它负责在非暂停状态下每过一秒减少remainingTime并在时间耗尽时触发状态转换。void handleTiming(unsigned long currentMillis) { // 如果当前状态是空闲或已暂停则不进行计时更新 if (currentState STATE_IDLE || isPaused) { return; } // 检查是否过去了至少1000毫秒1秒 // 注意处理millis()溢出回绕的情况使用减法比较是安全的。 if (currentMillis - lastUpdateTime 1000) { lastUpdateTime currentMillis; // 更新计时锚点 remainingTime--; // 剩余秒数减1 // 如果时间耗尽 if (remainingTime 0) { switch (currentState) { case STATE_FOCUS: pomodoroCount; // 完成一个番茄钟 Serial.print(Pomodoro completed! Count: ); Serial.println(pomodoroCount); // 判断进入短休息还是长休息 if (pomodoroCount POMODOROS_FOR_LONG_BREAK) { currentState STATE_LONG_BREAK; remainingTime LONG_BREAK_DURATION; Serial.println(Time for a LONG break!); } else { currentState STATE_SHORT_BREAK; remainingTime SHORT_BREAK_DURATION; Serial.println(Time for a SHORT break.); } triggerAlert(); // 触发提示未来实现 break; case STATE_SHORT_BREAK: case STATE_LONG_BREAK: // 休息结束开始下一个专注周期 currentState STATE_FOCUS; remainingTime FOCUS_DURATION; Serial.println(Break over. Back to FOCUS!); triggerAlert(); // 触发提示 break; default: // 不应该到达这里 break; } } } }handleBluetoothCommand()函数负责从串口缓冲区读取完整指令行并调用解析函数。这里使用Serial.available()检查是否有数据并使用readStringUntil(\n)来读取。void handleBluetoothCommand() { while (Serial.available() 0) { String incomingCommand Serial.readStringUntil(\n); if (incomingCommand.length() 0) { parseCommand(incomingCommand); // 调用指令解析函数 } } }updateStatusDisplay函数目前只是简单地将状态和剩余时间通过串口打印出来便于调试。在实际项目中这部分代码会被替换为驱动OLED屏幕刷新的逻辑。void updateStatusDisplay(unsigned long currentMillis) { // 为了减少串口输出频率可以每0.5秒或1秒更新一次状态 static unsigned long lastStatusUpdate 0; if (currentMillis - lastStatusUpdate 1000) { lastStatusUpdate currentMillis; Serial.print(State: ); Serial.print(getStateString(currentState)); Serial.print( | Time left: ); // 将剩余秒数转换为分:秒格式 int minutes remainingTime / 60; int seconds remainingTime % 60; if (minutes 10) Serial.print(0); Serial.print(minutes); Serial.print(:); if (seconds 10) Serial.print(0); Serial.println(seconds); if (isPaused) { Serial.println([PAUSED]); } } }4.4 蓝牙指令解析函数详解parseCommand函数是蓝牙通信的“大脑”它需要健壮地处理各种可能收到的指令包括错误指令。下面是一个更完整的示例void parseCommand(String cmd) { cmd.trim(); cmd.toUpperCase(); if (cmd.length() 0) return; // 忽略空指令 if (cmd START) { if (currentState STATE_IDLE) { currentState STATE_FOCUS; remainingTime FOCUS_DURATION; lastUpdateTime millis(); isPaused false; Serial.println(OK STARTED); } else { Serial.println(ERROR NOT IDLE); } } else if (cmd PAUSE) { if (!isPaused currentState ! STATE_IDLE) { isPaused true; pauseStartTime millis(); // 记录暂停开始时间 Serial.println(OK PAUSED); } else { Serial.println(ERROR ALREADY PAUSED OR IDLE); } } else if (cmd RESUME) { if (isPaused currentState ! STATE_IDLE) { isPaused false; // 补偿暂停期间的时间防止计时器“偷跑” accumulatedPauseTime (millis() - pauseStartTime); lastUpdateTime (millis() - pauseStartTime); // 调整上次更新时间点 Serial.println(OK RESUMED); } else { Serial.println(ERROR NOT PAUSED OR IDLE); } } else if (cmd STOP) { currentState STATE_IDLE; isPaused false; pomodoroCount 0; // 重置番茄钟计数 Serial.println(OK STOPPED); } else if (cmd.startsWith(SET )) { // 指令格式: SET TYPE VALUE int firstSpace cmd.indexOf( ); int secondSpace cmd.indexOf( , firstSpace 1); if (firstSpace ! -1 secondSpace ! -1) { String type cmd.substring(firstSpace 1, secondSpace); String valStr cmd.substring(secondSpace 1); long value valStr.toInt(); if (value 0) { Serial.println(ERROR VALUE MUST BE POSITIVE); return; } bool success false; if (type FOCUS) { FOCUS_DURATION value; success true; } else if (type SHORT) { SHORT_BREAK_DURATION value; success true; } else if (type LONG) { LONG_BREAK_DURATION value; success true; } if (success) { Serial.print(OK ); Serial.print(type); Serial.print( SET TO ); Serial.println(value); // 如果当前状态是对应的状态且计时未开始可以更新remainingTime // 这里逻辑较复杂可根据需要添加 } else { Serial.println(ERROR UNKNOWN SET TYPE); } } else { Serial.println(ERROR INVALID SET FORMAT); } } else if (cmd STATUS?) { // 返回更详细的JSON格式状态信息便于APP解析 Serial.print({); Serial.print(\state\:\); Serial.print(getStateString(currentState)); Serial.print(\,\timeLeft\:); Serial.print(remainingTime); Serial.print(,\pomodoroCount\:); Serial.print(pomodoroCount); Serial.print(,\isPaused\:); Serial.print(isPaused ? true : false); Serial.println(}); } else { Serial.print(ERROR UNKNOWN COMMAND: ); Serial.println(cmd); } } // 辅助函数将状态枚举转换为字符串 String getStateString(PomodoroState state) { switch (state) { case STATE_IDLE: return IDLE; case STATE_FOCUS: return FOCUS; case STATE_SHORT_BREAK: return SHORT_BREAK; case STATE_LONG_BREAK: return LONG_BREAK; default: return UNKNOWN; } }5. 手机端控制与调试实战5.1 蓝牙串口APP的选择与连接要让手机和Bluno Beetle对话你需要一个支持蓝牙串口通信SPP或低功耗蓝牙BLE串口功能的APP。对于Bluno BeetleCC2540它支持传统的蓝牙2.1EDRSPP和蓝牙4.0BLE。我推荐使用BLE连接因为它功耗更低连接更快速。在手机应用商店搜索“BLE串口”或“蓝牙串口调试助手”可以找到很多选择例如“Serial Bluetooth Terminal”、“BLE调试器”、“nRF Connect”等。以“Serial Bluetooth Terminal”为例操作步骤如下给Bluno Beetle上电通过USB线连接电脑或充电宝。打开手机蓝牙并打开APP。在APP内扫描蓝牙设备。你应该能找到一个名为“DFRobotBluno”或类似的设备初始名称可能因固件而异。点击连接。连接成功后APP通常会显示“Connected”状态并出现一个发送指令的输入框和一个接收信息的显示区域。设置通信参数在APP设置中找到波特率Baud Rate选项将其设置为115200与我们在Serial.begin(115200)中设置的一致。数据位、停止位、校验位通常保持默认8N1即可。连接成功后你应该能在APP的接收区看到Bluno Beetle启动时发送的欢迎信息“Bluno Beetle Pomodoro Timer Ready.”。5.2 指令发送测试与状态监控现在你可以在APP的发送框中输入我们定义的指令进行测试了。基础控制测试发送START接收区应返回 “OK STARTED”并开始每秒打印状态和倒计时。发送PAUSE返回 “OK PAUSED”倒计时停止。发送RESUME返回 “OK RESUMED”倒计时继续。发送STOP返回 “OK STOPPED”状态回到空闲番茄钟计数清零。参数设置测试发送SET FOCUS 300设置专注时间为5分钟返回 “OK FOCUS SET TO 300”。发送START观察倒计时是否从300秒开始。发送SET LONG 600设置长休息为10分钟。状态查询测试在任何时候发送STATUS?你会收到一个JSON格式的字符串例如{state:FOCUS,timeLeft:287,pomodoroCount:1,isPaused:false}。这个结构化的数据非常便于专业的手机APP进行解析和界面展示。实操心得指令交互的鲁棒性在测试时我故意发送了一些错误指令如STAR少字母、SET FOCUS ABC非数字参数、PAUSE在空闲状态下。完善的解析函数应该能优雅地处理这些情况返回明确的错误信息如 “ERROR UNKNOWN COMMAND: STAR”, “ERROR VALUE MUST BE POSITIVE”而不是让程序崩溃或卡死。这在产品化过程中至关重要能有效应对用户的误操作。5.3 常见连接与通信问题排查在实际操作中你可能会遇到一些问题。以下是一个快速排查清单问题现象可能原因解决方案手机搜不到设备1. Bluno Beetle未上电。2. 蓝牙模块未进入可发现模式某些模块需要特定AT指令。3. 手机蓝牙或APP权限未打开。1. 检查USB供电。2. 对于Bluno Beetle上电即进入可发现模式。其他模块需查阅手册。3. 检查手机系统设置和APP的蓝牙权限。连接后马上断开1. 波特率不匹配。2. 蓝牙配对问题某些旧版SPP协议需要配对码。1. 确保APP和代码中的波特率均为115200。2. 尝试在手机蓝牙设置中手动配对设备配对码通常是1234或0000。发送指令无反应1. 指令格式错误如未以换行符结尾。2. 发送了未知指令。3. 串口缓冲区溢出。1. 检查APP是否设置了“发送新行”Send Newline或“添加回车换行”。2. 发送STATUS?测试基础通信。3. 在代码中增加Serial.flush()或在发送指令间增加短暂延迟。接收数据乱码波特率不匹配。确认双方波特率严格一致。尝试9600, 57600, 115200等常见波特率。计时不准1.millis()溢出处理不当。2. 主循环中有阻塞操作如误用delay()。1. 使用(currentMillis - lastUpdateTime) interval的比较方式可自然处理溢出。2. 检查代码确保所有延时都使用millis()非阻塞方式。6. 项目总结与未来扩展方向至此一个具备完整核心逻辑的蓝牙番茄钟控制器就完成了。它已经可以通过手机蓝牙指令进行启动、暂停、恢复、停止、参数设置和状态查询。所有的计时逻辑都在本地稳定运行不受手机连接后断开的影响真正实现了“设置后即可离线专注”的目标。这个PT1版本已经实现了项目的“大脑”和“神经”。你可以把它看作一个完整的后台服务。接下来的扩展方向非常明确增加视觉输出PT2计划接入一个I2C OLED屏幕将updateStatusDisplay函数中的串口打印逻辑改为驱动屏幕显示。需要学习Adafruit_SSD1306或U8g2这类OLED库的使用。增加听觉/物理提示PT2/PT3计划简单版连接一个有源蜂鸣器到数字引脚在triggerAlert()函数中让引脚输出一段频率的PWM信号发出“滴滴”声。高级版连接一个继电器模块。继电器可以控制更高电压的电路比如直接开关你的桌面台灯。当专注时间结束时让台灯闪烁几下这种物理世界的反馈比声音更有冲击力。如果需要控制多个设备就可以引入之前提到的74HC154译码器来扩展IO口。开发专属手机APP目前我们使用通用的串口调试APP体验比较简陋。你可以用MIT App Inventor、Flutter或React Native等工具开发一个简单的专用APP提供漂亮的按钮、计时器动画和历史统计功能通过BLE协议与Bluno Beetle通信发送和解析我们定义好的指令与JSON状态。美化与封装为整个项目设计一个3D打印或亚克力切割的外壳将Bluno Beetle、电池、屏幕、按钮集成进去做成一个精致的桌面摆件。这个项目从想法到实现最深的体会是嵌入式开发是一个将抽象逻辑状态机、计时与物理世界蓝牙电波、继电器开关连接起来的过程。每一步从非阻塞延迟的巧妙实现到蓝牙指令协议的精心设计再到对各种异常情况的处理都让我对“稳定可靠”这四个字有了更具体的认识。当你按下手机上的“START”看到自己组装的设备开始倒计时那种创造和控制的满足感是单纯使用现成产品无法比拟的。希望这个详细的拆解能帮你顺利复现这个项目甚至激发出更多改进的想法。