一、项目描述
1.项目需求
完成一个基于Sipeed M1s Dock的网络相机。具体要求为:完成相机驱动,定时拍摄图片,并将图片通过网络传到电脑或服务器,实现长时间拍摄;通过电脑端编程将图片合成为一个视频。
2.设计思路
使用Sipeed提供的示例代码中的camera_streaming_through_wifi例程,实现相机驱动、图片定时拍摄与图片上传功能;在PC端编写python代码,搭建服务端以接收并实时显示图片内容,达到实时网络相机的效果。
设备端依次执行摄像头初始化、WiFi初始化、WiFi连接,成功后便执行图片(已压缩为jpg格式)上传操作。
上传获取的一帧图片时,设备端首先会发送四个字节长度的数据,告知服务端将要发送的图片的大小,并等待服务端回复;接收到服务端回复的一字节任意内容后,设备端就会发送图片数据;图片数据发送结束后,便重复上述操作发送下一帧图片。
服务端运行python代码,搭建TCP服务端并监听指定ip地址及端口发来的连接请求;与设备端连接成功后开始接收数据,依次执行接收四字节的图片大小数据、回复一个字节的任意内容(本项目设置为一个空格,即“ ”,对应ASCII码为0x20)、接收图片数据、保存图片、显示图片,之后便重复此循环。
3.软件流程图
二、项目硬件介绍
Sipeed M1s Dock是基于Sipeed M1s模组来设计的一款核心板,引出了MIPI CSI、SPI LCD等FPC接口,免去接线难的烦恼。使用最精简的设计,用于客户对模组进行模组评估,或者爱好者直接上手游玩等用途。
板卡主芯片为BL808 RISC-V 480MHz + NPU BLAI-100;板载USB转UART调试器,可实现一键点击烧录,无需按实体按键;接有1.69寸240x280电容触摸屏和200W像素摄像头;支持2.4G WIFI / BT / BLE;板载1个模拟麦克风、1个LED和1个TF卡座;引出一路USB-OTG到USB Type-C接口。
开发板完备支持FreeRTOS,提供有C语言SDK,可以通过串口或虚拟磁盘拖拽两种方式实现固件下载,此外还支持AI推理框架和AI模型的使用,可以实现如语音关键词识别、人脸识别等AI应用功能。
本项目主要使用到了开发板的摄像头、WiFi等模块。
三、项目实现
1.模块介绍
设备端:设备端开发需要搭建环境。首先,在Linux系统下,访问以下两个链接:
SDK:https://gitee.com/Sipeed/M1s_BL808_SDK.git
Example:https://gitee.com/Sipeed/M1s_BL808_example.git
按照第二个链接的readme文件中的setup步骤搭建环境,能够正常编译示例代码即环境搭建成功。
现在根目录(bl808)下有两个文件夹:M1s_BL808_SDK中包含了Sipeed提供的SDK文件,主程序的代码依赖其中的库文件运行;M1s_BL808_example文件夹下则是实现功能的示例工程文件。
对于前者我们要关注的是如下路径:
~\M1s_BL808_SDK\components\sipeed\e907\m1s_e907_xram\src
在该路径中的m1s_e907_xram_wifi.c文件
对于后者我们要关注的是:
~\M1s_BL808_example\c906_app\camera_streaming_through_wifi
在该路径中的main.c文件,以及:
~\M1s_BL808_example\e907_app
这个路径。
两者与设备端WiFi连接有关。c906和e907为处理器的两个核,在连接WiFi时,WiFi SSID和服务端ip地址等参数参数传入c906中,但实际的WiFi连接与数据推流是由e907完成的。
接下来将介绍在烧写运行程序前需要执行的修改步骤。
(1)修改e907 firmware
修改后的firmware已在附件中提供(firmware.bin),可直接在后续步骤中使用,下文则是对修改过程的说明。
2023.2.27,官方发布了新的firmware修复了这个问题,经实验本项目代码与新firmware完全兼容(只要ip地址和端口等配置正确)。
(demo说明:https://wiki.sipeed.com/hardware/zh/maix/m1s/other/start.html,与本项目内容基本一致,但demo里的服务端python似乎更流畅,不过我也懒得改了,反正能用╮(╯_╰)╭)
上述工作过程中,两核之间的参数传递由m1s_e907_xram_wifi.c完成。可能是开发者疏忽,代码219~220行处服务端的ip地址和端口号是固定的,没有用到c906传来的参数,需要修改。(后续更新中该问题可能会被修复,读者请自行判断是否需要修改。官方还真的修复了,随便挑一个用吧······)
client_addr.sin_port = htons(8888);
client_addr.sin_addr.s_addr = inet_addr("10.42.0.1");
将htons后的参数改为private.port,将inet_addr后的参数改为private.ip,该代码才可正常工作。
client_addr.sin_port = htons(private.port);
client_addr.sin_addr.s_addr = inet_addr(private.ip);
此时因为我们修改了e907核的SDK文件,其固件需要重新编译。在终端中打开e907_app目录,执行以下指令:
export BL_SDK_PATH=$(pwd)/../M1s_BL808_SDK
./build.sh firmware
第一条指令导入了SDK路径,第二条指令则编译firmware工程。编译完成后会生成build_out文件夹,该文件夹下的firmare.bin就是我们刚刚修改好的e907固件。保存好该文件,准备后续烧录。
(2)修改主程序文件
打开camera_streaming_through_wifi文件夹下的main.c文件,将第20行后的参数改为你的设备端将要连接的WiFi的SSID和密码,将第21行后的参数改为你的设备端将要连接的服务端的ip地址和端口号。
m1s_xram_wifi_connect("liuxo_desktop", "12345678");
m1s_xram_wifi_upload_stream("10.42.0.1", 8888);
最后在终端中打开c906_app文件夹(注意不是工程文件夹!),执行以下指令:
export BL_SDK_PATH=$(pwd)/../M1s_BL808_SDK
./build.sh camera_streaming_through_wifi
编译刚刚修改的工程,第一条执行过则不需要。编译完成后会生成build_out文件夹,该文件夹下的d0fw.bin就是我们刚刚修改好的工程程序,保存好该文件,准备后续烧录。
(3)程序固件等烧录
在以下网址中下载名为Bouffalo Lab Dev Cube的程序:
https://dev.bouffalolab.com/download
该程序为博流官方的图形化烧录程序。接下来按照以下网址中“3.1.1. 图形化界面烧录”的步骤进行烧录:
https://wiki.sipeed.com/hardware/zh/maix/m1s/other/start.html
其中firmware和d0fw使用之前编译好的.bin文件。
设备端开发完成。
服务端:服务端代码只需修改设定的ip地址和端口号即可直接运行。下面将介绍代码中每一部分所执行的操作。
参数定义:本部分定义了服务端的ip地址与端口号,以及存储图像文件的临时文件名。前两个参数根据你在主程序文件中设定的值修改。
# 参数定义
bind_ip = "192.168.0.104" # 服务端ip地址
bind_port = 9999 # 服务端端口号
filename = 'frame.jpg' # 保存的一帧图像的文件名
搭建TCP服务端并监听连接:此处使用了python的socket库,使用定义好的ip地址与端口号搭建TCP服务器,并监听设备端连接。一旦接收到设备端的连接请求,程序就会向下运行。
# 运行TCP服务端
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind((bind_ip,bind_port))
server.listen(1)
print("[*] Listening on %s:%d" % (bind_ip,bind_port))
client,addr = server.accept()
print("[*] Accepted connection from: %s:%d" % (addr[0], addr[1]))
数据处理循环:设备端与服务端连接后,首先会发送长度为4字节的图像大小数据,并等待服务端回复后才会发送图像数据。
以此数据传输过程为基础,服务端首先使用socket.recv()方法接收4字节数据,将其转换为int型变量(data_size),该变量值即为将接收的图像数据的大小。回复设备端1个字节的内容后,再次使用recv方法接收图像数据,保存到recv_image变量中。
需要指出的是,设备端发送数据时,同一图像的数据或其大小数据可能被分在两个TCP数据包中发送,使用单独的一次recv方法可能会存在接收不完全、传输未同步(即服务端没有正确捕捉到数据开始与结束的标志位处)的问题,代码中通过两次连续的recv方法解决。
# 读取下一帧图像大小
recv_size = client.recv(4)
if len(recv_size) < 4: # 未完全接收
print('Data size is not completely received!')
recv_append = client.recv(4-len(recv_size))
recv_size = recv_size + recv_append
print('recv_size :',end = ' ')
print(recv_size)
data_size = int.from_bytes(recv_size, "little")
print('data_size =',end = ' ')
print(data_size)
# 回复客户端,令其发送当前帧图像
client.sendall(bytes(' ','UTF-8'))
# 读取图像数据
recv_image = client.recv(data_size)
if len(recv_image) < data_size: # 未完全接收
print('Image data is not completely received!')
recv_append = client.recv(data_size-len(recv_image))
recv_image = recv_image + recv_append
print('recv_image :',end = ' ')
print(recv_image)
接下来将获取的图像保存。
# 保存图像
with open(filename,'wb') as f:
f.write(recv_image)
最后使用cv2库实现图像的读取与显示。
# 显示图像
jpg_image = cv.imread(filename)
cv.imshow('Streaming',jpg_image)
cv.waitKey(1)
所以,本服务端程序能够实时显示摄像机画面是通过连续存储、读取与显示静态图像实现的,因为处理每一帧图像所需的时间极短,所以视觉效果上便是连续的视频流播放。
2.整体实现及效果
烧录固件,运行设备端与服务端程序,效果如下(以下图片从演示视频中截取):
四、遇到的问题
使用官方示例代码时,发现设备可以正常连接WiFi,但始终无法与服务端连接。经交流群友指点,理解了双核工作方式后发现是参数传递上出现了问题。此外,修改后重新编译firmware时,使用的Linux虚拟机经常会闪退,可能是工程过大的原因。限制build.sh中的线程数后,问题得以解决,设备也可与服务端正常连接。
测试服务端的数据处理功能时,最开始往往会在运行一段时间后出错。经串口调试后发现是通信未同步导致的。jpg格式的图片有固定的开始与结束标志,若接收端未实现同步,保存的图像数据就不可能被解码器正确解码。最后通过条件判断与两次连续的recv方法解决。
五、后期计划
本项目显示视频的方式可能并不常用也并不合适,后期会考虑改进显示视频的方式。官方的好像也差不多······
目前本项目只能实时显示摄像头捕捉的画面,没有实现录像功能,将来会考虑在服务端或设备端加入手动录制功能,让用户像使用手机的录像功能一样使用本项目的应用,保存需要的片段。
开发板附带的显示屏没有用到,考虑增加显示驱动将摄像头画面实时显示在显示屏上的功能。
服务端程序没有考虑到设备端主动断开连接的情形,若在服务端进行数据处理时强行断开设备端连接会出错,代码还有不完善的地方(这里官方做得比较完善,可以参考下)。