一、项目介绍
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云平台配置
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性能的原因,客户端的视频流会存在一定的时延