项目实战:基于ELFboard的远程监测系统

一、项目介绍

1.1 项目目标

基于imx6ull硬件平台,构建一个功能强大的远程检测系统。系统能够自动采集各种传感器数据,包括温度、湿度、电压等,并实时上传至云端服务器,并且能够根据采集到的传感器数据对设备进行自动化控制,如设置电压阈值,当采集到的电压大于阈值时,开启LED1。

在用户端,实现对采集到的传感器数据进行处理、分析和可视化,便于用户远程监控和管理,还可以实现对设备的远程控制。集成高清摄像头,将采集到的视频数据传输至客户端,实现对设备的远程实时监控。

1.2 项目硬件

  • ElfBoard ELF1 开发板
  • wifi网络
  • usb免驱摄像头
  • linux服务器

1.3 软件环境

  • 阿里云物联网平台
  • nginx
  • python
  • flask

二、项目方案

2.1 远程监控

采用rtmp协议,设备端使用ffmpeg采集摄像头数据并推流至云端,云端使用nginx提供web服务,并使用nginx-http-flv-module提供rtmp服务,用户端采用web界面,并使用flv.js进行拉流播放。

2.2 数据检测与设备控制

传感器数据传输以及设备的远程控制通过阿里云物联网平台,采用MQTT协议。

三、数据检测与设备控制

3.1 MQTT云平台配置

参考 ELFboard学习(九):MQTT

3.2 传感器数据采集与上传

基于linux sdk中的data_model_basic_demo.c进行修改。

3.2.1 温湿度数据采集

#define AHT20_DEV "/dev/aht20"
int get_aht20(float* ath20_data)
{
        int fd;
        unsigned int databuf[2];
        int c1,t1; 
        float hum,temp;
        int ret = 0;
 
        fd = open(AHT20_DEV, O_RDWR);
        if(fd < 0) {
                printf("can't open file %s\r\n", AHT20_DEV);
                return -1;
        }
 
        ret = read(fd, databuf, sizeof(databuf));
        if(ret == 0) {                        
            c1 = databuf[0]*1000/1024/1024;  
            t1 = databuf[1] *200*10/1024/1024-500;
            hum = (float)c1/10.0;
            temp = (float)t1/10.0;

            printf("hum = %0.2f temp = %0.2f \r\n",hum,temp);
        *ath20_data = hum;
        *(ath20_data+1) = temp;
        }

        close(fd);
    return 0;
}

3.2.2 电压数据采集

#define voltage5_raw "/sys/bus/iio/devices/iio:device0/in_voltage5_raw"
#define voltage_scale "/sys/bus/iio/devices/iio:device0/in_voltage_scale"
float get_adc(void)
{
        int raw_fd, scale_fd;
        char buff[20];
        int raw;
        double scale;

        /* 1.打开文件 */
        raw_fd = open(voltage5_raw, O_RDONLY);
        if(raw_fd < 0){
                printf("open raw_fd failed!\n");
                return -1;
        }
        scale_fd = open(voltage_scale, O_RDONLY);
        if(scale_fd < 0){
                printf("open scale_fd failed!\n");
                return -1;
        }

        /* 2.读取文件 */
        // rewind(raw_fd);   // 将光标移回文件开头
        read(raw_fd, buff, sizeof(buff));
        raw = atoi(buff);
        memset(buff, 0, sizeof(buff));
        // rewind(scale_fd);   // 将光标移回文件开头
        read(scale_fd, buff, sizeof(buff));
        scale = atof(buff);
        printf("ADC原始值:%d,电压值:%.3fV\r\n", raw, raw * scale / 1000.f);
        close(raw_fd);
        close(scale_fd);
        return raw * scale / 1000.f;
}

3.2.3 LED状态采集与控制

#define LED1_BRIGHTNESS "/sys/class/leds/led1/brightness"
#define LED2_BRIGHTNESS "/sys/class/leds/led2/brightness"
int get_led(int led_sel)
{
    int led;
    char buff[20];
    int state=0;
    if(led_sel == 2)
    {
        led=open(LED2_BRIGHTNESS, O_RDWR);
    }else{
        led=open(LED1_BRIGHTNESS, O_RDWR);
    }
    if(led<0)
    {
        perror("open device led error");
        exit(1);
    }

        read(led, buff, sizeof(buff));
        state = atoi(buff);

    close(led);
    return state;
}

void set_led(int led_sel, char state)
{
    int led;
    if(led_sel == 2)
    {
        led=open(LED2_BRIGHTNESS, O_RDWR);
    }else{
        led=open(LED1_BRIGHTNESS, O_RDWR);
    }
    if(led<0)
    {
        perror("open device led error");
        exit(1);
    }

    write(led, &state, 1);//0->48,1->49
    close(led);
}

3.2.4 自动化控制

