45 changed files with 3048 additions and 178 deletions
@ -0,0 +1,58 @@ |
|||||||
|
import request, { IWorkspaceResponse } from '/@/api/http/request' |
||||||
|
import { ELocalStorageKey } from '/@/types' |
||||||
|
|
||||||
|
// DRC 链路
|
||||||
|
const DRC_API_PREFIX = '/control/api/v1' |
||||||
|
const workspaceId: string = localStorage.getItem(ELocalStorageKey.WorkspaceId) || '' |
||||||
|
|
||||||
|
export interface PostDrcBody { |
||||||
|
client_id?: string // token过期时,用于续期则必填
|
||||||
|
expire_sec?: number // 过期时间,单位秒,默认3600
|
||||||
|
} |
||||||
|
|
||||||
|
export interface DrcParams { |
||||||
|
address: string |
||||||
|
username: string |
||||||
|
password: string |
||||||
|
client_id: string |
||||||
|
expire_time: number // 过期时间
|
||||||
|
enable_tls: boolean // 是否开启tls
|
||||||
|
} |
||||||
|
|
||||||
|
// 获取 mqtt 连接认证
|
||||||
|
export async function postDrc (body: PostDrcBody): Promise<IWorkspaceResponse<DrcParams>> { |
||||||
|
const resp = await request.post(`${DRC_API_PREFIX}/workspaces/${workspaceId}/drc/connect`, body) |
||||||
|
return resp.data |
||||||
|
} |
||||||
|
|
||||||
|
export interface DrcEnterBody { |
||||||
|
client_id: string |
||||||
|
dock_sn: string |
||||||
|
expire_sec?: number // 过期时间,单位秒,默认3600
|
||||||
|
device_info?: { |
||||||
|
osd_frequency?: number |
||||||
|
hsi_frequency?: number |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export interface DrcEnterResp { |
||||||
|
sub: string[] // 需要订阅接收的topic
|
||||||
|
pub: string[] // 推送的topic地址
|
||||||
|
} |
||||||
|
|
||||||
|
// 进入飞行控制 (建立drc连接&获取云控控制权)
|
||||||
|
export async function postDrcEnter (body: DrcEnterBody): Promise<IWorkspaceResponse<DrcEnterResp>> { |
||||||
|
const resp = await request.post(`${DRC_API_PREFIX}/workspaces/${workspaceId}/drc/enter`, body) |
||||||
|
return resp.data |
||||||
|
} |
||||||
|
|
||||||
|
export interface DrcExitBody { |
||||||
|
client_id: string |
||||||
|
dock_sn: string |
||||||
|
} |
||||||
|
|
||||||
|
// 退出飞行控制 (退出drc连接&退出云控控制权)
|
||||||
|
export async function postDrcExit (body: DrcExitBody): Promise<IWorkspaceResponse<null>> { |
||||||
|
const resp = await request.post(`${DRC_API_PREFIX}/workspaces/${workspaceId}/drc/exit`, body) |
||||||
|
return resp.data |
||||||
|
} |
@ -0,0 +1,58 @@ |
|||||||
|
import request, { IWorkspaceResponse } from '/@/api/http/request' |
||||||
|
// import { ELocalStorageKey } from '/@/types'
|
||||||
|
|
||||||
|
const API_PREFIX = '/control/api/v1' |
||||||
|
// const workspaceId: string = localStorage.getItem(ELocalStorageKey.WorkspaceId) || '
|
||||||
|
|
||||||
|
// 获取飞行控制权
|
||||||
|
export async function postFlightAuth (sn: string): Promise<IWorkspaceResponse<null>> { |
||||||
|
const resp = await request.post(`${API_PREFIX}/devices/${sn}/authority/flight`) |
||||||
|
return resp.data |
||||||
|
} |
||||||
|
export enum WaylineLostControlActionInCommandFlight { |
||||||
|
CONTINUE = 0, |
||||||
|
EXEC_LOST_ACTION = 1 |
||||||
|
} |
||||||
|
export enum LostControlActionInCommandFLight { |
||||||
|
HOVER = 0, // 悬停
|
||||||
|
Land = 1, // 着陆
|
||||||
|
RETURN_HOME = 2, // 返航
|
||||||
|
} |
||||||
|
export interface PointBody { |
||||||
|
latitude: number; |
||||||
|
longitude: number; |
||||||
|
height: number; |
||||||
|
} |
||||||
|
export interface PostFlyToPointBody { |
||||||
|
max_speed: number, |
||||||
|
points: PointBody[] |
||||||
|
} |
||||||
|
|
||||||
|
// 飞向目标点
|
||||||
|
export async function postFlyToPoint (sn: string, body: PostFlyToPointBody): Promise<IWorkspaceResponse<null>> { |
||||||
|
const resp = await request.post(`${API_PREFIX}/devices/${sn}/jobs/fly-to-point`, body) |
||||||
|
return resp.data |
||||||
|
} |
||||||
|
|
||||||
|
// 停止飞向目标点
|
||||||
|
export async function deleteFlyToPoint (sn: string): Promise<IWorkspaceResponse<null>> { |
||||||
|
const resp = await request.delete(`${API_PREFIX}/devices/${sn}/jobs/fly-to-point`) |
||||||
|
return resp.data |
||||||
|
} |
||||||
|
|
||||||
|
export interface PostTakeoffToPointBody{ |
||||||
|
target_height: number; |
||||||
|
target_latitude: number; |
||||||
|
target_longitude: number; |
||||||
|
security_takeoff_height: number; // 安全起飞高
|
||||||
|
max_speed: number; // flyto过程中能达到的最大速度, 单位m/s 跟飞机档位有关
|
||||||
|
rc_lost_action: LostControlActionInCommandFLight; // 失控行为
|
||||||
|
rth_altitude: number; // 返航高度
|
||||||
|
exit_wayline_when_rc_lost: WaylineLostControlActionInCommandFlight |
||||||
|
} |
||||||
|
|
||||||
|
// 一键起飞
|
||||||
|
export async function postTakeoffToPoint (sn: string, body: PostTakeoffToPointBody): Promise<IWorkspaceResponse<null>> { |
||||||
|
const resp = await request.post(`${API_PREFIX}/devices/${sn}/jobs/takeoff-to-point`, body) |
||||||
|
return resp.data |
||||||
|
} |
@ -0,0 +1,93 @@ |
|||||||
|
import request, { IWorkspaceResponse } from '/@/api/http/request' |
||||||
|
import { CameraType, CameraMode } from '/@/types/live-stream' |
||||||
|
import { GimbalResetMode } from '/@/types/drone-control' |
||||||
|
// import { ELocalStorageKey } from '/@/types'
|
||||||
|
|
||||||
|
const API_PREFIX = '/control/api/v1' |
||||||
|
// const workspaceId: string = localStorage.getItem(ELocalStorageKey.WorkspaceId) || '
|
||||||
|
|
||||||
|
export interface PostPayloadAuthBody { |
||||||
|
payload_index: string |
||||||
|
} |
||||||
|
|
||||||
|
// 获取负载控制权
|
||||||
|
export async function postPayloadAuth (sn: string, body: PostPayloadAuthBody): Promise<IWorkspaceResponse<null>> { |
||||||
|
const resp = await request.post(`${API_PREFIX}/devices/${sn}/authority/payload`, body) |
||||||
|
return resp.data |
||||||
|
} |
||||||
|
|
||||||
|
// TODO: 画面拖动控制
|
||||||
|
export enum PayloadCommandsEnum { |
||||||
|
CameraModeSwitch = 'camera_mode_switch', |
||||||
|
CameraPhotoTake = 'camera_photo_take', |
||||||
|
CameraRecordingStart = 'camera_recording_start', |
||||||
|
CameraRecordingStop = 'camera_recording_stop', |
||||||
|
CameraFocalLengthSet = 'camera_focal_length_set', |
||||||
|
GimbalReset = 'gimbal_reset', |
||||||
|
CameraAim = 'camera_aim' |
||||||
|
} |
||||||
|
|
||||||
|
export interface PostCameraModeBody { |
||||||
|
payload_index: string |
||||||
|
camera_mode: CameraMode |
||||||
|
} |
||||||
|
|
||||||
|
export interface PostCameraPhotoBody { |
||||||
|
payload_index: string |
||||||
|
} |
||||||
|
|
||||||
|
export interface PostCameraRecordingBody { |
||||||
|
payload_index: string |
||||||
|
} |
||||||
|
|
||||||
|
export interface DeleteCameraRecordingParams { |
||||||
|
payload_index: string |
||||||
|
} |
||||||
|
|
||||||
|
export interface PostCameraFocalLengthBody { |
||||||
|
payload_index: string, |
||||||
|
camera_type: CameraType, |
||||||
|
zoom_factor: number |
||||||
|
} |
||||||
|
|
||||||
|
export interface PostGimbalResetBody{ |
||||||
|
payload_index: string, |
||||||
|
reset_mode: GimbalResetMode, |
||||||
|
} |
||||||
|
|
||||||
|
export interface PostCameraAimBody{ |
||||||
|
payload_index: string, |
||||||
|
camera_type: CameraType, |
||||||
|
locked: boolean, |
||||||
|
x: number, |
||||||
|
y: number, |
||||||
|
} |
||||||
|
|
||||||
|
export type PostPayloadCommandsBody = { |
||||||
|
cmd: PayloadCommandsEnum.CameraModeSwitch, |
||||||
|
data: PostCameraModeBody |
||||||
|
} | { |
||||||
|
cmd: PayloadCommandsEnum.CameraPhotoTake, |
||||||
|
data: PostCameraPhotoBody |
||||||
|
} | { |
||||||
|
cmd: PayloadCommandsEnum.CameraRecordingStart, |
||||||
|
data: PostCameraRecordingBody |
||||||
|
} | { |
||||||
|
cmd: PayloadCommandsEnum.CameraRecordingStop, |
||||||
|
data: DeleteCameraRecordingParams |
||||||
|
} | { |
||||||
|
cmd: PayloadCommandsEnum.CameraFocalLengthSet, |
||||||
|
data: PostCameraFocalLengthBody |
||||||
|
} | { |
||||||
|
cmd: PayloadCommandsEnum.GimbalReset, |
||||||
|
data: PostGimbalResetBody |
||||||
|
} | { |
||||||
|
cmd: PayloadCommandsEnum.CameraAim, |
||||||
|
data: PostCameraAimBody |
||||||
|
} |
||||||
|
|
||||||
|
// 发送负载名称
|
||||||
|
export async function postPayloadCommands (sn: string, body: PostPayloadCommandsBody): Promise<IWorkspaceResponse<null>> { |
||||||
|
const resp = await request.post(`${API_PREFIX}/devices/${sn}/payload/commands`, body) |
||||||
|
return resp.data |
||||||
|
} |
@ -0,0 +1,34 @@ |
|||||||
|
<template> |
||||||
|
<div class="drone-control-info-wrap"> |
||||||
|
<a-textarea v-model:value="info" placeholder="drc info" :rows="5" disabled/> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script lang="ts" setup> |
||||||
|
import { ref, defineProps, watch } from 'vue' |
||||||
|
|
||||||
|
const props = defineProps<{ |
||||||
|
message?: string, |
||||||
|
}>() |
||||||
|
|
||||||
|
const info = ref('') |
||||||
|
watch(() => props.message, message => { |
||||||
|
info.value = message || '' |
||||||
|
}, { |
||||||
|
immediate: true |
||||||
|
}) |
||||||
|
|
||||||
|
// const emit = defineEmits(['cancel', 'confirm']) |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss" scoped> |
||||||
|
.drone-control-info-wrap { |
||||||
|
&::v-deep{ |
||||||
|
textarea.ant-input { |
||||||
|
background-color: #000; |
||||||
|
color: #fff; |
||||||
|
white-space: pre-wrap; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,756 @@ |
|||||||
|
<template> |
||||||
|
<div class="drone-control-wrapper"> |
||||||
|
<div class="drone-control-header">Drone Flight Control</div> |
||||||
|
<div class="drone-control-box"> |
||||||
|
<div class="box"> |
||||||
|
<div class="row"> |
||||||
|
<div class="drone-control"><Button :ghost="!flightController" size="small" @click="onClickFightControl">{{ flightController ? 'Exit Remote Control' : 'Enter Remote Control'}}</Button></div> |
||||||
|
</div> |
||||||
|
<div class="row"> |
||||||
|
<div class="drone-control-direction"> |
||||||
|
<Button size="small" ghost @mousedown="onMouseDown(KeyCode.KEY_Q)" @onmouseup="onMouseUp"> |
||||||
|
<template #icon><UndoOutlined /></template><span class="word">Q</span> |
||||||
|
</Button> |
||||||
|
<Button size="small" ghost @mousedown="onMouseDown(KeyCode.KEY_W)" @onmouseup="onMouseUp"> |
||||||
|
<template #icon><UpOutlined/></template><span class="word">W</span> |
||||||
|
</Button> |
||||||
|
<Button size="small" ghost @mousedown="onMouseDown(KeyCode.KEY_E)" @onmouseup="onMouseUp"> |
||||||
|
<template #icon><RedoOutlined /></template><span class="word">E</span> |
||||||
|
</Button> |
||||||
|
<Button size="small" ghost @mousedown="onMouseDown(KeyCode.ARROW_UP)" @onmouseup="onMouseUp"> |
||||||
|
<template #icon><ArrowUpOutlined /></template> |
||||||
|
</Button> |
||||||
|
<br /> |
||||||
|
<Button size="small" ghost @mousedown="onMouseDown(KeyCode.KEY_A)" @onmouseup="onMouseUp"> |
||||||
|
<template #icon><LeftOutlined/></template><span class="word">A</span> |
||||||
|
</Button> |
||||||
|
<Button size="small" ghost @mousedown="onMouseDown(KeyCode.KEY_S)" @onmouseup="onMouseUp"> |
||||||
|
<template #icon><DownOutlined/></template><span class="word">S</span> |
||||||
|
</Button> |
||||||
|
<Button size="small" ghost @mousedown="onMouseDown(KeyCode.KEY_D)" @onmouseup="onMouseUp"> |
||||||
|
<template #icon><RightOutlined/></template><span class="word">D</span> |
||||||
|
</Button> |
||||||
|
<Button size="small" ghost @mousedown="onMouseDown(KeyCode.ARROW_DOWN)" @onmouseup="onMouseUp"> |
||||||
|
<template #icon><ArrowDownOutlined /></template> |
||||||
|
</Button> |
||||||
|
</div> |
||||||
|
<Button type="primary" size="small" danger ghost @click="handleEmergencyStop" > |
||||||
|
<template #icon><PauseCircleOutlined/></template><span>Break</span> |
||||||
|
</Button> |
||||||
|
</div> |
||||||
|
<div class="row"> |
||||||
|
<DroneControlPopover |
||||||
|
:visible="flyToPointPopoverData.visible" |
||||||
|
:loading="flyToPointPopoverData.loading" |
||||||
|
@confirm="($event) => onFlyToConfirm(true)" |
||||||
|
@cancel="($event) =>onFlyToConfirm(false)" |
||||||
|
> |
||||||
|
<template #formContent> |
||||||
|
<div class="form-content"> |
||||||
|
<div> |
||||||
|
<span class="form-label">latitude:</span> |
||||||
|
<a-input-number v-model:value="flyToPointPopoverData.latitude"/> |
||||||
|
</div> |
||||||
|
<div> |
||||||
|
<span class="form-label">longitude:</span> |
||||||
|
<a-input-number v-model:value="flyToPointPopoverData.longitude"/> |
||||||
|
</div> |
||||||
|
<div> |
||||||
|
<span class="form-label">height(m):</span> |
||||||
|
<a-input-number v-model:value="flyToPointPopoverData.height"/> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
<Button size="small" ghost @click="onShowFlyToPopover" > |
||||||
|
<span>Fly to</span> |
||||||
|
</Button> |
||||||
|
</DroneControlPopover> |
||||||
|
<Button size="small" ghost @click="onStopFlyToPoint" > |
||||||
|
<span>Stop Fly to</span> |
||||||
|
</Button> |
||||||
|
<DroneControlPopover |
||||||
|
:visible="takeoffToPointPopoverData.visible" |
||||||
|
:loading="takeoffToPointPopoverData.loading" |
||||||
|
@confirm="($event) => onTakeoffToPointConfirm(true)" |
||||||
|
@cancel="($event) =>onTakeoffToPointConfirm(false)" |
||||||
|
> |
||||||
|
<template #formContent> |
||||||
|
<div class="form-content"> |
||||||
|
<div> |
||||||
|
<span class="form-label">latitude:</span> |
||||||
|
<a-input-number v-model:value="takeoffToPointPopoverData.latitude"/> |
||||||
|
</div> |
||||||
|
<div> |
||||||
|
<span class="form-label">longitude:</span> |
||||||
|
<a-input-number v-model:value="takeoffToPointPopoverData.longitude"/> |
||||||
|
</div> |
||||||
|
<div> |
||||||
|
<span class="form-label">height(m):</span> |
||||||
|
<a-input-number v-model:value="takeoffToPointPopoverData.height"/> |
||||||
|
</div> |
||||||
|
<div> |
||||||
|
<span class="form-label">Safe Takeoff Altitude(m):</span> |
||||||
|
<a-input-number v-model:value="takeoffToPointPopoverData.securityTakeoffHeight"/> |
||||||
|
</div> |
||||||
|
<div> |
||||||
|
<span class="form-label">Return-to-Home Altitude(m):</span> |
||||||
|
<a-input-number v-model:value="takeoffToPointPopoverData.rthAltitude"/> |
||||||
|
</div> |
||||||
|
<div> |
||||||
|
<span class="form-label">Lost Action:</span> |
||||||
|
<a-select |
||||||
|
v-model:value="takeoffToPointPopoverData.rcLostAction" |
||||||
|
style="width: 120px" |
||||||
|
:options="LostControlActionInCommandFLightOptions" |
||||||
|
></a-select> |
||||||
|
</div> |
||||||
|
<div> |
||||||
|
<span class="form-label">Wayline Lost Action:</span> |
||||||
|
<a-select |
||||||
|
v-model:value="takeoffToPointPopoverData.exitWaylineWhenRcLost" |
||||||
|
style="width: 120px" |
||||||
|
:options="WaylineLostControlActionInCommandFlightOptions" |
||||||
|
></a-select> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
<Button size="small" ghost @click="onShowTakeoffToPointPopover" > |
||||||
|
<span>Take off</span> |
||||||
|
</Button> |
||||||
|
</DroneControlPopover> |
||||||
|
<Button :loading="cmdItem.loading" size="small" ghost @click="sendControlCmd(cmdItem, index)"> |
||||||
|
{{ cmdItem.operateText }} |
||||||
|
</Button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="box"> |
||||||
|
<div class="row"> |
||||||
|
<Select v-model:value="payloadSelectInfo.value" style="width: 110px; marginRight: 5px" :options="payloadSelectInfo.options" @change="handlePayloadChange"/> |
||||||
|
<div class="drone-control"> |
||||||
|
<Button type="primary" size="small" @click="onAuthPayload">Payload Control</Button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="row"> |
||||||
|
<DroneControlPopover |
||||||
|
:visible="gimbalResetPopoverData.visible" |
||||||
|
:loading="gimbalResetPopoverData.loading" |
||||||
|
@confirm="($event) => onGimbalResetConfirm(true)" |
||||||
|
@cancel="($event) =>onGimbalResetConfirm(false)" |
||||||
|
> |
||||||
|
<template #formContent> |
||||||
|
<div class="form-content"> |
||||||
|
<div> |
||||||
|
<span class="form-label">reset mode:</span> |
||||||
|
<a-select |
||||||
|
v-model:value="gimbalResetPopoverData.resetMode" |
||||||
|
style="width: 180px" |
||||||
|
:options="GimbalResetModeOptions" |
||||||
|
></a-select> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
<Button size="small" ghost @click="onShowGimbalResetPopover"> |
||||||
|
<span>Gimbal Reset</span> |
||||||
|
</Button> |
||||||
|
</DroneControlPopover> |
||||||
|
<Button size="small" ghost @click="onSwitchCameraMode"> |
||||||
|
<span>Camera Mode Switch</span> |
||||||
|
</Button> |
||||||
|
</div> |
||||||
|
<div class="row"> |
||||||
|
<Button size="small" ghost @click="onStartCameraRecording"> |
||||||
|
<span>Start Recording</span> |
||||||
|
</Button> |
||||||
|
<Button size="small" ghost @click="onStopCameraRecording"> |
||||||
|
<span>Stop Recording</span> |
||||||
|
</Button> |
||||||
|
</div> |
||||||
|
<div class="row"> |
||||||
|
<Button size="small" ghost @click="onTakeCameraPhoto"> |
||||||
|
<span>Take Photo</span> |
||||||
|
</Button> |
||||||
|
<DroneControlPopover |
||||||
|
:visible="zoomFactorPopoverData.visible" |
||||||
|
:loading="zoomFactorPopoverData.loading" |
||||||
|
@confirm="($event) => onZoomFactorConfirm(true)" |
||||||
|
@cancel="($event) =>onZoomFactorConfirm(false)" |
||||||
|
> |
||||||
|
<template #formContent> |
||||||
|
<div class="form-content"> |
||||||
|
<div> |
||||||
|
<span class="form-label">camera type:</span> |
||||||
|
<a-select |
||||||
|
v-model:value="zoomFactorPopoverData.cameraType" |
||||||
|
style="width: 120px" |
||||||
|
:options="ZoomCameraTypeOptions" |
||||||
|
></a-select> |
||||||
|
</div> |
||||||
|
<div> |
||||||
|
<span class="form-label">zoom factor:</span> |
||||||
|
<a-input-number v-model:value="zoomFactorPopoverData.zoomFactor" :min="2" :max="200" /> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
<Button size="small" ghost @click="($event) => onShowZoomFactorPopover()"> |
||||||
|
<span class="word" @click=";">Zoom</span> |
||||||
|
</Button> |
||||||
|
</DroneControlPopover> |
||||||
|
<DroneControlPopover |
||||||
|
:visible="cameraAimPopoverData.visible" |
||||||
|
:loading="cameraAimPopoverData.loading" |
||||||
|
@confirm="($event) => onCameraAimConfirm(true)" |
||||||
|
@cancel="($event) =>onCameraAimConfirm(false)" |
||||||
|
> |
||||||
|
<template #formContent> |
||||||
|
<div class="form-content"> |
||||||
|
<div> |
||||||
|
<span class="form-label">camera type:</span> |
||||||
|
<a-select |
||||||
|
v-model:value="cameraAimPopoverData.cameraType" |
||||||
|
style="width: 120px" |
||||||
|
:options="CameraTypeOptions" |
||||||
|
></a-select> |
||||||
|
</div> |
||||||
|
<div> |
||||||
|
<span class="form-label">locked:</span> |
||||||
|
<a-switch v-model:checked="cameraAimPopoverData.locked"/> |
||||||
|
</div> |
||||||
|
<div> |
||||||
|
<span class="form-label">x:</span> |
||||||
|
<a-input-number v-model:value="cameraAimPopoverData.x" :min="0" :max="1"/> |
||||||
|
</div> |
||||||
|
<div> |
||||||
|
<span class="form-label">y:</span> |
||||||
|
<a-input-number v-model:value="cameraAimPopoverData.y" :min="0" :max="1"/> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
<Button size="small" ghost @click="($event) => onShowCameraAimPopover()"> |
||||||
|
<span class="word" @click=";">AIM</span> |
||||||
|
</Button> |
||||||
|
</DroneControlPopover> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<!-- 信息提示 --> |
||||||
|
<DroneControlInfoPanel :message="drcInfo"></DroneControlInfoPanel> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script setup lang="ts"> |
||||||
|
import { defineProps, reactive, ref, watch, computed, onMounted, watchEffect } from 'vue' |
||||||
|
import { Select, message, Button } from 'ant-design-vue' |
||||||
|
import { PayloadInfo, DeviceInfoType, ControlSource, DeviceOsdCamera, DrcStateEnum } from '/@/types/device' |
||||||
|
import { useMyStore } from '/@/store' |
||||||
|
import { postDrcEnter, postDrcExit } from '/@/api/drc' |
||||||
|
import { useMqtt, DeviceTopicInfo } from './use-mqtt' |
||||||
|
import { DownOutlined, UpOutlined, LeftOutlined, RightOutlined, PauseCircleOutlined, UndoOutlined, RedoOutlined, ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons-vue' |
||||||
|
import { useManualControl, KeyCode } from './use-manual-control' |
||||||
|
import { usePayloadControl } from './use-payload-control' |
||||||
|
import { CameraMode, CameraType, CameraTypeOptions, ZoomCameraTypeOptions, CameraListItem } from '/@/types/live-stream' |
||||||
|
import { useDroneControlWsEvent } from './use-drone-control-ws-event' |
||||||
|
import { useDroneControlMqttEvent } from './use-drone-control-mqtt-event' |
||||||
|
import { postFlightAuth, LostControlActionInCommandFLight, WaylineLostControlActionInCommandFlight } from '/@/api/drone-control/drone' |
||||||
|
import { useDroneControl } from './use-drone-control' |
||||||
|
import { GimbalResetMode, GimbalResetModeOptions, LostControlActionInCommandFLightOptions, WaylineLostControlActionInCommandFlightOptions } from '/@/types/drone-control' |
||||||
|
import DroneControlPopover from './DroneControlPopover.vue' |
||||||
|
import DroneControlInfoPanel from './DroneControlInfoPanel.vue' |
||||||
|
import { noDebugCmdList as baseCmdList, DeviceCmdItem, DeviceCmd } from '/@/types/device-cmd' |
||||||
|
import { useDockControl } from './use-dock-control' |
||||||
|
|
||||||
|
const props = defineProps<{ |
||||||
|
sn: string, |
||||||
|
deviceInfo: DeviceInfoType, |
||||||
|
payloads: null | PayloadInfo[] |
||||||
|
}>() |
||||||
|
|
||||||
|
const store = useMyStore() |
||||||
|
const clientId = computed(() => { |
||||||
|
return store.state.clientId |
||||||
|
}) |
||||||
|
|
||||||
|
const initCmdList = baseCmdList.find(item => item.cmdKey === DeviceCmd.ReturnHome) |
||||||
|
const cmdItem = ref(initCmdList) |
||||||
|
|
||||||
|
const { |
||||||
|
sendDockControlCmd |
||||||
|
} = useDockControl() |
||||||
|
|
||||||
|
async function sendControlCmd (cmdItem: DeviceCmdItem, index: number) { |
||||||
|
cmdItem.loading = true |
||||||
|
const result = await sendDockControlCmd({ |
||||||
|
sn: props.sn, |
||||||
|
cmd: cmdItem.cmdKey, |
||||||
|
action: cmdItem.action |
||||||
|
}, false) |
||||||
|
if (result && flightController.value) { |
||||||
|
exitFlightCOntrol() |
||||||
|
} |
||||||
|
cmdItem.loading = false |
||||||
|
} |
||||||
|
|
||||||
|
const { flyToPoint, stopFlyToPoint, takeoffToPoint } = useDroneControl() |
||||||
|
const MAX_SPEED = 14 |
||||||
|
|
||||||
|
const flyToPointPopoverData = reactive({ |
||||||
|
visible: false, |
||||||
|
loading: false, |
||||||
|
latitude: null as null | number, |
||||||
|
longitude: null as null | number, |
||||||
|
height: null as null | number, |
||||||
|
maxSpeed: MAX_SPEED, |
||||||
|
}) |
||||||
|
|
||||||
|
function onShowFlyToPopover () { |
||||||
|
flyToPointPopoverData.visible = !flyToPointPopoverData.visible |
||||||
|
flyToPointPopoverData.loading = false |
||||||
|
flyToPointPopoverData.latitude = null |
||||||
|
flyToPointPopoverData.longitude = null |
||||||
|
flyToPointPopoverData.height = null |
||||||
|
} |
||||||
|
|
||||||
|
async function onFlyToConfirm (confirm: boolean) { |
||||||
|
if (confirm) { |
||||||
|
if (!flyToPointPopoverData.height || !flyToPointPopoverData.latitude || !flyToPointPopoverData.longitude) { |
||||||
|
message.error('Input error') |
||||||
|
return |
||||||
|
} |
||||||
|
try { |
||||||
|
await flyToPoint(props.sn, { |
||||||
|
max_speed: flyToPointPopoverData.maxSpeed, |
||||||
|
points: [ |
||||||
|
{ |
||||||
|
latitude: flyToPointPopoverData.latitude, |
||||||
|
longitude: flyToPointPopoverData.longitude, |
||||||
|
height: flyToPointPopoverData.height |
||||||
|
} |
||||||
|
] |
||||||
|
}) |
||||||
|
} catch (error) { |
||||||
|
} |
||||||
|
} |
||||||
|
flyToPointPopoverData.visible = false |
||||||
|
} |
||||||
|
|
||||||
|
async function onStopFlyToPoint () { |
||||||
|
await stopFlyToPoint(props.sn) |
||||||
|
} |
||||||
|
|
||||||
|
const takeoffToPointPopoverData = reactive({ |
||||||
|
visible: false, |
||||||
|
loading: false, |
||||||
|
latitude: null as null | number, |
||||||
|
longitude: null as null | number, |
||||||
|
height: null as null | number, |
||||||
|
securityTakeoffHeight: null as null | number, |
||||||
|
maxSpeed: MAX_SPEED, |
||||||
|
rthAltitude: null as null | number, |
||||||
|
rcLostAction: LostControlActionInCommandFLight.RETURN_HOME, |
||||||
|
exitWaylineWhenRcLost: WaylineLostControlActionInCommandFlight.RETURN_HOME |
||||||
|
}) |
||||||
|
|
||||||
|
function onShowTakeoffToPointPopover () { |
||||||
|
takeoffToPointPopoverData.visible = !takeoffToPointPopoverData.visible |
||||||
|
takeoffToPointPopoverData.loading = false |
||||||
|
takeoffToPointPopoverData.latitude = null |
||||||
|
takeoffToPointPopoverData.longitude = null |
||||||
|
takeoffToPointPopoverData.securityTakeoffHeight = null |
||||||
|
takeoffToPointPopoverData.rthAltitude = null |
||||||
|
takeoffToPointPopoverData.rcLostAction = LostControlActionInCommandFLight.RETURN_HOME |
||||||
|
takeoffToPointPopoverData.exitWaylineWhenRcLost = WaylineLostControlActionInCommandFlight.RETURN_HOME |
||||||
|
} |
||||||
|
|
||||||
|
async function onTakeoffToPointConfirm (confirm: boolean) { |
||||||
|
if (confirm) { |
||||||
|
if (!takeoffToPointPopoverData.height || |
||||||
|
!takeoffToPointPopoverData.latitude || |
||||||
|
!takeoffToPointPopoverData.longitude || |
||||||
|
!takeoffToPointPopoverData.securityTakeoffHeight || |
||||||
|
!takeoffToPointPopoverData.rthAltitude) { |
||||||
|
message.error('Input error') |
||||||
|
return |
||||||
|
} |
||||||
|
try { |
||||||
|
await takeoffToPoint(props.sn, { |
||||||
|
target_latitude: takeoffToPointPopoverData.latitude, |
||||||
|
target_longitude: takeoffToPointPopoverData.longitude, |
||||||
|
target_height: takeoffToPointPopoverData.height, |
||||||
|
security_takeoff_height: takeoffToPointPopoverData.securityTakeoffHeight, |
||||||
|
rth_altitude: takeoffToPointPopoverData.rthAltitude, |
||||||
|
max_speed: takeoffToPointPopoverData.maxSpeed, |
||||||
|
rc_lost_action: takeoffToPointPopoverData.rcLostAction, |
||||||
|
exit_wayline_when_rc_lost: takeoffToPointPopoverData.exitWaylineWhenRcLost |
||||||
|
}) |
||||||
|
} catch (error) { |
||||||
|
} |
||||||
|
} |
||||||
|
takeoffToPointPopoverData.visible = false |
||||||
|
} |
||||||
|
|
||||||
|
const deviceTopicInfo: DeviceTopicInfo = reactive({ |
||||||
|
sn: props.sn, |
||||||
|
pubTopic: '', |
||||||
|
subTopic: '' |
||||||
|
}) |
||||||
|
|
||||||
|
useMqtt(deviceTopicInfo) |
||||||
|
|
||||||
|
// 飞行控制 |
||||||
|
const drcState = computed(() => { |
||||||
|
return store.state.deviceState?.dockInfo[props.sn]?.link_osd?.drc_state === DrcStateEnum.CONNECTED |
||||||
|
}) |
||||||
|
const flightController = ref(drcState.value) |
||||||
|
|
||||||
|
async function onClickFightControl () { |
||||||
|
if (flightController.value) { |
||||||
|
exitFlightCOntrol() |
||||||
|
return |
||||||
|
} |
||||||
|
enterFlightControl() |
||||||
|
} |
||||||
|
|
||||||
|
// 进入飞行控制 |
||||||
|
async function enterFlightControl () { |
||||||
|
try { |
||||||
|
const { code, data } = await postDrcEnter({ |
||||||
|
client_id: clientId.value, |
||||||
|
dock_sn: props.sn, |
||||||
|
}) |
||||||
|
if (code === 0) { |
||||||
|
flightController.value = true |
||||||
|
if (data.sub && data.sub.length > 0) { |
||||||
|
deviceTopicInfo.subTopic = data.sub[0] |
||||||
|
} |
||||||
|
if (data.pub && data.pub.length > 0) { |
||||||
|
deviceTopicInfo.pubTopic = data.pub[0] |
||||||
|
} |
||||||
|
// 获取飞行控制权 |
||||||
|
if (droneControlSource.value !== ControlSource.A) { |
||||||
|
await postFlightAuth(props.sn) |
||||||
|
} |
||||||
|
message.success('Get flight control successfully') |
||||||
|
} |
||||||
|
} catch (error: any) { |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 退出飞行控制 |
||||||
|
async function exitFlightCOntrol () { |
||||||
|
try { |
||||||
|
const { code } = await postDrcExit({ |
||||||
|
client_id: clientId.value, |
||||||
|
dock_sn: props.sn, |
||||||
|
}) |
||||||
|
if (code === 0) { |
||||||
|
flightController.value = false |
||||||
|
deviceTopicInfo.subTopic = '' |
||||||
|
deviceTopicInfo.pubTopic = '' |
||||||
|
message.success('Exit flight control') |
||||||
|
} |
||||||
|
} catch (error: any) { |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// drc mqtt message |
||||||
|
const { drcInfo } = useDroneControlMqttEvent(props.sn) |
||||||
|
|
||||||
|
const { |
||||||
|
handleKeyup, |
||||||
|
handleEmergencyStop, |
||||||
|
resetControlState, |
||||||
|
} = useManualControl(deviceTopicInfo, flightController) |
||||||
|
|
||||||
|
function onMouseDown (type: KeyCode) { |
||||||
|
handleKeyup(type) |
||||||
|
} |
||||||
|
|
||||||
|
function onMouseUp () { |
||||||
|
resetControlState() |
||||||
|
} |
||||||
|
|
||||||
|
// 负载控制 |
||||||
|
const payloadSelectInfo = { |
||||||
|
value: null as any, |
||||||
|
controlSource: undefined as undefined | ControlSource, |
||||||
|
options: [] as any, |
||||||
|
payloadIndex: '' as string, |
||||||
|
camera: undefined as undefined | DeviceOsdCamera // 当前负载osd信息 |
||||||
|
} |
||||||
|
|
||||||
|
const handlePayloadChange = (value: string) => { |
||||||
|
const payload = props.payloads?.find(item => item.payload_sn === value) |
||||||
|
if (payload) { |
||||||
|
payloadSelectInfo.payloadIndex = payload.payload_index || '' |
||||||
|
payloadSelectInfo.controlSource = payload.control_source |
||||||
|
payloadSelectInfo.camera = undefined |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// function getCurrentCamera (cameraList: CameraListItem[], cameraIndex?: string):CameraListItem | null { |
||||||
|
// let camera = null |
||||||
|
// cameraList.forEach(item => { |
||||||
|
// if (item.camera_index === cameraIndex) { |
||||||
|
// camera = item |
||||||
|
// } |
||||||
|
// }) |
||||||
|
// return camera |
||||||
|
// } |
||||||
|
|
||||||
|
// const currentCamera = computed(() => { |
||||||
|
// return getCurrentCamera(props.deviceInfo.dock.basic_osd.live_capacity?.device_list[0]?.camera_list as CameraListItem[], camera_index) |
||||||
|
// }) |
||||||
|
// 更新负载信息 |
||||||
|
watch(() => props.payloads, (payloads) => { |
||||||
|
if (payloads && payloads.length > 0) { |
||||||
|
payloadSelectInfo.value = payloads[0].payload_sn |
||||||
|
payloadSelectInfo.controlSource = payloads[0].control_source || ControlSource.B |
||||||
|
payloadSelectInfo.payloadIndex = payloads[0].payload_index || '' |
||||||
|
payloadSelectInfo.options = payloads.map(item => ({ label: item.payload_name, value: item.payload_sn })) |
||||||
|
payloadSelectInfo.camera = undefined |
||||||
|
} else { |
||||||
|
payloadSelectInfo.value = null |
||||||
|
payloadSelectInfo.controlSource = undefined |
||||||
|
payloadSelectInfo.options = [] |
||||||
|
payloadSelectInfo.payloadIndex = '' |
||||||
|
payloadSelectInfo.camera = undefined |
||||||
|
} |
||||||
|
}, { |
||||||
|
immediate: true, |
||||||
|
deep: true |
||||||
|
}) |
||||||
|
watch(() => props.deviceInfo.device, (droneOsd) => { |
||||||
|
if (droneOsd && droneOsd.cameras) { |
||||||
|
payloadSelectInfo.camera = droneOsd.cameras.find(item => item.payload_index === payloadSelectInfo.payloadIndex) |
||||||
|
} else { |
||||||
|
payloadSelectInfo.camera = undefined |
||||||
|
} |
||||||
|
}, { |
||||||
|
immediate: true, |
||||||
|
deep: true |
||||||
|
}) |
||||||
|
|
||||||
|
// ws 消息通知 |
||||||
|
const { droneControlSource, payloadControlSource } = useDroneControlWsEvent(props.sn, payloadSelectInfo.value) |
||||||
|
watch(() => payloadControlSource, (controlSource) => { |
||||||
|
payloadSelectInfo.controlSource = controlSource.value |
||||||
|
}, { |
||||||
|
immediate: true, |
||||||
|
deep: true |
||||||
|
}) |
||||||
|
const { |
||||||
|
checkPayloadAuth, |
||||||
|
authPayload, |
||||||
|
resetGimbal, |
||||||
|
switchCameraMode, |
||||||
|
takeCameraPhoto, |
||||||
|
startCameraRecording, |
||||||
|
stopCameraRecording, |
||||||
|
changeCameraFocalLength, |
||||||
|
cameraAim, |
||||||
|
} = usePayloadControl() |
||||||
|
|
||||||
|
async function onAuthPayload () { |
||||||
|
const result = await authPayload(props.sn, payloadSelectInfo.payloadIndex) |
||||||
|
if (result) { |
||||||
|
payloadControlSource.value = ControlSource.A |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const gimbalResetPopoverData = reactive({ |
||||||
|
visible: false, |
||||||
|
loading: false, |
||||||
|
resetMode: null as null | GimbalResetMode, |
||||||
|
}) |
||||||
|
|
||||||
|
function onShowGimbalResetPopover () { |
||||||
|
gimbalResetPopoverData.visible = !gimbalResetPopoverData.visible |
||||||
|
gimbalResetPopoverData.loading = false |
||||||
|
gimbalResetPopoverData.resetMode = null |
||||||
|
} |
||||||
|
|
||||||
|
async function onGimbalResetConfirm (confirm: boolean) { |
||||||
|
if (confirm) { |
||||||
|
if (gimbalResetPopoverData.resetMode === null) { |
||||||
|
message.error('Please select reset mode') |
||||||
|
return |
||||||
|
} |
||||||
|
gimbalResetPopoverData.loading = true |
||||||
|
try { |
||||||
|
await resetGimbal(props.sn, { |
||||||
|
payload_index: payloadSelectInfo.payloadIndex, |
||||||
|
reset_mode: gimbalResetPopoverData.resetMode |
||||||
|
}) |
||||||
|
} catch (err) { |
||||||
|
} |
||||||
|
} |
||||||
|
gimbalResetPopoverData.visible = false |
||||||
|
} |
||||||
|
|
||||||
|
async function onSwitchCameraMode () { |
||||||
|
if (!checkPayloadAuth(payloadSelectInfo.controlSource)) { |
||||||
|
return |
||||||
|
} |
||||||
|
const currentCameraMode = payloadSelectInfo.camera?.camera_mode |
||||||
|
await switchCameraMode(props.sn, { |
||||||
|
payload_index: payloadSelectInfo.payloadIndex, |
||||||
|
camera_mode: currentCameraMode === CameraMode.Photo ? CameraMode.Video : CameraMode.Photo |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
async function onTakeCameraPhoto () { |
||||||
|
if (!checkPayloadAuth(payloadSelectInfo.controlSource)) { |
||||||
|
return |
||||||
|
} |
||||||
|
await takeCameraPhoto(props.sn, payloadSelectInfo.payloadIndex) |
||||||
|
} |
||||||
|
|
||||||
|
async function onStartCameraRecording () { |
||||||
|
if (!checkPayloadAuth(payloadSelectInfo.controlSource)) { |
||||||
|
return |
||||||
|
} |
||||||
|
await startCameraRecording(props.sn, payloadSelectInfo.payloadIndex) |
||||||
|
} |
||||||
|
|
||||||
|
async function onStopCameraRecording () { |
||||||
|
if (!checkPayloadAuth(payloadSelectInfo.controlSource)) { |
||||||
|
return |
||||||
|
} |
||||||
|
await stopCameraRecording(props.sn, payloadSelectInfo.payloadIndex) |
||||||
|
} |
||||||
|
|
||||||
|
const zoomFactorPopoverData = reactive({ |
||||||
|
visible: false, |
||||||
|
loading: false, |
||||||
|
cameraType: null as null | CameraType, |
||||||
|
zoomFactor: null as null | number, |
||||||
|
}) |
||||||
|
|
||||||
|
function onShowZoomFactorPopover () { |
||||||
|
zoomFactorPopoverData.visible = !zoomFactorPopoverData.visible |
||||||
|
zoomFactorPopoverData.loading = false |
||||||
|
zoomFactorPopoverData.cameraType = null |
||||||
|
zoomFactorPopoverData.zoomFactor = null |
||||||
|
} |
||||||
|
|
||||||
|
async function onZoomFactorConfirm (confirm: boolean) { |
||||||
|
if (confirm) { |
||||||
|
if (!zoomFactorPopoverData.zoomFactor || zoomFactorPopoverData.cameraType === null) { |
||||||
|
message.error('Please input Zoom Factor') |
||||||
|
return |
||||||
|
} |
||||||
|
zoomFactorPopoverData.loading = true |
||||||
|
try { |
||||||
|
await changeCameraFocalLength(props.sn, { |
||||||
|
payload_index: payloadSelectInfo.payloadIndex, |
||||||
|
camera_type: zoomFactorPopoverData.cameraType, |
||||||
|
zoom_factor: zoomFactorPopoverData.zoomFactor |
||||||
|
}) |
||||||
|
} catch (err) { |
||||||
|
} |
||||||
|
} |
||||||
|
zoomFactorPopoverData.visible = false |
||||||
|
} |
||||||
|
|
||||||
|
const cameraAimPopoverData = reactive({ |
||||||
|
visible: false, |
||||||
|
loading: false, |
||||||
|
cameraType: null as null | CameraType, |
||||||
|
locked: false, |
||||||
|
x: null as null | number, |
||||||
|
y: null as null | number, |
||||||
|
}) |
||||||
|
|
||||||
|
function onShowCameraAimPopover () { |
||||||
|
cameraAimPopoverData.visible = !cameraAimPopoverData.visible |
||||||
|
cameraAimPopoverData.loading = false |
||||||
|
cameraAimPopoverData.cameraType = null |
||||||
|
cameraAimPopoverData.locked = false |
||||||
|
cameraAimPopoverData.x = null |
||||||
|
cameraAimPopoverData.y = null |
||||||
|
} |
||||||
|
|
||||||
|
async function onCameraAimConfirm (confirm: boolean) { |
||||||
|
if (confirm) { |
||||||
|
if (cameraAimPopoverData.cameraType === null || cameraAimPopoverData.x === null || cameraAimPopoverData.y === null) { |
||||||
|
message.error('Input error') |
||||||
|
return |
||||||
|
} |
||||||
|
try { |
||||||
|
await cameraAim(props.sn, { |
||||||
|
payload_index: payloadSelectInfo.payloadIndex, |
||||||
|
camera_type: cameraAimPopoverData.cameraType, |
||||||
|
locked: cameraAimPopoverData.locked, |
||||||
|
x: cameraAimPopoverData.x, |
||||||
|
y: cameraAimPopoverData.y, |
||||||
|
}) |
||||||
|
} catch (error) { |
||||||
|
} |
||||||
|
} |
||||||
|
cameraAimPopoverData.visible = false |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang='scss' scoped> |
||||||
|
.drone-control-wrapper{ |
||||||
|
// border-bottom: 1px solid #515151; |
||||||
|
|
||||||
|
.drone-control-header{ |
||||||
|
font-size: 14px; |
||||||
|
font-weight: 600; |
||||||
|
padding: 10px 10px 0px; |
||||||
|
} |
||||||
|
|
||||||
|
.drone-control-box { |
||||||
|
display: flex; |
||||||
|
flex-wrap: 1; |
||||||
|
.box { |
||||||
|
width: 50%; |
||||||
|
padding: 5px; |
||||||
|
border: 0.5px solid rgba(255,255,255,0.3); |
||||||
|
|
||||||
|
.row { |
||||||
|
display: flex; |
||||||
|
flex-wrap: wrap; |
||||||
|
padding: 2px; |
||||||
|
|
||||||
|
+ .row{ |
||||||
|
margin-bottom: 6px; |
||||||
|
} |
||||||
|
|
||||||
|
&::v-deep{ |
||||||
|
.ant-btn{ |
||||||
|
font-size: 12px; |
||||||
|
padding: 0px 4px; |
||||||
|
margin-right: 5px; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.drone-control{ |
||||||
|
&::v-deep{ |
||||||
|
|
||||||
|
.ant-select-single:not(.ant-select-customize-input) .ant-select-selector{ |
||||||
|
padding: 0 2px; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.drone-control-direction{ |
||||||
|
margin-right: 10px; |
||||||
|
|
||||||
|
.ant-btn { |
||||||
|
// padding: 0px 1px; |
||||||
|
margin-right: 0; |
||||||
|
} |
||||||
|
|
||||||
|
.word{ |
||||||
|
width: 12px; |
||||||
|
margin-left: 2px; |
||||||
|
font-size: 12px; |
||||||
|
color: #aaa; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,115 @@ |
|||||||
|
<template> |
||||||
|
<a-popover :visible="state.sVisible" |
||||||
|
trigger="click" |
||||||
|
v-bind="$attrs" |
||||||
|
:overlay-class-name="overlayClassName" |
||||||
|
placement="bottom" |
||||||
|
@visibleChange=";" |
||||||
|
v-on="$attrs"> |
||||||
|
<template #content> |
||||||
|
<div class="title-content"> |
||||||
|
</div> |
||||||
|
<slot name="formContent" /> |
||||||
|
<div class="uranus-popconfirm-btns"> |
||||||
|
<a-button size="sm" |
||||||
|
@click="onCancel"> |
||||||
|
{{ cancelText || 'cancel'}} |
||||||
|
</a-button> |
||||||
|
<a-button size="sm" |
||||||
|
:loading="loading" |
||||||
|
type="primary" |
||||||
|
class="confirm-btn" |
||||||
|
@click="onConfirm"> |
||||||
|
{{ okText || 'ok' }} |
||||||
|
</a-button> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
<template v-if="$slots.default"> |
||||||
|
<slot></slot> |
||||||
|
</template> |
||||||
|
</a-popover> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script lang="ts" setup> |
||||||
|
import { defineProps, defineEmits, reactive, watch, computed } from 'vue' |
||||||
|
|
||||||
|
const props = defineProps<{ |
||||||
|
visible?: boolean, |
||||||
|
loading?: Boolean, |
||||||
|
disabled?: Boolean, |
||||||
|
title?: String, |
||||||
|
okText?: String, |
||||||
|
cancelText?: String, |
||||||
|
width?: Number, |
||||||
|
}>() |
||||||
|
|
||||||
|
const emit = defineEmits(['cancel', 'confirm']) |
||||||
|
|
||||||
|
const state = reactive({ |
||||||
|
sVisible: false, |
||||||
|
loading: false, |
||||||
|
}) |
||||||
|
|
||||||
|
watch(() => props.visible, (val) => { |
||||||
|
state.sVisible = val || false |
||||||
|
}) |
||||||
|
|
||||||
|
const loading = computed(() => { |
||||||
|
return props.loading |
||||||
|
}) |
||||||
|
const okLabel = computed(() => { |
||||||
|
return props.loading ? '' : '确定' |
||||||
|
}) |
||||||
|
|
||||||
|
const overlayClassName = computed(() => { |
||||||
|
const classList = ['drone-control-popconfirm'] |
||||||
|
return classList.join(' ') |
||||||
|
}) |
||||||
|
|
||||||
|
function onConfirm (e: Event) { |
||||||
|
if (props.disabled) { |
||||||
|
return |
||||||
|
} |
||||||
|
emit('confirm', e) |
||||||
|
} |
||||||
|
|
||||||
|
function onCancel (e: Event) { |
||||||
|
state.sVisible = false |
||||||
|
emit('cancel', e) |
||||||
|
} |
||||||
|
|
||||||
|
</script> |
||||||
|
|
||||||
|
<style lang="scss"> |
||||||
|
.drone-control-popconfirm { |
||||||
|
min-width: 300px; |
||||||
|
|
||||||
|
.uranus-popconfirm-btns{ |
||||||
|
display: flex; |
||||||
|
padding: 10px 0px; |
||||||
|
justify-content: flex-end; |
||||||
|
|
||||||
|
.confirm-btn{ |
||||||
|
margin-left: 10px; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.form-content{ |
||||||
|
display: flex; |
||||||
|
flex-direction: column; |
||||||
|
|
||||||
|
> div { |
||||||
|
display: flex; |
||||||
|
margin-bottom: 5px; |
||||||
|
|
||||||
|
.form-label { |
||||||
|
flex: 1 0 60px; |
||||||
|
margin-right: 10px; |
||||||
|
} |
||||||
|
|
||||||
|
> div { |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</style> |
@ -0,0 +1,71 @@ |
|||||||
|
|
||||||
|
import { |
||||||
|
ref, |
||||||
|
watch, |
||||||
|
computed, |
||||||
|
onUnmounted, |
||||||
|
} from 'vue' |
||||||
|
import { useMyStore } from '/@/store' |
||||||
|
import { postDrc } from '/@/api/drc' |
||||||
|
import { |
||||||
|
UranusMqtt, |
||||||
|
} from '/@/mqtt' |
||||||
|
|
||||||
|
type StatusOptions = { |
||||||
|
status: 'close'; |
||||||
|
event?: CloseEvent; |
||||||
|
} | { |
||||||
|
status: 'open'; |
||||||
|
retryCount: number; |
||||||
|
} | { |
||||||
|
status: 'pending'; |
||||||
|
} |
||||||
|
|
||||||
|
export function useConnectMqtt () { |
||||||
|
const store = useMyStore() |
||||||
|
const dockOsdVisible = computed(() => { |
||||||
|
return store.state.osdVisible && store.state.osdVisible.visible && store.state.osdVisible.is_dock |
||||||
|
}) |
||||||
|
const mqttState = ref<UranusMqtt | null>(null) |
||||||
|
|
||||||
|
// 监听已打开的设备小窗 窗口数量
|
||||||
|
watch(() => dockOsdVisible.value, async (val) => { |
||||||
|
// 1.打开小窗
|
||||||
|
// 2.设备拥有飞行控制权
|
||||||
|
// 3.请求建立mqtt连接的认证信息
|
||||||
|
if (val) { |
||||||
|
if (mqttState.value) return |
||||||
|
const result = await postDrc({}) |
||||||
|
if (result?.code === 0) { |
||||||
|
const { address, client_id, username, password, expire_time } = result.data |
||||||
|
// @TODO: 校验 expire_time
|
||||||
|
mqttState.value = new UranusMqtt(address, { |
||||||
|
clientId: client_id, |
||||||
|
username, |
||||||
|
password, |
||||||
|
}) |
||||||
|
mqttState.value?.initMqtt() |
||||||
|
mqttState.value?.on('onStatus', (statusOptions: StatusOptions) => { |
||||||
|
// @TODO: 异常case
|
||||||
|
}) |
||||||
|
|
||||||
|
store.commit('SET_MQTT_STATE', mqttState.value) |
||||||
|
store.commit('SET_CLIENT_ID', client_id) |
||||||
|
} |
||||||
|
// @TODO: 认证失败case
|
||||||
|
return |
||||||
|
} |
||||||
|
// 关闭所有小窗后
|
||||||
|
// 1.销毁mqtt连接重置mqtt状态
|
||||||
|
if (mqttState?.value) { |
||||||
|
mqttState.value?.destroyed() |
||||||
|
mqttState.value = null |
||||||
|
store.commit('SET_MQTT_STATE', null) |
||||||
|
store.commit('SET_CLIENT_ID', '') |
||||||
|
} |
||||||
|
}, { immediate: true }) |
||||||
|
|
||||||
|
onUnmounted(() => { |
||||||
|
mqttState.value?.destroyed() |
||||||
|
}) |
||||||
|
} |
@ -0,0 +1,61 @@ |
|||||||
|
import { ref, onMounted, onBeforeUnmount } from 'vue' |
||||||
|
import EventBus from '/@/event-bus/' |
||||||
|
import { |
||||||
|
DRC_METHOD, |
||||||
|
DRCHsiInfo, |
||||||
|
DRCOsdInfo, |
||||||
|
DRCDelayTimeInfo, |
||||||
|
} from '/@/types/drc' |
||||||
|
|
||||||
|
export function useDroneControlMqttEvent (sn: string) { |
||||||
|
const drcInfo = ref('') |
||||||
|
const hsiInfo = ref('') |
||||||
|
const osdInfo = ref('') |
||||||
|
const delayInfo = ref('') |
||||||
|
|
||||||
|
function handleHsiInfo (data: DRCHsiInfo) { |
||||||
|
hsiInfo.value = `method: ${DRC_METHOD.HSI_INFO_PUSH}\r\n ${JSON.stringify(data)}\r\n ` |
||||||
|
} |
||||||
|
|
||||||
|
function handleOsdInfo (data: DRCOsdInfo) { |
||||||
|
osdInfo.value = `method: ${DRC_METHOD.OSD_INFO_PUSH}\r\n ${JSON.stringify(data)}\r\n ` |
||||||
|
} |
||||||
|
|
||||||
|
function handleDelayTimeInfo (data: DRCDelayTimeInfo) { |
||||||
|
delayInfo.value = `method: ${DRC_METHOD.DELAY_TIME_INFO_PUSH}\r\n ${JSON.stringify(data)}\r\n ` |
||||||
|
} |
||||||
|
|
||||||
|
function handleDroneControlMqttEvent (payload: any) { |
||||||
|
if (!payload || !payload.method) { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
switch (payload.method) { |
||||||
|
case DRC_METHOD.HSI_INFO_PUSH: { |
||||||
|
handleHsiInfo(payload.data) |
||||||
|
break |
||||||
|
} |
||||||
|
case DRC_METHOD.OSD_INFO_PUSH: { |
||||||
|
handleOsdInfo(payload.data) |
||||||
|
break |
||||||
|
} |
||||||
|
case DRC_METHOD.DELAY_TIME_INFO_PUSH: { |
||||||
|
handleDelayTimeInfo(payload.data) |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
drcInfo.value = hsiInfo.value + osdInfo.value + delayInfo.value |
||||||
|
} |
||||||
|
|
||||||
|
onMounted(() => { |
||||||
|
EventBus.on('droneControlMqttInfo', handleDroneControlMqttEvent) |
||||||
|
}) |
||||||
|
|
||||||
|
onBeforeUnmount(() => { |
||||||
|
EventBus.off('droneControlMqttInfo', handleDroneControlMqttEvent) |
||||||
|
}) |
||||||
|
|
||||||
|
return { |
||||||
|
drcInfo: drcInfo |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,97 @@ |
|||||||
|
import { message, notification } from 'ant-design-vue' |
||||||
|
import { ref, onMounted, onBeforeUnmount } from 'vue' |
||||||
|
import EventBus from '/@/event-bus/' |
||||||
|
import { EBizCode } from '/@/types' |
||||||
|
import { ControlSource } from '/@/types/device' |
||||||
|
import { ControlSourceChangeType, ControlSourceChangeInfo, FlyToPointMessage, TakeoffToPointMessage, DrcModeExitNotifyMessage, DrcStatusNotifyMessage } from '/@/types/drone-control' |
||||||
|
|
||||||
|
export interface UseDroneControlWsEventParams { |
||||||
|
} |
||||||
|
|
||||||
|
export function useDroneControlWsEvent (sn: string, payloadSn: string, funcs?: UseDroneControlWsEventParams) { |
||||||
|
const droneControlSource = ref(ControlSource.A) |
||||||
|
const payloadControlSource = ref(ControlSource.B) |
||||||
|
function onControlSourceChange (data: ControlSourceChangeInfo) { |
||||||
|
if (data.type === ControlSourceChangeType.Flight && data.sn === sn) { |
||||||
|
droneControlSource.value = data.control_source |
||||||
|
message.info(`Flight control is changed to ${droneControlSource.value}`) |
||||||
|
return |
||||||
|
} |
||||||
|
if (data.type === ControlSourceChangeType.Payload && data.sn === payloadSn) { |
||||||
|
payloadControlSource.value = data.control_source |
||||||
|
message.info(`Payload control is changed to ${ payloadControlSource.value }.`) |
||||||
|
return |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function handleProgress(key: string, message: string, error: number) { |
||||||
|
if (error !== 0) { |
||||||
|
notification.error({ |
||||||
|
key: key, |
||||||
|
message: key + 'Error code:' + error, |
||||||
|
description: message, |
||||||
|
duration: null |
||||||
|
}) |
||||||
|
} else { |
||||||
|
notification.info({ |
||||||
|
key: key, |
||||||
|
message: key, |
||||||
|
description: message, |
||||||
|
duration: 30 |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function handleDroneControlWsEvent (payload: any) { |
||||||
|
if (!payload) { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
switch (payload.biz_code) { |
||||||
|
case EBizCode.ControlSourceChange: { |
||||||
|
onControlSourceChange(payload.data) |
||||||
|
break |
||||||
|
} |
||||||
|
case EBizCode.FlyToPointProgress: { |
||||||
|
const { sn: deviceSn, result, message: msg } = payload.data as FlyToPointMessage |
||||||
|
if (deviceSn !== sn) return |
||||||
|
handleProgress(EBizCode.FlyToPointProgress, `device(sn: ${deviceSn}) ${msg}`, result) |
||||||
|
break |
||||||
|
} |
||||||
|
case EBizCode.TakeoffToPointProgress: { |
||||||
|
const { sn: deviceSn, result, message: msg } = payload.data as TakeoffToPointMessage |
||||||
|
if (deviceSn !== sn) return |
||||||
|
handleProgress(EBizCode.TakeoffToPointProgress, `device(sn: ${deviceSn}) ${msg}`, result) |
||||||
|
break |
||||||
|
} |
||||||
|
case EBizCode.JoystickInvalidNotify: { |
||||||
|
const { sn: deviceSn, result, message: msg } = payload.data as DrcModeExitNotifyMessage |
||||||
|
if (deviceSn !== sn) return |
||||||
|
handleProgress(EBizCode.JoystickInvalidNotify, `device(sn: ${deviceSn}) ${msg}`, result) |
||||||
|
break |
||||||
|
} |
||||||
|
case EBizCode.DrcStatusNotify: { |
||||||
|
const { sn: deviceSn, result, message: msg } = payload.data as DrcStatusNotifyMessage |
||||||
|
if (deviceSn !== sn) return |
||||||
|
// handleProgress(EBizCode.DrcStatusNotify, `device(sn: ${deviceSn}) ${msg}`, result)
|
||||||
|
|
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
// eslint-disable-next-line no-unused-expressions
|
||||||
|
// console.log('payload.biz_code', payload.data)
|
||||||
|
} |
||||||
|
|
||||||
|
onMounted(() => { |
||||||
|
EventBus.on('droneControlWs', handleDroneControlWsEvent) |
||||||
|
}) |
||||||
|
|
||||||
|
onBeforeUnmount(() => { |
||||||
|
EventBus.off('droneControlWs', handleDroneControlWsEvent) |
||||||
|
}) |
||||||
|
|
||||||
|
return { |
||||||
|
droneControlSource: droneControlSource, |
||||||
|
payloadControlSource: payloadControlSource |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,40 @@ |
|||||||
|
import { ref } from 'vue' |
||||||
|
import { postFlyToPoint, PostFlyToPointBody, deleteFlyToPoint, postTakeoffToPoint, PostTakeoffToPointBody } from '/@/api/drone-control/drone' |
||||||
|
import { message } from 'ant-design-vue' |
||||||
|
|
||||||
|
export function useDroneControl () { |
||||||
|
const droneControlPanelVisible = ref(false) |
||||||
|
|
||||||
|
function setDroneControlPanelVisible (visible: boolean) { |
||||||
|
droneControlPanelVisible.value = visible |
||||||
|
} |
||||||
|
|
||||||
|
async function flyToPoint (sn: string, body: PostFlyToPointBody) { |
||||||
|
const { code } = await postFlyToPoint(sn, body) |
||||||
|
if (code === 0) { |
||||||
|
message.success('Fly to') |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async function stopFlyToPoint (sn: string) { |
||||||
|
const { code } = await deleteFlyToPoint(sn) |
||||||
|
if (code === 0) { |
||||||
|
message.success('Stop fly to') |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async function takeoffToPoint (sn: string, body: PostTakeoffToPointBody) { |
||||||
|
const { code } = await postTakeoffToPoint(sn, body) |
||||||
|
if (code === 0) { |
||||||
|
message.success('Take off successfully') |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return { |
||||||
|
droneControlPanelVisible, |
||||||
|
setDroneControlPanelVisible, |
||||||
|
flyToPoint, |
||||||
|
stopFlyToPoint, |
||||||
|
takeoffToPoint |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,162 @@ |
|||||||
|
import { |
||||||
|
ref, |
||||||
|
onUnmounted, |
||||||
|
watch, |
||||||
|
Ref, |
||||||
|
} from 'vue' |
||||||
|
import { message } from 'ant-design-vue' |
||||||
|
import { |
||||||
|
DRC_METHOD, |
||||||
|
DroneControlProtocol, |
||||||
|
} from '/@/types/drc' |
||||||
|
import { |
||||||
|
useMqtt, |
||||||
|
DeviceTopicInfo |
||||||
|
} from './use-mqtt' |
||||||
|
|
||||||
|
let myInterval: any |
||||||
|
|
||||||
|
export enum KeyCode { |
||||||
|
KEY_W = 'KeyW', |
||||||
|
KEY_A = 'KeyA', |
||||||
|
KEY_S = 'KeyS', |
||||||
|
KEY_D = 'KeyD', |
||||||
|
KEY_Q = 'KeyQ', |
||||||
|
KEY_E = 'KeyE', |
||||||
|
ARROW_UP = 'ArrowUp', |
||||||
|
ARROW_DOWN = 'ArrowDown', |
||||||
|
} |
||||||
|
|
||||||
|
export function useManualControl (deviceTopicInfo: DeviceTopicInfo, isCurrentFlightController: Ref<boolean>) { |
||||||
|
const activeCodeKey = ref(null) as Ref<KeyCode | null> |
||||||
|
const mqttHooks = useMqtt(deviceTopicInfo) |
||||||
|
|
||||||
|
function handlePublish (params: DroneControlProtocol) { |
||||||
|
const body = { |
||||||
|
method: DRC_METHOD.DRONE_CONTROL, |
||||||
|
data: params, |
||||||
|
} |
||||||
|
handleClearInterval() |
||||||
|
myInterval = setInterval(() => { |
||||||
|
window.console.log('keyCode>>>>', activeCodeKey.value, body) |
||||||
|
body.data.seq = new Date().getTime() |
||||||
|
mqttHooks?.publishMqtt(deviceTopicInfo.pubTopic, body, { qos: 0 }) |
||||||
|
}, 50) |
||||||
|
} |
||||||
|
|
||||||
|
function handleKeyup (keyCode: KeyCode) { |
||||||
|
if (!deviceTopicInfo.pubTopic) { |
||||||
|
message.error('请确保已经建立DRC链路') |
||||||
|
return |
||||||
|
} |
||||||
|
const SPEED = 5 // check
|
||||||
|
const HEIGHT = 5 // check
|
||||||
|
const W_SPEED = 20 // 机头角速度
|
||||||
|
switch (keyCode) { |
||||||
|
case 'KeyA': |
||||||
|
if (activeCodeKey.value === keyCode) return |
||||||
|
handlePublish({ y: -SPEED }) |
||||||
|
activeCodeKey.value = keyCode |
||||||
|
break |
||||||
|
case 'KeyW': |
||||||
|
if (activeCodeKey.value === keyCode) return |
||||||
|
handlePublish({ x: SPEED }) |
||||||
|
activeCodeKey.value = keyCode |
||||||
|
break |
||||||
|
case 'KeyS': |
||||||
|
if (activeCodeKey.value === keyCode) return |
||||||
|
handlePublish({ x: -SPEED }) |
||||||
|
activeCodeKey.value = keyCode |
||||||
|
break |
||||||
|
case 'KeyD': |
||||||
|
if (activeCodeKey.value === keyCode) return |
||||||
|
handlePublish({ y: SPEED }) |
||||||
|
activeCodeKey.value = keyCode |
||||||
|
break |
||||||
|
case 'ArrowUp': |
||||||
|
if (activeCodeKey.value === keyCode) return |
||||||
|
handlePublish({ h: HEIGHT }) |
||||||
|
activeCodeKey.value = keyCode |
||||||
|
break |
||||||
|
case 'ArrowDown': |
||||||
|
if (activeCodeKey.value === keyCode) return |
||||||
|
handlePublish({ h: -HEIGHT }) |
||||||
|
activeCodeKey.value = keyCode |
||||||
|
break |
||||||
|
case 'KeyQ': |
||||||
|
if (activeCodeKey.value === keyCode) return |
||||||
|
handlePublish({ w: -W_SPEED }) |
||||||
|
activeCodeKey.value = keyCode |
||||||
|
break |
||||||
|
case 'KeyE': |
||||||
|
if (activeCodeKey.value === keyCode) return |
||||||
|
handlePublish({ w: W_SPEED }) |
||||||
|
activeCodeKey.value = keyCode |
||||||
|
break |
||||||
|
default: |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function handleClearInterval () { |
||||||
|
clearInterval(myInterval) |
||||||
|
myInterval = undefined |
||||||
|
} |
||||||
|
|
||||||
|
function resetControlState () { |
||||||
|
activeCodeKey.value = null |
||||||
|
handleClearInterval() |
||||||
|
} |
||||||
|
|
||||||
|
function onKeyup () { |
||||||
|
resetControlState() |
||||||
|
} |
||||||
|
|
||||||
|
function onKeydown (e: KeyboardEvent) { |
||||||
|
handleKeyup(e.code as KeyCode) |
||||||
|
} |
||||||
|
|
||||||
|
function startKeyboardManualControl () { |
||||||
|
window.addEventListener('keydown', onKeydown) |
||||||
|
window.addEventListener('keyup', onKeyup) |
||||||
|
} |
||||||
|
|
||||||
|
function closeKeyboardManualControl () { |
||||||
|
resetControlState() |
||||||
|
window.removeEventListener('keydown', onKeydown) |
||||||
|
window.removeEventListener('keyup', onKeyup) |
||||||
|
} |
||||||
|
|
||||||
|
watch(() => isCurrentFlightController.value, (val) => { |
||||||
|
if (val && deviceTopicInfo.pubTopic) { |
||||||
|
startKeyboardManualControl() |
||||||
|
} else { |
||||||
|
closeKeyboardManualControl() |
||||||
|
} |
||||||
|
}, { immediate: true }) |
||||||
|
|
||||||
|
onUnmounted(() => { |
||||||
|
closeKeyboardManualControl() |
||||||
|
}) |
||||||
|
|
||||||
|
function handleEmergencyStop () { |
||||||
|
if (!deviceTopicInfo.pubTopic) { |
||||||
|
message.error('请确保已经建立DRC链路') |
||||||
|
return |
||||||
|
} |
||||||
|
const body = { |
||||||
|
method: DRC_METHOD.DRONE_EMERGENCY_STOP, |
||||||
|
data: {} |
||||||
|
} |
||||||
|
resetControlState() |
||||||
|
window.console.log('handleEmergencyStop>>>>', deviceTopicInfo.pubTopic, body) |
||||||
|
mqttHooks?.publishMqtt(deviceTopicInfo.pubTopic, body, { qos: 1 }) |
||||||
|
} |
||||||
|
|
||||||
|
return { |
||||||
|
activeCodeKey, |
||||||
|
handleKeyup, |
||||||
|
handleEmergencyStop, |
||||||
|
resetControlState, |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,132 @@ |
|||||||
|
import { |
||||||
|
ref, |
||||||
|
reactive, |
||||||
|
computed, |
||||||
|
watch, |
||||||
|
onUnmounted, |
||||||
|
} from 'vue' |
||||||
|
import { |
||||||
|
IClientPublishOptions, |
||||||
|
IPublishPacket, |
||||||
|
} from '/@/mqtt' |
||||||
|
import { useMyStore } from '/@/store' |
||||||
|
import { |
||||||
|
DRC_METHOD, |
||||||
|
} from '/@/types/drc' |
||||||
|
import EventBus from '/@/event-bus' |
||||||
|
|
||||||
|
export interface DeviceTopicInfo{ |
||||||
|
sn: string |
||||||
|
pubTopic: string |
||||||
|
subTopic: string |
||||||
|
} |
||||||
|
|
||||||
|
type MessageMqtt = (topic: string, payload: Buffer, packet: IPublishPacket) => void | Promise<void> |
||||||
|
|
||||||
|
export function useMqtt (deviceTopicInfo: DeviceTopicInfo) { |
||||||
|
let cacheSubscribeArr: { |
||||||
|
topic: string; |
||||||
|
callback?: MessageMqtt; |
||||||
|
}[] = [] |
||||||
|
|
||||||
|
const store = useMyStore() |
||||||
|
|
||||||
|
const mqttState = computed(() => { |
||||||
|
return store.state.mqttState |
||||||
|
}) |
||||||
|
|
||||||
|
function publishMqtt (topic: string, body: object, ots?: IClientPublishOptions) { |
||||||
|
// const buffer = Buffer.from(JSON.stringify(body))
|
||||||
|
mqttState.value?.publishMqtt(topic, JSON.stringify(body), ots) |
||||||
|
} |
||||||
|
|
||||||
|
function subscribeMqtt (topic: string, handleMessageMqtt?: MessageMqtt) { |
||||||
|
mqttState.value?.subscribeMqtt(topic) |
||||||
|
const handler = handleMessageMqtt || onMessageMqtt |
||||||
|
mqttState.value?.on('onMessageMqtt', handler) |
||||||
|
cacheSubscribeArr.push({ |
||||||
|
topic, |
||||||
|
callback: handler, |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
function onMessageMqtt (message: any) { |
||||||
|
if (cacheSubscribeArr.findIndex(item => item.topic === message?.topic) !== -1) { |
||||||
|
const payloadStr = new TextDecoder('utf-8').decode(message?.payload) |
||||||
|
const payloadObj = JSON.parse(payloadStr) |
||||||
|
switch (payloadObj?.method) { |
||||||
|
case DRC_METHOD.HEART_BEAT: |
||||||
|
break |
||||||
|
case DRC_METHOD.DELAY_TIME_INFO_PUSH: |
||||||
|
case DRC_METHOD.HSI_INFO_PUSH: |
||||||
|
case DRC_METHOD.OSD_INFO_PUSH: |
||||||
|
EventBus.emit('droneControlMqttInfo', payloadObj) |
||||||
|
break |
||||||
|
default: |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function unsubscribeDrc () { |
||||||
|
// 销毁已订阅事件
|
||||||
|
cacheSubscribeArr.forEach(item => { |
||||||
|
mqttState.value?.off('onMessageMqtt', item.callback) |
||||||
|
mqttState.value?.unsubscribeMqtt(item.topic) |
||||||
|
}) |
||||||
|
cacheSubscribeArr = [] |
||||||
|
} |
||||||
|
|
||||||
|
// 心跳
|
||||||
|
const heartBeatSeq = ref(0) |
||||||
|
const state = reactive({ |
||||||
|
heartState: new Map<string, { |
||||||
|
pingInterval: any; |
||||||
|
}>(), |
||||||
|
}) |
||||||
|
|
||||||
|
// 监听云控控制权
|
||||||
|
watch(() => deviceTopicInfo, (val, oldVal) => { |
||||||
|
if (val.subTopic !== '') { |
||||||
|
// 1.订阅topic
|
||||||
|
subscribeMqtt(deviceTopicInfo.subTopic) |
||||||
|
// 2.发心跳
|
||||||
|
publishDrcPing(deviceTopicInfo.sn) |
||||||
|
} else { |
||||||
|
clearInterval(state.heartState.get(deviceTopicInfo.sn)?.pingInterval) |
||||||
|
state.heartState.delete(deviceTopicInfo.sn) |
||||||
|
heartBeatSeq.value = 0 |
||||||
|
} |
||||||
|
}, { immediate: true, deep: true }) |
||||||
|
|
||||||
|
function publishDrcPing (sn: string) { |
||||||
|
const body = { |
||||||
|
method: DRC_METHOD.HEART_BEAT, |
||||||
|
data: { |
||||||
|
ts: new Date().getTime(), |
||||||
|
seq: heartBeatSeq.value, |
||||||
|
}, |
||||||
|
} |
||||||
|
const pingInterval = setInterval(() => { |
||||||
|
if (!mqttState.value) return |
||||||
|
heartBeatSeq.value += 1 |
||||||
|
body.data.ts = new Date().getTime() |
||||||
|
body.data.seq = heartBeatSeq.value |
||||||
|
publishMqtt(deviceTopicInfo.pubTopic, body, { qos: 0 }) |
||||||
|
}, 1000) |
||||||
|
state.heartState.set(sn, { |
||||||
|
pingInterval, |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
onUnmounted(() => { |
||||||
|
unsubscribeDrc() |
||||||
|
heartBeatSeq.value = 0 |
||||||
|
}) |
||||||
|
|
||||||
|
return { |
||||||
|
mqttState, |
||||||
|
publishMqtt, |
||||||
|
subscribeMqtt, |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,120 @@ |
|||||||
|
import { message } from 'ant-design-vue' |
||||||
|
import { |
||||||
|
postPayloadAuth, |
||||||
|
postPayloadCommands, |
||||||
|
PayloadCommandsEnum, |
||||||
|
PostCameraModeBody, |
||||||
|
PostCameraFocalLengthBody, |
||||||
|
PostGimbalResetBody, |
||||||
|
PostCameraAimBody, |
||||||
|
} from '/@/api/drone-control/payload' |
||||||
|
import { ControlSource } from '/@/types/device' |
||||||
|
|
||||||
|
export function usePayloadControl () { |
||||||
|
function checkPayloadAuth (controlSource?: ControlSource) { |
||||||
|
if (controlSource !== ControlSource.A) { |
||||||
|
message.error('Get Payload Control first') |
||||||
|
return false |
||||||
|
} |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
async function authPayload (sn: string, payloadIndx: string) { |
||||||
|
const { code } = await postPayloadAuth(sn, { |
||||||
|
payload_index: payloadIndx |
||||||
|
}) |
||||||
|
if (code === 0) { |
||||||
|
message.success('Get Payload Control successfully') |
||||||
|
return true |
||||||
|
} |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
async function resetGimbal (sn: string, data: PostGimbalResetBody) { |
||||||
|
const { code } = await postPayloadCommands(sn, { |
||||||
|
cmd: PayloadCommandsEnum.GimbalReset, |
||||||
|
data: data |
||||||
|
}) |
||||||
|
if (code === 0) { |
||||||
|
message.success('Gimbal Reset successfully') |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async function switchCameraMode (sn: string, data: PostCameraModeBody) { |
||||||
|
const { code } = await postPayloadCommands(sn, { |
||||||
|
cmd: PayloadCommandsEnum.CameraModeSwitch, |
||||||
|
data: data |
||||||
|
}) |
||||||
|
if (code === 0) { |
||||||
|
message.success('Camera Mode Switch successfully') |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async function takeCameraPhoto (sn: string, payloadIndx: string) { |
||||||
|
const { code } = await postPayloadCommands(sn, { |
||||||
|
cmd: PayloadCommandsEnum.CameraPhotoTake, |
||||||
|
data: { |
||||||
|
payload_index: payloadIndx |
||||||
|
} |
||||||
|
}) |
||||||
|
if (code === 0) { |
||||||
|
message.success('Take Photo successfully') |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async function startCameraRecording (sn: string, payloadIndx: string) { |
||||||
|
const { code } = await postPayloadCommands(sn, { |
||||||
|
cmd: PayloadCommandsEnum.CameraRecordingStart, |
||||||
|
data: { |
||||||
|
payload_index: payloadIndx |
||||||
|
} |
||||||
|
}) |
||||||
|
if (code === 0) { |
||||||
|
message.success('Start Recording successfully') |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async function stopCameraRecording (sn: string, payloadIndx: string) { |
||||||
|
const { code } = await postPayloadCommands(sn, { |
||||||
|
cmd: PayloadCommandsEnum.CameraRecordingStop, |
||||||
|
data: { |
||||||
|
payload_index: payloadIndx |
||||||
|
} |
||||||
|
}) |
||||||
|
if (code === 0) { |
||||||
|
message.success('Stop Recording successfully') |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async function changeCameraFocalLength (sn: string, data: PostCameraFocalLengthBody) { |
||||||
|
const { code } = await postPayloadCommands(sn, { |
||||||
|
cmd: PayloadCommandsEnum.CameraFocalLengthSet, |
||||||
|
data: data, |
||||||
|
}) |
||||||
|
if (code === 0) { |
||||||
|
message.success('Zoom successfully') |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
async function cameraAim (sn: string, data: PostCameraAimBody) { |
||||||
|
const { code } = await postPayloadCommands(sn, { |
||||||
|
cmd: PayloadCommandsEnum.CameraAim, |
||||||
|
data: data, |
||||||
|
}) |
||||||
|
if (code === 0) { |
||||||
|
message.success('Zoom Aim successfully') |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return { |
||||||
|
checkPayloadAuth, |
||||||
|
authPayload, |
||||||
|
resetGimbal, |
||||||
|
switchCameraMode, |
||||||
|
takeCameraPhoto, |
||||||
|
startCameraRecording, |
||||||
|
stopCameraRecording, |
||||||
|
changeCameraFocalLength, |
||||||
|
cameraAim, |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,12 @@ |
|||||||
|
|
||||||
|
import { |
||||||
|
IClientOptions, |
||||||
|
} from 'mqtt' |
||||||
|
|
||||||
|
export const OPTIONS: IClientOptions = { |
||||||
|
clean: true, // true: 清除会话, false: 保留会话
|
||||||
|
connectTimeout: 10000, // mqtt 超时时间
|
||||||
|
resubscribe: true, // 断开重连后,再次订阅原订阅
|
||||||
|
reconnectPeriod: 10000, // 重连间隔时间: 5s
|
||||||
|
keepalive: 1, // 心跳间隔时间:1s
|
||||||
|
} |
@ -0,0 +1,117 @@ |
|||||||
|
import EventEmitter from 'eventemitter3' |
||||||
|
import { |
||||||
|
OPTIONS, |
||||||
|
} from './config' |
||||||
|
import { |
||||||
|
connect, |
||||||
|
MqttClient, |
||||||
|
IClientPublishOptions, |
||||||
|
IPublishPacket, |
||||||
|
Packet, |
||||||
|
ISubscriptionGrant, |
||||||
|
IClientOptions, |
||||||
|
} from 'mqtt/dist/mqtt.min' |
||||||
|
|
||||||
|
export class UranusMqtt extends EventEmitter { |
||||||
|
_url: string |
||||||
|
_options?: IClientOptions |
||||||
|
_client: MqttClient | null |
||||||
|
_hasInit: boolean |
||||||
|
|
||||||
|
constructor (url?: string, options?: IClientOptions) { |
||||||
|
super() |
||||||
|
this._url = url || '' |
||||||
|
this._options = options |
||||||
|
this._client = null |
||||||
|
this._hasInit = false |
||||||
|
} |
||||||
|
|
||||||
|
initMqtt = () => { |
||||||
|
// 仅初始化一次
|
||||||
|
if (this._hasInit) return |
||||||
|
// 建立连接
|
||||||
|
this._client = connect(this._url, { |
||||||
|
...OPTIONS, |
||||||
|
...this._options, |
||||||
|
}) |
||||||
|
this._hasInit = true |
||||||
|
if (this._client) { |
||||||
|
this._client.on('reconnect', this._onReconnect) |
||||||
|
|
||||||
|
// 消息监听
|
||||||
|
this._client.on('message', this._onMessage) |
||||||
|
|
||||||
|
// 连接关闭
|
||||||
|
this._client.on('close', this._onClose) |
||||||
|
|
||||||
|
// 连接异常
|
||||||
|
this._client.on('error', this._onError) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// 发布
|
||||||
|
publishMqtt = (topic: string, body: string | Buffer, opts?: IClientPublishOptions) => { |
||||||
|
if (!this._client?.connected) { |
||||||
|
this.initMqtt() |
||||||
|
} |
||||||
|
this._client?.publish(topic, body, opts || {}, (error?: Error, packet?: Packet) => { |
||||||
|
if (error) { |
||||||
|
window.console.error('mqtt publish error,', error, packet) |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// 订阅
|
||||||
|
subscribeMqtt = (topic: string) => { |
||||||
|
if (!this._client?.connected) { |
||||||
|
this.initMqtt() |
||||||
|
} |
||||||
|
window.console.log('subscribeMqtt>>>>>', topic) |
||||||
|
this._client?.subscribe(topic, (error: Error, granted: ISubscriptionGrant[]) => { |
||||||
|
window.console.log('mqtt subscribe,', error, granted) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// 取消订阅
|
||||||
|
unsubscribeMqtt = (topic: string) => { |
||||||
|
window.console.log('mqtt unsubscribeMqtt,', topic) |
||||||
|
this._client?.unsubscribe(topic) |
||||||
|
} |
||||||
|
|
||||||
|
// 关闭 mqtt 客户端
|
||||||
|
destroyed = () => { |
||||||
|
window.console.log('mqtt destroyed') |
||||||
|
this._client?.end() |
||||||
|
} |
||||||
|
|
||||||
|
_onReconnect = () => { |
||||||
|
if (this._client) { window.console.error('mqtt reconnect,') } |
||||||
|
} |
||||||
|
|
||||||
|
_onMessage = (topic: string, payload: Buffer, packet: IPublishPacket) => { |
||||||
|
this.emit('onMessageMqtt', { topic, payload, packet }) |
||||||
|
} |
||||||
|
|
||||||
|
_onClose = () => { |
||||||
|
// 连接异常关闭会自动重连
|
||||||
|
window.console.error('mqtt close,') |
||||||
|
this.emit('onStatus', { |
||||||
|
status: 'close', |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
_onError = (error: Error) => { |
||||||
|
// 连接错误会自动重连
|
||||||
|
window.console.error('mqtt error,', error) |
||||||
|
this.emit('onStatus', { |
||||||
|
status: 'error', |
||||||
|
data: error, |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export { |
||||||
|
IClientOptions, |
||||||
|
IPublishPacket, |
||||||
|
IClientPublishOptions, |
||||||
|
} |
@ -0,0 +1,4 @@ |
|||||||
|
declare module 'mqtt/dist/mqtt.min' { |
||||||
|
import MQTT from 'mqtt' |
||||||
|
export = MQTT |
||||||
|
} |
@ -0,0 +1,71 @@ |
|||||||
|
export enum DRC_METHOD { |
||||||
|
HEART_BEAT = 'heart_beat', |
||||||
|
DRONE_CONTROL = 'drone_control', // 飞行控制-虚拟摇杆
|
||||||
|
DRONE_EMERGENCY_STOP = 'drone_emergency_stop', // 急停
|
||||||
|
OSD_INFO_PUSH = 'osd_info_push', // 高频osd信息上报
|
||||||
|
HSI_INFO_PUSH = 'hsi_info_push', // 避障信息上报
|
||||||
|
DELAY_TIME_INFO_PUSH = 'delay_info_push', // 图传链路延时信息上报
|
||||||
|
} |
||||||
|
|
||||||
|
// 手动控制
|
||||||
|
export interface DroneControlProtocol { |
||||||
|
x?: number; // 水平方向速度,正值为A指令 负值为D指令 单位:m/s
|
||||||
|
y?: number; // 前进后退方向速度,正值为W指令 负值为S指令 单位:m/s
|
||||||
|
h?: number;// 上下高度值,正值为上升指令 负值为下降指令 单位:m
|
||||||
|
w?: number; // 机头角速度,正值为顺时针,负值为逆时针 单位:degree/s (web端暂无此设计)
|
||||||
|
step_x?: number; // 水平方向步长
|
||||||
|
step_y?: number; // 前后方向步长
|
||||||
|
step_h?: number; // 高度方向步长
|
||||||
|
step_w?: number; // 机头转向步长
|
||||||
|
seq?: number; // 从0计时
|
||||||
|
freq?: number; // 指令发送频率
|
||||||
|
delay_time?: number; // 指令从发送到设备端接收可容忍的时间 发送频率+链路传输时长
|
||||||
|
} |
||||||
|
|
||||||
|
// 低延时osd
|
||||||
|
export interface DRCOsdInfo { |
||||||
|
attitude_head: number;// 飞机姿态head角,单位:度
|
||||||
|
latitude: number;// 飞机经纬度
|
||||||
|
longitude: number; |
||||||
|
altitude: number; |
||||||
|
speed_x: number; |
||||||
|
speed_y: number; |
||||||
|
speed_z: number; |
||||||
|
gimbal_pitch: number;// 云台pitch角
|
||||||
|
gimbal_roll: number;// 云台roll角
|
||||||
|
gimbal_yaw: number;// 云台yaw角
|
||||||
|
} |
||||||
|
|
||||||
|
// 态势感知-HSI
|
||||||
|
export interface DRCHsiInfo { |
||||||
|
up_distance: number;// 上方的障碍物距离,单位:mm
|
||||||
|
down_distance: number;// 下方的障碍物距离,单位:mm
|
||||||
|
around_distances: number[]; // 水平方向观察点,分布在[0,360)区间,表示障碍物与飞机距离,单位为mm。 0对应机头方向正前方,顺时针分布,例如0度为机头正前方,90度为飞机正右方
|
||||||
|
up_enable: boolean; // 上视避障开关状态,true:已开启 false:已关闭
|
||||||
|
up_work: boolean; // 上视避障工作状态,true:正常工作 false:异常或离线
|
||||||
|
down_enable: boolean; // 下视避障开关状态,true:已开启 false:已关闭
|
||||||
|
down_work: boolean; // 下视避障工作状态,true:正常工作 false:异常或离线
|
||||||
|
left_enable: boolean; // 左视避障开关状态,true:已开启 false:已关闭
|
||||||
|
left_work: boolean; // 左视避障工作状态,true:正常工作 false:异常或离线
|
||||||
|
right_enable: boolean; // 右视避障开关状态,true:已开启 false:已关闭
|
||||||
|
right_work: boolean; // 右视避障工作状态,true:正常工作 false:异常或离线
|
||||||
|
front_enable: boolean; // 前视避障开关状态,true:已开启 false:已关闭
|
||||||
|
front_work: boolean; // 前视避障工作状态,true:正常工作 false:异常或离线
|
||||||
|
back_enable: boolean; // 后视避障开关状态,true:已开启 false:已关闭
|
||||||
|
back_work: boolean; // 后视避障工作状态,true:正常工作 false:异常或离线
|
||||||
|
vertical_enable: boolean; // 垂直方向综合开关状态,当本协议中上、下视开关状态均为true时,输出true:已开启,否则输出false:已关闭
|
||||||
|
vertical_work: boolean; // 垂直方向避障工作状态,当本协议中上、下视工作均为true时,输出true:正常工作,否则输出false:异常或离线
|
||||||
|
horizontal_enable: boolean; // 水平方向综合开关状态,当本协议中前、后、左、右、开关状态均为true时,输出true:已开启,否则输出false:已关闭
|
||||||
|
horizontal_work: boolean; // 水平方向避障工作综合状态,当本协议中前、后、左、右视工作均为true时,输出true:正常工作,否则输出false:异常或离线
|
||||||
|
} |
||||||
|
|
||||||
|
export interface LiveViewDelayItem { |
||||||
|
video_id: string; |
||||||
|
liveview_delay_time: number; |
||||||
|
} |
||||||
|
|
||||||
|
// 链路时延信息
|
||||||
|
export interface DRCDelayTimeInfo { |
||||||
|
sdr_cmd_delay: number; // sdr链路命令延时,单位:ms
|
||||||
|
liveview_delay_list: LiveViewDelayItem[]; |
||||||
|
} |
@ -0,0 +1,68 @@ |
|||||||
|
import { ControlSource } from './device' |
||||||
|
import { LostControlActionInCommandFLight, WaylineLostControlActionInCommandFlight } from '/@/api/drone-control/drone' |
||||||
|
|
||||||
|
export enum ControlSourceChangeType { |
||||||
|
Flight = 1, |
||||||
|
Payload = 2, |
||||||
|
} |
||||||
|
|
||||||
|
// 控制权变化消息
|
||||||
|
export interface ControlSourceChangeInfo { |
||||||
|
sn: string, |
||||||
|
type: ControlSourceChangeType, |
||||||
|
control_source: ControlSource |
||||||
|
} |
||||||
|
|
||||||
|
// 飞向目标点结果
|
||||||
|
export interface FlyToPointMessage { |
||||||
|
sn: string, |
||||||
|
result: number, |
||||||
|
message: string, |
||||||
|
} |
||||||
|
|
||||||
|
// 一键起飞结果
|
||||||
|
export interface TakeoffToPointMessage { |
||||||
|
sn: string, |
||||||
|
result: number, |
||||||
|
message: string, |
||||||
|
} |
||||||
|
|
||||||
|
// 设备端退出drc模式
|
||||||
|
export interface DrcModeExitNotifyMessage { |
||||||
|
sn: string, |
||||||
|
result: number, |
||||||
|
message: string, |
||||||
|
} |
||||||
|
|
||||||
|
// 飞行控制模式状态
|
||||||
|
export interface DrcStatusNotifyMessage { |
||||||
|
sn: string, |
||||||
|
result: number, |
||||||
|
message: string, |
||||||
|
} |
||||||
|
|
||||||
|
export const WaylineLostControlActionInCommandFlightOptions = [ |
||||||
|
{ label: 'Continue', value: WaylineLostControlActionInCommandFlight.CONTINUE }, |
||||||
|
{ label: 'Execute Lost Action', value: WaylineLostControlActionInCommandFlight.EXEC_LOST_ACTION } |
||||||
|
] |
||||||
|
|
||||||
|
export const LostControlActionInCommandFLightOptions = [ |
||||||
|
{ label: 'Return Home', value: LostControlActionInCommandFLight.RETURN_HOME }, |
||||||
|
{ label: 'Hover', value: LostControlActionInCommandFLight.HOVER }, |
||||||
|
{ label: 'Landing', value: LostControlActionInCommandFLight.Land } |
||||||
|
] |
||||||
|
|
||||||
|
// 云台重置模式
|
||||||
|
export enum GimbalResetMode { |
||||||
|
Recenter = 0, |
||||||
|
Down = 1, |
||||||
|
RecenterGimbalPan = 2, |
||||||
|
PitchDown = 3, |
||||||
|
} |
||||||
|
|
||||||
|
export const GimbalResetModeOptions = [ |
||||||
|
{ label: 'Gimbal Recenter', value: GimbalResetMode.Recenter }, |
||||||
|
{ label: 'Gimbal down', value: GimbalResetMode.Down }, |
||||||
|
{ label: 'Recenter Gimbal Pan', value: GimbalResetMode.RecenterGimbalPan }, |
||||||
|
{ label: 'Gimbal Pitch Down', value: GimbalResetMode.PitchDown } |
||||||
|
] |
Loading…
Reference in new issue