feat(audio): 实现实时语音会话功能支持设备分离和音频缓冲优化
- 配置Tomcat支持20MB大JSON请求处理PPE报警中的Base64图片 - 扩展AudioService支持独立的扬声器和麦克风设备ID参数处理 - 实现浏览器音频推送器,提供音频帧缓冲和定时推送机制 - 添加音频数据protobuf定义,支持多种音频格式和编解码参数 - 重构音频会话管理,实现音频队列、统计监控和性能优化 - 增强心跳检测机制,改进机器人离线状态处理逻辑
This commit is contained in:
parent
152cae0323
commit
ecda2b0ebc
@ -0,0 +1,123 @@
|
|||||||
|
package com.cmvr.web.controller.inspection;
|
||||||
|
|
||||||
|
import com.cmvr.common.annotation.Anonymous;
|
||||||
|
import com.cmvr.inspection.domain.dto.alert.AlertEnvelope;
|
||||||
|
import com.cmvr.inspection.exception.DetectionAlertBadRequestException;
|
||||||
|
import com.cmvr.inspection.service.IInspectionDetectionAlertService;
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import io.swagger.annotations.Api;
|
||||||
|
import io.swagger.annotations.ApiOperation;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.dao.DuplicateKeyException;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestHeader;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import javax.servlet.ServletInputStream;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* cmvr_edge_ai 检测报警推送入口。
|
||||||
|
*
|
||||||
|
* 该接口由边缘服务主动调用,使用HTTP状态码表达处理结果,不使用项目通用AjaxResult包装。
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Anonymous
|
||||||
|
@RestController
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Api(tags = "智能巡检--PPE报警接收")
|
||||||
|
public class InspectionDetectionAlertController
|
||||||
|
{
|
||||||
|
/** 包含Base64图片的JSON请求最大允许20MB,防止异常请求耗尽服务端内存。 */
|
||||||
|
private static final int MAX_REQUEST_BYTES = 20 * 1024 * 1024;
|
||||||
|
|
||||||
|
private final IInspectionDetectionAlertService detectionAlertService;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接收PPE违规报警。重复事件视为已处理,仍返回204,避免边缘端持续重试。
|
||||||
|
*/
|
||||||
|
@ApiOperation("接收边缘AI的PPE违规报警")
|
||||||
|
@PostMapping(value = "/v1/detection-alerts", consumes = MediaType.APPLICATION_JSON_VALUE)
|
||||||
|
public ResponseEntity<Void> receive(
|
||||||
|
@RequestHeader(value = "Idempotency-Key", required = false) String idempotencyKey,
|
||||||
|
HttpServletRequest request)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
String requestBody = readRequestBody(request);
|
||||||
|
AlertEnvelope envelope = objectMapper.readValue(requestBody, AlertEnvelope.class);
|
||||||
|
detectionAlertService.receive(envelope, idempotencyKey);
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
catch (JsonProcessingException | DetectionAlertBadRequestException ex)
|
||||||
|
{
|
||||||
|
log.warn("拒绝不合法的PPE报警请求,idempotencyKey={},原因={}", idempotencyKey, ex.getMessage());
|
||||||
|
return ResponseEntity.badRequest().build();
|
||||||
|
}
|
||||||
|
catch (DuplicateKeyException ex)
|
||||||
|
{
|
||||||
|
// 预查与插入之间仍可能发生并发,由数据库唯一索引完成最终幂等。
|
||||||
|
log.info("忽略并发重复的PPE报警,idempotencyKey={}", idempotencyKey);
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
catch (IOException ex)
|
||||||
|
{
|
||||||
|
log.warn("读取PPE报警请求失败,idempotencyKey={},原因={}", idempotencyKey, ex.getMessage());
|
||||||
|
return ResponseEntity.badRequest().build();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
log.error("PPE报警处理暂时失败,idempotencyKey={}", idempotencyKey, ex);
|
||||||
|
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String readRequestBody(HttpServletRequest request) throws IOException
|
||||||
|
{
|
||||||
|
long contentLength = request.getContentLengthLong();
|
||||||
|
if (contentLength > MAX_REQUEST_BYTES)
|
||||||
|
{
|
||||||
|
throw new DetectionAlertBadRequestException("请求体不能超过20MB");
|
||||||
|
}
|
||||||
|
|
||||||
|
try (ServletInputStream inputStream = request.getInputStream();
|
||||||
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(initialCapacity(contentLength)))
|
||||||
|
{
|
||||||
|
byte[] buffer = new byte[8192];
|
||||||
|
int total = 0;
|
||||||
|
int length;
|
||||||
|
while ((length = inputStream.read(buffer)) != -1)
|
||||||
|
{
|
||||||
|
total += length;
|
||||||
|
if (total > MAX_REQUEST_BYTES)
|
||||||
|
{
|
||||||
|
throw new DetectionAlertBadRequestException("请求体不能超过20MB");
|
||||||
|
}
|
||||||
|
outputStream.write(buffer, 0, length);
|
||||||
|
}
|
||||||
|
if (total == 0)
|
||||||
|
{
|
||||||
|
throw new DetectionAlertBadRequestException("请求体不能为空");
|
||||||
|
}
|
||||||
|
return new String(outputStream.toByteArray(), StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int initialCapacity(long contentLength)
|
||||||
|
{
|
||||||
|
if (contentLength <= 0 || contentLength > MAX_REQUEST_BYTES)
|
||||||
|
{
|
||||||
|
return 8192;
|
||||||
|
}
|
||||||
|
return (int) contentLength;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -21,6 +21,9 @@ server:
|
|||||||
tomcat:
|
tomcat:
|
||||||
# tomcat的URI编码
|
# tomcat的URI编码
|
||||||
uri-encoding: UTF-8
|
uri-encoding: UTF-8
|
||||||
|
# PPE报警包含Base64图片,允许读取较大的JSON请求,同时限制异常请求的吞入大小
|
||||||
|
max-http-form-post-size: 20MB
|
||||||
|
max-swallow-size: 20MB
|
||||||
# 连接数满后的排队数,默认为100
|
# 连接数满后的排队数,默认为100
|
||||||
accept-count: 1000
|
accept-count: 1000
|
||||||
threads:
|
threads:
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -47,8 +47,10 @@ public class GrpcServiceManager {
|
|||||||
clientFactories.put(SystemServiceGrpc.SystemServiceBlockingStub.class, new GrpcClientFactory<>(SystemServiceGrpc::newBlockingStub));
|
clientFactories.put(SystemServiceGrpc.SystemServiceBlockingStub.class, new GrpcClientFactory<>(SystemServiceGrpc::newBlockingStub));
|
||||||
// 注册扬声器服务的 Stub
|
// 注册扬声器服务的 Stub
|
||||||
clientFactories.put(SpeakerServiceGrpc.SpeakerServiceBlockingStub.class, new GrpcClientFactory<>(SpeakerServiceGrpc::newBlockingStub));
|
clientFactories.put(SpeakerServiceGrpc.SpeakerServiceBlockingStub.class, new GrpcClientFactory<>(SpeakerServiceGrpc::newBlockingStub));
|
||||||
|
clientFactories.put(SpeakerServiceGrpc.SpeakerServiceStub.class, new GrpcClientFactory<>(SpeakerServiceGrpc::newStub));
|
||||||
// 注册麦克风服务的 Stub
|
// 注册麦克风服务的 Stub
|
||||||
clientFactories.put(MicPhoneServiceGrpc.MicPhoneServiceBlockingStub.class, new GrpcClientFactory<>(MicPhoneServiceGrpc::newBlockingStub));
|
clientFactories.put(MicPhoneServiceGrpc.MicPhoneServiceBlockingStub.class, new GrpcClientFactory<>(MicPhoneServiceGrpc::newBlockingStub));
|
||||||
|
clientFactories.put(MicPhoneServiceGrpc.MicPhoneServiceStub.class, new GrpcClientFactory<>(MicPhoneServiceGrpc::newStub));
|
||||||
// 注册机器人头部服务的 Stub
|
// 注册机器人头部服务的 Stub
|
||||||
clientFactories.put(BioHeadServiceGrpc.BioHeadServiceBlockingStub.class, new GrpcClientFactory<>(BioHeadServiceGrpc::newBlockingStub));
|
clientFactories.put(BioHeadServiceGrpc.BioHeadServiceBlockingStub.class, new GrpcClientFactory<>(BioHeadServiceGrpc::newBlockingStub));
|
||||||
// 注册机器人灵巧手服务的 Stub
|
// 注册机器人灵巧手服务的 Stub
|
||||||
@ -60,7 +62,6 @@ public class GrpcServiceManager {
|
|||||||
// 注册agv服务的stub
|
// 注册agv服务的stub
|
||||||
clientFactories.put(AgvServiceGrpc.AgvServiceBlockingStub.class, new GrpcClientFactory<>(AgvServiceGrpc::newBlockingStub));
|
clientFactories.put(AgvServiceGrpc.AgvServiceBlockingStub.class, new GrpcClientFactory<>(AgvServiceGrpc::newBlockingStub));
|
||||||
// 注册机器人实时语音双向流服务的异步Stub
|
// 注册机器人实时语音双向流服务的异步Stub
|
||||||
clientFactories.put(RobotAudioServiceGrpc.RobotAudioServiceStub.class, new GrpcClientFactory<>(RobotAudioServiceGrpc::newStub));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -21,6 +21,12 @@ import java.nio.ByteBuffer;
|
|||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.ScheduledFuture;
|
||||||
|
import java.util.concurrent.ThreadFactory;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -40,6 +46,12 @@ public class AudioService implements RobotAudioStreamListener {
|
|||||||
|
|
||||||
/** 机器人心跳超时时间:机器人每 1 秒上报一次,超过 5 秒判定离线。 */
|
/** 机器人心跳超时时间:机器人每 1 秒上报一次,超过 5 秒判定离线。 */
|
||||||
private static final long ROBOT_HEARTBEAT_TIMEOUT_MS = 5000L;
|
private static final long ROBOT_HEARTBEAT_TIMEOUT_MS = 5000L;
|
||||||
|
private static final int BROWSER_AUDIO_FRAME_BYTES = 960;
|
||||||
|
private static final int BROWSER_AUDIO_FRAME_MS = 10;
|
||||||
|
private static final int TARGET_BROWSER_AUDIO_QUEUE_FRAMES = 4;
|
||||||
|
private static final int MAX_BROWSER_AUDIO_CATCH_UP_FRAMES = 4;
|
||||||
|
private static final int WARN_BROWSER_AUDIO_QUEUE_FRAMES = 100;
|
||||||
|
private static final long BROWSER_AUDIO_PUSH_INTERVAL_MS = 5L;
|
||||||
|
|
||||||
private final RobotAudioGrpcAdapter robotAudioGrpcAdapter;
|
private final RobotAudioGrpcAdapter robotAudioGrpcAdapter;
|
||||||
|
|
||||||
@ -166,13 +178,25 @@ public class AudioService implements RobotAudioStreamListener {
|
|||||||
private void handleJoin(WebSocketSession webSocketSession, JsonNode jsonNode) {
|
private void handleJoin(WebSocketSession webSocketSession, JsonNode jsonNode) {
|
||||||
String terminalId = getText(jsonNode, "terminalId");
|
String terminalId = getText(jsonNode, "terminalId");
|
||||||
String deviceId = getText(jsonNode, "deviceId");
|
String deviceId = getText(jsonNode, "deviceId");
|
||||||
|
String speakerDeviceId = getText(jsonNode, "speakerDeviceId");
|
||||||
|
String micDeviceId = getText(jsonNode, "micDeviceId");
|
||||||
String operatorId = getText(jsonNode, "operatorId");
|
String operatorId = getText(jsonNode, "operatorId");
|
||||||
String sessionId = getText(jsonNode, "sessionId");
|
String sessionId = getText(jsonNode, "sessionId");
|
||||||
|
|
||||||
if (terminalId == null || deviceId == null) {
|
if (speakerDeviceId == null) {
|
||||||
sendError(webSocketSession, sessionId, "terminalId和deviceId不能为空");
|
speakerDeviceId = deviceId;
|
||||||
|
}
|
||||||
|
if (micDeviceId == null) {
|
||||||
|
micDeviceId = deviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (terminalId == null || speakerDeviceId == null || micDeviceId == null) {
|
||||||
|
sendError(webSocketSession, sessionId, "terminalId、speakerDeviceId、micDeviceId不能为空");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (deviceId == null) {
|
||||||
|
deviceId = speakerDeviceId;
|
||||||
|
}
|
||||||
if (sessionId == null) {
|
if (sessionId == null) {
|
||||||
sessionId = IdUtils.fastSimpleUUID();
|
sessionId = IdUtils.fastSimpleUUID();
|
||||||
}
|
}
|
||||||
@ -191,24 +215,28 @@ public class AudioService implements RobotAudioStreamListener {
|
|||||||
session.sessionId = sessionId;
|
session.sessionId = sessionId;
|
||||||
session.terminalId = terminalId;
|
session.terminalId = terminalId;
|
||||||
session.deviceId = deviceId;
|
session.deviceId = deviceId;
|
||||||
|
session.speakerDeviceId = speakerDeviceId;
|
||||||
|
session.micDeviceId = micDeviceId;
|
||||||
session.operatorId = operatorId;
|
session.operatorId = operatorId;
|
||||||
session.webSocketSession = webSocketSession;
|
session.webSocketSession = webSocketSession;
|
||||||
session.state = AudioSessionState.CONNECTING;
|
session.state = AudioSessionState.CONNECTING;
|
||||||
session.createdAt = System.currentTimeMillis();
|
session.createdAt = System.currentTimeMillis();
|
||||||
session.lastBrowserSignalAt = session.createdAt;
|
session.lastBrowserSignalAt = session.createdAt;
|
||||||
session.lastRobotHeartbeatAt = session.createdAt;
|
session.lastRobotHeartbeatAt = session.createdAt;
|
||||||
|
startBrowserAudioPusher(session);
|
||||||
|
|
||||||
sessionMap.put(sessionId, session);
|
sessionMap.put(sessionId, session);
|
||||||
webSocketSessionMap.put(webSocketSession.getId(), sessionId);
|
webSocketSessionMap.put(webSocketSession.getId(), sessionId);
|
||||||
try {
|
try {
|
||||||
session.robotStream = robotAudioGrpcAdapter.openStream(terminalId, deviceId, sessionId, operatorId, this);
|
session.robotStream = robotAudioGrpcAdapter.openStream(terminalId, speakerDeviceId, micDeviceId, sessionId, operatorId, this);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
sessionMap.remove(sessionId);
|
sessionMap.remove(sessionId);
|
||||||
webSocketSessionMap.remove(webSocketSession.getId());
|
webSocketSessionMap.remove(webSocketSession.getId());
|
||||||
|
stopBrowserAudioPusher(session);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
sendState(session, "joined");
|
sendState(session, "joined");
|
||||||
log.info("实时语音会话创建成功,sessionId={},terminalId={},deviceId={}", sessionId, terminalId, deviceId);
|
log.info("实时语音会话创建成功,sessionId={},terminalId={},speakerDeviceId={},micDeviceId={}", sessionId, terminalId, speakerDeviceId, micDeviceId);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("创建实时语音会话失败,sessionId={}", sessionId, e);
|
log.error("创建实时语音会话失败,sessionId={}", sessionId, e);
|
||||||
sendError(webSocketSession, sessionId, "创建实时语音会话失败:" + e.getMessage());
|
sendError(webSocketSession, sessionId, "创建实时语音会话失败:" + e.getMessage());
|
||||||
@ -294,6 +322,7 @@ public class AudioService implements RobotAudioStreamListener {
|
|||||||
try {
|
try {
|
||||||
session.state = AudioSessionState.CLOSED;
|
session.state = AudioSessionState.CLOSED;
|
||||||
removeWebSocketMapping(session);
|
removeWebSocketMapping(session);
|
||||||
|
stopBrowserAudioPusher(session);
|
||||||
if (session.robotStream != null) {
|
if (session.robotStream != null) {
|
||||||
session.robotStream.sendStop(reason);
|
session.robotStream.sendStop(reason);
|
||||||
}
|
}
|
||||||
@ -318,12 +347,13 @@ public class AudioService implements RobotAudioStreamListener {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (now - session.lastRobotHeartbeatAt > ROBOT_HEARTBEAT_TIMEOUT_MS) {
|
if (now - session.lastRobotHeartbeatAt > ROBOT_HEARTBEAT_TIMEOUT_MS) {
|
||||||
log.warn("机器人心跳超时,sessionId={},terminalId={},deviceId={}",
|
log.warn("机器人心跳超时,sessionId={},terminalId={},speakerDeviceId={},micDeviceId={}",
|
||||||
session.sessionId, session.terminalId, session.deviceId);
|
session.sessionId, session.terminalId, session.speakerDeviceId, session.micDeviceId);
|
||||||
// 先从业务会话表摘除,避免 cancel/close 触发的异步回调再次进入同一个会话并重复关闭 native 对象。
|
// 先从业务会话表摘除,避免 cancel/close 触发的异步回调再次进入同一个会话并重复关闭 native 对象。
|
||||||
iterator.remove();
|
iterator.remove();
|
||||||
session.state = AudioSessionState.ROBOT_OFFLINE;
|
session.state = AudioSessionState.ROBOT_OFFLINE;
|
||||||
sendState(session, "robotOffline");
|
sendState(session, "robotOffline");
|
||||||
|
stopBrowserAudioPusher(session);
|
||||||
if (session.robotStream != null) {
|
if (session.robotStream != null) {
|
||||||
session.robotStream.cancel("robot heartbeat timeout");
|
session.robotStream.cancel("robot heartbeat timeout");
|
||||||
}
|
}
|
||||||
@ -359,7 +389,7 @@ public class AudioService implements RobotAudioStreamListener {
|
|||||||
}
|
}
|
||||||
session.lastRobotAudioAt = System.currentTimeMillis();
|
session.lastRobotAudioAt = System.currentTimeMillis();
|
||||||
session.lastRobotHeartbeatAt = session.lastRobotAudioAt;
|
session.lastRobotHeartbeatAt = session.lastRobotAudioAt;
|
||||||
sendBinary(session.webSocketSession, pcm);
|
enqueueRobotAudioForBrowser(session, pcm);
|
||||||
Consumer<byte[]> consumer = session.robotAudioConsumer;
|
Consumer<byte[]> consumer = session.robotAudioConsumer;
|
||||||
if (consumer != null) {
|
if (consumer != null) {
|
||||||
try {
|
try {
|
||||||
@ -412,12 +442,14 @@ public class AudioService implements RobotAudioStreamListener {
|
|||||||
sendState(session, "grpcCompleted");
|
sendState(session, "grpcCompleted");
|
||||||
}
|
}
|
||||||
sessionMap.remove(sessionId);
|
sessionMap.remove(sessionId);
|
||||||
|
stopBrowserAudioPusher(session);
|
||||||
removeWebSocketMapping(session);
|
removeWebSocketMapping(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void failSession(AudioSession session, String reason, boolean cancelRobotStream) {
|
private void failSession(AudioSession session, String reason, boolean cancelRobotStream) {
|
||||||
sessionMap.remove(session.sessionId, session);
|
sessionMap.remove(session.sessionId, session);
|
||||||
session.state = AudioSessionState.FAILED;
|
session.state = AudioSessionState.FAILED;
|
||||||
|
stopBrowserAudioPusher(session);
|
||||||
ObjectNode event = baseMessage("error", session.sessionId);
|
ObjectNode event = baseMessage("error", session.sessionId);
|
||||||
event.put("message", reason == null ? "未知错误" : reason);
|
event.put("message", reason == null ? "未知错误" : reason);
|
||||||
event.put("state", session.state.name());
|
event.put("state", session.state.name());
|
||||||
@ -436,6 +468,8 @@ public class AudioService implements RobotAudioStreamListener {
|
|||||||
ObjectNode event = baseMessage(type, session.sessionId);
|
ObjectNode event = baseMessage(type, session.sessionId);
|
||||||
event.put("terminalId", session.terminalId);
|
event.put("terminalId", session.terminalId);
|
||||||
event.put("deviceId", session.deviceId);
|
event.put("deviceId", session.deviceId);
|
||||||
|
event.put("speakerDeviceId", session.speakerDeviceId);
|
||||||
|
event.put("micDeviceId", session.micDeviceId);
|
||||||
event.put("state", session.state.name());
|
event.put("state", session.state.name());
|
||||||
event.put("createdAt", session.createdAt);
|
event.put("createdAt", session.createdAt);
|
||||||
event.put("lastRobotHeartbeatAt", session.lastRobotHeartbeatAt);
|
event.put("lastRobotHeartbeatAt", session.lastRobotHeartbeatAt);
|
||||||
@ -462,6 +496,108 @@ public class AudioService implements RobotAudioStreamListener {
|
|||||||
sendText(session, toJson(event));
|
sendText(session, toJson(event));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void startBrowserAudioPusher(AudioSession session) {
|
||||||
|
if (session == null || session.robotBrowserAudioExecutor != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
session.robotBrowserAudioExecutor = Executors.newSingleThreadScheduledExecutor(
|
||||||
|
new NamedThreadFactory("robot-browser-audio-" + session.sessionId));
|
||||||
|
session.robotBrowserAudioFuture = session.robotBrowserAudioExecutor.scheduleAtFixedRate(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
int queued = session.robotBrowserAudioQueue.size();
|
||||||
|
int framesToSend = queued > TARGET_BROWSER_AUDIO_QUEUE_FRAMES
|
||||||
|
? Math.min(MAX_BROWSER_AUDIO_CATCH_UP_FRAMES, queued - TARGET_BROWSER_AUDIO_QUEUE_FRAMES + 1)
|
||||||
|
: 1;
|
||||||
|
for (int i = 0; i < framesToSend; i++) {
|
||||||
|
byte[] frame = session.robotBrowserAudioQueue.poll();
|
||||||
|
if (frame == null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
sendBinary(session.webSocketSession, frame);
|
||||||
|
session.robotBrowserAudioSent++;
|
||||||
|
}
|
||||||
|
logRobotBrowserAudioStatsIfNeeded(session, "robotAudioPush");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("推送机器人音频到浏览器失败,sessionId={}", session.sessionId, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 0L, BROWSER_AUDIO_PUSH_INTERVAL_MS, TimeUnit.MILLISECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void enqueueRobotAudioForBrowser(AudioSession session, byte[] pcm) {
|
||||||
|
if (session == null || pcm == null || pcm.length == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
synchronized (session.robotBrowserAudioLock) {
|
||||||
|
byte[] merged = new byte[session.robotBrowserRemainder.length + pcm.length];
|
||||||
|
System.arraycopy(session.robotBrowserRemainder, 0, merged, 0, session.robotBrowserRemainder.length);
|
||||||
|
System.arraycopy(pcm, 0, merged, session.robotBrowserRemainder.length, pcm.length);
|
||||||
|
|
||||||
|
int offset = 0;
|
||||||
|
while (merged.length - offset >= BROWSER_AUDIO_FRAME_BYTES) {
|
||||||
|
byte[] frame = new byte[BROWSER_AUDIO_FRAME_BYTES];
|
||||||
|
System.arraycopy(merged, offset, frame, 0, BROWSER_AUDIO_FRAME_BYTES);
|
||||||
|
offerRobotAudioFrame(session, frame);
|
||||||
|
offset += BROWSER_AUDIO_FRAME_BYTES;
|
||||||
|
}
|
||||||
|
|
||||||
|
int remain = merged.length - offset;
|
||||||
|
session.robotBrowserRemainder = new byte[remain];
|
||||||
|
if (remain > 0) {
|
||||||
|
System.arraycopy(merged, offset, session.robotBrowserRemainder, 0, remain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logRobotBrowserAudioStatsIfNeeded(session, "robotAudioEnqueue");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void offerRobotAudioFrame(AudioSession session, byte[] frame) {
|
||||||
|
session.robotBrowserAudioQueue.offer(frame);
|
||||||
|
session.robotBrowserAudioEnqueued++;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stopBrowserAudioPusher(AudioSession session) {
|
||||||
|
if (session == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ScheduledFuture<?> future = session.robotBrowserAudioFuture;
|
||||||
|
if (future != null) {
|
||||||
|
future.cancel(false);
|
||||||
|
session.robotBrowserAudioFuture = null;
|
||||||
|
}
|
||||||
|
ScheduledExecutorService executor = session.robotBrowserAudioExecutor;
|
||||||
|
if (executor != null) {
|
||||||
|
executor.shutdownNow();
|
||||||
|
session.robotBrowserAudioExecutor = null;
|
||||||
|
}
|
||||||
|
synchronized (session.robotBrowserAudioLock) {
|
||||||
|
session.robotBrowserAudioQueue.clear();
|
||||||
|
session.robotBrowserRemainder = new byte[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void logRobotBrowserAudioStatsIfNeeded(AudioSession session, String source) {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
if (now - session.lastRobotBrowserAudioLogAt < 5000L) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
session.lastRobotBrowserAudioLogAt = now;
|
||||||
|
int queued = session.robotBrowserAudioQueue.size();
|
||||||
|
log.info("机器人到浏览器音频统计,source={},sessionId={},enqueued={},sent={},dropped={},queued={},remainderBytes={}",
|
||||||
|
source,
|
||||||
|
session.sessionId,
|
||||||
|
session.robotBrowserAudioEnqueued,
|
||||||
|
session.robotBrowserAudioSent,
|
||||||
|
session.robotBrowserAudioDropped,
|
||||||
|
queued,
|
||||||
|
session.robotBrowserRemainder.length);
|
||||||
|
if (queued > WARN_BROWSER_AUDIO_QUEUE_FRAMES) {
|
||||||
|
log.warn("机器人到浏览器音频队列积压较高,sessionId={},queued={},estimatedDelayMs={},请检查前端播放消费或网络发送是否跟不上",
|
||||||
|
session.sessionId, queued, queued * BROWSER_AUDIO_FRAME_MS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void sendBinary(WebSocketSession session, byte[] pcm) {
|
private void sendBinary(WebSocketSession session, byte[] pcm) {
|
||||||
if (session == null || !session.isOpen() || pcm == null || pcm.length == 0) {
|
if (session == null || !session.isOpen() || pcm == null || pcm.length == 0) {
|
||||||
return;
|
return;
|
||||||
@ -571,10 +707,21 @@ public class AudioService implements RobotAudioStreamListener {
|
|||||||
private String sessionId;
|
private String sessionId;
|
||||||
private String terminalId;
|
private String terminalId;
|
||||||
private String deviceId;
|
private String deviceId;
|
||||||
|
private String speakerDeviceId;
|
||||||
|
private String micDeviceId;
|
||||||
private String operatorId;
|
private String operatorId;
|
||||||
private volatile WebSocketSession webSocketSession;
|
private volatile WebSocketSession webSocketSession;
|
||||||
private volatile RobotAudioStream robotStream;
|
private volatile RobotAudioStream robotStream;
|
||||||
private volatile Consumer<byte[]> robotAudioConsumer;
|
private volatile Consumer<byte[]> robotAudioConsumer;
|
||||||
|
private volatile ScheduledExecutorService robotBrowserAudioExecutor;
|
||||||
|
private volatile ScheduledFuture<?> robotBrowserAudioFuture;
|
||||||
|
private final LinkedBlockingQueue<byte[]> robotBrowserAudioQueue = new LinkedBlockingQueue<>();
|
||||||
|
private final Object robotBrowserAudioLock = new Object();
|
||||||
|
private byte[] robotBrowserRemainder = new byte[0];
|
||||||
|
private volatile long robotBrowserAudioEnqueued;
|
||||||
|
private volatile long robotBrowserAudioSent;
|
||||||
|
private volatile long robotBrowserAudioDropped;
|
||||||
|
private volatile long lastRobotBrowserAudioLogAt;
|
||||||
private volatile AudioSessionState state;
|
private volatile AudioSessionState state;
|
||||||
private volatile long createdAt;
|
private volatile long createdAt;
|
||||||
private volatile long lastBrowserSignalAt;
|
private volatile long lastBrowserSignalAt;
|
||||||
@ -588,4 +735,19 @@ public class AudioService implements RobotAudioStreamListener {
|
|||||||
private String sdpMid;
|
private String sdpMid;
|
||||||
private int sdpMLineIndex;
|
private int sdpMLineIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static class NamedThreadFactory implements ThreadFactory {
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
private NamedThreadFactory(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Thread newThread(Runnable runnable) {
|
||||||
|
Thread thread = new Thread(runnable, name);
|
||||||
|
thread.setDaemon(true);
|
||||||
|
return thread;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -170,6 +170,37 @@ public final class MicPhoneServiceGrpc {
|
|||||||
return getResumeRecordMethod;
|
return getResumeRecordMethod;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static volatile io.grpc.MethodDescriptor<cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Request,
|
||||||
|
cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Feedback> getStreamAudioMethod;
|
||||||
|
|
||||||
|
@io.grpc.stub.annotations.RpcMethod(
|
||||||
|
fullMethodName = SERVICE_NAME + '/' + "StreamAudio",
|
||||||
|
requestType = cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Request.class,
|
||||||
|
responseType = cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Feedback.class,
|
||||||
|
methodType = io.grpc.MethodDescriptor.MethodType.SERVER_STREAMING)
|
||||||
|
public static io.grpc.MethodDescriptor<cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Request,
|
||||||
|
cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Feedback> getStreamAudioMethod() {
|
||||||
|
io.grpc.MethodDescriptor<cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Request, cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Feedback> getStreamAudioMethod;
|
||||||
|
if ((getStreamAudioMethod = MicPhoneServiceGrpc.getStreamAudioMethod) == null) {
|
||||||
|
synchronized (MicPhoneServiceGrpc.class) {
|
||||||
|
if ((getStreamAudioMethod = MicPhoneServiceGrpc.getStreamAudioMethod) == null) {
|
||||||
|
MicPhoneServiceGrpc.getStreamAudioMethod = getStreamAudioMethod =
|
||||||
|
io.grpc.MethodDescriptor.<cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Request, cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Feedback>newBuilder()
|
||||||
|
.setType(io.grpc.MethodDescriptor.MethodType.SERVER_STREAMING)
|
||||||
|
.setFullMethodName(generateFullMethodName(SERVICE_NAME, "StreamAudio"))
|
||||||
|
.setSampledToLocalTracing(true)
|
||||||
|
.setRequestMarshaller(io.grpc.protobuf.ProtoUtils.marshaller(
|
||||||
|
cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Request.getDefaultInstance()))
|
||||||
|
.setResponseMarshaller(io.grpc.protobuf.ProtoUtils.marshaller(
|
||||||
|
cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Feedback.getDefaultInstance()))
|
||||||
|
.setSchemaDescriptor(new MicPhoneServiceMethodDescriptorSupplier("StreamAudio"))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return getStreamAudioMethod;
|
||||||
|
}
|
||||||
|
|
||||||
private static volatile io.grpc.MethodDescriptor<cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Request,
|
private static volatile io.grpc.MethodDescriptor<cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Request,
|
||||||
cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Feedback> getSetVolumeMethod;
|
cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Feedback> getSetVolumeMethod;
|
||||||
|
|
||||||
@ -281,9 +312,6 @@ public final class MicPhoneServiceGrpc {
|
|||||||
public static abstract class MicPhoneServiceImplBase implements io.grpc.BindableService {
|
public static abstract class MicPhoneServiceImplBase implements io.grpc.BindableService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <pre>
|
|
||||||
* 基本控制
|
|
||||||
* </pre>
|
|
||||||
*/
|
*/
|
||||||
public void getStatus(cmvr.api.MicrophoneCommand.GetMicStateCommand.Request request,
|
public void getStatus(cmvr.api.MicrophoneCommand.GetMicStateCommand.Request request,
|
||||||
io.grpc.stub.StreamObserver<cmvr.api.MicrophoneCommand.GetMicStateCommand.Feedback> responseObserver) {
|
io.grpc.stub.StreamObserver<cmvr.api.MicrophoneCommand.GetMicStateCommand.Feedback> responseObserver) {
|
||||||
@ -319,9 +347,13 @@ public final class MicPhoneServiceGrpc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <pre>
|
*/
|
||||||
* 音量控制
|
public void streamAudio(cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Request request,
|
||||||
* </pre>
|
io.grpc.stub.StreamObserver<cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Feedback> responseObserver) {
|
||||||
|
io.grpc.stub.ServerCalls.asyncUnimplementedUnaryCall(getStreamAudioMethod(), responseObserver);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
*/
|
*/
|
||||||
public void setVolume(cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Request request,
|
public void setVolume(cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Request request,
|
||||||
io.grpc.stub.StreamObserver<cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Feedback> responseObserver) {
|
io.grpc.stub.StreamObserver<cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Feedback> responseObserver) {
|
||||||
@ -372,6 +404,13 @@ public final class MicPhoneServiceGrpc {
|
|||||||
cmvr.api.MicrophoneCommand.ResumeMicRecordingCommand.Request,
|
cmvr.api.MicrophoneCommand.ResumeMicRecordingCommand.Request,
|
||||||
cmvr.api.MicrophoneCommand.ResumeMicRecordingCommand.Feedback>(
|
cmvr.api.MicrophoneCommand.ResumeMicRecordingCommand.Feedback>(
|
||||||
this, METHODID_RESUME_RECORD)))
|
this, METHODID_RESUME_RECORD)))
|
||||||
|
.addMethod(
|
||||||
|
getStreamAudioMethod(),
|
||||||
|
io.grpc.stub.ServerCalls.asyncServerStreamingCall(
|
||||||
|
new MethodHandlers<
|
||||||
|
cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Request,
|
||||||
|
cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Feedback>(
|
||||||
|
this, METHODID_STREAM_AUDIO)))
|
||||||
.addMethod(
|
.addMethod(
|
||||||
getSetVolumeMethod(),
|
getSetVolumeMethod(),
|
||||||
io.grpc.stub.ServerCalls.asyncUnaryCall(
|
io.grpc.stub.ServerCalls.asyncUnaryCall(
|
||||||
@ -405,9 +444,6 @@ public final class MicPhoneServiceGrpc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <pre>
|
|
||||||
* 基本控制
|
|
||||||
* </pre>
|
|
||||||
*/
|
*/
|
||||||
public void getStatus(cmvr.api.MicrophoneCommand.GetMicStateCommand.Request request,
|
public void getStatus(cmvr.api.MicrophoneCommand.GetMicStateCommand.Request request,
|
||||||
io.grpc.stub.StreamObserver<cmvr.api.MicrophoneCommand.GetMicStateCommand.Feedback> responseObserver) {
|
io.grpc.stub.StreamObserver<cmvr.api.MicrophoneCommand.GetMicStateCommand.Feedback> responseObserver) {
|
||||||
@ -448,9 +484,14 @@ public final class MicPhoneServiceGrpc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <pre>
|
*/
|
||||||
* 音量控制
|
public void streamAudio(cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Request request,
|
||||||
* </pre>
|
io.grpc.stub.StreamObserver<cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Feedback> responseObserver) {
|
||||||
|
io.grpc.stub.ClientCalls.asyncServerStreamingCall(
|
||||||
|
getChannel().newCall(getStreamAudioMethod(), getCallOptions()), request, responseObserver);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
*/
|
*/
|
||||||
public void setVolume(cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Request request,
|
public void setVolume(cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Request request,
|
||||||
io.grpc.stub.StreamObserver<cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Feedback> responseObserver) {
|
io.grpc.stub.StreamObserver<cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Feedback> responseObserver) {
|
||||||
@ -482,9 +523,6 @@ public final class MicPhoneServiceGrpc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <pre>
|
|
||||||
* 基本控制
|
|
||||||
* </pre>
|
|
||||||
*/
|
*/
|
||||||
public cmvr.api.MicrophoneCommand.GetMicStateCommand.Feedback getStatus(cmvr.api.MicrophoneCommand.GetMicStateCommand.Request request) {
|
public cmvr.api.MicrophoneCommand.GetMicStateCommand.Feedback getStatus(cmvr.api.MicrophoneCommand.GetMicStateCommand.Request request) {
|
||||||
return io.grpc.stub.ClientCalls.blockingUnaryCall(
|
return io.grpc.stub.ClientCalls.blockingUnaryCall(
|
||||||
@ -520,9 +558,14 @@ public final class MicPhoneServiceGrpc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <pre>
|
*/
|
||||||
* 音量控制
|
public java.util.Iterator<cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Feedback> streamAudio(
|
||||||
* </pre>
|
cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Request request) {
|
||||||
|
return io.grpc.stub.ClientCalls.blockingServerStreamingCall(
|
||||||
|
getChannel(), getStreamAudioMethod(), getCallOptions(), request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
*/
|
*/
|
||||||
public cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Feedback setVolume(cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Request request) {
|
public cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Feedback setVolume(cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Request request) {
|
||||||
return io.grpc.stub.ClientCalls.blockingUnaryCall(
|
return io.grpc.stub.ClientCalls.blockingUnaryCall(
|
||||||
@ -552,9 +595,6 @@ public final class MicPhoneServiceGrpc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <pre>
|
|
||||||
* 基本控制
|
|
||||||
* </pre>
|
|
||||||
*/
|
*/
|
||||||
public com.google.common.util.concurrent.ListenableFuture<cmvr.api.MicrophoneCommand.GetMicStateCommand.Feedback> getStatus(
|
public com.google.common.util.concurrent.ListenableFuture<cmvr.api.MicrophoneCommand.GetMicStateCommand.Feedback> getStatus(
|
||||||
cmvr.api.MicrophoneCommand.GetMicStateCommand.Request request) {
|
cmvr.api.MicrophoneCommand.GetMicStateCommand.Request request) {
|
||||||
@ -595,9 +635,6 @@ public final class MicPhoneServiceGrpc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <pre>
|
|
||||||
* 音量控制
|
|
||||||
* </pre>
|
|
||||||
*/
|
*/
|
||||||
public com.google.common.util.concurrent.ListenableFuture<cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Feedback> setVolume(
|
public com.google.common.util.concurrent.ListenableFuture<cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Feedback> setVolume(
|
||||||
cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Request request) {
|
cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Request request) {
|
||||||
@ -619,8 +656,9 @@ public final class MicPhoneServiceGrpc {
|
|||||||
private static final int METHODID_STOP_RECORD = 2;
|
private static final int METHODID_STOP_RECORD = 2;
|
||||||
private static final int METHODID_PAUSE_RECORD = 3;
|
private static final int METHODID_PAUSE_RECORD = 3;
|
||||||
private static final int METHODID_RESUME_RECORD = 4;
|
private static final int METHODID_RESUME_RECORD = 4;
|
||||||
private static final int METHODID_SET_VOLUME = 5;
|
private static final int METHODID_STREAM_AUDIO = 5;
|
||||||
private static final int METHODID_GET_VOLUME = 6;
|
private static final int METHODID_SET_VOLUME = 6;
|
||||||
|
private static final int METHODID_GET_VOLUME = 7;
|
||||||
|
|
||||||
private static final class MethodHandlers<Req, Resp> implements
|
private static final class MethodHandlers<Req, Resp> implements
|
||||||
io.grpc.stub.ServerCalls.UnaryMethod<Req, Resp>,
|
io.grpc.stub.ServerCalls.UnaryMethod<Req, Resp>,
|
||||||
@ -659,6 +697,10 @@ public final class MicPhoneServiceGrpc {
|
|||||||
serviceImpl.resumeRecord((cmvr.api.MicrophoneCommand.ResumeMicRecordingCommand.Request) request,
|
serviceImpl.resumeRecord((cmvr.api.MicrophoneCommand.ResumeMicRecordingCommand.Request) request,
|
||||||
(io.grpc.stub.StreamObserver<cmvr.api.MicrophoneCommand.ResumeMicRecordingCommand.Feedback>) responseObserver);
|
(io.grpc.stub.StreamObserver<cmvr.api.MicrophoneCommand.ResumeMicRecordingCommand.Feedback>) responseObserver);
|
||||||
break;
|
break;
|
||||||
|
case METHODID_STREAM_AUDIO:
|
||||||
|
serviceImpl.streamAudio((cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Request) request,
|
||||||
|
(io.grpc.stub.StreamObserver<cmvr.api.MicrophoneCommand.StreamMicAudioCommand.Feedback>) responseObserver);
|
||||||
|
break;
|
||||||
case METHODID_SET_VOLUME:
|
case METHODID_SET_VOLUME:
|
||||||
serviceImpl.setVolume((cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Request) request,
|
serviceImpl.setVolume((cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Request) request,
|
||||||
(io.grpc.stub.StreamObserver<cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Feedback>) responseObserver);
|
(io.grpc.stub.StreamObserver<cmvr.api.MicrophoneCommand.SetMicPhoneVolumeCommand.Feedback>) responseObserver);
|
||||||
@ -733,6 +775,7 @@ public final class MicPhoneServiceGrpc {
|
|||||||
.addMethod(getStopRecordMethod())
|
.addMethod(getStopRecordMethod())
|
||||||
.addMethod(getPauseRecordMethod())
|
.addMethod(getPauseRecordMethod())
|
||||||
.addMethod(getResumeRecordMethod())
|
.addMethod(getResumeRecordMethod())
|
||||||
|
.addMethod(getStreamAudioMethod())
|
||||||
.addMethod(getSetVolumeMethod())
|
.addMethod(getSetVolumeMethod())
|
||||||
.addMethod(getGetVolumeMethod())
|
.addMethod(getGetVolumeMethod())
|
||||||
.build();
|
.build();
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -25,7 +25,7 @@ public final class MicrophoneService {
|
|||||||
java.lang.String[] descriptorData = {
|
java.lang.String[] descriptorData = {
|
||||||
"\n!cmvr/api/microphone_service.proto\022\010cmv" +
|
"\n!cmvr/api/microphone_service.proto\022\010cmv" +
|
||||||
"r.api\032!cmvr/api/microphone_command.proto" +
|
"r.api\032!cmvr/api/microphone_command.proto" +
|
||||||
"2\327\005\n\017MicPhoneService\022X\n\tGetStatus\022$.cmvr" +
|
"2\273\006\n\017MicPhoneService\022X\n\tGetStatus\022$.cmvr" +
|
||||||
".api.GetMicStateCommand.Request\032%.cmvr.a" +
|
".api.GetMicStateCommand.Request\032%.cmvr.a" +
|
||||||
"pi.GetMicStateCommand.Feedback\022f\n\013StartR" +
|
"pi.GetMicStateCommand.Feedback\022f\n\013StartR" +
|
||||||
"ecord\022*.cmvr.api.StartMicRecordingComman" +
|
"ecord\022*.cmvr.api.StartMicRecordingComman" +
|
||||||
@ -38,12 +38,14 @@ public final class MicrophoneService {
|
|||||||
"ingCommand.Feedback\022i\n\014ResumeRecord\022+.cm" +
|
"ingCommand.Feedback\022i\n\014ResumeRecord\022+.cm" +
|
||||||
"vr.api.ResumeMicRecordingCommand.Request" +
|
"vr.api.ResumeMicRecordingCommand.Request" +
|
||||||
"\032,.cmvr.api.ResumeMicRecordingCommand.Fe" +
|
"\032,.cmvr.api.ResumeMicRecordingCommand.Fe" +
|
||||||
"edback\022d\n\tSetVolume\022*.cmvr.api.SetMicPho" +
|
"edback\022b\n\013StreamAudio\022\'.cmvr.api.StreamM" +
|
||||||
"neVolumeCommand.Request\032+.cmvr.api.SetMi" +
|
"icAudioCommand.Request\032(.cmvr.api.Stream" +
|
||||||
"cPhoneVolumeCommand.Feedback\022d\n\tGetVolum" +
|
"MicAudioCommand.Feedback0\001\022d\n\tSetVolume\022" +
|
||||||
"e\022*.cmvr.api.GetMicPhoneVolumeCommand.Re" +
|
"*.cmvr.api.SetMicPhoneVolumeCommand.Requ" +
|
||||||
"quest\032+.cmvr.api.GetMicPhoneVolumeComman" +
|
"est\032+.cmvr.api.SetMicPhoneVolumeCommand." +
|
||||||
"d.Feedbackb\006proto3"
|
"Feedback\022d\n\tGetVolume\022*.cmvr.api.GetMicP" +
|
||||||
|
"honeVolumeCommand.Request\032+.cmvr.api.Get" +
|
||||||
|
"MicPhoneVolumeCommand.Feedbackb\006proto3"
|
||||||
};
|
};
|
||||||
descriptor = com.google.protobuf.Descriptors.FileDescriptor
|
descriptor = com.google.protobuf.Descriptors.FileDescriptor
|
||||||
.internalBuildGeneratedFileFrom(descriptorData,
|
.internalBuildGeneratedFileFrom(descriptorData,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1,280 +0,0 @@
|
|||||||
package cmvr.api;
|
|
||||||
|
|
||||||
import static io.grpc.MethodDescriptor.generateFullMethodName;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* <pre>
|
|
||||||
* Real-time bidirectional robot audio service.
|
|
||||||
* Java backend is the gRPC client; robot terminal is the gRPC server.
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
@javax.annotation.Generated(
|
|
||||||
value = "by gRPC proto compiler (version 1.52.0)",
|
|
||||||
comments = "Source: cmvr/api/robot_audio.proto")
|
|
||||||
@io.grpc.stub.annotations.GrpcGenerated
|
|
||||||
public final class RobotAudioServiceGrpc {
|
|
||||||
|
|
||||||
private RobotAudioServiceGrpc() {}
|
|
||||||
|
|
||||||
public static final String SERVICE_NAME = "cmvr.api.RobotAudioService";
|
|
||||||
|
|
||||||
// Static method descriptors that strictly reflect the proto.
|
|
||||||
private static volatile io.grpc.MethodDescriptor<cmvr.api.RobotAudio.RobotAudioRequest,
|
|
||||||
cmvr.api.RobotAudio.RobotAudioResponse> getAudioTalkMethod;
|
|
||||||
|
|
||||||
@io.grpc.stub.annotations.RpcMethod(
|
|
||||||
fullMethodName = SERVICE_NAME + '/' + "AudioTalk",
|
|
||||||
requestType = cmvr.api.RobotAudio.RobotAudioRequest.class,
|
|
||||||
responseType = cmvr.api.RobotAudio.RobotAudioResponse.class,
|
|
||||||
methodType = io.grpc.MethodDescriptor.MethodType.BIDI_STREAMING)
|
|
||||||
public static io.grpc.MethodDescriptor<cmvr.api.RobotAudio.RobotAudioRequest,
|
|
||||||
cmvr.api.RobotAudio.RobotAudioResponse> getAudioTalkMethod() {
|
|
||||||
io.grpc.MethodDescriptor<cmvr.api.RobotAudio.RobotAudioRequest, cmvr.api.RobotAudio.RobotAudioResponse> getAudioTalkMethod;
|
|
||||||
if ((getAudioTalkMethod = RobotAudioServiceGrpc.getAudioTalkMethod) == null) {
|
|
||||||
synchronized (RobotAudioServiceGrpc.class) {
|
|
||||||
if ((getAudioTalkMethod = RobotAudioServiceGrpc.getAudioTalkMethod) == null) {
|
|
||||||
RobotAudioServiceGrpc.getAudioTalkMethod = getAudioTalkMethod =
|
|
||||||
io.grpc.MethodDescriptor.<cmvr.api.RobotAudio.RobotAudioRequest, cmvr.api.RobotAudio.RobotAudioResponse>newBuilder()
|
|
||||||
.setType(io.grpc.MethodDescriptor.MethodType.BIDI_STREAMING)
|
|
||||||
.setFullMethodName(generateFullMethodName(SERVICE_NAME, "AudioTalk"))
|
|
||||||
.setSampledToLocalTracing(true)
|
|
||||||
.setRequestMarshaller(io.grpc.protobuf.ProtoUtils.marshaller(
|
|
||||||
cmvr.api.RobotAudio.RobotAudioRequest.getDefaultInstance()))
|
|
||||||
.setResponseMarshaller(io.grpc.protobuf.ProtoUtils.marshaller(
|
|
||||||
cmvr.api.RobotAudio.RobotAudioResponse.getDefaultInstance()))
|
|
||||||
.setSchemaDescriptor(new RobotAudioServiceMethodDescriptorSupplier("AudioTalk"))
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return getAudioTalkMethod;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new async stub that supports all call types for the service
|
|
||||||
*/
|
|
||||||
public static RobotAudioServiceStub newStub(io.grpc.Channel channel) {
|
|
||||||
io.grpc.stub.AbstractStub.StubFactory<RobotAudioServiceStub> factory =
|
|
||||||
new io.grpc.stub.AbstractStub.StubFactory<RobotAudioServiceStub>() {
|
|
||||||
@java.lang.Override
|
|
||||||
public RobotAudioServiceStub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
|
|
||||||
return new RobotAudioServiceStub(channel, callOptions);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return RobotAudioServiceStub.newStub(factory, channel);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new blocking-style stub that supports unary and streaming output calls on the service
|
|
||||||
*/
|
|
||||||
public static RobotAudioServiceBlockingStub newBlockingStub(
|
|
||||||
io.grpc.Channel channel) {
|
|
||||||
io.grpc.stub.AbstractStub.StubFactory<RobotAudioServiceBlockingStub> factory =
|
|
||||||
new io.grpc.stub.AbstractStub.StubFactory<RobotAudioServiceBlockingStub>() {
|
|
||||||
@java.lang.Override
|
|
||||||
public RobotAudioServiceBlockingStub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
|
|
||||||
return new RobotAudioServiceBlockingStub(channel, callOptions);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return RobotAudioServiceBlockingStub.newStub(factory, channel);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new ListenableFuture-style stub that supports unary calls on the service
|
|
||||||
*/
|
|
||||||
public static RobotAudioServiceFutureStub newFutureStub(
|
|
||||||
io.grpc.Channel channel) {
|
|
||||||
io.grpc.stub.AbstractStub.StubFactory<RobotAudioServiceFutureStub> factory =
|
|
||||||
new io.grpc.stub.AbstractStub.StubFactory<RobotAudioServiceFutureStub>() {
|
|
||||||
@java.lang.Override
|
|
||||||
public RobotAudioServiceFutureStub newStub(io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
|
|
||||||
return new RobotAudioServiceFutureStub(channel, callOptions);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return RobotAudioServiceFutureStub.newStub(factory, channel);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* <pre>
|
|
||||||
* Real-time bidirectional robot audio service.
|
|
||||||
* Java backend is the gRPC client; robot terminal is the gRPC server.
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
public static abstract class RobotAudioServiceImplBase implements io.grpc.BindableService {
|
|
||||||
|
|
||||||
/**
|
|
||||||
*/
|
|
||||||
public io.grpc.stub.StreamObserver<cmvr.api.RobotAudio.RobotAudioRequest> audioTalk(
|
|
||||||
io.grpc.stub.StreamObserver<cmvr.api.RobotAudio.RobotAudioResponse> responseObserver) {
|
|
||||||
return io.grpc.stub.ServerCalls.asyncUnimplementedStreamingCall(getAudioTalkMethod(), responseObserver);
|
|
||||||
}
|
|
||||||
|
|
||||||
@java.lang.Override public final io.grpc.ServerServiceDefinition bindService() {
|
|
||||||
return io.grpc.ServerServiceDefinition.builder(getServiceDescriptor())
|
|
||||||
.addMethod(
|
|
||||||
getAudioTalkMethod(),
|
|
||||||
io.grpc.stub.ServerCalls.asyncBidiStreamingCall(
|
|
||||||
new MethodHandlers<
|
|
||||||
cmvr.api.RobotAudio.RobotAudioRequest,
|
|
||||||
cmvr.api.RobotAudio.RobotAudioResponse>(
|
|
||||||
this, METHODID_AUDIO_TALK)))
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* <pre>
|
|
||||||
* Real-time bidirectional robot audio service.
|
|
||||||
* Java backend is the gRPC client; robot terminal is the gRPC server.
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
public static final class RobotAudioServiceStub extends io.grpc.stub.AbstractAsyncStub<RobotAudioServiceStub> {
|
|
||||||
private RobotAudioServiceStub(
|
|
||||||
io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
|
|
||||||
super(channel, callOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
@java.lang.Override
|
|
||||||
protected RobotAudioServiceStub build(
|
|
||||||
io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
|
|
||||||
return new RobotAudioServiceStub(channel, callOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*/
|
|
||||||
public io.grpc.stub.StreamObserver<cmvr.api.RobotAudio.RobotAudioRequest> audioTalk(
|
|
||||||
io.grpc.stub.StreamObserver<cmvr.api.RobotAudio.RobotAudioResponse> responseObserver) {
|
|
||||||
return io.grpc.stub.ClientCalls.asyncBidiStreamingCall(
|
|
||||||
getChannel().newCall(getAudioTalkMethod(), getCallOptions()), responseObserver);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* <pre>
|
|
||||||
* Real-time bidirectional robot audio service.
|
|
||||||
* Java backend is the gRPC client; robot terminal is the gRPC server.
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
public static final class RobotAudioServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub<RobotAudioServiceBlockingStub> {
|
|
||||||
private RobotAudioServiceBlockingStub(
|
|
||||||
io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
|
|
||||||
super(channel, callOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
@java.lang.Override
|
|
||||||
protected RobotAudioServiceBlockingStub build(
|
|
||||||
io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
|
|
||||||
return new RobotAudioServiceBlockingStub(channel, callOptions);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* <pre>
|
|
||||||
* Real-time bidirectional robot audio service.
|
|
||||||
* Java backend is the gRPC client; robot terminal is the gRPC server.
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
public static final class RobotAudioServiceFutureStub extends io.grpc.stub.AbstractFutureStub<RobotAudioServiceFutureStub> {
|
|
||||||
private RobotAudioServiceFutureStub(
|
|
||||||
io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
|
|
||||||
super(channel, callOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
@java.lang.Override
|
|
||||||
protected RobotAudioServiceFutureStub build(
|
|
||||||
io.grpc.Channel channel, io.grpc.CallOptions callOptions) {
|
|
||||||
return new RobotAudioServiceFutureStub(channel, callOptions);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final int METHODID_AUDIO_TALK = 0;
|
|
||||||
|
|
||||||
private static final class MethodHandlers<Req, Resp> implements
|
|
||||||
io.grpc.stub.ServerCalls.UnaryMethod<Req, Resp>,
|
|
||||||
io.grpc.stub.ServerCalls.ServerStreamingMethod<Req, Resp>,
|
|
||||||
io.grpc.stub.ServerCalls.ClientStreamingMethod<Req, Resp>,
|
|
||||||
io.grpc.stub.ServerCalls.BidiStreamingMethod<Req, Resp> {
|
|
||||||
private final RobotAudioServiceImplBase serviceImpl;
|
|
||||||
private final int methodId;
|
|
||||||
|
|
||||||
MethodHandlers(RobotAudioServiceImplBase serviceImpl, int methodId) {
|
|
||||||
this.serviceImpl = serviceImpl;
|
|
||||||
this.methodId = methodId;
|
|
||||||
}
|
|
||||||
|
|
||||||
@java.lang.Override
|
|
||||||
@java.lang.SuppressWarnings("unchecked")
|
|
||||||
public void invoke(Req request, io.grpc.stub.StreamObserver<Resp> responseObserver) {
|
|
||||||
switch (methodId) {
|
|
||||||
default:
|
|
||||||
throw new AssertionError();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@java.lang.Override
|
|
||||||
@java.lang.SuppressWarnings("unchecked")
|
|
||||||
public io.grpc.stub.StreamObserver<Req> invoke(
|
|
||||||
io.grpc.stub.StreamObserver<Resp> responseObserver) {
|
|
||||||
switch (methodId) {
|
|
||||||
case METHODID_AUDIO_TALK:
|
|
||||||
return (io.grpc.stub.StreamObserver<Req>) serviceImpl.audioTalk(
|
|
||||||
(io.grpc.stub.StreamObserver<cmvr.api.RobotAudio.RobotAudioResponse>) responseObserver);
|
|
||||||
default:
|
|
||||||
throw new AssertionError();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static abstract class RobotAudioServiceBaseDescriptorSupplier
|
|
||||||
implements io.grpc.protobuf.ProtoFileDescriptorSupplier, io.grpc.protobuf.ProtoServiceDescriptorSupplier {
|
|
||||||
RobotAudioServiceBaseDescriptorSupplier() {}
|
|
||||||
|
|
||||||
@java.lang.Override
|
|
||||||
public com.google.protobuf.Descriptors.FileDescriptor getFileDescriptor() {
|
|
||||||
return cmvr.api.RobotAudio.getDescriptor();
|
|
||||||
}
|
|
||||||
|
|
||||||
@java.lang.Override
|
|
||||||
public com.google.protobuf.Descriptors.ServiceDescriptor getServiceDescriptor() {
|
|
||||||
return getFileDescriptor().findServiceByName("RobotAudioService");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class RobotAudioServiceFileDescriptorSupplier
|
|
||||||
extends RobotAudioServiceBaseDescriptorSupplier {
|
|
||||||
RobotAudioServiceFileDescriptorSupplier() {}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final class RobotAudioServiceMethodDescriptorSupplier
|
|
||||||
extends RobotAudioServiceBaseDescriptorSupplier
|
|
||||||
implements io.grpc.protobuf.ProtoMethodDescriptorSupplier {
|
|
||||||
private final String methodName;
|
|
||||||
|
|
||||||
RobotAudioServiceMethodDescriptorSupplier(String methodName) {
|
|
||||||
this.methodName = methodName;
|
|
||||||
}
|
|
||||||
|
|
||||||
@java.lang.Override
|
|
||||||
public com.google.protobuf.Descriptors.MethodDescriptor getMethodDescriptor() {
|
|
||||||
return getServiceDescriptor().findMethodByName(methodName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static volatile io.grpc.ServiceDescriptor serviceDescriptor;
|
|
||||||
|
|
||||||
public static io.grpc.ServiceDescriptor getServiceDescriptor() {
|
|
||||||
io.grpc.ServiceDescriptor result = serviceDescriptor;
|
|
||||||
if (result == null) {
|
|
||||||
synchronized (RobotAudioServiceGrpc.class) {
|
|
||||||
result = serviceDescriptor;
|
|
||||||
if (result == null) {
|
|
||||||
serviceDescriptor = result = io.grpc.ServiceDescriptor.newBuilder(SERVICE_NAME)
|
|
||||||
.setSchemaDescriptor(new RobotAudioServiceFileDescriptorSupplier())
|
|
||||||
.addMethod(getAudioTalkMethod())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -77,6 +77,37 @@ public final class SpeakerServiceGrpc {
|
|||||||
return getPlayAudioMethod;
|
return getPlayAudioMethod;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static volatile io.grpc.MethodDescriptor<cmvr.api.SpeakerCommand.StreamSpeakerAudioCommand.Request,
|
||||||
|
cmvr.api.SpeakerCommand.StreamSpeakerAudioCommand.Feedback> getStreamAudioMethod;
|
||||||
|
|
||||||
|
@io.grpc.stub.annotations.RpcMethod(
|
||||||
|
fullMethodName = SERVICE_NAME + '/' + "StreamAudio",
|
||||||
|
requestType = cmvr.api.SpeakerCommand.StreamSpeakerAudioCommand.Request.class,
|
||||||
|
responseType = cmvr.api.SpeakerCommand.StreamSpeakerAudioCommand.Feedback.class,
|
||||||
|
methodType = io.grpc.MethodDescriptor.MethodType.CLIENT_STREAMING)
|
||||||
|
public static io.grpc.MethodDescriptor<cmvr.api.SpeakerCommand.StreamSpeakerAudioCommand.Request,
|
||||||
|
cmvr.api.SpeakerCommand.StreamSpeakerAudioCommand.Feedback> getStreamAudioMethod() {
|
||||||
|
io.grpc.MethodDescriptor<cmvr.api.SpeakerCommand.StreamSpeakerAudioCommand.Request, cmvr.api.SpeakerCommand.StreamSpeakerAudioCommand.Feedback> getStreamAudioMethod;
|
||||||
|
if ((getStreamAudioMethod = SpeakerServiceGrpc.getStreamAudioMethod) == null) {
|
||||||
|
synchronized (SpeakerServiceGrpc.class) {
|
||||||
|
if ((getStreamAudioMethod = SpeakerServiceGrpc.getStreamAudioMethod) == null) {
|
||||||
|
SpeakerServiceGrpc.getStreamAudioMethod = getStreamAudioMethod =
|
||||||
|
io.grpc.MethodDescriptor.<cmvr.api.SpeakerCommand.StreamSpeakerAudioCommand.Request, cmvr.api.SpeakerCommand.StreamSpeakerAudioCommand.Feedback>newBuilder()
|
||||||
|
.setType(io.grpc.MethodDescriptor.MethodType.CLIENT_STREAMING)
|
||||||
|
.setFullMethodName(generateFullMethodName(SERVICE_NAME, "StreamAudio"))
|
||||||
|
.setSampledToLocalTracing(true)
|
||||||
|
.setRequestMarshaller(io.grpc.protobuf.ProtoUtils.marshaller(
|
||||||
|
cmvr.api.SpeakerCommand.StreamSpeakerAudioCommand.Request.getDefaultInstance()))
|
||||||
|
.setResponseMarshaller(io.grpc.protobuf.ProtoUtils.marshaller(
|
||||||
|
cmvr.api.SpeakerCommand.StreamSpeakerAudioCommand.Feedback.getDefaultInstance()))
|
||||||
|
.setSchemaDescriptor(new SpeakerServiceMethodDescriptorSupplier("StreamAudio"))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return getStreamAudioMethod;
|
||||||
|
}
|
||||||
|
|
||||||
private static volatile io.grpc.MethodDescriptor<cmvr.api.SpeakerCommand.StopSpeakerCommand.Request,
|
private static volatile io.grpc.MethodDescriptor<cmvr.api.SpeakerCommand.StopSpeakerCommand.Request,
|
||||||
cmvr.api.SpeakerCommand.StopSpeakerCommand.Feedback> getStopPlaybackMethod;
|
cmvr.api.SpeakerCommand.StopSpeakerCommand.Feedback> getStopPlaybackMethod;
|
||||||
|
|
||||||
@ -281,9 +312,6 @@ public final class SpeakerServiceGrpc {
|
|||||||
public static abstract class SpeakerServiceImplBase implements io.grpc.BindableService {
|
public static abstract class SpeakerServiceImplBase implements io.grpc.BindableService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <pre>
|
|
||||||
* 基本控制
|
|
||||||
* </pre>
|
|
||||||
*/
|
*/
|
||||||
public void getStatus(cmvr.api.SpeakerCommand.GetSpeakerStateCommand.Request request,
|
public void getStatus(cmvr.api.SpeakerCommand.GetSpeakerStateCommand.Request request,
|
||||||
io.grpc.stub.StreamObserver<cmvr.api.SpeakerCommand.GetSpeakerStateCommand.Feedback> responseObserver) {
|
io.grpc.stub.StreamObserver<cmvr.api.SpeakerCommand.GetSpeakerStateCommand.Feedback> responseObserver) {
|
||||||
@ -297,6 +325,13 @@ public final class SpeakerServiceGrpc {
|
|||||||
io.grpc.stub.ServerCalls.asyncUnimplementedUnaryCall(getPlayAudioMethod(), responseObserver);
|
io.grpc.stub.ServerCalls.asyncUnimplementedUnaryCall(getPlayAudioMethod(), responseObserver);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*/
|
||||||
|
public io.grpc.stub.StreamObserver<cmvr.api.SpeakerCommand.StreamSpeakerAudioCommand.Request> streamAudio(
|
||||||
|
io.grpc.stub.StreamObserver<cmvr.api.SpeakerCommand.StreamSpeakerAudioCommand.Feedback> responseObserver) {
|
||||||
|
return io.grpc.stub.ServerCalls.asyncUnimplementedStreamingCall(getStreamAudioMethod(), responseObserver);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*/
|
*/
|
||||||
public void stopPlayback(cmvr.api.SpeakerCommand.StopSpeakerCommand.Request request,
|
public void stopPlayback(cmvr.api.SpeakerCommand.StopSpeakerCommand.Request request,
|
||||||
@ -319,9 +354,6 @@ public final class SpeakerServiceGrpc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <pre>
|
|
||||||
* 音量控制
|
|
||||||
* </pre>
|
|
||||||
*/
|
*/
|
||||||
public void setVolume(cmvr.api.SpeakerCommand.SetSpeakerVolumeCommand.Request request,
|
public void setVolume(cmvr.api.SpeakerCommand.SetSpeakerVolumeCommand.Request request,
|
||||||
io.grpc.stub.StreamObserver<cmvr.api.SpeakerCommand.SetSpeakerVolumeCommand.Feedback> responseObserver) {
|
io.grpc.stub.StreamObserver<cmvr.api.SpeakerCommand.SetSpeakerVolumeCommand.Feedback> responseObserver) {
|
||||||
@ -351,6 +383,13 @@ public final class SpeakerServiceGrpc {
|
|||||||
cmvr.api.SpeakerCommand.PlayAudioCommand.Request,
|
cmvr.api.SpeakerCommand.PlayAudioCommand.Request,
|
||||||
cmvr.api.SpeakerCommand.PlayAudioCommand.Feedback>(
|
cmvr.api.SpeakerCommand.PlayAudioCommand.Feedback>(
|
||||||
this, METHODID_PLAY_AUDIO)))
|
this, METHODID_PLAY_AUDIO)))
|
||||||
|
.addMethod(
|
||||||
|
getStreamAudioMethod(),
|
||||||
|
io.grpc.stub.ServerCalls.asyncClientStreamingCall(
|
||||||
|
new MethodHandlers<
|
||||||
|
cmvr.api.SpeakerCommand.StreamSpeakerAudioCommand.Request,
|
||||||
|
cmvr.api.SpeakerCommand.StreamSpeakerAudioCommand.Feedback>(
|
||||||
|
this, METHODID_STREAM_AUDIO)))
|
||||||
.addMethod(
|
.addMethod(
|
||||||
getStopPlaybackMethod(),
|
getStopPlaybackMethod(),
|
||||||
io.grpc.stub.ServerCalls.asyncUnaryCall(
|
io.grpc.stub.ServerCalls.asyncUnaryCall(
|
||||||
@ -405,9 +444,6 @@ public final class SpeakerServiceGrpc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <pre>
|
|
||||||
* 基本控制
|
|
||||||
* </pre>
|
|
||||||
*/
|
*/
|
||||||
public void getStatus(cmvr.api.SpeakerCommand.GetSpeakerStateCommand.Request request,
|
public void getStatus(cmvr.api.SpeakerCommand.GetSpeakerStateCommand.Request request,
|
||||||
io.grpc.stub.StreamObserver<cmvr.api.SpeakerCommand.GetSpeakerStateCommand.Feedback> responseObserver) {
|
io.grpc.stub.StreamObserver<cmvr.api.SpeakerCommand.GetSpeakerStateCommand.Feedback> responseObserver) {
|
||||||
@ -423,6 +459,14 @@ public final class SpeakerServiceGrpc {
|
|||||||
getChannel().newCall(getPlayAudioMethod(), getCallOptions()), request, responseObserver);
|
getChannel().newCall(getPlayAudioMethod(), getCallOptions()), request, responseObserver);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*/
|
||||||
|
public io.grpc.stub.StreamObserver<cmvr.api.SpeakerCommand.StreamSpeakerAudioCommand.Request> streamAudio(
|
||||||
|
io.grpc.stub.StreamObserver<cmvr.api.SpeakerCommand.StreamSpeakerAudioCommand.Feedback> responseObserver) {
|
||||||
|
return io.grpc.stub.ClientCalls.asyncClientStreamingCall(
|
||||||
|
getChannel().newCall(getStreamAudioMethod(), getCallOptions()), responseObserver);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*/
|
*/
|
||||||
public void stopPlayback(cmvr.api.SpeakerCommand.StopSpeakerCommand.Request request,
|
public void stopPlayback(cmvr.api.SpeakerCommand.StopSpeakerCommand.Request request,
|
||||||
@ -448,9 +492,6 @@ public final class SpeakerServiceGrpc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <pre>
|
|
||||||
* 音量控制
|
|
||||||
* </pre>
|
|
||||||
*/
|
*/
|
||||||
public void setVolume(cmvr.api.SpeakerCommand.SetSpeakerVolumeCommand.Request request,
|
public void setVolume(cmvr.api.SpeakerCommand.SetSpeakerVolumeCommand.Request request,
|
||||||
io.grpc.stub.StreamObserver<cmvr.api.SpeakerCommand.SetSpeakerVolumeCommand.Feedback> responseObserver) {
|
io.grpc.stub.StreamObserver<cmvr.api.SpeakerCommand.SetSpeakerVolumeCommand.Feedback> responseObserver) {
|
||||||
@ -482,9 +523,6 @@ public final class SpeakerServiceGrpc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <pre>
|
|
||||||
* 基本控制
|
|
||||||
* </pre>
|
|
||||||
*/
|
*/
|
||||||
public cmvr.api.SpeakerCommand.GetSpeakerStateCommand.Feedback getStatus(cmvr.api.SpeakerCommand.GetSpeakerStateCommand.Request request) {
|
public cmvr.api.SpeakerCommand.GetSpeakerStateCommand.Feedback getStatus(cmvr.api.SpeakerCommand.GetSpeakerStateCommand.Request request) {
|
||||||
return io.grpc.stub.ClientCalls.blockingUnaryCall(
|
return io.grpc.stub.ClientCalls.blockingUnaryCall(
|
||||||
@ -520,9 +558,6 @@ public final class SpeakerServiceGrpc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <pre>
|
|
||||||
* 音量控制
|
|
||||||
* </pre>
|
|
||||||
*/
|
*/
|
||||||
public cmvr.api.SpeakerCommand.SetSpeakerVolumeCommand.Feedback setVolume(cmvr.api.SpeakerCommand.SetSpeakerVolumeCommand.Request request) {
|
public cmvr.api.SpeakerCommand.SetSpeakerVolumeCommand.Feedback setVolume(cmvr.api.SpeakerCommand.SetSpeakerVolumeCommand.Request request) {
|
||||||
return io.grpc.stub.ClientCalls.blockingUnaryCall(
|
return io.grpc.stub.ClientCalls.blockingUnaryCall(
|
||||||
@ -552,9 +587,6 @@ public final class SpeakerServiceGrpc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <pre>
|
|
||||||
* 基本控制
|
|
||||||
* </pre>
|
|
||||||
*/
|
*/
|
||||||
public com.google.common.util.concurrent.ListenableFuture<cmvr.api.SpeakerCommand.GetSpeakerStateCommand.Feedback> getStatus(
|
public com.google.common.util.concurrent.ListenableFuture<cmvr.api.SpeakerCommand.GetSpeakerStateCommand.Feedback> getStatus(
|
||||||
cmvr.api.SpeakerCommand.GetSpeakerStateCommand.Request request) {
|
cmvr.api.SpeakerCommand.GetSpeakerStateCommand.Request request) {
|
||||||
@ -595,9 +627,6 @@ public final class SpeakerServiceGrpc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <pre>
|
|
||||||
* 音量控制
|
|
||||||
* </pre>
|
|
||||||
*/
|
*/
|
||||||
public com.google.common.util.concurrent.ListenableFuture<cmvr.api.SpeakerCommand.SetSpeakerVolumeCommand.Feedback> setVolume(
|
public com.google.common.util.concurrent.ListenableFuture<cmvr.api.SpeakerCommand.SetSpeakerVolumeCommand.Feedback> setVolume(
|
||||||
cmvr.api.SpeakerCommand.SetSpeakerVolumeCommand.Request request) {
|
cmvr.api.SpeakerCommand.SetSpeakerVolumeCommand.Request request) {
|
||||||
@ -621,6 +650,7 @@ public final class SpeakerServiceGrpc {
|
|||||||
private static final int METHODID_RESUME_PLAYBACK = 4;
|
private static final int METHODID_RESUME_PLAYBACK = 4;
|
||||||
private static final int METHODID_SET_VOLUME = 5;
|
private static final int METHODID_SET_VOLUME = 5;
|
||||||
private static final int METHODID_GET_VOLUME = 6;
|
private static final int METHODID_GET_VOLUME = 6;
|
||||||
|
private static final int METHODID_STREAM_AUDIO = 7;
|
||||||
|
|
||||||
private static final class MethodHandlers<Req, Resp> implements
|
private static final class MethodHandlers<Req, Resp> implements
|
||||||
io.grpc.stub.ServerCalls.UnaryMethod<Req, Resp>,
|
io.grpc.stub.ServerCalls.UnaryMethod<Req, Resp>,
|
||||||
@ -677,6 +707,9 @@ public final class SpeakerServiceGrpc {
|
|||||||
public io.grpc.stub.StreamObserver<Req> invoke(
|
public io.grpc.stub.StreamObserver<Req> invoke(
|
||||||
io.grpc.stub.StreamObserver<Resp> responseObserver) {
|
io.grpc.stub.StreamObserver<Resp> responseObserver) {
|
||||||
switch (methodId) {
|
switch (methodId) {
|
||||||
|
case METHODID_STREAM_AUDIO:
|
||||||
|
return (io.grpc.stub.StreamObserver<Req>) serviceImpl.streamAudio(
|
||||||
|
(io.grpc.stub.StreamObserver<cmvr.api.SpeakerCommand.StreamSpeakerAudioCommand.Feedback>) responseObserver);
|
||||||
default:
|
default:
|
||||||
throw new AssertionError();
|
throw new AssertionError();
|
||||||
}
|
}
|
||||||
@ -730,6 +763,7 @@ public final class SpeakerServiceGrpc {
|
|||||||
.setSchemaDescriptor(new SpeakerServiceFileDescriptorSupplier())
|
.setSchemaDescriptor(new SpeakerServiceFileDescriptorSupplier())
|
||||||
.addMethod(getGetStatusMethod())
|
.addMethod(getGetStatusMethod())
|
||||||
.addMethod(getPlayAudioMethod())
|
.addMethod(getPlayAudioMethod())
|
||||||
|
.addMethod(getStreamAudioMethod())
|
||||||
.addMethod(getStopPlaybackMethod())
|
.addMethod(getStopPlaybackMethod())
|
||||||
.addMethod(getPausePlaybackMethod())
|
.addMethod(getPausePlaybackMethod())
|
||||||
.addMethod(getResumePlaybackMethod())
|
.addMethod(getResumePlaybackMethod())
|
||||||
|
|||||||
@ -24,25 +24,28 @@ public final class SpeakerServiceOuterClass {
|
|||||||
static {
|
static {
|
||||||
java.lang.String[] descriptorData = {
|
java.lang.String[] descriptorData = {
|
||||||
"\n\036cmvr/api/speaker_service.proto\022\010cmvr.a" +
|
"\n\036cmvr/api/speaker_service.proto\022\010cmvr.a" +
|
||||||
"pi\032\036cmvr/api/speaker_command.proto2\260\005\n\016S" +
|
"pi\032\036cmvr/api/speaker_command.proto2\234\006\n\016S" +
|
||||||
"peakerService\022`\n\tGetStatus\022(.cmvr.api.Ge" +
|
"peakerService\022`\n\tGetStatus\022(.cmvr.api.Ge" +
|
||||||
"tSpeakerStateCommand.Request\032).cmvr.api." +
|
"tSpeakerStateCommand.Request\032).cmvr.api." +
|
||||||
"GetSpeakerStateCommand.Feedback\022T\n\tPlayA" +
|
"GetSpeakerStateCommand.Feedback\022T\n\tPlayA" +
|
||||||
"udio\022\".cmvr.api.PlayAudioCommand.Request" +
|
"udio\022\".cmvr.api.PlayAudioCommand.Request" +
|
||||||
"\032#.cmvr.api.PlayAudioCommand.Feedback\022[\n" +
|
"\032#.cmvr.api.PlayAudioCommand.Feedback\022j\n" +
|
||||||
"\014StopPlayback\022$.cmvr.api.StopSpeakerComm" +
|
"\013StreamAudio\022+.cmvr.api.StreamSpeakerAud" +
|
||||||
"and.Request\032%.cmvr.api.StopSpeakerComman" +
|
"ioCommand.Request\032,.cmvr.api.StreamSpeak" +
|
||||||
"d.Feedback\022^\n\rPausePlayback\022%.cmvr.api.P" +
|
"erAudioCommand.Feedback(\001\022[\n\014StopPlaybac" +
|
||||||
"auseSpeakerCommand.Request\032&.cmvr.api.Pa" +
|
"k\022$.cmvr.api.StopSpeakerCommand.Request\032" +
|
||||||
"useSpeakerCommand.Feedback\022a\n\016ResumePlay" +
|
"%.cmvr.api.StopSpeakerCommand.Feedback\022^" +
|
||||||
"back\022&.cmvr.api.ResumeSpeakerCommand.Req" +
|
"\n\rPausePlayback\022%.cmvr.api.PauseSpeakerC" +
|
||||||
"uest\032\'.cmvr.api.ResumeSpeakerCommand.Fee" +
|
"ommand.Request\032&.cmvr.api.PauseSpeakerCo" +
|
||||||
"dback\022b\n\tSetVolume\022).cmvr.api.SetSpeaker" +
|
"mmand.Feedback\022a\n\016ResumePlayback\022&.cmvr." +
|
||||||
"VolumeCommand.Request\032*.cmvr.api.SetSpea" +
|
"api.ResumeSpeakerCommand.Request\032\'.cmvr." +
|
||||||
"kerVolumeCommand.Feedback\022b\n\tGetVolume\022)" +
|
"api.ResumeSpeakerCommand.Feedback\022b\n\tSet" +
|
||||||
".cmvr.api.GetSpeakerVolumeCommand.Reques" +
|
"Volume\022).cmvr.api.SetSpeakerVolumeComman" +
|
||||||
"t\032*.cmvr.api.GetSpeakerVolumeCommand.Fee" +
|
"d.Request\032*.cmvr.api.SetSpeakerVolumeCom" +
|
||||||
"dbackb\006proto3"
|
"mand.Feedback\022b\n\tGetVolume\022).cmvr.api.Ge" +
|
||||||
|
"tSpeakerVolumeCommand.Request\032*.cmvr.api" +
|
||||||
|
".GetSpeakerVolumeCommand.Feedbackb\006proto" +
|
||||||
|
"3"
|
||||||
};
|
};
|
||||||
descriptor = com.google.protobuf.Descriptors.FileDescriptor
|
descriptor = com.google.protobuf.Descriptors.FileDescriptor
|
||||||
.internalBuildGeneratedFileFrom(descriptorData,
|
.internalBuildGeneratedFileFrom(descriptorData,
|
||||||
|
|||||||
@ -29,6 +29,22 @@ message CommandHeader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message AudioData {
|
||||||
|
enum AudioFormat {
|
||||||
|
PCM = 0;
|
||||||
|
MP3 = 1;
|
||||||
|
AAC = 2;
|
||||||
|
WAV = 3;
|
||||||
|
}
|
||||||
|
bytes data = 1;
|
||||||
|
int32 sample_rate = 2;
|
||||||
|
int32 channels = 3;
|
||||||
|
AudioFormat format = 4;
|
||||||
|
string codec = 5;
|
||||||
|
int64 pts = 6;
|
||||||
|
int32 nb_samples = 7;
|
||||||
|
}
|
||||||
|
|
||||||
message ConfigParam {
|
message ConfigParam {
|
||||||
|
|
||||||
string param_name = 1;
|
string param_name = 1;
|
||||||
|
|||||||
@ -11,6 +11,7 @@ message MicState {
|
|||||||
int32 volume = 4;
|
int32 volume = 4;
|
||||||
string error_message = 5;
|
string error_message = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
message GetMicStateCommand {
|
message GetMicStateCommand {
|
||||||
message Request {
|
message Request {
|
||||||
CommandHeader.Request header = 1;
|
CommandHeader.Request header = 1;
|
||||||
@ -21,6 +22,7 @@ message GetMicStateCommand {
|
|||||||
MicState state = 2;
|
MicState state = 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
message StartMicRecordingCommand {
|
message StartMicRecordingCommand {
|
||||||
message Request {
|
message Request {
|
||||||
CommandHeader.Request header = 1;
|
CommandHeader.Request header = 1;
|
||||||
@ -59,10 +61,20 @@ message ResumeMicRecordingCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message StreamMicAudioCommand {
|
||||||
|
message Request {
|
||||||
|
CommandHeader.Request header = 1;
|
||||||
|
}
|
||||||
|
message Feedback {
|
||||||
|
CommandHeader.Feedback header = 1;
|
||||||
|
AudioData audio = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
message SetMicPhoneVolumeCommand {
|
message SetMicPhoneVolumeCommand {
|
||||||
message Request {
|
message Request {
|
||||||
CommandHeader.Request header = 1;
|
CommandHeader.Request header = 1;
|
||||||
int32 volume = 2; // 音量值(0 ~ 100)
|
int32 volume = 2;
|
||||||
}
|
}
|
||||||
message Feedback {
|
message Feedback {
|
||||||
CommandHeader.Feedback header = 1;
|
CommandHeader.Feedback header = 1;
|
||||||
@ -75,7 +87,6 @@ message GetMicPhoneVolumeCommand {
|
|||||||
}
|
}
|
||||||
message Feedback {
|
message Feedback {
|
||||||
CommandHeader.Feedback header = 1;
|
CommandHeader.Feedback header = 1;
|
||||||
int32 volume = 2; // 当前音量值
|
int32 volume = 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,17 +4,14 @@ import "cmvr/api/microphone_command.proto";
|
|||||||
|
|
||||||
package cmvr.api;
|
package cmvr.api;
|
||||||
|
|
||||||
|
|
||||||
service MicPhoneService {
|
service MicPhoneService {
|
||||||
// 基本控制
|
|
||||||
rpc GetStatus(GetMicStateCommand.Request) returns (GetMicStateCommand.Feedback);
|
rpc GetStatus(GetMicStateCommand.Request) returns (GetMicStateCommand.Feedback);
|
||||||
rpc StartRecord(StartMicRecordingCommand.Request) returns (StartMicRecordingCommand.Feedback);
|
rpc StartRecord(StartMicRecordingCommand.Request) returns (StartMicRecordingCommand.Feedback);
|
||||||
rpc StopRecord(StopMicRecordingCommand.Request) returns (StopMicRecordingCommand.Feedback);
|
rpc StopRecord(StopMicRecordingCommand.Request) returns (StopMicRecordingCommand.Feedback);
|
||||||
rpc PauseRecord(PauseMicRecordingCommand.Request) returns (PauseMicRecordingCommand.Feedback);
|
rpc PauseRecord(PauseMicRecordingCommand.Request) returns (PauseMicRecordingCommand.Feedback);
|
||||||
rpc ResumeRecord(ResumeMicRecordingCommand.Request) returns (ResumeMicRecordingCommand.Feedback);
|
rpc ResumeRecord(ResumeMicRecordingCommand.Request) returns (ResumeMicRecordingCommand.Feedback);
|
||||||
|
rpc StreamAudio(StreamMicAudioCommand.Request) returns (stream StreamMicAudioCommand.Feedback);
|
||||||
|
|
||||||
// 音量控制
|
|
||||||
rpc SetVolume(SetMicPhoneVolumeCommand.Request) returns (SetMicPhoneVolumeCommand.Feedback);
|
rpc SetVolume(SetMicPhoneVolumeCommand.Request) returns (SetMicPhoneVolumeCommand.Feedback);
|
||||||
rpc GetVolume(GetMicPhoneVolumeCommand.Request) returns (GetMicPhoneVolumeCommand.Feedback);
|
rpc GetVolume(GetMicPhoneVolumeCommand.Request) returns (GetMicPhoneVolumeCommand.Feedback);
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -1,81 +0,0 @@
|
|||||||
syntax = "proto3";
|
|
||||||
|
|
||||||
import "cmvr/api/common.proto";
|
|
||||||
|
|
||||||
package cmvr.api;
|
|
||||||
|
|
||||||
// Real-time bidirectional robot audio service.
|
|
||||||
// Java backend is the gRPC client; robot terminal is the gRPC server.
|
|
||||||
service RobotAudioService {
|
|
||||||
rpc AudioTalk(stream RobotAudioRequest) returns (stream RobotAudioResponse);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fixed audio format: PCM_S16LE, 48 kHz, 16-bit, mono.
|
|
||||||
// Both directions use 10 ms frames: 480 samples, 960 bytes.
|
|
||||||
message RobotAudioFormat {
|
|
||||||
int32 sample_rate = 1;
|
|
||||||
int32 channels = 2;
|
|
||||||
int32 bits_per_sample = 3;
|
|
||||||
string encoding = 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
message RobotAudioStart {
|
|
||||||
string session_id = 1;
|
|
||||||
string operator_id = 2;
|
|
||||||
RobotAudioFormat format = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
message RobotAudioFrame {
|
|
||||||
string session_id = 1;
|
|
||||||
int64 sequence = 2;
|
|
||||||
// Robot upload: capture timestamp. Browser downlink: backend send timestamp.
|
|
||||||
int64 timestamp_ms = 3;
|
|
||||||
// PCM_S16LE. Fixed 10 ms / 960 bytes at 48 kHz mono 16-bit.
|
|
||||||
bytes pcm = 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
message RobotAudioHeartbeat {
|
|
||||||
string session_id = 1;
|
|
||||||
int64 timestamp_ms = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
message RobotAudioStop {
|
|
||||||
string session_id = 1;
|
|
||||||
string reason = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
message RobotAudioError {
|
|
||||||
string session_id = 1;
|
|
||||||
int32 code = 2;
|
|
||||||
string message = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
message RobotAudioStartAck {
|
|
||||||
string session_id = 1;
|
|
||||||
bool accepted = 2;
|
|
||||||
string message = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
message RobotAudioRequest {
|
|
||||||
CommandHeader.Request header = 1;
|
|
||||||
|
|
||||||
oneof payload {
|
|
||||||
RobotAudioStart start = 2;
|
|
||||||
RobotAudioFrame audio_frame = 3;
|
|
||||||
RobotAudioHeartbeat heartbeat = 4;
|
|
||||||
RobotAudioStop stop = 5;
|
|
||||||
RobotAudioError error = 6;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
message RobotAudioResponse {
|
|
||||||
CommandHeader.Feedback header = 1;
|
|
||||||
|
|
||||||
oneof payload {
|
|
||||||
RobotAudioStartAck start_ack = 2;
|
|
||||||
RobotAudioFrame audio_frame = 3;
|
|
||||||
RobotAudioHeartbeat heartbeat = 4;
|
|
||||||
RobotAudioStop stop = 5;
|
|
||||||
RobotAudioError error = 6;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -4,33 +4,15 @@ import "cmvr/api/common.proto";
|
|||||||
|
|
||||||
package cmvr.api;
|
package cmvr.api;
|
||||||
|
|
||||||
|
|
||||||
// 音频数据格式(与麦克风类似,但用于输出)
|
|
||||||
message AudioData {
|
|
||||||
enum AudioFormat {
|
|
||||||
PCM = 0;
|
|
||||||
MP3 = 1;
|
|
||||||
AAC = 2;
|
|
||||||
WAV = 3;
|
|
||||||
}
|
|
||||||
bytes data = 1; // 音频二进制数据
|
|
||||||
int32 sample_rate = 2; // 采样率(Hz)
|
|
||||||
int32 channels = 3; // 声道数
|
|
||||||
AudioFormat format = 4; // 音频格式
|
|
||||||
string codec = 5; // 编码方式
|
|
||||||
}
|
|
||||||
|
|
||||||
// 扬声器状态
|
|
||||||
message SpeakerState {
|
message SpeakerState {
|
||||||
bool is_initialized = 1; // 是否已初始化
|
bool is_initialized = 1;
|
||||||
bool is_running = 2; // 是否正在播放
|
bool is_running = 2;
|
||||||
bool is_decoding = 3;
|
bool is_decoding = 3;
|
||||||
bool is_paused = 5;
|
bool is_paused = 5;
|
||||||
int32 volume = 6; // 当前音量(0 ~ 100)
|
int32 volume = 6;
|
||||||
string error_message = 7; // 错误信息
|
string error_message = 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 各种扬声器命令
|
|
||||||
message GetSpeakerStateCommand {
|
message GetSpeakerStateCommand {
|
||||||
message Request {
|
message Request {
|
||||||
CommandHeader.Request header = 1;
|
CommandHeader.Request header = 1;
|
||||||
@ -47,7 +29,19 @@ message PlayAudioCommand {
|
|||||||
CommandHeader.Request header = 1;
|
CommandHeader.Request header = 1;
|
||||||
string audio_path = 2;
|
string audio_path = 2;
|
||||||
}
|
}
|
||||||
message Feedback { CommandHeader.Feedback header = 1; }
|
message Feedback {
|
||||||
|
CommandHeader.Feedback header = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message StreamSpeakerAudioCommand {
|
||||||
|
message Request {
|
||||||
|
CommandHeader.Request header = 1;
|
||||||
|
AudioData audio = 2;
|
||||||
|
}
|
||||||
|
message Feedback {
|
||||||
|
CommandHeader.Feedback header = 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
message StopSpeakerCommand {
|
message StopSpeakerCommand {
|
||||||
@ -80,7 +74,7 @@ message ResumeSpeakerCommand {
|
|||||||
message SetSpeakerVolumeCommand {
|
message SetSpeakerVolumeCommand {
|
||||||
message Request {
|
message Request {
|
||||||
CommandHeader.Request header = 1;
|
CommandHeader.Request header = 1;
|
||||||
int32 volume = 2; // 音量值(0 ~ 100)
|
int32 volume = 2;
|
||||||
}
|
}
|
||||||
message Feedback {
|
message Feedback {
|
||||||
CommandHeader.Feedback header = 1;
|
CommandHeader.Feedback header = 1;
|
||||||
@ -93,7 +87,6 @@ message GetSpeakerVolumeCommand {
|
|||||||
}
|
}
|
||||||
message Feedback {
|
message Feedback {
|
||||||
CommandHeader.Feedback header = 1;
|
CommandHeader.Feedback header = 1;
|
||||||
int32 volume = 2; // 当前音量值
|
int32 volume = 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,17 +4,14 @@ import "cmvr/api/speaker_command.proto";
|
|||||||
|
|
||||||
package cmvr.api;
|
package cmvr.api;
|
||||||
|
|
||||||
|
|
||||||
service SpeakerService {
|
service SpeakerService {
|
||||||
// 基本控制
|
|
||||||
rpc GetStatus(GetSpeakerStateCommand.Request) returns (GetSpeakerStateCommand.Feedback);
|
rpc GetStatus(GetSpeakerStateCommand.Request) returns (GetSpeakerStateCommand.Feedback);
|
||||||
rpc PlayAudio(PlayAudioCommand.Request) returns (PlayAudioCommand.Feedback);
|
rpc PlayAudio(PlayAudioCommand.Request) returns (PlayAudioCommand.Feedback);
|
||||||
|
rpc StreamAudio(stream StreamSpeakerAudioCommand.Request) returns (StreamSpeakerAudioCommand.Feedback);
|
||||||
rpc StopPlayback(StopSpeakerCommand.Request) returns (StopSpeakerCommand.Feedback);
|
rpc StopPlayback(StopSpeakerCommand.Request) returns (StopSpeakerCommand.Feedback);
|
||||||
rpc PausePlayback(PauseSpeakerCommand.Request) returns (PauseSpeakerCommand.Feedback);
|
rpc PausePlayback(PauseSpeakerCommand.Request) returns (PauseSpeakerCommand.Feedback);
|
||||||
rpc ResumePlayback(ResumeSpeakerCommand.Request) returns (ResumeSpeakerCommand.Feedback);
|
rpc ResumePlayback(ResumeSpeakerCommand.Request) returns (ResumeSpeakerCommand.Feedback);
|
||||||
|
|
||||||
// 音量控制
|
|
||||||
rpc SetVolume(SetSpeakerVolumeCommand.Request) returns (SetSpeakerVolumeCommand.Feedback);
|
rpc SetVolume(SetSpeakerVolumeCommand.Request) returns (SetSpeakerVolumeCommand.Feedback);
|
||||||
rpc GetVolume(GetSpeakerVolumeCommand.Request) returns (GetSpeakerVolumeCommand.Feedback);
|
rpc GetVolume(GetSpeakerVolumeCommand.Request) returns (GetSpeakerVolumeCommand.Feedback);
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -0,0 +1,52 @@
|
|||||||
|
package com.cmvr.inspection.domain;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 边缘 AI 推送的 PPE 违规报警主表实体。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@TableName("inspection_detection_alert")
|
||||||
|
public class InspectionDetectionAlert implements Serializable
|
||||||
|
{
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@TableId(value = "id", type = IdType.INPUT)
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
private String eventId;
|
||||||
|
private String idempotencyKey;
|
||||||
|
@TableField("schema_version")
|
||||||
|
private String schema;
|
||||||
|
private String sourceId;
|
||||||
|
@TableField("source_sequence")
|
||||||
|
private Long sequence;
|
||||||
|
private Long capturedAtNs;
|
||||||
|
private Long receivedAtNs;
|
||||||
|
private String traceId;
|
||||||
|
private String sessionId;
|
||||||
|
private String inputPort;
|
||||||
|
private String ruleId;
|
||||||
|
private String modelId;
|
||||||
|
private String modelName;
|
||||||
|
private String labelsJson;
|
||||||
|
@TableField("detection_scope")
|
||||||
|
private String scope;
|
||||||
|
private String scopeId;
|
||||||
|
private Integer hitCount;
|
||||||
|
private Double windowMs;
|
||||||
|
private Long firstSeenNs;
|
||||||
|
private Long lastSeenNs;
|
||||||
|
private Long triggeredAtNs;
|
||||||
|
private Double maxConfidence;
|
||||||
|
private String imagePath;
|
||||||
|
private String rawJson;
|
||||||
|
private Date createdAt;
|
||||||
|
}
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
package com.cmvr.inspection.domain;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PPE 报警检测框明细实体。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@TableName("inspection_detection_detail")
|
||||||
|
public class InspectionDetectionDetail implements Serializable
|
||||||
|
{
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@TableId(value = "id", type = IdType.INPUT)
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
private String alertId;
|
||||||
|
private String label;
|
||||||
|
private Double confidence;
|
||||||
|
private Double xMin;
|
||||||
|
private Double yMin;
|
||||||
|
private Double xMax;
|
||||||
|
private Double yMax;
|
||||||
|
private Long trackId;
|
||||||
|
}
|
||||||
@ -0,0 +1,55 @@
|
|||||||
|
package com.cmvr.inspection.domain.dto.alert;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 边缘 AI 推送消息的统一信封。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public class AlertEnvelope
|
||||||
|
{
|
||||||
|
/** 消息结构版本,当前固定为 DetectionAlert/v1。 */
|
||||||
|
@JsonProperty("schema")
|
||||||
|
private String schema;
|
||||||
|
|
||||||
|
/** 产生消息的视频源标识。 */
|
||||||
|
@JsonProperty("source_id")
|
||||||
|
private String sourceId;
|
||||||
|
|
||||||
|
/** 视频源内递增序号。 */
|
||||||
|
@JsonProperty("sequence")
|
||||||
|
private Long sequence;
|
||||||
|
|
||||||
|
/** 图像采集时间,Unix 纳秒时间戳。 */
|
||||||
|
@JsonProperty("captured_at_ns")
|
||||||
|
private Long capturedAtNs;
|
||||||
|
|
||||||
|
/** 边缘端接收时间,Unix 纳秒时间戳。 */
|
||||||
|
@JsonProperty("received_at_ns")
|
||||||
|
private Long receivedAtNs;
|
||||||
|
|
||||||
|
/** 全链路追踪标识。 */
|
||||||
|
@JsonProperty("trace_id")
|
||||||
|
private String traceId;
|
||||||
|
|
||||||
|
/** 边缘推理会话标识。 */
|
||||||
|
@JsonProperty("session_id")
|
||||||
|
private String sessionId;
|
||||||
|
|
||||||
|
/** 产生消息的输入端口。 */
|
||||||
|
@JsonProperty("input_port")
|
||||||
|
private String inputPort;
|
||||||
|
|
||||||
|
/** 扩展属性。 */
|
||||||
|
@JsonProperty("attributes")
|
||||||
|
private Map<String, Object> attributes;
|
||||||
|
|
||||||
|
/** PPE 违规报警内容。 */
|
||||||
|
@JsonProperty("payload")
|
||||||
|
private DetectionAlert payload;
|
||||||
|
}
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
package com.cmvr.inspection.domain.dto.alert;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 报警现场图片。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public class AlertImage
|
||||||
|
{
|
||||||
|
/** 图片媒体类型,例如 image/jpeg。 */
|
||||||
|
@JsonProperty("media_type")
|
||||||
|
private String mediaType;
|
||||||
|
|
||||||
|
@JsonProperty("width")
|
||||||
|
private Integer width;
|
||||||
|
|
||||||
|
@JsonProperty("height")
|
||||||
|
private Integer height;
|
||||||
|
|
||||||
|
/** 编码方式,当前协议固定为 base64。 */
|
||||||
|
@JsonProperty("encoding")
|
||||||
|
private String encoding;
|
||||||
|
|
||||||
|
/** Base64 编码后的图片内容。 */
|
||||||
|
@JsonProperty("data")
|
||||||
|
@ToString.Exclude
|
||||||
|
private String data;
|
||||||
|
}
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
package com.cmvr.inspection.domain.dto.alert;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测目标的像素坐标框。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public class BoundingBox
|
||||||
|
{
|
||||||
|
@JsonProperty("x_min")
|
||||||
|
private Double xMin;
|
||||||
|
|
||||||
|
@JsonProperty("y_min")
|
||||||
|
private Double yMin;
|
||||||
|
|
||||||
|
@JsonProperty("x_max")
|
||||||
|
private Double xMax;
|
||||||
|
|
||||||
|
@JsonProperty("y_max")
|
||||||
|
private Double yMax;
|
||||||
|
}
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
package com.cmvr.inspection.domain.dto.alert;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单个 PPE 检测结果。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public class Detection
|
||||||
|
{
|
||||||
|
/** 检测标签。 */
|
||||||
|
@JsonProperty("label")
|
||||||
|
private String label;
|
||||||
|
|
||||||
|
/** 检测置信度。 */
|
||||||
|
@JsonProperty("confidence")
|
||||||
|
private Double confidence;
|
||||||
|
|
||||||
|
/** 目标框坐标。 */
|
||||||
|
@JsonProperty("box")
|
||||||
|
private BoundingBox box;
|
||||||
|
|
||||||
|
/** 跟踪目标标识。 */
|
||||||
|
@JsonProperty("track_id")
|
||||||
|
private Long trackId;
|
||||||
|
}
|
||||||
@ -0,0 +1,75 @@
|
|||||||
|
package com.cmvr.inspection.domain.dto.alert;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PPE 违规报警业务数据。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public class DetectionAlert
|
||||||
|
{
|
||||||
|
/** 边缘端生成的全局事件标识。 */
|
||||||
|
@JsonProperty("event_id")
|
||||||
|
private String eventId;
|
||||||
|
|
||||||
|
/** 命中的规则标识。 */
|
||||||
|
@JsonProperty("rule_id")
|
||||||
|
private String ruleId;
|
||||||
|
|
||||||
|
/** 模型版本标识。 */
|
||||||
|
@JsonProperty("model_id")
|
||||||
|
private String modelId;
|
||||||
|
|
||||||
|
/** 模型显示名称。 */
|
||||||
|
@JsonProperty("model_name")
|
||||||
|
private String modelName;
|
||||||
|
|
||||||
|
/** 命中的标签集合。 */
|
||||||
|
@JsonProperty("labels")
|
||||||
|
private List<String> labels;
|
||||||
|
|
||||||
|
/** 规则统计范围。 */
|
||||||
|
@JsonProperty("scope")
|
||||||
|
private String scope;
|
||||||
|
|
||||||
|
/** 规则统计范围对应的对象标识。 */
|
||||||
|
@JsonProperty("scope_id")
|
||||||
|
private String scopeId;
|
||||||
|
|
||||||
|
/** 时间窗口内命中次数。 */
|
||||||
|
@JsonProperty("hit_count")
|
||||||
|
private Integer hitCount;
|
||||||
|
|
||||||
|
/** 规则统计窗口,单位毫秒。 */
|
||||||
|
@JsonProperty("window_ms")
|
||||||
|
private Double windowMs;
|
||||||
|
|
||||||
|
/** 首次命中时间,Unix 纳秒时间戳。 */
|
||||||
|
@JsonProperty("first_seen_ns")
|
||||||
|
private Long firstSeenNs;
|
||||||
|
|
||||||
|
/** 最后命中时间,Unix 纳秒时间戳。 */
|
||||||
|
@JsonProperty("last_seen_ns")
|
||||||
|
private Long lastSeenNs;
|
||||||
|
|
||||||
|
/** 触发报警时间,Unix 纳秒时间戳。 */
|
||||||
|
@JsonProperty("triggered_at_ns")
|
||||||
|
private Long triggeredAtNs;
|
||||||
|
|
||||||
|
/** 本次报警中的最大置信度。 */
|
||||||
|
@JsonProperty("max_confidence")
|
||||||
|
private Double maxConfidence;
|
||||||
|
|
||||||
|
/** 报警关联的检测框。 */
|
||||||
|
@JsonProperty("detections")
|
||||||
|
private List<Detection> detections;
|
||||||
|
|
||||||
|
/** 报警现场图片,允许为空。 */
|
||||||
|
@JsonProperty("image")
|
||||||
|
private AlertImage image;
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
package com.cmvr.inspection.exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 边缘检测报警请求参数不符合协议。
|
||||||
|
*/
|
||||||
|
public class DetectionAlertBadRequestException extends RuntimeException
|
||||||
|
{
|
||||||
|
public DetectionAlertBadRequestException(String message)
|
||||||
|
{
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DetectionAlertBadRequestException(String message, Throwable cause)
|
||||||
|
{
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
package com.cmvr.inspection.exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据库或对象存储暂时不可用,边缘端可稍后重试。
|
||||||
|
*/
|
||||||
|
public class DetectionAlertTemporaryException extends RuntimeException
|
||||||
|
{
|
||||||
|
public DetectionAlertTemporaryException(String message)
|
||||||
|
{
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DetectionAlertTemporaryException(String message, Throwable cause)
|
||||||
|
{
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
package com.cmvr.inspection.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.cmvr.inspection.domain.InspectionDetectionAlert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PPE 违规报警主表 Mapper。
|
||||||
|
*/
|
||||||
|
public interface InspectionDetectionAlertMapper extends BaseMapper<InspectionDetectionAlert>
|
||||||
|
{
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
package com.cmvr.inspection.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.cmvr.inspection.domain.InspectionDetectionDetail;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PPE 报警检测框明细 Mapper。
|
||||||
|
*/
|
||||||
|
public interface InspectionDetectionDetailMapper extends BaseMapper<InspectionDetectionDetail>
|
||||||
|
{
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
package com.cmvr.inspection.service;
|
||||||
|
|
||||||
|
import com.cmvr.inspection.domain.dto.alert.AlertEnvelope;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 边缘 AI 检测报警接收服务。
|
||||||
|
*/
|
||||||
|
public interface IInspectionDetectionAlertService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 接收并持久化一条 PPE 违规报警。
|
||||||
|
*
|
||||||
|
* @param envelope 报警信封
|
||||||
|
* @param requestIdempotencyKey HTTP 请求头中的幂等键,允许为空
|
||||||
|
* @return true 表示本次新增,false 表示重复事件
|
||||||
|
*/
|
||||||
|
boolean receive(AlertEnvelope envelope, String requestIdempotencyKey);
|
||||||
|
}
|
||||||
@ -0,0 +1,391 @@
|
|||||||
|
package com.cmvr.inspection.service.impl;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.cmvr.common.config.properties.MinioProperties;
|
||||||
|
import com.cmvr.common.core.minio.MinioService;
|
||||||
|
import com.cmvr.inspection.domain.InspectionDetectionAlert;
|
||||||
|
import com.cmvr.inspection.domain.InspectionDetectionDetail;
|
||||||
|
import com.cmvr.inspection.domain.dto.alert.AlertEnvelope;
|
||||||
|
import com.cmvr.inspection.domain.dto.alert.AlertImage;
|
||||||
|
import com.cmvr.inspection.domain.dto.alert.BoundingBox;
|
||||||
|
import com.cmvr.inspection.domain.dto.alert.Detection;
|
||||||
|
import com.cmvr.inspection.exception.DetectionAlertBadRequestException;
|
||||||
|
import com.cmvr.inspection.exception.DetectionAlertTemporaryException;
|
||||||
|
import com.cmvr.inspection.mapper.InspectionDetectionAlertMapper;
|
||||||
|
import com.cmvr.inspection.mapper.InspectionDetectionDetailMapper;
|
||||||
|
import com.cmvr.inspection.service.IInspectionDetectionAlertService;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PPE 违规报警接收业务实现。
|
||||||
|
*
|
||||||
|
* 数据库唯一索引是并发幂等的最终保障;业务预查用于减少重复请求的无效写入。
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class InspectionDetectionAlertServiceImpl implements IInspectionDetectionAlertService
|
||||||
|
{
|
||||||
|
private static final String SUPPORTED_SCHEMA = "DetectionAlert/v1";
|
||||||
|
private static final String IMAGE_ENCODING_BASE64 = "base64";
|
||||||
|
private static final String IMAGE_OBJECT_PREFIX = "inspection/detection-alerts";
|
||||||
|
private static final int MAX_IDEMPOTENCY_KEY_LENGTH = 191;
|
||||||
|
private static final int MAX_EVENT_ID_LENGTH = 128;
|
||||||
|
private static final int MAX_IMAGE_BYTES = 10 * 1024 * 1024;
|
||||||
|
private static final int MAX_BASE64_LENGTH = (MAX_IMAGE_BYTES * 4 / 3) + 8;
|
||||||
|
|
||||||
|
private final InspectionDetectionAlertMapper alertMapper;
|
||||||
|
private final InspectionDetectionDetailMapper detailMapper;
|
||||||
|
private final MinioService minioService;
|
||||||
|
private final MinioProperties minioProperties;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public boolean receive(AlertEnvelope envelope, String requestIdempotencyKey)
|
||||||
|
{
|
||||||
|
String idempotencyKey = validateAndResolveIdempotencyKey(envelope, requestIdempotencyKey);
|
||||||
|
String eventId = trimToNull(envelope.getPayload().getEventId());
|
||||||
|
|
||||||
|
if (exists(idempotencyKey, eventId))
|
||||||
|
{
|
||||||
|
log.info("忽略重复PPE报警,idempotencyKey={},eventId={}", idempotencyKey, eventId);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
InspectionDetectionAlert alert = buildAlert(envelope, idempotencyKey);
|
||||||
|
StoredImage storedImage = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
alertMapper.insert(alert);
|
||||||
|
|
||||||
|
AlertImage image = envelope.getPayload().getImage();
|
||||||
|
if (image != null)
|
||||||
|
{
|
||||||
|
storedImage = uploadImage(alert.getId(), image);
|
||||||
|
alert.setImagePath(storedImage.url);
|
||||||
|
if (alertMapper.updateById(alert) != 1)
|
||||||
|
{
|
||||||
|
throw new DetectionAlertTemporaryException("更新报警图片地址失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
saveDetectionDetails(alert.getId(), envelope.getPayload().getDetections());
|
||||||
|
log.info("PPE报警接收成功,alertId={},eventId={},sourceId={}",
|
||||||
|
alert.getId(), eventId, envelope.getSourceId());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (RuntimeException ex)
|
||||||
|
{
|
||||||
|
// 数据库事务无法回滚 MinIO,对后续步骤失败时主动删除已上传对象。
|
||||||
|
if (storedImage != null)
|
||||||
|
{
|
||||||
|
deleteUploadedImageQuietly(storedImage.objectName);
|
||||||
|
}
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验协议关键字段,并按请求头、event_id、trace_id 的顺序生成幂等键。
|
||||||
|
*/
|
||||||
|
private String validateAndResolveIdempotencyKey(AlertEnvelope envelope, String requestIdempotencyKey)
|
||||||
|
{
|
||||||
|
if (envelope == null)
|
||||||
|
{
|
||||||
|
throw new DetectionAlertBadRequestException("请求体不能为空");
|
||||||
|
}
|
||||||
|
if (!SUPPORTED_SCHEMA.equals(envelope.getSchema()))
|
||||||
|
{
|
||||||
|
throw new DetectionAlertBadRequestException("schema必须为" + SUPPORTED_SCHEMA);
|
||||||
|
}
|
||||||
|
if (envelope.getPayload() == null)
|
||||||
|
{
|
||||||
|
throw new DetectionAlertBadRequestException("payload不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
String eventId = trimToNull(envelope.getPayload().getEventId());
|
||||||
|
if (eventId != null && eventId.length() > MAX_EVENT_ID_LENGTH)
|
||||||
|
{
|
||||||
|
throw new DetectionAlertBadRequestException("payload.event_id长度不能超过" + MAX_EVENT_ID_LENGTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
String idempotencyKey = trimToNull(requestIdempotencyKey);
|
||||||
|
if (idempotencyKey == null)
|
||||||
|
{
|
||||||
|
idempotencyKey = eventId;
|
||||||
|
}
|
||||||
|
if (idempotencyKey == null)
|
||||||
|
{
|
||||||
|
idempotencyKey = trimToNull(envelope.getTraceId());
|
||||||
|
}
|
||||||
|
if (idempotencyKey == null)
|
||||||
|
{
|
||||||
|
throw new DetectionAlertBadRequestException("Idempotency-Key、payload.event_id和trace_id不能同时为空");
|
||||||
|
}
|
||||||
|
if (idempotencyKey.length() > MAX_IDEMPOTENCY_KEY_LENGTH)
|
||||||
|
{
|
||||||
|
throw new DetectionAlertBadRequestException("幂等键长度不能超过" + MAX_IDEMPOTENCY_KEY_LENGTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
validateImage(envelope.getPayload().getImage());
|
||||||
|
return idempotencyKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateImage(AlertImage image)
|
||||||
|
{
|
||||||
|
if (image == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!IMAGE_ENCODING_BASE64.equalsIgnoreCase(trimToEmpty(image.getEncoding())))
|
||||||
|
{
|
||||||
|
throw new DetectionAlertBadRequestException("payload.image.encoding必须为base64");
|
||||||
|
}
|
||||||
|
if (StringUtils.isBlank(image.getData()))
|
||||||
|
{
|
||||||
|
throw new DetectionAlertBadRequestException("payload.image.data不能为空");
|
||||||
|
}
|
||||||
|
if (image.getData().length() > MAX_BASE64_LENGTH)
|
||||||
|
{
|
||||||
|
throw new DetectionAlertBadRequestException("报警图片不能超过10MB");
|
||||||
|
}
|
||||||
|
String mediaType = trimToNull(image.getMediaType());
|
||||||
|
if (mediaType == null || !mediaType.toLowerCase(Locale.ROOT).startsWith("image/"))
|
||||||
|
{
|
||||||
|
throw new DetectionAlertBadRequestException("payload.image.media_type必须是图片类型");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean exists(String idempotencyKey, String eventId)
|
||||||
|
{
|
||||||
|
LambdaQueryWrapper<InspectionDetectionAlert> query = new LambdaQueryWrapper<>();
|
||||||
|
query.eq(InspectionDetectionAlert::getIdempotencyKey, idempotencyKey);
|
||||||
|
if (eventId != null)
|
||||||
|
{
|
||||||
|
query.or().eq(InspectionDetectionAlert::getEventId, eventId);
|
||||||
|
}
|
||||||
|
return alertMapper.selectCount(query) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private InspectionDetectionAlert buildAlert(AlertEnvelope envelope, String idempotencyKey)
|
||||||
|
{
|
||||||
|
com.cmvr.inspection.domain.dto.alert.DetectionAlert payload = envelope.getPayload();
|
||||||
|
InspectionDetectionAlert alert = new InspectionDetectionAlert();
|
||||||
|
alert.setId(newId());
|
||||||
|
alert.setEventId(trimToNull(payload.getEventId()));
|
||||||
|
alert.setIdempotencyKey(idempotencyKey);
|
||||||
|
alert.setSchema(envelope.getSchema());
|
||||||
|
alert.setSourceId(envelope.getSourceId());
|
||||||
|
alert.setSequence(envelope.getSequence());
|
||||||
|
alert.setCapturedAtNs(envelope.getCapturedAtNs());
|
||||||
|
alert.setReceivedAtNs(envelope.getReceivedAtNs());
|
||||||
|
alert.setTraceId(envelope.getTraceId());
|
||||||
|
alert.setSessionId(envelope.getSessionId());
|
||||||
|
alert.setInputPort(envelope.getInputPort());
|
||||||
|
alert.setRuleId(payload.getRuleId());
|
||||||
|
alert.setModelId(payload.getModelId());
|
||||||
|
alert.setModelName(payload.getModelName());
|
||||||
|
alert.setLabelsJson(writeJson(payload.getLabels(), "序列化labels失败"));
|
||||||
|
alert.setScope(payload.getScope());
|
||||||
|
alert.setScopeId(payload.getScopeId());
|
||||||
|
alert.setHitCount(payload.getHitCount());
|
||||||
|
alert.setWindowMs(payload.getWindowMs());
|
||||||
|
alert.setFirstSeenNs(payload.getFirstSeenNs());
|
||||||
|
alert.setLastSeenNs(payload.getLastSeenNs());
|
||||||
|
alert.setTriggeredAtNs(payload.getTriggeredAtNs());
|
||||||
|
alert.setMaxConfidence(payload.getMaxConfidence());
|
||||||
|
alert.setRawJson(buildSanitizedRawJson(envelope));
|
||||||
|
alert.setCreatedAt(new Date());
|
||||||
|
return alert;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* raw_json保留协议元数据,但明确移除Base64图片,避免图片以另一种形式进入数据库。
|
||||||
|
*/
|
||||||
|
private String buildSanitizedRawJson(AlertEnvelope envelope)
|
||||||
|
{
|
||||||
|
AlertImage image = envelope.getPayload().getImage();
|
||||||
|
String imageData = image == null ? null : image.getData();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 避免构建JSON树时复制一份大Base64字符串;finally中恢复仅供随后上传MinIO。
|
||||||
|
if (image != null)
|
||||||
|
{
|
||||||
|
image.setData(null);
|
||||||
|
}
|
||||||
|
return objectMapper.writeValueAsString(envelope);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new DetectionAlertTemporaryException("序列化报警原始数据失败", ex);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (image != null)
|
||||||
|
{
|
||||||
|
image.setData(imageData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String writeJson(Object value, String errorMessage)
|
||||||
|
{
|
||||||
|
if (value == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return objectMapper.writeValueAsString(value);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new DetectionAlertTemporaryException(errorMessage, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private StoredImage uploadImage(String alertId, AlertImage image)
|
||||||
|
{
|
||||||
|
final byte[] imageBytes;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
imageBytes = Base64.getDecoder().decode(image.getData().getBytes(StandardCharsets.US_ASCII));
|
||||||
|
}
|
||||||
|
catch (IllegalArgumentException ex)
|
||||||
|
{
|
||||||
|
throw new DetectionAlertBadRequestException("payload.image.data不是有效的Base64", ex);
|
||||||
|
}
|
||||||
|
if (imageBytes.length > MAX_IMAGE_BYTES)
|
||||||
|
{
|
||||||
|
throw new DetectionAlertBadRequestException("报警图片不能超过10MB");
|
||||||
|
}
|
||||||
|
|
||||||
|
String extension = extensionFor(image.getMediaType());
|
||||||
|
String datePath = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
|
||||||
|
String objectName = IMAGE_OBJECT_PREFIX + "/" + datePath + "/" + alertId + extension;
|
||||||
|
try (ByteArrayInputStream inputStream = new ByteArrayInputStream(imageBytes))
|
||||||
|
{
|
||||||
|
minioService.uploadStream(minioProperties.getBucketName(), objectName, inputStream,
|
||||||
|
imageBytes.length, image.getMediaType());
|
||||||
|
return new StoredImage(objectName, buildMinioUrl(objectName));
|
||||||
|
}
|
||||||
|
catch (DetectionAlertBadRequestException ex)
|
||||||
|
{
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new DetectionAlertTemporaryException("上传报警图片到MinIO失败", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveDetectionDetails(String alertId, List<Detection> detections)
|
||||||
|
{
|
||||||
|
if (detections == null || detections.isEmpty())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (Detection detection : detections)
|
||||||
|
{
|
||||||
|
if (detection == null)
|
||||||
|
{
|
||||||
|
throw new DetectionAlertBadRequestException("payload.detections不能包含null元素");
|
||||||
|
}
|
||||||
|
InspectionDetectionDetail detail = new InspectionDetectionDetail();
|
||||||
|
detail.setId(newId());
|
||||||
|
detail.setAlertId(alertId);
|
||||||
|
detail.setLabel(detection.getLabel());
|
||||||
|
detail.setConfidence(detection.getConfidence());
|
||||||
|
detail.setTrackId(detection.getTrackId());
|
||||||
|
BoundingBox box = detection.getBox();
|
||||||
|
if (box != null)
|
||||||
|
{
|
||||||
|
detail.setXMin(box.getXMin());
|
||||||
|
detail.setYMin(box.getYMin());
|
||||||
|
detail.setXMax(box.getXMax());
|
||||||
|
detail.setYMax(box.getYMax());
|
||||||
|
}
|
||||||
|
detailMapper.insert(detail);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildMinioUrl(String objectName)
|
||||||
|
{
|
||||||
|
String baseUrl = StringUtils.removeEnd(minioProperties.getUrl(), "/");
|
||||||
|
return baseUrl + "/" + minioProperties.getBucketName() + "/" + objectName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteUploadedImageQuietly(String objectName)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
minioService.deleteFile(minioProperties.getBucketName(), objectName);
|
||||||
|
}
|
||||||
|
catch (Exception cleanupEx)
|
||||||
|
{
|
||||||
|
log.error("回滚PPE报警时删除MinIO对象失败,objectName={}", objectName, cleanupEx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extensionFor(String mediaType)
|
||||||
|
{
|
||||||
|
String normalized = trimToEmpty(mediaType).toLowerCase(Locale.ROOT);
|
||||||
|
if ("image/jpeg".equals(normalized) || "image/jpg".equals(normalized))
|
||||||
|
{
|
||||||
|
return ".jpg";
|
||||||
|
}
|
||||||
|
if ("image/png".equals(normalized))
|
||||||
|
{
|
||||||
|
return ".png";
|
||||||
|
}
|
||||||
|
if ("image/webp".equals(normalized))
|
||||||
|
{
|
||||||
|
return ".webp";
|
||||||
|
}
|
||||||
|
// 未知图片类型使用无执行语义的后缀,实际Content-Type仍按协议上传。
|
||||||
|
return ".img";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String trimToNull(String value)
|
||||||
|
{
|
||||||
|
return StringUtils.trimToNull(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String trimToEmpty(String value)
|
||||||
|
{
|
||||||
|
return StringUtils.trimToEmpty(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String newId()
|
||||||
|
{
|
||||||
|
return UUID.randomUUID().toString().replace("-", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class StoredImage
|
||||||
|
{
|
||||||
|
private final String objectName;
|
||||||
|
private final String url;
|
||||||
|
|
||||||
|
private StoredImage(String objectName, String url)
|
||||||
|
{
|
||||||
|
this.objectName = objectName;
|
||||||
|
this.url = url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
54
sql/inspection_detection_alert.sql
Normal file
54
sql/inspection_detection_alert.sql
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
-- PPE违规报警接收表。
|
||||||
|
-- 本脚本用于已有数据库增量部署,不会删除现有巡检数据。
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `inspection_detection_alert` (
|
||||||
|
`id` varchar(32) NOT NULL COMMENT '主键ID',
|
||||||
|
`event_id` varchar(128) DEFAULT NULL COMMENT '边缘端事件ID',
|
||||||
|
`idempotency_key` varchar(191) NOT NULL COMMENT 'HTTP幂等键',
|
||||||
|
`schema_version` varchar(64) NOT NULL COMMENT '消息结构版本',
|
||||||
|
`source_id` varchar(128) DEFAULT NULL COMMENT '视频源标识',
|
||||||
|
`source_sequence` bigint DEFAULT NULL COMMENT '视频源内消息序号',
|
||||||
|
`captured_at_ns` bigint DEFAULT NULL COMMENT '采集时间Unix纳秒',
|
||||||
|
`received_at_ns` bigint DEFAULT NULL COMMENT '边缘端接收时间Unix纳秒',
|
||||||
|
`trace_id` varchar(128) DEFAULT NULL COMMENT '链路追踪ID',
|
||||||
|
`session_id` varchar(128) DEFAULT NULL COMMENT '边缘推理会话ID',
|
||||||
|
`input_port` varchar(128) DEFAULT NULL COMMENT '消息输入端口',
|
||||||
|
`rule_id` varchar(128) DEFAULT NULL COMMENT '命中的规则ID',
|
||||||
|
`model_id` varchar(255) DEFAULT NULL COMMENT '模型版本ID',
|
||||||
|
`model_name` varchar(255) DEFAULT NULL COMMENT '模型名称',
|
||||||
|
`labels_json` text COMMENT '命中标签JSON',
|
||||||
|
`detection_scope` varchar(64) DEFAULT NULL COMMENT '规则统计范围',
|
||||||
|
`scope_id` varchar(128) DEFAULT NULL COMMENT '统计范围对象ID',
|
||||||
|
`hit_count` int DEFAULT NULL COMMENT '窗口内命中次数',
|
||||||
|
`window_ms` decimal(12,3) DEFAULT NULL COMMENT '统计窗口毫秒',
|
||||||
|
`first_seen_ns` bigint DEFAULT NULL COMMENT '首次命中时间Unix纳秒',
|
||||||
|
`last_seen_ns` bigint DEFAULT NULL COMMENT '最后命中时间Unix纳秒',
|
||||||
|
`triggered_at_ns` bigint DEFAULT NULL COMMENT '报警触发时间Unix纳秒',
|
||||||
|
`max_confidence` decimal(8,6) DEFAULT NULL COMMENT '最大置信度',
|
||||||
|
`image_path` varchar(1000) DEFAULT NULL COMMENT 'MinIO图片URL',
|
||||||
|
`raw_json` mediumtext COMMENT '已移除Base64图片数据的原始协议JSON',
|
||||||
|
`created_at` datetime(3) NOT NULL COMMENT '平台接收时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_detection_alert_event_id` (`event_id`),
|
||||||
|
UNIQUE KEY `uk_detection_alert_idempotency_key` (`idempotency_key`),
|
||||||
|
KEY `idx_detection_alert_source_id` (`source_id`),
|
||||||
|
KEY `idx_detection_alert_rule_id` (`rule_id`),
|
||||||
|
KEY `idx_detection_alert_triggered_at` (`triggered_at_ns`),
|
||||||
|
KEY `idx_detection_alert_created_at` (`created_at`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='巡检PPE违规报警主表';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `inspection_detection_detail` (
|
||||||
|
`id` varchar(32) NOT NULL COMMENT '主键ID',
|
||||||
|
`alert_id` varchar(32) NOT NULL COMMENT '报警主表ID',
|
||||||
|
`label` varchar(128) DEFAULT NULL COMMENT '检测标签',
|
||||||
|
`confidence` decimal(8,6) DEFAULT NULL COMMENT '检测置信度',
|
||||||
|
`x_min` decimal(12,4) DEFAULT NULL COMMENT '目标框左上X坐标',
|
||||||
|
`y_min` decimal(12,4) DEFAULT NULL COMMENT '目标框左上Y坐标',
|
||||||
|
`x_max` decimal(12,4) DEFAULT NULL COMMENT '目标框右下X坐标',
|
||||||
|
`y_max` decimal(12,4) DEFAULT NULL COMMENT '目标框右下Y坐标',
|
||||||
|
`track_id` bigint DEFAULT NULL COMMENT '目标跟踪ID',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `idx_detection_detail_alert_id` (`alert_id`),
|
||||||
|
CONSTRAINT `fk_detection_detail_alert` FOREIGN KEY (`alert_id`)
|
||||||
|
REFERENCES `inspection_detection_alert` (`id`) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='巡检PPE报警检测框明细表';
|
||||||
Loading…
Reference in New Issue
Block a user