feat: 增加云台功能控制

This commit is contained in:
2025-11-01 15:02:00 +08:00
parent b3b34b5d91
commit 9b6b0263c7
3 changed files with 151 additions and 59 deletions

67
app.py
View File

@@ -11,7 +11,8 @@ logger = logging.getLogger(__name__)
# Flask 应用初始化 # Flask 应用初始化
app = Flask(__name__) app = Flask(__name__)
app.config['SECRET_KEY'] = 'h2Ms4pw9GzvwiFHyNPhH' # 请更换为安全的密钥 app.config['SECRET_KEY'] = 'h2Ms4pw9GzvwiFHyNPhH' # 请更换为安全的密钥
socketio = SocketIO(app, cors_allowed_origins="*") # socketio = SocketIO(app, cors_allowed_origins="*")
socketio = SocketIO(app)
# 全局变量存储摇杆数据 # 全局变量存储摇杆数据
joystick_data = {} joystick_data = {}
@@ -38,7 +39,7 @@ def handle_joystick_data(data):
# print(f"force: {force} x: {x}, y: {y}") # print(f"force: {force} x: {x}, y: {y}")
ptz.set_pitch_speed(int(y * 100)) ptz.set_pitch_speed(int(y * 100))
ptz.set_yaw_speed(int(x * 100)) ptz.set_yaw_speed(int(x * -100))
# direction = data.get('direction', '') # direction = data.get('direction', '')
# logger.info(f"收到摇杆数据:{direction}") # logger.info(f"收到摇杆数据:{direction}")
@@ -85,27 +86,49 @@ def handle_ping():
"""处理心跳检测""" """处理心跳检测"""
emit('pong') emit('pong')
@app.route('/get_joystick_data') @socketio.on('gimbal_center')
def get_joystick_data(): def handle_gimbal_center():
"""API 端点:获取当前摇杆数据""" """处理云台中心"""
from flask import jsonify ptz.center()
return jsonify(joystick_data) logger.info(f"云台回中")
@app.route('/control/<action>') @socketio.on('gimbal_pip')
def robot_control(action): def handle_gimbal_pip(data):
""" """处理云台PIP"""
示例控制端点 mode = int(data)
可用于直接控制机器人动作 if mode >= 0 and mode <= 3:
""" ptz.set_pip_mode(mode)
from flask import jsonify logger.info(f"云台PIP {mode}")
valid_actions = ['forward', 'backward', 'left', 'right', 'stop']
@socketio.on('gimbal_ir_mode')
def handle_gimbal_ir_mode(data):
"""处理云台IR"""
mode = int(data)
if mode >= 0 and mode <= 9:
ptz.set_ir_mode(mode)
logger.info(f"云台IR {mode}")
# @app.route('/get_joystick_data')
# def get_joystick_data():
# """API 端点:获取当前摇杆数据"""
# from flask import jsonify
# return jsonify(joystick_data)
# @app.route('/control/<action>')
# def robot_control(action):
# """
# 示例控制端点
# 可用于直接控制机器人动作
# """
# from flask import jsonify
# valid_actions = ['forward', 'backward', 'left', 'right', 'stop']
if action in valid_actions: # if action in valid_actions:
logger.info(f"执行机器人控制:{action}") # logger.info(f"执行机器人控制:{action}")
# 在这里添加实际的机器人控制逻辑 # # 在这里添加实际的机器人控制逻辑
return jsonify({'status': 'success', 'action': action}) # return jsonify({'status': 'success', 'action': action})
else: # else:
return jsonify({'status': 'error', 'message': '无效的动作'}), 400 # return jsonify({'status': 'error', 'message': '无效的动作'}), 400
if __name__ == '__main__': if __name__ == '__main__':
socketio.run(app, host='0.0.0.0', port=5000, debug=True) socketio.run(app, host='10.21.31.250', port=5000, debug=True)

View File

@@ -185,20 +185,20 @@ if __name__ == "__main__":
ptz.center() ptz.center()
time.sleep(1) time.sleep(1)
ptz.set_pitch_speed(99) # ptz.set_pitch_speed(99)
time.sleep(1) # time.sleep(1)
ptz.set_pitch_speed(0) # ptz.set_pitch_speed(0)
time.sleep(1) # time.sleep(1)
ptz.set_yaw_speed(-99) # ptz.set_yaw_speed(-99)
time.sleep(1) # time.sleep(1)
ptz.set_yaw_speed(0) # ptz.set_yaw_speed(0)
time.sleep(1) # time.sleep(1)
ptz.set_roll_speed(-99) # ptz.set_roll_speed(-99)
time.sleep(1) # time.sleep(1)
ptz.set_roll_speed(0) # ptz.set_roll_speed(0)
time.sleep(1) # time.sleep(1)
print("All commands sent.") print("All commands sent.")

