本帖最后由 水精灵 于 2024-8-23 11:41 编辑

以下文章来源于MrFeng的学习笔记 ,作者Mr.Feng。

6-1.png

ElfBoard的“自创一派”共创社由19名来自各大高校的共创官组成,在不到一个月的时间里已经建立起浓厚的学习氛围,在这里每位共创官跨越不同的学科背景,交融思想、共享资源,迅速提升自身在嵌入式技术领域的专业素养。

值得一提的是,社群内部已经涌现出许多富有创意的产品设计理念与技术解决方案,今天就跟大家分享一名共创官完成的项目报告“基于ELF 1的远程监测系统”。

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

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

1.2 项目硬件
1.ElfBoard ELF 1 开发板
2.WiFi(RTL8723DU)
3.USB免驱摄像头
4.Linux服务器

1.3 软件环境
1.阿里云物联网平台
2.Nginx
3.Python
4.Flask

二、项目方案
2.1 远程监控
采用RTMP协议,设备端使用FFmpeg采集摄像头数据并推流至云端,云端使用Nginx提供Web服务,并使用nginx-http-flv-module提供RTMP服务,用户端采用Web界面,并使用flv.js进行拉流播放。

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

三、数据检测与设备控制
MQTT云平台配置
参考 ElfBoard学习(九):MQTT

传感器数据采集与上传
基于Linux SDK中的data_model_basic_demo.c进行修改。

