本帖最后由 gada888 于 2026-8-23 23:29 编辑
用行空板做多界面交互面板——用状态机切换页面
很多人第一次做硬件项目,会把所有控件堆在一个页面里:一个按钮控制灯,一个滑块调亮度,再加几个状态文字。功能少时还能运行,功能一多就很难维护。本课不从“画一个按钮”开始,而是做一个真正可扩展的多界面控制面板:主页显示灯光状态,设置页调整亮度,点击按钮切换页面,页面状态和硬件状态彼此独立。
【

】
## 先看最终效果
最终界面有两个页面:主页显示“灯:开/关”、当前亮度、灯带引脚和两个操作按钮;设置页提供亮度滑块、颜色按钮以及返回主页按钮。灯带使用 Digital RGB LED 模块,数据线接行空板 P21。点击“切换灯光”后,灯带状态改变;点击“进入设置页面”后,屏幕切换到设置页。
【 】
硬件清单很克制:行空板 M10、Digital RGB LED 模块、杜邦线和 USB 供电。这个选择适合课堂演示,因为接线少,学生能把注意力放在页面逻辑和状态管理上。
## 先定义程序状态
```python
current_page = "home"
light_on = False
brightness = 80
current_color = (255, 0, 0)
```
这几个变量就是程序的“当前状态”。`current_page` 只负责页面,`light_on`、`brightness` 和 `current_color` 只负责灯光。这样做的关键好处是:切换页面不会意外关灯,调亮度也不会改变当前页面。
## 页面不要写成一团
把界面拆成三个函数:`build_home_page()` 创建主页控件,`build_settings_page()` 创建设置页控件,`render_current_page()` 根据 `current_page` 决定显示哪一页。页面按钮只修改状态并调用渲染函数。
```python
def go_settings():
global current_page
current_page = "settings"
render_current_page()
def go_home():
global current_page
current_page = "home"
render_current_page()
```
【配图 3:
】


## 状态切换和硬件控制分开
“切换灯光”按钮只负责改变 `light_on`,真正的灯带输出交给 `apply_light_state()`。如果灯光打开,就按当前颜色和亮度点亮;如果关闭,就把亮度设为零或发送关闭指令。
```python
def toggle_light():
global light_on
light_on = not light_on
apply_light_state()
render_current_page()
```
【配图 4:
】

## 亮度滑块为什么要单独处理
滑块产生的是连续数值,按钮产生的是离散事件。滑块回调只更新 `brightness`,然后调用灯光函数;它不应该重新创建整个页面。这样拖动滑块时,页面不会闪烁,灯光也能即时变化。
```python
def on_brightness_change(value):
global brightness
brightness = int(value)
apply_light_state()
```
## 运行验证清单
1. 上电后默认进入主页;
2. 点击切换按钮,灯光状态和页面文字同步变化;
3. 进入设置页,拖动亮度滑块,灯光立即变化;
4. 返回主页后,之前设置的亮度和灯光状态仍然保留;
5. 反复切换页面,不出现重复控件或界面错位。
如果界面切换后出现重复控件,检查是否每次切页都重新创建了控件;如果灯光状态错乱,检查 `light_on` 是否误写成局部变量;如果滑块能动但灯不变,检查回调是否调用了硬件输出函数。
## 这节课真正要学的是什么
这个项目训练的是三个可迁移的习惯:先定义状态,再设计页面;把界面事件和硬件动作分开;用一个渲染入口保证页面显示一致。后面做温度仪表盘、植物提醒器或灯效控制器时,都可以复用同样的结构。
【配图 5:
】

