ard-admin/src/main/java/com/ard/web/controller/work/UavManualFlyController.java
@@ -108,6 +108,25 @@ return AjaxResult.success(); } @ApiOperation(value = "开始环绕飞行", notes = "速度speed:0~20 单位m/s " + "环绕半径radius:单位m,"+ "飞行模式flyMode:0-节能模式【飞机直线飞到目标点,需飞机在空中】," + "1-安全模式【飞机飞行至目标点高度再平飞到目标点,飞机可以在地面或空中】 默认为节能模式") @ApiOperationSupport(order = 9) @PostMapping("/flyRoundControl") public AjaxResult flyRoundControl(@RequestBody FlyRoundControlDTO flyRoundControlDTO) { tyService.FlyRoundControl(flyRoundControlDTO); return AjaxResult.success(); } @ApiOperation("停止环绕飞行") @ApiOperationSupport(order = 10) @PostMapping("/flyRoundControlCancel") public AjaxResult flyRoundControlCancel(@RequestBody FlyRoundControlCancelDTO flyRoundControlCancelDTO) { tyService.FlyRoundControlCancel(flyRoundControlCancelDTO); return AjaxResult.success(); } @ApiOperation(value = "紧急制动", notes = "动作参数actionParam: 0-解除紧急制动 1-紧急制动") @ApiOperationSupport(order = 11) @PostMapping("/emeragencyBrake") ard-route/src/main/java/com/ard/route/domain/route/ArdRouteInfo.java
@@ -64,4 +64,7 @@ /** 航线数据 */ private byte[] routeData; /** 行点总数 */ private Integer pointCount; } ard-route/src/main/java/com/ard/route/service/impl/ArdRouteInfoServiceImpl.java
@@ -21,6 +21,7 @@ import com.ard.route.domain.dto.ArdNoflyDto; import com.ard.route.domain.route.ArdRouteInfo; import com.ard.route.domain.sixhe.ArdRouteChange; import com.ard.route.domain.vo.KmzInfoVo; import com.ard.route.domain.vo.KmzResultVo; import com.ard.route.mapper.ArdRouteInfoMapper; import com.ard.route.service.IArdRouteInfoService; @@ -121,6 +122,7 @@ KmzResultVo kmzResult = routeService.buildKmz(ardRouteInfo.getUavRouteReq(), ardRouteInfo.getName()); ardRouteInfo.setPath(kmzResult.getPath()); ardRouteInfo.setReadOnly("0"); ardRouteInfo.setPointCount(getPointCount(ardRouteInfo)); ardRouteInfoMapper.insertArdRouteInfo(ardRouteInfo); return kmzResult.getPath(); } @@ -149,7 +151,15 @@ } ardRouteInfo.setPath(kmzResult.getPath()); } ardRouteInfo.setPointCount(getPointCount(ardRouteInfo)); return ardRouteInfoMapper.updateArdRouteInfo(ardRouteInfo); } private Integer getPointCount(ArdRouteInfo ardRouteInfo) { if (ardRouteInfo.getUavRouteReq().getRoutePointList() != null) { return ardRouteInfo.getUavRouteReq().getRoutePointList().size(); } return 0; } /** @@ -194,6 +204,7 @@ information.setPath(url); information.setGroupId("0"); // 默认分组 information.setReadOnly("1"); // 默认只读 // 增加航线实体类信息 insertRouteInfomation(information); return MinioUploadResult.builder() .url(url) @@ -331,9 +342,10 @@ if (autoFlightSpeed != null){ planLineData.setAutoSpeed(8); } if (globalTransitionalSpeed != null){ planLineData.setMaxSpeed(15); } // if (globalTransitionalSpeed != null){ // planLineData.setMaxSpeed(15); // } if (exitOnRcLostAction != null){ planLineData.setFinishedAction(1); } @@ -359,11 +371,11 @@ if (height != null){ pointData.setHeight(height.floatValue()); } Double longitude = routePointReq.getLatitude(); Double longitude = routePointReq.getLongitude(); if (longitude != null){ pointData.setLng(longitude.floatValue()); } Double latitude = routePointReq.getLongitude(); Double latitude = routePointReq.getLatitude(); if (latitude != null){ pointData.setLat(latitude.floatValue()); } ard-route/src/main/java/com/ard/route/service/impl/UavRouteServiceImpl.java
@@ -178,40 +178,18 @@ String kml = XML_HEADER + xStream.toXML(kmlInfo); String wpml = XML_HEADER + xStream.toXML(wpmlInfo); KmzResultVo result = new KmzResultVo(); // 判断本地生成kmz文件 if (isLocal) { try (FileOutputStream fileOutputStream = new FileOutputStream(localFilePath + fileName + ".kmz"); ZipOutputStream zipOutputStream = new ZipOutputStream(fileOutputStream)) { zipOutputStream.setLevel(0); // 0 表示不压缩,存储方式 // 创建KMZ文件内容 buildZipFile("wpmz/template.kml", zipOutputStream, kml); buildZipFile("wpmz/waylines.wpml", zipOutputStream, wpml); } catch (Exception e) { throw new RuntimeException(e); } result.setPath(localFilePath + fileName + ".kmz"); return result; }else { try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); ZipOutputStream zipOutputStream = new ZipOutputStream(baos)) { zipOutputStream.setLevel(0); // 创建 wpmz 目录中的 template.kml 文件条目 buildZipFile("wpmz/template.kml", zipOutputStream, kml); // 创建 wpmz 目录中的 waylines.wpml 文件条目 buildZipFile("wpmz/waylines.wpml", zipOutputStream, wpml); zipOutputStream.finish(); zipOutputStream.flush(); // 获取文件大小 Double fileSize = (double) baos.size(); // 字节数 // 创建MultipartFile对象 MultipartFile multipartFile = new MockMultipartFile( @@ -225,7 +203,6 @@ return result; } catch (Exception e) { throw new RuntimeException("KMZ文件生成或上传失败", e); } } } ard-route/src/main/java/com/ard/route/util/ToolUtils.java
@@ -9,10 +9,6 @@ */ public class ToolUtils { public static Boolean isLocal = false; public static String localFilePath = "KmzFile/kmz/"; public static String kmzPath = "kmz"; /** ard-route/src/main/resources/mapper/ArdRouteInfoMapper.xml
@@ -18,6 +18,7 @@ <result property="createTime" column="create_time" /> <result property="updateBy" column="update_by" /> <result property="updateTime" column="update_time" /> <result property="pointCount" column="point_count" /> </resultMap> <sql id="selectArdRouteInfoVo"> @@ -33,7 +34,8 @@ create_by, create_time, update_by, update_time update_time, point_count from ard_route_info </sql> @@ -74,6 +76,7 @@ <if test="createTime != null">create_time,</if> <if test="updateBy != null">update_by,</if> <if test="updateTime != null">update_time,</if> <if test="pointCount != null">point_count,</if> </trim> <trim prefix="values (" suffix=")" suffixOverrides=","> <if test="id != null">#{id},</if> @@ -89,6 +92,7 @@ <if test="createTime != null">#{createTime},</if> <if test="updateBy != null">#{updateBy},</if> <if test="updateTime != null">#{updateTime},</if> <if test="pointCount != null">#{pointCount},</if> </trim> </insert> @@ -107,6 +111,7 @@ <if test="createTime != null">create_time = #{createTime},</if> <if test="updateBy != null">update_by = #{updateBy},</if> <if test="updateTime != null">update_time = #{updateTime},</if> <if test="pointCount != null">point_count = #{pointCount},</if> </trim> where id = #{id} </update> @@ -135,7 +140,7 @@ </delete> <select id="selectArdRouteInfoByGroupId" parameterType="String" resultMap="ArdRouteInfoResult"> select id,name,path,group_id,type,aircraft,payload,read_only,create_by,create_time,update_by,update_time from ard_route_info select id,name,path,group_id,type,aircraft,payload,read_only,create_by,create_time,update_by,update_time,point_count from ard_route_info <where> <if test="groupId != null "> and group_id = #{groupId}</if> </where> ard-work/src/main/java/com/ard/work/component/netty/command/manual/FlyRoundControlCancelCmd.java
New file @@ -0,0 +1,33 @@ package com.ard.work.component.netty.command.manual; import com.ard.work.component.netty.command.BaseCmd; import com.ard.work.domain.dto.manual.FlyRoundControlCancelDTO; import com.ard.work.domain.dto.manual.FlyRoundControlDTO; import io.netty.buffer.ByteBuf; /** * 环绕飞行控制指令 * * @author liusuyi * @date 2024年10月26日15:43 **/ public class FlyRoundControlCancelCmd extends BaseCmd { public FlyRoundControlCancelCmd(FlyRoundControlCancelDTO flyRoundControlCancelDTO) { setDataHeader((short) 0x7479); setDataLength((short) 18); setBoxSn(flyRoundControlCancelDTO.getBoxSn().getBytes()); setCmdCode((byte) 0xD1); } public void encode(ByteBuf out) { out.writeShort(getDataHeader()); // 写入数据头 out.writeShort(getDataLength()); // 写入数据长度 out.writeBytes(getBoxSn()); // 写入盒子SN out.writeByte(getCmdCode()); // 写入命令码 out.writeByte(0x00); // 写入加密标志 out.writeByte(0x3C); // 写入动作编号 } } ard-work/src/main/java/com/ard/work/component/netty/command/manual/FlyRoundControlCmd.java
New file @@ -0,0 +1,50 @@ package com.ard.work.component.netty.command.manual; import com.ard.work.component.netty.command.BaseCmd; import com.ard.work.domain.dto.manual.FlyPointControlDTO; import com.ard.work.domain.dto.manual.FlyRoundControlDTO; import io.netty.buffer.ByteBuf; /** * 环绕飞行控制指令 * * @author liusuyi * @date 2024年10月26日15:43 **/ public class FlyRoundControlCmd extends BaseCmd { private final double longitude; // 兴趣点经度 private final double latitude; // 兴趣点纬度 private final float altitude; // 兴趣点高度 private final float speed; // 环绕速度 private final float radius; // 环绕半径 private final byte flyMode; // 飞行模式 public FlyRoundControlCmd(FlyRoundControlDTO flyRoundControlDTO) { setDataHeader((short) 0x7479); setDataLength((short) 47); setBoxSn(flyRoundControlDTO.getBoxSn().getBytes()); setCmdCode((byte) 0xD1); this.longitude = flyRoundControlDTO.getLongitude(); this.latitude = flyRoundControlDTO.getLatitude(); this.altitude = flyRoundControlDTO.getAltitude(); this.speed = flyRoundControlDTO.getSpeed(); this.radius= flyRoundControlDTO.getRadius(); this.flyMode = flyRoundControlDTO.getFlyMode(); } public void encode(ByteBuf out) { out.writeShort(getDataHeader()); // 写入数据头 out.writeShort(getDataLength()); // 写入数据长度 out.writeBytes(getBoxSn()); // 写入盒子SN out.writeByte(getCmdCode()); // 写入命令码 out.writeByte(0x00); // 写入加密标志 out.writeByte(0x3B); // 写入动作编号 out.writeDouble(longitude); // 写入兴趣点经度 out.writeDouble(latitude); // 写入兴趣点纬度 out.writeFloat(altitude); // 写入兴趣点高度 out.writeFloat(speed); // 写入环绕速度 out.writeFloat(radius); // 写入环绕半径 out.writeByte(flyMode); // 写入飞行模式 } } ard-work/src/main/java/com/ard/work/component/netty/command/manual/FlyRoundReplyCmd.java
New file @@ -0,0 +1,35 @@ package com.ard.work.component.netty.command.manual; import com.ard.work.component.netty.command.BaseCmd; import io.netty.buffer.ByteBuf; import lombok.Data; import lombok.EqualsAndHashCode; /** * 到达环绕位置回复指令 * * @author liusuyi * @date 2024年10月26日15:43 **/ @Data @EqualsAndHashCode(callSuper = false) public class FlyRoundReplyCmd extends BaseCmd { private final byte encryptFlag = 0x00; // 加密标志 private final byte actionCode = 0x3B; // 动作编号 private final byte result; // 执行结果 private final Integer errorCode; // 错误码 public FlyRoundReplyCmd(String boxSn, Byte result, Integer errorCode) { setDataHeader((short) 0x6A77); setDataLength((short) 19); setBoxSn(boxSn.getBytes()); setCmdCode((byte) 0xD1); this.result = result; this.errorCode = errorCode; } public void encode(ByteBuf out) { } } ard-work/src/main/java/com/ard/work/component/netty/handler/MessageProtocolDecoder.java
@@ -174,6 +174,8 @@ return new EmergencyBrakeReplyCmd(boxSn, result, errorCode); case 0x39: return new PointControlReachedReplyCmd(boxSn, result, errorCode); case 0x3B: return new FlyRoundReplyCmd(boxSn, result, errorCode); case 0x22: return new CameraModeReplyCmd(boxSn, result, errorCode); case 0x23: ard-work/src/main/java/com/ard/work/component/websocket/handler/TyMessageHandler.java
@@ -290,14 +290,7 @@ Long userId = Long.parseLong(userIdStr); WebSocketSessionManager.removeSession(userId); log.info("【cmd】size: {}", WebSocketSessionManager.getOnlineUserCount()); // 释放该用户所有 UAV 控制权 uavControlService.releaseAllControls(userId); uavStreamManager.stopAllPullStreamByUser(userId); uavStateManager.removeSubscribe(userId); uavTelemetryManager.removeSubscribe(userId); uavSignalManager.removeSubscribe(userId); uavControlManager.removeSubscribe(userId); // 延迟10秒删除token(若期间没重连) CompletableFuture.runAsync(() -> { @@ -309,6 +302,12 @@ if (!WebSocketSessionManager.containsUser(userId)) { LoginUser loginUser = tokenService.getLoginUserByUserId(userId); if (loginUser != null) { uavControlService.releaseAllControls(userId); uavStreamManager.stopAllPullStreamByUser(userId); uavStateManager.removeSubscribe(userId); uavTelemetryManager.removeSubscribe(userId); uavSignalManager.removeSubscribe(userId); uavControlManager.removeSubscribe(userId); tokenService.delLoginUser(loginUser.getToken(), userId); log.info("【cmd】用户 [{}] 超时未重连,已删除 token", userId); } ard-work/src/main/java/com/ard/work/domain/dto/manual/FlyRoundControlCancelDTO.java
New file @@ -0,0 +1,22 @@ package com.ard.work.domain.dto.manual; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; /** * 环绕飞行控制实体对象 * * @author liusuyi * @date 2024年10月26日9:02 **/ @Data @Builder @AllArgsConstructor @NoArgsConstructor public class FlyRoundControlCancelDTO { private String boxSn; } ard-work/src/main/java/com/ard/work/domain/dto/manual/FlyRoundControlDTO.java
New file @@ -0,0 +1,51 @@ package com.ard.work.domain.dto.manual; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; /** * 环绕飞行控制实体对象 * * @author liusuyi * @date 2024年10月26日9:02 **/ @Data @Builder @AllArgsConstructor @NoArgsConstructor public class FlyRoundControlDTO { private String boxSn; /** * 兴趣点经度 */ private Double longitude; /** * 兴趣点纬度 */ private Double latitude; /** * 兴趣点高度 */ private Float altitude; /** * 环绕速度 */ private Float speed; /** * 环绕半径 */ private Float radius; /** * 飞行模式 */ @Builder.Default private byte flyMode = 0; } ard-work/src/main/java/com/ard/work/service/ITyService.java
@@ -206,6 +206,17 @@ */ Boolean flyPointControlCancel(FlyPointControlCancelDTO flyPointControlCancelDTO); /** * 环绕飞行控制 */ Boolean FlyRoundControl(FlyRoundControlDTO flyRoundControlDTO); /** * 停止环绕飞行 */ Boolean FlyRoundControlCancel(FlyRoundControlCancelDTO flyRoundControlCancelDTO); /** * 紧急制动 */ ard-work/src/main/java/com/ard/work/service/impl/ArdFlightTaskServiceServiceImpl.java
@@ -1,5 +1,8 @@ package com.ard.work.service.impl; import com.ard.common.constant.CacheConstants; import com.ard.common.core.domain.entity.SysUser; import com.ard.common.core.redis.RedisCache; import com.ard.common.utils.DateUtils; import com.ard.common.utils.file.MimeTypeUtils; import com.ard.common.utils.uuid.IdUtils; @@ -16,8 +19,10 @@ import com.ard.work.mapper.ArdMediaMapper; import com.ard.work.service.IArdFlightTaskService; import com.ard.work.service.IArdServerInfoService; import com.ard.work.service.control.UavControlService; import com.ard.work.util.osSelect; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.stereotype.Service; @@ -57,7 +62,10 @@ private OssServiceContext ossServiceContext; @Resource private ThreadPoolTaskExecutor threadPoolTaskExecutor; // 使用线程池执行异步任务 @Resource private UavControlService uavControlService; @Autowired private RedisCache redisCache; /** * 查询飞行架次列表 * @@ -128,6 +136,14 @@ .startTime(DateUtils.dateTime(DateUtils.YYYY_MM_DD_HH_MM_SS, boxHistoryReceiver.getStartTime())) .endTime(DateUtils.dateTime(DateUtils.YYYY_MM_DD_HH_MM_SS, boxHistoryReceiver.getEndTime())) .build(); Long flyUserId = uavControlService.getCurrentController(flightTask.getBoxSn(), "fly"); if (flyUserId != null) { SysUser flyUser = redisCache.getCacheObject(CacheConstants.SYS_USER_KEY + flyUserId); if (flyUser != null && flyUser.getNickName() != null) { flightTask.setCreateBy(flyUser.getNickName()); } } flightTask.setCreateTime(DateUtils.getNowDate()); // 插入新记录 int rows = ardFlightTaskMapper.insertArdFlightTask(flightTask); @@ -144,12 +160,46 @@ } else { // 如果存在,则更新记录 log.debug("Updating task {}", boxHistoryReceiver.getTaskId()); updateFlightTask(ardFlightTask, boxHistoryReceiver); // 增量更新 updateVideoToDatabase(ardFlightTask, boxHistoryReceiver.getVideoPathList()); // 增量更新图片 updatePhotoToDatabase(ardFlightTask, serverId, boxHistoryReceiver.getBoxSn()); } } private void updateFlightTask(ArdFlightTask local, BoxHistoryReceiver remote) { boolean needUpdate = false; // endTime 更新(重要!!!) Date newEnd = DateUtils.dateTime(DateUtils.YYYY_MM_DD_HH_MM_SS, remote.getEndTime()); if (newEnd != null && (local.getEndTime() == null || !local.getEndTime().equals(newEnd))) { local.setEndTime(newEnd); needUpdate = true; } // photoCount 更新 int remotePhotoCount = Integer.parseInt(remote.getPhotoCount()); if (local.getPhotoCount() != remotePhotoCount) { local.setPhotoCount(remotePhotoCount); needUpdate = true; } // videoCount 更新 int remoteVideoCount = remote.getVideoPathList() != null ? remote.getVideoPathList().size() : 0; if (local.getVideoCount() != remoteVideoCount) { local.setVideoCount(remoteVideoCount); needUpdate = true; } if (needUpdate) { ardFlightTaskMapper.updateArdFlightTask(local); } // 其它媒体增量更新 updateVideoToDatabase(local, remote.getVideoPathList()); updatePhotoToDatabase(local, local.getServerId(), remote.getBoxSn()); } @@ -415,70 +465,6 @@ } // 生成视频缩略图 public ByteArrayInputStream generateThumbnail1(String videoFile, int skipFrames, int targetWidth) { // long start = System.currentTimeMillis(); // FFmpegFrameGrabber grabber = null; // // try { // grabber = new FFmpegFrameGrabber(videoFile); // grabber.start(); // // int totalFrames = grabber.getLengthInFrames(); // System.out.println("视频总帧数: " + totalFrames); // System.out.println("视频时长: " + grabber.getLengthInTime() / 1000000 + "秒"); // // Frame frame = null; // int currentFrame = 0; // // // 读取指定帧数后的第一帧有效图像 // while (currentFrame < totalFrames) { // frame = grabber.grabImage(); // if (frame == null) { // break; // } // // if (currentFrame > skipFrames && frame.image != null) { // break; // } // currentFrame++; // } // // if (frame == null || frame.image == null) { // throw new Exception("无法获取有效视频帧"); // } // // // 转换 Frame 为 BufferedImage // Java2DFrameConverter converter = new Java2DFrameConverter(); // BufferedImage image = converter.getBufferedImage(frame); // // // 按目标宽度等比缩放 // int targetHeight = (int) ((double) targetWidth / image.getWidth() * image.getHeight()); // BufferedImage scaledImage = new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_3BYTE_BGR); // scaledImage.getGraphics().drawImage(image, 0, 0, targetWidth, targetHeight, null); // // // 转成 ByteArrayInputStream // ByteArrayOutputStream baos = new ByteArrayOutputStream(); // ImageIO.write(scaledImage, "jpg", baos); // long cost = System.currentTimeMillis() - start; // System.out.println("处理耗时: " + cost + "ms"); // // return new ByteArrayInputStream(baos.toByteArray()); // // } catch (Exception e) { // throw new RuntimeException(e); // } finally { // if (grabber != null) { // try { // grabber.stop(); // grabber.close(); // } catch (Exception e) { // e.printStackTrace(); // } // } // } return null; } public ByteArrayInputStream generateThumbnail( String videoFile, int skipSeconds, @@ -492,6 +478,12 @@ try { List<String> command = new ArrayList<>(); command.add(ffmpegPath); command.add("-timeout"); command.add("3000000"); command.add("-rw_timeout"); command.add("3000000"); command.add("-ss"); command.add(String.format("00:00:%02d", skipSeconds)); command.add("-i"); ard-work/src/main/java/com/ard/work/service/impl/TyServiceImpl.java
@@ -603,6 +603,37 @@ return true; } /** * 环绕飞行控制 */ @Override public Boolean FlyRoundControl(FlyRoundControlDTO flyRoundControlDTO) { String boxSn = flyRoundControlDTO.getBoxSn(); ArdDroneInfo ardDroneInfo = ardDroneService.selectArdDroneByboxSn(boxSn); if (ardDroneInfo == null) { throw new ControlException("未找到对应的飞行器信息,boxSn:{}" + boxSn); } FlyRoundControlCmd flyPointControlCmd = new FlyRoundControlCmd(flyRoundControlDTO); this.sendMessage(ardDroneInfo.getServerId(), flyPointControlCmd); return true; } /** * 停止环绕飞行控制 */ @Override public Boolean FlyRoundControlCancel(FlyRoundControlCancelDTO flyRoundControlCancelDTO) { String boxSn = flyRoundControlCancelDTO.getBoxSn(); ArdDroneInfo ardDroneInfo = ardDroneService.selectArdDroneByboxSn(boxSn); if (ardDroneInfo == null) { throw new ControlException("未找到对应的飞行器信息,boxSn:{}" + boxSn); } FlyRoundControlCancelCmd flyRoundControlCancelCmd = new FlyRoundControlCancelCmd(flyRoundControlCancelDTO); this.sendMessage(ardDroneInfo.getServerId(), flyRoundControlCancelCmd); return true; } /** * 紧急制动 */ @@ -1042,6 +1073,12 @@ logPrint("设置返航点", returnPointSettingReplyCmd.getResult(), returnPointSettingReplyCmd.getErrorCode()); } //到达环绕位置回复 if (tyCommand instanceof FlyRoundReplyCmd) { FlyRoundReplyCmd flyRoundReplyCmd = (FlyRoundReplyCmd) tyCommand; logPrint("到达环绕位置", flyRoundReplyCmd.getResult(), flyRoundReplyCmd.getErrorCode()); } break; case (byte) 0xD2: //设置视频码流回复