温湿度数据采集
  1. #define AHT20_DEV "/dev/aht20"
  2. int get_aht20(float* ath20_data)
  3. {
  4.         int fd;
  5.         unsigned int databuf[2];
  6.         int c1,t1;
  7.         float hum,temp;
  8.         int ret = 0;

  9.         fd = open(AHT20_DEV, O_RDWR);
  10.         if(fd < 0) {
  11.                 printf("can't open file %s\r\n", AHT20_DEV);
  12.                 return -1;
  13.         }

  14.         ret = read(fd, databuf, sizeof(databuf));
  15.         if(ret == 0) {                        
  16.             c1 = databuf[0]*1000/1024/1024;  
  17.             t1 = databuf[1] *200*10/1024/1024-500;
  18.             hum = (float)c1/10.0;
  19.             temp = (float)t1/10.0;

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

  24.         close(fd);
  25.     return 0;
  26. }
复制代码

电压数据采集
  1. #define voltage5_raw "/sys/bus/iio/devices/iio:device0/in_voltage5_raw"
  2. #define voltage_scale "/sys/bus/iio/devices/iio:device0/in_voltage_scale"
  3. float get_adc(void)
  4. {
  5.         int raw_fd, scale_fd;
  6.         char buff[20];
  7.         int raw;
  8.         double scale;

  9.         /* 1.打开文件 */
  10.         raw_fd = open(voltage5_raw, O_RDONLY);
  11.         if(raw_fd < 0){
  12.                 printf("open raw_fd failed!\n");
  13.                 return -1;
  14.         }
  15.         scale_fd = open(voltage_scale, O_RDONLY);
  16.         if(scale_fd < 0){
  17.                 printf("open scale_fd failed!\n");
  18.                 return -1;
  19.         }

  20.         /* 2.读取文件 */
  21.         // rewind(raw_fd);   // 将光标移回文件开头
  22.         read(raw_fd, buff, sizeof(buff));
  23.         raw = atoi(buff);
  24.         memset(buff, 0, sizeof(buff));
  25.         // rewind(scale_fd);   // 将光标移回文件开头
  26.         read(scale_fd, buff, sizeof(buff));
  27.         scale = atof(buff);
  28.         printf("ADC原始值:%d,电压值:%.3fV\r\n", raw, raw * scale / 1000.f);
  29.         close(raw_fd);
  30.         close(scale_fd);
  31.         return raw * scale / 1000.f;
  32. }
复制代码


LED状态采集与控制
  1. #define LED1_BRIGHTNESS "/sys/class/leds/led1/brightness"
  2. #define LED2_BRIGHTNESS "/sys/class/leds/led2/brightness"
  3. int get_led(int led_sel)
  4. {
  5.     int led;
  6.     char buff[20];
  7.     int state=0;
  8.     if(led_sel == 2)
  9.     {
  10.         led=open(LED2_BRIGHTNESS, O_RDWR);
  11.     }else{
  12.         led=open(LED1_BRIGHTNESS, O_RDWR);
  13.     }
  14.     if(led<0)
  15.     {
  16.         perror("open device led error");
  17.         exit(1);
  18.     }

  19.         read(led, buff, sizeof(buff));
  20.         state = atoi(buff);

  21.     close(led);
  22.     return state;
  23. }

  24. void set_led(int led_sel, char state)
  25. {
  26.     int led;
  27.     if(led_sel == 2)
  28.     {
  29.         led=open(LED2_BRIGHTNESS, O_RDWR);
  30.     }else{
  31.         led=open(LED1_BRIGHTNESS, O_RDWR);
  32.     }
  33.     if(led<0)
  34.     {
  35.         perror("open device led error");
  36.         exit(1);
  37.     }

  38.     write(led, &state, 1);//0->48,1->49
  39.     close(led);
  40. }
复制代码

自动化控制
当ADC采集的电压大于阈值2.5V时自动开启LED1,低于时自动关闭LED1。
  1.         if(adc>2.5){
  2.             set_led(1,'1');
  3.         }else{
  4.             set_led(1,'0');
  5.         }
复制代码

数据上传
在main函数的while(1)中
  1.         adc=get_adc();
  2.         get_aht20(ath20_data);
  3.         led1_state = get_led(1);
  4.         led2_state = get_led(2)>0?1:0;

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

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

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

  14.         memset(data_str, 0, sizeof(data_str));
  15.         sprintf(data_str,"{"LEDSwitch": %d}", led1_state);
  16.         demo_send_property_post(dm_handle, data_str);

  17.         memset(data_str, 0, sizeof(data_str));
  18.         sprintf(data_str,"{"LEDSwitch2": %d}", led2_state);
  19.         demo_send_property_post(dm_handle, data_str);
复制代码

云端指令响应
由于云端传输的数据为JSON格式,因此需要使用cJSON进行解析。

添加cJSON
在components文件夹下添加cJSON相关文件
6-2.png


修改Makefile
6-3.png
在74行和78行后面要添加-lm,否则在编译的时候会报错。

实现代码
  1. static void demo_dm_recv_property_set(void *dm_handle, const aiot_dm_recv_t *recv, void *userdata)
  2. {
  3.     int led;
  4.     char state=0;
  5.     printf("demo_dm_recv_property_set msg_id = %ld, params = %.*s\r\n",
  6.            (unsigned long)recv->data.property_set.msg_id,
  7.            recv->data.property_set.params_len,
  8.            recv->data.property_set.params);

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

  13.     cjson_result = cJSON_Parse(recv->data.property_set.params);
  14.     if(cjson_result == NULL)
  15.     {
  16.         printf("parse fail.\n");
  17.         return;
  18.     }
  19.     //{"LEDSwitch":0}
  20.         cjson_set1 = cJSON_GetObjectItem(cjson_result,"LEDSwitch");
  21.     if(cjson_set1)
  22.     {
  23.         printf("LED1 set %d\n",cjson_set1->valueint);
  24.         state = cjson_set1->valueint+48;
  25.         
  26.         led=open(LED1_BRIGHTNESS, O_WRONLY);
  27.         if(led<0)
  28.         {
  29.             perror("open device led1");
  30.             exit(1);
  31.         }
  32.         write(led, &state, 1);//0->48,1->49
  33.         close(led);
  34.     }
  35.    
  36.     cjson_set2 = cJSON_GetObjectItem(cjson_result,"LEDSwitch2");
  37.     if(cjson_set2){
  38.         printf("LED2 set %d\n",cjson_set2->valueint);
  39.         state = cjson_set2->valueint+48;

  40.         led=open(LED2_BRIGHTNESS, O_WRONLY);
  41.         if(led<0)
  42.         {
  43.             perror("open device led1");
  44.             exit(1);
  45.         }
  46.         write(led, &state, 1);//0->48,1->49
  47.         close(led);   
  48.     }
  49.         
  50.         //释放内存
  51.         cJSON_Delete(cjson_result);

  52.     {
  53.         aiot_dm_msg_t msg;

  54.         memset(&msg, 0, sizeof(aiot_dm_msg_t));
  55.         msg.type = AIOT_DMMSG_PROPERTY_SET_REPLY;
  56.         msg.data.property_set_reply.msg_id = recv->data.property_set.msg_id;
  57.         msg.data.property_set_reply.code = 200;
  58.         msg.data.property_set_reply.data = "{}";
  59.         int32_t res = aiot_dm_send(dm_handle, &msg);
  60.         if (res < 0) {
  61.             printf("aiot_dm_send failed\r\n");
  62.         }
  63.     }
  64.    
  65. }
复制代码

四、视频监控
RTMP服务器搭建
云端服务器使用Nginx,但Nginx本身并不支持RTMP,需要使用相关的插件使其支持RTMP。此外由于网页端播放RTMP流需要Flash插件的支持,而目前Flash插件许多浏览器已不再支持,因此需要使用支持 HTTPS-FLV的nginx-http-flv-module,并通过flv.js实现RTMP流的播放。
这里首先需要下载Nginx和nginx-http-flv-module的源码,并采用编译的方式安装Nginx,具体步骤如下:
  1. ./configure --add-module=/usr/local/nginx/nginx-http-flv-module
  2. make&&make install
复制代码

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

  3. #worker_cpu_affinity  0001 0010 0100 1000;
  4. #worker_cpu_affinity  auto;

  5. error_log logs/error.log error;

  6. events {
  7.     worker_connections  4096;
  8. }

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

  12.     keepalive_timeout  65;

  13.     server {
  14.         listen       80;

  15.         location / {
  16.             root   html;
  17.             index  index.html;
  18.         }

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

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

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

  29.         location /hls {
  30.             types {
  31.                 application/vnd.apple.mpegurl m3u8;
  32.                 video/mp2t ts;
  33.             }

  34.             root /tmp;
  35.             add_header 'Cache-Control' 'no-cache';
  36.         }

  37.         location /dash {
  38.             root /tmp;
  39.             add_header 'Cache-Control' 'no-cache';
  40.         }

  41.         location /stat {
  42.             rtmp_stat all;
  43.             rtmp_stat_stylesheet stat.xsl;
  44.         }

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

  48.         location /control {
  49.             rtmp_control all;
  50.         }
  51.     }
  52. }

  53. rtmp_auto_push on;
  54. rtmp_auto_push_reconnect 1s;
  55. rtmp_socket_dir /tmp;

  56. rtmp {
  57.     out_queue           4096;
  58.     out_cork            8;
  59.     max_streams         128;
  60.     timeout             1s;
  61.     drop_idle_publisher 1s;

  62.     log_interval 5s;
  63.     log_size     1m;

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

  67.         application myapp {
  68.             live on;
  69.             gop_cache on;
  70.         }

  71.         application hls {
  72.             live on;
  73.             hls on;
  74.             hls_path /tmp/hls;
  75.         }

  76.         application dash {
  77.             live on;
  78.             dash on;
  79.             dash_path /tmp/dash;
  80.         }
  81.     }
  82. }
