Fleet Manager

Co-Authored-By: mike8643 <98910897+mike8643@users.noreply.github.com>
This commit is contained in:
FrogAi
2024-01-12 22:39:30 -07:00
parent ac5eb105f0
commit 8bb10b7785
26 changed files with 1154 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
# Fleet Manager
Fleet Manger on openpilot allows viewing dashcam footage, screen recordings, and error logs 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), and [royjr](https://github.com/royjr).
The network can be set up by Wi-Fi, mobile hotspot, or tethering on the comma device. Navigate to http://tici:8082/ OR http://ipAddress:8082 to access.

View File

@@ -0,0 +1,243 @@
#!/usr/bin/env python3
import os
import random
import secrets
import threading
import time
from flask import Flask, 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():
return render_template("footage.html", rows=fleet.all_routes())
@app.route("/screenrecords/")
@app.route("/screenrecords")
def screenrecords():
rows = fleet.list_files(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():
return render_template("error_logs.html", rows=fleet.list_files(fleet.ERROR_LOGS_PATH))
@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():
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")
elif fleet.get_nav_active():
if SearchInput == 2:
return render_template("nonprime.html", gmap_key=gmap_key, lon=lon, lat=lat)
else:
return render_template("nonprime.html", gmap_key=None, lon=None, lat=None)
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)
else:
return render_template("addr.html", gmap_key=None, lon=None, lat=None)
@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("/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)
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,285 @@
import os
import subprocess
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
# otisserv conversion
from common.params import Params
from urllib.parse import parse_qs, quote
import json
import requests
params = Params()
# 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):
return sorted(listdir_by_creation(path), reverse=True)
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 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_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")
l = json.loads(last_pos)
return l["longitude"], l["latitude"]
def get_locations():
data = params.get("ApiCache_NavDestinations", encoding='utf-8')
return data
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').rstrip('\x00'))
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 ""
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

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,18 @@
{% 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
</footer>
{% endblock %}

View File

@@ -0,0 +1,11 @@
{% extends "layout.html" %}
{% block title %}
FrogPilot Navigation
{% endblock %}
{% block main %}
{% with gmap_key=gmap_key, lon=lon, lat=lat %}
{% include "addr_input.html" %}
{% endwith %}
{% endblock %}

View File

@@ -0,0 +1,62 @@
{% block main %}
<!-- Your form markup -->
<form name="searchForm" method="post">
<fieldset class="uk-fieldset">
<div class="uk-margin">
<select class="uk-select" name="fav_val">
<option value="favorites">Select Saved Destinations</option>
<option value="home">Home</option>
<option value="work">Work</option>
<option value="fav1">Favorite 1</option>
<option value="fav2">Favorite 2</option>
<option value="fav3">Favorite 3</option>
</select>
<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,19 @@
{% extends "layout.html" %}
{% block title %}
addr_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 %}
About
{% 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,15 @@
{% extends "layout.html" %}
{% block title %}
Dashcam Routes
{% endblock %}
{% block main %}
<br>
<h1>Dashcam Routes</h1>
<br>
{% for row in rows %}
<a href="/footage/{{ row }}">{{ row }}</a><br>
{% endfor %}
<br><br>
{% endblock %}

View File

@@ -0,0 +1,18 @@
{% extends "layout.html" %}
{% block title %}
addr_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,14 @@
{% 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>
{% endblock %}

View File

@@ -0,0 +1,100 @@
<!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>
</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 %}
addr_input
{% 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 %}
addr_input
{% endblock %}
{% block main %}
{% with gmap_key=gmap_key, lon=lon, lat=lat %}
{% include "addr_input.html" %}
{% endwith %}
{% include "nav_directions.html" %}
{% endblock %}

View File

@@ -0,0 +1,9 @@
{% extends "layout.html" %}
{% block title %}
FrogPilot Navigation
{% 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,57 @@
{% 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 }}?0,qcamera">qcamera</a> -
<a href="{{ route }}?0,fcamera">fcamera</a> -
<a href="{{ route }}?0,dcamera">dcamera</a> -
<a href="{{ route }}?0,ecamera">ecamera</a>
<br><br>
{{ links }}
<script>
var video = document.getElementById('video');
var tracks = {
list: [{{ segments }}],
index: {{ query_segment }},
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 %}