FastBond4挑战部分-基于RDK X5 的多核异构工业危险行为实时检测与联动控制系统
一、所选主题和项目介绍
本项目聚焦于 人工智能在嵌入式系统中的应用,以地平线RDK X5边缘计算平台为核心,构建了一套面向工业现场的 AI视觉安全监控与联动控制系统。系统深度融合了 计算机视觉(图像识别)、多传感器感知、实时工业以太网控制 ,设计并实现了一套“视觉AI检测+环境感知+实时IO控制”三位一体的边缘计算系统。实现对跌倒、火灾等典型危险行为的智能识别与自主保护性响应,为工业自动化提供了高可靠、低延迟的边缘AI解决方案。
项目完整链路为:USB摄像头接入RDK X5,运行YOLOv8火灾/跌倒检测模型,推理结果实时推流至板载ZLMediaKit流媒体服务器(亦可通过Socket.IO直接推送JPEG帧至网页);同时,RDK X5通过CAN总线接收STM32F103节点采集的温湿度(SHT30、HDC1080)、气压(BMP280)、光照强度(OPT3001)及MPU6050、QMI8658A、LSM6DSMTR三组六轴姿态数据,所有数据通过Web后端实时展示在网页仪表盘上。RDK X5还部署了IgH EtherCAT主站协议栈,通过以太网接口与EtherCAT从站通信,网页端可实时查看两路数字输入状态(DI0/DI1),当AI模块判断发生跌倒或火灾时,系统自动通过EtherCAT下发指令,驱动从站上两个LED分别点亮实现就地报警,并在网页上弹出最高优先级报警提示。整个系统实现了检测、推流、数据显示、设备控制与联动的闭环,具有一定的工程实用价值。
二、使用到的所有硬件介绍
1. 地平线RDK X5(核心主控)
基于BPU计算架构的AI开发板,4核CPU,板载10TOPs算力BPU,拥有千兆以太网、USB3.0、CAN、WiFi等丰富接口。本项目中它承担AI推理、ZLM流媒体服务器(可选)、EtherCAT主站、CAN数据汇集和Web服务器五大核心角色。
2. 自制环境感知节点(MCU: STM32F103RCT6)
主控为Cortex-M3架构,256KB Flash,48KB RAM。外围挂载I2C1总线上的MPU6050、QMI8658A、LSM6DSMTR三款六轴传感器,以及I2C2总线上的SHT30、BMP280、OPT3001、HDC1080环境传感器。板载CAN收发器TJA1051,通过CAN总线以100ms周期上报环境数据帧与姿态帧。
3. 自制EtherCAT从站节点(MCU: STM32G474RET6 + LAN9252)
Cortex-M4F内核,内置高分辨率定时器。从站芯片使用Microchip LAN9252,通过SPI与G474交互,实现EtherCAT从站控制器功能。板上引出两颗红色/绿色高亮LED用于报警指示。在从站程序中,利用SSC工具生成的从站协议栈,实现了标准CoE对象字典,便于主站映射控制。
4. 其他配件
USB免驱高清摄像头(1080P/30fps)。
三、方案框图和项目设计思路介绍

设计思路:
- AI与流媒体一体化:利用RDK X5的BPU加速推理,搭配板载硬件视频编码能力,将检测框渲染后的视频流直接推送到同机部署的ZLMediaKit,避免数据搬运延迟,降低网络负载。
- 多协议高实时性通信:环境数据采集为低频、多传感器数据,选择CAN总线(500kbps)兼顾可靠性与成本;IO控制要求毫秒级确定性延时,采用EtherCAT保证硬实时。
- 模块化解耦:AI检测、环境采集、IO控制三个功能由独立硬件实现,仅通过RDK X5软件进行逻辑耦合,任意模块异常不影响其他子系统基本安全逻辑。
- 网页端集中管理:采用Flask-SocketIO后端+WebSocket,将环境数据、报警信息、AI识别结果推送给前端,前端可以实时监视EtherCAT输入输出状态。
四、原理图和PCB展示及介绍
本项目自制了两款PCB:环境采集板和EtherCAT从站板。两款板子各打样一版,最终完成稳定版本。
环境采集板
使用的订单物料如下:
F1C1-252012-1R5M Signal Transformer | 固定电感器 - DigiKey
PTS815SJM250SMTRLFS C&K | 触摸开关 - DigiKey
原理图:
最小系统部分

电源+CAN+RS485部分
六轴陀螺仪部分

环境传感器部分

