将 HTTP API 端点集中到 config.js,模式切换拆分为云台/底盘独立接口

- 新增 API_GIMBAL_MODE_SWITCH、API_CHASSIS_MODE_SWITCH、API_GIMBAL_PID_CONFIG、API_CHASSIS_PID_CONFIG
- 自动/手动页面改为独立的云台和底盘模式切换按钮
- 调试页面拆分为云台和底盘两大区域,各自包含模式切换和 PID 调参
This commit is contained in:
2026-06-07 02:09:40 +08:00
parent a2934822a3
commit 8f95dca704
4 changed files with 257 additions and 90 deletions
+7
View File
@@ -19,5 +19,12 @@ export const WHEEL_SEPARATION = 0.2 // 轮距(m)
export const GIMBAL_PITCH_DEG_S = 10
export const GIMBAL_YAW_DEG_S = 10
// HTTP API 端点
export const API_GIMBAL_MODE_SWITCH = `${HTTP_API_BASE}/api/gimbal/mode/switch`
export const API_CHASSIS_MODE_SWITCH = `${HTTP_API_BASE}/api/chassis/mode/switch`
export const API_LASER = `${HTTP_API_BASE}/api/laser`
export const API_GIMBAL_PID_CONFIG = `${HTTP_API_BASE}/api/gimbal/pidconfig`
export const API_CHASSIS_PID_CONFIG = `${HTTP_API_BASE}/api/chassis/pidconfig`
// 发布频率
export const PUBLISH_RATE = 30 // Hz
+20 -6
View File
@@ -2,7 +2,8 @@
<div class="auto-mode">
<div class="header">
<h2>自动模式</h2>
<button class="btn-switch" @click="switchToAuto">切换到当前模式</button>
<button class="btn-switch" @click="switchGimbalToAuto">云台切换到当前模式</button>
<button class="btn-switch" @click="switchChassisToAuto">底盘切换到当前模式</button>
</div>
<div class="stream-container">
<img ref="streamImg" :src="streamUrl" alt="MJPEG Stream" class="stream" />
@@ -13,7 +14,7 @@
<script setup>
import { ref } from 'vue'
import { onBeforeRouteLeave } from 'vue-router'
import { MJPEG_AUTO_URL, HTTP_API_BASE } from '../config.js'
import { MJPEG_AUTO_URL, API_GIMBAL_MODE_SWITCH, API_CHASSIS_MODE_SWITCH } from '../config.js'
const streamUrl = MJPEG_AUTO_URL
const streamImg = ref(null)
@@ -23,16 +24,29 @@ onBeforeRouteLeave((_to, _from, next) => {
next()
})
async function switchToAuto() {
async function switchGimbalToAuto() {
try {
await fetch(`${HTTP_API_BASE}/api/mode/switch`, {
await fetch(API_GIMBAL_MODE_SWITCH, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mode: 'auto' }),
})
console.log('已切换到自动模式')
console.log('云台已切换到自动模式')
} catch (e) {
console.error('模式切换失败:', e)
console.error('云台模式切换失败:', e)
}
}
async function switchChassisToAuto() {
try {
await fetch(API_CHASSIS_MODE_SWITCH, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mode: 'auto' }),
})
console.log('底盘已切换到自动模式')
} catch (e) {
console.error('底盘模式切换失败:', e)
}
}
</script>
+198 -67
View File
@@ -4,36 +4,89 @@
<h2>调试模式</h2>
</div>
<div class="section">
<h3>云台</h3>
<div class="mode-buttons">
<button class="btn-mode" @click="switchMode('auto')">切换到自动模式</button>
<button class="btn-mode" @click="switchMode('manual')">切换到手动模式</button>
<button class="btn-mode active" @click="switchMode('debug')">切换到调试模式</button>
<button class="btn-mode" @click="switchGimbalMode('auto')">云台自动</button>
<button class="btn-mode" @click="switchGimbalMode('manual')">云台手动</button>
<button class="btn-mode active" @click="switchGimbalMode('debug')">云台调试</button>
</div>
<div class="pid-grid">
<div class="pid-group">
<h3>视觉环 Yaw 电机</h3>
<label>Kp <input type="number" v-model="params.visualYaw.kp" step="0.001" /></label>
<label>Ki <input type="number" v-model="params.visualYaw.ki" step="0.001" /></label>
<label>Kd <input type="number" v-model="params.visualYaw.kd" step="0.001" /></label>
<label>Kp <input type="number" v-model="gimbalParams.visualYaw.kp" step="0.001" /></label>
<label>Ki <input type="number" v-model="gimbalParams.visualYaw.ki" step="0.001" /></label>
<label>Kd <input type="number" v-model="gimbalParams.visualYaw.kd" step="0.001" /></label>
</div>
<div class="pid-group">
<h3>视觉环 Pitch 电机</h3>
<label>Kp <input type="number" v-model="params.visualPitch.kp" step="0.001" /></label>
<label>Ki <input type="number" v-model="params.visualPitch.ki" step="0.001" /></label>
<label>Kd <input type="number" v-model="params.visualPitch.kd" step="0.001" /></label>
<label>Kp <input type="number" v-model="gimbalParams.visualPitch.kp" step="0.001" /></label>
<label>Ki <input type="number" v-model="gimbalParams.visualPitch.ki" step="0.001" /></label>
<label>Kd <input type="number" v-model="gimbalParams.visualPitch.kd" step="0.001" /></label>
</div>
<div class="pid-group">
<h3>角度环 Yaw 电机</h3>
<label>Kp <input type="number" v-model="params.angleYaw.kp" step="0.001" /></label>
<label>Ki <input type="number" v-model="params.angleYaw.ki" step="0.001" /></label>
<label>Kd <input type="number" v-model="params.angleYaw.kd" step="0.001" /></label>
<label>Kp <input type="number" v-model="gimbalParams.angleYaw.kp" step="0.001" /></label>
<label>Ki <input type="number" v-model="gimbalParams.angleYaw.ki" step="0.001" /></label>
<label>Kd <input type="number" v-model="gimbalParams.angleYaw.kd" step="0.001" /></label>
</div>
<div class="pid-group">
<h3>角度环 Pitch 电机</h3>
<label>Kp <input type="number" v-model="params.anglePitch.kp" step="0.001" /></label>
<label>Ki <input type="number" v-model="params.anglePitch.ki" step="0.001" /></label>
<label>Kd <input type="number" v-model="params.anglePitch.kd" step="0.001" /></label>
<label>Kp <input type="number" v-model="gimbalParams.anglePitch.kp" step="0.001" /></label>
<label>Ki <input type="number" v-model="gimbalParams.anglePitch.ki" step="0.001" /></label>
<label>Kd <input type="number" v-model="gimbalParams.anglePitch.kd" step="0.001" /></label>
</div>
</div>
<div class="pid-actions">
<button class="btn-load" :disabled="gimbalLoading" @click="loadGimbalConfig">
{{ gimbalLoading ? '加载中...' : '加载云台配置' }}
</button>
<button class="btn-publish" :disabled="gimbalSaving" @click="saveGimbalConfig">
{{ gimbalSaving ? '保存中...' : '保存云台配置' }}
</button>
</div>
</div>
<div class="section">
<h3>底盘</h3>
<div class="mode-buttons">
<button class="btn-mode" @click="switchChassisMode('auto')">底盘自动</button>
<button class="btn-mode" @click="switchChassisMode('manual')">底盘手动</button>
<button class="btn-mode active" @click="switchChassisMode('debug')">底盘调试</button>
</div>
<div class="pid-grid">
<div class="pid-group">
<h3>速度环 左电机</h3>
<label>Kp <input type="number" v-model="chassisParams.speedLeft.kp" step="0.001" /></label>
<label>Ki <input type="number" v-model="chassisParams.speedLeft.ki" step="0.001" /></label>
<label>Kd <input type="number" v-model="chassisParams.speedLeft.kd" step="0.001" /></label>
</div>
<div class="pid-group">
<h3>速度环 右电机</h3>
<label>Kp <input type="number" v-model="chassisParams.speedRight.kp" step="0.001" /></label>
<label>Ki <input type="number" v-model="chassisParams.speedRight.ki" step="0.001" /></label>
<label>Kd <input type="number" v-model="chassisParams.speedRight.kd" step="0.001" /></label>
</div>
<div class="pid-group">
<h3>角度环</h3>
<label>Kp <input type="number" v-model="chassisParams.angle.kp" step="0.001" /></label>
<label>Ki <input type="number" v-model="chassisParams.angle.ki" step="0.001" /></label>
<label>Kd <input type="number" v-model="chassisParams.angle.kd" step="0.001" /></label>
</div>
<div class="pid-group">
<h3>位置环</h3>
<label>Kp <input type="number" v-model="chassisParams.position.kp" step="0.001" /></label>
<label>Ki <input type="number" v-model="chassisParams.position.ki" step="0.001" /></label>
<label>Kd <input type="number" v-model="chassisParams.position.kd" step="0.001" /></label>
</div>
</div>
<div class="pid-actions">
<button class="btn-load" :disabled="chassisLoading" @click="loadChassisConfig">
{{ chassisLoading ? '加载中...' : '加载底盘配置' }}
</button>
<button class="btn-publish" :disabled="chassisSaving" @click="saveChassisConfig">
{{ chassisSaving ? '保存中...' : '保存底盘配置' }}
</button>
</div>
</div>
@@ -44,12 +97,6 @@
<button class="btn-laser" @click="toggleLaser">
激光: {{ laserOn ? 'ON' : 'OFF' }}
</button>
<button class="btn-load" :disabled="loading" @click="loadConfig">
{{ loading ? '加载中...' : '加载配置' }}
</button>
<button class="btn-publish" :disabled="saving" @click="saveConfig">
{{ saving ? '保存中...' : '保存配置' }}
</button>
</div>
<div class="stream-container">
@@ -61,7 +108,11 @@
<script setup>
import { reactive, ref, onMounted } from 'vue'
import { onBeforeRouteLeave } from 'vue-router'
import { HTTP_API_BASE, ROSBRIDGE_URL, MJPEG_DEBUG_URL } from '../config.js'
import {
ROSBRIDGE_URL, MJPEG_DEBUG_URL,
API_GIMBAL_MODE_SWITCH, API_CHASSIS_MODE_SWITCH,
API_LASER, API_GIMBAL_PID_CONFIG, API_CHASSIS_PID_CONFIG,
} from '../config.js'
import { useRosbridge } from '../composables/useRosbridge.js'
const streamUrl = MJPEG_DEBUG_URL
@@ -76,68 +127,91 @@ const { connected, connect } = useRosbridge()
connect(ROSBRIDGE_URL)
const laserOn = ref(false)
const loading = ref(false)
const saving = ref(false)
const gimbalLoading = ref(false)
const gimbalSaving = ref(false)
const chassisLoading = ref(false)
const chassisSaving = ref(false)
const defaults = {
const gimbalDefaults = {
visualYaw: { kp: 0, ki: 0, kd: 0 },
visualPitch: { kp: 0, ki: 0, kd: 0 },
angleYaw: { kp: 0, ki: 0, kd: 0 },
anglePitch: { kp: 0, ki: 0, kd: 0 },
}
const params = reactive(structuredClone(defaults))
const chassisDefaults = {
speedLeft: { kp: 0, ki: 0, kd: 0 },
speedRight: { kp: 0, ki: 0, kd: 0 },
angle: { kp: 0, ki: 0, kd: 0 },
position: { kp: 0, ki: 0, kd: 0 },
}
const gimbalParams = reactive(structuredClone(gimbalDefaults))
const chassisParams = reactive(structuredClone(chassisDefaults))
function snakeToCamel(key) {
return key.replace(/_([a-z])/g, (_, c) => c.toUpperCase())
}
async function loadConfig() {
loading.value = true
async function switchGimbalMode(mode) {
try {
const res = await fetch(`${HTTP_API_BASE}/api/pid/config`)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const data = await res.json()
for (const [snakeKey, val] of Object.entries(data)) {
const camelKey = snakeToCamel(snakeKey)
if (params[camelKey]) {
params[camelKey].kp = val.kp ?? 0
params[camelKey].ki = val.ki ?? 0
params[camelKey].kd = val.kd ?? 0
}
}
console.log('PID 配置已加载:', data)
} catch (e) {
console.error('加载 PID 配置失败:', e)
} finally {
loading.value = false
}
}
async function switchMode(mode) {
try {
await fetch(`${HTTP_API_BASE}/api/mode/switch`, {
await fetch(API_GIMBAL_MODE_SWITCH, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mode }),
})
console.log(`已切换到${mode}模式`)
console.log(`云台已切换到${mode}模式`)
} catch (e) {
console.error('模式切换失败:', e)
console.error('云台模式切换失败:', e)
}
}
async function saveConfig() {
async function switchChassisMode(mode) {
try {
await fetch(API_CHASSIS_MODE_SWITCH, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mode }),
})
console.log(`底盘已切换到${mode}模式`)
} catch (e) {
console.error('底盘模式切换失败:', e)
}
}
async function loadGimbalConfig() {
gimbalLoading.value = true
try {
const res = await fetch(API_GIMBAL_PID_CONFIG)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const data = await res.json()
for (const [snakeKey, val] of Object.entries(data)) {
const camelKey = snakeToCamel(snakeKey)
if (gimbalParams[camelKey]) {
gimbalParams[camelKey].kp = val.kp ?? 0
gimbalParams[camelKey].ki = val.ki ?? 0
gimbalParams[camelKey].kd = val.kd ?? 0
}
}
console.log('云台 PID 配置已加载:', data)
} catch (e) {
console.error('加载云台 PID 配置失败:', e)
} finally {
gimbalLoading.value = false
}
}
async function saveGimbalConfig() {
const payload = {
visual_yaw: params.visualYaw,
visual_pitch: params.visualPitch,
angle_yaw: params.angleYaw,
angle_pitch: params.anglePitch,
visual_yaw: gimbalParams.visualYaw,
visual_pitch: gimbalParams.visualPitch,
angle_yaw: gimbalParams.angleYaw,
angle_pitch: gimbalParams.anglePitch,
}
saving.value = true
gimbalSaving.value = true
try {
const res = await fetch(`${HTTP_API_BASE}/api/pid/config`, {
const res = await fetch(API_GIMBAL_PID_CONFIG, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
@@ -146,22 +220,67 @@ async function saveConfig() {
const err = await res.json().catch(() => ({}))
throw new Error(err.message || `HTTP ${res.status}`)
}
console.log('PID 配置已保存')
console.log('云台 PID 配置已保存')
} catch (e) {
console.error('保存 PID 配置失败:', e)
console.error('保存云台 PID 配置失败:', e)
} finally {
saving.value = false
gimbalSaving.value = false
}
}
onMounted(() => {
loadConfig()
})
async function loadChassisConfig() {
chassisLoading.value = true
try {
const res = await fetch(API_CHASSIS_PID_CONFIG)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const data = await res.json()
for (const [snakeKey, val] of Object.entries(data)) {
const camelKey = snakeToCamel(snakeKey)
if (chassisParams[camelKey]) {
chassisParams[camelKey].kp = val.kp ?? 0
chassisParams[camelKey].ki = val.ki ?? 0
chassisParams[camelKey].kd = val.kd ?? 0
}
}
console.log('底盘 PID 配置已加载:', data)
} catch (e) {
console.error('加载底盘 PID 配置失败:', e)
} finally {
chassisLoading.value = false
}
}
async function saveChassisConfig() {
const payload = {
speed_left: chassisParams.speedLeft,
speed_right: chassisParams.speedRight,
angle: chassisParams.angle,
position: chassisParams.position,
}
chassisSaving.value = true
try {
const res = await fetch(API_CHASSIS_PID_CONFIG, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
if (!res.ok) {
const err = await res.json().catch(() => ({}))
throw new Error(err.message || `HTTP ${res.status}`)
}
console.log('底盘 PID 配置已保存')
} catch (e) {
console.error('保存底盘 PID 配置失败:', e)
} finally {
chassisSaving.value = false
}
}
async function toggleLaser() {
try {
const action = laserOn.value ? 'off' : 'on'
await fetch(`${HTTP_API_BASE}/api/laser`, {
await fetch(API_LASER, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action }),
@@ -172,12 +291,22 @@ async function toggleLaser() {
console.error('激光切换失败:', e)
}
}
onMounted(() => {
loadGimbalConfig()
loadChassisConfig()
})
</script>
<style scoped>
.debug-mode { display: flex; flex-direction: column; gap: 20px; }
.header h2 { font-size: 18px; font-weight: 500; }
.section {
display: flex; flex-direction: column; gap: 12px;
}
.section > h3 { font-size: 15px; font-weight: 500; color: #ccc; margin: 0; }
.stream-container {
background: #000;
border-radius: 8px;
@@ -218,6 +347,8 @@ async function toggleLaser() {
}
.pid-group input:focus { border-color: #e94560; }
.pid-actions { display: flex; gap: 12px; }
.actions { display: flex; align-items: center; gap: 16px; }
.conn-status { font-size: 13px; color: #e94560; }
.conn-status.ok { color: #4ecca3; }
+22 -7
View File
@@ -5,7 +5,8 @@
<span class="conn-status" :class="{ ok: connected }">
rosbridge: {{ connected ? '已连接' : '未连接' }}
</span>
<button class="btn-switch" @click="switchToManual">切换到当前模式</button>
<button class="btn-switch" @click="switchGimbalToManual">云台切换到当前模式</button>
<button class="btn-switch" @click="switchChassisToManual">底盘切换到当前模式</button>
</div>
<div class="stream-container">
<img ref="streamImg" :src="streamUrl" alt="MJPEG Stream" class="stream" />
@@ -21,10 +22,11 @@
import { ref, onMounted, onUnmounted } from 'vue'
import { onBeforeRouteLeave } from 'vue-router'
import {
MJPEG_MANUAL_URL, HTTP_API_BASE, ROSBRIDGE_URL,
MJPEG_MANUAL_URL, ROSBRIDGE_URL,
CMD_WHEEL_TOPIC, CMD_GIMBAL_TOPIC,
MAX_LINEAR, MAX_ANGULAR, WHEEL_SEPARATION,
GIMBAL_PITCH_DEG_S, GIMBAL_YAW_DEG_S,
API_GIMBAL_MODE_SWITCH, API_CHASSIS_MODE_SWITCH, API_LASER,
} from '../config.js'
import { useRosbridge } from '../composables/useRosbridge.js'
@@ -98,7 +100,7 @@ function startLoop() {
async function toggleLaser() {
try {
const action = laserOn.value ? 'off' : 'on'
await fetch(`${HTTP_API_BASE}/api/laser`, {
await fetch(API_LASER, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action }),
@@ -110,16 +112,29 @@ async function toggleLaser() {
}
}
async function switchToManual() {
async function switchGimbalToManual() {
try {
await fetch(`${HTTP_API_BASE}/api/mode/switch`, {
await fetch(API_GIMBAL_MODE_SWITCH, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mode: 'manual' }),
})
console.log('已切换到手动模式')
console.log('云台已切换到手动模式')
} catch (e) {
console.error('模式切换失败:', e)
console.error('云台模式切换失败:', e)
}
}
async function switchChassisToManual() {
try {
await fetch(API_CHASSIS_MODE_SWITCH, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mode: 'manual' }),
})
console.log('底盘已切换到手动模式')
} catch (e) {
console.error('底盘模式切换失败:', e)
}
}