diff --git a/README.md b/README.md
new file mode 100644
index 0000000..30fdc07
--- /dev/null
+++ b/README.md
@@ -0,0 +1,249 @@
+# cmvr-iot
+
+cmvr-iot 是一个基于 Java 8 和 Spring Boot 2.5 的多模块后端工程,提供设备管理、边缘机器人控制、测试编排、巡检、TTS、AIMA、VI/TI 等业务能力。工程入口在 `cmvr-iot-admin`,其余模块按业务和基础能力拆分。
+
+## 技术栈
+
+- Java 8
+- Spring Boot 2.5.15
+- Spring Security
+- MyBatis Plus / MyBatis Plus Join
+- MySQL / Redis
+- Druid
+- Quartz
+- Knife4j / Swagger
+- gRPC Java 1.58.0
+- Protobuf / protoc-maven-plugin
+- MinIO
+
+## 模块说明
+
+| 模块 | 说明 |
+| --- | --- |
+| `cmvr-iot-admin` | Web 启动模块,包含 REST Controller、启动类和运行配置 |
+| `cmvr-iot-framework` | Web、安全、配置、通用框架能力 |
+| `cmvr-iot-common` | 公共工具、通用响应、基础常量和通用能力 |
+| `cmvr-iot-system` | 系统管理、用户、角色、菜单、字典等基础业务 |
+| `cmvr-iot-quartz` | 定时任务模块 |
+| `cmvr-iot-generator` | 代码生成模块 |
+| `cmvr-iot-device` | 设备领域模型与设备相关能力 |
+| `cmvr-iot-test` | 测试任务、编排、执行实例等业务 |
+| `cmvr-iot-vi` | VI 相关业务 |
+| `cmvr-iot-ti` | TI 相关业务 |
+| `cmvr-iot-evaluation` | 评测相关业务 |
+| `cmvr-iot-inspection` | 巡检机器人、地图、任务、告警等业务 |
+| `cmvr-iot-aima` | AIMA 相关业务 |
+| `cmvr-iot-tts` | TTS 语音合成相关业务 |
+| `cmvr-iot-api` | API 聚合模块 |
+| `cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib` | gRPC proto 与生成代码模块 |
+| `cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client` | 边缘设备 gRPC 客户端、设备控制服务和请求 VO |
+
+## 目录结构
+
+```text
+cmvr-iot
+├── cmvr-iot-admin # Spring Boot 启动模块
+├── cmvr-iot-common # 公共模块
+├── cmvr-iot-framework # 框架模块
+├── cmvr-iot-system # 系统管理模块
+├── cmvr-iot-device # 设备模块
+├── cmvr-iot-test # 测试业务模块
+├── cmvr-iot-api
+│ └── cmvr-iot-edge
+│ ├── cmvr-iot-grpc-lib # proto 与 gRPC 生成代码
+│ └── cmvr-iot-grpc-client # gRPC 客户端与边缘控制服务
+├── sql # 增量 SQL
+├── Dockerfile
+├── Jenkinsfile
+└── pom.xml # Maven 父工程
+```
+
+## 环境要求
+
+- JDK 1.8
+- Maven 3.6+
+- MySQL 5.7+/8.x
+- Redis
+- 可访问的 gRPC 机器人/边缘终端服务
+- 可选:MinIO、外部大模型/TTS/评测服务
+
+注意:`application-*.yml` 中包含环境相关地址、账号和密钥配置。实际部署或本地开发时,应使用本机配置、环境变量或配置中心覆盖,不要直接复用生产/测试环境敏感配置。
+
+## 配置文件
+
+主配置文件位于:
+
+```text
+cmvr-iot-admin/src/main/resources/application.yml
+```
+
+环境配置文件位于:
+
+```text
+cmvr-iot-admin/src/main/resources/application-dev.yml
+cmvr-iot-admin/src/main/resources/application-test.yml
+cmvr-iot-admin/src/main/resources/application-prod.yml
+```
+
+当前 `application.yml` 默认激活:
+
+```yaml
+spring:
+ profiles:
+ active: test
+```
+
+如需切换环境,可以修改 `spring.profiles.active`,或启动时指定:
+
+```bash
+java -jar cmvr-iot-admin-dev.jar --spring.profiles.active=dev
+```
+
+## 构建
+
+在项目根目录执行:
+
+```bash
+mvn clean package -DskipTests
+```
+
+只编译启动模块及其依赖:
+
+```bash
+mvn -pl cmvr-iot-admin -am compile -DskipTests
+```
+
+只重新生成并编译 gRPC 代码:
+
+```bash
+mvn -pl cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib -am compile -DskipTests
+```
+
+## 启动
+
+启动类:
+
+```text
+cmvr-iot-admin/src/main/java/com/cmvr/CmvrIotApplication.java
+```
+
+IDE 中直接运行 `CmvrIotApplication` 即可。
+
+打包后运行:
+
+```bash
+java -jar cmvr-iot-admin/target/cmvr-iot-admin-dev.jar --spring.profiles.active=dev
+```
+
+默认开发端口以当前 profile 配置为准,例如 `application-dev.yml` 中为:
+
+```yaml
+server:
+ port: 13080
+```
+
+## 接口文档
+
+项目集成 Knife4j / Swagger。服务启动后可访问:
+
+```text
+http://localhost:13080/doc.html
+```
+
+如果端口或上下文路径被 profile 覆盖,请以实际配置为准。
+
+## 数据库脚本
+
+增量脚本位于:
+
+```text
+sql/
+```
+
+当前包含:
+
+- `aima_module.sql`
+- `inspection_module.sql`
+- `tts_corpus_module.sql`
+
+初始化数据库时,需要结合目标环境的基础库结构和增量脚本执行。
+
+## gRPC 与边缘设备
+
+边缘设备相关代码集中在:
+
+```text
+cmvr-iot-api/cmvr-iot-edge/
+```
+
+proto 文件位于:
+
+```text
+cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib/src/main/proto/
+```
+
+gRPC 客户端服务位于:
+
+```text
+cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/main/java/com/cmvr/edge/client/
+```
+
+边缘控制器位于:
+
+```text
+cmvr-iot-admin/src/main/java/com/cmvr/web/controller/api/
+```
+
+新增或修改 proto 后,执行 Maven compile 会触发 `protoc-maven-plugin` 生成 Java 和 gRPC Stub 代码。业务代码应复用现有 gRPC 客户端管理工具获取 Stub,避免在业务层重复创建底层 `ManagedChannel`。
+
+## 实时语音对讲
+
+当前实时语音对讲链路为:
+
+```text
+浏览器 WebSocket Binary PCM <-> Java 后端 <-> gRPC 双向流 <-> 机器人终端
+```
+
+约定音频格式:
+
+- PCM_S16LE
+- 48000 Hz
+- 单声道
+- 16 bit
+- 10 ms 一帧
+- 960 bytes/帧
+
+相关文件:
+
+- WebSocket 控制器:`cmvr-iot-admin/src/main/java/com/cmvr/web/controller/api/WebRtcSignalingController.java`
+- 业务会话管理:`cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/main/java/com/cmvr/edge/client/service/AudioService.java`
+- gRPC 音频适配:`cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-client/src/main/java/com/cmvr/edge/client/adapter/RobotAudioGrpcAdapter.java`
+- proto:`cmvr-iot-api/cmvr-iot-edge/cmvr-iot-grpc-lib/src/main/proto/cmvr/api/robot_audio.proto`
+
+说明:历史 WebRTC 相关类可能仍保留在源码中,但当前浏览器音频传输链路使用 WebSocket 二进制 PCM。
+
+## 开发约定
+
+- Controller 层只做参数接收、基础校验和响应封装。
+- POST 控制类接口优先使用单个 `@RequestBody VO` 承载完整请求参数。
+- GET 查询类接口可以使用 query 参数绑定 VO。
+- 不建议在同一个接口中同时声明通用 VO 和零散业务参数。
+- gRPC 调用统一通过现有客户端管理类获取 Stub。
+- proto 修改后必须重新编译并确认生成代码已更新。
+- 不要提交本地日志、崩溃 dump、临时音频文件和包含真实密钥的配置。
+
+## 常用命令
+
+```bash
+# 编译全部模块
+mvn clean compile -DskipTests
+
+# 编译启动模块及依赖
+mvn -pl cmvr-iot-admin -am compile -DskipTests
+
+# 打包
+mvn clean package -DskipTests
+
+# 运行指定 profile
+java -jar cmvr-iot-admin/target/cmvr-iot-admin-dev.jar --spring.profiles.active=dev
+```
diff --git a/cmvr-iot-admin/src/main/java/com/cmvr/web/controller/api/EdgeAgvController.java b/cmvr-iot-admin/src/main/java/com/cmvr/web/controller/api/EdgeAgvController.java
index b006b69..53cc8fd 100644
--- a/cmvr-iot-admin/src/main/java/com/cmvr/web/controller/api/EdgeAgvController.java
+++ b/cmvr-iot-admin/src/main/java/com/cmvr/web/controller/api/EdgeAgvController.java
@@ -1,22 +1,36 @@
package com.cmvr.web.controller.api;
-import cmvr.msgs.*;
+import cmvr.msgs.Agv;
import com.cmvr.common.core.domain.AjaxResult;
import com.cmvr.edge.client.model.EdgeCommonVO;
+import com.cmvr.edge.client.model.agv.EdgeAgvMapNameVO;
+import com.cmvr.edge.client.model.agv.EdgeAgvNavigateToPoseVO;
+import com.cmvr.edge.client.model.agv.EdgeAgvNavigateToStationVO;
+import com.cmvr.edge.client.model.agv.EdgeAgvStartMappingVO;
+import com.cmvr.edge.client.model.agv.EdgeAgvUploadMapVO;
+import com.cmvr.edge.client.model.agv.EdgeAgvVelocityVO;
import com.cmvr.edge.client.service.EdgeAgvService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
-import io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor;
+import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
+import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
/**
- * 边缘系统AGV控制器
+ * 边缘系统 AGV 控制器。
+ *
+ *
接口入参约定:
+ * GET 查询类接口使用 query 参数绑定 VO;POST 控制类接口只接收一个 JSON 请求体 VO,
+ * 避免一个接口同时出现通用 VO 和零散业务参数,保证前端调用、Swagger 文档和后端绑定规则一致。
*
* @author cmvr-iot
* @since 2026-07-02
@@ -29,62 +43,24 @@ public class EdgeAgvController {
private final EdgeAgvService edgeAgvService;
+ /**
+ * 获取 AGV 运行时状态。
+ */
@ApiOperation("获取AGV运行时状态")
@GetMapping("/getRuntimeState")
- public AjaxResult getRuntimeState(EdgeCommonVO vo) {
+ public AjaxResult getRuntimeState(@Validated EdgeCommonVO vo) {
Agv.AgvRuntimeState state = edgeAgvService.getRuntimeState(vo);
- // 转换为Map返回
- java.util.Map resultMap = new java.util.HashMap<>();
- resultMap.put("timestamp", state.getTimestamp());
- resultMap.put("mode", state.getMode());
- resultMap.put("connected", state.getConnected());
- resultMap.put("localized", state.getLocalized());
- resultMap.put("moving", state.getMoving());
- resultMap.put("fault", state.getFault());
- resultMap.put("emergencyStopped", state.getEmergencyStopped());
-
- // 位置信息
- if (state.hasPose()) {
- java.util.Map poseMap = new java.util.HashMap<>();
- poseMap.put("x", state.getPose().getX());
- poseMap.put("y", state.getPose().getY());
- poseMap.put("theta", state.getPose().getTheta());
- resultMap.put("pose", poseMap);
- }
-
- // 速度信息
- if (state.hasVelocity()) {
- java.util.Map velocityMap = new java.util.HashMap<>();
- velocityMap.put("vx", state.getVelocity().getVx());
- velocityMap.put("vy", state.getVelocity().getVy());
- velocityMap.put("wz", state.getVelocity().getWz());
- resultMap.put("velocity", velocityMap);
- }
-
- // 电池信息
- if (state.hasBattery()) {
- java.util.Map batteryMap = new java.util.HashMap<>();
- batteryMap.put("percentage", state.getBattery().getPercentage());
- batteryMap.put("voltage", state.getBattery().getVoltage());
- batteryMap.put("current", state.getBattery().getCurrent());
- batteryMap.put("temperature", state.getBattery().getTemperature());
- batteryMap.put("charging", state.getBattery().getCharging());
- resultMap.put("battery", batteryMap);
- }
-
- resultMap.put("currentMap", state.getCurrentMap());
- resultMap.put("currentStation", state.getCurrentStation());
- resultMap.put("lastError", state.getLastError());
-
- return AjaxResult.ok(resultMap);
+ return AjaxResult.ok(toRuntimeStateMap(state));
}
+ /**
+ * 获取 AGV 当前导航状态。
+ */
@ApiOperation("获取导航状态")
@GetMapping("/getNavigationStatus")
- public AjaxResult getNavigationStatus(EdgeCommonVO vo) {
+ public AjaxResult getNavigationStatus(@Validated EdgeCommonVO vo) {
Agv.AgvNavigationStatus status = edgeAgvService.getNavigationStatus(vo);
- // 转换为Map返回
- java.util.Map resultMap = new java.util.HashMap<>();
+ Map resultMap = new HashMap<>();
resultMap.put("state", status.getState());
resultMap.put("type", status.getType());
resultMap.put("progress", status.getProgress());
@@ -92,162 +68,248 @@ public class EdgeAgvController {
return AjaxResult.ok(resultMap);
}
+ /**
+ * AGV 紧急停止。
+ */
@ApiOperation("紧急停止")
- @GetMapping("/emergencyStop")
- public AjaxResult emergencyStop(EdgeCommonVO vo) {
+ @PostMapping("/emergencyStop")
+ public AjaxResult emergencyStop(@RequestBody @Validated EdgeCommonVO vo) {
edgeAgvService.emergencyStop(vo);
return AjaxResult.ok();
}
+ /**
+ * 清除 AGV 故障。
+ */
@ApiOperation("清除故障")
- @GetMapping("/clearFault")
- public AjaxResult clearFault(EdgeCommonVO vo) {
+ @PostMapping("/clearFault")
+ public AjaxResult clearFault(@RequestBody @Validated EdgeCommonVO vo) {
edgeAgvService.clearFault(vo);
return AjaxResult.ok();
}
+ /**
+ * 导航到指定坐标。
+ */
@ApiOperation("导航到指定位置")
@PostMapping("/navigateToPose")
- public AjaxResult navigateToPose(EdgeCommonVO vo,
- @ApiParam("X坐标") double x,
- @ApiParam("Y坐标") double y,
- @ApiParam("角度") double theta) {
+ public AjaxResult navigateToPose(@RequestBody @Validated EdgeAgvNavigateToPoseVO vo) {
Agv.AgvPose2d pose = Agv.AgvPose2d.newBuilder()
- .setX(x)
- .setY(y)
- .setTheta(theta)
- .build();
+ .setX(vo.getX())
+ .setY(vo.getY())
+ .setTheta(vo.getTheta())
+ .build();
edgeAgvService.navigateToPose(vo, pose);
return AjaxResult.ok();
}
+ /**
+ * 导航到指定站点。
+ */
@ApiOperation("导航到站点")
- @GetMapping("/navigateToStation")
- public AjaxResult navigateToStation(EdgeCommonVO vo,
- @ApiParam("站点ID") String stationId) {
- edgeAgvService.navigateToStation(vo, stationId);
+ @PostMapping("/navigateToStation")
+ public AjaxResult navigateToStation(@RequestBody @Validated EdgeAgvNavigateToStationVO vo) {
+ edgeAgvService.navigateToStation(vo, vo.getStationId());
return AjaxResult.ok();
}
+ /**
+ * 暂停当前导航任务。
+ */
@ApiOperation("暂停导航")
- @GetMapping("/pauseNavigation")
- public AjaxResult pauseNavigation(EdgeCommonVO vo) {
+ @PostMapping("/pauseNavigation")
+ public AjaxResult pauseNavigation(@RequestBody @Validated EdgeCommonVO vo) {
edgeAgvService.pauseNavigation(vo);
return AjaxResult.ok();
}
+ /**
+ * 恢复当前导航任务。
+ */
@ApiOperation("恢复导航")
- @GetMapping("/resumeNavigation")
- public AjaxResult resumeNavigation(EdgeCommonVO vo) {
+ @PostMapping("/resumeNavigation")
+ public AjaxResult resumeNavigation(@RequestBody @Validated EdgeCommonVO vo) {
edgeAgvService.resumeNavigation(vo);
return AjaxResult.ok();
}
+ /**
+ * 取消当前导航任务。
+ */
@ApiOperation("取消导航")
- @GetMapping("/cancelNavigation")
- public AjaxResult cancelNavigation(EdgeCommonVO vo) {
+ @PostMapping("/cancelNavigation")
+ public AjaxResult cancelNavigation(@RequestBody @Validated EdgeCommonVO vo) {
edgeAgvService.cancelNavigation(vo);
return AjaxResult.ok();
}
+ /**
+ * 设置 AGV 速度。
+ */
@ApiOperation("设置速度")
@PostMapping("/setVelocity")
- public AjaxResult setVelocity(EdgeCommonVO vo,
- @ApiParam("线速度X") double vx,
- @ApiParam("线速度Y") double vy,
- @ApiParam("角速度") double wz) {
+ public AjaxResult setVelocity(@RequestBody @Validated EdgeAgvVelocityVO vo) {
Agv.AgvVelocity velocity = Agv.AgvVelocity.newBuilder()
- .setVx(vx)
- .setVy(vy)
- .setWz(wz)
- .build();
+ .setVx(vo.getVx())
+ .setVy(vo.getVy())
+ .setWz(vo.getWz())
+ .build();
edgeAgvService.setVelocity(vo, velocity);
return AjaxResult.ok();
}
+ /**
+ * 停止速度控制。
+ */
@ApiOperation("停止速度控制")
- @GetMapping("/stopVelocityControl")
- public AjaxResult stopVelocityControl(EdgeCommonVO vo) {
+ @PostMapping("/stopVelocityControl")
+ public AjaxResult stopVelocityControl(@RequestBody @Validated EdgeCommonVO vo) {
edgeAgvService.stopVelocityControl(vo);
return AjaxResult.ok();
}
+ /**
+ * 查询机器人本地所有地图。
+ */
@ApiOperation("列出所有地图")
@GetMapping("/listMaps")
- public AjaxResult listMaps(EdgeCommonVO vo) {
+ public AjaxResult listMaps(@Validated EdgeCommonVO vo) {
List maps = edgeAgvService.listMaps(vo);
return AjaxResult.ok(maps);
}
-
-
+ /**
+ * 查询机器人本地所有站点。
+ */
@ApiOperation("列出所有站点")
@GetMapping("/listStations")
- public AjaxResult listStations(EdgeCommonVO vo) {
+ public AjaxResult listStations(@Validated EdgeCommonVO vo) {
List stations = edgeAgvService.listStations(vo);
- // 转换为List