PCB展示

渲染图及实物图


EtherCAT IO板
原理图介绍
- 主控部分
采用STM32G474CEU6作为主控
- 电源部分
- 24V->5V采用RY8411,它具有4.5-40V宽输入范围 - 最大1A输出电流 - 过压/过流保护

- 5V->3.3V采用XC6210B332MR

- 24V->5V采用RY8411,它具有4.5-40V宽输入范围 - 最大1A输出电流 - 过压/过流保护
- ST_LINK_V2.1
采用stm32f103cbu6为主控
- CANFD
采用NXP的TJA1051TK/3/1收发器
- RS485
采用SIT3088ETK
- 拓展IO

- LAN9252

- 网口及电


PCB
stm32g474板卡

LAN9252板卡

实物图

五、 YOLOV8模型训练与转换部署
为实现高精度的跌倒与火灾检测,我针对性地训练了 YOLOv8 模型,并完成从 PC 端到 RDK X5 边缘端的完整迁移。
YOLOv8 的官方仓库地址: https://github.com/ultralytics/ultralytics.git
整体流程

5.1 数据集构建
我们收集了约2000张图片,其中跌倒图片800张,火灾图片1200张。使用 LabelImg 工具进行标注,按 8:2 随机划分为训练集(1600张)和验证集(400张),并编写符合 YOLO 格式的 fire_fall.yaml 配置文件,定义三类检测目标:Fire、Smoke、fall。
fire_fall.yaml文件
path: ../datasets/fire_fall
train: images/train
val: images/val
test:
names:
0: Fire
1: Smoke
2: fall
5.2 模型训练
基于 Ultralytics 官方仓库,使用预训练权重 yolov8n.pt 进行迁移学习。训练配置为:120 epochs,输入尺寸 640×640,batch size 16。训练完成后,最佳模型保存为 best.pt,并在 PC 端验证了正确的检测效果。
训练结果:
由于数据集太少,结果不是太好。
![]()
![]()
![]()
PC端测试结果
model = YOLO('runs/detect/fire_fall/weights/best.pt')
results = model('test_images/', save=True)

5.3 面向地平线 BPU 的模型导出
RDK X5 的 BPU 对模型结构有特定要求。我们修改了 ultralytics/nn/modules/head.py 中的 Detect 类,使 forward 方法在导出模式下输出 6 个独立的 NHWC 张量(每个尺度分别输出 bbox 和 cls),而非默认的合并输出。随后使用 model.export(format='onnx', opset=11) 导出 ONNX 模型,并用 Netron 验证了输出结构。
- 修改检测头 (head.py):核心任务是找到ultralytics/nn/modules/head.py中的Detect类,修改其forward方法,以在self.export为True时,输出 6 个独立的 NHWC 张量(BBox 和 Cls 分开,且每个尺度独立)。
def forward(self, x: list[torch.Tensor]):
"""Concatenates and returns predicted bounding boxes and class probabilities."""
# ---------- RDK X5 导出分支 ----------
if self.export:
outputs = []
for i in range(self.nl):
bbox = self.cv2[i](x[i]) # (bs, 4*reg_max, h, w)
cls = self.cv3[i](x[i]) # (bs, nc, h, w)
# 输出 NHWC 布局(如果后处理需要 NCHW,可注释掉这两行)
bbox = bbox.permute(0, 2, 3, 1).contiguous()
cls = cls.permute(0, 2, 3, 1).contiguous()
outputs.append(bbox)
outputs.append(cls)
return outputs
# ---------- 原始训练/推理代码 ----------
preds = self.forward_head(x, **self.one2many)
if self.end2end:
x_detach = [xi.detach() for xi in x]
one2one = self.forward_head(x_detach, **self.one2one)
preds = {"one2many": preds, "one2one": one2one}
if self.training:
return preds
y = self._inference(preds["one2one"] if self.end2end else preds)
if self.end2end:
y = self.postprocess(y.permute(0, 2, 1))
return y if self.export else (y, preds)
- 执行导出:使用修改后的 Ultralytics 代码执行导出。
from ultralytics import YOLO
model = YOLO('best.pt')
model.export(format='onnx', imgsz=640, opset=11) - 导出验证:使用 Netron 打开 ONNX 模型,确认输出是预期的 6 个独立张量。

