Skip to content
Merged

front #358

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
500 changes: 486 additions & 14 deletions selfdrive/carrot/README.md

Large diffs are not rendered by default.

79 changes: 76 additions & 3 deletions selfdrive/carrot/server/features/dashcam/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@
)

ROUTE_CACHE_TTL = 3.0
DASHCAM_ROUTE_LIMIT_DEFAULT = 40
DASHCAM_ROUTE_LIMIT_MAX = 200
DASHCAM_SEGMENT_LIMIT_DEFAULT = 10
DASHCAM_SEGMENT_LIMIT_MAX = 80
DASHCAM_OFFSET_MAX = 1000000
_route_cache_lock = threading.Lock()
_route_cache = {"time": 0.0, "routes": []}

Expand Down Expand Up @@ -51,19 +56,62 @@ def cached_dashcam_routes() -> list[dict]:
return list(routes)


def bounded_query_int(request: web.Request, name: str, default: int, maximum: int) -> int:
try:
value = int(request.query.get(name, str(default)) or default)
except (TypeError, ValueError):
value = default
return max(0 if name == "offset" else 1, min(maximum, value))


def route_with_segment_page(entry: dict, segment_offset: int = 0, segment_limit: int = DASHCAM_SEGMENT_LIMIT_DEFAULT) -> dict:
segments = list(entry.get("segmentFolders") or [])
total = len(segments)
offset = max(0, min(segment_offset, total))
limit = max(1, min(DASHCAM_SEGMENT_LIMIT_MAX, segment_limit))
end = min(offset + limit, total)
result = dict(entry)
result["segmentFolders"] = segments[offset:end]
result["segmentCount"] = int(entry.get("segmentCount") or total)
result["segmentOffset"] = offset
result["segmentLimit"] = limit
result["segmentsNextOffset"] = end if end < total else None
result["segmentsHasMore"] = end < total
return result


def find_dashcam_route(routes: list[dict], route: str) -> dict | None:
if not route or "/" in route or "\\" in route or route in (".", ".."):
return None
for entry in routes:
if entry.get("route") == route:
return entry
return None


async def api_dashcam_routes(request: web.Request) -> web.Response:
try:
offset = max(0, int(request.query.get("offset", "0") or 0))
limit = max(1, min(200, int(request.query.get("limit", "80") or 80)))
offset = bounded_query_int(request, "offset", 0, DASHCAM_OFFSET_MAX)
limit = bounded_query_int(request, "limit", DASHCAM_ROUTE_LIMIT_DEFAULT, DASHCAM_ROUTE_LIMIT_MAX)
segment_limit = bounded_query_int(
request,
"segment_limit",
DASHCAM_SEGMENT_LIMIT_DEFAULT,
DASHCAM_SEGMENT_LIMIT_MAX,
)
routes = await asyncio.to_thread(cached_dashcam_routes)
total = len(routes)
end = min(offset + limit, total)
return web.json_response({
"ok": True,
"routes": routes[offset:end],
"routes": [
route_with_segment_page(entry, 0, segment_limit)
for entry in routes[offset:end]
],
"root": DASHCAM_ROOT,
"offset": offset,
"limit": limit,
"segmentLimit": segment_limit,
"total": total,
"nextOffset": end if end < total else None,
"hasMore": end < total,
Expand All @@ -72,6 +120,30 @@ async def api_dashcam_routes(request: web.Request) -> web.Response:
return web.json_response({"ok": False, "error": str(e)}, status=500)


async def api_dashcam_segments(request: web.Request) -> web.Response:
try:
route = request.match_info.get("route", "")
offset = bounded_query_int(request, "offset", 0, DASHCAM_OFFSET_MAX)
limit = bounded_query_int(request, "limit", DASHCAM_SEGMENT_LIMIT_DEFAULT, DASHCAM_SEGMENT_LIMIT_MAX)
routes = await asyncio.to_thread(cached_dashcam_routes)
entry = find_dashcam_route(routes, route)
if not entry:
return web.json_response({"ok": False, "error": "route not found"}, status=404)
page = route_with_segment_page(entry, offset, limit)
return web.json_response({
"ok": True,
"route": route,
"segments": page["segmentFolders"],
"offset": page["segmentOffset"],
"limit": page["segmentLimit"],
"total": page["segmentCount"],
"nextOffset": page["segmentsNextOffset"],
"hasMore": page["segmentsHasMore"],
})
except Exception as e:
return web.json_response({"ok": False, "error": str(e)}, status=500)


async def api_dashcam_thumbnail(request: web.Request) -> web.StreamResponse:
segment = request.match_info.get("segment", "")
path = await asyncio.to_thread(ensure_thumbnail, segment)
Expand Down Expand Up @@ -199,6 +271,7 @@ async def api_dashcam_upload_cancel(request: web.Request) -> web.Response:

def register(app: web.Application) -> None:
app.router.add_get("/api/dashcam/routes", api_dashcam_routes)
app.router.add_get("/api/dashcam/segments/{route}", api_dashcam_segments)
app.router.add_get("/api/dashcam/thumbnail/{segment}", api_dashcam_thumbnail)
app.router.add_get("/api/dashcam/preview/{segment}", api_dashcam_preview)
app.router.add_get("/api/dashcam/video/{segment}", api_dashcam_video)
Expand Down
18 changes: 18 additions & 0 deletions selfdrive/carrot/server/features/static.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,23 @@
from ..services.web_settings import read_web_settings


_LANGUAGES_JSON_PATH = os.path.join(
os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))),
"ui", "translations", "languages.json",
)


