首先需要对项目名称进行纠正,起初以“物联网密码柜”来命名项目是因为我误以为蓝牙连接也属于物联网的一种,后来得知并不是,故特此将项目名称改为“蓝牙密码柜”。
截至上个阶段,已经实现了对矩阵键盘和OLED显示屏的驱动以及密码校验部分。本阶段的任务:
上个阶段仅实现了密码输入单一界面的功能,本阶段会对其它界面的功能进行完善。
菜单基本结构如下:
现在介绍一下各个界面之间的切换逻辑,在英国威廉希尔公司网站 长按‘ENTER’进入设置页面,在设置页面通过方向键(此页面的逻辑为2468按键对应方向键)切换设置项,如图,会有光标提示。
(顺带一提,上次的PCB布局不太合理,导致接线乱成一团,所以这次重新打了PCB)
在设置页面,如果按下‘ENTER’,则会进入对应的设置项目;如果按下‘BACK’,则会返回到英国威廉希尔公司网站 。
在特定设置项目下,如果按下‘ENTER’,会保存当前的设置,并返回到设置页面;如果按下‘BACK’,则会舍弃当前的设置,并返回到设置页面。
上个阶段,仅仅考虑了密码输入和校验功能,所以矩阵键盘的事件响应设计的并不是很合理,主要体现在按键按下直接驱动事件,无法根据不同场景响应不同事件。正确的做法应该是按键按下仅返回按下的按键,由另外的事件管理器决定应该调用什么方法。
优化后的按键检测方法如下:
def scan_keyboard(self):
"""扫描矩阵键盘并返回按下的按键(row, col),支持单击和长按"""
# 逐行扫描
for i, row in enumerate(self.rows):
# 当前行IO拉低
row.value = False
# 逐列检查
for j, col in enumerate(self.cols):
current_key = (i, j)
# 按键按下
if not col.value:
if self.press_time is None:
# 第一次检测到按键按下,记录时间
self.press_time = time.monotonic()
else:
# 已经记录了按键时间
press_duration = time.monotonic() - self.press_time
if press_duration > self.long_press_threshold:
# 长按事件
#print(f" {current_key} 长按")
self.press_time = None
self.pressed_key = None
return 'long_press', current_key
# 按键释放
else:
if self.press_time is not None:
# 单击事件
press_duration = time.monotonic() - self.press_time
if press_duration <= self.long_press_threshold:
#print(f" {current_key} 短按")
self.press_time = None
return 'short_press', current_key
else:
# 避免长按后再次触发单击
self.press_time = None
self.pressed_key = None
# 重新拉高行IO
row.value = True
# 没有按键按下
self.press_time = None
return None
这个阶段引入了设置页面,需要有交互入口,由于没有多余的按键,所以选择用‘ENTER’键的长按事件来进入设置页面。
本阶段为OLED屏幕显示添加了简单的UI,比较遗憾的是这次还是没能为OLED屏幕显示方法加上中文字库。我所使用的adafruit_ssd1306库基于adafruit的framebuffer,字库是直接读取一个font5x8.bin文件,几经查找,也没能找到关于修改字库或添加中文的资料,只好继续使用英文显示。
核心代码如下:
# 定义切换主题方法
def change_theme(self, bgColor=BLACK):
self.bgColor = bgColor
# 定义修改亮度方法
def set_brightness(self, brightness=127):
#这里通过修改对比度达到提高亮度的效果
self.oled.contrast(brightness)
# 定义显示设置界面方法,光标默认在第一个设置项上
def show_settings(self, index=0):
# 显示设置项
for i, setting in enumerate(settings):
y_position = 8 + (i*16)
self.oled.text(setting, 12, y_position, not self.bgColor, font_name='font5x8.bin', size=1)
cursor_position = 8 + (index*16)
self.oled.text("*", 2, cursor_position, not self.bgColor, font_name='font5x8.bin', size=1)
self.oled.show()
上个阶段提到,我购买的OLED屏幕是1315驱动的,但是所找到的驱动只有1306比较接近。这次怕优化OLED显示方法出现不兼容的问题,还特意重新买了1306驱动的OLED屏幕。
密码柜的实现中,有一个很关键的点就是应该如何实现“开锁”、“关锁”,舵机是一个很好的选择,驱动起来简单,价格也便宜。
驱动舵机的核心代码如下:
# 初始化ServoKit对象,指定使用的通道数
kit = ServoKit(channels=16)
# 定义舵机连接的通道
servo_channel = 0
# 定义打开方法,使舵机转到90度
def open_servo():
kit.servo[servo_channel].angle = 90
# 定义关闭方法,使舵机转到0度
def close_servo():
kit.servo[servo_channel].angle = 0
Silicon Labs xG24 Matter开发套件支持低功耗蓝牙协议BLE,非常适合作为蓝牙密码柜的主控。
首先到circuitpython的官方驱动库中找到BLE相关的驱动,下载和上传代码流程和之前一样,此处不再重复演示。
驱动BLE的核心代码如下:
def start_bluetooth(self):
# 启动蓝牙服务并监听命令
#print("启动蓝牙服务...")
while True:
if not self.uart_connection:
#print("尝试连接...")
for adv in self.ble.start_scan(ProvideServicesAdvertisement):
if UARTService in adv.services:
self.uart_connection = self.ble.connect(adv)
#print("已连接")
break
self.ble.stop_scan()
if self.uart_connection and self.uart_connection.connected:
uart_service = self.uart_connection[UARTService]
while uart_service.in_waiting:
# 读取蓝牙接收的指令
command = uart_service.readline().decode('utf-8').strip()
if command == "open":
self.open_servo()
#print("接收到开锁指令")
elif command == "close":
self.close_servo()
#print("接收到关锁指令")
else:
# 如果连接断开,则重置连接变量
self.uart_connection = None
time.sleep(1)
在编写各个功能代码的过程中,发现功能多起来之后各个功能之间有些地方会有些冲突,比如按键响应事件通过延时进行消除抖动,但这种延时的操作会造成系统资源闲置,耽误了其它事件的响应。于是决定引入RTOS,由操作系统统一管理任务,避免事件之间的冲突,同时也能提高系统的可维护性。
我在circuitpython的官方驱动库中没有找到RTOS相关的库,不过后面在一篇树莓派的文章内找到了circuitpython的RTOS库,这里贴出链接
引入RTOS的核心代码如下:
# 添加任务
def add_task(task):
if task.thread == None:
task.initialize()
tasks.append(task)
tasks.sort(key=lambda t: t.priority)
# 添加服务例程
def add_service_routine(service_routine):
service_routines.append(service_routine)
# 启动RTOS
def start(scheduler=None):
global tasks
if scheduler == None:
scheduler = pyRTOS.default_scheduler
run = True
while run:
for service in service_routines:
service()
messages = scheduler(tasks)
pyRTOS.deliver_messages(messages, tasks)
if len(tasks) == 0:
run = False
至此,蓝牙密码柜的基本功能已经全部实现。等后续有时间会继续对项目进行优化,例如结合机械结构设计制作完整的密码柜。
更多回帖