💡 划重点

  • 一块 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 并指定独立通道,或给舵机单独供电。

调试步骤

  1. 只接 OLED,烧录后确认能显示两行文字,I2C 地址扫描应为 0x3C。
  2. 单独接 PIR,串口打印电平变化,调节电位器让 1.5 米内入座能触发、不动时 30 秒内保持稳定。
  3. 单独接舵机,测试 0 度与 90 度两个位置,确认旗杆不会撞到 OLED。
  4. 接蜂鸣器,试听 1000 赫兹、1500 赫兹、2000 赫兹三种提示音,音量以不刺耳为准。
  5. 全模块联调,用串口观察四态迁移日志是否正确,重点验证「快速离座再入座」不会反复切换。
  6. 可选接入 Wi-Fi,把每次学习时长打印到本地服务器,观察一天数据是否连续。

设计图

桌面作业提醒机器人结构flow
1
感知输入
→
2
控制处理
→
3
执行反馈
→
4
数据上报