## 完整带解释代码
# -*- coding: utf-8 -*-
"""
UNIHIKER 触屏 + WS2812 单颗灯 控制面板示例
模块化结构说明:
- 硬件初始化与常量配置区
- 全局状态变量区(当前页面、灯状态、颜色、亮度等)
- 业务逻辑函数(控制灯光、计算背景色、页面路由)
- UI 绘制函数(主页 / 设置页)
- 程序入口与主循环
"""
# ====== 导入依赖 ======
from unihiker import GUI # 引入 UNIHIKER 的图形界面库 [web:56]
from pinpong.board import Board, Pin, NeoPixel # 引入主控板、引脚、WS2812 控制类 [web:52]
import time # 引入时间模块,用于主循环延时
# ============================================================
# 一、硬件初始化与常量配置
# ============================================================
# 初始化 UNIHIKER 主控板(必须在使用 pinpong 相关类前调用)[web:56]
Board("UNIHIKER").begin()
# 定义 WS2812 所在引脚(使用 UNIHIKER 上的 P23 引脚)
NEOPIXEL_PIN = Pin(Pin.P23) # 创建一个 Pin 对象,表示 P23 引脚 [web:52]
PIXELS_NUM = 1 # 使用的 WS2812 灯珠数量,这里只接了 1 颗
# 创建 NeoPixel 对象,用来控制 WS2812 灯条 / 灯珠
np = NeoPixel(NEOPIXEL_PIN, PIXELS_NUM) # 传入引脚和数量,即可控制对应灯珠 [web:52]
# 创建 GUI 对象,用于在 UNIHIKER 屏幕上绘图、显示文本、按钮等 [web:47]
gui = GUI()
# ============================================================
# 二、全局状态变量(业务状态)
# ============================================================
current_page = "home" # 当前界面:可以是 "home" 或 "settings"
light_on = False # 灯当前是否点亮:False=关,True=开
bg_color = (0, 0, 0) # 屏幕背景基础颜色(R,G,B),不含亮度系数
brightness = 100 # 整体亮度百分比,范围 0~100
rgb_color = (255, 255, 0) # WS2812 灯的基础颜色,默认黄色(255,255,0)
# ============================================================
# 三、业务逻辑函数:灯光控制 / 颜色计算 / 页面跳转
# ============================================================
def apply_led():
"""
根据当前全局状态:
- light_on 是否开灯
- rgb_color 灯的基础颜色
- brightness 亮度百分比
计算最终颜色并写入 WS2812 灯珠。
"""
if light_on: # 如果灯处于“开”的状态
factor = brightness / 100.0 # 将 0~100 的亮度转换为 0~1 的系数
r = int(rgb_color[0] * factor) # 计算加亮度后的红色分量
g = int(rgb_color[1] * factor) # 计算加亮度后的绿色分量
b = int(rgb_color[2] * factor) # 计算加亮度后的蓝色分量
np[0] = (r, g, b) # 把计算好的 RGB 写入第 0 号灯珠
else:
np[0] = (0, 0, 0) # 如果关灯,则写入全黑(熄灭)
def calc_bg_color():
"""
根据当前背景基础颜色 bg_color 和 brightness,
计算实际用于绘制背景的颜色(做一个简单的“亮度”效果)。
"""
factor = brightness / 100.0 # 亮度转为 0~1 的系数
r = int(bg_color[0] * factor) # 背景红色通道乘以系数
g = int(bg_color[1] * factor) # 背景绿色通道乘以系数
b = int(bg_color[2] * factor) # 背景蓝色通道乘以系数
return (r, g, b) # 返回新的 (R,G,B) 元组
def clear_screen():
"""
清空当前屏幕内容,并用当前背景色重新填充整个屏幕区域。
"""
gui.clear() # 清除所有已绘制内容
# 使用计算后的背景色填充整块屏幕(UNIHIKER 默认分辨率 240x320)[web:47]
gui.fill_rect(x=0, y=0, w=240, h=320, color=calc_bg_color())
def go_home():
"""
跳转到主页。
"""
global current_page # 声明使用全局变量 current_page
current_page = "home" # 修改当前页面状态为“home”
draw_home_page() # 重绘主页 UI
def go_settings():
"""
跳转到设置页面。
"""
global current_page # 声明使用全局变量 current_page
current_page = "settings" # 修改当前页面状态为“settings”
draw_settings_page() # 重绘设置页 UI
def toggle_light():
"""
切换灯光开关状态:如果原来是关,则打开;原来是开,则关闭。
"""
global light_on # 使用全局的灯状态
light_on = not light_on # 取反:True -> False,False -> True
apply_led() # 根据新状态更新实际灯光
draw_home_page() # 回到主页并刷新显示灯状态
# ------- 背景颜色设置逻辑 -------
def set_bg_red():
"""
设置屏幕背景为红色基调(受亮度影响)。
"""
global bg_color # 使用全局背景颜色
bg_color = (255, 0, 0) # 纯红色
redraw_current() # 重新绘制当前页面
def set_bg_green():
"""
设置屏幕背景为绿色基调。
"""
global bg_color
bg_color = (0, 160, 0) # 偏亮的绿色
redraw_current()
def set_bg_blue():
"""
设置屏幕背景为蓝色基调。
"""
global bg_color
bg_color = (0, 80, 200) # 偏亮的蓝色
redraw_current()
# ------- 灯光颜色设置逻辑 -------
def set_led_red():
"""
将 WS2812 灯设置为红色,并确保灯是开启状态。
"""
global rgb_color, light_on
rgb_color = (255, 0, 0) # 灯颜色设为红色
light_on = True # 确保灯处于“开”的状态
apply_led() # 写入灯珠
redraw_current() # 刷新当前页面(亮度文字等)
def set_led_green():
"""
将 WS2812 灯设置为绿色,并确保灯是开启状态。
"""
global rgb_color, light_on
rgb_color = (0, 255, 0) # 绿色
light_on = True
apply_led()
redraw_current()
def set_led_blue():
"""
将 WS2812 灯设置为蓝色,并确保灯是开启状态。
"""
global rgb_color, light_on
rgb_color = (0, 0, 255) # 蓝色
light_on = True
apply_led()
redraw_current()
# ------- 亮度调节逻辑 -------
def brightness_down():
"""
将亮度降低 10%,但不低于 10%。
"""
global brightness
brightness = max(10, brightness - 10) # 限制最小值为 10
apply_led() # 调整后更新灯光亮度
redraw_current() # 更新界面显示的亮度文字和背景
def brightness_up():
"""
将亮度提高 10%,但不超过 100%。
"""
global brightness
brightness = min(100, brightness + 10) # 限制最大值为 100
apply_led()
redraw_current()
def redraw_current():
"""
根据 current_page 的值,决定重绘哪个页面。
这样改变背景色 / 灯色 / 亮度后,都可以统一调用此函数刷新界面。
"""
if current_page == "settings": # 如果当前是设置页
draw_settings_page() # 重绘设置页
else:
draw_home_page() # 否则视为主页
# ============================================================
# 四、UI 绘制函数:主页 / 设置页
# ============================================================
def draw_home_page():
"""
绘制“主页”界面:
- 显示标题
- 显示一个圆形“灯泡”图标
- 显示当前灯状态文字
- 提示 WS2812 接线信息
- 提供“切换灯光”和“进入设置页面”的按钮
"""
clear_screen() # 先清空并涂背景
# 绘制标题文字(左上角)
gui.draw_text(
x=60, y=10,
text="控制面板 - 主页",
origin="nw", # 以左上角为对齐原点
color="white"
)
# 根据 light_on 状态,决定灯泡图标颜色
lamp_color = "yellow" if light_on else "gray"
# 在屏幕中央附近画一个圆,作为灯泡图标
gui.fill_circle(
x=120, y=120, # 圆心位置
r=40, # 半径
color=lamp_color
)
# 在圆下面写文字:“灯:开” 或 “灯:关”
gui.draw_text(
x=100, y=180,
text="灯:开" if light_on else "灯:关",
origin="nw",
color="white"
)
# 显示说明:WS2812 接在哪个引脚
gui.draw_text(
x=70, y=200,
text="WS2812 接在 P21", # 这里你可以改成真实接脚说明
origin="nw",
color="white"
)
# ---- “切换灯光”按钮 ----
gui.fill_round_rect(
x=40, y=220, # 按钮左上角位置
w=160, h=40, # 宽度、高度
r=8, # 圆角半径
color="orange", # 填充颜色
onclick=toggle_light # 点击回调函数 [web:50]
)
gui.draw_text(
x=80, y=235, # 按钮内的文字位置
text="切换灯光",
origin="nw",
color="black"
)
# ---- “进入设置页面”按钮 ----
gui.fill_round_rect(
x=40, y=270,
w=160, h=40,
r=8,
color="skyblue",
onclick=go_settings # 点击后跳转到设置页
)
gui.draw_text(
x=70, y=285,
text="进入设置页面",
origin="nw",
color="black"
)
def draw_settings_page():
"""
绘制“设置页面”界面:
- 顶部返回按钮
- 屏幕背景颜色选项(红 / 绿 / 蓝)
- WS2812 灯颜色选项(红灯 / 绿灯 / 蓝灯)
- 亮度显示与增减按钮
"""
clear_screen() # 清空并涂背景
# 页面标题
gui.draw_text(
x=80, y=10,
text="设置页面",
origin="nw",
color="white"
)
# ---- 左上角“返回”按钮 ----
gui.fill_round_rect(
x=10, y=10,
w=60, h=30,
r=6,
color="skyblue",
onclick=go_home # 点击回主页
)
gui.draw_text(
x=20, y=18,
text="返回",
origin="nw",
color="black"
)
# ========= 屏幕背景颜色区域 =========
gui.draw_text(
x=20, y=60,
text="屏幕背景颜色:",
origin="nw",
color="white"
)
# -- 背景:红色按钮 --
gui.fill_round_rect(
x=20, y=90,
w=60, h=40,
r=6,
color="red",
onclick=set_bg_red
)
gui.draw_text(
x=40, y=100,
text="红",
origin="nw",
color="black"
)
# -- 背景:绿色按钮 --
gui.fill_round_rect(
x=90, y=90,
w=60, h=40,
r=6,
color=(0, 160, 0),
onclick=set_bg_green
)
gui.draw_text(
x=110, y=100,
text="绿",
origin="nw",
color="black"
)
# -- 背景:蓝色按钮 --
gui.fill_round_rect(
x=160, y=90,
w=60, h=40,
r=6,
color=(0, 80, 200),
onclick=set_bg_blue
)
gui.draw_text(
x=180, y=100,
text="蓝",
origin="nw",
color="black"
)
# ========= WS2812 灯颜色区域 =========
gui.draw_text(
x=20, y=150,
text="WS2812 颜色:",
origin="nw",
color="white"
)
# -- 红灯按钮 --
gui.fill_round_rect(
x=20, y=180,
w=60, h=40,
r=6,
color="red",
onclick=set_led_red
)
gui.draw_text(
x=30, y=190,
text="红灯",
origin="nw",
color="black"
)
# -- 绿灯按钮 --
gui.fill_round_rect(
x=90, y=180,
w=60, h=40,
r=6,
color="green",
onclick=set_led_green
)
gui.draw_text(
x=100, y=190,
text="绿灯",
origin="nw",
color="black"
)
# -- 蓝灯按钮 --
gui.fill_round_rect(
x=160, y=180,
w=60, h=40,
r=6,
color="blue",
onclick=set_led_blue
)
gui.draw_text(
x=170, y=190,
text="蓝灯",
origin="nw",
color="black"
)
# ========= 亮度显示与调节 =========
gui.draw_text(
x=20, y=240,
text="亮度:{}%".format(brightness), # 显示当前亮度数值
origin="nw",
color="white"
)
# -- 亮度 -10 按钮 --
gui.fill_round_rect(
x=20, y=270,
w=80, h=40,
r=6,
color="gray",
onclick=brightness_down
)
gui.draw_text(
x=50, y=280,
text="-10",
origin="nw",
color="black"
)
# -- 亮度 +10 按钮 --
gui.fill_round_rect(
x=140, y=270,
w=80, h=40,
r=6,
color="gray",
onclick=brightness_up
)
gui.draw_text(
x=170, y=280,
text="+10",
origin="nw",
color="black"
)
# ============================================================
# 五、程序入口与主循环
# ============================================================
# 启动时先应用一次灯光状态(这里默认 light_on=False,相当于关灯)
apply_led()
# 默认先进入主页界面
draw_home_page()
# 主循环:在 UNIHIKER 上通常用一个死循环维持程序运行
while True:
# 这里没有额外逻辑,只是简单 sleep 以避免 CPU 占用过高
time.sleep(0.05)
|
|