复制代码

最后启动Nginx服务,即可完成RTMP服务器的搭建:
  1. cd /usr/local/nginx/sbin
  2. ./nginx
复制代码

本地推流
FFmpeg的编译配置参考:
摄像头采用的是USB免驱摄像头,将摄像头插入ElfBoard的USB口即可正常识别及工作,设备节点为/dev/video2。
之后可以使用v4l2-ctl工具查看并配置摄像头信息。
最后使用命令就能够实现推流:
  1. 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
复制代码

五、用户端设计
框架
使用Python编程,采用Web界面,并通过Flask提供Web服务以及后端数据处理能力。可以部署在云端,也可以在本地运行。界面如下所示:
6-4.png

视频拉流
Web用户端的视频拉流通过flv.js实现,首先需要在html文件中导入flv.js:
  1. <script src="https://cdn.bootcss.com/flv.js/1.5.0/flv.js"></script>
复制代码

之后设计Web页面播放器,具体代码如下:
  1. <div class="row mt-10">
  2.     <div class="col-lg-8 mx-auto">
  3.         <video id="videoElement" class="img-fluid" controls autoplay width="1024" height="576" muted>
  4.             Your browser is too old which doesn't support HTML5 video.
  5.         </video>
  6.     </div>
  7.     <!-- /column -->
  8. </div>
  9. <br>
  10. <div class="d-flex justify-content-center">
  11.     <!--<button οnclick="flv_load()">加载</button>-->
  12.     <button onclick="flv_start()">开始</button>
  13.     <button onclick="flv_pause()">停止</button>
  14. </div>
