model_train_dm/docs/边缘智能终端模型同步功能设计.md
2026-07-27 17:51:49 +08:00

929 lines
35 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 边缘智能终端模型同步功能设计
## 一、概述
### 1.1 系统架构
```
┌──────────────────────────────────────────────────────────────────┐
│ 云端服务器 (Cloud) │
│ ┌──────────┐ ┌────────────┐ ┌───────────┐ ┌──────────────┐ │
│ │ 认证服务 │ │ 心跳服务 │ │ 版本管理 │ │ 模型下载服务 │ │
│ │ (Token) │ │ (Heartbeat)│ │ (Version) │ │ (Download) │ │
│ └────┬─────┘ └─────┬──────┘ └─────┬─────┘ └──────┬───────┘ │
│ │ │ │ │ │
│ ┌────┴──────────────┴───────────────┴───────────────┴──────┐ │
│ │ REST API + WebSocket │ │
│ └──────────────────────────┬───────────────────────────────┘ │
│ │ │
│ ┌──────────────────────────┴───────────────────────────────┐ │
│ │ Django REST Framework + Channels (ASGI) │ │
│ │ MinIO 对象存储 (archived-model / trained-model) │ │
│ └──────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
HTTPS / WSS公网/内网)
┌─────────────────────────────┴────────────────────────────────────┐
│ 边缘智能终端 (Edge Device) │
│ ┌──────────┐ ┌────────────┐ ┌───────────┐ ┌──────────────┐ │
│ │ Token管理 │ │ 心跳上报 │ │ 版本检查 │ │ 模型下载更新 │ │
│ │ (每日刷新)│ │ (30s/次) │ │ (定时轮询) │ │ (差量更新) │ │
│ └──────────┘ └────────────┘ └───────────┘ └──────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Edge Runtime (Python 模拟程序 / 边缘盒子固件) │ │
│ │ 本地模型目录: /opt/edge/models/ │ │
│ │ 本地配置目录: /opt/edge/config/ │ │
│ └──────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
```
### 1.2 核心数据模型
基于已有数据表 `ai_smart_terminal`,核心字段如下:
| 字段 | 类型 | 说明 |
|------|------|------|
| `stcd` | CharField(40) | 终端唯一编码,边缘设备与云端一一对应 |
| `stnm` | CharField(255) | 终端名称 |
| `model_id` | CharField(40) | 已部署模型ID关联 AiAlgorithmModels |
| `model_name` | CharField(200) | 已部署模型文件名 |
| `model_version` | CharField(64) | 云端当前发布的模型版本 |
| `model_path` | CharField(600) | 模型文件 MinIO 存储路径 |
| `model_publish_time` | DateTimeField | 模型发布时间 |
| `edge_model_version` | CharField(64) | 边缘端当前运行的模型版本 |
| `edge_update_time` | DateTimeField | 边缘端最后一次模型更新时间 |
| `last_heartbeat_time` | DateTimeField | 边缘端最后心跳时间 |
| `online_status` | SmallIntegerField | 在线状态0=离线 1=在线 |
| `sync_check_interval` | IntegerField | 同步检查间隔(单位:分钟) |
---
## 二、云端服务功能设计
### 2.1 设备接入认证Token 生成)
**接口**: `POST /server/terminal/auth/`
**功能说明**:
- 边缘设备使用设备编码stcd向云端申请认证 Token
- Token 有效期 1 天24小时基于 JWT 机制
- Token 中携带 `stcd` ,区别于用户 Token
**请求参数**:
```json
{
"stcd": "DEVICE001"
}
```
**响应示例**:
```json
{
"code": 200,
"data": {
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"token_type": "bearer",
"expires_in": 86400,
"stcd": "DEVICE001"
}
}
```
**认证中间件**:
- 所有边缘设备 API 使用 `EdgeDeviceAuthentication` 认证类
- 验证 Token 中的 `stcd` 与请求参数/URL 中的 `stcd` 一致
- Token 过期返回 401设备端触发重新认证流程
**JWT Token 生成逻辑**:
```python
from rest_framework_simplejwt.tokens import AccessToken
from datetime import timedelta
def generate_device_token(stcd: str) -> str:
token = AccessToken()
token['stcd'] = stcd
token['type'] = 'device'
token.set_exp(lifetime=timedelta(days=1))
return str(token)
```
### 2.2 心跳时间更新
**接口**: `PUT /server/terminal/heartbeat/`(已有,需增强认证)
**功能说明**:
- 边缘设备定时调用(建议 30 秒/次)
- 更新 `last_heartbeat_time` 为当前时间
- 更新 `online_status` 为 1在线
- 若设备首次接入,同时设置 `dtin=1``dtin_tm`
**请求参数**:
```json
{
"stcd": "DEVICE001"
}
```
**请求头**:
```
Authorization: Bearer <device_token>
```
**响应示例**:
```json
{
"code": 200,
"data": {
"stcd": "DEVICE001",
"last_heartbeat_time": "2026-07-25T15:30:00Z",
"online_status": 1
}
}
```
### 2.3 离线状态检测(定时任务)
**实现方式**: Django Management Command + Celery Beat / APScheduler / 系统 Cron
**执行频率**: 每 30 秒执行一次
**检测逻辑**:
1. 查询所有 `online_status=1``last_heartbeat_time` 不为空的终端
2. 计算 `now() - last_heartbeat_time`
3. 若差值 > 5 分钟300秒则将 `online_status` 更新为 0
4. 通过 WebSocket 向前端管理页面推送离线通知
**核心代码逻辑**:
```python
# management/commands/check_terminal_offline.py
from django.core.management.base import BaseCommand
from django.utils.timezone import now
from datetime import timedelta
from apps.core.models import AiSmartTerminal
from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
class Command(BaseCommand):
help = '检测超时心跳的终端并标记为离线'
def handle(self, *args, **options):
threshold = now() - timedelta(minutes=5)
offline_terminals = AiSmartTerminal.objects.filter(
online_status=1,
last_heartbeat_time__lt=threshold,
)
count = offline_terminals.update(online_status=0)
if count > 0:
# WebSocket 推送离线通知到前端
channel_layer = get_channel_layer()
offline_list = list(
offline_terminals.values('stcd', 'stnm')
)
async_to_sync(channel_layer.group_send)(
"terminal_status",
{
"type": "terminal.status",
"data": {
"event": "offline",
"count": count,
"terminals": offline_list,
}
}
)
self.stdout.write(f"标记 {count} 个终端为离线")
```
**WebSocket 前端订阅**:
- 前端管理页面连接 WebSocket: `ws://host/ws/terminal/status/`
- 收到 `online_status=0` 消息时,实时更新终端列表中的在线状态标识
- 可触达前端消息提示(如 ElNotification
### 2.4 模型版本检查
**接口**: `GET /server/terminal/checkVersion/`
**功能说明**:
- 边缘设备定期调用,检查云端是否有新版本模型
- 对比 `edge_model_version`(边缘当前版本)与 `model_version`(云端发布版本)
- 返回是否需要更新
**请求参数**:
```
GET /server/terminal/checkVersion/?stcd=DEVICE001&edge_version=v1.0
```
**响应示例(有新版本)**:
```json
{
"code": 200,
"data": {
"having_newmodel": 1,
"model_name": "model-001.onnx",
"model_version": "v2.0",
"model_publish_time": "2026-07-25T12:00:00Z",
"sync_check_interval": 30
}
}
```
**响应示例(已是最新)**:
```json
{
"code": 200,
"data": {
"having_newmodel": 0,
"model_version": "v1.0",
"sync_check_interval": 30
}
}
```
**版本对比逻辑**:
-`edge_model_version` 为空(首次同步),`having_newmodel=1`
-`edge_model_version != model_version``having_newmodel=1`
-`edge_model_version == model_version``having_newmodel=0`
- 返回 `sync_check_interval` 供边缘端动态调整检查频率
### 2.5 模型下载
**接口**: `GET /server/terminal/downloadModel/`(已有,需增强)
**功能说明**:
- 边缘设备下载最新 ONNX 模型文件
- 支持断点续传(通过 Range 请求头)
- 返回文件流,`Content-Disposition` 包含文件名
- 下载完成后,边缘端调用回调接口更新 `edge_model_version``edge_update_time`
**请求参数**:
```
GET /server/terminal/downloadModel/?stcd=DEVICE001&format=onnx
```
**请求头**:
```
Authorization: Bearer <device_token>
Range: bytes=0-1048576 (可选,断点续传)
```
**响应**:
```
HTTP/1.1 200 OK
Content-Type: application/octet-stream
Content-Disposition: attachment; filename="model-001.onnx"
Content-Length: 10485760
```
**下载完成回调接口**: `PUT /server/terminal/updateModelVersion/`
```json
{
"stcd": "DEVICE001",
"edge_model_version": "v2.0"
}
```
---
## 三、边缘设备端设计(模拟程序)
### 3.1 目录结构
```
edge_simulator/
├── config/
│ ├── config.json # 配置文件(设备编码、云端地址等)
│ └── token.json # 缓存的认证 Token
├── models/ # 模型存储目录
│ ├── current/ # 当前运行模型
│ └── backup/ # 历史版本备份
├── logs/
│ └── edge.log
├── main.py # 主入口,调度各模块
├── auth.py # Token 管理与刷新
├── heartbeat.py # 心跳上报模块
├── version_check.py # 版本检查与下载
└── requirements.txt
```
### 3.2 配置文件 (config/config.json)
```json
{
"stcd": "DEVICE001",
"cloud_base_url": "http://192.168.1.60:8000",
"heartbeat_interval_sec": 30,
"version_check_interval_min": null,
"model_dir": "./models/current",
"backup_dir": "./models/backup",
"token_file": "./config/token.json",
"log_level": "INFO",
"retry_count": 3,
"retry_delay_sec": 5
}
```
### 3.3 模块详细设计
#### 3.3.1 Token 管理模块 (auth.py)
**功能**:
- 每日定时获取新 Token每天 00:00:00 执行)
- 首次启动时立即获取 Token
- 缓存 Token 到本地文件,记录过期时间
- 每次 API 调用前检查 Token 是否有效
**流程**:
```
开始
├── 检查本地 token.json 是否存在
│ ├── 存在 → 检查是否过期
│ │ ├── 未过期 → 直接使用
│ │ └── 已过期 → 调用认证接口获取新 Token
│ └── 不存在 → 调用认证接口获取新 Token
└── 保存 Token 到 token.json
└── 记录 expires_at 时间戳
```
**token.json 格式**:
```json
{
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"token_type": "bearer",
"expires_at": "2026-07-26T00:00:00Z",
"stcd": "DEVICE001"
}
```
#### 3.3.2 心跳上报模块 (heartbeat.py)
**功能**:
- 定时 30 秒调用云端心跳接口
- 携带设备 Token 进行认证
- 失败重试 3 次(间隔 2 秒)
- 记录心跳日志
**流程**:
```
定时触发 (每30秒)
├── 获取本地 Token
├── PUT /server/terminal/heartbeat/
│ Headers: Authorization: Bearer <token>
│ Body: {"stcd": "DEVICE001"}
├── 响应码 = 200 → 成功,记录日志
├── 响应码 = 401 → Token 过期,触发 Token 刷新 → 重试
├── 响应码 = 其他 / 超时 → 重试最多3次
└── 3次失败 → 记录错误日志,等待下次触发
```
#### 3.3.3 版本检查与模型下载模块 (version_check.py)
**功能**:
- 根据 `sync_check_interval` 定时检查云端模型版本
- 若有新版本,下载模型文件到本地(支持断点续传)
- 验证下载文件的 MD5/SHA256 完整性
- 备份旧版本模型
- 更新 `edge_model_version` 到云端
**流程**:
```
定时触发 (根据 sync_check_interval默认从云端获取)
├── 读取本地当前模型版本 (从本地 version.txt)
├── GET /server/terminal/checkVersion/
│ ?stcd=DEVICE001
│ &edge_version=v1.0
├── 响应 having_newmodel = 0 → 无更新,等待下次检查
├── 响应 having_newmodel = 1
│ │
│ ├── 更新本地 sync_check_interval
│ │
│ ├── 备份旧模型: models/current/ → models/backup/model-{version}/
│ │
│ ├── GET /server/terminal/downloadModel/
│ │ ?stcd=DEVICE001
│ │ &format=onnx
│ │ 支持断点续传 (Range 请求头)
│ │
│ ├── 下载完成 → 验证文件完整性
│ │ 验证方式: 响应头 X-File-SHA256 与实际文件对比
│ │
│ ├── 保存到 models/current/{model_filename}
│ ├── 更新本地 version.txt
│ │
│ └── PUT /server/terminal/updateModelVersion/
│ {"stcd": "DEVICE001", "edge_model_version": "v2.0"}
│ → 更新 edge_model_version 和 edge_update_time
└── 记录更新日志
```
#### 3.3.4 主调度程序 (main.py)
```python
"""
边缘智能终端模拟程序 - 主入口
"""
import json
import logging
import os
import signal
import sys
import threading
import time
from datetime import datetime
import requests
class EdgeDeviceSimulator:
"""边缘智能终端模拟器"""
def __init__(self, config_path: str = "./config/config.json"):
self.config = self._load_config(config_path)
self.stcd = self.config["stcd"]
self.secret = self.config["secret"]
self.base_url = self.config["cloud_base_url"]
self.running = True
self._setup_logging()
self._setup_signal_handlers()
self.token = None
self.token_refreshed_at = None
self.heartbeat_interval = self.config.get("heartbeat_interval_sec", 30)
self.version_check_interval = None # 从云端获取
self.last_version_check = None
# ── 配置与日志 ──────────────────────────────
def _load_config(self, path: str) -> dict:
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
def _setup_logging(self):
logging.basicConfig(
level=getattr(logging, self.config.get("log_level", "INFO")),
format="%(asctime)s [%(levelname)s] %(message)s",
handlers=[
logging.FileHandler("./logs/edge.log", encoding="utf-8"),
logging.StreamHandler(sys.stdout),
],
)
self.logger = logging.getLogger("EdgeDevice")
def _setup_signal_handlers(self):
signal.signal(signal.SIGINT, self._graceful_shutdown)
signal.signal(signal.SIGTERM, self._graceful_shutdown)
def _graceful_shutdown(self, signum, frame):
self.logger.info("收到退出信号,正在安全关闭...")
self.running = False
# ── Token 管理 ───────────────────────────────
def _load_token_from_file(self) -> dict | None:
token_file = self.config["token_file"]
if not os.path.exists(token_file):
return None
with open(token_file, "r", encoding="utf-8") as f:
data = json.load(f)
expires_at = datetime.fromisoformat(data["expires_at"])
if datetime.now() >= expires_at:
self.logger.info("本地 Token 已过期")
return None
return data
def _save_token_to_file(self, token_data: dict):
with open(self.config["token_file"], "w", encoding="utf-8") as f:
json.dump(token_data, f, indent=2)
def ensure_token(self) -> str:
"""确保 Token 有效,过期则重新获取"""
token_data = self._load_token_from_file()
if token_data:
return token_data["access_token"]
self.logger.info("获取新的设备 Token...")
resp = requests.post(
f"{self.base_url}/server/terminal/auth/",
json={"stcd": self.stcd, "secret": self.secret},
timeout=10,
)
resp.raise_for_status()
result = resp.json()
token_data = result["data"]
self._save_token_to_file(token_data)
self.logger.info(f"Token 获取成功, 有效期至 {token_data['expires_at']}")
return token_data["access_token"]
# ── 心跳上报 ──────────────────────────────────
def _send_heartbeat(self):
"""发送一次心跳"""
try:
token = self.ensure_token()
resp = requests.put(
f"{self.base_url}/server/terminal/heartbeat/",
json={"stcd": self.stcd},
headers={"Authorization": f"Bearer {token}"},
timeout=5,
)
if resp.status_code == 200:
self.logger.debug("心跳上报成功")
return True
elif resp.status_code == 401:
self.logger.warning("Token 认证失败,清除本地 Token 缓存")
os.remove(self.config["token_file"])
else:
self.logger.warning(f"心跳上报失败: {resp.status_code}")
except requests.RequestException as e:
self.logger.error(f"心跳上报异常: {e}")
return False
def _heartbeat_loop(self):
"""心跳上报定时循环"""
self.logger.info(f"心跳上报模块启动, 间隔 {self.heartbeat_interval} 秒")
while self.running:
self._send_heartbeat()
time.sleep(self.heartbeat_interval)
# ── 版本检查与模型更新 ─────────────────────────
def _check_version(self) -> dict | None:
"""检查云端模型版本"""
local_version = self._get_local_version()
try:
token = self.ensure_token()
resp = requests.get(
f"{self.base_url}/server/terminal/checkVersion/",
params={"stcd": self.stcd, "edge_version": local_version or ""},
headers={"Authorization": f"Bearer {token}"},
timeout=10,
)
resp.raise_for_status()
return resp.json()["data"]
except requests.RequestException as e:
self.logger.error(f"版本检查失败: {e}")
return None
def _get_local_version(self) -> str | None:
"""获取本地当前运行的模型版本"""
version_file = os.path.join(self.config["model_dir"], "version.txt")
if os.path.exists(version_file):
with open(version_file, "r") as f:
return f.read().strip()
return None
def _backup_current_model(self):
"""备份当前模型"""
model_dir = self.config["model_dir"]
backup_dir = self.config["backup_dir"]
local_version = self._get_local_version()
if not os.path.exists(model_dir) or not local_version:
return
version_backup = os.path.join(backup_dir, f"model-{local_version}")
os.makedirs(version_backup, exist_ok=True)
for f in os.listdir(model_dir):
src = os.path.join(model_dir, f)
dst = os.path.join(version_backup, f)
if os.path.isfile(src):
import shutil
shutil.copy2(src, dst)
self.logger.info(f"旧模型已备份到 {version_backup}")
def _download_model(self, model_name: str) -> str | None:
"""下载模型文件,支持断点续传"""
model_dir = self.config["model_dir"]
os.makedirs(model_dir, exist_ok=True)
save_path = os.path.join(model_dir, model_name)
try:
token = self.ensure_token()
url = f"{self.base_url}/server/terminal/downloadModel/"
params = {"stcd": self.stcd, "format": "onnx"}
headers = {"Authorization": f"Bearer {token}"}
self.logger.info(f"开始下载模型: {model_name}")
resp = requests.get(
url, params=params, headers=headers,
stream=True, timeout=300,
)
resp.raise_for_status()
total_size = int(resp.headers.get("Content-Length", 0))
downloaded = 0
with open(save_path, "wb") as f:
for chunk in resp.iter_content(chunk_size=8192):
f.write(chunk)
downloaded += len(chunk)
if total_size:
progress = downloaded / total_size * 100
if downloaded % (1024 * 1024 * 10) < 8192: # 每10MB打印
self.logger.info(
f"下载进度: {progress:.1f}% "
f"({downloaded}/{total_size})"
)
self.logger.info(f"模型下载完成, 保存至 {save_path}")
return save_path
except Exception as e:
self.logger.error(f"模型下载失败: {e}")
return None
def _update_edge_version(self, new_version: str):
"""通知云端已更新到新版本"""
try:
token = self.ensure_token()
resp = requests.put(
f"{self.base_url}/server/terminal/updateModelVersion/",
json={"stcd": self.stcd, "edge_model_version": new_version},
headers={"Authorization": f"Bearer {token}"},
timeout=10,
)
if resp.status_code == 200:
self.logger.info(f"边缘模型版本已更新: {new_version}")
return True
except Exception as e:
self.logger.error(f"云端版本更新通知失败: {e}")
return False
def _version_check_loop(self):
"""版本检查与更新定时循环"""
self.logger.info("版本检查模块启动")
while self.running:
result = self._check_version()
if result and result.get("having_newmodel") == 1:
model_name = result["model_name"]
new_version = result["model_version"]
self.logger.info(f"发现新版本: {new_version}, 模型: {model_name}")
# 备份旧模型
self._backup_current_model()
# 下载新模型
saved = self._download_model(model_name)
if saved:
# 更新本地版本记录
version_file = os.path.join(
self.config["model_dir"], "version.txt"
)
with open(version_file, "w") as f:
f.write(new_version)
# 通知云端
self._update_edge_version(new_version)
# 更新检查间隔
if result.get("sync_check_interval"):
self.version_check_interval = result["sync_check_interval"]
# 等待下次检查
interval = self.version_check_interval or 60 # 默认60分钟
self.logger.debug(f"下次版本检查: {interval} 分钟后")
time.sleep(interval * 60)
# ── 主入口 ────────────────────────────────────
def run(self):
"""启动边缘模拟器"""
self.logger.info(f"边缘智能终端启动, stcd={self.stcd}")
self.logger.info(f"云端地址: {self.base_url}")
# 启动心跳线程
heartbeat_thread = threading.Thread(
target=self._heartbeat_loop, daemon=True, name="Heartbeat"
)
heartbeat_thread.start()
# 启动版本检查线程
version_thread = threading.Thread(
target=self._version_check_loop, daemon=True, name="VersionCheck"
)
version_thread.start()
# 主线程等待
try:
while self.running:
time.sleep(1)
except KeyboardInterrupt:
pass
self.logger.info("边缘智能终端已安全退出")
if __name__ == "__main__":
simulator = EdgeDeviceSimulator()
simulator.run()
```
---
## 四、新增/修改 API 接口汇总
| 接口路径 | 方法 | 认证 | 说明 | 状态 |
|---------|------|------|------|------|
| `/server/terminal/auth/` | POST | 无(设备密钥认证) | 设备认证,获取 JWT Token | **新增** |
| `/server/terminal/heartbeat/` | PUT | 设备 Token | 更新心跳时间(已有,需增强认证) | **修改** |
| `/server/terminal/checkVersion/` | GET | 设备 Token | 检查云端模型版本 | **新增** |
| `/server/terminal/downloadModel/` | GET | 无历史遗留 | 下载模型文件(已有,需增强) | **修改** |
| `/server/terminal/updateModelVersion/` | PUT | 设备 Token | 更新边缘端模型版本 | **新增** |
| `ws://host/ws/terminal/status/` | WebSocket | 用户 Token | 推送终端在线状态变更 | **新增** |
---
## 五、安全性设计
### 5.1 认证体系
```
┌─────────────────────────────────────┐
│ 双层 Token 体系 │
├─────────────────────────────────────┤
│ 用户 Token (user) │
│ ├── 有效期: 60分钟 │
│ ├── 用途: 前端管理页面操作 │
│ └── sub: user │
│ │
│ 设备 Token (device) │
│ ├── 有效期: 24小时 │
│ ├── 用途: 边缘设备 API 调用 │
│ ├── sub: edge_device │
│ └── stcd: 设备编码 │
└─────────────────────────────────────┘
```
### 5.2 安全措施
1. **设备预置密钥**: 首次激活时下发唯一密钥,用于初始 Token 申请
2. **Token 绑定设备**: Token 中携带 `stcd`,服务端验证请求中的 `stcd` 与 Token 一致
3. **HTTPS 传输**: 生产环境强制 HTTPS防止中间人攻击
4. **请求频率限制**: 心跳接口限制 10次/分钟/设备,防止恶意刷接口
5. **模型下载签名**: 可扩展为预签名 URL限制下载有效期
---
## 六、部署与运维
### 6.1 云端定时任务配置
**方式一: Django Management Command + 系统 Cron**
```bash
# 每30秒执行一次离线检测
* * * * * /path/to/python manage.py check_terminal_offline
* * * * * sleep 30 && /path/to/python manage.py check_terminal_offline
```
**方式二: Celery Beat推荐**
```python
# celery.py
app.conf.beat_schedule = {
'check-terminal-offline': {
'task': 'apps.datasets.tasks.check_terminal_offline',
'schedule': 30.0,
},
}
```
### 6.2 边缘模拟器部署
```bash
# 安装依赖
pip install -r edge_simulator/requirements.txt
# 运行模拟器(前台)
python edge_simulator/main.py
# 运行模拟器(后台)
nohup python edge_simulator/main.py > /dev/null 2>&1 &
```
### 6.3 监控指标
| 指标 | 说明 | 告警阈值 |
|------|------|---------|
| 在线设备数 | online_status=1 的设备数 | 低于预期值的 80% |
| 离线设备数 | online_status=0 的设备数 | 超过预期值的 20% |
| 心跳延迟 | 心跳时间间隔 | 超过 60 秒 |
| 模型更新成功率 | 成功更新数/总更新数 | 低于 90% |
| Token 续期失败率 | Token 获取失败次数 | 连续 3 次失败 |
---
## 七、时序图
### 7.1 Token 获取流程
```
边缘设备 云端服务器
│ │
│ POST /server/terminal/auth/ │
│ {"stcd":"DEVICE001","secret":"**"}│
│ ─────────────────────────────> │
│ │── 验证设备编码与密钥
│ │── 生成JWT Token (24h)
│ 200 {access_token, ...} │
│ <───────────────────────────── │
│ │
│── 保存Token到本地文件 │
```
### 7.2 心跳 + 版本检查 + 模型更新完整流程
```
边缘设备 云端服务器 前端管理页
│ │ │
│═══ 心跳循环 (30s/次) ═══════════│ │
│ PUT /heartbeat/ [Token] │ │
│ ─────────────────────────────> │── 更新 last_heartbeat_time │
│ 200 OK │ │
│ <───────────────────────────── │ │
│ │ │
│═══ 离线检测 (云端定时30s) ═══════│ │
│ │── 检测超时5分钟的设备 │
│ │── online_status → 0 │
│ │── WebSocket推送 ───────────> │
│ │ {"event":"offline", ...} │
│ │ │── 更新UI
│ │ │
│═══ 版本检查 (定时) ══════════════│ │
│ GET /checkVersion/ │ │
│ ─────────────────────────────> │── 对比 model_version │
│ 200 {having_newmodel:1, ...} │ 与 edge_model_version │
│ <───────────────────────────── │ │
│ │ │
│═══ 模型下载 ═════════════════════│ │
│── 备份旧模型到 backup/ │ │
│ │ │
│ GET /downloadModel/ │ │
│ ─────────────────────────────> │── 从MinIO获取模型文件 │
│ 200 (文件流) │ │
│ <───────────────────────────── │ │
│── 保存到 models/current/ │ │
│── 更新本地 version.txt │ │
│ │ │
│ PUT /updateModelVersion/ │ │
│ ─────────────────────────────> │── 更新 edge_model_version │
│ 200 OK │ │
│ <───────────────────────────── │ │
```
---
## 八、实施清单
### 云端新增开发项
- [ ] 新增 `EdgeDeviceAuthentication` 认证类
- [ ] 新增 `POST /server/terminal/auth/` 设备认证接口
- [ ] 新增 `GET /server/terminal/checkVersion/` 版本检查接口
- [ ] 新增 `PUT /server/terminal/updateModelVersion/` 版本更新回调接口
- [ ] 修改 `PUT /server/terminal/heartbeat/` 增加设备 Token 认证
- [ ] 新增 `check_terminal_offline` 管理命令或 Celery 定时任务
- [ ] 新增 WebSocket Consumer (`TerminalStatusConsumer`) 推送在线状态
- [ ] 新增 `ws/terminal/status/` WebSocket 路由
- [ ] 前端管理页面订阅 WebSocket实时更新终端在线状态
### 边缘模拟器开发项
- [ ] 创建 `edge_simulator/` 目录结构
- [ ] 实现 `auth.py` Token 管理模块
- [ ] 实现 `heartbeat.py` 心跳上报模块
- [ ] 实现 `version_check.py` 版本检查与下载模块
- [ ] 实现 `main.py` 主调度程序
- [ ] 编写 `requirements.txt` 依赖文件
- [ ] 编写 Docker 部署配置(可选)
---
## 九、行业最佳实践参考
1. **OTA 更新策略**(参考物联网 OTA 标准)
- 差量更新 vs 全量更新:当前采用全量下载,未来可扩展为增量更新
- 灰度发布支持按终端分类sttp或指定设备列表分批推送版本
- 回滚机制:备份旧版本,支持云端指令触发回滚
2. **连接可靠性**(参考 MQTT/CoAP 协议设计)
- 断线重连边缘端实现指数退避重连1s → 2s → 4s → ... → max 60s
- 离线缓存:心跳失败时的状态记录,恢复后补报
- 连接池:单设备单连接,避免资源浪费
3. **安全性**(参考 IoT 安全最佳实践)
- 设备唯一身份stcd + 预置密钥组成设备指纹
- 通信加密TLS 1.2+
- 最小权限:设备 Token 仅能访问自己的数据和接口
- 定期轮换密钥(可选扩展)