当adc采集的电压大于阈值2.5V时自动开启LED1,低于时自动关闭LED1。

        if(adc>2.5){
            set_led(1,'1');
        }else{
            set_led(1,'0');
        }

3.3 数据上传

在main函数的while(1)中

        adc=get_adc();
        get_aht20(ath20_data);
        led1_state = get_led(1);
        led2_state = get_led(2)>0?1:0;

        demo_send_property_post(dm_handle, "{\"temperature\": 21.1}");
        sprintf(data_str,"{\"Voltage\": %.3f}", adc);
        demo_send_property_post(dm_handle, data_str);

        memset(data_str, 0, sizeof(data_str));
        sprintf(data_str,"{\"Humidity\": %.3f}", ath20_data[0]);
        demo_send_property_post(dm_handle, data_str);

        memset(data_str, 0, sizeof(data_str));
        sprintf(data_str,"{\"temperature\": %.3f}", ath20_data[1]);
        demo_send_property_post(dm_handle, data_str);

        memset(data_str, 0, sizeof(data_str));
        sprintf(data_str,"{\"LEDSwitch\": %d}", led1_state);
        demo_send_property_post(dm_handle, data_str);

        memset(data_str, 0, sizeof(data_str));
        sprintf(data_str,"{\"LEDSwitch2\": %d}", led2_state);
        demo_send_property_post(dm_handle, data_str);

3.4 云端指令响应

由于云端传输的数据为json格式,因此需要使用cjson进行解析。

3.4.1 添加cJson

在components文件夹下添加cJson相关文件
在这里插入图片描述

3.4.2 修改Makefile

在这里插入图片描述

在74行和78行后面要添加-lm,否则在编译的时候会报错。

3.4.3 实现代码

static void demo_dm_recv_property_set(void *dm_handle, const aiot_dm_recv_t *recv, void *userdata)
{
    int led;
    char state=0;
    printf("demo_dm_recv_property_set msg_id = %ld, params = %.*s\r\n",
           (unsigned long)recv->data.property_set.msg_id,
           recv->data.property_set.params_len,
           recv->data.property_set.params);

    /* TODO: 以下代码演示如何对来自云平台的属性设置指令进行应答, 用户可取消注释查看演示效果 */
    cJSON* cjson_result = NULL;
    cJSON* cjson_set1 = NULL;
    cJSON* cjson_set2 = NULL;

    cjson_result = cJSON_Parse(recv->data.property_set.params);
    if(cjson_result == NULL)
    {
        printf("parse fail.\n");
        return;
    }
    //{"LEDSwitch":0}
        cjson_set1 = cJSON_GetObjectItem(cjson_result,"LEDSwitch");
    if(cjson_set1)
    {
        printf("LED1 set %d\n",cjson_set1->valueint);
        state = cjson_set1->valueint+48;
        
        led=open(LED1_BRIGHTNESS, O_WRONLY);
        if(led<0)
        {
            perror("open device led1");
            exit(1);
        }
        write(led, &state, 1);//0->48,1->49
        close(led);
    }
    
    cjson_set2 = cJSON_GetObjectItem(cjson_result,"LEDSwitch2");
    if(cjson_set2){
        printf("LED2 set %d\n",cjson_set2->valueint);
        state = cjson_set2->valueint+48;

        led=open(LED2_BRIGHTNESS, O_WRONLY);
        if(led<0)
        {
            perror("open device led1");
            exit(1);
        }
        write(led, &state, 1);//0->48,1->49
        close(led);   
    }
        
        //释放内存
        cJSON_Delete(cjson_result);

    {
        aiot_dm_msg_t msg;

        memset(&msg, 0, sizeof(aiot_dm_msg_t));
        msg.type = AIOT_DMMSG_PROPERTY_SET_REPLY;
        msg.data.property_set_reply.msg_id = recv->data.property_set.msg_id;
        msg.data.property_set_reply.code = 200;
        msg.data.property_set_reply.data = "{}";
        int32_t res = aiot_dm_send(dm_handle, &msg);
        if (res < 0) {
            printf("aiot_dm_send failed\r\n");
        }
    }
    
}

四、视频监控

4.1 rtmp服务器搭建

云端服务器使用nginx,但nginx本身并不支持rtmp,需要使用相关的插件使其支持rtmp。此外由于网页端播放rtmp流需要flash插件的支持,而目前flash插件许多浏览器已不再支持,因此需要使用支持 HTTPS-FLV的nginx-http-flv-module,并通过flv.js实现rtmp流的播放。

这里首先需要下载nginx和nginx-http-flv-module的源码,并采用编译的方式安装nginx,具体步骤如下:

./configure --add-module=/usr/local/nginx/nginx-http-flv-module
make&&make install