5.4 地平线工具链转换
在地平线提供的 Docker 环境中,使用 hb_mapper checker 检查 ONNX 模型兼容性,编写 fire_fall.yaml 配置文件,指定 NV12 输入、数据归一化参数、校准数据集路径等信息。通过 hb_mapper makertbin 完成量化与编译,最终生成板端可推理的 fire_fall.bin 文件。
- 启动Docker
export version=v1.2.8
export ai_toolchain_package_path=/home/kone/RDK_X5/OE/horizon_x5_open_explorer_v1.2.8-py310_20240926
export dataset_path=/home/kone/RDK_X5/OE/dataset
# 启动docker镜像
sudo docker run -it --rm \
-v "$ai_toolchain_package_path":/open_explorer \
-v "$dataset_path":/data \
openexplorer/ai_toolchain_ubuntu_20_x5_cpu:v1.2.8-py310 - 模型检查 (hb_mapper checker)
在正式开始前,先用 hb_mapper checker命令检查 ONNX 模型的兼容性,避免有不支持的算子导致后续转换卡住。
hb_mapper checker --model-type onnx --march bayes-e --model fire_fall.onnx
- 准备校准数据和配置文件
修复文件名(批量重命名)在Docker 容器内,进入校准图片目录,执行以下命令,把括号全部删掉:for f in *; do
准备配置文件
new=$(echo "$f" | tr -d '()')
[ "$f" != "$new" ] && mv "$f" "$new"
done
ls # 检查是否还有特殊字符model_parameters:
执行
onnx_model: 'fire_fall.onnx'
march: "bayes-e"
working_dir: 'model_output' # 转换产物的输出目录
output_model_file_prefix: 'fire_fall'
input_parameters:
input_type_rt: 'nv12'
input_type_train: 'rgb'
input_layout_train: 'NCHW'
norm_type: 'data_scale'
scale_value: 0.003921568627451
calibration_parameters:
cal_data_dir: './calibration_data'
cal_data_type: 'float32'
preprocess_on: True # 工具链自动完成 resize 和格式转换
calibration_type: 'default'
compiler_parameters:
compile_mode: 'latency'
optimize_level: 'O3'hb_mapper makertbin --model-type onnx --config fire_fall.yam

生成的 fire_fall.bin 即为板端可用模型。
六、调试软件、软件流程图和关键代码介绍
6.1 软件流程图

