💡 划重点
- 一块 ESP32 做大脑,把「有人坐下」「坐了多久」「该提醒了」串成闭环。
- 用人体传感器判断在座,用 OLED 显示倒计时,用蜂鸣器做分级提示,用舵机举旗做「番茄钟结束」的物理动作。
- 整机物料控制在 40 元以内,无需联网也能跑;联网只是可选加分项。
- 核心逻辑是状态机:空闲 → 学习中 → 离座 → 完成,四态切换清晰,方便后面加功能。
需求洞察
目标用户是小学高年级到初中的学生,以及陪写作业的家长。真实痛点有三个:一是孩子写作业容易走神,一坐就是两小时但有效时间不到一半,时间感知很模糊;二是家长反复催促「快点写」,语言提醒容易变成情绪对抗,越催越慢;三是市面上番茄钟多为纯软件,手机一放上桌就变成干扰源,而机械计时器又无法感知「人还在不在」。
这个东西值得做的原因在于,它把「提醒」从人嘴里移到了桌面硬件上:孩子坐下自动开始计时,离座自动暂停并记录,时间到了用蜂鸣器和举起小旗提醒休息。它不评判、不唠叨,只是一个中立的第三方设备,家长也获得了客观的「今日在座时长」数据,沟通压力明显下降。
方案设计
感知层:HC-SR501 人体红外传感器朝座位方向安装,输出高电平表示有人在座;预留一路模拟输入,后续可加光敏电阻判断桌面亮度,提醒开灯。
控制层:ESP32 做主控,运行四态状态机。状态切换带 3 秒防抖,避免孩子转身、伸手拿书导致的误判离座。
执行层:0.96 寸 OLED 显示当前状态与倒计时;有源蜂鸣器做三档提示,坐下短鸣一声、剩余 1 分钟每 5 秒短鸣、结束时长鸣;SG90 舵机带动一面小旗或一块挡板,时间到时从水平转到 90 度立起,作为视觉强提醒。
日志/联网层:串口打印状态迁移日志,便于调试;可选 Wi-Fi 连接后,把每次在座时长通过 HTTP 或 MQTT 上报到本地服务器,形成一周学习曲线。
物料清单
| 物料 | 数量 | 用途 | 估算单价 | 小计 |
|---|---|---|---|---|
| ESP32 开发板 | 1 | 主控、联网 | 12 元 | 12 元 |
| 0.96 寸 OLED(I2C) | 1 | 显示状态与倒计时 | 8 元 | 8 元 |
| SG90 舵机 | 1 | 举旗物理提醒 | 5 元 | 5 元 |
| HC-SR501 人体传感器 | 1 | 检测在座 | 4 元 | 4 元 |
| 有源蜂鸣器模块 | 1 | 声音提醒 | 1 元 | 1 元 |
| 杜邦线、底板、小旗 | 1 套 | 接线与结构 | 6 元 | 6 元 |
| 合计 | 36 元 |
接线与结构
| 模块 | 引脚 | ESP32 引脚 |
|---|---|---|
| OLED | SDA | GPIO21 |
| OLED | SCL | GPIO22 |
| PIR | OUT | GPIO18 |
| 舵机 | 信号 | GPIO13 |
| 蜂鸣器 | 信号 | GPIO27 |
采用给定接线即可,无需改动。唯一要注意的是舵机供电:SG90 堵转电流可到 500 毫安以上,建议从外部 5V 供电,与 ESP32 共地,否则容易导致主控复位。结构上做一块 3 毫米厚亚克力或木板底座,PIR 固定在底座前侧朝座位方向,OLED 斜向上 45 度便于低头看,舵机固定在右侧,旗杆与桌面垂直,转 90 度后小旗正好进入视线。
核心代码
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <ESP32Servo.h>
Adafruit_SSD1306 oled(128, 64, &Wire;, -1);
Servo flagServo;
const int PIN_PIR = 18;
const int PIN_SERVO = 13;
const int PIN_BUZZ = 27;
const unsigned long STUDY_MS = 25UL * 60UL * 1000UL;
bool studying = false;
bool lastSeated = false;
unsigned long startAt = 0;
unsigned long lastBeep = 0;
void show(const char* l1, const char* l2) {
oled.clearDisplay();
oled.setTextSize(1);
oled.display();
}
void setup() {
Serial.begin(115200);
pinMode(PIN_PIR, INPUT);
pinMode(PIN_BUZZ, OUTPUT);
Wire.begin(21, 22);
flagServo.attach(PIN_SERVO);
flagServo.write(0);
show("Ready", "Sit down");
}
void loop() {
bool seated = digitalRead(PIN_PIR) == HIGH;
unsigned long now = millis();
if (seated && !lastSeated && !studying) { // 入座
studying = true; startAt = now;
tone(PIN_BUZZ, 1000, 120);
flagServo.write(0);
Serial.println("state=STUDY");
} else if (!seated && lastSeated && studying) { // 离座
studying = false;
show("Paused", "Back soon");
Serial.println("state=AWAY");
}
lastSeated = seated;
if (studying) {
unsigned long passed = now - startAt;
unsigned long left = passed < STUDY_MS ? (STUDY_MS - passed) / 1000 : 0;
char buf[16];
sprintf(buf, "%02lu:%02lu", left / 60, left % 60);
show("Study", buf);
if (left == 0) {
studying = false;
flagServo.write(90); // 举旗
tone(PIN_BUZZ, 2000, 600);
Serial.println("state=DONE");
} else if (left <= 60 && now - lastBeep > 5000) {
lastBeep = now; tone(PIN_BUZZ, 1500, 80);
}
}
delay(200);
}
提示:若舵机抖动或蜂鸣器异常,说明两者争用了 LEDC 定时器,可把蜂鸣器改为 ledcWriteTone 并指定独立通道,或给舵机单独供电。
调试步骤
- 只接 OLED,烧录后确认能显示两行文字,I2C 地址扫描应为 0x3C。
- 单独接 PIR,串口打印电平变化,调节电位器让 1.5 米内入座能触发、不动时 30 秒内保持稳定。
- 单独接舵机,测试 0 度与 90 度两个位置,确认旗杆不会撞到 OLED。
- 接蜂鸣器,试听 1000 赫兹、1500 赫兹、2000 赫兹三种提示音,音量以不刺耳为准。
- 全模块联调,用串口观察四态迁移日志是否正确,重点验证「快速离座再入座」不会反复切换。
- 可选接入 Wi-Fi,把每次学习时长打印到本地服务器,观察一天数据是否连续。