安装完成后,需要进入nginx安装目录(默认为/usr/local/nginx/),并在conf文件夹下对nginx.conf文件进行修改,增加rtmp功能(注意需要打开服务器的1935端口):

worker_processes  1;
#worker_processes  auto;

#worker_cpu_affinity  0001 0010 0100 1000;
#worker_cpu_affinity  auto;

error_log logs/error.log error;

events {
    worker_connections  4096;
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    keepalive_timeout  65;

    server {
        listen       80;

        location / {
            root   html;
            index  index.html;
        }

        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }

        location /live {
            flv_live on; #打开 HTTP 播放 FLV 直播流功能
            chunked_transfer_encoding on; #支持 'Transfer-Encoding: chunked' 方式回复

            add_header 'Access-Control-Allow-Origin' '*'; #添加额外的 HTTP 头
            add_header 'Access-Control-Allow-Credentials' 'true'; #添加额外的 HTTP 头
        }

        location /hls {
            types {
                application/vnd.apple.mpegurl m3u8;
                video/mp2t ts;
            }

            root /tmp;
            add_header 'Cache-Control' 'no-cache';
        }

        location /dash {
            root /tmp;
            add_header 'Cache-Control' 'no-cache';
        }

        location /stat {
            rtmp_stat all;
            rtmp_stat_stylesheet stat.xsl;
        }

        location /stat.xsl {
            root /var/www/rtmp;
        }

        location /control {
            rtmp_control all;
        }
    }
}

rtmp_auto_push on;
rtmp_auto_push_reconnect 1s;
rtmp_socket_dir /tmp;

rtmp {
    out_queue           4096;
    out_cork            8;
    max_streams         128;
    timeout             1s;
    drop_idle_publisher 1s;

    log_interval 5s;
    log_size     1m;

    server {
        listen 1935;
        server_name xxx.xxx.xx; #填入你自己的域名

        application myapp {
            live on;
            gop_cache on;
        }

        application hls {
            live on;
            hls on;
            hls_path /tmp/hls;
        }

        application dash {
            live on;
            dash on;
            dash_path /tmp/dash;
        }
    }
}

最后启动nginx服务,即可完成rtmp服务器的搭建:

cd /usr/local/nginx/sbin
./nginx

4.2 本地推流

ffmpeg的编译配置参考:
ELFboard学习(七):FFmpeg移植
摄像头采用的是usb免驱摄像头,将摄像头插入ELFboard的USB口即可正常识别及工作,设备节点为/dev/video2。
之后可以使用v4l2-ctl工具查看并配置摄像头信息
最后使用命令就能够实现推流:

ffmpeg -f video4linux2 -r 5 -s 320x240 -i /dev/video2 -c:v libx264 -preset ultrafast -tune zerolatency -r 5 -f flv rtmp://xxx.xxxxxx.xxx/live/test

五、用户端设计

5.1 框架

使用python编程,采用web界面,并通过flask提供web服务以及后端数据处理能力。可以部署在云端,也可以在本地运行。界面如下所示:
在这里插入图片描述

5.2视频拉流

web用户端的视频拉流通过flv.js实现,首先需要在html文件中导入flv.js:

<script src="https://cdn.bootcss.com/flv.js/1.5.0/flv.js"></script>

之后设计web页面播放器,具体代码如下:

<div class="row mt-10">
    <div class="col-lg-8 mx-auto">
        <video id="videoElement" class="img-fluid" controls autoplay width="1024" height="576" muted>
            Your browser is too old which doesn't support HTML5 video.
        </video>
    </div>
    <!-- /column -->
</div>
<br>
<div class="d-flex justify-content-center">
    <!--<button οnclick="flv_load()">加载</button>-->
    <button onclick="flv_start()">开始</button>
    <button onclick="flv_pause()">停止</button>
</div>
<script type="text/javascript">
    var player = document.getElementById('videoElement');
    if (flvjs.isSupported()) {
        var flvPlayer = flvjs.createPlayer({
            type: 'flv',
            url: 'http://xxx.xxxxx.xx/live?port=1935&app=myapp&stream=test',
            "isLive": true,
            hasAudio: false,
            hasVideo: true,
            //withCredentials: false,
            //cors: true
        }, {
            enableWorker: true,
            enableStashBuffer: false,
            lazyLoad: false,
            lazyLoadMaxDuration: 0,
            lazyLoadRecoverDuration: 0,
            deferLoadAfterSourceOpen: false,
            fixAudioTimestampGap: true,
            autoCleanupSourceBuffer: true,
        });
        flvPlayer.attachMediaElement(videoElement);
        flvPlayer.load(); //加载
        flv_start();
    }
    function flv_start() {
        player.play();
    }

    function flv_pause() {
        player.pause();
    }
</script>

5.3 远程数据的读取与指令下发

