274 lines
12 KiB
HTML
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> |