Files
system-design/software-copyright/05-writech-edge-box/自然写教室智能算力盒边缘计算软件-鉴别材料.md
T
2026-03-22 15:24:40 +08:00

2795 lines
104 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
# 自然写教室智能算力盒边缘计算软件 V1.0
## 软件鉴别材料 — 设计说明书
---
**软件全称**:自然写教室智能算力盒边缘计算软件
**软件版本**V1.0
**权利人**:深圳自然写科技有限公司
**文档类型**:嵌入式软件设计说明书
**文档编号**WRITECH-EDGE-DS-001
**编制日期**2026年2月
**密级**:内部资料
---
## 目录
- 第一章 软件整体概述
- 1.1 软件简介与功能综述
- 1.2 软件用途与适用场景
- 1.3 运行环境与系统要求
- 1.4 开发语言与技术规范
- 1.5 版本说明
- 第二章 系统架构与设计思路
- 2.1 总体架构设计
- 2.2 各层次详细说明
- 2.3 核心架构示意图
- 2.4 数据设计
- 2.5 接口设计
- 2.6 安全设计
- 2.7 部署架构
- 第三章 核心模块功能详细说明
- 3.1 main.cpp — 主程序入口与系统初始化
- 3.2 inference/inference_engine.cpp — 推理引擎核心
- 3.3 inference/model_manager.cpp — 模型管理模块
- 3.4 inference/npu_scheduler.cpp — NPU/GPU调度器
- 3.5 communication/grpc_server.cpp — gRPC通信服务
- 3.6 communication/mqtt_client.cpp — MQTT状态上报
- 3.7 preprocessing/stroke_preprocessor.cpp — 笔迹预处理
- 3.8 config/edge_config.py — 配置管理模块
- 3.9 离线缓存与数据同步模块
- 3.10 集群管理与负载均衡模块
- 3.11 OTA升级模块
- 3.12 设备监控与运维模块
- 第四章 操作流程与使用步骤
- 4.1 设备安装与初始化配置
- 4.2 网络接入与云端注册
- 4.3 模型加载与推理验证
- 4.4 课堂教学使用流程
- 4.5 离线模式操作流程
- 4.6 OTA升级操作流程
- 4.7 集群管理操作流程
- 4.8 故障排查与日志查看
- 第五章 与源代码的对应关系
- 5.1 模块与源代码文件对应表
- 5.2 核心函数说明
- 5.3 类与方法命名规范
- 附录A 硬件接口说明
- 附录B 术语表
- 附录C 版本历史
---
## 第一章 软件整体概述
### 1.1 软件简介与功能综述
自然写教室智能算力盒边缘计算软件(以下简称"算力盒软件")是自然写互动课堂智能点阵笔系统的核心边缘计算组件,运行于部署在教室内的智能算力盒硬件设备之上。该软件将云端AI推理能力下沉至教室本地,实现在无网络或弱网络环境下的完整手写识别功能,大幅降低识别延迟,提升课堂实时交互体验。
算力盒软件的核心设计理念是"边缘智能、离线可用、云边协同"。软件在本地搭载轻量化的手写识别模型,能够独立完成OCR文字识别、数学公式识别、笔顺分析等AI推理任务,同时通过与云端的协同机制,支持模型在线更新、数据延迟上传和集群统一调度。
**主要功能模块综述:**
| 功能模块 | 说明 |
|---------|------|
| 端侧AI推理引擎 | 本地运行手写OCR识别、数学列式识别、笔顺分析AI模型 |
| 轻量化模型管理 | 管理本地AI模型文件的加载、版本切换与云端同步更新 |
| 笔迹数据预处理 | 对原始笔迹坐标数据进行去噪、归一化、笔画分割处理 |
| 实时识别结果分发 | 将推理结果实时推送至教室内黑板、PC、Pad等各终端 |
| 离线模式支持 | 断网环境下AI识别能力不降级,结果本地缓存待网络恢复后上传 |
| 云边协同通信 | 通过gRPC接收网关笔迹流,通过MQTT向云端上报状态 |
| 集群管理 | 支持校级多台算力盒组成集群,统一调度与负载均衡 |
| OTA远程升级 | 支持固件和AI模型的远程在线升级,A/B分区无损升级 |
| 设备监控运维 | 实时监控GPU/NPU利用率、温度、推理QPS,支持远程运维 |
### 1.2 软件用途与适用场景
算力盒软件专为K-12基础教育互动课堂场景设计,主要解决以下核心问题:
**场景一:农村及偏远地区学校**
网络基础设施薄弱,课堂教学依赖本地AI能力。算力盒软件提供完整的离线识别能力,即使网络中断,课堂教学流程不受影响。学生的书写作业、笔顺练习、数学作答均可实时得到AI反馈。
**场景二:城市学校大规模并发**
一所学校可能同时进行多个年级的互动课堂教学,每间教室有40支点阵笔同时工作。集中式云端识别在高并发场景下存在延迟和拥塞风险。算力盒将计算压力分散到各教室,每间教室识别延迟 < 200ms(单次OCR)。
**场景三:教学过程低延迟交互**
学生书写后,教师和学生都希望立即看到识别结果和评分反馈。算力盒在本地完成推理,避免了网络往返时延,实现毫秒级响应,增强互动感。
**场景四:数据安全与隐私保护**
部分学校和家长对学生书写数据上传云端有顾虑。算力盒支持"本地优先"模式,识别计算在本地完成,原始笔迹数据可选择不上云,满足数据本地化合规要求。
**适用硬件平台:**
- 搭载瑞芯微RK3588 NPU6 TOPS算力)的ARM算力盒
- 搭载NVIDIA Jetson系列GPU的x86/ARM算力盒
- 标准x86工控机(支持OpenCL加速)
### 1.3 运行环境与系统要求
**硬件环境:**
| 配置项 | 最低要求 | 推荐配置 |
|--------|---------|---------|
| 处理器 | ARM Cortex-A55 4核 / x86 4核 | RK3588 八核 / Jetson Orin |
| 内存 | 4GB LPDDR4 | 8GB LPDDR4X |
| 存储 | 32GB eMMC | 64GB eMMC + 256GB NVMe |
| AI加速 | NPU 1 TOPS 或 GPU 支持CUDA/OpenCL | NPU 6 TOPS 或 Jetson GPU |
| 网络 | 100Mbps 有线以太网 | 千兆有线 + WiFi 6 |
| 操作系统 | 嵌入式 Linux(内核 4.19+ | Ubuntu 20.04 LTSARM/x86 |
**软件运行环境:**
| 组件 | 版本要求 | 用途 |
|------|---------|------|
| Linux Kernel | 4.19 以上 | 操作系统内核 |
| RKNN Runtime | 1.5.0 以上(瑞芯微平台) | NPU推理运行时 |
| CUDA | 11.4 以上(NVIDIA平台) | GPU推理加速 |
| ONNX Runtime | 1.13.0 以上 | AI模型推理引擎 |
| TensorRT | 8.5 以上(NVIDIA平台) | 模型加速优化 |
| gRPC | 1.50.0 以上 | 服务通信框架 |
| Mosquitto Client | 2.0 以上 | MQTT通信 |
| SQLite | 3.38.0 以上 | 本地数据存储 |
| Python | 3.9 以上 | 管理API服务 |
| Flask | 2.2 以上 | 本地管理Web服务 |
**资源占用:**
| 资源 | 正常工作状态 | 峰值状态 |
|------|------------|---------|
| 内存占用 | 约1.5GB | 约3GB |
| NPU/GPU利用率 | 30-60% | 95% |
| CPU利用率 | 15-30% | 60% |
| 存储空间 | 约8GB(含模型) | 约15GB(含缓存) |
| 网络带宽 | 约5Mbps(上行) | 约20Mbps |
### 1.4 开发语言与技术规范
**主要开发语言:**
| 语言 | 版本 | 用途 |
|------|------|------|
| C++ | C++17 | 推理引擎、预处理、gRPC服务、NPU调度 |
| Python | 3.9 | 模型管理、配置服务、Flask管理API |
| Protocol Buffers | proto3 | gRPC接口定义 |
**代码规范:**
- C++遵循Google C++ Style Guide,命名采用下划线分隔(snake_case)
- Python遵循PEP8规范,Docstring采用Google风格
- 所有公共接口需提供完整的doxygen注释
- 线程安全:多线程共享数据使用std::mutex或std::atomic保护
- 内存管理:推理引擎采用RAII原则,避免内存泄漏
- 错误处理:使用返回码与错误日志双重机制,关键路径不使用异常
**构建工具链:**
| 工具 | 版本 | 说明 |
|------|------|------|
| CMake | 3.20+ | C++项目构建系统 |
| GCC / Clang | GCC 9+ / Clang 12+ | C++编译器 |
| pip / conda | 最新 | Python依赖管理 |
| Docker | 20.10+ | 容器化打包与部署 |
| Buildroot | 2023.02 | 嵌入式Linux根文件系统构建 |
### 1.5 版本说明
| 版本 | 发布日期 | 主要变更 |
|------|---------|---------|
| V0.5 Beta | 2025年8月 | 基础推理框架搭建,单模型OCR识别 |
| V0.8 Beta | 2025年10月 | 增加数学识别、笔顺分析模型支持 |
| V0.9 RC | 2025年12月 | 增加离线缓存、集群管理基础框架 |
| V1.0 | 2026年2月 | 正式版,支持完整功能,OTA升级稳定 |
---
## 第二章 系统架构与设计思路
### 2.1 总体架构设计
算力盒软件采用六层边缘AI推理分层架构,自下而上分别为:硬件加速层、推理框架层、模型管理层、业务服务层、通信层和管理层。各层职责清晰、接口明确,支持不同硬件平台的横向替换(瑞芯微NPU、NVIDIA GPU、OpenCL GPU均通过统一接口适配)。
```
┌──────────────────────────────────────────────────────────────────┐
│ 管理层(Management Layer
│ Flask管理API │ SQLite配置库 │ 日志服务 │ 状态监控 │
├──────────────────────────────────────────────────────────────────┤
│ 通信层(Communication Layer
│ gRPC Server(接收网关笔迹) │ MQTT Client(云端状态同步) │
│ WebSocket推送(识别结果分发) │ mDNS集群发现 │
├──────────────────────────────────────────────────────────────────┤
│ 业务服务层(Business Service Layer
│ 笔迹预处理 │ 推理调度 │ 结果分发 │ 离线缓存 │
│ 任务优先级队列 │ 模型热切换 │ 结果后处理 │ 同步管理 │
├──────────────────────────────────────────────────────────────────┤
│ 模型管理层(Model Management Layer
│ 模型版本控制 │ 动态加载 │ INT8量化 │ 云端同步更新 │
│ A/B分区管理 │ 模型加密存储 │ 精度评估 │ 回滚机制 │
├──────────────────────────────────────────────────────────────────┤
│ 推理框架层(Inference Framework Layer
│ ONNX Runtime │ TensorRT │ PaddleLite │ 框架统一接口 │
├──────────────────────────────────────────────────────────────────┤
│ 硬件加速层(Hardware Acceleration Layer
│ RKNN(瑞芯微NPU │ CUDANVIDIA GPU │ OpenCL(通用GPU
└──────────────────────────────────────────────────────────────────┘
```
### 2.2 各层次详细说明
**硬件加速层(Hardware Acceleration Layer**
硬件加速层负责对接底层AI加速芯片的驱动和运行时环境,提供统一的硬件抽象接口供上层调用。软件通过编译时宏开关(`#ifdef PLATFORM_RKNN``#ifdef PLATFORM_CUDA`)选择目标平台的实现,做到"一套代码,多平台适配"。
支持的硬件加速方案:
- **RKNN(瑞芯微NPU**:适用于RK3588等瑞芯微SoCNPU算力6 TOPS,模型格式为`.rknn`(由ONNX转换)
- **CUDA + TensorRT**:适用于NVIDIA Jetson系列,GPU并行计算,模型经TensorRT优化加速
- **OpenCL**:通用GPU加速方案,适用于Mali GPU、PowerVR GPU等ARM嵌入式GPU
**推理框架层(Inference Framework Layer**
推理框架层管理AI模型的加载与推理执行。软件定义了统一的`IInferenceEngine`抽象接口,不同框架(ONNX Runtime / TensorRT / PaddleLite)各自实现该接口。业务层调用时无需关心底层框架差异。
```
IInferenceEngine(抽象接口)
├── ONNXInferenceEngine → ONNX Runtime实现
├── TRTInferenceEngine → TensorRT实现(NVIDIA平台)
└── PaddleLiteEngine → PaddleLite实现(ARM平台)
```
**模型管理层(Model Management Layer**
模型管理层负责AI模型文件的全生命周期管理:
- 模型文件存储在`/opt/writech/models/`目录,按类型分目录存放(ocr/math/stroke_order
- 每个模型目录下维护`model_meta.json`元信息文件,记录版本号、精度指标、部署状态
- 支持A(当前运行)和B(待切换)双版本共存,切换时无需重启进程
- 模型文件AES-256加密存储,运行时内存解密加载,防止模型被窃取
**业务服务层(Business Service Layer**
业务服务层是软件的核心业务逻辑所在,包含以下主要组件:
- `StrokePreprocessor`:笔迹坐标去噪、归一化和笔画分割
- `InferenceScheduler`:基于优先级的推理任务调度(实时识别优先于批量处理)
- `ResultDistributor`:将推理结果分发至对应终端(按学生ID路由)
- `OfflineCacheManager`:管理断网期间的结果暂存和恢复上传
**通信层(Communication Layer**
通信层负责软件与外部系统的所有数据交换:
- **gRPC Server**:监听来自教室网关的笔迹数据流(双向流式RPC)
- **WebSocket Server**:向教室内各终端(黑板/PC/Pad)推送识别结果
- **MQTT Client**:向云端上报算力盒运行状态(GPU利用率/温度/推理QPS)
- **mDNS服务**:在局域网内广播算力盒服务,支持自动发现和集群组建
**管理层(Management Layer**
管理层提供本地运维和远程管理能力:
- Flask管理API:提供RESTful接口用于查询状态、切换模型、查看日志
- SQLite配置库:存储设备配置、模型元信息、推理统计数据
- 日志服务:结构化日志输出,支持日志级别动态调整和远程传输
- 状态监控:定时采样GPU/CPU/内存/温度指标,触发阈值告警
### 2.3 核心架构示意图
**推理数据流完整路径:**
```
点阵笔(BLE
教室网关软件(04-writech-gateway
│ gRPC流式传输(InferenceService.ProcessStroke
算力盒通信层(gRPC Server
业务服务层
├─ StrokePreprocessor(笔迹预处理)
│ ├── 坐标去噪(中值滤波)
│ ├── 坐标归一化([0,1]区间缩放)
│ └── 笔画分割(落笔/抬笔事件切分)
├─ InferenceScheduler(推理调度)
│ ├── 实时任务队列(优先级HIGH,延迟≤200ms)
│ └── 批量任务队列(优先级NORMAL,延迟≤2s)
├─ InferenceEngine(推理执行)
│ ├── OCR识别:手写文字→文本
│ ├── 数学识别:列式→LaTeX+结果
│ └── 笔顺评分:笔画顺序→分数+错误位置
└─ ResultDistributor(结果分发)
├── WebSocket推送→智慧黑板
├── WebSocket推送→教师PC
└── gRPC回调→云端(MQTT异步上报)
```
**云边协同时序图:**
```
算力盒 云端平台
│ │
│── MQTT心跳上报(每30s)──→│
│← MQTT下发指令(检查更新)──│
│ │
│── HTTPS请求模型版本信息 ──→│
│← 返回最新版本元信息 ───────│
│ │
│── HTTPS下载模型文件包 ───→│
│(SHA-256校验 + RSA签名验证)│
│ │
│ 本地解密并加载新模型 │
│ 灰度切换:运行5分钟验证 │
│ 验证通过→正式切换 │
│ │
│── HTTPS上报切换结果 ─────→│
│ │
```
### 2.4 数据设计
**模型文件存储结构:**
```
/opt/writech/
├── models/
│ ├── ocr/
│ │ ├── model_a.rknn # 当前运行版本(加密)
│ │ ├── model_b.rknn # 待切换版本(加密)
│ │ └── model_meta.json # 模型元信息
│ ├── math/
│ │ ├── model_a.onnx
│ │ ├── model_b.onnx
│ │ └── model_meta.json
│ └── stroke_order/
│ ├── model_a.rknn
│ └── model_meta.json
├── cache/
│ ├── offline_results.db # 离线结果缓存(SQLite)
│ └── task_queue.db # 任务队列持久化(SQLite)
├── config/
│ └── edge_config.json # 设备配置文件
└── logs/
├── inference.log # 推理日志(每日轮转)
├── comm.log # 通信日志
└── system.log # 系统运行日志
```
**SQLite数据库表结构:**
`model_registry`表(模型注册表):
| 字段名 | 类型 | 说明 |
|--------|------|------|
| id | INTEGER PRIMARY KEY | 自增主键 |
| model_type | TEXT | 模型类型(ocr/math/stroke_order |
| version | TEXT | 版本号(如 v2.1.3 |
| file_path | TEXT | 加密模型文件路径 |
| accuracy | REAL | 验证集精度指标 |
| file_size | INTEGER | 文件大小(字节) |
| is_active | INTEGER | 是否当前激活(0/1 |
| partition | TEXT | 分区标识(A/B |
| created_at | TEXT | 入库时间(ISO8601 |
| updated_at | TEXT | 最后更新时间 |
`offline_result_cache`表(离线结果缓存):
| 字段名 | 类型 | 说明 |
|--------|------|------|
| id | INTEGER PRIMARY KEY | 自增主键 |
| student_id | TEXT | 学生唯一标识 |
| assignment_id | TEXT | 作业标识 |
| result_type | TEXT | 结果类型(ocr/math/stroke |
| result_json | TEXT | 识别结果JSON序列化数据 |
| infer_time | TEXT | 推理时间戳 |
| is_synced | INTEGER | 是否已同步云端(0/1 |
| retry_count | INTEGER | 重试上传次数 |
`inference_stats`表(推理统计):
| 字段名 | 类型 | 说明 |
|--------|------|------|
| id | INTEGER PRIMARY KEY | 自增主键 |
| model_type | TEXT | 模型类型 |
| latency_ms | INTEGER | 推理延迟(毫秒) |
| gpu_util | REAL | GPU/NPU利用率(百分比) |
| temperature | REAL | 设备温度(摄氏度) |
| timestamp | TEXT | 采样时间 |
`device_config`表(设备配置):
| 字段名 | 类型 | 说明 |
|--------|------|------|
| key | TEXT PRIMARY KEY | 配置键 |
| value | TEXT | 配置值 |
| updated_at | TEXT | 更新时间 |
**内存数据结构(C++):**
```cpp
// 推理任务数据结构(inference/inference_engine.h
struct InferenceTask {
std::string task_id; // 任务唯一IDUUID
std::string student_id; // 学生ID
std::string assignment_id; // 作业ID
InferType infer_type; // 推理类型(OCR/MATH/STROKE_ORDER
int priority; // 优先级(0=实时,1=批量)
std::vector<StrokePoint> strokes; // 预处理后的笔迹数据
int64_t received_ts; // 接收时间戳(毫秒)
int64_t deadline_ts; // 截止时间戳(超时丢弃)
};
// 笔迹坐标点(preprocessing/stroke_preprocessor.h
struct StrokePoint {
float x; // 归一化X坐标 [0.0, 1.0]
float y; // 归一化Y坐标 [0.0, 1.0]
float pressure; // 压感 [0.0, 1.0]
int64_t timestamp; // 毫秒时间戳
bool pen_up; // 是否为抬笔事件
};
// 推理结果数据结构
struct InferenceResult {
std::string task_id; // 对应任务ID
std::string student_id; // 学生ID
InferType result_type; // 结果类型
bool success; // 推理是否成功
std::string result_json; // 结果JSON字符串
float confidence; // 置信度 [0.0, 1.0]
int32_t latency_ms; // 实际推理耗时(毫秒)
};
```
### 2.5 接口设计
**gRPC接口定义(proto/inference_service.proto):**
```protobuf
syntax = "proto3";
package writech.edge;
// 算力盒推理服务接口
service InferenceService {
// 流式接收笔迹,流式返回推理结果(双向流式RPC)
rpc ProcessStroke (stream StrokeRequest) returns (stream InferenceResponse);
// 单次识别请求(一元RPC
rpc RecognizeOnce (RecognizeRequest) returns (InferenceResponse);
// 查询算力盒运行状态
rpc GetStatus (StatusRequest) returns (StatusResponse);
}
// 笔迹数据请求
message StrokeRequest {
string task_id = 1; // 任务ID
string student_id = 2; // 学生ID
string assignment_id = 3; // 作业ID
InferType type = 4; // 推理类型
repeated Point points = 5; // 笔迹坐标列表
int64 timestamp = 6; // 时间戳(毫秒)
}
// 坐标点
message Point {
float x = 1;
float y = 2;
float pressure = 3;
bool pen_up = 4;
}
// 推理类型枚举
enum InferType {
OCR = 0; // 文字OCR识别
MATH = 1; // 数学列式识别
STROKE_ORDER = 2; // 笔顺分析
WRITING_QUALITY = 3; // 书写质量评测
}
// 推理响应
message InferenceResponse {
string task_id = 1; // 任务ID
bool success = 2; // 是否成功
string result_json = 3; // 结果JSON(根据type解析)
float confidence = 4; // 置信度
int32 latency_ms = 5; // 推理耗时(毫秒)
string error_msg = 6; // 错误信息(失败时)
}
// 状态查询响应
message StatusResponse {
string device_id = 1; // 算力盒设备ID
float gpu_util = 2; // GPU/NPU利用率(%
float temperature = 3; // 设备温度(℃)
int32 queue_depth = 4; // 当前任务队列深度
float avg_latency_ms = 5; // 过去1分钟平均推理延迟
int32 active_models = 6; // 当前激活模型数量
bool offline_mode = 7; // 是否处于离线模式
}
```
**MQTT主题设计:**
| 主题 | 方向 | QoS | 说明 |
|------|------|-----|------|
| `edgebox/{device_id}/status` | 盒→云 | 1 | 每30秒上报设备状态(CPU/GPU/温度/QPS |
| `edgebox/{device_id}/command` | 云→盒 | 1 | 云端下发管理指令(重启/模型切换/OTA触发) |
| `edgebox/{device_id}/model/sync` | 云→盒 | 1 | 模型更新通知(包含新版本信息) |
| `edgebox/{device_id}/alarm` | 盒→云 | 2 | 设备告警(过热/推理失败/OOM) |
| `school/{school_id}/edgebox/discover` | 盒→局域网 | 0 | 组播广播,用于集群成员发现 |
**Flask本地管理API**
| 接口路径 | 方法 | 说明 |
|---------|------|------|
| `/api/status` | GET | 查询算力盒当前运行状态 |
| `/api/models` | GET | 列出所有已加载模型及版本 |
| `/api/models/switch` | POST | 切换激活模型版本(A/B切换) |
| `/api/infer/test` | POST | 测试推理接口(上传笔迹数据) |
| `/api/cache/stats` | GET | 查看离线缓存统计信息 |
| `/api/cache/sync` | POST | 手动触发离线数据同步上传 |
| `/api/logs` | GET | 查看最近500行日志 |
| `/api/config` | GET/PUT | 查看/修改设备配置 |
| `/api/restart` | POST | 重启推理服务 |
### 2.6 安全设计
**模型文件安全:**
AI模型是核心知识产权资产。所有模型文件均采用AES-256-GCM加密存储,解密密钥通过设备硬件序列号派生(PBKDF2算法),绑定硬件设备,拷贝到其他设备无法使用。推理运行时内存中解密加载,推理完成后密钥归零清除。
```
密钥派生流程:
设备SN + 固定盐值(salt)──PBKDF2-HMAC-SHA256──→ 256bit主密钥
主密钥 + 模型文件IV ──AES-256-GCM──→ 加密模型文件
```
**通信安全:**
- gRPC通信启用mTLS双向认证,算力盒和网关均需持有系统颁发的X.509证书
- MQTT通信使用TLS 1.3加密,证书由云端证书服务签发
- Flask管理API仅监听`127.0.0.1``192.168.x.x`局域网地址,不暴露外网
**OTA安全机制:**
```
升级包安全验证流程:
1. 下载升级包(HTTPS,服务端证书验证)
2. SHA-256文件完整性校验
3. RSA-2048签名验证(使用内置公钥)
4. 写入B分区(不覆盖当前A分区)
5. 下次启动时Bootloader验证B分区签名
6. 验证通过→切换到B分区启动
7. 运行10分钟无异常→确认升级成功,A分区标记为备份
8. 运行异常→自动回滚至A分区
```
**运行隔离:**
推理进程与管理进程(Flask)运行在独立的Linux进程中,通过Unix Domain Socket通信。推理进程采用`seccomp`系统调用过滤,仅允许必要的系统调用,防止漏洞利用。
**物理安全:**
每台算力盒烧录唯一设备ID和证书,与云端注册信息绑定。未经注册的设备即使接入学校网络,也无法与云端建立认证连接,确保系统不被非授权设备接管。
### 2.7 部署架构
**单教室部署模式(标准配置):**
```
┌─────────────────── 教室局域网 ──────────────────────┐
│ │
│ 点阵笔×40 │
│ │ BLE │
│ ▼ │
│ 网关设备(04-writech-gateway
│ │ gRPCTCP 50051
│ ▼ │
│ 算力盒(05-writech-edge-box
│ ├──WebSocket→ 智慧黑板(09-writech-app-board
│ ├──WebSocket→ 教师PC08-writech-app-pc
│ └──WebSocket→ 学生Pad10-writech-app-pad
│ │
└────────────────────│───────────────────────────────┘
│ MQTT over TLS / HTTPS
云端平台(01-writech-cloud-platform
```
**校级集群部署模式(大规模部署):**
```
┌─────────── 校园网 ──────────────────────────────┐
│ │
│ 教室A: 算力盒A ──┐ │
│ 教室B: 算力盒B ──┤── mDNS集群发现 │
│ 教室C: 算力盒C ──┘── gRPC集群负载均衡 │
│ │ │
│ 校级调度节点 │
│ (主算力盒承担调度角色) │
│ │ │
└───────────────────│─────────────────────────────┘
│ MQTT/HTTPS
云端平台(统一模型版本管理)
```
**A/B分区与存储布局:**
```
eMMC存储分区规划:
┌────────────────────────────────────────────┐
│ Bootloader (8MB) │
├────────────────────────────────────────────┤
│ 系统根分区 / (rootfs) (8GB) │
├────────────────────────────────────────────┤
│ App分区A - 推理软件当前版本 (2GB) │
├────────────────────────────────────────────┤
│ App分区B - 推理软件备份/待升级版本 (2GB) │
├────────────────────────────────────────────┤
│ 模型存储分区A - 当前激活模型 (8GB) │
├────────────────────────────────────────────┤
│ 模型存储分区B - 备份/待升级模型 (8GB) │
├────────────────────────────────────────────┤
│ 数据分区 /data - 缓存/日志/配置 (剩余空间) │
└────────────────────────────────────────────┘
```
---
## 第三章 核心模块功能详细说明
### 3.1 main.cpp — 主程序入口与系统初始化
`main.cpp`是算力盒软件的启动入口,负责整个软件的初始化流程编排和信号处理。
**初始化流程:**
```
main()
├─ 1. 解析命令行参数(配置文件路径、日志级别、平台选择)
├─ 2. 初始化日志系统(spdlog)
│ ├── 创建滚动日志文件(每日轮转,保留7天)
│ └── 初始化控制台日志输出
├─ 3. 加载配置文件(EdgeConfig::getInstance()
│ ├── 读取 edge_config.json
│ └── 初始化 SQLite 配置数据库
├─ 4. 硬件平台探测与初始化
│ ├── 探测NPU/GPU型号(读取 /proc/device-tree 或 nvidia-smi
│ └── 加载对应硬件加速驱动(rknn_init / CUDA初始化)
├─ 5. 初始化推理引擎(InferenceEngine::create(platform)
│ ├── 加载OCR模型(model_a.rknnAES解密)
│ ├── 加载数学识别模型
│ └── 加载笔顺分析模型
├─ 6. 启动业务服务
│ ├── InferenceScheduler(推理调度线程池,4个worker线程)
│ ├── ResultDistributor(结果分发线程)
│ └── OfflineCacheManager(离线缓存管理线程)
├─ 7. 启动通信服务
│ ├── gRPC Server(端口 50051mTLS配置)
│ ├── WebSocket Server(端口 8080,结果推送)
│ └── MQTT Client(连接云端BrokerTLS配置)
├─ 8. 启动管理服务
│ ├── Flask管理APIPython子进程,端口 5000
│ └── mDNS服务广播("_writech-edge._tcp"
├─ 9. 注册信号处理(SIGTERM/SIGINT → 优雅退出)
└─ 10. 进入主事件循环(阻塞等待退出信号)
```
**优雅退出流程:**
收到SIGTERM/SIGINT信号后,软件按以下顺序执行清理:
1. 停止接受新的gRPC请求(停止Accept新连接)
2. 等待正在处理的推理任务完成(超时5秒后强制终止)
3. 将未上报的离线结果刷写到SQLite
4. 关闭MQTT连接(发送DISCONNECT包)
5. 释放NPU/GPU推理资源
6. 关闭日志文件(flush缓冲区)
7. 退出进程
### 3.2 inference/inference_engine.cpp — 推理引擎核心
推理引擎是整个软件的计算核心,负责将预处理后的笔迹数据送入AI模型执行推理,并返回识别结果。
**类结构设计:**
```cpp
// 推理引擎抽象接口(inference/inference_engine.h
class IInferenceEngine {
public:
virtual ~IInferenceEngine() = default;
// 工厂方法,根据平台创建对应引擎实例
static std::unique_ptr<IInferenceEngine> create(
const std::string& platform); // "rknn" / "cuda" / "opencl"
// 初始化推理引擎,加载模型文件
virtual bool initialize(const ModelConfig& config) = 0;
// 执行单次OCR推理
virtual InferenceResult inferOCR(
const std::vector<StrokePoint>& strokes) = 0;
// 执行数学列式识别推理
virtual InferenceResult inferMath(
const std::vector<StrokePoint>& strokes) = 0;
// 执行笔顺分析推理
virtual InferenceResult inferStrokeOrder(
const std::string& target_char,
const std::vector<StrokePoint>& strokes) = 0;
// 热切换模型(切换A/B分区的模型文件,不重启进程)
virtual bool switchModel(const std::string& model_type,
const std::string& new_model_path) = 0;
// 释放推理资源
virtual void shutdown() = 0;
};
```
**RKNN引擎实现(推理执行核心):**
```cpp
// inference/rknn_engine.cppRKNN平台实现)
InferenceResult RKNNEngine::inferOCR(
const std::vector<StrokePoint>& strokes) {
auto start_ts = std::chrono::steady_clock::now();
// Step 1: 将笔迹坐标渲染为灰度图像张量
// 画布大小:224×224像素,笔画宽度3像素
cv::Mat canvas = renderStrokesToImage(strokes, 224, 224);
// Step 2: 图像归一化(减均值/除标准差,与训练预处理一致)
// mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]
cv::Mat normalized = normalizeImage(canvas);
// Step 3: 构建RKNN输入
rknn_input inputs[1];
inputs[0].index = 0;
inputs[0].type = RKNN_TENSOR_FLOAT32;
inputs[0].size = 224 * 224 * 3 * sizeof(float);
inputs[0].fmt = RKNN_TENSOR_NHWC;
inputs[0].buf = normalized.data;
int ret = rknn_inputs_set(ctx_, 1, inputs);
if (ret != RKNN_SUCC) {
return makeErrorResult("rknn_inputs_set failed: " +
std::to_string(ret));
}
// Step 4: 执行NPU推理
ret = rknn_run(ctx_, nullptr);
if (ret != RKNN_SUCC) {
return makeErrorResult("rknn_run failed: " +
std::to_string(ret));
}
// Step 5: 获取推理输出
rknn_output outputs[1];
outputs[0].want_float = 1;
outputs[0].index = 0;
ret = rknn_outputs_get(ctx_, 1, outputs, nullptr);
// Step 6: 后处理(CTC解码,将输出概率矩阵转换为文字)
std::string recognized_text = ctcDecode(
static_cast<float*>(outputs[0].buf),
outputs[0].size / sizeof(float));
float confidence = calculateConfidence(
static_cast<float*>(outputs[0].buf));
rknn_outputs_release(ctx_, 1, outputs);
// Step 7: 计算推理延迟
auto end_ts = std::chrono::steady_clock::now();
int32_t latency = std::chrono::duration_cast<
std::chrono::milliseconds>(end_ts - start_ts).count();
// 记录推理统计
stats_collector_->record(ModelType::OCR, latency,
getNPUUtilization());
return InferenceResult{
.success = true,
.result_json = buildOCRResultJson(recognized_text, confidence),
.confidence = confidence,
.latency_ms = latency
};
}
```
**性能优化设计:**
| 优化策略 | 实现方式 | 效果 |
|---------|---------|------|
| 模型量化 | INT8量化(精度损失<1% | 推理速度提升2-3倍 |
| 批处理推理 | 同一时刻多个学生笔迹合批推理 | GPU利用率提升30% |
| 内存复用 | 推理输入/输出缓冲区预分配复用 | 减少内存分配开销50% |
| 模型预热 | 启动时执行10次dummy推理 | 消除首次推理延迟抖动 |
| CPU+NPU流水线 | 预处理(CPU)与上批推理(NPU)并行执行 | 端到端延迟降低20% |
### 3.3 inference/model_manager.cpp — 模型管理模块
模型管理模块负责AI模型文件的全生命周期管理,包括模型注册、加密存储、版本切换和云端同步更新。
**模型版本切换流程(A/B热切换):**
```
收到云端模型更新通知(MQTT
├─ 1. 检查B分区磁盘空间(是否足够存放新模型)
├─ 2. HTTPS下载新模型文件到/tmp/目录
├─ 3. 验证完整性(SHA-256与服务端校验和比对)
├─ 4. 验证签名(RSA-2048公钥验证模型包签名)
├─ 5. AES解密新模型(临时缓冲区)
├─ 6. 写入B分区(/opt/writech/models/{type}/model_b.rknn
├─ 7. 更新model_meta.jsonB分区版本信息)
├─ 8. 通知InferenceEngine加载B分区模型(不切换激活)
├─ 9. 执行模型验证推理(使用标准测试集,验证精度)
├─ 10. 验证通过→原子切换active字段A→B
│ 验证失败→删除B分区文件,保留A分区,上报失败告警
└─ 11. 向云端确认升级结果(MQTT上报)
```
**模型注册表查询示例(Python管理API):**
```python
# config/edge_config.py — 模型管理相关逻辑
import sqlite3, json
from pathlib import Path
class ModelManager:
def __init__(self, db_path: str):
self.db_path = db_path
self.conn = sqlite3.connect(db_path)
def get_active_model(self, model_type: str) -> dict:
"""获取当前激活的模型元信息"""
cursor = self.conn.cursor()
cursor.execute("""
SELECT version, file_path, accuracy, is_active, partition
FROM model_registry
WHERE model_type = ? AND is_active = 1
ORDER BY updated_at DESC LIMIT 1
""", (model_type,))
row = cursor.fetchone()
if row:
return {
"version": row[0], "file_path": row[1],
"accuracy": row[2], "partition": row[4]
}
return None
def switch_model(self, model_type: str, target_partition: str) -> bool:
"""切换激活分区(A→B 或 B→A"""
try:
cursor = self.conn.cursor()
# 原子性更新:先清除所有active,再设置目标partition为active
cursor.execute("""
UPDATE model_registry
SET is_active = 0
WHERE model_type = ?
""", (model_type,))
cursor.execute("""
UPDATE model_registry
SET is_active = 1
WHERE model_type = ? AND partition = ?
""", (model_type, target_partition))
self.conn.commit()
return True
except sqlite3.Error as e:
self.conn.rollback()
return False
```
### 3.4 inference/npu_scheduler.cpp — NPU/GPU调度器
NPU/GPU调度器是并发推理请求的核心调度组件,实现优先级调度、资源限流和超时管理。
**调度器设计:**
```cpp
// inference/npu_scheduler.cpp
class NPUScheduler {
public:
NPUScheduler(int num_workers = 4);
// 提交推理任务到调度队列
// priority: 0=实时(课堂进行中),1=批量(课后批改)
std::future<InferenceResult> submit(
InferenceTask task, int priority = 0);
// 获取当前队列深度
int getQueueDepth() const;
// 获取平均推理延迟(最近1分钟)
float getAvgLatencyMs() const;
private:
// 优先级队列(最小堆,priority小的优先)
std::priority_queue<
InferenceTask,
std::vector<InferenceTask>,
TaskComparator> task_queue_;
std::mutex queue_mutex_;
std::condition_variable cv_;
// Worker线程池
std::vector<std::thread> workers_;
std::atomic<bool> running_{true};
// 推理引擎实例
std::shared_ptr<IInferenceEngine> engine_;
// Worker线程函数
void workerLoop();
// 超时检测(定时清理超期任务)
void timeoutChecker();
};
```
**任务优先级策略:**
| 任务来源 | 优先级 | 超时时间 | 说明 |
|---------|-------|---------|------|
| 课堂实时书写(笔迹Notify触发) | 0(最高) | 500ms | 学生正在书写中,需立即反馈 |
| 教师互动答题收卷 | 0(最高) | 500ms | 收卷后立即统计展示 |
| 作业批量批改 | 1(普通) | 5000ms | 课后批改,可稍有延迟 |
| 模型验证推理 | 2(低) | 30000ms | 新模型精度验证,不影响教学 |
### 3.5 communication/grpc_server.cpp — gRPC通信服务
gRPC服务是算力盒软件接收笔迹数据的入口,支持与网关软件之间的双向流式通信。
**流式RPC实现:**
```cpp
// communication/grpc_server.cpp
class InferenceServiceImpl : public InferenceService::Service {
public:
// 双向流式RPC:接收笔迹流,实时返回推理结果
grpc::Status ProcessStroke(
grpc::ServerContext* context,
grpc::ServerReaderWriter<InferenceResponse,
StrokeRequest>* stream) override {
StrokeRequest request;
std::string current_student_id;
std::vector<StrokePoint> stroke_buffer;
while (stream->Read(&request)) {
// 检查任务超时(防止僵死连接占用资源)
if (isTaskTimeout(request.task_id())) {
continue;
}
// 累积笔迹点
for (const auto& pt : request.points()) {
stroke_buffer.push_back({
pt.x(), pt.y(), pt.pressure(),
request.timestamp(), pt.pen_up()
});
}
// 检测到抬笔事件 → 触发推理
if (!stroke_buffer.empty() &&
stroke_buffer.back().pen_up) {
// 预处理
auto preprocessed = preprocessor_->process(stroke_buffer);
stroke_buffer.clear();
// 构建推理任务并提交到调度器
InferenceTask task{
.task_id = request.task_id(),
.student_id = request.student_id(),
.infer_type = (InferType)request.type(),
.priority = 0, // 课堂实时,最高优先级
.strokes = preprocessed,
.received_ts = getCurrentMs(),
.deadline_ts = getCurrentMs() + 500
};
auto future = scheduler_->submit(task, 0);
// 异步等待结果(最多等待400ms)
if (future.wait_for(std::chrono::milliseconds(400)) ==
std::future_status::ready) {
InferenceResult result = future.get();
InferenceResponse response;
response.set_task_id(result.task_id);
response.set_success(result.success);
response.set_result_json(result.result_json);
response.set_confidence(result.confidence);
response.set_latency_ms(result.latency_ms);
stream->Write(response);
// 同步触发结果分发到教室终端
distributor_->distribute(request.student_id(), result);
// 离线模式下缓存结果
if (offline_mode_) {
cache_manager_->cache(request.student_id(),
request.assignment_id(), result);
}
}
}
}
return grpc::Status::OK;
}
};
```
**连接管理与限流:**
- 最大并发连接数:100(每个网关建立1个长连接)
- 单连接最大流式请求速率:1000点/秒(超出则背压限流)
- 空闲连接超时:300秒(5分钟无数据则关闭连接)
- 服务器端mTLS:客户端(网关)须持有系统颁发证书
### 3.6 communication/mqtt_client.cpp — MQTT状态上报
MQTT客户端负责算力盒与云端平台的状态同步,采用Eclipse Mosquitto客户端库实现。
**状态上报数据格式(每30秒上报一次):**
```json
{
"device_id": "edge-box-cn-hz-001",
"school_id": "school_hangzhou_001",
"timestamp": 1706845200000,
"hardware": {
"npu_util_pct": 45.2,
"cpu_util_pct": 18.5,
"memory_used_mb": 1820,
"temperature_c": 52.3,
"storage_free_gb": 18.4
},
"inference": {
"total_requests": 1250,
"success_rate_pct": 99.8,
"avg_latency_ms": 87,
"p99_latency_ms": 178,
"queue_depth": 3
},
"models": {
"ocr_version": "v2.1.3",
"math_version": "v1.5.0",
"stroke_order_version": "v1.2.1"
},
"connectivity": {
"offline_mode": false,
"pending_sync_count": 0,
"last_cloud_sync_ts": 1706845170000
}
}
```
**断线重连策略:**
```cpp
// 指数退避重连(最大重连间隔:5分钟)
void MQTTClient::reconnect() {
int retry = 0;
while (!connected_ && running_) {
int wait_ms = std::min(1000 * (1 << retry), 300000);
LOG_WARN("MQTT disconnected, retry in {}ms (attempt {})",
wait_ms, retry + 1);
std::this_thread::sleep_for(
std::chrono::milliseconds(wait_ms));
if (mosquitto_reconnect(mosq_) == MOSQ_ERR_SUCCESS) {
connected_ = true;
LOG_INFO("MQTT reconnected successfully");
// 重订阅所有主题
for (const auto& topic : subscribed_topics_) {
mosquitto_subscribe(mosq_, nullptr,
topic.c_str(), 1);
}
break;
}
retry = std::min(retry + 1, 8); // 最大退避2^8=256秒
}
}
```
### 3.7 preprocessing/stroke_preprocessor.cpp — 笔迹预处理
笔迹预处理模块对原始笔迹坐标数据进行一系列数学处理,提升AI推理的识别精度。
**处理管道(Processing Pipeline):**
```
原始笔迹坐标流(来自网关)
▼ 步骤1:去抖动滤波
│ 中值滤波(窗口大小3),消除传感器噪声抖动
│ 过滤掉 pressure < 0.05 的无效采样点
▼ 步骤2:重采样(等时间间隔→等空间间隔)
│ 将原始时序点重采样为沿笔画路径均匀分布的点集
│ 目标密度:每3像素一个点(适配模型训练时的采样率)
▼ 步骤3:笔画分割
│ 按"抬笔事件(pen_up=true"将连续点流切分为独立笔画
│ 过滤掉点数 < 5 的无效笔画(抖动产生的伪笔画)
▼ 步骤4:包围盒归一化
│ 计算所有笔画的最小包围盒
│ 将坐标缩放到 [0.0, 1.0] × [0.0, 1.0] 范围
│ 保持长宽比(短边padding到正方形后缩放)
▼ 步骤5:笔顺序排序(仅用于笔顺分析任务)
│ 按笔画开始时间戳排序,确保笔画顺序正确
▼ 预处理完成的笔迹张量
```
**关键算法实现:**
```cpp
// preprocessing/stroke_preprocessor.cpp
std::vector<StrokePoint> StrokePreprocessor::normalizeToUnitBox(
const std::vector<StrokePoint>& strokes) {
// 计算所有点的边界框
float min_x = FLT_MAX, max_x = FLT_MIN;
float min_y = FLT_MAX, max_y = FLT_MIN;
for (const auto& p : strokes) {
min_x = std::min(min_x, p.x);
max_x = std::max(max_x, p.x);
min_y = std::min(min_y, p.y);
max_y = std::max(max_y, p.y);
}
float width = max_x - min_x;
float height = max_y - min_y;
float size = std::max(width, height);
// 防止除零(单点笔画)
if (size < 1e-6f) size = 1.0f;
// 居中归一化(短边居中padding)
float offset_x = (size - width) / 2.0f;
float offset_y = (size - height) / 2.0f;
std::vector<StrokePoint> normalized;
normalized.reserve(strokes.size());
for (const auto& p : strokes) {
normalized.push_back({
(p.x - min_x + offset_x) / size,
(p.y - min_y + offset_y) / size,
p.pressure,
p.timestamp,
p.pen_up
});
}
return normalized;
}
```
### 3.8 config/edge_config.py — 配置管理模块
配置管理模块负责算力盒所有运行参数的统一管理,支持本地JSON文件配置和远程动态配置下发。
**配置文件示例(edge_config.json):**
```json
{
"device": {
"device_id": "edge-box-cn-hz-001",
"school_id": "school_hangzhou_001",
"classroom_ids": ["room-a101", "room-a102"],
"hardware_platform": "rknn"
},
"inference": {
"max_concurrent_tasks": 8,
"realtime_timeout_ms": 500,
"batch_timeout_ms": 5000,
"worker_threads": 4,
"batch_size": 4
},
"models": {
"model_base_path": "/opt/writech/models",
"ocr_model": "ocr/model_a.rknn",
"math_model": "math/model_a.onnx",
"stroke_order_model": "stroke_order/model_a.rknn",
"model_encrypt_key_derivation": "PBKDF2-HMAC-SHA256"
},
"communication": {
"grpc_port": 50051,
"grpc_tls_cert": "/etc/writech/certs/server.crt",
"grpc_tls_key": "/etc/writech/certs/server.key",
"grpc_ca_cert": "/etc/writech/certs/ca.crt",
"websocket_port": 8080,
"mqtt_broker": "mqtt.writech.cn",
"mqtt_port": 8883,
"mqtt_client_cert": "/etc/writech/certs/mqtt.crt",
"mqtt_heartbeat_interval_s": 30
},
"storage": {
"db_path": "/opt/writech/cache/edge.db",
"log_path": "/opt/writech/logs",
"log_level": "INFO",
"log_max_size_mb": 50,
"log_keep_days": 7
},
"cloud": {
"cloud_api_base": "https://api.writech.cn",
"model_sync_check_interval_s": 3600,
"offline_cache_max_mb": 512,
"sync_batch_size": 100
}
}
```
### 3.9 离线缓存与数据同步模块
离线缓存模块在网络中断时保存推理结果,网络恢复后批量上传至云端,确保数据不丢失。
**离线模式触发与恢复:**
```
网络状态检测(每10秒ping云端API):
├── 连续3次失败 → 进入离线模式
│ ├── MQTT重连(指数退避)
│ ├── 推理结果写入SQLite离线缓存(而非直接上报)
│ ├── 向已连接终端广播"离线模式通知"
│ └── 状态日志记录
└── 网络恢复 → 退出离线模式
├── MQTT重连成功
├── 触发离线数据同步(批量上传缓存数据)
│ ├── 每批100条记录
│ ├── HTTPS POST到云端 /api/v1/edge/sync
│ ├── 成功后标记 is_synced=1
│ └── 失败则重试(最多5次)
└── 同步完成后清理已同步记录
```
**缓存容量管理:**
- 默认最大缓存容量:512MB
- 超过80%时触发告警(MQTT上报)
- 超过95%时启用FIFO淘汰策略(删除最旧的已推理结果)
- 优先保留未同步的最新结果,防止重要数据丢失
### 3.10 集群管理与负载均衡模块
多台算力盒可组成校级集群,实现统一调度和跨算力盒负载均衡。
**集群组建流程:**
```
算力盒启动时:
├─ 1. 通过mDNS广播自身服务
│ 服务类型:_writech-edge._tcp.local.
│ TXT记录:device_id, school_id, capacity, model_versions
├─ 2. 监听其他算力盒的mDNS广播
│ 收集同一school_id的所有算力盒信息
├─ 3. 选举集群主节点(Raft简化版)
│ 按device_id字典序最小的节点担任主节点
└─ 4. 主节点承担调度职责
├── 收集所有从节点的推理队列深度
├── 将新任务分派给队列最浅的节点
└── 从节点故障时自动接管其任务
```
**负载均衡策略:**
- 默认策略:最小连接数(将新任务分派给当前队列深度最小的节点)
- 温度感知:设备温度超过75℃时降低该节点权重,避免过热
- 模型版本感知:优先分派到与任务类型匹配的模型版本最高的节点
### 3.11 OTA升级模块
OTA模块支持软件固件和AI模型文件的远程在线升级,采用A/B分区方案保证升级安全性。
**升级流程状态机:**
```
IDLE(空闲)
│ 收到MQTT升级通知
CHECKING(检查版本)
│ 版本比对:新版本 > 当前版本
DOWNLOADING(下载中)
│ HTTPS分块下载(支持断点续传)
│ 实时校验SHA-256
VERIFYING(校验中)
│ RSA-2048签名验证
INSTALLING(写入分区B
│ 逐块写入 + 进度上报
TESTING(测试运行)
│ 加载B分区软件/模型,运行10分钟
│ 无错误且推理精度达标
SWITCHING(正式切换)
│ 原子切换A/B标志
DONE(升级完成)
│ MQTT上报成功
IDLE
```
### 3.12 设备监控与运维模块
监控模块实时采集算力盒硬件状态,通过MQTT上报至云端监控平台,支持阈值告警。
**监控指标采集(每10秒采样一次):**
| 指标 | 采集方式 | 告警阈值 |
|------|---------|---------|
| NPU/GPU利用率 | `/sys/kernel/debug/rknpu/load``nvidia-smi` | >90%持续30秒 |
| CPU利用率 | `/proc/stat` | >80%持续60秒 |
| 内存使用率 | `/proc/meminfo` | >90% |
| 设备温度 | `/sys/class/thermal/thermal_zone*/temp` | >80℃告警,>90℃降频 |
| 推理队列深度 | 内存计数器 | >50(积压严重) |
| 磁盘使用率 | `statvfs()` | >90% |
| 推理错误率 | 内存计数器 | >1%(最近100次) |
---
## 第四章 操作流程与使用步骤
### 4.1 设备安装与初始化配置
**步骤一:硬件安装**
将算力盒安装于教室内,通过千兆以太网线连接到教室交换机。同时通过网线连接教室网关(04-writech-gateway设备),确保两者在同一局域网段。
**步骤二:首次上电初始化**
设备上电后自动执行以下初始化流程:
1. 系统自检(BIOS→Bootloader→Linux内核启动)
2. 探测硬件AI加速器(NPU/GPU型号识别)
3. 读取内置配置(出厂默认`edge_config.json`
4. 尝试连接云端注册服务(HTTPS
**步骤三:网络与云端配置**
通过本地管理Web界面进行配置:
```
访问地址:http://192.168.x.x:5000/admin
(设备IP通过路由器DHCP分配,首次可查看设备屏幕或路由器DHCP列表)
配置界面示意:
┌─────────────────────────────────────────────────┐
│ 自然写算力盒 — 初始化配置 │
├─────────────────────────────────────────────────┤
│ [设备基本信息] │
│ 设备ID: [edge-box-cn-hz-001 ] │
│ 学校ID: [school_hangzhou_001 ] │
│ 教室ID: [room-a101, room-a102 ] │
│ │
│ [云端连接配置] │
│ 云端API地址: [api.writech.cn ] │
│ MQTT Broker: [mqtt.writech.cn:8883 ] │
│ 设备证书: [上传 .crt 文件 ] │
│ 设备私钥: [上传 .key 文件 ] │
│ │
│ [网关连接配置] │
│ 网关IP: [192.168.1.100 ] │
│ gRPC端口: [50051 ] │
│ │
│ [保存配置] [测试连接] [重启服务] │
└─────────────────────────────────────────────────┘
```
**步骤四:下载并部署AI模型**
配置完成后,点击"同步模型"按钮,软件将自动从云端下载最新AI模型文件:
```
模型同步界面示意:
┌───────────────────────────────────────────────┐
│ 模型同步 │
├───────────────────────────────────────────────┤
│ ✓ OCR识别模型 v2.1.3 下载中... 67% │
│ ○ 数学识别模型 v1.5.0 等待中... │
│ ○ 笔顺分析模型 v1.2.1 等待中... │
│ │
│ 预计剩余时间:约 3 分钟(依据网络速度) │
│ [暂停] [取消] │
└───────────────────────────────────────────────┘
```
### 4.2 网络接入与云端注册
**设备注册流程:**
1. 通过管理界面上传设备证书(由系统管理员从云端控制台下发)
2. 算力盒自动向`api.writech.cn/v1/device/register`发送注册请求
3. 注册成功后,设备ID和学校ID绑定关系写入云端数据库
4. 算力盒收到注册成功响应,自动订阅MQTT管理主题
**连接状态指示灯(设备前面板):**
| 指示灯 | 颜色/状态 | 说明 |
|--------|---------|------|
| POWER | 绿色常亮 | 系统正常运行 |
| NETWORK | 蓝色常亮 | 网络已连接 |
| NETWORK | 蓝色闪烁 | 网络连接中/重连中 |
| AI | 白色常亮 | AI推理引擎就绪 |
| AI | 白色闪烁 | 正在执行推理 |
| AI | 红色常亮 | AI引擎异常 |
### 4.3 模型加载与推理验证
**推理验证测试(通过管理API):**
```bash
# 通过本地管理API测试推理功能
curl -X POST http://localhost:5000/api/infer/test \
-H "Content-Type: application/json" \
-d '{
"type": "ocr",
"strokes": [
{"x": 0.1, "y": 0.3, "pressure": 0.8, "pen_up": false},
{"x": 0.2, "y": 0.4, "pressure": 0.9, "pen_up": false},
{"x": 0.3, "y": 0.3, "pressure": 0.7, "pen_up": true}
]
}'
# 预期返回:
{
"success": true,
"result_type": "ocr",
"text": "一",
"confidence": 0.95,
"latency_ms": 87
}
```
**推理性能基准测试(出厂验收标准):**
| 测试项目 | 合格标准 | 测试方法 |
|---------|---------|---------|
| OCR单次识别延迟P50 | ≤ 100ms | 连续100次识别取中位数 |
| OCR单次识别延迟P99 | ≤ 200ms | 连续100次识别取P99 |
| 40路并发OCR识别延迟 | ≤ 500ms | 40线程同时提交识别请求 |
| OCR识别准确率 | ≥ 95% | 标准测试集(1000个汉字) |
| 数学识别准确率 | ≥ 92% | 标准算式测试集(500题) |
| NPU持续工作温度 | ≤ 75℃ | 满负载运行1小时 |
### 4.4 课堂教学使用流程
算力盒软件作为后台服务运行,教师和学生无需直接操作算力盒,通过各自的终端APP感知其服务:
**课前准备(教师操作智慧黑板APP):**
```
教师操作步骤:
1. 在智慧黑板APP上选择"开始课堂"
2. 系统自动检测教室内的算力盒状态(绿色=就绪,黄色=初始化中)
3. 教师发布作业/试卷
4. 黑板APP通过WebSocket建立与算力盒的识别结果订阅连接
```
**课中实时识别(学生书写):**
```
学生书写流程:
1. 学生用点阵笔在点阵纸上书写
2. 笔迹坐标通过BLE传输到教室网关
3. 网关通过gRPC将笔迹流实时发送至算力盒
4. 算力盒完成预处理→推理→结果分发(全程<200ms)
5. 识别结果通过WebSocket推送至智慧黑板
6. 黑板大屏实时显示识别结果(文字/分数/笔顺反馈)
```
**课堂互动答题流程:**
```
答题收卷流程示意:
┌──────────────────────────────────────────────────┐
│ │
│ 教师点击"收卷" → 黑板APP发送收卷指令 │
│ │ │
│ ▼ │
│ 算力盒接收收卷指令 │
│ 批量处理已收到的所有学生笔迹(批量推理模式) │
│ │ │
│ ▼ │
│ 识别结果汇总(约5-10秒,取决于班级人数) │
│ │ │
│ ▼ │
│ 推送答题统计结果至黑板大屏: │
│ 正确率分布图 / 学生作答展示 / 典型错误分析 │
│ │
└──────────────────────────────────────────────────┘
```
### 4.5 离线模式操作流程
**离线模式自动切换(无需人工干预):**
当网络中断时,算力盒软件自动切换至离线模式:
```
离线模式状态提示(各终端APP通知):
┌─────────────────────────────────────┐
│ ⚠ 提示 │
│ 当前处于离线模式 │
│ AI识别功能正常可用(本地推理) │
│ 识别结果将在网络恢复后自动同步至云端 │
│ [知道了] │
└─────────────────────────────────────┘
```
**查看离线缓存状态(管理员):**
```bash
# 查询离线缓存统计
curl http://localhost:5000/api/cache/stats
# 返回:
{
"offline_mode": true,
"cached_results": 1250,
"pending_sync": 1250,
"cache_used_mb": 45.2,
"cache_max_mb": 512,
"oldest_record_ts": "2026-02-14T08:30:00Z"
}
```
### 4.6 OTA升级操作流程
**自动OTA升级流程(云端发起):**
1. 运维人员在云端控制台发布新版本(软件或模型)
2. 算力盒通过MQTT收到升级通知
3. 算力盒在后台静默下载升级包(不影响正常推理服务)
4. 下载完成并验证通过后,在教学时段结束后(默认22:00)执行升级切换
5. 升级完成后自动上报结果,云端控制台显示升级状态
**手动OTA触发(管理API):**
```bash
# 触发模型手动升级
curl -X POST http://localhost:5000/api/models/sync \
-H "Content-Type: application/json" \
-d '{"force": true}'
# 切换模型分区(A→B
curl -X POST http://localhost:5000/api/models/switch \
-H "Content-Type: application/json" \
-d '{"model_type": "ocr", "target_partition": "B"}'
```
### 4.7 集群管理操作流程
**查看集群状态:**
```
集群管理界面(主算力盒管理页面):
┌────────────────────────────────────────────────────────────┐
│ 算力盒集群状态 学校:杭州实验小学 │
├──────────────┬───────────┬──────────┬──────────┬───────────┤
│ 设备ID │ 状态 │ NPU利用率 │ 温度 │ 队列深度 │
├──────────────┼───────────┼──────────┼──────────┼───────────┤
│ edge-001 │ ● 正常 │ 45% │ 52℃ │ 3 │
│ edge-002 │ ● 正常 │ 38% │ 49℃ │ 1 │
│ edge-003 │ ● 正常 │ 62% │ 58℃ │ 7 │
│ edge-004 │ ○ 离线 │ - │ - │ - │
├──────────────┴───────────┴──────────┴──────────┴───────────┤
│ 集群总推理QPS: 420 平均延迟: 94ms 离线节点: 1 │
└────────────────────────────────────────────────────────────┘
```
### 4.8 故障排查与日志查看
**常见故障处理手册:**
| 故障现象 | 可能原因 | 处理步骤 |
|---------|---------|---------|
| 推理延迟突然升高(>500ms) | NPU过热降频 | 检查温度,清洁散热孔,降低并发数 |
| MQTT连接断开 | 网络波动 | 检查网络,等待自动重连(指数退避) |
| gRPC连接被拒绝 | 证书过期 | 重新颁发设备证书,重启gRPC服务 |
| 识别准确率下降 | 模型版本过旧 | 手动触发模型同步,更新至最新版 |
| 磁盘空间不足 | 日志/缓存堆积 | 清理旧日志,同步并清理离线缓存 |
| 设备重启后无法启动 | B分区升级包损坏 | Bootloader自动回滚A分区 |
**查看运行日志:**
```bash
# 查看最近100行推理日志
tail -n 100 /opt/writech/logs/inference.log
# 实时跟踪日志
tail -f /opt/writech/logs/inference.log
# 查看错误日志(grep过滤)
grep "ERROR\|WARN" /opt/writech/logs/inference.log | tail -50
# 通过管理API查看(远程访问)
curl http://192.168.x.x:5000/api/logs?level=ERROR&lines=100
```
---
## 第五章 与源代码的对应关系
### 5.1 模块名称与源代码文件对应表
| 文档章节 | 源代码文件 | 编程语言 | 说明 |
|---------|----------|---------|------|
| 主程序入口 | `main.cpp` | C++ | 软件启动入口,各模块初始化编排 |
| 推理引擎核心 | `inference/inference_engine.cpp` | C++ | 推理引擎抽象接口与工厂方法 |
| RKNN推理实现 | `inference/rknn_engine.cpp` | C++ | 瑞芯微NPU推理引擎实现 |
| CUDA推理实现 | `inference/cuda_engine.cpp` | C++ | NVIDIA GPUTensorRT)推理实现 |
| NPU/GPU调度器 | `inference/npu_scheduler.cpp` | C++ | 优先级任务调度器 |
| 模型管理 | `inference/model_manager.cpp` | C++ | 模型生命周期管理 |
| 笔迹预处理 | `preprocessing/stroke_preprocessor.cpp` | C++ | 坐标去噪、归一化、笔画分割 |
| gRPC通信服务 | `communication/grpc_server.cpp` | C++ | 接收网关笔迹数据流 |
| MQTT客户端 | `communication/mqtt_client.cpp` | C++ | 云端状态上报与指令接收 |
| 结果分发器 | `communication/result_distributor.cpp` | C++ | 识别结果推送至终端 |
| 离线缓存管理 | `cache/offline_cache_manager.cpp` | C++ | 离线结果缓存与同步 |
| 集群管理 | `cluster/cluster_manager.cpp` | C++ | mDNS发现与负载均衡 |
| OTA升级 | `ota/ota_manager.cpp` | C++ | 固件与模型远程升级 |
| 配置管理 | `config/edge_config.py` | Python | 配置读写与动态下发处理 |
| 模型管理API | `config/model_manager.py` | Python | 模型注册表管理(Python层) |
| 本地管理API | `management/app.py` | Python | Flask管理Web服务 |
| 监控采集 | `management/monitor.py` | Python | 硬件指标采集与上报 |
| gRPC接口定义 | `proto/inference_service.proto` | Protobuf | gRPC服务接口定义 |
| 构建脚本 | `CMakeLists.txt` | CMake | C++工程构建配置 |
| Docker配置 | `Dockerfile` | Docker | 容器化部署配置 |
### 5.2 核心函数说明
| 函数名 | 所在文件 | 功能说明 |
|--------|---------|---------|
| `main()` | `main.cpp` | 程序入口,按序初始化所有模块 |
| `IInferenceEngine::create()` | `inference_engine.cpp` | 工厂方法,按平台创建推理引擎 |
| `RKNNEngine::inferOCR()` | `rknn_engine.cpp` | RKNN平台OCR推理执行 |
| `RKNNEngine::inferMath()` | `rknn_engine.cpp` | RKNN平台数学识别推理 |
| `NPUScheduler::submit()` | `npu_scheduler.cpp` | 提交推理任务到优先级队列 |
| `NPUScheduler::workerLoop()` | `npu_scheduler.cpp` | Worker线程推理执行循环 |
| `ModelManager::switchModel()` | `model_manager.cpp` | A/B分区模型热切换 |
| `StrokePreprocessor::process()` | `stroke_preprocessor.cpp` | 预处理管道总入口 |
| `StrokePreprocessor::normalizeToUnitBox()` | `stroke_preprocessor.cpp` | 包围盒归一化 |
| `InferenceServiceImpl::ProcessStroke()` | `grpc_server.cpp` | gRPC流式接收并触发推理 |
| `MQTTClient::reconnect()` | `mqtt_client.cpp` | 指数退避断线重连 |
| `ResultDistributor::distribute()` | `result_distributor.cpp` | 结果分发至对应终端 |
| `OfflineCacheManager::cache()` | `offline_cache_manager.cpp` | 离线结果写入SQLite |
| `OfflineCacheManager::syncToCloud()` | `offline_cache_manager.cpp` | 批量同步至云端 |
| `OTAManager::startUpgrade()` | `ota_manager.cpp` | 启动OTA升级流程 |
| `ModelManager::get_active_model()` | `config/model_manager.py` | Python层获取激活模型信息 |
### 5.3 主要类与方法命名规范
**C++命名规范:**
| 规范 | 示例 |
|------|------|
| 类名:大驼峰 | `InferenceEngine``NPUScheduler``StrokePreprocessor` |
| 方法名:小驼峰 | `inferOCR()``switchModel()``normalizeToUnitBox()` |
| 成员变量:小写下划线,末尾加`_` | `task_queue_``running_``engine_` |
| 常量:大写下划线 | `MAX_CONCURRENT_TASKS``DEFAULT_TIMEOUT_MS` |
| 抽象接口:`I`前缀 | `IInferenceEngine``IResultHandler` |
| 实现类:平台前缀+功能 | `RKNNEngine``TRTEngine``OpenCLEngine` |
**Python命名规范(PEP8):**
| 规范 | 示例 |
|------|------|
| 类名:大驼峰 | `ModelManager``EdgeConfig``MonitorCollector` |
| 方法名:小写下划线 | `get_active_model()``switch_model()` |
| 配置键:小写下划线 | `device_id``mqtt_broker` |
---
## 附录A 硬件接口说明
### A.1 外部接口规格
| 接口类型 | 数量 | 规格 | 用途 |
|---------|------|------|------|
| RJ45以太网 | 2路 | 1000Base-T | 教室网络接入(网关互联 + 上行) |
| USB 3.0 | 4路 | Type-A | 外接存储/调试 |
| HDMI | 1路 | HDMI 2.0 | 本地监控输出 |
| 串口 | 1路 | RS232/RS485 | 调试控制台 |
| 电源 | 1路 | DC 12V/5A | 标准电源适配器 |
### A.2 网络端口使用说明
| 端口 | 协议 | 用途 | 方向 |
|------|------|------|------|
| 50051 | TCPgRPC | 接收网关笔迹数据流 | 入 |
| 8080 | TCPWebSocket | 向终端推送识别结果 | 出 |
| 5000 | TCPHTTP | 本地管理API | 双向 |
| 8883 | TCPMQTT over TLS | 云端状态同步 | 出 |
| 5353 | UDPmDNS | 集群成员发现 | 双向 |
| 443 | TCPHTTPS | 模型同步、OTA下载 | 出 |
---
## 附录B 术语表
| 术语 | 说明 |
|------|------|
| 算力盒 | 部署在教室内的边缘AI计算设备 |
| NPU | Neural Processing Unit,神经网络处理单元,专用于AI推理加速 |
| RKNN | 瑞芯微神经网络工具套件,支持RK3588等芯片的NPU推理 |
| TensorRT | NVIDIA的深度学习推理优化库 |
| ONNX | Open Neural Network Exchange,开放神经网络交换格式 |
| PaddleLite | 飞桨轻量化推理框架,适用于ARM移动端/嵌入式端 |
| gRPC | Google远程过程调用框架,基于HTTP/2 + Protobuf |
| mTLS | Mutual TLS,双向TLS认证 |
| mDNS | Multicast DNS,局域网内零配置服务发现 |
| A/B分区 | 双分区升级方案,保证升级失败可回滚 |
| INT8量化 | 将模型权重从FP32压缩为INT8整型,提升推理速度 |
| CTC解码 | Connectionist Temporal ClassificationOCR输出序列解码算法 |
| PBKDF2 | Password-Based Key Derivation Function 2,基于密码的密钥派生函数 |
| QPS | Queries Per Second,每秒查询(推理)次数 |
| AES-256-GCM | 256位密钥的AES加密,GCM模式(提供认证加密) |
---
## 附录C 版本历史
| 版本 | 日期 | 变更说明 | 编制人 |
|------|------|---------|--------|
| V0.5 Beta | 2025-08-01 | 基础推理框架搭建,单模型单路OCR识别 | 研发团队 |
| V0.8 Beta | 2025-10-15 | 增加数学识别、笔顺分析模型;gRPC通信框架 | 研发团队 |
| V0.9 RC | 2025-12-01 | 离线缓存、集群管理、MQTT状态上报 | 研发团队 |
| V1.0 | 2026-02-14 | 正式版:完整OTA升级、A/B分区、安全加固 | 研发团队 |
---
*文档编制:深圳自然写科技有限公司 研发部*
*文档版本:V1.0*
*最后更新:2026年2月14日*
*版权所有 © 2026 深圳自然写科技有限公司*
---
## 附录D 核心技术实现详述
### D.1 gRPC服务接口定义
算力盒通过gRPC协议对外提供AI推理服务,接口使用Protocol Buffers 3定义。
#### D.1.1 Protobuf接口定义
```protobuf
// proto/inference.proto
syntax = "proto3";
package writech.edge;
option java_package = "com.writech.edge.proto";
// 笔迹推理请求
message InferenceRequest {
string request_id = 1; // 请求唯一ID(用于幂等去重)
string task_type = 2; // 任务类型:OCR/MATH/STROKE_EVAL/GRAMMAR
bytes ink_data = 3; // 压缩笔迹数据(Deflate压缩)
InkMetadata metadata = 4; // 笔迹元数据
InferenceConfig config = 5; // 推理配置(可选)
}
message InkMetadata {
string student_id = 1; // 学生ID
string session_id = 2; // 课堂会话ID
string homework_id = 3; // 作业ID
int64 capture_time = 4; // 采集时间戳(毫秒)
float canvas_width = 5; // 画布宽度(mm
float canvas_height = 6; // 画布高度(mm
}
message InferenceConfig {
float confidence_threshold = 1; // 识别置信度阈值(默认0.7
bool return_candidates = 2; // 是否返回候选结果列表
int32 max_candidates = 3; // 最多返回候选数(默认5
bool use_language_model = 4; // 是否启用语言模型后处理
}
// 推理响应
message InferenceResponse {
string request_id = 1;
string task_type = 2;
int32 status_code = 3; // 0=成功,非0=错误码
string error_message = 4;
oneof result {
OcrResult ocr_result = 10;
MathResult math_result = 11;
StrokeResult stroke_result = 12;
GrammarResult grammar_result = 13;
}
int64 processing_time_us = 20; // 推理耗时(微秒)
float confidence = 21; // 整体置信度
}
message OcrResult {
string text = 1; // 识别出的文本
repeated CharBox char_boxes = 2; // 每个字符的位置框
repeated string candidates = 3; // 候选文本列表
string language = 4; // 识别出的语言(zh/en
}
message CharBox {
string char_value = 1;
float x = 2; float y = 3;
float width = 4; float height = 5;
float confidence = 6;
}
message MathResult {
string latex = 1; // LaTeX数学公式表达式
string plain_text = 2; // 纯文本表示(如 "x^2 + 2x + 1"
float confidence = 3;
}
message StrokeResult {
float stroke_score = 1; // 笔顺评分(0-100
string feedback = 2; // 笔顺评价反馈文字
repeated StrokeError errors = 3; // 错误笔顺列表
}
message StrokeError {
int32 stroke_index = 1; // 出错的笔画序号(1-based
string description = 2; // 错误描述
string correct_order = 3; // 正确顺序说明
}
message GrammarResult {
repeated GrammarError errors = 1;
int32 error_count = 2;
string corrected_text = 3;
}
message GrammarError {
int32 start_pos = 1;
int32 end_pos = 2;
string error_type = 3;
string suggestion = 4;
}
// gRPC服务定义
service EdgeInferenceService {
// 单次推理(同步)
rpc Infer(InferenceRequest) returns (InferenceResponse);
// 批量推理(异步流式)
rpc InferBatch(stream InferenceRequest) returns (stream InferenceResponse);
// 健康检查
rpc HealthCheck(HealthCheckRequest) returns (HealthCheckResponse);
// 获取设备状态(GPU占用、内存、温度等)
rpc GetDeviceStatus(DeviceStatusRequest) returns (DeviceStatusResponse);
}
message HealthCheckRequest {}
message HealthCheckResponse {
string status = 1; // "SERVING" / "NOT_SERVING"
float gpu_utilization = 2; // GPU使用率(0-100
float memory_used_mb = 3; // 已用显存(MB
float temperature_c = 4; // GPU温度(摄氏度)
int32 queue_depth = 5; // 当前推理队列深度
}
```
### D.2 TensorRT模型推理引擎
算力盒使用NVIDIA TensorRT对ONNX模型进行加速,实现低延迟边缘推理。
#### D.2.1 TensorRT推理封装
```cpp
// src/inference/trt_engine.cpp
#include "trt_engine.h"
#include <NvInfer.h>
#include <cuda_runtime.h>
#include <fstream>
class Logger : public nvinfer1::ILogger {
void log(Severity severity, const char* msg) noexcept override {
if (severity <= Severity::kWARNING) {
LOG_WARN("[TRT] %s", msg);
}
}
} gLogger;
TrtEngine::TrtEngine(const std::string& engine_path, int max_batch_size)
: max_batch_size_(max_batch_size) {
// 加载序列化的TensorRT引擎文件
std::ifstream file(engine_path, std::ios::binary);
if (!file.good()) {
throw std::runtime_error("Engine file not found: " + engine_path);
}
std::vector<char> buffer(
(std::istreambuf_iterator<char>(file)),
std::istreambuf_iterator<char>()
);
runtime_ = nvinfer1::createInferRuntime(gLogger);
engine_ = runtime_->deserializeCudaEngine(buffer.data(), buffer.size());
context_ = engine_->createExecutionContext();
// 分配GPU显存缓冲区
int n_bindings = engine_->getNbBindings();
buffers_.resize(n_bindings);
for (int i = 0; i < n_bindings; i++) {
nvinfer1::Dims dims = engine_->getBindingDimensions(i);
size_t volume = max_batch_size_;
for (int d = 1; d < dims.nbDims; d++) volume *= dims.d[d];
cudaMalloc(&buffers_[i], volume * sizeof(float));
}
cudaStreamCreate(&cuda_stream_);
LOG_INFO("TRT engine loaded: %s, bindings=%d", engine_path.c_str(), n_bindings);
}
/**
* 执行同步推理
* @param input_data 输入浮点数组(batch × channels × height × width
* @param batch_size 实际batch大小
* @param output_data 输出浮点数组
*/
void TrtEngine::infer(const float* input_data, int batch_size, float* output_data) {
// 设置动态batch尺寸
nvinfer1::Dims input_dims = engine_->getBindingDimensions(0);
input_dims.d[0] = batch_size;
context_->setBindingDimensions(0, input_dims);
// 计算输入数据大小
size_t input_size = batch_size;
for (int d = 1; d < input_dims.nbDims; d++) input_size *= input_dims.d[d];
// H2DCPU -> GPU
cudaMemcpyAsync(buffers_[0], input_data,
input_size * sizeof(float), cudaMemcpyHostToDevice, cuda_stream_);
// 推理
context_->enqueueV2(buffers_.data(), cuda_stream_, nullptr);
// D2HGPU -> CPU
nvinfer1::Dims output_dims = engine_->getBindingDimensions(1);
size_t output_size = batch_size;
for (int d = 1; d < output_dims.nbDims; d++) output_size *= output_dims.d[d];
cudaMemcpyAsync(output_data, buffers_[1],
output_size * sizeof(float), cudaMemcpyDeviceToHost, cuda_stream_);
// 等待推理完成
cudaStreamSynchronize(cuda_stream_);
}
TrtEngine::~TrtEngine() {
cudaStreamDestroy(cuda_stream_);
for (auto& buf : buffers_) {
if (buf) cudaFree(buf);
}
delete context_;
delete engine_;
delete runtime_;
}
```
### D.3 推理任务调度器
```python
# src/scheduler/inference_scheduler.py
import asyncio
import time
from collections import deque
from typing import Dict, List, Optional
from concurrent.futures import ThreadPoolExecutor
import logging
logger = logging.getLogger(__name__)
class InferenceTask:
"""单个推理任务"""
def __init__(self, request_id: str, task_type: str, data: bytes, priority: int = 0):
self.request_id = request_id
self.task_type = task_type
self.data = data
self.priority = priority
self.created_at = time.monotonic()
self.future: asyncio.Future = None
class InferenceScheduler:
"""
推理任务调度器
- 支持优先级队列(实时课堂 > 批改作业 > 后台分析)
- 支持动态批处理(自动等待凑批)
- 支持超时保护
"""
PRIORITY_HIGH = 10 # 实时课堂笔迹识别
PRIORITY_MEDIUM = 5 # 作业批改
PRIORITY_LOW = 1 # 后台统计分析
MAX_BATCH_SIZE = 8
BATCH_TIMEOUT_MS = 20 # 最长等待凑批时间(毫秒)
TASK_TIMEOUT_S = 5.0 # 任务超时时间(秒)
def __init__(self, engines: Dict):
self.engines = engines # {'ocr': TrtEngine, 'math': TrtEngine, ...}
self.queues: Dict[str, deque] = {
'ocr': deque(), 'math': deque(),
'stroke': deque(), 'grammar': deque()
}
self.executor = ThreadPoolExecutor(max_workers=4)
self.running = False
async def submit(self, task: InferenceTask) -> dict:
"""提交推理任务,返回推理结果"""
loop = asyncio.get_event_loop()
task.future = loop.create_future()
self.queues[task.task_type].append(task)
logger.debug(f"Task queued: {task.request_id}, type={task.task_type}, "
f"queue_len={len(self.queues[task.task_type])}")
try:
result = await asyncio.wait_for(task.future, timeout=self.TASK_TIMEOUT_S)
return result
except asyncio.TimeoutError:
logger.error(f"Task timeout: {task.request_id}")
raise TimeoutError(f"Inference timeout for {task.request_id}")
async def _dispatch_loop(self):
"""调度主循环:定期从队列取批量任务执行"""
while self.running:
for task_type, queue in self.queues.items():
if not queue:
continue
# 等待凑批
await asyncio.sleep(self.BATCH_TIMEOUT_MS / 1000)
# 提取一批任务(按优先级排序)
batch_tasks = []
while queue and len(batch_tasks) < self.MAX_BATCH_SIZE:
batch_tasks.append(queue.popleft())
batch_tasks.sort(key=lambda t: -t.priority)
if batch_tasks:
asyncio.create_task(
self._run_batch(task_type, batch_tasks)
)
await asyncio.sleep(0.001)
async def _run_batch(self, task_type: str, tasks: List[InferenceTask]):
"""在线程池中执行批量推理(避免阻塞事件循环)"""
loop = asyncio.get_event_loop()
try:
results = await loop.run_in_executor(
self.executor,
self._do_batch_inference,
task_type, tasks
)
for task, result in zip(tasks, results):
if not task.future.done():
task.future.set_result(result)
except Exception as e:
logger.error(f"Batch inference failed: {e}", exc_info=True)
for task in tasks:
if not task.future.done():
task.future.set_exception(e)
def _do_batch_inference(self, task_type: str,
tasks: List[InferenceTask]) -> List[dict]:
"""实际推理(在线程池中执行,不阻塞事件循环)"""
engine = self.engines.get(task_type)
if engine is None:
raise ValueError(f"No engine for task type: {task_type}")
start = time.monotonic()
# 准备批量输入(预处理:笔迹数据 -> 模型输入张量)
batch_input = self._preprocess_batch(task_type, tasks)
# 调用TRT推理
batch_output = engine.infer_batch(batch_input)
# 后处理:模型输出 -> 结构化结果
results = self._postprocess_batch(task_type, tasks, batch_output)
elapsed_us = int((time.monotonic() - start) * 1_000_000)
logger.info(f"Batch {task_type} x{len(tasks)}: {elapsed_us}us, "
f"avg={elapsed_us//len(tasks)}us/task")
return results
```
### D.4 MQTT状态上报
算力盒通过MQTT协议向云端上报设备状态(心跳、推理统计、告警信息)。
```python
# src/monitor/mqtt_reporter.py
import json, time, asyncio
import paho.mqtt.client as mqtt
import psutil
import subprocess
class MqttStatusReporter:
REPORT_INTERVAL = 60 # 每60秒上报一次
def __init__(self, broker: str, port: int, device_id: str, token: str):
self.device_id = device_id
self.topic_status = f"edge/{device_id}/status"
self.topic_alert = f"edge/{device_id}/alert"
self.client = mqtt.Client(client_id=device_id, protocol=mqtt.MQTTv5)
self.client.username_pw_set(device_id, token)
self.client.tls_set() # 启用TLS加密
self.client.connect_async(broker, port)
self.client.loop_start()
async def report_loop(self):
"""定期上报设备状态"""
while True:
status = self._collect_status()
payload = json.dumps(status)
result = self.client.publish(self.topic_status, payload, qos=1)
if result.rc != mqtt.MQTT_ERR_SUCCESS:
# MQTT上报失败,写入本地日志
logging.warning(f"MQTT publish failed: rc={result.rc}")
await asyncio.sleep(self.REPORT_INTERVAL)
def _collect_status(self) -> dict:
"""采集设备运行状态"""
# GPU状态(nvidia-smi
gpu_info = self._get_gpu_info()
return {
"device_id": self.device_id,
"timestamp": int(time.time() * 1000),
"cpu_percent": psutil.cpu_percent(interval=1),
"memory_percent": psutil.virtual_memory().percent,
"disk_percent": psutil.disk_usage('/').percent,
"gpu_utilization": gpu_info.get('utilization', 0),
"gpu_memory_used_mb": gpu_info.get('memory_used', 0),
"gpu_temperature_c": gpu_info.get('temperature', 0),
"inference_stats": self._get_inference_stats(),
"uptime_s": int(time.monotonic()),
}
def _get_gpu_info(self) -> dict:
try:
out = subprocess.check_output([
'nvidia-smi', '--query-gpu=utilization.gpu,memory.used,temperature.gpu',
'--format=csv,noheader,nounits'
]).decode().strip()
parts = out.split(', ')
return {
'utilization': int(parts[0]),
'memory_used': int(parts[1]),
'temperature': int(parts[2]),
}
except Exception:
return {}
```
---
## 附录E 部署与运维手册
### E.1 算力盒初始化配置
```bash
#!/bin/bash
# deploy/setup_edge_box.sh - 算力盒初始化脚本
set -e
DEVICE_ID=$1
DEVICE_TOKEN=$2
CLOUD_ENDPOINT=$3
if [ -z "$DEVICE_ID" ] || [ -z "$DEVICE_TOKEN" ]; then
echo "Usage: $0 <device_id> <token> <cloud_endpoint>"
exit 1
fi
echo "=== Writech Edge Box Setup ==="
echo "Device ID: $DEVICE_ID"
# 1. 写入设备配置文件
cat > /etc/writech/edge_config.json << EOF
{
"device_id": "$DEVICE_ID",
"device_token": "$DEVICE_TOKEN",
"cloud_endpoint": "$CLOUD_ENDPOINT",
"grpc_port": 50051,
"mqtt_broker": "mqtt.writech.com",
"mqtt_port": 8883,
"models_path": "/opt/writech/models",
"cache_path": "/var/writech/cache",
"log_level": "INFO",
"max_batch_size": 8,
"inference_workers": 4
}
EOF
# 2. 配置systemd服务(开机自启)
cat > /etc/systemd/system/writech-edge.service << EOF
[Unit]
Description=Writech Edge Box Inference Service
After=network.target
[Service]
Type=simple
User=writech
WorkingDirectory=/opt/writech/edge
ExecStart=/opt/writech/edge/bin/edge_server --config /etc/writech/edge_config.json
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
Environment=CUDA_VISIBLE_DEVICES=0
Environment=TRT_LOGGER_VERBOSITY=2
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable writech-edge
systemctl start writech-edge
echo "=== Setup Complete ==="
echo "Service status:"
systemctl status writech-edge --no-pager
```
### E.2 模型更新(OTA
算力盒支持通过MQTT控制指令触发远程模型更新(OTA),更新过程使用A/B双目录切换,保证更新失败时自动回滚。
| 步骤 | 操作 | 说明 |
|------|------|------|
| 1 | 云端推送OTA指令 | MQTT topic: `edge/{id}/control`payload: `{"cmd":"ota","url":"...","md5":"..."}` |
| 2 | 算力盒下载模型包 | HTTPS下载到 `/var/writech/ota_staging/` |
| 3 | MD5完整性校验 | 校验通过后继续,失败则上报告警并放弃 |
| 4 | 切换到备用目录 | 将新模型写入 `/opt/writech/models_b/`(当前使用 `_a/` |
| 5 | 热重载推理引擎 | 发送SIGUSR1信号,推理服务无缝加载新模型(不停服) |
| 6 | 验证新模型可用性 | 运行内置测试用例,P99延迟正常则提交更新 |
| 7 | 更新active目录符号链接 | `/opt/writech/models``models_b/` |
| 8 | 上报更新完成状态 | MQTT上报version和更新时间 |
---
*文档编制:深圳自然写科技有限公司 研发部*
*文档版本:V1.0(附录更新)*
*最后更新:2026年2月14日*
*版权所有 © 2026 深圳自然写科技有限公司*
---
## 附录F 性能基准测试
### F.1 推理延迟基准(NVIDIA Jetson AGX Xavier
| 任务类型 | 模型 | 批大小 | P50延迟 | P99延迟 | 吞吐量 |
|---------|------|--------|--------|--------|--------|
| 中文OCR | DB+CRNN TRT FP16 | 1 | 12ms | 28ms | 83 req/s |
| 中文OCR | DB+CRNN TRT FP16 | 8 | 65ms | 95ms | 123 req/s |
| 数学公式识别 | Im2Latex TRT INT8 | 1 | 18ms | 42ms | 55 req/s |
| 笔顺评分 | GRU TRT FP16 | 1 | 5ms | 11ms | 200 req/s |
| 语法检查 | LSTM TRT FP16 | 4 | 22ms | 48ms | 182 req/s |
### F.2 资源占用
| 指标 | 空载 | 满载(8并发) |
|------|------|-------------|
| GPU使用率 | 3% | 78% |
| 显存占用 | 1.2GB | 5.8GB |
| CPU使用率(8核) | 8% | 45% |
| 内存占用 | 2.1GB | 4.6GB |
| 功耗 | 18W | 55W |
| 散热温度 | 45°C | 72°C |
### F.3 可靠性测试
| 测试项目 | 持续时间 | 结果 |
|---------|---------|------|
| 连续运行 | 30天 | 0次崩溃,内存无泄漏 |
| 网络断线重连 | 200次 | 100%恢复,平均重连3.1秒 |
| OTA升级(A/B切换) | 50次 | 49次成功,1次回滚成功 |
| 高温环境(45°C)运行 | 72小时 | 温度保护触发2次,自动降频恢复 |
---
*本文档版权归深圳自然写科技有限公司所有,仅用于软件著作权登记鉴别,请勿用于其他商业用途。*
---
## 附录G 算力盒硬件规格与部署说明
### G.1 硬件规格
| 组件 | 规格 | 说明 |
|------|------|------|
| 主处理器 | NVIDIA Jetson AGX Xavier 32GB | ARM Cortex-A57 8核,Volta GPU512 CUDA核) |
| 内存 | 32GB LPDDR4x | 统一内存架构(CPU+GPU共享) |
| 存储 | 64GB eMMC + 1TB NVMe SSD | 系统+模型存储,日志数据存储 |
| 网络 | 千兆以太网 × 2 + WiFi 6 | 有线接校园网,WiFi备用 |
| 操作系统 | Ubuntu 20.04 LTS + JetPack 5.x | CUDA 11.4 + TensorRT 8.x |
| 功耗 | 15-30W(自适应) | 低负载自动降频节能 |
| 工作温度 | -10°C ~ 50°C | 工业级,适应教室环境 |
| 外形尺寸 | 105mm × 105mm × 65mm | 可壁挂或桌面放置 |
### G.2 软件组件版本
| 组件 | 版本 | 说明 |
|------|------|------|
| Python | 3.8.x | 推理服务主语言 |
| ONNX Runtime | 1.15.x | 模型推理框架(CUDA EP |
| TensorRT | 8.5.x | NVIDIA GPU加速推理 |
| gRPC | 1.54.x | 内部通信协议 |
| paho-mqtt | 1.6.x | MQTT客户端 |
| OpenCV | 4.7.x | 图像预处理 |
| Prometheus Client | 0.17.x | 指标暴露 |
| FastAPI | 0.100.x | HTTP REST管理接口 |
### G.3 网络架构
```
教室局域网(192.168.x.x/24
├── 算力盒(固定IP或DHCP
│ ├── gRPC :50051 ← 接受来自网关的推理请求
│ ├── MQTT 上行 → mqtt.writech.com:8883TLS
│ ├── HTTP :8080 ← 本地管理界面(仅内网)
│ └── Prometheus :9090 ← 监控指标采集
├── 网关设备(192.168.x.10
│ └── gRPC → 算力盒:50051(发送笔迹推理请求)
└── 学校路由器
└── WAN → 互联网(MQTT、OTA更新)
```
---
*本文档版权归深圳自然写科技有限公司所有,技术细节仅用于软件著作权登记鉴别。*
---
## 附录G 补充技术规格
### G.1 边缘推理优化详解
#### G.1.1 TensorRT推理引擎集成
算力盒集成NVIDIA TensorRT加速引擎,对OCR模型进行INT8量化优化:
```cpp
// tensorrt_engine.cpp
#include "NvInfer.h"
#include "NvOnnxParser.h"
class TensorRTEngine {
public:
bool buildFromOnnx(const std::string& onnx_path, bool use_int8) {
auto builder = nvinfer1::createInferBuilder(logger_);
auto config = builder->createBuilderConfig();
auto network = builder->createNetworkV2(
1U << static_cast<uint32_t>(
nvinfer1::NetworkDefinitionCreationFlag::kEXPLICIT_BATCH));
auto parser = nvonnxparser::createParser(*network, logger_);
parser->parseFromFile(onnx_path.c_str(),
static_cast<int>(nvinfer1::ILogger::Severity::kWARNING));
config->setMaxWorkspaceSize(1 << 28); // 256MB
if (use_int8) {
config->setFlag(nvinfer1::BuilderFlag::kINT8);
config->setInt8Calibrator(calibrator_.get());
}
engine_ = builder->buildEngineWithConfig(*network, *config);
context_ = engine_->createExecutionContext();
return engine_ != nullptr;
}
std::vector<float> infer(const cv::Mat& input) {
// 预处理:归一化到[-1,1]
cv::Mat blob;
cv::dnn::blobFromImage(input, blob, 1.0/127.5,
cv::Size(320, 32), cv::Scalar(127.5), true, false);
// 绑定输入输出缓冲区
void* buffers[2];
cudaMalloc(&buffers[0], input_size_);
cudaMalloc(&buffers[1], output_size_);
cudaMemcpy(buffers[0], blob.data, input_size_, cudaMemcpyHostToDevice);
context_->executeV2(buffers);
std::vector<float> output(output_size_ / sizeof(float));
cudaMemcpy(output.data(), buffers[1], output_size_, cudaMemcpyDeviceToHost);
cudaFree(buffers[0]);
cudaFree(buffers[1]);
return output;
}
private:
nvinfer1::ICudaEngine* engine_ = nullptr;
nvinfer1::IExecutionContext* context_ = nullptr;
std::unique_ptr<Int8Calibrator> calibrator_;
size_t input_size_, output_size_;
Logger logger_;
};
```
#### G.1.2 批处理队列优化
批处理请求聚合,提高GPU利用率:
```cpp
// batch_processor.cpp
class BatchProcessor {
static const int MAX_BATCH = 8;
static const int WAIT_MS = 10;
struct InferRequest {
cv::Mat image;
std::promise<InferResult> result;
std::chrono::steady_clock::time_point enqueue_time;
};
std::queue<InferRequest> queue_;
std::mutex mtx_;
std::condition_variable cv_;
public:
void processingLoop() {
while (running_) {
std::vector<InferRequest> batch;
{
std::unique_lock<std::mutex> lock(mtx_);
cv_.wait_for(lock, std::chrono::milliseconds(WAIT_MS),
[this] { return !queue_.empty(); });
// 聚合批次
while (!queue_.empty() && batch.size() < MAX_BATCH) {
batch.push_back(std::move(queue_.front()));
queue_.pop();
}
}
if (!batch.empty()) {
// 批量推理
std::vector<cv::Mat> images;
for (auto& req : batch) images.push_back(req.image);
auto results = engine_.inferBatch(images);
for (size_t i = 0; i < batch.size(); i++) {
batch[i].result.set_value(results[i]);
}
}
}
}
};
```
### G.2 网络拓扑自动发现
#### G.2.1 mDNS服务注册
```cpp
// mdns_service.cpp
#include <dns_sd.h>
class MdnsService {
DNSServiceRef service_ref_ = nullptr;
public:
bool registerService(const char* name, uint16_t port) {
// 构建TXT记录
TXTRecordRef txt;
TXTRecordCreate(&txt, 0, nullptr);
TXTRecordSetValue(&txt, "version", 3, "1.0");
TXTRecordSetValue(&txt, "model", 8, "EdgeBox1");
TXTRecordSetValue(&txt, "caps", 7, "ocr,tts");
DNSServiceErrorType err = DNSServiceRegister(
&service_ref_,
0, // flags
0, // interfaceIndex (all)
name, // service name
"_writech._tcp.", // service type
nullptr, // domain (default)
nullptr, // host (default)
htons(port), // port
TXTRecordGetLength(&txt), // txt length
TXTRecordGetBytesPtr(&txt), // txt record
registerCallback, // callback
this // context
);
TXTRecordDeallocate(&txt);
return err == kDNSServiceErr_NoError;
}
static void registerCallback(DNSServiceRef sdRef,
DNSServiceFlags flags, DNSServiceErrorType err,
const char* name, const char* type, const char* domain, void* ctx) {
if (err == kDNSServiceErr_NoError) {
LOG_INFO("mDNS registered: %s.%s%s", name, type, domain);
}
}
};
```
### G.3 本地模型管理
#### G.3.1 模型版本控制
```python
# model_manager.py
import hashlib
import json
from pathlib import Path
class ModelManager:
MODEL_DIR = Path("/opt/writech/models")
MANIFEST_FILE = MODEL_DIR / "manifest.json"
def __init__(self):
self.manifest = self._load_manifest()
def _load_manifest(self):
if self.MANIFEST_FILE.exists():
with open(self.MANIFEST_FILE) as f:
return json.load(f)
return {"models": {}}
def verify_model(self, name: str) -> bool:
"""校验模型文件完整性"""
if name not in self.manifest["models"]:
return False
info = self.manifest["models"][name]
model_path = self.MODEL_DIR / info["filename"]
if not model_path.exists():
return False
# SHA256校验
sha256 = hashlib.sha256()
with open(model_path, "rb") as f:
for chunk in iter(lambda: f.read(8192), b""):
sha256.update(chunk)
return sha256.hexdigest() == info["sha256"]
def update_model(self, name: str, url: str, expected_hash: str):
"""从云端更新模型"""
import requests
LOG.info(f"Downloading model {name} from {url}")
response = requests.get(url, stream=True, timeout=300)
tmp_path = self.MODEL_DIR / f"{name}.tmp"
sha256 = hashlib.sha256()
with open(tmp_path, "wb") as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
sha256.update(chunk)
actual_hash = sha256.hexdigest()
if actual_hash != expected_hash:
tmp_path.unlink()
raise ValueError(f"Hash mismatch: {actual_hash} != {expected_hash}")
# 原子替换
final_path = self.MODEL_DIR / f"{name}.engine"
tmp_path.rename(final_path)
self.manifest["models"][name] = {
"filename": f"{name}.engine",
"sha256": expected_hash,
"updated_at": datetime.utcnow().isoformat()
}
self._save_manifest()
LOG.info(f"Model {name} updated successfully")
```
### G.4 健康监控与告警
#### G.4.1 系统指标采集
```python
# health_monitor.py
import psutil
import GPUtil
import time
import threading
class HealthMonitor:
ALERT_CPU_THRESHOLD = 90.0 # CPU使用率告警阈值
ALERT_MEM_THRESHOLD = 85.0 # 内存使用率告警阈值
ALERT_GPU_TEMP_THRESHOLD = 80 # GPU温度告警阈值(℃)
ALERT_DISK_THRESHOLD = 90.0 # 磁盘使用率告警阈值
def __init__(self, alert_callback):
self.alert_cb = alert_callback
self.running = False
def start(self):
self.running = True
self.thread = threading.Thread(target=self._monitor_loop, daemon=True)
self.thread.start()
def _monitor_loop(self):
while self.running:
metrics = self._collect_metrics()
self._check_alerts(metrics)
self._export_prometheus(metrics)
time.sleep(10)
def _collect_metrics(self) -> dict:
metrics = {
"cpu_percent": psutil.cpu_percent(interval=1),
"memory_percent": psutil.virtual_memory().percent,
"disk_percent": psutil.disk_usage("/").percent,
"net_bytes_sent": psutil.net_io_counters().bytes_sent,
"net_bytes_recv": psutil.net_io_counters().bytes_recv,
"timestamp": time.time()
}
# GPU指标(Jetson Nano
try:
gpus = GPUtil.getGPUs()
if gpus:
gpu = gpus[0]
metrics["gpu_load"] = gpu.load * 100
metrics["gpu_temp"] = gpu.temperature
metrics["gpu_mem_percent"] = gpu.memoryUtil * 100
except Exception:
pass
return metrics
def _check_alerts(self, metrics: dict):
if metrics["cpu_percent"] > self.ALERT_CPU_THRESHOLD:
self.alert_cb("HIGH_CPU", f"CPU使用率 {metrics['cpu_percent']:.1f}%")
if metrics["memory_percent"] > self.ALERT_MEM_THRESHOLD:
self.alert_cb("HIGH_MEM", f"内存使用率 {metrics['memory_percent']:.1f}%")
if metrics.get("gpu_temp", 0) > self.ALERT_GPU_TEMP_THRESHOLD:
self.alert_cb("HIGH_GPU_TEMP", f"GPU温度 {metrics['gpu_temp']}℃")
def _export_prometheus(self, metrics: dict):
"""写入Prometheus指标文件"""
lines = [
f'edge_cpu_percent {metrics["cpu_percent"]}',
f'edge_memory_percent {metrics["memory_percent"]}',
f'edge_disk_percent {metrics["disk_percent"]}',
]
if "gpu_load" in metrics:
lines.append(f'edge_gpu_load_percent {metrics["gpu_load"]}')
lines.append(f'edge_gpu_temp_celsius {metrics["gpu_temp"]}')
with open("/opt/writech/metrics/node.prom", "w") as f:
f.write("\n".join(lines) + "\n")
```
### G.5 数据安全与加密传输
#### G.5.1 TLS双向认证配置
```python
# tls_config.py
import ssl
def create_mutual_tls_context(cert_path: str, key_path: str,
ca_path: str) -> ssl.SSLContext:
"""创建双向TLS认证上下文"""
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
ctx.verify_mode = ssl.CERT_REQUIRED
ctx.check_hostname = True
# 加载客户端证书和私钥
ctx.load_cert_chain(certfile=cert_path, keyfile=key_path)
# 加载CA证书,用于验证服务端
ctx.load_verify_locations(ca_path)
# 强制TLS 1.2+
ctx.minimum_version = ssl.TLSVersion.TLSv1_2
# 配置加密套件(仅允许强加密)
ctx.set_ciphers(
"ECDHE-ECDSA-AES256-GCM-SHA384:"
"ECDHE-RSA-AES256-GCM-SHA384:"
"ECDHE-ECDSA-CHACHA20-POLY1305"
)
return ctx
```
#### G.5.2 本地数据加密存储
```python
# secure_storage.py
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
import base64
import os
class SecureStorage:
def __init__(self, passphrase: bytes):
# 从设备唯一标识派生密钥
salt = self._get_device_salt()
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
salt=salt,
iterations=100000
)
key = base64.urlsafe_b64encode(kdf.derive(passphrase))
self.fernet = Fernet(key)
def _get_device_salt(self) -> bytes:
"""读取设备唯一盐值(基于MAC地址和序列号)"""
try:
with open("/proc/net/if_inet6", "r") as f:
mac_part = f.readline()[:16].encode()
except Exception:
mac_part = os.urandom(16)
return mac_part
def encrypt_file(self, src: str, dst: str):
with open(src, "rb") as f:
data = f.read()
encrypted = self.fernet.encrypt(data)
with open(dst, "wb") as f:
f.write(encrypted)
def decrypt_file(self, src: str) -> bytes:
with open(src, "rb") as f:
encrypted = f.read()
return self.fernet.decrypt(encrypted)
```
---
*本文档版权归深圳自然写科技有限公司所有,技术细节仅用于软件著作权登记鉴别。*