model_train_dm/docs/边缘智能终端模型同步功能设计.md

929 lines
35 KiB
Markdown
Raw Normal View History

2026-07-27 17:51:49 +08:00
# 边缘智能终端模型同步功能设计
## 一、概述
### 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 仅能访问自己的数据和接口
- 定期轮换密钥(可选扩展)