def _load_device_languages() -> list:
"""Read selfdrive/ui/translations/languages.json and return
a list of ``{code, name}`` dicts that the web client can use directly."""
try:
with open(_LANGUAGES_JSON_PATH, "r", encoding="utf-8") as f:
mapping = json.load(f) # e.g. {"English": "main_en", ...}
return [{"code": code, "name": name} for name, code in mapping.items()]
except Exception:
return []


def _build_bootstrap_payload() -> dict:
try:
device_values = get_param_values(["LanguageSetting"], {"LanguageSetting": ""})
Expand All @@ -17,6 +34,7 @@ def _build_bootstrap_payload() -> dict:
return {
"webSettings": read_web_settings(),
"deviceLanguage": device_language,
"deviceLanguages": _load_device_languages(),
}


Expand Down
39 changes: 36 additions & 3 deletions selfdrive/carrot/web/css/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,39 @@ body.dialog-open {
overflow: hidden;
}

/* ── Global Focus Ring ─────────────────────────────────────────
One accessible focus indicator for every interactive element.
Components may override this with a more specific :focus-visible
rule, but they should never *remove* the outline without providing
an equivalent visual cue. Uses tokens from tokens.css. */
:focus-visible {
outline: var(--focus-ring-width) solid var(--focus-ring-color);
outline-offset: var(--focus-ring-offset);
border-radius: inherit;
}

/* Mouse/touch interaction shouldn't show the ring — only keyboard. */
:focus:not(:focus-visible) {
outline: none;
}

/* ── Reduced Motion ────────────────────────────────────────────
Honors the user's OS-level "reduce motion" preference globally.
Animation/transition durations collapse to near-zero so things
still *snap* into their final state (preserving feedback) but
without movement. Individual components can opt out by being
more specific. */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.001ms !important;
scroll-behavior: auto !important;
}
}

/* Page-level overscroll/scroll lock.
The wrap layout itself is intentionally page-invariant (see .wrap below)
so menu transitions don't re-flow the container. Pages that need
Expand Down Expand Up @@ -132,7 +165,7 @@ body:has(.page-transitioning) {
}

.topbar .nav-btn:focus-visible {
background: color-mix(in srgb, var(--md-primary-cont) 14%, transparent);
background: var(--md-primary-state-soft);
}

.topbar .nav-btn .nav-icon {
Expand Down Expand Up @@ -181,7 +214,7 @@ body:has(.page-transitioning) {
.topbar .nav-btn.active {
color: var(--md-primary);
font-weight: 700;
background: color-mix(in srgb, var(--md-primary) 12%, transparent);
background: var(--md-primary-state-soft);
}

.topbar .nav-btn.recording,
Expand Down Expand Up @@ -565,7 +598,7 @@ body:has(.page-transitioning) {
}

.fab.active {
background: var(--md-primary-cont);
background: var(--md-primary-state-strong);
border-color: color-mix(in srgb, var(--md-primary) 35%, var(--md-outline-var));
color: var(--md-primary);
}
Expand Down
Loading
Loading