ard-admin/src/main/java/com/ard/web/controller/monitor/CacheController.java
@@ -44,6 +44,7 @@ caches.add(new SysCache(CacheConstants.RATE_LIMIT_KEY, "限流处理")); caches.add(new SysCache(CacheConstants.PWD_ERR_CNT_KEY, "密码错误次数")); caches.add(new SysCache(CacheConstants.CAMERA_LIST_KEY, "相机列表")); caches.add(new SysCache(CacheConstants.UAV_ONLINE_KEY, "在线无人机")); } @PreAuthorize("@ss.hasPermi('monitor:cache:list')") ard-admin/src/main/java/com/ard/web/controller/work/UavGimbalController.java
@@ -4,13 +4,16 @@ import com.ard.work.domain.dto.control.AbsAngleControlDTO; import com.ard.work.domain.dto.control.AngleControlDTO; import com.ard.work.domain.dto.control.SpeedControlDTO; import com.ard.work.service.IGimbalSpeedService; import com.ard.work.service.ITyService; import com.ard.work.service.impl.GimbalSpeedService; import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; @@ -28,10 +31,11 @@ @Resource private ITyService tyService; @Resource private GimbalSpeedService gimbalSpeedService; private IGimbalSpeedService gimbalSpeedService; @ApiOperation(value = "云台转动(基于角度)", notes = "actionCode值:0-回中,1-上,2-右上,3-右,4-右下,5-下,6-左下,7-左,8-左上,254-停止 \r\n rotationAngle值:取值范围:0~255【云台实际转动角度= 转动角度/100+2.5 单位:度】") @ApiOperation(value = "云台转动(基于角度)", notes = "actionCode值:0-回中,1-上,2-右上,3-右,4-右下,5-下,6-左下,7-左,8-左上,254-停止 \r\n " + "rotationAngle值:取值范围:0~255【云台实际转动角度= 转动角度/100+2.5 单位:度】") @ApiOperationSupport(order = 1) @PostMapping("/angleControl") public AjaxResult angleControl(@RequestBody AngleControlDTO angleControlDto) { ard-common/src/main/java/com/ard/common/constant/CacheConstants.java
@@ -51,4 +51,9 @@ * 所有相机 redis key */ public static final String CAMERA_LIST_KEY = "camera_list:"; /** * 在线无人机 redis key */ public static final String UAV_ONLINE_KEY = "uav_online:"; } ard-work/src/main/java/com/ard/work/service/IGimbalSpeedService.java
New file @@ -0,0 +1,16 @@ package com.ard.work.service; import com.ard.work.domain.dto.control.SpeedControlDTO; /** * 云台速度控制接口 * * @author lsy * @date 2025/10/28 */ public interface IGimbalSpeedService { void startControl(SpeedControlDTO speed); void stopControl(String boxSn); } ard-work/src/main/java/com/ard/work/service/impl/ArdDroneInfoServiceImpl.java
@@ -1,6 +1,8 @@ package com.ard.work.service.impl; import com.alibaba.fastjson2.JSONObject; import com.ard.common.constant.CacheConstants; import com.ard.common.core.redis.RedisCache; import com.ard.common.exception.ServiceException; import com.ard.common.utils.DateUtils; import com.ard.common.utils.uuid.IdUtils; @@ -17,6 +19,7 @@ import javax.annotation.Resource; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; /** @@ -37,6 +40,9 @@ @Resource private AuthorizeService authorizeService; @Resource private RedisCache redisCache; /** * 查询无人机盒子 @@ -121,7 +127,7 @@ /** * 批量新增无人机盒子 * * @param boxSn 盒子编码数组 * @param boxSn 盒子编码数组 * @param deptId 部门ID * @return 成功插入的记录数 */ @@ -251,23 +257,29 @@ /** * 同步盒子属性 * * @return 结果 */ @Scheduled(fixedRate = 1000 * 10) @Scheduled(fixedRate = 1000 * 5) public void syncBoxInfo() { List<ArdDroneInfo> ArdDroneeListEntity = selectArdDroneList(new ArdDroneInfo()); // 将本地列表转换为Map,方便快速查找 Map<String, ArdDroneInfo> localArdDroneMap = ArdDroneeListEntity.stream() List<ArdDroneInfo> localList = selectArdDroneList(new ArdDroneInfo()); Map<String, ArdDroneInfo> localMap = localList.stream() .collect(Collectors.toMap(ArdDroneInfo::getBoxSn, drone -> drone)); //遍历接口最新获取的无人机盒子信息 tyService.getBoxList().forEach(remoteArdDrone -> { ArdDroneInfo localArdDroneInfo = localArdDroneMap.get(remoteArdDrone.getBoxSn()); if (localArdDroneInfo != null) { if (!localArdDroneInfo.isSameAs(remoteArdDrone)) { // 修改无人机(只更新变化的属性) updateChangedAttributes(localArdDroneInfo, remoteArdDrone); updateArdDrone(localArdDroneInfo); } tyService.getBoxList().forEach(remote -> { String boxSn = remote.getBoxSn(); // ✅ 维护在线状态缓存 // 只有在线的无人机才存 Redis String redisKey = CacheConstants.UAV_ONLINE_KEY + boxSn; if (remote.getOnLine() == 1) { redisCache.setCacheObject(redisKey, remote, 10, TimeUnit.SECONDS); } else { redisCache.deleteObject(CacheConstants.UAV_ONLINE_KEY + boxSn); } // 数据库同步 ArdDroneInfo local = localMap.get(boxSn); if (local != null && !local.isSameAs(remote)) { updateChangedAttributes(local, remote); updateArdDrone(local); } }); } ard-work/src/main/java/com/ard/work/service/impl/GimbalSpeedService.java
@@ -1,38 +1,70 @@ package com.ard.work.service.impl; import com.ard.common.constant.CacheConstants; import com.ard.common.core.domain.model.LoginUser; import com.ard.common.core.redis.RedisCache; import com.ard.common.utils.SecurityUtils; import com.ard.work.domain.cache.UavStateCache; import com.ard.work.domain.dto.control.SpeedControlDTO; import com.ard.work.domain.dto.state.UavState; import com.ard.work.service.IGimbalSpeedService; import com.ard.work.service.ITyService; import lombok.extern.slf4j.Slf4j; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.util.Map; import java.util.concurrent.*; /** * 云台速度控制实现 * * @author lsy * @date 2025/10/28 */ @Service @Slf4j public class GimbalSpeedService { public class GimbalSpeedService implements IGimbalSpeedService { private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); private final Map<String, ScheduledFuture<?>> tasks = new ConcurrentHashMap<>(); @Resource private ITyService tyService; @Resource private RedisCache redisCache; @Override public synchronized void startControl(SpeedControlDTO speed) { String boxSn = speed.getBoxSn(); // ✅ 先判断是否在线 if (!isUavOnline(boxSn)) { throw new RuntimeException("无人机离线,无法启动云台控制: " + boxSn); } if (tasks.containsKey(boxSn)) { return; } LoginUser loginUser = SecurityUtils.getLoginUser();// 主线程获取 // ✅ 启动前先试探一次,确保正常 try { SecurityContextHolder.getContext().setAuthentication( new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities()) ); Boolean ok = tyService.speedControl(speed); if (ok == null || !ok) { throw new RuntimeException("初始速度控制指令失败"); } } catch (Exception e) { log.error("初次控制指令异常: {}", e.getMessage()); throw new RuntimeException("无法启动云台控制:" + e.getMessage()); } ScheduledFuture<?> future = executor.scheduleAtFixedRate(() -> { double currentYaw = getCurrentYaw(boxSn); log.warn("currentYaw: {}", currentYaw); if ((speed.getYawSpeed() < 0 && currentYaw <= -180) || (speed.getYawSpeed() > 0 && currentYaw >= 180)) { stopControl(boxSn); @@ -55,7 +87,7 @@ tasks.put(boxSn, future); } @Override public synchronized void stopControl(String boxSn) { ScheduledFuture<?> future = tasks.remove(boxSn); if (future != null) { @@ -71,4 +103,11 @@ } return uavState.getPtzState().getYaw(); } private boolean isUavOnline(String boxSn) { // 如果 Redis 中存在对应 key,则认为在线 Object obj = redisCache.getCacheObject(CacheConstants.UAV_ONLINE_KEY + boxSn); return obj != null; } }