mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-07-12 12:52:13 +08:00
BigUI WIP: Full screen settings exit + bread margins
This commit is contained in:
@@ -27,7 +27,7 @@ env_var_truthy() {
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
./onroad [jobs] (--c3 | --c4 | --raybig | --all | --replay-only) [-nav] [--cem] [--prefix name] <route-or-replay-args...>
|
||||
./onroad [jobs] (--c3 | --c4 | --raybig | --all | --replay-only) [-nav] [-alert] [--cem] [--prefix name] <route-or-replay-args...>
|
||||
|
||||
Examples:
|
||||
./onroad --c3 <route>
|
||||
@@ -35,6 +35,7 @@ Examples:
|
||||
./onroad --c4 -nav <route>
|
||||
./onroad --c4 --cem --demo
|
||||
./onroad --raybig --cem --demo
|
||||
./onroad --raybig --cem -alert --demo --no-loop
|
||||
./onroad --all <route>
|
||||
./onroad --replay-only --demo --no-vipc --no-loop
|
||||
|
||||
@@ -44,6 +45,7 @@ Notes:
|
||||
- Use multiple UI flags together if you want more than one desktop UI at once.
|
||||
- -nav injects a fake navigation demo stream and blocks replay from publishing navInstruction/navRoute.
|
||||
- --cem publishes fake CEM statuses for desktop visual review in the raylib UIs.
|
||||
- -alert blocks replay from publishing selfdriveState and fires a fake critical full-screen red alert (alertSize=full, alertStatus=critical) 20 seconds after the demo publisher starts (10s for replay route + UI to come up, plus 10s for the user to open Settings). Default alert text mimics a real controlsMismatch event; run tools/replay/fake_alert_demo.py directly to override --text1/--text2/--delay.
|
||||
EOF
|
||||
}
|
||||
|
||||
@@ -60,9 +62,11 @@ LEGACY_UI_SELECTION=""
|
||||
REPLAY_ONLY=0
|
||||
NAV_DEMO=0
|
||||
CEM_DEMO=0
|
||||
ALERT_DEMO=0
|
||||
REPLAY_PID=""
|
||||
NAV_PID=""
|
||||
CEM_PID=""
|
||||
ALERT_PID=""
|
||||
UI_PIDS=()
|
||||
|
||||
parse_args() {
|
||||
@@ -100,6 +104,10 @@ parse_args() {
|
||||
CEM_DEMO=1
|
||||
shift
|
||||
;;
|
||||
-alert|--alert|--alert-demo)
|
||||
ALERT_DEMO=1
|
||||
shift
|
||||
;;
|
||||
--ui)
|
||||
if [[ $# -lt 2 ]]; then
|
||||
echo "Missing value for --ui" >&2
|
||||
@@ -217,6 +225,9 @@ cleanup() {
|
||||
if [[ -n "${CEM_PID}" ]]; then
|
||||
kill "${CEM_PID}" >/dev/null 2>&1 || true
|
||||
fi
|
||||
if [[ -n "${ALERT_PID}" ]]; then
|
||||
kill "${ALERT_PID}" >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
for pid in "${UI_PIDS[@]-}"; do
|
||||
if [[ -n "${pid}" ]]; then
|
||||
@@ -232,6 +243,9 @@ cleanup() {
|
||||
if [[ -n "${CEM_PID}" ]]; then
|
||||
wait "${CEM_PID}" >/dev/null 2>&1 || true
|
||||
fi
|
||||
if [[ -n "${ALERT_PID}" ]]; then
|
||||
wait "${ALERT_PID}" >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
if [[ -n "${OPENPILOT_PREFIX:-}" && "${OPENPILOT_PREFIX}" == desktop-onroad-* ]]; then
|
||||
echo "Cleaning up temporary prefix environment (${OPENPILOT_PREFIX})..."
|
||||
@@ -305,6 +319,30 @@ ensure_nav_demo_replay_blocklist() {
|
||||
REPLAY_ARGS=(-b "${nav_services}" "${REPLAY_ARGS[@]}")
|
||||
}
|
||||
|
||||
ensure_alert_demo_replay_blocklist() {
|
||||
local alert_services="selfdriveState"
|
||||
local idx=0
|
||||
|
||||
for ((idx=0; idx<${#REPLAY_ARGS[@]}; idx++)); do
|
||||
case "${REPLAY_ARGS[$idx]}" in
|
||||
-b|--block)
|
||||
if (( idx + 1 >= ${#REPLAY_ARGS[@]} )); then
|
||||
echo "Missing value for ${REPLAY_ARGS[$idx]}" >&2
|
||||
exit 1
|
||||
fi
|
||||
REPLAY_ARGS[$((idx + 1))]="$(append_blocked_service_names "${REPLAY_ARGS[$((idx + 1))]}" "${alert_services}")"
|
||||
return
|
||||
;;
|
||||
--block=*)
|
||||
REPLAY_ARGS[$idx]="--block=$(append_blocked_service_names "${REPLAY_ARGS[$idx]#*=}" "${alert_services}")"
|
||||
return
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
REPLAY_ARGS=(-b "${alert_services}" "${REPLAY_ARGS[@]}")
|
||||
}
|
||||
|
||||
prepare_env() {
|
||||
source .venv/bin/activate
|
||||
|
||||
@@ -331,6 +369,7 @@ prepare_env() {
|
||||
export SP_ALLOW_DESKTOP_FAKE_WIFI=0
|
||||
export SP_ONROAD_NAV_DEMO="${NAV_DEMO}"
|
||||
export SP_CEM_DEMO="${CEM_DEMO}"
|
||||
export SP_ONROAD_ALERT_DEMO="${ALERT_DEMO}"
|
||||
|
||||
if [[ "$(uname -s)" == "Darwin" ]] || env_var_truthy "${ZMQ:-0}"; then
|
||||
export OPENPILOT_ZMQ_NAMESPACE="${PREFIX_ARG:-${OPENPILOT_ZMQ_NAMESPACE:-desktop-onroad-$$}}"
|
||||
@@ -423,6 +462,18 @@ launch_cem_demo() {
|
||||
fi
|
||||
}
|
||||
|
||||
launch_alert_demo() {
|
||||
echo "Starting fake critical alert demo publisher (fires after 20s on-road; blocks replay's selfdriveState)..."
|
||||
"${ROOT_DIR}/.venv/bin/python3" "${ROOT_DIR}/tools/replay/fake_alert_demo.py" &
|
||||
ALERT_PID=$!
|
||||
|
||||
sleep 0.5
|
||||
if ! kill -0 "${ALERT_PID}" >/dev/null 2>&1; then
|
||||
wait "${ALERT_PID}"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
launch_c3_ui() {
|
||||
local os_ext="linux"
|
||||
if [[ "$(uname -s)" == "Darwin" ]]; then
|
||||
@@ -473,6 +524,10 @@ if [[ "${NAV_DEMO}" == "1" ]]; then
|
||||
ensure_nav_demo_replay_blocklist
|
||||
fi
|
||||
|
||||
if [[ "${ALERT_DEMO}" == "1" ]]; then
|
||||
ensure_alert_demo_replay_blocklist
|
||||
fi
|
||||
|
||||
if [[ ${#REPLAY_ARGS[@]} -eq 0 ]]; then
|
||||
usage >&2
|
||||
exit 1
|
||||
@@ -520,6 +575,10 @@ if [[ "${CEM_DEMO}" == "1" ]]; then
|
||||
launch_cem_demo
|
||||
fi
|
||||
|
||||
if [[ "${ALERT_DEMO}" == "1" ]]; then
|
||||
launch_alert_demo
|
||||
fi
|
||||
|
||||
if [[ ${#UI_TARGETS[@]} -eq 0 ]]; then
|
||||
echo "Replay is running without UI windows. Press Ctrl-C to stop."
|
||||
wait "${REPLAY_PID}"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import pyray as rl
|
||||
from enum import IntEnum
|
||||
from cereal import log
|
||||
import cereal.messaging as messaging
|
||||
from openpilot.system.hardware import PC
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
@@ -46,7 +47,18 @@ class MainLayout(Widget):
|
||||
if not self._onboarding_window.completed:
|
||||
gui_app.push_widget(self._onboarding_window)
|
||||
|
||||
@staticmethod
|
||||
def _critical_full_alert_active() -> bool:
|
||||
if not ui_state.sm.recv_frame['selfdriveState']:
|
||||
return False
|
||||
ss = ui_state.sm['selfdriveState']
|
||||
return ss.alertSize == log.SelfdriveState.AlertSize.full and ss.alertStatus == log.SelfdriveState.AlertStatus.critical
|
||||
|
||||
def _render(self, _):
|
||||
if self._current_mode == MainState.SETTINGS and self._critical_full_alert_active():
|
||||
self._set_current_layout(MainState.ONROAD)
|
||||
return
|
||||
|
||||
self._handle_onroad_transition()
|
||||
self._render_main_content()
|
||||
|
||||
|
||||
@@ -1177,11 +1177,11 @@ class BreadcrumbController:
|
||||
target = 1.0 if self._expanded else 0.0
|
||||
self._expand_alpha += (target - self._expand_alpha) * ANIM_LERP
|
||||
|
||||
ACTIVE_SIZE = 58
|
||||
PAST_SIZE = 50
|
||||
CHEVRON_SIZE = 38
|
||||
CHEVRON_W = 32
|
||||
GAP = 23
|
||||
ACTIVE_SIZE = 48
|
||||
PAST_SIZE = 40
|
||||
CHEVRON_SIZE = 30
|
||||
CHEVRON_W = 26
|
||||
GAP = 20
|
||||
|
||||
center_y = rect.y + rect.height / 2
|
||||
|
||||
@@ -1198,8 +1198,8 @@ class BreadcrumbController:
|
||||
|
||||
mouse_pos = gui_app.last_mouse_event.pos
|
||||
|
||||
MIN_ACTIVE_SIZE = 34
|
||||
CAPSULE_W = 102
|
||||
MIN_ACTIVE_SIZE = 30
|
||||
CAPSULE_W = 92
|
||||
capsule_need = CAPSULE_W + GAP + CHEVRON_W + GAP
|
||||
|
||||
active_size = ACTIVE_SIZE
|
||||
@@ -1264,7 +1264,7 @@ class BreadcrumbController:
|
||||
current_x += GAP
|
||||
continue
|
||||
|
||||
capsule_w, capsule_h = CAPSULE_W, 58
|
||||
capsule_w, capsule_h = CAPSULE_W, 48
|
||||
cap_rect = rl.Rectangle(current_x, center_y - capsule_h / 2, capsule_w, capsule_h)
|
||||
hovered = point_hits(mouse_pos, cap_rect, None, pad_x=4, pad_y=6)
|
||||
|
||||
@@ -1297,10 +1297,10 @@ class BreadcrumbController:
|
||||
rl.draw_rectangle_rounded_lines_ex(cap_rect, 1.0, 16, 1.0, outline)
|
||||
|
||||
font_dots = gui_app.font(FontWeight.BOLD)
|
||||
dots_ts = measure_text_cached(font_dots, "...", 41)
|
||||
dots_ts = measure_text_cached(font_dots, "...", 34)
|
||||
rl.draw_text_ex(font_dots, "...",
|
||||
rl.Vector2(cap_rect.x + (cap_rect.width - dots_ts.x) / 2, center_y - dots_ts.y / 2),
|
||||
41, 0, dots_c)
|
||||
34, 0, dots_c)
|
||||
current_x += capsule_w + GAP
|
||||
|
||||
else:
|
||||
@@ -1326,7 +1326,7 @@ class BreadcrumbController:
|
||||
c_pressed = rl.Color(past_pressed.r, past_pressed.g, past_pressed.b, item_alpha)
|
||||
|
||||
ts = measure_text_cached(font, text, font_size)
|
||||
hit_rect = rl.Rectangle(current_x - 9, center_y - 44, ts.x + 17, 87)
|
||||
hit_rect = rl.Rectangle(current_x - 8, center_y - 38, ts.x + 16, 76)
|
||||
hovered = point_hits(mouse_pos, hit_rect, None, pad_x=0, pad_y=0)
|
||||
|
||||
# Only add to interactive rects if it's visible within the bounds
|
||||
@@ -3382,7 +3382,7 @@ class AetherSettingsView(PanelManagerView):
|
||||
|
||||
|
||||
def draw_back_button(pill_rect: rl.Rectangle, center_y: float, pressed: bool, hovered: bool) -> rl.Rectangle:
|
||||
back_size = 55
|
||||
back_size = 48
|
||||
back_x = pill_rect.x + 12
|
||||
btn = rl.Rectangle(back_x, center_y - back_size / 2, back_size, back_size)
|
||||
if pressed or hovered:
|
||||
@@ -3458,16 +3458,16 @@ class AetherCategoryDrawer(AetherSettingsView):
|
||||
rl.draw_rectangle_rec(rl.Rectangle(drawer_rect.x + drawer_rect.width - 4, drawer_rect.y, 4, drawer_rect.height), self._color)
|
||||
|
||||
# Draw header with navigation breadcrumbs
|
||||
header_rect = rl.Rectangle(drawer_rect.x + 36, drawer_rect.y + 24, drawer_rect.width - 72, 80)
|
||||
header_rect = rl.Rectangle(drawer_rect.x + 36, drawer_rect.y + 24, drawer_rect.width - 72, 60)
|
||||
if self._has_header:
|
||||
self._draw_header(header_rect)
|
||||
|
||||
# Set up scroll area (vertical scroll list)
|
||||
self._scroll_rect = rl.Rectangle(
|
||||
drawer_rect.x + 36,
|
||||
drawer_rect.y + 120,
|
||||
drawer_rect.y + 100,
|
||||
drawer_rect.width - 72,
|
||||
drawer_rect.height - 144
|
||||
drawer_rect.height - 124
|
||||
)
|
||||
|
||||
content_width = self._scroll_rect.width - AETHER_LIST_METRICS.content_right_gutter
|
||||
@@ -3518,7 +3518,7 @@ class AetherCategoryDrawer(AetherSettingsView):
|
||||
super()._activate_target(target_id)
|
||||
|
||||
def _draw_header(self, rect: rl.Rectangle):
|
||||
pill_h = 52
|
||||
pill_h = 48
|
||||
pill_rect = rl.Rectangle(rect.x, rect.y + (rect.height - pill_h) / 2, rect.width, pill_h)
|
||||
center_y = pill_rect.y + pill_h / 2
|
||||
|
||||
|
||||
@@ -273,7 +273,7 @@ class StarPilotLayout(Widget):
|
||||
self._commit_navigation()
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
TOP_BAR_HEIGHT = 160
|
||||
TOP_BAR_HEIGHT = 72
|
||||
content_rect = rl.Rectangle(rect.x, rect.y + TOP_BAR_HEIGHT, rect.width, rect.height - TOP_BAR_HEIGHT)
|
||||
|
||||
# Standardize width to perfectly match subpanel shells
|
||||
@@ -281,20 +281,20 @@ class StarPilotLayout(Widget):
|
||||
shell_x = rect.x + (rect.width - shell_w) / 2
|
||||
|
||||
# 0. Draw top bar with HubTile-style purple glow
|
||||
glass_rect = rl.Rectangle(shell_x, rect.y + 20, shell_w, TOP_BAR_HEIGHT - 35)
|
||||
glass_rect = rl.Rectangle(shell_x, rect.y + 2, shell_w, TOP_BAR_HEIGHT - 4)
|
||||
|
||||
# 0a. Purple glow rings — 4 concentric, fading outward (HubTile parity)
|
||||
for i in range(4, 0, -1):
|
||||
off = i * 2.5
|
||||
gr = rl.Rectangle(glass_rect.x - off, glass_rect.y - off, glass_rect.width + off * 2, glass_rect.height + off * 2)
|
||||
a = int(25 * (1.0 - i / 5))
|
||||
draw_rounded_fill(gr, rl.Color(139, 92, 246, max(0, min(255, a))), radius_px=145)
|
||||
draw_rounded_fill(gr, rl.Color(139, 92, 246, max(0, min(255, a))), radius_px=34)
|
||||
|
||||
# 0b. Dark fill — strict parity with HubTile _HUD_BG_ON
|
||||
draw_rounded_fill(glass_rect, rl.Color(12, 10, 18, 255), radius_px=145)
|
||||
draw_rounded_fill(glass_rect, rl.Color(12, 10, 18, 255), radius_px=34)
|
||||
|
||||
# 0c. Full bright purple border — strict parity
|
||||
draw_rounded_stroke(glass_rect, rl.Color(139, 92, 246, 255), radius_px=145)
|
||||
draw_rounded_stroke(glass_rect, rl.Color(139, 92, 246, 255), radius_px=34)
|
||||
|
||||
# 1. Draw breadcrumbs in top bar
|
||||
crumb_rect = rl.Rectangle(glass_rect.x, glass_rect.y, glass_rect.width, glass_rect.height)
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import signal
|
||||
import time
|
||||
|
||||
from cereal import car, log, messaging
|
||||
|
||||
AlertSize = log.SelfdriveState.AlertSize
|
||||
AlertStatus = log.SelfdriveState.AlertStatus
|
||||
AudibleAlert = log.SelfdriveState.AudibleAlert
|
||||
VisualAlert = car.CarControl.HUDControl.VisualAlert
|
||||
|
||||
|
||||
running = True
|
||||
|
||||
|
||||
def _handle_signal(_signum, _frame) -> None:
|
||||
global running
|
||||
running = False
|
||||
|
||||
|
||||
def build_msg(text1: str, text2: str, size: int, status: int,
|
||||
audible: int = AudibleAlert.none,
|
||||
visual: int = VisualAlert.none,
|
||||
alert_type: str = ""):
|
||||
m = messaging.new_message("selfdriveState")
|
||||
m.valid = True
|
||||
ss = m.selfdriveState
|
||||
ss.alertSize = size
|
||||
ss.alertStatus = status
|
||||
ss.alertText1 = text1
|
||||
ss.alertText2 = text2
|
||||
ss.alertType = alert_type
|
||||
ss.alertSound = audible
|
||||
ss.alertHudVisual = visual
|
||||
return m
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Publish a fake critical full-screen selfdriveState alert for desktop onroad replay.")
|
||||
parser.add_argument("--delay", type=float, default=20.0, help="Seconds to wait after start before firing the alert (allows replay route + UI to come up, then ~10s for the user to open Settings).")
|
||||
parser.add_argument("--hold-interval", type=float, default=0.25, help="Seconds between selfdriveState publishes.")
|
||||
parser.add_argument("--hold", type=float, default=5.0, help="Seconds to keep the critical alert active before clearing it.")
|
||||
parser.add_argument("--text1", type=str, default="TAKE CONTROL IMMEDIATELY", help="Primary alert text.")
|
||||
parser.add_argument("--text2", type=str, default="Controls Mismatch", help="Secondary alert text.")
|
||||
parser.add_argument("--audible", action="store_true", default=True, help="Include warningImmediate audible alert.")
|
||||
parser.add_argument("--silent", dest="audible", action="store_false", help="Omit audible alert (override --audible).")
|
||||
args = parser.parse_args()
|
||||
|
||||
signal.signal(signal.SIGINT, _handle_signal)
|
||||
signal.signal(signal.SIGTERM, _handle_signal)
|
||||
|
||||
pm = messaging.PubMaster(["selfdriveState"])
|
||||
|
||||
audible = AudibleAlert.warningImmediate if args.audible else AudibleAlert.none
|
||||
visual = VisualAlert.steerRequired
|
||||
|
||||
none_msg = build_msg("", "", AlertSize.none, AlertStatus.normal)
|
||||
fire_msg = build_msg(args.text1, args.text2, AlertSize.full, AlertStatus.critical,
|
||||
audible=audible, visual=visual,
|
||||
alert_type="controlsMismatch/immediateDisable")
|
||||
|
||||
print(f"fake_alert_demo: waiting {args.delay:.1f}s before firing critical alert...", flush=True)
|
||||
|
||||
end = time.monotonic() + args.delay
|
||||
while running and time.monotonic() < end:
|
||||
pm.send("selfdriveState", none_msg)
|
||||
none_msg.clear_write_flag()
|
||||
time.sleep(args.hold_interval)
|
||||
|
||||
if not running:
|
||||
return
|
||||
|
||||
print(f"fake_alert_demo: firing critical red alert: '{args.text1}' / '{args.text2}' for {args.hold:.1f}s", flush=True)
|
||||
|
||||
held = time.monotonic() + args.hold
|
||||
try:
|
||||
while running and time.monotonic() < held:
|
||||
pm.send("selfdriveState", fire_msg)
|
||||
fire_msg.clear_write_flag()
|
||||
time.sleep(args.hold_interval)
|
||||
finally:
|
||||
print("fake_alert_demo: clearing alert (sending AlertSize.none) so UI fades out...", flush=True)
|
||||
clear = build_msg("", "", AlertSize.none, AlertStatus.normal)
|
||||
for _ in range(3):
|
||||
pm.send("selfdriveState", clear)
|
||||
clear.clear_write_flag()
|
||||
time.sleep(args.hold_interval)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user