View File

@@ -1,5 +1,6 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -12,6 +13,7 @@
background-color: #f0f0f0; background-color: #f0f0f0;
min-height: 100vh; min-height: 100vh;
} }
.container { .container {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
@@ -21,42 +23,50 @@
margin: 0 auto; margin: 0 auto;
height: calc(100vh - 40px); height: calc(100vh - 40px);
} }
.camera-frame { .camera-frame {
border: 1px solid #ccc; border: 1px solid #ccc;
border-radius: 8px; border-radius: 8px;
overflow: hidden; overflow: hidden;
background-color: #fff; background-color: #fff;
box-shadow: 0 2px 4px rgba(0,0,0,0.1); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease; transition: all 0.3s ease;
position: relative; position: relative;
} }
.camera-frame.main-view { .camera-frame.main-view {
grid-column: 1 / span 2; grid-column: 1 / span 2;
grid-row: 1 / span 2; grid-row: 1 / span 2;
z-index: 10; z-index: 10;
border: 3px solid #4a90e2; border: 3px solid #4a90e2;
box-shadow: 0 8px 16px rgba(0,0,0,0.2); box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
} }
.camera-frame.main-view .camera-title { .camera-frame.main-view .camera-title {
background-color: #2c5aa0; background-color: #2c5aa0;
font-size: 1.2em; font-size: 1.2em;
} }
.camera-frame.front-view { .camera-frame.front-view {
grid-column: 3 / span 1; grid-column: 3 / span 1;
grid-row: 1 / span 1; grid-row: 1 / span 1;
} }
.camera-frame.back-view { .camera-frame.back-view {
grid-column: 3 / span 1; grid-column: 3 / span 1;
grid-row: 2 / span 1; grid-row: 2 / span 1;
} }
.camera-frame.left-view { .camera-frame.left-view {
grid-column: 1 / span 1; grid-column: 1 / span 1;
grid-row: 3 / span 1; grid-row: 3 / span 1;
} }
.camera-frame.right-view { .camera-frame.right-view {
grid-column: 2 / span 1; grid-column: 2 / span 1;
grid-row: 3 / span 1; grid-row: 3 / span 1;
} }
.joystick-container { .joystick-container {
grid-column: 3 / span 1; grid-column: 3 / span 1;
grid-row: 3 / span 1; grid-row: 3 / span 1;
@@ -65,8 +75,9 @@
align-items: center; align-items: center;
background-color: #f8f9fa; background-color: #f8f9fa;
border-radius: 8px; border-radius: 8px;
box-shadow: inset 0 2px 4px rgba(0,0,0,0.1); box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);
} }
.camera-title { .camera-title {
background-color: #4a90e2; background-color: #4a90e2;
color: white; color: white;
@@ -76,14 +87,17 @@
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
} }
.camera-title:hover { .camera-title:hover {
background-color: #3a7bc8; background-color: #3a7bc8;
} }
iframe { iframe {
width: 100%; width: 100%;
height: 100%; height: 100%;
border: none; border: none;
} }
.status-indicator { .status-indicator {
position: absolute; position: absolute;
top: 5px; top: 5px;
@@ -93,20 +107,25 @@
border-radius: 50%; border-radius: 50%;
z-index: 5; z-index: 5;
} }
virtual-joystick { virtual-joystick {
--radius: 100px; --radius: 100px;
} }
virtual-joystick::part(joystick) { virtual-joystick::part(joystick) {
background-color: rgba(255, 255, 255, 0.2); background-color: rgba(255, 255, 255, 0.2);
border: 2px solid rgba(74, 144, 226, 0.5); border: 2px solid rgba(74, 144, 226, 0.5);
} }
virtual-joystick::part(joystick):before { virtual-joystick::part(joystick):before {
background-color: rgba(74, 144, 226, 0.2); background-color: rgba(74, 144, 226, 0.2);
} }
virtual-joystick::part(joystick):after { virtual-joystick::part(joystick):after {
background-color: #4a90e2; background-color: #4a90e2;
border: 1px solid #2c5aa0; border: 1px solid #2c5aa0;
} }
#connection-status { #connection-status {
position: fixed; position: fixed;
top: 10px; top: 10px;
@@ -116,17 +135,43 @@
color: white; color: white;
font-weight: bold; font-weight: bold;
} }
.connected { .connected {
background-color: #28a745; background-color: #28a745;
} }
.disconnected { .disconnected {
background-color: #dc3545; background-color: #dc3545;
} }
</style> </style>
</head> </head>
<body> <body>
<div id="connection-status" class="disconnected">未连接</div> <div id="connection-status" class="disconnected">未连接</div>
<div class="gimbal">
<button id="gimbal_center">云台回中</button>
<!-- <button id="gimbal_pip">画中画</button> -->
<!-- 下拉选框 -->
<select id="gimbal_pip">
<option value="0">RGB主</option>
<option value="1">仅RGB</option>
<option value="2">仅IR</option>
<option value="3">IR主</option>
</select>
<select id="ir_mode">
<option value="0">0</option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
<option value="6">6</option>
<option value="7">7</option>
<option value="8">8</option>
<option value="9">9</option>
</select>
</div>
<div class="container"> <div class="container">
<div class="camera-frame main-view" data-camera="cam_gimbal"> <div class="camera-frame main-view" data-camera="cam_gimbal">
<div class="camera-title">云台</div> <div class="camera-title">云台</div>
@@ -161,39 +206,62 @@
<!-- Socket.IO 客户端库 --> <!-- Socket.IO 客户端库 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.2/socket.io.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.2/socket.io.js"></script>
<script src="{{ url_for('static', filename='js/virtual-joystick.js') }}"></script> <script src="{{ url_for('static', filename='js/virtual-joystick.js') }}"></script>
<script> <script>
// 初始化 Socket.IO 连接 // 初始化 Socket.IO 连接
const socket = io(); const socket = io();
// 连接状态管理 // 连接状态管理
const connectionStatus = document.getElementById('connection-status'); const connectionStatus = document.getElementById('connection-status');
socket.on('connect', function() { socket.on('connect', function () {
console.log('WebSocket 连接已建立'); console.log('WebSocket 连接已建立');
connectionStatus.textContent = '已连接'; connectionStatus.textContent = '已连接';
connectionStatus.className = 'connected'; connectionStatus.className = 'connected';
}); });
socket.on('disconnect', function() { socket.on('disconnect', function () {
console.log('WebSocket 连接已断开'); console.log('WebSocket 连接已断开');
connectionStatus.textContent = '已断开'; connectionStatus.textContent = '已断开';
connectionStatus.className = 'disconnected'; connectionStatus.className = 'disconnected';
}); });
socket.on('connection_response', function(data) { // socket.on('connection_response', function (data) {
console.log('服务器响应:', data); // console.log('服务器响应:', data);
// });
// socket.on('data_received', function (data) {
// console.log('数据确认:', data);
// });
const button_gimbal_center = document.getElementById('gimbal_center');
// const button_gimbal_pip = document.getElementById('gimbal_pip');
const select_gimbal_pip = document.getElementById('gimbal_pip');
const select_ir_mode = document.getElementById('ir_mode');
button_gimbal_center.addEventListener('click', function () {
socket.emit('gimbal_center');
}); });
socket.on('data_received', function(data) { // button_gimbal_pip.addEventListener('click', function () {
console.log('数据确认:', data); // socket.emit('gimbal_pip');
}); // });
select_gimbal_pip.addEventListener('change', function () {
const selectedValue = select_gimbal_pip.value;
socket.emit('gimbal_pip', selectedValue);
})
select_ir_mode.addEventListener('change', function () {
const selectedValue = select_ir_mode.value;
socket.emit('gimbal_ir_mode', selectedValue);
})
// 获取摇杆元素 // 获取摇杆元素
const joystick = document.querySelector('virtual-joystick'); const joystick = document.querySelector('virtual-joystick');
// 监听摇杆移动事件 // 监听摇杆移动事件
joystick.addEventListener('joystickmove', function(e) { joystick.addEventListener('joystickmove', function (e) {
const data = { const data = {
x: parseFloat(joystick.dataset.x), x: parseFloat(joystick.dataset.x),
y: parseFloat(joystick.dataset.y), y: parseFloat(joystick.dataset.y),
@@ -203,13 +271,13 @@
hypot: parseFloat(joystick.dataset.hypot) || 0, hypot: parseFloat(joystick.dataset.hypot) || 0,
timestamp: Date.now() timestamp: Date.now()
}; };
// 发送数据到服务器 // 发送数据到服务器
socket.emit('joystick_data', data); socket.emit('joystick_data', data);
}); });
// 监听摇杆抬起事件 // 监听摇杆抬起事件
joystick.addEventListener('joystickup', function(e) { joystick.addEventListener('joystickup', function (e) {
// 发送停止信号 // 发送停止信号
const stopData = { const stopData = {
x: 0, x: 0,
@@ -221,10 +289,10 @@
timestamp: Date.now(), timestamp: Date.now(),
action: 'stop' action: 'stop'
}; };
socket.emit('joystick_data', stopData); socket.emit('joystick_data', stopData);
}); });
// 心跳检测 // 心跳检测
setInterval(() => { setInterval(() => {
if (socket.connected) { if (socket.connected) {
@@ -233,4 +301,5 @@
}, 30000); // 每 30 秒发送一次心跳 }, 30000); // 每 30 秒发送一次心跳
</script> </script>
</body> </body>
</html> </html>