<template>
|
<!-- <div class="ptzControlBtnBox">-->
|
<div class="ptzControlBtnBox" v-show="isTakeOff">
|
<BaseControl/>
|
<div class="ptzControlBtn b-r">
|
<div
|
v-for="(item, index) in ptzBtns"
|
:style="item.style"
|
class="ptzControlItem"
|
:class="{ isActiveBtn: isActiveBtn(item.key) }"
|
@touchstart="onMouseDown(item.key)"
|
@touchend="onMouseUp"
|
></div>
|
|
<div
|
class="ptzControlItemIcon"
|
v-for="(item, index) in ptzBtns"
|
:style="{ transform: `rotate(${index * 90}deg)` }"
|
>
|
<el-icon>
|
<CaretRight />
|
</el-icon>
|
</div>
|
<div class="circles2 b-r">
|
<div class="circles3 b-r">
|
云台
|
</div>
|
</div>
|
</div>
|
</div>
|
</template>
|
|
<script setup>
|
import { KeyCode, useManualControl } from '@/hooks/controlDrone/useManualControl'
|
import { droneController, exitController, postDrc, returnHome, returnHomeCancel } from '@/api/drc'
|
import { ElMessage } from 'element-plus'
|
import { UranusMqtt } from '@/utils/mqtt'
|
import {
|
ArrowDown,
|
ArrowLeft,
|
ArrowRight,
|
ArrowUp,
|
Bottom,
|
CaretRight,
|
Minus,
|
Plus,
|
RefreshLeft,
|
RefreshRight,
|
} from '@element-plus/icons-vue'
|
import _ from 'lodash'
|
import BaseControl from '@/components/CurrentTaskDetails/ControlPanel/BaseControl.vue'
|
import EventBus from '@/utils/eventBus'
|
import { getPayloadControlApi } from '@/api/payload'
|
import { directionMap } from '@/const/drc'
|
import { useRunOnce } from '@/hooks'
|
import { decryptAES } from '@/utils/cryptoUtils'
|
import { useMqtt } from '@/hooks/controlDrone/useMqtt'
|
import { analysisPointLineKmz } from '@/views/RoutePlan/PointAirLine/pointWayLineUtils'
|
import { showToast } from 'vant'
|
|
const wsInfo = inject('wsInfo')
|
const device_osd_host = computed(() => wsInfo?.value?.device_osd?.data?.host || {})
|
const dock_osd_host = computed(() => wsInfo?.value?.dock_osd?.data?.host || {})
|
const taskDetails = inject('taskDetails')
|
const dockSn = inject('dockSn')
|
const droneSn = inject('droneSn')
|
const trueAltitude = inject('trueAltitude')
|
const client_id = inject('client_id')
|
const isBackDock = inject('isBackDock')
|
const isAutoControl = inject('isAutoControl')
|
const isManualControl = inject('isManualControl')
|
const showUpKeyPanel = inject('showUpKeyPanel')
|
const showFunBlock = inject('showFunBlock')
|
const isTakeOff = inject('isTakeOff')
|
const isMaxMap = inject('isMaxMap')
|
const hasPayloadControl = inject('hasPayloadControl')
|
const currentWayLine = inject('currentWayLine')
|
const job_id = computed(() => currentWayLine.value?.job_id)
|
|
const apiDisable = inject('apiDisable')
|
const zoomFactor = inject('zoomFactor')
|
const cameraParams = inject('cameraParams')
|
const activeSlider = inject('activeSlider')
|
const droneStatus = inject('droneStatus')
|
const workspace_id = inject('workspace_id')
|
const activeBtnList = ref([])
|
|
function isActiveBtn(key) {
|
return activeBtnList.value.includes(key)
|
}
|
|
let mqttState = null
|
const speed = ref(5)
|
provide('speed', speed)
|
|
const deviceTopicInfo = ref({
|
pubTopic: '',
|
subTopic: '',
|
})
|
// 控制对象
|
let manualControl = null
|
const activeKeyboard = ref(null)
|
const compassOptions = computed(() => {
|
return {
|
pitchAngle: pitchAngle.value.angle,
|
trueAltitude: trueAltitude.value,
|
yawAngle: yawAngle.value.angle,
|
}
|
})
|
|
const list1 = [
|
{ key: KeyCode.KEY_Q, text: 'Q', icon: RefreshLeft },
|
{ key: KeyCode.KEY_W, text: 'W', icon: ArrowUp },
|
{ key: KeyCode.KEY_E, text: 'E', icon: RefreshRight },
|
]
|
const list2 = [
|
{ key: KeyCode.KEY_A, text: 'A', icon: ArrowLeft },
|
{ key: KeyCode.KEY_S, text: 'S', icon: ArrowDown },
|
{ key: KeyCode.KEY_D, text: 'D', icon: ArrowRight },
|
]
|
|
const ptzBtns = [
|
{ name: '上', key: KeyCode.ARROW_UP, operate: 'up', style: { top: '-70%' } },
|
{ name: '右', key: KeyCode.ARROW_RIGHT, operate: 'right', style: { left: '70%' } },
|
{ name: '下', key: KeyCode.ARROW_DOWN, operate: 'down', style: { top: '70%' } },
|
{ name: '左', key: KeyCode.ARROW_LEFT, operate: 'left', style: { left: '-70%' } },
|
]
|
|
const baseInfoList = ref([
|
[
|
{ name: '焦距倍数', value: 0 },
|
{ name: '俯仰角度', value: 0, unit: '' },
|
{ name: '横向角度', value: 0, unit: '' },
|
],
|
[
|
{ name: '储存', value: 0, unit: 'G' },
|
{ name: '方向', value: 0 },
|
{ name: '方向', value: 0 },
|
],
|
])
|
|
const pitchAngle = computed(() => {
|
const { payloads } = device_osd_host?.value || {}
|
const gimbal_pitch = payloads?.[0]?.gimbal_pitch || 0
|
let direction = ''
|
if (gimbal_pitch > -2 && gimbal_pitch < 2) {
|
direction = '正前'
|
} else if (gimbal_pitch >= 2 && gimbal_pitch < 90) {
|
direction = '斜上'
|
} else if (gimbal_pitch === 90) {
|
direction = '正上'
|
} else if (gimbal_pitch <= -2 && gimbal_pitch > -90) {
|
direction = '斜下'
|
} else if (gimbal_pitch === -90 || gimbal_pitch < -90) {
|
direction = '正下'
|
}
|
return {
|
angle: _.round(gimbal_pitch || 0, 1),
|
direction,
|
}
|
})
|
const yawAngle = computed(() => {
|
let { payloads, attitude_head } = device_osd_host?.value || {}
|
const gimbal_yaw = payloads?.[0]?.gimbal_yaw || 0
|
attitude_head = attitude_head || 0
|
let yaw = ''
|
if (gimbal_yaw > 180) {
|
yaw = gimbal_yaw
|
} else {
|
yaw = gimbal_yaw - attitude_head
|
}
|
let result = 0
|
if (yaw < 0) {
|
result = yaw + 360
|
}
|
if (yaw > 0) {
|
result = yaw
|
}
|
if ((yaw > -2 && yaw < 2) || parseInt(attitude_head) === parseInt(gimbal_yaw)) {
|
result = attitude_head < 0 ? 180 + (180 + attitude_head) : attitude_head
|
}
|
const roundResult = Math.round(result)
|
let direction = ''
|
for (const item of directionMap) {
|
if (roundResult >= item.min && roundResult <= item.max) {
|
direction = item.value
|
break
|
}
|
}
|
return {
|
angle: _.round(result, 1),
|
direction,
|
}
|
})
|
const isRecording = inject('isRecording')
|
const recordingTime = inject('recordingTime')
|
|
// 设置录像时间
|
function setRecordTime({ recording_state = 0, record_time = 0 }) {
|
isRecording.value = !!recording_state
|
if (recording_state) {
|
const hours = String(Math.floor(record_time / 3600)).padStart(2, '0')
|
const minutes = String(Math.floor((record_time % 3600) / 60)).padStart(2, '0')
|
const secs = String(record_time % 60).padStart(2, '0')
|
recordingTime.value = `${hours}:${minutes}:${secs}`
|
} else {
|
recordingTime.value = '00:00:00'
|
}
|
}
|
|
// 设置变焦倍率
|
function setZoomFactor(camerasInfo) {
|
let zoom_factor
|
const irLevel = camerasInfo?.ir_zoom_factor
|
const zoomLevel = camerasInfo?.zoom_factor
|
if (cameraParams.value.camera_type === 'wide') {
|
zoom_factor = zoomFactor.value.zoom.min - 1
|
} else if (cameraParams.value.camera_type === 'ir') {
|
zoom_factor = irLevel
|
} else {
|
zoom_factor = zoomLevel
|
}
|
zoom_factor = _.round(zoom_factor, 1)
|
if (!activeSlider.value) {
|
cameraParams.value.zoom_factor = zoom_factor
|
}
|
baseInfoList.value[0][0].value = zoom_factor
|
}
|
|
// 基本信息赋值
|
const baseInfoChange = () => {
|
const newUsedStorage = dock_osd_host.value?.storage?.used
|
const camerasInfo = device_osd_host.value?.cameras?.[0] || {}
|
setRecordTime(camerasInfo)
|
setZoomFactor(camerasInfo)
|
const usedStorageGB = _.round(newUsedStorage / 1024 / 1024, 2)
|
baseInfoList.value[0][1].value = pitchAngle.value.angle
|
baseInfoList.value[0][2].value = yawAngle.value.angle
|
if (newUsedStorage !== undefined) baseInfoList.value[1][0].value = usedStorageGB
|
baseInfoList.value[1][1].value = pitchAngle.value.direction
|
baseInfoList.value[1][2].value = yawAngle.value.direction
|
}
|
const throttleBaseInfoChange = _.throttle(baseInfoChange, 1000)
|
watch(wsInfo, throttleBaseInfoChange, { immediate: true, deep: true })
|
|
// 按下操作
|
function onMouseDown(type) {
|
manualControl?.handleKeyup(type)
|
}
|
|
// 弹起操作
|
function onMouseUp() {
|
manualControl?.resetControlState()
|
}
|
|
// 取消手动控制
|
function cancelManualControl() {
|
if ([undefined, 0, 14].includes(droneStatus.value.mode_code)) {
|
return showToast('当前状态无法操作')
|
}
|
if (isAutoControl.value) return showToast('当前已经是自动飞行中')
|
if (isBackDock.value) return showToast('当前正在返航,无法自动控制')
|
apiDisable.value = true
|
exitController({ client_id: client_id.value, dock_sn: dockSn.value, job_id: job_id.value })
|
.then(res => {
|
deviceTopicInfo.value.subTopic = ''
|
deviceTopicInfo.value.pubTopic = ''
|
showToast('退出飞行控制成功')
|
})
|
.catch(e => {})
|
.finally(() => {
|
apiDisable.value = false
|
})
|
}
|
|
// 获得有效载荷控制
|
function getPayloadControl() {
|
if (hasPayloadControl.value || !dockSn.value) return
|
getPayloadControlApi({ sn: dockSn.value }).then(res => {
|
hasPayloadControl.value = true
|
console.log('成功获得有效载荷控制--(云台操作等等)')
|
})
|
}
|
|
// 手动控制
|
function control(params = {}) {
|
const { hasTips = true } = params
|
if (!dockSn.value) return ElMessage.error('系统错误,未获取到dock_sn')
|
if (droneStatus.value.mode_code === 10) return showToast('自动降落时不允许手动控制')
|
if (droneStatus.value.mode_code === 9) return showToast('请先取消返航')
|
if ([undefined, 0,1,2,10, 14].includes(droneStatus.value.mode_code)) return showToast('当前状态无法操作')
|
apiDisable.value = true
|
droneController({ client_id: client_id.value, dock_sn: dockSn.value, job_id: job_id.value })
|
.then(res => {
|
const { data } = res.data
|
if (data.sub && data.sub?.length > 0) {
|
deviceTopicInfo.value.subTopic = data.sub[0]
|
}
|
if (data.pub && data.pub?.length > 0) {
|
deviceTopicInfo.value.pubTopic = data.pub[0]
|
}
|
showUpKeyPanel.value = true
|
hasTips && showToast('暂停成功')
|
getPayloadControl()
|
isMaxMap.value = false
|
})
|
.finally(() => {
|
apiDisable.value = false
|
})
|
}
|
|
// 返航
|
async function onBackDock() {
|
if (![5, 17, 3, 16].includes(droneStatus.value.mode_code)) {
|
return showToast('当前状态无法操作')
|
}
|
apiDisable.value = true
|
try {
|
await returnHome(dockSn?.value)
|
showToast('返航操作成功')
|
} catch (e) {
|
} finally {
|
apiDisable.value = false
|
}
|
}
|
|
// 取消返航
|
async function cancelBackDock() {
|
if (droneStatus.value.mode_code !== 9) {
|
return showToast('当前状态不允许取消返航')
|
}
|
apiDisable.value = true
|
returnHomeCancel({ dock_sn: dockSn.value, client_id: client_id.value, job_id: job_id.value })
|
.then(res => {
|
const { data } = res.data
|
if (data.sub && data.sub?.length > 0) {
|
deviceTopicInfo.value.subTopic = data.sub[0]
|
}
|
if (data.pub && data.pub?.length > 0) {
|
deviceTopicInfo.value.pubTopic = data.pub[0]
|
}
|
showToast('取消返航成功')
|
// 取消返航 后 就要开始手动控制了 获取一下负载
|
getPayloadControl()
|
})
|
.finally(() => {
|
apiDisable.value = false
|
})
|
}
|
|
// 创建mqtt连接
|
const createConnect = async () => {
|
const result = await postDrc({}, workspace_id.value)
|
if (result?.code === 0) {
|
const { address, client_id: clientId, username, password } = result.data
|
mqttState = new UranusMqtt(address, { clientId, username, password: decryptAES(password) })
|
mqttState?.initMqtt()
|
client_id.value = clientId
|
}
|
}
|
|
// 销毁连接
|
const destroyConnect = () => {
|
if (mqttState) {
|
mqttState?.destroyed()
|
mqttState = null
|
client_id.value = ''
|
}
|
deviceTopicInfo.value.pubTopic = ''
|
deviceTopicInfo.value.subTopic = ''
|
}
|
|
// 返航或取消返航
|
const returnOrCancelReturn = () => {
|
if ([undefined, 0, 14].includes(droneStatus.value.mode_code)) {
|
return showToast('当前状态无法操作')
|
}
|
isBackDock.value ? cancelBackDock() : onBackDock()
|
}
|
|
// useManualControl里面用的参数
|
const paramsRef = computed(() => ({
|
droneSn: droneSn.value,
|
dockSn: dockSn.value,
|
speed: speed.value,
|
}))
|
|
let mqttHooks
|
|
function setSearchlight(mode) {
|
console.log(deviceTopicInfo.value.pubTopic)
|
mqttHooks?.publishMqtt(
|
deviceTopicInfo.value.pubTopic,
|
{
|
data: {
|
group: 0, //{"0":"主探照灯"}
|
mode: mode, //{ "0": "关闭", "1": "常量", "2": "爆闪", "3": "快速爆闪", "4": "交替爆闪" }
|
psdk_index: 5, //psdk 负载设备索引
|
},
|
method: 'drc_light_mode_set',
|
},
|
{ qos: 0 }
|
)
|
}
|
|
// 编辑飞行速度
|
function editSpeed(val) {
|
speed.value = Math.min(15, Math.max(1, speed.value + val))
|
}
|
|
const modelCode = computed(() => droneStatus?.value?.mode_code)
|
|
const getFileSpeedOnce = useRunOnce(async () => {
|
const {auto_flight_speed} = await analysisPointLineKmz(currentWayLine?.value?.url)
|
speed.value = auto_flight_speed
|
})
|
|
watch(
|
taskDetails,
|
() => {
|
currentWayLine.value?.url && getFileSpeedOnce()
|
},
|
{ immediate: true }
|
)
|
|
watch(workspace_id, async () => {
|
if (workspace_id.value && mqttState === null && client_id.value === '') {
|
await createConnect()
|
mqttHooks = useMqtt(mqttState, deviceTopicInfo.value)
|
manualControl = useManualControl(
|
mqttHooks,
|
deviceTopicInfo.value,
|
paramsRef,
|
isManualControl,
|
modelCode,
|
activeBtnList
|
)
|
manualControl?.openKeyMonitor()
|
}
|
})
|
|
onMounted(async () => {
|
EventBus.on('controlPanel-control', control)
|
EventBus.on('controlPanel-cancelManualControl', cancelManualControl)
|
EventBus.on('controlPanel-returnOrCancelReturn', returnOrCancelReturn)
|
EventBus.on('controlPanel-onMouseDown', onMouseDown)
|
EventBus.on('controlPanel-getPayloadControl', getPayloadControl)
|
EventBus.on('controlPanel-setSearchlight', setSearchlight)
|
EventBus.on('controlPanel-editSpeed', editSpeed)
|
})
|
|
onBeforeUnmount(() => {
|
manualControl?.removeKeyMonitor()
|
manualControl = null
|
EventBus.off('controlPanel-control', control)
|
EventBus.off('controlPanel-cancelManualControl', cancelManualControl)
|
EventBus.off('controlPanel-returnOrCancelReturn', returnOrCancelReturn)
|
EventBus.off('controlPanel-onMouseDown', onMouseDown)
|
EventBus.off('controlPanel-getPayloadControl', getPayloadControl)
|
EventBus.off('controlPanel-setSearchlight', setSearchlight)
|
EventBus.off('controlPanel-editSpeed', editSpeed)
|
destroyConnect()
|
})
|
</script>
|
|
<style scoped lang="scss">
|
.b-r {
|
border-radius: 50%;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
}
|
.ptzControlBtnBox {
|
z-index: 1;
|
color: #ffffff;
|
position: absolute;
|
right: half(110);
|
bottom: half(10);
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
|
.ptzControlBtn {
|
position: relative;
|
overflow: hidden;
|
width: half(143);
|
height: half(143);
|
background: rgba(22, 22, 22, 0.5); /* 半透明背景 */
|
|
.ptzControlItemIcon {
|
pointer-events: none;
|
width: half(135);
|
display: flex;
|
justify-content: right;
|
font-size: half(30);
|
position: absolute;
|
}
|
|
.ptzControlItem {
|
position: absolute;
|
width: 100%;
|
height: 100%;
|
transform: rotate(45deg);
|
|
&.isActiveBtn,
|
&:hover {
|
cursor: pointer;
|
box-shadow: 0 0 20px 5px rgba(0, 0, 0, 0.3);
|
}
|
}
|
|
.circles2 {
|
width: half(79);
|
height: half(79);
|
//background: rgba(255,255,255,0.25);
|
background: rgba(22, 22, 22, 0.5);
|
border: 1px solid rgba(0, 0, 0, 0.2);
|
|
.circles3 {
|
width: half(42);
|
height: half(42);
|
background: #ffffff;
|
z-index: 1;
|
display: flex;
|
align-items: center;
|
justify-content:center;
|
font-family: Source Han Sans CN, Source Han Sans CN;
|
font-weight: 500;
|
font-size: half(12);
|
color: #242424;
|
}
|
}
|
}
|
}
|
</style>
|