diff --git a/selfdrive/frogpilot/fleetmanager/README.md b/selfdrive/frogpilot/fleetmanager/README.md new file mode 100644 index 0000000..1315df9 --- /dev/null +++ b/selfdrive/frogpilot/fleetmanager/README.md @@ -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. diff --git a/selfdrive/frogpilot/fleetmanager/fleet_manager.py b/selfdrive/frogpilot/fleetmanager/fleet_manager.py new file mode 100644 index 0000000..1e76777 --- /dev/null +++ b/selfdrive/frogpilot/fleetmanager/fleet_manager.py @@ -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//") +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//") +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/") +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 += ""+segment+"
" + 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:

" + fleet.SCREENRECORD_PATH) + return render_template("screenrecords.html", rows=rows, clip=rows[0]) + + +@app.route("/screenrecords/") +def screenrecord(clip): + return render_template("screenrecords.html", rows=fleet.list_files(fleet.SCREENRECORD_PATH), clip=clip) + + +@app.route("/screenrecords/play/pipe/") +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/") +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/") +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/", 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() diff --git a/selfdrive/frogpilot/fleetmanager/helpers.py b/selfdrive/frogpilot/fleetmanager/helpers.py new file mode 100644 index 0000000..08b867c --- /dev/null +++ b/selfdrive/frogpilot/fleetmanager/helpers.py @@ -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 diff --git a/selfdrive/frogpilot/fleetmanager/static/favicon.ico b/selfdrive/frogpilot/fleetmanager/static/favicon.ico new file mode 100644 index 0000000..2f24c58 --- /dev/null +++ b/selfdrive/frogpilot/fleetmanager/static/favicon.ico @@ -0,0 +1 @@ +../../../selfdrive/assets/img_spinner_comma.png \ No newline at end of file diff --git a/selfdrive/frogpilot/fleetmanager/static/frog.png b/selfdrive/frogpilot/fleetmanager/static/frog.png new file mode 100644 index 0000000..5285f0b Binary files /dev/null and b/selfdrive/frogpilot/fleetmanager/static/frog.png differ diff --git a/selfdrive/frogpilot/fleetmanager/templates/about.html b/selfdrive/frogpilot/fleetmanager/templates/about.html new file mode 100644 index 0000000..9212b2b --- /dev/null +++ b/selfdrive/frogpilot/fleetmanager/templates/about.html @@ -0,0 +1,18 @@ +{% extends "layout.html" %} + +{% block title %} + About +{% endblock %} + +{% block main %} +
+

About

+
+
+ Special thanks to:

+ ntegan1
+ royjr
+ AlexandreSato
+ actuallylemoncurd +
+{% endblock %} diff --git a/selfdrive/frogpilot/fleetmanager/templates/addr.html b/selfdrive/frogpilot/fleetmanager/templates/addr.html new file mode 100644 index 0000000..0b9b012 --- /dev/null +++ b/selfdrive/frogpilot/fleetmanager/templates/addr.html @@ -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 %} diff --git a/selfdrive/frogpilot/fleetmanager/templates/addr_input.html b/selfdrive/frogpilot/fleetmanager/templates/addr_input.html new file mode 100644 index 0000000..22e182a --- /dev/null +++ b/selfdrive/frogpilot/fleetmanager/templates/addr_input.html @@ -0,0 +1,62 @@ +{% block main %} + +
+
+
+ + + +
+
+
+ + +{% endblock %} diff --git a/selfdrive/frogpilot/fleetmanager/templates/app_token_input.html b/selfdrive/frogpilot/fleetmanager/templates/app_token_input.html new file mode 100644 index 0000000..775e6a4 --- /dev/null +++ b/selfdrive/frogpilot/fleetmanager/templates/app_token_input.html @@ -0,0 +1,19 @@ +{% extends "layout.html" %} + +{% block title %} + addr_input +{% endblock %} + +{% block main %} +
+
+ Set your Mapbox Secret Token +
{{msg}}
+
+ + +
+
+
+ +{% endblock %} \ No newline at end of file diff --git a/selfdrive/frogpilot/fleetmanager/templates/error.html b/selfdrive/frogpilot/fleetmanager/templates/error.html new file mode 100644 index 0000000..cd0886d --- /dev/null +++ b/selfdrive/frogpilot/fleetmanager/templates/error.html @@ -0,0 +1,11 @@ +{% extends "layout.html" %} + +{% block title %} + About +{% endblock %} + +{% block main %} +
Oops +



