边缘计算+WEB端应用融合:AI行为识别智能监控系统搭建指南 -- 边缘设备和云端通信接口及保护进程(四)

专栏目录
边缘计算+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()
相关推荐
赵青临的辉10 分钟前
基础数学:线性代数与概率论在AI中的应用
人工智能·线性代数·概率论
小众AI20 分钟前
Void: Cursor 的开源平替
人工智能·ai编程
资深の小白26 分钟前
一个基于 Spring Boot 的实现,用于代理百度 AI 的 OCR 接口
人工智能·spring boot·百度
二川bro33 分钟前
从AlphaGo到ChatGPT:AI技术如何一步步改变世界?
人工智能·chatgpt
码农新猿类37 分钟前
帧差法识别
人工智能·opencv·计算机视觉
cdut_suye44 分钟前
【Linux系统】从 C 语言文件操作到系统调用的核心原理
java·linux·数据结构·c++·人工智能·机器学习·云计算
梁下轻语的秋缘1 小时前
前馈神经网络回归(ANN Regression)从原理到实战
人工智能·神经网络·回归
xu_wenming1 小时前
华为Watch的ECG功能技术分析
人工智能·嵌入式硬件·算法
meisongqing1 小时前
【软件工程】机器学习多缺陷定位技术分析
人工智能·机器学习·软件工程·缺陷定位
高工智能汽车1 小时前
大模型浪潮下,黑芝麻智能高性能芯片助力汽车辅助驾驶变革
人工智能·汽车