Files
wrobot/v1.4/python_flask/wrobot.html
T
2025-12-26 23:28:23 +08:00

274 lines
12 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Wrobot</title>
<link rel="stylesheet" href="{{url_for('static', filename='bootstrap/dist/css/bootstrap.min.css')}}">
<link rel="stylesheet" href="{{url_for('static', filename='css/main.min.css')}}">
<link rel="stylesheet" href="{{url_for('static', filename='font-awesome-4.7.0/css/font-awesome.min.css')}}">
<script src="{{url_for('static', filename='js/jquery-3.6.0.min.js')}}"></script>
<script src="{{url_for('static', filename='bootstrap/dist/js/bootstrap.bundle.min.js')}}"></script>
<style>
#nipple-log{height:auto;min-height:50px;max-height:200px;overflow-y:auto;border:1px solid #ddd;padding:5px}
.joystick-container{width:300px;height:300px;margin:0 auto;border:2px solid #ddd;border-radius:2px;position:relative;background-color:#f8f9fa;cursor:grab;touch-action:none;user-select:none}
.joystick-container:active{cursor:grabbing}
.joystick-container::before{content:'';position:absolute;left:10%;top:50%;width:80%;height:1px;background-color:#ddd;transform:translateY(-50%);z-index:1}
.joystick-container::after{content:'';position:absolute;left:50%;top:10%;width:1px;height:80%;background-color:#ddd;transform:translateX(-50%);z-index:1}
.joystick-btn{width:50px;height:50px;background-color:#0d6efd;border-radius:50%;position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);box-shadow:0 2px 8px rgba(0,0,0,.3);transition:background-color .2s;will-change:transform;z-index:2}
.joystick-btn:hover{background-color:#0b5ed7}
.joystick-btn.dragging{background-color:#0a58ca;box-shadow:0 4px 12px rgba(0,0,0,.4)}
.angle-mark{position:absolute;color:#666;font-size:12px;font-weight:500;z-index:1}
.mark-x-0{left:5px;top:50%;transform:translateY(-50%)}
.mark-x-180{right:5px;top:50%;transform:translateY(-50%)}
.mark-y-0{top:5px;left:50%;transform:translateX(-50%)}
.mark-y-180{bottom:5px;left:50%;transform:translateX(-50%)}
.angle-display{text-align:center;margin-top:15px;font-size:16px;color:#333;font-weight:500}
.angle-item{margin:5px 0}
#head_video{background-color:#f1f1f1;transition:opacity .1s ease}
</style>
</head>
<body>
<div class="container h-100">
<!-- 设备&控制栏 -->
<div class="row gx-3 my-3">
<div class="col-12 d-flex align-items-center gap-3">
<span class="text-secondary d-flex">设备ID:</span>
<select class="form-select form-select-sm h3 blue1 w-auto " id="device_id">
{% for k in device_list %}
<option value="{{k}}" {{ 'selected' if k == device_id else '' }}>{{k}}</option>
{% endfor %}
</select>
<span class="text-secondary d-flex">跟踪:</span>
<select class="form-select form-select-sm h3 blue1 w-auto" id="follow_flag">
{% for k,v in follow_list.items() %}
<option value="{{v}}" {{ 'selected' if k == follow_id else '' }}>{{k}}</option>
{% endfor %}
</select>
<button id="reset_btn" class="btn btn-outline-primary btn-sm">还原姿态</button>
</div>
</div>
<!-- 摄像头+摇杆布局 -->
<div class="row gx-3 justify-content-center align-items-center">
<div class="col-md-6">
<!-- 核心优化:直接指向实时流地址,浏览器自动刷新 -->
<img class="img-fluid" id="head_video" src="/wrobot_cam/{{device_id}}" alt="摄像头视频" style="width:100%;height:auto;">
</div>
<div class="col-md-3">
<div class="joystick-container" id="joystickContainer">
<span class="angle-mark mark-x-0">180°</span>
<span class="angle-mark mark-x-180">0°</span>
<span class="angle-mark mark-y-0">180°</span>
<span class="angle-mark mark-y-180">0°</span>
<div class="joystick-btn" id="joystickBtn"></div>
</div>
<!-- 舵机角度显示 -->
<div class="angle-display">
<div class="angle-item">S1舵机(X轴):<span id="s1Angle">90</span>°</div>
<div class="angle-item">S2舵机(Y轴):<span id="s2Angle">90</span>°</div>
</div>
</div>
</div>
<!-- 日志框 -->
<div class="d-flex gap-2 my-3">
<div class="small w-100" id="nipple-log"></div>
</div>
</div>
<script>
// ===================== 全局配置 =====================
const AJAX_SEND_INTERVAL = 100; // AJAX节流间隔(ms)
const angleUpdateInterval = 500; // 角度显示节流间隔(ms)
let isDragging = false;
let lastSendTime = 0;
let lastAngleUpdateTime = 0;
let hasSetTrackOff = false; // 新增:标记是否已设置跟踪为关闭,避免重复触发
// ===================== DOM元素缓存 =====================
const $deviceId = $('#device_id');
const $followFlag = $('#follow_flag');
const $resetBtn = $('#reset_btn');
const $headVideo = $('#head_video');
const $joystickContainer = $('#joystickContainer');
const $joystickBtn = $('#joystickBtn');
const $s1Angle = $('#s1Angle');
const $s2Angle = $('#s2Angle');
const $nippleLog = $('#nipple-log');
// 摇杆按钮尺寸缓存
const btnHalfWidth = $joystickBtn.width() / 2;
const btnHalfHeight = $joystickBtn.height() / 2;
// ===================== AJAX核心函数(所有交互) =====================
function sendActionTxt(device_id, action_text) {
const now = Date.now();
if (now - lastSendTime < AJAX_SEND_INTERVAL) return false;
lastSendTime = now;
$.post("/wrobot/api/SendActionTxt", {
device_id: device_id,
action_text: action_text,
ignoreMe: new Date().getTime()
}, function (data, textstatus) {
if (data['ret'] != 0) {
return false;
}
addLogToNippleLog(data);
})
}
function sendVoiceTxt(device_id, voice_text) {
$.post("/wrobot/api/SendVoiceTxt", {
device_id: device_id,
voice_text: voice_text,
ignoreMe: new Date().getTime()
}, function (data, textstatus) {
if (data['ret'] != 0) {
return false;
}
addLogToNippleLog(data);
})
}
function setFollowFlag(device_id, follow_flag) {
$.post("/wrobot/api/SetFollowFlag", {
device_id: device_id,
follow_flag: follow_flag,
ignoreMe: new Date().getTime()
}, function (data, textstatus) {
if (data['ret'] != 0) {
return false;
}
addLogToNippleLog(data);
})
}
// ===================== 摇杆控制逻辑 =====================
function resetJoystick() {
// 复位摇杆位置
$joystickBtn.removeClass('dragging').css('transform', 'translate(-50%, -50%)');
const resetAngle = 90;
$s1Angle.text(resetAngle);
$s2Angle.text(resetAngle);
// 发送复位指令
const deviceId = $deviceId.val();
sendActionTxt(deviceId, `S1:${resetAngle},S2:${resetAngle};`);
setFollowFlag(deviceId, 'none');
addLogToNippleLog({ type: 'info', msg: '摇杆复位,舵机角度重置为90°' });
}
// 摇杆按下
$joystickBtn.on('mousedown touchstart', function(e) {
e.preventDefault();
isDragging = true;
$joystickBtn.addClass('dragging');
});
// 摇杆移动
$joystickContainer.on('mousemove touchmove', function(e) {
if (!isDragging) return;
// 适配触摸/鼠标事件
const event = e.type.includes('touch') ? e.touches[0] : e;
const clientX = event.clientX;
const clientY = event.clientY;
// 计算容器位置和偏移
const containerRect = $joystickContainer[0].getBoundingClientRect();
const containerWidth = containerRect.width;
const containerHeight = containerRect.height;
const containerHalfWidth = containerWidth / 2;
const containerHalfHeight = containerHeight / 2;
const offsetX = clientX - (containerRect.left + containerHalfWidth);
const offsetY = clientY - (containerRect.top + containerHalfHeight);
// 边界限制
const maxOffsetX = containerHalfWidth - btnHalfWidth;
const maxOffsetY = containerHalfHeight - btnHalfHeight;
const clampedOffsetX = Math.max(-maxOffsetX, Math.min(offsetX, maxOffsetX));
const clampedOffsetY = Math.max(-maxOffsetY, Math.min(offsetY, maxOffsetY));
// 更新摇杆位置
$joystickBtn.css('transform', `translate(calc(-50% + ${clampedOffsetX}px), calc(-50% + ${clampedOffsetY}px))`);
// 计算舵机角度
const s1Angle = Math.round(180 - ((clampedOffsetX + maxOffsetX) / (2 * maxOffsetX)) * 180);
const s2Angle = Math.round(180 - ((clampedOffsetY + maxOffsetY) / (2 * maxOffsetY)) * 180);
// 新增核心逻辑:拖动摇杆时自动切换跟踪模式为关闭(track_off)
if (!hasSetTrackOff) {
const trackOffVal = 'track_off'; // 请确认该值与follow_list中「关闭跟踪」对应的value一致
// 如果当前不是关闭跟踪模式,则切换并触发change事件
if ($followFlag.val() !== trackOffVal) {
$followFlag.val(trackOffVal).trigger('change');
addLogToNippleLog({ type: 'info', msg: '拖动摇杆,自动关闭跟踪模式' });
}
hasSetTrackOff = true; // 标记已设置,避免重复触发
}
// 节流更新角度和发送指令
const now = Date.now();
if (now - lastAngleUpdateTime > angleUpdateInterval) {
lastAngleUpdateTime = now;
$s1Angle.text(s1Angle);
$s2Angle.text(s2Angle);
sendActionTxt($deviceId.val(), `S1:${s1Angle},S2:${s2Angle};`);
}
});
// 摇杆松开/离开
$joystickContainer.on('mouseup mouseleave touchend touchcancel', function() {
if (isDragging) {
isDragging = false;
$joystickBtn.removeClass('dragging');
hasSetTrackOff = false; // 重置标记,下次拖动可再次触发
// 如需松开复位,取消注释下面这行
// resetJoystick();
}
});
// ===================== 日志辅助函数 =====================
function addLogToNippleLog(logData) {
const timeStr = new Date().toLocaleTimeString();
const logHtml = `
<div class="log-item ${logData.type}">
<small>[${timeStr}]</small> ${logData.msg}
${logData.data ? '<br>' + JSON.stringify(logData.data, null, 2) : ''}
</div>
`;
$nippleLog.prepend(logHtml);
// 最多保留20条日志,避免性能问题
if ($nippleLog.children().length > 20) {
$nippleLog.children().last().remove();
}
$nippleLog.scrollTop(0);
}
// ===================== 事件绑定 =====================
$(function() {
// 复位按钮
$resetBtn.on('click', resetJoystick);
// 跟踪模式变更
$followFlag.on('change', function() {
setFollowFlag($deviceId.val(), $(this).val());
});
// 设备ID变更时更新图像流地址
$deviceId.on('change', function() {
const deviceId = $(this).val();
$headVideo.attr('src', `/wrobot_cam/${deviceId}`);
});
});
</script>
</body>
</html>