FrogPilot community - Fleet Manager

Credit goes to mike8643!

https: //github.com/mike8643
Co-Authored-By: mike8643 <98910897+mike8643@users.noreply.github.com>
This commit is contained in:
FrogAi 2024-05-09 22:21:36 -07:00
parent 42fb2b8dc2
commit b76d283f96
31 changed files with 1864 additions and 0 deletions

View File

@ -0,0 +1,5 @@
# Fleet Manager
Fleet Manger 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,349 @@
#!/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 random
import secrets
import threading
import time
from flask import Flask, jsonify, render_template, Response, request, send_from_directory, session, redirect, url_for
import requests
from requests.exceptions import ConnectionError
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
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 # 5KiB
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/<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")
if str(request.query_string) == "b''":
query_segment = str("0")
query_type = "qcamera"
else:
query_segment = (str(request.query_string).split(","))[0][2:]
query_type = (str(request.query_string).split(","))[1][:-1]
links = ""
segments = ""
for segment in fleet.segments_in_route(route):
links += "<a href='"+route+"?"+segment.split("--")[2]+","+query_type+"'>"+segment+"</a><br>"
segments += "'"+segment+"',"
return render_template("route.html", route=route, query_type=query_type, links=links, segments=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)
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)
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 at:<br><br>" + fleet.SCREENRECORD_PATH)
return render_template("screenrecords.html", rows=rows, clip=rows[0])
@app.route("/screenrecords/<clip>")
def screenrecord(clip):
return render_template("screenrecords.html", rows=fleet.list_files(fleet.SCREENRECORD_PATH), 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 = float(0.0)
lat = float(0.0)
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 fleet.get_nav_active():
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
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,465 @@
# 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
import time
# otisserv conversion
from common.params import Params, ParamKeyType
from flask import render_template, request, session
from functools import wraps
from pathlib import Path
from openpilot.system.hardware import PC
from openpilot.system.hardware.hw import Paths
from openpilot.system.loggerd.uploader import listdir_by_creation
from tools.lib.route import SegmentName
from typing import List
from openpilot.system.loggerd.xattr_cache import getxattr
# otisserv conversion
from urllib.parse import parse_qs, 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/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):
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 += ["-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 += ["-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():
try:
nav_destinations = json.loads(params.get("ApiCache_NavDestinations", encoding='utf8'))
except TypeError:
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 += "&proximity=%s,%s" % (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):
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", "{\"latitude\": %f, \"longitude\": %f, \"place_name\": \"%s\"}" % (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":
id = None
if len(type_label_ids["recent"]) > 10:
dests.pop(type_label_ids["recent"][-1])
else:
id = type_label_ids[save_type]
if id is None:
dests.insert(0, new_dest)
else:
dests[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
def get_all_toggle_values():
toggle_values = {}
for key in params.all_keys():
key = key.decode('utf-8') if isinstance(key, bytes) else key
if params.get_key_type(key) & ParamKeyType.FROGPILOT_STORAGE:
try:
value = params.get(key)
value = value.decode('utf-8') if isinstance(value, bytes) else value
except Exception:
value = "0"
toggle_values[key] = value 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)

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,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 unformated %}
<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,28 @@
{% extends "layout.html" %}
{% block title %}
Dashcam Routes
{% endblock %}
{% block main %}
<br>
<h1>Dashcam Routes</h1>
<br>
<div class="row">
{% for row, gif 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;">
<img src="/previewgif/{{ gif }}" class="card-img-top" alt="GIF">
<div class="card-body">
<p class="card-text">{{ row }}</p>
<div class="d-flex justify-content-between align-items-center">
<div class="btn-group">
<a href="/footage/{{ row }}" class="btn btn-sm btn-outline-secondary">View Footage</a>
</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% 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,16 @@
{% extends "layout.html" %}
{% block title %}
Home
{% endblock %}
{% block main %}
<br>
<h1>Fleet Manager</h1>
<br>
<a href='/footage'>View Dashcam Footage</a><br>
<br><a href='/preserved'>Access Preserved Footage</a><br>
<br><a href='/screenrecords'>View Screen Recordings</a><br>
<br><a href='/error_logs'>Access Error Logs</a><br>
<br><a href='/about'>About Fleet Manager</a><br>
{% 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>FrogPilot: {% 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/frog.png" /> FrogPilot </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 unformated %}{% 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,68 @@
{% 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="320" height="240" controls autoplay="autoplay" style="background:black">
</video>
<br><br>
current segment: <span id="currentsegment"></span>
<br>
current view: <span id="currentview"></span>
<br>
<a download="{{ route }}-{{ query_type }}.mp4" href="/footage/full/{{ query_type }}/{{ route }}">download full route {{ query_type }}</a>
<br><br>
<a href="{{ route }}?{{ query_segment }},qcamera">qcamera</a> -
<a href="{{ route }}?{{ query_segment }},fcamera">fcamera</a> -
<a href="{{ route }}?{{ query_segment }},dcamera">dcamera</a> -
<a href="{{ route }}?{{ query_segment }},ecamera">ecamera</a>
<br><br>
{{ links }}
<script>
var video = document.getElementById('video');
var segments = [{{ segments }}];
var q_segment = {{ query_segment }};
var q_index = 0;
for (var i = 0; i < segments.length; i++) {
var segment = segments[i];
var q_val = segment.split('--').slice(2).join('--');
if (parseInt(q_val) === q_segment) {
q_index = i;
break
}
}
var tracks = {
list: segments,
index: q_index,
next: function() {
if (this.index == this.list.length - 1) this.index = 0;
else {
this.index += 1;
}
},
play: function() {
return ( "{{ query_type }}/" + this.list[this.index] );
}
}
video.addEventListener('ended', function(e) {
tracks.next();
video.src = tracks.play();
document.getElementById("currentsegment").textContent=video.src.split("/")[5];
document.getElementById("currentview").textContent=video.src.split("/")[4];
video.load();
video.play();
});
video.src = tracks.play();
document.getElementById("currentsegment").textContent=video.src.split("/")[5];
document.getElementById("currentview").textContent=video.src.split("/")[4];
</script>
{% endautoescape %}
<br><br>
{% endblock %}

View File

@ -0,0 +1,31 @@
{% extends "layout.html" %}
{% block title %}
Screen Recordings
{% endblock %}
{% block main %}
<br>
<h1>Screen Recordings</h1>
<br>
<video id="video" width="320" height="240" controls autoplay="autoplay" style="background:black">
</video>
<br><br>
current view: <span id="mycurrentview"></span>
<br>
<a href="/screenrecords/download/{{ clip }}">download: {{ clip }}</a><br><br>
<script>
var video = document.getElementById("video");
var track = {
play: function() {
return ( "/screenrecords/play/pipe/" + '{{ clip }}' );
}
}
video.src = track.play();
document.getElementById("mycurrentview").textContent=video.src.split("/")[6];
</script>
{% for row in rows %}
<a href="/screenrecords/{{ row }}">{{ row }}</a><br>
{% endfor %}
<br><br>
{% endblock %}

View File

@ -0,0 +1,78 @@
{% extends "layout.html" %}
{% block title %}
Tools
{% endblock %}
{% block main %}
<style>
.center {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
flex-wrap: wrap;
}
.textarea-container {
width: 80%;
margin-top: 20px;
}
textarea {
width: 100%;
height: 200px;
}
button {
margin-top: 10px;
}
</style>
<div class="center">
<h1>Toggle Values</h1>
<div class="textarea-container">
<textarea id="toggleValuesBox" placeholder='Paste new values or retrieve current...'></textarea>
<button type="button" id="retrieveButton">Retrieve Toggle Values</button>
<button type="button" id="submitButton">Submit New Values</button>
</div>
</div>
<script>
const toggleValuesBox = document.getElementById('toggleValuesBox');
const retrieveButton = document.getElementById('retrieveButton');
const submitButton = document.getElementById('submitButton');
retrieveButton.addEventListener('click', function() {
fetch('/get_toggle_values')
.then(response => response.json())
.then(data => {
toggleValuesBox.value = JSON.stringify(data, null, 2);
})
.catch(error => console.error('Error fetching toggle values:', error));
});
submitButton.addEventListener('click', function() {
let inputData;
try {
inputData = JSON.parse(toggleValuesBox.value);
} catch (error) {
alert('Invalid JSON format');
console.error('Error parsing JSON input:', error);
return;
}
fetch('/store_toggle_values', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(inputData),
})
.then(response => response.json().then(data => ({ status: response.status, body: data })))
.then(result => {
if (result.status === 200) {
alert('Values stored successfully.');
} else {
alert('Error storing values: ' + result.body.error);
}
})
.catch(error => console.error('Error storing toggle values:', error));
});
</script>
{% endblock %}

View File

@ -172,6 +172,42 @@ class RouteEngine:
resp.raise_for_status()
r = resp.json()
r1 = resp.json()
# Function to remove specified keys recursively unnessary for display
def remove_keys(obj, keys_to_remove):
if isinstance(obj, list):
return [remove_keys(item, keys_to_remove) for item in obj]
elif isinstance(obj, dict):
return {key: remove_keys(value, keys_to_remove) for key, value in obj.items() if key not in keys_to_remove}
else:
return obj
keys_to_remove = ['geometry', 'annotation', 'incidents', 'intersections', 'components', 'sub', 'waypoints']
self.r2 = remove_keys(r1, keys_to_remove)
self.r3 = {}
# Add items for display under "routes"
if 'routes' in self.r2 and len(self.r2['routes']) > 0:
first_route = self.r2['routes'][0]
nav_destination_json = self.params.get('NavDestination')
try:
nav_destination_data = json.loads(nav_destination_json)
place_name = nav_destination_data.get('place_name', 'Default Place Name')
first_route['Destination'] = place_name
first_route['Metric'] = self.params.get_bool("IsMetric")
self.r3['CurrentStep'] = 0
self.r3['uuid'] = self.r2['uuid']
except json.JSONDecodeError as e:
print(f"Error decoding JSON: {e}")
# Save slim json as file
with open('navdirections.json', 'w') as json_file:
json.dump(self.r2, json_file, indent=4)
with open('CurrentStep.json', 'w') as json_file:
json.dump(self.r3, json_file, indent=4)
if len(r['routes']):
self.route = r['routes'][0]['legs'][0]['steps']
self.route_geometry = []
@ -307,6 +343,13 @@ class RouteEngine:
if self.step_idx + 1 < len(self.route):
self.step_idx += 1
self.reset_recompute_limits()
# Update the 'CurrentStep' value in the JSON
if 'routes' in self.r2 and len(self.r2['routes']) > 0:
self.r3['CurrentStep'] = self.step_idx
# Write the modified JSON data back to the file
with open('CurrentStep.json', 'w') as json_file:
json.dump(self.r3, json_file, indent=4)
else:
cloudlog.warning("Destination reached")

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:76a780657d874ef7d9464ae576c45392128a38bed4f963305be6ba2c12d04699
size 108573

View File

@ -0,0 +1,16 @@
{% extends "layout.html" %}
{% block title %}
Home
{% endblock %}
{% block main %}
<br>
<h1>Fleet Manager</h1>
<br>
<a href='/footage'>View Dashcam Footage</a><br>
<br><a href='/screenrecords'>View Screen Recordings</a><br>
<br><a href='/error_logs'>Access Error Logs</a><br>
<br><a href='/addr_input'>Navigation</a><br>
<br><a href='/tools'>Tools</a><br>
{% endblock %}

View File

@ -90,6 +90,7 @@ procs = [
PythonProcess("webjoystick", "tools.bodyteleop.web", notcar),
# FrogPilot processes
PythonProcess("fleet_manager", "selfdrive.frogpilot.fleetmanager.fleet_manager", always_run),
PythonProcess("frogpilot_process", "selfdrive.frogpilot.frogpilot_process", always_run),
]