42 files added
20 files modified
| | |
| | | </execution> |
| | | </executions> |
| | | </plugin> |
| | | <!-- 资源过滤使用 @ 作为分隔符,替换 application.yml 中的 @spring.profiles.active@ --> |
| | | <plugin> |
| | | <groupId>org.apache.maven.plugins</groupId> |
| | | <artifactId>maven-resources-plugin</artifactId> |
| | | <configuration> |
| | | <delimiters> |
| | | <delimiter>@</delimiter> |
| | | </delimiters> |
| | | <useDefaultDelimiters>false</useDefaultDelimiters> |
| | | </configuration> |
| | | </plugin> |
| | | <plugin> |
| | | <groupId>org.apache.maven.plugins</groupId> |
| | | <artifactId>maven-war-plugin</artifactId> |
| New file |
| | |
| | | /* |
| | | * 【机场1】从 dock2 原类 com.ard.web.controller.sample.CommandController 复制/独立适配(DJI Dock 1 / M30)。 |
| | | * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。 |
| | | */ |
| | | package com.ard.web.controller.sample.dock1; |
| | | |
| | | import com.ard.sample.common.model.ResponseResult; |
| | | import com.ard.sample.manage.model.param.CommandParam; |
| | | import com.ard.sample.dock1.manage.service.ICommandService; |
| | | import io.swagger.annotations.Api; |
| | | import io.swagger.annotations.ApiOperation; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.web.bind.annotation.*; |
| | | import com.ard.web.controller.sample.CommandController; |
| | | |
| | | /** |
| | | * @author sean |
| | | * @version 1.2 |
| | | * @date 2022/7/29 |
| | | */ |
| | | @Api(tags = "指令控制") |
| | | @RestController |
| | | @Slf4j |
| | | @RequestMapping("/dock1/devices") |
| | | public class Dock1CommandController { |
| | | |
| | | @Autowired |
| | | private ICommandService commandService; |
| | | |
| | | @ApiOperation("负载控制-拍照") |
| | | @PostMapping("/{sn}/command/cameraPhotoTake") |
| | | public ResponseResult cameraPhotoTake(@PathVariable String sn, |
| | | @RequestBody CommandParam param) { |
| | | return commandService.cameraPhotoTake(sn, param); |
| | | } |
| | | |
| | | } |
| New file |
| | |
| | | /* |
| | | * 【机场1】从 dock2 原类 com.ard.web.controller.sample.DeviceController 复制/独立适配(DJI Dock 1 / M30)。 |
| | | * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。 |
| | | */ |
| | | package com.ard.web.controller.sample.dock1; |
| | | |
| | | import com.ard.common.core.controller.BaseController; |
| | | import com.ard.common.core.page.TableDataInfo; |
| | | import com.ard.device.camera.domain.ArdCamera; |
| | | import com.ard.sample.common.error.CommonErrorEnum; |
| | | import com.ard.sample.common.model.PaginationData; |
| | | import com.ard.sample.common.model.ResponseResult; |
| | | import com.ard.sample.component.mqtt.model.ChannelName; |
| | | import com.ard.sample.component.mqtt.model.CommonTopicReceiver; |
| | | import com.ard.sample.component.mqtt.model.CommonTopicResponse; |
| | | import com.ard.sample.manage.model.dto.DeviceDTO; |
| | | import com.ard.sample.manage.model.dto.DeviceFirmwareUpgradeDTO; |
| | | import com.ard.sample.manage.model.entity.DeviceEntity; |
| | | import com.ard.sample.manage.model.enums.DeviceFirmwareStatusEnum; |
| | | import com.ard.sample.manage.model.enums.DeviceSetPropertyEnum; |
| | | import com.ard.sample.manage.model.receiver.StatusGatewayReceiver; |
| | | import com.ard.sample.manage.service.IDeviceService; |
| | | import com.baomidou.mybatisplus.core.conditions.Wrapper; |
| | | import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; |
| | | import com.fasterxml.jackson.databind.JsonNode; |
| | | import io.swagger.annotations.Api; |
| | | import io.swagger.annotations.ApiOperation; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.security.access.prepost.PreAuthorize; |
| | | import org.springframework.web.bind.annotation.*; |
| | | |
| | | import java.time.ZoneId; |
| | | import java.util.List; |
| | | import java.util.Optional; |
| | | |
| | | import static com.ard.common.utils.PageUtils.startPage; |
| | | import com.ard.web.controller.sample.DeviceController; |
| | | |
| | | /** |
| | | * @author sean.zhou |
| | | * @version 0.1 |
| | | * @date 2021/11/15 |
| | | */ |
| | | @Api(tags = "设备管理") |
| | | @RestController |
| | | @Slf4j |
| | | @RequestMapping("/dock1/device") |
| | | public class Dock1DeviceController extends BaseController { |
| | | |
| | | @Autowired |
| | | private IDeviceService deviceService; |
| | | |
| | | /** |
| | | * 处理上线的消息 |
| | | * |
| | | * @param receiver The drone information is not empty. |
| | | */ |
| | | // 机场1:不复制原 ServiceActivator(inputChannel = ChannelName.INBOUND_STATUS_ONLINE),消费职责由共享层原类承担 |
| | | public void deviceOnline(CommonTopicReceiver<StatusGatewayReceiver> receiver) { |
| | | boolean online = deviceService.deviceOnline(receiver.getData()); |
| | | if (online) { |
| | | // Notify pilot that the drone is online successfully. |
| | | deviceService.publishStatusReply(receiver.getData().getSn(), |
| | | CommonTopicResponse.builder() |
| | | .tid(receiver.getTid()) |
| | | .bid(receiver.getBid()) |
| | | .timestamp(System.currentTimeMillis()) |
| | | .method(receiver.getMethod()) |
| | | .build()); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 处理离线的消息 |
| | | * |
| | | * @param receiver The drone information is empty. |
| | | */ |
| | | // 机场1:不复制原 ServiceActivator(inputChannel = ChannelName.INBOUND_STATUS_OFFLINE),消费职责由共享层原类承担 |
| | | public void deviceOffline(CommonTopicReceiver<StatusGatewayReceiver> receiver) { |
| | | |
| | | boolean offline = deviceService.deviceOffline(receiver.getData()); |
| | | if (offline) { |
| | | // Notify pilot that the device is offline successfully. |
| | | deviceService.publishStatusReply(receiver.getData().getSn(), |
| | | CommonTopicResponse.builder() |
| | | .tid(receiver.getTid()) |
| | | .bid(receiver.getBid()) |
| | | .timestamp(System.currentTimeMillis()) |
| | | .method(receiver.getMethod()) |
| | | .build()); |
| | | |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 获取所有在线设备列表 |
| | | * |
| | | * @return |
| | | */ |
| | | @ApiOperation("设备列表") |
| | | @PreAuthorize("@ss.hasPermi('device:dock:list')") |
| | | @GetMapping("/list") |
| | | public ResponseResult<List<DeviceDTO>> getDevices(@RequestParam(required = false) String nickname) { |
| | | // List<DeviceDTO> devicesList = deviceService.getDevicesTopoForWeb(); |
| | | List<DeviceDTO> devicesList = deviceService.getDockList(nickname); |
| | | return ResponseResult.success(devicesList); |
| | | } |
| | | |
| | | // /** |
| | | // * 获取按nickname筛选在线设备列表 |
| | | // * |
| | | // * @return |
| | | // */ |
| | | // @ApiOperation("设备列表-按nickname筛选") |
| | | // @GetMapping("/cameralist") |
| | | // public ResponseResult<List<DeviceDTO>> getCameraDevices(@RequestParam(required = false) String nickname) { |
| | | // List<DeviceDTO> devicesList = deviceService.getDockCameraList(nickname); |
| | | // return ResponseResult.success(devicesList); |
| | | // } |
| | | |
| | | /** |
| | | * 绑定设备 |
| | | * |
| | | * @param device |
| | | * @param deviceSn |
| | | * @return |
| | | */ |
| | | @ApiOperation("设备绑定") |
| | | @PostMapping("/{device_sn}/binding") |
| | | public ResponseResult bindDevice(@RequestBody DeviceDTO device, @PathVariable("device_sn") String deviceSn) { |
| | | device.setDeviceSn(deviceSn); |
| | | boolean isUpd = deviceService.bindDevice(device); |
| | | return isUpd ? ResponseResult.success() : ResponseResult.error(); |
| | | } |
| | | |
| | | /** |
| | | * Obtain device information according to device sn. |
| | | * |
| | | * @param deviceSn |
| | | * @return |
| | | */ |
| | | @ApiOperation("设备信息") |
| | | @GetMapping("/{device_sn}") |
| | | public ResponseResult getDevice(@PathVariable("device_sn") String deviceSn) { |
| | | Optional<DeviceDTO> deviceOpt = deviceService.getDeviceBySn(deviceSn); |
| | | return deviceOpt.isEmpty() ? ResponseResult.error("device not found.") : |
| | | ResponseResult.success(deviceOpt.get()); |
| | | } |
| | | |
| | | /** |
| | | * Get the binding devices list |
| | | * 获取绑定设备列表 |
| | | * |
| | | * @param page |
| | | * @param pageSize |
| | | * @return |
| | | */ |
| | | @ApiOperation("绑定设备列表") |
| | | @GetMapping("/list/bound") |
| | | @PreAuthorize("@ss.hasPermi('device:dock:list')") |
| | | public ResponseResult<PaginationData<DeviceDTO>> getBoundDevicesWithDomain(Integer domain, |
| | | @RequestParam(defaultValue = "1") Long page, |
| | | @RequestParam(value = "page_size", |
| | | defaultValue = "50") Long pageSize, |
| | | @RequestParam(required = false) Long deptId) { |
| | | PaginationData<DeviceDTO> devices = deviceService.getBoundDevicesWithDomain(page, pageSize, domain, deptId); |
| | | |
| | | return ResponseResult.success(devices); |
| | | } |
| | | |
| | | /** |
| | | * Removing the binding state of the device. |
| | | * |
| | | * @param deviceSn |
| | | * @return |
| | | */ |
| | | @ApiOperation("设备解绑") |
| | | @DeleteMapping("/{device_sn}/unbinding") |
| | | public ResponseResult unbindingDevice(@PathVariable("device_sn") String deviceSn) { |
| | | deviceService.unbindDevice(deviceSn); |
| | | return ResponseResult.success(); |
| | | } |
| | | |
| | | /** |
| | | * Update device information. |
| | | * |
| | | * @param device |
| | | * @param deviceSn |
| | | * @return |
| | | */ |
| | | @ApiOperation("设备更新") |
| | | @PutMapping("/{device_sn}") |
| | | public ResponseResult updateDevice(@RequestBody DeviceDTO device, |
| | | @PathVariable("device_sn") String deviceSn) { |
| | | device.setDeviceSn(deviceSn); |
| | | boolean isUpd = deviceService.updateDevice(device); |
| | | return isUpd ? ResponseResult.success() : ResponseResult.error(); |
| | | } |
| | | |
| | | /** |
| | | * Delivers offline firmware upgrade tasks. |
| | | * |
| | | * @param upgradeDTOS |
| | | * @return |
| | | */ |
| | | @ApiOperation("固件升级") |
| | | @PostMapping("/ota") |
| | | public ResponseResult createOtaJob(@RequestBody List<DeviceFirmwareUpgradeDTO> upgradeDTOS) { |
| | | return deviceService.createDeviceOtaJob(upgradeDTOS); |
| | | } |
| | | |
| | | /** |
| | | * Set the property parameters of the drone. |
| | | * |
| | | * @param dockSn |
| | | * @param param |
| | | * @return |
| | | */ |
| | | @ApiOperation("属性设置") |
| | | @PutMapping("{device_sn}/property") |
| | | public ResponseResult devicePropertySet(@PathVariable("device_sn") String dockSn, |
| | | @RequestBody JsonNode param) { |
| | | if (param.size() != 1) { |
| | | //return ResponseResult.error(CommonErrorEnum.ILLEGAL_ARGUMENT); |
| | | return ResponseResult.error(999,"参数不合法"); |
| | | } |
| | | String property = param.fieldNames().next(); |
| | | Optional<DeviceSetPropertyEnum> propertyEnumOpt = DeviceSetPropertyEnum.find(property); |
| | | if (propertyEnumOpt.isEmpty()) { |
| | | //return ResponseResult.error(CommonErrorEnum.ILLEGAL_ARGUMENT); |
| | | return ResponseResult.error(999,"参数无效"); |
| | | } |
| | | return deviceService.devicePropertySet(dockSn, propertyEnumOpt.get(), param); |
| | | // return ResponseResult.success(); |
| | | } |
| | | |
| | | //.get(property) |
| | | |
| | | // /** |
| | | // * 查询机场设备列表 |
| | | // */ |
| | | // @PreAuthorize("@ss.hasPermi('device:camera:list')") |
| | | // @GetMapping("/test/list") |
| | | // @ApiOperation("查询机场设备列表") |
| | | // public TableDataInfo list(DeviceDTO device) { |
| | | // startPage(); |
| | | // DeviceEntity deviceEntity = deviceDTO2Entity(device); |
| | | // QueryWrapper<DeviceEntity> queryWrapper=new QueryWrapper<>(deviceEntity); |
| | | // List<DeviceEntity> list = deviceService.selectList(queryWrapper); |
| | | // return getDataTable(list); |
| | | // } |
| | | |
| | | private DeviceEntity deviceDTO2Entity(DeviceDTO dto) { |
| | | DeviceEntity.DeviceEntityBuilder builder = DeviceEntity.builder(); |
| | | if (dto == null) { |
| | | return builder.build(); |
| | | } |
| | | |
| | | return builder.deviceSn(dto.getDeviceSn()) |
| | | .userId(dto.getUserId()) |
| | | .nickname(dto.getNickname()) |
| | | .boundStatus(dto.getBoundStatus()) |
| | | .loginTime(dto.getLoginTime() != null ? |
| | | dto.getLoginTime().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli() : null) |
| | | .boundTime(dto.getBoundTime() != null ? |
| | | dto.getBoundTime().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli() : null) |
| | | .childSn(dto.getChildDeviceSn()) |
| | | .domain(dto.getDomain()) |
| | | .firmwareVersion(dto.getFirmwareVersion()) |
| | | .workType(dto.getWorkType()) |
| | | .webrtcUrl(dto.getWebrtcUrl()) |
| | | .cameraWebrtcUrl(dto.getCameraWebrtcUrl()) |
| | | .cameraState(dto.getCameraState()) |
| | | .compatibleStatus(dto.getFirmwareStatus() == null ? null : |
| | | DeviceFirmwareStatusEnum.CONSISTENT_UPGRADE != DeviceFirmwareStatusEnum.find(dto.getFirmwareStatus())) |
| | | .build(); |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * 【机场1】从 dock2 原类 com.ard.web.controller.sample.DeviceFirmwareController 复制/独立适配(DJI Dock 1 / M30)。 |
| | | * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。 |
| | | */ |
| | | package com.ard.web.controller.sample.dock1; |
| | | |
| | | import com.ard.common.core.domain.model.LoginUser; |
| | | import com.ard.common.utils.SecurityUtils; |
| | | import com.ard.sample.common.model.PaginationData; |
| | | import com.ard.sample.common.model.ResponseResult; |
| | | import com.ard.sample.manage.model.dto.DeviceFirmwareDTO; |
| | | import com.ard.sample.manage.model.dto.DeviceFirmwareNoteDTO; |
| | | import com.ard.sample.manage.model.dto.FirmwareFileProperties; |
| | | import com.ard.sample.manage.model.param.DeviceFirmwareQueryParam; |
| | | import com.ard.sample.manage.model.param.DeviceFirmwareUpdateParam; |
| | | import com.ard.sample.manage.model.param.DeviceFirmwareUploadParam; |
| | | import com.ard.sample.manage.service.IDeviceFirmwareService; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.validation.annotation.Validated; |
| | | import org.springframework.web.bind.annotation.*; |
| | | import org.springframework.web.multipart.MultipartFile; |
| | | |
| | | import javax.validation.Valid; |
| | | import javax.validation.constraints.NotNull; |
| | | import java.util.List; |
| | | import java.util.Optional; |
| | | import java.util.stream.Collectors; |
| | | import com.ard.web.controller.sample.DeviceFirmwareController; |
| | | |
| | | /** |
| | | * @author sean |
| | | * @version 1.2 |
| | | * @date 2022/8/16 |
| | | */ |
| | | @RestController |
| | | @RequestMapping("/dock1/workspaces") |
| | | @Validated |
| | | public class Dock1DeviceFirmwareController { |
| | | |
| | | @Autowired |
| | | private IDeviceFirmwareService service; |
| | | |
| | | /** |
| | | * Get the latest firmware version information for this device model. |
| | | * @param deviceNames |
| | | * @return |
| | | */ |
| | | @GetMapping("/firmware-release-notes/latest") |
| | | public ResponseResult<List<DeviceFirmwareNoteDTO>> getLatestFirmwareNote(@RequestParam("device_name") List<String> deviceNames) { |
| | | |
| | | List<DeviceFirmwareNoteDTO> releaseNotes = deviceNames.stream() |
| | | .map(deviceName -> service.getLatestFirmwareReleaseNote(deviceName)) |
| | | .filter(Optional::isPresent) |
| | | .map(Optional::get) |
| | | .collect(Collectors.toList()); |
| | | |
| | | return ResponseResult.success(releaseNotes); |
| | | } |
| | | |
| | | /** |
| | | * Query firmware information based on parameters. |
| | | * @param workspaceId |
| | | * @param param |
| | | * @return |
| | | */ |
| | | @GetMapping("/firmwares") |
| | | public ResponseResult<PaginationData<DeviceFirmwareDTO>> getAllFirmwarePagination(@Valid DeviceFirmwareQueryParam param) { |
| | | |
| | | PaginationData<DeviceFirmwareDTO> data = service.getAllFirmwarePagination(param); |
| | | return ResponseResult.success(data); |
| | | } |
| | | |
| | | /** |
| | | * Import firmware file for device upgrades. |
| | | * @param file |
| | | * @param param |
| | | * @return |
| | | */ |
| | | @PostMapping("/firmwares/file/upload") |
| | | public ResponseResult importFirmwareFile(@NotNull(message = "No file received.") MultipartFile file, |
| | | @Valid DeviceFirmwareUploadParam param) { |
| | | |
| | | if (!file.getOriginalFilename().endsWith(FirmwareFileProperties.FIRMWARE_FILE_SUFFIX)) { |
| | | return ResponseResult.error("The file format is incorrect."); |
| | | } |
| | | LoginUser loginUser = SecurityUtils.getLoginUser(); |
| | | String creator = loginUser.getUsername(); |
| | | |
| | | service.importFirmwareFile(creator, param, file); |
| | | return ResponseResult.success(); |
| | | } |
| | | |
| | | /** |
| | | * Change the firmware availability status. |
| | | * @param workspaceId |
| | | * @param firmwareId |
| | | * @param param |
| | | * @return |
| | | */ |
| | | @PutMapping("/{workspace_id}/firmwares/{firmware_id}") |
| | | public ResponseResult changeFirmwareStatus(@PathVariable("workspace_id") String workspaceId, |
| | | @PathVariable("firmware_id") String firmwareId, |
| | | @Valid @RequestBody DeviceFirmwareUpdateParam param) { |
| | | |
| | | service.updateFirmwareInfo(DeviceFirmwareDTO.builder() |
| | | .firmwareId(firmwareId).firmwareStatus(param.getStatus()).build()); |
| | | return ResponseResult.success(); |
| | | } |
| | | |
| | | |
| | | } |
| New file |
| | |
| | | /* |
| | | * 【机场1】从 dock2 原类 com.ard.web.controller.sample.DeviceHmsController 复制/独立适配(DJI Dock 1 / M30)。 |
| | | * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。 |
| | | */ |
| | | package com.ard.web.controller.sample.dock1; |
| | | |
| | | import com.ard.sample.common.model.PaginationData; |
| | | import com.ard.sample.common.model.ResponseResult; |
| | | import com.ard.sample.manage.model.dto.DeviceHmsDTO; |
| | | import com.ard.sample.manage.model.param.DeviceHmsQueryParam; |
| | | import com.ard.sample.manage.service.IDeviceHmsService; |
| | | import io.swagger.annotations.Api; |
| | | import io.swagger.annotations.ApiOperation; |
| | | import io.swagger.v3.oas.annotations.Operation; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.web.bind.annotation.*; |
| | | |
| | | import java.util.HashSet; |
| | | import java.util.List; |
| | | import java.util.Set; |
| | | import com.ard.web.controller.sample.DeviceHmsController; |
| | | |
| | | /** |
| | | * @author sean |
| | | * @version 1.1 |
| | | * @date 2022/7/7 |
| | | */ |
| | | @Api(tags = "设备HMS管理") |
| | | @RestController |
| | | @Slf4j |
| | | @RequestMapping("/dock1/devices") |
| | | public class Dock1DeviceHmsController { |
| | | |
| | | @Autowired |
| | | private IDeviceHmsService deviceHmsService; |
| | | |
| | | /** |
| | | * 查询设备hms信息的页面。/{workspace_id},@PathVariable("workspace_id") String workspaceId |
| | | * @param param |
| | | * @return |
| | | */ |
| | | @Operation(summary = "获取设备HMS信息", description = "参数传空则忽略该条件,返回所有符合其他条件的数据") |
| | | @ApiOperation("获取设备HMS信息") |
| | | @GetMapping("/devices/hms") |
| | | public ResponseResult<PaginationData<DeviceHmsDTO>> getHmsInformation(DeviceHmsQueryParam param) { |
| | | if (param == null) { |
| | | param = new DeviceHmsQueryParam(); |
| | | } |
| | | PaginationData<DeviceHmsDTO> devices = deviceHmsService.getDeviceHmsByParam(param); |
| | | |
| | | return ResponseResult.success(devices); |
| | | } |
| | | |
| | | /** |
| | | * Update unread hms messages to read status. |
| | | * @param deviceSn |
| | | * @return |
| | | */ |
| | | @ApiOperation("更新HMS消息为已读状态") |
| | | @PutMapping("/devices/hms/{device_sn}") |
| | | public ResponseResult updateReadHmsByDeviceSn(@PathVariable("device_sn") String deviceSn) { |
| | | deviceHmsService.updateUnreadHms(deviceSn); |
| | | return ResponseResult.success(); |
| | | } |
| | | |
| | | /** |
| | | * Get hms messages for a single device. |
| | | * @param deviceSn |
| | | * @return |
| | | */ |
| | | @ApiOperation("获取设备未读HMS消息") |
| | | @GetMapping("/devices/hms/{device_sn}") |
| | | public ResponseResult<List<DeviceHmsDTO>> getUnreadHmsByDeviceSn(@PathVariable("device_sn") String deviceSn) { |
| | | PaginationData<DeviceHmsDTO> paginationData = deviceHmsService.getDeviceHmsByParam( |
| | | DeviceHmsQueryParam.builder() |
| | | .deviceSn(new HashSet<>(Set.of(deviceSn))) |
| | | .updateTime(0L) |
| | | .build()); |
| | | return ResponseResult.success(paginationData.getList()); |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * 【机场1】从 dock2 原类 com.ard.web.controller.sample.DeviceLogsController 复制/独立适配(DJI Dock 1 / M30)。 |
| | | * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。 |
| | | */ |
| | | package com.ard.web.controller.sample.dock1; |
| | | |
| | | import com.ard.common.core.domain.model.LoginUser; |
| | | import com.ard.common.utils.SecurityUtils; |
| | | import com.ard.sample.common.model.PaginationData; |
| | | import com.ard.sample.common.model.ResponseResult; |
| | | import com.ard.sample.manage.model.dto.DeviceLogsDTO; |
| | | import com.ard.sample.manage.model.param.DeviceLogsCreateParam; |
| | | import com.ard.sample.manage.model.param.DeviceLogsQueryParam; |
| | | import com.ard.sample.manage.model.param.LogsFileUpdateParam; |
| | | import com.ard.sample.manage.service.IDeviceLogsService; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.web.bind.annotation.*; |
| | | import javax.servlet.http.HttpServletRequest; |
| | | import javax.servlet.http.HttpServletResponse; |
| | | import java.net.URL; |
| | | import java.util.List; |
| | | import com.ard.web.controller.sample.DeviceLogsController; |
| | | |
| | | /** |
| | | * @author sean |
| | | * @version 1.2 |
| | | * @date 2022/9/7 |
| | | */ |
| | | @RestController |
| | | @Slf4j |
| | | @RequestMapping("/dock1/workspaces") |
| | | public class Dock1DeviceLogsController { |
| | | |
| | | @Autowired |
| | | private IDeviceLogsService deviceLogsService; |
| | | |
| | | /** |
| | | * Obtain the device upload log list by paging according to the query parameters. |
| | | * @param workspaceId |
| | | * @param deviceSn |
| | | * @param param |
| | | * @return |
| | | */ |
| | | @GetMapping("/{workspace_id}/devices/{device_sn}/logs-uploaded") |
| | | public ResponseResult getUploadedLogs(DeviceLogsQueryParam param, @PathVariable("workspace_id") String workspaceId, |
| | | @PathVariable("device_sn") String deviceSn) { |
| | | PaginationData<DeviceLogsDTO> data = deviceLogsService.getUploadedLogs(deviceSn, param); |
| | | return ResponseResult.success(data); |
| | | } |
| | | |
| | | /** |
| | | * Get a list of log files that can be uploaded in real time. |
| | | * @param workspaceId |
| | | * @param deviceSn |
| | | * @param domainList |
| | | * @return |
| | | */ |
| | | @GetMapping("/{workspace_id}/devices/{device_sn}/logs") |
| | | public ResponseResult getLogsBySn(@PathVariable("workspace_id") String workspaceId, |
| | | @PathVariable("device_sn") String deviceSn, |
| | | @RequestParam("domain_list") List<String> domainList) { |
| | | return deviceLogsService.getRealTimeLogs(deviceSn, domainList); |
| | | } |
| | | |
| | | /** |
| | | * Initiate a log upload request to the gateway. |
| | | * @return |
| | | */ |
| | | @PostMapping("/{workspace_id}/devices/{device_sn}/logs") |
| | | public ResponseResult uploadLogs(@PathVariable("workspace_id") String workspaceId, |
| | | @PathVariable("device_sn") String deviceSn, |
| | | HttpServletRequest request, @RequestBody DeviceLogsCreateParam param) { |
| | | |
| | | LoginUser loginUser = SecurityUtils.getLoginUser(); |
| | | return deviceLogsService.pushFileUpload(loginUser.getUsername(), deviceSn, param); |
| | | } |
| | | |
| | | /** |
| | | * Cancel logs file upload. |
| | | * @return |
| | | */ |
| | | @DeleteMapping("/{workspace_id}/devices/{device_sn}/logs") |
| | | public ResponseResult cancelUploadedLogs(@PathVariable("workspace_id") String workspaceId, |
| | | @PathVariable("device_sn") String deviceSn, |
| | | @RequestBody LogsFileUpdateParam param) { |
| | | |
| | | return deviceLogsService.pushUpdateFile(deviceSn, param); |
| | | } |
| | | |
| | | /** |
| | | * Delete upload history. |
| | | * @return |
| | | */ |
| | | @DeleteMapping("/{workspace_id}/devices/{device_sn}/logs/{logs_id}") |
| | | public ResponseResult deleteUploadedLogs(@PathVariable("workspace_id") String workspaceId, |
| | | @PathVariable("device_sn") String deviceSn, |
| | | @PathVariable("logs_id") String logsId) { |
| | | deviceLogsService.deleteLogs(deviceSn, logsId); |
| | | return ResponseResult.success(); |
| | | } |
| | | /** |
| | | * Query the download address of the file according to the wayline file id, |
| | | * and redirect to this address directly for download. |
| | | * @param workspaceId |
| | | * @param fileId |
| | | * @param logsId |
| | | * @param response |
| | | */ |
| | | @GetMapping("/{workspace_id}/logs/{logs_id}/url/{file_id}") |
| | | public ResponseResult getFileUrl(@PathVariable(name = "workspace_id") String workspaceId, |
| | | @PathVariable(name = "file_id") String fileId, |
| | | @PathVariable(name = "logs_id") String logsId, HttpServletResponse response) { |
| | | |
| | | try { |
| | | URL url = deviceLogsService.getLogsFileUrl(logsId, fileId); |
| | | return ResponseResult.success(url.toString()); |
| | | } catch (Exception e) { |
| | | log.error("Failed to get the logs file download address."); |
| | | e.printStackTrace(); |
| | | } |
| | | return ResponseResult.error("Failed to get the logs file download address."); |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * 【机场1】从 dock2 原类 com.ard.web.controller.sample.DevicePayloadController 复制/独立适配(DJI Dock 1 / M30)。 |
| | | * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。 |
| | | */ |
| | | package com.ard.web.controller.sample.dock1; |
| | | |
| | | import com.ard.sample.component.mqtt.model.ChannelName; |
| | | import com.ard.sample.manage.model.receiver.DeviceBasicReceiver; |
| | | import com.ard.sample.manage.service.IDevicePayloadService; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.messaging.MessageHeaders; |
| | | import org.springframework.web.bind.annotation.RestController; |
| | | import com.ard.web.controller.sample.DevicePayloadController; |
| | | |
| | | /** |
| | | * @author sean.zhou |
| | | * @date 2021/11/19 |
| | | * @version 0.1 |
| | | */ |
| | | @RestController |
| | | public class Dock1DevicePayloadController { |
| | | |
| | | @Autowired |
| | | private IDevicePayloadService devicePayloadService; |
| | | |
| | | /** |
| | | * Handles messages in the state topic about basic drone data. |
| | | * |
| | | * Note: Only the data of the drone payload is handled here. You can handle other data from the drone |
| | | * according to your business needs. |
| | | * @param deviceBasic basic drone data |
| | | * @param headers |
| | | */ |
| | | // 机场1:不复制原 ServiceActivator(inputChannel = ChannelName.INBOUND_STATE_BASIC),消费职责由共享层原类承担 |
| | | public void stateBasic(DeviceBasicReceiver deviceBasic, MessageHeaders headers) { |
| | | // 由于收到的负载数据的payloadSn为null 而且同时收到2个 暂时无法处理 先注释掉 以后再说 |
| | | // devicePayloadService.saveDeviceBasicPayload(deviceBasic.getPayloads(), headers.getTimestamp()); |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * 【机场1】从 dock2 原类 com.ard.web.controller.sample.DockController 复制/独立适配(DJI Dock 1 / M30)。 |
| | | * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。 |
| | | */ |
| | | package com.ard.web.controller.sample.dock1; |
| | | |
| | | import com.ard.common.core.domain.model.LoginUser; |
| | | import com.ard.common.utils.SecurityUtils; |
| | | import com.ard.sample.common.model.ResponseResult; |
| | | import com.ard.sample.manage.model.param.RemoteDebugParam; |
| | | import com.ard.sample.dock1.manage.service.IControlService; |
| | | import io.swagger.annotations.Api; |
| | | import io.swagger.annotations.ApiOperation; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.web.bind.annotation.*; |
| | | import com.ard.web.controller.sample.DockController; |
| | | |
| | | /** |
| | | * @author sean |
| | | * @version 1.2 |
| | | * @date 2022/7/29 |
| | | */ |
| | | @Api(tags = "机场控制") |
| | | @RestController |
| | | @Slf4j |
| | | @RequestMapping("/dock1/devices") |
| | | public class Dock1DockController { |
| | | |
| | | @Autowired |
| | | private IControlService controlService; |
| | | |
| | | @ApiOperation("创建控制任务") |
| | | @PostMapping("/{sn}/jobs/{service_identifier}") |
| | | public ResponseResult createControlJob(@PathVariable String sn, |
| | | @PathVariable("service_identifier") String serviceIdentifier, |
| | | @RequestBody(required = false) RemoteDebugParam param) { |
| | | // 获取谁在调接口,后续可能要websocket发给前端在多路直播中显示 |
| | | //LoginUser loginUser = SecurityUtils.getLoginUser(); |
| | | return controlService.controlDock(sn, serviceIdentifier, param); |
| | | } |
| | | |
| | | } |
| New file |
| | |
| | | /* |
| | | * 【机场1】从 dock2 原类 com.ard.web.controller.sample.LiveStreamController 复制/独立适配(DJI Dock 1 / M30)。 |
| | | * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。 |
| | | */ |
| | | package com.ard.web.controller.sample.dock1; |
| | | |
| | | import com.ard.common.core.domain.entity.SysUser; |
| | | import com.ard.common.utils.SecurityUtils; |
| | | import com.ard.sample.common.model.ResponseResult; |
| | | import com.ard.sample.component.mqtt.model.ChannelName; |
| | | import com.ard.sample.component.websocket.handler.LiveStreamHandler; |
| | | import com.ard.sample.manage.model.dto.CapacityDeviceDTO; |
| | | import com.ard.sample.manage.model.dto.LiveTypeDTO; |
| | | import com.ard.sample.manage.model.param.RemoteDebugParam; |
| | | import com.ard.sample.manage.model.receiver.LiveCapacityReceiver; |
| | | import com.ard.sample.manage.model.receiver.LiveStatusTwoReceiver; |
| | | import com.ard.sample.manage.service.IDeviceService; |
| | | import com.ard.sample.dock1.manage.service.ILiveStreamService; |
| | | import com.fasterxml.jackson.databind.ObjectMapper; |
| | | import io.swagger.annotations.Api; |
| | | import io.swagger.annotations.ApiOperation; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.messaging.MessageHeaders; |
| | | import org.springframework.security.access.prepost.PreAuthorize; |
| | | import org.springframework.web.bind.annotation.*; |
| | | |
| | | import javax.servlet.http.HttpServletRequest; |
| | | import java.util.List; |
| | | import com.ard.web.controller.sample.LiveStreamController; |
| | | |
| | | /** |
| | | * @author sean.zhou |
| | | * @version 0.1 |
| | | * @date 2021/11/19 |
| | | */ |
| | | |
| | | @Api(tags = "直播功能") |
| | | @RestController |
| | | @Slf4j |
| | | @RequestMapping("/dock1/live") |
| | | public class Dock1LiveStreamController { |
| | | |
| | | @Autowired |
| | | private ILiveStreamService liveStreamService; |
| | | |
| | | @Autowired |
| | | private LiveStreamHandler _liveStreamHandler; |
| | | |
| | | @Autowired |
| | | private ObjectMapper mapper; |
| | | |
| | | /** |
| | | * Analyze the live streaming capabilities of drones. |
| | | * This data is necessary if drones are required for live streaming. |
| | | * |
| | | * @param liveCapacity the capacity of drone and dock |
| | | */ |
| | | // 机场1:不复制原 ServiceActivator(inputChannel = ChannelName.INBOUND_STATE_CAPACITY),消费职责由共享层原类承担 |
| | | public void stateCapacity(LiveCapacityReceiver liveCapacity, MessageHeaders headers) { |
| | | liveStreamService.saveLiveCapacity(liveCapacity, headers.getTimestamp()); |
| | | } |
| | | |
| | | |
| | | /** |
| | | * 处理摄像头返回状态 |
| | | * */ |
| | | // 机场1:不复制原 ServiceActivator(inputChannel = ChannelName.INBOUND_STATE_STATUS),消费职责由共享层原类承担 |
| | | public void LiveStateStatus(LiveStatusTwoReceiver liveReceiver, MessageHeaders headers) { |
| | | // log.info("摄像头返回状态通道INBOUND_STATE_STATUS:{}", liveReceiver); |
| | | } |
| | | |
| | | /** |
| | | * Get live capability data of all drones in the current user's workspace from the database. |
| | | * |
| | | * @param request |
| | | * @return live capability |
| | | */ |
| | | @ApiOperation(value = "获取直播能力数据", notes = "获取当前用户工作空间下所有无人机的直播能力数据") |
| | | // @PreAuthorize("@ss.hasPermi('dock:live:capacity')") |
| | | @GetMapping("/capacity") |
| | | public ResponseResult<List<CapacityDeviceDTO>> getLiveCapacity(HttpServletRequest request) { |
| | | // Get information about the current user. |
| | | List<CapacityDeviceDTO> liveCapacity = liveStreamService.getLiveCapacity(); |
| | | |
| | | return ResponseResult.success(liveCapacity); |
| | | } |
| | | |
| | | @ApiOperation(value = "获取sn设备的摄像头模式", notes = "获取sn设备的直播能力数据中摄像头的videotype数组") |
| | | @GetMapping("/videotype") |
| | | public ResponseResult<List<String>> getVideoType(String sn){ |
| | | return liveStreamService.getLiveLens(sn); |
| | | } |
| | | |
| | | @ApiOperation("获取video_id") |
| | | @PostMapping("/videoid/{sn}") |
| | | public ResponseResult<LiveTypeDTO> getVideoId(@PathVariable String sn) { |
| | | LiveTypeDTO liveTypeDTO = new LiveTypeDTO(); |
| | | try{ |
| | | liveTypeDTO = _liveStreamHandler.getLiveParam(sn); |
| | | |
| | | } |
| | | catch (Exception e){ |
| | | // videoId = e.getMessage(); |
| | | e.printStackTrace(); |
| | | } |
| | | return ResponseResult.success(liveTypeDTO); |
| | | } |
| | | |
| | | /** |
| | | * Live streaming according to the parameters passed in from the web side. |
| | | * |
| | | * @param liveParam Live streaming parameters. |
| | | * @return |
| | | */ |
| | | @ApiOperation(value = "开始直播", notes = "根据前端传入的参数开始直播") |
| | | // @PreAuthorize("@ss.hasPermi('dock:live:start')") |
| | | @PostMapping("/streams/start") |
| | | public ResponseResult liveStart(@RequestBody LiveTypeDTO liveParam) { |
| | | return liveStreamService.liveStart(liveParam); |
| | | } |
| | | |
| | | /** |
| | | * Stop live streaming according to the parameters passed in from the web side. |
| | | * |
| | | * @param liveParam Live streaming parameters. |
| | | * @return |
| | | */ |
| | | @ApiOperation(value = "停止直播", notes = "根据前端传入的参数停止直播") |
| | | // @PreAuthorize("@ss.hasPermi('dock:live:stop')") |
| | | @PostMapping("/streams/stop") |
| | | public ResponseResult liveStop(@RequestBody LiveTypeDTO liveParam) { |
| | | return liveStreamService.liveStop(liveParam.getVideoId()); |
| | | } |
| | | |
| | | /** |
| | | * Set the quality of the live streaming according to the parameters passed in from the web side. |
| | | * |
| | | * @param liveParam Live streaming parameters. |
| | | * @return |
| | | */ |
| | | @ApiOperation(value = "设置直播质量", notes = "根据前端传入的参数设置直播质量") |
| | | // @PreAuthorize("@ss.hasPermi('dock:live:quality')") |
| | | @PostMapping("/quality/switch") |
| | | public ResponseResult liveSetQuality(@RequestBody LiveTypeDTO liveParam) { |
| | | return liveStreamService.liveSetQuality(liveParam); |
| | | } |
| | | |
| | | @ApiOperation(value = "切换镜头", notes = "根据前端传入的参数切换镜头") |
| | | // @PreAuthorize("@ss.hasPermi('dock:live:lens')") |
| | | @PostMapping("/lens/switch") |
| | | public ResponseResult liveLensChange(@RequestBody LiveTypeDTO liveParam) { |
| | | return liveStreamService.liveLensChange(liveParam); |
| | | } |
| | | |
| | | @ApiOperation(value = "机场直播相机切换", notes = "根据前端传入的参数切换相机") |
| | | // @PreAuthorize("@ss.hasPermi('dock:live:camera')") |
| | | @PostMapping("/camera/switch") |
| | | public ResponseResult liveCameraChange(@RequestBody LiveTypeDTO liveParam) { |
| | | return liveStreamService.liveCameraChange(liveParam); |
| | | } |
| | | |
| | | } |
| New file |
| | |
| | | /* |
| | | * 【机场1】从 dock2 原类 com.ard.web.controller.sample.TopologyController 复制/独立适配(DJI Dock 1 / M30)。 |
| | | * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。 |
| | | */ |
| | | package com.ard.web.controller.sample.dock1; |
| | | |
| | | import com.ard.sample.common.model.ResponseResult; |
| | | import com.ard.sample.manage.model.dto.TopologyDTO; |
| | | import com.ard.sample.manage.service.ITopologyService; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.web.bind.annotation.GetMapping; |
| | | import org.springframework.web.bind.annotation.PathVariable; |
| | | import org.springframework.web.bind.annotation.RequestMapping; |
| | | import org.springframework.web.bind.annotation.RestController; |
| | | |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.concurrent.ConcurrentHashMap; |
| | | import com.ard.web.controller.sample.TopologyController; |
| | | |
| | | /** |
| | | * @author sean |
| | | * @version 0.2 |
| | | * @date 2021/12/8 |
| | | */ |
| | | @RestController |
| | | @RequestMapping("/dock1/workspaces") |
| | | public class Dock1TopologyController { |
| | | |
| | | @Autowired |
| | | private ITopologyService topologyService; |
| | | |
| | | /** |
| | | * Get the topology list of all devices in the current user workspace for pilot display. |
| | | * @param workspaceId |
| | | * @return |
| | | */ |
| | | @GetMapping("/{workspace_id}/devices/topologies") |
| | | public ResponseResult<Map<String, List<TopologyDTO>>> getDevicesTopologiesForPilot( |
| | | @PathVariable(name = "workspace_id") String workspaceId) { |
| | | List<TopologyDTO> topologyList = topologyService.getDeviceTopology(workspaceId); |
| | | return ResponseResult.success(new ConcurrentHashMap<>(Map.of("list", topologyList))); |
| | | } |
| | | |
| | | } |
| New file |
| | |
| | | /* |
| | | * 【机场1】从 dock2 原类 com.ard.web.controller.sample.media.FileController 复制/独立适配(DJI Dock 1 / M30)。 |
| | | * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。 |
| | | */ |
| | | package com.ard.web.controller.sample.dock1.media; |
| | | |
| | | import com.ard.sample.common.model.PaginationData; |
| | | import com.ard.sample.common.model.ResponseResult; |
| | | import com.ard.sample.media.model.MediaFileDTO; |
| | | import com.ard.sample.media.service.IFileService; |
| | | import io.swagger.annotations.Api; |
| | | import io.swagger.annotations.ApiOperation; |
| | | import io.swagger.annotations.ApiParam; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.web.bind.annotation.*; |
| | | |
| | | import javax.servlet.http.HttpServletResponse; |
| | | import java.io.IOException; |
| | | import java.net.URL; |
| | | import com.ard.web.controller.sample.media.FileController; |
| | | |
| | | /** |
| | | * @author sean |
| | | * @version 0.2 |
| | | * @date 2021/12/9 |
| | | */ |
| | | @Api(tags = "媒体文件管理") |
| | | @RestController |
| | | @RequestMapping("/dock1/media/files") |
| | | public class Dock1FileController { |
| | | |
| | | @Autowired |
| | | private IFileService fileService; |
| | | |
| | | /** |
| | | * Get information about all the media files in this workspace based on the workspace id. |
| | | * @return |
| | | */ |
| | | @ApiOperation("获取所有媒体文件列表") |
| | | @GetMapping("/files") |
| | | public ResponseResult<PaginationData<MediaFileDTO>> getFilesList(@RequestParam(defaultValue = "1") Long page, |
| | | @RequestParam(name = "page_size", defaultValue = "10") Long pageSize, |
| | | @ApiParam(value = "任务ID,筛选指定任务下的文件", required = false) |
| | | @RequestParam(name = "job_id", required = false) String jobId, |
| | | @ApiParam(value = "开始时间", required = false) |
| | | @RequestParam(name = "start_time", required = false) Long startTime, |
| | | @ApiParam(value = "结束时间", required = false) |
| | | @RequestParam(name = "end_time", required = false) Long endTime) { |
| | | PaginationData<MediaFileDTO> filesList = fileService.getMediaFilesPagination(page, pageSize, jobId, startTime, endTime); |
| | | return ResponseResult.success(filesList); |
| | | } |
| | | |
| | | /** |
| | | * Query the download address of the file according to the media file id, |
| | | * and redirect to this address directly for download. |
| | | * @param fileId |
| | | * @param response |
| | | */ |
| | | @ApiOperation("根据id获取媒体文件地址") |
| | | @GetMapping("/file/{file_id}/url") |
| | | public void getFileUrl(@PathVariable(name = "file_id") String fileId, HttpServletResponse response) { |
| | | |
| | | try { |
| | | URL url = fileService.getObjectUrl(fileId); |
| | | response.sendRedirect(url.toString()); |
| | | } catch (IOException e) { |
| | | e.printStackTrace(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 根据任务id获取全部媒体库文件列表 |
| | | * |
| | | * @param page 页码,默认值为1,从1开始计数 |
| | | * @param pageSize 每页条数,默认值为10,参数名映射为page_size(前端传参用) |
| | | * @param jobId 任务ID,必填参数,用于筛选指定任务下的媒体文件 |
| | | * @param isType 文件类型筛选标识,非必填(若需必填可加required = true): |
| | | * 1 - 筛选图片文件(按 .jpeg/.jpg 扩展名,兼容各机型镜头前缀 W/Z/T/V); |
| | | * 0 - 筛选视频文件(按 .mp4 扩展名) |
| | | * @return 分页的媒体文件列表数据 |
| | | * */ |
| | | @ApiOperation("获取任务中媒体文件列表") |
| | | @GetMapping("/jobfiles") |
| | | public ResponseResult<PaginationData<MediaFileDTO>> getJobFiles(@ApiParam(value = "页码,默认1", defaultValue = "1") |
| | | @RequestParam(defaultValue = "1") Long page, |
| | | @ApiParam(value = "每页条数,默认10", defaultValue = "10") |
| | | @RequestParam(name = "page_size", defaultValue = "10") Long pageSize, |
| | | @ApiParam(value = "任务ID(必填),筛选指定任务下的文件", required = true) |
| | | @RequestParam(name = "job_id") String jobId, |
| | | @ApiParam(value = "文件类型筛选:1=图片文件,0=视频文件") |
| | | @RequestParam(name = "isType") String isType) { |
| | | PaginationData<MediaFileDTO> filesList = fileService.getJobFiles(page, pageSize, jobId, isType); |
| | | return ResponseResult.success(filesList); |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * 【机场1】从 dock2 原类 com.ard.web.controller.sample.media.MediaController 复制/独立适配(DJI Dock 1 / M30)。 |
| | | * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。 |
| | | */ |
| | | package com.ard.web.controller.sample.dock1.media; |
| | | |
| | | import com.ard.sample.common.model.ResponseResult; |
| | | import com.ard.sample.component.mqtt.model.MapKeyConst; |
| | | import com.ard.sample.media.model.FileUploadDTO; |
| | | import com.ard.sample.media.service.IMediaService; |
| | | import com.fasterxml.jackson.core.JsonProcessingException; |
| | | import io.swagger.annotations.Api; |
| | | import io.swagger.annotations.ApiOperation; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.web.bind.annotation.*; |
| | | |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.concurrent.ConcurrentHashMap; |
| | | import com.ard.web.controller.sample.media.MediaController; |
| | | |
| | | /** |
| | | * @author sean/workspaces |
| | | * @version 0.2 |
| | | * @date 2021/12/7 |
| | | */ |
| | | @Api(tags = "媒体库管理") |
| | | @Slf4j |
| | | @RestController |
| | | @RequestMapping("/dock1/media") |
| | | public class Dock1MediaController { |
| | | |
| | | @Autowired |
| | | private IMediaService mediaService; |
| | | |
| | | /** |
| | | * Check if the file has been uploaded by the fingerprint. |
| | | * @param file |
| | | * @return |
| | | */ |
| | | @ApiOperation("检查文件是否已上传") |
| | | @PostMapping("/fast-upload") |
| | | public ResponseResult fastUpload( @RequestBody FileUploadDTO file) { |
| | | |
| | | boolean isExist = mediaService.fastUpload(file.getFingerprint()); |
| | | |
| | | return isExist ? ResponseResult.success() : ResponseResult.error(file.getFingerprint() + "don't exist."); |
| | | } |
| | | |
| | | /** |
| | | * When the file is uploaded to the storage server by pilot, |
| | | * the basic information of the file is reported through this interface. |
| | | * @param file |
| | | * @return |
| | | */ |
| | | @ApiOperation("上报文件基本信息") |
| | | @PostMapping("/upload-callback") |
| | | public ResponseResult<String> uploadCallback(@RequestBody FileUploadDTO file) { |
| | | mediaService.saveMediaFile(file); |
| | | return ResponseResult.success(file.getObjectKey()); |
| | | |
| | | } |
| | | |
| | | /** |
| | | * 根据工作区 ID 和微小指纹的集合查询此工作区中已存在的文件。 |
| | | * @param tinyFingerprints There is only one tiny_fingerprint parameter in the body. |
| | | * But it is not recommended to use Map to receive the parameter. |
| | | * @return |
| | | */ |
| | | @ApiOperation("查询已存在的文件") |
| | | @PostMapping("/files/tiny-fingerprints") |
| | | public ResponseResult<Map<String, List<String>>> uploadCallback( |
| | | @RequestBody Map<String, List<String>> tinyFingerprints){ |
| | | |
| | | List<String> existingList = mediaService.getExistTinyFingerprints(tinyFingerprints.get(MapKeyConst.TINY_FINGERPRINTS)); |
| | | return ResponseResult.success(new ConcurrentHashMap<>(Map.of(MapKeyConst.TINY_FINGERPRINTS, existingList))); |
| | | } |
| | | |
| | | } |
| New file |
| | |
| | | /* |
| | | * 【机场1】从 dock2 原类 com.ard.web.controller.sample.wayline.WaylineFileController 复制/独立适配(DJI Dock 1 / M30)。 |
| | | * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。 |
| | | */ |
| | | package com.ard.web.controller.sample.dock1.wayline; |
| | | |
| | | import com.ard.common.constant.HttpStatus; |
| | | import com.ard.common.core.controller.BaseController; |
| | | import com.ard.common.core.domain.model.LoginUser; |
| | | import com.ard.common.core.page.TableDataInfo; |
| | | import com.ard.common.utils.SecurityUtils; |
| | | import com.ard.sample.common.model.PaginationData; |
| | | import com.ard.sample.common.model.ResponseResult; |
| | | import com.ard.sample.wayline.model.dto.WaylineFileDTO; |
| | | import com.ard.sample.wayline.model.dto.WaylineFileUploadDTO; |
| | | import com.ard.sample.wayline.model.param.WaylineQueryParam; |
| | | import com.ard.sample.dock1.wayline.service.IWaylineFileService; |
| | | import io.swagger.annotations.Api; |
| | | import io.swagger.annotations.ApiOperation; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.web.bind.annotation.*; |
| | | import org.springframework.web.multipart.MultipartFile; |
| | | |
| | | import javax.servlet.http.HttpServletResponse; |
| | | import java.io.IOException; |
| | | import java.net.URL; |
| | | import java.sql.SQLException; |
| | | import java.util.List; |
| | | import java.util.Objects; |
| | | import com.ard.web.controller.sample.wayline.WaylineFileController; |
| | | |
| | | /** |
| | | * @author sean |
| | | * @version 0.3 |
| | | * @date 2021/12/22 |
| | | */ |
| | | @Api(tags="航线管理") |
| | | @RestController |
| | | @RequestMapping("/dock1/wayline") |
| | | public class Dock1WaylineFileController extends BaseController { |
| | | |
| | | @Autowired |
| | | private IWaylineFileService waylineFileService; |
| | | |
| | | /** |
| | | * Query the basic data of the wayline file according to the query conditions. |
| | | * The query condition field in pilot is fixed. |
| | | * @return |
| | | */ |
| | | @ApiOperation("查询航线列表") |
| | | @GetMapping("/list") |
| | | public TableDataInfo getWaylinesPagination(WaylineQueryParam param) { |
| | | |
| | | PaginationData<WaylineFileDTO> data = waylineFileService.getWaylinesByParam(param); |
| | | |
| | | TableDataInfo rspData = new TableDataInfo(); |
| | | rspData.setCode(HttpStatus.SUCCESS); |
| | | rspData.setMsg("查询成功"); |
| | | rspData.setRows(data.getList()); |
| | | rspData.setTotal(data.getPagination().getTotal()); |
| | | return rspData; |
| | | } |
| | | |
| | | /** |
| | | * Query the download address of the file according to the wayline file id, |
| | | * and redirect to this address directly for download. |
| | | * @param waylineId |
| | | * @param response |
| | | */ |
| | | @ApiOperation("下载航线文件") |
| | | @GetMapping("/download") |
| | | public void getFileUrl(String waylineId, HttpServletResponse response) { |
| | | try { |
| | | URL url = waylineFileService.getObjectUrl(waylineId); |
| | | response.sendRedirect(url.toString()); |
| | | |
| | | } catch (IOException | SQLException e) { |
| | | e.printStackTrace(); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * When the wayline file is uploaded to the storage server by pilot, |
| | | * the basic information of the file is reported through this interface. |
| | | * @param uploadFile |
| | | * @return |
| | | */ |
| | | @ApiOperation("上传航线") |
| | | @PostMapping("/upload") |
| | | public ResponseResult uploadCallBack(@RequestBody WaylineFileUploadDTO uploadFile) { |
| | | |
| | | LoginUser loginUser = SecurityUtils.getLoginUser(); |
| | | WaylineFileDTO metadata = uploadFile.getMetadata(); |
| | | metadata.setUsername(loginUser.getUsername()); |
| | | metadata.setObjectKey(uploadFile.getObjectKey()); |
| | | metadata.setName(uploadFile.getName()); |
| | | |
| | | int id = waylineFileService.saveWaylineFile(metadata); |
| | | |
| | | return id <= 0 ? ResponseResult.error() : ResponseResult.success(); |
| | | } |
| | | |
| | | /** |
| | | * Favorite the wayline file according to the wayline file id. |
| | | * @param ids wayline file id |
| | | * @return |
| | | */ |
| | | @ApiOperation("收藏航线") |
| | | @PostMapping("/favorites") |
| | | public ResponseResult markFavorite(@RequestParam(name = "id") List<String> ids) { |
| | | boolean isMark = waylineFileService.markFavorite(ids, true); |
| | | |
| | | return isMark ? ResponseResult.success() : ResponseResult.error(); |
| | | } |
| | | |
| | | /** |
| | | * Delete the favorites of this wayline file based on the wayline file id. |
| | | * @param ids wayline file id |
| | | * @return |
| | | */ |
| | | @ApiOperation("取消收藏航线") |
| | | @DeleteMapping("/favorites") |
| | | public ResponseResult unmarkFavorite(@RequestParam(name = "id") List<String> ids) { |
| | | boolean isMark = waylineFileService.markFavorite(ids, false); |
| | | |
| | | return isMark ? ResponseResult.success() : ResponseResult.error(); |
| | | } |
| | | |
| | | /** |
| | | * Checking whether the name already exists according to the wayline name must ensure the uniqueness of the wayline name. |
| | | * This interface will be called when uploading waylines and must be available. |
| | | * @param names |
| | | * @return |
| | | */ |
| | | @ApiOperation("检查航线名称是否重复") |
| | | @GetMapping("/checkDuplicateNames") |
| | | public ResponseResult checkDuplicateNames(@RequestParam(name = "name") List<String> names) { |
| | | List<String> existNamesList = waylineFileService.getDuplicateNames(names); |
| | | return ResponseResult.success(existNamesList); |
| | | } |
| | | |
| | | /** |
| | | * Delete the wayline file in the workspace according to the wayline id. |
| | | * @param waylineId |
| | | * @return |
| | | */ |
| | | @ApiOperation("删除航线") |
| | | @DeleteMapping("/delete/{wayline_id}") |
| | | public ResponseResult deleteWayline(@PathVariable(name = "wayline_id") String waylineId) { |
| | | boolean isDel = waylineFileService.deleteByWaylineId(waylineId); |
| | | return isDel ? ResponseResult.success() : ResponseResult.error("Failed to delete wayline."); |
| | | } |
| | | |
| | | /** |
| | | * Import kmz wayline files. |
| | | * @param file |
| | | * @return |
| | | */ |
| | | @ApiOperation("导入航线") |
| | | @PostMapping("/importKmzFile") |
| | | public ResponseResult importKmzFile(@RequestPart("file") MultipartFile file) { |
| | | if (Objects.isNull(file)) { |
| | | return ResponseResult.error("No file received."); |
| | | } |
| | | LoginUser loginUser = SecurityUtils.getLoginUser(); |
| | | String creator = loginUser.getUsername(); |
| | | waylineFileService.importKmzFile(file, creator); |
| | | return ResponseResult.success(); |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * 【机场1】从 dock2 原类 com.ard.web.controller.sample.wayline.WaylineJobController 复制/独立适配(DJI Dock 1 / M30)。 |
| | | * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。 |
| | | */ |
| | | package com.ard.web.controller.sample.dock1.wayline; |
| | | |
| | | import com.ard.common.core.domain.model.LoginUser; |
| | | import com.ard.common.utils.SecurityUtils; |
| | | import com.ard.sample.common.model.PaginationData; |
| | | import com.ard.sample.common.model.ResponseResult; |
| | | import com.ard.sample.wayline.model.dto.WaylineJobDTO; |
| | | import com.ard.sample.wayline.model.param.CreateJobParam; |
| | | import com.ard.sample.dock1.wayline.service.IWaylineJobService; |
| | | import io.swagger.annotations.Api; |
| | | import io.swagger.annotations.ApiOperation; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.web.bind.annotation.*; |
| | | import javax.validation.Valid; |
| | | import java.sql.SQLException; |
| | | import java.util.Optional; |
| | | import java.util.Set; |
| | | import com.ard.web.controller.sample.wayline.WaylineJobController; |
| | | |
| | | /** |
| | | * @author sean |
| | | * @version 1.1 |
| | | * @date 2022/6/1 |
| | | */ |
| | | @Api(tags = "航线任务") |
| | | @RequestMapping("/dock1/wayline") |
| | | @RestController |
| | | public class Dock1WaylineJobController { |
| | | |
| | | @Autowired |
| | | private IWaylineJobService waylineJobService; |
| | | |
| | | /** |
| | | * Create a wayline task for the Dock. |
| | | * @param param |
| | | * @return |
| | | * @throws SQLException |
| | | */ |
| | | @ApiOperation("创建航线任务") |
| | | @PostMapping("/flight-tasks") |
| | | public ResponseResult createJob(@Valid @RequestBody CreateJobParam param) throws SQLException { |
| | | LoginUser loginUser = SecurityUtils.getLoginUser(); |
| | | return waylineJobService.publishFlightTask(param, loginUser); |
| | | } |
| | | |
| | | @ApiOperation("获取航线任务") |
| | | @GetMapping("/flight-id") |
| | | public ResponseResult<WaylineJobDTO> getJob(@RequestParam(name = "job_id") String jobId) { |
| | | try { |
| | | if(jobId == null){ |
| | | return ResponseResult.error(98,"任务ID不能为空"); |
| | | } |
| | | Optional<WaylineJobDTO> waylineJobDTO = waylineJobService.getJobByJobId(jobId); |
| | | if(waylineJobDTO.isEmpty()) { |
| | | return ResponseResult.error(99,"任务不存在"); |
| | | } |
| | | return ResponseResult.success(waylineJobDTO.get()); |
| | | } catch (Exception e) { |
| | | return ResponseResult.error(e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Paginate through all jobs. |
| | | * @param page |
| | | * @param pageSize |
| | | * @return |
| | | */ |
| | | @ApiOperation("航线任务列表") |
| | | @GetMapping("/jobs") |
| | | public ResponseResult<PaginationData<WaylineJobDTO>> getJobs(@RequestParam(defaultValue = "1") Long page, |
| | | @RequestParam(name = "page_size", defaultValue = "10") Long pageSize, |
| | | @RequestParam(required = false) Integer status, |
| | | @RequestParam(required = false) Long startTime, |
| | | @RequestParam(required = false) Long endTime, |
| | | @RequestParam(required = false) Integer taskType, |
| | | @RequestParam(required = false) String name, |
| | | @RequestParam(required = false) Boolean isComplete) { |
| | | try { |
| | | // PaginationData<WaylineJobDTO> data = waylineJobService.getJobsByWorkspaceId(page, pageSize);defaultValue |
| | | PaginationData<WaylineJobDTO> data = waylineJobService.getJobsList(page, pageSize, status, startTime, endTime, taskType, name, isComplete); |
| | | return ResponseResult.success(data); |
| | | } |
| | | catch (Exception e) { |
| | | return ResponseResult.error(e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Send the command to cancel the jobs. |
| | | * @param jobIds |
| | | * @return |
| | | * @throws SQLException |
| | | */ |
| | | @ApiOperation("取消航线任务") |
| | | @DeleteMapping("/jobs") |
| | | public ResponseResult publishCancelJob(@RequestParam(name = "job_id") Set<String> jobIds){ |
| | | return waylineJobService.cancelFlightTask(jobIds); |
| | | } |
| | | |
| | | /** |
| | | * Set the media files for this job to upload immediately. |
| | | * @param jobId |
| | | * @return |
| | | */ |
| | | @ApiOperation("设置航线任务上传最高优先级") |
| | | @PostMapping("/{workspace_id}/jobs/{job_id}/media-highest") |
| | | public ResponseResult uploadMediaHighestPriority(@PathVariable(name = "job_id") String jobId) { |
| | | waylineJobService.uploadMediaHighestPriority(jobId); |
| | | return ResponseResult.success(); |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * 【机场1】从 dock2 原类 com.ard.web.controller.sample.wayline.WaylineTrajectoryController 复制/独立适配(DJI Dock 1 / M30)。 |
| | | * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。 |
| | | */ |
| | | package com.ard.web.controller.sample.dock1.wayline; |
| | | |
| | | import com.ard.sample.common.model.ResponseResult; |
| | | import com.ard.sample.manage.model.dto.WaylineTrajectoryDTO; |
| | | // 机场1:与 OSD 轨迹写入共用同一个轨迹服务实例,保证 job_id 能查到完整轨迹。 |
| | | import com.ard.sample.manage.service.IWaylineTrajectoryService; |
| | | import io.swagger.annotations.Api; |
| | | import io.swagger.annotations.ApiOperation; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.web.bind.annotation.GetMapping; |
| | | import org.springframework.web.bind.annotation.RequestMapping; |
| | | import org.springframework.web.bind.annotation.RequestParam; |
| | | import org.springframework.web.bind.annotation.RestController; |
| | | |
| | | import java.util.Optional; |
| | | import com.ard.web.controller.sample.wayline.WaylineTrajectoryController; |
| | | |
| | | @Api(tags = "航线历史任务轨迹") |
| | | @RequestMapping("/dock1/waylineTrajectory") |
| | | @RestController |
| | | public class Dock1WaylineTrajectoryController { |
| | | @Autowired |
| | | private IWaylineTrajectoryService _waylineTrajectoryService; |
| | | |
| | | @ApiOperation("获取航线任务轨迹") |
| | | @GetMapping("/flight-id") |
| | | public ResponseResult<WaylineTrajectoryDTO> getJob(@RequestParam(name = "job_id") String jobId) { |
| | | try { |
| | | if(jobId == null){ |
| | | return ResponseResult.error(98,"任务ID不能为空"); |
| | | } |
| | | WaylineTrajectoryDTO waylineJobDTO = _waylineTrajectoryService.getWayLineTrajectorybyJobID(jobId); |
| | | if(waylineJobDTO==null) { |
| | | return ResponseResult.error(99,"任务不存在"); |
| | | } |
| | | return ResponseResult.success(waylineJobDTO); |
| | | } catch (Exception e) { |
| | | return ResponseResult.error(97, "获取航线轨迹发生异常:"+e.getMessage()); |
| | | } |
| | | } |
| | | } |
| | | |
| | |
| | | * @param pageSize 每页条数,默认值为10,参数名映射为page_size(前端传参用) |
| | | * @param jobId 任务ID,必填参数,用于筛选指定任务下的媒体文件 |
| | | * @param isType 文件类型筛选标识,非必填(若需必填可加required = true): |
| | | * 1 - 筛选文件名包含V.jpeg的文件; |
| | | * 0 - 筛选文件名包含V.mp4的文件 |
| | | * 1 - 筛选图片文件(按 .jpeg/.jpg 扩展名,兼容各机型镜头前缀 W/Z/T/V); |
| | | * 0 - 筛选视频文件(按 .mp4 扩展名) |
| | | * @return 分页的媒体文件列表数据 |
| | | * */ |
| | | @ApiOperation("获取任务中媒体文件列表") |
| | |
| | | @RequestParam(name = "page_size", defaultValue = "10") Long pageSize, |
| | | @ApiParam(value = "任务ID(必填),筛选指定任务下的文件", required = true) |
| | | @RequestParam(name = "job_id") String jobId, |
| | | @ApiParam(value = "文件类型筛选:1=V.jpeg文件,0=V.mp4文件") |
| | | @ApiParam(value = "文件类型筛选:1=图片文件,0=视频文件") |
| | | @RequestParam(name = "isType") String isType) { |
| | | PaginationData<MediaFileDTO> filesList = fileService.getJobFiles(page, pageSize, jobId, isType); |
| | | return ResponseResult.success(filesList); |
| | |
| | | druid: |
| | | # 主库数据源 |
| | | master: |
| | | url: jdbc:mysql://192.168.10.139:3306/ard-uav?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 |
| | | # url: jdbc:mysql://10.67.52.22:3306/ard-uav?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 |
| | | # username: root |
| | | # password: Ard12345* |
| | | #四厂数据源 |
| | | url: jdbc:mysql://10.67.52.22:3306/ard-uav-inspection?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 |
| | | username: root |
| | | password: Ard12345* |
| | | password: Uh09100802* |
| | | # 从库数据源 |
| | | slave: |
| | | # 从数据源开关/默认关闭 |
| | |
| | | id: 143640 |
| | | key: da5babf38a5d05b0f46a7d82910da1b |
| | | license: khjd7FzUTzJgaArNv97XdA2nKQtYfESOvBHLqjC6MpvPqxCfuQ82nOlTgzoiLp7UoZujARopN2A1ctPFaJ8oHiHQrn3IRV4wqN3hdSM40xXj02YVvTWQBT54WS8Ks+E8Zatp3gk2T0+7cM5a5a3mWzxLMv7CNqw2z9pFBoOBdpY= |
| | | host: 192.168.10.139 |
| | | host: 10.67.52.22 |
| | | port: 123 |
| | | |
| | | #流媒体 |
| | | mediamtx: |
| | | apiHost: 192.168.10.139 |
| | | playHost: 192.168.10.139 |
| | | apiHost: 10.67.52.22 |
| | | playHost: 10.67.52.22 |
| | | https: true |
| | | pushHost: 192.168.10.139 |
| | | pushHost: 10.67.52.22 |
| | | rtmpPort: 1935 |
| | | rtspPort: 8554 |
| | | webrtcPort: 8889 |
| | |
| | | # mqtt配置 |
| | | mqtt: |
| | | protocol: tcp |
| | | host: 192.168.10.139 |
| | | host: 10.67.52.22 |
| | | port: 1883 |
| | | username: admin |
| | | password: public |
| | |
| | | oss: |
| | | enable: true |
| | | provider: minio |
| | | endpoint: https://192.168.10.139:9001 |
| | | endpoint: https://10.67.52.22:9001 |
| | | access-key: admin |
| | | secret-key: ard12345 |
| | | bucket: cloud-bucket |
| | |
| | | |
| | | # 机场单独用的oss配置,要http地址 |
| | | dock: |
| | | endpoint: http://192.168.10.139:9011 |
| | | endpoint: http://10.67.52.22:9011 |
| | | bucket: dockfile |
| | | |
| | | # 点云文件夹路径配置 |
| | | pointCloud: |
| | | url : https://192.168.10.139:3001/tileset/ |
| | | url : https://10.67.52.22:3001/tileset/ |
| | |
| | | @XStreamAlias("wpml:payloadEnumValue") |
| | | private String payloadEnumValue; |
| | | |
| | | /** |
| | | * 负载子类型。机场1(M30/M30T)生成 KMZ 时需要该字段,其余机型保持原输出。 |
| | | */ |
| | | @XStreamAlias("wpml:payloadSubEnumValue") |
| | | private String payloadSubEnumValue; |
| | | |
| | | @XStreamAlias("wpml:payloadPositionIndex") |
| | | private String payloadPositionIndex; |
| | | |
| | |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.web.multipart.MultipartFile; |
| | | import javax.annotation.Resource; |
| | | import static com.ard.route.enums.kml.DroneEnumValueEnums.M30_M30T; |
| | | import static com.ard.route.enums.kml.DroneEnumValueEnums.M350_RTK; |
| | | import static com.ard.route.util.InitializationUtils.divided23; |
| | | import static com.ard.route.util.ToolUtils.*; |
| | |
| | | }else { |
| | | ardRouteInfo.getUavRouteReq().setHeightMode("relativeToStartPoint"); |
| | | } |
| | | ardRouteInfo.getUavRouteReq().setDroneType(subStringDroneType(ardRouteInfo.getAircraft())); |
| | | KmzResultVo kmzResult = routeService.buildKmz(ardRouteInfo.getUavRouteReq(),ardRouteInfo.getName()); |
| | | UavRouteReq uavRouteReq = ardRouteInfo.getUavRouteReq(); |
| | | uavRouteReq.setDroneType(subStringDroneType(ardRouteInfo.getAircraft())); |
| | | // 机场1:M30/M30T 需保留 aircraft 中的子类型(0-67-0 / 0-67-1), |
| | | // 否则 M30T 会按 M30 生成 KMZ(payload 52 而非 53) |
| | | if (M30_M30T.getValue().equals(uavRouteReq.getDroneType())) { |
| | | uavRouteReq.setSubDroneType(subStringSubDroneType(ardRouteInfo.getAircraft())); |
| | | } |
| | | KmzResultVo kmzResult = routeService.buildKmz(uavRouteReq,ardRouteInfo.getName()); |
| | | zoomProcessing(ardRouteInfo); |
| | | ardRouteInfo.setPath(kmzResult.getPath()); |
| | | ardRouteInfo.setReadOnly("0"); |
| | |
| | | // 先删除航线 |
| | | delOssService(route); |
| | | // 再生成航线 |
| | | ardRouteInfo.getUavRouteReq().setDroneType(subStringDroneType(ardRouteInfo.getAircraft())); |
| | | UavRouteReq uavRouteReq = ardRouteInfo.getUavRouteReq(); |
| | | uavRouteReq.setDroneType(subStringDroneType(ardRouteInfo.getAircraft())); |
| | | // 机场1:M30T(0-67-1) 需同步子类型,避免编辑后按 M30 重新生成 KMZ |
| | | if (M30_M30T.getValue().equals(uavRouteReq.getDroneType())) { |
| | | uavRouteReq.setSubDroneType(subStringSubDroneType(ardRouteInfo.getAircraft())); |
| | | } |
| | | zoomProcessing(ardRouteInfo); |
| | | kmzResult = routeService.buildKmz(ardRouteInfo.getUavRouteReq(), ardRouteInfo.getName()); |
| | | kmzResult = routeService.buildKmz(uavRouteReq, ardRouteInfo.getName()); |
| | | if ("更新航线kmz文件失败".equals(kmzResult.getData())) { |
| | | return 0; |
| | | } |
| | |
| | | import com.ard.route.domain.sixhe.ArdRouteActions; |
| | | import com.ard.route.domain.sixhe.ArdRouteChange; |
| | | import com.ard.route.domain.sixhe.ArdRoutePoints; |
| | | import com.ard.route.enums.kml.DroneEnumValueEnums; |
| | | import com.ard.route.enums.kml.PayloadTypeEnums; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.List; |
| | | import java.util.Objects; |
| | | |
| | | import static com.ard.route.enums.kml.DroneEnumValueEnums.M350_RTK; |
| | | import static com.ard.route.util.ToolUtils.getDoublePoint; |
| | |
| | | uavRouteReq.getRoutePointList().get(0).getHeight()); |
| | | } |
| | | //uavRouteReq.setDroneType(89); |
| | | // 机场1:M30/M30T 使用机身自带双光/三光相机,payloadEnumValue 分别为 52/53, |
| | | // 不能沿用 H20T(43)。其余机型保持原有逻辑,机场2/3 的航线生成结果不变。 |
| | | if (Objects.equals(DroneEnumValueEnums.M30_M30T.getValue(), uavRouteReq.getDroneType())) { |
| | | if (uavRouteReq.getSubDroneType() == null) { |
| | | uavRouteReq.setSubDroneType(0); |
| | | } |
| | | uavRouteReq.setPayloadType(Objects.equals(1, uavRouteReq.getSubDroneType()) |
| | | ? PayloadTypeEnums.M30T.getValue() : PayloadTypeEnums.M30.getValue()); |
| | | } else { |
| | | uavRouteReq.setSubDroneType(0); |
| | | uavRouteReq.setPayloadType(43); |
| | | } |
| | | uavRouteReq.setPayloadPosition(0); |
| | | uavRouteReq.setImageFormat("wide"); |
| | | uavRouteReq.setFinishAction("goHome"); |
| | |
| | | public static KmlPayloadInfo buildKmlPayloadInfo(Integer payloadType, Integer payloadPosition) { |
| | | KmlPayloadInfo kmlPayloadInfo = new KmlPayloadInfo(); |
| | | kmlPayloadInfo.setPayloadEnumValue(String.valueOf(payloadType)); |
| | | // 机场1:M30/M30T 的 WPML 需要 payloadSubEnumValue,否则机场侧校验可能不通过; |
| | | // 其余机型保持原有输出,不影响机场2/3。 |
| | | if (Objects.equals(PayloadTypeEnums.M30.getValue(), payloadType) |
| | | || Objects.equals(PayloadTypeEnums.M30T.getValue(), payloadType)) { |
| | | kmlPayloadInfo.setPayloadSubEnumValue("0"); |
| | | } |
| | | kmlPayloadInfo.setPayloadPositionIndex(String.valueOf(payloadPosition)); |
| | | return kmlPayloadInfo; |
| | | } |
| | |
| | | |
| | | import com.ard.inspection.domain.ArdPointCloud; |
| | | import com.ard.route.domain.djuav.UavRouteReq; |
| | | import com.ard.route.enums.kml.DroneEnumValueEnums; |
| | | import java.io.*; |
| | | import java.time.LocalDateTime; |
| | | import java.time.format.DateTimeFormatter; |
| | | import java.util.List; |
| | | import java.util.Objects; |
| | | |
| | | /** |
| | | * 工具类 |
| | |
| | | } |
| | | |
| | | /** |
| | | * 截取机型子类型 |
| | | * 参数: "0-67-1" 返回 1;格式不足三段时返回 0 |
| | | */ |
| | | public static Integer subStringSubDroneType(String str) { |
| | | String[] parts = str.split("-"); |
| | | return parts.length > 2 ? Integer.valueOf(parts[2]) : 0; |
| | | } |
| | | |
| | | /** |
| | | * 截取字符串 |
| | | * 获取经纬度高度 |
| | | */ |
| | |
| | | * 获取无人机类型方法 |
| | | */ |
| | | public static String droneTypeData(UavRouteReq uavRouteReq) { |
| | | // 机场1:M30(67-0) / M30T(67-1) 需要保留 subType,避免航线记录的 aircraft 都变成 0-67-0。 |
| | | // 其它机型保持原有返回值,机场2/3 不受影响。 |
| | | if (Objects.equals(DroneEnumValueEnums.M30_M30T.getValue(), uavRouteReq.getDroneType())) { |
| | | return "0-" + uavRouteReq.getDroneType() + "-" |
| | | + (uavRouteReq.getSubDroneType() == null ? 0 : uavRouteReq.getSubDroneType()); |
| | | } |
| | | return "0-" + uavRouteReq.getDroneType() + "-0"; |
| | | } |
| | | |
| | |
| | | if (vo.getKmlInfo().getDocument().getFolder().getTemplateType() != null){ |
| | | uavRouteReq.setTemplateType(vo.getKmlInfo().getDocument().getFolder().getTemplateType()); |
| | | } |
| | | uavRouteReq.setDroneType(Integer.valueOf(vo.getKmlInfo().getDocument().getKmlMissionConfig().getDroneInfo().getDroneEnumValue())); |
| | | KmlDroneInfo droneInfo = vo.getKmlInfo().getDocument().getKmlMissionConfig().getDroneInfo(); |
| | | uavRouteReq.setDroneType(Integer.valueOf(droneInfo.getDroneEnumValue())); |
| | | // 机场1:M30/M30T 依赖 droneSubEnumValue 区分(0-67-0 / 0-67-1),导入 KMZ 时需一并保留 |
| | | if (droneInfo.getDroneSubEnumValue() != null && !droneInfo.getDroneSubEnumValue().isEmpty()) { |
| | | uavRouteReq.setSubDroneType(Integer.valueOf(droneInfo.getDroneSubEnumValue())); |
| | | } |
| | | uavRouteReq.setImageFormat(vo.getKmlInfo().getDocument().getFolder().getPayloadParam().getImageFormat()); |
| | | uavRouteReq.setGlobalHeight(Double.valueOf(vo.getKmlInfo().getDocument().getFolder().getGlobalHeight())); |
| | | uavRouteReq.setAutoFlightSpeed(Double.valueOf(vo.getKmlInfo().getDocument().getFolder().getAutoFlightSpeed())); |
| | |
| | | return new DirectChannel(); |
| | | } |
| | | |
| | | /** |
| | | * 机场1:航线准备异常通知通道。 |
| | | */ |
| | | @Bean(name = ChannelName.INBOUND_EVENTS_FLIGHT_SETUP_EXCEPTION_NOTIFY) |
| | | public MessageChannel eventsFlightSetupExceptionNotifyChannel() { |
| | | return new DirectChannel(); |
| | | } |
| | | |
| | | @Bean(name = ChannelName.INBOUND_EVENTS_FILE_UPLOAD_CALLBACK) |
| | | public MessageChannel eventsFileUploadCallbackChannel() { |
| | | return new DirectChannel(); |
| | |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.beans.factory.annotation.Qualifier; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.util.StringUtils; |
| | | |
| | | import java.util.List; |
| | | import java.util.Map; |
| | |
| | | List<LiveStatusTwoReceiver> list = mapper.convertValue(dataNode.get(StateDataEnum.LIVESTATUS.getDesc()), |
| | | new TypeReference<List<LiveStatusTwoReceiver>>() {}); |
| | | for (LiveStatusTwoReceiver receiver : list) { |
| | | if(receiver.getVideoId()!=null && receiver.getVideoId().contains(childSn)){ |
| | | // 机场1(Dock1)未连接飞行器时 childDeviceSn 为空,跳过匹配,避免 String.contains(null) 抛 NPE |
| | | if (StringUtils.hasText(childSn) && receiver.getVideoId() != null |
| | | && receiver.getVideoId().contains(childSn)) { |
| | | stateReceiver.setData(receiver); |
| | | continue; |
| | | } |
| | |
| | | catch (Exception e){ |
| | | log.error("接收state直播设备状态反馈转换异常:{}",e); |
| | | } |
| | | // 无子设备 SN 时不写缓存,避免产生 COVER_STATE_PREFIX + null 的非法 Redis key |
| | | if (StringUtils.hasText(childSn)) { |
| | | _deviceService.setChildRedisCamera(childSn, liveStatus); |
| | | } |
| | | } |
| | | |
| | | return stateReceiver; |
| | | } |
| | |
| | | |
| | | public static final String INBOUND_EVENTS_FLIGHT_TASK_PROGRESS = "inboundEventsFlightTaskProgress"; |
| | | |
| | | /** |
| | | * 机场1:航线准备异常通知(flight_setup_exception_notify),Dock1/M30 新增事件。 |
| | | */ |
| | | public static final String INBOUND_EVENTS_FLIGHT_SETUP_EXCEPTION_NOTIFY = "inboundEventsFlightSetupExceptionNotify"; |
| | | |
| | | public static final String INBOUND_EVENTS_FILE_UPLOAD_CALLBACK = "inboundEventsFileUploadCallback"; |
| | | |
| | | public static final String INBOUND_REQUESTS_AIRPORT_BIND_STATUS = "inboundRequestsAirportBindStatus"; |
| | |
| | | |
| | | FLIGHT_TASK_PROGRESS("flighttask_progress", ChannelName.INBOUND_EVENTS_FLIGHT_TASK_PROGRESS), |
| | | |
| | | /** |
| | | * 机场1:航线准备异常通知(Dock1/M30 新增事件,共享层原枚举缺少该方法)。 |
| | | */ |
| | | FLIGHT_SETUP_EXCEPTION_NOTIFY("flight_setup_exception_notify", ChannelName.INBOUND_EVENTS_FLIGHT_SETUP_EXCEPTION_NOTIFY), |
| | | |
| | | FILE_UPLOAD_CALLBACK("file_upload_callback", ChannelName.INBOUND_EVENTS_FILE_UPLOAD_CALLBACK), |
| | | |
| | | HMS("hms", ChannelName.INBOUND_EVENTS_HMS), |
| | |
| | | import java.util.concurrent.ConcurrentHashMap; |
| | | import java.util.concurrent.Executors; |
| | | import java.util.concurrent.ScheduledExecutorService; |
| | | import java.util.concurrent.ScheduledFuture; |
| | | import java.util.concurrent.TimeUnit; |
| | | |
| | | /** |
| | |
| | | private static final long HEARTBEAT_TIMEOUT = 90; |
| | | /** 心跳检测任务间隔(秒):定时扫描超时连接 */ |
| | | private static final long HEARTBEAT_CHECK_INTERVAL = 10; |
| | | /** |
| | | * 停流延迟(秒):页面刷新或网络抖动会先断开旧连接再建立新连接, |
| | | * 断开时立即 live_stop 会让新页面需要重新起流,视频出现空档甚至丢失。 |
| | | * 延迟期间重新订阅则取消停止。 |
| | | */ |
| | | private static final long LIVE_STOP_DELAY_SECONDS = 30; |
| | | |
| | | // ====================== 核心存储(线程安全) ====================== |
| | | /** 用户会话映射:userId -> WebSocketSession */ |
| | |
| | | private final Map<WebSocketSession, Object> sessionLocks = new ConcurrentHashMap<>(); |
| | | /** 用户最后心跳时间:userId -> 最后心跳时间戳(毫秒) */ |
| | | private final Map<String, Long> userLastHeartbeatTime = new ConcurrentHashMap<>(); |
| | | /** 待执行的延迟停流任务:设备SN -> 定时任务 */ |
| | | private final Map<String, ScheduledFuture<?>> pendingLiveStops = new ConcurrentHashMap<>(); |
| | | |
| | | // ====================== 线程池 ====================== |
| | | /** 心跳检测线程池(单线程守护线程) */ |
| | | private final ScheduledExecutorService heartbeatCheckExecutor = Executors.newSingleThreadScheduledExecutor(runnable -> { |
| | | Thread thread = new Thread(runnable, "websocket-heartbeat-check-thread"); |
| | | thread.setDaemon(true); // 守护线程,不阻塞JVM退出 |
| | | return thread; |
| | | }); |
| | | |
| | | /** 延迟停流线程池(单线程守护线程),停流可能走 HTTP,独立线程避免阻塞心跳检测 */ |
| | | private final ScheduledExecutorService liveStopExecutor = Executors.newSingleThreadScheduledExecutor(runnable -> { |
| | | Thread thread = new Thread(runnable, "websocket-live-stop-thread"); |
| | | thread.setDaemon(true); |
| | | return thread; |
| | | }); |
| | | |
| | |
| | | if (isSubscribe) { |
| | | // 订阅操作:不存在则创建,存在则添加用户 |
| | | subscriptions.computeIfAbsent(subscriptionKey, k -> ConcurrentHashMap.newKeySet()).add(userId); |
| | | // 页面刷新会先断连再重连,新订阅到达时取消上一次断连触发的延迟停流 |
| | | if (MessageType.LIVESTREAM.getType().equals(type)) { |
| | | cancelPendingLiveStop(sn); |
| | | } |
| | | // log.info("用户[{}]订阅设备[{}]的[{}]类型消息", userId, sn, type); |
| | | |
| | | // // 直播类型订阅:触发开启直播 |
| | |
| | | subscriberIds.remove(userId); |
| | | log.info("用户[{}]取消订阅设备[{}]的[{}]类型消息", userId, sn, type); |
| | | |
| | | // 订阅组为空时移除,并停止直播(直播类型) |
| | | // 订阅组为空时移除,并延迟停止直播(直播类型)。 |
| | | // 不立即停流:页面刷新会先断连再重连,立即停流会导致视频长时间黑屏。 |
| | | if (subscriberIds.isEmpty()) { |
| | | subscriptions.remove(subscriptionKey); |
| | | if (MessageType.LIVESTREAM.getType().equals(type)) { |
| | | _liveStreamService.StopLiveStreaming(sn); |
| | | log.info("设备[{}]直播流已停止(无订阅用户)", sn); |
| | | scheduleStopLiveStream(sn); |
| | | } |
| | | } |
| | | } |
| | |
| | | |
| | | // 线程安全发送消息 |
| | | try { |
| | | if(MessageType.LIVESTREAM.getType().equals(type)) { |
| | | // 指定了接收用户时只推给该用户;uId 为空(如后台定时任务起流)时推给全部订阅者 |
| | | if (MessageType.LIVESTREAM.getType().equals(type) && uId != null && !uId.isEmpty()) { |
| | | WebSocketSession liveSession = userSessions.get(uId); |
| | | if (liveSession != null && liveSession.isOpen()) { |
| | | sendMessage(liveSession, content); |
| | | log.info("单独向用户[{}]推送设备[{}]的[{}]类型消息:{}", uId, sn, type, content); |
| | | } |
| | | return; |
| | | } |
| | | sendMessage(session, content); |
| | |
| | | if (subscriberIds.remove(userId)) { |
| | | log.debug("用户[{}]已移除订阅:{}", userId, entry.getKey()); |
| | | |
| | | // 订阅组为空时移除,并停止直播(直播类型) |
| | | // 订阅组为空时移除,并延迟停止直播(直播类型)。 |
| | | // 页面刷新/短暂断网会重建连接,延迟停流可避免视频空档;期间重新订阅会取消停止。 |
| | | if (subscriberIds.isEmpty()) { |
| | | String subscriptionKey = entry.getKey(); |
| | | if (subscriptionKey.contains(MessageType.LIVESTREAM.getType())) { |
| | | String sn = subscriptionKey.split(":")[0]; |
| | | _liveStreamService.StopLiveStreaming(sn); |
| | | log.info("设备[{}]无订阅用户,已停止直播流", sn); |
| | | scheduleStopLiveStream(sn); |
| | | } |
| | | iterator.remove(); // 安全移除空订阅组 |
| | | } |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 延迟停止直播流。 |
| | | * 页面刷新/网络抖动会先断开旧连接再建立新连接,断开时立即停流会让新页面重新起流, |
| | | * 视频出现空档甚至丢失;这里延迟 LIVE_STOP_DELAY_SECONDS 秒再停,期间有新订阅则取消。 |
| | | */ |
| | | private void scheduleStopLiveStream(String sn) { |
| | | String subscriptionKey = getSubscriptionKey(sn, MessageType.LIVESTREAM.getType()); |
| | | pendingLiveStops.compute(sn, (key, oldFuture) -> { |
| | | if (oldFuture != null && !oldFuture.isDone()) { |
| | | oldFuture.cancel(false); |
| | | } |
| | | return liveStopExecutor.schedule(() -> { |
| | | pendingLiveStops.remove(sn); |
| | | Set<String> subscriberIds = subscriptions.get(subscriptionKey); |
| | | // 延迟期间又有用户订阅(如页面刷新重连),不停止直播 |
| | | if (subscriberIds != null && !subscriberIds.isEmpty()) { |
| | | log.info("设备[{}]延迟停流期间重新订阅,继续直播", sn); |
| | | return; |
| | | } |
| | | _liveStreamService.StopLiveStreaming(sn); |
| | | log.info("设备[{}]无订阅用户(已延迟{}秒),停止直播流", sn, LIVE_STOP_DELAY_SECONDS); |
| | | }, LIVE_STOP_DELAY_SECONDS, TimeUnit.SECONDS); |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * 取消待执行的延迟停流任务(设备重新被订阅时调用)。 |
| | | */ |
| | | private void cancelPendingLiveStop(String sn) { |
| | | ScheduledFuture<?> future = pendingLiveStops.remove(sn); |
| | | if (future != null && !future.isDone()) { |
| | | future.cancel(false); |
| | | } |
| | | } |
| | | |
| | |
| | | log.error("WebSocket心跳检测线程池关闭被中断", e); |
| | | } |
| | | } |
| | | // 关闭延迟停流线程池 |
| | | if (liveStopExecutor != null && !liveStopExecutor.isShutdown()) { |
| | | liveStopExecutor.shutdownNow(); |
| | | } |
| | | log.info("WebSocket处理器资源已销毁"); |
| | | } |
| | | } |
| | |
| | | |
| | | public void StopLiveStreaming(String sn) { |
| | | ResponseResult rlive = null; |
| | | // String resultLive = ""; |
| | | try { |
| | | // 获取直播参数 |
| | | LiveTypeDTO param = getLiveParam(sn); |
| | | // 停止直播逻辑 |
| | | rlive = _liveStreamService.liveStop(param.getVideoId()); |
| | | log.info("SN: {}-停止直播!返回值:{}", sn, rlive); |
| | | // rlive.setGateway(sn); |
| | | // rlive.setType(MessageType.LIVESTREAM.getType()); |
| | | // resultLive = _objectMapper.writeValueAsString(rlive); |
| | | RedisOpsUtils.del(RedisConst.WEBSOCKET_VIDEO_ADDRESS_PREFIX + sn); |
| | | log.info("SN: {}-停止直播,并删除redis直播信息!", sn); |
| | | } catch (Exception e) { |
| | | log.info("SN: {}-停止直播时发生异常,原因:{}", sn,e.getMessage()); |
| | | e.printStackTrace(); |
| | | log.info("SN: {}-停止直播时发生异常,原因:{}", sn, e.getMessage(), e); |
| | | } finally { |
| | | // 无论停流是否成功都清理缓存地址: |
| | | // 设备离线等原因导致停流异常时,若不清 Redis,后续订阅会直接拿到失效地址, |
| | | // 页面持续黑屏,只有等设备离线清理缓存后才能恢复 |
| | | RedisOpsUtils.del(RedisConst.WEBSOCKET_VIDEO_ADDRESS_PREFIX + sn); |
| | | log.info("SN: {}-已删除redis直播信息!", sn); |
| | | } |
| | | // 停止直播不返回数据,直接删除redis中的直播地址标记即可;并且方法在websocket连接断开后执行。 |
| | | } |
| | | |
| | | public String getResult(String sn, String msg) { |
| | |
| | | continue; |
| | | } |
| | | List<CapacityCameraDTO> camerasList = device.getCamerasList(); |
| | | for (CapacityCameraDTO camera : camerasList) { |
| | | // 机场3中飞行器,没有name,需要其他条件确定主摄像头参数 |
| | | for(CapacityVideoDTO video : camera.getVideosList()) |
| | | { |
| | | if(video.getSwitchVideoTypes().size() == 1 && camera.getName() == null) |
| | | { |
| | | if (camerasList == null || camerasList.isEmpty()) { |
| | | continue; |
| | | } |
| | | } |
| | | // camera.getVideosList().get(0).getSwitchVideoTypes().add("main"); |
| | | // if(camera.getVideosList().size() == 1 && camera.getName() == null) { |
| | | // continue; |
| | | // } |
| | | // if (camera.getName() == null || camera.getName().isEmpty()) { |
| | | // continue; |
| | | // } |
| | | String videoid = device.getSn() + "/" + camera.getIndex(); |
| | | for (CapacityCameraDTO camera : camerasList) { |
| | | List<CapacityVideoDTO> videosList = camera.getVideosList(); |
| | | if (videosList == null || videosList.isEmpty()) { |
| | | continue; |
| | | } |
| | | // 机场1:M30 / M30T 相机索引为 52-x-x / 53-x-x,live_capacity 可能同时上报 |
| | | // normal-0、zoom-0、ir-0 多条码流。默认优先选 normal 码流(可见光), |
| | | // 避免原来“循环取最后一条”时默认落到红外。其他机型保持原行为(取最后一条)。 |
| | | boolean m30SeriesCamera = camera.getIndex() != null |
| | | && (camera.getIndex().startsWith("52-") || camera.getIndex().startsWith("53-")); |
| | | CapacityVideoDTO selectedVideo = null; |
| | | for (CapacityVideoDTO video : videosList) { |
| | | videoid = videoid + "/" + video.getIndex(); |
| | | selectedVideo = video; |
| | | if (m30SeriesCamera && video.getIndex() != null |
| | | && video.getIndex().startsWith("normal")) { |
| | | break; |
| | | } |
| | | } |
| | | if (selectedVideo == null) { |
| | | continue; |
| | | } |
| | | List<String> switchVideoTypes = selectedVideo.getSwitchVideoTypes(); |
| | | if (switchVideoTypes == null || switchVideoTypes.isEmpty()) { |
| | | // 没有镜头类型信息时跳过该相机,避免 get(0) 越界 |
| | | continue; |
| | | } |
| | | // 机场1:video_id 格式固定为 {sn}/{camera_index}/{video_index}。 |
| | | // 按选中的码流独立构造,单码流设备结果与原来完全一致。 |
| | | String videoid = device.getSn() + "/" + camera.getIndex() + "/" + selectedVideo.getIndex(); |
| | | param.setUrlType(1); |
| | | param.setUrl(""); |
| | | param.setVideoId(videoid); |
| | | param.setVideoQuality(1); |
| | | param.setVideoType(video.getSwitchVideoTypes().get(0)); |
| | | } |
| | | param.setVideoType(switchVideoTypes.get(0)); |
| | | } |
| | | } |
| | | return param; |
| New file |
| | |
| | | /* |
| | | * 【机场1】dock1 独有 OSD 子结构(DJI Dock 1 / M30)。 |
| | | * 官方定义见上云 API Dock1 分册 properties:drone_authority_info(飞行器控制权状态更新)。 |
| | | */ |
| | | package com.ard.sample.dock1.manage.model.receiver; |
| | | |
| | | import com.fasterxml.jackson.annotation.JsonProperty; |
| | | import lombok.Data; |
| | | |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * 【机场1】飞行器控制权状态更新 struct(dock2 无此结构) |
| | | */ |
| | | @Data |
| | | public class Dock1DroneAuthorityInfoReceiver { |
| | | |
| | | /** |
| | | * 飞行控制权 enum_string {"":"未获取","A":"A控","B":"B控"} |
| | | * */ |
| | | @JsonProperty("control_source") |
| | | private String controlSource; |
| | | |
| | | /** |
| | | * 飞行控制权是否锁定(负载控制权不支持锁定) bool |
| | | * */ |
| | | @JsonProperty("locked") |
| | | private Boolean locked; |
| | | |
| | | /** |
| | | * 负载 array {"size": -, "item_type": struct} |
| | | * */ |
| | | @JsonProperty("payloads") |
| | | private List<Dock1DroneAuthorityPayloadReceiver> payloads; |
| | | } |
| New file |
| | |
| | | /* |
| | | * 【机场1】dock1 独有 OSD 子结构(DJI Dock 1 / M30)。 |
| | | * 官方定义见上云 API Dock1 分册 properties:drone_authority_info.payloads 数组元素。 |
| | | */ |
| | | package com.ard.sample.dock1.manage.model.receiver; |
| | | |
| | | import com.fasterxml.jackson.annotation.JsonProperty; |
| | | import lombok.Data; |
| | | |
| | | /** |
| | | * 【机场1】飞行器控制权 - 负载控制权 struct(drone_authority_info.payloads 元素) |
| | | */ |
| | | @Data |
| | | public class Dock1DroneAuthorityPayloadReceiver { |
| | | |
| | | /** |
| | | * 负载控制权 enum_string {"":"未获取","A":"A控","B":"B控"} |
| | | * */ |
| | | @JsonProperty("control_source") |
| | | private String controlSource; |
| | | |
| | | /** |
| | | * 负载编号 text 格式 {type-subtype-gimbalindex} |
| | | * */ |
| | | @JsonProperty("payload_index") |
| | | private String payloadIndex; |
| | | |
| | | /** |
| | | * 负载序列号(SN) text |
| | | * */ |
| | | @JsonProperty("sn") |
| | | private String sn; |
| | | } |
| New file |
| | |
| | | /* |
| | | * 【机场1】从 dock2 原类 com.ard.sample.manage.model.receiver.OsdDockReceiver 复制/独立适配(DJI Dock 1 / M30)。 |
| | | * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。 |
| | | */ |
| | | package com.ard.sample.dock1.manage.model.receiver; |
| | | |
| | | import com.fasterxml.jackson.annotation.JsonProperty; |
| | | import lombok.Data; |
| | | import com.ard.sample.manage.model.receiver.AlternateLandPointReceiver; |
| | | import com.ard.sample.manage.model.receiver.BackupBatteryReceiver; |
| | | import com.ard.sample.manage.model.receiver.DeviceMaintainStatusReceiver; |
| | | import com.ard.sample.manage.model.receiver.DockMediaFileDetailReceiver; |
| | | import com.ard.sample.manage.model.receiver.DockSdrReceiver; |
| | | import com.ard.sample.manage.model.receiver.DockSubDeviceReceiver; |
| | | import com.ard.sample.manage.model.receiver.DockWirelessLinkReceiver; |
| | | import com.ard.sample.manage.model.receiver.DroneBatteryMaintenanceInfoReceiver; |
| | | import com.ard.sample.manage.model.receiver.DroneChargeStateReceiver; |
| | | import com.ard.sample.manage.model.receiver.NetworkStateReceiver; |
| | | import com.ard.sample.manage.model.receiver.OsdDockReceiver; |
| | | import com.ard.sample.manage.model.receiver.PositionStateReceiver; |
| | | import com.ard.sample.manage.model.receiver.StorageReceiver; |
| | | |
| | | /** |
| | | * @author sean |
| | | * @version 1.3 |
| | | * @date 2022/11/3 |
| | | */ |
| | | @Data |
| | | public class Dock1OsdDockReceiver { |
| | | |
| | | /** |
| | | * 网络状态 struct |
| | | * */ |
| | | @JsonProperty("network_state") |
| | | private NetworkStateReceiver networkState; |
| | | |
| | | /** |
| | | * 飞行器是否在舱 enum_int {"0":"舱外","1":"舱内"} |
| | | * */ |
| | | @JsonProperty("drone_in_dock") |
| | | private Integer droneInDock; |
| | | |
| | | /** |
| | | * 飞行器充电状态 struct |
| | | * */ |
| | | @JsonProperty("drone_charge_state") |
| | | private DroneChargeStateReceiver droneChargeState; |
| | | |
| | | /** |
| | | * 降雨量 enum_int |
| | | * "0":"无雨" |
| | | * "1":"小雨" |
| | | * "2":"中雨" |
| | | * "3":"大雨" |
| | | * */ |
| | | @JsonProperty("rainfall") |
| | | private Integer rainfall; |
| | | |
| | | /** |
| | | * 风速 float {"unit_name":"米每秒 / m/s"} |
| | | * */ |
| | | @JsonProperty("wind_speed") |
| | | private Float windSpeed; |
| | | |
| | | /** |
| | | * 环境温度 float {"unit_name":"摄氏度 / °C"} |
| | | * */ |
| | | @JsonProperty("environment_temperature") |
| | | private Float environmentTemperature; |
| | | |
| | | /** |
| | | * 舱内温度 float {"unit_name":"摄氏度 / °C"} |
| | | * */ |
| | | @JsonProperty("temperature") |
| | | private Float temperature; |
| | | |
| | | /** |
| | | * 舱内湿度 float {"unit_name":"相对湿度 / %RH"} |
| | | * */ |
| | | @JsonProperty("humidity") |
| | | private Integer humidity; |
| | | |
| | | /** |
| | | * 纬度 double {"max":"90","min":"-90","step":"0.01"} |
| | | * */ |
| | | @JsonProperty("latitude") |
| | | private Double latitude; |
| | | |
| | | /** |
| | | * 经度 double {"max":"180","min":"-180","step":"0.01"} |
| | | * */ |
| | | @JsonProperty("longitude") |
| | | private Double longitude; |
| | | |
| | | /** |
| | | * 椭球高度 double {"unit_name":"米 / m"} |
| | | * */ |
| | | @JsonProperty("height") |
| | | private Double height; |
| | | |
| | | /** |
| | | * 备降点 struct |
| | | * */ |
| | | @JsonProperty("alternate_land_point") |
| | | private AlternateLandPointReceiver alternateLandPoint; |
| | | |
| | | /** |
| | | * 首次上电时间 int {"unit_name":"毫秒 / ms"} |
| | | * */ |
| | | @JsonProperty("first_power_on") |
| | | private Long firstPowerOn; |
| | | |
| | | /** |
| | | * 搜星状态 struct |
| | | * */ |
| | | @JsonProperty("position_state") |
| | | private PositionStateReceiver positionState; |
| | | |
| | | /** |
| | | * 存储容量 struct |
| | | * */ |
| | | @JsonProperty("storage") |
| | | private StorageReceiver storage; |
| | | |
| | | /** |
| | | * 机场状态 enum_int |
| | | * "0":"空闲中", |
| | | * "1":"现场调试", |
| | | * "2":"远程调试", |
| | | * "3":"固件升级中", |
| | | * "4":"作业中", |
| | | * "5":"待标定" |
| | | * */ |
| | | @JsonProperty("mode_code") |
| | | private Integer modeCode; |
| | | |
| | | /** |
| | | * 舱盖状态 enum_int |
| | | * "0":"关闭", |
| | | * "1":"打开", |
| | | * "2":"半开", |
| | | * "3":"舱盖状态异常" |
| | | * */ |
| | | @JsonProperty("cover_state") |
| | | private Integer coverState; |
| | | |
| | | /** |
| | | *机场静音模式 enum_int {"0":"非静音模式","1":"静音模式"} |
| | | * */ |
| | | @JsonProperty("silent_mode") |
| | | private Integer silentMode; |
| | | |
| | | /** |
| | | * 补光灯状态 enum_int {"0":"关闭","1":"打开"} |
| | | * */ |
| | | @JsonProperty("supplement_light_state") |
| | | private Integer supplementLightState; |
| | | |
| | | /** |
| | | * 紧急停止按钮状态 enum_int {"0":"关闭","1":"开启"} |
| | | * */ |
| | | @JsonProperty("emergency_stop_state") |
| | | private Integer emergencyStopState; |
| | | |
| | | /** |
| | | * 空调模式 上云文档中没有注释 |
| | | * */ |
| | | @JsonProperty("air_conditioner_mode") |
| | | private Integer airConditionerMode; |
| | | |
| | | /** |
| | | * 电池运行模式 enum_int {"1":"计划模式","2":"待命模式"} |
| | | * */ |
| | | @JsonProperty("battery_store_mode") |
| | | private Integer batteryStoreMode; |
| | | |
| | | /** |
| | | * 机场声光报警状态 enum_int {"0":"关闭","1":"开启"} |
| | | * */ |
| | | @JsonProperty("alarm_state") |
| | | private Integer alarmState; |
| | | |
| | | /** |
| | | * 推杆状态 enum_int {"0":"关闭","1":"打开","2":"半开","3":"推杆状态异常"} |
| | | * */ |
| | | @JsonProperty("putter_state") |
| | | private Integer putterState; |
| | | |
| | | /** |
| | | * 子设备状态 struct |
| | | * */ |
| | | @JsonProperty("sub_device") |
| | | private DockSubDeviceReceiver subDevice; |
| | | |
| | | /** |
| | | * 机场累计作业次数 int {"unit_name":"次 / count"} |
| | | * */ |
| | | @JsonProperty("job_number") |
| | | private Integer jobNumber; |
| | | |
| | | /** |
| | | * 机场累计运行时长 int {"unit_name":"秒 / s"} |
| | | * */ |
| | | @JsonProperty("acc_time") |
| | | private Long accTime; |
| | | |
| | | /** |
| | | * 机场激活时间(unix 时间戳) int {"unit_name":"秒 / s"} |
| | | * */ |
| | | @JsonProperty("activation_time") |
| | | private Long activationTime; |
| | | |
| | | /** |
| | | * 保养信息 struct |
| | | * */ |
| | | @JsonProperty("maintain_status") |
| | | private DeviceMaintainStatusReceiver maintainStatus; |
| | | |
| | | /** |
| | | * 市电电压 int {"unit_name":"伏特 / V"} |
| | | * */ |
| | | @JsonProperty("electric_supply_voltage") |
| | | private Integer electricSupplyVoltage; |
| | | |
| | | /** |
| | | * 工作电压 int {"unit_name":"毫伏 / mV"} |
| | | * */ |
| | | @JsonProperty("working_voltage") |
| | | private Integer workingVoltage; |
| | | |
| | | /** |
| | | * 工作电流 float {"unit_name":"毫安 / mA"} |
| | | * */ |
| | | @JsonProperty("working_current") |
| | | private Integer workingCurrent; |
| | | |
| | | /** |
| | | * 机场备用电池信息 struct |
| | | * */ |
| | | @JsonProperty("backup_battery") |
| | | private BackupBatteryReceiver backupBattery; |
| | | |
| | | /** |
| | | * 飞行器电池保养信息 struct |
| | | * */ |
| | | @JsonProperty("drone_battery_maintenance_info") |
| | | private DroneBatteryMaintenanceInfoReceiver droneBatteryMaintenanceInfo; |
| | | |
| | | /** |
| | | * 机场任务状态 enum_int |
| | | * "0":"作业准备中", |
| | | * "1":"飞行作业中", |
| | | * "2":"作业后状态恢复", |
| | | * "3":"自定义飞行区更新中", |
| | | * "4":"地形障碍物更新中", |
| | | * "5":"任务空闲", |
| | | * "255":"飞行器异常", |
| | | * "256":"未知状态" |
| | | * */ |
| | | @JsonProperty("flighttask_step_code") |
| | | private Integer flighttaskStepCode; |
| | | |
| | | /** |
| | | * 飞行任务准备容量 上云文档中没有 |
| | | * */ |
| | | @JsonProperty("flighttask_prepare_capacity") |
| | | private Integer flighttaskPrepareCapacity; |
| | | |
| | | /** |
| | | * 媒体文件上传细节 struct |
| | | * */ |
| | | @JsonProperty("media_file_detail") |
| | | private DockMediaFileDetailReceiver mediaFileDetail; |
| | | |
| | | /** |
| | | * |
| | | * */ |
| | | @JsonProperty("sdr") |
| | | private DockSdrReceiver sdr; |
| | | |
| | | /** |
| | | * 图传链路 struct |
| | | * */ |
| | | @JsonProperty("wireless_link") |
| | | private DockWirelessLinkReceiver wirelessLink; |
| | | |
| | | // 机场1:C2 以下 3 个字段为 dock1 独有(dock2 分册无此定义),官方文案取自上云 API Dock1 properties |
| | | |
| | | /** |
| | | * DRC 链路的状态 enum_int {"0":"未连接","1":"连接中","2":"已连接"} |
| | | * */ |
| | | @JsonProperty("drc_state") |
| | | private Integer drcState; |
| | | |
| | | /** |
| | | * 飞行器控制权状态更新 struct |
| | | * */ |
| | | @JsonProperty("drone_authority_info") |
| | | private Dock1DroneAuthorityInfoReceiver droneAuthorityInfo; |
| | | |
| | | /** |
| | | * 机场的航线解析库版本号 text |
| | | * */ |
| | | @JsonProperty("wpmz_version") |
| | | private String wpmzVersion; |
| | | } |
| New file |
| | |
| | | /* |
| | | * 【机场1】M30 / M30T 飞行器 OSD 接收器。 |
| | | * 共享层 OsdSubDeviceReceiver 只覆盖机场2/3 机型字段,这里在其基础上补充 M30 专属字段; |
| | | * 不影响共享类与机场2/3 的解析逻辑。 |
| | | */ |
| | | package com.ard.sample.dock1.manage.model.receiver; |
| | | |
| | | import com.ard.sample.manage.model.receiver.OsdSubDeviceReceiver; |
| | | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; |
| | | import com.fasterxml.jackson.annotation.JsonProperty; |
| | | import com.fasterxml.jackson.databind.PropertyNamingStrategy; |
| | | import com.fasterxml.jackson.databind.annotation.JsonNaming; |
| | | import lombok.Data; |
| | | import lombok.EqualsAndHashCode; |
| | | |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * 机场1(M30 / M30T)飞行器 OSD 数据。 |
| | | * |
| | | * @author ard |
| | | */ |
| | | @Data |
| | | @EqualsAndHashCode(callSuper = true) |
| | | @JsonIgnoreProperties(ignoreUnknown = true) |
| | | @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class) |
| | | public class Dock1OsdSubDeviceReceiver extends OsdSubDeviceReceiver { |
| | | |
| | | /** |
| | | * 国家/地区代码。 |
| | | */ |
| | | @JsonProperty("country") |
| | | private String country; |
| | | |
| | | /** |
| | | * 当前指挥官飞行模式。 |
| | | * 注意:M30/M30T 不同固件可能上报枚举整数或布尔值,用 Object 兼容,避免整帧 OSD 解析失败。 |
| | | */ |
| | | @JsonProperty("current_commander_flight_mode") |
| | | private Object currentCommanderFlightMode; |
| | | |
| | | /** |
| | | * 遥控器失联后是否退出航线。 |
| | | * 注意:M30/M30T 不同固件可能上报枚举整数或布尔值,用 Object 兼容。 |
| | | */ |
| | | @JsonProperty("exit_wayline_when_rc_lost") |
| | | private Object exitWaylineWhenRcLost; |
| | | |
| | | /** |
| | | * PSDK UI 资源。 |
| | | */ |
| | | @JsonProperty("psdk_ui_resource") |
| | | private List<Object> psdkUiResource; |
| | | |
| | | /** |
| | | * PSDK 控件值。 |
| | | */ |
| | | @JsonProperty("psdk_widget_values") |
| | | private List<Object> psdkWidgetValues; |
| | | |
| | | /** |
| | | * 遥控器失联动作。 |
| | | * 注意:M30/M30T 不同固件可能上报枚举整数或布尔值,用 Object 兼容。 |
| | | */ |
| | | @JsonProperty("rc_lost_action") |
| | | private Object rcLostAction; |
| | | |
| | | /** |
| | | * RID 状态。 |
| | | * 注意:M30T 实测上报布尔值(false/true),部分固件上报整数, |
| | | * 原定义为 Integer 会导致整帧 OSD 反序列化失败(状态/遥测全部丢失),这里用 Object 兼容。 |
| | | */ |
| | | @JsonProperty("rid_state") |
| | | private Object ridState; |
| | | |
| | | /** |
| | | * 返航高度。 |
| | | */ |
| | | @JsonProperty("rth_altitude") |
| | | private Integer rthAltitude; |
| | | |
| | | /** |
| | | * 航线文件版本。 |
| | | */ |
| | | @JsonProperty("wpmz_version") |
| | | private String wpmzVersion; |
| | | |
| | | } |
| New file |
| | |
| | | /* |
| | | * 【机场1】从 dock2 原类 com.ard.sample.manage.service.ICommandService 复制/独立适配(DJI Dock 1 / M30)。 |
| | | * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。 |
| | | */ |
| | | package com.ard.sample.dock1.manage.service; |
| | | |
| | | import com.ard.sample.common.model.ResponseResult; |
| | | import com.ard.sample.manage.model.param.CommandParam; |
| | | import org.springframework.messaging.MessageHeaders; |
| | | |
| | | /** |
| | | * @author sean |
| | | * @version 1.2 |
| | | * @date 2022/7/29 |
| | | */ |
| | | public interface ICommandService { |
| | | |
| | | /** |
| | | * 负载控制-拍照 |
| | | * @param sn |
| | | * @return |
| | | */ |
| | | ResponseResult cameraPhotoTake(String sn, CommandParam param); |
| | | |
| | | |
| | | } |
| New file |
| | |
| | | /* |
| | | * 【机场1】从 dock2 原类 com.ard.sample.manage.service.IControlService 复制/独立适配(DJI Dock 1 / M30)。 |
| | | * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。 |
| | | */ |
| | | package com.ard.sample.dock1.manage.service; |
| | | |
| | | import com.ard.sample.common.model.ResponseResult; |
| | | import com.ard.sample.component.mqtt.model.CommonTopicReceiver; |
| | | import com.ard.sample.manage.model.param.RemoteDebugParam; |
| | | import org.springframework.messaging.MessageHeaders; |
| | | |
| | | /** |
| | | * @author sean |
| | | * @version 1.2 |
| | | * @date 2022/7/29 |
| | | */ |
| | | public interface IControlService { |
| | | |
| | | /** |
| | | * Remotely debug the dock via commands. |
| | | * @param sn |
| | | * @param serviceIdentifier |
| | | * @param param |
| | | * @return |
| | | */ |
| | | ResponseResult controlDock(String sn, String serviceIdentifier, RemoteDebugParam param); |
| | | |
| | | /** |
| | | * Handles multi-state command progress information. |
| | | * @param receiver |
| | | * @param headers |
| | | */ |
| | | void handleControlProgress(CommonTopicReceiver receiver, MessageHeaders headers); |
| | | |
| | | } |
| New file |
| | |
| | | /* |
| | | * 【机场1】从 dock2 原类 com.ard.sample.manage.service.ILiveStreamService 复制/独立适配(DJI Dock 1 / M30)。 |
| | | * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。 |
| | | */ |
| | | package com.ard.sample.dock1.manage.service; |
| | | |
| | | import com.ard.sample.common.model.ResponseResult; |
| | | import com.ard.sample.manage.model.dto.CapacityDeviceDTO; |
| | | import com.ard.sample.manage.model.dto.LiveTypeDTO; |
| | | import com.ard.sample.manage.model.receiver.LiveCapacityReceiver; |
| | | |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * @author sean.zhou |
| | | * @date 2021/11/19 |
| | | * @version 0.1 |
| | | */ |
| | | public interface ILiveStreamService { |
| | | |
| | | /** |
| | | * Get all the drone data that can be broadcast live in this workspace. |
| | | * @return |
| | | */ |
| | | List<CapacityDeviceDTO> getLiveCapacity(); |
| | | |
| | | /** |
| | | * Save live capability data. |
| | | * @param liveCapacityReceiver |
| | | * @param timestamp |
| | | */ |
| | | void saveLiveCapacity(LiveCapacityReceiver liveCapacityReceiver, Long timestamp); |
| | | |
| | | /** |
| | | * Initiate a live streaming by publishing mqtt message. |
| | | * @param liveParam Parameters needed for on-demand. |
| | | * @return |
| | | */ |
| | | ResponseResult liveStart(LiveTypeDTO liveParam); |
| | | |
| | | /** |
| | | * Stop the live streaming by publishing mqtt message. |
| | | * @param videoId |
| | | * @return |
| | | */ |
| | | ResponseResult liveStop(String videoId); |
| | | |
| | | /** |
| | | * Readjust the clarity of the live streaming by publishing mqtt messages. |
| | | * @param liveParam |
| | | * @return |
| | | */ |
| | | ResponseResult liveSetQuality(LiveTypeDTO liveParam); |
| | | |
| | | /** |
| | | * Switches the lens of the device during the live streaming. |
| | | * @param liveParam |
| | | * @return |
| | | */ |
| | | ResponseResult liveLensChange(LiveTypeDTO liveParam); |
| | | |
| | | /** |
| | | * 获取直播能力中摄像头模式(广角、变焦、红外) |
| | | * */ |
| | | ResponseResult<List<String>> getLiveLens(String sn); |
| | | |
| | | /** |
| | | * 直播相机切换 |
| | | * @param liveParam |
| | | * @return |
| | | */ |
| | | ResponseResult liveCameraChange(LiveTypeDTO liveParam); |
| | | } |
| New file |
| | |
| | | /* |
| | | * 【机场1】从 dock2 原类 com.ard.sample.manage.service.IWaylineTrajectoryService 复制/独立适配(DJI Dock 1 / M30)。 |
| | | * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。 |
| | | */ |
| | | package com.ard.sample.dock1.manage.service; |
| | | |
| | | import com.ard.sample.manage.model.dto.WaylineTrajectoryDTO; |
| | | import com.ard.sample.manage.model.entity.WaylineTrajectoryEntity; |
| | | |
| | | import java.util.List; |
| | | |
| | | public interface IWaylineTrajectoryService { |
| | | |
| | | /** |
| | | * 增加list数据,type为:0时,表示轨迹点列表;type为:1时,表示任务状态数据。 |
| | | * @param sn 设备序列号 |
| | | * @param type 类型 |
| | | * @param dataJson json数据 |
| | | * */ |
| | | void setList(String sn, String type, String dataJson); |
| | | |
| | | /** |
| | | * 返回航线轨迹完整的数据,拼接的轨迹点列表和任务状态数据。 |
| | | * @param jobId 任务ID |
| | | * @return 航线轨迹完整的数据 |
| | | * */ |
| | | WaylineTrajectoryDTO getWayLineTrajectorybyJobID(String jobId); |
| | | } |
| New file |
| | |
| | | /* |
| | | * 【机场1】从 dock2 原类 com.ard.sample.manage.service.impl.CommandServiceImpl 复制/独立适配(DJI Dock 1 / M30)。 |
| | | * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。 |
| | | */ |
| | | package com.ard.sample.dock1.manage.service.impl; |
| | | |
| | | import com.ard.sample.common.error.CommonErrorEnum; |
| | | import com.ard.sample.common.model.ResponseResult; |
| | | import com.ard.sample.component.mqtt.model.*; |
| | | import com.ard.sample.component.mqtt.service.IMessageSenderService; |
| | | import com.ard.common.core.redis.RedisConst; |
| | | import com.ard.common.core.redis.RedisOpsUtils; |
| | | import com.ard.sample.manage.model.dto.DeviceDTO; |
| | | import com.ard.sample.manage.model.param.CommandParam; |
| | | import com.ard.sample.dock1.manage.service.ICommandService; |
| | | import com.ard.sample.manage.service.IDeviceService; |
| | | import com.fasterxml.jackson.core.type.TypeReference; |
| | | import com.fasterxml.jackson.databind.ObjectMapper; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.integration.mqtt.support.MqttHeaders; |
| | | import org.springframework.messaging.MessageHeaders; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | import javax.annotation.Resource; |
| | | import java.util.UUID; |
| | | import com.ard.sample.manage.service.impl.CommandServiceImpl; |
| | | |
| | | /** |
| | | * Description: |
| | | * Author: lsy |
| | | * Date: 2025年09月24日9:00 |
| | | **/ |
| | | @Slf4j |
| | | @Service |
| | | public class Dock1CommandServiceImpl implements ICommandService { |
| | | |
| | | @Autowired |
| | | private ObjectMapper mapper; |
| | | @Resource |
| | | private IDeviceService deviceService; |
| | | @Resource |
| | | private IMessageSenderService messageSenderService; |
| | | /** |
| | | * 负载控制-拍照 |
| | | * @param sn |
| | | * @return |
| | | */ |
| | | @Override |
| | | public ResponseResult cameraPhotoTake(String sn, CommandParam param) { |
| | | |
| | | boolean isExist = deviceService.checkDeviceOnline(sn); |
| | | if (!isExist) { |
| | | return ResponseResult.error("The dock is offline."); |
| | | } |
| | | String topic = TopicConst.THING_MODEL_PRE + TopicConst.PRODUCT + sn + TopicConst.SERVICES_SUF; |
| | | String bid = UUID.randomUUID().toString(); |
| | | ServiceReply serviceReplyOpt = messageSenderService.publishWithReply( |
| | | topic, CommonTopicResponse.builder() |
| | | .tid(UUID.randomUUID().toString()) |
| | | .bid(bid) |
| | | .method("camera_photo_take") |
| | | .timestamp(System.currentTimeMillis()) |
| | | .data(param) |
| | | .build()); |
| | | |
| | | ServiceReply<EventsOutputReceiver> serviceReply = mapper.convertValue( |
| | | serviceReplyOpt, new TypeReference<ServiceReply<EventsOutputReceiver>>() {}); |
| | | if (ResponseResult.CODE_SUCCESS != serviceReply.getResult()) { |
| | | return ResponseResult.error(serviceReply.getResult(), CommonErrorEnum.getMsgByCode(serviceReply.getResult())); |
| | | } |
| | | // if (controlMethodEnum.getProgress()) { |
| | | // RedisOpsUtils.setWithExpire(serviceIdentifier + RedisConst.DELIMITER + bid, sn, |
| | | // RedisConst.DEVICE_ALIVE_SECOND * RedisConst.DEVICE_ALIVE_SECOND); |
| | | // } |
| | | return ResponseResult.success(); |
| | | } |
| | | |
| | | // 机场1:不复制原 ServiceActivator(inputChannel = ChannelName.INBOUND_EVENTS_CAMERA_PHOTO_TAKE_PROGRESS),消费职责由共享层原类承担 |
| | | public void handleCameraPhotoTakeProgress(CommonTopicReceiver receiver, MessageHeaders headers) { |
| | | String key = receiver.getMethod() + RedisConst.DELIMITER + receiver.getBid(); |
| | | if (RedisOpsUtils.getExpire(key) <= 0) { |
| | | return; |
| | | } |
| | | String sn = RedisOpsUtils.get(key).toString(); |
| | | |
| | | EventsReceiver<EventsOutputReceiver> eventsReceiver = mapper.convertValue(receiver.getData(), |
| | | new TypeReference<EventsReceiver<EventsOutputReceiver>>(){}); |
| | | eventsReceiver.setBid(receiver.getBid()); |
| | | eventsReceiver.setSn(sn); |
| | | |
| | | log.info("SN: {}, {} ===> CameraPhotoTak progress: {}", |
| | | sn, receiver.getMethod(), eventsReceiver.getOutput().getProgress().toString()); |
| | | |
| | | if (eventsReceiver.getResult() != ResponseResult.CODE_SUCCESS) { |
| | | log.error("SN: {}, {} ===> Error code: {}", sn, receiver.getMethod(), eventsReceiver.getResult()); |
| | | } |
| | | |
| | | if (eventsReceiver.getOutput().getProgress().getPercent() == 100 || |
| | | EventsResultStatusEnum.find(eventsReceiver.getOutput().getStatus()).getEnd()) { |
| | | RedisOpsUtils.del(key); |
| | | } |
| | | |
| | | DeviceDTO device = (DeviceDTO) RedisOpsUtils.get(RedisConst.DEVICE_ONLINE_PREFIX + sn); |
| | | // webSocketMessageService.sendBatch( |
| | | // WebSocketManager.getValueWithWorkspaceAndUserType( |
| | | // device.getWorkspaceId(), UserTypeEnum.WEB.getVal()), |
| | | // CustomWebSocketMessage.builder() |
| | | // .data(eventsReceiver) |
| | | // .timestamp(System.currentTimeMillis()) |
| | | // .bizCode(receiver.getMethod()) |
| | | // .build()); |
| | | |
| | | if (receiver.getNeedReply() != null && receiver.getNeedReply() == 1) { |
| | | String topic = headers.get(MqttHeaders.RECEIVED_TOPIC) + TopicConst._REPLY_SUF; |
| | | messageSenderService.publish(topic, |
| | | CommonTopicResponse.builder() |
| | | .tid(receiver.getTid()) |
| | | .bid(receiver.getBid()) |
| | | .method(receiver.getMethod()) |
| | | .timestamp(System.currentTimeMillis()) |
| | | .data(RequestsReply.success()) |
| | | .build()); |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * 【机场1】从 dock2 原类 com.ard.sample.manage.service.impl.ControlServiceImpl 复制/独立适配(DJI Dock 1 / M30)。 |
| | | * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。 |
| | | */ |
| | | package com.ard.sample.dock1.manage.service.impl; |
| | | |
| | | import com.ard.sample.common.error.CommonErrorEnum; |
| | | import com.ard.sample.common.model.ResponseResult; |
| | | import com.ard.sample.component.mqtt.model.*; |
| | | import com.ard.sample.component.mqtt.service.IMessageSenderService; |
| | | import com.ard.common.core.redis.RedisConst; |
| | | import com.ard.common.core.redis.RedisOpsUtils; |
| | | import com.ard.sample.component.websocket.enums.MessageType; |
| | | import com.ard.sample.component.websocket.handler.DockWebSocketHandler; |
| | | import com.ard.sample.component.websocket.handler.LiveStreamHandler; |
| | | import com.ard.sample.component.websocket.service.ISendMessageService; |
| | | import com.ard.sample.manage.model.dto.BasicDeviceProperty; |
| | | import com.ard.sample.manage.model.dto.DeviceDTO; |
| | | import com.ard.sample.manage.model.enums.DeviceDomainEnum; |
| | | import com.ard.sample.manage.model.enums.RemoteControlMethodEnum; |
| | | import com.ard.sample.manage.model.param.RemoteDebugParam; |
| | | import com.ard.sample.dock1.manage.service.IControlService; |
| | | import com.ard.sample.manage.service.IDeviceService; |
| | | import com.fasterxml.jackson.core.type.TypeReference; |
| | | import com.fasterxml.jackson.databind.ObjectMapper; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.integration.mqtt.support.MqttHeaders; |
| | | import org.springframework.messaging.MessageHeaders; |
| | | import org.springframework.stereotype.Service; |
| | | import java.util.Objects; |
| | | import java.util.UUID; |
| | | import com.ard.sample.manage.service.impl.ControlServiceImpl; |
| | | |
| | | /** |
| | | * @author sean |
| | | * @version 1.2 |
| | | * @date 2022/7/29 |
| | | */ |
| | | @Service |
| | | @Slf4j |
| | | public class Dock1ControlServiceImpl implements IControlService { |
| | | |
| | | /** |
| | | * 机场1:Dock1 设备类型。 |
| | | */ |
| | | private static final int DOCK1_DEVICE_TYPE = 1; |
| | | |
| | | @Autowired |
| | | private IMessageSenderService messageSenderService; |
| | | |
| | | @Autowired |
| | | private DockWebSocketHandler _dockWebSocketHandler; |
| | | |
| | | @Autowired |
| | | private ISendMessageService webSocketMessageService; |
| | | |
| | | @Autowired |
| | | private IDeviceService _deviceService; |
| | | |
| | | @Autowired |
| | | private LiveStreamHandler _liveStreamHandler; |
| | | |
| | | // @Autowired |
| | | // private IWebSocketManageService webSocketManageService; |
| | | |
| | | @Autowired |
| | | private IDeviceService deviceService; |
| | | |
| | | @Autowired |
| | | private ObjectMapper mapper; |
| | | |
| | | @Override |
| | | public ResponseResult controlDock(String sn, String serviceIdentifier, RemoteDebugParam param) { |
| | | RemoteControlMethodEnum controlMethodEnum = RemoteControlMethodEnum.find(serviceIdentifier); |
| | | if (RemoteControlMethodEnum.UNKNOWN == controlMethodEnum) { |
| | | return ResponseResult.error("The " + serviceIdentifier + " method does not exist."); |
| | | } |
| | | |
| | | |
| | | Object data = ""; |
| | | // Add parameter validation. |
| | | if (Objects.nonNull(controlMethodEnum.getClazz())) { |
| | | if (Objects.isNull(param)) { |
| | | return ResponseResult.error(CommonErrorEnum.ILLEGAL_ARGUMENT); |
| | | } |
| | | BasicDeviceProperty basicDeviceProperty = null; |
| | | if(serviceIdentifier.equals("sdr_workmode_switch")){ |
| | | basicDeviceProperty= mapper.convertValue(param.getLink_workmode(), controlMethodEnum.getClazz()); |
| | | } |
| | | else |
| | | { |
| | | basicDeviceProperty = mapper.convertValue(param.getAction(), controlMethodEnum.getClazz()); |
| | | } |
| | | if (!basicDeviceProperty.valid()) { |
| | | return ResponseResult.error(CommonErrorEnum.ILLEGAL_ARGUMENT); |
| | | } |
| | | data = basicDeviceProperty; |
| | | } |
| | | |
| | | boolean isExist = deviceService.checkDeviceOnline(sn); |
| | | if (!isExist) { |
| | | return ResponseResult.error(CommonErrorEnum.DEVICE_OFFLINE.getErrorCode(), CommonErrorEnum.DEVICE_OFFLINE.getErrorMsg()); |
| | | } |
| | | DeviceDTO device = (DeviceDTO) RedisOpsUtils.get(RedisConst.DEVICE_ONLINE_PREFIX + sn); |
| | | // 机场1:/dock1 控制接口只允许操作 Dock1,避免机场2/3 的 SN 误走 dock1 链路。 |
| | | if (device == null || !Objects.equals(DOCK1_DEVICE_TYPE, device.getType()) |
| | | || !Objects.equals(DeviceDomainEnum.DOCK.getVal(), device.getDomain())) { |
| | | return ResponseResult.error(314013004, "该设备不属于机场1,请使用对应的机场接口操作!"); |
| | | } |
| | | // 重启机场时清redis视频直播地址,机场重启不掉线。但是流停了。 |
| | | if(RemoteControlMethodEnum.DEVICE_REBOOT == controlMethodEnum) { |
| | | // 重启机场时,读取舱盖状态,开舱盖不让重启,关舱盖重启 |
| | | Integer cover_state = (Integer)RedisOpsUtils.get(RedisConst.COVER_STATE_OSD_PREFIX + device.getDeviceSn()); |
| | | if(cover_state != null && cover_state != 0) { |
| | | return ResponseResult.error(111111,"请关闭舱盖再执行重启操作!"); |
| | | } |
| | | RedisOpsUtils.del(RedisConst.WEBSOCKET_VIDEO_ADDRESS_PREFIX + sn); |
| | | RedisOpsUtils.del(RedisConst.COVER_STATE_PREFIX + sn); |
| | | RedisOpsUtils.del(RedisConst.WEBSOCKET_VIDEO_ADDRESS_PREFIX + device.getChildDeviceSn()); |
| | | } |
| | | if(RemoteControlMethodEnum.DRONE_CLOSE == controlMethodEnum) { |
| | | // 飞行器关机时drone_close,关闭飞行器视频流 |
| | | _liveStreamHandler.StopLiveStreaming(device.getChildDeviceSn()); |
| | | RedisOpsUtils.del(RedisConst.WEBSOCKET_VIDEO_ADDRESS_PREFIX + device.getChildDeviceSn()); |
| | | log.info("SN: {}-飞行器关机,停止直播并删除redis直播信息!", device.getChildDeviceSn()); |
| | | } |
| | | String topic = TopicConst.THING_MODEL_PRE + TopicConst.PRODUCT + sn + TopicConst.SERVICES_SUF; |
| | | String bid = UUID.randomUUID().toString(); |
| | | ServiceReply serviceReplyOpt = messageSenderService.publishWithReply( |
| | | topic, CommonTopicResponse.builder() |
| | | .tid(UUID.randomUUID().toString()) |
| | | .bid(bid) |
| | | .method(serviceIdentifier) |
| | | .timestamp(System.currentTimeMillis()) |
| | | .data(data) |
| | | .build()); |
| | | |
| | | ServiceReply<EventsOutputReceiver> serviceReply = mapper.convertValue( |
| | | serviceReplyOpt, new TypeReference<ServiceReply<EventsOutputReceiver>>() {}); |
| | | if (ResponseResult.CODE_SUCCESS != serviceReply.getResult()) { |
| | | String errorMsg = CommonErrorEnum.getMsgByCode(serviceReply.getResult()); |
| | | return ResponseResult.error(serviceReply.getResult(),errorMsg); |
| | | // return ResponseResult.error(serviceReply.getResult(), |
| | | // Objects.nonNull(serviceReply.getOutput()) ? serviceReply.getOutput().getStatus() : "errorCode: " |
| | | // + serviceReply.getResult()); |
| | | } |
| | | if (controlMethodEnum.getProgress()) { |
| | | RedisOpsUtils.setWithExpire(serviceIdentifier + RedisConst.DELIMITER + bid, sn, |
| | | RedisConst.DEVICE_ALIVE_SECOND * RedisConst.DEVICE_ALIVE_SECOND); |
| | | } |
| | | // 执行操作成功后,更新摄像头内外镜头状态 |
| | | // 机场1:Dock1 只有舱外单相机,camera_position 恒为 1,不存在舱内/舱外切换; |
| | | // 共享层在开盖时写 0 是 Dock2 双相机的语义,这里按 Dock1 修正。 |
| | | if(RemoteControlMethodEnum.COVER_OPEN == controlMethodEnum) { |
| | | _deviceService.setRedisCameraPosition(sn,1); |
| | | } |
| | | if(RemoteControlMethodEnum.COVER_CLOSE == controlMethodEnum) { |
| | | _deviceService.setRedisCameraPosition(sn,1); |
| | | } |
| | | |
| | | return ResponseResult.success(); |
| | | } |
| | | |
| | | @Override |
| | | // 机场1:不复制原 ServiceActivator(inputChannel = ChannelName.INBOUND_EVENTS_CONTROL_PROGRESS),消费职责由共享层原类承担 |
| | | public void handleControlProgress(CommonTopicReceiver receiver, MessageHeaders headers) { |
| | | String key = receiver.getMethod() + RedisConst.DELIMITER + receiver.getBid(); |
| | | if (RedisOpsUtils.getExpire(key) <= 0) { |
| | | return; |
| | | } |
| | | // 更新设备列表状态--关闭舱盖中... |
| | | String method = receiver.getMethod(); |
| | | String deviceSn = receiver.getGateway(); |
| | | if(method.equals("cover_close")){ |
| | | _deviceService.updateWorkType(deviceSn, 2L); |
| | | RedisOpsUtils.set(RedisConst.OSD_STATUS_PREFIX + deviceSn, 2); |
| | | } |
| | | String sn = RedisOpsUtils.get(key).toString(); |
| | | receiver.setType(MessageType.PROGRESS.getType()); |
| | | EventsReceiver<EventsOutputReceiver> eventsReceiver = mapper.convertValue(receiver.getData(), |
| | | new TypeReference<EventsReceiver<EventsOutputReceiver>>(){}); |
| | | eventsReceiver.setBid(receiver.getBid()); |
| | | eventsReceiver.setGateway(sn); |
| | | eventsReceiver.setType(MessageType.PROGRESS.getType()); |
| | | eventsReceiver.setMethod(receiver.getMethod()); |
| | | |
| | | log.info("SN: {}, {} ===> Control progress: {}", |
| | | sn, receiver.getMethod(), eventsReceiver.getOutput().getProgress().toString()); |
| | | |
| | | if (eventsReceiver.getResult() != ResponseResult.CODE_SUCCESS) { |
| | | log.error("SN: {}, {} ===> Error code: {}", sn, receiver.getMethod(), eventsReceiver.getResult()); |
| | | } |
| | | |
| | | if (eventsReceiver.getOutput().getProgress().getPercent() == 100 || |
| | | EventsResultStatusEnum.find(eventsReceiver.getOutput().getStatus()).getEnd()) { |
| | | RedisOpsUtils.del(key); |
| | | } |
| | | |
| | | DeviceDTO device = (DeviceDTO) RedisOpsUtils.get(RedisConst.DEVICE_ONLINE_PREFIX + sn); |
| | | // webSocketMessageService.sendBatch( |
| | | // WebSocketManager.getValueWithWorkspaceAndUserType( |
| | | // device.getWorkspaceId(), UserTypeEnum.WEB.getVal()), |
| | | // CustomWebSocketMessage.builder() |
| | | // .data(eventsReceiver) |
| | | // .timestamp(System.currentTimeMillis()) |
| | | // .bizCode(receiver.getMethod()) |
| | | // .build()); |
| | | // 更新设备列表状态--飞行器开机中... |
| | | if(method.equals("drone_open")){ |
| | | _deviceService.updateWorkType(device.getChildDeviceSn(), 7L); |
| | | RedisOpsUtils.set(RedisConst.OSD_STATUS_PREFIX + device.getChildDeviceSn(), 7); |
| | | } |
| | | // 机场重启状态--ok后删除redis状态 |
| | | if(method.equals("device_reboot")){ |
| | | if(eventsReceiver.getOutput().getProgress().getPercent() == 100){ |
| | | RedisOpsUtils.del(RedisConst.WEBSOCKET_VIDEO_ADDRESS_PREFIX + sn); |
| | | RedisOpsUtils.del(RedisConst.COVER_STATE_PREFIX + sn); |
| | | RedisOpsUtils.del(RedisConst.WEBSOCKET_VIDEO_ADDRESS_PREFIX + device.getChildDeviceSn()); |
| | | } |
| | | } |
| | | |
| | | |
| | | String dockProgress =""; |
| | | try { |
| | | dockProgress = mapper.writeValueAsString(eventsReceiver); |
| | | } |
| | | catch (Exception e) { |
| | | e.printStackTrace(); |
| | | } |
| | | // 设备控制进度消息websocket发给前端 |
| | | _dockWebSocketHandler.sendMessageToSubscribers(sn, MessageType.PROGRESS.getType(), dockProgress, ""); |
| | | |
| | | if (receiver.getNeedReply() != null && receiver.getNeedReply() == 1) { |
| | | String topic = headers.get(MqttHeaders.RECEIVED_TOPIC) + TopicConst._REPLY_SUF; |
| | | messageSenderService.publish(topic, |
| | | CommonTopicResponse.builder() |
| | | .tid(receiver.getTid()) |
| | | .bid(receiver.getBid()) |
| | | .method(receiver.getMethod()) |
| | | .timestamp(System.currentTimeMillis()) |
| | | .data(RequestsReply.success()) |
| | | .build()); |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * 【机场1】从 dock2 原类 com.ard.sample.manage.service.impl.DeviceOSDServiceImpl 复制/独立适配(DJI Dock 1 / M30)。 |
| | | * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。 |
| | | */ |
| | | package com.ard.sample.dock1.manage.service.impl; |
| | | |
| | | import com.ard.common.core.redis.RedisConst; |
| | | import com.ard.common.core.redis.RedisOpsUtils; |
| | | import com.ard.sample.component.mqtt.model.CommonTopicReceiver; |
| | | |
| | | import com.ard.sample.component.websocket.config.ConcurrentWebSocketSession; |
| | | import com.ard.sample.component.websocket.enums.MessageType; |
| | | import com.ard.sample.component.websocket.handler.DockWebSocketHandler; |
| | | import com.ard.sample.component.websocket.model.BizCodeEnum; |
| | | import com.ard.sample.component.websocket.model.CustomWebSocketMessage; |
| | | import com.ard.sample.manage.model.dto.DeviceDTO; |
| | | import com.ard.sample.manage.model.dto.TelemetryDTO; |
| | | import com.ard.sample.manage.model.dto.TelemetryDeviceDTO; |
| | | import com.ard.sample.manage.model.enums.DeviceDomainEnum; |
| | | import com.ard.sample.manage.model.receiver.OsdSubDeviceReceiver; |
| | | // 机场1:使用带 M30 专属字段的子类解析飞行器 OSD(仍兼容共享层类型)。 |
| | | import com.ard.sample.dock1.manage.model.receiver.Dock1OsdSubDeviceReceiver; |
| | | import com.ard.sample.manage.service.IDeviceService; |
| | | // 机场1:轨迹缓存必须与共享任务进度(FlightTaskServiceImpl 的 type="1" 写入)使用同一个服务实例, |
| | | // 否则 type="0" 的轨迹点写进 dock1 独立缓存、而 type="1" 的任务状态写在共享缓存,两处永远无法配对。 |
| | | // 这里复用共享层 IWaylineTrajectoryService,不修改共享层实现。 |
| | | import com.ard.sample.manage.service.IWaylineTrajectoryService; |
| | | import com.fasterxml.jackson.core.JsonProcessingException; |
| | | import com.fasterxml.jackson.databind.JsonNode; |
| | | import com.fasterxml.jackson.databind.node.ObjectNode; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.beans.factory.annotation.Qualifier; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | import java.util.Collection; |
| | | import com.ard.sample.manage.service.impl.AbstractTSAService; |
| | | import com.ard.sample.manage.service.impl.DeviceOSDServiceImpl; |
| | | |
| | | /** |
| | | * @author sean |
| | | * @version 0.3 |
| | | * @date 2022/2/21 |
| | | */ |
| | | @Service |
| | | @Slf4j |
| | | public class Dock1DeviceOSDServiceImpl extends AbstractTSAService { |
| | | |
| | | @Autowired |
| | | private IDeviceService _deviceService; |
| | | |
| | | @Autowired |
| | | private IWaylineTrajectoryService _waylineTrajectoryService; |
| | | |
| | | @Autowired |
| | | private DockWebSocketHandler _dockWebSocketHandler; |
| | | |
| | | protected Dock1DeviceOSDServiceImpl(@Autowired @Qualifier("dock1DockOSDServiceImpl") AbstractTSAService tsaService) { |
| | | super(tsaService); |
| | | } |
| | | |
| | | @Override |
| | | public void pushTelemetryData(Collection<ConcurrentWebSocketSession> sessions, |
| | | CustomWebSocketMessage<TelemetryDTO> message, Object osdData) { |
| | | if (osdData instanceof OsdSubDeviceReceiver) { |
| | | OsdSubDeviceReceiver data = (OsdSubDeviceReceiver) osdData; |
| | | TelemetryDTO telemetry = message.getData(); |
| | | telemetry.setHost(TelemetryDeviceDTO.builder() |
| | | .latitude(data.getLatitude()) |
| | | .longitude(data.getLongitude()) |
| | | .altitude(data.getElevation()) |
| | | .attitudeHead(data.getAttitudeHead()) |
| | | .elevation(data.getElevation()) |
| | | .horizontalSpeed(data.getHorizontalSpeed()) |
| | | .verticalSpeed(data.getVerticalSpeed()) |
| | | .build()); |
| | | |
| | | this.sendMessageService.sendBatch(sessions, message); |
| | | } |
| | | } |
| | | @Override |
| | | public void handleOSD(CommonTopicReceiver receiver, DeviceDTO device, |
| | | Collection<ConcurrentWebSocketSession> webSessions, |
| | | CustomWebSocketMessage<TelemetryDTO> wsMessage) { |
| | | if (DeviceDomainEnum.SUB_DEVICE.getVal() == device.getDomain()) { |
| | | wsMessage.setBizCode(BizCodeEnum.DEVICE_OSD.getCode()); |
| | | |
| | | // 机场1:M30/M30T 的 OSD 比共享类多 9 个字段,用 dock1 子类承接。 |
| | | // 机场1:Dock1 的飞行器 OSD 中 payloads 本身就是数组,convertValue 已经完成解析; |
| | | // 共享层按 device.payloadsList(扁平 key)二次覆盖的逻辑不适用于 dock1, |
| | | // 且该列表当前恒为 null,会在每条 OSD 上触发 NPE 并打印误导性告警,故 dock1 链直接跳过。 |
| | | // 容错:单个字段类型异常(如固件把 rid_state 上报成布尔)不应丢弃整帧 OSD, |
| | | // 解析失败时降级为共享接收器,保证状态、遥测、轨迹链路继续工作。 |
| | | OsdSubDeviceReceiver data; |
| | | try { |
| | | data = mapper.convertValue(receiver.getData(), Dock1OsdSubDeviceReceiver.class); |
| | | } catch (IllegalArgumentException e) { |
| | | log.warn("机场1 飞行器 OSD 解析失败,降级为共享接收器:{}", e.getMessage()); |
| | | data = mapper.convertValue(receiver.getData(), OsdSubDeviceReceiver.class); |
| | | } |
| | | //data.setChildDeviceSn(device.getDeviceSn()); |
| | | String dockOsd = ""; |
| | | receiver.setType(MessageType.STATUS.getType()+"-aircraft"); |
| | | // receiver.setData(data); |
| | | try { |
| | | dockOsd=mapper.writeValueAsString(receiver); |
| | | JsonNode jsonNode = mapper.readTree(dockOsd); |
| | | //在JSON的data下插入childDeviceSn,飞行器的sn |
| | | if(jsonNode.get("data") instanceof ObjectNode){ |
| | | ObjectNode objectNode = (ObjectNode) jsonNode.get("data"); |
| | | objectNode.put("childDeviceSn", device.getDeviceSn()); |
| | | } |
| | | if(jsonNode.has("data")){ |
| | | |
| | | if(jsonNode.get("data").has("mode_code")) { |
| | | Long code = jsonNode.get("data").get("mode_code").asLong(); |
| | | if ("0,1,2,3,4,5,8,9,10,11,13,14".contains(code.toString())) { |
| | | _deviceService.updateWorkType(device.getDeviceSn(), code); |
| | | RedisOpsUtils.set(RedisConst.OSD_STATUS_PREFIX + device.getDeviceSn(), code); |
| | | } |
| | | } |
| | | } |
| | | dockOsd=mapper.writeValueAsString(jsonNode); |
| | | } catch (NullPointerException | JsonProcessingException e) { |
| | | log.warn("Please remount the payload, or restart the drone. Otherwise the data of the payload will not be received."); |
| | | } |
| | | |
| | | RedisOpsUtils.setWithExpire(RedisConst.OSD_PREFIX + device.getDeviceSn(), data, RedisConst.DEVICE_ALIVE_SECOND); |
| | | wsMessage.getData().setHost(data); |
| | | |
| | | // 发送给websocket客户端 |
| | | _dockWebSocketHandler.sendMessageToSubscribers(device.getDeviceSn(), MessageType.STATUS.getType(), dockOsd, ""); |
| | | |
| | | // 将飞行器的OSD数据dockOsd入库;device.getDeviceSn()为飞行器sn,这里要机场的sn,试试receiver.getGateway(); |
| | | _waylineTrajectoryService.setList(receiver.getGateway(),"0", dockOsd); |
| | | |
| | | sendMessageService.sendBatch(webSessions, wsMessage); |
| | | this.pushTelemetryData(data, device.getDeviceSn()); |
| | | } |
| | | tsaService.handleOSD(receiver, device, webSessions, wsMessage); |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * 【机场1】从 dock2 原类 com.ard.sample.manage.service.impl.DockOSDServiceImpl 复制/独立适配(DJI Dock 1 / M30)。 |
| | | * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。 |
| | | */ |
| | | package com.ard.sample.dock1.manage.service.impl; |
| | | |
| | | import com.ard.common.core.redis.RedisConst; |
| | | import com.ard.common.core.redis.RedisOpsUtils; |
| | | import com.ard.sample.component.mqtt.model.CommonTopicReceiver; |
| | | import com.ard.sample.component.websocket.config.ConcurrentWebSocketSession; |
| | | import com.ard.sample.component.websocket.enums.MessageType; |
| | | import com.ard.sample.component.websocket.handler.DockWebSocketHandler; |
| | | import com.ard.sample.component.websocket.model.BizCodeEnum; |
| | | import com.ard.sample.component.websocket.model.CustomWebSocketMessage; |
| | | import com.ard.sample.manage.model.dto.DeviceDTO; |
| | | import com.ard.sample.manage.model.dto.TelemetryDTO; |
| | | import com.ard.sample.manage.model.enums.DeviceDomainEnum; |
| | | import com.ard.sample.dock1.manage.model.receiver.Dock1OsdDockReceiver; |
| | | import com.ard.sample.manage.service.IDeviceService; |
| | | import com.fasterxml.jackson.databind.JsonNode; |
| | | import com.fasterxml.jackson.databind.node.ObjectNode; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | import java.util.Collection; |
| | | import com.ard.sample.manage.service.impl.AbstractTSAService; |
| | | import com.ard.sample.manage.service.impl.DockOSDServiceImpl; |
| | | |
| | | /** |
| | | * @author sean |
| | | * @version 1.0 |
| | | * @date 2022/5/11 |
| | | */ |
| | | @Service |
| | | public class Dock1DockOSDServiceImpl extends AbstractTSAService { |
| | | |
| | | @Autowired |
| | | private DockWebSocketHandler _dockWebSocketHandler; |
| | | |
| | | @Autowired |
| | | private IDeviceService _deviceService; |
| | | |
| | | public Dock1DockOSDServiceImpl() { |
| | | super(null); |
| | | } |
| | | |
| | | |
| | | @Override |
| | | public void pushTelemetryData(Collection<ConcurrentWebSocketSession> sessions, |
| | | CustomWebSocketMessage<TelemetryDTO> message, Object Object) { |
| | | |
| | | } |
| | | |
| | | @Override |
| | | public void handleOSD(CommonTopicReceiver receiver, DeviceDTO device, |
| | | Collection<ConcurrentWebSocketSession> webSessions, |
| | | CustomWebSocketMessage<TelemetryDTO> wsMessage) { |
| | | |
| | | if (DeviceDomainEnum.DOCK.getVal() == device.getDomain()) { |
| | | wsMessage.setBizCode(BizCodeEnum.DOCK_OSD.getCode()); |
| | | Dock1OsdDockReceiver data = mapper.convertValue(receiver.getData(), Dock1OsdDockReceiver.class); |
| | | // receiver.setType(MessageType.STATUS.getType()); |
| | | String dockOsd = ""; |
| | | String dockOsdData = ""; |
| | | try { |
| | | dockOsdData = mapper.writeValueAsString(receiver.getData()); |
| | | if (dockOsdData.contains("flighttask_step_code")) { |
| | | JsonNode jsonNode = mapper.readTree(dockOsdData); |
| | | if (jsonNode.has("flighttask_step_code")) { |
| | | String deviceSn = device.getDeviceSn(); |
| | | Long code = jsonNode.get("flighttask_step_code").asLong(); |
| | | if (code != null) { |
| | | //只要状态 "0":"作业准备中","1":"飞行作业中","5":"任务空闲" |
| | | if (code == 0 || code == 1 || code == 5) { |
| | | _deviceService.updateWorkType(deviceSn, code); |
| | | RedisOpsUtils.set(RedisConst.OSD_STATUS_PREFIX + deviceSn, code); |
| | | } |
| | | } |
| | | } |
| | | receiver.setType(MessageType.STATUS.getType() + "-flighttask"); |
| | | } else if (dockOsdData.contains("network_state")) { |
| | | JsonNode jsonNode = mapper.readTree(dockOsdData); |
| | | // 机场1:Dock1 刚上线时 network_state 可能不带 sub_device(飞行器尚未入舱), |
| | | // 原写法直接 jsonNode.get("sub_device").has(...) 会抛 NPE,导致整帧 OSD 丢失。 |
| | | JsonNode subDeviceNode = jsonNode.get("sub_device"); |
| | | JsonNode droneInDockNode = jsonNode.get("drone_in_dock"); |
| | | if (subDeviceNode != null && !subDeviceNode.isNull() |
| | | && droneInDockNode != null && !droneInDockNode.isNull() |
| | | && subDeviceNode.has("device_online_status") |
| | | && subDeviceNode.has("device_sn")) { |
| | | String childDeviceSn = subDeviceNode.get("device_sn").asText(); |
| | | Long inDock = droneInDockNode.asLong(); |
| | | Long deviceOnlineStatus = subDeviceNode.get("device_online_status").asLong(); |
| | | if (inDock.equals(1L) && deviceOnlineStatus.equals(0L)) {// 飞行器在舱内且关机状态为6 |
| | | _deviceService.updateWorkType(childDeviceSn, 6L); |
| | | RedisOpsUtils.set(RedisConst.OSD_STATUS_PREFIX + device.getChildDeviceSn(), 6); |
| | | } |
| | | } |
| | | if (jsonNode.has("cover_state")) { |
| | | // 舱盖状态存redis |
| | | RedisOpsUtils.set(RedisConst.COVER_STATE_OSD_PREFIX + device.getDeviceSn(), jsonNode.get("cover_state").asInt()); |
| | | // //实时监控机场内外舱盖状态 |
| | | // Integer coverState = jsonNode.get("cover_state").asInt(); |
| | | // if(coverState != null && (coverState == 1 || coverState == 0)) { |
| | | // // 执行0和1的互转逻辑,结果赋值给变量(保持Integer类型) |
| | | // Integer newCoverState = coverState == 0 ? 1 : 0; |
| | | // _deviceService.setRedisCameraPosition(device.getDeviceSn(), newCoverState); |
| | | // } |
| | | |
| | | } |
| | | receiver.setType(MessageType.STATUS.getType() + "-network"); |
| | | } else if (dockOsdData.contains("job_number")) { |
| | | receiver.setType(MessageType.STATUS.getType() + "-job"); |
| | | } |
| | | dockOsd = mapper.writeValueAsString(receiver); |
| | | } catch (Exception e) { |
| | | e.printStackTrace(); |
| | | } |
| | | wsMessage.getData().setHost(data); |
| | | sendMessageService.sendBatch(webSessions, wsMessage); |
| | | // 发送给websocket客户端 |
| | | _dockWebSocketHandler.sendMessageToSubscribers(device.getDeviceSn(), MessageType.STATUS.getType(), dockOsd, ""); |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * 【机场1】航线准备异常通知消费者(flight_setup_exception_notify)。 |
| | | * Dock1/M30 在 flighttask_prepare 失败时会发送该事件;共享层原枚举未注册此方法, |
| | | * 消息会被路由到 default 通道后丢弃。这里新增独立通道与 dock1 消费者, |
| | | * 仅处理 Dock1 设备,机场2/3 收到同名事件时保持原行为(只记录日志)。 |
| | | */ |
| | | package com.ard.sample.dock1.manage.service.impl; |
| | | |
| | | import com.ard.sample.component.mqtt.model.ChannelName; |
| | | import com.ard.sample.component.mqtt.model.CommonTopicReceiver; |
| | | import com.ard.sample.component.websocket.enums.MessageType; |
| | | import com.ard.sample.component.websocket.handler.DockWebSocketHandler; |
| | | import com.ard.sample.manage.model.dto.DeviceDTO; |
| | | import com.ard.sample.manage.service.IDeviceService; |
| | | import com.fasterxml.jackson.databind.ObjectMapper; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.integration.annotation.ServiceActivator; |
| | | import org.springframework.messaging.MessageHeaders; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | import java.util.Objects; |
| | | import java.util.Optional; |
| | | |
| | | /** |
| | | * 机场1航线准备异常通知处理器。 |
| | | * |
| | | * @author ard |
| | | */ |
| | | @Slf4j |
| | | @Service |
| | | public class Dock1FlightSetupExceptionHandler { |
| | | |
| | | /** |
| | | * 机场1:Dock1 设备类型。 |
| | | */ |
| | | private static final int DOCK1_DEVICE_TYPE = 1; |
| | | |
| | | @Autowired |
| | | private IDeviceService deviceService; |
| | | |
| | | @Autowired |
| | | private DockWebSocketHandler dockWebSocketHandler; |
| | | |
| | | @Autowired |
| | | private ObjectMapper mapper; |
| | | |
| | | @ServiceActivator(inputChannel = ChannelName.INBOUND_EVENTS_FLIGHT_SETUP_EXCEPTION_NOTIFY) |
| | | public void handleFlightSetupException(CommonTopicReceiver receiver, MessageHeaders headers) { |
| | | String dockSn = receiver.getGateway(); |
| | | Optional<DeviceDTO> deviceOpt = deviceService.getDeviceBySn(dockSn); |
| | | if (deviceOpt.isEmpty() || !Objects.equals(DOCK1_DEVICE_TYPE, deviceOpt.get().getType())) { |
| | | // 机场2/3 的事件仍按原逻辑只记录日志,不改变其行为。 |
| | | log.info("非机场1设备的航线准备异常通知,忽略。sn={}, method={}, data={}", |
| | | dockSn, receiver.getMethod(), receiver.getData()); |
| | | return; |
| | | } |
| | | |
| | | log.warn("机场1航线准备异常:sn={}, bid={}, data={}", dockSn, receiver.getBid(), receiver.getData()); |
| | | try { |
| | | // 原样推送给前端,便于机场1页面展示准备失败原因。 |
| | | dockWebSocketHandler.sendMessageToSubscribers(dockSn, MessageType.STATUS.getType(), |
| | | mapper.writeValueAsString(receiver), ""); |
| | | } catch (Exception e) { |
| | | log.error("机场1航线准备异常通知推送失败:sn={}", dockSn, e); |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * 【机场1】从 dock2 原类 com.ard.sample.manage.service.impl.GatewayOSDServiceImpl 复制/独立适配(DJI Dock 1 / M30)。 |
| | | * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。 |
| | | */ |
| | | package com.ard.sample.dock1.manage.service.impl; |
| | | |
| | | import com.ard.sample.component.mqtt.model.CommonTopicReceiver; |
| | | import com.ard.sample.component.websocket.config.ConcurrentWebSocketSession; |
| | | import com.ard.sample.component.websocket.model.BizCodeEnum; |
| | | import com.ard.sample.component.websocket.model.CustomWebSocketMessage; |
| | | import com.ard.sample.manage.model.dto.DeviceDTO; |
| | | import com.ard.sample.manage.model.dto.TelemetryDTO; |
| | | import com.ard.sample.manage.model.dto.TelemetryDeviceDTO; |
| | | import com.ard.sample.manage.model.enums.DeviceDomainEnum; |
| | | import com.ard.sample.manage.model.receiver.OsdGatewayReceiver; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.beans.factory.annotation.Qualifier; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | import java.util.Collection; |
| | | import com.ard.sample.manage.service.impl.AbstractTSAService; |
| | | import com.ard.sample.manage.service.impl.GatewayOSDServiceImpl; |
| | | |
| | | /** |
| | | * @author sean |
| | | * @version 0.3 |
| | | * @date 2022/2/21 |
| | | */ |
| | | @Service |
| | | public class Dock1GatewayOSDServiceImpl extends AbstractTSAService { |
| | | |
| | | public Dock1GatewayOSDServiceImpl(@Autowired @Qualifier("dock1DeviceOSDServiceImpl") AbstractTSAService tsaService) { |
| | | super(tsaService); |
| | | } |
| | | |
| | | @Override |
| | | public void pushTelemetryData(Collection<ConcurrentWebSocketSession> sessions, |
| | | CustomWebSocketMessage<TelemetryDTO> message, Object osdData) { |
| | | if (osdData instanceof OsdGatewayReceiver) { |
| | | OsdGatewayReceiver data = (OsdGatewayReceiver) osdData; |
| | | TelemetryDTO telemetry = message.getData(); |
| | | telemetry.setHost(TelemetryDeviceDTO.builder() |
| | | .latitude(data.getLatitude()) |
| | | .longitude(data.getLongitude()) |
| | | .build()); |
| | | this.sendMessageService.sendBatch(sessions, message); |
| | | return; |
| | | } |
| | | tsaService.pushTelemetryData(sessions, message, osdData); |
| | | } |
| | | |
| | | @Override |
| | | public void handleOSD(CommonTopicReceiver receiver, DeviceDTO device, |
| | | Collection<ConcurrentWebSocketSession> webSessions, |
| | | CustomWebSocketMessage<TelemetryDTO> wsMessage) { |
| | | if (DeviceDomainEnum.GATEWAY.getVal() == device.getDomain()) { |
| | | |
| | | wsMessage.setBizCode(BizCodeEnum.GATEWAY_OSD.getCode()); |
| | | OsdGatewayReceiver data = mapper.convertValue(receiver.getData(), OsdGatewayReceiver.class); |
| | | wsMessage.getData().setHost(data); |
| | | |
| | | this.sendMessageService.sendBatch(webSessions, wsMessage); |
| | | |
| | | this.pushTelemetryData(data, device.getDeviceSn()); |
| | | return; |
| | | } |
| | | |
| | | tsaService.handleOSD(receiver, device, webSessions, wsMessage); |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * 【机场1】从 dock2 原类 com.ard.sample.manage.service.impl.LiveStreamServiceImpl 复制/独立适配(DJI Dock 1 / M30)。 |
| | | * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。 |
| | | */ |
| | | package com.ard.sample.dock1.manage.service.impl; |
| | | |
| | | import com.ard.sample.common.error.CommonErrorEnum; |
| | | import com.ard.sample.common.error.LiveErrorEnum; |
| | | import com.ard.sample.common.model.ResponseResult; |
| | | import com.ard.sample.component.mqtt.model.CommonTopicResponse; |
| | | import com.ard.sample.component.mqtt.model.ServiceReply; |
| | | import com.ard.sample.component.mqtt.service.IMessageSenderService; |
| | | import com.ard.common.core.redis.RedisConst; |
| | | import com.ard.common.core.redis.RedisOpsUtils; |
| | | import com.ard.sample.manage.model.dto.*; |
| | | import com.ard.sample.manage.model.enums.DeviceDomainEnum; |
| | | import com.ard.sample.manage.model.enums.LiveStreamMethodEnum; |
| | | import com.ard.sample.manage.model.enums.LiveUrlTypeEnum; |
| | | import com.ard.sample.manage.model.enums.LiveVideoQualityEnum; |
| | | import com.ard.sample.manage.model.param.DeviceQueryParam; |
| | | import com.ard.sample.manage.model.receiver.CapacityDeviceReceiver; |
| | | import com.ard.sample.manage.model.receiver.LiveCapacityReceiver; |
| | | import com.ard.sample.manage.service.ICapacityCameraService; |
| | | import com.ard.sample.manage.service.IDeviceService; |
| | | import com.ard.sample.dock1.manage.service.ILiveStreamService; |
| | | import com.ard.vtdu.config.MediamtxConfig; |
| | | import com.ard.vtdu.domain.ArdVtdu; |
| | | import com.ard.vtdu.service.IArdVtduService; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | import org.springframework.util.StringUtils; |
| | | |
| | | import javax.annotation.Resource; |
| | | import java.lang.reflect.Field; |
| | | import java.util.*; |
| | | import java.util.concurrent.ConcurrentHashMap; |
| | | import java.util.stream.Collectors; |
| | | |
| | | import static com.ard.sample.component.mqtt.model.TopicConst.*; |
| | | import com.ard.sample.manage.service.impl.LiveStreamServiceImpl; |
| | | |
| | | /** |
| | | * @author sean.zhou |
| | | * @version 0.1 |
| | | * @date 2021/11/22 |
| | | */ |
| | | @Slf4j |
| | | @Service |
| | | //@Transactional |
| | | public class Dock1LiveStreamServiceImpl implements ILiveStreamService { |
| | | |
| | | /** |
| | | * 机场1:Dock1 设备类型。 |
| | | */ |
| | | private static final int DOCK1_DOCK_TYPE = 1; |
| | | |
| | | /** |
| | | * 机场1:M30 / M30T 飞行器设备类型。 |
| | | */ |
| | | private static final int DOCK1_AIRCRAFT_TYPE = 67; |
| | | |
| | | @Autowired |
| | | private ICapacityCameraService capacityCameraService; |
| | | |
| | | @Autowired |
| | | private IDeviceService deviceService; |
| | | |
| | | @Autowired |
| | | private IMessageSenderService messageSender; |
| | | |
| | | @Resource |
| | | private IArdVtduService ardVtduService; |
| | | @Resource |
| | | private MediamtxConfig mediamtxConfig; |
| | | |
| | | @Override |
| | | public List<CapacityDeviceDTO> getLiveCapacity() { |
| | | |
| | | // Query all devices in this workspace. |
| | | List<DeviceDTO> devicesList = deviceService.getDevicesByParams( |
| | | DeviceQueryParam.builder() |
| | | .domains(List.of(DeviceDomainEnum.SUB_DEVICE.getVal(), DeviceDomainEnum.DOCK.getVal())) |
| | | .build()); |
| | | |
| | | // 机场1:只返回 Dock1(type=1)和 M30/M30T(type=67)的直播能力, |
| | | // 避免 /dock1/live/capacity 混入机场2/3 的设备。 |
| | | return devicesList.stream() |
| | | .filter(device -> Objects.equals(DOCK1_DOCK_TYPE, device.getType()) |
| | | || Objects.equals(DOCK1_AIRCRAFT_TYPE, device.getType())) |
| | | .filter(device -> RedisOpsUtils.checkExist(RedisConst.DEVICE_ONLINE_PREFIX + device.getDeviceSn())) |
| | | .map(device -> CapacityDeviceDTO.builder() |
| | | .name(Objects.requireNonNullElse(device.getNickname(), device.getDeviceName())) |
| | | .sn(device.getDeviceSn()) |
| | | .camerasList(capacityCameraService.getCapacityCameraByDeviceSn(device.getDeviceSn())) |
| | | .build()) |
| | | .collect(Collectors.toList()); |
| | | } |
| | | |
| | | @Override |
| | | public void saveLiveCapacity(LiveCapacityReceiver liveCapacityReceiver, Long timestamp) { |
| | | // Solve timing problems |
| | | if (liveCapacityReceiver == null) { |
| | | //2025-10-28 zwy,这里有个空指针异常问题,如果liveCapacityReceiver为空,则不应该执行下面的代码。 |
| | | //机场设备在刚开机时,发送的state状态时,直播能力前三次发送的是空,可能是由于刚开机,等待各设备开机,第四次为正常数据。 |
| | | return; |
| | | } |
| | | for (CapacityDeviceReceiver capacityDeviceReceiver : liveCapacityReceiver.getDeviceList()) { |
| | | long last = (long) Objects.requireNonNullElse( |
| | | RedisOpsUtils.get(RedisConst.LIVE_CAPACITY + capacityDeviceReceiver.getSn()), 0L); |
| | | if (last > timestamp) { |
| | | return; |
| | | } |
| | | capacityCameraService.saveCapacityCameraReceiverList( |
| | | capacityDeviceReceiver.getCameraList(), |
| | | capacityDeviceReceiver.getSn(), timestamp); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public ResponseResult liveStart(LiveTypeDTO liveParam) { |
| | | String rtmpPublishUrl = |
| | | "rtmp://" + mediamtxConfig.getPushHost() + ":" + mediamtxConfig.getRtmpPort() + "/" + liveParam.getVideoId(); |
| | | liveParam.setUrl(rtmpPublishUrl); |
| | | // Check if this lens is available live. |
| | | ResponseResult responseResult = this.checkBeforeLive(liveParam.getVideoId()); |
| | | if (ResponseResult.CODE_SUCCESS != responseResult.getCode()) { |
| | | return responseResult; |
| | | } |
| | | |
| | | // 将机场推流代码放到入库代码后面。 |
| | | DeviceDTO data = (DeviceDTO) responseResult.getData(); |
| | | // target topic |
| | | String respTopic = THING_MODEL_PRE + PRODUCT + |
| | | data.getDeviceSn() + SERVICES_SUF; |
| | | ServiceReply receiveReply=new ServiceReply(); |
| | | // ServiceReply receiveReply = this.publishLiveStart(respTopic, liveParam); |
| | | |
| | | // if (ResponseResult.CODE_SUCCESS != receiveReply.getResult()) { |
| | | // log.info("机场直播开启失败,错误码:{},错误描述:{}", receiveReply.getResult(), LiveErrorEnum.find(receiveReply.getResult()).getErrorMsg()); |
| | | // return ResponseResult.error(receiveReply.getResult(), LiveErrorEnum.find(receiveReply.getResult()).getErrorMsg()); |
| | | // } |
| | | |
| | | LiveUrlTypeEnum urlType = LiveUrlTypeEnum.find(liveParam.getUrlType()); |
| | | LiveDTO live = new LiveDTO(); |
| | | |
| | | switch (urlType) { |
| | | case RTMP: |
| | | String name = liveParam.getVideoId(); |
| | | // 拼接地址 |
| | | String rtspUrl = |
| | | "rtsp://" + mediamtxConfig.getPlayHost() + ":" + mediamtxConfig.getRtspPort() + "/" + name; // |
| | | // 拼接 webrtcUrl |
| | | String rtmpUrl = |
| | | "rtmp://" + mediamtxConfig.getPlayHost() + ":" + mediamtxConfig.getRtmpPort() + "/" + name; // |
| | | // 拼接 webrtcUrl |
| | | String protocol = mediamtxConfig.isHttps() ? "https://" : "http://"; |
| | | String webrtcUrl = |
| | | protocol + mediamtxConfig.getPlayHost() + ":" + mediamtxConfig.getWebrtcPort() + "/" + name; // 拼接 |
| | | // webrtcUrl |
| | | |
| | | //不管插入数据库成不成功,都返回地址 |
| | | live.setRtmpUrl(rtmpUrl); |
| | | live.setRtspUrl(rtspUrl); |
| | | live.setWebrtcUrl(webrtcUrl); |
| | | |
| | | // 如果没有,插入新的记录 |
| | | ArdVtdu vtdu = |
| | | ArdVtdu.builder().name(name).rtspSource("publisher").rtspUrl(rtspUrl).rtmpUrl(rtmpUrl).webrtcUrl(webrtcUrl).build(); |
| | | // 在停止直播时不删除Vtdu表记录,所以在开启直播时再插入会报错, |
| | | // 所以要先查询是否存在该记录,如果已经存在记录,直接返回直播地址 |
| | | // 查询是否已经存在该记录 |
| | | ArdVtdu ardVtdu = ardVtduService.selectArdVtduByName(vtdu.getName()); |
| | | int i = 0; |
| | | log.info("机场视频插入前vtdu:{}",vtdu); |
| | | if(ardVtdu == null){ |
| | | i = ardVtduService.insertArdVtdu(vtdu); // 保存到媒体表 |
| | | log.info("机场视频插入后 i:{}",i); |
| | | if (i > 0) { |
| | | live.setRtmpUrl(rtmpUrl); |
| | | live.setRtspUrl(rtspUrl); |
| | | live.setWebrtcUrl(webrtcUrl); |
| | | } |
| | | } |
| | | else{ |
| | | i = ardVtduService.updateArdVtdu(vtdu); |
| | | log.info("机场视频更新后 i:{}",i); |
| | | if (i > 0) { |
| | | live.setRtmpUrl(rtmpUrl); |
| | | live.setRtspUrl(rtspUrl); |
| | | live.setWebrtcUrl(webrtcUrl); |
| | | } |
| | | } |
| | | //int i = ardVtduService.insertArdVtdu(vtdu); // 保存到媒体表 |
| | | // if (i > 0) { |
| | | // live.setRtmpUrl(rtmpUrl); |
| | | // live.setRtspUrl(rtspUrl); |
| | | // live.setWebrtcUrl(webrtcUrl); |
| | | // } |
| | | receiveReply = this.publishLiveStart(respTopic, liveParam); |
| | | |
| | | if (ResponseResult.CODE_SUCCESS != receiveReply.getResult()) { |
| | | log.info("机场直播开启失败,错误码:{},错误描述:{}", receiveReply.getResult(), LiveErrorEnum.find(receiveReply.getResult()).getErrorMsg()); |
| | | return ResponseResult.error(receiveReply.getResult(), LiveErrorEnum.find(receiveReply.getResult()).getErrorMsg()); |
| | | } |
| | | break; |
| | | case RTSP: |
| | | receiveReply = this.publishLiveStart(respTopic, liveParam); |
| | | |
| | | if (ResponseResult.CODE_SUCCESS != receiveReply.getResult()) { |
| | | log.info("机场直播开启失败,错误码:{},错误描述:{}", receiveReply.getResult(), LiveErrorEnum.find(receiveReply.getResult()).getErrorMsg()); |
| | | return ResponseResult.error(receiveReply.getResult(), LiveErrorEnum.find(receiveReply.getResult()).getErrorMsg()); |
| | | } |
| | | Object url = Objects.requireNonNullElse(receiveReply.getOutput(), receiveReply.getInfo()); |
| | | this.resolveUrlUser(String.valueOf(url), live); |
| | | break; |
| | | case UNKNOWN: |
| | | return ResponseResult.error(LiveErrorEnum.URL_TYPE_NOT_SUPPORTED); |
| | | } |
| | | |
| | | |
| | | |
| | | // 开始直播时发送机场相机摄像头默认信息(舱外)到所有在线用户,只取机场sn,发机场的摄像头信息 |
| | | if(data.getChildDeviceSn()!=null) { |
| | | String sn = data.getDeviceSn(); |
| | | deviceService.setRedisCameraPosition(sn, 1); |
| | | } |
| | | return ResponseResult.success(live); |
| | | } |
| | | |
| | | @Override |
| | | public ResponseResult liveStop(String videoId) { |
| | | ResponseResult<DeviceDTO> responseResult = this.checkBeforeLive(videoId); |
| | | if (responseResult.getCode() != 0) { |
| | | return responseResult; |
| | | } |
| | | |
| | | String respTopic = THING_MODEL_PRE + PRODUCT + responseResult.getData().getDeviceSn() + SERVICES_SUF; |
| | | |
| | | ServiceReply receiveReply = this.publishLiveStop(respTopic, videoId); |
| | | if (receiveReply.getResult() != 0) { |
| | | log.info("videoId:{},直播停止失败,错误码:{},错误描述:{}", videoId, receiveReply.getResult(), LiveErrorEnum.find(receiveReply.getResult()).getErrorMsg()); |
| | | return ResponseResult.error(LiveErrorEnum.find(receiveReply.getResult())); |
| | | } |
| | | //注释掉删除流媒体管理库中的信息 |
| | | // ardVtduService.deleteArdVtduByName(videoId); |
| | | return ResponseResult.success(); |
| | | } |
| | | |
| | | @Override |
| | | public ResponseResult liveSetQuality(LiveTypeDTO liveParam) { |
| | | if (liveParam.getVideoQuality() == null || |
| | | LiveVideoQualityEnum.UNKNOWN == LiveVideoQualityEnum.find(liveParam.getVideoQuality())) { |
| | | return ResponseResult.error(LiveErrorEnum.ERROR_PARAMETERS); |
| | | } |
| | | |
| | | ResponseResult<DeviceDTO> responseResult = this.checkBeforeLive(liveParam.getVideoId()); |
| | | |
| | | if (responseResult.getCode() != 0) { |
| | | return responseResult; |
| | | } |
| | | |
| | | String respTopic = THING_MODEL_PRE + PRODUCT + responseResult.getData().getDeviceSn() + SERVICES_SUF; |
| | | |
| | | ServiceReply receiveReply = this.publishLiveSetQuality(respTopic, liveParam); |
| | | if (ResponseResult.CODE_SUCCESS != receiveReply.getResult()) { |
| | | return ResponseResult.error(LiveErrorEnum.find(receiveReply.getResult())); |
| | | } |
| | | |
| | | return ResponseResult.success(); |
| | | } |
| | | |
| | | @Override |
| | | public ResponseResult liveLensChange(LiveTypeDTO liveParam) { |
| | | // 机场1:C3 修订(2026-10-19)——原「单舱外相机」只针对机场相机切换(live_camera_change, |
| | | // 见下方 liveCameraChange);飞行器相机镜头(广角/变焦/红外)由 M30/M30T 支持, |
| | | // live_lens_change 恢复为与共享层一致的下发逻辑。 |
| | | log.info("切换镜头参数:{}", liveParam); |
| | | ResponseResult<DeviceDTO> responseResult = this.checkBeforeLive(liveParam.getVideoId()); |
| | | if (ResponseResult.CODE_SUCCESS != responseResult.getCode()) { |
| | | return responseResult; |
| | | } |
| | | if (DeviceDomainEnum.GATEWAY.getVal() == responseResult.getData().getDomain()) { |
| | | return ResponseResult.error(LiveErrorEnum.FUNCTION_NOT_SUPPORT); |
| | | } |
| | | |
| | | String respTopic = THING_MODEL_PRE + PRODUCT + responseResult.getData().getDeviceSn() + SERVICES_SUF; |
| | | |
| | | ServiceReply receiveReply = this.publishLiveLensChange(respTopic, liveParam); |
| | | |
| | | if (ResponseResult.CODE_SUCCESS != receiveReply.getResult()) { |
| | | Integer code = receiveReply.getResult(); |
| | | if (code == 325003) { |
| | | // 指令响应失败,请重试。低配机型不支持红外时返回该错误码 |
| | | return ResponseResult.error(325003000, LiveErrorEnum.COMMAND_FAILED.getErrorMsg() + "请确认无人机是否支持此功能!"); |
| | | } |
| | | return ResponseResult.error(LiveErrorEnum.find(receiveReply.getResult())); |
| | | } |
| | | |
| | | return ResponseResult.success(); |
| | | } |
| | | |
| | | @Override |
| | | public ResponseResult<List<String>> getLiveLens(String sn) { |
| | | List<String> videoTypes = new ArrayList<>(); |
| | | // 检查设备是否在线 |
| | | if (!deviceService.checkDeviceOnline(sn)) { |
| | | log.info("SN: {}-设备不在线,请检查网络连接!", sn); |
| | | // 抛弃出异常,给调用者处理 |
| | | //throw new Exception("设备不在线,请检查网络连接!"); |
| | | return ResponseResult.error(CommonErrorEnum.DEVICE_OFFLINE.getErrorCode(),CommonErrorEnum.DEVICE_OFFLINE.getErrorMsg()); |
| | | } |
| | | // 读取直播能力拼接参数 |
| | | List<CapacityDeviceDTO> devicesList = getLiveCapacity(); |
| | | if (devicesList == null || devicesList.isEmpty()) { |
| | | log.info("SN: {}-未获得设备直播能力!", sn); |
| | | // 抛弃出异常,给调用者处理 |
| | | //throw new Exception("未获得设备直播能力!"); |
| | | return ResponseResult.error(22222200,"未获得设备直播能力!"); |
| | | } |
| | | for (CapacityDeviceDTO device : devicesList) { |
| | | if (!device.getSn().equals(sn)) { |
| | | continue; |
| | | } |
| | | List<CapacityCameraDTO> camerasList = device.getCamerasList(); |
| | | if (camerasList == null || camerasList.isEmpty()) { |
| | | continue; |
| | | } |
| | | for (CapacityCameraDTO camera : camerasList) { |
| | | List<CapacityVideoDTO> videosList = camera.getVideosList(); |
| | | if (videosList == null || videosList.isEmpty()) { |
| | | continue; |
| | | } |
| | | // 机场1:M30 / M30T 相机(52-x-x / 53-x-x)优先返回 normal 码流的镜头类型, |
| | | // 避免循环取最后一条时默认落到红外;无 normal 码流时退回任意一条有效记录。 |
| | | // 其他机型保持原逻辑。 |
| | | boolean m30SeriesCamera = camera.getIndex() != null |
| | | && (camera.getIndex().startsWith("52-") || camera.getIndex().startsWith("53-")); |
| | | for (CapacityVideoDTO video : videosList) { |
| | | List<String> switchTypes = video.getSwitchVideoTypes(); |
| | | if (switchTypes == null || switchTypes.isEmpty()) { |
| | | continue; |
| | | } |
| | | if (m30SeriesCamera) { |
| | | if (video.getIndex() != null && video.getIndex().startsWith("normal")) { |
| | | videoTypes = switchTypes; |
| | | break; |
| | | } |
| | | if (videoTypes.isEmpty()) { |
| | | videoTypes = switchTypes; |
| | | } |
| | | continue; |
| | | } |
| | | // 机场3中飞行器,没有name,需要其他条件确定主摄像头参数 |
| | | if (switchTypes.size() == 1 && camera.getName() == null) { |
| | | continue; |
| | | } |
| | | videoTypes = switchTypes; |
| | | } |
| | | } |
| | | } |
| | | |
| | | return ResponseResult.success(videoTypes); |
| | | } |
| | | |
| | | @Override |
| | | public ResponseResult liveCameraChange(LiveTypeDTO liveParam) { |
| | | // 机场1:C3 dock1 为单舱外相机(camera_position 恒为 1),不支持切换相机,整个功能禁用。 |
| | | // 如需恢复:参考共享层 com.ard.sample.manage.service.impl.LiveStreamServiceImpl#liveCameraChange |
| | | log.info("机场1:liveCameraChange 已禁用(单舱外相机),忽略参数:{}", liveParam); |
| | | return ResponseResult.error(LiveErrorEnum.FUNCTION_NOT_SUPPORT); |
| | | } |
| | | |
| | | |
| | | // 机场1:C3 修订——liveLensChange 已恢复(飞行器镜头切换),本方法为实际下发入口 |
| | | private ServiceReply publishLiveLensChange(String respTopic, LiveTypeDTO liveParam) { |
| | | CommonTopicResponse<LiveTypeDTO> response = new CommonTopicResponse<>(); |
| | | response.setTid(UUID.randomUUID().toString()); |
| | | response.setBid(UUID.randomUUID().toString()); |
| | | response.setMethod(LiveStreamMethodEnum.LIVE_LENS_CHANGE.getMethod()); |
| | | response.setData(liveParam); |
| | | |
| | | return messageSender.publishWithReply(respTopic, response); |
| | | } |
| | | |
| | | // 机场1:C3 liveCameraChange 已禁用,本方法保留备用(如需恢复,见 liveCameraChange 方法头注释) |
| | | private ServiceReply publishLiveCameraChange(String respTopic, LiveTypeDTO liveParam) { |
| | | CommonTopicResponse<LiveTypeDTO> response = new CommonTopicResponse<>(); |
| | | response.setTid(UUID.randomUUID().toString()); |
| | | response.setBid(UUID.randomUUID().toString()); |
| | | response.setMethod(LiveStreamMethodEnum.LIVE_CAMERA_CHANGE.getMethod()); |
| | | response.setData(liveParam); |
| | | |
| | | return messageSender.publishWithReply(respTopic, response); |
| | | } |
| | | |
| | | /** |
| | | * Check if this lens is available live. |
| | | * |
| | | * @param videoId |
| | | * @return |
| | | */ |
| | | private ResponseResult<DeviceDTO> checkBeforeLive(String videoId) { |
| | | if (!StringUtils.hasText(videoId)) { |
| | | return ResponseResult.error(LiveErrorEnum.ERROR_PARAMETERS); |
| | | } |
| | | String[] videoIdArr = videoId.split("/"); |
| | | // drone sn / enumeration value of the location where the payload is mounted / payload lens |
| | | if (videoIdArr.length != 3) { |
| | | return ResponseResult.error(LiveErrorEnum.ERROR_PARAMETERS); |
| | | } |
| | | |
| | | Optional<DeviceDTO> deviceOpt = deviceService.getDeviceBySn(videoIdArr[0]); |
| | | // Check if the gateway device connected to this drone exists |
| | | if (deviceOpt.isEmpty()) { |
| | | return ResponseResult.error(LiveErrorEnum.NO_AIRCRAFT); |
| | | } |
| | | |
| | | if (DeviceDomainEnum.DOCK.getVal() == deviceOpt.get().getDomain()) { |
| | | if (!Objects.equals(DOCK1_DOCK_TYPE, deviceOpt.get().getType())) { |
| | | return ResponseResult.error(314013004, "该设备不属于机场1,请使用对应的机场接口操作!"); |
| | | } |
| | | return ResponseResult.success(deviceOpt.get()); |
| | | } |
| | | List<DeviceDTO> gatewayList = deviceService.getDevicesByParams( |
| | | DeviceQueryParam.builder() |
| | | .childSn(videoIdArr[0]) |
| | | .build()); |
| | | if (gatewayList.isEmpty()) { |
| | | return ResponseResult.error(LiveErrorEnum.NO_FLIGHT_CONTROL); |
| | | } |
| | | // 机场1:只允许 Dock1 下的飞行器走 /dock1 直播链路。 |
| | | if (!Objects.equals(DOCK1_DOCK_TYPE, gatewayList.get(0).getType())) { |
| | | return ResponseResult.error(314013004, "该设备不属于机场1,请使用对应的机场接口操作!"); |
| | | } |
| | | |
| | | return ResponseResult.success(gatewayList.get(0)); |
| | | } |
| | | |
| | | /** |
| | | * When using rtsp live, the account and password are parsed from the information returned by the pilot. |
| | | * |
| | | * @param url |
| | | * @param live |
| | | */ |
| | | private void resolveUrlUser(String url, LiveDTO live) { |
| | | if (!StringUtils.hasText(url)) { |
| | | return; |
| | | } |
| | | int start = url.indexOf("//"); |
| | | int end = url.lastIndexOf("@"); |
| | | String user = url.substring(start + 2, end); |
| | | |
| | | url = url.replace(user + "@", ""); |
| | | String[] userArr = user.split(":"); |
| | | // live.setUsername(userArr[0]); |
| | | // live.setPassword(userArr[1]); |
| | | // live.setUrl(url); |
| | | } |
| | | |
| | | /** |
| | | * When using GB28181 live, url parameters are resolved into objects. |
| | | * |
| | | * @param url |
| | | * @return |
| | | */ |
| | | private LiveUrlGB28181DTO urlToGB28181(String url) { |
| | | String[] arr = url.split("\\=|\\&"); |
| | | LiveUrlGB28181DTO gb28181 = new LiveUrlGB28181DTO(); |
| | | try { |
| | | Class<LiveUrlGB28181DTO> clazz = LiveUrlGB28181DTO.class; |
| | | for (int i = 0; i < arr.length - 1; i += 2) { |
| | | Field field = clazz.getDeclaredField(arr[i]); |
| | | field.setAccessible(true); |
| | | |
| | | if (field.getType().equals(Integer.class)) { |
| | | field.set(gb28181, Integer.valueOf(arr[i + 1])); |
| | | continue; |
| | | } |
| | | field.set(gb28181, arr[i + 1]); |
| | | } |
| | | } catch (NoSuchFieldException | IllegalAccessException e) { |
| | | e.printStackTrace(); |
| | | } |
| | | return gb28181; |
| | | } |
| | | |
| | | /** |
| | | * Send a message to the pilot via mqtt to start the live streaming. |
| | | * |
| | | * @param topic |
| | | * @param liveParam |
| | | * @return |
| | | */ |
| | | private ServiceReply publishLiveStart(String topic, LiveTypeDTO liveParam) { |
| | | CommonTopicResponse<LiveTypeDTO> response = new CommonTopicResponse<>(); |
| | | response.setTid(UUID.randomUUID().toString()); |
| | | response.setBid(UUID.randomUUID().toString()); |
| | | response.setData(liveParam); |
| | | response.setMethod(LiveStreamMethodEnum.LIVE_START_PUSH.getMethod()); |
| | | |
| | | return messageSender.publishWithReply(topic, response); |
| | | } |
| | | |
| | | /** |
| | | * Send a message to the pilot via mqtt to set quality. |
| | | * |
| | | * @param respTopic |
| | | * @param liveParam |
| | | * @return |
| | | */ |
| | | private ServiceReply publishLiveSetQuality(String respTopic, LiveTypeDTO liveParam) { |
| | | Map<String, Object> data = new ConcurrentHashMap<>(Map.of( |
| | | "video_id", liveParam.getVideoId(), |
| | | "video_quality", liveParam.getVideoQuality())); |
| | | CommonTopicResponse<Map<String, Object>> response = new CommonTopicResponse<>(); |
| | | response.setTid(UUID.randomUUID().toString()); |
| | | response.setBid(UUID.randomUUID().toString()); |
| | | response.setMethod(LiveStreamMethodEnum.LIVE_SET_QUALITY.getMethod()); |
| | | response.setData(data); |
| | | |
| | | return messageSender.publishWithReply(respTopic, response); |
| | | } |
| | | |
| | | /** |
| | | * Send a message to the pilot via mqtt to stop the live streaming. |
| | | * |
| | | * @param topic |
| | | * @param videoId |
| | | * @return |
| | | */ |
| | | private ServiceReply publishLiveStop(String topic, String videoId) { |
| | | Map<String, String> data = new ConcurrentHashMap<>(Map.of("video_id", videoId)); |
| | | CommonTopicResponse<Map<String, String>> response = new CommonTopicResponse<>(); |
| | | response.setTid(UUID.randomUUID().toString()); |
| | | response.setBid(UUID.randomUUID().toString()); |
| | | response.setData(data); |
| | | response.setMethod(LiveStreamMethodEnum.LIVE_STOP_PUSH.getMethod()); |
| | | |
| | | return messageSender.publishWithReply(topic, response); |
| | | } |
| | | |
| | | } |
| New file |
| | |
| | | /* |
| | | * 【机场1】从 dock2 原类 com.ard.sample.manage.service.impl.WaylineTrajectoryServiceImpl 复制/独立适配(DJI Dock 1 / M30)。 |
| | | * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。 |
| | | */ |
| | | package com.ard.sample.dock1.manage.service.impl; |
| | | |
| | | import cn.hutool.core.util.SerializeUtil; |
| | | import com.ard.common.core.text.Convert; |
| | | import com.ard.sample.component.mqtt.model.EventsReceiver; |
| | | import com.ard.sample.manage.mapper.IWaylineTrajectoryMapper; |
| | | import com.ard.sample.manage.model.dto.WaylineTrajectoryDTO; |
| | | import com.ard.sample.manage.model.entity.WaylineTrajectoryEntity; |
| | | import com.ard.sample.dock1.manage.service.IWaylineTrajectoryService; |
| | | import com.ard.sample.wayline.model.dto.WaylineJobDTO; |
| | | import com.ard.sample.wayline.model.dto.WaylineTaskProgressReceiver; |
| | | import com.ard.sample.dock1.wayline.service.IWaylineJobService; |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.fasterxml.jackson.core.type.TypeReference; |
| | | import com.fasterxml.jackson.databind.ObjectMapper; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.scheduling.annotation.Async; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | import javax.annotation.Resource; |
| | | import java.util.ArrayList; |
| | | import java.util.List; |
| | | import java.util.Optional; |
| | | import java.util.concurrent.ConcurrentHashMap; |
| | | import com.ard.sample.manage.service.impl.WaylineTrajectoryServiceImpl; |
| | | |
| | | @Service |
| | | @Slf4j |
| | | public class Dock1WaylineTrajectoryServiceImpl implements IWaylineTrajectoryService { |
| | | |
| | | @Resource |
| | | private IWaylineTrajectoryMapper _waylineTrajectoryMapper; |
| | | |
| | | @Autowired |
| | | private IWaylineJobService _waylineJobService; |
| | | |
| | | @Autowired |
| | | protected ObjectMapper mapper; |
| | | /** |
| | | * 轨迹点列表缓存 |
| | | */ |
| | | private final ConcurrentHashMap<String, List<String>> cacheTrajectoryMap = new ConcurrentHashMap<>(); |
| | | |
| | | /** |
| | | * 任务状态数据缓存 |
| | | */ |
| | | private final ConcurrentHashMap<String, List<String>> cacheTaskMap = new ConcurrentHashMap<>(); |
| | | |
| | | /** |
| | | * 给数据加个进度 |
| | | */ |
| | | // private final ConcurrentHashMap<String, String> cacheProgress = new ConcurrentHashMap<>(); |
| | | /** |
| | | * 分片保存阈值条数 |
| | | */ |
| | | private static final int CHUNK_SIZE = 300; |
| | | |
| | | /** |
| | | * 增加list数据,type为:0时,表示轨迹点列表;type为:1时,表示任务状态数据。 |
| | | * @param sn 设备序列号 |
| | | * @param type 类型 |
| | | * @param dataJson json数据 |
| | | */ |
| | | @Override |
| | | public void setList(String sn, String type, String dataJson) { |
| | | List<String> listTrajectory = cacheTrajectoryMap.computeIfAbsent(sn, k -> new ArrayList<>()); |
| | | List<String> listTask = cacheTaskMap.computeIfAbsent(sn, k -> new ArrayList<>()); |
| | | // String progress = cacheProgress.computeIfAbsent(sn, k -> "0"); |
| | | // 取进度值,就为了判断ok一个任务的结束 |
| | | // Integer pro = Convert.toInt(cacheProgress.get(sn)); |
| | | // 取jobid、status字段值 |
| | | String jobId = ""; |
| | | String isOK = ""; |
| | | if (type.equals("0")) { |
| | | // 飞行轨迹数据 |
| | | if(!listTask.isEmpty()) { |
| | | listTrajectory.add(dataJson); |
| | | log.info("SN:{} 轨迹点数据添加到列表缓存,当前列表数量:{}", sn, listTrajectory.size()); |
| | | } |
| | | else { |
| | | cacheTrajectoryMap.put(sn, new ArrayList<>()); |
| | | log.info("SN:{} 任务状态数据为空,当前无任务,不保存轨迹点数据", sn); |
| | | } |
| | | // log.info("SN:{} 轨迹点数据:{}", sn, dataJson); |
| | | } |
| | | if (type.equals("1")) { |
| | | // 任务状态数据 |
| | | listTask.add(dataJson); |
| | | log.info("SN:{} 任务状态数据添加到列表缓存,当前列表数量:{}", sn, listTask.size()); |
| | | } |
| | | // jobid、status字段赋值 |
| | | if (!listTask.isEmpty()) { |
| | | String json = listTask.get(listTask.size() - 1); |
| | | log.info("SN:{} 当前任务记录最后一条数据:{}", sn, json); |
| | | try { |
| | | EventsReceiver<WaylineTaskProgressReceiver> eventsReceiver = mapper.readValue(json, |
| | | new TypeReference<EventsReceiver<WaylineTaskProgressReceiver>>() { |
| | | }); |
| | | jobId = eventsReceiver.getOutput().getExt().getFlightId(); |
| | | isOK = eventsReceiver.getOutput().getStatus(); |
| | | |
| | | log.info("SN:{} 当前任务jobId:{},任务执行进度isOK:{}", sn, jobId, isOK); |
| | | |
| | | } catch (Exception e) { |
| | | log.error("SN:{} 解析任务状态JSON失败: {}", sn, e.getMessage()); |
| | | log.error("SN:{} JSON内容: {}", sn, json); |
| | | } |
| | | } |
| | | else { |
| | | // 当前任务状态数据为空时,清空缓存 |
| | | log.info("SN:{} 任务状态数据为空,当前无任务清空缓存", sn); |
| | | // 清空缓存 |
| | | cacheTrajectoryMap.put(sn, new ArrayList<>()); |
| | | cacheTaskMap.put(sn, new ArrayList<>()); |
| | | return; |
| | | } |
| | | // listTrajectory.size() >= CHUNK_SIZE || |
| | | if (listTask.size() >= CHUNK_SIZE) { |
| | | // 分片保存 |
| | | save(sn, jobId, "0"); |
| | | log.info("SN:{} 当前任务保存成功,jobId:{}, listTrajectory.size():{},listTask.size():{}", |
| | | sn, jobId, listTrajectory.size(), listTask.size()); |
| | | // log.info("当前任务保存成功,jobId:{},进度:{}",jobId , progress); |
| | | } else { |
| | | // 判断任务状态json中的status字段,如果为OK,则保存数据,一条完整的航线数据结束。 |
| | | try { |
| | | if (isOK.equals("ok")) { |
| | | String progress = "ok"; |
| | | if (getWaylineTrajectoryByProgress(jobId, progress)) { |
| | | log.info("SN:{} 当前任务已完成,不再保存数据!jobId:{},progress:{}", sn, jobId, progress); |
| | | // 清空缓存 |
| | | cacheTrajectoryMap.put(sn, new ArrayList<>()); |
| | | cacheTaskMap.put(sn, new ArrayList<>()); |
| | | return; |
| | | } |
| | | save(sn, jobId, progress); |
| | | log.info("SN:{} 当前任务保存成功,jobId:{},进度:{}", sn,jobId , progress); |
| | | } |
| | | } catch (Exception e) { |
| | | log.error("SN:{} json解析异常!{}", sn, e.getMessage()); |
| | | } |
| | | } |
| | | } |
| | | |
| | | @Async |
| | | public void save(String sn, String JobId, String progress) { |
| | | // List<String> trajectoryList = cacheTrajectoryMap.get(sn); |
| | | // List<String> taskList = cacheTaskMap.get(sn); |
| | | // String progress = cacheProgress.get(sn); |
| | | // 关键修复:原子性remove获取缓存(拿到当前所有数据)+ 清空缓存,避免异步竞态 |
| | | List<String> trajectoryList = cacheTrajectoryMap.remove(sn); |
| | | List<String> taskList = cacheTaskMap.remove(sn); |
| | | // 处理remove返回null的情况(缓存中无该sn) |
| | | trajectoryList = trajectoryList == null ? new ArrayList<>() : trajectoryList; |
| | | taskList = taskList == null ? new ArrayList<>() : taskList; |
| | | // 序列化前打印关键日志(确认拿到的数据) |
| | | log.info("SN:{} save方法原子性获取缓存 - 轨迹数据size:{},任务数据size:{}", |
| | | sn, trajectoryList.size(), taskList.size()); |
| | | |
| | | // 空数据直接返回 |
| | | if (trajectoryList.isEmpty() && taskList.isEmpty()) { |
| | | log.warn("SN:{} 原子性获取的缓存数据为空,无需保存!jobId:{}", sn, JobId); |
| | | return; |
| | | } |
| | | try { |
| | | log.info("SN:{} 开始保存轨迹点列表数据!jobId:{},轨迹数量:{},任务状态数量:{}", |
| | | sn, JobId, trajectoryList.size(), taskList.size()); |
| | | byte[] blobTrajectory = SerializeUtil.serialize(trajectoryList); |
| | | byte[] blobTask = SerializeUtil.serialize(taskList); |
| | | log.info("SN:{} 序列化结果 - 轨迹字节长度:{},任务字节长度:{}", |
| | | sn, blobTrajectory.length, blobTask.length); |
| | | |
| | | Optional<WaylineJobDTO> waylineJobDTO = _waylineJobService.getJobByJobId(JobId); |
| | | WaylineTrajectoryEntity TrajectoryEntity = new WaylineTrajectoryEntity(); |
| | | if(waylineJobDTO.isPresent()) { |
| | | TrajectoryEntity.setJobId(JobId); |
| | | TrajectoryEntity.setName(waylineJobDTO.get().getJobName()); |
| | | TrajectoryEntity.setFileId(waylineJobDTO.get().getFileId()); |
| | | TrajectoryEntity.setFileName(waylineJobDTO.get().getFileName()); |
| | | TrajectoryEntity.setDockSn(sn); |
| | | TrajectoryEntity.setProgress(progress); |
| | | TrajectoryEntity.setCreateTime(System.currentTimeMillis()); |
| | | TrajectoryEntity.setUpdateTime(System.currentTimeMillis()); |
| | | TrajectoryEntity.setTrajectoryList(blobTrajectory); |
| | | TrajectoryEntity.setTaskList(blobTask); |
| | | // 保存轨迹点列表数据 |
| | | int i = _waylineTrajectoryMapper.insert(TrajectoryEntity); |
| | | if (i > 0) { |
| | | log.info("保存轨迹点列表数据成功!jobId:{}", JobId); |
| | | // 清空缓存 |
| | | cacheTrajectoryMap.put(sn, new ArrayList<>()); |
| | | cacheTaskMap.put(sn, new ArrayList<>()); |
| | | // if (progress.equals("ok")) { |
| | | // cacheProgress.put(sn, "0"); |
| | | // } |
| | | } |
| | | } |
| | | } catch (Exception e) { |
| | | log.error("SN:{} 保存数据异常!jobId:{}, 异常信息:{}", sn, JobId, e.getMessage(), e); |
| | | // 异常时把数据放回缓存(避免数据丢失) |
| | | if (!trajectoryList.isEmpty()) { |
| | | cacheTrajectoryMap.computeIfAbsent(sn, k -> new ArrayList<>()).addAll(trajectoryList); |
| | | } |
| | | if (!taskList.isEmpty()) { |
| | | cacheTaskMap.computeIfAbsent(sn, k -> new ArrayList<>()).addAll(taskList); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 返回航线轨迹完整的数据,拼接的轨迹点列表和任务状态数据。 |
| | | * |
| | | * @param jobId 任务ID |
| | | * @return 航线轨迹完整的数据 |
| | | */ |
| | | @Override |
| | | public WaylineTrajectoryDTO getWayLineTrajectorybyJobID(String jobId) { |
| | | |
| | | List<WaylineTrajectoryEntity> waylineTrajectoryEntityList = new ArrayList<>(_waylineTrajectoryMapper.selectList(new LambdaQueryWrapper<WaylineTrajectoryEntity>() |
| | | .eq(jobId != null, WaylineTrajectoryEntity::getJobId, jobId) |
| | | .orderByAsc(WaylineTrajectoryEntity::getId))); |
| | | |
| | | return TrajectoryEntityConvertToDTO(waylineTrajectoryEntityList); |
| | | } |
| | | |
| | | /** |
| | | * 根据作业ID和进度获取航线轨迹 |
| | | * |
| | | * @param jobId 作业ID,可为null |
| | | * @param progress 进度值,可为null |
| | | * @return 当存在符合条件的航线轨迹时返回true,否则返回false |
| | | */ |
| | | public Boolean getWaylineTrajectoryByProgress(String jobId, String progress) { |
| | | List<WaylineTrajectoryEntity> waylineTrajectoryEntityList = new ArrayList<>(_waylineTrajectoryMapper.selectList(new LambdaQueryWrapper<WaylineTrajectoryEntity>() |
| | | .eq(jobId != null, WaylineTrajectoryEntity::getJobId, jobId) |
| | | .eq(progress != null, WaylineTrajectoryEntity::getProgress, progress) |
| | | .orderByAsc(WaylineTrajectoryEntity::getId))); |
| | | |
| | | return !waylineTrajectoryEntityList.isEmpty(); |
| | | } |
| | | |
| | | |
| | | public WaylineTrajectoryDTO TrajectoryEntityConvertToDTO(List<WaylineTrajectoryEntity> entity) { |
| | | if (entity == null) { |
| | | return null; |
| | | } |
| | | WaylineTrajectoryDTO dto = new WaylineTrajectoryDTO(); |
| | | try { |
| | | dto.setId(entity.get(0).getId()); |
| | | dto.setJobId(entity.get(0).getJobId()); |
| | | dto.setName(entity.get(0).getName()); |
| | | dto.setFileId(entity.get(0).getFileId()); |
| | | dto.setFileName(entity.get(0).getFileName()); |
| | | dto.setDockSn(entity.get(0).getDockSn()); |
| | | dto.setProgress(entity.get(0).getProgress()); |
| | | dto.setCreateTime(entity.get(0).getCreateTime()); |
| | | dto.setUpdateTime(entity.get(0).getUpdateTime()); |
| | | for (WaylineTrajectoryEntity e : entity) { |
| | | dto.getTrajectoryList().addAll(SerializeUtil.deserialize(e.getTrajectoryList(), List.class)); |
| | | dto.getTaskList().addAll(SerializeUtil.deserialize(e.getTaskList(), List.class)); |
| | | } |
| | | } |
| | | catch (Exception e) { |
| | | log.error("获取任务id轨迹点列表数据失败!{}", e.getMessage()); |
| | | } |
| | | |
| | | return dto; |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * 【机场1】从 dock2 原类 com.ard.sample.wayline.model.dto.WaylineTaskCreateDTO 复制/独立适配(DJI Dock 1 / M30)。 |
| | | * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。 |
| | | */ |
| | | package com.ard.sample.dock1.wayline.model.dto; |
| | | |
| | | import com.fasterxml.jackson.annotation.JsonInclude; |
| | | import com.fasterxml.jackson.annotation.JsonProperty; |
| | | import lombok.AllArgsConstructor; |
| | | import lombok.Builder; |
| | | import lombok.Data; |
| | | import lombok.NoArgsConstructor; |
| | | import com.ard.sample.wayline.model.dto.WaylineSimulateMissionDTO; |
| | | import com.ard.sample.wayline.model.dto.WaylineTaskCreateDTO; |
| | | import com.ard.sample.wayline.model.dto.WaylineTaskFileDTO; |
| | | |
| | | /** |
| | | * @author sean |
| | | * @version 1.1 |
| | | * @date 2022/6/1 |
| | | */ |
| | | // 序列化时忽略所有 null 字段 |
| | | @JsonInclude(JsonInclude.Include.NON_NULL) |
| | | @Data |
| | | @Builder |
| | | @AllArgsConstructor |
| | | @NoArgsConstructor |
| | | public class Dock1WaylineTaskCreateDTO { |
| | | |
| | | /** |
| | | * 计划 ID |
| | | * */ |
| | | @JsonProperty("flight_id") |
| | | private String flightId; |
| | | |
| | | /** |
| | | * 任务类型 "0":"立即任务","1":"定时任务","2":"条件任务" |
| | | * 立即任务和定时任务均由execute_time指定执行时间; |
| | | * 条件任务支持ready_conditions字段指定任务就绪条件, |
| | | * 设备可在指定时间段内满足就绪条件后即可执行; |
| | | * 立即任务媒体上传优先级最高, |
| | | * 定时任务和条件任务媒体上传优先级相同 |
| | | * */ |
| | | @JsonProperty("task_type") |
| | | private Integer taskType; |
| | | |
| | | // 机场1:C4 dock1(M30)官方无 wayline_precision_type(waylineType)字段,已剔除不下发 |
| | | |
| | | /** |
| | | * 开始执行时间 |
| | | * 任务开始执行时间毫秒时间戳。可选字段。当 task_type 为 0 或 1 时必填,为 2 时非必填。 |
| | | * */ |
| | | @JsonProperty("execute_time") |
| | | private Long executeTime; |
| | | |
| | | /** |
| | | * 航线文件对象 |
| | | * */ |
| | | @JsonProperty("file") |
| | | private WaylineTaskFileDTO file; |
| | | |
| | | /** |
| | | * 返航高度 "max":1500,"min":20,"unit_name":"米 / m" |
| | | * */ |
| | | @JsonProperty("rth_altitude") |
| | | private Integer rthAltitude; |
| | | |
| | | // 机场1:C4 dock1(M30)官方无 rth_mode(rthMode)字段,已剔除不下发 |
| | | |
| | | /** |
| | | * 遥控器失控动作 "0":"返航","1":"悬停","2":"降落" |
| | | * 失控动作,当前固定传的值是 0,即返航。注意该枚举值定义跟飞控跟机场定义的不一致,机场端会进行转换。 |
| | | * */ |
| | | @JsonProperty("out_of_control_action") |
| | | private Integer outOfControlAction; |
| | | |
| | | // @JsonProperty("simulate_mission") |
| | | // private WaylineSimulateMissionDTO simulateMission; |
| | | } |
| New file |
| | |
| | | /* |
| | | * 【机场1】从 dock2 原类 com.ard.sample.wayline.service.IFlightTaskService 复制/独立适配(DJI Dock 1 / M30)。 |
| | | * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。 |
| | | */ |
| | | package com.ard.sample.dock1.wayline.service; |
| | | |
| | | import com.ard.sample.component.mqtt.model.CommonTopicReceiver; |
| | | import org.springframework.messaging.MessageHeaders; |
| | | |
| | | /** |
| | | * @author sean |
| | | * @version 1.1 |
| | | * @date 2022/6/9 |
| | | */ |
| | | public interface IFlightTaskService { |
| | | |
| | | /** |
| | | * Handle the progress messages of the flight tasks reported by the dock. |
| | | * @param receiver |
| | | */ |
| | | void handleProgress(CommonTopicReceiver receiver, MessageHeaders headers); |
| | | } |
| New file |
| | |
| | | /* |
| | | * 【机场1】从 dock2 原类 com.ard.sample.wayline.service.IWaylineFileService 复制/独立适配(DJI Dock 1 / M30)。 |
| | | * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。 |
| | | */ |
| | | package com.ard.sample.dock1.wayline.service; |
| | | |
| | | import com.ard.sample.common.model.PaginationData; |
| | | import com.ard.sample.wayline.model.dto.WaylineFileDTO; |
| | | import com.ard.sample.wayline.model.param.WaylineQueryParam; |
| | | import org.springframework.web.multipart.MultipartFile; |
| | | |
| | | import java.net.URL; |
| | | import java.sql.SQLException; |
| | | import java.util.List; |
| | | import java.util.Optional; |
| | | |
| | | /** |
| | | * @author sean |
| | | * @version 0.3 |
| | | * @date 2021/12/22 |
| | | */ |
| | | public interface IWaylineFileService { |
| | | |
| | | /** |
| | | * Perform paging queries based on query parameters. |
| | | * @param param |
| | | * @return |
| | | */ |
| | | PaginationData<WaylineFileDTO> getWaylinesByParam(WaylineQueryParam param); |
| | | |
| | | /** |
| | | * Query the information of this wayline file according to the wayline file id. |
| | | * @param waylineId |
| | | * @return |
| | | */ |
| | | Optional<WaylineFileDTO> getWaylineByWaylineId(String waylineId); |
| | | |
| | | /** |
| | | * Get the download address of the file object. |
| | | * @param waylineId |
| | | * @return |
| | | */ |
| | | URL getObjectUrl(String waylineId) throws SQLException; |
| | | |
| | | /** |
| | | * Save the basic information of the wayline file. |
| | | * @param metadata |
| | | * @return |
| | | */ |
| | | Integer saveWaylineFile(WaylineFileDTO metadata); |
| | | |
| | | /** |
| | | * Updates whether the file is collected or not based on the passed parameters. |
| | | * @param ids wayline id |
| | | * @param isFavorite Whether the wayline file is favorited or not. |
| | | * @return |
| | | */ |
| | | Boolean markFavorite(List<String> ids, Boolean isFavorite); |
| | | |
| | | /** |
| | | * Batch query for duplicate file names in workspace. |
| | | * @param names |
| | | * @return |
| | | */ |
| | | List<String> getDuplicateNames(List<String> names); |
| | | |
| | | /** |
| | | * Delete the wayline file based on the wayline id. |
| | | * @param waylineId |
| | | */ |
| | | Boolean deleteByWaylineId(String waylineId); |
| | | |
| | | /** |
| | | * Import kmz wayline file. |
| | | * @param file |
| | | * @param creator |
| | | * @return |
| | | */ |
| | | void importKmzFile(MultipartFile file, String creator); |
| | | } |
| New file |
| | |
| | | /* |
| | | * 【机场1】从 dock2 原类 com.ard.sample.wayline.service.IWaylineJobService 复制/独立适配(DJI Dock 1 / M30)。 |
| | | * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。 |
| | | */ |
| | | package com.ard.sample.dock1.wayline.service; |
| | | |
| | | import com.ard.common.core.domain.model.LoginUser; |
| | | import com.ard.sample.common.model.PaginationData; |
| | | import com.ard.sample.common.model.ResponseResult; |
| | | import com.ard.sample.component.mqtt.model.CommonTopicReceiver; |
| | | import com.ard.sample.wayline.model.dto.WaylineJobDTO; |
| | | import com.ard.sample.wayline.model.enums.WaylineJobStatusEnum; |
| | | import com.ard.sample.wayline.model.param.CreateJobParam; |
| | | import org.springframework.messaging.MessageHeaders; |
| | | import java.sql.SQLException; |
| | | import java.util.Collection; |
| | | import java.util.List; |
| | | import java.util.Optional; |
| | | |
| | | /** |
| | | * @author sean |
| | | * @version 1.1 |
| | | * @date 2022/6/1 |
| | | */ |
| | | public interface IWaylineJobService { |
| | | |
| | | /** |
| | | * Create wayline job in the database. |
| | | * @param param |
| | | * @param username user info |
| | | * @param beginTime The time the job started. |
| | | * @param endTime The time the job ended. |
| | | * @return |
| | | */ |
| | | Optional<WaylineJobDTO> createWaylineJob(CreateJobParam param, String username, Long beginTime, Long endTime); |
| | | |
| | | /** |
| | | * Create a sub-task based on the information of the parent task. |
| | | |
| | | * @param parentId |
| | | * @return |
| | | */ |
| | | Optional<WaylineJobDTO> createWaylineJobByParent(String parentId); |
| | | |
| | | /** |
| | | * Issue wayline mission to the dock. |
| | | * @param param |
| | | * @param loginUser user info |
| | | * @return |
| | | */ |
| | | ResponseResult publishFlightTask(CreateJobParam param, LoginUser loginUser) throws SQLException; |
| | | |
| | | ResponseResult publishFlightTaskPrepare(Optional<WaylineJobDTO> waylineJobOpt) throws SQLException; |
| | | |
| | | /** |
| | | * Execute the task immediately. |
| | | * @param jobId |
| | | * @throws SQLException |
| | | * @return |
| | | */ |
| | | Boolean executeFlightTask(String jobId); |
| | | |
| | | /** |
| | | * Cancel the task Base on job Ids. |
| | | * @param jobId |
| | | * @throws SQLException |
| | | */ |
| | | ResponseResult cancelFlightTask(Collection<String> jobId); |
| | | // void cancelFlightTask(Collection<String> jobIds); |
| | | |
| | | /** |
| | | * Cancel the dock tasks that have been issued but have not yet been executed. |
| | | * @param dockSn |
| | | * @param jobIds |
| | | */ |
| | | ResponseResult publishCancelTask(String dockSn, List<String> jobIds); |
| | | |
| | | /** |
| | | * 删除任务 |
| | | * */ |
| | | void deleteTask(String id); |
| | | |
| | | /** |
| | | * Query wayline jobs based on conditions. |
| | | * @param jobIds |
| | | * @param status |
| | | * @return |
| | | */ |
| | | List<WaylineJobDTO> getJobsByConditions( Collection<String> jobIds, WaylineJobStatusEnum status); |
| | | |
| | | /** |
| | | * Query job information based on job id. |
| | | * @param jobId |
| | | * @return job information |
| | | */ |
| | | Optional<WaylineJobDTO> getJobByJobId(String jobId); |
| | | |
| | | /** |
| | | * Update job data. |
| | | * @param dto |
| | | * @return |
| | | */ |
| | | Boolean updateJob(WaylineJobDTO dto); |
| | | |
| | | /** |
| | | * Paginate through all jobs in this workspace. |
| | | * @param page |
| | | * @param pageSize |
| | | * @return |
| | | */ |
| | | PaginationData<WaylineJobDTO> getJobsByWorkspaceId(long page, long pageSize); |
| | | |
| | | PaginationData<WaylineJobDTO> getJobsList(long page, long pageSize, Integer status, |
| | | Long startTime, Long endTime, Integer taskType, |
| | | String name, Boolean isComplete); |
| | | |
| | | /** |
| | | * Process to get interface data of flight mission resources. |
| | | * @param receiver |
| | | * @param headers |
| | | */ |
| | | void flightTaskResourceGet(CommonTopicReceiver receiver, MessageHeaders headers); |
| | | |
| | | /** |
| | | * Set the media files for this job to upload immediately. |
| | | * @param jobId |
| | | */ |
| | | void uploadMediaHighestPriority(String jobId); |
| | | } |
| New file |
| | |
| | | /* |
| | | * 【机场1】从 dock2 原类 com.ard.sample.wayline.service.impl.FlightTaskServiceImpl 复制/独立适配(DJI Dock 1 / M30)。 |
| | | * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。 |
| | | */ |
| | | package com.ard.sample.dock1.wayline.service.impl; |
| | | |
| | | import com.ard.common.utils.uuid.IdUtils; |
| | | import com.ard.route.domain.route.ArdRouteInfo; |
| | | import com.ard.route.service.IArdRouteInfoService; |
| | | import com.ard.route.service.IArdRouteMissionService; |
| | | import com.ard.sample.common.error.CommonErrorEnum; |
| | | import com.ard.sample.common.model.ResponseResult; |
| | | import com.ard.sample.component.mqtt.model.*; |
| | | import com.ard.sample.component.mqtt.service.IMessageSenderService; |
| | | import com.ard.common.core.redis.RedisConst; |
| | | import com.ard.common.core.redis.RedisOpsUtils; |
| | | import com.ard.sample.component.websocket.enums.MessageType; |
| | | import com.ard.sample.component.websocket.handler.DockWebSocketHandler; |
| | | import com.ard.sample.component.websocket.model.BizCodeEnum; |
| | | import com.ard.sample.component.websocket.model.CustomWebSocketMessage; |
| | | import com.ard.sample.component.websocket.service.ISendMessageService; |
| | | import com.ard.sample.component.websocket.service.IWebSocketManageService; |
| | | import com.ard.sample.manage.model.dto.DeviceDTO; |
| | | import com.ard.sample.manage.model.enums.UserTypeEnum; |
| | | // 机场1:任务状态(type="1")与轨迹点(type="0")必须写入同一个共享轨迹服务缓存。 |
| | | import com.ard.sample.manage.service.IWaylineTrajectoryService; |
| | | import com.ard.sample.media.model.MediaFileCountDTO; |
| | | import com.ard.sample.wayline.model.dto.WaylineJobDTO; |
| | | import com.ard.sample.wayline.model.dto.WaylineTaskProgressExt; |
| | | import com.ard.sample.wayline.model.dto.WaylineTaskProgressReceiver; |
| | | import com.ard.sample.wayline.model.entity.WaylineJobHistoryEntity; |
| | | import com.ard.sample.wayline.model.enums.WaylineJobStatusEnum; |
| | | import com.ard.sample.dock1.wayline.service.IFlightTaskService; |
| | | import com.ard.sample.wayline.service.IWaylineJobHistoryService; |
| | | import com.ard.sample.dock1.wayline.service.IWaylineJobService; |
| | | import com.fasterxml.jackson.core.type.TypeReference; |
| | | import com.fasterxml.jackson.databind.ObjectMapper; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.apache.http.HttpStatus; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.integration.mqtt.support.MqttHeaders; |
| | | import org.springframework.messaging.MessageHeaders; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | import java.time.LocalDateTime; |
| | | import java.util.HashMap; |
| | | import java.util.Map; |
| | | import java.util.Objects; |
| | | import java.util.Optional; |
| | | import java.util.concurrent.TimeUnit; |
| | | import com.ard.sample.wayline.service.impl.FlightTaskServiceImpl; |
| | | |
| | | /** |
| | | * @author sean |
| | | * @version 1.1 |
| | | * @date 2022/6/9 |
| | | */ |
| | | @Service |
| | | @Slf4j |
| | | public class Dock1FlightTaskServiceImpl implements IFlightTaskService { |
| | | |
| | | @Autowired |
| | | private IMessageSenderService messageSender; |
| | | |
| | | @Autowired |
| | | private ObjectMapper mapper; |
| | | |
| | | @Autowired |
| | | private DockWebSocketHandler _dockWebSocketHandler; |
| | | |
| | | @Autowired |
| | | private ISendMessageService websocketMessageService; |
| | | |
| | | @Autowired |
| | | private IWebSocketManageService webSocketManageService; |
| | | |
| | | @Autowired |
| | | private IWaylineJobService waylineJobService; |
| | | |
| | | @Autowired |
| | | private IWaylineTrajectoryService _waylineTrajectoryService; |
| | | |
| | | @Autowired |
| | | private IWaylineJobHistoryService _waylineJobHistoryService; |
| | | |
| | | @Autowired |
| | | private IArdRouteMissionService _ardRouteMissionService; |
| | | |
| | | @Autowired |
| | | private IArdRouteInfoService _ardRouteInfoService; |
| | | |
| | | @Override |
| | | // 机场1:不复制原 ServiceActivator(inputChannel = ChannelName.INBOUND_EVENTS_FLIGHT_TASK_PROGRESS),消费职责由共享层原类承担 |
| | | public void handleProgress(CommonTopicReceiver receiver, MessageHeaders headers) { |
| | | String receivedTopic = String.valueOf(headers.get(MqttHeaders.RECEIVED_TOPIC)); |
| | | String dockSn = receivedTopic.substring((TopicConst.THING_MODEL_PRE + TopicConst.PRODUCT).length(), |
| | | receivedTopic.indexOf(TopicConst.EVENTS_SUF)); |
| | | EventsReceiver<WaylineTaskProgressReceiver> eventsReceiver = mapper.convertValue(receiver.getData(), |
| | | new TypeReference<EventsReceiver<WaylineTaskProgressReceiver>>() { |
| | | }); |
| | | eventsReceiver.setBid(receiver.getBid()); |
| | | eventsReceiver.setSn(receiver.getGateway()); |
| | | eventsReceiver.setGateway(receiver.getGateway()); |
| | | eventsReceiver.setType(MessageType.STATUS.getType() + "-flighttask_progress"); |
| | | eventsReceiver.setMethod(EventsMethodEnum.FLIGHT_TASK_PROGRESS.getMethod()); |
| | | |
| | | |
| | | WaylineTaskProgressReceiver output = eventsReceiver.getOutput(); |
| | | |
| | | log.info("Task progress: {}", output.getProgress().toString()); |
| | | |
| | | if (eventsReceiver.getResult() != ResponseResult.CODE_SUCCESS) { |
| | | String errorMsg = CommonErrorEnum.getMsgByCode(eventsReceiver.getResult()); |
| | | log.error("Task progress ===> Error code: {},Error Msg: {}", eventsReceiver.getResult(), errorMsg); |
| | | } |
| | | |
| | | EventsResultStatusEnum statusEnum = EventsResultStatusEnum.find(output.getStatus()); |
| | | String key = RedisConst.WAYLINE_JOB_RUNNING_PREFIX + dockSn; |
| | | RedisOpsUtils.setWithExpire(key, eventsReceiver, RedisConst.DEVICE_ALIVE_SECOND * RedisConst.DEVICE_ALIVE_SECOND); |
| | | |
| | | if (statusEnum.getEnd()) { |
| | | WaylineJobDTO job = WaylineJobDTO.builder() |
| | | .jobId(receiver.getBid()) |
| | | .status(WaylineJobStatusEnum.SUCCESS.getVal()) |
| | | .completedTime(LocalDateTime.now()) |
| | | .mediaCount(output.getExt().getMediaCount()) |
| | | .build(); |
| | | |
| | | // record the update of the media count. |
| | | if (Objects.nonNull(job.getMediaCount()) && job.getMediaCount() != 0) { |
| | | RedisOpsUtils.hashSet(RedisConst.MEDIA_FILE_PREFIX + receiver.getGateway(), job.getJobId(), |
| | | MediaFileCountDTO.builder().jobId(receiver.getBid()).mediaCount(job.getMediaCount()).uploadedCount(0).build()); |
| | | } |
| | | |
| | | if (EventsResultStatusEnum.OK != statusEnum) { |
| | | job.setCode(eventsReceiver.getResult()); |
| | | job.setStatus(WaylineJobStatusEnum.FAILED.getVal()); |
| | | } |
| | | |
| | | waylineJobService.updateJob(job); |
| | | RedisOpsUtils.del(key); |
| | | RedisOpsUtils.del(RedisConst.WAYLINE_JOB_PAUSED_PREFIX + receiver.getBid()); |
| | | } |
| | | |
| | | DeviceDTO device = (DeviceDTO) RedisOpsUtils.get(RedisConst.DEVICE_ONLINE_PREFIX + receiver.getGateway()); |
| | | websocketMessageService.sendBatch( |
| | | webSocketManageService.getValueAndUserType(UserTypeEnum.WEB.getVal()), |
| | | CustomWebSocketMessage.builder() |
| | | .data(eventsReceiver) |
| | | .timestamp(System.currentTimeMillis()) |
| | | .bizCode(BizCodeEnum.FLIGHT_TASK_PROGRESS.getCode()) |
| | | .build()); |
| | | |
| | | if (receiver.getNeedReply() != null && receiver.getNeedReply() == 1) { |
| | | messageSender.publish(receivedTopic + TopicConst._REPLY_SUF, |
| | | CommonTopicResponse.builder() |
| | | .tid(receiver.getTid()) |
| | | .bid(receiver.getBid()) |
| | | .method(EventsMethodEnum.FLIGHT_TASK_PROGRESS.getMethod()) |
| | | .timestamp(System.currentTimeMillis()) |
| | | .data(RequestsReply.success()) |
| | | .build()); |
| | | } |
| | | // 进度 |
| | | // if (eventsReceiver.getOutput().getProgress().getPercent() >= 95) { |
| | | // return; |
| | | // } |
| | | |
| | | |
| | | |
| | | String json = ""; |
| | | try { |
| | | json = mapper.writeValueAsString(eventsReceiver); |
| | | } catch (Exception e) { |
| | | e.printStackTrace(); |
| | | } |
| | | _dockWebSocketHandler.sendMessageToSubscribers(dockSn, MessageType.STATUS.getType(), json, ""); |
| | | |
| | | _waylineTrajectoryService.setList(dockSn,"1",json); |
| | | |
| | | // 计划任务成功后,保存计划任务到历史记录 |
| | | String status = eventsReceiver.getOutput().getStatus(); |
| | | String jobId = eventsReceiver.getOutput().getExt().getFlightId(); |
| | | if(status.equals("ok") && jobId != null) { |
| | | Optional<WaylineJobDTO> job = waylineJobService.getJobByJobId(jobId); |
| | | if(job.isPresent()) { |
| | | if (_waylineJobHistoryService.getJobByJobId(jobId).isPresent()) { |
| | | return; |
| | | } |
| | | WaylineJobHistoryEntity waylineJobHistoryEntity = _waylineJobHistoryService.dto2Entity(job.get()); |
| | | Optional<WaylineJobDTO> jobHistory = _waylineJobHistoryService.createWaylineJob(waylineJobHistoryEntity); |
| | | if(jobHistory.isPresent()) { |
| | | log.info("任务执行成功,保存到历史记录。 {}", jobHistory); |
| | | } |
| | | // 同步更新4.2的计划任务的状态;等待夏吉建的接口。1:已执行 0:未执行 |
| | | // 在任务中查找航线文件id,取航线文件信息中的状态,是3的修改状态 |
| | | ArdRouteInfo ardRouteInfo = _ardRouteInfoService.selectArdRouteInfoById(job.get().getFileId()); |
| | | if(ardRouteInfo != null && ardRouteInfo.getType().equals("3")) { |
| | | _ardRouteMissionService.updateArdRouteMissionStatus(jobId, "1"); |
| | | log.info("任务航线-执行成功,更新航线状态。 jobId:{}", jobId); |
| | | } |
| | | } |
| | | |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 定时检查并处理航线定时任务 |
| | | * <p> |
| | | * 该方法每5秒执行一次,从Redis有序集合中获取最早需要执行的任务,检查其执行时间: |
| | | * 1. 若任务已过期(超过当前时间30秒),则标记为失败并删除 |
| | | * 2. 若任务在允许的时间窗口内(当前时间±30秒),则执行任务并删除 |
| | | * </p> |
| | | * |
| | | * <p>任务ID格式:{workspace_id}:{dock_sn}:{job_id}</p> |
| | | */ |
| | | // 机场1:不复制原 Scheduled 定时任务,定时职责由共享层原类承担 |
| | | private void checkScheduledJob() { |
| | | Object jobIdValue = RedisOpsUtils.zGetMin(RedisConst.WAYLINE_JOB_TIMED_EXECUTE); |
| | | if (Objects.isNull(jobIdValue)) { |
| | | return; |
| | | } |
| | | log.info("检查 wayline 的定时任务。 {}", jobIdValue); |
| | | // format: {workspace_id}:{dock_sn}:{job_id} |
| | | String[] jobArr = String.valueOf(jobIdValue).split(RedisConst.DELIMITER); |
| | | double time = RedisOpsUtils.zScore(RedisConst.WAYLINE_JOB_TIMED_EXECUTE, jobIdValue); |
| | | // region 取定时任务信息websocket发给前端 |
| | | // 定时任务执行前,判断有没有任务正在执行,在执行则不发送websocket消息 |
| | | String key = RedisConst.WAYLINE_JOB_RUNNING_PREFIX + jobArr[0]; |
| | | if (!RedisOpsUtils.checkExist(key)) { |
| | | // 取到待执行的定时任务ID,获取任务信息websocket发给前端 |
| | | // pending status状态待执行 |
| | | EventsReceiver<WaylineTaskProgressReceiver> eventsReceiver = new EventsReceiver<>(); |
| | | WaylineTaskProgressReceiver waylineTaskProgressReceiver = new WaylineTaskProgressReceiver(); |
| | | WaylineTaskProgressExt waylineTaskProgressExt = new WaylineTaskProgressExt(); |
| | | waylineTaskProgressExt.setFlightId(jobArr[1]); |
| | | waylineTaskProgressReceiver.setStatus("pending"); |
| | | waylineTaskProgressReceiver.setExt(waylineTaskProgressExt); |
| | | eventsReceiver.setOutput(waylineTaskProgressReceiver); |
| | | eventsReceiver.setResult(ResponseResult.CODE_SUCCESS); |
| | | eventsReceiver.setBid(IdUtils.simpleUUID()); |
| | | eventsReceiver.setSn(jobArr[0]); |
| | | eventsReceiver.setGateway(jobArr[0]); |
| | | eventsReceiver.setType(MessageType.STATUS.getType() + "-flighttask_progress"); |
| | | eventsReceiver.setMethod(EventsMethodEnum.FLIGHT_TASK_PROGRESS.getMethod()); |
| | | String json = ""; |
| | | try { |
| | | json = mapper.writeValueAsString(eventsReceiver); |
| | | } catch (Exception e) { |
| | | e.printStackTrace(); |
| | | } |
| | | _dockWebSocketHandler.sendMessageToSubscribers(jobArr[0], MessageType.STATUS.getType(), json, ""); |
| | | log.info("定时任务自定义状态为待执行,json: {}", json); |
| | | } |
| | | // endregion |
| | | long now = System.currentTimeMillis(); |
| | | int offset = 30_000; |
| | | |
| | | // 过期任务会直接删除。 |
| | | if (time < now - offset) { |
| | | log.info("定时任务超时,jobId: {}", jobArr[1]); |
| | | RedisOpsUtils.zRemove(RedisConst.WAYLINE_JOB_TIMED_EXECUTE, jobIdValue); |
| | | waylineJobService.updateJob(WaylineJobDTO.builder() |
| | | .jobId(jobArr[1]) |
| | | .status(WaylineJobStatusEnum.FAILED.getVal()) |
| | | .executeTime(LocalDateTime.now()) |
| | | .completedTime(LocalDateTime.now()) |
| | | .code(HttpStatus.SC_REQUEST_TIMEOUT).build()); |
| | | return; |
| | | } |
| | | |
| | | if (now <= time && time <= now + offset) { |
| | | try { |
| | | waylineJobService.executeFlightTask(jobArr[1]); |
| | | } catch (Exception e) { |
| | | log.info("计划中的任务交付失败了。jobId: {},异常信息:{}", jobArr[1], e.getMessage()); |
| | | waylineJobService.updateJob(WaylineJobDTO.builder() |
| | | .jobId(jobArr[1]) |
| | | .status(WaylineJobStatusEnum.FAILED.getVal()) |
| | | .executeTime(LocalDateTime.now()) |
| | | .completedTime(LocalDateTime.now()) |
| | | .code(HttpStatus.SC_INTERNAL_SERVER_ERROR).build()); |
| | | } finally { |
| | | RedisOpsUtils.zRemove(RedisConst.WAYLINE_JOB_TIMED_EXECUTE, jobIdValue); |
| | | } |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * 【机场1】从 dock2 原类 com.ard.sample.wayline.service.impl.WaylineFileServiceImpl 复制/独立适配(DJI Dock 1 / M30)。 |
| | | * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。 |
| | | */ |
| | | package com.ard.sample.dock1.wayline.service.impl; |
| | | |
| | | import com.ard.common.config.OssConfiguration; |
| | | import com.ard.oss.service.impl.OssServiceContext; |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; |
| | | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
| | | import com.ard.common.core.page.PageDomain; |
| | | import com.ard.common.core.page.TableSupport; |
| | | import com.ard.sample.common.model.Pagination; |
| | | import com.ard.sample.common.model.PaginationData; |
| | | import com.ard.sample.manage.model.enums.DeviceDomainEnum; |
| | | import com.ard.sample.wayline.mapper.IWaylineFileMapper; |
| | | import com.ard.sample.wayline.model.dto.KmzFileProperties; |
| | | import com.ard.sample.wayline.model.dto.WaylineFileDTO; |
| | | import com.ard.sample.wayline.model.entity.WaylineFileEntity; |
| | | import com.ard.sample.wayline.model.param.WaylineQueryParam; |
| | | import com.ard.sample.dock1.wayline.service.IWaylineFileService; |
| | | import org.dom4j.Document; |
| | | import org.dom4j.DocumentException; |
| | | import org.dom4j.Node; |
| | | import org.dom4j.io.SAXReader; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | import org.springframework.util.DigestUtils; |
| | | import org.springframework.util.StringUtils; |
| | | import org.springframework.web.multipart.MultipartFile; |
| | | |
| | | import java.io.File; |
| | | import java.io.IOException; |
| | | import java.io.InputStream; |
| | | import java.net.URL; |
| | | import java.nio.charset.StandardCharsets; |
| | | import java.sql.SQLException; |
| | | import java.util.*; |
| | | import java.util.stream.Collectors; |
| | | import java.util.zip.ZipEntry; |
| | | import java.util.zip.ZipInputStream; |
| | | |
| | | import static com.ard.sample.wayline.model.dto.KmzFileProperties.WAYLINE_FILE_SUFFIX; |
| | | import com.ard.sample.wayline.service.impl.WaylineFileServiceImpl; |
| | | |
| | | /** |
| | | * @author sean |
| | | * @version 0.3 |
| | | * @date 2021/12/22 |
| | | */ |
| | | @Service |
| | | @Transactional |
| | | public class Dock1WaylineFileServiceImpl implements IWaylineFileService { |
| | | |
| | | @Autowired |
| | | private IWaylineFileMapper mapper; |
| | | |
| | | @Autowired |
| | | private OssServiceContext ossService; |
| | | |
| | | @Override |
| | | public PaginationData<WaylineFileDTO> getWaylinesByParam(WaylineQueryParam param) { |
| | | PageDomain pageDomain = TableSupport.buildPageRequest(); // 从请求获取分页参数 |
| | | // Paging Query |
| | | Page<WaylineFileEntity> page = mapper.selectPage( |
| | | new Page<WaylineFileEntity>(pageDomain.getPageNum(), pageDomain.getPageSize()), |
| | | new LambdaQueryWrapper<WaylineFileEntity>() |
| | | .eq(param.isFavorited(), WaylineFileEntity::getFavorited, param.isFavorited()) |
| | | .and(param.getTemplateType() != null, wrapper -> { |
| | | for (Integer type : param.getTemplateType()) { |
| | | wrapper.like(WaylineFileEntity::getTemplateTypes, type).or(); |
| | | } |
| | | }) |
| | | // There is a risk of SQL injection |
| | | .last(StringUtils.hasText(param.getOrderBy()), " order by " + param.getOrderBy()) |
| | | ); |
| | | |
| | | // Wrap the results of a paging query into a custom paging object. |
| | | List<WaylineFileDTO> records = page.getRecords() |
| | | .stream() |
| | | .map(this::entityConvertToDTO) |
| | | .collect(Collectors.toList()); |
| | | |
| | | return new PaginationData<>(records, new Pagination(page)); |
| | | } |
| | | |
| | | @Override |
| | | public Optional<WaylineFileDTO> getWaylineByWaylineId(String waylineId) { |
| | | return Optional.ofNullable( |
| | | this.entityConvertToDTO( |
| | | mapper.selectOne( |
| | | new LambdaQueryWrapper<WaylineFileEntity>() |
| | | .eq(WaylineFileEntity::getWaylineId, waylineId)))); |
| | | } |
| | | |
| | | @Override |
| | | public URL getObjectUrl(String waylineId) throws SQLException { |
| | | Optional<WaylineFileDTO> waylineOpt = this.getWaylineByWaylineId(waylineId); |
| | | if (waylineOpt.isEmpty()) { |
| | | throw new SQLException(waylineId + " does not exist."); |
| | | } |
| | | return ossService.getObjectUrl(OssConfiguration.bucket, waylineOpt.get().getObjectKey()); |
| | | } |
| | | |
| | | @Override |
| | | public Integer saveWaylineFile(WaylineFileDTO metadata) { |
| | | WaylineFileEntity file = this.dtoConvertToEntity(metadata); |
| | | file.setWaylineId(UUID.randomUUID().toString()); |
| | | if (!StringUtils.hasText(file.getSign())) { |
| | | try (InputStream object = ossService.getObject(OssConfiguration.bucket, metadata.getObjectKey())) { |
| | | if (object.available() == 0) { |
| | | throw new RuntimeException("The file " + metadata.getObjectKey() + |
| | | " does not exist in the bucket[" + OssConfiguration.bucket + "]."); |
| | | } |
| | | file.setSign(DigestUtils.md5DigestAsHex(object)); |
| | | } catch (IOException e) { |
| | | e.printStackTrace(); |
| | | } |
| | | } |
| | | int insertId = mapper.insert(file); |
| | | return insertId > 0 ? file.getId() : insertId; |
| | | } |
| | | |
| | | @Override |
| | | public Boolean markFavorite(List<String> waylineIds, Boolean isFavorite) { |
| | | if (waylineIds.isEmpty()) { |
| | | return false; |
| | | } |
| | | if (isFavorite == null) { |
| | | return true; |
| | | } |
| | | return mapper.update(null, new LambdaUpdateWrapper<WaylineFileEntity>() |
| | | .set(WaylineFileEntity::getFavorited, isFavorite) |
| | | .in(WaylineFileEntity::getWaylineId, waylineIds)) > 0; |
| | | } |
| | | |
| | | @Override |
| | | public List<String> getDuplicateNames(List<String> names) { |
| | | return mapper.selectList(new LambdaQueryWrapper<WaylineFileEntity>() |
| | | .in(WaylineFileEntity::getName, names)) |
| | | .stream() |
| | | .map(WaylineFileEntity::getName) |
| | | .collect(Collectors.toList()); |
| | | } |
| | | |
| | | @Override |
| | | public Boolean deleteByWaylineId(String waylineId) { |
| | | Optional<WaylineFileDTO> waylineOpt = this.getWaylineByWaylineId(waylineId); |
| | | if (waylineOpt.isEmpty()) { |
| | | return true; |
| | | } |
| | | WaylineFileDTO wayline = waylineOpt.get(); |
| | | boolean isDel = mapper.delete(new LambdaUpdateWrapper<WaylineFileEntity>() |
| | | .eq(WaylineFileEntity::getWaylineId, waylineId)) |
| | | > 0; |
| | | if (!isDel) { |
| | | return false; |
| | | } |
| | | return ossService.deleteObject(OssConfiguration.bucket, wayline.getObjectKey()); |
| | | } |
| | | |
| | | @Override |
| | | public void importKmzFile(MultipartFile file, String creator) { |
| | | Optional<WaylineFileDTO> waylineFileOpt = validKmzFile(file); |
| | | if (waylineFileOpt.isEmpty()) { |
| | | throw new RuntimeException("The file format is incorrect."); |
| | | } |
| | | |
| | | try { |
| | | WaylineFileDTO waylineFile = waylineFileOpt.get(); |
| | | waylineFile.setUsername(creator); |
| | | // 使用 try-with-resources 确保流关闭 |
| | | try (InputStream input = file.getInputStream()) { |
| | | ossService.putObject(OssConfiguration.bucket, waylineFile.getObjectKey(), input); |
| | | } |
| | | this.saveWaylineFile(waylineFile); |
| | | } catch (IOException e) { |
| | | e.printStackTrace(); |
| | | } |
| | | } |
| | | |
| | | private Optional<WaylineFileDTO> validKmzFile(MultipartFile file) { |
| | | String filename = file.getOriginalFilename(); |
| | | if (Objects.nonNull(filename) && !filename.endsWith(WAYLINE_FILE_SUFFIX)) { |
| | | throw new RuntimeException("The file format is incorrect."); |
| | | } |
| | | try (ZipInputStream unzipFile = new ZipInputStream(file.getInputStream(), StandardCharsets.UTF_8)) { |
| | | |
| | | ZipEntry nextEntry = unzipFile.getNextEntry(); |
| | | while (Objects.nonNull(nextEntry)) { |
| | | boolean isWaylines = (KmzFileProperties.FILE_DIR_FIRST + "/" + KmzFileProperties.FILE_DIR_SECOND_WAYLINES).equals(nextEntry.getName()); |
| | | if (!isWaylines) { |
| | | nextEntry = unzipFile.getNextEntry(); |
| | | continue; |
| | | } |
| | | SAXReader reader = new SAXReader(); |
| | | Document document = reader.read(unzipFile); |
| | | if (!StandardCharsets.UTF_8.name().equals(document.getXMLEncoding())) { |
| | | throw new RuntimeException("The file encoding format is incorrect."); |
| | | } |
| | | |
| | | Node droneNode = document.selectSingleNode("//" + KmzFileProperties.TAG_WPML_PREFIX + KmzFileProperties.TAG_DRONE_INFO); |
| | | Node payloadNode = document.selectSingleNode("//" + KmzFileProperties.TAG_WPML_PREFIX + KmzFileProperties.TAG_PAYLOAD_INFO); |
| | | if (Objects.isNull(droneNode) || Objects.isNull(payloadNode)) { |
| | | throw new RuntimeException("The file format is incorrect."); |
| | | } |
| | | |
| | | String type = droneNode.valueOf(KmzFileProperties.TAG_WPML_PREFIX + KmzFileProperties.TAG_DRONE_ENUM_VALUE); |
| | | String subType = droneNode.valueOf(KmzFileProperties.TAG_WPML_PREFIX + KmzFileProperties.TAG_DRONE_SUB_ENUM_VALUE); |
| | | String payloadType = payloadNode.valueOf(KmzFileProperties.TAG_WPML_PREFIX + KmzFileProperties.TAG_PAYLOAD_ENUM_VALUE); |
| | | String payloadSubType = payloadNode.valueOf(KmzFileProperties.TAG_WPML_PREFIX + KmzFileProperties.TAG_PAYLOAD_SUB_ENUM_VALUE); |
| | | String templateId = document.valueOf("//" + KmzFileProperties.TAG_WPML_PREFIX + KmzFileProperties.TAG_TEMPLATE_ID); |
| | | |
| | | if (!StringUtils.hasText(type) || !StringUtils.hasText(subType) || |
| | | !StringUtils.hasText(payloadSubType) || !StringUtils.hasText(payloadType) || |
| | | !StringUtils.hasText(templateId)) { |
| | | throw new RuntimeException("The file format is incorrect."); |
| | | } |
| | | |
| | | return Optional.of(WaylineFileDTO.builder() |
| | | .droneModelKey(String.format("%s-%s-%s", DeviceDomainEnum.SUB_DEVICE.getVal(), type, subType)) |
| | | .payloadModelKeys(List.of(String.format("%s-%s-%s",DeviceDomainEnum.PAYLOAD.getVal(), payloadType, payloadSubType))) |
| | | .objectKey(OssConfiguration.objectDirPrefix + File.separator + filename) |
| | | .name(filename.substring(0, filename.lastIndexOf(WAYLINE_FILE_SUFFIX))) |
| | | .sign(DigestUtils.md5DigestAsHex(file.getInputStream())) |
| | | .templateTypes(List.of(Integer.parseInt(templateId))) |
| | | .build()); |
| | | } |
| | | |
| | | } catch (IOException | DocumentException e) { |
| | | e.printStackTrace(); |
| | | } |
| | | return Optional.empty(); |
| | | } |
| | | /** |
| | | * Convert database entity objects into wayline data transfer object. |
| | | * @param entity |
| | | * @return |
| | | */ |
| | | private WaylineFileDTO entityConvertToDTO(WaylineFileEntity entity) { |
| | | if (entity == null) { |
| | | return null; |
| | | } |
| | | return WaylineFileDTO.builder() |
| | | .droneModelKey(entity.getDroneModelKey()) |
| | | .favorited(entity.getFavorited()) |
| | | .name(entity.getName()) |
| | | .payloadModelKeys(entity.getPayloadModelKeys() != null ? |
| | | Arrays.asList(entity.getPayloadModelKeys().split(",")) : null) |
| | | .templateTypes(Arrays.stream(entity.getTemplateTypes().split(",")) |
| | | .map(Integer::parseInt) |
| | | .collect(Collectors.toList())) |
| | | .username(entity.getUsername()) |
| | | .objectKey(entity.getObjectKey()) |
| | | .sign(entity.getSign()) |
| | | .updateTime(entity.getUpdateTime()) |
| | | .waylineId(entity.getWaylineId()) |
| | | .build(); |
| | | |
| | | } |
| | | |
| | | /** |
| | | * Convert the received wayline object into a database entity object. |
| | | * @param file |
| | | * @return |
| | | */ |
| | | private WaylineFileEntity dtoConvertToEntity(WaylineFileDTO file) { |
| | | WaylineFileEntity.WaylineFileEntityBuilder builder = WaylineFileEntity.builder(); |
| | | |
| | | if (file != null) { |
| | | builder.droneModelKey(file.getDroneModelKey()) |
| | | .name(file.getName()) |
| | | .username(file.getUsername()) |
| | | .objectKey(file.getObjectKey()) |
| | | // Separate multiple payload data with ",". |
| | | .payloadModelKeys(String.join(",", file.getPayloadModelKeys())) |
| | | .templateTypes(file.getTemplateTypes().stream() |
| | | .map(String::valueOf) |
| | | .collect(Collectors.joining(","))) |
| | | .favorited(file.getFavorited()) |
| | | .sign(file.getSign()) |
| | | .build(); |
| | | } |
| | | |
| | | return builder.build(); |
| | | } |
| | | } |
| New file |
| | |
| | | /* |
| | | * 【机场1】从 dock2 原类 com.ard.sample.wayline.service.impl.WaylineJobServiceImpl 复制/独立适配(DJI Dock 1 / M30)。 |
| | | * 与 dock2 原类同服务共存;本文件内的适配差异以「// 机场1:」内联注释标出。 |
| | | */ |
| | | package com.ard.sample.dock1.wayline.service.impl; |
| | | |
| | | import com.ard.common.config.OssConfiguration; |
| | | import com.ard.oss.service.impl.OssServiceContext; |
| | | import com.ard.route.domain.route.ArdRouteInfo; |
| | | import com.ard.route.service.IArdRouteInfoService; |
| | | import com.ard.sample.config.DockConfiguration; |
| | | import com.ard.sample.utils.TimestampUtils; |
| | | import com.baomidou.mybatisplus.core.conditions.Wrapper; |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; |
| | | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
| | | import com.ard.common.core.domain.model.LoginUser; |
| | | import com.ard.sample.common.error.CommonErrorEnum; |
| | | import com.ard.sample.common.model.Pagination; |
| | | import com.ard.sample.common.model.PaginationData; |
| | | import com.ard.sample.common.model.ResponseResult; |
| | | import com.ard.sample.component.mqtt.model.*; |
| | | import com.ard.sample.component.mqtt.service.IMessageSenderService; |
| | | import com.ard.common.core.redis.RedisConst; |
| | | import com.ard.common.core.redis.RedisOpsUtils; |
| | | import com.ard.sample.manage.model.dto.DeviceDTO; |
| | | import com.ard.sample.manage.model.enums.DeviceDomainEnum; |
| | | import com.ard.sample.manage.model.param.DeviceQueryParam; |
| | | import com.ard.sample.manage.service.IDeviceService; |
| | | import com.ard.sample.media.model.MediaFileCountDTO; |
| | | import com.ard.sample.media.model.MediaMethodEnum; |
| | | import com.ard.sample.media.service.IFileService; |
| | | import com.ard.sample.wayline.mapper.IWaylineJobMapper; |
| | | import com.ard.sample.wayline.model.dto.*; |
| | | import com.ard.sample.wayline.model.entity.WaylineJobEntity; |
| | | import com.ard.sample.wayline.model.enums.WaylineJobStatusEnum; |
| | | import com.ard.sample.wayline.model.enums.WaylineMethodEnum; |
| | | import com.ard.sample.wayline.model.enums.WaylineTaskTypeEnum; |
| | | import com.ard.sample.wayline.model.param.CreateJobParam; |
| | | import com.ard.sample.dock1.wayline.service.IWaylineJobService; |
| | | import com.fasterxml.jackson.core.type.TypeReference; |
| | | import com.fasterxml.jackson.databind.ObjectMapper; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.apache.commons.codec.digest.DigestUtils; |
| | | import org.apache.ibatis.annotations.Param; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.integration.mqtt.support.MqttHeaders; |
| | | import org.springframework.messaging.MessageHeaders; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.transaction.annotation.Isolation; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | import org.springframework.util.CollectionUtils; |
| | | import org.springframework.util.StringUtils; |
| | | |
| | | import javax.annotation.Resource; |
| | | import java.io.IOException; |
| | | import java.io.InputStream; |
| | | import java.net.URL; |
| | | import java.sql.SQLException; |
| | | import java.time.Instant; |
| | | import java.time.LocalDateTime; |
| | | import java.time.ZoneId; |
| | | import java.util.*; |
| | | import java.util.stream.Collectors; |
| | | |
| | | import static com.ard.route.util.ToolUtils.kmzPath; |
| | | import static com.ard.route.util.ToolUtils.subStringMinioUrlName; |
| | | import com.ard.sample.dock1.wayline.model.dto.Dock1WaylineTaskCreateDTO; |
| | | import com.ard.sample.wayline.service.impl.WaylineJobServiceImpl; |
| | | |
| | | /** |
| | | * @author sean |
| | | * @version 1.1 |
| | | * @date 2022/6/1 |
| | | */ |
| | | @Service |
| | | @Transactional |
| | | @Slf4j |
| | | @SuppressWarnings("all") |
| | | public class Dock1WaylineJobServiceImpl implements IWaylineJobService { |
| | | |
| | | /** |
| | | * 机场1 设备类型(DJI Dock 1)。 |
| | | */ |
| | | private static final int DOCK1_DEVICE_TYPE = 1; |
| | | |
| | | @Autowired |
| | | private IWaylineJobMapper mapper; |
| | | |
| | | // @Autowired |
| | | // private IWaylineFileService waylineFileService; |
| | | @Autowired |
| | | private TimestampUtils _timestampUtils; |
| | | |
| | | @Autowired |
| | | private IArdRouteInfoService _ardRouteInfoService; |
| | | |
| | | @Resource |
| | | private OssServiceContext _ossService; |
| | | |
| | | @Autowired |
| | | private IDeviceService deviceService; |
| | | |
| | | @Autowired |
| | | private IMessageSenderService messageSender; |
| | | |
| | | @Autowired |
| | | private ObjectMapper objectMapper; |
| | | |
| | | @Autowired |
| | | private IFileService fileService; |
| | | |
| | | /** |
| | | * 插入航路作业 |
| | | * |
| | | * @param jobEntity 航路作业实体 |
| | | * @return 航路作业DTO的Optional包装对象,如果插入失败则返回Optional.empty() |
| | | */ |
| | | private Optional<WaylineJobDTO> insertWaylineJob(WaylineJobEntity jobEntity) { |
| | | int id = mapper.insert(jobEntity); |
| | | if (id <= 0) { |
| | | return Optional.empty(); |
| | | } |
| | | return Optional.ofNullable(this.entity2Dto(jobEntity)); |
| | | } |
| | | |
| | | /** |
| | | * 创建航线任务 |
| | | * |
| | | * @param param 任务参数 |
| | | * @param username 用户名 |
| | | * @param beginTime 开始时间 |
| | | * @param endTime 结束时间 |
| | | * @return Optional<WaylineJobDTO> 返回创建的航线任务对象,若创建失败则返回Optional.empty() |
| | | */ |
| | | @Override |
| | | public Optional<WaylineJobDTO> createWaylineJob(CreateJobParam param, String username, Long beginTime, Long endTime) { |
| | | if (Objects.isNull(param)) { |
| | | return Optional.empty(); |
| | | } |
| | | // Immediate tasks, allocating time on the backend. |
| | | WaylineJobEntity jobEntity = WaylineJobEntity.builder() |
| | | .name(param.getName()) |
| | | .dockSn(param.getDockSn()) |
| | | .fileId(param.getFileId()) |
| | | .username(username) |
| | | .jobId(UUID.randomUUID().toString()) |
| | | .beginTime(beginTime) |
| | | .endTime(endTime) |
| | | .status(WaylineJobStatusEnum.PENDING.getVal()) |
| | | .taskType(param.getTaskType()) |
| | | .waylineType(param.getWaylineType()) |
| | | .outOfControlAction(param.getOutOfControlAction()) |
| | | .rthAltitude(param.getRthAltitude()) |
| | | .rthMode(param.getRth_mode()) |
| | | .mediaCount(0) |
| | | .parentId(param.getParentId()) |
| | | .loopSum(param.getLoopSum()) |
| | | .build(); |
| | | |
| | | return insertWaylineJob(jobEntity); |
| | | } |
| | | |
| | | /** |
| | | * 根据父作业ID创建新的作业 |
| | | * |
| | | * @param parentId 父作业ID |
| | | * @return 新创建的作业的DTO对象,如果父作业不存在,则返回Optional.empty() |
| | | */ |
| | | @Override |
| | | public Optional<WaylineJobDTO> createWaylineJobByParent(String parentId) { |
| | | Optional<WaylineJobDTO> parentJobOpt = this.getJobByJobId(parentId); |
| | | if (parentJobOpt.isEmpty()) { |
| | | return Optional.empty(); |
| | | } |
| | | WaylineJobEntity jobEntity = this.dto2Entity(parentJobOpt.get()); |
| | | jobEntity.setJobId(UUID.randomUUID().toString()); |
| | | jobEntity.setErrorCode(null); |
| | | jobEntity.setCompletedTime(null); |
| | | jobEntity.setExecuteTime(null); |
| | | jobEntity.setStatus(WaylineJobStatusEnum.PENDING.getVal()); |
| | | jobEntity.setParentId(parentId); |
| | | |
| | | return this.insertWaylineJob(jobEntity); |
| | | } |
| | | |
| | | /** |
| | | * 发布飞行任务 |
| | | * |
| | | * @param param 任务参数 |
| | | * @param loginUser 登录用户 |
| | | * @return ResponseResult 返回结果 |
| | | * @throws SQLException SQL异常 |
| | | */ |
| | | @Override |
| | | public ResponseResult publishFlightTask(CreateJobParam param, LoginUser loginUser) throws SQLException { |
| | | if (WaylineTaskTypeEnum.IMMEDIATE.getVal() == param.getTaskType()) { |
| | | param.setExecuteTime(System.currentTimeMillis()); |
| | | } |
| | | if (param.getExecuteTime() < _timestampUtils.plusMinutesByMillis(System.currentTimeMillis(), 10) && param.getTaskType() == WaylineTaskTypeEnum.TIMED.getVal()) { |
| | | return ResponseResult.error(1234,"定时任务执行时间不能小于当前时间的后10分钟!"); |
| | | } |
| | | // 机场1:共享 WaylineLoopTaskServiceImpl 在生成循环子任务时会固定下发 rth_mode=1, |
| | | // 该字段 Dock1(M30)的 flighttask_prepare 不支持。在共享调度器按机型分流之前, |
| | | // 机场1 不创建循环任务,避免向设备下发非法参数。 |
| | | if (param.getTaskType() == WaylineTaskTypeEnum.LOOP.getVal()) { |
| | | return ResponseResult.error(314013003, "机场1暂不支持循环任务,请选择立即执行或定时任务!"); |
| | | } |
| | | // 机场1:/dock1 接口只允许给 Dock1(domain=3, type=1)创建任务, |
| | | // 避免误把机场2/3 的 SN 传进来,导致任务落到错误链路。 |
| | | if (!isDock1Device(param.getDockSn())) { |
| | | return ResponseResult.error(314013004, "该设备不属于机场1,请使用对应的机场接口操作!"); |
| | | } |
| | | param.setRth_mode(1); |
| | | Optional<WaylineJobDTO> waylineJobOpt = this.createWaylineJob(param, loginUser.getUsername(), |
| | | param.getExecuteTime(), param.getExecuteTime()); |
| | | |
| | | if (waylineJobOpt.isEmpty()) { |
| | | return ResponseResult.error(314013000, "创建机场任务失败!"); |
| | | } |
| | | |
| | | WaylineJobDTO waylineJob = waylineJobOpt.get(); |
| | | |
| | | boolean isOnline = deviceService.checkDeviceOnline(waylineJob.getDockSn()); |
| | | if (!isOnline) { |
| | | return ResponseResult.error(CommonErrorEnum.DEVICE_OFFLINE.getErrorMsg()); |
| | | } |
| | | |
| | | // get wayline file |
| | | // Optional<WaylineFileDTO> waylineFile = waylineFileService.getWaylineByWaylineId(waylineJob.getFileId()); |
| | | // if (waylineFile.isEmpty()) { |
| | | // return ResponseResult.error("机场任务航线不存在!"); |
| | | // } |
| | | // 换航线表 |
| | | ArdRouteInfo ardRouteOpt = _ardRouteInfoService.selectArdRouteInfoById(waylineJob.getFileId()); |
| | | if (ardRouteOpt == null || ardRouteOpt.getPath() == null || ardRouteOpt.getPath().isEmpty()) { |
| | | return ResponseResult.error(314013001, "机场任务航线不存在!"); |
| | | } |
| | | |
| | | // get file url |
| | | // URL url = waylineFileService.getObjectUrl(waylineFile.get().getWaylineId()); |
| | | URL url = null; |
| | | String minioUrl = subStringMinioUrlName(ardRouteOpt.getPath()); |
| | | String md5 = ""; |
| | | String httpUrl = ""; |
| | | try (InputStream inputStream = _ossService.getObject(kmzPath, minioUrl)) { |
| | | md5 = DigestUtils.md5Hex(inputStream); |
| | | url = new URL(ardRouteOpt.getPath()); |
| | | log.info("机场任务航线文件url地址转换前:{}", url); |
| | | httpUrl = url.toString().replace(OssConfiguration.endpoint, DockConfiguration.endpoint); |
| | | log.info("机场任务航线文件url地址转换后:{}", httpUrl); |
| | | log.info("机场任务航线文件md5值:{}", md5); |
| | | } catch (Exception e) { |
| | | log.error("机场任务航线文件地址错误!{}", e.getMessage()); |
| | | return ResponseResult.error(314013002, "机场任务航线文件地址错误!" + e.getMessage()); |
| | | } |
| | | |
| | | log.info("机场任务-rth_mode的值:{}", waylineJob.getRthMode()); |
| | | |
| | | Dock1WaylineTaskCreateDTO flightTask = Dock1WaylineTaskCreateDTO.builder() |
| | | .flightId(waylineJob.getJobId()) |
| | | .executeTime(waylineJob.getBeginTime().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()) |
| | | .taskType(waylineJob.getTaskType()) |
| | | // 机场1:C4 dock1(M30)无 wayline_precision_type,不下发 |
| | | .rthAltitude(waylineJob.getRthAltitude()) |
| | | .outOfControlAction(waylineJob.getOutOfControlAction()) |
| | | // 机场1:C4 dock1(M30)无 rth_mode,不下发 |
| | | .file(WaylineTaskFileDTO.builder() |
| | | // .url(url.toString().replace("https://192.168.10.103:9001", "http://192.168.10.103:9011")) |
| | | // .fingerprint(waylineFile.get().getSign()) |
| | | // .replace("https://", "http://") |
| | | // .replace("9001", "9011") |
| | | .url(httpUrl) |
| | | .fingerprint(md5) |
| | | .build()) |
| | | // .simulateMission(WaylineSimulateMissionDTO.builder() |
| | | // .isEnable(1) |
| | | // .latitude(46.55819013988211) |
| | | // .longitude(125.14652370145113) |
| | | // .build()) |
| | | .build(); |
| | | |
| | | String topic = TopicConst.THING_MODEL_PRE + TopicConst.PRODUCT + |
| | | waylineJob.getDockSn() + TopicConst.SERVICES_SUF; |
| | | CommonTopicResponse<Object> response = CommonTopicResponse.builder() |
| | | .tid(UUID.randomUUID().toString()) |
| | | .bid(waylineJob.getJobId()) |
| | | .timestamp(System.currentTimeMillis()) |
| | | .data(flightTask) |
| | | .method(WaylineMethodEnum.FLIGHT_TASK_PREPARE.getMethod()) |
| | | .build(); |
| | | |
| | | // 下发参数打印 |
| | | try { |
| | | String json = objectMapper.writeValueAsString(response); |
| | | log.info("机场下发航线任务的参数response:{}", json); |
| | | } catch (Exception e) { |
| | | log.error("机场下发航线任务的参数response异常:{}", e.getMessage()); |
| | | } |
| | | |
| | | ServiceReply serviceReply = messageSender.publishWithReply(topic, response); |
| | | |
| | | log.info("下发任务结果: {}", serviceReply.getResult()); |
| | | |
| | | if (ResponseResult.CODE_SUCCESS != serviceReply.getResult()) { |
| | | String errorMsg = CommonErrorEnum.getMsgByCode(serviceReply.getResult()); |
| | | log.info("Prepare task ====> Error code: {},Error Msg:{}", serviceReply.getResult(), errorMsg); |
| | | this.updateJob(WaylineJobDTO.builder() |
| | | .jobId(waylineJob.getJobId()) |
| | | .executeTime(LocalDateTime.now()) |
| | | .status(WaylineJobStatusEnum.FAILED.getVal()) |
| | | .completedTime(LocalDateTime.now()) |
| | | .code(serviceReply.getResult()).build()); |
| | | //return ResponseResult.error("Prepare task ====> Error code: " + serviceReply.getResult()); |
| | | return ResponseResult.error(serviceReply.getResult(), errorMsg); |
| | | } |
| | | |
| | | // 飞行命令先注释掉 |
| | | // Issue an immediate task execution command. |
| | | if (WaylineTaskTypeEnum.IMMEDIATE.getVal() == waylineJob.getTaskType()) { |
| | | if (!executeFlightTask(waylineJob.getJobId())) { |
| | | return ResponseResult.error("无法执行作业。"); |
| | | } |
| | | } |
| | | |
| | | if (WaylineTaskTypeEnum.TIMED.getVal() == waylineJob.getTaskType()) { |
| | | boolean isAdd = RedisOpsUtils.zAdd(RedisConst.WAYLINE_JOB_TIMED_EXECUTE, |
| | | waylineJob.getDockSn() + RedisConst.DELIMITER + waylineJob.getJobId(), |
| | | waylineJob.getBeginTime().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()); |
| | | if (!isAdd) { |
| | | return ResponseResult.error("无法创建计划作业。"); |
| | | } |
| | | } |
| | | |
| | | return ResponseResult.success(); |
| | | } |
| | | |
| | | public ResponseResult publishFlightTaskPrepare(Optional<WaylineJobDTO> waylineJobOpt) throws SQLException { |
| | | |
| | | if (waylineJobOpt.isEmpty()) { |
| | | return ResponseResult.error(314013000, "创建机场任务失败!"); |
| | | } |
| | | WaylineJobDTO waylineJob = waylineJobOpt.get(); |
| | | boolean isOnline = deviceService.checkDeviceOnline(waylineJob.getDockSn()); |
| | | if (!isOnline) { |
| | | return ResponseResult.error("机场不在线!"); |
| | | } |
| | | // 换航线表 |
| | | ArdRouteInfo ardRouteOpt = _ardRouteInfoService.selectArdRouteInfoById(waylineJob.getFileId()); |
| | | if (ardRouteOpt == null || ardRouteOpt.getPath() == null || ardRouteOpt.getPath().isEmpty()) { |
| | | return ResponseResult.error(314013001, "机场任务航线不存在!"); |
| | | } |
| | | // get file url |
| | | URL url = null; |
| | | String minioUrl = subStringMinioUrlName(ardRouteOpt.getPath()); |
| | | String md5 = ""; |
| | | String httpUrl = ""; |
| | | try (InputStream inputStream = _ossService.getObject(kmzPath, minioUrl)) { |
| | | md5 = DigestUtils.md5Hex(inputStream); |
| | | url = new URL(ardRouteOpt.getPath()); |
| | | log.info("机场任务航线文件url地址转换前:{}", url); |
| | | httpUrl = url.toString().replace(OssConfiguration.endpoint, DockConfiguration.endpoint); |
| | | log.info("机场任务航线文件url地址转换后:{}", httpUrl); |
| | | log.info("机场任务航线文件md5值:{}", md5); |
| | | } catch (Exception e) { |
| | | log.error("机场任务航线文件地址错误!{}", e.getMessage()); |
| | | return ResponseResult.error(314013002, "机场任务航线文件地址错误!" + e.getMessage()); |
| | | } |
| | | |
| | | Dock1WaylineTaskCreateDTO flightTask = Dock1WaylineTaskCreateDTO.builder() |
| | | .flightId(waylineJob.getJobId()) |
| | | .executeTime(waylineJob.getBeginTime().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()) |
| | | .taskType(waylineJob.getTaskType()) |
| | | // 机场1:C4 dock1(M30)无 wayline_precision_type,不下发 |
| | | .rthAltitude(waylineJob.getRthAltitude()) |
| | | .outOfControlAction(waylineJob.getOutOfControlAction()) |
| | | // 机场1:C4 dock1(M30)无 rth_mode,不下发 |
| | | .file(WaylineTaskFileDTO.builder() |
| | | .url(httpUrl) |
| | | // .fingerprint(waylineFile.get().getSign()) |
| | | .fingerprint(md5) |
| | | .build()) |
| | | // .simulateMission(WaylineSimulateMissionDTO.builder() |
| | | // .isEnable(1) |
| | | // .latitude(46.558831038) |
| | | // .longitude(125.148072319) |
| | | // .build()) |
| | | .build(); |
| | | |
| | | // 下发参数打印 |
| | | try { |
| | | String json = objectMapper.writeValueAsString(flightTask); |
| | | log.info("机场下发循环任务子任务-定时任务的航线任务的参数flightTask:{}", json); |
| | | } catch (Exception e) { |
| | | log.error("机场下发循环任务子任务-定时任务的航线任务的参数flightTask异常:{}", e.getMessage()); |
| | | } |
| | | |
| | | String topic = TopicConst.THING_MODEL_PRE + TopicConst.PRODUCT + |
| | | waylineJob.getDockSn() + TopicConst.SERVICES_SUF; |
| | | CommonTopicResponse<Object> response = CommonTopicResponse.builder() |
| | | .tid(UUID.randomUUID().toString()) |
| | | .bid(waylineJob.getJobId()) |
| | | .timestamp(System.currentTimeMillis()) |
| | | .data(flightTask) |
| | | .method(WaylineMethodEnum.FLIGHT_TASK_PREPARE.getMethod()) |
| | | .build(); |
| | | |
| | | ServiceReply serviceReply = messageSender.publishWithReply(topic, response); |
| | | |
| | | log.info("下发循环任务子任务-定时任务的结果: {}", serviceReply.getResult()); |
| | | log.info("机场循环任务子任务-定时任务的航线文件url地址:{}", httpUrl); |
| | | log.info("机场循环任务子任务-定时任务的航线文件md5值:{}", md5); |
| | | |
| | | if (ResponseResult.CODE_SUCCESS != serviceReply.getResult()) { |
| | | String errorMsg = CommonErrorEnum.getMsgByCode(serviceReply.getResult()); |
| | | log.info("Prepare task ====> Error code: {},Error Msg: {}", serviceReply.getResult(), errorMsg); |
| | | this.updateJob(WaylineJobDTO.builder() |
| | | .jobId(waylineJob.getJobId()) |
| | | .executeTime(LocalDateTime.now()) |
| | | .status(WaylineJobStatusEnum.FAILED.getVal()) |
| | | .completedTime(LocalDateTime.now()) |
| | | .code(serviceReply.getResult()).build()); |
| | | // return ResponseResult.error("Prepare task ====> Error code: " + serviceReply.getResult()); |
| | | return ResponseResult.error(serviceReply.getResult(), errorMsg); |
| | | } |
| | | |
| | | // 飞行命令先注释掉 |
| | | // Issue an immediate task execution command. |
| | | // if (WaylineTaskTypeEnum.IMMEDIATE.getVal() == waylineJob.getTaskType()) { |
| | | // if (!executeFlightTask(waylineJob.getJobId())) { |
| | | // return ResponseResult.error("无法执行作业。"); |
| | | // } |
| | | // } |
| | | |
| | | if (WaylineTaskTypeEnum.TIMED.getVal() == waylineJob.getTaskType()) { |
| | | boolean isAdd = RedisOpsUtils.zAdd(RedisConst.WAYLINE_JOB_TIMED_EXECUTE, |
| | | waylineJob.getDockSn() + RedisConst.DELIMITER + waylineJob.getJobId(), |
| | | waylineJob.getBeginTime().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()); |
| | | if (!isAdd) { |
| | | return ResponseResult.error("无法创建计划作业。"); |
| | | } |
| | | } |
| | | |
| | | return ResponseResult.success(); |
| | | } |
| | | |
| | | /** |
| | | * 执行飞行任务 |
| | | * |
| | | * @param jobId 任务ID |
| | | * @return 如果任务执行成功返回true,否则返回false |
| | | * @throws IllegalArgumentException 如果任务不存在 |
| | | * @throws RuntimeException 如果设备处于离线状态 |
| | | */ |
| | | @Override |
| | | public Boolean executeFlightTask(String jobId) { |
| | | // get job |
| | | Optional<WaylineJobDTO> waylineJob = this.getJobByJobId(jobId); |
| | | if (waylineJob.isEmpty()) { |
| | | throw new IllegalArgumentException("计划任务不存在。"); |
| | | } |
| | | |
| | | boolean isOnline = deviceService.checkDeviceOnline(waylineJob.get().getDockSn()); |
| | | if (!isOnline) { |
| | | // throw new RuntimeException("Dock is offline."); |
| | | throw new RuntimeException(CommonErrorEnum.DEVICE_OFFLINE.getErrorMsg()); |
| | | } |
| | | |
| | | WaylineJobDTO job = waylineJob.get(); |
| | | Dock1WaylineTaskCreateDTO flightTask = Dock1WaylineTaskCreateDTO.builder().flightId(jobId).build(); |
| | | // flightTask中,只有任务id是有值的,其他字段不返回 |
| | | String topic = TopicConst.THING_MODEL_PRE + TopicConst.PRODUCT + |
| | | job.getDockSn() + TopicConst.SERVICES_SUF; |
| | | CommonTopicResponse<Object> response = CommonTopicResponse.builder() |
| | | .tid(UUID.randomUUID().toString()) |
| | | .bid(jobId) |
| | | .timestamp(System.currentTimeMillis()) |
| | | .data(flightTask) |
| | | .method(WaylineMethodEnum.FLIGHT_TASK_EXECUTE.getMethod()) |
| | | .build(); |
| | | |
| | | // 下发参数打印 |
| | | try { |
| | | String json = objectMapper.writeValueAsString(response); |
| | | log.info("机场下发-执行飞行任务-的参数response:{}", json); |
| | | } catch (Exception e) { |
| | | log.error("机场下发-执行飞行任务-的参数response异常:{}", e.getMessage()); |
| | | } |
| | | |
| | | ServiceReply serviceReply = messageSender.publishWithReply(topic, response); |
| | | |
| | | log.info("下发-执行飞行任务-结果: {}", serviceReply.getResult()); |
| | | |
| | | if (ResponseResult.CODE_SUCCESS != serviceReply.getResult()) { |
| | | String errorMsg = CommonErrorEnum.getMsgByCode(serviceReply.getResult()); |
| | | log.info("Execute job ====> Error code: {},Error Msg:{}", serviceReply.getResult(), errorMsg); |
| | | this.updateJob(WaylineJobDTO.builder() |
| | | .jobId(jobId) |
| | | .executeTime(LocalDateTime.now()) |
| | | .status(WaylineJobStatusEnum.FAILED.getVal()) |
| | | .completedTime(LocalDateTime.now()) |
| | | .code(serviceReply.getResult()).build()); |
| | | return false; |
| | | } |
| | | |
| | | this.updateJob(WaylineJobDTO.builder() |
| | | .jobId(jobId) |
| | | .executeTime(LocalDateTime.now()) |
| | | .status(WaylineJobStatusEnum.IN_PROGRESS.getVal()) |
| | | .build()); |
| | | RedisOpsUtils.setWithExpire(RedisConst.WAYLINE_JOB_RUNNING_PREFIX + job.getDockSn(), |
| | | EventsReceiver.<WaylineTaskProgressReceiver>builder().bid(jobId).sn(job.getDockSn()).build(), |
| | | RedisConst.DEVICE_ALIVE_SECOND * RedisConst.DEVICE_ALIVE_SECOND); |
| | | return true; |
| | | } |
| | | |
| | | /** |
| | | * 取消飞行任务 |
| | | * |
| | | * @param jobIds 需要取消的任务ID集合 |
| | | * @throws IllegalArgumentException 如果任务状态不正确,抛出非法参数异常 |
| | | */ |
| | | @Override |
| | | public ResponseResult cancelFlightTask(Collection<String> jobIds) { |
| | | if (CollectionUtils.isEmpty(jobIds)) { |
| | | return ResponseResult.error(98, "任务ID不能为空"); |
| | | } |
| | | // 机场1:按机场分组,逐个下发取消,不能在第一组处理完后直接 return, |
| | | // 否则同一请求中包含多个机场(或多个 SN)时,后面的任务不会被取消。 |
| | | Map<String, Set<String>> dockJobs = new LinkedHashMap<>(); |
| | | for (String jobId : jobIds) { |
| | | Optional<WaylineJobDTO> jobOpt = this.getJobByJobId(jobId); |
| | | if (jobOpt.isEmpty()) { |
| | | return ResponseResult.error("任务不存在,jobId: " + jobId); |
| | | } |
| | | WaylineJobDTO job = jobOpt.get(); |
| | | // 机场1:/dock1 接口只允许取消机场1(Dock1)的任务,避免通过 jobId 误取消机场2/3 的任务。 |
| | | if (!isDock1Device(job.getDockSn())) { |
| | | return ResponseResult.error(314013004, "该任务不属于机场1,请使用对应的机场接口操作!"); |
| | | } |
| | | // 循环主任务只是模板,真正的执行体是子任务:主任务本地置为已取消,执行中的子任务下发取消。 |
| | | if (job.getTaskType() != null && job.getTaskType() == WaylineTaskTypeEnum.LOOP.getVal()) { |
| | | for (WaylineJobDTO child : this.getJobByJobParentId(jobId)) { |
| | | if (isJobEnded(child.getStatus())) { |
| | | continue; |
| | | } |
| | | if (Objects.equals(child.getStatus(), WaylineJobStatusEnum.IN_PROGRESS.getVal())) { |
| | | dockJobs.computeIfAbsent(child.getDockSn(), k -> new LinkedHashSet<>()).add(child.getJobId()); |
| | | } else { |
| | | this.cancelJobLocally(child.getJobId(), child.getDockSn()); |
| | | } |
| | | } |
| | | this.cancelJobLocally(jobId, job.getDockSn()); |
| | | continue; |
| | | } |
| | | if (isJobEnded(job.getStatus())) { |
| | | continue; |
| | | } |
| | | dockJobs.computeIfAbsent(job.getDockSn(), k -> new LinkedHashSet<>()).add(jobId); |
| | | } |
| | | |
| | | ResponseResult errorResult = null; |
| | | for (Map.Entry<String, Set<String>> entry : dockJobs.entrySet()) { |
| | | ResponseResult result = this.publishCancelTask(entry.getKey(), new ArrayList<>(entry.getValue())); |
| | | if (ResponseResult.CODE_SUCCESS != result.getCode() && errorResult == null) { |
| | | errorResult = result; |
| | | } |
| | | } |
| | | return errorResult == null ? ResponseResult.success() : errorResult; |
| | | } |
| | | |
| | | /** |
| | | * 机场1:判断 SN 是否为 Dock1 机场设备(domain=3, type=1)。 |
| | | * |
| | | * @param sn 设备序列号 |
| | | * @return true 表示属于机场1 |
| | | */ |
| | | private boolean isDock1Device(String sn) { |
| | | if (!StringUtils.hasText(sn)) { |
| | | return false; |
| | | } |
| | | Optional<DeviceDTO> deviceOpt = deviceService.getDeviceBySn(sn); |
| | | return deviceOpt.isPresent() |
| | | && Objects.equals(DeviceDomainEnum.DOCK.getVal(), deviceOpt.get().getDomain()) |
| | | && Objects.equals(DOCK1_DEVICE_TYPE, deviceOpt.get().getType()); |
| | | } |
| | | |
| | | /** |
| | | * 机场1:任务是否已经处于终态(成功/取消/失败/未知)。 |
| | | */ |
| | | private boolean isJobEnded(Integer status) { |
| | | if (status == null) { |
| | | return false; |
| | | } |
| | | return Boolean.TRUE.equals(WaylineJobStatusEnum.find(status).getEnd()); |
| | | } |
| | | |
| | | /** |
| | | * 机场1:只在本地把任务标记为已取消,并清理定时执行队列,不删除历史记录。 |
| | | */ |
| | | private void cancelJobLocally(String jobId, String dockSn) { |
| | | this.updateJob(WaylineJobDTO.builder() |
| | | .jobId(jobId) |
| | | .status(WaylineJobStatusEnum.CANCEL.getVal()) |
| | | .completedTime(LocalDateTime.now()) |
| | | .build()); |
| | | if (dockSn != null) { |
| | | RedisOpsUtils.zRemove(RedisConst.WAYLINE_JOB_TIMED_EXECUTE, |
| | | dockSn + RedisConst.DELIMITER + jobId); |
| | | } |
| | | } |
| | | |
| | | // region 代码备份 |
| | | // @Override |
| | | // public void cancelFlightTask(Collection<String> jobIds) { |
| | | // List<WaylineJobDTO> waylineJobs = getJobsByConditions(jobIds, WaylineJobStatusEnum.PENDING); |
| | | // |
| | | //// Set<String> waylineJobIds = waylineJobs.stream().map(WaylineJobDTO::getJobId).collect(Collectors.toSet()); |
| | | //// // Check if the task status is correct. |
| | | //// boolean isErr = !jobIds.removeAll(waylineJobIds) || !jobIds.isEmpty(); |
| | | //// if (isErr) { |
| | | //// throw new IllegalArgumentException("任务开始后,无法取消。" + Arrays.toString(jobIds.toArray())); |
| | | ////// throw new IllegalArgumentException("These tasks have an incorrect status and cannot be canceled. " |
| | | ////// + Arrays.toString(jobIds.toArray())); |
| | | //// } |
| | | // |
| | | // // Group job id by dock sn. |
| | | // Map<String, List<String>> dockJobs = waylineJobs.stream() |
| | | // .collect(Collectors.groupingBy(WaylineJobDTO::getDockSn, |
| | | // Collectors.mapping(WaylineJobDTO::getJobId, Collectors.toList()))); |
| | | // dockJobs.forEach((dockSn, idList) -> this.publishCancelTask(dockSn, idList)); |
| | | // |
| | | // } |
| | | |
| | | //@Override |
| | | //public void publishCancelTask(String dockSn, List<String> jobIds) { |
| | | // |
| | | // boolean isOnline = deviceService.checkDeviceOnline(dockSn); |
| | | // if (!isOnline) { |
| | | // Integer errorCode = CommonErrorEnum.DEVICE_OFFLINE.getErrorCode(); |
| | | // |
| | | // //return ResponseResult.error(errorCode, CommonErrorEnum.getMsgByCode(errorCode))); |
| | | // throw new RuntimeException(CommonErrorEnum.DEVICE_OFFLINE.getErrorMsg()); |
| | | // } |
| | | // String topic = TopicConst.THING_MODEL_PRE + TopicConst.PRODUCT + dockSn + TopicConst.SERVICES_SUF; |
| | | // |
| | | // CommonTopicResponse<Object> response = CommonTopicResponse.builder() |
| | | // .tid(UUID.randomUUID().toString()) |
| | | // .bid(UUID.randomUUID().toString()) |
| | | // .timestamp(System.currentTimeMillis()) |
| | | // .data(Map.of(MapKeyConst.FLIGHT_IDS, jobIds)) |
| | | // .method(WaylineMethodEnum.FLIGHT_TASK_CANCEL.getMethod()) |
| | | // .build(); |
| | | // |
| | | // ServiceReply serviceReply = messageSender.publishWithReply(topic, response); |
| | | // if (ResponseResult.CODE_SUCCESS != serviceReply.getResult()) { |
| | | // String errorMsg = CommonErrorEnum.getMsgByCode(serviceReply.getResult()); |
| | | // log.info("取消飞行任务失败 ====> 设备sn: {}, 错误码: {}, {}", dockSn, serviceReply.getResult(), errorMsg); |
| | | // //throw new RuntimeException("Failed to cancel the wayline job of " + dockSn); |
| | | // throw new RuntimeException("设备:" + dockSn + "取消飞行任务失败,错误码:" + serviceReply.getResult() + "," + errorMsg); |
| | | // } |
| | | // |
| | | // for (String jobId : jobIds) { |
| | | // this.updateJob(WaylineJobDTO.builder() |
| | | // .jobId(jobId) |
| | | // .status(WaylineJobStatusEnum.CANCEL.getVal()) |
| | | // .completedTime(LocalDateTime.now()) |
| | | // .build()); |
| | | // RedisOpsUtils.zRemove(RedisConst.WAYLINE_JOB_TIMED_EXECUTE, RedisConst.DELIMITER + dockSn + RedisConst.DELIMITER + jobId); |
| | | // // 原流程是修改表中数据的status状态值,现在的精力是直接删除 |
| | | // this.deleteTask(jobId); |
| | | // } |
| | | // |
| | | //} |
| | | // endregion |
| | | |
| | | /** |
| | | * 发布取消任务 |
| | | * |
| | | * @param dockSn 设备序列号 |
| | | * @param jobIds 任务ID列表 |
| | | * @throws RuntimeException 如果设备离线或取消任务失败,则抛出运行时异常 |
| | | */ |
| | | @Override |
| | | public ResponseResult publishCancelTask(String dockSn, List<String> jobIds) { |
| | | if (CollectionUtils.isEmpty(jobIds)) { |
| | | return ResponseResult.success(); |
| | | } |
| | | for (String jobId : jobIds) { |
| | | Optional<WaylineJobDTO> waylineJobDTO = this.getJobByJobId(jobId); |
| | | if (waylineJobDTO.isEmpty()) { |
| | | return ResponseResult.error("任务不存在,jobId: " + jobId); |
| | | } |
| | | } |
| | | boolean isOnline = deviceService.checkDeviceOnline(dockSn); |
| | | if (!isOnline) { |
| | | Integer errorCode = CommonErrorEnum.DEVICE_OFFLINE.getErrorCode(); |
| | | return ResponseResult.error(errorCode, CommonErrorEnum.getMsgByCode(errorCode)); |
| | | } |
| | | String topic = TopicConst.THING_MODEL_PRE + TopicConst.PRODUCT + dockSn + TopicConst.SERVICES_SUF; |
| | | |
| | | CommonTopicResponse<Object> response = CommonTopicResponse.builder() |
| | | .tid(UUID.randomUUID().toString()) |
| | | .bid(UUID.randomUUID().toString()) |
| | | .timestamp(System.currentTimeMillis()) |
| | | .data(Map.of(MapKeyConst.FLIGHT_IDS, jobIds)) |
| | | .method(WaylineMethodEnum.FLIGHT_TASK_CANCEL.getMethod()) |
| | | .build(); |
| | | |
| | | ServiceReply serviceReply = messageSender.publishWithReply(topic, response); |
| | | if (ResponseResult.CODE_SUCCESS != serviceReply.getResult()) { |
| | | String errorMsg = CommonErrorEnum.getMsgByCode(serviceReply.getResult()); |
| | | log.info("取消飞行任务失败 ====> 设备sn: {}, 错误码: {}, {}", dockSn, serviceReply.getResult(), errorMsg); |
| | | return ResponseResult.error(serviceReply.getResult(), errorMsg); |
| | | } |
| | | |
| | | // 机场1:取消成功后只更新状态并清理定时队列,保留历史记录,避免“取消”等同于“删除”。 |
| | | for (String jobId : jobIds) { |
| | | this.cancelJobLocally(jobId, dockSn); |
| | | } |
| | | return ResponseResult.success(); |
| | | } |
| | | |
| | | @Override |
| | | public void deleteTask(String jobid) { |
| | | mapper.delete(new LambdaQueryWrapper<WaylineJobEntity>() |
| | | .eq(WaylineJobEntity::getJobId, jobid)); |
| | | } |
| | | |
| | | /** |
| | | * 根据条件查询作业列表 |
| | | * |
| | | * @param jobIds 作业ID集合 |
| | | * @param status 作业状态枚举 |
| | | * @return 满足条件的作业DTO列表 |
| | | */ |
| | | @Override |
| | | public List<WaylineJobDTO> getJobsByConditions(Collection<String> jobIds, WaylineJobStatusEnum status) { |
| | | return mapper.selectList( |
| | | new LambdaQueryWrapper<WaylineJobEntity>() |
| | | // 机场1:原实现误用 ne(取的是“非该状态”的任务),修正为 eq。 |
| | | .eq(Objects.nonNull(status), WaylineJobEntity::getStatus, status.getVal()) |
| | | .and(!CollectionUtils.isEmpty(jobIds), |
| | | wrapper -> jobIds.forEach(id -> wrapper.eq(WaylineJobEntity::getJobId, id).or()))) |
| | | .stream() |
| | | .map(this::entity2Dto) |
| | | .collect(Collectors.toList()); |
| | | } |
| | | |
| | | /** |
| | | * 根据任务ID获取任务详情 |
| | | * |
| | | * @param jobId 任务ID |
| | | * @return Optional<WaylineJobDTO> 包含任务详情的Optional对象,如果不存在则返回空的Optional |
| | | */ |
| | | @Override |
| | | public Optional<WaylineJobDTO> getJobByJobId(String jobId) { |
| | | WaylineJobEntity jobEntity = mapper.selectOne( |
| | | new LambdaQueryWrapper<WaylineJobEntity>() |
| | | .eq(WaylineJobEntity::getJobId, jobId)); |
| | | return Optional.ofNullable(entity2Dto(jobEntity)); |
| | | } |
| | | |
| | | public List<WaylineJobDTO> getJobByJobParentId(String parentId) { |
| | | List<WaylineJobEntity> jobEntities = mapper.selectList( |
| | | new LambdaQueryWrapper<WaylineJobEntity>() |
| | | .eq(WaylineJobEntity::getParentId, parentId)); |
| | | if (CollectionUtils.isEmpty(jobEntities)) { |
| | | return Collections.emptyList(); // 返回不可变空列表,更安全 |
| | | } |
| | | return jobEntities.stream() |
| | | .map(this::entity2Dto) // 调用原有的单个转换方法 |
| | | .collect(Collectors.toList()); |
| | | } |
| | | |
| | | /** |
| | | * 更新作业信息 |
| | | * |
| | | * @param dto 作业信息传输对象 |
| | | * @return 更新成功返回true,否则返回false |
| | | */ |
| | | @Override |
| | | public Boolean updateJob(WaylineJobDTO dto) { |
| | | return mapper.update(this.dto2Entity(dto), |
| | | new LambdaUpdateWrapper<WaylineJobEntity>() |
| | | .eq(WaylineJobEntity::getJobId, dto.getJobId())) > 0; |
| | | } |
| | | |
| | | /** |
| | | * 根据工作区ID获取工作线任务分页数据 |
| | | * |
| | | * @param page 页码,从1开始 |
| | | * @param pageSize 每页显示条数 |
| | | * @return PaginationData 包含工作线任务分页数据的对象 |
| | | */ |
| | | @Override |
| | | public PaginationData<WaylineJobDTO> getJobsByWorkspaceId(long page, long pageSize) { |
| | | Page<WaylineJobEntity> pageData = mapper.selectPage( |
| | | new Page<WaylineJobEntity>(page, pageSize), |
| | | new LambdaQueryWrapper<WaylineJobEntity>() |
| | | .orderByDesc(WaylineJobEntity::getId)); |
| | | List<WaylineJobDTO> records = pageData.getRecords() |
| | | .stream() |
| | | .map(this::entity2Dto) |
| | | .collect(Collectors.toList()); |
| | | |
| | | return new PaginationData<WaylineJobDTO>(records, new Pagination(pageData)); |
| | | } |
| | | |
| | | /** |
| | | * 根据条件筛选获取任务分页数据 |
| | | * |
| | | * @param page 页码,从1开始 |
| | | * @param pageSize 每页显示条数 |
| | | * @return PaginationData 包含工作线任务分页数据的对象 |
| | | */ |
| | | @Override |
| | | public PaginationData<WaylineJobDTO> getJobsList(long page, long pageSize, Integer status, |
| | | Long startTime, Long endTime, Integer taskType, String name, Boolean isComplete) { |
| | | // 机场1:只查询机场1(Dock1)设备下的任务,避免 /dock1 接口返回机场2/3 的任务。 |
| | | List<String> dock1SnList = deviceService.getDevicesByParams(DeviceQueryParam.builder() |
| | | .deviceType(DOCK1_DEVICE_TYPE) |
| | | .domains(Collections.singletonList(DeviceDomainEnum.DOCK.getVal())) |
| | | .build()) |
| | | .stream() |
| | | .map(DeviceDTO::getDeviceSn) |
| | | .collect(Collectors.toList()); |
| | | if (CollectionUtils.isEmpty(dock1SnList)) { |
| | | Page<WaylineJobEntity> emptyPage = new Page<>(page, pageSize); |
| | | return new PaginationData<>(Collections.emptyList(), new Pagination(emptyPage)); |
| | | } |
| | | Page<WaylineJobEntity> pageData = mapper.selectPage( |
| | | new Page<WaylineJobEntity>(page, pageSize), |
| | | new LambdaQueryWrapper<WaylineJobEntity>() |
| | | .in(WaylineJobEntity::getDockSn, dock1SnList) |
| | | .isNull(WaylineJobEntity::getParentId) |
| | | // 实际开始时间筛选,允许为空,为空则不进行筛选 |
| | | .ge(startTime != null, WaylineJobEntity::getBeginTime, startTime) |
| | | .le(endTime != null, WaylineJobEntity::getBeginTime, endTime) |
| | | // 任务类型筛选,允许为空,为空则不进行筛选 |
| | | .eq(taskType != null, WaylineJobEntity::getTaskType, taskType) |
| | | // 任务名称筛选,允许为空,为空则不进行筛选;....模糊查询 |
| | | .like(name != null, WaylineJobEntity::getName, "%" + name + "%") |
| | | // 根据是否完成筛选,允许为空,为空则不进行筛选 |
| | | .eq(isComplete != null && isComplete, WaylineJobEntity::getStatus, 3) |
| | | .ne(isComplete != null && !isComplete, WaylineJobEntity::getStatus, 3) |
| | | // 任务状态筛选,允许为空,为空则不进行筛选 |
| | | .eq(status != null, WaylineJobEntity::getStatus, status) |
| | | .orderByDesc(WaylineJobEntity::getBeginTime)); |
| | | List<WaylineJobDTO> records = pageData.getRecords() |
| | | .stream() |
| | | .map(this::entity2Dto) |
| | | .peek(job -> { |
| | | List<WaylineJobDTO> childOpt = this.getJobByJobParentId(job.getJobId()); |
| | | job.setChildren(childOpt); |
| | | }) |
| | | .collect(Collectors.toList()); |
| | | |
| | | return new PaginationData<WaylineJobDTO>(records, new Pagination(pageData)); |
| | | } |
| | | |
| | | public List<WaylineJobEntity> selectList(Long taskType, Long executeTime, String parentId) { |
| | | return mapper.selectList(new LambdaQueryWrapper<WaylineJobEntity>() |
| | | .eq(taskType != null, WaylineJobEntity::getTaskType, taskType) |
| | | .ne(WaylineJobEntity::getStatus, WaylineJobStatusEnum.SUCCESS.getVal()) |
| | | .eq(executeTime != null, WaylineJobEntity::getBeginTime, executeTime) |
| | | .eq(parentId != null, WaylineJobEntity::getParentId, parentId) |
| | | .ne(WaylineJobEntity::getStatus, 3) |
| | | .orderByDesc(WaylineJobEntity::getId)); |
| | | } |
| | | |
| | | /** |
| | | * 获取所有待定状态的定时任务,未执行即将要执行的。 |
| | | */ |
| | | public List<WaylineJobEntity> selectScheduledTaskList() { |
| | | // "1":"定时任务" |
| | | return mapper.selectList(new LambdaQueryWrapper<WaylineJobEntity>() |
| | | .eq(WaylineJobEntity::getTaskType, 1) |
| | | .eq(WaylineJobEntity::getStatus, WaylineJobStatusEnum.PENDING.getVal()) |
| | | .gt(WaylineJobEntity::getBeginTime, System.currentTimeMillis()) |
| | | .orderByDesc(WaylineJobEntity::getBeginTime)); |
| | | } |
| | | |
| | | |
| | | /** |
| | | * 根据航班任务ID获取航班任务资源 |
| | | * |
| | | * @param receiver 接收到的消息对象 |
| | | * @param headers 消息头信息 |
| | | * @Override 覆盖父类方法 |
| | | * @ServiceActivator(inputChannel = ChannelName.INBOUND_REQUESTS_FLIGHT_TASK_RESOURCE_GET, outputChannel = ChannelName.OUTBOUND) |
| | | * 指定输入通道和输出通道 |
| | | * @Transactional(isolation = Isolation.READ_UNCOMMITTED) |
| | | * 设置事务隔离级别为未提交读 |
| | | */ |
| | | @Override |
| | | // 机场1:不复制原 ServiceActivator(inputChannel = ChannelName.INBOUND_REQUESTS_FLIGHT_TASK_RESOURCE_GET),消费职责由共享层原类承担 |
| | | @Transactional(isolation = Isolation.READ_UNCOMMITTED) |
| | | public void flightTaskResourceGet(CommonTopicReceiver receiver, MessageHeaders headers) { |
| | | Map<String, String> jobIdMap = objectMapper.convertValue(receiver.getData(), new TypeReference<Map<String, String>>() { |
| | | }); |
| | | String jobId = jobIdMap.get(MapKeyConst.FLIGHT_ID); |
| | | |
| | | log.info("机场通过flightTaskResourceGet获取航线文件地址,jobId: " + jobId); |
| | | |
| | | CommonTopicResponse.CommonTopicResponseBuilder<RequestsReply> builder = CommonTopicResponse.<RequestsReply>builder() |
| | | .tid(receiver.getTid()) |
| | | .bid(receiver.getBid()) |
| | | .method(RequestsMethodEnum.FLIGHT_TASK_RESOURCE_GET.getMethod()) |
| | | .timestamp(System.currentTimeMillis()); |
| | | |
| | | String topic = headers.get(MqttHeaders.RECEIVED_TOPIC).toString() + TopicConst._REPLY_SUF; |
| | | |
| | | DeviceDTO device = (DeviceDTO) RedisOpsUtils.get(RedisConst.DEVICE_ONLINE_PREFIX + receiver.getGateway()); |
| | | Optional<WaylineJobDTO> waylineJobOpt = this.getJobByJobId(jobId); |
| | | if (waylineJobOpt.isEmpty()) { |
| | | builder.data(RequestsReply.error(CommonErrorEnum.ILLEGAL_ARGUMENT)); |
| | | messageSender.publish(topic, builder.build()); |
| | | return; |
| | | } |
| | | |
| | | WaylineJobDTO waylineJob = waylineJobOpt.get(); |
| | | |
| | | // get wayline file |
| | | // Optional<WaylineFileDTO> waylineFile = waylineFileService.getWaylineByWaylineId(waylineJob.getFileId()); |
| | | // if (waylineFile.isEmpty()) { |
| | | // builder.data(RequestsReply.error(CommonErrorEnum.ILLEGAL_ARGUMENT)); |
| | | // messageSender.publish(topic, builder.build()); |
| | | // return; |
| | | // } |
| | | // 修改航线文件名称,从ard_route_info航线表中取; |
| | | // 换航线表 |
| | | ArdRouteInfo ardRouteOpt = _ardRouteInfoService.selectArdRouteInfoById(waylineJob.getFileId()); |
| | | if (ardRouteOpt == null) { |
| | | builder.data(RequestsReply.error(CommonErrorEnum.ILLEGAL_ARGUMENT)); |
| | | messageSender.publish(topic, builder.build()); |
| | | return; |
| | | } |
| | | |
| | | // get file url |
| | | URL url = null; |
| | | String minioUrl = subStringMinioUrlName(ardRouteOpt.getPath()); |
| | | String md5 = ""; |
| | | String httpUrl = ""; |
| | | try (InputStream inputStream = _ossService.getObject(kmzPath, minioUrl)) { |
| | | md5 = DigestUtils.md5Hex(inputStream); |
| | | url = new URL(ardRouteOpt.getPath()); |
| | | |
| | | log.info("机场通过flightTaskResourceGet获取航线文件地址转换前:{}", url); |
| | | httpUrl = url.toString().replace(OssConfiguration.endpoint, DockConfiguration.endpoint); |
| | | log.info("机场通过flightTaskResourceGet获取航线文件地址转换后:{}", httpUrl); |
| | | |
| | | log.info("机场通过flightTaskResourceGet获取航线文件地址,jobId: " + jobId + " 返回信息为url:" + httpUrl + " md5:" + md5); |
| | | // } |
| | | // try { |
| | | // url = waylineFileService.getObjectUrl(waylineFile.get().getWaylineId()); |
| | | // url = new URL(ardRouteOpt.getUpPath()); |
| | | |
| | | // Map<String, Object> jsonResourceGet = new HashMap<>(); |
| | | // jsonResourceGet.put("url", url.toString().replace("https//192.168.10.103:9001", "http//192.168.10.103:9002")); |
| | | // jsonResourceGet.put("fingerprint", md5); |
| | | // .replace("https://", "http://") |
| | | // .replace("9001", "9011") |
| | | |
| | | builder.data(RequestsReply.success(Dock1WaylineTaskCreateDTO.builder() |
| | | .file(WaylineTaskFileDTO.builder() |
| | | .url(httpUrl) |
| | | .fingerprint(md5) |
| | | .build()) |
| | | .build())); |
| | | |
| | | } catch (NullPointerException | IOException e) { |
| | | e.printStackTrace(); |
| | | builder.data(RequestsReply.error(CommonErrorEnum.ILLEGAL_ARGUMENT)); |
| | | messageSender.publish(topic, builder.build()); |
| | | return; |
| | | } |
| | | |
| | | // 下发参数打印 |
| | | try { |
| | | String json = objectMapper.writeValueAsString(builder.build()); |
| | | log.info("机场通过flighttask_resource_get获取航线文件的参数flightTask:{}", json); |
| | | } catch (Exception e) { |
| | | log.error("机场通过flighttask_resource_get获取航线文件的参数flightTask异常:{}", e.getMessage()); |
| | | } |
| | | |
| | | messageSender.publish(topic, builder.build()); |
| | | |
| | | } |
| | | |
| | | /** |
| | | * 上传媒体文件的最高优先级 |
| | | * |
| | | * @param jobId 作业ID |
| | | * @throws RuntimeException 如果作业ID不合法或设置优先级失败 |
| | | */ |
| | | @Override |
| | | public void uploadMediaHighestPriority(String jobId) { |
| | | Optional<WaylineJobDTO> jobOpt = getJobByJobId(jobId); |
| | | if (jobOpt.isEmpty()) { |
| | | throw new RuntimeException(CommonErrorEnum.ILLEGAL_ARGUMENT.getErrorMsg()); |
| | | } |
| | | // 机场1:只允许对 Dock1 的任务设置媒体上传优先级。 |
| | | if (!isDock1Device(jobOpt.get().getDockSn())) { |
| | | throw new RuntimeException("该任务不属于机场1,请使用对应的机场接口操作!"); |
| | | } |
| | | |
| | | String dockSn = jobOpt.get().getDockSn(); |
| | | String key = RedisConst.MEDIA_HIGHEST_PRIORITY_PREFIX + dockSn; |
| | | if (RedisOpsUtils.checkExist(key) && |
| | | jobId.equals(((MediaFileCountDTO) RedisOpsUtils.get(key)).getJobId())) { |
| | | return; |
| | | } |
| | | |
| | | ServiceReply reply = messageSender.publishWithReply(TopicConst.THING_MODEL_PRE + TopicConst.PRODUCT + dockSn + TopicConst.SERVICES_SUF, |
| | | CommonTopicResponse.builder() |
| | | .tid(UUID.randomUUID().toString()) |
| | | .bid(UUID.randomUUID().toString()) |
| | | .timestamp(System.currentTimeMillis()) |
| | | .method(MediaMethodEnum.UPLOAD_FLIGHT_TASK_MEDIA_PRIORITIZE.getMethod()) |
| | | .data(Map.of(MapKeyConst.FLIGHT_ID, jobId)) |
| | | .build()); |
| | | if (ResponseResult.CODE_SUCCESS != reply.getResult()) { |
| | | String errorMsg = CommonErrorEnum.getMsgByCode(reply.getResult()); |
| | | throw new RuntimeException("未能设置媒体作业上传优先级. Error Code: " + reply.getResult() + ", Error Msg: " + errorMsg); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 将WaylineJobDTO对象转换为WaylineJobEntity对象 |
| | | * |
| | | * @param dto WaylineJobDTO对象 |
| | | * @return WaylineJobEntity对象 |
| | | */ |
| | | private WaylineJobEntity dto2Entity(WaylineJobDTO dto) { |
| | | WaylineJobEntity.WaylineJobEntityBuilder builder = WaylineJobEntity.builder(); |
| | | if (dto == null) { |
| | | return builder.build(); |
| | | } |
| | | if (Objects.nonNull(dto.getBeginTime())) { |
| | | builder.beginTime(dto.getBeginTime().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()); |
| | | } |
| | | if (Objects.nonNull(dto.getEndTime())) { |
| | | builder.endTime(dto.getEndTime().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()); |
| | | } |
| | | if (Objects.nonNull(dto.getExecuteTime())) { |
| | | builder.executeTime(dto.getExecuteTime().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()); |
| | | } |
| | | if (Objects.nonNull(dto.getCompletedTime())) { |
| | | builder.completedTime(dto.getCompletedTime().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()); |
| | | } |
| | | return builder.status(dto.getStatus()) |
| | | .mediaCount(dto.getMediaCount()) |
| | | .name(dto.getJobName()) |
| | | .errorCode(dto.getCode()) |
| | | .jobId(dto.getJobId()) |
| | | .fileId(dto.getFileId()) |
| | | .dockSn(dto.getDockSn()) |
| | | .taskType(dto.getTaskType()) |
| | | .waylineType(dto.getWaylineType()) |
| | | .username(dto.getUsername()) |
| | | .rthAltitude(dto.getRthAltitude()) |
| | | .outOfControlAction(dto.getOutOfControlAction()) |
| | | .parentId(dto.getParentId()) |
| | | .loopSum(dto.getLoopSum()) |
| | | .build(); |
| | | } |
| | | |
| | | /** |
| | | * 将实体对象转换为数据传输对象 |
| | | * |
| | | * @param entity WaylineJobEntity 实体对象 |
| | | * @return WaylineJobDTO 数据传输对象 |
| | | */ |
| | | private WaylineJobDTO entity2Dto(WaylineJobEntity entity) { |
| | | if (entity == null) { |
| | | return null; |
| | | } |
| | | // 修改航线文件名称,从ard_route_info航线表中取; |
| | | String fileId = entity.getFileId(); |
| | | String fileName = ""; |
| | | try { |
| | | if (fileId != null) { |
| | | ArdRouteInfo ardRouteInfo = _ardRouteInfoService.selectArdRouteInfoById(fileId); |
| | | if (ardRouteInfo != null) { |
| | | fileName = Optional.ofNullable(ardRouteInfo.getName()).orElse(""); |
| | | } |
| | | } |
| | | } catch (Exception e) { |
| | | log.info("获取航线名称失败,fileId:{},异常原因:{}", fileId, e.getMessage()); |
| | | fileName = ""; |
| | | } |
| | | WaylineJobDTO.WaylineJobDTOBuilder builder = WaylineJobDTO.builder() |
| | | .jobId(entity.getJobId()) |
| | | .jobName(entity.getName()) |
| | | .fileId(entity.getFileId()) |
| | | // .fileName(waylineFileService.getWaylineByWaylineId(entity.getFileId()) |
| | | // .orElse(WaylineFileDTO.builder().build()).getName()) |
| | | .fileName(fileName) |
| | | .dockSn(entity.getDockSn()) |
| | | .dockName(deviceService.getDeviceBySn(entity.getDockSn()) |
| | | .orElse(DeviceDTO.builder().build()).getNickname()) |
| | | .username(entity.getUsername()) |
| | | .status(WaylineJobStatusEnum.IN_PROGRESS.getVal() == entity.getStatus() && |
| | | RedisOpsUtils.checkExist(RedisConst.WAYLINE_JOB_PAUSED_PREFIX + entity.getJobId()) ? |
| | | WaylineJobStatusEnum.PAUSED.getVal() : entity.getStatus()) |
| | | .code(entity.getErrorCode()) |
| | | .beginTime(LocalDateTime.ofInstant(Instant.ofEpochMilli(entity.getBeginTime()), ZoneId.systemDefault())) |
| | | .endTime(Objects.nonNull(entity.getEndTime()) ? |
| | | LocalDateTime.ofInstant(Instant.ofEpochMilli(entity.getEndTime()), ZoneId.systemDefault()) : null) |
| | | .executeTime(Objects.nonNull(entity.getExecuteTime()) ? |
| | | LocalDateTime.ofInstant(Instant.ofEpochMilli(entity.getExecuteTime()), ZoneId.systemDefault()) : null) |
| | | .completedTime(WaylineJobStatusEnum.find(entity.getStatus()).getEnd() ? |
| | | LocalDateTime.ofInstant(Instant.ofEpochMilli(entity.getUpdateTime()), ZoneId.systemDefault()) : null) |
| | | .taskType(entity.getTaskType()) |
| | | .waylineType(entity.getWaylineType()) |
| | | .rthAltitude(entity.getRthAltitude()) |
| | | .rthMode(entity.getRthMode()) |
| | | .outOfControlAction(entity.getOutOfControlAction()) |
| | | .mediaCount(entity.getMediaCount()) |
| | | .loopSum(entity.getLoopSum()); |
| | | |
| | | |
| | | if (Objects.nonNull(entity.getEndTime())) { |
| | | builder.endTime(LocalDateTime.ofInstant(Instant.ofEpochMilli(entity.getEndTime()), ZoneId.systemDefault())); |
| | | } |
| | | if (WaylineJobStatusEnum.IN_PROGRESS.getVal() == entity.getStatus() && RedisOpsUtils.getExpire(entity.getJobId()) > 0) { |
| | | EventsReceiver<WaylineTaskProgressReceiver> taskProgress = (EventsReceiver<WaylineTaskProgressReceiver>) RedisOpsUtils.get(RedisConst.WAYLINE_JOB_RUNNING_PREFIX + entity.getDockSn()); |
| | | if (Objects.nonNull(taskProgress.getOutput()) && Objects.nonNull(taskProgress.getOutput().getProgress())) { |
| | | builder.progress(taskProgress.getOutput().getProgress().getPercent()); |
| | | } |
| | | } |
| | | |
| | | if (entity.getMediaCount() == 0) { |
| | | return builder.build(); |
| | | } |
| | | |
| | | // sync the number of media files |
| | | String key = RedisConst.MEDIA_HIGHEST_PRIORITY_PREFIX + entity.getDockSn(); |
| | | String countKey = RedisConst.MEDIA_FILE_PREFIX + entity.getDockSn(); |
| | | Object mediaFileCount = RedisOpsUtils.hashGet(countKey, entity.getJobId()); |
| | | if (Objects.nonNull(mediaFileCount)) { |
| | | builder.uploadedCount(((MediaFileCountDTO) mediaFileCount).getUploadedCount()) |
| | | .uploading(RedisOpsUtils.checkExist(key) && entity.getJobId().equals(((MediaFileCountDTO) RedisOpsUtils.get(key)).getJobId())); |
| | | return builder.build(); |
| | | } |
| | | |
| | | int uploadedSize = fileService.getFilesByWorkspaceAndJobId(entity.getJobId()).size(); |
| | | // All media for this job have been uploaded. |
| | | if (uploadedSize >= entity.getMediaCount()) { |
| | | return builder.uploadedCount(uploadedSize).build(); |
| | | } |
| | | RedisOpsUtils.hashSet(countKey, entity.getJobId(), |
| | | MediaFileCountDTO.builder() |
| | | .jobId(entity.getJobId()) |
| | | .mediaCount(entity.getMediaCount()) |
| | | .uploadedCount(uploadedSize).build()); |
| | | return builder.build(); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.ard.sample.manage.model.enums; |
| | | |
| | | import lombok.Getter; |
| | | |
| | | /** |
| | | * 【机场1】机场机型代际枚举(Dock1 适配共享层新增)。 |
| | | * |
| | | * <p>判据为 device_model_key(三段 {@code domain-type-sub_type},落库逻辑见 |
| | | * DeviceServiceImpl.bindDevice2Entity)中的 type 段: |
| | | * 机场设备(domain=3)Dock1=1 / Dock2=2;飞行器(domain=0)M30 系列=67 / M3D 系列=91。 |
| | | * |
| | | * @author ard |
| | | * @date 2026/10/16 |
| | | */ |
| | | @Getter |
| | | public enum DockModelEnum { |
| | | |
| | | /** 机场1(DJI Dock 1):飞行器为 M30/M30T(type=67,sub_type 0=M30 / 1=M30T) */ |
| | | DOCK1(1, 67), |
| | | |
| | | /** 机场2(DJI Dock 2):飞行器为 M3D/M3TD(type=91,sub_type 0=M3D / 1=M3TD) */ |
| | | DOCK2(2, 91); |
| | | |
| | | /** 机场设备(domain=3)的 device type */ |
| | | private final int dockType; |
| | | |
| | | /** 飞行器(domain=0)的 device type */ |
| | | private final int droneType; |
| | | |
| | | DockModelEnum(int dockType, int droneType) { |
| | | this.dockType = dockType; |
| | | this.droneType = droneType; |
| | | } |
| | | |
| | | /** |
| | | * 按机场设备的 device type 解析机型。 |
| | | * |
| | | * @param type 机场设备 type(dock1=1 / dock2=2) |
| | | * @return 对应机型;未知返回 null |
| | | */ |
| | | public static DockModelEnum fromDockType(int type) { |
| | | for (DockModelEnum model : values()) { |
| | | if (model.dockType == type) { |
| | | return model; |
| | | } |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | /** |
| | | * 按飞行器的 device type 解析机型。 |
| | | * |
| | | * @param type 飞行器 type(M30 系列=67 / M3D 系列=91) |
| | | * @return 对应机型;未知返回 null |
| | | */ |
| | | public static DockModelEnum fromDroneType(int type) { |
| | | for (DockModelEnum model : values()) { |
| | | if (model.droneType == type) { |
| | | return model; |
| | | } |
| | | } |
| | | return null; |
| | | } |
| | | } |
| | |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.apache.poi.ss.formula.functions.T; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.beans.factory.annotation.Qualifier; |
| | | import org.springframework.dao.DuplicateKeyException; |
| | | import org.springframework.integration.annotation.ServiceActivator; |
| | | import org.springframework.integration.mqtt.support.MqttHeaders; |
| | | import org.springframework.messaging.Message; |
| | |
| | | @Resource |
| | | private AuthorizeService authorizeService; |
| | | |
| | | // 机场1:OSD 按机型分发(原为 @Qualifier("gatewayOSDServiceImpl") ITSAService tsaService,直连 dock2 链头) |
| | | @Autowired |
| | | @Qualifier("gatewayOSDServiceImpl") |
| | | private ITSAService tsaService; |
| | | private DockOsdDispatcher dockOsdDispatcher; |
| | | |
| | | @Resource |
| | | private ISysDeptService sysDeptService; |
| | |
| | | return; |
| | | } |
| | | |
| | | DeviceDTO subDevice = |
| | | getDevicesByParams(DeviceQueryParam.builder().deviceSn(gateway.getChildDeviceSn()).build()).get(0); |
| | | List<DeviceDTO> subDevices = |
| | | getDevicesByParams(DeviceQueryParam.builder().deviceSn(gateway.getChildDeviceSn()).build()); |
| | | if (CollectionUtils.isEmpty(subDevices)) { |
| | | log.warn("网关 {} 的子设备 {} 不存在,跳过拓扑拼接", gateway.getDeviceSn(), gateway.getChildDeviceSn()); |
| | | return; |
| | | } |
| | | DeviceDTO subDevice = subDevices.get(0); |
| | | subDevice.setStatus(RedisOpsUtils.checkExist(RedisConst.DEVICE_ONLINE_PREFIX + subDevice.getDeviceSn())); |
| | | gateway.setChildren(subDevice); |
| | | |
| | |
| | | if (!StringUtils.hasText(gateway.getChildDeviceSn())) { |
| | | return; |
| | | } |
| | | DeviceDTO subDevice = |
| | | getDevicesByParams(DeviceQueryParam.builder().deviceSn(gateway.getChildDeviceSn()).build()).get(0); |
| | | List<DeviceDTO> subDevices = |
| | | getDevicesByParams(DeviceQueryParam.builder().deviceSn(gateway.getChildDeviceSn()).build()); |
| | | if (CollectionUtils.isEmpty(subDevices)) { |
| | | log.warn("网关 {} 的子设备 {} 不存在,跳过拓扑拼接", gateway.getDeviceSn(), gateway.getChildDeviceSn()); |
| | | return; |
| | | } |
| | | DeviceDTO subDevice = subDevices.get(0); |
| | | subDevice.setStatus(RedisOpsUtils.checkExist(RedisConst.DEVICE_ONLINE_PREFIX + subDevice.getDeviceSn())); |
| | | gateway.setChildren(subDevice); |
| | | } |
| | |
| | | }) |
| | | .eq(param.getBoundStatus() != null, DeviceEntity::getBoundStatus, |
| | | param.getBoundStatus()) |
| | | .like(param.getNickname()!=null, DeviceEntity::getNickname, "%" + param.getNickname() + "%") |
| | | .like(StringUtils.hasText(param.getNickname()), DeviceEntity::getNickname, "%" + param.getNickname() + "%") |
| | | .orderBy(param.isOrderBy(), |
| | | param.isAsc(), DeviceEntity::getId)) |
| | | .stream() |
| | |
| | | .getValueAndUserType(UserTypeEnum.WEB.getVal()); |
| | | |
| | | |
| | | tsaService.handleOSD(receiver, device, webSessions, wsMessage); |
| | | // 机场1:OSD 按机型分发(dock1=67 → dock1 链;其余透传 dock2 原链) |
| | | dockOsdDispatcher.dispatch(receiver, device, webSessions, wsMessage); |
| | | |
| | | } catch (NullPointerException e) { |
| | | log.error("处理OSD消息NPE,headers={}, payload={}", |
| | |
| | | } |
| | | // 调用验证方法 |
| | | |
| | | try { |
| | | return mapper.insert(entity) > 0 ? Optional.of(entity) : Optional.empty(); |
| | | } catch (DuplicateKeyException e) { |
| | | // 机场首次上线时 MQTT QoS1 可能重复投递 update_topo(或多线程并发), |
| | | // 两个线程同时查到“设备不存在”后会并发 insert,后插入者触发唯一键冲突。 |
| | | // 此时数据已被另一线程写入,退化为更新即可,避免整条状态消息处理失败。 |
| | | DeviceEntity exist = mapper.selectOne( |
| | | new LambdaQueryWrapper<DeviceEntity>() |
| | | .eq(DeviceEntity::getDeviceSn, entity.getDeviceSn())); |
| | | if (exist == null) { |
| | | throw e; |
| | | } |
| | | entity.setId(exist.getId()); |
| | | mapper.updateById(entity); |
| | | return Optional.of(exist); |
| | | } |
| | | } |
| | | |
| | | /** |
| New file |
| | |
| | | package com.ard.sample.manage.service.impl; |
| | | |
| | | import com.ard.sample.manage.model.dto.DeviceDTO; |
| | | import com.ard.sample.manage.model.enums.DeviceDomainEnum; |
| | | import com.ard.sample.manage.model.enums.DockModelEnum; |
| | | |
| | | /** |
| | | * 【机场1】机型解析器(Dock1 适配共享层新增)。 |
| | | * |
| | | * <p>从 DeviceDTO 的 domain/type(即 device_model_key 三段 domain-type-sub_type 的落库结果) |
| | | * 解析设备所属的机场代际:机场设备按 type 判 1/2,飞行器按 type 判 67/91。 |
| | | * 无法识别(遥控器、未绑定设备、type 为空等)返回 null——调用方应兜底走 dock2 原链, |
| | | * 保证 dock2 与改造前行为完全一致。 |
| | | * |
| | | * @author ard |
| | | * @date 2026/10/16 |
| | | */ |
| | | public final class DockModelResolver { |
| | | |
| | | private DockModelResolver() { |
| | | } |
| | | |
| | | /** |
| | | * 解析设备所属机型。 |
| | | * |
| | | * @param device 在线设备(DeviceDTO 的 domain/type/subType 由绑定/上线时 device_model_key 三段解析落库) |
| | | * @return {@link DockModelEnum#DOCK1} / {@link DockModelEnum#DOCK2};无法识别返回 null |
| | | */ |
| | | public static DockModelEnum resolve(DeviceDTO device) { |
| | | if (device == null || device.getDomain() == null || device.getType() == null) { |
| | | return null; |
| | | } |
| | | if (DeviceDomainEnum.DOCK.getVal() == device.getDomain()) { |
| | | return DockModelEnum.fromDockType(device.getType()); |
| | | } |
| | | if (DeviceDomainEnum.SUB_DEVICE.getVal() == device.getDomain()) { |
| | | return DockModelEnum.fromDroneType(device.getType()); |
| | | } |
| | | return null; |
| | | } |
| | | } |
| | |
| | | import java.util.HashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.concurrent.ConcurrentHashMap; |
| | | import java.util.concurrent.TimeUnit; |
| | | |
| | | @Slf4j |
| | |
| | | RedisOpsUtils.set(RedisConst.OSD_STATUS_PREFIX + device.getDeviceSn(), 3); |
| | | RedisOpsUtils.del(RedisConst.WEBSOCKET_VIDEO_ADDRESS_PREFIX + device.getDeviceSn()); |
| | | RedisOpsUtils.del(RedisConst.COVER_STATE_PREFIX + device.getDeviceSn()); |
| | | } |
| | | else { |
| | | StartLiveStreaming(device.getDeviceSn()); |
| | | } |
| | | if(!_deviceService.checkDeviceOnline(device.getChildDeviceSn())){ |
| | | _deviceService.updateWorkType(device.getChildDeviceSn(), 6L); |
| | | RedisOpsUtils.del(RedisConst.WEBSOCKET_VIDEO_ADDRESS_PREFIX + device.getChildDeviceSn()); |
| | | RedisOpsUtils.del(RedisConst.COVER_STATE_PREFIX + device.getChildDeviceSn()); |
| | | } |
| | | else{ |
| | | StartLiveStreaming(device.getChildDeviceSn()); |
| | | clearLiveRetry(device.getDeviceSn()); |
| | | } else { |
| | | tryStartLiveStreaming(device.getDeviceSn()); |
| | | } |
| | | |
| | | // 机场1:未绑定飞行器时 childDeviceSn 为空,跳过,避免无效的在线判断和起流 |
| | | String childDeviceSn = device.getChildDeviceSn(); |
| | | if (childDeviceSn == null || childDeviceSn.isEmpty()) { |
| | | continue; |
| | | } |
| | | if (!_deviceService.checkDeviceOnline(childDeviceSn)) { |
| | | _deviceService.updateWorkType(childDeviceSn, 6L); |
| | | RedisOpsUtils.del(RedisConst.WEBSOCKET_VIDEO_ADDRESS_PREFIX + childDeviceSn); |
| | | RedisOpsUtils.del(RedisConst.COVER_STATE_PREFIX + childDeviceSn); |
| | | clearLiveRetry(childDeviceSn); |
| | | } else if (isDronePoweredOff(childDeviceSn)) { |
| | | // 飞行器舱内关机(6)或开机中(7)时相机尚未就绪,起流必然失败(如 513006),直接跳过 |
| | | clearLiveRetry(childDeviceSn); |
| | | } else { |
| | | tryStartLiveStreaming(childDeviceSn); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 按退避策略尝试开启直播。 |
| | | * 原实现每 2 秒对未就绪设备执行一次“停-开”,开机阶段会刷出大量 513006/613011 日志; |
| | | * 这里失败后按 2s、4s、8s、16s、30s 递增间隔重试。 |
| | | */ |
| | | private void tryStartLiveStreaming(String sn) { |
| | | if (!canRetryLive(sn)) { |
| | | return; |
| | | } |
| | | markLiveRetry(sn, StartLiveStreaming(sn)); |
| | | } |
| | | |
| | | /** |
| | | * 飞行器是否处于关机或开机中状态。 |
| | | * 6-舱内关机(机场 OSD 写入),7-开机中(下发 drone_open 时写入)。 |
| | | */ |
| | | private boolean isDronePoweredOff(String childDeviceSn) { |
| | | Object status = RedisOpsUtils.get(RedisConst.OSD_STATUS_PREFIX + childDeviceSn); |
| | | if (status == null) { |
| | | return false; |
| | | } |
| | | String value = String.valueOf(status); |
| | | return "6".equals(value) || "7".equals(value); |
| | | } |
| | | |
| | | /** |
| | | * 连续失败次数与下次允许重试的时间戳。 |
| | | */ |
| | | private static class LiveRetryState { |
| | | private int failures; |
| | | private long nextRetryTime; |
| | | } |
| | | |
| | | private static final long LIVE_RETRY_BASE_DELAY_MS = 2000L; |
| | | private static final long LIVE_RETRY_MAX_DELAY_MS = 30000L; |
| | | |
| | | private final Map<String, LiveRetryState> liveRetryStates = new ConcurrentHashMap<>(); |
| | | |
| | | private boolean canRetryLive(String sn) { |
| | | LiveRetryState state = liveRetryStates.get(sn); |
| | | return state == null || System.currentTimeMillis() >= state.nextRetryTime; |
| | | } |
| | | |
| | | private void markLiveRetry(String sn, boolean success) { |
| | | if (success) { |
| | | liveRetryStates.remove(sn); |
| | | return; |
| | | } |
| | | LiveRetryState state = liveRetryStates.computeIfAbsent(sn, k -> new LiveRetryState()); |
| | | state.failures++; |
| | | long delay = Math.min(LIVE_RETRY_BASE_DELAY_MS << Math.min(state.failures - 1, 4), LIVE_RETRY_MAX_DELAY_MS); |
| | | state.nextRetryTime = System.currentTimeMillis() + delay; |
| | | log.info("SN: {}-直播启动失败(第 {} 次),{} 秒后重试", sn, state.failures, delay / 1000); |
| | | } |
| | | |
| | | private void clearLiveRetry(String sn) { |
| | | liveRetryStates.remove(sn); |
| | | } |
| | | |
| | | /** |
| | |
| | | return jsonString; |
| | | } |
| | | |
| | | public void StartLiveStreaming(String sn) { |
| | | /** |
| | | * 开启直播(后台定时任务调用)。 |
| | | * |
| | | * @return true-已存在可用地址或起流成功;false-起流失败 |
| | | */ |
| | | public boolean StartLiveStreaming(String sn) { |
| | | //调用开始直播 |
| | | ResponseResult rlive = null; |
| | | String resultLive = ""; |
| | |
| | | // 判断param是否为空,不为空则返回正确的直播参数,为null则未获取直播参数,不为空则继续操作 |
| | | if (param == null) { |
| | | log.info("SN: {}-未获取到直播参数param为null,无法开启直播!", sn); |
| | | return; |
| | | return false; |
| | | } |
| | | // 如果存在直播则读取直播地址返回 |
| | | if (RedisOpsUtils.checkExist(RedisConst.WEBSOCKET_VIDEO_ADDRESS_PREFIX + sn)) { |
| | | resultLive = String.valueOf(RedisOpsUtils.get(RedisConst.WEBSOCKET_VIDEO_ADDRESS_PREFIX + sn)); |
| | | // log.info("SN: {}-redis中存在相机的播放地址信息,直接返回地址!", sn); |
| | | return true; |
| | | } else { |
| | | // log.info("SN: {}-redis中未找到相机的播放地址信息!停止直播后再开启直播", sn); |
| | | _liveStreamHandler.StopLiveStreaming(sn); |
| | |
| | | // if (isWeb) { |
| | | // log.info("SN: {}-更新webrtc地址成功!", sn); |
| | | // } |
| | | // 定时任务是后台自动起流,前端并没有发订阅,必须主动把地址推给已订阅该设备的页面; |
| | | // 否则飞行器起飞后相机就绪、后台起流成功,页面却收不到地址,只能刷新后才出画面 |
| | | _dockWebSocketHandler.sendMessageToSubscribers(sn, MessageType.LIVESTREAM.getType(), resultLive, ""); |
| | | return true; |
| | | } |
| | | log.info("SN: {}-开启直播失败!返回值:{}", sn, rlive); |
| | | } |
| | | } catch (Exception e) { |
| | | log.info("订时任务推流-SN: {}-开启直播失败!原因:{}", sn, e.getMessage()); |
| | | e.printStackTrace(); |
| | | } |
| | | return false; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.ard.sample.manage.service.impl; |
| | | |
| | | import com.ard.sample.component.mqtt.model.CommonTopicReceiver; |
| | | import com.ard.sample.component.websocket.config.ConcurrentWebSocketSession; |
| | | import com.ard.sample.component.websocket.model.CustomWebSocketMessage; |
| | | import com.ard.sample.manage.model.dto.DeviceDTO; |
| | | import com.ard.sample.manage.model.dto.TelemetryDTO; |
| | | import com.ard.sample.manage.model.enums.DockModelEnum; |
| | | import com.ard.sample.manage.service.ITSAService; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.beans.factory.annotation.Qualifier; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | import java.util.Collection; |
| | | |
| | | /** |
| | | * 【机场1】OSD 入站分发器(Dock1 适配共享层新增,唯一侵入共享层的分流点)。 |
| | | * |
| | | * <p>DeviceServiceImpl.handleOSD 收到 INBOUND_OSD 消息后委托本类,按设备机型分发到两条 OSD 处理链: |
| | | * <ul> |
| | | * <li>DOCK1(67) → dock1 链:{@code dock1GatewayOSDServiceImpl → dock1DeviceOSDServiceImpl → dock1DockOSDServiceImpl}</li> |
| | | * <li>其余(DOCK2(91) / 遥控器等无法识别来源)→ dock2 原链: |
| | | * {@code gatewayOSDServiceImpl → deviceOSDServiceImpl → dockOSDServiceImpl}(行为与改造前完全一致)</li> |
| | | * </ul> |
| | | * dock1 链为普通 @Service(不带 @ServiceActivator),INBOUND_OSD 通道消费者仍只有 DeviceServiceImpl 一个。 |
| | | * |
| | | * @author ard |
| | | * @date 2026/10/16 |
| | | */ |
| | | @Service |
| | | public class DockOsdDispatcher { |
| | | |
| | | @Autowired |
| | | @Qualifier("gatewayOSDServiceImpl") |
| | | private ITSAService dock2TsaService; |
| | | |
| | | @Autowired |
| | | @Qualifier("dock1GatewayOSDServiceImpl") |
| | | private ITSAService dock1TsaService; |
| | | |
| | | /** |
| | | * 分发 OSD 消息到对应机型的处理链。 |
| | | * |
| | | * @param receiver OSD 报文 |
| | | * @param device 消息来源设备(含 domain/type/subType) |
| | | * @param webSessions 目标 WebSocket 会话 |
| | | * @param wsMessage 待推送的 WebSocket 消息 |
| | | */ |
| | | public void dispatch(CommonTopicReceiver receiver, DeviceDTO device, |
| | | Collection<ConcurrentWebSocketSession> webSessions, |
| | | CustomWebSocketMessage<TelemetryDTO> wsMessage) { |
| | | DockModelEnum model = DockModelResolver.resolve(device); |
| | | if (DockModelEnum.DOCK1 == model) { |
| | | dock1TsaService.handleOSD(receiver, device, webSessions, wsMessage); |
| | | return; |
| | | } |
| | | // dock2 及一切无法识别的来源:原链透传,保证 dock2 行为零变化 |
| | | dock2TsaService.handleOSD(receiver, device, webSessions, wsMessage); |
| | | } |
| | | } |
| | |
| | | */ |
| | | @Override |
| | | public PaginationData<MediaFileDTO> getJobFiles(long page, long pageSize,String jobId, String isType) { |
| | | Page<MediaFileEntity> pageData = mapper.selectPage( |
| | | new Page<MediaFileEntity>(page, pageSize), |
| | | new LambdaQueryWrapper<MediaFileEntity>() |
| | | .eq(Objects.nonNull(jobId), MediaFileEntity::getJobId, jobId) |
| | | // .orderByDesc(MediaFileEntity::getId)); |
| | | // isType=1 时匹配 V.jpeg |
| | | .like("1".equals(isType), MediaFileEntity::getFileName, "V.jpeg") |
| | | // isType=0 时匹配 V.mp4 |
| | | .like("0".equals(isType), MediaFileEntity::getFileName, "V.mp4") |
| | | // 排序修改:按start_time正序(升序)排列 |
| | | .orderByAsc(MediaFileEntity::getStartTime)); // 关键改动1:替换排序方式 |
| | | LambdaQueryWrapper<MediaFileEntity> queryWrapper = new LambdaQueryWrapper<MediaFileEntity>() |
| | | .eq(Objects.nonNull(jobId), MediaFileEntity::getJobId, jobId); |
| | | // 按文件扩展名区分图片/视频,不再写死镜头前缀: |
| | | // Dock2 可见光文件名为 _V.jpeg/_V.mp4;机场1(M30T)三光相机为 _W/_Z/_T.jpeg(广角/变焦/红外)及 _W.mp4。 |
| | | // 原实现写死 V.jpeg/V.mp4,导致机场1的媒体文件在计划库中查询不到。 |
| | | if ("1".equals(isType)) { |
| | | queryWrapper.and(w -> w.like(MediaFileEntity::getFileName, ".jpeg") |
| | | .or().like(MediaFileEntity::getFileName, ".jpg")); |
| | | } else if ("0".equals(isType)) { |
| | | queryWrapper.like(MediaFileEntity::getFileName, ".mp4"); |
| | | } |
| | | // 排序:按 start_time 正序(升序)排列 |
| | | queryWrapper.orderByAsc(MediaFileEntity::getStartTime); |
| | | |
| | | Page<MediaFileEntity> pageData = mapper.selectPage(new Page<MediaFileEntity>(page, pageSize), queryWrapper); |
| | | |
| | | List<MediaFileDTO> records = pageData.getRecords() |
| | | .stream() |
| | |
| | | |
| | | import com.ard.common.config.OssConfiguration; |
| | | import com.ard.oss.model.CredentialsDTO; |
| | | import com.ard.sample.common.error.CommonErrorEnum; |
| | | import com.ard.sample.component.mqtt.model.*; |
| | | import com.ard.sample.component.mqtt.service.IMessageSenderService; |
| | | import com.ard.oss.model.StsCredentialsDTO; |
| | |
| | | @ServiceActivator(inputChannel = ChannelName.INBOUND_REQUESTS_STORAGE_CONFIG_GET, outputChannel = ChannelName.OUTBOUND) |
| | | public void replyConfigGet(CommonTopicReceiver receiver, MessageHeaders headers) { |
| | | // 20251219 这里是获取上传临时凭证的接口,稍候分析下是否满足条件,如果可以的话就直接回复临时凭证即可 |
| | | RequestsReply reply; |
| | | try { |
| | | reply = RequestsReply.success(this.getSTSCredentials()); |
| | | } catch (Exception e) { |
| | | // 机场1 上送 storage_config_get 时,若 MinIO STS 不可用,原实现会直接抛异常且不回复, |
| | | // 机场会每 5 秒重试一次并持续刷 error.log。这里回一个明确的失败应答,避免消息处理中断。 |
| | | // 底层 MinIOServiceImpl 已打印完整堆栈,此处仅记录关键信息。 |
| | | log.error("获取媒体上传临时凭证失败,gateway={},原因:{}", receiver.getGateway(), e.getMessage()); |
| | | reply = RequestsReply.error(CommonErrorEnum.SYSTEM_ERROR); |
| | | } |
| | | CommonTopicResponse<RequestsReply> response = CommonTopicResponse.<RequestsReply>builder() |
| | | .tid(receiver.getTid()) |
| | | .bid(receiver.getBid()) |
| | | .data(RequestsReply.success(this.getSTSCredentials())) |
| | | .data(reply) |
| | | .timestamp(System.currentTimeMillis()) |
| | | .method(RequestsMethodEnum.STORAGE_CONFIG_GET.getMethod()) |
| | | .build(); |
| New file |
| | |
| | | -- ===================================================================== |
| | | -- 机场1(DJI Dock 1 / M30)设备型号字典数据 |
| | | -- 表:manage_device_dictionary(DJI 上云线设备型号字典) |
| | | -- |
| | | -- 背景:经核实 2026-10-16,2025-11-06 数据备份 |
| | | -- (sql/backup/20251106091248.nb3)中以下数据行已随 |
| | | -- DJI 官方 Demo 种子数据存在('DJI Dock'=id18 / 'Matrice 30'=id2 / |
| | | -- 'Matrice 30T'=id3 等),开发库无需执行即可直接联调。 |
| | | -- 本脚本为幂等兜底:跨环境部署(dev / prod / daqing)若缺失则补齐, |
| | | -- 已存在则跳过,可重复执行。 |
| | | -- |
| | | -- 消费点(皆为共享层,由 dock1/dock2 两条链共用): |
| | | -- DeviceServiceImpl#deviceGatewayConvertToDeviceEntity :793 机场上线 domain=3 |
| | | -- DeviceServiceImpl#subDeviceConvertToDeviceEntity :827 飞行器上线 domain=0 |
| | | -- DeviceServiceImpl#bindDevice2Entity :1373 设备绑定 |
| | | -- DevicePayloadServiceImpl :205 负载上线 domain=1 |
| | | -- CapacityCameraServiceImpl :68 相机能力上报 domain=1 |
| | | -- FileServiceImpl :209 媒体上传 domain=1 |
| | | -- |
| | | -- 官方文档:https://developer.dji.com/doc/cloud-api-tutorial/cn/api-reference/ |
| | | -- dock-to-cloud/mqtt/dock/dock1/properties.html |
| | | -- (device_model_key 三段式 domain-type-sub_type 即下列前 3 列) |
| | | -- ===================================================================== |
| | | |
| | | -- 机场1(Dock1):domain=3(机场), device_type=1, sub_type=0 |
| | | INSERT INTO `manage_device_dictionary` (`domain`, `device_type`, `sub_type`, `device_name`, `device_desc`) |
| | | SELECT 3, 1, 0, 'DJI Dock', 'DJI Airport' FROM DUAL |
| | | WHERE NOT EXISTS (SELECT 1 FROM `manage_device_dictionary` WHERE `domain` = 3 AND `device_type` = 1 AND `sub_type` = 0); |
| | | |
| | | -- 飞行器 M30:domain=0(飞行器), device_type=67, sub_type=0 |
| | | INSERT INTO `manage_device_dictionary` (`domain`, `device_type`, `sub_type`, `device_name`, `device_desc`) |
| | | SELECT 0, 67, 0, 'Matrice 30', NULL FROM DUAL |
| | | WHERE NOT EXISTS (SELECT 1 FROM `manage_device_dictionary` WHERE `domain` = 0 AND `device_type` = 67 AND `sub_type` = 0); |
| | | |
| | | -- 飞行器 M30T:domain=0, device_type=67, sub_type=1 |
| | | INSERT INTO `manage_device_dictionary` (`domain`, `device_type`, `sub_type`, `device_name`, `device_desc`) |
| | | SELECT 0, 67, 1, 'Matrice 30T', NULL FROM DUAL |
| | | WHERE NOT EXISTS (SELECT 1 FROM `manage_device_dictionary` WHERE `domain` = 0 AND `device_type` = 67 AND `sub_type` = 1); |
| | | |
| | | -- M30 相机:domain=1(负载), device_type=52, sub_type=0 |
| | | INSERT INTO `manage_device_dictionary` (`domain`, `device_type`, `sub_type`, `device_name`, `device_desc`) |
| | | SELECT 1, 52, 0, 'M30 Camera', NULL FROM DUAL |
| | | WHERE NOT EXISTS (SELECT 1 FROM `manage_device_dictionary` WHERE `domain` = 1 AND `device_type` = 52 AND `sub_type` = 0); |
| | | |
| | | -- M30T 相机:domain=1, device_type=53, sub_type=0 |
| | | INSERT INTO `manage_device_dictionary` (`domain`, `device_type`, `sub_type`, `device_name`, `device_desc`) |
| | | SELECT 1, 53, 0, 'M30T Camera', NULL FROM DUAL |
| | | WHERE NOT EXISTS (SELECT 1 FROM `manage_device_dictionary` WHERE `domain` = 1 AND `device_type` = 53 AND `sub_type` = 0); |
| | | |
| | | -- 机场相机:domain=1, device_type=165, sub_type=0 |
| | | INSERT INTO `manage_device_dictionary` (`domain`, `device_type`, `sub_type`, `device_name`, `device_desc`) |
| | | SELECT 1, 165, 0, 'DJI Dock Camera', NULL FROM DUAL |
| | | WHERE NOT EXISTS (SELECT 1 FROM `manage_device_dictionary` WHERE `domain` = 1 AND `device_type` = 165 AND `sub_type` = 0); |
| | | |
| | | -- M30 遥控器:domain=2(遥控器), device_type=119, sub_type=0 |
| | | INSERT INTO `manage_device_dictionary` (`domain`, `device_type`, `sub_type`, `device_name`, `device_desc`) |
| | | SELECT 2, 119, 0, 'Matrice 30 Smart Controller', 'Remote control for M30' FROM DUAL |
| | | WHERE NOT EXISTS (SELECT 1 FROM `manage_device_dictionary` WHERE `domain` = 2 AND `device_type` = 119 AND `sub_type` = 0); |
| New file |
| | |
| | | -- ===================================================================== |
| | | -- 机场1(DJI Dock 1 / M30、M30T)前端航线机型字典兜底 |
| | | -- 表:sys_dict_type / sys_dict_data |
| | | -- |
| | | -- 背景: |
| | | -- uav-project3 的航线新建页(views/airway/components/createAirway.vue) |
| | | -- 与计划新建页(views/plan/components/addPlan.vue)都通过 |
| | | -- useDict('dji_m30_series') 读取 M30 系列机型选项。 |
| | | -- 若部署环境的 sys_dict_data 中缺少 0-67-1(Matrice 30T), |
| | | -- 则无法在航线库中新建 M30T 航线,计划页也会显示“暂无类型”。 |
| | | -- |
| | | -- 说明: |
| | | -- 本脚本仅做幂等兜底:已存在同 dict_type/dict_value 时跳过,可重复执行。 |
| | | -- M30(0-67-0)与 M30T(0-67-1)的 device_type 均为 67, |
| | | -- 子类型 sub_type 分别为 0 / 1(见 sql/dock1_dictionary.sql)。 |
| | | -- ===================================================================== |
| | | |
| | | -- 字典类型:dji_m30_series |
| | | INSERT INTO `sys_dict_type` (`dict_name`, `dict_type`, `status`, `create_by`, `create_time`, `remark`) |
| | | SELECT '经纬 M30 系列', 'dji_m30_series', '0', 'admin', sysdate(), '机场1(Dock1)航线机型' |
| | | FROM DUAL |
| | | WHERE NOT EXISTS (SELECT 1 FROM `sys_dict_type` WHERE `dict_type` = 'dji_m30_series'); |
| | | |
| | | -- 机型:M30 -> 0-67-0 |
| | | INSERT INTO `sys_dict_data` (`dict_sort`, `dict_label`, `dict_value`, `dict_type`, `is_default`, `status`, `create_by`, `create_time`, `remark`) |
| | | SELECT 1, 'Matrice 30', '0-67-0', 'dji_m30_series', 'N', '0', 'admin', sysdate(), '机场1 飞行器 M30' |
| | | FROM DUAL |
| | | WHERE NOT EXISTS (SELECT 1 FROM `sys_dict_data` WHERE `dict_type` = 'dji_m30_series' AND `dict_value` = '0-67-0'); |
| | | |
| | | -- 机型:M30T -> 0-67-1 |
| | | INSERT INTO `sys_dict_data` (`dict_sort`, `dict_label`, `dict_value`, `dict_type`, `is_default`, `status`, `create_by`, `create_time`, `remark`) |
| | | SELECT 2, 'Matrice 30T', '0-67-1', 'dji_m30_series', 'N', '0', 'admin', sysdate(), '机场1 飞行器 M30T' |
| | | FROM DUAL |
| | | WHERE NOT EXISTS (SELECT 1 FROM `sys_dict_data` WHERE `dict_type` = 'dji_m30_series' AND `dict_value` = '0-67-1'); |