diff --git a/onroad b/onroad new file mode 100755 index 00000000..08fb5e8f --- /dev/null +++ b/onroad @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +exec "${ROOT_DIR}/scripts/host_tool_runner.sh" onroad "$@" diff --git a/scripts/host_tool_runner.sh b/scripts/host_tool_runner.sh index 91af81da..7d7208c0 100755 --- a/scripts/host_tool_runner.sh +++ b/scripts/host_tool_runner.sh @@ -27,6 +27,7 @@ Commands: c3 Launch the desktop Qt UI from the isolated host cache. c4 Launch the small raylib UI from the isolated host cache. raybig Launch the large raylib UI from the isolated host cache. + onroad Launch replay plus desktop UI(s) from the isolated host cache. replay Build and run replay from the isolated host cache. cabana Build and run cabana from the isolated host cache. plotjuggler Run PlotJuggler helper from the isolated host cache. @@ -43,6 +44,7 @@ Notes: - For c3/c4/raybig, pass the jobs count first to preserve existing shorthand: ./dev c3 8 ./dev raybig 12 + - `./onroad --c3 f08912a233c1584f/2022-08-11--18-02-41/1` launches replay plus the selected desktop UI. - `./dev sync` refreshes all host buckets. Use `./dev sync cabana` to sync one. EOF } @@ -61,7 +63,7 @@ resolve_host_bucket() { local name="${1:-shared}" case "${name}" in - shared|default|ui|c3|c4|raybig|replay|shell) + shared|default|ui|c3|c4|raybig|onroad|replay|shell) echo "shared" ;; cabana) @@ -212,6 +214,17 @@ purge_host_python_artifacts() { "${WORK_DIR}/msgq_repo/msgq/visionipc/visionipc_pyx.cpp" } +purge_host_desktop_ui_artifacts() { + rm -f \ + "${WORK_DIR}/selfdrive/ui/libqt_widgets.a" \ + "${WORK_DIR}/selfdrive/ui/libqt_util.a" \ + "${WORK_DIR}/selfdrive/ui/assets.o" \ + "${WORK_DIR}/selfdrive/ui/main.o" \ + "${WORK_DIR}/selfdrive/ui/moc_ui.o" \ + "${WORK_DIR}/selfdrive/ui/ui.o" \ + "${WORK_DIR}/selfdrive/ui/ui" +} + ensure_host_python_tools() { ensure_venv @@ -237,6 +250,22 @@ ensure_host_python_tools() { ) } +sync_host_generated_headers() { + if ! command -v capnpc >/dev/null 2>&1; then + return + fi + + ( + cd "${WORK_DIR}" + capnpc --src-prefix=cereal \ + cereal/log.capnp \ + cereal/car.capnp \ + cereal/legacy.capnp \ + cereal/custom.capnp \ + -o c++:cereal/gen/cpp/ + ) +} + sync_worktree() { ensure_venv @@ -264,6 +293,8 @@ sync_worktree() { "selfdrive/ui/ui" "selfdrive/ui/ui.macos" "selfdrive/ui/ui.larch64" + "selfdrive/ui/libqt_widgets.a" + "selfdrive/ui/libqt_util.a" "cereal/libcereal.a" "cereal/libsocketmaster.a" "cereal/messaging/bridge" @@ -294,7 +325,9 @@ sync_worktree() { done rsync "${rsync_args[@]}" "${ROOT_DIR}/" "${WORK_DIR}/" + purge_host_desktop_ui_artifacts rm -f "${WORK_DIR}/third_party/libjson11.a" "${WORK_DIR}/third_party/libkaitai.a" + sync_host_generated_headers ensure_host_python_tools rm -rf "${WORK_DIR}/.venv" ln -s "${HOST_VENV}" "${WORK_DIR}/.venv" @@ -396,6 +429,18 @@ launch_raybig() { run_in_worktree "${WORK_DIR}/scripts/launch_ui_raybig_desktop.sh" "${jobs}" "$@" } +launch_onroad() { + local jobs + jobs="$(default_jobs)" + if [[ "${1:-}" =~ ^[0-9]+$ ]]; then + jobs="$1" + shift || true + fi + + sync_worktree + run_in_worktree "${WORK_DIR}/scripts/launch_onroad_desktop.sh" "${jobs}" "$@" +} + launch_replay() { local jobs jobs="$(default_jobs)" @@ -473,7 +518,7 @@ main() { help|-h|--help) usage ;; - c3|c4|raybig|replay|shell) + c3|c4|raybig|onroad|replay|shell) set_host_bucket "shared" acquire_host_lock "${command} $*" ;; @@ -517,6 +562,9 @@ main() { raybig) launch_raybig "$@" ;; + onroad) + launch_onroad "$@" + ;; replay) launch_replay "$@" ;; diff --git a/scripts/launch_onroad_desktop.sh b/scripts/launch_onroad_desktop.sh new file mode 100755 index 00000000..50740aaf --- /dev/null +++ b/scripts/launch_onroad_desktop.sh @@ -0,0 +1,386 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "${ROOT_DIR}" + +if [[ ! -f .venv/bin/activate ]]; then + echo "Missing .venv. Run tools/install_python_dependencies.sh first." + exit 1 +fi + +default_jobs() { + if command -v nproc >/dev/null 2>&1; then + nproc + elif command -v sysctl >/dev/null 2>&1; then + sysctl -n hw.ncpu + else + echo 8 + fi +} + +env_var_truthy() { + [[ "${1:-}" =~ ^(1|true|yes|on)$ ]] +} + +usage() { + cat <<'EOF' +Usage: + ./onroad [jobs] (--c3 | --c4 | --raybig | --all | --replay-only) [--prefix name] + +Examples: + ./onroad --c3 f08912a233c1584f/2022-08-11--18-02-41/1 + ./onroad --c4 f08912a233c1584f/2022-08-11--18-02-41/1 --start 30 + ./onroad --all f08912a233c1584f/2022-08-11--18-02-41/1 + ./onroad --replay-only --demo --no-vipc --no-loop + +Notes: + - This is host/dev only. It uses the isolated host worktree and does not touch the device path. + - A private comma connect route still requires tools/lib/auth.py before replay can download it. + - Use multiple UI flags together if you want more than one desktop UI at once. +EOF +} + +jobs="$(default_jobs)" +if [[ "${1:-}" =~ ^[0-9]+$ ]]; then + jobs="$1" + shift || true +fi + +PREFIX_ARG="" +REPLAY_ARGS=() +UI_TARGETS=() +LEGACY_UI_SELECTION="" +REPLAY_ONLY=0 +REPLAY_PID="" +UI_PIDS=() + +parse_args() { + while [[ $# -gt 0 ]]; do + case "$1" in + -h|--help) + usage + exit 0 + ;; + --c3) + UI_TARGETS+=(c3) + shift + ;; + --c4) + UI_TARGETS+=(c4) + shift + ;; + --raybig) + UI_TARGETS+=(raybig) + shift + ;; + --all) + UI_TARGETS+=(c3 c4 raybig) + shift + ;; + --replay-only) + REPLAY_ONLY=1 + shift + ;; + --ui) + if [[ $# -lt 2 ]]; then + echo "Missing value for --ui" >&2 + exit 1 + fi + LEGACY_UI_SELECTION="$2" + shift 2 + ;; + --ui=*) + LEGACY_UI_SELECTION="${1#*=}" + shift + ;; + -p|--prefix) + if [[ $# -lt 2 ]]; then + echo "Missing value for $1" >&2 + exit 1 + fi + PREFIX_ARG="$2" + shift 2 + ;; + --prefix=*) + PREFIX_ARG="${1#*=}" + shift + ;; + --) + shift + REPLAY_ARGS+=("$@") + break + ;; + *) + REPLAY_ARGS+=("$1") + shift + ;; + esac + done +} + +expand_ui_targets() { + local selection="${1// /}" + + case "${selection,,}" in + all|"") + UI_TARGETS=(c3 c4 raybig) + return + ;; + none) + UI_TARGETS=() + return + ;; + esac + + local raw_targets=() + IFS=',' read -r -a raw_targets <<< "${selection}" + + local normalized=() + local raw="" + for raw in "${raw_targets[@]}"; do + case "${raw,,}" in + c3|c4|raybig) + normalized+=("${raw,,}") + ;; + *) + echo "Unknown UI target in --ui: ${raw}" >&2 + echo "Valid values: all, none, c3, c4, raybig" >&2 + exit 1 + ;; + esac + done + + local ordered_targets=(c3 c4 raybig) + local target="" + for target in "${ordered_targets[@]}"; do + local candidate="" + for candidate in "${normalized[@]}"; do + if [[ "${candidate}" == "${target}" ]]; then + UI_TARGETS+=("${target}") + break + fi + done + done +} + +dedupe_ui_targets() { + local ordered_targets=(c3 c4 raybig) + local deduped=() + local target="" + for target in "${ordered_targets[@]}"; do + local candidate="" + for candidate in "${UI_TARGETS[@]-}"; do + if [[ "${candidate}" == "${target}" ]]; then + deduped+=("${target}") + break + fi + done + done + UI_TARGETS=("${deduped[@]-}") +} + +cleanup() { + local exit_code=$? + trap - EXIT INT TERM + + local pid="" + for pid in "${UI_PIDS[@]-}"; do + if [[ -n "${pid}" ]]; then + kill "${pid}" >/dev/null 2>&1 || true + fi + done + if [[ -n "${REPLAY_PID}" ]]; then + kill "${REPLAY_PID}" >/dev/null 2>&1 || true + fi + + for pid in "${UI_PIDS[@]-}"; do + if [[ -n "${pid}" ]]; then + wait "${pid}" >/dev/null 2>&1 || true + fi + done + if [[ -n "${REPLAY_PID}" ]]; then + wait "${REPLAY_PID}" >/dev/null 2>&1 || true + fi + + exit "${exit_code}" +} + +prepare_env() { + source .venv/bin/activate + + if [[ -d /opt/homebrew/bin ]]; then + export PATH="/opt/homebrew/bin:${PATH}" + fi + + export PATH="${ROOT_DIR}/.venv/bin:${PATH}" + export PYTHONPATH="${ROOT_DIR}:${ROOT_DIR}/starpilot/third_party" + local repo_dir="" + for repo_dir in "${ROOT_DIR}"/*_repo; do + [[ -d "${repo_dir}" ]] && export PYTHONPATH="${PYTHONPATH}:${repo_dir}" + done + [[ -d "${ROOT_DIR}/third_party/acados" ]] && export PYTHONPATH="${PYTHONPATH}:${ROOT_DIR}/third_party/acados" + + export BASEDIR="${ROOT_DIR}" + export NOBOARD=1 + export SIMULATION=1 + export SKIP_FW_QUERY=1 + export USE_WEBCAM=1 + export SP_C3_FAKE_WIFI=0 + export SP_C4_FAKE_WIFI=0 + export SP_RAYBIG_FAKE_WIFI=0 + export SP_ALLOW_DESKTOP_FAKE_WIFI=0 + + if [[ "$(uname -s)" == "Darwin" ]] || env_var_truthy "${ZMQ:-0}"; then + if [[ -n "${PREFIX_ARG}" || -n "${OPENPILOT_PREFIX:-}" ]]; then + echo "Ignoring OPENPILOT_PREFIX because the ZMQ backend does not support prefixes." >&2 + fi + unset OPENPILOT_PREFIX + else + export OPENPILOT_PREFIX="${PREFIX_ARG:-${OPENPILOT_PREFIX:-desktop-onroad-$$}}" + fi +} + +seed_params() { + "${ROOT_DIR}/.venv/bin/python3" - <<'PY' +from openpilot.common.params import Params +from openpilot.system.version import terms_version, training_version + +params = Params() +params.put("HasAcceptedTerms", terms_version) +params.put("CompletedTrainingVersion", training_version) +params.put_bool("OpenpilotEnabledToggle", True) +params.put_bool("IsDriverViewEnabled", False) +params.put_bool("ForceOnroad", False) +params.put_bool("ForceOffroad", False) +PY +} + +build_replay() { + SP_DISABLE_AUTO_DEVICE_SCONS=1 "${ROOT_DIR}/.venv/bin/scons" --extras -j"${jobs}" tools/replay/replay +} + +prepare_c3_runtime() { + SP_C3_COMPILE_ONLY=1 "${ROOT_DIR}/scripts/launch_ui_desktop.sh" "${jobs}" +} + +prepare_python_ui_runtime() { + SP_KEEP_DESKTOP_RUNTIME_ARTIFACTS=1 SP_C4_COMPILE_ONLY=1 "${ROOT_DIR}/scripts/launch_ui_c4_desktop.sh" "${jobs}" +} + +launch_replay() { + local replay_cmd=( + "${ROOT_DIR}/tools/replay/replay" + --headless + --dcam + --ecam + ) + replay_cmd+=("${REPLAY_ARGS[@]}") + + "${replay_cmd[@]}" & + REPLAY_PID=$! + + sleep 1 + if ! kill -0 "${REPLAY_PID}" >/dev/null 2>&1; then + wait "${REPLAY_PID}" + return 1 + fi +} + +launch_c3_ui() { + local os_ext="linux" + if [[ "$(uname -s)" == "Darwin" ]]; then + os_ext="macos" + fi + + local host_ui="${ROOT_DIR}/selfdrive/ui/ui.${os_ext}" + if [[ ! -x "${host_ui}" ]]; then + echo "Missing ${host_ui}. C3 build did not produce the desktop binary." >&2 + return 1 + fi + + "${host_ui}" & + UI_PIDS+=("$!") +} + +launch_python_ui() { + local big="$1" + ( + export BIG="${big}" + exec "${ROOT_DIR}/.venv/bin/python3" "${ROOT_DIR}/selfdrive/ui/ui.py" + ) & + UI_PIDS+=("$!") +} + +parse_args "$@" + +if [[ -n "${LEGACY_UI_SELECTION}" ]]; then + expand_ui_targets "${LEGACY_UI_SELECTION}" +fi + +if [[ "${REPLAY_ONLY}" == "1" ]]; then + UI_TARGETS=() +else + dedupe_ui_targets +fi + +if [[ ${#REPLAY_ARGS[@]} -eq 0 ]]; then + usage >&2 + exit 1 +fi + +if [[ "${REPLAY_ONLY}" != "1" && ${#UI_TARGETS[@]} -eq 0 ]]; then + echo "Select at least one UI with --c3, --c4, --raybig, or use --replay-only." >&2 + exit 1 +fi + +prepare_env +trap cleanup EXIT INT TERM + +echo "Using OPENPILOT_PREFIX=${OPENPILOT_PREFIX:-}" +echo "Preparing replay and desktop UI runtime..." + +build_replay + +case " ${UI_TARGETS[*]-} " in + *" c3 "*) + prepare_c3_runtime + ;; +esac + +case " ${UI_TARGETS[*]-} " in + *" c4 "*|*" raybig "*) + prepare_python_ui_runtime + ;; +esac + +seed_params + +echo "Starting replay: ${REPLAY_ARGS[*]}" +launch_replay + +if [[ ${#UI_TARGETS[@]} -eq 0 ]]; then + echo "Replay is running without UI windows. Press Ctrl-C to stop." + wait "${REPLAY_PID}" + exit 0 +fi + +echo "Launching UIs: ${UI_TARGETS[*]}" + +local_target="" +for local_target in "${UI_TARGETS[@]}"; do + case "${local_target}" in + c3) + launch_c3_ui + ;; + c4) + launch_python_ui 0 + ;; + raybig) + launch_python_ui 1 + ;; + esac +done + +wait "${UI_PIDS[@]}" diff --git a/scripts/launch_ui_c4_desktop.sh b/scripts/launch_ui_c4_desktop.sh index 99890e39..6a63a237 100755 --- a/scripts/launch_ui_c4_desktop.sh +++ b/scripts/launch_ui_c4_desktop.sh @@ -110,8 +110,10 @@ cleanup() { kill "${FAKE_WIFI_PID}" >/dev/null 2>&1 || true wait "${FAKE_WIFI_PID}" >/dev/null 2>&1 || true fi - restore_runtime_artifacts - restore_new_tracked_changes + if [[ ! "${SP_KEEP_DESKTOP_RUNTIME_ARTIFACTS:-0}" =~ ^(1|true|yes|on)$ ]]; then + restore_runtime_artifacts + restore_new_tracked_changes + fi rm -f "${PRE_TRACKED_DIRTY}" "${POST_TRACKED_DIRTY}" } diff --git a/scripts/launch_ui_desktop.sh b/scripts/launch_ui_desktop.sh index c05b5e7e..8676d6b4 100755 --- a/scripts/launch_ui_desktop.sh +++ b/scripts/launch_ui_desktop.sh @@ -182,6 +182,7 @@ prepare_host_artifacts() { purge_objects "${ROOT_DIR}/selfdrive/ui" purge_objects "${ROOT_DIR}/starpilot/ui" + rm -f "${ROOT_DIR}/selfdrive/ui/libqt_widgets.a" "${ROOT_DIR}/selfdrive/ui/libqt_util.a" rm -f "${ROOT_DIR}/selfdrive/ui/ui" } @@ -201,6 +202,8 @@ if [[ "$(uname -s)" == "Darwin" ]]; then # Keep desktop host builds on Apple toolchain even if shell exports Homebrew llvm. export CC="/usr/bin/clang" export CXX="/usr/bin/clang++" + export AR="/usr/bin/ar" + export RANLIB="/usr/bin/ranlib" fi unset CPATH C_INCLUDE_PATH CPLUS_INCLUDE_PATH CPPFLAGS CFLAGS CXXFLAGS LDFLAGS diff --git a/scripts/launch_ui_raybig_desktop.sh b/scripts/launch_ui_raybig_desktop.sh index 9f88f9d2..d37c8438 100755 --- a/scripts/launch_ui_raybig_desktop.sh +++ b/scripts/launch_ui_raybig_desktop.sh @@ -110,8 +110,10 @@ cleanup() { kill "${FAKE_WIFI_PID}" >/dev/null 2>&1 || true wait "${FAKE_WIFI_PID}" >/dev/null 2>&1 || true fi - restore_runtime_artifacts - restore_new_tracked_changes + if [[ ! "${SP_KEEP_DESKTOP_RUNTIME_ARTIFACTS:-0}" =~ ^(1|true|yes|on)$ ]]; then + restore_runtime_artifacts + restore_new_tracked_changes + fi rm -f "${PRE_TRACKED_DIRTY}" "${POST_TRACKED_DIRTY}" } diff --git a/tools/STARPILOT_DEVELOPMENT.md b/tools/STARPILOT_DEVELOPMENT.md index cad6f266..81cbf261 100644 --- a/tools/STARPILOT_DEVELOPMENT.md +++ b/tools/STARPILOT_DEVELOPMENT.md @@ -73,6 +73,7 @@ All three entrypoints do the same thing. `./dev` is the shortest general-purpose ### Available host commands - `./dev replay [args...]` +- `./onroad [jobs] (--c3 | --c4 | --raybig | --all | --replay-only) ` - `./dev cabana [args...]` - `./dev plotjuggler [args...]` - `./dev juggle [args...]` @@ -91,6 +92,9 @@ Examples: ```bash ./dev replay +./onroad --c3 f08912a233c1584f/2022-08-11--18-02-41/1 +./onroad --c4 f08912a233c1584f/2022-08-11--18-02-41/1 --start 30 +./onroad --all f08912a233c1584f/2022-08-11--18-02-41/1 ./dev plotjuggler --help ./dev cabana ./c3 8 @@ -159,6 +163,12 @@ Use `./dev ...` when: - you want host-native `.so` files separated from the main repo - you do not want AI tools or git status confused by temporary build churn +Use `./onroad ...` when: + +- you want a recorded route to drive the desktop UI(s) on PC +- you need replay and UI to share the same isolated host runtime and messaging prefix +- you want the default side-by-side desktop UI launch without running separate replay/UI commands + Use `./c3`, `./c4`, or `./raybig` when: - you want the desktop UI variants diff --git a/tools/replay/README.md b/tools/replay/README.md index 794c08f6..e56eeef1 100644 --- a/tools/replay/README.md +++ b/tools/replay/README.md @@ -77,6 +77,7 @@ Options: --no-vipc do not output video --all do output all messages including uiDebug, userBookmark. this may causes issues when used along with UI + --headless run replay without the ncurses console UI Arguments: route the drive to replay. find your drives at @@ -91,6 +92,12 @@ tools/replay/replay cd selfdrive/ui && ./ui.py ``` +For the StarPilot host workflow, the combined desktop launcher is: + +```bash +./onroad --c3 +``` + ## Work with plotjuggler If you want to use replay with plotjuggler, you can stream messages by running: diff --git a/tools/replay/main.cc b/tools/replay/main.cc index 46090121..5ee6f5b5 100644 --- a/tools/replay/main.cc +++ b/tools/replay/main.cc @@ -5,6 +5,7 @@ #include #include +#include "common/util.h" #include "common/prefix.h" #include "tools/replay/consoleui.h" #include "tools/replay/replay.h" @@ -31,6 +32,7 @@ Options: --no-hw-decoder Disable HW video decoding --no-vipc Do not output video --all Output all messages including bookmarkButton, uiDebug, userBookmark + --headless Run replay without the ncurses console UI -h, --help Show this help message )"; @@ -42,6 +44,7 @@ struct ReplayConfig { std::string prefix; uint32_t flags = REPLAY_FLAG_NONE; bool auto_source = false; + bool headless = false; int start_seconds = 0; int cache_segments = -1; float playback_speed = -1; @@ -66,6 +69,7 @@ bool parseArgs(int argc, char *argv[], ReplayConfig &config) { {"no-hw-decoder", no_argument, nullptr, 0}, {"no-vipc", no_argument, nullptr, 0}, {"all", no_argument, nullptr, 0}, + {"headless", no_argument, nullptr, 0}, {"help", no_argument, nullptr, 'h'}, {nullptr, 0, nullptr, 0}, // Terminating entry }; @@ -100,6 +104,7 @@ bool parseArgs(int argc, char *argv[], ReplayConfig &config) { std::string name = cli_options[option_index].name; if (name == "demo") config.route = DEMO_ROUTE; else if (name == "auto") config.auto_source = true; + else if (name == "headless") config.headless = true; else config.flags |= flag_map.at(name); break; } @@ -149,6 +154,15 @@ int main(int argc, char *argv[]) { return 1; } + if (config.headless) { + replay.start(config.start_seconds); + ExitHandler do_exit; + while (!do_exit) { + util::sleep_for(100); + } + return 0; + } + ConsoleUI console_ui(&replay); replay.start(config.start_seconds); return console_ui.exec();