Compare commits

..

10 Commits

Author SHA1 Message Date
机械小鸽
68fa4aea93
Add files via upload 2025-04-01 20:54:46 +08:00
机械小鸽
e2a80356c8
汉化大部分内容
* car

* v

* 22

* 22
2025-03-31 22:23:07 +08:00
机械小鸽
37da15755b
Dev (#17)
* app

* add

* add

* update and teest

* add service

* 1
2025-03-29 20:07:23 +08:00
机械小鸽
e3d6f80710
1 2025-03-29 10:39:26 +08:00
机械小鸽
439c6e7d7f
增加说明 2025-03-29 10:36:32 +08:00
机械小鸽
341f9478ec
改善导航
NA
2025-03-29 10:24:50 +08:00
机械小鸽
b659f68bc8
Update tmap_addr_input.html 2025-03-29 10:22:28 +08:00
机械小鸽
044e8371a4
tmap
增加导航
2025-03-28 23:54:06 +08:00
机械小鸽
779c2d32c4
感谢 2025-03-28 19:40:06 +08:00
机械小鸽
b630cc541c
update 2025-03-28 18:55:38 +08:00
13 changed files with 2702 additions and 1046 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,10 @@
#!/usr/bin/bash
# 设置 Mapbox 公钥和密钥
echo "pk.eyJ1IjoibWF3ZWl5dXdlaXdlaSIsImEiOiJjbGtrdjhhMHUwbWwzM3VwYzVtbXkwY2lrIn0.-u-QP8-tfrhAIpvQiNMsOw" > /data/params/d/MapboxPublicKey
echo "sk.eyJ1IjoibWF3ZWl5dXdlaXdlaSIsImEiOiJjbG15NHN1dDMwdWc5MmxwaDdkZ3Z5dHNyIn0.pk06qTKkAZyBC1Z37v8i0A" > /data/params/d/MapboxSecretKey
# 设置适当的权限
chmod 644 /data/params/d/MapboxPublicKey
chmod 644 /data/params/d/MapboxSecretKey
exec ./launch_chffrplus.sh

View 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
View 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()

View 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()

View File

@ -281,6 +281,16 @@ def amap_addr_input():
amap_key, amap_key_2 = fleet.get_amap_key()
return render_template("amap_addr_input.html", lon=lon, lat=lat, amap_key=amap_key, amap_key_2=amap_key_2)
@app.route("/tmap_addr_input", methods=['GET', 'POST'])
def tmap_addr_input():
if request.method == 'POST':
postvars = request.form.to_dict()
fleet.nav_confirmed(postvars)
return redirect(url_for('tmap_addr_input'))
else:
lon, lat = fleet.get_last_lon_lat()
return render_template("tmap_addr_input.html", lon=lon, lat=lat)
@app.route("/CurrentStep.json", methods=['GET'])
def find_CurrentStep():
directory = "/data/openpilot/selfdrive/manager/"
@ -336,6 +346,34 @@ def store_toggle_values_route():
except Exception as e:
return jsonify({"error": "Failed to update values", "details": str(e)}), 400
@app.route("/get_nav_status", methods=['GET'])
def get_nav_status():
nav_active = fleet.get_nav_active()
return jsonify({
"active": nav_active
})
@app.route("/get_system_status", methods=['GET'])
def get_system_status():
nav_active = fleet.get_nav_active()
gps_status = fleet.get_gps_status()
network_status = fleet.check_network_status()
return jsonify({
"nav_status": {
"active": nav_active,
"state": "导航中" if nav_active else "待机"
},
"gps_status": {
"active": gps_status["active"],
"signal": gps_status["signal"]
},
"network_status": {
"connected": network_status["connected"],
"type": network_status["type"]
}
})
def main():
try:
set_core_affinity([0, 1, 2, 3])

View File

@ -221,9 +221,10 @@ def ffplay_mp4_wrap_process_builder(file_name):
)
def get_nav_active():
if params.get("NavDestination", encoding='utf8') is not None:
return True
else:
try:
with open('/data/params/d/NavDestination', 'r') as f:
return f.read().strip() != ""
except:
return False
def get_public_token():
@ -244,20 +245,29 @@ def get_amap_key():
return (token.strip() if token is not None else None, token2.strip() if token2 is not None else None)
def get_SearchInput():
SearchInput = params.get_int("SearchInput")
return SearchInput
try:
with open('/data/params/d/SearchInput', 'r') as f:
return int(f.read())
except:
return 0
def get_PrimeType():
PrimeType = params.get_int("PrimeType")
return PrimeType
try:
with open('/data/params/d/PrimeType', 'r') as f:
return int(f.read())
except:
return 0
def get_last_lon_lat():
last_pos = params.get("LastGPSPosition")
if last_pos:
l = json.loads(last_pos)
else:
return 0.0, 0.0
return l["longitude"], l["latitude"]
try:
with open('/data/params/d/LastGPSPosition', 'r') as f:
content = f.read().strip()
if content:
lat, lon = map(float, content.split(","))
return lon, lat
except:
pass
return 116.397128, 39.916527 # 默认北京天安门坐标
def get_locations():
data = params.get("ApiCache_NavDestinations", encoding='utf-8')
@ -463,3 +473,30 @@ def store_toggle_values(updated_values):
params_memory.put_bool("FrogPilotTogglesUpdated", True)
time.sleep(1)
params_memory.put_bool("FrogPilotTogglesUpdated", False)
def get_gps_status():
try:
# 读取GPS状态
with open('/data/params/d/LastGPSPosition', 'r') as f:
content = f.read().strip()
if not content:
return {"active": False, "signal": "无信号"}
return {
"active": True,
"signal": "正常"
}
except:
return {"active": False, "signal": "未知"}
def check_network_status():
try:
# 检查网络连接
result = subprocess.run(['ping', '-c', '1', '-W', '1', '8.8.8.8'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
return {
"connected": result.returncode == 0,
"type": "已连接" if result.returncode == 0 else "未连接"
}
except:
return {"connected": False, "type": "未知"}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

@ -1,16 +1,146 @@
{% extends "layout.html" %}
{% block title %}
Home
Fleet Manager 主页
{% endblock %}
{% block main %}
<br>
<h1>Fleet Manager</h1>
<br>
<a href='/footage'>View Dashcam Footage</a><br>
<br><a href='/preserved'>Access Preserved Footage</a><br>
<br><a href='/screenrecords'>View Screen Recordings</a><br>
<br><a href='/error_logs'>Access Error Logs</a><br>
<br><a href='/about'>About Fleet Manager</a><br>
<!-- UIkit CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/uikit@3.9.2/dist/css/uikit.min.css" />
<!-- UIkit JS -->
<script src="https://cdn.jsdelivr.net/npm/uikit@3.9.2/dist/js/uikit.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/uikit@3.9.2/dist/js/uikit-icons.min.js"></script>
<div class="uk-container uk-container-small">
<h1 class="uk-heading-medium uk-text-center uk-margin-medium-top">Fleet Manager</h1>
<!-- 导航部分 -->
<div class="uk-card uk-card-default uk-card-body uk-margin-medium">
<h3 class="uk-card-title">导航服务</h3>
<div class="uk-grid-small uk-child-width-1-2@s uk-text-center" uk-grid>
<div>
<a href="/addr_input" class="uk-button uk-button-primary uk-width-1-1">
<span uk-icon="location"></span> 地址输入导航
</a>
</div>
<div>
<a href="/amap_addr_input" class="uk-button uk-button-primary uk-width-1-1">
<span uk-icon="location"></span> 高德地图导航
</a>
</div>
<div>
<a href="/tmap_addr_input" class="uk-button uk-button-primary uk-width-1-1">
<span uk-icon="location"></span> 腾讯地图导航
</a>
</div>
<div>
<a href="/tools" class="uk-button uk-button-secondary uk-width-1-1">
<span uk-icon="settings"></span> 系统设置
</a>
</div>
</div>
</div>
<!-- 视频和日志部分 -->
<div class="uk-card uk-card-default uk-card-body uk-margin-medium">
<h3 class="uk-card-title">视频和日志</h3>
<div class="uk-grid-small uk-child-width-1-2@s uk-text-center" uk-grid>
<div>
<div class="uk-card uk-card-default uk-card-body uk-box-shadow-small">
<h4>视频记录</h4>
<a href="/footage" class="uk-button uk-button-default uk-width-1-1 uk-margin-small-bottom">
<span uk-icon="video-camera"></span> 查看行车记录
</a>
<a href="/preserved" class="uk-button uk-button-default uk-width-1-1 uk-margin-small-bottom">
<span uk-icon="album"></span> 已保存视频
</a>
<a href="/screenrecords" class="uk-button uk-button-default uk-width-1-1">
<span uk-icon="desktop"></span> 屏幕录制
</a>
</div>
</div>
<div>
<div class="uk-card uk-card-default uk-card-body uk-box-shadow-small">
<h4>系统日志</h4>
<a href="/error_logs" class="uk-button uk-button-default uk-width-1-1 uk-margin-small-bottom">
<span uk-icon="warning"></span> 错误日志
</a>
<a href="/about" class="uk-button uk-button-default uk-width-1-1">
<span uk-icon="info"></span> 关于系统
</a>
</div>
</div>
</div>
</div>
<!-- 系统状态部分 -->
<div class="uk-card uk-card-default uk-card-body uk-margin-medium">
<h3 class="uk-card-title">系统状态</h3>
<div id="system-status" class="uk-grid-small uk-child-width-1-3@s uk-text-center" uk-grid>
<div>
<div class="uk-card uk-card-default uk-card-body uk-box-shadow-small">
<h4>导航状态</h4>
<div id="nav-status">
<span class="uk-label uk-label-warning">待机</span>
</div>
</div>
</div>
<div>
<div class="uk-card uk-card-default uk-card-body uk-box-shadow-small">
<h4>GPS信号</h4>
<div id="gps-status">
<span class="uk-label uk-label-warning">检测中</span>
</div>
</div>
</div>
<div>
<div class="uk-card uk-card-default uk-card-body uk-box-shadow-small">
<h4>网络状态</h4>
<div id="network-status">
<span class="uk-label uk-label-warning">检测中</span>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
// 定期更新系统状态
function updateSystemStatus() {
fetch('/get_system_status')
.then(response => response.json())
.then(data => {
// 更新导航状态
document.getElementById('nav-status').innerHTML =
`<span class="uk-label ${data.nav_status.active ? 'uk-label-success' : 'uk-label-warning'}">
${data.nav_status.state}
</span>`;
// 更新GPS状态
document.getElementById('gps-status').innerHTML =
`<span class="uk-label ${data.gps_status.active ? 'uk-label-success' : 'uk-label-warning'}">
${data.gps_status.signal}
</span>`;
// 更新网络状态
document.getElementById('network-status').innerHTML =
`<span class="uk-label ${data.network_status.connected ? 'uk-label-success' : 'uk-label-warning'}">
${data.network_status.type}
</span>`;
})
.catch(error => {
console.error('Error:', error);
// 发生错误时显示错误状态
['nav-status', 'gps-status', 'network-status'].forEach(id => {
document.getElementById(id).innerHTML =
`<span class="uk-label uk-label-danger">错误</span>`;
});
});
}
// 每5秒更新一次状态
setInterval(updateSystemStatus, 5000);
updateSystemStatus(); // 初始更新
</script>
{% endblock %}

View File

@ -0,0 +1,495 @@
{% extends "layout.html" %}
{% block title %}
腾讯地图导航
{% endblock %}
{% block main %}
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="initial-scale=1.0, user-scalable=no, width=device-width">
<title>地点搜索与导航</title>
<!-- UIkit CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/uikit@3.9.2/dist/css/uikit.min.css" />
<!-- UIkit JS -->
<script src="https://cdn.jsdelivr.net/npm/uikit@3.9.2/dist/js/uikit.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/uikit@3.9.2/dist/js/uikit-icons.min.js"></script>
<style type="text/css">
body {
margin: 0;
height: 100%;
width: 100%;
position: absolute;
}
#mapContainer {
width: 100%;
height: 80vh;
position: relative;
}
.search-box {
position: absolute;
top: 20px;
left: 20px;
right: 20px;
z-index: 999;
background: white;
padding: 10px;
border-radius: 3px;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
.search-results {
position: absolute;
top: 80px;
left: 20px;
right: 20px;
z-index: 998;
background: white;
border-radius: 3px;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
max-height: 300px;
overflow-y: auto;
display: none;
}
.search-result-item {
padding: 10px;
border-bottom: 1px solid #eee;
cursor: pointer;
}
.search-result-item:hover {
background-color: #f5f5f5;
}
</style>
<div class="search-box">
<div class="uk-grid-small" uk-grid>
<div class="uk-width-1-4">
<select id="save_type" class="uk-select">
<option value="recent">最近</option>
<option value="home">住家</option>
<option value="work">工作</option>
</select>
</div>
<div class="uk-width-expand">
<div class="uk-inline uk-width-1-1">
<span class="uk-form-icon" uk-icon="icon: search"></span>
<input class="uk-input" type="text" id="keyword"
placeholder="请输入关键字搜索地点"
autocomplete="off">
</div>
</div>
<div class="uk-width-auto">
<button class="uk-button uk-button-primary" onclick="searchPlace(document.getElementById('keyword').value)">
<span uk-icon="icon: search"></span> 搜索
</button>
</div>
</div>
</div>
<div id="search-results" class="search-results"></div>
<div id="mapContainer"></div>
<script charset="utf-8" src="https://map.qq.com/api/gljs?v=1.exp&key=BDMBZ-LZQ63-GUG37-OCHES-2ESXV-Q5BVC"></script>
<script type="text/javascript">
var map, marker, infoWindow;
var searchMarkers = [];
var initialPosition;
// 修改路径以确保能找到图标
var markerIconDefault = '../static/marker.png';
var markerIconBlue = '../static/marker.png'; // 使用同一个图标
// 添加Base64编码的备用图标
var markerIconDefaultBase64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAApCAYAAADAk4LOAAAGmklEQVRYw7VXeUyTZxjvNnfELFuyIzOabermMZEeQC/OclkO49CpOHXOLJl/CAURuYbQi3KLgEhbrhZ1aDwmaoGqKII6odATmH/scDFbdC7LvFqOCc+e95s73nc5enviW/7NC5xvPO/ne37vc6ETcbIi3Tg4GXRUIIsHzMOV4bXDIG1ZXasCzS2f2NTfmx0FcWyfxeIfCCIg8O4PLnJYPLQJjFnADUUi3P2JLpxV6VxsRonrGCS9VEjSQ9E+CYl60csasJFE5K948dlXTuQFr4GHrxqlwwQ5HrwiT+o0/lOgRsHKcHXcMXKzJ3Du11C6D1jGBRQwJpBFBsbivXC9xNN+Pw0FjbUP5XzBe261NvVp6DeF4HNXKjPwluE8wbj2WZdbigvrLhXe7CbG6cOTnhPsT0O6tx8XfrSXhx4Wq41bTyI3tDcCQLOQgn0z5nG1ISY0S2Mf2xSH0HcT0Jh8HAi2HRM4yzPdYrRGHf/y1+Ac2vxggXZpE8VZ4Mmx/oKDdEwCmXR5zOk51x4UGJnm9y4CKs1tI4b0Zn5zS6tVmQ+snR5D8a2dl3wzTu8enggR80xH3ktFr1eIKelxk9ojHvBaL9V+48S+cuD1oD0Qmwt6ZiVuFXNzXJoabewoX0dcdmw/+wK9HV1BErkB8QhLUCZw/aqzVjTqTniT6HV0dQk4+tUw89axMjQ5ieojXD2bPkEd+s1ljW7JslQt7wo4gVN7MgZmCLi0CeDII0HbMm8sWl/DEfhSU0r31OuUHiVJM2aFperZV0yXu+cj7YnB4kJgRw14kf0YcC5iDOLRZhYY7ZvzzuJJmZnAvi1HZK8XM11FUCV4dWnOBs2k2j0P8dc95vlxybNaECYslJt6SKuBCFQZza3iu0PYtk4+xh/NLDCvEMwPyCQ5Y8LjkIeqZXa/TZp7pS53Z3pbdREg7/U80IQHALuOGnEn0nL768qX3KL5DxrPmFcgwNWO3gL++bq78xLl4KmXUXd4ARP9z+BdReUXbFSIbZl9PtsxgxOCzboi2cexnwTwj8Si+fC9jrOmD4/4/+ALbM3GOPMk83VzeCRvckY/sZek88BTCZURADr4yw8QAl8KALQRmKmgTIPBptVxehrdUFSw2AB0FkzkoHvHo31Cf/2xPOazs39Gayabk2P6StDQWXgY0n4OhAJrgJoYFPeBhyY2K4HmmiRBuYL25nzEMgZ3DWmAdr/NfhomM9TxrrC1mmCIgQs1BygBiCpwbF5erV5jYOFDLUQAg52A7WuJ9N8DJ7OZbKEhU5J8wGTb+AXTl1dqxeQJkCv0TxHVUxkXVIqpAMT7rJzGvnzTTdcGavsLC/Y3EO4JuGf4K1NW3HADQXmWmDx/zZ43+wVY0xQ4Lwn/7e8yYGRjq7Brz9EfRvZpx89vMBmxeuMYE1jQojzGJR556mXbGLXJn78bLPSDRVaZeEYrLDSv2L5q+1zDQN6m3hkyf6X7LSMTfuCPH+qRZ88gXJfC+KbQ8crYNB0HTEuc5/nImHoSJTPAewjc/y5OGbouZoMSUPdtyYx6PgHL3Y6JlWjlLqQTVQl1U1YG7UVj5v9Pt/hkCe3GhF6WgusZJ9jpqN19K0qbSjOIxg4xtSgbhO2nlFpUhDCGZ//QiYx1uRPnB+g9PdFGJQLkXnwDHi7UFNL2YB9+K0qf84QfvXmDjw8Hp8RG9ZF3yZ1mUoibMSJ3F3qVSoZiYDl7cXzvONO/53Z/7OO3Mzu/8Gv2Nszz3v9XosPRzAGjJfgz2nzjqOz8i9wJrTKJhH7x3HdKQgW5Z7K+wSJA2wD10wceBvv6C4iDPkbP0KZK0ASoKO1GVwd0hQXed25CdUlLuFqr1n6fB+l2QPpVpNmzLcm8/n/sbPhh/x1jvuS6cMDqNHZ7TE0kdGF+522Ms/qslLnL4j7ejAkpMH818O7cS1qy22bRr0ltxoidgWM1QvqwRZmcMTTE8z+/L+iibyijb54TLXNlqd+P01LdBdi+NbBfC9go/ynjykJOTgf7/C6KxFET+28HuG/j+JDd5AWKFsOofm9Q/y4QFpYthUP8XfcZ6Lq6xX/o8lOxeMjH5u+PgJ3XgMy8NlqsGut1a9rP4T39C5dEZ/u7mCeCY8OTWs3n5P3n3hu5HdjXHPlgZM7UpwpOlJtBr5EYl12yCBZVcxzFUcTGSHaFdExuD7/uZxd7O2Ie/+GD5HuVeR/FhNXSlRPSnVmP3S9xH+rNZWXCMlEo1XaWdDyT/7hDGQ7qWUQvQ0H8kgK2GLHjtPb3NlAMwwHXZq85wOJS/J/vCfDULuuc1w5xGNBR/ZIxOMf4wYHsP5YuwXvONV/hgQKNT+pyH2e48faP48n6iLWzafcYn2xcMEF9op/Fh7B3ONjyMau8qjJxTfbr8E3r7RYyC0ndvpG3hITQTXKO5k9ZQM7Zn4KtWniw0rPBiidwZRdxn5bA3LzcEUABZ/4xCb9+5QpQqBTQ2+3Xdgu+/eJP8Suy8RgAAAABJRU5ErkJggg==';
var markerIconBlueBase64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAApCAYAAADAk4LOAAAFgUlEQVR4Aa1XA5BjWRTN2oW17d3YaZtr2962HUzbDNpjszW24mRt28p47v7zq/bXZtrp/lWnXr337j3nnnvuFdcMDA8FDh69Xw8BRQnGyDdCY/X1rVpXb1xdGf2CgufMeiF4fY4QTKwvKRLJBPrDw36bQbH+8Vg54nW5XeaEsaX2XMaoktMsZB9OMWjsmaxjXzM6DtgBq+juYZX9s1gHwbtyXnqBLp7YlOLZFckN2gQsfXo76xvgUOrTCzQDlq4gw+KqmiQvbH+vQP7lJPuaCTjiPuDh/6YQY0OHUKb/mFVnvPBCf0+nbQTQE5hDlgPO6JLyY8d94K5IR1QT0qwzsSWlwP+EsdEdpGgIYleQbLKcNH7Yw+iPZu12L8oRgwYGytKmrJRrGmylMNMqLIpvn71lgM3+HAukaiWHY1Ii3ViHxzu4YkmoELpTyjVF9g0IKCnJnJDJxL8QUJjuOWWmdOYCiNyUmgSwBDeCRJ3BkW582zQy3hKXvZXU18CYGMgjBurq8vLiEgbPgwXRDCRFS2RWYFNG6YBC8oQvZQku2XgEPrRP6RgUAAxlpMZsUD82dTip3VORACNXC2LbXAdCXN/btMD8XocjQFC9n7PjwEO0Dv70l52/Px24k1Buu42t0HDmD5mi7LXwJwiU3gUXB/lKsf0XqDeOlQDTIbUFahVp6neifb2j43OjS0PT9xrZklTPQctAm0MQQUq0mcXd9lKCZefPNnUKk0f5zeD+nj2wMDY0FjnxEjU9ufWQgp4QVvRUGg42O0pj4qDoiXj3TmG5Pm0GXEfBw/HWmDeWLDHcZr7E5xVOkCTygXsUYgXKEEWdTcNR+lklL3ab8F72vKn2Fpf5fJQiJTE0XBMQmYrQDqYmzcaGgXeHH6HE+JGJ4pIrDG/u3BaHU3Z1T+6+vYy+1urNG1Dpeqxw+3GQ5xfuQLrHf/c3yY8+f3lySJgkt1e0VAFMd/5KnL3+NZbXUHfmrLFXcszVsy/Gg5dhuPLNrR11PJGMMICAwJNrwQEfVJZiwwbU4p0EX1HUQSQKUUFXdL00bZJnzAIBBfk/h1trGL+hTfdXQovzqb5m9tnh/OzlOPGx8/uTX39ovVccWq9QhwVQppEXY9Bk/cJv/R8UTxIxkiOuM7CZxo4TqwYcWDNApmyiFOdyDXaQEo0dGMDIgkT8kPz85TOx1tmM4VKWC2adWKsHER+GCSITMRUt5AS7tC1UH6jlv6Zcpj7QAFVLjMelBEUBcphQCI6wgG7fWqxMJ1QxMAXwVdOvRT7NdVesrJoFdVrU+y6UO8jaNrNKiZnKAGDiF0a0jCBHW5N9x5mDTaEejDD2q0v616PntKLd21H9fz1X1u2zHXY6qOZ6KW8D00z/3Loyh/Cx2CbZ8wgyQAAAABJRU5ErkJggg==';
// 添加图标加载检查
function checkImageExists(url, callback) {
var img = new Image();
img.onload = function() { callback(true); };
img.onerror = function() { callback(false); };
img.src = url;
}
// 初始化地图
function initMap() {
// 检查图标是否可用
checkImageExists(markerIconDefault, function(exists) {
if (!exists) {
console.error("图标加载失败,尝试其他路径");
// 尝试其他可能的路径
markerIconDefault = '/static/marker.png';
markerIconBlue = '/static/marker.png';
// 如果仍然失败使用Base64编码的图标
checkImageExists(markerIconDefault, function(exists) {
if (!exists) {
console.error("备用路径也失败使用Base64图标");
markerIconDefault = markerIconDefaultBase64;
markerIconBlue = markerIconBlueBase64;
}
// 现在初始化地图和标记
initMapWithCheckedIcons();
});
} else {
// 图标存在,初始化地图和标记
initMapWithCheckedIcons();
}
});
}
// 使用已检查的图标初始化地图
function initMapWithCheckedIcons() {
// 使用Number()转换确保经纬度是数字
initialPosition = new TMap.LatLng(Number("{{lat}}"), Number("{{lon}}"));
map = new TMap.Map('mapContainer', {
center: initialPosition,
zoom: 15,
showControl: true
});
// 创建当前位置标记
marker = new TMap.MultiMarker({
map: map,
styles: {
"default": new TMap.MarkerStyle({
width: 25,
height: 35,
anchor: { x: 12.5, y: 35 },
src: markerIconDefault
})
},
geometries: [{
id: 'current',
position: initialPosition,
styleId: 'default'
}]
});
// 添加点击事件
map.on('click', handleMapClick);
// 添加标记点击事件
marker.on('click', function(evt) {
showInfoWindow(
evt.geometry.position,
"选定位置",
"(" + evt.geometry.position.lat.toFixed(6) + ", " + evt.geometry.position.lng.toFixed(6) + ")"
);
});
}
// 处理地图点击
function handleMapClick(evt) {
var position = evt.latLng;
// 进行坐标逆解析,获取地点信息
getAddressByLocation(position, function(result) {
// 更新当前位置标记
marker.updateGeometries([{
id: 'current',
position: position,
styleId: 'default'
}]);
// 使用搜索框文本或逆解析结果显示信息窗口
var keyword = document.getElementById('keyword').value;
if (keyword && keyword.trim() !== '') {
showInfoWindow(position, keyword, result && result.address);
} else if (result && result.address) {
showInfoWindow(position, result.formatted_addresses?.recommend || "选定位置", result.address);
} else {
showInfoWindow(position, "选定位置", `(${position.lat.toFixed(6)}, ${position.lng.toFixed(6)})`);
}
// 隐藏搜索结果
document.getElementById('search-results').style.display = 'none';
});
}
// 根据坐标获取地址信息
function getAddressByLocation(location, callback) {
var callbackName = 'jsonp_reverseGeo_' + Math.round(100000 * Math.random());
window[callbackName] = function(res) {
// 移除script标签
var scriptTags = document.getElementsByTagName('script');
for (var i = 0; i < scriptTags.length; i++) {
if (scriptTags[i].src.indexOf(callbackName) > -1) {
document.body.removeChild(scriptTags[i]);
break;
}
}
if (res.status === 0 && res.result) {
callback(res.result);
} else {
callback(null);
}
// 清理回调函数
delete window[callbackName];
};
// 构建请求URL
var url = "https://apis.map.qq.com/ws/geocoder/v1/?location=" +
location.lat + "," + location.lng +
"&key=BDMBZ-LZQ63-GUG37-OCHES-2ESXV-Q5BVC&output=jsonp&callback=" + callbackName;
// 发送JSONP请求
var script = document.createElement('script');
script.src = url;
script.onerror = function() {
callback(null);
delete window[callbackName];
};
document.body.appendChild(script);
}
// 显示搜索结果
function displaySearchResults(places) {
if (!places || places.length === 0) return;
// 清除之前的搜索标记
clearSearchMarkers();
// 准备标记数据
var geometries = [];
// 创建结果标记
places.forEach(function(place, index) {
var position = new TMap.LatLng(place.location.lat, place.location.lng);
geometries.push({
id: 'search_' + index,
position: position,
styleId: 'default',
properties: {
title: place.title,
address: place.address
}
});
});
// 创建标记图层 - 使用蓝色水滴图标
var resultMarker = new TMap.MultiMarker({
map: map,
styles: {
"default": new TMap.MarkerStyle({
width: 25,
height: 35,
anchor: { x: 12.5, y: 35 },
src: markerIconBlue // 使用蓝色图标
})
},
geometries: geometries
});
// 添加点击事件
resultMarker.on('click', function(evt) {
var position = evt.geometry.position;
var props = evt.geometry.properties;
// 使用正确的标题和地址信息
showInfoWindow(position, props.title, props.address);
// 更新当前标记
marker.updateGeometries([{
id: 'current',
position: position,
styleId: 'default'
}]);
// 隐藏搜索结果
document.getElementById('search-results').style.display = 'none';
});
// 保存到标记数组用于后续清除
searchMarkers.push(resultMarker);
// 调整地图视野以包含所有标记
if (places.length === 1) {
// 单个结果直接定位
map.setCenter(geometries[0].position);
map.setZoom(16);
// 自动打开信息窗口
showInfoWindow(
geometries[0].position,
places[0].title,
places[0].address
);
} else {
// 多个结果时调整视野包含所有标记
var bounds = new TMap.LatLngBounds();
geometries.forEach(function(geo) {
bounds.extend(geo.position);
});
map.fitBounds(bounds);
// 显示提示
UIkit.notification({
message: `找到 ${places.length} 个结果,请点击标记查看详情`,
status: 'success',
pos: 'top-center',
timeout: 3000
});
}
}
// 显示信息窗口
function showInfoWindow(position, title, address) {
if (infoWindow) {
infoWindow.close();
}
// 获取真实的地点名称和地址
var placeName = title || document.getElementById('keyword').value || "选定位置";
var placeAddress = address || `(${position.lat.toFixed(6)}, ${position.lng.toFixed(6)})`;
console.log("显示信息窗口:", placeName, placeAddress);
// 创建信息窗口
infoWindow = new TMap.InfoWindow({
map: map,
position: position,
content: `
<div class="uk-card uk-card-default uk-card-body" style="padding:15px; min-width:220px;">
<h3 style="margin-top:0; margin-bottom:10px; font-size:16px; font-weight:bold;">${placeName}</h3>
<p style="margin:0; color:#666; font-size:14px;">${placeAddress}</p>
<div style="margin-top:15px; text-align:center;">
<form name="navForm" method="post" onsubmit="return validateNavForm()">
<input type="hidden" name="lat" value="${position.lat}">
<input type="hidden" name="lon" value="${position.lng}">
<input type="hidden" name="save_type" value="${document.getElementById('save_type').value}">
<input type="hidden" id="place_name_field" name="place_name" value="${placeName}">
<input class="uk-button uk-button-primary" type="submit" value="导航" style="width:100px;">
</form>
</div>
</div>
`,
offset: { x: 0, y: -35 }
});
// 打开信息窗口
infoWindow.open();
}
// 验证导航表单确保地名被传递
function validateNavForm() {
var placeNameField = document.getElementById('place_name_field');
// 如果地名为空,尝试从搜索框获取
if (!placeNameField.value || placeNameField.value === "选定位置") {
var keyword = document.getElementById('keyword').value;
if (keyword && keyword.trim() !== '') {
placeNameField.value = keyword;
console.log("从搜索框获取地名:", keyword);
}
}
// 调试输出
console.log("提交导航表单,地名:", placeNameField.value);
return true;
}
// 搜索地点
function searchPlace(keyword) {
if (!keyword || keyword.trim() === '') {
UIkit.notification({
message: '请输入搜索关键词',
status: 'warning',
pos: 'top-center',
timeout: 3000
});
return;
}
// 显示加载提示
console.log("开始搜索:", keyword);
UIkit.notification({
message: '正在搜索...',
status: 'primary',
pos: 'top-center',
timeout: 2000
});
// 清除之前的标记
clearSearchMarkers();
// 创建JSONP回调
var callbackName = 'jsonp_search_' + Math.round(100000 * Math.random());
// 定义回调函数
window[callbackName] = function(res) {
console.log("搜索结果:", res);
// 移除script标签
var scriptTags = document.getElementsByTagName('script');
for (var i = 0; i < scriptTags.length; i++) {
if (scriptTags[i].src.indexOf(callbackName) > -1) {
document.body.removeChild(scriptTags[i]);
break;
}
}
// 处理搜索结果
if (res.status === 0 && res.data && res.data.length > 0) {
// 显示搜索结果
displaySearchResults(res.data);
} else {
UIkit.notification({
message: '未找到相关结果',
status: 'warning',
pos: 'top-center',
timeout: 3000
});
}
// 清理回调函数
delete window[callbackName];
};
// 构建搜索请求URL
var searchUrl = "https://apis.map.qq.com/ws/place/v1/search?keyword=" +
encodeURIComponent(keyword) +
"&boundary=region(全国,0)" +
"&page_size=10" +
"&page_index=1" +
"&key=BDMBZ-LZQ63-GUG37-OCHES-2ESXV-Q5BVC&output=jsonp&callback=" + callbackName;
// 发送JSONP请求
var script = document.createElement('script');
script.src = searchUrl;
script.onerror = function() {
UIkit.notification({
message: '搜索请求失败,请检查网络连接',
status: 'danger',
pos: 'top-center',
timeout: 3000
});
delete window[callbackName];
};
document.body.appendChild(script);
}
// 清除搜索标记
function clearSearchMarkers() {
searchMarkers.forEach(function(marker) {
marker.setMap(null);
});
searchMarkers = [];
}
// 初始化地图
initMap();
</script>
{% endblock %}

View File

@ -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}

View File

@ -791,7 +791,7 @@
</message>
<message>
<source>NVMe drive not mounted.</source>
<translation>NVMe固态硬盘未被挂载</translation>
<translation>GM 使openpilot知识库</translation>
</message>
<message>
<source>Unsupported NVMe drive detected. Device may draw significantly more power and overheat due to the unsupported NVMe.</source>

View File

@ -437,7 +437,7 @@
</message>
<message>
<source>NVMe drive not mounted.</source>
<translation>NVMe </translation>
<translation>GM 使openpilot知识库</translation>
</message>
<message>
<source>Unsupported NVMe drive detected. Device may draw significantly more power and overheat due to the unsupported NVMe.</source>