858 lines
36 KiB
Python
858 lines
36 KiB
Python
![]() |
#!/usr/bin/env python3
|
|||
|
import json
|
|||
|
import socket
|
|||
|
import threading
|
|||
|
import time
|
|||
|
import traceback
|
|||
|
from datetime import datetime
|
|||
|
import argparse
|
|||
|
from flask import Flask, render_template_string, jsonify, render_template
|
|||
|
|
|||
|
class CommaWebListener:
|
|||
|
def __init__(self, port=8088):
|
|||
|
"""初始化接收器"""
|
|||
|
self.port = port
|
|||
|
self.data = {}
|
|||
|
self.last_update = 0
|
|||
|
self.running = True
|
|||
|
self.device_ip = None
|
|||
|
|
|||
|
def start_listening(self):
|
|||
|
"""启动UDP监听"""
|
|||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|||
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|||
|
try:
|
|||
|
sock.bind(('0.0.0.0', self.port))
|
|||
|
print(f"正在监听端口 {self.port} 的广播数据...")
|
|||
|
|
|||
|
while self.running:
|
|||
|
try:
|
|||
|
data, addr = sock.recvfrom(4096)
|
|||
|
self.device_ip = addr[0]
|
|||
|
try:
|
|||
|
self.data = json.loads(data.decode('utf-8'))
|
|||
|
self.last_update = time.time()
|
|||
|
except json.JSONDecodeError:
|
|||
|
print(f"接收到无效的JSON数据: {data[:100]}...")
|
|||
|
except Exception as e:
|
|||
|
print(f"接收数据时出错: {e}")
|
|||
|
except Exception as e:
|
|||
|
print(f"无法绑定到端口 {self.port}: {e}")
|
|||
|
finally:
|
|||
|
sock.close()
|
|||
|
|
|||
|
# HTML模板
|
|||
|
HTML_TEMPLATE = """
|
|||
|
<!DOCTYPE html>
|
|||
|
<html lang="zh-CN">
|
|||
|
<head>
|
|||
|
<meta charset="UTF-8">
|
|||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|||
|
<title>CommaAssist 数据监视器</title>
|
|||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
|||
|
<style>
|
|||
|
body {
|
|||
|
font-family: Arial, sans-serif;
|
|||
|
margin: 20px;
|
|||
|
background-color: #f5f5f5;
|
|||
|
color: #333;
|
|||
|
}
|
|||
|
.container {
|
|||
|
max-width: 1200px;
|
|||
|
margin: 0 auto;
|
|||
|
}
|
|||
|
.header {
|
|||
|
text-align: center;
|
|||
|
margin-bottom: 20px;
|
|||
|
}
|
|||
|
.status {
|
|||
|
text-align: center;
|
|||
|
padding: 10px;
|
|||
|
margin-bottom: 20px;
|
|||
|
border-radius: 5px;
|
|||
|
}
|
|||
|
.waiting {
|
|||
|
background-color: #fff3cd;
|
|||
|
color: #856404;
|
|||
|
}
|
|||
|
.connected {
|
|||
|
background-color: #d4edda;
|
|||
|
color: #155724;
|
|||
|
}
|
|||
|
.expired {
|
|||
|
background-color: #f8d7da;
|
|||
|
color: #721c24;
|
|||
|
}
|
|||
|
.card {
|
|||
|
background-color: white;
|
|||
|
border-radius: 8px;
|
|||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|||
|
margin-bottom: 20px;
|
|||
|
}
|
|||
|
.card-header {
|
|||
|
font-weight: bold;
|
|||
|
font-size: 18px;
|
|||
|
border-bottom: 1px solid #eee;
|
|||
|
padding: 15px;
|
|||
|
background-color: #f8f9fa;
|
|||
|
}
|
|||
|
.card-body {
|
|||
|
padding: 15px;
|
|||
|
}
|
|||
|
.row {
|
|||
|
margin-bottom: 10px;
|
|||
|
}
|
|||
|
.col-6 strong {
|
|||
|
color: #666;
|
|||
|
}
|
|||
|
.badge {
|
|||
|
font-size: 100%;
|
|||
|
}
|
|||
|
.bg-primary {
|
|||
|
background-color: #007bff;
|
|||
|
}
|
|||
|
.bg-success {
|
|||
|
background-color: #28a745;
|
|||
|
}
|
|||
|
.bg-danger {
|
|||
|
background-color: #dc3545;
|
|||
|
}
|
|||
|
.bg-warning {
|
|||
|
background-color: #ffc107;
|
|||
|
color: #212529;
|
|||
|
}
|
|||
|
.bg-info {
|
|||
|
background-color: #17a2b8;
|
|||
|
}
|
|||
|
.nav-tabs {
|
|||
|
margin-bottom: 20px;
|
|||
|
}
|
|||
|
.tab-content {
|
|||
|
padding-top: 20px;
|
|||
|
}
|
|||
|
.map-container {
|
|||
|
height: 300px;
|
|||
|
width: 100%;
|
|||
|
background-color: #eee;
|
|||
|
margin-top: 10px;
|
|||
|
border-radius: 5px;
|
|||
|
}
|
|||
|
.controls {
|
|||
|
margin-bottom: 20px;
|
|||
|
}
|
|||
|
.auto-refresh {
|
|||
|
display: inline-block;
|
|||
|
margin-right: 15px;
|
|||
|
}
|
|||
|
pre {
|
|||
|
background-color: #f5f5f5;
|
|||
|
padding: 15px;
|
|||
|
border-radius: 5px;
|
|||
|
white-space: pre-wrap;
|
|||
|
word-break: break-all;
|
|||
|
}
|
|||
|
</style>
|
|||
|
</head>
|
|||
|
<body>
|
|||
|
<div class="container">
|
|||
|
<div class="header">
|
|||
|
<h1>CommaAssist 数据监视器</h1>
|
|||
|
</div>
|
|||
|
|
|||
|
<div id="status-container" class="status waiting">
|
|||
|
等待来自comma3的数据...
|
|||
|
</div>
|
|||
|
|
|||
|
<div class="controls">
|
|||
|
<label class="auto-refresh">
|
|||
|
<input type="checkbox" id="auto-refresh" checked> 自动刷新 (1秒)
|
|||
|
</label>
|
|||
|
<button class="btn btn-primary float-end" onclick="fetchData()">刷新数据</button>
|
|||
|
<div style="clear: both;"></div>
|
|||
|
</div>
|
|||
|
|
|||
|
<ul class="nav nav-tabs" id="myTab" role="tablist">
|
|||
|
<li class="nav-item" role="presentation">
|
|||
|
<button class="nav-link active" id="car-tab" data-bs-toggle="tab" data-bs-target="#car-tab-pane" type="button" role="tab">
|
|||
|
车辆信息
|
|||
|
</button>
|
|||
|
</li>
|
|||
|
<li class="nav-item" role="presentation">
|
|||
|
<button class="nav-link" id="device-tab" data-bs-toggle="tab" data-bs-target="#device-tab-pane" type="button" role="tab">
|
|||
|
设备信息
|
|||
|
</button>
|
|||
|
</li>
|
|||
|
<li class="nav-item" role="presentation">
|
|||
|
<button class="nav-link" id="location-tab" data-bs-toggle="tab" data-bs-target="#location-tab-pane" type="button" role="tab">
|
|||
|
位置信息
|
|||
|
</button>
|
|||
|
</li>
|
|||
|
<li class="nav-item" role="presentation">
|
|||
|
<button class="nav-link" id="json-tab" data-bs-toggle="tab" data-bs-target="#json-tab-pane" type="button" role="tab">
|
|||
|
原始数据
|
|||
|
</button>
|
|||
|
</li>
|
|||
|
</ul>
|
|||
|
|
|||
|
<div class="tab-content" id="myTabContent">
|
|||
|
<!-- 车辆信息标签页 -->
|
|||
|
<div class="tab-pane fade show active" id="car-tab-pane" role="tabpanel" aria-labelledby="car-tab" tabindex="0">
|
|||
|
<div class="row">
|
|||
|
<div class="col-md-6">
|
|||
|
<div class="card">
|
|||
|
<div class="card-header">基本车辆信息</div>
|
|||
|
<div class="card-body">
|
|||
|
<div id="vehicle-status-container">
|
|||
|
<div class="alert alert-warning">等待车辆数据...</div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
|
|||
|
<div class="card">
|
|||
|
<div class="card-header">方向盘与控制系统</div>
|
|||
|
<div class="card-body">
|
|||
|
<div id="steering-system-container">
|
|||
|
<div class="alert alert-warning">等待车辆数据...</div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
|
|||
|
<div class="card">
|
|||
|
<div class="card-header">详细车辆信息</div>
|
|||
|
<div class="card-body">
|
|||
|
<div id="detailed-vehicle-info-container">
|
|||
|
<div class="alert alert-warning">等待车辆数据...</div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
|
|||
|
<div class="col-md-6">
|
|||
|
<div class="card">
|
|||
|
<div class="card-header">踏板与制动系统</div>
|
|||
|
<div class="card-body">
|
|||
|
<div id="pedal-status-container">
|
|||
|
<div class="alert alert-warning">等待车辆数据...</div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
|
|||
|
<div class="card">
|
|||
|
<div class="card-header">车门与信号灯</div>
|
|||
|
<div class="card-body">
|
|||
|
<div id="door-lights-container">
|
|||
|
<div class="alert alert-warning">等待车辆数据...</div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
|
|||
|
<div class="card">
|
|||
|
<div class="card-header">巡航控制</div>
|
|||
|
<div class="card-body">
|
|||
|
<div id="cruise-info-container">
|
|||
|
<div class="alert alert-warning">等待车辆数据...</div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
|
|||
|
<!-- 设备信息标签页 -->
|
|||
|
<div class="tab-pane fade" id="device-tab-pane" role="tabpanel" aria-labelledby="device-tab" tabindex="0">
|
|||
|
<div class="card">
|
|||
|
<div class="card-header">设备状态</div>
|
|||
|
<div class="card-body">
|
|||
|
<div id="device-info-container">
|
|||
|
<div class="alert alert-warning">等待设备数据...</div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
|
|||
|
<div class="card">
|
|||
|
<div class="card-header">系统资源</div>
|
|||
|
<div class="card-body">
|
|||
|
<div id="system-resources-container">
|
|||
|
<div class="alert alert-warning">等待设备数据...</div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
|
|||
|
<!-- 位置信息标签页 -->
|
|||
|
<div class="tab-pane fade" id="location-tab-pane" role="tabpanel" aria-labelledby="location-tab" tabindex="0">
|
|||
|
<div class="row">
|
|||
|
<div class="col-md-6">
|
|||
|
<div class="card">
|
|||
|
<div class="card-header">GPS位置</div>
|
|||
|
<div class="card-body">
|
|||
|
<div id="gps-info-container">
|
|||
|
<div class="alert alert-warning">等待GPS数据...</div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
|
|||
|
<div class="card">
|
|||
|
<div class="card-header">导航信息</div>
|
|||
|
<div class="card-body">
|
|||
|
<div id="navigation-container">
|
|||
|
<div class="alert alert-warning">等待导航数据...</div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
|
|||
|
<div class="col-md-6">
|
|||
|
<div class="card">
|
|||
|
<div class="card-header">地图</div>
|
|||
|
<div class="card-body">
|
|||
|
<div id="map" class="map-container"></div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
|
|||
|
<!-- 原始数据标签页 -->
|
|||
|
<div class="tab-pane fade" id="json-tab-pane" role="tabpanel" aria-labelledby="json-tab" tabindex="0">
|
|||
|
<div class="card">
|
|||
|
<div class="card-header">原始JSON数据</div>
|
|||
|
<div class="card-body">
|
|||
|
<pre id="raw-data">等待数据...</pre>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
|
|||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
|||
|
<script>
|
|||
|
let map, marker;
|
|||
|
let lastValidLatLng = null;
|
|||
|
let activeTab = 'car'; // 默认显示车辆信息标签
|
|||
|
|
|||
|
// 根据车辆状态自动切换标签
|
|||
|
function autoSwitchTabs(isCarActive) {
|
|||
|
// 如果车辆启动,切换到车辆标签;否则切换到设备标签
|
|||
|
if (isCarActive && activeTab !== 'car') {
|
|||
|
document.getElementById('car-tab').click();
|
|||
|
activeTab = 'car';
|
|||
|
} else if (!isCarActive && activeTab === 'car') {
|
|||
|
document.getElementById('device-tab').click();
|
|||
|
activeTab = 'device';
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function initMap() {
|
|||
|
if (typeof google !== 'undefined') {
|
|||
|
const defaultPos = {lat: 39.9042, lng: 116.4074}; // 默认位置:北京
|
|||
|
map = new google.maps.Map(document.getElementById('map'), {
|
|||
|
zoom: 16,
|
|||
|
center: defaultPos,
|
|||
|
mapTypeId: 'roadmap'
|
|||
|
});
|
|||
|
|
|||
|
marker = new google.maps.Marker({
|
|||
|
position: defaultPos,
|
|||
|
map: map,
|
|||
|
title: 'Comma3位置'
|
|||
|
});
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function updateMap(lat, lng, bearing) {
|
|||
|
if (typeof google !== 'undefined' && map && marker) {
|
|||
|
const latLng = new google.maps.LatLng(lat, lng);
|
|||
|
|
|||
|
// 更新标记位置
|
|||
|
marker.setPosition(latLng);
|
|||
|
|
|||
|
// 根据方向旋转标记
|
|||
|
if (bearing !== undefined) {
|
|||
|
// 如果我们有自定义的带方向的标记图标,可以在这里设置
|
|||
|
}
|
|||
|
|
|||
|
// 平滑移动地图中心
|
|||
|
map.panTo(latLng);
|
|||
|
|
|||
|
lastValidLatLng = {lat, lng};
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
function formatDataRow(label, value, badgeClass = null) {
|
|||
|
let valueHtml = value;
|
|||
|
if (badgeClass) {
|
|||
|
valueHtml = `<span class="badge ${badgeClass}">${value}</span>`;
|
|||
|
}
|
|||
|
|
|||
|
return `
|
|||
|
<div class="row">
|
|||
|
<div class="col-6">
|
|||
|
<strong>${label}</strong>
|
|||
|
</div>
|
|||
|
<div class="col-6">
|
|||
|
${valueHtml}
|
|||
|
</div>
|
|||
|
</div>
|
|||
|
`;
|
|||
|
}
|
|||
|
|
|||
|
function updateVehicleStatus(car) {
|
|||
|
const speed = car.speed || 0;
|
|||
|
const isCarMoving = speed > 1.0;
|
|||
|
|
|||
|
let html = '';
|
|||
|
|
|||
|
html += formatDataRow('运行状态', isCarMoving ? '行驶中' : '静止', isCarMoving ? 'bg-success' : 'bg-secondary');
|
|||
|
html += formatDataRow('当前速度', `${speed.toFixed(1)} km/h`, 'bg-primary');
|
|||
|
html += formatDataRow('档位', car.gear_shifter || 'Unknown');
|
|||
|
|
|||
|
document.getElementById('vehicle-status-container').innerHTML = html;
|
|||
|
|
|||
|
return isCarMoving;
|
|||
|
}
|
|||
|
|
|||
|
function updateSteeringSystem(car) {
|
|||
|
let html = '';
|
|||
|
|
|||
|
html += formatDataRow('方向盘角度', `${(car.steering_angle || 0).toFixed(1)}°`);
|
|||
|
html += formatDataRow('转向力矩', `${(car.steering_torque || 0).toFixed(1)} Nm`);
|
|||
|
|
|||
|
let blinkerStatus = '';
|
|||
|
if (car.left_blinker && car.right_blinker) blinkerStatus = '双闪';
|
|||
|
else if (car.left_blinker) blinkerStatus = '左转';
|
|||
|
else if (car.right_blinker) blinkerStatus = '右转';
|
|||
|
else blinkerStatus = '关闭';
|
|||
|
|
|||
|
html += formatDataRow('转向灯', blinkerStatus);
|
|||
|
|
|||
|
document.getElementById('steering-system-container').innerHTML = html;
|
|||
|
}
|
|||
|
|
|||
|
function updatePedalStatus(car) {
|
|||
|
let html = '';
|
|||
|
|
|||
|
const brakeStatus = car.brake_pressed ? '已踩下' : '释放';
|
|||
|
const gasStatus = car.gas_pressed ? '已踩下' : '释放';
|
|||
|
|
|||
|
html += formatDataRow('制动踏板', brakeStatus, car.brake_pressed ? 'bg-danger' : 'bg-secondary');
|
|||
|
html += formatDataRow('油门踏板', gasStatus, car.gas_pressed ? 'bg-success' : 'bg-secondary');
|
|||
|
|
|||
|
document.getElementById('pedal-status-container').innerHTML = html;
|
|||
|
}
|
|||
|
|
|||
|
function updateDoorLights(car) {
|
|||
|
let html = '';
|
|||
|
|
|||
|
const doorStatus = car.door_open ? '打开' : '关闭';
|
|||
|
html += formatDataRow('车门状态', doorStatus, car.door_open ? 'bg-danger' : 'bg-success');
|
|||
|
|
|||
|
html += formatDataRow('左转向灯', car.left_blinker ? '开启' : '关闭', car.left_blinker ? 'bg-warning' : 'bg-secondary');
|
|||
|
html += formatDataRow('右转向灯', car.right_blinker ? '开启' : '关闭', car.right_blinker ? 'bg-warning' : 'bg-secondary');
|
|||
|
|
|||
|
document.getElementById('door-lights-container').innerHTML = html;
|
|||
|
}
|
|||
|
|
|||
|
function updateCruiseInfo(car) {
|
|||
|
let html = '';
|
|||
|
|
|||
|
html += formatDataRow('巡航速度', `${(car.cruise_speed || 0).toFixed(1)} km/h`, 'bg-info');
|
|||
|
|
|||
|
document.getElementById('cruise-info-container').innerHTML = html;
|
|||
|
}
|
|||
|
|
|||
|
function updateDeviceInfo(device) {
|
|||
|
let html = '';
|
|||
|
|
|||
|
html += formatDataRow('设备IP', device.ip || 'Unknown');
|
|||
|
|
|||
|
const battery = device.battery || {};
|
|||
|
const batPercent = battery.percent || 0;
|
|||
|
let batClass = 'bg-danger';
|
|||
|
if (batPercent > 50) batClass = 'bg-success';
|
|||
|
else if (batPercent > 20) batClass = 'bg-warning';
|
|||
|
|
|||
|
html += formatDataRow('电池电量', `${batPercent}%`, batClass);
|
|||
|
html += formatDataRow('电池电压', `${(battery.voltage || 0).toFixed(2)} V`);
|
|||
|
html += formatDataRow('电池电流', `${(battery.status || 0).toFixed(2)} A`);
|
|||
|
|
|||
|
document.getElementById('device-info-container').innerHTML = html;
|
|||
|
}
|
|||
|
|
|||
|
function updateSystemResources(device) {
|
|||
|
let html = '';
|
|||
|
|
|||
|
let memClass = 'bg-success';
|
|||
|
if (device.mem_usage > 80) memClass = 'bg-danger';
|
|||
|
else if (device.mem_usage > 60) memClass = 'bg-warning';
|
|||
|
|
|||
|
html += formatDataRow('内存使用', `${(device.mem_usage || 0).toFixed(1)}%`, memClass);
|
|||
|
html += formatDataRow('CPU温度', `${(device.cpu_temp || 0).toFixed(1)}°C`);
|
|||
|
html += formatDataRow('存储空间', `剩余 ${(device.free_space || 0).toFixed(1)}%`);
|
|||
|
|
|||
|
document.getElementById('system-resources-container').innerHTML = html;
|
|||
|
}
|
|||
|
|
|||
|
function updateGpsInfo(location) {
|
|||
|
if (!location.gps_valid) {
|
|||
|
document.getElementById('gps-info-container').innerHTML =
|
|||
|
'<div class="alert alert-warning">GPS信号无效或未获取</div>';
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
let html = '';
|
|||
|
|
|||
|
html += formatDataRow('纬度', location.latitude.toFixed(6));
|
|||
|
html += formatDataRow('经度', location.longitude.toFixed(6));
|
|||
|
html += formatDataRow('方向', `${location.bearing.toFixed(1)}°`);
|
|||
|
html += formatDataRow('海拔', `${location.altitude.toFixed(1)} m`);
|
|||
|
html += formatDataRow('GPS精度', `${location.accuracy.toFixed(1)} m`);
|
|||
|
html += formatDataRow('GPS速度', `${location.speed.toFixed(1)} km/h`, 'bg-primary');
|
|||
|
|
|||
|
document.getElementById('gps-info-container').innerHTML = html;
|
|||
|
|
|||
|
// 更新地图
|
|||
|
updateMap(location.latitude, location.longitude, location.bearing);
|
|||
|
}
|
|||
|
|
|||
|
function updateNavigation(nav) {
|
|||
|
if (!nav || Object.keys(nav).length === 0) {
|
|||
|
document.getElementById('navigation-container').innerHTML =
|
|||
|
'<div class="alert alert-info">没有活动的导航</div>';
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
let html = '';
|
|||
|
|
|||
|
// 格式化距离显示
|
|||
|
const distRemaining = nav.distance_remaining || 0;
|
|||
|
let distText = '';
|
|||
|
if (distRemaining > 1000) {
|
|||
|
distText = `${(distRemaining / 1000).toFixed(1)} km`;
|
|||
|
} else {
|
|||
|
distText = `${Math.round(distRemaining)} m`;
|
|||
|
}
|
|||
|
|
|||
|
// 格式化时间显示
|
|||
|
const timeRemaining = nav.time_remaining || 0;
|
|||
|
const minutes = Math.floor(timeRemaining / 60);
|
|||
|
const seconds = timeRemaining % 60;
|
|||
|
|
|||
|
html += formatDataRow('剩余距离', distText, 'bg-info');
|
|||
|
html += formatDataRow('剩余时间', `${minutes}分${seconds}秒`, 'bg-info');
|
|||
|
html += formatDataRow('道路限速', `${(nav.speed_limit || 0).toFixed(1)} km/h`, 'bg-danger');
|
|||
|
|
|||
|
if (nav.maneuver_distance > 0) {
|
|||
|
html += formatDataRow('下一动作', nav.maneuver_text, 'bg-warning');
|
|||
|
html += formatDataRow('动作距离', `${nav.maneuver_distance} m`);
|
|||
|
}
|
|||
|
|
|||
|
document.getElementById('navigation-container').innerHTML = html;
|
|||
|
}
|
|||
|
|
|||
|
function updateDetailedVehicleInfo(carInfo) {
|
|||
|
if (!carInfo || !carInfo.details) {
|
|||
|
document.getElementById('detailed-vehicle-info-container').innerHTML =
|
|||
|
'<div class="alert alert-info">没有详细车辆信息</div>';
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
let html = '';
|
|||
|
|
|||
|
// 基本信息
|
|||
|
if (carInfo.basic) {
|
|||
|
html += '<h5>基本信息</h5>';
|
|||
|
html += '<div class="mb-3">';
|
|||
|
html += formatDataRow('车型', carInfo.basic.car_model);
|
|||
|
html += formatDataRow('车辆指纹', carInfo.basic.fingerprint);
|
|||
|
html += formatDataRow('车重', carInfo.basic.weight);
|
|||
|
html += formatDataRow('轴距', carInfo.basic.wheelbase);
|
|||
|
html += formatDataRow('转向比', carInfo.basic.steering_ratio);
|
|||
|
html += '</div>';
|
|||
|
}
|
|||
|
|
|||
|
// 巡航信息
|
|||
|
if (carInfo.details.cruise) {
|
|||
|
const cruise = carInfo.details.cruise;
|
|||
|
html += '<h5>巡航控制</h5>';
|
|||
|
html += '<div class="mb-3">';
|
|||
|
html += formatDataRow('巡航状态', cruise.enabled ? '开启' : '关闭', cruise.enabled ? 'bg-success' : 'bg-secondary');
|
|||
|
html += formatDataRow('自适应巡航', cruise.available ? '可用' : '不可用');
|
|||
|
html += formatDataRow('设定速度', `${(cruise.speed || 0).toFixed(1)} km/h`, 'bg-info');
|
|||
|
if (cruise.gap !== undefined) {
|
|||
|
html += formatDataRow('跟车距离', cruise.gap);
|
|||
|
}
|
|||
|
html += '</div>';
|
|||
|
}
|
|||
|
|
|||
|
// 车轮速度
|
|||
|
if (carInfo.details.wheel_speeds) {
|
|||
|
const ws = carInfo.details.wheel_speeds;
|
|||
|
html += '<h5>车轮速度</h5>';
|
|||
|
html += '<div class="mb-3">';
|
|||
|
html += formatDataRow('左前', `${(ws.fl || 0).toFixed(1)} km/h`);
|
|||
|
html += formatDataRow('右前', `${(ws.fr || 0).toFixed(1)} km/h`);
|
|||
|
html += formatDataRow('左后', `${(ws.rl || 0).toFixed(1)} km/h`);
|
|||
|
html += formatDataRow('右后', `${(ws.rr || 0).toFixed(1)} km/h`);
|
|||
|
html += '</div>';
|
|||
|
}
|
|||
|
|
|||
|
// 方向盘信息
|
|||
|
if (carInfo.details.steering) {
|
|||
|
const steering = carInfo.details.steering;
|
|||
|
html += '<h5>方向盘系统</h5>';
|
|||
|
html += '<div class="mb-3">';
|
|||
|
html += formatDataRow('方向盘角度', `${(steering.angle || 0).toFixed(1)}°`);
|
|||
|
html += formatDataRow('方向盘力矩', `${(steering.torque || 0).toFixed(1)} Nm`);
|
|||
|
if (steering.rate !== undefined) {
|
|||
|
html += formatDataRow('转向速率', `${steering.rate.toFixed(1)}°/s`);
|
|||
|
}
|
|||
|
html += '</div>';
|
|||
|
}
|
|||
|
|
|||
|
// 安全系统
|
|||
|
if (carInfo.details.safety_systems && Object.keys(carInfo.details.safety_systems).length > 0) {
|
|||
|
const safety = carInfo.details.safety_systems;
|
|||
|
html += '<h5>安全系统</h5>';
|
|||
|
html += '<div class="mb-3">';
|
|||
|
if (safety.esp_disabled !== undefined) {
|
|||
|
html += formatDataRow('ESP状态', safety.esp_disabled ? '禁用' : '正常', safety.esp_disabled ? 'bg-warning' : 'bg-success');
|
|||
|
}
|
|||
|
if (safety.abs_active !== undefined) {
|
|||
|
html += formatDataRow('ABS状态', safety.abs_active ? '激活' : '正常', safety.abs_active ? 'bg-warning' : 'bg-success');
|
|||
|
}
|
|||
|
if (safety.tcs_active !== undefined) {
|
|||
|
html += formatDataRow('牵引力控制', safety.tcs_active ? '激活' : '正常', safety.tcs_active ? 'bg-warning' : 'bg-success');
|
|||
|
}
|
|||
|
if (safety.collision_warning !== undefined) {
|
|||
|
html += formatDataRow('碰撞警告', safety.collision_warning ? '警告' : '正常', safety.collision_warning ? 'bg-danger' : 'bg-success');
|
|||
|
}
|
|||
|
html += '</div>';
|
|||
|
}
|
|||
|
|
|||
|
// 车门信息
|
|||
|
if (carInfo.details.doors && Object.keys(carInfo.details.doors).length > 0) {
|
|||
|
const doors = carInfo.details.doors;
|
|||
|
html += '<h5>车门状态</h5>';
|
|||
|
html += '<div class="mb-3">';
|
|||
|
html += formatDataRow('驾驶员门', doors.driver ? '打开' : '关闭', doors.driver ? 'bg-danger' : 'bg-success');
|
|||
|
if (doors.passenger !== undefined) {
|
|||
|
html += formatDataRow('乘客门', doors.passenger ? '打开' : '关闭', doors.passenger ? 'bg-danger' : 'bg-success');
|
|||
|
}
|
|||
|
if (doors.trunk !== undefined) {
|
|||
|
html += formatDataRow('行李箱', doors.trunk ? '打开' : '关闭', doors.trunk ? 'bg-danger' : 'bg-success');
|
|||
|
}
|
|||
|
if (doors.hood !== undefined) {
|
|||
|
html += formatDataRow('引擎盖', doors.hood ? '打开' : '关闭', doors.hood ? 'bg-danger' : 'bg-success');
|
|||
|
}
|
|||
|
if (carInfo.status && carInfo.status.seatbelt_unlatched !== undefined) {
|
|||
|
html += formatDataRow('安全带', carInfo.status.seatbelt_unlatched ? '未系' : '已系', carInfo.status.seatbelt_unlatched ? 'bg-danger' : 'bg-success');
|
|||
|
}
|
|||
|
html += '</div>';
|
|||
|
}
|
|||
|
|
|||
|
// 灯光状态
|
|||
|
if (carInfo.details.lights && Object.keys(carInfo.details.lights).length > 0) {
|
|||
|
const lights = carInfo.details.lights;
|
|||
|
html += '<h5>灯光状态</h5>';
|
|||
|
html += '<div class="mb-3">';
|
|||
|
html += formatDataRow('左转向灯', lights.left_blinker ? '开启' : '关闭', lights.left_blinker ? 'bg-warning' : 'bg-secondary');
|
|||
|
html += formatDataRow('右转向灯', lights.right_blinker ? '开启' : '关闭', lights.right_blinker ? 'bg-warning' : 'bg-secondary');
|
|||
|
if (lights.high_beam !== undefined) {
|
|||
|
html += formatDataRow('远光灯', lights.high_beam ? '开启' : '关闭', lights.high_beam ? 'bg-info' : 'bg-secondary');
|
|||
|
}
|
|||
|
if (lights.low_beam !== undefined) {
|
|||
|
html += formatDataRow('近光灯', lights.low_beam ? '开启' : '关闭', lights.low_beam ? 'bg-info' : 'bg-secondary');
|
|||
|
}
|
|||
|
html += '</div>';
|
|||
|
}
|
|||
|
|
|||
|
// 盲点监测
|
|||
|
if (carInfo.details.blind_spot && Object.keys(carInfo.details.blind_spot).length > 0) {
|
|||
|
const bs = carInfo.details.blind_spot;
|
|||
|
html += '<h5>盲点监测</h5>';
|
|||
|
html += '<div class="mb-3">';
|
|||
|
if (bs.left !== undefined) {
|
|||
|
html += formatDataRow('左侧', bs.left ? '检测到车辆' : '无车辆', bs.left ? 'bg-warning' : 'bg-success');
|
|||
|
}
|
|||
|
if (bs.right !== undefined) {
|
|||
|
html += formatDataRow('右侧', bs.right ? '检测到车辆' : '无车辆', bs.right ? 'bg-warning' : 'bg-success');
|
|||
|
}
|
|||
|
html += '</div>';
|
|||
|
}
|
|||
|
|
|||
|
// 其他信息
|
|||
|
if (carInfo.details.other && Object.keys(carInfo.details.other).length > 0) {
|
|||
|
const other = carInfo.details.other;
|
|||
|
html += '<h5>其他信息</h5>';
|
|||
|
html += '<div class="mb-3">';
|
|||
|
if (other.outside_temp !== undefined) {
|
|||
|
html += formatDataRow('车外温度', `${other.outside_temp.toFixed(1)}°C`);
|
|||
|
}
|
|||
|
if (other.fuel_range !== undefined) {
|
|||
|
html += formatDataRow('续航里程', `${other.fuel_range.toFixed(1)} km`);
|
|||
|
}
|
|||
|
if (other.odometer !== undefined) {
|
|||
|
html += formatDataRow('里程表', `${other.odometer.toFixed(1)} km`);
|
|||
|
}
|
|||
|
if (other.fuel_consumption !== undefined) {
|
|||
|
html += formatDataRow('油耗', `${other.fuel_consumption.toFixed(1)} L/100km`);
|
|||
|
}
|
|||
|
html += '</div>';
|
|||
|
}
|
|||
|
|
|||
|
// 如果没有任何详细信息
|
|||
|
if (html === '') {
|
|||
|
html = '<div class="alert alert-info">没有获取到详细车辆信息</div>';
|
|||
|
}
|
|||
|
|
|||
|
document.getElementById('detailed-vehicle-info-container').innerHTML = html;
|
|||
|
}
|
|||
|
|
|||
|
function fetchData() {
|
|||
|
fetch('/api/data')
|
|||
|
.then(response => response.json())
|
|||
|
.then(data => {
|
|||
|
const statusContainer = document.getElementById('status-container');
|
|||
|
|
|||
|
if (!data || !data.data || Object.keys(data.data).length === 0) {
|
|||
|
statusContainer.className = 'status waiting';
|
|||
|
statusContainer.textContent = '等待来自comma3的数据...';
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
const currentTime = new Date().getTime() / 1000;
|
|||
|
if (currentTime - data.last_update > 5) {
|
|||
|
statusContainer.className = 'status expired';
|
|||
|
statusContainer.textContent = `数据已过期! 上次更新: ${new Date(data.last_update * 1000).toLocaleTimeString()}`;
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
// 数据有效,更新UI
|
|||
|
statusContainer.className = 'status connected';
|
|||
|
statusContainer.textContent = `已连接到 ${data.device_ip || 'Comma3设备'} - 最后更新: ${new Date(data.last_update * 1000).toLocaleTimeString()}`;
|
|||
|
|
|||
|
// 获取数据
|
|||
|
const carData = data.data.car || {};
|
|||
|
const deviceData = data.data.device || {};
|
|||
|
const locationData = data.data.location || {};
|
|||
|
const navData = data.data.navigation || {};
|
|||
|
|
|||
|
// 先更新车辆状态,并获取车辆是否启动
|
|||
|
const isCarActive = updateVehicleStatus(carData);
|
|||
|
|
|||
|
// 根据车辆状态自动切换标签页
|
|||
|
autoSwitchTabs(isCarActive);
|
|||
|
|
|||
|
// 更新所有数据区域
|
|||
|
updateSteeringSystem(carData);
|
|||
|
updatePedalStatus(carData);
|
|||
|
updateDoorLights(carData);
|
|||
|
updateCruiseInfo(carData);
|
|||
|
updateDeviceInfo(deviceData);
|
|||
|
updateSystemResources(deviceData);
|
|||
|
updateGpsInfo(locationData);
|
|||
|
updateNavigation(navData);
|
|||
|
updateDetailedVehicleInfo(data.data.car_info);
|
|||
|
|
|||
|
// 更新原始数据
|
|||
|
document.getElementById('raw-data').textContent = JSON.stringify(data.data, null, 2);
|
|||
|
})
|
|||
|
.catch(error => {
|
|||
|
console.error('获取数据出错:', error);
|
|||
|
document.getElementById('status-container').className = 'status expired';
|
|||
|
document.getElementById('status-container').textContent = '连接错误: ' + error.message;
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
// 标签页切换处理
|
|||
|
document.addEventListener('DOMContentLoaded', function() {
|
|||
|
const tabEls = document.querySelectorAll('button[data-bs-toggle="tab"]');
|
|||
|
tabEls.forEach(tabEl => {
|
|||
|
tabEl.addEventListener('shown.bs.tab', function (event) {
|
|||
|
// 更新当前活动标签
|
|||
|
const id = event.target.id;
|
|||
|
if (id.includes('car')) activeTab = 'car';
|
|||
|
else if (id.includes('device')) activeTab = 'device';
|
|||
|
else if (id.includes('location')) activeTab = 'location';
|
|||
|
else activeTab = 'json';
|
|||
|
});
|
|||
|
});
|
|||
|
|
|||
|
fetchData();
|
|||
|
|
|||
|
// 设置自动刷新
|
|||
|
const autoRefreshCheckbox = document.getElementById('auto-refresh');
|
|||
|
let refreshInterval;
|
|||
|
|
|||
|
function setAutoRefresh() {
|
|||
|
if (autoRefreshCheckbox.checked) {
|
|||
|
refreshInterval = setInterval(fetchData, 1000);
|
|||
|
} else {
|
|||
|
clearInterval(refreshInterval);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
autoRefreshCheckbox.addEventListener('change', setAutoRefresh);
|
|||
|
setAutoRefresh();
|
|||
|
});
|
|||
|
</script>
|
|||
|
|
|||
|
<!-- 加载Google地图API (需要替换为您自己的API密钥) -->
|
|||
|
<script async defer
|
|||
|
src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=initMap">
|
|||
|
</script>
|
|||
|
</body>
|
|||
|
</html>
|
|||
|
"""
|
|||
|
|
|||
|
def create_app(listener):
|
|||
|
app = Flask(__name__)
|
|||
|
|
|||
|
@app.route('/')
|
|||
|
def index():
|
|||
|
"""主页"""
|
|||
|
return render_template_string(HTML_TEMPLATE)
|
|||
|
|
|||
|
@app.route('/api/data')
|
|||
|
def get_data():
|
|||
|
"""提供JSON数据API"""
|
|||
|
return jsonify({
|
|||
|
'data': listener.data,
|
|||
|
'last_update': listener.last_update,
|
|||
|
'device_ip': listener.device_ip
|
|||
|
})
|
|||
|
|
|||
|
return app
|
|||
|
|
|||
|
def main():
|
|||
|
"""主函数"""
|
|||
|
parser = argparse.ArgumentParser(description='CommaAssist Web监视器')
|
|||
|
parser.add_argument('-p', '--port', type=int, default=8088, help='监听comma设备的UDP端口(默认: 8088)')
|
|||
|
parser.add_argument('-w', '--web-port', type=int, default=5000, help='Web服务器端口(默认: 5000)')
|
|||
|
parser.add_argument('--host', default='0.0.0.0', help='Web服务器监听地址(默认: 0.0.0.0)')
|
|||
|
args = parser.parse_args()
|
|||
|
|
|||
|
# 创建监听器
|
|||
|
listener = CommaWebListener(port=args.port)
|
|||
|
|
|||
|
# 启动接收线程
|
|||
|
receiver_thread = threading.Thread(target=listener.start_listening)
|
|||
|
receiver_thread.daemon = True
|
|||
|
receiver_thread.start()
|
|||
|
|
|||
|
# 创建Flask应用
|
|||
|
app = create_app(listener)
|
|||
|
|
|||
|
# 启动Web服务器
|
|||
|
try:
|
|||
|
print(f"Web服务已启动,请访问 http://{args.host}:{args.web_port}/")
|
|||
|
app.run(host=args.host, port=args.web_port)
|
|||
|
except KeyboardInterrupt:
|
|||
|
print("\n退出...")
|
|||
|
finally:
|
|||
|
listener.running = False
|
|||
|
receiver_thread.join(timeout=1.0)
|
|||
|
|
|||
|
if __name__ == "__main__":
|
|||
|
main()
|