+ {{ error | safe }} +{% endblock %} diff --git a/selfdrive/frogpilot/fleetmanager/templates/error_log.html b/selfdrive/frogpilot/fleetmanager/templates/error_log.html new file mode 100644 index 0000000..e8f527e --- /dev/null +++ b/selfdrive/frogpilot/fleetmanager/templates/error_log.html @@ -0,0 +1,16 @@ +{% extends "layout.html" %} + +{% block title %} + Error Log +{% endblock %} + +{% block main %} +
+

Error Log of
{{ file_name }}

+
+{% endblock %} + +{% block unformated %} +
{{ file_content }}
+

+{% endblock %} diff --git a/selfdrive/frogpilot/fleetmanager/templates/error_logs.html b/selfdrive/frogpilot/fleetmanager/templates/error_logs.html new file mode 100644 index 0000000..103551f --- /dev/null +++ b/selfdrive/frogpilot/fleetmanager/templates/error_logs.html @@ -0,0 +1,14 @@ +{% extends "layout.html" %} + +{% block title %} + Error Logs +{% endblock %} + +{% block main %} +
+

Error Logs

+
+ {% for row in rows %} + {{ row }}
+ {% endfor %} +{% endblock %} diff --git a/selfdrive/frogpilot/fleetmanager/templates/footage.html b/selfdrive/frogpilot/fleetmanager/templates/footage.html new file mode 100644 index 0000000..9693be8 --- /dev/null +++ b/selfdrive/frogpilot/fleetmanager/templates/footage.html @@ -0,0 +1,15 @@ +{% extends "layout.html" %} + +{% block title %} + Dashcam Routes +{% endblock %} + +{% block main %} +
+

Dashcam Routes

+
+ {% for row in rows %} + {{ row }}
+ {% endfor %} +

+{% endblock %} diff --git a/selfdrive/frogpilot/fleetmanager/templates/gmap_key_input.html b/selfdrive/frogpilot/fleetmanager/templates/gmap_key_input.html new file mode 100644 index 0000000..74a6f1f --- /dev/null +++ b/selfdrive/frogpilot/fleetmanager/templates/gmap_key_input.html @@ -0,0 +1,18 @@ +{% extends "layout.html" %} + +{% block title %} + addr_input +{% endblock %} + +{% block main %} +
+
+ Set your Google Map API Key +
+ + +
+
+
+ +{% endblock %} \ No newline at end of file diff --git a/selfdrive/frogpilot/fleetmanager/templates/index.html b/selfdrive/frogpilot/fleetmanager/templates/index.html new file mode 100644 index 0000000..38debd4 --- /dev/null +++ b/selfdrive/frogpilot/fleetmanager/templates/index.html @@ -0,0 +1,14 @@ +{% extends "layout.html" %} + +{% block title %} + Home +{% endblock %} + +{% block main %} +
+

Fleet Manager