6.2 关键代码示例
CAN数据接收(小端字节序)
def can_recv(sock, timeout=0.5):
sock.settimeout(timeout)
try:
raw = sock.recv(CAN_MTU)
if len(raw) >= 5:
can_id, dlc = struct.unpack('<IB', raw[:5])
can_id &= 0x1FFFFFFF
dlc &= 0x0F
payload = raw[8:8+dlc]
return can_id, payload
except socket.timeout:
pass
return None, None
EtherCAT LED控制
def set_led(led_index, state):
val = 0xFFFF if state else 0x0000
subprocess.run([
"ethercat", "download", "--type", "uint16", "-p0",
f"0x7000", str(led_index + 1), f"0x{val:04X}"
], check=False)
AI报警去抖逻辑
if current_fire or current_smoke:
fire_count += 1
else:
fire_count = 0
if current_fall:
fall_count += 1
else:
fall_count = 0
fire_alarm = fire_count >= ALARM_THRESH
fall_alarm = fall_count >= ALARM_THRESH
WebSocket实时推送
socketio.emit('env_data', env)
socketio.emit('mpu_data', mpu)
socketio.emit('led_state', {'led0': current_led0, 'led1': current_led1})
6.3 前端仪表盘
前端采用深色科幻风格,基于Chart.js绘制三组IMU姿态实时曲线,利用Canvas绘制六个圆形环境数据仪表盘,每个仪表盘上方标注传感器型号(如SHT30 Temp、BMP280 Press等)。EtherCAT IO状态通过LED圆点直观显示,报警事件以脉冲动画突出。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>RDK X5 Industrial Safety Monitor</title>
<style>
:root { --bg: #0a0a0f; --panel: #111122; --border: #1e1e3f; --text: #ccc; --accent: #00ffcc; --fire: #ff3b3b; --fall: #ff8c00; }
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Segoe UI', sans-serif; background: var(--bg); color: var(--text); overflow-x: hidden; }
#particles { position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 0; }
.container { position: relative; z-index: 1; display: flex; flex-wrap: wrap; padding: 15px; gap: 15px; }
.video-box { flex: 2; min-width: 500px; background: var(--panel); border: 1px solid var(--border); border-radius: 12px; padding: 10px; box-shadow: 0 0 20px rgba(0,255,204,0.1); }
.video-box h3 { color: var(--accent); margin-bottom: 10px; font-weight: 300; letter-spacing: 2px; }
.video-box img { width: 100%; border-radius: 8px; border: 1px solid var(--border); }
.panel { flex: 1; min-width: 320px; display: flex; flex-direction: column; gap: 15px; }
.card { background: var(--panel); border: 1px solid var(--border); border-radius: 12px; padding: 15px; box-shadow: 0 0 15px rgba(0,255,204,0.05); }
.card h4 { color: var(--accent); margin-bottom: 10px; font-weight: 400; text-transform: uppercase; letter-spacing: 1px; }
.env-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.gauge { text-align: center; }
.gauge-source { font-size: 0.65rem; color: #888; margin-bottom: 2px; text-transform: uppercase; }
.gauge canvas { width: 70px; height: 70px; }
.gauge-label { font-size: 0.8rem; margin-top: 2px; color: #aaa; }
.alarm { padding: 12px; margin: 5px 0; border-radius: 8px; font-weight: bold; display: none; animation: pulse 1s infinite; }
@keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.6; } 100% { opacity: 1; } }
.fire-alarm { background: var(--fire); color: white; }
.fall-alarm { background: var(--fall); color: white; }
.led-dot { transition: background 0.2s; }
.chart-container { height: 150px; margin: 10px 0; }
canvas { max-width: 100%; }
</style>
</head>
<body>
<canvas id="particles"></canvas>
<div class="container">
<div class="video-box">
<h3>Live Camera</h3>
<img id="video" src="" alt="Video Stream">
</div>
<div class="panel">
<div class="card">
<h4>Environment</h4>
<div class="env-grid">
<div class="gauge">
<div class="gauge-source">SHT30 Temp</div>
<canvas id="gauge_temp"></canvas>
<div class="gauge-label" id="temp_label">--°C</div>
</div>
<div class="gauge">
<div class="gauge-source">SHT30 Hum</div>
<canvas id="gauge_hum"></canvas>
<div class="gauge-label" id="hum_label">--%</div>
</div>
<div class="gauge">
<div class="gauge-source">BMP280 Press</div>
<canvas id="gauge_press"></canvas>
<div class="gauge-label" id="press_label">--hPa</div>
</div>
<div class="gauge">
<div class="gauge-source">OPT3001 Light</div>
<canvas id="gauge_lux"></canvas>
<div class="gauge-label" id="lux_label">--lux</div>
</div>
<div class="gauge">
<div class="gauge-source">HDC1080 Temp</div>
<canvas id="gauge_hdc_temp"></canvas>
<div class="gauge-label" id="hdc_temp_label">--°C</div>
</div>
<div class="gauge">
<div class="gauge-source">HDC1080 Hum</div>
<canvas id="gauge_hdc_hum"></canvas>
<div class="gauge-label" id="hdc_hum_label">--%</div>
</div>
</div>
</div>
<div class="card">
<h4>Alarms</h4>
<div id="fire_alarm" class="alarm fire-alarm">FIRE!</div>
<div id="fall_alarm" class="alarm fall-alarm">FALL DETECTED!</div>
</div>
<div class="card">
<h4>EtherCAT IO</h4>
<div style="display:flex; align-items:center; gap:15px; flex-wrap:wrap;">
<div style="flex:1; background:#1a1a2e; padding:10px; border-radius:8px;">
<div style="margin-bottom:8px;">Output LEDs</div>
<div>
<span class="led-dot" id="led0_state" style="display:inline-block; width:14px; height:14px; border-radius:50%; background:#444; vertical-align:middle;"></span> LED0 (Fire)
<span class="led-dot" id="led1_state" style="display:inline-block; width:14px; height:14px; border-radius:50%; background:#444; vertical-align:middle; margin-left:15px;"></span> LED1 (Fall)
</div>
</div>
<div style="flex:1; background:#1a1a2e; padding:10px; border-radius:8px;">
<div style="margin-bottom:8px;">Input DI</div>
<div>
<span class="led-dot" id="di0_led" style="display:inline-block; width:14px; height:14px; border-radius:50%; background:#444; vertical-align:middle;"></span> DI0
<span class="led-dot" id="di1_led" style="display:inline-block; width:14px; height:14px; border-radius:50%; background:#444; vertical-align:middle; margin-left:15px;"></span> DI1
</div>
<div style="font-family:monospace; margin-top:6px; font-size:0.85rem;" id="io_raw">Overall: 0x--</div>
</div>
</div>
</div>
</div>
</div>
<div class="container" style="padding-top:0;">
<div class="card" style="width:100%;"><h4>MPU6050 Attitude</h4><div class="chart-container"><canvas id="mpu_chart"></canvas></div></div>
<div class="card" style="width:100%;"><h4>QMI8658A Attitude</h4><div class="chart-container"><canvas id="qmi_chart"></canvas></div></div>
<div class="card" style="width:100%;"><h4>LSM6DSMTR Attitude</h4><div class="chart-container"><canvas id="lsm_chart"></canvas></div></div>
</div>
<script src="https://cdn.socket.io/4.5.0/socket.io.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<script>
// Particle background
const canvas = document.getElementById('particles');
const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const particles = [];
for (let i = 0; i < 80; i++) {
particles.push({ x: Math.random() * canvas.width, y: Math.random() * canvas.height, r: Math.random() * 2 + 1, dx: (Math.random() - 0.5) * 0.5, dy: (Math.random() - 0.5) * 0.5 });
}
function drawParticles() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'rgba(0,255,204,0.2)';
for (let p of particles) {
ctx.beginPath();
ctx.arc(p.x, p.y, p.r, 0, Math.PI*2);
ctx.fill();
p.x += p.dx;
p.y += p.dy;
if (p.x < 0 || p.x > canvas.width) p.dx *= -1;
if (p.y < 0 || p.y > canvas.height) p.dy *= -1;
}
requestAnimationFrame(drawParticles);
}
drawParticles();
window.addEventListener('resize', () => { canvas.width = window.innerWidth; canvas.height = window.innerHeight; });
// Gauges
function drawGauge(id, value, max, label, unit) {
const c = document.getElementById(id);
if (!c) return;
const w = c.width = 70, h = c.height = 70;
const gctx = c.getContext('2d');
gctx.clearRect(0,0,w,h);
const centerX = w/2, centerY = h/2, radius = 28;
const start = Math.PI * 0.75, end = Math.PI * 2.25;
const percent = Math.min(value / max, 1);
gctx.beginPath();
gctx.arc(centerX, centerY, radius, start, end);
gctx.strokeStyle = '#333';
gctx.lineWidth = 6;
gctx.stroke();
gctx.beginPath();
gctx.arc(centerX, centerY, radius, start, start + (end - start) * percent);
gctx.strokeStyle = '#00ffcc';
gctx.lineWidth = 6;
gctx.stroke();
gctx.fillStyle = '#fff';
gctx.font = 'bold 12px sans-serif';
gctx.textAlign = 'center';
gctx.fillText(parseFloat(value).toFixed(1), centerX, centerY + 4);
gctx.font = '9px sans-serif';
gctx.fillText(unit, centerX, centerY + 16);
document.getElementById(label).textContent = value.toFixed(1) + ' ' + unit;
}
// Charts
function createLineChart(canvasId) {
const ctx = document.getElementById(canvasId).getContext('2d');
return new Chart(ctx, {
type: 'line',
data: {
labels: [],
datasets: [
{ label: 'ax', data: [], borderColor: '#ff6384', pointRadius: 0, borderWidth: 1 },
{ label: 'ay', data: [], borderColor: '#36a2eb', pointRadius: 0, borderWidth: 1 },
{ label: 'az', data: [], borderColor: '#ffce56', pointRadius: 0, borderWidth: 1 },
{ label: 'gx', data: [], borderColor: '#4bc0c0', pointRadius: 0, borderWidth: 1 },
{ label: 'gy', data: [], borderColor: '#9966ff', pointRadius: 0, borderWidth: 1 },
{ label: 'gz', data: [], borderColor: '#ff9f40', pointRadius: 0, borderWidth: 1 }
]
},
options: {
responsive: true, maintainAspectRatio: false, animation: false,
scales: { x: { display: false }, y: { grid: { color: '#2a2a4a' }, ticks: { color: '#ccc' } } },
plugins: { legend: { labels: { color: '#aaa', usePointStyle: true } } }
}
});
}
const mpuChart = createLineChart('mpu_chart');
const qmiChart = createLineChart('qmi_chart');
const lsmChart = createLineChart('lsm_chart');
const MAX_POINTS = 50;
function updateChart(chart, dataObj) {
chart.data.labels.push('');
chart.data.datasets[0].data.push(dataObj.ax);
chart.data.datasets[1].data.push(dataObj.ay);
chart.data.datasets[2].data.push(dataObj.az);
chart.data.datasets[3].data.push(dataObj.gx);
chart.data.datasets[4].data.push(dataObj.gy);
chart.data.datasets[5].data.push(dataObj.gz);
if (chart.data.labels.length > MAX_POINTS) {
chart.data.labels.shift();
chart.data.datasets.forEach(ds => ds.data.shift());
}
chart.update();
}
// Socket.IO
const socket = io();
socket.on('video_frame', (data) => {
document.getElementById('video').src = 'data:image/jpeg;base64,' + data.img;
});
socket.on('env_data', (data) => {
drawGauge('gauge_temp', data.sht_temp, 50, 'temp_label', 'degC');
drawGauge('gauge_hum', data.sht_hum, 100, 'hum_label', '%');
drawGauge('gauge_press', data.bmp_press/100, 1100, 'press_label', 'hPa');
drawGauge('gauge_lux', data.opt_lux, 1000, 'lux_label', 'lux');
drawGauge('gauge_hdc_temp', data.hdc_temp, 50, 'hdc_temp_label', 'degC');
drawGauge('gauge_hdc_hum', data.hdc_hum, 100, 'hdc_hum_label', '%');
});
socket.on('mpu_data', (data) => { updateChart(mpuChart, data); });
socket.on('qmi_data', (data) => { updateChart(qmiChart, data); });
socket.on('lsm_data', (data) => { updateChart(lsmChart, data); });
socket.on('detection', (data) => {
document.getElementById('fire_alarm').style.display = data.fire ? 'block' : 'none';
document.getElementById('fall_alarm').style.display = data.fall ? 'block' : 'none';
});
socket.on('led_state', (data) => {
document.getElementById('led0_state').style.background = data.led0 ? '#ff4d4d' : '#444';
document.getElementById('led1_state').style.background = data.led1 ? '#ff8c00' : '#444';
});
socket.on('io_state', (data) => {
document.getElementById('di0_led').style.background = data.di0 ? '#00ffcc' : '#444';
document.getElementById('di1_led').style.background = data.di1 ? '#00ffcc' : '#444';
document.getElementById('io_raw').textContent = 'Overall: 0x' + data.raw.toString(16).toUpperCase().padStart(4, '0');
});
</script>
</body>
</html>
七、硬件功能展示图及说明
- 整体接线图:RDK X5与两块自研板卡连接,摄像头置于模拟厂房场景

