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的D2(SDA)和D3(SCL)即可。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 Beetle(CC2540),它支持传统的蓝牙2.1+EDR(SPP)和蓝牙4.0(BLE)。我推荐使用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”,看到自己组装的设备开始倒计时,那种创造和控制的满足感,是单纯使用现成产品无法比拟的。希望这个详细的拆解,能帮你顺利复现这个项目,甚至激发出更多改进的想法。