+
+ View Dashcam Footage
+
View Screen Recordings
+
Access Error Logs
+{% endblock %} diff --git a/selfdrive/frogpilot/fleetmanager/templates/layout.html b/selfdrive/frogpilot/fleetmanager/templates/layout.html new file mode 100644 index 0000000..5fed96e --- /dev/null +++ b/selfdrive/frogpilot/fleetmanager/templates/layout.html @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + FrogPilot: {% block title %}{% endblock %} + + + +
{% block main %}{% endblock %}
+ {% block unformated %}{% endblock %} + + + + + + + diff --git a/selfdrive/frogpilot/fleetmanager/templates/nav_confirmation.html b/selfdrive/frogpilot/fleetmanager/templates/nav_confirmation.html new file mode 100644 index 0000000..e0e4c4c --- /dev/null +++ b/selfdrive/frogpilot/fleetmanager/templates/nav_confirmation.html @@ -0,0 +1,28 @@ +{% extends "layout.html" %} + +{% block title %} + addr_input +{% endblock %} + +{% block main %} +
+
{{addr}}
+
+
+
+ + + + + +
+
+
+{% endblock %} \ No newline at end of file diff --git a/selfdrive/frogpilot/fleetmanager/templates/nav_directions.html b/selfdrive/frogpilot/fleetmanager/templates/nav_directions.html new file mode 100644 index 0000000..c59582e --- /dev/null +++ b/selfdrive/frogpilot/fleetmanager/templates/nav_directions.html @@ -0,0 +1,149 @@ +{% block main %} +
+
+ + +{% endblock %} diff --git a/selfdrive/frogpilot/fleetmanager/templates/nonprime.html b/selfdrive/frogpilot/fleetmanager/templates/nonprime.html new file mode 100644 index 0000000..9f5d933 --- /dev/null +++ b/selfdrive/frogpilot/fleetmanager/templates/nonprime.html @@ -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 %} diff --git a/selfdrive/frogpilot/fleetmanager/templates/prime.html b/selfdrive/frogpilot/fleetmanager/templates/prime.html new file mode 100644 index 0000000..e414384 --- /dev/null +++ b/selfdrive/frogpilot/fleetmanager/templates/prime.html @@ -0,0 +1,9 @@ +{% extends "layout.html" %} + +{% block title %} +FrogPilot Navigation +{% endblock %} + +{% block main %} +{% include "nav_directions.html" %} +{% endblock %} diff --git a/selfdrive/frogpilot/fleetmanager/templates/public_token_input.html b/selfdrive/frogpilot/fleetmanager/templates/public_token_input.html new file mode 100644 index 0000000..b64a880 --- /dev/null +++ b/selfdrive/frogpilot/fleetmanager/templates/public_token_input.html @@ -0,0 +1,19 @@ +{% extends "layout.html" %} + +{% block title %} + addr_input +{% endblock %} + +{% block main %} +
+
+ Set your Mapbox Public Token +
{{msg}}
+
+ + +
+
+
+ +{% endblock %} \ No newline at end of file diff --git a/selfdrive/frogpilot/fleetmanager/templates/route.html b/selfdrive/frogpilot/fleetmanager/templates/route.html new file mode 100644 index 0000000..3d78cd0 --- /dev/null +++ b/selfdrive/frogpilot/fleetmanager/templates/route.html @@ -0,0 +1,57 @@ +{% extends "layout.html" %} + +{% block title %} + Dashcam Segments +{% endblock %} + + +{% block main %} +{% autoescape false %} +
+

Dashcam Segments (one per minute)

+
+ +

+ current segment: +
+ current view: +
+ download full route {{ query_type }} +

+ qcamera - + fcamera - + dcamera - + ecamera +

+ {{ links }} + +{% endautoescape %} +

+{% endblock %} diff --git a/selfdrive/frogpilot/fleetmanager/templates/screenrecords.html b/selfdrive/frogpilot/fleetmanager/templates/screenrecords.html new file mode 100644 index 0000000..e1c995e --- /dev/null +++ b/selfdrive/frogpilot/fleetmanager/templates/screenrecords.html @@ -0,0 +1,31 @@ +{% extends "layout.html" %} + +{% block title %} + Screen Recordings +{% endblock %} + +{% block main %} +
+

Screen Recordings

+
+ +

+ current view: +
+ download: {{ clip }}

+ + {% for row in rows %} + {{ row }}
+ {% endfor %} +

+{% endblock %} diff --git a/selfdrive/manager/process_config.py b/selfdrive/manager/process_config.py index c6bb087..4c2a896 100644 --- a/selfdrive/manager/process_config.py +++ b/selfdrive/manager/process_config.py @@ -102,6 +102,7 @@ procs = [ PythonProcess("webjoystick", "tools.bodyteleop.web", notcar), # FrogPilot processes + PythonProcess("fleet_manager", "selfdrive.frogpilot.fleetmanager.fleet_manager", always_run), ] managed_processes = {p.name: p for p in procs} diff --git a/system/fleetmanager/static/frog.png b/system/fleetmanager/static/frog.png new file mode 100644 index 0000000..5285f0b Binary files /dev/null and b/system/fleetmanager/static/frog.png differ diff --git a/system/fleetmanager/templates/index.html b/system/fleetmanager/templates/index.html new file mode 100644 index 0000000..776d64e --- /dev/null +++ b/system/fleetmanager/templates/index.html @@ -0,0 +1,15 @@ +{% extends "layout.html" %} + +{% block title %} + Home +{% endblock %} + +{% block main %} +
+

Fleet Manager

+
+ View Dashcam Footage
+
View Screen Recordings
+
Access Error Logs
+
Navigation
+{% endblock %}