Release 260111

This commit is contained in:
Comma Device
2026-01-11 18:23:29 +08:00
commit 3721ecbf8a
2601 changed files with 855070 additions and 0 deletions

View 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.

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

View File

@@ -0,0 +1 @@
../../../selfdrive/assets/img_spinner_comma.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

View 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 %}

View 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 %}

View 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 %}

View 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 %}

View File

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

View File

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

View 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 %}

View File

@@ -0,0 +1,11 @@
{% extends "layout.html" %}
{% block title %}
Error
{% endblock %}
{% block main %}
<br> Oops
<br><br><br><br>
{{ error | safe }}
{% endblock %}

View 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 %}

View 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 %}

View 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 %}

View File

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

View 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 %}

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

View File

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

View 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 %}

View 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 %}

View 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 %}

View File

@@ -0,0 +1,9 @@
{% extends "layout.html" %}
{% block title %}
Driving Directions
{% endblock %}
{% block main %}
{% include "nav_directions.html" %}
{% endblock %}

View File

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

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

View File

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

View 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 %}