2023寒假在家练——使用M1s Dock实现网络相机
使用M1s Dock实现网络相机,通过TCP将视频流推送到电脑端进行显示和保存
标签
嵌入式系统
2023寒假在家练
yekai
更新2023-03-28
中国计量大学
333

任务需求及分析

我选择的是任务五:网络相机

具体要求如下:

  • 完成相机驱动,定时拍摄图片,并将图片通过网络传到电脑或服务器,实现长时间拍摄
  • 通过电脑端编程将图片合成为一个视频

Sipeed团队为我们提供了一个通过网络推送摄像头获取到的图片的demo:camera_streaming_through_wifi,在此基础上,我修复了一些bug并做了一些易用性方面的优化,还制作了一个上位机用于接收、显示图片和存储为视频。

硬件介绍

本次使用的开发板为来自Sipeed的M1s Dock开发板,主要包含了一个博流智能的BL808的SoC,附带了一个摄像头和屏幕。关于硬件的介绍具体可参考Sipeed的Wiki:M1s DOCK 开发板 - Sipeed Wiki

软件流程图

FltTmuc7uqvpJbRw0sg--xG0sDLY

推流交互细节

Fl-Rj4ZI-838eeBh68fiTDJXExUy

嵌入式部分代码

由于主要的连接WiFi和推流部分代码已由Sipeed提供并运行在E907核中,我们的主要代码只需要给E907核传参即可。不过在此之前,需要修复Sipeed遗留在SDK中的问题并重新编译刷入固件(现在应该已经修复了这个问题)。重新给E907刷入固件的教程可参考Sipeed提供的仓库sipeed/M1s_BL808_example: M1s_BL808_example (github.com)中的说明Flash Guide - 4.Download e907 firmware

diff --git a/components/sipeed/e907/m1s_e907_xram/src/m1s_e907_xram_wifi.c b/components/sipeed/e907/m1s_e907_xram/src/m1s_e907_xram_wifi.c
index 36ea2f8..2bad7dc 100644
--- a/components/sipeed/e907/m1s_e907_xram/src/m1s_e907_xram_wifi.c
+++ b/components/sipeed/e907/m1s_e907_xram/src/m1s_e907_xram_wifi.c
@@ -216,8 +216,8 @@ _retry:
         }

         client_addr.sin_family = AF_INET;
-        client_addr.sin_port = htons(8888);
-        client_addr.sin_addr.s_addr = inet_addr("10.42.0.1");
+        client_addr.sin_port = htons(private.port);
+        client_addr.sin_addr.s_addr = inet_addr(private.ip);
         memset(&(client_addr.sin_zero), 0, sizeof(client_addr.sin_zero));

程序主体参考camera_streaming_through_wifi例程即可。为了能方便地修改Wifi信息及服务端信息,我添加了从Flash中读取文件并解析这些信息。

读取文件部分代码

// get wifi ssid and password from flash(udisk in PC)

    fatfs_register();

    int fd = -1;
    char *file_name = "/flash/wifi.json";
    if (0 > (fd = aos_open(file_name, 0))) {
        printf("[failed] open file\r\n");
        return;
    }

    int file_length = aos_lseek(fd, 0, SEEK_END);
    if (0 >= file_length) {
        aos_close(fd);
        printf("[failed] the length of file is less then zero\r\n");
        return;
    }

    uint8_t *file_buf = NULL;
    if (NULL == (file_buf = malloc(file_length + 1))) {
        aos_close(fd);
        printf("[failed] malloc buffer to read file\r\n");
        return;
    }
    memset(file_buf, 0, file_length + 1); // make sure the last byte is '\0'

    aos_lseek(fd, 0, SEEK_SET);
    if (file_length != aos_read(fd, file_buf, file_length)) {
        aos_close(fd);
        printf("[failed] err occur while reading file\r\n");
        return;
    }
    aos_close(fd);

    printf("read from file:%s\r\n", file_buf);

解析部分使用了cJSON库,需要把cJSON仓库加入编译

修改文件夹内的bouffalo.mk

COMPONENT_ADD_INCLUDEDIRS += . \
							 cJSON

COMPONENT_SRCS := 

COMPONENT_OBJS := $(patsubst %.c,%.o, $(COMPONENT_SRCS))

COMPONENT_SRCDIRS := . \
					 cJSON
 

解析代码具体如下

// get the wifi info from json file
    cJSON* cjson_root = NULL;
    cJSON* cjson_ssid = NULL;
    cjson_root = cJSON_Parse((const char *)file_buf);
    if (NULL == cjson_root) {
        printf("[failed] parse json file\r\n");
        return;
    }
    cjson_ssid = cJSON_GetObjectItem(cjson_root, "ssid");
    if (NULL == cjson_ssid) {
        printf("[failed] get ssid from json file\r\n");
        return;
    }
    char* ssid = cjson_ssid->valuestring;
// 解析ssid ip port信息类似,省略

之后就是根据读出的信息连接WiFi并推流

    m1s_xram_wifi_init();
    m1s_xram_wifi_connect(ssid, password);
    m1s_xram_wifi_upload_stream(ip, port); 

为了避免内存泄漏问题,最后加上

    cJSON_Delete(cjson_root); 

之后在M1s Dock插上电脑虚拟出的U盘中新建文件wifi.json并填入信息就会让BL808连接对应WiFi并对指定地址和端口推流了。

