专栏目录
边缘计算+WEB端应用融合:AI行为识别智能监控系统搭建指南 -- 整体介绍(一)
边缘计算+WEB端应用融合:AI行为识别智能监控系统搭建指南 -- 边缘设备图像识别及部署(二)
边缘计算+WEB端应用融合:AI行为识别智能监控系统搭建指南 -- 推流及公网环境(三)PART1
边缘计算+WEB端应用融合:AI行为识别智能监控系统搭建指南 -- 推流及公网环境(三)PART2
边缘计算+WEB端应用融合:AI行为识别智能监控系统搭建指南 -- 边缘设备和云端通信接口及保护进程(四)
前言
边缘计算设备部署后基本就脱离普通运维控制了,如果那么多设备都要一台台的配置,势必会非常麻烦。所以从运维角度需要有一个简便的方法进行配置。不可能让人24小时盯着设备运行是否正常。
云端系统的终端设备通信接口
接口的开发都是基于若依的后端框架,使用@Anonymous
让接口可以任意访问。
/registerAndAliveDevice
这个接口是用来注册和记录设备心跳的,当设备第一次上线数据库中还没有接口的唯一编号,将会产生一条空记录,需要用户在网页端对设备信息进行维护。
java
/**
* 注册及设备心跳接口
*
*/
@Anonymous
@ApiOperation("注册及设备心跳接口")
@PostMapping("/registerAndAliveDevice")
public AjaxResult registerAndAliveDevice(String deviceNo) {
if(StringUtils.isEmpty(deviceNo))
return error("设备编码不能为空!");
// 获取设备信息
YunyanDevice yunyandevice = yunYanEdgeService.registerAndAliveDevice(deviceNo);
List<YunyanCamera> yunyanCameraList = null;
// 如果设备信息不为空,就从系统中获取摄像头信息
if(yunyandevice!=null)
yunyanCameraList = yunYanEdgeService.selectYunyanCameraListByDeviceId(yunyandevice.getDeviceId());
EdgeVo edgeVo = new EdgeVo(yunyandevice,yunyanCameraList);
return success(edgeVo);
}
/reloadComplete
重置设备、摄像头状态接口。通过/registerAndAliveDevice
会获取到设备、摄像头是否需要重置,当重置完成通过调用接口返回给平台重新出实话重置状态防止重复加载。
java
/**
* 重制reload状态,可以重制设备和摄像头
* @param tempNo
* @param type
* @return
*/
@Anonymous
@ApiOperation("重制reload状态")
@PostMapping("/reloadComplete")
public AjaxResult reloadComplete(String tempNo,String type) {
if (StringUtils.isEmpty(type) || StringUtils.isEmpty(tempNo)) {
return error("参数不能为空");
}
switch (type) {
case "device":
yunyanDeviceService.reloadComplete(tempNo);
break;
case "camera":
yunyanCameraService.reloadComplete(tempNo);
break;
default:
break;
}
return success();
}
/model/download
模型文件下载接口。当摄像头对应的模型需要重置,会向云端获取模型文件。
java
/**
* 根据modelcode下载模型文件
*/
@Anonymous
@GetMapping("/download")
public AjaxResult downloadModelFile(String modelCode, HttpServletResponse response, HttpServletRequest request)
{
try
{
// modelCode就是文件的原始名称
if(StringUtils.isEmpty(modelCode))
return error("模型编码不能为空");
// throw new Exception("模型编码不能为空");
//根据modelCode获取模型文件路径
String filePath = yunyanModelService.getModelPathByModelCode(modelCode);
if(StringUtils.isEmpty(filePath))
return error("未找到模型文件");
// throw new Exception("未找到模型文件");
if (!FileUtils.checkAllowDownload(filePath))
{
return error(StringUtils.format("文件名称({})非法,不允许下载。 ", filePath));
// throw new Exception(StringUtils.format("文件名称({})非法,不允许下载。 ", filePaht));
}
//保持原文件名下载
String realFileName = modelCode;
int dirLastIndex = Constants.RESOURCE_PREFIX.length() + 1;
filePath = StringUtils.substring(filePath, dirLastIndex);
filePath = RuoYiConfig.getProfile() +'/'+ filePath;
File file = new File(filePath);
if (!file.exists())
return error("文件不存在");
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
FileUtils.setAttachmentResponseHeader(response, realFileName);
FileUtils.writeBytes(filePath, response.getOutputStream());
// 代表下载成功
return null;
}
catch (Exception e)
{
log.error("下载文件失败", e);
return error("下载文件失败");
}
}
/sendCamearStatus
推送摄像头状态,一台设备可以支撑多路摄像头的识别,所有加上单路摄像头状态返回接口来配合网页端显示。
java
/**
* 发送摄像头状态
*/
@Anonymous
@PostMapping("/sendCamearStatus")
public AjaxResult sendCamearStatus(String cameraNo,String cameraStatus,String cameraStatusContent) {
if(StringUtils.isEmpty(cameraNo))
return error("摄像头编号不能为空");
if(StringUtils.isEmpty(cameraStatus))
return error("摄像头状态不能为空");
YunyanCamera yunyanCamera = new YunyanCamera();
yunyanCamera.setCameraNo(cameraNo);
yunyanCamera.setCameraStatus(cameraStatus);
yunyanCamera.setCameraStatusContent(cameraStatusContent);
yunyanCameraService.updateYunyanCameraByCameraNo(yunyanCamera);
return success();
}
/sendDeviceStatus
设备状态发送接口,设备的正常运行需要启动反向代理、流媒体服务、识别主程序,当其中任意程序出现问题都由设备状态程序控制。
java
@Anonymous
@PostMapping("/sendDeviceStatus")
public AjaxResult sendDeviceStatus(String deviceNo,String status,String statusContent){
if(StringUtils.isEmpty(deviceNo))
return error("设备编号不能为空");
if(StringUtils.isEmpty(status))
return error("设备状态不能为空");
YunyanDevice yunyanDevice = new YunyanDevice();
yunyanDevice.setDeviceNo(deviceNo);
yunyanDevice.setStatus(status);
yunyanDevice.setStatusContent(statusContent);
yunyanDeviceService.updateYunyanDeviceByDeviceNo(yunyanDevice);
return success();
}
保护进程防止终端异常
保护进程是为了保证程序在终端上可以正常运行,如果设备断网终端、断电重启、摄像头断链等情况发生,程序无法正常执行。通过保护程序进行持续控制。保护程序选用python进行编写,当然这份工作就交给AI来完成了。
为了方便起见将所有程序的启动、停止、重启都加上了shell脚本。
python
import os
import json
import requests
import random
import subprocess
import time
# 设置脚本目录和文件路径
BASE_URL = "http://XXXXXXXX:XXXX"
script_dir = '/usr/local/yunyancv/'
script_path = os.path.join(script_dir, 'yunyan-cv.py')
edge_file_path = os.path.join(script_dir, 'edge.yunyan')
frpc_config_path = os.path.join(script_dir, 'yunyanfrpc.toml')
rtc_param_path = os.path.join(script_dir, 'rtc.config')
proxy_server_script_path = os.path.join(script_dir, 'proxy_server.sh')
yunyan_service_script_path = os.path.join(script_dir, 'yunyan_server.sh')
rtc_service_script_path = os.path.join(script_dir, 'rtc-server.sh')
camera_config_dir = '/usr/local/yunyan/'
region_config_dir = os.path.join(camera_config_dir, 'config/')
weights_dir = os.path.join(camera_config_dir, 'weights/')
# 步骤1
def generate_random_string(length):
letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
return ''.join(random.choice(letters) for i in range(length))
def check_edge_file():
if os.path.exists(edge_file_path):
with open(edge_file_path, 'r') as f:
return f.read().strip()
else:
random_string = generate_random_string(50)
with open(edge_file_path, 'w') as f:
f.write(random_string)
return random_string
def register_and_alive_device(device_no):
url = f"{BASE_URL}/yunyan/edge/registerAndAliveDevice"
payload = {"deviceNo": device_no}
response = requests.post(url, data=payload)
if response.status_code == 200:
return response.json()["data"]
else:
raise Exception(f"Failed to register and alive device. Status Code: {response.status_code}")
# 步骤2
def parse_and_reload(data, temp_no):
if_reload = data.get("ifReload", "0")
frp_param = data.get("frpParam", "")
rtc_param = data.get("rtcParam", "")
if if_reload == "1":
with open(frpc_config_path, 'w') as frpc_file:
frpc_file.write(frp_param)
with open(rtc_param_path, 'w') as rtc_param_file:
rtc_param_file.write('rtc_param="'+rtc_param+'"')
# 启动脚本
restart_proxy_server()
restart_rtc_service()
reload_complete_url = f"{BASE_URL}/yunyan/edge/reloadComplete"
reload_payload = {"tempNo": temp_no, "type": "device"}
requests.post(reload_complete_url, data=reload_payload)
# 重启代理服务器
def restart_proxy_server():
subprocess.run([proxy_server_script_path, 'restart'], check=True)
# 步骤3
def process_camera_list(camera_list):
existing_camera_files = set()
for camera in camera_list:
if_reload = camera.get("ifReload", "0")
param = camera.get("param", "")
area_param = camera.get("areaParam", "")
model_labels = camera.get("modelLabels", "")
model_code = camera.get("modelCode", "")
camera_no = camera.get("cameraNo", "")
# 记录已存在的配置文件
existing_camera_files.add(f"yunyan_config_{camera_no}.ini")
# 确保目录存在
os.makedirs(camera_config_dir, exist_ok=True)
os.makedirs(region_config_dir, exist_ok=True)
os.makedirs(weights_dir, exist_ok=True)
if if_reload == "1":
# 写入配置文件
with open(os.path.join(camera_config_dir, f"yunyan_config_{camera_no}.ini"), 'w') as config_file:
config_file.write(param)
with open(os.path.join(region_config_dir, f"region_{camera_no}.txt"), 'w') as region_file:
region_file.write(area_param)
with open(os.path.join(weights_dir, f"labels_list_{camera_no}.txt"), 'w') as labels_file:
labels_file.write(model_labels)
# 请求下载模型
download_model(model_code)
# 删除不存在的.ini配置文件
ini_files_to_delete = set(os.listdir(camera_config_dir)) - existing_camera_files
for ini_file_to_delete in ini_files_to_delete:
if ini_file_to_delete.endswith('.ini'):
file_path = os.path.join(camera_config_dir, ini_file_to_delete)
os.remove(file_path)
print(f"Deleted: {file_path}")
# 下载模型
def download_model(model_code):
download_url = f"{BASE_URL}/yunyan/model/download?modelCode={model_code}"
response = requests.get(download_url)
if response.status_code == 200 and response.headers.get('Content-Type') == 'application/octet-stream':
model_path = os.path.join(weights_dir, f"{model_code}")
with open(model_path, 'wb') as model_file:
model_file.write(response.content)
else:
raise Exception(f"Failed to download model. Status Code: {response.status_code}")
# 重启云眼服务
def restart_yunyan_service():
subprocess.run([yunyan_service_script_path, 'restart'], check=True)
# 重启webrtc服务
def restart_rtc_service():
subprocess.run([rtc_service_script_path, 'restart'], check=True)
# 检查进程
def is_process_running_by_name(process_name):
try:
subprocess.run(["pgrep", process_name], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
print(f"{process_name} IS RUNNING")
return True
except subprocess.CalledProcessError:
print(f"{process_name} IS DEAD")
return False
# 检查进程
def is_process_running_by_name_and_param(process_name,param):
command = f"ps -aux | grep {process_name} | grep {param}"
result = subprocess.run(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
if result.returncode == 0 and param+".ini" in result.stdout:
print(f"{process_name} ({param}) 正在运行.")
return True
else:
print(f"{process_name} ({param}) 未运行.")
return False
# 主程序
def main():
try:
# 记录脚本执行开始时间
start_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
print(f"Script started at: {start_time}")
device_no = check_edge_file()
data = register_and_alive_device(device_no)
parse_and_reload(data, device_no)
camera_list = data.get("camreaList", [])
# 如果 camera_list 为空,直接退出
if not camera_list:
print("No cameras to process. Exiting.")
return
if any(camera.get("ifReload", "0") == "1" for camera in camera_list):
process_camera_list(camera_list)
# 重启云眼服务
restart_yunyan_service()
# 调用接口告知服务器已经重启摄像头
for camera in camera_list:
if camera.get("ifReload", "0") == "1":
camera_no = camera.get("cameraNo", "")
reload_complete_url = f"{BASE_URL}/yunyan/edge/reloadComplete"
reload_payload = {"tempNo": camera_no, "type": "camera"}
requests.post(reload_complete_url, data=reload_payload)
# 记录脚本执行成功标志和结束时间START
end_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
print(f"Script executed successfully at: {end_time}")
print("==============================START ALIVE CHECK START==============================")
# 循环摄像头NO获取摄像头状态后进行推送
for camera in camera_list:
camera_no = camera.get("cameraNo", "")
camera_status_payload=""
if not is_process_running_by_name_and_param("yunyan_v1.0",camera_no):
camera_status_payload = {"cameraNo": camera_no, "cameraStatus": "1", "cameraStatusContent": end_time+"链路丢失"}
else:
camera_status_payload = {"cameraNo": camera_no, "cameraStatus": "0", "cameraStatusContent": end_time+"链路正常"}
requests.post(f"{BASE_URL}/yunyan/edge/sendCamearStatus", data=camera_status_payload)
edge_status_content = end_time
edge_status = 0
if not is_process_running_by_name("yunyanfrp"):
restart_proxy_server()
edge_status=2
edge_status_content = edge_status_content+" frp 重启;"
if not is_process_running_by_name("yunyan_v1.0"):
restart_yunyan_service()
edge_status=2
edge_status_content = edge_status_content+" 云眼重启;"
if not is_process_running_by_name("webrtc-streamer"):
restart_rtc_service()
edge_status=2
edge_status_content = edge_status_content+" webrtc重启;"
edge_status_payload = {"deviceNo": device_no, "status": edge_status, "statusContent": edge_status_content}
requests.post(f"{BASE_URL}/yunyan/edge/sendDeviceStatus", data=edge_status_payload)
print("==============================START ALIVE CHECK END==============================")
except Exception as e:
# 记录脚本执行失败标志和异常信息
error_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
print(f"Script failed at: {error_time}")
print(f"Error: {str(e)}")
if __name__ == "__main__":
main()