929 lines
35 KiB
Markdown
929 lines
35 KiB
Markdown
# 边缘智能终端模型同步功能设计
|
||
|
||
## 一、概述
|
||
|
||
### 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 仅能访问自己的数据和接口
|
||
- 定期轮换密钥(可选扩展)
|