wshuo 发表于 2021-11-15 15:00

写一个树莓派管理系统

[源码及模型下载](https://wshuo.lanzoui.com/iqnNBwiwtkb)

### 1. 前言

我有一个树莓派和 oled IIC接口 128x64的屏幕,另外我买了树莓派的排线的摄像头。我总想在oled屏幕上显示些什么,一般也就显示一下系统信息,显示一些动画,感觉没什么意思(也不怎么实用),所以我花时间做了一个可以控制树莓派的系统。也花时间用sketchup画了一个简单的外壳模型。

排线屏幕接口我采用插拔的杜邦线接头,这样方便换屏幕,接线地方简单用电烙铁焊接一下然后用热缩管包裹起来。

### 2. 外壳模型

!(https://img-blog.csdnimg.cn/2021111203013687.JPG?)

我有9层外壳,去掉最上面的,用新画的这个就行,四个立柱是固定oled屏幕的,圆洞是风扇的,方框是摄像头的,至于螺丝孔位我没有留,因为我总觉得3d打印的孔位不结实,不如后期我自己用小电转转几个洞,然后固定上螺丝。

![切片](https://img-blog.csdnimg.cn/2021111203050849.JPG?)

弄好了就可以切片了,然后用我廉价的3D打印机打印出模型。 模型看起来很简单,但是画的时候也花了很长时间,每个地方尺寸我都是用游标卡尺测量的,但是打印机精度比较低,后期我用打磨工具(小电转)打磨了一下,还可以。洞的大小总是打印小,可能是我没有增加水平扩展的缘故吧。

最终效果:

!(https://img-blog.csdnimg.cn/20211112031056812.jpg)

看起来不咋好看,但是还算稳定。

### 3.手机设置

手机使用软件"蓝牙串口"软件控制:

!(https://img-blog.csdnimg.cn/20211113021501421.jpg?)

!(https://img-blog.csdnimg.cn/20211113021515968.jpg?)

### 4.系统的设计

我想实现一个用手机控制树莓派上的一些服务的系统,并且可以实现自定义命令,系统中的每一个选项由json 配置文件决定,这样可以易于拓展功能。开始时我通过网络控制树莓派,树莓派上建立socket服务,手机上发送http请求,但是这种操作很傻,因为你不能保证树莓派到一个环境中肯定是连接到网络上的,如果网络断了就不能控制树莓派了。后来我改成了通过蓝牙串口 **rfcomm** 协议控制。

!(https://img-blog.csdnimg.cn/20211112205519300.png?)

oled屏幕驱动我使用的是Adafruit-SSD1306,这个驱动渲染使用的图像对象是一个PIL库的Image对象 , 如果需要拓展系统上功能,可以修改 json文件,并添加子任务来实现,而不需要在做Image上的处理。

command文件夹中存放子任务模块:扫描wifi二维码,显示系统信息,显示动画。

`createJson.py`:

```python
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))
```

!(https://img-blog.csdnimg.cn/20211112213050663.jpg)

#### 4.1 监听通信线程

开始时,监听蓝牙发送指令与收到指令处理分别独立于两个线程,只能分别采用轮询的方式,后来我发现这样对于cpu占用过高,为了考虑资源占用问题和指令的实时性问题,我将指令处理控制放到蓝牙监听循环中,这样在没有收到指令的时候,就会阻塞住,而收到指令的时候会立刻对指令进行处理。减少资源占用,并且提高了指令的实时性。

```python
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,说明子线程正在运行。

```python
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启动动画



!(https://img-blog.csdnimg.cn/20211112225050870.png)

只有一张图片,不够炫酷,用opencv掩模的方法将其做成动画。

!(https://img-blog.csdnimg.cn/20211112225426177.gif)

`createStartPic.py`

```python
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 = 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)
    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过渡动画

过渡动画参考开机动画的代码,也就是将上一个状态的图片当做背景,新的图片为最终要展示的图片:

!(https://img-blog.csdnimg.cn/20211112230837205.gif)

代码可以精确的控制每一帧的延时,过渡动画的快慢都可以进行精细的调节。

但是我感觉浪费些性能,所以过渡动画功能目前没有加到代码中。

### 7 调试方面

我电脑是win7,在调试写这个控制系统的时候调试起来非常麻烦,每次修改完需要上传到树莓派,才能看到显示的效果。后来我想到,驱动库调用就是PIL图像对象。那么我可以将其转换为 opencv的图像对象(numpy数组),然后进行显示,这也是我文章中上面几个截图中的显示。而要做到不进行任何修改就可以上传,这里调用的**Adafruit_SSD1306**驱动库,所以我在项目目录中直接建立一个名为 Adafruit_SSD1306 的模块,里面调用opencv将图像用一个子线程显示出来:

```python
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 = 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


ExecStart=/usr/bin/python3 /root/raspi/main.py


WantedBy=multi-user.target
```

增加到开机自启中:

```bash
systemctl enable rfcomm
```

关闭开机自启:

```shell
systemctl disable rfcomm
```

管理服务:

```shell
service rfcomm stop #停止
service rfcomm start #启动
service rfcomm restart #重启
```

### 9.使用

依赖:

Adafruit_SSD1306
opencv 4.1.0 (手动编译)

可参考我的几篇文章:

[树莓派编译opencv4](https://wshuo.blog.csdn.net/article/details/121234399)

[树莓派蓝牙rfcomm协议通信](https://wshuo.blog.csdn.net/article/details/121280147)

上传树莓派不需上传项目目录下 **Adafruit_SSD1306.py**。

### 10.其它

为了写这篇文章我用到了一个gif转换生成的工具,我发现生成的gif带有水印,需要充值才可以去水印,简单研究了一下,我猜测水印是png叠加的,所以用正则表达式匹配png文件头和尾,得到了水印图片,然后将其修改成完全透明,然后将修改完的完全透明图像替换到exe中,改完png图像比原始图像要小,所以我进行了补0操作,然后二进制替换就实现了去水印的效果。毕竟gif工具只是临时使用,就只是为了展示一下效果代码效果(我买过这个公司的软件终身会员(不过听说也是github开源项目改的,很不良心))。建议以后软件水印在软件代码逻辑中进行生成,这样尽量防止被破解。破解和反破解相互促进发展。

wshuo 发表于 2021-11-15 17:01

头铁又刚 发表于 2021-11-15 15:34
请问一下树莓派4做nas是不会有点浪费?目前我手上有一个

没啥浪费的,不用才是浪费,树莓派可以跑多个服务。

wshuo 发表于 2021-11-16 18:53

TXYYSMJ 发表于 2021-11-16 15:27
很有帮助,请问外壳是用什么软件画的?用什么材质打印的?

画模型用的草图大师sketchup,切片用cura, 打印耗材是pla

elevo 发表于 2021-11-15 15:11

建模什么的还是很强的呀

zhuantoude 发表于 2021-11-15 15:22

看着就高大强

头铁又刚 发表于 2021-11-15 15:34

请问一下树莓派4做nas是不会有点浪费?目前我手上有一个

aLong2016 发表于 2021-11-15 16:02

酷酷的。{:1_921:}

ldwz 发表于 2021-11-15 16:09

这个~~~就是传说中的,高手吗???还三地打印~~羡慕~

blindcat 发表于 2021-11-15 16:09

大佬真NB

liangchengjiang 发表于 2021-11-15 16:10

感觉很厉害,大赞~

wangshizf 发表于 2021-11-15 16:19

很厉害,感谢分享

Aaron-x 发表于 2021-11-15 16:37

挖槽牛批啊
页: [1] 2 3
查看完整版本: 写一个树莓派管理系统