{
    "ssid":"<your wifi ssid>",
    "password":"<your wifi password>",
    "ip": "<your server ip>",
    "port": <your server port>
}

Fqo7wNyv3Bc4xDljL7RSQVgPfoBA

PC端代码

PC部分主要分为接收和显示存储

接收部分主要面向socket套接字编程,根据前文提及的协议接收,并把图片丢到队列中

def handle_tcp(sock, addr):

    global img_queue

    print(f"new client: {addr}")
    length = 0
    buffer = b""
    while True:
        data = sock.recv(1500)
        if not data:
            break
        # print(f"recv: {[hex(int(each)) for each in data]}")
        # 第一次接收,单独4byte
        if length == 0:
            length = data[-4] + (data[-3] << 8) + \
                (data[-2] << 16) + (data[-1] << 24)
            # print(f"length: {length}")
            sock.send(b"1")
            continue
        # 后面的接收,
        # 根据0xff 0xd8和0xff 0xd9判断开始和结束
        # 为了避免末尾小包,先拼包
        buffer += data
        if data[0] == 0xff and data[1] == 0xd8:
            # print("frame start")
            buffer = data
        elif buffer[-6] == 0xff and buffer[-5] == 0xd9:
            # print("frame end")
            # print(f"recv frame length: {len(buffer) - 4}")
            length = buffer[-4] + (buffer[-3] << 8) + \
                (buffer[-2] << 16) + (buffer[-1] << 24)
            # print(f"next frame length: {length}")
            buffer = buffer[:-4]
            img_queue.put(buffer)
            sock.send(b"1")

def server_thread():
    print("start server")
    s = socket.socket()
    s.bind(('', 8888))
    s.listen()
    # 等待接收一个client,为这个client开一个线程接收
    while True:
        print("wait for client")
        sock, addr = s.accept()
        t = threading.Thread(target=handle_tcp, args=(sock, addr))
        t.setDaemon(True)
        t.start()
        time.sleep(0.1)

显示和存储部分主要使用opencv的api,由于opencv不自带编解码器,需要使用思科开源的openh264-1.8.0.dll动态库来进行编码,放置于python文件同个文件夹即可(暂未测试除win外的平台)。

def ui_thread():

    global img_queue
    global video

    print("start ui")
    img = None

    filename = time.strftime("%Y-%m-%d_%H-%M-%S", time.localtime())

    while True:
        try:
            img1 = img_queue.get(timeout=0.05)
            img1 = np.frombuffer(img1, np.uint8)
            img = cv.imdecode(img1, cv.IMREAD_ANYCOLOR)
            if video is None:
                (height, width, _) = img.shape
                print(f"video width: {width}, height: {height}")
                os.path.exists("video") or os.mkdir("video")
                video = cv.VideoWriter(
                    f'video/{filename}.mp4',
                    cv.VideoWriter_fourcc(*'avc1'),
                    20.0,
                    (width, height)
                )
                print(f"start record video: {filename}.mp4")
        except:
            if img is None:
                continue
        cv.imshow("frame", img)
        video_mutex.acquire()
        video.write(img)
        video_mutex.release()
        if cv.waitKey(1) & 0xFF == ord('q'):
            break
    
    print("quit from ui")
    signal.raise_signal(signal.SIGABRT)

需要注意的是需要在结束录制时调用video.release(),否则会导致文件不完整打不开的问题。

并且,python使用threading库会导致Ctrl+C的响应问题,我们需要重写系统信号的接收。

def save_video():
    global video
    if video is not None:
        video_mutex.acquire()
        video.release()
        video_mutex.release()
    else:
        print("video is None")

if __name__ == '__main__':

    def quit(signum, frame):
        save_video()
        print("quit")
        sys.exit()

    signal.signal(signal.SIGINT, quit)
    signal.signal(signal.SIGTERM, quit)
    signal.signal(signal.SIGABRT, quit)

    t = threading.Thread(target=server_thread, args=())
    t.setDaemon(True)
    t.start()

    t = threading.Thread(target=ui_thread, args=())
    t.setDaemon(True)
    t.start()

    while True:
        try:
            time.sleep(0.1)
        except:
            break

实现的效果

推流

FuB_bz-53uWS8twBYuv5egUwqbcy

存储

FieveSmjfixAJAYaVBqrF2bl7jZ_

FpWEIlVZ-IfGF1LT3pUAAxX06x20

遇到的问题及解决方案

  • Server等不到Client的连接:检查防火墙,一般来说,将网络改为专用网络而不是公用网络即可。

Fohec_OxosoyjaOuspEBvFa33Fo4

  • opencv录制使用h264编码库openh264版本问题:需要使用1.8.0而不是2.x。
  • opencv录制后不存在文件问题:需要首先建立文件夹,而不会自动建。
  • opencv录制文件打开问题:检查img和opencv设置的视频帧的大小。需要注意的是,视频录制传入的参的宽高于img的shape顺序相反。
  • python多线程不响应Ctrl+C问题:需要设置线程为守护t.setDaemon(True),重定向signal,并且主进程不能阻塞(如等待新client连接等)

 

附件下载
src_bl808_c906.zip
bl808 c906核代码
src_host.zip
上位机代码
团队介绍
已经大四啦
团队成员
yekai
一个大三的电子小白
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2023 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号