- 网页仪表盘:左侧显示实时视频(带有检测框标注,分别用红色框标火灾、蓝色框标人体),右侧以仪表和曲线展示温湿度、光照、MPU6050翻滚角。


- 报警展示:触发跌倒后,网页弹出“FALL DETECTED!”红色横幅,视频画面叠加“FALL ALERT”文字;同时EtherCAT从站的红色LED点亮。火灾触发时蓝色LED点亮,网页弹出“FIRE!”警告。

- IO状态:网页上实时显示DI0、DI1的高低电平(绿色圆点表示高电平,灰色表示低电平),以及整体十六进制输入状态字。
- 数据推送:通过手机浏览器访问同一网络下的网页,所有数据与视频流畅显示,延迟<300ms。


八、设计中遇到的难题和解决方法
1. EtherCAT主站无法启动
现象:执行ethercat slaves时报“Failed to open master device /dev/EtherCAT0”。
解决:检查内核模块是否加载,执行sudo modprobe ec_master && sudo modprobe ec_generic,并配置/etc/ethercat.conf绑定对应网卡MAC,重启ethercat服务后设备文件出现,主站正常工作。
2. CAN总线数据字节序错误
现象:RDK X5端接收到的CAN ID异常(0x1010000),数据无法解析。
解决:RDK X5采用ARM64小端架构,而代码最初使用大端解析,改为struct.unpack('<IB', raw[:5])后数据完全正确,温度、姿态均实时更新。
3. Flask模板编码错误
现象:网页返回500 Internal Server Error,错误日志提示UnicodeDecodeError。
解决:index.html文件内混入了非UTF-8字符。重新在终端用cat命令生成纯ASCII的HTML文件,彻底解决编码问题。
九、心得体会
这个项目让我深刻体会到了“软件定义硬件”在边缘计算中的价值。RDK X5强大的AI算力和丰富的接口使我们可以用一块板子替代过去“工控机+AI加速卡+网关”的组合,但同时也对软件架构设计提出了极高要求。多协议、多任务并发下,合理分配CPU资源、严格区分实时与非实时线程是系统稳定运行的基石。


