# 边缘智能终端模型同步功能设计 ## 一、概述 ### 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 ``` **响应示例**: ```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 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 │ 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 仅能访问自己的数据和接口 - 定期轮换密钥(可选扩展)