源码及模型下载
1. 前言
我有一个树莓派和 oled IIC接口 128x64的屏幕,另外我买了树莓派的排线的摄像头。我总想在oled屏幕上显示些什么,一般也就显示一下系统信息,显示一些动画,感觉没什么意思(也不怎么实用),所以我花时间做了一个可以控制树莓派的系统。也花时间用sketchup画了一个简单的外壳模型。
排线屏幕接口我采用插拔的杜邦线接头,这样方便换屏幕,接线地方简单用电烙铁焊接一下然后用热缩管包裹起来。
2. 外壳模型
我有9层外壳,去掉最上面的,用新画的这个就行,四个立柱是固定oled屏幕的,圆洞是风扇的,方框是摄像头的,至于螺丝孔位我没有留,因为我总觉得3d打印的孔位不结实,不如后期我自己用小电转转几个洞,然后固定上螺丝。
弄好了就可以切片了,然后用我廉价的3D打印机打印出模型。 模型看起来很简单,但是画的时候也花了很长时间,每个地方尺寸我都是用游标卡尺测量的,但是打印机精度比较低,后期我用打磨工具(小电转)打磨了一下,还可以。 洞的大小总是打印小,可能是我没有增加水平扩展的缘故吧。
最终效果:
看起来不咋好看,但是还算稳定。
3.手机设置
手机使用软件"蓝牙串口"软件控制:
4.系统的设计
我想实现一个用手机控制树莓派上的一些服务的系统,并且可以实现自定义命令,系统中的每一个选项由json 配置文件决定,这样可以易于拓展功能。开始时我通过网络控制树莓派,树莓派上建立socket服务,手机上发送http请求,但是这种操作很傻,因为你不能保证树莓派到一个环境中肯定是连接到网络上的,如果网络断了就不能控制树莓派了。后来我改成了通过蓝牙串口 rfcomm 协议控制。
oled屏幕驱动我使用的是Adafruit-SSD1306,这个驱动渲染使用的图像对象是一个PIL库的Image对象 , 如果需要拓展系统上功能,可以修改 json文件,并添加子任务来实现,而不需要在做Image上的处理。
command文件夹中存放子任务模块:扫描wifi二维码,显示系统信息,显示动画。
createJson.py
:
import json
data = [
{"text":"设置","function":"test","child":
[
{"text":"wifi设置","function":"JscanQRcode"},
{"text":"service设置","function":"test"},
{"text":"其它设置","function":"test"},
]
},
{"text":"显示动画(Bad Apple)","function":"JtestMovie"},
{"text":"显示系统信息","function":"JdisplayInfo"},
{"text":"重启系统","function":"Jrestart"},
{"text":"设置5","function":"test"},
{"text":"设置6","function":"test"},
{"text":"设置7","function":"test"},
{"text":"设置8","function":"test"},
{"text":"设置9","function":"test"},
{"text":"设置10","function":"test"},
{"text":"设置11","function":"test"},
{"text":"其它设置","function":"test"},
]
with open("config.json",'w') as f:
f.write(json.dumps(data))
4.1 监听通信线程
开始时,监听蓝牙发送指令与收到指令处理分别独立于两个线程,只能分别采用轮询的方式,后来我发现这样对于cpu占用过高,为了考虑资源占用问题和指令的实时性问题,我将指令处理控制放到蓝牙监听循环中,这样在没有收到指令的时候,就会阻塞住,而收到指令的时候会立刻对指令进行处理。减少资源占用,并且提高了指令的实时性。
server_sock=bluetooth.BluetoothSocket(bluetooth.RFCOMM)
server_sock.bind(("",1))
server_sock.listen(1)
while True:
client_sock,address = server_sock.accept()
while True:
try:
control = client_sock.recv(1024).decode()
判断control, 我定义了5种,分别为 上,下,确认,返回,息屏,5个指令
....
except Exception as e:
print(e)
client_sock.close()
break
这里使用两个while 嵌套是因为,rfcomm协议需要一直连接才能通信,每次传输完指令后不会断开连接,这样读取每次的指令都会再第二层循环中, 而如果没有连接的时候就会阻塞在server_sock.accept()
,在连接过程中,断开连接会抛出异常,抛出异常就会执行到 break 从而跳出第二层循环,执行到server_sock.accept()
阻塞住,重新等待新的连接到达。
这里我设计了一个息屏指令,因为我发现之前一直显示系统信息导致有一些烧屏效果,屏幕在全亮的时候有一些字暗痕,所以最好不要让屏幕一直亮着。
4.2 主线程与子任务线程
子任务与主线程不能在同一线程中,否则必然会阻塞到子任务中,从而不能获得下一条指令(也就是不知道什么时候退出),所以子任务的执行需要独立于主线程之外,用另一个线程去执行。独立出主线程外就可以接受到什么时候返回了。
另外就是主线程接受到返回指令后如何通知子线程退出,这里我采用通过文件的方式来传递的消息,当子任务开始运行前,在项目目录中写入一个文件is_running
其中值为1,表示子任务正在运行,然后在子任务循环中判断内容是否为0,如果为0立刻退出,而如果主线程收到返回指令后立刻修改 is_running 中值为0,这样就可以通知子任务结束了。
这里也会产生一个问题,当主线程向is_running中写入值为0执行完毕后,子线程未必立刻退出,而这个时候子线程与主线程可能在同时显示图像(在子任务中有时也可能调用oled屏幕显示一些内容,例如显示动画,显示系统信息),此时会照成冲突,导致oled屏幕不稳定,会出现亮屏,闪屏的现象。所以要保证同一时间,只能有一个线程向oled屏幕中显示图像。加锁是一种解决办法,不过我采用的是一种判定当前线程数量来判断子线程是否真的结束len(threading.enumerate()) > 1
,如果线程数量大于1,说明子线程正在运行。
while len(threading.enumerate()) > 1:
pass
采用这种方式进行阻塞,从而防止多个线程对oled屏幕显示图像,导致oled屏幕不稳定。
5.扫描WIFI二维码功能
小米手机可以分享wifi (以二维码的方式),二维码包含 wifi 信息 ssid, password,以及使用的加密协议。通过这些信息就可以树莓派连接wifi了。 opencv库中有识别二维码的类cv2.QRCodeDetector()
,但是这个功能需要opencv4.0及以后的版本,树莓派执行使用 pip3 安装opencv4.0 及以后肯定会失败的,因为 在 build_wheel 的时候占用大量内存,还要各种原因,无奈,我只能下载opencv源码手动进行的编译。 编译成功后就可以调用这个类了。
这里我想用 oled 屏幕显示摄像头的内容,这种默认肯定是显示不了的,因为oled屏幕只能显示黑白两种状态,也就是图像经过二值化处理后的信息。大致能看清手机轮廓信息,这些也就够了。
二维码扫描成功解析获得的字符串信息,然后将其写入/etc/wpa_supplicant/wpa_supplicant.conf
配置文件,然后重启树莓派系统。这样重启后的树莓派系统就可以连接到wifi了。(这个系统软件肯定是需要root权限运行的,否则就修改不了配置文件)
6.动画
动画的实现是多个图片切换形成的。
6.1启动动画
只有一张图片,不够炫酷,用opencv掩模的方法将其做成动画。
createStartPic.py
import os
import cv2
import numpy as np
width = 128
height = 64
img = cv2.imread("result.png")
mask = np.zeros((height,width,3),np.uint8)
backImg = np.zeros((height,width,3),np.uint8)
for x in range(width):
for y in range(height):
if y % 2 and x % 2:
backImg[y,x,:] = 255
for i in range(100):
temp_mask = mask.copy()
cv2.circle(temp_mask,(width//2-1,height//2),i,(255,255,255),-1)
temp_mask = cv2.cvtColor(temp_mask,cv2.COLOR_BGR2GRAY)
temp_mask = cv2.threshold(temp_mask,150,255,cv2.THRESH_BINARY)[1]
inv_mask = 255 - temp_mask
result = cv2.bitwise_and(img,img,mask=temp_mask)
backImgTemp = cv2.bitwise_and(backImg,backImg,mask=inv_mask)
# cv2.imwrite(f"startPic2/{i}.jpg",backImgTemp+result)
cv2.imshow("title",backImgTemp+result)
cv2.waitKey(10)
然后将每一帧保持一个图片,使用的时候直接调PIL库读取每一帧图像。当然也可以直接用 opencv临时绘制,但是我觉得浪费性能,所以还是将其每一帧保存成图片了。
6.2过渡动画
过渡动画参考开机动画的代码,也就是将上一个状态的图片当做背景,新的图片为最终要展示的图片:
代码可以精确的控制每一帧的延时,过渡动画的快慢都可以进行精细的调节。
但是我感觉浪费些性能,所以过渡动画功能目前没有加到代码中。
7 调试方面
我电脑是win7,在调试写这个控制系统的时候调试起来非常麻烦,每次修改完需要上传到树莓派,才能看到显示的效果。后来我想到,驱动库调用就是PIL图像对象。那么我可以将其转换为 opencv的图像对象(numpy数组),然后进行显示,这也是我文章中上面几个截图中的显示。而要做到不进行任何修改就可以上传,这里调用的Adafruit_SSD1306驱动库,所以我在项目目录中直接建立一个名为 Adafruit_SSD1306 的模块,里面调用opencv将图像用一个子线程显示出来:
import cv2
import numpy as np
from PIL import Image
from threading import Thread
class SSD1306_128_64:
def __init__(self,rst):
self.img = Image.new('1', (128,64))
Thread(target=self.display_thread).start()
def begin(self):
pass
def clear(self):
self.img = Image.new('1', (128,64))
def display_thread(self):
while True:
img = np.asarray(self.img,dtype="uint8")
img[img==1] = 255
cv2.imshow("title",img)
cv2.waitKey(10)
def display(self):
pass
def image(self,image):
self.img = image
上传的时候,不上传 Adafruit_SSD1306.py 文件即可。 当然这里只能作为调试oled屏幕显示方面,不能作为具体任务执行时调试,因为windows没有对应的命令,并且子任务线程判定也会进行干扰。
8.将其配置成服务
新建文件: /etc/systemd/system/rfcomm.service
:
Description=RFCOMM service
After=bluetooth.service
Requires=bluetooth.service
[Service]
ExecStart=/usr/bin/python3 /root/raspi/main.py
[Install]
WantedBy=multi-user.target
增加到开机自启中:
systemctl enable rfcomm
关闭开机自启:
systemctl disable rfcomm
管理服务:
service rfcomm stop #停止
service rfcomm start #启动
service rfcomm restart #重启
9.使用
依赖:
Adafruit_SSD1306
opencv 4.1.0 (手动编译)
可参考我的几篇文章:
树莓派编译opencv4
树莓派蓝牙rfcomm协议通信
上传树莓派不需上传项目目录下 Adafruit_SSD1306.py。
10.其它
为了写这篇文章我用到了一个gif转换生成的工具,我发现生成的gif带有水印,需要充值才可以去水印,简单研究了一下,我猜测水印是png叠加的,所以用正则表达式匹配png文件头和尾,得到了水印图片,然后将其修改成完全透明,然后将修改完的完全透明图像替换到exe中,改完png图像比原始图像要小,所以我进行了补0操作,然后二进制替换就实现了去水印的效果。毕竟gif工具只是临时使用,就只是为了展示一下效果代码效果(我买过这个公司的软件终身会员(不过听说也是github开源项目改的,很不良心))。建议以后软件水印在软件代码逻辑中进行生成,这样尽量防止被破解。破解和反破解相互促进发展。