1 简介
我有一个Raspberry Pi和一个oled IIC接口128×64屏幕。 另外,我还买了一个Raspberry Pi有线摄像头。 我总是想在OLED屏幕上显示一些东西,它通常只显示系统信息和一些动画。 感觉没意思(而且不太实用),所以我花了时间做了一个可以控制树莓派的系统。 我还花时间用sketchup画了一个简单的外壳模型。
电缆屏接口我使用的是插入式杜邦电缆连接器,这样可以方便地更换屏幕。 接线区域只需用烙铁焊接一下,然后用热缩管包裹即可。
2. 外壳模型
我有9层肠衣。 拆掉上面的一层,只使用新涂漆的一层。 四个支柱用于固定OLED屏幕。 圆孔是风扇孔,方框是摄像头孔。 至于螺丝孔,我没有留,因为我总觉得3D打印的孔不牢固,所以以后不妨用小电机转几个孔,然后再固定螺丝。
完成后,就可以将其切片并使用我的廉价 3D 打印机打印出模型。 模型看起来很简单,但是画起来却花了很长时间。 我用游标卡尺测量了各个地方的尺寸,但是打印机的精度比较低。 后来我用砂光工具(小电动转子)打磨了一下,就可以了。 。 孔的尺寸总是打印得小,可能是因为我没有增加水平扩展。
最终效果:
看起来不太好,但是很稳定。
3.手机设置
手机使用软件“蓝牙串口”软件控制:
4、系统设计
我想实现一个用手机控制Raspberry Pi上的一些服务的系统,并且可以实现自定义命令。 系统中的每个选项都由一个json配置文件决定,这样可以方便地扩展功能。 一开始我是通过网络控制树莓派,在树莓派上建立socket服务,在手机上发送http请求。 然而,这个操作很愚蠢,因为你不能保证树莓派进入一个环境时一定会连接到网络。 如果网络断了,你就无法控制Raspberry Pi。 后来我改成通过蓝牙串口rfcomm协议控制。
我使用 Adafruit-SSD1306 作为 oled 屏幕驱动程序。 该驱动程序用于渲染的图像对象是PIL库的Image对象。 如果需要扩展系统上的功能,可以修改json文件,添加子任务来实现,无需动手。 图像处理。
子任务模块存放在command文件夹中:扫描wifi二维码、显示系统信息、显示动画。
创建Json.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、密码以及使用的加密协议。 有了这些信息,Raspberry Pi 就可以连接到 WiFi。 opencv库中有一个类cv2.QRCodeDetector()可以识别二维码,但该功能需要opencv4.0及以上版本。 树莓派在使用pip3安装opencv4.0及更高版本的时候肯定会失败,因为在build_wheel的时候占用了大量的内存,而且由于种种原因,我无奈只能下载opencv源码手动编译。 编译成功后就可以调用这个类了。
这里我想用OLED屏幕来显示相机的内容。 默认情况下肯定不会显示这个,因为OLED屏幕只能显示黑白,这是图像经过二值化后的信息。 大致看到手机的轮廓信息就足够了。
然后将成功解析二维码扫描得到的字符串信息写入到/etc/wpa_supplicant/wpa_supplicant.conf配置文件中,然后重启Raspberry Pi系统。 这样重启后的树莓派系统就可以连接wifi了。 (这个系统软件肯定需要root权限才能运行,否则配置文件无法修改)
6.动画
动画的实现是通过多张图片的切换形成的。
6.1 开始动画
只有一张图,不够炫酷。 使用opencv mask方法将其制作成动画。
创建开始图片.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开源项目修改过的,很不择手段))。 建议以后在软件代码逻辑中生成软件水印,尽可能防止破解。 破解与反破解相互促进发展。