diff --git a/openpilot/selfdrive/carrot/server/features/__init__.py b/openpilot/selfdrive/carrot/server/features/__init__.py index 1644d819cc..8eb49944c4 100644 --- a/openpilot/selfdrive/carrot/server/features/__init__.py +++ b/openpilot/selfdrive/carrot/server/features/__init__.py @@ -18,6 +18,7 @@ tools, vision_diag, vision_test, + web_sound, web_settings, ws, youtube_live, @@ -45,3 +46,4 @@ def register_all(app: web.Application) -> None: youtube_live.register(app) vision_test.register(app) vision_diag.register(app) + web_sound.register(app) diff --git a/openpilot/selfdrive/carrot/server/features/static.py b/openpilot/selfdrive/carrot/server/features/static.py index 547ca0f832..eb911547cc 100644 --- a/openpilot/selfdrive/carrot/server/features/static.py +++ b/openpilot/selfdrive/carrot/server/features/static.py @@ -27,13 +27,19 @@ def _load_device_languages() -> list: def _build_bootstrap_payload() -> dict: try: - device_values = get_param_values(["LanguageSetting"], {"LanguageSetting": ""}) + device_values = get_param_values( + ["LanguageSetting", "SoundLanguageSetting"], + {"LanguageSetting": "", "SoundLanguageSetting": "auto"}, + ) device_language = device_values.get("LanguageSetting", "") + sound_language = device_values.get("SoundLanguageSetting", "auto") except Exception: device_language = "" + sound_language = "auto" return { "webSettings": read_web_settings(), "deviceLanguage": device_language, + "soundLanguage": sound_language, "deviceLanguages": _load_device_languages(), } diff --git a/openpilot/selfdrive/carrot/server/features/web_sound.py b/openpilot/selfdrive/carrot/server/features/web_sound.py new file mode 100644 index 0000000000..3f7467a971 --- /dev/null +++ b/openpilot/selfdrive/carrot/server/features/web_sound.py @@ -0,0 +1,160 @@ +from __future__ import annotations + +import asyncio +import time + +from aiohttp import WSMsgType, web + +from openpilot.cereal import car, messaging +from openpilot.common.params import Params +from openpilot.selfdrive.car.openpilot_toggle import CruiseMainOpenpilotToggle + + +AudibleAlert = car.CarControl.HUDControl.AudibleAlert +ButtonType = car.CarState.ButtonEvent.Type +SELFDRIVE_STATE_TIMEOUT = 5.0 + + +def _enum_raw(value, default: int = 0) -> int: + try: + return int(getattr(value, "raw", value)) + except (TypeError, ValueError): + return default + + +def _message_valid(sm, service: str) -> bool: + try: + return bool(sm.valid[service]) + except Exception: + return False + + +def _param_percent(params: Params, key: str) -> float: + try: + return max(0.0, min(2.0, float(params.get_int(key)) / 100.0)) + except Exception: + return 1.0 + + +def _param_string(value) -> str: + if isinstance(value, (bytes, bytearray, memoryview)): + return bytes(value).decode("utf-8", errors="replace") + return str(value or "") + + +def _sound_directory(params: Params) -> str: + try: + sound_language = _param_string(params.get("SoundLanguageSetting", return_default=True)).strip() + if not sound_language or sound_language.lower() == "auto": + sound_language = _param_string(params.get("LanguageSetting", return_default=True)).strip() or "en" + except Exception: + sound_language = "en" + normalized = sound_language.replace("_", "-").lower() + if normalized.startswith("main-"): + normalized = normalized[5:] + if normalized == "ko" or normalized.startswith("ko-"): + return "sounds" + if normalized in ("zh-chs", "zh-hans") or normalized.startswith("zh"): + return "sounds_chs" + return "sounds_eng" + + +def _is_tizi() -> bool: + try: + from openpilot.system.hardware import HARDWARE + return HARDWARE.get_device_type() == "tizi" + except Exception: + return False + + +async def _send_sound_states(ws: web.WebSocketResponse) -> None: + sm = messaging.SubMaster(["selfdriveState", "carrotMan", "carState"]) + params = Params() + cruise_main_toggle = CruiseMainOpenpilotToggle(ButtonType.mainCruise) + last_signature = None + prompt_sequence = 0 + sequence = 0 + next_param_read = 0.0 + volume = 1.0 + engage_volume = 1.0 + sound_directory = "sounds_eng" + emitted_countdown = 100 + tizi = _is_tizi() + + while not ws.closed: + sm.update(0) + now = time.monotonic() + + enabled = bool(sm["selfdriveState"].enabled) if _message_valid(sm, "selfdriveState") else False + alert = _enum_raw(sm["selfdriveState"].alertSound) if _message_valid(sm, "selfdriveState") else 0 + + try: + missing_for = now - float(sm.recv_time["selfdriveState"]) + except Exception: + missing_for = 0.0 + if missing_for > SELFDRIVE_STATE_TIMEOUT: + if enabled and missing_for < SELFDRIVE_STATE_TIMEOUT + 10.0: + alert = _enum_raw(AudibleAlert.warningImmediate) + else: + alert = _enum_raw(AudibleAlert.none) + + countdown = 100 + if _message_valid(sm, "carrotMan"): + try: + countdown = int(sm["carrotMan"].leftSec) + except (TypeError, ValueError): + countdown = 100 + if last_signature is None or sm.updated["selfdriveState"]: + emitted_countdown = countdown + + button_events = sm["carState"].buttonEvents if _message_valid(sm, "carState") else () + if cruise_main_toggle.update(button_events, enabled, now=now): + prompt_sequence += 1 + alert = _enum_raw(AudibleAlert.prompt) + + if now >= next_param_read: + volume = _param_percent(params, "SoundVolumeAdjust") + engage_volume = _param_percent(params, "SoundVolumeAdjustEngage") + sound_directory = _sound_directory(params) + next_param_read = now + 1.0 + + signature = (alert, emitted_countdown, prompt_sequence, volume, engage_volume, sound_directory, tizi) + if signature != last_signature: + sequence += 1 + await ws.send_json({ + "type": "soundState", + "sequence": sequence, + "alert": alert, + "countdown": emitted_countdown, + "promptSequence": prompt_sequence, + "volume": volume, + "engageVolume": engage_volume, + "soundDirectory": sound_directory, + "tizi": tizi, + }) + last_signature = signature + + await asyncio.sleep(0.05) + + +async def ws_web_sound(request: web.Request) -> web.WebSocketResponse: + ws = web.WebSocketResponse(heartbeat=20, max_msg_size=64 * 1024, compress=False) + await ws.prepare(request) + sender = asyncio.create_task(_send_sound_states(ws)) + try: + async for msg in ws: + if msg.type in (WSMsgType.CLOSE, WSMsgType.CLOSING, WSMsgType.ERROR): + break + finally: + sender.cancel() + try: + await sender + except asyncio.CancelledError: + pass + except Exception: + pass + return ws + + +def register(app: web.Application) -> None: + app.router.add_get("/ws/web_sound", ws_web_sound) diff --git a/openpilot/selfdrive/carrot/web/assets/alert_camera.svg b/openpilot/selfdrive/carrot/web/assets/alert_camera.svg new file mode 100644 index 0000000000..9ca3cc9cbf --- /dev/null +++ b/openpilot/selfdrive/carrot/web/assets/alert_camera.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f1086279a32f49f68de1db4e89a93aa2aae779395cfb908e5f6a0cbf2bf7dae6 +size 989 diff --git a/openpilot/selfdrive/carrot/web/assets/alert_police.svg b/openpilot/selfdrive/carrot/web/assets/alert_police.svg new file mode 100644 index 0000000000..96fdd53484 --- /dev/null +++ b/openpilot/selfdrive/carrot/web/assets/alert_police.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d0dea6745d5cd5cf8206ced256310fed587555b3309d80cad46a4c066a931401 +size 707 diff --git a/openpilot/selfdrive/carrot/web/css/components/mini_hud.css b/openpilot/selfdrive/carrot/web/css/components/mini_hud.css index e3b9d7787b..17e9f3cd11 100644 --- a/openpilot/selfdrive/carrot/web/css/components/mini_hud.css +++ b/openpilot/selfdrive/carrot/web/css/components/mini_hud.css @@ -19,23 +19,46 @@ --mini-set-font: 60px; --mini-speed-scale-x: 1; --mini-set-scale-x: 1; - --mini-limit-size: 240px; - --mini-limit-font: 96px; + --mini-speed-slot-width: 260px; + --mini-set-slot-width: 112px; + --mini-speed-gap: 12px; + --mini-gear-left: 68%; + --mini-gear-bottom: var(--mini-set-font); + --mini-gear-font: 18px; + --mini-gear-height: 31px; + --mini-limit-size: 260px; + --mini-limit-font: 104px; + --mini-limit-caption-font: 34px; --mini-limit-label-font: 34px; --mini-badge-font: 46px; --mini-alert-font: 34px; - --mini-mode-font: 62px; - --mini-gear-font: 136px; + --mini-alert-label-font: calc(var(--mini-alert-font) * 0.46); + --mini-alert-label-width: calc(var(--mini-alert-font) * 2.24); + --mini-alert-row-radius: calc(9px * var(--mini-ui-scale)); + --mini-alert-row-border: rgba(98, 245, 167, 0.20); + --mini-alert-row-bg: rgba(31, 82, 58, 0.30); + --mini-alert-label-bg: rgba(98, 245, 167, 0.105); + --mini-detail-height: 220px; + --mini-mode-font: 48px; + --mini-mode-size: 160px; --mini-radius: calc(20px * var(--mini-ui-scale)); --mini-padding: calc(12px * var(--mini-ui-scale)); --mini-section-gap: calc(10px * var(--mini-ui-scale)); --mini-band-height: calc(54px * var(--mini-ui-scale)); - --mini-panel-top: rgba(13, 56, 38, 1); - --mini-panel-mid: rgba(18, 65, 44, 1); - --mini-panel-bottom: rgba(8, 31, 21, 1); - --mini-panel-border: rgba(255, 255, 255, 0.18); - --mini-text-shadow-strong: 0 1.4px 3.6px rgba(0, 0, 0, 0.94), 0 0 1.2px rgba(0, 0, 0, 0.62); + --mini-chip-radius: calc(12px * var(--mini-ui-scale)); /* app --r-md look for the detail chips */ + --mini-panel-top: #0b3726; + --mini-panel-mid: #0f402d; + --mini-panel-bottom: #07140f; + --mini-panel-border: rgba(255, 255, 255, 0.16); + --mini-surface: rgba(255, 255, 255, 0.06); + --mini-surface-strong: rgba(255, 255, 255, 0.10); + --mini-text: #f7fff9; + --mini-muted: rgba(247, 255, 249, 0.66); + --mini-accent: #35f2a0; + --mini-warning: #ffd24a; + --mini-danger: #ff304f; + --mini-text-shadow-strong: 0 1px 2.4px rgba(0, 0, 0, 0.72); position: fixed; inset: 0; @@ -51,7 +74,7 @@ linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.01) 38%, transparent 72%), linear-gradient(145deg, var(--mini-panel-top), var(--mini-panel-mid) 58%, var(--mini-panel-bottom)); box-shadow: 0 12px 20px rgba(0, 0, 0, 0.54); - color: #fff; + color: var(--mini-text); isolation: isolate; contain: strict; user-select: none; @@ -103,7 +126,7 @@ justify-content: center; padding: 0 calc(var(--mini-section-gap) * 0.24); overflow: hidden; - color: #fff; + color: var(--mini-text); font-size: var(--mini-top-font); font-weight: 950; line-height: 1; @@ -116,6 +139,26 @@ border-left: 1px solid rgba(255, 255, 255, 0.24); } +.carrot-mini-hud__cpu { + min-width: 0; + max-width: 100%; + padding: 0; + border: 0; + background: transparent; + color: inherit; + font: inherit; + line-height: inherit; + letter-spacing: inherit; + text-shadow: inherit; + white-space: nowrap; + cursor: pointer; +} + +.carrot-mini-hud__cpu:focus-visible { + outline: max(1px, calc(2px * var(--mini-ui-scale))) solid rgba(247, 255, 249, 0.78); + outline-offset: calc(3px * var(--mini-ui-scale)); +} + /* ── Main region: limit | detail, or stock ───────────────────── */ .carrot-mini-hud__main { position: relative; @@ -123,7 +166,7 @@ min-height: 0; display: grid; grid-template-columns: minmax(0, 48fr) minmax(0, 52fr); - column-gap: calc(var(--mini-section-gap) * 1.10); + column-gap: calc(var(--mini-section-gap) * 1.28); align-items: center; padding: 0 calc(var(--mini-padding) * 0.18); } @@ -207,158 +250,367 @@ display: none !important; } +/* ── US MUTCD rectangular sign (imperial) — same CSS technique as the circle, + only the shape/colors differ; toggled by root[data-limit-style="us"]. ── */ +.carrot-mini-hud__limit-us-caption { + display: none; +} + +.carrot-mini-hud[data-limit-style="us"] .carrot-mini-hud__limit-sign { + width: calc(var(--mini-limit-size) * 0.82); + height: var(--mini-limit-size); + border-radius: calc(var(--mini-limit-size) * 0.09); + border: max(3px, calc(var(--mini-limit-size) * 0.052)) solid #050807; + background: #ffffff; + gap: calc(var(--mini-limit-size) * 0.015); + padding: calc(var(--mini-limit-size) * 0.055) 0; + box-shadow: + 0 14px 30px rgba(0, 0, 0, 0.40), + inset 0 0 0 2px rgba(0, 0, 0, 0.05); +} + +.carrot-mini-hud[data-limit-style="us"] .carrot-mini-hud__limit-us-caption { + display: flex; + flex-direction: column; + align-items: center; + gap: calc(var(--mini-limit-size) * 0.006); + color: #050807; + font-size: var(--mini-limit-caption-font); + font-weight: 900; + line-height: 0.98; + letter-spacing: 0.01em; + white-space: nowrap; +} + +.carrot-mini-hud[data-limit-style="us"] .carrot-mini-hud__limit-sign > span { + line-height: 0.86; +} + +/* US sign carries no Korean SDI decorations */ +.carrot-mini-hud[data-limit-style="us"] .carrot-mini-hud__limit-badge, +.carrot-mini-hud[data-limit-style="us"] .carrot-mini-hud__limit-sign > small { + display: none !important; +} + +/* ── View-test badge (mini_hud_demo.js, ?mhud_test=1 only) ── */ +.carrot-mini-hud .carrot-mini-hud__demo-tag { + position: absolute; /* beats the `.carrot-mini-hud > *` position:relative */ + top: 3px; + left: 3px; + z-index: 20; + padding: 1px 6px; + border-radius: 5px; + background: rgba(0, 0, 0, 0.58); + color: #8fe3ff; + font: 800 10px/1.25 system-ui, -apple-system, "Segoe UI", sans-serif; + letter-spacing: 0.02em; + white-space: nowrap; + pointer-events: none; +} + +.carrot-mini-hud .carrot-mini-hud__demo-tag[hidden] { + display: none !important; +} + /* ── Detail (alert) zone: countdown / distance / type ────────── */ .carrot-mini-hud__alert { grid-column: 2; grid-row: 1; justify-self: stretch; - align-self: stretch; + align-self: center; min-width: 0; + height: var(--mini-detail-height); min-height: 0; - display: flex; - flex-direction: column; - justify-content: center; - gap: calc(var(--mini-section-gap) * 1.90); + max-height: 100%; + display: grid; + grid-template-rows: repeat(4, minmax(0, 1fr)); + align-content: stretch; + align-items: stretch; + gap: calc(var(--mini-section-gap) * 0.52); padding: - 0 - calc(var(--mini-padding) * 0.06) - 0 - clamp(6px, calc(var(--mini-padding) * 1.05), 24px); + calc(var(--mini-section-gap) * 0.18) + calc(var(--mini-padding) * 0.34) + calc(var(--mini-section-gap) * 0.18) + calc(var(--mini-padding) * 0.10); } -.carrot-mini-hud__alert > * { - min-width: 0; - color: #fff; - font-size: var(--mini-alert-font); - font-weight: 950; - line-height: 0.92; - letter-spacing: 0; - text-shadow: var(--mini-text-shadow-strong); - font-variant-numeric: tabular-nums; - white-space: nowrap; +.carrot-mini-hud__limit-visual { + position: relative; + width: max-content; + height: max-content; + display: grid; + place-items: center; } -.carrot-mini-hud__alert > strong { - color: #ff6a76; +.carrot-mini-hud__alert-badge { + --mini-alert-badge-glow: rgba(53, 242, 160, 0.48); + position: absolute; + top: calc(var(--mini-limit-size) * -0.075); + right: calc(var(--mini-limit-size) * -0.09); + z-index: 3; + width: calc(var(--mini-limit-size) * 0.47); + height: calc(var(--mini-limit-size) * 0.47); + object-fit: contain; + filter: + drop-shadow(1.5px 0 0 rgba(255, 255, 255, 0.98)) + drop-shadow(-1.5px 0 0 rgba(255, 255, 255, 0.98)) + drop-shadow(0 1.5px 0 rgba(255, 255, 255, 0.98)) + drop-shadow(0 -1.5px 0 rgba(255, 255, 255, 0.98)) + drop-shadow(1px 1px 0 rgba(255, 255, 255, 0.90)) + drop-shadow(-1px -1px 0 rgba(255, 255, 255, 0.90)) + drop-shadow(0 calc(8px * var(--mini-ui-scale)) calc(9px * var(--mini-ui-scale)) rgba(0, 0, 0, 0.76)) + drop-shadow(0 0 calc(12px * var(--mini-ui-scale)) var(--mini-alert-badge-glow)); + animation: carrot-mini-hud-alert-badge-in 180ms ease-out; +} + +.carrot-mini-hud__alert-badge[data-kind="police"] { + --mini-alert-badge-glow: rgba(78, 153, 255, 0.58); +} + +.carrot-mini-hud[data-limit-style="us"] .carrot-mini-hud__alert-badge { + top: calc(var(--mini-limit-size) * -0.10); + right: calc(var(--mini-limit-size) * -0.17); +} + +.carrot-mini-hud__alert-badge[hidden] { + display: none !important; } -.carrot-mini-hud[data-alert-kind="police"] .carrot-mini-hud__alert > strong, -.carrot-mini-hud[data-alert-kind="bump"] .carrot-mini-hud__alert > strong { - color: #ffc24b; +@keyframes carrot-mini-hud-alert-badge-in { + from { opacity: 0; transform: scale(0.76); } + to { opacity: 1; transform: scale(1); } } -/* ── Stock zone (source = STOCK): drive mode + gear ──────────── */ -.carrot-mini-hud__stock { - grid-column: 1 / -1; +.carrot-mini-hud__stock-mode { + grid-column: 1; grid-row: 1; align-self: stretch; min-width: 0; min-height: 0; - display: grid; - grid-template-columns: minmax(0, 48fr) minmax(0, 52fr); + display: flex; align-items: center; + justify-content: center; } -.carrot-mini-hud__stock[hidden], -.carrot-mini-hud__limit[hidden], -.carrot-mini-hud__alert[hidden] { +.carrot-mini-hud__stock-mode[hidden] { display: none !important; } -.carrot-mini-hud__drive-mode, -.carrot-mini-hud__gear { +.carrot-mini-hud__drive-mode { + --mini-mode-accent: #eaf4ef; + --mini-mode-fill: #050a08; + position: relative; + overflow: visible; + width: var(--mini-mode-size); min-width: 0; + height: auto; + min-height: 0; display: flex; align-items: center; justify-content: center; + padding: calc(var(--mini-mode-font) * 0.12) 0; } -.carrot-mini-hud__drive-mode { - width: 92%; - justify-self: center; - min-height: calc(var(--mini-mode-font) * 1.62); - padding: 0 calc(var(--mini-mode-font) * 0.18); - border: max(1px, calc(2px * var(--mini-ui-scale))) solid rgba(255, 255, 255, 0.28); - border-radius: 999px; - background: rgba(255, 255, 255, 0.035); - box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.10); +.carrot-mini-hud__drive-mode > span { + max-width: none; + overflow: visible; + color: var(--mini-mode-fill); + font-family: inherit; + font-size: var(--mini-mode-font); + font-stretch: condensed; + font-weight: 950; + line-height: 0.94; + letter-spacing: 0; + -webkit-text-stroke: max(2px, calc(2.6px * var(--mini-ui-scale))) rgba(255, 255, 255, 0.98); + paint-order: stroke fill; + text-shadow: 0 calc(2px * var(--mini-ui-scale)) calc(4px * var(--mini-ui-scale)) rgba(0, 0, 0, 0.72); + white-space: nowrap; +} + +.carrot-mini-hud__drive-mode.is-demo-interactive { + cursor: pointer; + touch-action: manipulation; +} + +.carrot-mini-hud__drive-mode.is-demo-interactive:active { + transform: translateY(calc(1px * var(--mini-ui-scale))); +} + +.carrot-mini-hud__drive-mode.is-demo-interactive:focus-visible { + outline: max(1px, calc(2px * var(--mini-ui-scale))) solid var(--mini-mode-accent); + outline-offset: calc(3px * var(--mini-ui-scale)); } .carrot-mini-hud__drive-mode[data-mini-hud-drive-kind="eco"] { - border-color: rgba(73, 221, 130, 0.42); + --mini-mode-accent: #49dd82; } .carrot-mini-hud__drive-mode[data-mini-hud-drive-kind="safe"] { - border-color: rgba(120, 213, 238, 0.42); + --mini-mode-accent: #78d5ee; } .carrot-mini-hud__drive-mode[data-mini-hud-drive-kind="sport"] { - border-color: rgba(255, 102, 115, 0.42); + --mini-mode-accent: #ff6673; } -.carrot-mini-hud__drive-mode > span { - max-width: 100%; - overflow: hidden; - color: #fff; - font-family: "Roboto Condensed", "Arial Narrow", "Noto Sans Condensed", "sans-serif-condensed", sans-serif; - font-size: var(--mini-mode-font); - font-stretch: condensed; +.carrot-mini-hud__alert > * { + min-width: 0; + color: var(--mini-text); + font-size: var(--mini-alert-font); font-weight: 950; - line-height: 0.94; + line-height: 1; letter-spacing: 0; - text-overflow: clip; - text-shadow: var(--mini-text-shadow-strong); + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.64); + font-variant-numeric: tabular-nums; white-space: nowrap; } -.carrot-mini-hud__drive-mode[data-mini-hud-drive-kind="eco"] > span { - color: #49dd82; +.carrot-mini-hud__alert > strong { + color: var(--mini-accent); } -.carrot-mini-hud__drive-mode[data-mini-hud-drive-kind="safe"] > span { - color: #78d5ee; +.carrot-mini-hud[data-alert-kind="police"] .carrot-mini-hud__alert > strong, +.carrot-mini-hud[data-alert-kind="bump"] .carrot-mini-hud__alert > strong { + color: var(--mini-warning); } -.carrot-mini-hud__drive-mode[data-mini-hud-drive-kind="sport"] > span { - color: #ff6673; +/* ── Detail chips (countdown / distance / temp) ─────────────────────────────── + Rounded-rect chips borrowing the app's --r-md / .chip structure (translucent + hairline border + faint fill), kept in the minihud green palette, no icons. + label = small + dim, value = prominent. */ +.carrot-mini-hud__chip { + width: 100%; + height: 100%; + min-height: 0; + display: grid; + grid-template-columns: auto minmax(0, 1fr); + align-items: center; + gap: calc(var(--mini-alert-font) * 0.26); + padding: 0; + border: 1px solid var(--mini-alert-row-border); + border-radius: var(--mini-alert-row-radius); + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.052), rgba(255, 255, 255, 0.022)), + var(--mini-alert-row-bg); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.052); + line-height: 1; } -.carrot-mini-hud__gear { - align-items: baseline; - gap: calc(4px * var(--mini-ui-scale)); - white-space: nowrap; +.carrot-mini-hud__chip + .carrot-mini-hud__chip { + padding-top: 0; + border-top: 0; } -.carrot-mini-hud__gear small { - color: rgba(255, 255, 255, 0.58); - font-size: calc(var(--mini-gear-font) * 0.34); - font-weight: 900; - line-height: 1; - text-shadow: var(--mini-text-shadow-strong); +.carrot-mini-hud__chip[hidden] { + display: none !important; } -.carrot-mini-hud__gear small[hidden] { - display: none !important; +.carrot-mini-hud__chip[data-mini-hud-empty="1"] > b { + opacity: 0.48; } -.carrot-mini-hud__gear strong { - color: #fff; - font-size: var(--mini-gear-font); +.carrot-mini-hud__chip-label { + align-self: stretch; + width: var(--mini-alert-label-width); + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0 calc(var(--mini-alert-font) * 0.16); + border-right: 1px solid rgba(98, 245, 167, 0.16); + border-radius: + calc(var(--mini-alert-row-radius) - 1px) + 0 + 0 + calc(var(--mini-alert-row-radius) - 1px); + background: var(--mini-alert-label-bg); + color: rgba(247, 255, 249, 0.96); + font-size: var(--mini-alert-label-font); font-weight: 950; - line-height: 0.9; letter-spacing: 0; + text-transform: uppercase; + line-height: 1.05; +} + +.carrot-mini-hud__chip > b { + min-width: 0; + justify-self: start; + text-align: left; + padding-right: calc(var(--mini-alert-font) * 0.30); + font-weight: 950; font-variant-numeric: tabular-nums; - text-shadow: var(--mini-text-shadow-strong); +} + +.carrot-mini-hud__gap-value { + width: 100%; + display: grid; + grid-template-columns: auto minmax(0, 1fr); + align-items: center; + gap: calc(var(--mini-alert-font) * 0.18); +} + +.carrot-mini-hud__gap-signal { + width: 100%; + min-width: 0; + height: calc(var(--mini-alert-font) * 0.34); + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + align-items: stretch; + gap: calc(var(--mini-alert-font) * 0.085); +} + +.carrot-mini-hud__gap-signal > i { + width: 100%; + height: 100%; + border-radius: calc(var(--mini-alert-font) * 0.10); + background: rgba(247, 255, 249, 0.22); + box-shadow: inset 0 0 0 1px rgba(247, 255, 249, 0.09); +} + +.carrot-mini-hud__gap-signal[data-level="1"] > i:nth-child(-n+1), +.carrot-mini-hud__gap-signal[data-level="2"] > i:nth-child(-n+2), +.carrot-mini-hud__gap-signal[data-level="3"] > i:nth-child(-n+3), +.carrot-mini-hud__gap-signal[data-level="4"] > i:nth-child(-n+4) { + background: var(--mini-accent); + box-shadow: 0 0 calc(5px * var(--mini-ui-scale)) rgba(53, 242, 160, 0.32); +} + +/* temp chip carries the accel/decel color; its rounded shape comes from .chip. */ +.carrot-mini-hud__temp { + /* Wins over the reddish `.carrot-mini-hud__alert > strong` and the police/bump + amber rules above, which target the same element slot. */ + color: var(--mini-accent) !important; +} + +.carrot-mini-hud__temp[data-mini-hud-temp-decel="1"] { + color: var(--mini-warning) !important; +} + +.carrot-mini-hud[data-source="nav"] [data-mini-hud-temp-zone] { + grid-row: 3; +} + +.carrot-mini-hud[data-source="nav"] [data-mini-hud-gap-zone] { + grid-row: 4; +} + +/* Shared visibility rules for the main-band zones. */ +.carrot-mini-hud__limit[hidden], +.carrot-mini-hud__alert[hidden] { + display: none !important; } /* ── Speed row: current speed (70%) | set speed (30%) ────────── */ .carrot-mini-hud__speed-row { + position: relative; flex: 0 0 34%; min-height: 0; display: grid; - grid-template-columns: minmax(0, 70fr) minmax(0, 30fr); + grid-template-columns: + minmax(0, var(--mini-speed-slot-width)) + minmax(0, var(--mini-set-slot-width)); + justify-content: center; align-items: end; - column-gap: 0; + column-gap: var(--mini-speed-gap); margin-bottom: calc(var(--mini-section-gap) * 0.26); } @@ -377,27 +629,69 @@ } .carrot-mini-hud__speed { - justify-content: flex-start; + justify-content: center; padding: calc(var(--mini-padding) * 0.18) + 0 calc(var(--mini-padding) * 0.10) - calc(var(--mini-padding) * 0.10) - calc(var(--mini-padding) * 0.16); + 0; color: #fff; font-size: var(--mini-speed-font); line-height: 0.84; + text-align: center; } .carrot-mini-hud__set-speed { - justify-content: flex-end; + justify-content: center; padding: calc(var(--mini-padding) * 0.18) - calc(var(--mini-padding) * 0.16) + 0 calc(var(--mini-padding) * 0.10) - calc(var(--mini-padding) * 0.10); + 0; color: #f4f7fb; font-size: var(--mini-set-font); line-height: 0.92; + text-align: center; +} + +.carrot-mini-hud__gear-badge { + position: absolute; + left: var(--mini-gear-left); + right: auto; + bottom: var(--mini-gear-bottom); + min-width: calc(var(--mini-gear-font) * 2.6); + height: var(--mini-gear-height); + display: inline-flex; + align-items: center; + justify-content: center; + gap: calc(3px * var(--mini-ui-scale)); + padding: 0 calc(var(--mini-gear-font) * 0.40); + border: max(1px, calc(1.5px * var(--mini-ui-scale))) solid rgba(247, 255, 249, 0.34); + border-radius: calc(var(--mini-gear-font) * 0.43); + background: rgba(5, 17, 12, 0.90); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.08), + 0 calc(3px * var(--mini-ui-scale)) calc(7px * var(--mini-ui-scale)) rgba(0, 0, 0, 0.46); + color: rgba(247, 255, 249, 0.86); + font-size: var(--mini-gear-font); + font-weight: 900; + line-height: 1; + text-shadow: var(--mini-text-shadow-strong); + white-space: nowrap; +} + +.carrot-mini-hud__gear-badge[hidden] { + display: none !important; +} + +.carrot-mini-hud__gear-badge > strong { + color: #ffffff; + font-size: inherit; + font-variant-numeric: tabular-nums; +} + +.carrot-mini-hud__gear-badge[data-mini-hud-gear-known="0"] { + opacity: 0.64; } .carrot-mini-hud__speed > span, @@ -416,12 +710,6 @@ transform: scaleX(var(--mini-set-scale-x)); } -.carrot-mini-hud.is-three-digit .carrot-mini-hud__speed, -.carrot-mini-hud.is-three-digit .carrot-mini-hud__set-speed { - justify-content: center; - text-align: center; -} - .carrot-mini-hud.is-three-digit .carrot-mini-hud__speed > span, .carrot-mini-hud.is-three-digit .carrot-mini-hud__set-speed > span { font-family: "Roboto Condensed", "Arial Narrow", "Noto Sans Condensed", "sans-serif-condensed", sans-serif; diff --git a/openpilot/selfdrive/carrot/web/css/pages/drive.css b/openpilot/selfdrive/carrot/web/css/pages/drive.css index ba1520f2f9..e1ffed922e 100644 --- a/openpilot/selfdrive/carrot/web/css/pages/drive.css +++ b/openpilot/selfdrive/carrot/web/css/pages/drive.css @@ -1182,6 +1182,76 @@ body[data-page="carrot"] #driveHudCard.driveHudCard--loading { color: var(--md-primary); } +.carrot-stage__controlBtn--sound { + min-width: 68px; +} + +.carrot-stage__controlBtn--sound.is-active { + border-color: color-mix(in srgb, #49dd82 72%, rgba(255, 255, 255, 0.22)); + background: color-mix(in srgb, #49dd82 18%, var(--md-surface-cont-h)); + color: #b9ffd2; + box-shadow: 0 0 0 1px rgba(73, 221, 130, 0.12), 0 0 14px rgba(73, 221, 130, 0.12), 0 8px 22px rgba(0, 0, 0, 0.24); +} + +.app-dialog--web-sound .app-dialog__sheet { + width: min(calc(100vw - 32px), 430px); +} + +.app-dialog--web-sound .app-dialog__body { + white-space: normal; +} + +.web-sound-dialog { + display: grid; + gap: 18px; +} + +.web-sound-dialog__description { + margin: 0; + color: var(--md-on-surface-var); + font-size: 14px; + line-height: 1.55; +} + +.web-sound-dialog__toggle { + min-height: 58px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 20px; + padding: 0 4px; + color: var(--md-on-surface); + font-size: 15px; + font-weight: 850; + line-height: 1.2; +} + +.web-sound-dialog__volume { + display: grid; + gap: 10px; + padding: 2px 4px 0; + color: var(--md-on-surface); + font-size: 14px; + font-weight: 800; +} + +.web-sound-dialog__volume-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; +} + +.web-sound-dialog__volume-head output { + color: var(--md-primary); + font-variant-numeric: tabular-nums; +} + +.web-sound-dialog__volume-slider { + width: 100%; + accent-color: var(--md-primary); +} + .carrot-stage__controlBtn--record { min-width: 74px; } diff --git a/openpilot/selfdrive/carrot/web/index.html b/openpilot/selfdrive/carrot/web/index.html index 37816d7682..789fe40245 100644 --- a/openpilot/selfdrive/carrot/web/index.html +++ b/openpilot/selfdrive/carrot/web/index.html @@ -132,9 +132,9 @@ - + - + @@ -527,7 +527,8 @@
${getUIText( + "web_sound_description", + "This feature plays driving alerts on the connected phone or browser for clone devices that cannot play sound.", + )}
+ + +