这一部分通过后端python编程实现,并提供相应的web接口。前后端的交互通过ajax请求实现。

class Sample:
    def __init__(self):
        pass

    @staticmethod
    def create_client(
            access_key_id: str,
            access_key_secret: str,
    ) -> OpenApiClient:
        """
        使用AK&SK初始化账号Client
        @param access_key_id:
        @param access_key_secret:
        @return: Client
        @throws Exception
        """
        config = open_api_models.Config(
            # 必填,您的 AccessKey ID,
            access_key_id=access_key_id,
            # 必填,您的 AccessKey Secret,
            access_key_secret=access_key_secret
        )
        # Endpoint 请参考 https://api.aliyun.com/product/Iot
        config.endpoint = f'iot.cn-shanghai.aliyuncs.com'
        return OpenApiClient(config)

    @staticmethod
    def create_set_info() -> open_api_models.Params:
        """
        API 相关
        @param path: params
        @return: OpenApi.Params
        """
        params = open_api_models.Params(
            # 接口名称,
            action='SetDeviceProperty',
            # 接口版本,
            version='2018-01-20',
            # 接口协议,
            protocol='HTTPS',
            # 接口 HTTP 方法,
            method='POST',
            auth_type='AK',
            style='RPC',
            # 接口 PATH,
            pathname=f'/',
            # 接口请求体内容格式,
            req_body_type='formData',
            # 接口响应体内容格式,
            body_type='json'
        )
        return params

    @staticmethod
    def create_get_info() -> open_api_models.Params:
        """
        API 相关
        @param path: params
        @return: OpenApi.Params
        """
        params = open_api_models.Params(
            # 接口名称,
            action='QueryDeviceOriginalPropertyStatus',
            # 接口版本,
            version='2018-01-20',
            # 接口协议,
            protocol='HTTPS',
            # 接口 HTTP 方法,
            method='POST',
            auth_type='AK',
            style='RPC',
            # 接口 PATH,
            pathname=f'/',
            # 接口请求体内容格式,
            req_body_type='formData',
            # 接口响应体内容格式,
            body_type='json'
        )
        return params

    @staticmethod
    def main():
        client = Sample.create_client(access_key_id, access_key_secret)
        params = Sample.create_get_info()
        # query params
        queries = {}
        queries['PageSize'] = 10
        queries['ProductKey'] = 'xxxxxxxxxx'
        queries['DeviceName'] = 'xxxx'
        queries['Asc'] = 0
        # body params
        body = {}
        body['ApiProduct'] = None
        body['ApiRevision'] = None
        # runtime options
        runtime = util_models.RuntimeOptions()
        request = open_api_models.OpenApiRequest(
            query=OpenApiUtilClient.query(queries),
            body=body
        )
        # 复制代码运行请自行打印 API 的返回值
        # 返回值为 Map 类型,可从 Map 中获得三类数据:响应体 body、响应头 headers、HTTP 返回的状态码 statusCode。
        response = client.call_api(params, request, runtime)
        body = response['body']
        Data = body['Data']
        List = Data['List']
        Proper = List['PropertyStatusDataInfo']
        Temp = json.loads(Proper[0]['Value'])
        Volt = json.loads(Proper[1]['Value'])
        Led2 = json.loads(Proper[2]['Value'])
        Led1 = json.loads(Proper[3]['Value'])
        Humi = json.loads(Proper[4]['Value'])
        message = {
            'humi': Humi['data'],
            'temp': Temp['data'],
            'volt': Volt['data'],
            'led1': Led1['data'],
            'led2': Led2['data'],
        }
        return jsonify(message)

    @staticmethod
    def main_set(item: str):
        client = Sample.create_client(access_key_id, access_key_secret)
        params = Sample.create_set_info()
        # query params
        queries = {}
        queries['ProductKey'] = 'xxxxxxxxxxxx'
        queries['DeviceName'] = 'xxxx'
        queries['Items'] = item  # '{"LEDSwitch":0}'
        # body params
        body = {}
        body['ApiProduct'] = None
        body['ApiRevision'] = None
        # runtime options
        runtime = util_models.RuntimeOptions()
        request = open_api_models.OpenApiRequest(
            query=OpenApiUtilClient.query(queries),
            body=body
        )
        # 复制代码运行请自行打印 API 的返回值
        # 返回值为 Map 类型,可从 Map 中获得三类数据:响应体 body、响应头 headers、HTTP 返回的状态码 statusCode。
        resp = client.call_api(params, request, runtime)
        body = resp['body']
        data = body['Success']
        return str(data)

六、演示效果

基于ELFBoard的远程监测系统

代码请在公众号:“通信电子坊” 回复:“ELFboard项目代码”获取

注意:由于网络以及imx6ull性能的原因,客户端的视频流会存在一定的时延

  • 21
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zxfeng~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值