本帖最后由 水精灵 于 2024-8-23 11:41 编辑
以下文章来源于MrFeng的学习笔记 ,作者Mr.Feng。
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进行修改。
温湿度数据采集
- #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;
- }
复制代码
电压数据采集
- #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;
- }
复制代码
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);
- }
复制代码
自动化控制
当ADC采集的电压大于阈值2.5V时自动开启LED1,低于时自动关闭LED1。
- if(adc>2.5){
- set_led(1,'1');
- }else{
- set_led(1,'0');
- }
复制代码
数据上传
在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);
复制代码
云端指令响应
由于云端传输的数据为JSON格式,因此需要使用cJSON进行解析。
添加cJSON
在components文件夹下添加cJSON相关文件
修改Makefile
在74行和78行后面要添加-lm,否则在编译的时候会报错。
实现代码
- 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");
- }
- }
-
- }
复制代码
四、视频监控
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端口):
最后启动Nginx服务,即可完成RTMP服务器的搭建:
- cd /usr/local/nginx/sbin
- ./nginx
复制代码
本地推流 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
复制代码
五、用户端设计 框架 使用Python编程,采用Web界面,并通过Flask提供Web服务以及后端数据处理能力。可以部署在云端,也可以在本地运行。界面如下所示:
视频拉流 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>
复制代码
远程数据的读取与指令下发 这一部分通过后端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)
复制代码
演示效果 |