之前跟大家分享了禅意时钟,有朋友问我能不能做个番茄钟,就顺手写了一个。今天来跟大家聊聊这个番茄钟的实现细节。
番茄钟这个概念相信大家都知道:工作 25 分钟,休息 5 分钟,每四个番茄钟长休息一次。但真正写起来,还是有不少门道的。
先说说界面设计。番茄钟的界面需要展示:
顶部:当前状态(空闲/工作中/休息中/已暂停)
中间:大号倒计时显示
进度条:用圆点展示工作进度
番茄计数:完成了多少个番茄
底部:时间选择框(5/10/15/20/25分钟)

布局坐标参考:
屏幕尺寸:540 x 960(竖屏)
顶部状态文字:y=60
倒计时显示:y=240
进度圆点:y=390
番茄计数:y=550
分隔线:y=620
时间选择框:y=750
底部提示:y=880
番茄钟的交互设计遵循"所见即所用"的原则,每个状态的界面显示和用户可执行的操作保持一致。
| 空闲 | "番茄钟" | "点击时间框,开始计时" | 单指点击时间框开始计时 |
| 工作中 | "专注工作中" | "双指点击暂停" | 单指点击时间框无响应;双指暂停 |
| 已暂停 | "已暂停" | "双指点击继续" | 双指继续 |
| 休息中 | "休息一下"或"长休息" | "双指点击停止" | 双指停止,返回空闲 |
┌─────────────────────────────────────┐ │ 顶部状态区 (y=60) │ │ "专注工作中" / "已暂停" │ ├─────────────────────────────────────┤ │ │ │ 大号倒计时 (y=240) │ │ 04:45 │ │ │ │ ●●●●●●●●○○ (y=390) │ │ 进度圆点 │ │ │ │ 已完成 3 个番茄 (y=550) │ │ │ ├─────────────────────────────────────┤ │ 分隔线 (y=620) │ ├─────────────────────────────────────┤ │ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ │ │ │ 5 │ │10 │ │15 │ │20 │ │25 │ │ │ │分钟│ │分钟│ │分钟│ │分钟│ │分钟│ │ │ └───┘ └───┘ └───┘ └───┘ └───┘ │ │ 时间选择框 (y=750) │ ├─────────────────────────────────────┤ │ 底部提示 (y=880) │ │ "双指点击暂停" / "双指点击继续" │ └─────────────────────────────────────┘
┌─────────────────┐ │ │ 点击时间框 │ │ 双指点击 ───────────────> IDLE ───────────────> WORK │ │ │ │ 计时结束 │ │ ───────────────> BREAK │ │ │ │ 双指点击 │ │ ───────────────> IDLE │ │ │ PAUSED <────────────────── │ │ │ │ 双指点击 │ │ ───────────────> WORK │ │ │ │ 双指点击 │ │ ───────────────> IDLE └─────────────────┘
单指点击时间框:
仅在 IDLE 状态下有效
点击后直接进入计时状态,无确认步骤
重新点击时间框可重新选择时长
双指点击:
WORK 状态:暂停计时
PAUSED 状态:继续计时
BREAK/LONG_BREAK 状态:停止,返回 IDLE
时间框选择:
仅在 IDLE 状态下可选
选中项高亮显示
计时开始后不可更改
进度圆点:
仅在工作状态显示
8 个圆点代表一个番茄钟周期
填充数量 = (总时长 - 剩余时间) / 总时长 * 8
番茄钟有 5 种状态,用状态机来管理再合适不过:
# 状态常量 STATE_WORK = 0 # 工作中 STATE_BREAK = 1 # 短休息 STATE_LONG_BREAK = 2 # 长休息 STATE_IDLE = 3 # 空闲 STATE_PAUSED = 4 # 已暂停 STATE_NAMES = ["工作中", "短休息", "长休息", "空闲", "已暂停"]
状态转换图:
点击时间框 ────────────────> IDLE ───────────> WORK ───────────> BREAK ───────────> IDLE <────────── <────────── <────────── 长休息 计时结束 (4个番茄后)
这是番茄钟最关键的部分。墨水屏不能频繁刷新,但番茄钟需要实时显示倒计时。
我设计了一套三级刷新策略:
刷新场景刷新方式说明
| 启动/进入计时/进入休息/返回IDLE | 全屏刷新 | 状态彻底改变时 |
| 每分钟 | 全屏刷新 | 整分钟时刻 |
| 每15秒(30秒/45秒) | 局部刷新 | 只刷新时间和进度 |
| 暂停/继续 | 最小局部刷新 | 只刷新顶部状态+时间+底部提示 |
def loop():
# 计时中的刷新逻辑
if current_state == STATE_WORK or current_state == STATE_BREAK or current_state == STATE_LONG_BREAK:
elapsed = time.time() - start_time
display_time = get_display_time()
# 刷新时刻:remaining = 285, 270, 255, 240, ... (即 remaining_time - n*15)
# 刷新条件:display_time <= remaining_time - (refresh_count + 1) * 15
next_refresh_threshold = remaining_time - (refresh_count + 1) * 15
if display_time <= next_refresh_threshold and display_time > 0:
if elapsed % 60 == 0 and elapsed > 0:
# 整分钟时全屏刷新
log("%d秒到,全屏刷新" % int(elapsed))
draw_all()
else:
# 15秒、30秒、45秒时局部刷新
log("%d秒到,局部刷新 remaining=%s" % (int(elapsed), format_time(display_time)))
partial_refresh_time_progress()
refresh_count += 1这是最难的部分。用户要求:暂停后再继续,刷新节奏不能乱。
举个例子:
选择 5 分钟(05:00)
工作到 04:41 时暂停(已过去 19 秒)
继续后,下一次刷新应该在 04:30(再过 11 秒)
而不是从 04:41 重新算 15 秒
实现方案:
# 全局变量 elapsed_at_pause = 0 # 暂停时的总elapsed def pause_timer(): global current_state, paused_remaining, start_time, remaining_time, elapsed_at_pause if current_state == STATE_WORK: paused_remaining = get_display_time() elapsed_at_pause = time.time() - start_time # 保存暂停时的总elapsed current_state = STATE_PAUSED partial_refresh_minimal() start_time = 0 def resume_timer(): global current_state, remaining_time, start_time, refresh_count, elapsed_at_pause if current_state == STATE_PAUSED: current_state = STATE_WORK # 关键:start_time 向后偏移,保持 elapsed 的连续性 start_time = time.time() - elapsed_at_pause partial_refresh_minimal()
这样 get_display_time() 就能正确计算:remaining_time - (elapsed_at_pause + new_elapsed)
M5Paper 支持多点触控,我的设计是:
触摸方式功能
| 单指点击时间框 | 开始计时 |
| 单指点击其他区域 | 空闲时不操作 |
| 双指点击 | 暂停/继续/停止(根据当前状态) |
def handle_touch(x, y, count): if count == 1: # 单指 if current_state == STATE_IDLE: if is_in_time_box(x, y): check_time_option_touch(x, y) start_work() elif count == 2: # 双指 if current_state == STATE_WORK: pause_timer() elif current_state == STATE_PAUSED: resume_timer() else: stop_timer()
注意:触摸释放时才执行动作,而不是按下时。这样可以避免快速点击产生的误判。
def connect_wifi(): wlan = network.WLAN(network.STA_IF) wlan.active(True) if not wlan.isconnected(): wlan.disconnect() wlan.connect(WIFI_SSID, WIFI_PASSWORD) timeout = 30 while not wlan.isconnected() and timeout > 0: time.sleep(1) timeout -= 1 return wlan.isconnected() def sync_time(): try: ntptime.settime() return True except: return False
# 02_番茄钟 - Pomodoro Timer
# 功能:番茄工作法计时器,可选时长
# 设计:简洁专注,墨水屏友好
import M5
from M5 import *
import time
import ntptime
import network
# ============== WiFi配置 ==============
WIFI_SSID = "OpenBSD"
WIFI_PASSWORD = "********"
# ============== 屏幕配置 ==============
SCREEN_WIDTH = 540
SCREEN_HEIGHT = 960
# ============== 颜色定义 ==============
COLOR_BG = 0xFFFFFF
COLOR_WORK = 0x333333
COLOR_BREAK = 0x666666
COLOR_HINT = 0x999999
COLOR_LINE = 0xCCCCCC
COLOR_SELECTED = 0x333333
# ============== 状态常量 ==============
STATE_WORK = 0
STATE_BREAK = 1
STATE_LONG_BREAK = 2
STATE_IDLE = 3
STATE_PAUSED = 4
STATE_NAMES = ["工作中", "短休息", "长休息", "空闲", "已暂停"]
# ============== 可选时长 ==============
TIME_OPTIONS = [5*60, 10*60, 15*60, 20*60, 25*60]
# ============== 布局参数 ==============
TIME_BOX_WIDTH = 80
TIME_BOX_HEIGHT = 80
TIME_BOX_GAP = 20
TIME_BOX_START_X = 30
TIME_BOX_Y = 750 # 时间框靠近底部
# ============== 状态变量 ==============
current_state = STATE_IDLE
remaining_time = 25 * 60
selected_index = 4
start_time = 0
paused_remaining = 0
elapsed_at_pause = 0
pomodoros_completed = 0
last_minute = -1 # 上次刷新时的分钟
last_15s_refresh = -1 # 上次15秒刷新的时间桶(已废弃,用 last_refresh_elapsed 替代)
last_refresh_elapsed = 0 # 上次刷新的相对秒数(已废弃)
refresh_count = 0 # 已刷新的15秒次数
touch_was_pressed = False
last_touch_x = 0
last_touch_y = 0
last_touch_count = 0 # 保存按下时的触摸数量
loop_count = 0
# ============== WiFi和网络 ==============
def connect_wifi():
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
if not wlan.isconnected():
wlan.disconnect()
log("WiFi正在连接: %s" % WIFI_SSID)
wlan.connect(WIFI_SSID, WIFI_PASSWORD)
timeout = 30
while not wlan.isconnected() and timeout > 0:
time.sleep(1)
timeout -= 1
if wlan.isconnected():
log("WiFi已连接,IP: %s" % wlan.ifconfig()[0])
return True
else:
log("WiFi连接失败")
return False
def sync_time():
try:
ntptime.settime()
log("NTP时间同步成功")
return True
except Exception as e:
log("NTP时间同步失败: %s" % str(e))
return False
def log(msg):
"""带时间戳的日志输出"""
t = time.localtime()
# 替换selected显示
msg = msg.replace("selected=%d(%s秒)" % (selected_index, TIME_OPTIONS[selected_index]),
"selected=%d(%d分钟)" % (selected_index, TIME_OPTIONS[selected_index] // 60))
print("%04d-%02d-%02d %02d:%02d:%02d [APP] %s" % (t[0], t[1], t[2], t[3], t[4], t[5], msg))
# ============== 工具函数 ==============
def format_time(seconds):
mins = seconds // 60
secs = seconds % 60
return "{:02d}:{:02d}".format(mins, secs)
def get_display_time():
"""获取当前剩余时间"""
if start_time == 0:
return remaining_time
elapsed = time.time() - start_time
t = remaining_time - elapsed
if t < 0:
return 0
return int(t) # 直接返回秒数
def get_15s_bucket():
"""获取当前15秒的时间桶"""
return int(time.time() // 15)
def get_minute_bucket():
"""获取当前分钟桶(从epoch开始的分钟)"""
return int(time.time() // 60)
# ============== 绘制函数 ==============
def draw_background():
M5.Lcd.clear(COLOR_BG)
def draw_timer():
"""绘制计时器区域"""
if current_state == STATE_WORK:
state_text = "专注工作中"
color = COLOR_WORK
elif current_state == STATE_BREAK:
state_text = "休息一下"
color = COLOR_BREAK
elif current_state == STATE_LONG_BREAK:
state_text = "长休息"
color = COLOR_BREAK
elif current_state == STATE_PAUSED:
state_text = "已暂停"
color = COLOR_BREAK
else:
state_text = "番茄钟"
color = COLOR_WORK
# 状态文字
M5.Lcd.setFont(M5.Lcd.FONTS.EFontCN24)
M5.Lcd.setTextColor(color, COLOR_BG)
state_width = len(state_text) * 24
x = int((SCREEN_WIDTH - state_width) // 2)
M5.Lcd.setCursor(x, 60)
M5.Lcd.print(state_text)
# 大时间显示 - DejaVu72
display_time = get_display_time()
time_str = format_time(display_time)
M5.Lcd.setFont(M5.Lcd.FONTS.DejaVu72)
M5.Lcd.setTextColor(COLOR_WORK, COLOR_BG)
x = int((SCREEN_WIDTH - 260) // 2)
M5.Lcd.setCursor(x, 240)
M5.Lcd.print(time_str)
def draw_progress():
"""绘制进度指示"""
if current_state != STATE_WORK:
return
progress = 1 - get_display_time() / TIME_OPTIONS[selected_index]
if progress < 0:
progress = 0
total_dots = 8
filled_dots = int(progress * total_dots)
dots_y = 390
dots_start_x = 150
M5.Lcd.setFont(M5.Lcd.FONTS.DejaVu24)
for i in range(total_dots):
dot_x = dots_start_x + i * 30
if i < filled_dots:
M5.Lcd.fillCircle(dot_x, dots_y, 6, COLOR_WORK)
else:
M5.Lcd.drawCircle(dot_x, dots_y, 6, COLOR_LINE)
def draw_pomodoros():
"""绘制番茄计数"""
M5.Lcd.setFont(M5.Lcd.FONTS.EFontCN24)
M5.Lcd.setTextColor(COLOR_BREAK, COLOR_BG)
text = "已完成 {} 个番茄".format(pomodoros_completed)
text_width = len(text) * 24
x = int((SCREEN_WIDTH - text_width) // 2)
M5.Lcd.setCursor(x, 550)
M5.Lcd.print(text)
def draw_divider():
"""绘制分隔线"""
line_y = 620
M5.Lcd.drawLine(30, line_y, SCREEN_WIDTH - 30, line_y, COLOR_LINE)
def draw_time_options():
"""绘制时间选项框"""
box_y = TIME_BOX_Y
for i, secs in enumerate(TIME_OPTIONS):
box_x = TIME_BOX_START_X + i * (TIME_BOX_WIDTH + TIME_BOX_GAP)
is_selected = (i == selected_index)
if is_selected:
M5.Lcd.fillRect(box_x, box_y, TIME_BOX_WIDTH, TIME_BOX_HEIGHT, COLOR_SELECTED)
M5.Lcd.setTextColor(COLOR_BG, COLOR_SELECTED)
else:
M5.Lcd.fillRect(box_x, box_y, TIME_BOX_WIDTH, TIME_BOX_HEIGHT, COLOR_BG)
M5.Lcd.drawRect(box_x, box_y, TIME_BOX_WIDTH, TIME_BOX_HEIGHT, COLOR_WORK)
M5.Lcd.setTextColor(COLOR_WORK, COLOR_BG)
M5.Lcd.setFont(M5.Lcd.FONTS.DejaVu40)
num_text = str(secs // 60)
num_width = len(num_text) * 30
num_x = box_x + (TIME_BOX_WIDTH - num_width) // 2
M5.Lcd.setCursor(num_x, box_y + 5)
M5.Lcd.print(num_text)
M5.Lcd.setFont(M5.Lcd.FONTS.EFontCN24)
min_text = "分钟"
min_width = len(min_text) * 24
min_x = box_x + (TIME_BOX_WIDTH - min_width) // 2
M5.Lcd.setCursor(min_x, box_y + 45)
M5.Lcd.print(min_text)
def draw_hint():
"""绘制提示文字"""
M5.Lcd.setFont(M5.Lcd.FONTS.EFontCN24)
M5.Lcd.setTextColor(COLOR_HINT, COLOR_BG)
if current_state == STATE_IDLE:
hint = "点击时间框,开始计时"
elif current_state == STATE_WORK:
hint = "双指点击暂停"
elif current_state == STATE_PAUSED:
hint = "双指点击继续"
else:
hint = "双指点击停止"
text_width = len(hint) * 20
x = int((SCREEN_WIDTH - text_width) // 2)
M5.Lcd.setCursor(x, 880)
M5.Lcd.print(hint)
def draw_all():
log(">>> 全屏刷新 <<<")
draw_background()
draw_timer()
draw_progress()
draw_pomodoros()
draw_divider()
draw_time_options()
draw_hint()
def partial_refresh_time_progress():
"""局部刷新时间和进度(每15秒自动刷新)"""
display_time = get_display_time()
log(">>> 局部刷新时间 <<< remaining=%s" % format_time(display_time))
# 只刷新时间和进度
M5.Lcd.fillRect(140, 240, 260, 90, COLOR_BG)
time_str = format_time(display_time)
M5.Lcd.setFont(M5.Lcd.FONTS.DejaVu72)
M5.Lcd.setTextColor(COLOR_WORK, COLOR_BG)
x = int((SCREEN_WIDTH - 260) // 2)
M5.Lcd.setCursor(x, 240)
M5.Lcd.print(time_str)
partial_refresh_progress()
def partial_refresh_minimal():
"""最小局部刷新(暂停/继续时调用):顶部状态+时间+底部提示"""
display_time = get_display_time()
log(">>> 最小局部刷新 <<< remaining=%s" % format_time(display_time))
# 刷新顶部状态文字
if current_state == STATE_WORK:
state_text = "专注工作中"
color = COLOR_WORK
elif current_state == STATE_PAUSED:
state_text = "已暂停"
color = COLOR_BREAK
else:
state_text = "番茄钟"
color = COLOR_WORK
M5.Lcd.fillRect(0, 60, SCREEN_WIDTH, 50, COLOR_BG)
M5.Lcd.setFont(M5.Lcd.FONTS.EFontCN24)
M5.Lcd.setTextColor(color, COLOR_BG)
state_width = len(state_text) * 24
x = int((SCREEN_WIDTH - state_width) // 2)
M5.Lcd.setCursor(x, 60)
M5.Lcd.print(state_text)
# 刷新时间显示
M5.Lcd.fillRect(140, 240, 260, 90, COLOR_BG)
time_str = format_time(display_time)
M5.Lcd.setFont(M5.Lcd.FONTS.DejaVu72)
M5.Lcd.setTextColor(COLOR_WORK, COLOR_BG)
x = int((SCREEN_WIDTH - 260) // 2)
M5.Lcd.setCursor(x, 240)
M5.Lcd.print(time_str)
# 刷新底部提示文字
M5.Lcd.fillRect(0, 870, SCREEN_WIDTH, 40, COLOR_BG)
M5.Lcd.setFont(M5.Lcd.FONTS.EFontCN24)
M5.Lcd.setTextColor(COLOR_HINT, COLOR_BG)
if current_state == STATE_PAUSED:
hint = "双指点击继续"
else:
hint = "双指点击暂停"
text_width = len(hint) * 20
x = int((SCREEN_WIDTH - text_width) // 2)
M5.Lcd.setCursor(x, 880)
M5.Lcd.print(hint)
def partial_refresh_all():
"""完整局部刷新(进入计时时调用)"""
display_time = get_display_time()
log(">>> 完整局部刷新 <<< remaining=%s" % format_time(display_time))
# 刷新顶部状态文字
if current_state == STATE_WORK:
state_text = "专注工作中"
color = COLOR_WORK
elif current_state == STATE_PAUSED:
state_text = "已暂停"
color = COLOR_BREAK
else:
state_text = "番茄钟"
color = COLOR_WORK
M5.Lcd.fillRect(0, 60, SCREEN_WIDTH, 50, COLOR_BG)
M5.Lcd.setFont(M5.Lcd.FONTS.EFontCN24)
M5.Lcd.setTextColor(color, COLOR_BG)
state_width = len(state_text) * 24
x = int((SCREEN_WIDTH - state_width) // 2)
M5.Lcd.setCursor(x, 60)
M5.Lcd.print(state_text)
# 刷新时间显示
M5.Lcd.fillRect(140, 240, 260, 90, COLOR_BG)
time_str = format_time(display_time)
M5.Lcd.setFont(M5.Lcd.FONTS.DejaVu72)
M5.Lcd.setTextColor(COLOR_WORK, COLOR_BG)
x = int((SCREEN_WIDTH - 260) // 2)
M5.Lcd.setCursor(x, 240)
M5.Lcd.print(time_str)
partial_refresh_progress()
# 刷新底部提示文字
M5.Lcd.fillRect(0, 870, SCREEN_WIDTH, 40, COLOR_BG)
M5.Lcd.setFont(M5.Lcd.FONTS.EFontCN24)
M5.Lcd.setTextColor(COLOR_HINT, COLOR_BG)
if current_state == STATE_PAUSED:
hint = "双指点击继续"
else:
hint = "双指点击暂停"
text_width = len(hint) * 20
x = int((SCREEN_WIDTH - text_width) // 2)
M5.Lcd.setCursor(x, 880)
M5.Lcd.print(hint)
# 刷新时间框选中状态
box_y = TIME_BOX_Y
for i, secs in enumerate(TIME_OPTIONS):
box_x = TIME_BOX_START_X + i * (TIME_BOX_WIDTH + TIME_BOX_GAP)
is_selected = (i == selected_index)
if is_selected:
M5.Lcd.fillRect(box_x, box_y, TIME_BOX_WIDTH, TIME_BOX_HEIGHT, COLOR_SELECTED)
M5.Lcd.setTextColor(COLOR_BG, COLOR_SELECTED)
else:
M5.Lcd.fillRect(box_x, box_y, TIME_BOX_WIDTH, TIME_BOX_HEIGHT, COLOR_BG)
M5.Lcd.drawRect(box_x, box_y, TIME_BOX_WIDTH, TIME_BOX_HEIGHT, COLOR_WORK)
M5.Lcd.setTextColor(COLOR_WORK, COLOR_BG)
M5.Lcd.setFont(M5.Lcd.FONTS.DejaVu40)
num_text = str(secs // 60)
num_width = len(num_text) * 30
num_x = box_x + (TIME_BOX_WIDTH - num_width) // 2
M5.Lcd.setCursor(num_x, box_y + 5)
M5.Lcd.print(num_text)
M5.Lcd.setFont(M5.Lcd.FONTS.EFontCN24)
min_text = "分钟"
min_width = len(min_text) * 24
min_x = box_x + (TIME_BOX_WIDTH - min_width) // 2
M5.Lcd.setCursor(min_x, box_y + 45)
M5.Lcd.print(min_text)
def partial_refresh_progress():
"""局部刷新进度"""
if current_state != STATE_WORK:
return
M5.Lcd.fillRect(140, 380, 260, 30, COLOR_BG)
progress = 1 - get_display_time() / TIME_OPTIONS[selected_index]
if progress < 0:
progress = 0
total_dots = 8
filled_dots = int(progress * total_dots)
dots_y = 390
dots_start_x = 150
M5.Lcd.setFont(M5.Lcd.FONTS.DejaVu24)
for i in range(total_dots):
dot_x = dots_start_x + i * 30
if i < filled_dots:
M5.Lcd.fillCircle(dot_x, dots_y, 6, COLOR_WORK)
else:
M5.Lcd.drawCircle(dot_x, dots_y, 6, COLOR_LINE)
# ============== 触摸处理 ==============
def is_in_time_box(x, y):
"""检查坐标是否在时间框区域内"""
for i in range(len(TIME_OPTIONS)):
box_x = TIME_BOX_START_X + i * (TIME_BOX_WIDTH + TIME_BOX_GAP)
if (box_x <= x <= box_x + TIME_BOX_WIDTH and
TIME_BOX_Y <= y <= TIME_BOX_Y + TIME_BOX_HEIGHT):
return True
return False
def check_time_option_touch(x, y):
"""检查是否点击了时间选项"""
global selected_index
for i in range(len(TIME_OPTIONS)):
box_x = TIME_BOX_START_X + i * (TIME_BOX_WIDTH + TIME_BOX_GAP)
if (box_x <= x <= box_x + TIME_BOX_WIDTH and
TIME_BOX_Y <= y <= TIME_BOX_Y + TIME_BOX_HEIGHT):
log("点击选项框 %d (%s秒)" % (i, TIME_OPTIONS[i]))
if selected_index != i:
old_index = selected_index
selected_index = i
log("选项变化: %d -> %d" % (old_index, selected_index))
return True
else:
log("点击当前选中项,无变化")
return False
log("未点击选项框")
return False
def handle_touch(x, y, count):
"""处理触摸"""
global last_touch_x, last_touch_y, last_minute
log("触摸: count=%d x=%d y=%d" % (count, x, y))
if count == 1:
# 单指
if current_state == STATE_IDLE:
if is_in_time_box(x, y):
check_time_option_touch(x, y)
start_work()
else:
log("IDLE单指点击非时间框区域,不操作")
else:
log("%s状态下单指点击,不操作" % STATE_NAMES[current_state])
elif count == 2:
# 双指
if current_state == STATE_WORK:
pause_timer()
elif current_state == STATE_PAUSED:
resume_timer()
else:
stop_timer()
last_touch_x = x
last_touch_y = y
def start_work():
global current_state, remaining_time, start_time, last_minute, last_15s_refresh, refresh_count, elapsed_at_pause
log("状态变化: IDLE -> WORK | selected=%d(%s)" % (selected_index, TIME_OPTIONS[selected_index] // 60))
current_state = STATE_WORK
remaining_time = TIME_OPTIONS[selected_index]
start_time = time.time()
last_minute = get_minute_bucket()
last_15s_refresh = get_15s_bucket()
refresh_count = 0
elapsed_at_pause = 0
# 进入计时,全屏刷新
draw_all()
def pause_timer():
global current_state, paused_remaining, start_time, remaining_time, elapsed_at_pause
if current_state == STATE_WORK:
paused_remaining = get_display_time()
elapsed_at_pause = time.time() - start_time # 保存暂停时的总elapsed
log("状态变化: WORK -> PAUSED | paused=%s elapsed=%d" % (format_time(paused_remaining), int(elapsed_at_pause)))
# 先切换状态
current_state = STATE_PAUSED
# 再刷新
partial_refresh_minimal()
start_time = 0
def resume_timer():
global current_state, remaining_time, start_time, last_minute, last_15s_refresh, refresh_count, elapsed_at_pause
if current_state == STATE_PAUSED:
current_state = STATE_WORK
# 恢复时保持remaining_time为原始值,start_time向后偏移
# 这样get_display_time() = remaining_time - (elapsed_at_pause + new_elapsed)
start_time = time.time() - elapsed_at_pause
last_minute = get_minute_bucket()
last_15s_refresh = get_15s_bucket()
log("状态变化: PAUSED -> WORK | remaining=%s elapsed=%d refresh_count=%d" % (format_time(get_display_time()), int(elapsed_at_pause), refresh_count))
# 继续时最小局部刷新
partial_refresh_minimal()
def start_break():
global current_state, remaining_time, start_time, pomodoros_completed, last_minute, last_15s_refresh, refresh_count
pomodoros_completed += 1
log("状态变化: WORK -> BREAK | pomodoros=%d" % pomodoros_completed)
if pomodoros_completed % 4 == 0:
log("进入长休息(15分钟)")
current_state = STATE_LONG_BREAK
remaining_time = 15 * 60
else:
log("进入短休息(5分钟)")
current_state = STATE_BREAK
remaining_time = 5 * 60
start_time = time.time()
last_minute = get_minute_bucket()
last_15s_refresh = get_15s_bucket()
refresh_count = 0
last_refresh_elapsed = 0
# 完成番茄后进入休息,全屏刷新
draw_all()
def stop_timer():
global current_state, remaining_time, start_time, paused_remaining
log("状态变化: %s -> IDLE" % STATE_NAMES[current_state])
current_state = STATE_IDLE
remaining_time = TIME_OPTIONS[selected_index]
start_time = 0
paused_remaining = 0
draw_all()
# ============== 主循环 ==============
def setup():
global last_minute, last_15s_refresh, last_touch_x, last_touch_y, last_touch_count
M5.begin()
M5.Lcd.setEpdMode(M5.Lcd.EPDMode.EPD_FAST)
if connect_wifi():
sync_time()
remaining_time = TIME_OPTIONS[selected_index]
last_minute = get_minute_bucket()
last_15s_refresh = get_15s_bucket()
refresh_count = 0
last_touch_x = 0
last_touch_y = 0
last_touch_count = 0
log("番茄钟初始化完成")
log("初始状态: %s selected=%d(%s秒)" % (STATE_NAMES[current_state], selected_index, TIME_OPTIONS[selected_index]))
draw_all()
def loop():
global loop_count, last_minute, last_15s_refresh, refresh_count, touch_was_pressed, last_touch_x, last_touch_y, last_touch_count
loop_count += 1
M5.update()
touch_count = M5.Touch.getCount()
if touch_count > 0:
touch = M5.Touch
x = touch.getX()
y = touch.getY()
if x > 0 or y > 0:
if not touch_was_pressed:
log("触摸按下: count=%d x=%d y=%d" % (touch_count, x, y))
touch_was_pressed = True
last_touch_x = x
last_touch_y = y
last_touch_count = touch_count # 保存按下时的触摸数量
elif touch_was_pressed:
log("触摸释放: count=%d x=%d y=%d" % (last_touch_count, last_touch_x, last_touch_y))
touch_was_pressed = False
handle_touch(last_touch_x, last_touch_y, last_touch_count) # 使用保存的触摸数量
last_touch_x = 0
last_touch_y = 0
last_touch_count = 0
# 计时中的刷新逻辑
if current_state == STATE_WORK or current_state == STATE_BREAK or current_state == STATE_LONG_BREAK:
elapsed = time.time() - start_time
display_time = get_display_time()
# 刷新时刻:remaining = 285, 270, 255, 240, ... (即 remaining_time - n*15)
# 刷新条件:display_time <= remaining_time - (refresh_count + 1) * 15
next_refresh_threshold = remaining_time - (refresh_count + 1) * 15
if display_time <= next_refresh_threshold and display_time > 0:
if elapsed % 60 == 0 and elapsed > 0:
# 整分钟时全屏刷新
log("%d秒到,全屏刷新" % int(elapsed))
draw_all()
else:
# 15秒、30秒、45秒时局部刷新
log("%d秒到,局部刷新 remaining=%s" % (int(elapsed), format_time(display_time)))
partial_refresh_time_progress()
refresh_count += 1
if loop_count <= 3 or loop_count % 50 == 0:
log("循环#%d state=%s" % (loop_count, STATE_NAMES[current_state]))
# 检查计时完成
if current_state == STATE_WORK:
if get_display_time() <= 0:
log("计时完成,开始休息")
start_break()
elif current_state == STATE_BREAK or current_state == STATE_LONG_BREAK:
if get_display_time() <= 0:
log("休息结束,停止")
stop_timer()
time.sleep_ms(100)
if __name__ == "__main__":
setup()
while True:
loop()



墨水屏刷新策略是灵魂
全屏刷新:状态改变时
局部刷新:每15秒
最小刷新:暂停/继续时
基于 remaining 计算刷新时刻
刷新点:285秒、270秒、255秒...
这样暂停/继续后刷新节奏不会乱
start_time 偏移实现暂停继续
start_time = time.time() - elapsed_at_pause
保持 elapsed 的数学连续性
触摸检测用释放事件
避免按下时误判
保存按下时的触摸数量
WiFi 连接要加超时
否则连不上会卡死
NTP 同步也要 try-except
专注工作/学习
远程办公时间管理
考试复习倒计时
厨房烹饪计时
好了,番茄钟的开发经验就分享到这里。
我要赚赏金