复制代码

  1. <script type="text/javascript">
  2.     var player = document.getElementById('videoElement');
  3.     if (flvjs.isSupported()) {
  4.         var flvPlayer = flvjs.createPlayer({
  5.             type: 'flv',
  6.             url: 'http://xxx.xxxxx.xx/live?port=1935&app=myapp&stream=test',
  7.             "isLive": true,
  8.             hasAudio: false,
  9.             hasVideo: true,
  10.             //withCredentials: false,
  11.             //cors: true
  12.         }, {
  13.             enableWorker: true,
  14.             enableStashBuffer: false,
  15.             lazyLoad: false,
  16.             lazyLoadMaxDuration: 0,
  17.             lazyLoadRecoverDuration: 0,
  18.             deferLoadAfterSourceOpen: false,
  19.             fixAudioTimestampGap: true,
  20.             autoCleanupSourceBuffer: true,
  21.         });
  22.         flvPlayer.attachMediaElement(videoElement);
  23.         flvPlayer.load(); //加载
  24.         flv_start();
  25.     }
  26.     function flv_start() {
  27.         player.play();
  28.     }

  29.     function flv_pause() {
  30.         player.pause();
  31.     }
  32. </script>
复制代码

远程数据的读取与指令下发
这一部分通过后端Python编程实现,并提供相应的Web接口。前后端的交互通过ajax请求实现。
  1. class Sample:
  2.     def __init__(self):
  3.         pass

  4.     @staticmethod
  5.     def create_client(
  6.             access_key_id: str,
  7.             access_key_secret: str,
  8.     ) -> OpenApiClient:
  9.         """
  10.         使用AK&SK初始化账号Client
  11.         @param access_key_id:
  12.         @param access_key_secret:
  13.         @return: Client
  14.         @throws Exception
  15.         """
  16.         config = open_api_models.Config(
  17.             # 必填,您的 AccessKey ID,
  18.             access_key_id=access_key_id,
  19.             # 必填,您的 AccessKey Secret,
  20.             access_key_secret=access_key_secret
  21.         )
  22.         # Endpoint 请参考 https://api.aliyun.com/product/Iot
  23.         config.endpoint = f'iot.cn-shanghai.aliyuncs.com'
  24.         return OpenApiClient(config)

  25.     @staticmethod
  26.     def create_set_info() -> open_api_models.Params:
  27.         """
  28.         API 相关
  29.         @param path: params
  30.         @return: OpenApi.Params
  31.         """
  32.         params = open_api_models.Params(
  33.             # 接口名称,
  34.             action='SetDeviceProperty',
  35.             # 接口版本,
  36.             version='2018-01-20',
  37.             # 接口协议,
  38.             protocol='HTTPS',
  39.             # 接口 HTTP 方法,
  40.             method='POST',
  41.             auth_type='AK',
  42.             style='RPC',
  43.             # 接口 PATH,
  44.             pathname=f'/',
  45.             # 接口请求体内容格式,
  46.             req_body_type='formData',
  47.             # 接口响应体内容格式,
  48.             body_type='json'
  49.         )
  50.         return params

  51.     @staticmethod
  52.     def create_get_info() -> open_api_models.Params:
  53.         """
  54.         API 相关
  55.         @param path: params
  56.         @return: OpenApi.Params
  57.         """
  58.         params = open_api_models.Params(
  59.             # 接口名称,
  60.             action='QueryDeviceOriginalPropertyStatus',
  61.             # 接口版本,
  62.             version='2018-01-20',
  63.             # 接口协议,
  64.             protocol='HTTPS',
  65.             # 接口 HTTP 方法,
  66.             method='POST',
  67.             auth_type='AK',
  68.             style='RPC',
  69.             # 接口 PATH,
  70.             pathname=f'/',
  71.             # 接口请求体内容格式,
  72.             req_body_type='formData',
  73.             # 接口响应体内容格式,
  74.             body_type='json'
  75.         )
  76.         return params

  77.     @staticmethod
  78.     def main():
  79.         client = Sample.create_client(access_key_id, access_key_secret)
  80.         params = Sample.create_get_info()
  81.         # query params
  82.         queries = {}
  83.         queries['PageSize'] = 10
  84.         queries['ProductKey'] = 'xxxxxxxxxx'
  85.         queries['DeviceName'] = 'xxxx'
  86.         queries['Asc'] = 0
  87.         # body params
  88.         body = {}
  89.         body['ApiProduct'] = None
  90.         body['ApiRevision'] = None
  91.         # runtime options
  92.         runtime = util_models.RuntimeOptions()
  93.         request = open_api_models.OpenApiRequest(
  94.             query=OpenApiUtilClient.query(queries),
  95.             body=body
  96.         )
  97.         # 复制代码运行请自行打印 API 的返回值
  98.         # 返回值为 Map 类型,可从 Map 中获得三类数据:响应体 body、响应头 headers、HTTP 返回的状态码 statusCode。
  99.         response = client.call_api(params, request, runtime)
  100.         body = response['body']
  101.         Data = body['Data']
  102.         List = Data['List']
  103.         Proper = List['PropertyStatusDataInfo']
  104.         Temp = json.loads(Proper[0]['Value'])
  105.         Volt = json.loads(Proper[1]['Value'])
  106.         Led2 = json.loads(Proper[2]['Value'])
  107.         Led1 = json.loads(Proper[3]['Value'])
  108.         Humi = json.loads(Proper[4]['Value'])
  109.         message = {
  110.             'humi': Humi['data'],
  111.             'temp': Temp['data'],
  112.             'volt': Volt['data'],
  113.             'led1': Led1['data'],
  114.             'led2': Led2['data'],
  115.         }
  116.         return jsonify(message)

  117.     @staticmethod
  118.     def main_set(item: str):
  119.         client = Sample.create_client(access_key_id, access_key_secret)
  120.         params = Sample.create_set_info()
  121.         # query params
  122.         queries = {}
  123.         queries['ProductKey'] = 'xxxxxxxxxxxx'
  124.         queries['DeviceName'] = 'xxxx'
  125.         queries['Items'] = item  # '{"LEDSwitch":0}'
  126.         # body params
  127.         body = {}
  128.         body['ApiProduct'] = None
  129.         body['ApiRevision'] = None
  130.         # runtime options
  131.         runtime = util_models.RuntimeOptions()
  132.         request = open_api_models.OpenApiRequest(
  133.             query=OpenApiUtilClient.query(queries),
  134.             body=body
  135.         )
  136.         # 复制代码运行请自行打印 API 的返回值
  137.         # 返回值为 Map 类型,可从 Map 中获得三类数据:响应体 body、响应头 headers、HTTP 返回的状态码 statusCode。
  138.         resp = client.call_api(params, request, runtime)
  139.         body = resp['body']
  140.         data = body['Success']
  141.         return str(data)
复制代码

演示效果
    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    Powered by Discuz! X3.5  © 2001-2013 Comsenz Inc.