Compare commits
10 Commits
c53fb29ae7
...
68fa4aea93
Author | SHA1 | Date | |
---|---|---|---|
![]() |
68fa4aea93 | ||
![]() |
e2a80356c8 | ||
![]() |
37da15755b | ||
![]() |
e3d6f80710 | ||
![]() |
439c6e7d7f | ||
![]() |
341f9478ec | ||
![]() |
b659f68bc8 | ||
![]() |
044e8371a4 | ||
![]() |
779c2d32c4 | ||
![]() |
b630cc541c |
1096
RELEASES.md
1096
RELEASES.md
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
|
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()
|
@ -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])
|
||||
|
@ -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": "未知"}
|
||||
|
BIN
selfdrive/frogpilot/fleetmanager/static/marker.png
Normal file
BIN
selfdrive/frogpilot/fleetmanager/static/marker.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.7 KiB |
@ -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 %}
|
||||
|
495
selfdrive/frogpilot/fleetmanager/templates/tmap_addr_input.html
Normal file
495
selfdrive/frogpilot/fleetmanager/templates/tmap_addr_input.html
Normal 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 %}
|
@ -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}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user