6 files modified
9 files added
| | |
| | | "disable-devtool": "^0.3.8", |
| | | "echarts": "^5.6.0", |
| | | "element-plus": "^2.9.3", |
| | | "eventemitter3": "^5.0.1", |
| | | "highlight.js": "^11.9.0", |
| | | "js-base64": "^3.7.4", |
| | | "js-cookie": "^3.0.0", |
| | | "js-md5": "^0.7.3", |
| | | "jszip": "^3.10.1", |
| | | "lodash": "^4.17.21", |
| | | "mitt": "^3.0.1", |
| | | "mqtt": "^5.11.0", |
| | | "nprogress": "^0.2.0", |
| | | "postcss-pxtorem": "^6.1.0", |
| | | "reconnecting-websocket": "^4.4.0", |
| New file |
| | |
| | | import request from '@/axios' |
| | | |
| | | // DRC 链路 |
| | | const DRC_API_PREFIX = '/drone-device-core/control/api/v1' |
| | | |
| | | // 获取 mqtt 连接认证 |
| | | export async function postDrc (body,workspaceId) { |
| | | const resp = await request.post(`${DRC_API_PREFIX}/workspaces/${workspaceId}/drc/connect`, body) |
| | | return resp.data |
| | | } |
| | | |
| | | // /control/api/v1/workspaces/82f6008b-2068-448c-9094-881e212f31c3/drc/connect |
| | | |
| | | // 进入飞行控制 (建立drc连接&获取云控控制权) |
| | | export async function postDrcEnter (body,workspaceId) { |
| | | const resp = await request.post(`${DRC_API_PREFIX}/workspaces/${workspaceId}/drc/enter`, body) |
| | | return resp.data |
| | | } |
| | | |
| | | // 退出飞行控制 (退出drc连接&退出云控控制权) |
| | | export async function postDrcExit (body,workspaceId) { |
| | | const resp = await request.post(`${DRC_API_PREFIX}/workspaces/${workspaceId}/drc/exit`, body) |
| | | return resp.data |
| | | } |
| | | |
| | | // 无人控制 |
| | | export async function droneController(data) { |
| | | return request({ |
| | | url: '/drone-device-core/dp/home/drc/droneController', |
| | | method: 'post', |
| | | data, |
| | | }) |
| | | } |
| | | |
| | | // 无人机退出控制 |
| | | export async function exitController(data) { |
| | | return request({ |
| | | url: '/drone-device-core/dp/home/drc/exitController', |
| | | method: 'post', |
| | | data, |
| | | }) |
| | | } |
| New file |
| | |
| | | export const 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', // 图传链路延时信息上报 |
| | | } |
| New file |
| | |
| | | import mitt from 'mitt' |
| | | |
| | | const emitter = mitt() |
| | | |
| | | export default emitter |
| New file |
| | |
| | | import { useStore } from 'vuex' |
| | | import { postDrc } from '@/api/drc' |
| | | import { UranusMqtt } from '@/mqtt' |
| | | |
| | | export function useConnectDrone() { |
| | | const store = useStore() |
| | | |
| | | |
| | | const deviceOsdInfo = inject('deviceOsdInfo') |
| | | const taskDetails = inject('taskDetails') |
| | | |
| | | |
| | | const mqttState = ref(null) |
| | | const client_id = ref(null) |
| | | |
| | | onMounted(async ()=>{ |
| | | if (mqttState.value) return |
| | | |
| | | const workspace_id = taskDetails.value.way_lines[0].workspace_id |
| | | const result = await postDrc({},workspace_id) |
| | | 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 => { |
| | | // @TODO: 异常case |
| | | }) |
| | | |
| | | store.commit('SET_MQTT_STATE', mqttState.value) |
| | | store.commit('SET_CLIENT_ID', client_id) |
| | | } |
| | | // @TODO: 认证失败case |
| | | }) |
| | | |
| | | |
| | | onBeforeUnmount(() => { |
| | | if (mqttState?.value) { |
| | | mqttState.value?.destroyed() |
| | | mqttState.value = null |
| | | store.commit('SET_MQTT_STATE', null) |
| | | store.commit('SET_CLIENT_ID', '') |
| | | } |
| | | }) |
| | | |
| | | return { |
| | | mqttState, |
| | | client_id |
| | | } |
| | | } |
| New file |
| | |
| | | import { DRC_METHOD } from '@/const/drc.js' |
| | | import { useMqtt } from '@/hooks/controlDrone/useMqtt' |
| | | import { ElMessage } from 'element-plus' |
| | | |
| | | let myInterval |
| | | |
| | | export const 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, isCurrentFlightController) { |
| | | const activeCodeKey = ref(null) |
| | | |
| | | const mqttHooks = useMqtt(deviceTopicInfo) |
| | | let seq = 0 |
| | | |
| | | function handlePublish(params) { |
| | | const body = { |
| | | method: DRC_METHOD.DRONE_CONTROL, |
| | | data: params, |
| | | } |
| | | handleClearInterval() |
| | | myInterval = setInterval(() => { |
| | | body.data.seq = seq++ |
| | | seq++ |
| | | window.console.log('keyCode>>>>', activeCodeKey.value, body) |
| | | mqttHooks?.publishMqtt(deviceTopicInfo.pubTopic, body, { qos: 0 }) |
| | | }, 50) |
| | | } |
| | | |
| | | function handleKeyup(keyCode) { |
| | | if (!deviceTopicInfo.pubTopic) { |
| | | ElMessage.error('请确保已经建立DRC链路') |
| | | return |
| | | } |
| | | const SPEED = 5 // check |
| | | const HEIGHT = 5 // check |
| | | const W_SPEED = 20 // 机头角速度 |
| | | seq = 0 |
| | | 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 |
| | | seq = 0 |
| | | handleClearInterval() |
| | | } |
| | | |
| | | function onKeyup() { |
| | | resetControlState() |
| | | } |
| | | |
| | | function onKeydown(e) { |
| | | handleKeyup(e.code) |
| | | } |
| | | |
| | | 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) { |
| | | ElMessage.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, |
| | | } |
| | | } |
| New file |
| | |
| | | import { |
| | | DRC_METHOD, |
| | | } from '@/const/drc.js' |
| | | import EventBus from '@/event-bus' |
| | | import { useStore } from 'vuex' |
| | | |
| | | |
| | | |
| | | export function useMqtt (deviceTopicInfo) { |
| | | let cacheSubscribeArr= [] |
| | | |
| | | const store = useStore() |
| | | |
| | | const mqttState = computed(() => { |
| | | return store.state.common.mqttState |
| | | }) |
| | | |
| | | function publishMqtt (topic, body, ots) { |
| | | mqttState.value?.publishMqtt(topic, JSON.stringify(body), ots) |
| | | } |
| | | |
| | | function subscribeMqtt (topic, handleMessageMqtt) { |
| | | mqttState.value?.subscribeMqtt(topic) |
| | | const handler = handleMessageMqtt || onMessageMqtt |
| | | mqttState.value?.on('onMessageMqtt', handler) |
| | | cacheSubscribeArr.push({ |
| | | topic, |
| | | callback: handler, |
| | | }) |
| | | } |
| | | |
| | | function onMessageMqtt (message) { |
| | | 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: |
| | | case DRC_METHOD.DRONE_CONTROL: |
| | | case DRC_METHOD.DRONE_EMERGENCY_STOP: |
| | | 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(), |
| | | }) |
| | | |
| | | // 监听云控控制权 |
| | | 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) { |
| | | 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, |
| | | } |
| | | } |
| New file |
| | |
| | | |
| | | export const OPTIONS = { |
| | | clean: true, // true: 清除会话, false: 保留会话 |
| | | connectTimeout: 10000, // mqtt 超时时间 |
| | | resubscribe: true, // 断开重连后,再次订阅原订阅 |
| | | reconnectPeriod: 10000, // 重连间隔时间: 5s |
| | | keepalive: 1, // 心跳间隔时间:1s |
| | | } |
| New file |
| | |
| | | import EventEmitter from 'eventemitter3' |
| | | import { OPTIONS } from './config' |
| | | import mqtt from 'mqtt' |
| | | |
| | | export class UranusMqtt extends EventEmitter { |
| | | constructor(url, options) { |
| | | super() |
| | | this._url = url || '' |
| | | this._options = options |
| | | this._client = null |
| | | this._hasInit = false |
| | | } |
| | | |
| | | initMqtt = () => { |
| | | // 仅初始化一次 |
| | | if (this._hasInit) return |
| | | // 建立连接 |
| | | this._client = mqtt.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, body, opts) => { |
| | | if (!this._client?.connected) { |
| | | this.initMqtt() |
| | | } |
| | | this._client?.publish(topic, body, opts || {}, (error, packet) => { |
| | | if (error) { |
| | | window.console.error('mqtt publish error,', error, packet) |
| | | } |
| | | }) |
| | | } |
| | | |
| | | // 订阅 |
| | | subscribeMqtt = topic => { |
| | | if (!this._client?.connected) { |
| | | this.initMqtt() |
| | | } |
| | | window.console.log('subscribeMqtt>>>>>', topic) |
| | | this._client?.subscribe(topic, (error, granted) => { |
| | | window.console.log('mqtt subscribe,', error, granted) |
| | | }) |
| | | } |
| | | |
| | | // 取消订阅 |
| | | unsubscribeMqtt = topic => { |
| | | 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, payload, packet) => { |
| | | this.emit('onMessageMqtt', { topic, payload, packet }) |
| | | } |
| | | |
| | | _onClose = () => { |
| | | // 连接异常关闭会自动重连 |
| | | window.console.error('mqtt close,') |
| | | this.emit('onStatus', { |
| | | status: 'close', |
| | | }) |
| | | } |
| | | |
| | | _onError = error => { |
| | | // 连接错误会自动重连 |
| | | window.console.error('mqtt error,', error) |
| | | this.emit('onStatus', { |
| | | status: 'error', |
| | | data: error, |
| | | }) |
| | | } |
| | | } |
| | |
| | | isMenu: true, |
| | | isSearch: false, |
| | | isRefresh: true, |
| | | mqttState: null, // mqtt 实例 |
| | | clientId: null, // 客户端ID实例 |
| | | isLock: getStore({ name: 'isLock' }), |
| | | colorName: getStore({ name: 'colorName' }) || '#2C77F1', |
| | | themeName: getStore({ name: 'themeName' }) || 'theme-go', |
| | |
| | | state.mapSetting = data |
| | | loadLAYER() |
| | | }, |
| | | SET_MQTT_STATE (state, mqttState) { |
| | | state.mqttState = mqttState |
| | | }, |
| | | SET_CLIENT_ID (state, id) { |
| | | state.clientId = id |
| | | }, |
| | | SET_LANGUAGE: (state, language) => { |
| | | state.language = language |
| | | setStore({ |
| | |
| | | <div class="pointControl"> |
| | | <div class="direction"> |
| | | <div class="blackBg directionUp"> |
| | | <div v-for="item in upperRowButton" :key="item.text"> |
| | | <el-icon :class="item.icon"></el-icon> |
| | | <div |
| | | :class="{ hotkeyBtn: true, activeKey: currentKey === item.text }" |
| | | @click="() => mousedown(item.text)" |
| | | v-hold="() => mousedown(item.text)" |
| | | > |
| | | {{ item.text }} |
| | | </div> |
| | | </div> |
| | | <el-button type="primary" @click="control">控制</el-button> |
| | | <el-button type="primary" @click="cancelControl">取消控制</el-button> |
| | | <el-button type="primary" ghost @mousedown="onMouseDown(KeyCode.KEY_Q)" @mouseup="onMouseUp">q</el-button> |
| | | </div> |
| | | <div class="blackBg directionDown"> |
| | | <div v-for="item in lowerRowButton" :key="item.text"> |
| | | <el-icon :class="item.icon"></el-icon> |
| | | <div |
| | | :class="{ hotkeyBtn: true, activeKey: currentKey === item.text }" |
| | | @click="() => mousedown(item.text)" |
| | | v-hold="() => mousedown(item.text)" |
| | | > |
| | | {{ item.text }} |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div class="blackBg directionDown"></div> |
| | | </div> |
| | | |
| | | <ControlComPass /> |
| | | |
| | | <div class="height-direction"> |
| | | <div class="blackBg height-direction-btn"> |
| | | <el-icon class="el-icon-top" /> |
| | | <div :class="{ hotkeyBtn: true, activeKey: currentKey === 'C' }" v-hold="() => mousedown('C')">C</div> |
| | | </div> |
| | | <div class="blackBg height-direction-btn"> |
| | | <div :class="{ hotkeyBtn: true, activeKey: currentKey === 'Z' }" v-hold="() => mousedown('Z')">Z</div> |
| | | <el-icon class="el-icon-bottom" /> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | <script setup> |
| | | import vHold from '@/directive/hold' |
| | | import ControlComPass from './ControlComPass/ControlComPass.vue' |
| | | import { |
| | | KeyCode, |
| | | useManualControl, |
| | | } from '@/hooks/controlDrone/useManualControl' |
| | | import { useMqtt } from '@/hooks/controlDrone/useMqtt' |
| | | import { useConnectDrone } from '@/hooks/controlDrone/useConnectDrone' |
| | | import { droneController, exitController, postDrcExit } from '@/api/drc' |
| | | import { ElMessage } from 'element-plus' |
| | | import { useStore } from 'vuex' |
| | | |
| | | const lowerRowButton = [ |
| | | { icon: 'el-icon-arrow-left', text: 'A' }, |
| | | { icon: 'el-icon-arrow-down', text: 'S' }, |
| | | { icon: 'el-icon-arrow-right', text: 'D' }, |
| | | ] |
| | | const upperRowButton = [ |
| | | { icon: 'el-icon-refresh-left', text: 'Q' }, |
| | | { icon: 'el-icon-arrow-up', text: 'W' }, |
| | | { icon: 'el-icon-refresh-right', text: 'E' }, |
| | | ] |
| | | let currentKey = ref('') |
| | | let keyReset = null |
| | | let visible = false |
| | | const deviceOsdInfo = inject('deviceOsdInfo') |
| | | const taskDetails = inject('taskDetails') |
| | | |
| | | function mousedown(key) { |
| | | if (key === 'Q') { |
| | | console.log(6666666666) |
| | | } |
| | | } |
| | | |
| | | function handleKeydown(e) { |
| | | currentKey.value = e.key.toUpperCase() |
| | | mousedown(e.key.toUpperCase()) |
| | | } |
| | | |
| | | function handleKeyup() { |
| | | currentKey.value = null |
| | | } |
| | | |
| | | onMounted(() => { |
| | | window.addEventListener('keydown', handleKeydown) |
| | | window.addEventListener('keyup', handleKeyup) |
| | | const deviceTopicInfo = ref({ |
| | | sn: deviceOsdInfo.value?.data?.sn, |
| | | pubTopic: '', |
| | | subTopic: '', |
| | | }) |
| | | onBeforeUnmount(() => { |
| | | window.removeEventListener('keydown', handleKeydown) |
| | | window.removeEventListener('keyup', handleKeyup) |
| | | }) |
| | | const flightController = ref(false) |
| | | console.log('控制面板') |
| | | |
| | | // 连接无人机mqtt 成功获得有效控制 |
| | | useConnectDrone() |
| | | |
| | | // 订阅消息 |
| | | useMqtt(deviceTopicInfo.value) |
| | | |
| | | // 使用手动控制 |
| | | const { handleKeyup, handleEmergencyStop, resetControlState } = useManualControl( |
| | | deviceTopicInfo.value, |
| | | flightController |
| | | ) |
| | | |
| | | function onMouseDown(type) { |
| | | console.log('anxia') |
| | | handleKeyup(type) |
| | | } |
| | | |
| | | const store = useStore() |
| | | const clientId = computed(() => store.state.common.clientId) |
| | | const dock_sn = computed(() => taskDetails.value.device_sns[0]) |
| | | |
| | | function cancelControl() { |
| | | exitController({ client_id: clientId.value, dock_sn:dock_sn.value }) |
| | | .then(res => { |
| | | flightController.value = false |
| | | deviceTopicInfo.value.subTopic = '' |
| | | deviceTopicInfo.value.pubTopic = '' |
| | | ElMessage.success('退出飞行控制成功') |
| | | }) |
| | | .catch(e => {}) |
| | | } |
| | | |
| | | // 控制 |
| | | function control() { |
| | | if (!clientId.value) return ElMessage.error('无人机不在空中,不能进入指挥飞行模式。') |
| | | if (!dock_sn.value) return ElMessage.error('系统错误,未获取到dock_sn') |
| | | droneController({ client_id: clientId.value, dock_sn:dock_sn.value }).then(res => { |
| | | flightController.value = true |
| | | 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] |
| | | } |
| | | ElMessage.success('控制成功') |
| | | }) |
| | | } |
| | | |
| | | function onMouseUp() { |
| | | console.log('弹起') |
| | | resetControlState() |
| | | } |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | |
| | | align-items: center; |
| | | } |
| | | |
| | | //变量 |
| | | $FSColor: #00ee8b; |
| | | |
| | | .hotkeyBtn { |
| | | background: #3c3c3c; |
| | | border-radius: 2px; |
| | | width: 24px; |
| | | height: 24px; |
| | | font-size: 14px; |
| | | line-height: 24px; |
| | | text-align: center; |
| | | color: #fff; |
| | | cursor: pointer; |
| | | -webkit-user-select: none; |
| | | user-select: none; |
| | | |
| | | &:hover { |
| | | background: $FSColor; |
| | | } |
| | | } |
| | | |
| | | .pointControl { |
| | | position: absolute; |
| | | bottom: 0; |
| | | right: 0; |
| | | width: 1540px; |
| | | height: 217px; |
| | | background: rgba(255, 255, 255, 0.3); |
| | | border-radius: 40px 0px 40px 40px; |
| | | display: flex; |
| | | justify-content: center; |
| | | align-items: flex-end; |
| | | color: white; |
| | | gap: 0 10px; |
| | | pointer-events: all; |
| | | width: 100%; |
| | | height: 200px; |
| | | |
| | | .activeKey { |
| | | background: $FSColor; |
| | | } |
| | | |
| | | .blackBg { |
| | | background: rgba(0, 0, 0, 0.65); |
| | | padding: 5px 5px; |
| | | border-radius: 5px; |
| | | } |
| | | |
| | | .direction { |
| | | display: flex; |
| | | flex-direction: column; |
| | | width: 100px; |
| | | |
| | | .speedBox { |
| | | color: $FSColor; |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | margin: 5px 0; |
| | | gap: 0 5px; |
| | | } |
| | | |
| | | .editBox { |
| | | display: flex; |
| | | justify-content: end; |
| | | gap: 0 5px; |
| | | margin-bottom: 5px; |
| | | |
| | | .current-point { |
| | | font-size: 20px; |
| | | display: flex; |
| | | align-items: center; |
| | | color: $FSColor; |
| | | } |
| | | |
| | | .editPointBtn { |
| | | &:hover { |
| | | cursor: pointer; |
| | | background: $FSColor; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .directionUp, |
| | | .directionDown { |
| | | display: flex; |
| | | justify-content: space-around; |
| | | text-align: center; |
| | | height: 55px; |
| | | |
| | | > div { |
| | | display: flex; |
| | | flex-direction: column; |
| | | justify-content: space-between; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .height-direction { |
| | | width: 100px; |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: end; |
| | | gap: 5px 0; |
| | | |
| | | .height-direction-btn { |
| | | width: 35px; |
| | | height: 55px; |
| | | text-align: center; |
| | | display: flex; |
| | | flex-direction: column; |
| | | justify-content: space-between; |
| | | } |
| | | } |
| | | } |
| | | </style> |
| | |
| | | :destroy-on-close="true" |
| | | > |
| | | <div class="content-container" v-if="isShow"> |
| | | <TaskDetailsHead/> |
| | | <!-- 视频直播 --> |
| | | <div class="video-container"> |
| | | <LiveVideo :videoUrl="currentLiveUrl" /> |
| | | </div> |
| | | <!-- 展示地图 --> |
| | | <RealTimeMap /> |
| | | <RealTimeMap class="realTimeMap" /> |
| | | </div> |
| | | <ControlPanel/> |
| | | <ControlPanel v-if="deviceOsdInfo?.data?.sn" /> |
| | | </el-dialog> |
| | | </template> |
| | | |
| | |
| | | import { useConnectWebSocket } from '@/utils/websocket/connect-websocket' |
| | | import { EBizCode } from '@/utils/staticData/enums' |
| | | import ControlPanel from '@/views/TaskManage/TaskIntermediateContent/CurrentTaskDetails/ControlPanel.vue' |
| | | import { KeyCode } from '@/hooks/controlDrone/useManualControl' |
| | | import TaskDetailsHead from '@/views/TaskManage/TaskIntermediateContent/CurrentTaskDetails/TaskDetailsHead.vue' |
| | | |
| | | const isShow = defineModel('show') |
| | | const props = defineProps({ |
| | |
| | | let taskDetails = ref({}) |
| | | const currentLiveUrl = ref('') |
| | | const machineNestUrl = ref('') |
| | | const dockLiveUrl = ref('') |
| | | |
| | | const droneLiveUrl = ref('') |
| | | provide('taskDetails', taskDetails) |
| | | |
| | | const deviceOsdInfo = ref({}) |
| | |
| | | // 机巢直播 |
| | | const getDeviceLiveUrl = async () => { |
| | | if (machineNestUrl.value) return machineNestUrl.value |
| | | const res = await liveStart(taskDetails.value.device_sns[0],'') |
| | | const res = await liveStart(taskDetails.value.device_sns[0], '') |
| | | machineNestUrl.value = res.data.data.rtcs_url |
| | | return machineNestUrl.value |
| | | } |
| | | |
| | | // 无人机直播 |
| | | const getDockLiveUrl = async dockSn => { |
| | | if (dockLiveUrl.value) return dockLiveUrl.value |
| | | const res = await liveStart(dockSn,'') |
| | | dockLiveUrl.value = res.data.data.rtcs_url |
| | | return dockLiveUrl.value |
| | | if (droneLiveUrl.value) return droneLiveUrl.value |
| | | const res = await liveStart(dockSn, '') |
| | | droneLiveUrl.value = res.data.data.rtcs_url |
| | | return droneLiveUrl.value |
| | | } |
| | | |
| | | // 设置当前直播地址 |
| | |
| | | }) |
| | | </script> |
| | | |
| | | <style lang="scss" scoped> |
| | | <style lang="scss"> |
| | | .current-task-details { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | |
| | | .content-container { |
| | | display: flex; |
| | | // gap: 20px; |
| | | height: 600px; |
| | | |
| | | .video-container { |
| | | width: 50%; |
| | | padding-right: 10px; |
| | | .el-dialog { |
| | | position: relative; |
| | | margin-top: 38px; |
| | | width: 1782px; |
| | | height: 1002px; |
| | | padding: 0; |
| | | .el-dialog__body { |
| | | width: 100%; |
| | | height: 100%; |
| | | } |
| | | .el-dialog__header { |
| | | height: 0; |
| | | overflow: hidden; |
| | | padding: 0; |
| | | |
| | | .el-dialog__headerbtn { |
| | | position: absolute; |
| | | right: -40px; |
| | | top: -40px; |
| | | |
| | | .el-dialog__close { |
| | | font-size: 30px; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | </style> |
| | | |
| | | <style lang="scss" scoped> |
| | | .content-container { |
| | | height: 100%; |
| | | width: 100%; |
| | | |
| | | .video-container { |
| | | width: 100%; |
| | | height: 100%; |
| | | } |
| | | |
| | | |
| | | |
| | | .realTimeMap { |
| | | position: absolute; |
| | | left: -1px; |
| | | bottom: -1px; |
| | | width: 218px; |
| | | height: 217px; |
| | | border-radius: 0px 20px 0px 40px; |
| | | border: 1px solid #62a1ff; |
| | | } |
| | | } |
| | | </style> |
| | |
| | | </script> |
| | | <style scoped lang="scss"> |
| | | #currentTaskMap { |
| | | width: 50%; |
| | | padding-left: 10px; |
| | | height: 100%; |
| | | |
| | | :deep() { |
| | | .cesium-viewer { |
| New file |
| | |
| | | <template> |
| | | <div class="detailsHead"> |
| | | <div class="droneName">小蓝工业园</div> |
| | | <div class="infoListBox"> |
| | | <div v-for="item in infoList"> |
| | | <div class="infoValue">{{ item.value }}</div> |
| | | <div class="infoTitle">{{ item.title }}</div> |
| | | </div> |
| | | </div> |
| | | <div class="controlBtn"> |
| | | <el-icon class="refresh"><Refresh /></el-icon> |
| | | <div class="switchBtn" @click="switchBtn"> |
| | | <div :class="{ open: open }">NO</div> |
| | | <div :class="{ open: !open }">OFF</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { Refresh } from '@element-plus/icons-vue' |
| | | |
| | | const open = ref(true) |
| | | const switchBtn = () => { |
| | | open.value = !open.value |
| | | } |
| | | const infoList = [ |
| | | { title: '实时真高', value: '0' }, |
| | | { title: '绝对高度', value: '0' }, |
| | | { title: '水平速度', value: '0' }, |
| | | { title: '垂直速度', value: '0' }, |
| | | { title: '经度', value: '0' }, |
| | | { title: '纬度', value: '0' }, |
| | | { title: '4G信号', value: '0' }, |
| | | { title: 'SDR信号', value: '0' }, |
| | | { title: 'GPS搜星数', value: '0' }, |
| | | { title: 'RTK搜星数', value: '0' }, |
| | | { title: '距离机场', value: '0' }, |
| | | { title: '飞行时长', value: '0' }, |
| | | { title: '电池电量', value: '0' }, |
| | | ] |
| | | </script> |
| | | |
| | | <style scoped lang="scss"> |
| | | .detailsHead { |
| | | position: absolute; |
| | | top: 0; |
| | | z-index: 5; |
| | | width: 100%; |
| | | height: 68px; |
| | | background: rgba(255, 255, 255, 0.1); /* 半透明背景 */ |
| | | backdrop-filter: blur(3px); |
| | | padding: 0 31px; |
| | | display: flex; |
| | | align-items: center; |
| | | |
| | | .droneName { |
| | | width: 132px; |
| | | height: 42px; |
| | | background: rgba(74, 72, 72, 0.54); |
| | | box-shadow: 0px 4px 72px 0px rgba(0, 0, 0, 0.25); |
| | | border-radius: 8px 8px 8px 8px; |
| | | border: 1px solid rgba(255, 255, 255, 0.99); |
| | | font-family: Segoe UI, Segoe UI; |
| | | font-weight: normal; |
| | | font-size: 18px; |
| | | color: #ededed; |
| | | text-align: center; |
| | | line-height: 42px; |
| | | } |
| | | |
| | | .infoListBox { |
| | | width: 0; |
| | | flex: 1; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-around; |
| | | font-family: Segoe UI, Segoe UI; |
| | | font-weight: 400; |
| | | |
| | | .infoValue { |
| | | font-size: 20px; |
| | | color: #ffffff; |
| | | line-height: 15px; |
| | | margin-bottom: 10px; |
| | | } |
| | | |
| | | .infoTitle { |
| | | font-size: 12px; |
| | | color: #d2e8fa; |
| | | line-height: 15px; |
| | | } |
| | | } |
| | | |
| | | .controlBtn { |
| | | width: 130px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | |
| | | .refresh{ |
| | | color: white; |
| | | font-size: 30px; |
| | | cursor: pointer; |
| | | } |
| | | |
| | | .switchBtn { |
| | | width: 70px; |
| | | height: 30px; |
| | | box-shadow: 2px 4px 20px 0px rgba(0, 13, 26, 0.23); |
| | | border-radius: 4px 4px 4px 4px; |
| | | border: 1px solid #ffffff; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | font-family: Segoe UI, Segoe UI; |
| | | font-weight: bold; |
| | | font-size: 14px; |
| | | line-height: 30px; |
| | | text-align: center; |
| | | color: #ffffff; |
| | | cursor: pointer; |
| | | |
| | | > div { |
| | | width: 50%; |
| | | } |
| | | |
| | | .open { |
| | | background: #ffffff; |
| | | color: #242424; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | </style> |