前言
首先需要对项目名称进行纠正,起初以“物联网密码柜”来命名项目是因为我误以为蓝牙连接也属于物联网的一种,后来得知并不是,故特此将项目名称改为“蓝牙密码柜”。
截至上个阶段,已经实现了对矩阵键盘和OLED显示屏的驱动以及密码校验部分。本阶段的任务:
- 引入菜单逻辑,并优化矩阵键盘响应事件,根据所处不同场景响应对应事件
- 优化OLED显示方法,设计简单UI
- 驱动舵机,结合机械结构设计实现开锁、关锁的目的
- 驱动蓝牙,可通过其它蓝牙设备进行开锁、关锁
- 优化代码架构,引入RTOS,提高系统可维护性
菜单逻辑
上个阶段仅实现了密码输入单一界面的功能,本阶段会对其它界面的功能进行完善。
菜单基本结构如下:
- 首页(密码输入界面)
- 设置页面
- 主题修改(其实就是把背景颜色和文字颜色取反,类似于白天/黑夜主题切换)
- 亮度修改
- 密码修改
现在介绍一下各个界面之间的切换逻辑,在首页长按‘ENTER’进入设置页面,在设置页面通过方向键(此页面的逻辑为2468按键对应方向键)切换设置项,如图,会有光标提示。
(顺带一提,上次的PCB布局不太合理,导致接线乱成一团,所以这次重新打了PCB)
在设置页面,如果按下‘ENTER’,则会进入对应的设置项目;如果按下‘BACK’,则会返回到首页。
在特定设置项目下,如果按下‘ENTER’,会保存当前的设置,并返回到设置页面;如果按下‘BACK’,则会舍弃当前的设置,并返回到设置页面。
矩阵键盘响应优化
上个阶段,仅仅考虑了密码输入和校验功能,所以矩阵键盘的事件响应设计的并不是很合理,主要体现在按键按下直接驱动事件,无法根据不同场景响应不同事件。正确的做法应该是按键按下仅返回按下的按键,由另外的事件管理器决定应该调用什么方法。
优化后的按键检测方法如下:
def scan_keyboard(self):
"""扫描矩阵键盘并返回按下的按键(row, col),支持单击和长按"""
for i, row in enumerate(self.rows):
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:
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:
self.press_time = None
return 'short_press', current_key
else:
self.press_time = None
self.pressed_key = None
row.value = True
self.press_time = None
return None
这个阶段引入了设置页面,需要有交互入口,由于没有多余的按键,所以选择用‘ENTER’键的长按事件来进入设置页面。
OLED简单UI
本阶段为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屏幕。
驱动舵机
密码柜的实现中,有一个很关键的点就是应该如何实现“开锁”、“关锁”,舵机是一个很好的选择,驱动起来简单,价格也便宜。
驱动舵机的核心代码如下:
kit = ServoKit(channels=16)
servo_channel = 0
def open_servo():
kit.servo[servo_channel].angle = 90
def close_servo():
kit.servo[servo_channel].angle = 0
驱动蓝牙
Silicon Labs xG24 Matter开发套件支持低功耗蓝牙协议BLE,非常适合作为蓝牙密码柜的主控。
首先到circuitpython的官方驱动库中找到BLE相关的驱动,下载和上传代码流程和之前一样,此处不再重复演示。
驱动BLE的核心代码如下:
def start_bluetooth(self):
while True:
if not self.uart_connection:
for adv in self.ble.start_scan(ProvideServicesAdvertisement):
if UARTService in adv.services:
self.uart_connection = self.ble.connect(adv)
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()
elif command == "close":
self.close_servo()
else:
self.uart_connection = None
time.sleep(1)
引入RTOS
在编写各个功能代码的过程中,发现功能多起来之后各个功能之间有些地方会有些冲突,比如按键响应事件通过延时进行消除抖动,但这种延时的操作会造成系统资源闲置,耽误了其它事件的响应。于是决定引入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)
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
总结
至此,蓝牙密码柜的基本功能已经全部实现。等后续有时间会继续对项目进行优化,例如结合机械结构设计制作完整的密码柜。