parent
e3d6f80710
commit
37da15755b
536
selfdrive/app/commalisten.py
Normal file
536
selfdrive/app/commalisten.py
Normal file
@ -0,0 +1,536 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import socket
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime
|
||||
import argparse
|
||||
import traceback
|
||||
|
||||
# 处理Windows环境
|
||||
try:
|
||||
import curses
|
||||
except ImportError:
|
||||
try:
|
||||
# Windows平台上尝试导入windows-curses
|
||||
import windows_curses as curses
|
||||
except ImportError:
|
||||
print("无法导入curses模块。在Windows上需要安装windows-curses包。")
|
||||
print("请运行: pip install windows-curses")
|
||||
sys.exit(1)
|
||||
|
||||
class CommaListener:
|
||||
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()
|
||||
|
||||
class UI:
|
||||
def __init__(self, listener):
|
||||
"""初始化UI"""
|
||||
self.listener = listener
|
||||
self.stdscr = None
|
||||
self.running = True
|
||||
self.current_page = 0
|
||||
self.pages = ["车辆信息", "设备信息", "位置信息", "扩展车辆信息", "原始数据"]
|
||||
self.max_pages = len(self.pages)
|
||||
|
||||
# 颜色配对
|
||||
self.COLOR_NORMAL = 1
|
||||
self.COLOR_HIGHLIGHT = 2
|
||||
self.COLOR_STATUS_OK = 3
|
||||
self.COLOR_STATUS_WARNING = 4
|
||||
self.COLOR_STATUS_ERROR = 5
|
||||
self.COLOR_HEADER = 6
|
||||
self.COLOR_DATA = 7
|
||||
self.COLOR_TITLE = 8
|
||||
|
||||
# 布局参数
|
||||
self.max_height = 0
|
||||
self.max_width = 0
|
||||
|
||||
def run(self):
|
||||
"""运行UI"""
|
||||
curses.wrapper(self.main)
|
||||
|
||||
def main(self, stdscr):
|
||||
"""主UI循环"""
|
||||
self.stdscr = stdscr
|
||||
self.init_colors()
|
||||
|
||||
curses.curs_set(0) # 隐藏光标
|
||||
self.stdscr.timeout(100) # 设置getch非阻塞超时
|
||||
|
||||
while self.running:
|
||||
self.stdscr.clear()
|
||||
self.max_height, self.max_width = self.stdscr.getmaxyx()
|
||||
|
||||
# 处理按键
|
||||
self.handle_input()
|
||||
|
||||
# 绘制界面
|
||||
self.draw_header()
|
||||
self.draw_status()
|
||||
|
||||
# 根据当前页面绘制内容
|
||||
if self.current_page == 0:
|
||||
self.draw_car_info()
|
||||
elif self.current_page == 1:
|
||||
self.draw_device_info()
|
||||
elif self.current_page == 2:
|
||||
self.draw_location_info()
|
||||
elif self.current_page == 3:
|
||||
self.draw_extended_car_info()
|
||||
elif self.current_page == 4:
|
||||
self.draw_raw_data()
|
||||
|
||||
self.draw_footer()
|
||||
|
||||
self.stdscr.refresh()
|
||||
time.sleep(0.1)
|
||||
|
||||
def init_colors(self):
|
||||
"""初始化颜色配对"""
|
||||
curses.start_color()
|
||||
curses.use_default_colors()
|
||||
curses.init_pair(self.COLOR_NORMAL, curses.COLOR_WHITE, -1)
|
||||
curses.init_pair(self.COLOR_HIGHLIGHT, curses.COLOR_BLACK, curses.COLOR_WHITE)
|
||||
curses.init_pair(self.COLOR_STATUS_OK, curses.COLOR_GREEN, -1)
|
||||
curses.init_pair(self.COLOR_STATUS_WARNING, curses.COLOR_YELLOW, -1)
|
||||
curses.init_pair(self.COLOR_STATUS_ERROR, curses.COLOR_RED, -1)
|
||||
curses.init_pair(self.COLOR_HEADER, curses.COLOR_CYAN, -1)
|
||||
curses.init_pair(self.COLOR_DATA, curses.COLOR_GREEN, -1)
|
||||
curses.init_pair(self.COLOR_TITLE, curses.COLOR_MAGENTA, -1)
|
||||
|
||||
def handle_input(self):
|
||||
"""处理按键"""
|
||||
key = self.stdscr.getch()
|
||||
if key == ord('q'):
|
||||
self.running = False
|
||||
elif key == ord('n') or key == curses.KEY_RIGHT:
|
||||
self.current_page = (self.current_page + 1) % self.max_pages
|
||||
elif key == ord('p') or key == curses.KEY_LEFT:
|
||||
self.current_page = (self.current_page - 1) % self.max_pages
|
||||
|
||||
def draw_header(self):
|
||||
"""绘制顶部标题栏"""
|
||||
title = "CommaAssist 数据监视器"
|
||||
x = max(0, (self.max_width - len(title)) // 2)
|
||||
self.stdscr.attron(curses.color_pair(self.COLOR_HEADER) | curses.A_BOLD)
|
||||
self.safe_addstr(0, x, title)
|
||||
self.stdscr.attroff(curses.color_pair(self.COLOR_HEADER) | curses.A_BOLD)
|
||||
|
||||
# 绘制页面标签
|
||||
self.safe_addstr(1, 0, " " * (self.max_width - 1), curses.color_pair(self.COLOR_HIGHLIGHT))
|
||||
x = 2
|
||||
for i, page in enumerate(self.pages):
|
||||
if i == self.current_page:
|
||||
self.safe_addstr(1, x, f" {page} ", curses.color_pair(self.COLOR_HIGHLIGHT) | curses.A_BOLD)
|
||||
else:
|
||||
self.safe_addstr(1, x, f" {page} ", curses.color_pair(self.COLOR_NORMAL))
|
||||
x += len(page) + 3
|
||||
|
||||
def draw_status(self):
|
||||
"""绘制状态栏"""
|
||||
current_time = time.time()
|
||||
status_line = 2
|
||||
|
||||
if not self.listener.data:
|
||||
self.safe_addstr(status_line, 0, "等待数据...", curses.color_pair(self.COLOR_STATUS_WARNING))
|
||||
return
|
||||
|
||||
time_diff = current_time - self.listener.last_update
|
||||
if time_diff > 5:
|
||||
self.safe_addstr(status_line, 0, f"数据已过期! 上次更新: {datetime.fromtimestamp(self.listener.last_update).strftime('%H:%M:%S')}",
|
||||
curses.color_pair(self.COLOR_STATUS_ERROR))
|
||||
else:
|
||||
self.safe_addstr(status_line, 0, f"已连接到 {self.listener.device_ip} - 最后更新: {datetime.fromtimestamp(self.listener.last_update).strftime('%H:%M:%S')}",
|
||||
curses.color_pair(self.COLOR_STATUS_OK))
|
||||
|
||||
def draw_footer(self):
|
||||
"""绘制底部控制栏"""
|
||||
footer = "操作: [q]退出 [←/p]上一页 [→/n]下一页"
|
||||
y = self.max_height - 1
|
||||
x = max(0, (self.max_width - len(footer)) // 2)
|
||||
|
||||
# 防止超出屏幕边界
|
||||
try:
|
||||
# 修复:确保不超出屏幕边界
|
||||
if y > 0 and self.max_width > 0:
|
||||
# 使用safe_addstr方法避免边界问题
|
||||
self.safe_addstr(y, 0, " " * (self.max_width - 1), curses.color_pair(self.COLOR_HIGHLIGHT))
|
||||
self.safe_addstr(y, x, footer[:self.max_width - x - 1], curses.color_pair(self.COLOR_HIGHLIGHT))
|
||||
except curses.error:
|
||||
# 忽略curses边界错误
|
||||
pass
|
||||
|
||||
def safe_addstr(self, y, x, text, attr=0):
|
||||
"""安全地添加字符串,避免边界问题"""
|
||||
try:
|
||||
# 确保y和x是有效坐标
|
||||
height, width = self.stdscr.getmaxyx()
|
||||
if y < 0 or y >= height or x < 0:
|
||||
return
|
||||
|
||||
# 计算可以显示的最大长度
|
||||
max_len = min(len(text), width - x - 1)
|
||||
if max_len <= 0:
|
||||
return
|
||||
|
||||
# 确保不会写入最后一个字符位置
|
||||
self.stdscr.addstr(y, x, text[:max_len], attr)
|
||||
except curses.error:
|
||||
# 捕获并忽略curses错误
|
||||
pass
|
||||
|
||||
def draw_car_info(self):
|
||||
"""绘制车辆信息"""
|
||||
if not self.listener.data or 'car' not in self.listener.data:
|
||||
self.draw_no_data("车辆数据不可用")
|
||||
return
|
||||
|
||||
car = self.listener.data.get('car', {})
|
||||
|
||||
self.draw_section_title("基本车辆信息", 4)
|
||||
|
||||
# 速度和档位
|
||||
data = [
|
||||
("当前速度", f"{car.get('speed', 0):.1f} km/h"),
|
||||
("巡航速度", f"{car.get('cruise_speed', 0):.1f} km/h"),
|
||||
("档位", car.get('gear_shifter', 'Unknown')),
|
||||
("车门状态", "打开" if car.get('door_open', False) else "关闭"),
|
||||
]
|
||||
self.draw_data_section(data, 5, 0, self.max_width // 2)
|
||||
|
||||
# 方向盘和踏板
|
||||
data = [
|
||||
("方向盘角度", f"{car.get('steering_angle', 0):.1f}°"),
|
||||
("转向力矩", f"{car.get('steering_torque', 0):.1f} Nm"),
|
||||
("制动踏板", "已踩下" if car.get('brake_pressed', False) else "释放"),
|
||||
("油门踏板", "已踩下" if car.get('gas_pressed', False) else "释放"),
|
||||
]
|
||||
self.draw_data_section(data, 5, self.max_width // 2, self.max_width // 2)
|
||||
|
||||
# 转向灯状态
|
||||
self.draw_section_title("信号灯状态", 10)
|
||||
left_blinker = car.get('left_blinker', False)
|
||||
right_blinker = car.get('right_blinker', False)
|
||||
|
||||
blinker_status = "无"
|
||||
if left_blinker and right_blinker:
|
||||
blinker_status = "双闪"
|
||||
elif left_blinker:
|
||||
blinker_status = "左转"
|
||||
elif right_blinker:
|
||||
blinker_status = "右转"
|
||||
|
||||
self.safe_addstr(11, 2, f"转向灯: {blinker_status}", curses.color_pair(self.COLOR_DATA))
|
||||
|
||||
def draw_device_info(self):
|
||||
"""绘制设备信息"""
|
||||
if not self.listener.data or 'device' not in self.listener.data:
|
||||
self.draw_no_data("设备数据不可用")
|
||||
return
|
||||
|
||||
device = self.listener.data.get('device', {})
|
||||
|
||||
self.draw_section_title("设备状态", 4)
|
||||
|
||||
# 设备基本信息
|
||||
data = [
|
||||
("IP地址", device.get('ip', 'Unknown')),
|
||||
("内存使用", f"{device.get('mem_usage', 0):.1f}%"),
|
||||
("CPU温度", f"{device.get('cpu_temp', 0):.1f}°C"),
|
||||
("可用空间", f"{device.get('free_space', 0):.1f}%"),
|
||||
]
|
||||
self.draw_data_section(data, 5, 0, self.max_width // 2)
|
||||
|
||||
# 电池信息
|
||||
battery = device.get('battery', {})
|
||||
data = [
|
||||
("电池电量", f"{battery.get('percent', 0)}%"),
|
||||
("电池电压", f"{battery.get('voltage', 0):.2f}V"),
|
||||
("电池电流", f"{battery.get('status', 0):.2f}A"),
|
||||
("充电状态", "充电中" if not battery.get('charging', False) else "未充电"),
|
||||
]
|
||||
self.draw_data_section(data, 5, self.max_width // 2, self.max_width // 2)
|
||||
|
||||
def draw_location_info(self):
|
||||
"""绘制位置信息"""
|
||||
if not self.listener.data or 'location' not in self.listener.data:
|
||||
self.draw_no_data("位置数据不可用")
|
||||
return
|
||||
|
||||
location = self.listener.data.get('location', {})
|
||||
|
||||
if not location.get('gps_valid', False):
|
||||
self.draw_section_title("GPS状态", 4)
|
||||
self.safe_addstr(5, 2, "GPS信号无效或未获取", curses.color_pair(self.COLOR_STATUS_ERROR))
|
||||
return
|
||||
|
||||
self.draw_section_title("GPS位置", 4)
|
||||
|
||||
# GPS基本信息
|
||||
data = [
|
||||
("纬度", f"{location.get('latitude', 0):.6f}"),
|
||||
("经度", f"{location.get('longitude', 0):.6f}"),
|
||||
("海拔", f"{location.get('altitude', 0):.1f}m"),
|
||||
("GPS精度", f"{location.get('accuracy', 0):.1f}m"),
|
||||
]
|
||||
self.draw_data_section(data, 5, 0, self.max_width // 2)
|
||||
|
||||
# 运动信息
|
||||
data = [
|
||||
("方向", f"{location.get('bearing', 0):.1f}°"),
|
||||
("GPS速度", f"{location.get('speed', 0):.1f}km/h"),
|
||||
]
|
||||
self.draw_data_section(data, 5, self.max_width // 2, self.max_width // 2)
|
||||
|
||||
# 导航信息
|
||||
if 'navigation' in self.listener.data:
|
||||
nav = self.listener.data.get('navigation', {})
|
||||
|
||||
self.draw_section_title("导航信息", 10)
|
||||
|
||||
# 格式化距离
|
||||
dist = nav.get('distance_remaining', 0)
|
||||
if dist > 1000:
|
||||
dist_str = f"{dist/1000:.1f}km"
|
||||
else:
|
||||
dist_str = f"{dist:.0f}m"
|
||||
|
||||
# 格式化时间
|
||||
time_sec = nav.get('time_remaining', 0)
|
||||
minutes = int(time_sec // 60)
|
||||
seconds = int(time_sec % 60)
|
||||
time_str = f"{minutes}分{seconds}秒"
|
||||
|
||||
data = [
|
||||
("剩余距离", dist_str),
|
||||
("剩余时间", time_str),
|
||||
("道路限速", f"{nav.get('speed_limit', 0):.1f}km/h"),
|
||||
]
|
||||
self.draw_data_section(data, 11, 0, self.max_width // 2)
|
||||
|
||||
if nav.get('maneuver_distance', 0) > 0:
|
||||
data = [
|
||||
("下一动作", nav.get('maneuver_text', '')),
|
||||
("动作距离", f"{nav.get('maneuver_distance', 0)}m"),
|
||||
]
|
||||
self.draw_data_section(data, 11, self.max_width // 2, self.max_width // 2)
|
||||
|
||||
def draw_extended_car_info(self):
|
||||
"""绘制扩展车辆信息"""
|
||||
if not self.listener.data or 'car_info' not in self.listener.data:
|
||||
self.draw_no_data("扩展车辆数据不可用")
|
||||
return
|
||||
|
||||
car_info = self.listener.data.get('car_info', {})
|
||||
|
||||
# 绘制基本信息
|
||||
if 'basic' in car_info:
|
||||
basic = car_info.get('basic', {})
|
||||
self.draw_section_title("车辆基本信息", 4)
|
||||
|
||||
data = [
|
||||
("车型", basic.get('car_model', 'Unknown')),
|
||||
("车辆指纹", basic.get('fingerprint', 'Unknown')),
|
||||
("重量", basic.get('weight', 'Unknown')),
|
||||
("轴距", basic.get('wheelbase', 'Unknown')),
|
||||
("转向比", basic.get('steering_ratio', 'Unknown')),
|
||||
]
|
||||
self.draw_data_section(data, 5, 0, self.max_width)
|
||||
|
||||
# 绘制详细车辆信息
|
||||
if 'details' not in car_info:
|
||||
return
|
||||
|
||||
details = car_info.get('details', {})
|
||||
row = 10
|
||||
|
||||
# 巡航控制信息
|
||||
if 'cruise' in details:
|
||||
cruise = details.get('cruise', {})
|
||||
self.draw_section_title("巡航控制", row)
|
||||
row += 1
|
||||
|
||||
data = [
|
||||
("巡航状态", "开启" if cruise.get('enabled', False) else "关闭"),
|
||||
("自适应巡航", "可用" if cruise.get('available', False) else "不可用"),
|
||||
("设定速度", f"{cruise.get('speed', 0):.1f}km/h"),
|
||||
]
|
||||
|
||||
if 'gap' in cruise:
|
||||
data.append(("跟车距离", str(cruise.get('gap', 0))))
|
||||
|
||||
self.draw_data_section(data, row, 0, self.max_width)
|
||||
row += len(data) + 1
|
||||
|
||||
# 车轮速度
|
||||
if 'wheel_speeds' in details:
|
||||
ws = details.get('wheel_speeds', {})
|
||||
self.draw_section_title("车轮速度", row)
|
||||
row += 1
|
||||
|
||||
data = [
|
||||
("左前", f"{ws.get('fl', 0):.1f}km/h"),
|
||||
("右前", f"{ws.get('fr', 0):.1f}km/h"),
|
||||
("左后", f"{ws.get('rl', 0):.1f}km/h"),
|
||||
("右后", f"{ws.get('rr', 0):.1f}km/h"),
|
||||
]
|
||||
self.draw_data_section(data, row, 0, self.max_width // 2)
|
||||
row += len(data) + 1
|
||||
|
||||
# 安全系统
|
||||
if 'safety_systems' in details and details['safety_systems']:
|
||||
ss = details.get('safety_systems', {})
|
||||
self.draw_section_title("安全系统", row)
|
||||
row += 1
|
||||
|
||||
data = []
|
||||
if 'esp_disabled' in ss:
|
||||
data.append(("ESP状态", "禁用" if ss.get('esp_disabled', False) else "正常"))
|
||||
if 'abs_active' in ss:
|
||||
data.append(("ABS状态", "激活" if ss.get('abs_active', False) else "正常"))
|
||||
if 'tcs_active' in ss:
|
||||
data.append(("牵引力控制", "激活" if ss.get('tcs_active', False) else "正常"))
|
||||
if 'collision_warning' in ss:
|
||||
data.append(("碰撞警告", "警告" if ss.get('collision_warning', False) else "正常"))
|
||||
|
||||
if data:
|
||||
self.draw_data_section(data, row, 0, self.max_width // 2)
|
||||
row += len(data) + 1
|
||||
|
||||
# 盲点检测
|
||||
if 'blind_spot' in details and details['blind_spot']:
|
||||
bs = details.get('blind_spot', {})
|
||||
self.draw_section_title("盲点监测", row)
|
||||
row += 1
|
||||
|
||||
data = []
|
||||
if 'left' in bs:
|
||||
data.append(("左侧", "检测到车辆" if bs.get('left', False) else "无车辆"))
|
||||
if 'right' in bs:
|
||||
data.append(("右侧", "检测到车辆" if bs.get('right', False) else "无车辆"))
|
||||
|
||||
if data:
|
||||
self.draw_data_section(data, row, 0, self.max_width // 2)
|
||||
row += len(data) + 1
|
||||
|
||||
# 其他信息
|
||||
if 'other' in details and details['other']:
|
||||
other = details.get('other', {})
|
||||
self.draw_section_title("其他信息", row)
|
||||
row += 1
|
||||
|
||||
data = []
|
||||
if 'outside_temp' in other:
|
||||
data.append(("车外温度", f"{other.get('outside_temp', 0):.1f}°C"))
|
||||
if 'fuel_range' in other:
|
||||
data.append(("续航里程", f"{other.get('fuel_range', 0):.1f}km"))
|
||||
if 'odometer' in other:
|
||||
data.append(("里程表", f"{other.get('odometer', 0):.1f}km"))
|
||||
if 'fuel_consumption' in other:
|
||||
data.append(("油耗", f"{other.get('fuel_consumption', 0):.1f}L/100km"))
|
||||
|
||||
if data:
|
||||
self.draw_data_section(data, row, 0, self.max_width // 2)
|
||||
|
||||
def draw_raw_data(self):
|
||||
"""绘制原始数据"""
|
||||
if not self.listener.data:
|
||||
self.draw_no_data("没有数据")
|
||||
return
|
||||
|
||||
self.draw_section_title("原始JSON数据", 4)
|
||||
|
||||
try:
|
||||
json_str = json.dumps(self.listener.data, indent=2)
|
||||
lines = json_str.split('\n')
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
if 4 + i + 1 >= self.max_height - 1: # 保留底部状态栏
|
||||
self.safe_addstr(4 + i, 0, "... (内容过多无法完全显示)", curses.color_pair(self.COLOR_STATUS_WARNING))
|
||||
break
|
||||
if len(line) > self.max_width:
|
||||
line = line[:self.max_width - 3] + "..."
|
||||
self.safe_addstr(4 + i + 1, 0, line)
|
||||
except Exception as e:
|
||||
self.safe_addstr(5, 0, f"无法显示JSON数据: {e}", curses.color_pair(self.COLOR_STATUS_ERROR))
|
||||
|
||||
def draw_no_data(self, message):
|
||||
"""绘制无数据消息"""
|
||||
y = self.max_height // 2
|
||||
x = max(0, (self.max_width - len(message)) // 2)
|
||||
self.safe_addstr(y, x, message, curses.color_pair(self.COLOR_STATUS_WARNING))
|
||||
|
||||
def draw_section_title(self, title, row):
|
||||
"""绘制区域标题"""
|
||||
self.safe_addstr(row, 0, title, curses.color_pair(self.COLOR_TITLE) | curses.A_BOLD)
|
||||
|
||||
def draw_data_section(self, data_list, start_row, start_col, width):
|
||||
"""绘制数据区域"""
|
||||
for i, (label, value) in enumerate(data_list):
|
||||
row = start_row + i
|
||||
|
||||
if row >= self.max_height - 1: # 避免超出屏幕底部
|
||||
break
|
||||
|
||||
self.safe_addstr(row, start_col + 2, f"{label}: ", curses.color_pair(self.COLOR_NORMAL))
|
||||
try:
|
||||
self.stdscr.addstr(value, curses.color_pair(self.COLOR_DATA))
|
||||
except curses.error:
|
||||
pass
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
parser = argparse.ArgumentParser(description='CommaAssist 终端监控器')
|
||||
parser.add_argument('-p', '--port', type=int, default=8088, help='监听端口(默认: 8088)')
|
||||
args = parser.parse_args()
|
||||
|
||||
# 创建并启动监听器
|
||||
listener = CommaListener(port=args.port)
|
||||
listener_thread = threading.Thread(target=listener.start_listening)
|
||||
listener_thread.daemon = True
|
||||
listener_thread.start()
|
||||
|
||||
# 创建并运行UI
|
||||
ui = UI(listener)
|
||||
try:
|
||||
ui.run()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
listener.running = False
|
||||
print("正在退出...")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
498
selfdrive/app/commassit.py
Normal file
498
selfdrive/app/commassit.py
Normal file
@ -0,0 +1,498 @@
|
||||
#!/usr/bin/env python3
|
||||
import fcntl
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import socket
|
||||
import struct
|
||||
import threading
|
||||
import time
|
||||
import traceback
|
||||
import zmq
|
||||
from datetime import datetime
|
||||
|
||||
import cereal.messaging as messaging
|
||||
from openpilot.common.realtime import Ratekeeper
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.hardware import PC, TICI
|
||||
try:
|
||||
from selfdrive.car.car_helpers import interfaces
|
||||
HAS_CAR_INTERFACES = True
|
||||
except ImportError:
|
||||
HAS_CAR_INTERFACES = False
|
||||
interfaces = None
|
||||
|
||||
class CommaAssist:
|
||||
def __init__(self):
|
||||
print("初始化 CommaAssist 服务...")
|
||||
# 初始化参数
|
||||
self.params = Params()
|
||||
|
||||
# 订阅需要的数据源
|
||||
self.sm = messaging.SubMaster(['deviceState', 'carState', 'controlsState',
|
||||
'longitudinalPlan', 'liveLocationKalman',
|
||||
'navInstruction', 'modelV2'])
|
||||
|
||||
# 网络相关配置
|
||||
self.broadcast_ip = self.get_broadcast_address()
|
||||
if self.broadcast_ip is None:
|
||||
self.broadcast_ip = "255.255.255.255" # 使用通用广播地址作为备选
|
||||
self.broadcast_port = 8088
|
||||
self.ip_address = "0.0.0.0"
|
||||
self.is_running = True
|
||||
|
||||
# 获取车辆信息
|
||||
self.car_info = {}
|
||||
self.load_car_info()
|
||||
|
||||
# 启动广播线程
|
||||
threading.Thread(target=self.broadcast_data).start()
|
||||
|
||||
def load_car_info(self):
|
||||
"""加载车辆基本信息"""
|
||||
try:
|
||||
# 获取车辆型号信息
|
||||
try:
|
||||
car_model = self.params.get("CarModel", encoding='utf8')
|
||||
self.car_info["car_name"] = car_model if car_model else "Unknown"
|
||||
except Exception as e:
|
||||
print(f"无法获取CarModel参数: {e}")
|
||||
try:
|
||||
# 尝试获取其他可能的车辆参数
|
||||
car_params = self.params.get("CarParamsCache", encoding='utf8')
|
||||
self.car_info["car_name"] = "通过CarParamsCache获取" if car_params else "Unknown"
|
||||
except Exception:
|
||||
self.car_info["car_name"] = "Unknown Model"
|
||||
car_model = None
|
||||
|
||||
# 检查车辆接口是否可用
|
||||
if not HAS_CAR_INTERFACES:
|
||||
print("车辆接口模块不可用")
|
||||
elif not car_model:
|
||||
print("无有效的车型信息")
|
||||
elif not isinstance(interfaces, list):
|
||||
print("车辆接口不是列表类型,尝试转换...")
|
||||
# 尝试获取车辆接口的具体实现
|
||||
if hasattr(interfaces, '__call__'):
|
||||
# 如果interfaces是一个函数,尝试直接获取车辆指纹
|
||||
try:
|
||||
self.car_info["car_fingerprint"] = f"直接从车型{car_model}获取"
|
||||
print(f"直接从车型识别: {car_model}")
|
||||
except Exception as e:
|
||||
print(f"无法从车型直接获取指纹: {e}")
|
||||
else:
|
||||
# 正常遍历接口列表
|
||||
print("尝试从车辆接口中获取指纹信息...")
|
||||
for interface in interfaces:
|
||||
if not hasattr(interface, 'CHECKSUM'):
|
||||
continue
|
||||
|
||||
try:
|
||||
if isinstance(interface.CHECKSUM, dict) and 'pt' in interface.CHECKSUM:
|
||||
if car_model in interface.CHECKSUM["pt"]:
|
||||
platform = interface
|
||||
self.car_info["car_fingerprint"] = platform.config.platform_str
|
||||
|
||||
# 获取车辆规格参数
|
||||
specs = platform.config.specs
|
||||
if specs:
|
||||
if hasattr(specs, 'mass'):
|
||||
self.car_info["mass"] = specs.mass
|
||||
if hasattr(specs, 'wheelbase'):
|
||||
self.car_info["wheelbase"] = specs.wheelbase
|
||||
if hasattr(specs, 'steerRatio'):
|
||||
self.car_info["steerRatio"] = specs.steerRatio
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"处理特定车辆接口异常: {e}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"加载车辆信息失败: {e}")
|
||||
traceback.print_exc()
|
||||
|
||||
# 确保基本字段存在,避免后续访问出错
|
||||
if "car_name" not in self.car_info:
|
||||
self.car_info["car_name"] = "Unknown Model"
|
||||
if "car_fingerprint" not in self.car_info:
|
||||
self.car_info["car_fingerprint"] = "Unknown Fingerprint"
|
||||
|
||||
print(f"车辆信息加载完成: {self.car_info}")
|
||||
|
||||
def get_broadcast_address(self):
|
||||
"""获取广播地址"""
|
||||
try:
|
||||
if PC:
|
||||
iface = b'br0'
|
||||
else:
|
||||
iface = b'wlan0'
|
||||
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
|
||||
ip = fcntl.ioctl(
|
||||
s.fileno(),
|
||||
0x8919, # SIOCGIFADDR
|
||||
struct.pack('256s', iface)
|
||||
)[20:24]
|
||||
ip_str = socket.inet_ntoa(ip)
|
||||
print(f"获取到IP地址: {ip_str}")
|
||||
# 从IP地址构造广播地址
|
||||
ip_parts = ip_str.split('.')
|
||||
return f"{ip_parts[0]}.{ip_parts[1]}.{ip_parts[2]}.255"
|
||||
except (OSError, Exception) as e:
|
||||
print(f"获取广播地址失败: {e}")
|
||||
return None
|
||||
|
||||
def get_local_ip(self):
|
||||
"""获取本地IP地址"""
|
||||
try:
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
|
||||
s.connect(("8.8.8.8", 80))
|
||||
return s.getsockname()[0]
|
||||
except Exception as e:
|
||||
print(f"获取本地IP失败: {e}")
|
||||
return "127.0.0.1"
|
||||
|
||||
def make_data_message(self):
|
||||
"""构建广播消息内容"""
|
||||
# 基本消息结构
|
||||
message = {
|
||||
"timestamp": int(time.time()),
|
||||
"device": {
|
||||
"ip": self.ip_address,
|
||||
"battery": {},
|
||||
"mem_usage": 0,
|
||||
"cpu_temp": 0,
|
||||
"free_space": 0
|
||||
},
|
||||
"car": {
|
||||
"speed": 0,
|
||||
"cruise_speed": 0,
|
||||
"gear_shifter": "unknown",
|
||||
"steering_angle": 0,
|
||||
"steering_torque": 0,
|
||||
"brake_pressed": False,
|
||||
"gas_pressed": False,
|
||||
"door_open": False,
|
||||
"left_blinker": False,
|
||||
"right_blinker": False
|
||||
},
|
||||
"location": {
|
||||
"latitude": 0,
|
||||
"longitude": 0,
|
||||
"bearing": 0,
|
||||
"speed": 0,
|
||||
"altitude": 0,
|
||||
"accuracy": 0,
|
||||
"gps_valid": False
|
||||
},
|
||||
"car_info": {
|
||||
"basic": {
|
||||
"car_model": self.car_info.get("car_name", "Unknown"),
|
||||
"fingerprint": self.car_info.get("car_fingerprint", "Unknown"),
|
||||
"weight": f"{self.car_info.get('mass', 0):.0f} kg" if 'mass' in self.car_info else "Unknown",
|
||||
"wheelbase": f"{self.car_info.get('wheelbase', 0):.3f} m" if 'wheelbase' in self.car_info else "Unknown",
|
||||
"steering_ratio": f"{self.car_info.get('steerRatio', 0):.1f}" if 'steerRatio' in self.car_info else "Unknown"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# 安全地获取设备信息
|
||||
try:
|
||||
if self.sm.updated['deviceState'] and self.sm.valid['deviceState']:
|
||||
device_state = self.sm['deviceState']
|
||||
|
||||
# 获取设备信息 - 使用getattr安全地访问属性
|
||||
message["device"]["mem_usage"] = getattr(device_state, 'memoryUsagePercent', 0)
|
||||
message["device"]["free_space"] = getattr(device_state, 'freeSpacePercent', 0)
|
||||
|
||||
# CPU温度
|
||||
cpu_temps = getattr(device_state, 'cpuTempC', [])
|
||||
if isinstance(cpu_temps, list) and len(cpu_temps) > 0:
|
||||
message["device"]["cpu_temp"] = cpu_temps[0]
|
||||
|
||||
# 电池信息
|
||||
try: # 额外的错误处理,因为这是常见错误点
|
||||
message["device"]["battery"]["percent"] = getattr(device_state, 'batteryPercent', 0)
|
||||
message["device"]["battery"]["status"] = getattr(device_state, 'batteryCurrent', 0)
|
||||
message["device"]["battery"]["voltage"] = getattr(device_state, 'batteryVoltage', 0)
|
||||
message["device"]["battery"]["charging"] = getattr(device_state, 'chargingError', False)
|
||||
except Exception as e:
|
||||
print(f"获取电池信息失败: {e}")
|
||||
except Exception as e:
|
||||
print(f"获取设备状态出错: {e}")
|
||||
|
||||
# 安全地获取车辆信息
|
||||
try:
|
||||
if self.sm.updated['carState'] and self.sm.valid['carState']:
|
||||
CS = self.sm['carState']
|
||||
|
||||
# 基本车辆信息
|
||||
message["car"]["speed"] = getattr(CS, 'vEgo', 0) * 3.6 # m/s转km/h
|
||||
message["car"]["gear_shifter"] = str(getattr(CS, 'gearShifter', "unknown"))
|
||||
message["car"]["steering_angle"] = getattr(CS, 'steeringAngleDeg', 0)
|
||||
message["car"]["steering_torque"] = getattr(CS, 'steeringTorque', 0)
|
||||
message["car"]["brake_pressed"] = getattr(CS, 'brakePressed', False)
|
||||
message["car"]["gas_pressed"] = getattr(CS, 'gasPressed', False)
|
||||
message["car"]["door_open"] = getattr(CS, 'doorOpen', False)
|
||||
message["car"]["left_blinker"] = getattr(CS, 'leftBlinker', False)
|
||||
message["car"]["right_blinker"] = getattr(CS, 'rightBlinker', False)
|
||||
|
||||
# 扩展的车辆状态信息
|
||||
is_car_started = getattr(CS, 'vEgo', 0) > 0.1
|
||||
is_car_engaged = False
|
||||
|
||||
# 详细车辆信息
|
||||
car_details = {}
|
||||
|
||||
# 车辆状态
|
||||
status = {
|
||||
"running_status": "Moving" if is_car_started else "Stopped",
|
||||
"door_open": getattr(CS, 'doorOpen', False),
|
||||
"seatbelt_unlatched": getattr(CS, 'seatbeltUnlatched', False),
|
||||
}
|
||||
|
||||
# 引擎信息
|
||||
engine_info = {}
|
||||
if hasattr(CS, 'engineRpm') and CS.engineRpm > 0:
|
||||
engine_info["rpm"] = f"{CS.engineRpm:.0f}"
|
||||
car_details["engine"] = engine_info
|
||||
|
||||
# 巡航控制
|
||||
cruise_info = {}
|
||||
if hasattr(CS, 'cruiseState'):
|
||||
is_car_engaged = getattr(CS.cruiseState, 'enabled', False)
|
||||
cruise_info["enabled"] = getattr(CS.cruiseState, 'enabled', False)
|
||||
cruise_info["available"] = getattr(CS.cruiseState, 'available', False)
|
||||
cruise_info["speed"] = getattr(CS.cruiseState, 'speed', 0) * 3.6
|
||||
|
||||
if hasattr(CS, 'pcmCruiseGap'):
|
||||
cruise_info["gap"] = CS.pcmCruiseGap
|
||||
|
||||
status["cruise_engaged"] = is_car_engaged
|
||||
car_details["cruise"] = cruise_info
|
||||
|
||||
# 车轮速度
|
||||
wheel_speeds = {}
|
||||
if hasattr(CS, 'wheelSpeeds'):
|
||||
ws = CS.wheelSpeeds
|
||||
wheel_speeds["fl"] = getattr(ws, 'fl', 0) * 3.6
|
||||
wheel_speeds["fr"] = getattr(ws, 'fr', 0) * 3.6
|
||||
wheel_speeds["rl"] = getattr(ws, 'rl', 0) * 3.6
|
||||
wheel_speeds["rr"] = getattr(ws, 'rr', 0) * 3.6
|
||||
car_details["wheel_speeds"] = wheel_speeds
|
||||
|
||||
# 方向盘信息
|
||||
steering = {
|
||||
"angle": getattr(CS, 'steeringAngleDeg', 0),
|
||||
"torque": getattr(CS, 'steeringTorque', 0),
|
||||
}
|
||||
if hasattr(CS, 'steeringRateDeg'):
|
||||
steering["rate"] = CS.steeringRateDeg
|
||||
car_details["steering"] = steering
|
||||
|
||||
# 踏板状态
|
||||
pedals = {
|
||||
"gas_pressed": getattr(CS, 'gasPressed', False),
|
||||
"brake_pressed": getattr(CS, 'brakePressed', False),
|
||||
}
|
||||
if hasattr(CS, 'gas'):
|
||||
pedals["throttle_position"] = CS.gas * 100
|
||||
if hasattr(CS, 'brake'):
|
||||
pedals["brake_pressure"] = CS.brake * 100
|
||||
car_details["pedals"] = pedals
|
||||
|
||||
# 安全系统
|
||||
safety_systems = {}
|
||||
if hasattr(CS, 'espDisabled'):
|
||||
safety_systems["esp_disabled"] = CS.espDisabled
|
||||
if hasattr(CS, 'absActive'):
|
||||
safety_systems["abs_active"] = CS.absActive
|
||||
if hasattr(CS, 'tcsActive'):
|
||||
safety_systems["tcs_active"] = CS.tcsActive
|
||||
if hasattr(CS, 'collisionWarning'):
|
||||
safety_systems["collision_warning"] = CS.collisionWarning
|
||||
car_details["safety_systems"] = safety_systems
|
||||
|
||||
# 车门状态
|
||||
doors = {
|
||||
"driver": getattr(CS, 'doorOpen', False)
|
||||
}
|
||||
if hasattr(CS, 'passengerDoorOpen'):
|
||||
doors["passenger"] = CS.passengerDoorOpen
|
||||
if hasattr(CS, 'trunkOpen'):
|
||||
doors["trunk"] = CS.trunkOpen
|
||||
if hasattr(CS, 'hoodOpen'):
|
||||
doors["hood"] = CS.hoodOpen
|
||||
car_details["doors"] = doors
|
||||
|
||||
# 灯光状态
|
||||
lights = {
|
||||
"left_blinker": getattr(CS, 'leftBlinker', False),
|
||||
"right_blinker": getattr(CS, 'rightBlinker', False),
|
||||
}
|
||||
if hasattr(CS, 'genericToggle'):
|
||||
lights["high_beam"] = CS.genericToggle
|
||||
if hasattr(CS, 'lowBeamOn'):
|
||||
lights["low_beam"] = CS.lowBeamOn
|
||||
car_details["lights"] = lights
|
||||
|
||||
# 盲点监测
|
||||
blind_spot = {}
|
||||
if hasattr(CS, 'leftBlindspot'):
|
||||
blind_spot["left"] = CS.leftBlindspot
|
||||
if hasattr(CS, 'rightBlindspot'):
|
||||
blind_spot["right"] = CS.rightBlindspot
|
||||
if blind_spot:
|
||||
car_details["blind_spot"] = blind_spot
|
||||
|
||||
# 其他可选信息
|
||||
other_info = {}
|
||||
if hasattr(CS, 'outsideTemp'):
|
||||
other_info["outside_temp"] = CS.outsideTemp
|
||||
if hasattr(CS, 'fuelGauge'):
|
||||
other_info["fuel_range"] = CS.fuelGauge
|
||||
if hasattr(CS, 'odometer'):
|
||||
other_info["odometer"] = CS.odometer
|
||||
if hasattr(CS, 'instantFuelConsumption'):
|
||||
other_info["fuel_consumption"] = CS.instantFuelConsumption
|
||||
if other_info:
|
||||
car_details["other"] = other_info
|
||||
|
||||
# 更新状态和详细信息
|
||||
message["car_info"]["status"] = status
|
||||
message["car_info"]["details"] = car_details
|
||||
|
||||
if self.sm.updated['controlsState'] and self.sm.valid['controlsState']:
|
||||
controls_state = self.sm['controlsState']
|
||||
message["car"]["cruise_speed"] = getattr(controls_state, 'vCruise', 0)
|
||||
|
||||
# 额外的控制状态信息
|
||||
controls_info = {}
|
||||
if hasattr(controls_state, 'enabled'):
|
||||
controls_info["enabled"] = controls_state.enabled
|
||||
if hasattr(controls_state, 'active'):
|
||||
controls_info["active"] = controls_state.active
|
||||
if hasattr(controls_state, 'alertText1'):
|
||||
controls_info["alert_text"] = controls_state.alertText1
|
||||
if controls_info:
|
||||
message["car_info"]["controls"] = controls_info
|
||||
|
||||
except Exception as e:
|
||||
print(f"获取车辆信息出错: {e}")
|
||||
traceback.print_exc()
|
||||
|
||||
# 安全地获取GPS位置信息
|
||||
try:
|
||||
if self.sm.updated['liveLocationKalman'] and self.sm.valid['liveLocationKalman']:
|
||||
location = self.sm['liveLocationKalman']
|
||||
|
||||
# 检查GPS是否有效
|
||||
location_status = getattr(location, 'status', -1)
|
||||
position_valid = False
|
||||
if hasattr(location, 'positionGeodetic'):
|
||||
position_valid = getattr(location.positionGeodetic, 'valid', False)
|
||||
|
||||
gps_valid = (location_status == 0) and position_valid
|
||||
message["location"]["gps_valid"] = gps_valid
|
||||
|
||||
if gps_valid and hasattr(location, 'positionGeodetic') and hasattr(location.positionGeodetic, 'value'):
|
||||
# 获取位置信息
|
||||
pos_value = location.positionGeodetic.value
|
||||
if len(pos_value) >= 3:
|
||||
message["location"]["latitude"] = pos_value[0]
|
||||
message["location"]["longitude"] = pos_value[1]
|
||||
message["location"]["altitude"] = pos_value[2]
|
||||
|
||||
# 获取精度信息
|
||||
if hasattr(location, 'positionGeodeticStd') and hasattr(location.positionGeodeticStd, 'value'):
|
||||
std_value = location.positionGeodeticStd.value
|
||||
if len(std_value) > 0:
|
||||
message["location"]["accuracy"] = std_value[0]
|
||||
|
||||
# 获取方向信息
|
||||
if hasattr(location, 'calibratedOrientationNED') and hasattr(location.calibratedOrientationNED, 'value'):
|
||||
orientation = location.calibratedOrientationNED.value
|
||||
if len(orientation) > 2:
|
||||
message["location"]["bearing"] = math.degrees(orientation[2])
|
||||
|
||||
# 设置速度信息
|
||||
car_state = self.sm['carState'] if self.sm.valid['carState'] else None
|
||||
if car_state and hasattr(car_state, 'vEgo'):
|
||||
message["location"]["speed"] = car_state.vEgo * 3.6
|
||||
except Exception as e:
|
||||
print(f"获取位置信息出错: {e}")
|
||||
|
||||
# 如果有导航指令,添加导航信息
|
||||
try:
|
||||
if self.sm.valid['navInstruction']:
|
||||
nav_instruction = self.sm['navInstruction']
|
||||
|
||||
nav_info = {}
|
||||
nav_info["distance_remaining"] = getattr(nav_instruction, 'distanceRemaining', 0)
|
||||
nav_info["time_remaining"] = getattr(nav_instruction, 'timeRemaining', 0)
|
||||
nav_info["speed_limit"] = getattr(nav_instruction, 'speedLimit', 0) * 3.6
|
||||
nav_info["maneuver_distance"] = getattr(nav_instruction, 'maneuverDistance', 0)
|
||||
nav_info["maneuver_type"] = getattr(nav_instruction, 'maneuverType', "")
|
||||
nav_info["maneuver_modifier"] = getattr(nav_instruction, 'maneuverModifier', "")
|
||||
nav_info["maneuver_text"] = getattr(nav_instruction, 'maneuverPrimaryText', "")
|
||||
|
||||
message["navigation"] = nav_info
|
||||
except Exception as e:
|
||||
print(f"获取导航信息出错: {e}")
|
||||
|
||||
try:
|
||||
return json.dumps(message)
|
||||
except Exception as e:
|
||||
print(f"序列化消息出错: {e}")
|
||||
return "{}"
|
||||
|
||||
def broadcast_data(self):
|
||||
"""定期发送数据到广播地址"""
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
|
||||
rk = Ratekeeper(10, print_delay_threshold=None) # 10Hz广播频率
|
||||
|
||||
print(f"开始广播数据到 {self.broadcast_ip}:{self.broadcast_port}")
|
||||
|
||||
while self.is_running:
|
||||
try:
|
||||
# 更新数据
|
||||
self.sm.update(0)
|
||||
|
||||
# 更新IP地址
|
||||
ip_address = self.get_local_ip()
|
||||
if ip_address != self.ip_address:
|
||||
self.ip_address = ip_address
|
||||
print(f"IP地址已更新: {ip_address}")
|
||||
|
||||
# 构建并发送消息
|
||||
msg = self.make_data_message()
|
||||
dat = msg.encode('utf-8')
|
||||
sock.sendto(dat, (self.broadcast_ip, self.broadcast_port))
|
||||
|
||||
# 减少日志输出频率
|
||||
if rk.frame % 50 == 0: # 每5秒打印一次日志
|
||||
print(f"广播数据: {self.broadcast_ip}:{self.broadcast_port}")
|
||||
|
||||
rk.keep_time()
|
||||
except Exception as e:
|
||||
print(f"广播数据错误: {e}")
|
||||
traceback.print_exc()
|
||||
time.sleep(1)
|
||||
|
||||
def main(gctx=None):
|
||||
"""主函数
|
||||
支持作为独立程序运行或由process_config启动
|
||||
gctx参数用于与openpilot进程管理系统兼容
|
||||
"""
|
||||
comma_assist = CommaAssist()
|
||||
|
||||
# 保持主线程运行
|
||||
try:
|
||||
while True:
|
||||
time.sleep(10) # 主线程休眠
|
||||
except KeyboardInterrupt:
|
||||
comma_assist.is_running = False
|
||||
print("CommaAssist服务已停止")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
858
selfdrive/app/commawebview.py
Normal file
858
selfdrive/app/commawebview.py
Normal file
@ -0,0 +1,858 @@
|
||||
#!/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()
|
@ -101,6 +101,9 @@ procs = [
|
||||
PythonProcess("fleet_manager", "selfdrive.frogpilot.fleetmanager.fleet_manager", always_run),
|
||||
PythonProcess("frogpilot_process", "selfdrive.frogpilot.frogpilot_process", always_run),
|
||||
PythonProcess("mapd", "selfdrive.frogpilot.navigation.mapd", always_run),
|
||||
|
||||
# CommaAssist process
|
||||
PythonProcess("commassist", "selfdrive.app.commassit", always_run),
|
||||
]
|
||||
|
||||
managed_processes = {p.name: p for p in procs}
|
||||
|
Loading…
x
Reference in New Issue
Block a user