Release 260111
This commit is contained in:
5
selfdrive/frogpilot/fleetmanager/README.md
Normal file
5
selfdrive/frogpilot/fleetmanager/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Fleet Manager
|
||||
|
||||
Fleet Manager on openpilot allows viewing dashcam footage, screen recordings, error logs and on-device navigation by connecting to the comma device via the same network, with your mobile device or PC. Big thanks to [actuallylemoncurd](https://github.com/actuallylemoncurd), [AlexandreSato](https://github.com/alexandreSato), [ntegan1](https://github.com/ntegan1), [royjr](https://github.com/royjr), [sunnyhaibin] (https://github.com/sunnypilot), [dragonpilot](https://github.com/dragonpilot-community) and [chatgpt](https://chat.openai.com/).
|
||||
|
||||
The network can be set up by Wi-Fi, mobile hotspot, or tethering on the comma device. Navigate to http://ipAddress:8082 to access.
|
||||
711
selfdrive/frogpilot/fleetmanager/fleet_manager.py
Normal file
711
selfdrive/frogpilot/fleetmanager/fleet_manager.py
Normal file
@@ -0,0 +1,711 @@
|
||||
#!/usr/bin/env python3
|
||||
# otisserv - Copyright (c) 2019-, Rick Lan, dragonpilot community, and a number of other of contributors.
|
||||
# Fleet Manager - [actuallylemoncurd](https://github.com/actuallylemoncurd), [AlexandreSato](https://github.com/alexandreSato),
|
||||
# [ntegan1](https://github.com/ntegan1), [royjr](https://github.com/royjr), and [sunnyhaibin] (https://github.com/sunnypilot)
|
||||
# Almost everything else - ChatGPT
|
||||
# dirty PR pusher - mike8643
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
import os
|
||||
import secrets
|
||||
|
||||
from flask import Flask, jsonify, render_template, Response, request, send_from_directory, redirect, url_for, abort
|
||||
from openpilot.common.realtime import set_core_affinity
|
||||
import openpilot.selfdrive.frogpilot.fleetmanager.helpers as fleet
|
||||
from openpilot.system.hardware.hw import Paths
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
import traceback
|
||||
from ftplib import FTP
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from cereal import log, messaging
|
||||
from cereal import log, messaging
|
||||
import time
|
||||
from functools import wraps
|
||||
from openpilot.opendbc_repo.opendbc.car.interfaces import CarInterfaceBase
|
||||
from openpilot.opendbc_repo.opendbc.car.values import PLATFORMS
|
||||
|
||||
# Initialize messaging
|
||||
sm = messaging.SubMaster(['carState'])
|
||||
tempseg = -1
|
||||
temproute = "None"
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route("/")
|
||||
def home_page():
|
||||
return render_template("index.html")
|
||||
|
||||
@app.errorhandler(500)
|
||||
def internal_error(exception):
|
||||
print('500 error caught')
|
||||
tberror = traceback.format_exc()
|
||||
return render_template("error.html", error=tberror)
|
||||
|
||||
@app.route("/footage/full/<cameratype>/<route>")
|
||||
def full(cameratype, route):
|
||||
chunk_size = 1024 * 512
|
||||
file_name = cameratype + (".ts" if cameratype == "qcamera" else ".hevc")
|
||||
vidlist = "|".join(Paths.log_root() + "/" + segment + "/" + file_name for segment in fleet.segments_in_route(route))
|
||||
|
||||
def generate_buffered_stream():
|
||||
with fleet.ffmpeg_mp4_concat_wrap_process_builder(vidlist, cameratype, chunk_size) as process:
|
||||
for chunk in iter(lambda: process.stdout.read(chunk_size), b""):
|
||||
yield bytes(chunk)
|
||||
return Response(generate_buffered_stream(), status=200, mimetype='video/mp4')
|
||||
|
||||
@app.route("/footage/full/rlog/<route>/<segment>")
|
||||
def download_rlog(route, segment):
|
||||
file_name = Paths.log_root() + route + "--" + segment + "/"
|
||||
print("download_route=", route, file_name, segment)
|
||||
return send_from_directory(file_name, "rlog.zst", as_attachment=True)
|
||||
|
||||
@app.route("/footage/full/qcamera/<route>/<segment>")
|
||||
def download_qcamera(route, segment):
|
||||
file_name = Paths.log_root() + route + "--" + segment + "/"
|
||||
print("download_route=", route, file_name, segment)
|
||||
return send_from_directory(file_name, "qcamera.ts", as_attachment=True)
|
||||
|
||||
@app.route("/footage/full/fcamera/<route>/<segment>")
|
||||
def download_fcamera(route, segment):
|
||||
file_name = Paths.log_root() + route + "--" + segment + "/"
|
||||
print("download_route=", route, file_name, segment)
|
||||
return send_from_directory(file_name, "fcamera.hevc", as_attachment=True)
|
||||
|
||||
@app.route("/footage/full/dcamera/<route>/<segment>")
|
||||
def download_dcamera(route, segment):
|
||||
file_name = Paths.log_root() + route + "--" + segment + "/"
|
||||
print("download_route=", route, file_name, segment)
|
||||
return send_from_directory(file_name, "dcamera.hevc", as_attachment=True)
|
||||
|
||||
@app.route("/footage/full/ecamera/<route>/<segment>")
|
||||
def download_ecamera(route, segment):
|
||||
file_name = Paths.log_root() + route + "--" + segment + "/"
|
||||
print("download_route=", route, file_name, segment)
|
||||
return send_from_directory(file_name, "ecamera.hevc", as_attachment=True)
|
||||
|
||||
def upload_folder_to_ftp(local_folder, directory, remote_path):
|
||||
from tqdm import tqdm
|
||||
ftp_server = "shind0.synology.me"
|
||||
ftp_port = 8021
|
||||
ftp_username = "carrotpilot"
|
||||
ftp_password = "Ekdrmsvkdlffjt7710"
|
||||
ftp = FTP()
|
||||
ftp.connect(ftp_server, ftp_port)
|
||||
ftp.login(ftp_username, ftp_password)
|
||||
|
||||
ftp.cwd("routes")
|
||||
|
||||
try:
|
||||
def create_path(path):
|
||||
try:
|
||||
ftp.mkd(path)
|
||||
except:
|
||||
pass
|
||||
ftp.cwd(path)
|
||||
|
||||
for part in [directory, remote_path]:
|
||||
create_path(part)
|
||||
|
||||
files = []
|
||||
for root, _, filenames in os.walk(local_folder):
|
||||
for filename in filenames:
|
||||
if filename in ['rlog.zst', 'qcamera.ts']:
|
||||
files.append(os.path.join(root, filename))
|
||||
|
||||
with tqdm(total=len(files), desc="Uploading Files", unit="file") as pbar:
|
||||
for local_file in files:
|
||||
filename = os.path.basename(local_file)
|
||||
try:
|
||||
with open(local_file, 'rb') as f:
|
||||
ftp.storbinary(f'STOR {filename}', f)
|
||||
pbar.update(1)
|
||||
except Exception as e:
|
||||
print(f"Failed to upload {local_file}: {e}")
|
||||
|
||||
ftp.quit()
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"FTP Upload Error: {e}")
|
||||
return False
|
||||
|
||||
|
||||
@app.route("/folder-info")
|
||||
def get_folder_info():
|
||||
path = request.args.get('path')
|
||||
if not path or not os.path.exists(path):
|
||||
return jsonify({'error': 'Folder not found'}), 404
|
||||
|
||||
try:
|
||||
folder_name = os.path.basename(path)
|
||||
seg_num = int(folder_name.split('--')[2])
|
||||
stat_info = os.stat(path)
|
||||
created_time = stat_info.st_ctime
|
||||
if seg_num == 0:
|
||||
try:
|
||||
base_name = '--'.join(folder_name.split('--')[:2])
|
||||
seg1_path = os.path.join(os.path.dirname(path), f"{base_name}--1")
|
||||
|
||||
if os.path.exists(seg1_path):
|
||||
seg1_stat = os.stat(seg1_path)
|
||||
created_time = seg1_stat.st_ctime - 60
|
||||
except Exception as e:
|
||||
print(f"Error calculating time for segment 0: {e}")
|
||||
formatted_date = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(created_time))
|
||||
total_size = 0
|
||||
for dirpath, dirnames, filenames in os.walk(path):
|
||||
for f in filenames:
|
||||
fp = os.path.join(dirpath, f)
|
||||
try:
|
||||
total_size += os.path.getsize(fp)
|
||||
except OSError:
|
||||
continue
|
||||
|
||||
return jsonify({
|
||||
'created_date': formatted_date,
|
||||
'size': total_size,
|
||||
'status': 'success'
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route("/folder-date")
|
||||
def get_folder_date():
|
||||
path = request.args.get('path')
|
||||
subtract_minutes = int(request.args.get('subtract_minutes', 0))
|
||||
|
||||
if not path or not os.path.exists(path):
|
||||
return jsonify({'error': 'Folder not found'}), 404
|
||||
|
||||
try:
|
||||
stat_info = os.stat(path)
|
||||
created_time = stat_info.st_ctime
|
||||
|
||||
if subtract_minutes > 0:
|
||||
created_time -= subtract_minutes * 60
|
||||
|
||||
formatted_date = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(created_time))
|
||||
|
||||
return jsonify({
|
||||
'date': formatted_date,
|
||||
'status': 'success'
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
@app.route("/folder-exists")
|
||||
def check_folder_exists():
|
||||
path = request.args.get('path')
|
||||
exists = os.path.exists(path) if path else False
|
||||
return jsonify({'exists': exists})
|
||||
|
||||
@app.route("/footage/full/upload_carrot/<route>/<segment>", methods=['POST'])
|
||||
def upload_carrot(route, segment):
|
||||
global tempseg
|
||||
global temproute
|
||||
if tempseg != segment or temproute != route:
|
||||
local_folder = os.path.join(Paths.log_root(), f"{route}--{segment}")
|
||||
if not os.path.isdir(local_folder):
|
||||
abort(404, "Folder not found")
|
||||
car_selected = Params().get("CarName", "none").decode('utf-8')
|
||||
dongle_id = Params().get("DongleId", "unknown").decode('utf-8')
|
||||
directory = f"{car_selected} {dongle_id}"
|
||||
success = upload_folder_to_ftp(local_folder, directory, f"{route}--{segment}")
|
||||
if success:
|
||||
temproute = route
|
||||
tempseg = segment
|
||||
return "All files uploaded successfully", 200
|
||||
else:
|
||||
return "Failed to upload files", 500
|
||||
else:
|
||||
return "Segment already uploaded", 200
|
||||
|
||||
@app.template_filter('datetimeformat')
|
||||
def datetimeformat_filter(filename):
|
||||
try:
|
||||
date_part = filename[:8]
|
||||
time_part = filename[9:15]
|
||||
|
||||
year = date_part[:4]
|
||||
month = date_part[4:6]
|
||||
day = date_part[6:8]
|
||||
|
||||
hour = time_part[:2]
|
||||
minute = time_part[2:4]
|
||||
|
||||
return f"{year}년 {month}월 {day}일 {hour}시 {minute}분"
|
||||
except:
|
||||
return filename
|
||||
|
||||
@app.route("/file-size")
|
||||
def get_file_size():
|
||||
path = request.args.get('path')
|
||||
try:
|
||||
size = os.path.getsize(path)
|
||||
return jsonify({'size': size, 'status': 'success'})
|
||||
except Exception as e:
|
||||
return jsonify({'size': 0, 'status': str(e)}), 404
|
||||
|
||||
@app.route("/footage/<cameratype>/<segment>")
|
||||
def fcamera(cameratype, segment):
|
||||
if not fleet.is_valid_segment(segment):
|
||||
return render_template("error.html", error="invalid segment")
|
||||
file_name = Paths.log_root() + "/" + segment + "/" + cameratype + (".ts" if cameratype == "qcamera" else ".hevc")
|
||||
return Response(fleet.ffmpeg_mp4_wrap_process_builder(file_name).stdout.read(), status=200, mimetype='video/mp4')
|
||||
|
||||
|
||||
@app.route("/footage/<route>")
|
||||
def route(route):
|
||||
if len(route) != 20:
|
||||
return render_template("error.html", error="route not found")
|
||||
|
||||
query_params = request.query_string.decode('utf-8').split(',')
|
||||
if len(query_params) >= 2:
|
||||
query_segment = query_params[0]
|
||||
query_type = query_params[1]
|
||||
else:
|
||||
query_segment = "0"
|
||||
query_type = "qcamera"
|
||||
|
||||
links = []
|
||||
segments = []
|
||||
for segment in fleet.segments_in_route(route):
|
||||
seg_num = segment.split("--")[2]
|
||||
links.append(f'<a href="{route}?{seg_num},{query_type}">{segment}</a>')
|
||||
segments.append(f"'{segment}'")
|
||||
|
||||
return render_template("route.html",
|
||||
route=route,
|
||||
query_type=query_type,
|
||||
links="<br>".join(links),
|
||||
segments=",".join(segments),
|
||||
query_segment=query_segment)
|
||||
|
||||
|
||||
@app.route("/footage/")
|
||||
@app.route("/footage")
|
||||
def footage():
|
||||
route_paths = fleet.all_routes()
|
||||
gifs = []
|
||||
for route_path in route_paths:
|
||||
input_path = Paths.log_root() + route_path + "--0/qcamera.ts"
|
||||
output_path = Paths.log_root() + route_path + "--0/preview.gif"
|
||||
fleet.video_to_img(input_path, output_path)
|
||||
gif_path = route_path + "--0/preview.gif"
|
||||
gifs.append(gif_path)
|
||||
zipped = zip(route_paths, gifs, strict=False)
|
||||
return render_template("footage.html", zipped=zipped)
|
||||
|
||||
@app.route("/preserved/")
|
||||
@app.route("/preserved")
|
||||
def preserved():
|
||||
query_type = "qcamera"
|
||||
route_paths = []
|
||||
gifs = []
|
||||
segments = fleet.preserved_routes()
|
||||
for segment in segments:
|
||||
input_path = Paths.log_root() + segment + "/qcamera.ts"
|
||||
output_path = Paths.log_root() + segment + "/preview.gif"
|
||||
fleet.video_to_img(input_path, output_path)
|
||||
split_segment = segment.split("--")
|
||||
route_paths.append(f"{split_segment[0]}--{split_segment[1]}?{split_segment[2]},{query_type}")
|
||||
gif_path = segment + "/preview.gif"
|
||||
gifs.append(gif_path)
|
||||
|
||||
zipped = zip(route_paths, gifs, segments, strict=False)
|
||||
return render_template("preserved.html", zipped=zipped)
|
||||
|
||||
@app.route("/screenrecords/")
|
||||
@app.route("/screenrecords")
|
||||
def screenrecords():
|
||||
rows = fleet.list_file(fleet.SCREENRECORD_PATH)
|
||||
if not rows:
|
||||
return render_template("error.html", error="no screenrecords found")
|
||||
files_with_size = []
|
||||
for file in rows:
|
||||
file_path = os.path.join(fleet.SCREENRECORD_PATH, file)
|
||||
size_bytes = os.path.getsize(file_path) if os.path.exists(file_path) else 0
|
||||
files_with_size.append((file, size_bytes))
|
||||
return render_template("screenrecords.html", rows=files_with_size, clip=rows[0])
|
||||
|
||||
|
||||
@app.route("/screenrecords/<clip>")
|
||||
def screenrecord(clip):
|
||||
rows = fleet.list_file(fleet.SCREENRECORD_PATH)
|
||||
files_with_size = []
|
||||
for file in rows:
|
||||
file_path = os.path.join(fleet.SCREENRECORD_PATH, file)
|
||||
size_bytes = os.path.getsize(file_path) if os.path.exists(file_path) else 0
|
||||
files_with_size.append((file, size_bytes))
|
||||
return render_template("screenrecords.html", rows=files_with_size, clip=clip)
|
||||
|
||||
|
||||
@app.route("/screenrecords/play/pipe/<file>")
|
||||
def videoscreenrecord(file):
|
||||
file_name = fleet.SCREENRECORD_PATH + file
|
||||
return Response(fleet.ffplay_mp4_wrap_process_builder(file_name).stdout.read(), status=200, mimetype='video/mp4')
|
||||
|
||||
|
||||
@app.route("/screenrecords/download/<clip>")
|
||||
def download_file(clip):
|
||||
return send_from_directory(fleet.SCREENRECORD_PATH, clip, as_attachment=True)
|
||||
|
||||
|
||||
@app.route("/about")
|
||||
def about():
|
||||
return render_template("about.html")
|
||||
|
||||
|
||||
@app.route("/error_logs")
|
||||
def error_logs():
|
||||
rows = fleet.list_file(fleet.ERROR_LOGS_PATH)
|
||||
if not rows:
|
||||
return render_template("error.html", error="no error logs found at:<br><br>" + fleet.ERROR_LOGS_PATH)
|
||||
return render_template("error_logs.html", rows=rows)
|
||||
|
||||
|
||||
@app.route("/error_logs/<file_name>")
|
||||
def open_error_log(file_name):
|
||||
f = open(fleet.ERROR_LOGS_PATH + file_name)
|
||||
error = f.read()
|
||||
return render_template("error_log.html", file_name=file_name, file_content=error)
|
||||
|
||||
@app.route("/addr_input", methods=['GET', 'POST'])
|
||||
def addr_input():
|
||||
preload = fleet.preload_favs()
|
||||
SearchInput = fleet.get_SearchInput()
|
||||
token = fleet.get_public_token()
|
||||
s_token = fleet.get_app_token()
|
||||
gmap_key = fleet.get_gmap_key()
|
||||
PrimeType = fleet.get_PrimeType()
|
||||
lon = 0.0
|
||||
lat = 0.0
|
||||
print(f"Request method: {request.method}, SearchInput: {SearchInput}, token: {token}, s_token: {s_token}, gmap_key: {gmap_key}, PrimeType: {PrimeType}")
|
||||
if request.method == 'POST':
|
||||
valid_addr = False
|
||||
postvars = request.form.to_dict()
|
||||
addr, lon, lat, valid_addr, token = fleet.parse_addr(postvars, lon, lat, valid_addr, token)
|
||||
if not valid_addr:
|
||||
# If address is not found, try searching
|
||||
postvars = request.form.to_dict()
|
||||
addr = request.form.get('addr_val')
|
||||
addr, lon, lat, valid_addr, token = fleet.search_addr(postvars, lon, lat, valid_addr, token)
|
||||
if valid_addr:
|
||||
# If a valid address is found, redirect to nav_confirmation
|
||||
return redirect(url_for('nav_confirmation', addr=addr, lon=lon, lat=lat))
|
||||
else:
|
||||
return render_template("error.html")
|
||||
#elif PrimeType != 0:
|
||||
# return render_template("prime.html")
|
||||
# amap stuff
|
||||
elif SearchInput == 1:
|
||||
amap_key, amap_key_2 = fleet.get_amap_key()
|
||||
if amap_key == "" or amap_key is None or amap_key_2 == "" or amap_key_2 is None:
|
||||
return redirect(url_for('amap_key_input'))
|
||||
elif token == "" or token is None:
|
||||
return redirect(url_for('public_token_input'))
|
||||
elif s_token == "" or s_token is None:
|
||||
return redirect(url_for('app_token_input'))
|
||||
else:
|
||||
return redirect(url_for('amap_addr_input'))
|
||||
elif False: #fleet.get_nav_active(): # carrot: 그냥지움... 이것때문에 토큰을 안물어보는듯...
|
||||
if SearchInput == 2:
|
||||
return render_template("nonprime.html",
|
||||
gmap_key=gmap_key, lon=lon, lat=lat,
|
||||
home=preload[0],work=preload[1], fav1=preload[2], fav2=preload[3], fav3=preload[4])
|
||||
else:
|
||||
return render_template("nonprime.html",
|
||||
gmap_key=None, lon=None, lat=None,
|
||||
home=preload[0], work=preload[1], fav1=preload[2], fav2=preload[3], fav3=preload[4])
|
||||
elif token == "" or token is None:
|
||||
return redirect(url_for('public_token_input'))
|
||||
elif s_token == "" or s_token is None:
|
||||
return redirect(url_for('app_token_input'))
|
||||
elif SearchInput == 2:
|
||||
lon, lat = fleet.get_last_lon_lat()
|
||||
if gmap_key == "" or gmap_key is None:
|
||||
return redirect(url_for('gmap_key_input'))
|
||||
else:
|
||||
return render_template("addr.html",
|
||||
gmap_key=gmap_key, lon=lon, lat=lat,
|
||||
home=preload[0], work=preload[1], fav1=preload[2], fav2=preload[3], fav3=preload[4])
|
||||
else:
|
||||
return render_template("addr.html",
|
||||
gmap_key=None, lon=None, lat=None,
|
||||
home=preload[0], work=preload[1], fav1=preload[2], fav2=preload[3], fav3=preload[4])
|
||||
|
||||
@app.route("/nav_confirmation", methods=['GET', 'POST'])
|
||||
def nav_confirmation():
|
||||
token = fleet.get_public_token()
|
||||
lon = request.args.get('lon')
|
||||
lat = request.args.get('lat')
|
||||
addr = request.args.get('addr')
|
||||
if request.method == 'POST':
|
||||
postvars = request.form.to_dict()
|
||||
fleet.nav_confirmed(postvars)
|
||||
return redirect(url_for('addr_input'))
|
||||
else:
|
||||
return render_template("nav_confirmation.html", addr=addr, lon=lon, lat=lat, token=token)
|
||||
|
||||
@app.route("/public_token_input", methods=['GET', 'POST'])
|
||||
def public_token_input():
|
||||
if request.method == 'POST':
|
||||
postvars = request.form.to_dict()
|
||||
fleet.public_token_input(postvars)
|
||||
return redirect(url_for('addr_input'))
|
||||
else:
|
||||
return render_template("public_token_input.html")
|
||||
|
||||
@app.route("/app_token_input", methods=['GET', 'POST'])
|
||||
def app_token_input():
|
||||
if request.method == 'POST':
|
||||
postvars = request.form.to_dict()
|
||||
fleet.app_token_input(postvars)
|
||||
return redirect(url_for('addr_input'))
|
||||
else:
|
||||
return render_template("app_token_input.html")
|
||||
|
||||
@app.route("/gmap_key_input", methods=['GET', 'POST'])
|
||||
def gmap_key_input():
|
||||
if request.method == 'POST':
|
||||
postvars = request.form.to_dict()
|
||||
fleet.gmap_key_input(postvars)
|
||||
return redirect(url_for('addr_input'))
|
||||
else:
|
||||
return render_template("gmap_key_input.html")
|
||||
|
||||
@app.route("/amap_key_input", methods=['GET', 'POST'])
|
||||
def amap_key_input():
|
||||
if request.method == 'POST':
|
||||
postvars = request.form.to_dict()
|
||||
fleet.amap_key_input(postvars)
|
||||
return redirect(url_for('amap_addr_input'))
|
||||
else:
|
||||
return render_template("amap_key_input.html")
|
||||
|
||||
@app.route("/amap_addr_input", methods=['GET', 'POST'])
|
||||
def amap_addr_input():
|
||||
if request.method == 'POST':
|
||||
postvars = request.form.to_dict()
|
||||
fleet.nav_confirmed(postvars)
|
||||
return redirect(url_for('amap_addr_input'))
|
||||
else:
|
||||
lon, lat = fleet.get_last_lon_lat()
|
||||
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("/CurrentStep.json", methods=['GET'])
|
||||
def find_CurrentStep():
|
||||
directory = "/data/openpilot/selfdrive/manager/"
|
||||
filename = "CurrentStep.json"
|
||||
return send_from_directory(directory, filename, as_attachment=True)
|
||||
|
||||
@app.route("/navdirections.json", methods=['GET'])
|
||||
def find_nav_directions():
|
||||
directory = "/data/openpilot/selfdrive/manager/"
|
||||
filename = "navdirections.json"
|
||||
return send_from_directory(directory, filename, as_attachment=True)
|
||||
|
||||
@app.route("/locations", methods=['GET'])
|
||||
def get_locations():
|
||||
data = fleet.get_locations()
|
||||
return Response(data, content_type="application/json")
|
||||
|
||||
@app.route("/set_destination", methods=['POST'])
|
||||
def set_destination():
|
||||
valid_addr = False
|
||||
postvars = request.get_json()
|
||||
data, valid_addr = fleet.set_destination(postvars, valid_addr)
|
||||
if valid_addr:
|
||||
return Response('{"success": true}', content_type='application/json')
|
||||
else:
|
||||
return Response('{"success": false}', content_type='application/json')
|
||||
|
||||
@app.route("/navigation/<file_name>", methods=['GET'])
|
||||
def find_navicon(file_name):
|
||||
directory = "/data/openpilot/selfdrive/assets/navigation/"
|
||||
return send_from_directory(directory, file_name, as_attachment=True)
|
||||
|
||||
@app.route("/previewgif/<path:file_path>", methods=['GET'])
|
||||
def find_previewgif(file_path):
|
||||
directory = "/data/media/0/realdata/"
|
||||
return send_from_directory(directory, file_path, as_attachment=True)
|
||||
|
||||
@app.route("/tools", methods=['GET'])
|
||||
def tools_route():
|
||||
return render_template("tools.html")
|
||||
|
||||
@app.route("/get_toggle_values", methods=['GET'])
|
||||
def get_toggle_values_route():
|
||||
toggle_values = fleet.get_all_toggle_values()
|
||||
return jsonify(toggle_values)
|
||||
|
||||
@app.route("/store_toggle_values", methods=['POST'])
|
||||
def store_toggle_values_route():
|
||||
try:
|
||||
updated_values = request.get_json()
|
||||
fleet.store_toggle_values(updated_values)
|
||||
return jsonify({"message": "Values updated successfully"}), 200
|
||||
except Exception as e:
|
||||
return jsonify({"error": "Failed to update values", "details": str(e)}), 400
|
||||
|
||||
@app.route("/carinfo")
|
||||
def carinfo():
|
||||
try:
|
||||
params = Params()
|
||||
# 更新消息
|
||||
sm.update()
|
||||
|
||||
# 获取车辆基本信息
|
||||
try:
|
||||
car_name = params.get("CarName", encoding='utf8')
|
||||
if car_name in PLATFORMS:
|
||||
platform = PLATFORMS[car_name]
|
||||
car_fingerprint = platform.config.platform_str
|
||||
car_specs = platform.config.specs
|
||||
else:
|
||||
car_fingerprint = "Unknown Fingerprint"
|
||||
car_specs = None
|
||||
except Exception as e:
|
||||
print(f"Failed to get vehicle basic info: {e}")
|
||||
car_name = "Unknown Model"
|
||||
car_fingerprint = "Unknown Fingerprint"
|
||||
car_specs = None
|
||||
|
||||
# 获取车辆状态信息
|
||||
try:
|
||||
CS = sm['carState']
|
||||
|
||||
# 基本状态判断
|
||||
is_car_started = CS.vEgo > 0.1
|
||||
is_car_engaged = CS.cruiseState.enabled
|
||||
|
||||
# 构建基础信息
|
||||
car_info = {
|
||||
"Vehicle Status": {
|
||||
"Running Status": "Moving" if is_car_started else "Stopped",
|
||||
"Cruise System": "Enabled" if is_car_engaged else "Disabled",
|
||||
"Current Speed": f"{CS.vEgo * 3.6:.1f} km/h",
|
||||
"Engine RPM": f"{CS.engineRPM:.0f} RPM" if hasattr(CS, 'engineRPM') and CS.engineRPM > 0 else "Unknown",
|
||||
"Gear Position": str(CS.gearShifter) if hasattr(CS, 'gearShifter') else "Unknown"
|
||||
},
|
||||
"Basic Information": {
|
||||
"Car Model": car_name,
|
||||
"Fingerprint": str(car_fingerprint),
|
||||
"Weight": f"{car_specs.mass:.0f} kg" if car_specs and hasattr(car_specs, 'mass') else "Unknown",
|
||||
"Wheelbase": f"{car_specs.wheelbase:.3f} m" if car_specs and hasattr(car_specs, 'wheelbase') else "Unknown",
|
||||
"Steering Ratio": f"{car_specs.steerRatio:.1f}" if car_specs and hasattr(car_specs, 'steerRatio') else "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
# 详细信息
|
||||
if is_car_started or is_car_engaged:
|
||||
car_info.update({
|
||||
"Cruise Information": {
|
||||
"Cruise Status": "On" if CS.cruiseState.enabled else "Off",
|
||||
"Adaptive Cruise": "On" if CS.cruiseState.available else "Off",
|
||||
"Set Speed": f"{CS.cruiseState.speed * 3.6:.1f} km/h" if CS.cruiseState.speed > 0 else "Not Set",
|
||||
"Following Distance": str(CS.cruiseState.followDistance) if hasattr(CS.cruiseState, 'followDistance') else "Unknown"
|
||||
},
|
||||
"Wheel Speeds": {
|
||||
"Front Left": f"{CS.wheelSpeeds.fl * 3.6:.1f} km/h",
|
||||
"Front Right": f"{CS.wheelSpeeds.fr * 3.6:.1f} km/h",
|
||||
"Rear Left": f"{CS.wheelSpeeds.rl * 3.6:.1f} km/h",
|
||||
"Rear Right": f"{CS.wheelSpeeds.rr * 3.6:.1f} km/h"
|
||||
},
|
||||
"Steering System": {
|
||||
"Steering Angle": f"{CS.steeringAngleDeg:.1f}°",
|
||||
"Steering Torque": f"{CS.steeringTorque:.1f} Nm",
|
||||
"Steering Rate": f"{CS.steeringRateDeg:.1f}°/s",
|
||||
"Lane Departure": "Yes" if CS.leftBlinker or CS.rightBlinker else "No"
|
||||
},
|
||||
"Pedal Status": {
|
||||
"Throttle Position": f"{CS.gas * 100:.1f}%",
|
||||
"Brake Pressure": f"{CS.brake * 100:.1f}%",
|
||||
"Gas Pedal": "Pressed" if CS.gasPressed else "Released",
|
||||
"Brake Pedal": "Pressed" if CS.brakePressed else "Released"
|
||||
},
|
||||
"Safety Systems": {
|
||||
"ESP Status": "Active" if CS.espDisabled else "Normal",
|
||||
"ABS Status": "Active" if hasattr(CS, 'absActive') and CS.absActive else "Normal",
|
||||
"Traction Control": "Active" if hasattr(CS, 'tcsActive') and CS.tcsActive else "Normal",
|
||||
"Collision Warning": "Warning" if hasattr(CS, 'collisionWarning') and CS.collisionWarning else "Normal"
|
||||
},
|
||||
"Door Status": {
|
||||
"Driver Door": "Open" if CS.doorOpen else "Closed",
|
||||
"Passenger Door": "Open" if hasattr(CS, 'passengerDoorOpen') and CS.passengerDoorOpen else "Closed",
|
||||
"Trunk": "Open" if hasattr(CS, 'trunkOpen') and CS.trunkOpen else "Closed",
|
||||
"Hood": "Open" if hasattr(CS, 'hoodOpen') and CS.hoodOpen else "Closed",
|
||||
"Seatbelt": "Unbuckled" if CS.seatbeltUnlatched else "Buckled"
|
||||
},
|
||||
"Light Status": {
|
||||
"Left Turn Signal": "On" if CS.leftBlinker else "Off",
|
||||
"Right Turn Signal": "On" if CS.rightBlinker else "Off",
|
||||
"High Beam": "On" if CS.genericToggle else "Off",
|
||||
"Low Beam": "On" if hasattr(CS, 'lowBeamOn') and CS.lowBeamOn else "Off"
|
||||
},
|
||||
"Blind Spot Monitor": {
|
||||
"Left Side": "Vehicle Detected" if CS.leftBlindspot else "Clear",
|
||||
"Right Side": "Vehicle Detected" if CS.rightBlindspot else "Clear"
|
||||
}
|
||||
})
|
||||
|
||||
# 添加可选的其他信息
|
||||
other_info = {}
|
||||
if hasattr(CS, 'outsideTemp'):
|
||||
other_info["Outside Temperature"] = f"{CS.outsideTemp:.1f}°C"
|
||||
if hasattr(CS, 'fuelGauge'):
|
||||
other_info["Range"] = f"{CS.fuelGauge:.1f}km"
|
||||
if hasattr(CS, 'odometer'):
|
||||
other_info["Odometer"] = f"{CS.odometer:.1f}km"
|
||||
if hasattr(CS, 'instantFuelConsumption'):
|
||||
other_info["Instant Fuel Consumption"] = f"{CS.instantFuelConsumption:.1f}L/100km"
|
||||
|
||||
if other_info:
|
||||
car_info["Other Information"] = other_info
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error getting vehicle state info: {str(e)}")
|
||||
traceback.print_exc()
|
||||
car_info = {
|
||||
"Basic Information": {
|
||||
"Car Model": car_name,
|
||||
"Fingerprint": str(car_fingerprint)
|
||||
},
|
||||
"Status": "Unable to get vehicle state information, please check if the vehicle is started"
|
||||
}
|
||||
|
||||
return render_template("carinfo.html", car_info=car_info)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error rendering carinfo page: {str(e)}")
|
||||
traceback.print_exc()
|
||||
return render_template("carinfo.html", car_info={"error": f"Error getting vehicle information: {str(e)}"})
|
||||
|
||||
def main():
|
||||
try:
|
||||
set_core_affinity([0, 1, 2, 3])
|
||||
except Exception:
|
||||
cloudlog.exception("fleet_manager: failed to set core affinity")
|
||||
app.secret_key = secrets.token_hex(32)
|
||||
app.run(host="0.0.0.0", port=8082)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
474
selfdrive/frogpilot/fleetmanager/helpers.py
Normal file
474
selfdrive/frogpilot/fleetmanager/helpers.py
Normal file
@@ -0,0 +1,474 @@
|
||||
# otisserv - Copyright (c) 2019-, Rick Lan, dragonpilot community, and a number of other of contributors.
|
||||
# Fleet Manager - [actuallylemoncurd](https://github.com/actuallylemoncurd), [AlexandreSato](https://github.com/alexandreSato),
|
||||
# [ntegan1](https://github.com/ntegan1), [royjr](https://github.com/royjr), and [sunnyhaibin] (https://github.com/sunnypilot)
|
||||
# Almost everything else - ChatGPT
|
||||
# dirty PR pusher - mike8643
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import requests
|
||||
import subprocess
|
||||
# otisserv conversion
|
||||
from pathlib import Path
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.hardware import PC
|
||||
from openpilot.system.hardware.hw import Paths
|
||||
from openpilot.system.loggerd.uploader import listdir_by_creation
|
||||
from openpilot.tools.lib.route import SegmentName
|
||||
from openpilot.system.loggerd.xattr_cache import getxattr
|
||||
|
||||
# otisserv conversion
|
||||
from urllib.parse import quote
|
||||
|
||||
pi = 3.1415926535897932384626
|
||||
x_pi = 3.14159265358979324 * 3000.0 / 180.0
|
||||
a = 6378245.0
|
||||
ee = 0.00669342162296594323
|
||||
|
||||
params = Params()
|
||||
params_memory = Params("/dev/shm/params")
|
||||
#params_storage = Params("/persist/comma/params")
|
||||
|
||||
PRESERVE_ATTR_NAME = 'user.preserve'
|
||||
PRESERVE_ATTR_VALUE = b'1'
|
||||
PRESERVE_COUNT = 5
|
||||
|
||||
|
||||
# path to openpilot screen recordings and error logs
|
||||
if PC:
|
||||
SCREENRECORD_PATH = os.path.join(str(Path.home()), ".comma", "media", "0", "videos", "")
|
||||
ERROR_LOGS_PATH = os.path.join(str(Path.home()), ".comma", "community", "crashes", "")
|
||||
else:
|
||||
SCREENRECORD_PATH = "/data/media/0/videos/"
|
||||
ERROR_LOGS_PATH = "/data/community/crashes/"
|
||||
|
||||
|
||||
def list_files(path): # still used for footage
|
||||
return sorted(listdir_by_creation(path), reverse=True)
|
||||
|
||||
|
||||
def list_file(path): # new function for screenrecords/error-logs
|
||||
if os.path.exists(path):
|
||||
files = os.listdir(path)
|
||||
sorted_files = sorted(files, reverse=True)
|
||||
else:
|
||||
return [] # Return an empty list if there are no files or directory
|
||||
return sorted_files
|
||||
|
||||
|
||||
def is_valid_segment(segment):
|
||||
try:
|
||||
segment_to_segment_name(Paths.log_root(), segment)
|
||||
return True
|
||||
except AssertionError:
|
||||
return False
|
||||
|
||||
|
||||
def segment_to_segment_name(data_dir, segment):
|
||||
fake_dongle = "ffffffffffffffff"
|
||||
return SegmentName(str(os.path.join(data_dir, fake_dongle + "|" + segment)))
|
||||
|
||||
|
||||
def all_segment_names():
|
||||
segments = []
|
||||
for segment in listdir_by_creation(Paths.log_root()):
|
||||
try:
|
||||
segments.append(segment_to_segment_name(Paths.log_root(), segment))
|
||||
except AssertionError:
|
||||
pass
|
||||
return segments
|
||||
|
||||
|
||||
def all_routes():
|
||||
segment_names = all_segment_names()
|
||||
route_names = [segment_name.route_name for segment_name in segment_names]
|
||||
route_times = [route_name.time_str for route_name in route_names]
|
||||
unique_routes = list(dict.fromkeys(route_times))
|
||||
return sorted(unique_routes, reverse=True)
|
||||
|
||||
def preserved_routes():
|
||||
dirs = listdir_by_creation(Paths.log_root())
|
||||
preserved_segments = get_preserved_segments(dirs)
|
||||
return sorted(preserved_segments, reverse=True)
|
||||
|
||||
def has_preserve_xattr(d: str) -> bool:
|
||||
return getxattr(os.path.join(Paths.log_root(), d), PRESERVE_ATTR_NAME) == PRESERVE_ATTR_VALUE
|
||||
|
||||
def get_preserved_segments(dirs_by_creation: list[str]) -> list[str]:
|
||||
preserved = []
|
||||
for n, d in enumerate(filter(has_preserve_xattr, reversed(dirs_by_creation))):
|
||||
if n == PRESERVE_COUNT:
|
||||
break
|
||||
date_str, _, seg_str = d.rpartition("--")
|
||||
|
||||
# ignore non-segment directories
|
||||
if not date_str:
|
||||
continue
|
||||
try:
|
||||
seg_num = int(seg_str)
|
||||
except ValueError:
|
||||
continue
|
||||
# preserve segment and its prior
|
||||
preserved.append(d)
|
||||
|
||||
return preserved
|
||||
|
||||
def video_to_gif(input_path, output_path, fps=1, duration=6): # not used right now but can if want longer animated gif
|
||||
if os.path.exists(output_path):
|
||||
return
|
||||
command = [
|
||||
'ffmpeg', '-y', '-i', input_path,
|
||||
'-filter_complex',
|
||||
f'fps={fps},scale=240:-1:flags=lanczos,setpts=0.1*PTS,split[s0][s1];[s0]palettegen=max_colors=32[p];[s1][p]paletteuse=dither=bayer',
|
||||
'-t', str(duration), output_path
|
||||
]
|
||||
subprocess.run(command)
|
||||
print(f"GIF file created: {output_path}")
|
||||
|
||||
def video_to_img(input_path, output_path, fps=1, duration=6):
|
||||
if os.path.exists(output_path):
|
||||
print("video_to_img path exist=", output_path)
|
||||
return
|
||||
subprocess.run(['ffmpeg', '-y', '-i', input_path, '-ss', '5', '-vframes', '1', output_path])
|
||||
print(f"GIF file created: {output_path}")
|
||||
|
||||
def segments_in_route(route):
|
||||
segment_names = [segment_name for segment_name in all_segment_names() if segment_name.time_str == route]
|
||||
segments = [segment_name.time_str + "--" + str(segment_name.segment_num) for segment_name in segment_names]
|
||||
return segments
|
||||
|
||||
|
||||
def ffmpeg_mp4_concat_wrap_process_builder(file_list, cameratype, chunk_size=1024*512):
|
||||
command_line = ["ffmpeg"]
|
||||
if not cameratype == "qcamera":
|
||||
command_line += ["-f", "hevc"]
|
||||
command_line += ["-r", "20"]
|
||||
command_line += ["-i", "concat:" + file_list]
|
||||
command_line += ["-c", "copy"]
|
||||
command_line += ["-bsf:a", "aac_adtstoasc"]
|
||||
command_line += ["-map", "0"]
|
||||
if not cameratype == "qcamera":
|
||||
command_line += ["-vtag", "hvc1"]
|
||||
command_line += ["-f", "mp4"]
|
||||
command_line += ["-movflags", "empty_moov"]
|
||||
command_line += ["-"]
|
||||
return subprocess.Popen(
|
||||
command_line, stdout=subprocess.PIPE,
|
||||
bufsize=chunk_size
|
||||
)
|
||||
|
||||
|
||||
def ffmpeg_mp4_wrap_process_builder(filename):
|
||||
"""Returns a process that will wrap the given filename
|
||||
inside a mp4 container, for easier playback by browsers
|
||||
and other devices. Primary use case is streaming segment videos
|
||||
to the vidserver tool.
|
||||
filename is expected to be a pathname to one of the following
|
||||
/path/to/a/qcamera.ts
|
||||
/path/to/a/dcamera.hevc
|
||||
/path/to/a/ecamera.hevc
|
||||
/path/to/a/fcamera.hevc
|
||||
"""
|
||||
basename = filename.rsplit("/")[-1]
|
||||
extension = basename.rsplit(".")[-1]
|
||||
command_line = ["ffmpeg"]
|
||||
if extension == "hevc":
|
||||
command_line += ["-f", "hevc"]
|
||||
command_line += ["-r", "20"]
|
||||
command_line += ["-i", filename]
|
||||
command_line += ["-c", "copy"]
|
||||
|
||||
command_line += ["-bsf:a", "aac_adtstoasc"]
|
||||
|
||||
command_line += ["-map", "0"]
|
||||
if extension == "hevc":
|
||||
command_line += ["-vtag", "hvc1"]
|
||||
command_line += ["-f", "mp4"]
|
||||
command_line += ["-movflags", "empty_moov"]
|
||||
command_line += ["-"]
|
||||
return subprocess.Popen(
|
||||
command_line, stdout=subprocess.PIPE
|
||||
)
|
||||
|
||||
|
||||
def ffplay_mp4_wrap_process_builder(file_name):
|
||||
command_line = ["ffmpeg"]
|
||||
command_line += ["-i", file_name]
|
||||
command_line += ["-c", "copy"]
|
||||
command_line += ["-map", "0"]
|
||||
command_line += ["-f", "mp4"]
|
||||
command_line += ["-movflags", "empty_moov"]
|
||||
command_line += ["-"]
|
||||
return subprocess.Popen(
|
||||
command_line, stdout=subprocess.PIPE
|
||||
)
|
||||
|
||||
def get_nav_active():
|
||||
if params.get("NavDestination", encoding='utf8') is not None:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def get_public_token():
|
||||
token = params.get("MapboxPublicKey", encoding='utf8')
|
||||
return token.strip() if token is not None else None
|
||||
|
||||
def get_app_token():
|
||||
token = params.get("MapboxSecretKey", encoding='utf8')
|
||||
return token.strip() if token is not None else None
|
||||
|
||||
def get_gmap_key():
|
||||
token = params.get("GMapKey", encoding='utf8')
|
||||
return token.strip() if token is not None else None
|
||||
|
||||
def get_amap_key():
|
||||
token = params.get("AMapKey1", encoding='utf8')
|
||||
token2 = params.get("AMapKey2", encoding='utf8')
|
||||
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
|
||||
|
||||
def get_PrimeType():
|
||||
PrimeType = params.get_int("PrimeType")
|
||||
return PrimeType
|
||||
|
||||
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"]
|
||||
|
||||
def get_locations():
|
||||
data = params.get("ApiCache_NavDestinations", encoding='utf-8')
|
||||
return data
|
||||
|
||||
def preload_favs():
|
||||
raw_json = params.get("ApiCache_NavDestinations", encoding='utf8')
|
||||
if raw_json is None:
|
||||
return (None, None, None, None, None)
|
||||
|
||||
try:
|
||||
nav_destinations = json.loads(raw_json)
|
||||
except (TypeError, json.JSONDecodeError):
|
||||
return (None, None, None, None, None)
|
||||
|
||||
locations = {"home": None, "work": None, "fav1": None, "fav2": None, "fav3": None}
|
||||
|
||||
for item in nav_destinations:
|
||||
label = item.get("label")
|
||||
if label in locations and locations[label] is None:
|
||||
locations[label] = item.get("place_name")
|
||||
|
||||
return tuple(locations.values())
|
||||
|
||||
def parse_addr(postvars, lon, lat, valid_addr, token):
|
||||
addr = postvars.get("fav_val", [""])
|
||||
real_addr = None
|
||||
if addr != "favorites":
|
||||
try:
|
||||
dests = json.loads(params.get("ApiCache_NavDestinations", encoding='utf8'))
|
||||
except TypeError:
|
||||
dests = json.loads("[]")
|
||||
for item in dests:
|
||||
if "label" in item and item["label"] == addr:
|
||||
lat, lon, real_addr = item["latitude"], item["longitude"], item["place_name"]
|
||||
break
|
||||
return (real_addr, lon, lat, real_addr is not None, token)
|
||||
|
||||
def search_addr(postvars, lon, lat, valid_addr, token):
|
||||
if "addr_val" in postvars:
|
||||
addr = postvars.get("addr_val")
|
||||
if addr != "":
|
||||
# Properly encode the address to handle spaces
|
||||
addr_encoded = quote(addr)
|
||||
query = f"https://api.mapbox.com/geocoding/v5/mapbox.places/{addr_encoded}.json?access_token={token}&limit=1"
|
||||
# focus on place around last gps position
|
||||
lngi, lati = get_last_lon_lat()
|
||||
query += f"&proximity={lngi},{lati}"
|
||||
r = requests.get(query)
|
||||
if r.status_code != 200:
|
||||
return (addr, lon, lat, valid_addr, token)
|
||||
j = json.loads(r.text)
|
||||
if not j["features"]:
|
||||
return (addr, lon, lat, valid_addr, token)
|
||||
lon, lat = j["features"][0]["geometry"]["coordinates"]
|
||||
valid_addr = True
|
||||
return (addr, lon, lat, valid_addr, token)
|
||||
|
||||
def set_destination(postvars, valid_addr):
|
||||
if postvars.get("latitude") is not None and postvars.get("longitude") is not None:
|
||||
postvars["lat"] = postvars.get("latitude")
|
||||
postvars["lon"] = postvars.get("longitude")
|
||||
postvars["save_type"] = "recent"
|
||||
nav_confirmed(postvars)
|
||||
valid_addr = True
|
||||
else:
|
||||
addr = postvars.get("place_name")
|
||||
token = get_public_token()
|
||||
data, lon, lat, valid_addr, token = search_addr(addr, lon, lat, valid_addr, token)
|
||||
postvars["lat"] = lat
|
||||
postvars["lon"] = lon
|
||||
postvars["save_type"] = "recent"
|
||||
nav_confirmed(postvars)
|
||||
valid_addr= True
|
||||
return postvars, valid_addr
|
||||
|
||||
def nav_confirmed(postvars):
|
||||
print(f"nav_confirmed {postvars}")
|
||||
if postvars is not None:
|
||||
lat = float(postvars.get("lat"))
|
||||
lng = float(postvars.get("lon"))
|
||||
save_type = postvars.get("save_type")
|
||||
name = postvars.get("name") if postvars.get("name") is not None else ""
|
||||
if params.get_int("SearchInput") == 1:
|
||||
lng, lat = gcj02towgs84(lng, lat)
|
||||
params.put("NavDestination", f'{{"latitude": {lat:.6f}, "longitude": {lng:.6f}, "place_name": "{name}"}}')
|
||||
print(f"nav_confirmed {lat}, {lng}, {name}")
|
||||
if name == "":
|
||||
name = str(lat) + "," + str(lng)
|
||||
new_dest = {"latitude": float(lat), "longitude": float(lng), "place_name": name}
|
||||
if save_type == "recent":
|
||||
new_dest["save_type"] = "recent"
|
||||
else:
|
||||
new_dest["save_type"] = "favorite"
|
||||
new_dest["label"] = save_type
|
||||
val = params.get("ApiCache_NavDestinations", encoding='utf8')
|
||||
if val is not None:
|
||||
val = val.rstrip('\x00')
|
||||
dests = [] if val is None else json.loads(val)
|
||||
# type idx
|
||||
type_label_ids = {"home": None, "work": None, "fav1": None, "fav2": None, "fav3": None, "recent": []}
|
||||
idx = 0
|
||||
for d in dests:
|
||||
if d["save_type"] == "favorite":
|
||||
type_label_ids[d["label"]] = idx
|
||||
else:
|
||||
type_label_ids["recent"].append(idx)
|
||||
idx += 1
|
||||
if save_type == "recent":
|
||||
dest_id = None
|
||||
if len(type_label_ids["recent"]) > 10:
|
||||
dests.pop(type_label_ids["recent"][-1])
|
||||
else:
|
||||
dest_id = type_label_ids[save_type]
|
||||
if dest_id is None:
|
||||
dests.insert(0, new_dest)
|
||||
else:
|
||||
dests[dest_id] = new_dest
|
||||
params.put("ApiCache_NavDestinations", json.dumps(dests).rstrip("\n\r"))
|
||||
|
||||
def public_token_input(postvars):
|
||||
if postvars is None or "pk_token_val" not in postvars or postvars.get("pk_token_val")[0] == "":
|
||||
return postvars
|
||||
else:
|
||||
token = postvars.get("pk_token_val").strip()
|
||||
if "pk." not in token:
|
||||
return postvars
|
||||
else:
|
||||
params.put("MapboxPublicKey", token)
|
||||
return token
|
||||
|
||||
def app_token_input(postvars):
|
||||
if postvars is None or "sk_token_val" not in postvars or postvars.get("sk_token_val")[0] == "":
|
||||
return postvars
|
||||
else:
|
||||
token = postvars.get("sk_token_val").strip()
|
||||
if "sk." not in token:
|
||||
return postvars
|
||||
else:
|
||||
params.put("MapboxSecretKey", token)
|
||||
return token
|
||||
|
||||
def gmap_key_input(postvars):
|
||||
if postvars is None or "gmap_key_val" not in postvars or postvars.get("gmap_key_val")[0] == "":
|
||||
return postvars
|
||||
else:
|
||||
token = postvars.get("gmap_key_val").strip()
|
||||
params.put("GMapKey", token)
|
||||
return token
|
||||
|
||||
def amap_key_input(postvars):
|
||||
if postvars is None or "amap_key_val" not in postvars or postvars.get("amap_key_val")[0] == "":
|
||||
return postvars
|
||||
else:
|
||||
token = postvars.get("amap_key_val").strip()
|
||||
token2 = postvars.get("amap_key_val_2").strip()
|
||||
params.put("AMapKey1", token)
|
||||
params.put("AMapKey2", token2)
|
||||
return token
|
||||
|
||||
def gcj02towgs84(lng, lat):
|
||||
dlat = transform_lat(lng - 105.0, lat - 35.0)
|
||||
dlng = transform_lng(lng - 105.0, lat - 35.0)
|
||||
radlat = lat / 180.0 * pi
|
||||
magic = math.sin(radlat)
|
||||
magic = 1 - ee * magic * magic
|
||||
sqrtmagic = math.sqrt(magic)
|
||||
dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * pi)
|
||||
dlng = (dlng * 180.0) / (a / sqrtmagic * math.cos(radlat) * pi)
|
||||
mglat = lat + dlat
|
||||
mglng = lng + dlng
|
||||
return [lng * 2 - mglng, lat * 2 - mglat]
|
||||
|
||||
def transform_lat(lng, lat):
|
||||
ret = -100.0 + 2.0 * lng + 3.0 * lat + 0.2 * lat * lat + 0.1 * lng * lat + 0.2 * math.sqrt(abs(lng))
|
||||
ret += (20.0 * math.sin(6.0 * lng * pi) + 20.0 * math.sin(2.0 * lng * pi)) * 2.0 / 3.0
|
||||
ret += (20.0 * math.sin(lat * pi) + 40.0 * math.sin(lat / 3.0 * pi)) * 2.0 / 3.0
|
||||
ret += (160.0 * math.sin(lat / 12.0 * pi) + 320 * math.sin(lat * pi / 30.0)) * 2.0 / 3.0
|
||||
return ret
|
||||
|
||||
def transform_lng(lng, lat):
|
||||
ret = 300.0 + lng + 2.0 * lat + 0.1 * lng * lng + 0.1 * lng * lat + 0.1 * math.sqrt(abs(lng))
|
||||
ret += (20.0 * math.sin(6.0 * lng * pi) + 20.0 * math.sin(2.0 * lng * pi)) * 2.0 / 3.0
|
||||
ret += (20.0 * math.sin(lng * pi) + 40.0 * math.sin(lng / 3.0 * pi)) * 2.0 / 3.0
|
||||
ret += (150.0 * math.sin(lng / 12.0 * pi) + 300.0 * math.sin(lng / 30.0 * pi)) * 2.0 / 3.0
|
||||
return ret
|
||||
|
||||
from openpilot.system.manager.manager import get_default_params_key
|
||||
def get_all_toggle_values():
|
||||
all_keys = get_default_params_key()
|
||||
|
||||
toggle_values = {}
|
||||
for key in all_keys:
|
||||
try:
|
||||
value = params.get(key)
|
||||
except Exception:
|
||||
value = b"0"
|
||||
toggle_values[key] = value.decode('utf-8') if value is not None else "0"
|
||||
|
||||
return toggle_values
|
||||
|
||||
def store_toggle_values(updated_values):
|
||||
for key, value in updated_values.items():
|
||||
try:
|
||||
params.put(key, value.encode('utf-8'))
|
||||
#params_storage.put(key, value.encode('utf-8'))
|
||||
except Exception as e:
|
||||
print(f"Failed to update {key}: {e}")
|
||||
|
||||
#params_memory.put_bool("FrogPilotTogglesUpdated", True)
|
||||
#time.sleep(1)
|
||||
#params_memory.put_bool("FrogPilotTogglesUpdated", False)
|
||||
BIN
selfdrive/frogpilot/fleetmanager/static/carrot.png
Normal file
BIN
selfdrive/frogpilot/fleetmanager/static/carrot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
1
selfdrive/frogpilot/fleetmanager/static/favicon.ico
Normal file
1
selfdrive/frogpilot/fleetmanager/static/favicon.ico
Normal file
@@ -0,0 +1 @@
|
||||
../../../selfdrive/assets/img_spinner_comma.png
|
||||
BIN
selfdrive/frogpilot/fleetmanager/static/frog.png
Normal file
BIN
selfdrive/frogpilot/fleetmanager/static/frog.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 106 KiB |
21
selfdrive/frogpilot/fleetmanager/templates/about.html
Normal file
21
selfdrive/frogpilot/fleetmanager/templates/about.html
Normal file
@@ -0,0 +1,21 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block title %}
|
||||
About
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<br>
|
||||
<h1>About</h1>
|
||||
<br>
|
||||
<footer class="small text-center text-muted" style="word-wrap: break-word;">
|
||||
Special thanks to:<br><br>
|
||||
ntegan1<br>
|
||||
royjr<br>
|
||||
AlexandreSato<br>
|
||||
actuallylemoncurd<br>
|
||||
sunnyhaibin<br>
|
||||
dragonpilot<br>
|
||||
chatgpt<br>
|
||||
</footer>
|
||||
{% endblock %}
|
||||
11
selfdrive/frogpilot/fleetmanager/templates/addr.html
Normal file
11
selfdrive/frogpilot/fleetmanager/templates/addr.html
Normal file
@@ -0,0 +1,11 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block title %}
|
||||
Navigation
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% with gmap_key=gmap_key, lon=lon, lat=lat, home=home, work=work, fav1=fav1, fav2=fav2, fav3=fav3 %}
|
||||
{% include "addr_input.html" %}
|
||||
{% endwith %}
|
||||
{% endblock %}
|
||||
64
selfdrive/frogpilot/fleetmanager/templates/addr_input.html
Normal file
64
selfdrive/frogpilot/fleetmanager/templates/addr_input.html
Normal file
@@ -0,0 +1,64 @@
|
||||
{% block main %}
|
||||
<!-- Your form markup -->
|
||||
<form name="searchForm" method="post">
|
||||
<fieldset class="uk-fieldset">
|
||||
<div class="uk-margin">
|
||||
{% if home or work or fav1 or fav2 or fav3 %}
|
||||
<select class="uk-select" name="fav_val">
|
||||
<option value="favorites">Select Saved Destinations</option>
|
||||
{% if home %} <option value="home">Home: {{home}}</option> {% endif %}
|
||||
{% if work %} <option value="work">Work: {{work}}</option> {% endif %}
|
||||
{% if fav1 %} <option value="fav1">Favorite 1: {{fav1}}</option> {% endif %}
|
||||
{% if fav2 %} <option value="fav2">Favorite 2: {{fav2}}</option> {% endif %}
|
||||
{% if fav3 %} <option value="fav3">Favorite 3: {{fav3}}</option> {% endif %}
|
||||
</select>
|
||||
{% endif %}
|
||||
<input class="uk-input" type="text" name="addr_val" id="pac-input" placeholder="Search a place">
|
||||
<input class="uk-button uk-button-primary uk-width-1-1 uk-margin-small-bottom" type="submit" value="Search">
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
<!-- Include the Google Maps Places API script conditionally with JavaScript -->
|
||||
<script>
|
||||
// attach gmap_key to variable
|
||||
let gmap = "{{gmap_key}}";
|
||||
|
||||
// Check if gmap_key is defined
|
||||
if (gmap && gmap !== "None") {
|
||||
var script = document.createElement('script');
|
||||
script.src = 'https://maps.googleapis.com/maps/api/js?key={{gmap_key}}&libraries=places&callback=initAutocomplete';
|
||||
script.async = true;
|
||||
script.defer = true;
|
||||
document.head.appendChild(script);
|
||||
|
||||
// Define the callback function for place_changed
|
||||
function onPlaceChanged() {
|
||||
var place = autocomplete.getPlace();
|
||||
|
||||
// Check if the place has a formatted address
|
||||
if (place.formatted_address) {
|
||||
// Set the value of the input field to the formatted address
|
||||
document.getElementById('pac-input').value = place.formatted_address;
|
||||
}
|
||||
}
|
||||
|
||||
// Define the autocomplete variable
|
||||
var autocomplete;
|
||||
|
||||
// Define the initAutocomplete function with initial bounds
|
||||
function initAutocomplete() {
|
||||
var center = new google.maps.LatLng({{lat}}, {{lon}});
|
||||
var bounds = new google.maps.Circle({ center: center, radius: 5000 }).getBounds();
|
||||
|
||||
autocomplete = new google.maps.places.Autocomplete(
|
||||
document.getElementById('pac-input'),
|
||||
{
|
||||
bounds: bounds // Set initial bounds here
|
||||
}
|
||||
);
|
||||
|
||||
autocomplete.addListener('place_changed', onPlaceChanged);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
215
selfdrive/frogpilot/fleetmanager/templates/amap_addr_input.html
Normal file
215
selfdrive/frogpilot/fleetmanager/templates/amap_addr_input.html
Normal file
@@ -0,0 +1,215 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block title %}
|
||||
amap_addr_input
|
||||
{% endblock %}
|
||||
{% block main %}
|
||||
<!-- Head section moved into body -->
|
||||
<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>
|
||||
|
||||
<script type="text/javascript">
|
||||
window._AMapSecurityConfig = {
|
||||
securityJsCode:'{{amap_key_2}}',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style type="text/css">
|
||||
body {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
#mapContainer {
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
#amap-container {
|
||||
height: 400px; /* Adjust height as needed */
|
||||
}
|
||||
|
||||
.button-group {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
font-size: 12px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.button-group .button {
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
background-color: #0D9BF2;
|
||||
color: #FFF;
|
||||
border: 0;
|
||||
outline: none;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
border-radius: 3px;
|
||||
margin-bottom: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.amap-info-content {
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script type="text/javascript"
|
||||
src="https://webapi.amap.com/maps?v=1.4.2&key={{amap_key}}"></script>
|
||||
|
||||
<!-- Rest of the HTML body content -->
|
||||
<div class="uk-grid-match uk-grid-small uk-text-center" uk-grid>
|
||||
<div class="uk-width-1-3@m">
|
||||
<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@m">
|
||||
<input class="uk-input" type="text" id="keyword" name="keyword"
|
||||
placeholder="请输入关键字:(选定后搜索)" onfocus='this.value=""' />
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" id="longitude" />
|
||||
<input type="hidden" id="latitude" />
|
||||
<div style="height: 600px" id="container"></div>
|
||||
<script type="text/javascript">
|
||||
var windowsArr = [];
|
||||
var markers = [];
|
||||
var map = new AMap.Map("container", {
|
||||
resizeEnable: true,
|
||||
center: [{{lat}}, {{lon}}], //地图中心点
|
||||
zoom: 13, //地图显示的缩放级别
|
||||
keyboardEnable: false,
|
||||
});
|
||||
|
||||
var infoWindow;
|
||||
function openInfo(name, addr, lng, lat) {
|
||||
//构建信息窗体中显示的内容
|
||||
var info = [];
|
||||
info.push('<div class="uk-card uk-card-default uk-card-body">');
|
||||
info.push('<a class="uk-card-badge uk-label" onClick="javascript:infoWindow.close()" uk-close></a>');
|
||||
info.push("<h3 style=\"padding-top: 10px;\" class=\"uk-card-title\">" + name + "</h3>");
|
||||
info.push("<p>" + addr + "</p>");
|
||||
info.push('<div class="uk-card-footer">');
|
||||
info.push('<form name="navForm" method="post">');
|
||||
info.push(' <input type="hidden" name="lat" value="' + lat + '">');
|
||||
info.push(' <input type="hidden" name="lon" value="' + lng + '">');
|
||||
info.push(' <input type="hidden" name="save_type" value="' + document.getElementById("save_type").value + '">');
|
||||
info.push(' <input type="hidden" name="name" value="' + name + '">');
|
||||
info.push(' <input class="uk-button uk-button-primary" type="submit" value="导航" >');
|
||||
info.push('</form>');
|
||||
info.push('</div>');
|
||||
info.push("</div>");
|
||||
|
||||
var pos = new AMap.LngLat(lng, lat)
|
||||
infoWindow = new AMap.InfoWindow({
|
||||
position: pos,
|
||||
isCustom: true,
|
||||
offset: new AMap.Pixel(0, -30),
|
||||
content: info.join(""), //使用默认信息窗体框样式,显示信息内容
|
||||
});
|
||||
|
||||
infoWindow.open(map, pos);
|
||||
}
|
||||
AMap.plugin(["AMap.Autocomplete", "AMap.PlaceSearch"], function () {
|
||||
var autoOptions = {
|
||||
city: "全国", //城市,默认全国
|
||||
input: "keyword", //使用联想输入的input的id
|
||||
};
|
||||
autocomplete = new AMap.Autocomplete(autoOptions);
|
||||
var placeSearch = new AMap.PlaceSearch({
|
||||
map: "",
|
||||
});
|
||||
AMap.event.addListener(autocomplete, "select", function (e) {
|
||||
//TODO 针对选中的poi实现自己的功能
|
||||
//重寫搜尋點及其提示資訊begin=====
|
||||
placeSearch.setCity(e.poi.adcode);
|
||||
if (e.poi && e.poi.location) {
|
||||
map.setZoom(17);
|
||||
map.setCenter(e.poi.location);
|
||||
}
|
||||
placeSearch.search(e.poi.name, check_dest); //關鍵字查詢查詢
|
||||
|
||||
function check_dest(status, result) {
|
||||
if (status === "complete" && result.info === "OK") {
|
||||
for (var h = 0; h < result.poiList.pois.length; h++) {
|
||||
//返回搜尋列表迴圈繫結marker
|
||||
var jy = result.poiList.pois[h]["location"]; //經緯度
|
||||
var name = result.poiList.pois[h]["name"]; //地址
|
||||
marker = new AMap.Marker({
|
||||
//加點
|
||||
map: map,
|
||||
position: jy,
|
||||
});
|
||||
marker.extData = {
|
||||
getLng: jy["lng"],
|
||||
getLat: jy["lat"],
|
||||
name: name,
|
||||
address: result.poiList.pois[h]["address"],
|
||||
}; //自定義想傳入的引數
|
||||
|
||||
marker.on("click", function (e) {
|
||||
var hs = e.target.extData;
|
||||
var content = openInfo(
|
||||
hs["name"],
|
||||
hs["address"],
|
||||
hs["getLng"],
|
||||
hs["getLat"]
|
||||
);
|
||||
});
|
||||
markers.push(marker);
|
||||
}
|
||||
}
|
||||
}
|
||||
//重寫搜尋點及其提示資訊end=====
|
||||
});
|
||||
});
|
||||
var clickEventListener = map.on('click', function(e) {
|
||||
map.remove(markers);
|
||||
document.getElementById('longitude').value = e.lnglat.getLng();
|
||||
document.getElementById('latitude').value = e.lnglat.getLat();
|
||||
lnglatXY = [e.lnglat.getLng(), e.lnglat.getLat()];
|
||||
var marker = new AMap.Marker({
|
||||
//加點
|
||||
map: map,
|
||||
position: lnglatXY,
|
||||
});
|
||||
marker.extData = {
|
||||
getLng: e.lnglat.getLng(),
|
||||
getLat: e.lnglat.getLat(),
|
||||
}; //自定義想傳入的引數
|
||||
|
||||
marker.on("click", function (e) {
|
||||
var hs = e.target.extData;
|
||||
var content = openInfo(
|
||||
"",
|
||||
"(" + hs["getLat"] + ", " + hs["getLng"] + ")",
|
||||
hs["getLng"],
|
||||
hs["getLat"]
|
||||
);
|
||||
});
|
||||
markers.push(marker);
|
||||
if (typeof(infoWindow) != "undefined") {
|
||||
infoWindow.close();
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,20 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block title %}
|
||||
amap_key_input
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<form name="setAmapTokenForm" method="post">
|
||||
<fieldset class="uk-fieldset">
|
||||
<legend class="uk-legend">请输入您的高德地图 API KEY</legend>
|
||||
<div style="color: red">因系统升级,若于 2021/12/02 前申请 key 的人请重新申请新的「<b>key</b>」和「<b>安全密钥</b>」配对。</div>
|
||||
<div class="uk-margin">
|
||||
<input class="uk-input" type="text" name="amap_key_val" placeholder="KEY">
|
||||
<input class="uk-input" type="text" name="amap_key_val_2" placeholder="安全密钥">
|
||||
<input class="uk-button uk-button-primary uk-width-1-1 uk-margin-small-bottom" type="submit" value="设置">
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,19 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block title %}
|
||||
MapBox key input
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<form name="setSkTokenForm" method="post">
|
||||
<fieldset class="uk-fieldset">
|
||||
<legend class="uk-legend">Set your Mapbox <b>Secret Token</b></legend>
|
||||
<div style="padding: 5px; color: red; font-weight: bold;">{{msg}}</div>
|
||||
<div class="uk-margin">
|
||||
<input class="uk-input" type="text" name="sk_token_val" placeholder="sk.xxxxxxx...">
|
||||
<input class="uk-button uk-button-primary uk-width-1-1 uk-margin-small-bottom" type="submit" value="Set">
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
49
selfdrive/frogpilot/fleetmanager/templates/carinfo.html
Normal file
49
selfdrive/frogpilot/fleetmanager/templates/carinfo.html
Normal file
@@ -0,0 +1,49 @@
|
||||
{% extends "layout.html" %}
|
||||
{% block title %}Vehicle Information{% endblock %}
|
||||
{% block main %}
|
||||
|
||||
<div class="container">
|
||||
<h1 class="text-center mb-4">Vehicle Information</h1>
|
||||
|
||||
{% if car_info %}
|
||||
{% if car_info.get("error") %}
|
||||
<div class="alert alert-danger">
|
||||
{{ car_info.get("error") }}
|
||||
</div>
|
||||
{% else %}
|
||||
{% for section, data in car_info.items() %}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">{{ section }}</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if data is mapping %}
|
||||
{% for key, value in data.items() %}
|
||||
<div class="row mb-2">
|
||||
<div class="col-6">
|
||||
<strong>{{ key }}</strong>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
{% if value is string and "km/h" in value %}
|
||||
<span class="badge bg-primary">{{ value }}</span>
|
||||
{% else %}
|
||||
{{ value }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{{ data }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="alert alert-warning">
|
||||
Unable to retrieve vehicle information
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
11
selfdrive/frogpilot/fleetmanager/templates/error.html
Normal file
11
selfdrive/frogpilot/fleetmanager/templates/error.html
Normal file
@@ -0,0 +1,11 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block title %}
|
||||
Error
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<br> Oops
|
||||
<br><br><br><br>
|
||||
{{ error | safe }}
|
||||
{% endblock %}
|
||||
16
selfdrive/frogpilot/fleetmanager/templates/error_log.html
Normal file
16
selfdrive/frogpilot/fleetmanager/templates/error_log.html
Normal file
@@ -0,0 +1,16 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block title %}
|
||||
Error Log
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<br>
|
||||
<h1>Error Log of<br>{{ file_name }}</h1>
|
||||
<br>
|
||||
{% endblock %}
|
||||
|
||||
{% block unformatted %}
|
||||
<pre style="font-size: x-small; margin: 20px;">{{ file_content }}</pre>
|
||||
<br><br>
|
||||
{% endblock %}
|
||||
14
selfdrive/frogpilot/fleetmanager/templates/error_logs.html
Normal file
14
selfdrive/frogpilot/fleetmanager/templates/error_logs.html
Normal file
@@ -0,0 +1,14 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block title %}
|
||||
Error Logs
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<br>
|
||||
<h1>Error Logs</h1>
|
||||
<br>
|
||||
{% for row in rows %}
|
||||
<a href="/error_logs/{{ row }}">{{ row }}</a><br>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
136
selfdrive/frogpilot/fleetmanager/templates/footage.html
Normal file
136
selfdrive/frogpilot/fleetmanager/templates/footage.html
Normal file
@@ -0,0 +1,136 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block title %}
|
||||
Dashcam Routes
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<div class="container-fluid px-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-5">
|
||||
<h1 class="display-5 fw-bold text-white text-shadow-bold">Dashcam Recordings</h1>
|
||||
<i class="fas fa-video fa-2x text-primary"></i>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
{% for row, gif in zipped %}
|
||||
<div class="col-12 col-sm-6 col-md-4 col-lg-3">
|
||||
<div class="card h-100 border-0 shadow-lg hover-effect">
|
||||
<div class="image-container" style="height: 200px; overflow: hidden;">
|
||||
<img src="/previewgif/{{ gif }}"
|
||||
class="card-img-top img-fluid h-100 object-fit-cover"
|
||||
alt="Recording preview"
|
||||
style="filter: brightness(0.9);">
|
||||
</div>
|
||||
|
||||
<div class="card-body bg-dark-gradient position-relative" style="z-index: 10;">
|
||||
<hr class="text-white-50 mt-0 mb-3">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<span class="badge bg-dark bg-opacity-75 fs-6 text-nowrap">
|
||||
<i class="fas fa-clock me-2"></i>{{ row }}
|
||||
</span>
|
||||
<a href="/footage/{{ row }}"
|
||||
class="btn btn-primary btn-sm rounded-pill px-3 hover-grow position-relative"
|
||||
style="z-index: 11;">
|
||||
<i class="fas fa-play me-2"></i>View
|
||||
</a>
|
||||
</div>
|
||||
<div class="text-white-50 small" id="date-{{ row|replace('--', '__') }}">
|
||||
<i class="fas fa-calendar-day me-1"></i> Loading date...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.querySelectorAll('[id^="date-"]').forEach(element => {
|
||||
const route = element.id.replace('date-', '').replace('__', '--');
|
||||
fetchFolderDate(route, element);
|
||||
});
|
||||
});
|
||||
|
||||
async function fetchFolderDate(route, element) {
|
||||
try {
|
||||
const seg0Path = `/data/media/0/realdata/${route}--0`;
|
||||
const seg1Path = `/data/media/0/realdata/${route}--1`;
|
||||
|
||||
let response;
|
||||
|
||||
const seg1Exists = await checkFolderExists(seg1Path);
|
||||
|
||||
if (seg1Exists) {
|
||||
response = await fetch(`/folder-date?path=${encodeURIComponent(seg1Path)}&subtract_minutes=1`);
|
||||
} else {
|
||||
response = await fetch(`/folder-date?path=${encodeURIComponent(seg0Path)}`);
|
||||
}
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.date) {
|
||||
element.innerHTML = `<i class="fas fa-calendar-day me-1"></i> ${data.date}`;
|
||||
} else {
|
||||
element.textContent = "Date not available";
|
||||
}
|
||||
} else {
|
||||
throw new Error('Failed to fetch date');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error fetching date for ${route}:`, error);
|
||||
element.textContent = "Date not available";
|
||||
}
|
||||
}
|
||||
|
||||
async function checkFolderExists(path) {
|
||||
try {
|
||||
const response = await fetch(`/folder-exists?path=${encodeURIComponent(path)}`);
|
||||
const data = await response.json();
|
||||
return data.exists;
|
||||
} catch (error) {
|
||||
console.error(`Error checking folder existence for ${path}:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.hover-effect {
|
||||
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
background: linear-gradient(145deg, #1a1e21, #23282b);
|
||||
border-radius: 12px!important;
|
||||
}
|
||||
|
||||
.hover-effect:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 8px 25px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.bg-dark-gradient {
|
||||
background: linear-gradient(to bottom, #2a2e32, #1f2327);
|
||||
}
|
||||
|
||||
.text-shadow-bold {
|
||||
text-shadow: 0 2px 4px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.hover-grow {
|
||||
transition: transform 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.hover-grow:hover {
|
||||
transform: scale(1.05);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.object-fit-cover {
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
}
|
||||
.badge.bg-dark {
|
||||
font-size: 1rem !important;
|
||||
padding: 0.5em 0.8em;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,18 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block title %}
|
||||
GMap key input
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<form name="setGmapTokenForm" method="post">
|
||||
<fieldset class="uk-fieldset">
|
||||
<legend class="uk-legend">Set your Google Map API Key</legend>
|
||||
<div class="uk-margin">
|
||||
<input class="uk-input" type="text" name="gmap_key_val" placeholder="Google Map API KEY">
|
||||
<input class="uk-button uk-button-primary uk-width-1-1 uk-margin-small-bottom" type="submit" value="Submit">
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
87
selfdrive/frogpilot/fleetmanager/templates/index.html
Normal file
87
selfdrive/frogpilot/fleetmanager/templates/index.html
Normal file
@@ -0,0 +1,87 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block title %}
|
||||
Fleet Manager Dashboard
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<div class="container-fluid">
|
||||
<div class="text-center mb-5">
|
||||
<h1 class="display-4">🚚 Fleet Manager</h1>
|
||||
<p class="lead text-muted">Comprehensive fleet management solution</p>
|
||||
</div>
|
||||
|
||||
<div class="row g-4 justify-content-center">
|
||||
<div class="col-12 col-md-6 col-lg-4">
|
||||
<div class="card hover-effect h-100">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<i class="fas fa-car fa-2x me-2"></i>Vehicle Operations
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<a href="/carinfo" class="btn btn-outline-primary w-100 mb-3">
|
||||
<i class="fas fa-info-circle me-2"></i>Vehicle Information
|
||||
</a>
|
||||
<a href="/preserved" class="btn btn-outline-success w-100 mb-3">
|
||||
<i class="fas fa-archive me-2"></i>Preserved Footage
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6 col-lg-4">
|
||||
<div class="card hover-effect h-100">
|
||||
<div class="card-header bg-info text-white">
|
||||
<i class="fas fa-video fa-2x me-2"></i>Media Management
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<a href="/footage" class="btn btn-outline-info w-100 mb-3">
|
||||
<i class="fas fa-dashcube me-2"></i>Dashcam Footage
|
||||
</a>
|
||||
<a href="/screenrecords" class="btn btn-outline-warning w-100 mb-3">
|
||||
<i class="fas fa-film me-2"></i>Screen Recordings
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-md-6 col-lg-4">
|
||||
<div class="card hover-effect h-100">
|
||||
<div class="card-header bg-secondary text-white">
|
||||
<i class="fas fa-cogs fa-2x me-2"></i>System Management
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<a href="/error_logs" class="btn btn-outline-danger w-100 mb-3">
|
||||
<i class="fas fa-bug me-2"></i>Error Logs
|
||||
</a>
|
||||
<a href="/about" class="btn btn-outline-dark w-100 mb-3">
|
||||
<i class="fas fa-info-circle me-2"></i>About System
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.hover-effect {
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
border-radius: 15px;
|
||||
}
|
||||
.hover-effect:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 20px rgba(0,0,0,0.1);
|
||||
}
|
||||
.card-header {
|
||||
border-radius: 15px 15px 0 0 !important;
|
||||
}
|
||||
.btn {
|
||||
transition: all 0.2s;
|
||||
text-align: left;
|
||||
padding: 1rem;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.btn:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
103
selfdrive/frogpilot/fleetmanager/templates/layout.html
Normal file
103
selfdrive/frogpilot/fleetmanager/templates/layout.html
Normal file
@@ -0,0 +1,103 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" id="htmlElement">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="initial-scale=1, width=device-width">
|
||||
<link href="/static/favicon.ico" rel="icon">
|
||||
<!-- http://getbootstrap.com/docs/5.3/ -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-KK94CHFLLe+nY2dmCWGMq91rCGa5gtU4mk92HdvYe+M/SXH301p5ILy+dN9+nJOZ" crossorigin="anonymous">
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ENjdO4Dr2bkBIFxQpeoTz1HIcje39Wm4jDKdf19U8gI4ddQ3GYNS7NTKfAdVQSZe" crossorigin="anonymous"></script>
|
||||
<style>
|
||||
.navbar-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.navbar-brand img {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
</style>
|
||||
<!-- 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>
|
||||
/* Dark mode styles */
|
||||
#htmlElement.dark-mode,
|
||||
#htmlElement.dark-mode input,
|
||||
#htmlElement.dark-mode select,
|
||||
#htmlElement.dark-mode body,
|
||||
#htmlElement.dark-mode h1 {
|
||||
background-color: #121212; /* Dark background color */
|
||||
color: #ffffff; /* Light text color */
|
||||
}
|
||||
.nav-link {
|
||||
display: inline-block;
|
||||
padding: 0 15px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
<title>CarrotPilot: {% block title %}{% endblock %}</title>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-fixed-top navbar-expand-sm navbar-dark bg-dark">
|
||||
<div class="container">
|
||||
<a href="/" class="navbar-brand mb-0 h1">
|
||||
<img class="d-inline-block align-top mr-2" src="/static/carrot.png" /> CarrotPilot </a>
|
||||
<button type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" class="navbar-toggler" aria-controls="navbarNav" aria-expanded="false" arial-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse ml-auto" id="navbarNav">
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item active">
|
||||
<a href="/footage" class="nav-link">Dashcam Routes</a>
|
||||
</li>
|
||||
<li class="nav-item active">
|
||||
<a href="/screenrecords" class="nav-link">Screen Recordings</a>
|
||||
</li>
|
||||
<li class="nav-item active">
|
||||
<a href="/error_logs" class="nav-link">Error Logs</a>
|
||||
</li>
|
||||
<li class="nav-item active">
|
||||
<a href="/addr_input" class="nav-link">Navigation</a>
|
||||
</li>
|
||||
<li class="nav-item active">
|
||||
<a href="/tools" class="nav-link">Tools</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<main class="container-fluid p-7 text-center"> {% block main %}{% endblock %} </main>
|
||||
{% block unformatted %}{% endblock %}
|
||||
|
||||
<button class="uk-button uk-button-default uk-margin-small-right" onclick="toggleDarkMode()">Toggle Dark Mode</button>
|
||||
|
||||
<script>
|
||||
function setCookie(name, value, days) {
|
||||
const date = new Date();
|
||||
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
|
||||
document.cookie = `${name}=${value};expires=${date.toUTCString()};path=/`;
|
||||
}
|
||||
function getCookie(name) {
|
||||
return document.cookie.split('; ')
|
||||
.find(row => row.startsWith(name))
|
||||
?.split('=')[1] || null;
|
||||
}
|
||||
function toggleDarkMode() {
|
||||
console.log('Toggle Dark Mode function called');
|
||||
const htmlElement = document.documentElement;
|
||||
htmlElement.classList.toggle('dark-mode');
|
||||
setCookie('darkMode', htmlElement.classList.contains('dark-mode'), 365);
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const htmlElement = document.documentElement;
|
||||
htmlElement.classList.toggle('dark-mode', getCookie('darkMode') === 'true');
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,28 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block title %}
|
||||
Nav Search Confirmation
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<div><img src="https://api.mapbox.com/styles/v1/mapbox/streets-v11/static/pin-s-l+000({{lon}},{{lat}})/{{lon}},{{lat}},14/300x300?access_token={{token}}" /></div>
|
||||
<div style="padding: 5px; font-size: 10px;">{{addr}}</div>
|
||||
<form name="navForm" method="post">
|
||||
<fieldset class="uk-fieldset">
|
||||
<div class="uk-margin">
|
||||
<input type="hidden" name="name" value="{{addr}}">
|
||||
<input type="hidden" name="lat" value="{{lat}}">
|
||||
<input type="hidden" name="lon" value="{{lon}}">
|
||||
<select id="save_type" name="save_type" class="uk-select">
|
||||
<option value="recent">Recent</option>
|
||||
<option value="home">Set Home</option>
|
||||
<option value="work">Set Work</option>
|
||||
<option value="fav1">Set Favorite 1</option>
|
||||
<option value="fav2">Set Favorite 2</option>
|
||||
<option value="fav3">Set Favorite 3</option>
|
||||
</select>
|
||||
<input class="uk-button uk-button-primary uk-width-1-1 uk-margin-small-bottom" type="submit" value="Start Navigation">
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
{% endblock %}
|
||||
149
selfdrive/frogpilot/fleetmanager/templates/nav_directions.html
Normal file
149
selfdrive/frogpilot/fleetmanager/templates/nav_directions.html
Normal file
@@ -0,0 +1,149 @@
|
||||
{% block main %}
|
||||
<div id="destinationHeading" style="font-weight: bold;"></div>
|
||||
<div id="jsonOutput" style="text-align: left;"></div>
|
||||
<style>
|
||||
/* Added CSS styles to display images and text on the same line */
|
||||
#jsonOutput span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#jsonOutput img {
|
||||
margin-right: 10px; /* Adjust the margin as needed */
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
let useMetricUnits = false;
|
||||
let previousNavdirectionsUuid = null;
|
||||
let previousCurrentStepUuid = null;
|
||||
let jsonData = null;
|
||||
let initNav = 0;
|
||||
|
||||
async function loadCurrentStep() {
|
||||
try {
|
||||
const response = await fetch('CurrentStep.json'); // Load CurrentStep.json
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch CurrentStep.json.');
|
||||
}
|
||||
|
||||
const json = await response.json();
|
||||
return json;
|
||||
} catch (error) {
|
||||
console.error('Error fetching or parsing CurrentStep.json:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadNavdirectionsData() {
|
||||
try {
|
||||
const response = await fetch('navdirections.json'); // Load navdirections.json
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch JSON file. Status: ${response.status}`);
|
||||
}
|
||||
|
||||
const json = await response.json();
|
||||
|
||||
// Check if the UUIDs match
|
||||
const match = json.uuid === previousCurrentStepUuid;
|
||||
|
||||
previousNavdirectionsUuid = json.uuid;
|
||||
jsonData = json;
|
||||
initNav = 1;
|
||||
return jsonData;
|
||||
} catch (error) {
|
||||
console.error('Error fetching or parsing JSON data:', error);
|
||||
return jsonData; // Return the existing data on error
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchAndDisplayData() {
|
||||
const currentStepData = await loadCurrentStep();
|
||||
|
||||
if (currentStepData !== null) {
|
||||
// Set the initial value for `currentStep` based on `CurrentStep.json`
|
||||
previousCurrentStepUuid = currentStepData.uuid;
|
||||
}
|
||||
|
||||
if (currentStepData.uuid != previousNavdirectionsUuid) {
|
||||
await loadNavdirectionsData();
|
||||
}
|
||||
|
||||
if (initNav === 0) {
|
||||
await loadNavdirectionsData();
|
||||
}
|
||||
|
||||
// Check if jsonData is available and proceed
|
||||
if (jsonData) {
|
||||
// Access the data you need from the loaded JSON
|
||||
const firstRoute = jsonData.routes[0];
|
||||
const firstLeg = firstRoute.legs[0];
|
||||
const steps = firstLeg.steps;
|
||||
const destination = firstRoute.Destination;
|
||||
|
||||
// Determine whether to use metric or imperial units based on the 'Metric' key
|
||||
useMetricUnits = firstRoute.Metric === true; // Removed `const` to update the global useMetricUnits
|
||||
|
||||
// Display the 'destination' value on the webpage
|
||||
const destinationHeading = document.getElementById('destinationHeading');
|
||||
destinationHeading.textContent = `Destination: ${destination}`;
|
||||
|
||||
// Display values from the steps
|
||||
const jsonOutputDiv = document.getElementById('jsonOutput');
|
||||
jsonOutputDiv.innerHTML = '';
|
||||
|
||||
for (let i = currentStepData.CurrentStep; i < steps.length - 1; i++) {
|
||||
const step = steps[i];
|
||||
const instruction0 = steps[i].maneuver.instruction;
|
||||
const instruction = steps[i + 1].maneuver.instruction;
|
||||
const maneuverType = steps[i + 1].maneuver.type;
|
||||
const modifier = steps[i + 1].maneuver.modifier;
|
||||
let distance = step.distance;
|
||||
|
||||
if (!useMetricUnits) {
|
||||
// Convert distance to miles if using imperial units
|
||||
distance = distance * 0.000621371;
|
||||
} else {
|
||||
distance = distance / 1000; // Convert meters to kilometers
|
||||
}
|
||||
const sanitizedManeuverType = maneuverType.replace(/\s+/g, '_');
|
||||
const sanitizedModifier = modifier ? `_${modifier.replace(/\s+/g, '_')}` : '';
|
||||
const filterStyle = !htmlElement.classList.contains('dark-mode') ? 'filter: invert(100%);' : '';
|
||||
// Display the values on the webpage
|
||||
if (i === 0) {
|
||||
jsonOutputDiv.innerHTML += `
|
||||
<hr>
|
||||
<span>
|
||||
<img src="/navigation/direction_depart.png" alt="${maneuverType} icon" width="25" height="25" style="${filterStyle}">
|
||||
<p>${instruction0}</p>
|
||||
</span>
|
||||
<hr>
|
||||
`;
|
||||
}
|
||||
jsonOutputDiv.innerHTML += `
|
||||
<span>
|
||||
<img src="/navigation/direction_${sanitizedManeuverType}${sanitizedModifier}.png" alt="${maneuverType} icon" width="25" height="25" style="${filterStyle}">
|
||||
<p>In ${distance.toFixed(1)} ${useMetricUnits ? 'km' : 'miles'}: ${instruction}</p>
|
||||
</span>
|
||||
<hr>
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load `CurrentStep.json` initially
|
||||
loadCurrentStep().then((currentStepData) => {
|
||||
if (currentStepData !== null) {
|
||||
// Set the initial value for `currentStep` based on `CurrentStep.json`
|
||||
previousCurrentStepUuid = currentStepData.uuid;
|
||||
loadNavdirectionsData();
|
||||
// Fetch and display data initially
|
||||
fetchAndDisplayData();
|
||||
}
|
||||
});
|
||||
|
||||
// Periodically fetch `CurrentStep.json` and display data every 5 seconds
|
||||
setInterval(fetchAndDisplayData, 5000); // Adjust the interval as needed (in milliseconds)
|
||||
</script>
|
||||
{% endblock %}
|
||||
13
selfdrive/frogpilot/fleetmanager/templates/nonprime.html
Normal file
13
selfdrive/frogpilot/fleetmanager/templates/nonprime.html
Normal file
@@ -0,0 +1,13 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block title %}
|
||||
Nav Driving Directions
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% with gmap_key=gmap_key, lon=lon, lat=lat, home=home, work=work, fav1=fav1, fav2=fav2, fav3=fav3 %}
|
||||
{% include "addr_input.html" %}
|
||||
{% endwith %}
|
||||
|
||||
{% include "nav_directions.html" %}
|
||||
{% endblock %}
|
||||
30
selfdrive/frogpilot/fleetmanager/templates/preserved.html
Normal file
30
selfdrive/frogpilot/fleetmanager/templates/preserved.html
Normal file
@@ -0,0 +1,30 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block title %}
|
||||
Preserved Routes
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<br>
|
||||
<h1>Preserved Routes</h1>
|
||||
<br>
|
||||
<div class="row">
|
||||
{% for route_path, gif_path, segment in zipped %}
|
||||
<div class="col-xs-6 col-sm-4 col-md-3">
|
||||
<div class="card mb-4 shadow-sm" style="background-color: #212529; color: white;">
|
||||
<div class="gif-container">
|
||||
<img src="/previewgif/{{ gif_path }}" class="card-img-top static-gif" alt="GIF">
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="card-text">{{ segment }}</p>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div class="btn-group">
|
||||
<a href="/footage/{{ route_path }}" class="btn btn-sm btn-outline-secondary">View Footage</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
9
selfdrive/frogpilot/fleetmanager/templates/prime.html
Normal file
9
selfdrive/frogpilot/fleetmanager/templates/prime.html
Normal file
@@ -0,0 +1,9 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block title %}
|
||||
Driving Directions
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% include "nav_directions.html" %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,19 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block title %}
|
||||
addr_input
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<form name="setPkTokenForm" method="post">
|
||||
<fieldset class="uk-fieldset">
|
||||
<legend class="uk-legend">Set your Mapbox <b>Public Token</b></legend>
|
||||
<div style="padding: 5px; color: red; font-weight: bold;">{{msg}}</div>
|
||||
<div class="uk-margin">
|
||||
<input class="uk-input" type="text" name="pk_token_val" placeholder="pk.xxxxxxx...">
|
||||
<input class="uk-button uk-button-primary uk-width-1-1 uk-margin-small-bottom" type="submit" value="Set">
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
409
selfdrive/frogpilot/fleetmanager/templates/route.html
Normal file
409
selfdrive/frogpilot/fleetmanager/templates/route.html
Normal file
@@ -0,0 +1,409 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block title %}
|
||||
Dashcam Segments
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% autoescape false %}
|
||||
<br>
|
||||
<h1>Dashcam Segments (one per minute)</h1>
|
||||
<br>
|
||||
<video id="video" width="640" height="480" controls autoplay style="background:black">
|
||||
</video>
|
||||
<br><br>
|
||||
<div class="camera-switcher d-flex justify-content-center gap-2 mb-3">
|
||||
<a href="{{ route }}?{{ query_segment }},qcamera"
|
||||
class="btn btn-sm {% if query_type == 'qcamera' %}btn-primary{% else %}btn-outline-primary{% endif %}">qcamera</a>
|
||||
<a href="{{ route }}?{{ query_segment }},fcamera"
|
||||
class="btn btn-sm {% if query_type == 'fcamera' %}btn-primary{% else %}btn-outline-primary{% endif %}">fcamera</a>
|
||||
<a href="{{ route }}?{{ query_segment }},dcamera"
|
||||
class="btn btn-sm {% if query_type == 'dcamera' %}btn-primary{% else %}btn-outline-primary{% endif %}">dcamera</a>
|
||||
<a href="{{ route }}?{{ query_segment }},ecamera"
|
||||
class="btn btn-sm {% if query_type == 'ecamera' %}btn-primary{% else %}btn-outline-primary{% endif %}">ecamera</a>
|
||||
</div>
|
||||
<div class="video-info">
|
||||
current segment: <span id="currentsegment" class="badge bg-primary"></span>
|
||||
<br>
|
||||
current view: <span id="currentview" class="badge bg-secondary"></span>
|
||||
<br>
|
||||
date: <span id="folderDate" class="badge bg-info"></span>
|
||||
</div>
|
||||
<br>
|
||||
<div class="download-section text-center">
|
||||
<div class="d-flex flex-column align-items-center gap-3">
|
||||
<div>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span>Full Downloads:</span>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<a download="{{ route }}-qcamera.mp4" href="/footage/full/qcamera/{{ route }}" class="btn btn-sm btn-outline-primary">qcamera</a>
|
||||
<a download="{{ route }}-fcamera.mp4" href="/footage/full/fcamera/{{ route }}" class="btn btn-sm btn-outline-primary">fcamera</a>
|
||||
<a download="{{ route }}-dcamera.mp4" href="/footage/full/dcamera/{{ route }}" class="btn btn-sm btn-outline-primary">dcamera</a>
|
||||
<a download="{{ route }}-ecamera.mp4" href="/footage/full/ecamera/{{ route }}" class="btn btn-sm btn-outline-primary">ecamera</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span>Segment Downloads:</span>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<a download="{{ route }}-rlog-{{query_segment}}.mp4" href="/footage/full/rlog/{{ route }}/{{query_segment}}" class="btn btn-sm btn-outline-secondary">rlog</a>
|
||||
<a download="{{ route }}-qcamera-{{query_segment}}.mp4" href="/footage/full/qcamera/{{ route }}/{{query_segment}}" class="btn btn-sm btn-outline-secondary">qcamera</a>
|
||||
<a download="{{ route }}-fcamera-{{query_segment}}.mp4" href="/footage/full/fcamera/{{ route }}/{{query_segment}}" class="btn btn-sm btn-outline-secondary">fcamera</a>
|
||||
<a download="{{ route }}-dcamera-{{query_segment}}.mp4" href="/footage/full/dcamera/{{ route }}/{{query_segment}}" class="btn btn-sm btn-outline-secondary">dcamera</a>
|
||||
<a download="{{ route }}-ecamera-{{query_segment}}.mp4" href="/footage/full/ecamera/{{ route }}/{{query_segment}}" class="btn btn-sm btn-outline-secondary">ecamera</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="segment-controls mt-4">
|
||||
<h3>Segment List</h3>
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<button onclick="toggleAllSegments()" class="btn btn-sm btn-outline-secondary me-2">Select All</button>
|
||||
<div id="selectedCount" class="text-muted">0 segments selected</div>
|
||||
</div>
|
||||
|
||||
<div class="segment-list" style="max-height: 400px; overflow-y: auto;">
|
||||
{% for segment in segments.split(',') %}
|
||||
{% set clean_segment = segment.strip().strip("'") %}
|
||||
{% if clean_segment %}
|
||||
{% set seg_num = clean_segment.split('--')[2] %}
|
||||
<div class="segment-item d-flex align-items-start py-2 border-bottom ps-2">
|
||||
<input type="checkbox"
|
||||
name="selected_segments"
|
||||
value="{{ clean_segment }}"
|
||||
class="form-check-input me-3 segment-checkbox mt-1"
|
||||
style="transform: scale(1.5)">
|
||||
<div class="d-flex flex-column">
|
||||
<a href="{{ route }}?{{ seg_num }},{{ query_type }}" class="text-decoration-none">
|
||||
{{ clean_segment }}
|
||||
</a>
|
||||
<small class="text-muted" id="segment-info-{{ seg_num }}">
|
||||
<i class="bi bi-clock-history me-1"></i><span id="segment-date-{{ seg_num }}">Loading date...</span>
|
||||
<span class="mx-2">•</span>
|
||||
<i class="bi bi-hdd me-1"></i><span id="segment-size-{{ seg_num }}">Loading size...</span>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<button class="btn btn-danger" onclick="uploadSelectedSegments()">
|
||||
<i class="bi bi-upload me-2"></i>
|
||||
Upload All Selected Routes To Carrot Server
|
||||
</button>
|
||||
<div id="uploadStatus" class="mt-2"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const segments = [{{ segments }}];
|
||||
const currentSegment = "{{ route }}--{{ query_segment }}";
|
||||
let currentSegmentIndex = segments.findIndex(seg => seg === currentSegment);
|
||||
if (currentSegmentIndex === -1) currentSegmentIndex = 0;
|
||||
|
||||
const video = document.getElementById('video');
|
||||
video.src = `/footage/{{ query_type }}/${currentSegment}`;
|
||||
document.getElementById("currentsegment").textContent = currentSegment;
|
||||
document.getElementById("currentview").textContent = "{{ query_type }}";
|
||||
video.load();
|
||||
video.play().catch(e => console.log("Autoplay prevented:", e));
|
||||
|
||||
video.addEventListener('ended', function() {
|
||||
currentSegmentIndex = (currentSegmentIndex + 1) % segments.length;
|
||||
const nextSegment = segments[currentSegmentIndex];
|
||||
const segNum = nextSegment.split('--')[2];
|
||||
window.location.href = `{{ route }}?${segNum},{{ query_type }}`;
|
||||
});
|
||||
|
||||
function toggleAllSegments() {
|
||||
const checkboxes = document.querySelectorAll('.segment-checkbox');
|
||||
const allChecked = Array.from(checkboxes).every(cb => cb.checked);
|
||||
checkboxes.forEach(cb => cb.checked = !allChecked);
|
||||
updateSelectionCount();
|
||||
}
|
||||
|
||||
function updateSelectionCount() {
|
||||
const count = document.querySelectorAll('.segment-checkbox:checked').length;
|
||||
document.getElementById('selectedCount').textContent = `${count} segments selected`;
|
||||
}
|
||||
|
||||
async function getFolderInfo(folderPath) {
|
||||
try {
|
||||
const response = await fetch(`/folder-info?path=${encodeURIComponent(folderPath)}`);
|
||||
if (!response.ok) throw new Error('Network response was not ok');
|
||||
return await response.json();
|
||||
} catch (e) {
|
||||
console.error(`Error getting folder info for ${folderPath}:`, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(timestamp) {
|
||||
const date = new Date(timestamp * 1000);
|
||||
return date.toLocaleString();
|
||||
}
|
||||
|
||||
function formatBytes(bytes, decimals = 2) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
async function updateCurrentFolderInfo() {
|
||||
const folderPath = `/data/media/0/realdata/${currentSegment}`;
|
||||
const info = await getFolderInfo(folderPath);
|
||||
const dateElement = document.getElementById('folderDate');
|
||||
|
||||
if (info && info.created_date) {
|
||||
dateElement.textContent = info.created_date;
|
||||
if (info.size) {
|
||||
dateElement.textContent += ` • ${formatBytes(info.size)}`;
|
||||
}
|
||||
} else {
|
||||
dateElement.textContent = "정보 없음";
|
||||
}
|
||||
}
|
||||
|
||||
async function updateSegmentListInfo() {
|
||||
for (const segment of segments) {
|
||||
const cleanSegment = segment.trim().replace(/'/g, '');
|
||||
if (!cleanSegment) continue;
|
||||
|
||||
const segNum = cleanSegment.split('--')[2];
|
||||
const folderPath = `/data/media/0/realdata/${cleanSegment}`;
|
||||
|
||||
const info = await getFolderInfo(folderPath);
|
||||
const dateElement = document.getElementById(`segment-date-${segNum}`);
|
||||
const sizeElement = document.getElementById(`segment-size-${segNum}`);
|
||||
|
||||
if (info) {
|
||||
if (info.created_date) {
|
||||
dateElement.textContent = info.created_date;
|
||||
} else {
|
||||
dateElement.textContent = "날짜 없음";
|
||||
}
|
||||
if (info.size) {
|
||||
sizeElement.textContent = formatBytes(info.size);
|
||||
} else {
|
||||
sizeElement.textContent = "크기 없음";
|
||||
}
|
||||
} else {
|
||||
dateElement.textContent = "정보 없음";
|
||||
sizeElement.textContent = "정보 없음";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadSelectedSegments() {
|
||||
const video = document.getElementById('video');
|
||||
video.pause();
|
||||
const selected = Array.from(document.querySelectorAll('.segment-checkbox:checked'))
|
||||
.map(cb => cb.value);
|
||||
|
||||
if(selected.length === 0) {
|
||||
showUploadStatus('No segments selected!', 'danger');
|
||||
return;
|
||||
}
|
||||
|
||||
const progressUI = `
|
||||
<div class="upload-progress mt-3">
|
||||
<div class="progress mb-2">
|
||||
<div id="uploadProgressBar" class="progress-bar progress-bar-striped progress-bar-animated"
|
||||
role="progressbar" style="width: 0%"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6 text-start">
|
||||
<small id="uploadProgressText">Preparing upload...</small>
|
||||
</div>
|
||||
<div class="col-6 text-end">
|
||||
<small id="uploadSpeed">-</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-1">
|
||||
<div class="col-12">
|
||||
<small id="uploadFileInfo" class="text-muted">-</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
const statusDiv = document.getElementById('uploadStatus');
|
||||
statusDiv.innerHTML = progressUI;
|
||||
|
||||
try {
|
||||
let totalSize = 0;
|
||||
let uploadedSize = 0;
|
||||
const startTime = Date.now();
|
||||
const uploads = [];
|
||||
const selectedSegmentsCount = selected.length;
|
||||
|
||||
document.getElementById('uploadProgressText').textContent = 'Calculating total size...';
|
||||
for (const segment of selected) {
|
||||
const segmentNum = segment.split('--')[2];
|
||||
const files = ['rlog.zst', 'qcamera.ts'];
|
||||
|
||||
for (const file of files) {
|
||||
try {
|
||||
const response = await fetch(`/file-size?path=/data/media/0/realdata/{{ route }}--${segmentNum}/${file}`);
|
||||
const { size } = await response.json();
|
||||
if (size) {
|
||||
totalSize += size;
|
||||
uploads.push({ segmentNum, file, size });
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`Size check failed for ${file}`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const initialUploadMsg = `Uploading ${uploads.length} files (${selectedSegmentsCount} Segments) ${formatBytes(totalSize)}...`;
|
||||
showUploadStatus(initialUploadMsg, 'info');
|
||||
document.getElementById('uploadProgressText').textContent = initialUploadMsg;
|
||||
document.getElementById('uploadFileInfo').textContent =
|
||||
`${selectedSegmentsCount} Segments | ${uploads.length} files | ${formatBytes(totalSize)}`;
|
||||
|
||||
for (const { segmentNum, file, size } of uploads) {
|
||||
try {
|
||||
const filePath = `/data/media/0/realdata/{{ route }}--${segmentNum}/${file}`;
|
||||
document.getElementById('uploadFileInfo').textContent =
|
||||
`Uploading: ${file} (${formatBytes(size)})`;
|
||||
|
||||
const formData = new FormData();
|
||||
const blob = await fetch(filePath).then(r => r.blob());
|
||||
formData.append('file', blob, file);
|
||||
formData.append('segment', segmentNum);
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', `/footage/full/upload_carrot/{{ route }}/${segmentNum}`, true);
|
||||
|
||||
xhr.upload.onprogress = function(e) {
|
||||
if (e.lengthComputable && totalSize > 0) {
|
||||
const loaded = uploadedSize + e.loaded;
|
||||
const percent = Math.round(loaded / totalSize * 100);
|
||||
const elapsed = (Date.now() - startTime) / 1000;
|
||||
const speed = elapsed > 0 ? loaded / (1024 * 1024 * elapsed) : 0;
|
||||
|
||||
document.getElementById('uploadProgressBar').style.width = `${percent}%`;
|
||||
document.getElementById('uploadProgressText').textContent =
|
||||
`${percent}% (${formatBytes(loaded)}/${formatBytes(totalSize)})`;
|
||||
document.getElementById('uploadSpeed').textContent =
|
||||
`${speed.toFixed(2)} MB/s`;
|
||||
}
|
||||
};
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
xhr.onload = () => {
|
||||
if (xhr.status === 200) {
|
||||
uploadedSize += size;
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`Upload failed: ${xhr.statusText}`));
|
||||
}
|
||||
};
|
||||
xhr.onerror = () => reject(new Error('Upload failed'));
|
||||
xhr.send(formData);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Upload error for ${file}:`, error);
|
||||
showUploadStatus(`Error: ${error.message}`, 'danger');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const successAlert = document.createElement('div');
|
||||
successAlert.className = 'alert alert-success';
|
||||
successAlert.textContent = `Upload complete! ${selectedSegmentsCount} segments uploaded`;
|
||||
statusDiv.prepend(successAlert);
|
||||
|
||||
const initialAlert = statusDiv.querySelector('.alert-info');
|
||||
if (initialAlert) initialAlert.remove();
|
||||
|
||||
const progressElements = document.querySelectorAll('.upload-progress, #uploadProgressBar, #uploadProgressText, #uploadSpeed, #uploadFileInfo');
|
||||
progressElements.forEach(el => el.remove());
|
||||
|
||||
} catch (error) {
|
||||
console.error('Upload failed:', error);
|
||||
showUploadStatus(`Upload failed: ${error.message}`, 'danger');
|
||||
document.getElementById('uploadProgressBar').classList.remove('progress-bar-animated');
|
||||
document.getElementById('uploadProgressBar').classList.add('bg-danger');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function showUploadStatus(message, type, append = false) {
|
||||
const statusDiv = document.getElementById('uploadStatus');
|
||||
if (!append) {
|
||||
const alertDiv = document.createElement('div');
|
||||
alertDiv.className = `alert alert-${type} mb-2`;
|
||||
alertDiv.textContent = message;
|
||||
statusDiv.prepend(alertDiv);
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
updateSelectionCount();
|
||||
updateCurrentFolderInfo();
|
||||
updateSegmentListInfo();
|
||||
});
|
||||
</script>
|
||||
{% endautoescape %}
|
||||
<br><br>
|
||||
{% endblock %}
|
||||
|
||||
<style>
|
||||
.upload-progress {
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.progress {
|
||||
height: 20px;
|
||||
}
|
||||
.progress-bar {
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
.download-section .btn-group .btn {
|
||||
border-radius: 20px !important;
|
||||
margin: 0 2px;
|
||||
}
|
||||
.download-section span.me-2 {
|
||||
font-size: 0.9em;
|
||||
color: #666;
|
||||
}
|
||||
.download-section .btn {
|
||||
min-width: 90px;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
.download-section .d-flex.align-items-center {
|
||||
flex-wrap: wrap;
|
||||
row-gap: 8px;
|
||||
}
|
||||
.camera-switcher .btn {
|
||||
min-width: 80px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.segment-item {
|
||||
align-items: flex-start !important;
|
||||
}
|
||||
.segment-item small {
|
||||
font-size: 0.8rem;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.segment-item .form-check-input {
|
||||
margin-top: 0.3rem;
|
||||
}
|
||||
.badge {
|
||||
font-weight: 500;
|
||||
}
|
||||
#folderDate {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,94 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block title %}
|
||||
Screen Recordings
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<div class="container-fluid">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12 col-lg-8">
|
||||
<div class="card shadow mb-5">
|
||||
<div class="card-header bg-dark text-white">
|
||||
<i class="fas fa-video me-2"></i>Currently Playing
|
||||
</div>
|
||||
<div class="card-body text-center">
|
||||
<video id="video" class="screen-player" controls autoplay>
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
<div class="mt-3">
|
||||
<span class="badge bg-secondary">
|
||||
<i class="fas fa-eye me-2"></i>
|
||||
<span id="mycurrentview"></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer bg-light">
|
||||
<a href="/screenrecords/download/{{ clip }}" class="btn btn-danger btn-sm">
|
||||
<i class="fas fa-download me-2"></i>Download Current Recording
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<i class="fas fa-list-ul me-2"></i>Available Recordings
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="list-group">
|
||||
{% for row in rows %}
|
||||
<a href="/screenrecords/{{ row[0] }}" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<i class="fas fa-file-video me-3 text-primary"></i>
|
||||
{{ row[0]|replace('.mp4', '')|replace('_', ' ') }}
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<small class="text-muted d-block">
|
||||
{{ row[0]|datetimeformat }}
|
||||
</small>
|
||||
<small class="text-muted">
|
||||
{{ (row[1] / 1024 / 1024)|round(2) }} MB
|
||||
</small>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.screen-player {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
max-width: 640px;
|
||||
border-radius: 10px;
|
||||
background: #000;
|
||||
border: 3px solid #dee2e6;
|
||||
}
|
||||
|
||||
.list-group-item {
|
||||
transition: all 0.2s;
|
||||
margin: 2px 0;
|
||||
border-radius: 5px!important;
|
||||
}
|
||||
|
||||
.list-group-item:hover {
|
||||
transform: translateX(5px);
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
const video = document.getElementById("video");
|
||||
video.src = "/screenrecords/play/pipe/{{ clip }}";
|
||||
document.getElementById("mycurrentview").textContent = "{{ clip }}".replace('.mp4', '');
|
||||
|
||||
video.addEventListener('play', () => {
|
||||
video.style.maxWidth = '100%';
|
||||
video.style.height = 'auto';
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
86
selfdrive/frogpilot/fleetmanager/templates/tools.html
Normal file
86
selfdrive/frogpilot/fleetmanager/templates/tools.html
Normal file
@@ -0,0 +1,86 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block title %}
|
||||
Tools
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<div class="container">
|
||||
<h1 class="my-4">Toggle Values Manager</h1>
|
||||
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-body">
|
||||
<textarea
|
||||
id="toggleValuesBox"
|
||||
class="form-control"
|
||||
style="height: 300px; font-family: monospace;"
|
||||
placeholder='Enter Values here...'
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||
<button
|
||||
id="retrieveButton"
|
||||
class="btn btn-primary"
|
||||
onclick="fetchToggleValues()"
|
||||
>
|
||||
<i class="fas fa-download me-2"></i>Get Values
|
||||
</button>
|
||||
<button
|
||||
id="submitButton"
|
||||
class="btn btn-success"
|
||||
onclick="saveToggleValues()"
|
||||
>
|
||||
<i class="fas fa-upload me-2"></i>Save Values
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function fetchToggleValues() {
|
||||
try {
|
||||
const response = await fetch('/get_toggle_values');
|
||||
const data = await response.json();
|
||||
document.getElementById('toggleValuesBox').value =
|
||||
JSON.stringify(data, null, 2);
|
||||
} catch (error) {
|
||||
alert('Error fetching values');
|
||||
}
|
||||
}
|
||||
|
||||
async function saveToggleValues() {
|
||||
const textarea = document.getElementById('toggleValuesBox');
|
||||
try {
|
||||
const jsonData = JSON.parse(textarea.value);
|
||||
|
||||
const response = await fetch('/store_toggle_values', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(jsonData),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert('Successfully saved!');
|
||||
} else {
|
||||
throw new Error('Save failed');
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Invalid JSON format!');
|
||||
textarea.classList.add('is-invalid');
|
||||
setTimeout(() => textarea.classList.remove('is-invalid'), 2000);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.card {
|
||||
border-radius: 12px;
|
||||
}
|
||||
textarea {
|
||||
white-space: pre;
|
||||
overflow-wrap: normal;
|
||||
overflow-x: auto;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user